* glb (color-assimp2 branch) + model-viewer
* color support + customizable output format
* cleanup
* disable monaco on iOS
* reinstate STL viewer when output is stl
* default to stl
* fix iOS editor
* script to build openscad-wasm from a branch / local repo
* update fork link + build instructions
* Update build-openscad-wasm.sh
* Update initial-state.ts
* update openscad3 refs
* update wasm build script
* Update Dockerfile.wasm
* add jszip dep
* multimaterial: color conversion + 3MF face painting
* ignore ts error about compression apis
* Update tsconfig.json
* multimaterial support w/ extruder colors input component
* fix wasm build script
* symlinks for openscad.{js,wasm} under src
* fix error line shift for bundled files
* remove obsolete build script
* set OPENSCADPATH env var
* render-colors feature renamed
* Customizer + prepare Multi-asset support
* hide customizer button if unselected, hide it if no params,
* Create build-openscad-wasm.sh
* update build script
* complete sound
* fetchSource util
* wider rotation allowance
* use CSG-modules.scad as default
* Update CustomizerPanel.tsx
* Update index.html
* capture save, allow Ctrl+Shift+Save to save project
* customizer: fieldsets are better than accordion
* expand customizer tabs by default
* tooltip for reset buttons
* pretty c++ exceptions
* Delete inline-openscad-worker.ts
* shuffle ux
* use uzip instead of jszip for 3mf materialization
* handle legacy links
* update links to fork
* use render-modifiers feature from color-assimp2 in preview mode
* include NopSCADlib's STL files in archive
* bind global f5 / f6
* update homepage back to openscad2
* styling fixes (editor alpha, customizer margins)
* Add help link for customizer
* fix customizer sliders (also need to show value input)
* fix vector customizer component
* style fallback editor
* shorter logs button
* customizer: rm reset button tooltip
* customizer: allow dnd on viewer below, tweak background
* add lights to modelviewer using an env image (gltf lights hard to plug across assimp for now)
* update customizer styles
* Update .gitignore
* add license for UZIP.js (MIT)
* Update skybox-lights.jpg
* Update actions.ts
* include NopSCADlib's STLs
* Update LICENSE.md
* include YAPP_Box examples
* limit customizer height
* Update skybox-lights.jpg
* Update skybox-lights.jpg
* Update openscad-worker.ts
* build-openscad-wasm.sh fix: docker won't mount files outside home directory
* Use Google CDN instead of unpkg for modelviewer
* tmp updates before merge
* Add ccache support to build-openscad-wasm.sh
* Fix makefile
* Update README.md
* Fix ts imports
* Delete build-openscad-wasm.sh
* Crude off -> glb converter
* Remove assimp related code
* gltf diffs
* Build using head openscad
* readme: Update build prerequisites
* make: attempt to build wasm binary in parallel first
* Remove 3mf extruder color mapping & fix render / export flows (render all 3d to off)
* off2glb: triangulate faces + add default color
* Add simple e2e puppeteer test
* ci: update pinned build to master output artefact
* Update Makefile
* Update test.yml
* Update test.yml
* Update test.yml
* Update test.yml
* ci: use relative node version names
* Update test.yml
* state: Remove renderFormat, introduce is2D
* disable service worker in devel mode
* remove dead code
* build: fix NODE_ENV define
* ci: simplify env vars
* cleanups
* Update README.md
184 lines
No EOL
5.8 KiB
TypeScript
184 lines
No EOL
5.8 KiB
TypeScript
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
|
|
|
import { Source } from "./state/app-state";
|
|
|
|
export function mapObject(o: any, f: (key: string, value: any) => any, ifPred: (key: string) => boolean) {
|
|
const ret = [];
|
|
for (const key of Object.keys(o)) {
|
|
if (ifPred && !ifPred(key)) {
|
|
continue;
|
|
}
|
|
ret.push(f(key, o[key]));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
type Killer = () => void;
|
|
export type AbortablePromise<T> = Promise<T> & {kill: Killer}
|
|
export function AbortablePromise<T>(f: (resolve: (result: T) => void, reject: (error: any) => void) => Killer): AbortablePromise<T>
|
|
{
|
|
let kill: Killer;
|
|
const promise = new Promise<T>((res, rej) => {
|
|
kill = f(res, rej);
|
|
});
|
|
return Object.assign(promise, {kill: kill!});
|
|
}
|
|
|
|
// <T extends any[]>(...args: T)
|
|
export function turnIntoDelayableExecution<T extends any[], R>(
|
|
delay: number,
|
|
job: (...args: T) => AbortablePromise<R>) {
|
|
let pendingId: number | null;
|
|
let runningJobKillSignal: (() => void) | null;
|
|
// return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
|
|
// (async () => {
|
|
// try {
|
|
// const result = await job;
|
|
// // console.log(result);
|
|
|
|
// let parameterSet: ParameterSet | undefined = undefined;
|
|
// if (result.outputs && result.outputs.length == 1) {
|
|
// let [[, content]] = result.outputs;
|
|
// content = new TextDecoder().decode(content as any);
|
|
// try {
|
|
// parameterSet = JSON.parse(content)
|
|
// // console.log('PARAMETER SET', JSON.stringify(parameterSet, null, 2))
|
|
// } catch (e) {
|
|
// console.error(`Error while parsing parameter set: ${e}\n${content}`);
|
|
// }
|
|
// } else {
|
|
// console.error('No output from runner!');
|
|
// }
|
|
|
|
// res({
|
|
// ...processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {
|
|
// sourcePath: sources[0].path,
|
|
// skipLines: 1,
|
|
// }}),
|
|
// parameterSet,
|
|
// });
|
|
// } catch (e) {
|
|
// console.error(e);
|
|
// rej(e);
|
|
// }
|
|
// })()
|
|
// return () => job.kill();
|
|
// });
|
|
//return (...args: T) => async ({now, callback}: {now: boolean, callback: (result?: R, error?: any) => void}) => {
|
|
return (...args: T) => ({now}: {now: boolean}) => AbortablePromise<R>((resolve, reject) => {
|
|
let abortablePromise: AbortablePromise<R> | undefined = undefined;
|
|
(async () => {
|
|
const doExecute = async () => {
|
|
if (runningJobKillSignal) {
|
|
runningJobKillSignal();
|
|
runningJobKillSignal = null;
|
|
}
|
|
abortablePromise = job(...args);
|
|
runningJobKillSignal = abortablePromise.kill;
|
|
try {
|
|
resolve(await abortablePromise);
|
|
} catch (e) {
|
|
reject(e);
|
|
} finally {
|
|
runningJobKillSignal = null;
|
|
}
|
|
}
|
|
if (pendingId) {
|
|
clearTimeout(pendingId);
|
|
pendingId = null;
|
|
}
|
|
if (now) {
|
|
doExecute();
|
|
} else {
|
|
pendingId = window.setTimeout(doExecute, delay);
|
|
}
|
|
})();
|
|
return () => abortablePromise?.kill();
|
|
});
|
|
}
|
|
|
|
export function validateStringEnum<T extends string>(
|
|
s: T, values: T[],
|
|
orElse: (s: string) => T = s => { throw new Error(`Unexpected value: ${s} (valid values: ${values.join(', ')})`); }): T {
|
|
return values.indexOf(s) < 0 ? orElse(s) : s;
|
|
}
|
|
export const validateBoolean = (s: boolean, orElse: () => boolean = () => false) => typeof s === 'boolean' ? s : orElse();
|
|
export const validateString = (s: string, orElse: () => string = () => '') => s != null && typeof s === 'string' ? s : orElse();
|
|
export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[] = () => []) => {
|
|
if (!(a instanceof Array)) return orElse();
|
|
return a.map(validateElement);
|
|
};
|
|
|
|
export function formatBytes(n: number) {
|
|
if (n < 1024) {
|
|
return `${Math.floor(n)} bytes`;
|
|
}
|
|
n /= 1024;
|
|
if (n < 1024) {
|
|
return `${Math.floor(n * 10) / 10} kB`;
|
|
}
|
|
n /= 1024;
|
|
return `${Math.floor(n * 10) / 10} MB`;
|
|
}
|
|
|
|
export function formatMillis(n: number) {
|
|
if (n < 1000)
|
|
return `${Math.floor(n)}ms`;
|
|
|
|
return `${Math.floor(n / 100) / 10}sec`;
|
|
}
|
|
|
|
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
|
|
export function registerCustomAppHeightCSSProperty() {
|
|
const updateAppHeight = () => {
|
|
document.documentElement.style.setProperty('--app-height', `${window.innerHeight}px`)
|
|
}
|
|
window.addEventListener('resize', updateAppHeight)
|
|
updateAppHeight();
|
|
}
|
|
|
|
// In PWA mode, persist files in LocalStorage instead of the hash fragment.
|
|
export const isInStandaloneMode =
|
|
// true
|
|
Boolean(('standalone' in window.navigator) && (window.navigator.standalone));
|
|
|
|
export function downloadUrl(url: string, filename: string) {
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.setAttribute('download', filename)
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.parentNode?.removeChild(link);
|
|
}
|
|
|
|
export async function fetchSource({content, path, url}: Source) {
|
|
if (content) {
|
|
return content;
|
|
} else if (url) {
|
|
if (path.endsWith('.scad') || path.endsWith('.json')) {
|
|
content = await (await fetch(url)).text();
|
|
return content;
|
|
} else {
|
|
// Fetch bytes
|
|
const response = await fetch(url);
|
|
const buffer = await response.arrayBuffer();
|
|
const data = new Uint8Array(buffer);
|
|
return data;
|
|
}
|
|
} else {
|
|
throw new Error('Invalid source: ' + JSON.stringify({path, content, url}));
|
|
}
|
|
}
|
|
|
|
export function readFileAsDataURL(file: File) {
|
|
// TO data URI:
|
|
return new Promise<string>((res, rej) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
res(reader.result as string);
|
|
}
|
|
reader.onerror = rej;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
// return URL.createObjectURL(file);
|
|
} |