openscad-playground/src/utils.ts
Olivier Chafik f3cb9ae3f9
🌈 Colorful Playground (#37)
* 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
2024-12-22 22:41:35 +00:00

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);
}