Gooood progress
This commit is contained in:
parent
f996bbc1fb
commit
6528baa7b6
17 changed files with 752 additions and 171 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,10 +1,6 @@
|
|||
node_modules
|
||||
libs
|
||||
dist
|
||||
public/libraries
|
||||
public/openscad-worker-inlined.js
|
||||
public/openscad.js
|
||||
public/openscad.wasm
|
||||
js
|
||||
src/*.js
|
||||
build
|
||||
src/wasm
|
||||
package-lock.json
|
||||
|
|
@ -18,6 +18,7 @@ Should work:
|
|||
|
||||
Planned:
|
||||
|
||||
- Open any file in the VFS. Setup [OPENSCADPATH](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Libraries#Setting_OPENSCADPATH)
|
||||
- Customizer support. Probably by adding --export-json or --export-format=customizer-json to OpenSCAD.
|
||||
- Mobile (iOS) editing support: switch to https://www.npmjs.com/package/react-codemirror ?
|
||||
- Preview rendering: have OpenSCAD export the preview scene to a rich format (e.g. glTF, with some parts being translucent when prefixed w/ % modifier) and display it using https://modelviewer.dev/ maybe)
|
||||
|
|
|
|||
|
|
@ -25,17 +25,10 @@
|
|||
content="OpenSCAD Playground: edit .scad models and render them to STL from the browser (WebAssembly build OpenSCAD, with Monaco editor and STL Viewer)"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
|
||||
<!-- https://developers.google.com/web/fundamentals/web-app-manifest/ -->
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>OpenSCAD Playground</title>
|
||||
<style>
|
||||
#root,
|
||||
|
|
@ -47,6 +40,45 @@
|
|||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.p-tabmenu-nav {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.absolute-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.monaco-editor {
|
||||
background-color: rgba(255,255,255,0.5) !important;
|
||||
}
|
||||
.overflow-guard,
|
||||
.monaco-scrollable-element,
|
||||
.monaco-editor-background {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.opacity-animated {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 600px),
|
||||
only screen and (max-width: 600px),
|
||||
only screen and (max-width: 767px),
|
||||
only screen and (max-aspect-ratio: 1/2) {
|
||||
.github-fork-ribbon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>.github-fork-ribbon:before { background-color: #333; }</style>
|
||||
|
||||
|
|
@ -61,15 +93,5 @@
|
|||
|
||||
<noscript>You need to enable JavaScript to run the OpenSCAD Playground.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
517
src/App.tsx
517
src/App.tsx
|
|
@ -1,7 +1,7 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import React, { forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
||||
import {ModelContext, State} from './app-state'
|
||||
import React, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
||||
import {ModelContext, MultiLayoutComponentId, SingleLayoutComponentId, State} from './app-state'
|
||||
import Editor, { loader, Monaco } from '@monaco-editor/react';
|
||||
// import './App.css';
|
||||
import openscadEditorOptions from './language/openscad-editor-options';
|
||||
|
|
@ -10,6 +10,18 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
|||
import {StlViewer} from "react-stl-viewer";
|
||||
import { Button } from 'primereact/button';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { TreeSelect } from 'primereact/treeselect';
|
||||
import TreeNode from 'primereact/treenode';
|
||||
import { TabMenu } from 'primereact/tabmenu';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Menu } from 'primereact/menu';
|
||||
import { ToggleButton } from 'primereact/togglebutton';
|
||||
import { getParentDir } from './filesystem';
|
||||
import { BreadCrumb } from 'primereact/breadcrumb';
|
||||
import { ScrollPanel } from 'primereact/scrollpanel';
|
||||
import { Splitter, SplitterPanel } from 'primereact/splitter';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
|
||||
// import "primereact/resources/themes/lara-light-indigo/theme.css";
|
||||
// import "primereact/resources/primereact.min.css";
|
||||
|
|
@ -18,17 +30,70 @@ import { ProgressBar } from 'primereact/progressbar';
|
|||
let monacoInstance: Monaco
|
||||
loader.init().then(mi => monacoInstance = mi);
|
||||
|
||||
function EditorPanel() {
|
||||
const isFileWritable = (path: string) => getParentDir(path) === '/home'
|
||||
|
||||
const FSContext = React.createContext<FS | undefined>(undefined);
|
||||
|
||||
function listFilesAsNodes(fs: FS, path: string): TreeNode[] {
|
||||
const files: [string, string][] = []
|
||||
const dirs: [string, string][] = []
|
||||
for (const name of fs.readdirSync(path)) {
|
||||
const childPath = `${path}/${name}`;
|
||||
const stat = fs.lstatSync(childPath);
|
||||
const isDirectory = stat.isDirectory();
|
||||
if (!isDirectory && !name.endsWith('.scad')) {
|
||||
continue;
|
||||
}
|
||||
(isDirectory ? dirs : files).push([name, childPath]);
|
||||
}
|
||||
[files, dirs].forEach(arr => arr.sort(([a], [b]) => a.localeCompare(b)));
|
||||
|
||||
const nodes: TreeNode[] = []
|
||||
for (const [arr, isDirectory] of [[files, false], [dirs, true]] as [[string, string][], boolean][]) {
|
||||
for (const [name, path] of arr) {
|
||||
const children = isDirectory ? listFilesAsNodes(fs, path) : undefined;
|
||||
if (isDirectory && children!.length == 0) {
|
||||
continue;
|
||||
}
|
||||
nodes.push({
|
||||
// icon: path == '/home' ? 'pi-home' : ...
|
||||
icon: isDirectory ? 'pi pi-folder' : isFileWritable(path) ? 'pi pi-file' : 'pi pi-lock',
|
||||
label: name,
|
||||
data: path,
|
||||
key: path,
|
||||
children,
|
||||
selectable: !isDirectory // && (name == 'LICENSE' || name.endsWith('.scad') || name.endsWith('.scad')
|
||||
});
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function EditorPanel({className, style}: {className?: string, style?: CSSProperties}) {
|
||||
|
||||
const model = useContext(ModelContext);
|
||||
if (!model) throw new Error('No model');
|
||||
|
||||
const state = model.state;
|
||||
|
||||
const fs = useContext(FSContext);
|
||||
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
|
||||
// const [modelFile, setModelFile] = useState(state.params.sourcePath);
|
||||
|
||||
if (editor) {
|
||||
const checkerRun = model.state.lastCheckerRun;
|
||||
const checkerRun = state.lastCheckerRun;
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel && checkerRun) {
|
||||
if (editorModel) {
|
||||
if (checkerRun) {
|
||||
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', checkerRun.markers);
|
||||
}
|
||||
// if (modelFile !== state.params.sourcePath) {
|
||||
// editor.setModel(monaco.editor.createModel('', 'openscad'));
|
||||
// setModelFile(state.params.sourcePath)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const onMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editor.addAction({
|
||||
id: "openscad-render",
|
||||
|
|
@ -48,59 +113,116 @@ function EditorPanel() {
|
|||
setEditor(editor)
|
||||
}
|
||||
|
||||
// const breadCrumbsHome: MenuItem = {
|
||||
// icon: 'pi pi-home'
|
||||
// }
|
||||
|
||||
// const breadCrumbsItems: MenuItem[] = [
|
||||
// {
|
||||
// label: 'input.scad',
|
||||
// command: () => alert('ok')
|
||||
// }
|
||||
// ]
|
||||
|
||||
const fsItems = fs && listFilesAsNodes(fs, '/home') || [];
|
||||
|
||||
// console.log('getParentDir(', state.params.sourcePath, ')', getParentDir(state.params.sourcePath))
|
||||
return (
|
||||
<div className="editor-panel" style={{
|
||||
<div className={`editor-panel ${className ?? ''}`} style={{
|
||||
// maxWidth: '5 0vw',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
// position: 'relative',
|
||||
// width: '100%', height: '100%',
|
||||
...(style ?? {})
|
||||
}}>
|
||||
{/* <BreadCrumb model={breadCrumbsItems} home={breadCrumbsHome}/> */}
|
||||
<TreeSelect
|
||||
title='OpenSCAD Playground Files'
|
||||
value={state.params.sourcePath}
|
||||
onChange={(e) => model.openFile(String(e.value))}
|
||||
filter
|
||||
options={fsItems} />
|
||||
|
||||
{/* <SplitPane type="vertical"> */}
|
||||
{/* <div className='flex flex-column'> */}
|
||||
{/* <Splitter style={{
|
||||
// height: '300px'
|
||||
}} layout="vertical"> */}
|
||||
{/* <SplitterPanel className="flex flex-column align-items-center justify-content-center"
|
||||
style={{
|
||||
// width: '100%', height: '100%'
|
||||
}}> */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
flex: 1
|
||||
}}>
|
||||
<Editor
|
||||
className="openscad-editor"
|
||||
className="openscad-editor absolute-fill"
|
||||
defaultLanguage="openscad"
|
||||
value={model.state.params.source}
|
||||
path={state.params.sourcePath}
|
||||
value={state.params.source}
|
||||
onChange={s => model.source = s ?? ''}
|
||||
onMount={onMount} // TODO: This looks a bit silly, does it trigger a re-render??
|
||||
options={openscadEditorOptions}
|
||||
height="50vh"/>
|
||||
|
||||
<div className="logs-container">
|
||||
<pre><code id="logs">{model.state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
|
||||
options={{
|
||||
...openscadEditorOptions,
|
||||
readOnly: !isFileWritable(state.params.sourcePath)
|
||||
}}
|
||||
// height="100%"
|
||||
// width="100%"
|
||||
// height="80vh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* </SplitterPanel>
|
||||
<SplitterPanel className="flex flex-column align-items-center justify-content-center" style={{position: 'relative'}}> */}
|
||||
{/* <ScrollPanel style={{
|
||||
// width: '100%', height: '200px'
|
||||
}} className="custombar2"> */}
|
||||
<div style={{
|
||||
display: state.view.logs ? undefined : 'none',
|
||||
overflowY: 'scroll',
|
||||
height: 'calc(min(200px, 30vh))',
|
||||
// position: 'relative',
|
||||
// maxWidth: '100%',
|
||||
// maxWidth: '50vw',
|
||||
}}>
|
||||
<pre><code id="logs" style={{
|
||||
// maxWidth: '200px'
|
||||
}}>{state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
|
||||
</div>
|
||||
{/* </ScrollPanel> */}
|
||||
{/* <div className="logs-container">
|
||||
</div> */}
|
||||
{/* </SplitterPanel>
|
||||
</Splitter> */}
|
||||
{/* </SplitPane> */}
|
||||
{/* </div> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ViewerPanel({className, style}: {className?: string, style?: CSSProperties}) {
|
||||
const model = useContext(ModelContext);
|
||||
if (!model) throw new Error('No model');
|
||||
|
||||
export function App({initialState}: {initialState: State}) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
|
||||
|
||||
if (editor) {
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel && state.lastCheckerRun) {
|
||||
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', state.lastCheckerRun.markers);
|
||||
}
|
||||
}
|
||||
|
||||
const model = new Model(state, setState);
|
||||
useEffect(() => model.init());
|
||||
|
||||
// const stlModelRef = forwardRef();
|
||||
const state = model.state;
|
||||
|
||||
return (
|
||||
<ModelContext.Provider value={model}>
|
||||
<div style={{display: 'flex', flexDirection: 'column', flex: 1}}>
|
||||
<div style={{display: 'flex', flexDirection: 'row', flex: 1}}>
|
||||
<div style={{width: "50vw"}}>
|
||||
<EditorPanel/>
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'column', flex: 1, position: 'relative'}}>
|
||||
{(state.rendering || state.previewing) &&
|
||||
<ProgressBar mode="indeterminate" style={{ height: '6px' }}></ProgressBar>}
|
||||
<div className={className}
|
||||
style={{display: 'flex', flexDirection: 'column',
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...(style ?? {})
|
||||
}}>
|
||||
{state.output?.stlFileURL &&
|
||||
<StlViewer
|
||||
style={{
|
||||
flex: 1
|
||||
// width: '100%'
|
||||
}}
|
||||
// ref={stlModelRef}
|
||||
showAxes={true}
|
||||
|
|
@ -115,31 +237,312 @@ export function App({initialState}: {initialState: State}) {
|
|||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style={{display: 'flex', flexDirection: 'row', gap: '10px'}}>
|
||||
<Button onClick={() => model.render({isPreview: false, now: true})}>Render</Button>
|
||||
{state.output && state.output.formattedElapsedMillis}
|
||||
|
||||
{state.previewing && 'previewing... '}
|
||||
{state.rendering && 'rendering... '}
|
||||
{state.checkingSyntax && 'checking syntax... '}
|
||||
)
|
||||
}
|
||||
|
||||
{state.output && (
|
||||
<a href={state.output.stlFileURL}
|
||||
target="_blank"
|
||||
download={state.output.isPreview ? "preview.stl" : "render.stl"}
|
||||
title="STL Download">
|
||||
{state.output.isPreview ? "preview.stl" : "render.stl"} ({state.output.formattedStlFileSize})
|
||||
</a>
|
||||
)}
|
||||
function downloadOutput(state: State) {
|
||||
if (!state.output) return;
|
||||
const a = document.createElement('a')
|
||||
a.href = state.output.stlFileURL
|
||||
a.download = state.output.isPreview ? 'preview.stl' : 'render.stl';
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
|
||||
function Footer({style}: {style?: CSSProperties}) {
|
||||
const model = useContext(ModelContext);
|
||||
if (!model) throw new Error('No model');
|
||||
|
||||
const menu = useRef<Menu>(null);
|
||||
|
||||
const state = model.state;
|
||||
|
||||
const severityByMarkerSeverity = new Map<monaco.MarkerSeverity, 'danger' | 'warning' | 'info'>([
|
||||
[monaco.MarkerSeverity.Error, 'danger'],
|
||||
[monaco.MarkerSeverity.Warning, 'warning'],
|
||||
[monaco.MarkerSeverity.Info, 'info'],
|
||||
]);
|
||||
const markers = state.lastCheckerRun?.markers ?? [];
|
||||
const getBadge = (s: monaco.MarkerSeverity) => {
|
||||
const count = markers.filter(m => m.severity == s).length;
|
||||
const sev = s == monaco.MarkerSeverity.Error ? 'danger'
|
||||
: s == monaco.MarkerSeverity.Warning ? 'warning'
|
||||
: s == monaco.MarkerSeverity.Info ? 'info'
|
||||
: 'success';
|
||||
return <>{count > 0 && <Badge value={count} severity={severityByMarkerSeverity.get(s)}></Badge>}</>;
|
||||
};
|
||||
|
||||
|
||||
const maxMarkerSeverity = markers.length == 0 ? undefined : markers.map(m => m.severity).reduce((a, b) => Math.max(a, b));
|
||||
|
||||
return <>
|
||||
<ProgressBar mode="indeterminate"
|
||||
style={{
|
||||
marginLeft: '5px',
|
||||
marginRight: '5px',
|
||||
visibility: state.rendering || state.previewing || state.checkingSyntax
|
||||
? 'visible' : 'hidden',
|
||||
height: '6px' }}></ProgressBar>
|
||||
|
||||
<div className="flex flex-row gap-1" style={{
|
||||
// display: 'flex',
|
||||
// flexDirection: 'row',
|
||||
// gap: '10px',
|
||||
// verticalAlign: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '5px',
|
||||
...(style ?? {})
|
||||
}}>
|
||||
<Button onClick={() => model.render({isPreview: false, now: true})}
|
||||
loading={state.rendering}
|
||||
icon="pi pi-refresh"
|
||||
label="Render" />
|
||||
|
||||
{/* {state.error != null &&
|
||||
<i className="pi pi-exclamation-circle" style={{color: 'red'}} title={state.error}></i>} */}
|
||||
|
||||
<Button type="button"
|
||||
// label={state.view.logs ? "Hide logs" : "Show logs"}
|
||||
label="Logs"
|
||||
icon="pi pi-align-left"
|
||||
text={!state.view.logs}
|
||||
onClick={() => model.logsVisible = !state.view.logs}
|
||||
// onLabel="Logs" offLabel="Logs"
|
||||
// onIcon="pi pi-align-left"
|
||||
// offIcon="pi pi-align-left"
|
||||
className={maxMarkerSeverity && `p-button-${severityByMarkerSeverity.get(maxMarkerSeverity) ?? 'success'}`}
|
||||
>
|
||||
{getBadge(monaco.MarkerSeverity.Error)}
|
||||
{getBadge(monaco.MarkerSeverity.Warning)}
|
||||
{getBadge(monaco.MarkerSeverity.Info)}
|
||||
</Button>
|
||||
|
||||
{/* {state.output &&
|
||||
<span style={{color: 'blue'}}>
|
||||
<i className="pi pi-stopwatch" title={state.error}></i>
|
||||
<span style={{margin: '5px'}}>{state.output.formattedElapsedMillis}</span>
|
||||
</span>} */}
|
||||
|
||||
{/* {state.previewing && 'previewing... '}
|
||||
{state.rendering && 'rendering... '}
|
||||
{state.checkingSyntax && 'checking syntax... '} */}
|
||||
|
||||
{/* <span style={{flex: 1}}></span> */}
|
||||
|
||||
<span style={{flex: 1}}></span>
|
||||
|
||||
<a target="_blank" className="text-fragment" href="http://openscad.org/documentation.html">Docs</a> |
|
||||
<a target="_blank" className="text-fragment" href="http://openscad.org/cheatsheet/">Cheatsheet</a> |
|
||||
<a target="_blank" href="https://github.com/openscad/openscad-playground/blob/rewrite1/LICENSE.md">LICENSES</a>
|
||||
{state.output && (
|
||||
<Button icon='pi pi-download'
|
||||
title={`Download ${state.output.isPreview ? "preview.stl" : "render.stl"} (${state.output.formattedStlFileSize})`}
|
||||
severity="secondary"
|
||||
text
|
||||
// label={state.output.isPreview ? "preview.stl" : "render.stl"}
|
||||
iconPos='right'
|
||||
onClick={() => downloadOutput(state)} />
|
||||
// <a href={state.output.stlFileURL}
|
||||
// target="_blank"
|
||||
// download={state.output.isPreview ? "preview.stl" : "render.stl"}
|
||||
// title="STL Download">
|
||||
// {state.output.isPreview ? "preview.stl" : "render.stl"} ({state.output.formattedStlFileSize})
|
||||
// </a>
|
||||
)}
|
||||
|
||||
<ToggleButton
|
||||
style={{
|
||||
// flex: 1,
|
||||
}}
|
||||
onIcon='pi pi-table'
|
||||
offIcon='pi pi-table'
|
||||
onLabel=''
|
||||
offLabel=''
|
||||
// onLabel='Multi'
|
||||
// offLabel='Single'
|
||||
title='Change between single and multiple layout'
|
||||
checked={state.view.layout.mode === 'multi'}
|
||||
onChange={e => model.changeLayout(e.value ? 'multi' : 'single')} />
|
||||
|
||||
<Menu model={[
|
||||
{
|
||||
label: "openscad-playground",
|
||||
icon: 'pi pi-github',
|
||||
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'LICENSES',
|
||||
icon: 'pi pi-info-circle',
|
||||
command: () => window.open('https://github.com/openscad/openscad-playground/blob/rewrite1/LICENSE.md', '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'OpenSCAD Docs',
|
||||
icon: 'pi pi-book',
|
||||
command: () => window.open('http://openscad.org/documentation.html', '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'OpenSCAD Cheatsheet',
|
||||
icon: 'pi pi-palette',
|
||||
command: () => window.open('http://openscad.org/cheatsheet/', '_blank'),
|
||||
},
|
||||
{
|
||||
label: 'BOSL2 Cheatsheet',
|
||||
icon: 'pi pi-palette',
|
||||
command: () => window.open('https://github.com/revarbat/BOSL2/wiki/CheatSheet', '_blank'),
|
||||
},
|
||||
] as MenuItem[]} popup ref={menu} />
|
||||
<Button title="Help & Licenses" rounded icon="pi pi-question-circle" onClick={(e) => menu.current && menu.current.toggle(e)} />
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export function App({initialState, fs}: {initialState: State, fs: FS}) {
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
const model = new Model(fs, state, setState);
|
||||
useEffect(() => model.init());
|
||||
|
||||
// const breadCrumbsItems: MenuItem[] = [
|
||||
// {
|
||||
// label: 'input.scad',
|
||||
// command: () => alert('ok')
|
||||
|
||||
const singleTargets: {id: SingleLayoutComponentId, icon: string, label: string}[] = [
|
||||
{ id: 'editor', icon: 'pi pi-pencil', label: 'Edit' },
|
||||
{ id: 'viewer', icon: 'pi pi-box', label: 'View' },
|
||||
{ id: 'customizer', icon: 'pi pi-cog', label: 'Customize' },
|
||||
];
|
||||
const multiTargets = singleTargets;
|
||||
// const multiTargets: {id: MultiLayoutComponentId, icon: string, label: string}[] = [
|
||||
// { id: 'editor', icon: 'pi pi-pencil', label: 'Editor' },
|
||||
// { id: 'viewer', icon: 'pi pi-box', label: 'Viewer' },
|
||||
// { id: 'customizer', icon: 'pi pi-cog', label: 'Customizer' },
|
||||
// ];
|
||||
const zIndexOfPanelsDependingOnFocus = {
|
||||
editor: {
|
||||
editor: 3,
|
||||
viewer: 1,
|
||||
customizer: 0,
|
||||
},
|
||||
viewer: {
|
||||
editor: 2,
|
||||
viewer: 3,
|
||||
customizer: 2,
|
||||
},
|
||||
customizer: {
|
||||
editor: 0,
|
||||
viewer: 0,
|
||||
customizer: 3,
|
||||
}
|
||||
}
|
||||
|
||||
const layout = state.view.layout
|
||||
const mode = state.view.layout.mode;
|
||||
function getPanelStyle(id: MultiLayoutComponentId): CSSProperties {
|
||||
if (layout.mode === 'multi') {
|
||||
const itemCount = (layout.editor ? 1 : 0) + (layout.viewer ? 1 : 0) + (layout.customizer ? 1 : 0)
|
||||
return {
|
||||
flex: 1,
|
||||
maxWidth: Math.floor(100/itemCount) + '%',
|
||||
display: (state.view.layout as any)[id] ? 'flex' : 'none'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
flex: 1,
|
||||
zIndex: Number((zIndexOfPanelsDependingOnFocus as any)[id][layout.focus]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelContext.Provider value={model}>
|
||||
<FSContext.Provider value={fs}>
|
||||
<div className='flex flex-column' style={{height: '100vh'}}>
|
||||
<div className=""
|
||||
// style={{
|
||||
// backgroundImage: 'url("logo192.png")',
|
||||
// backgroundPositionX: 'left',
|
||||
// backgroundPositionY: 'top',
|
||||
// backgroundRepeat: 'no-repeat',
|
||||
// }}
|
||||
>
|
||||
|
||||
|
||||
{/* <h1>
|
||||
OpenSCAD
|
||||
</h1> */}
|
||||
<div className='flex flex-row' style={{
|
||||
// justifyContent: 'center',
|
||||
margin: '5px',
|
||||
// marginTop: '70px',
|
||||
}}>
|
||||
|
||||
{/* <span style={{flex: 1}} /> */}
|
||||
|
||||
{state.view.layout.mode === 'multi'
|
||||
? <div className='flex flex-row gap-1' style={{
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
margin: '5px'
|
||||
}}>
|
||||
{multiTargets.map(({icon, label, id}) =>
|
||||
// <Button
|
||||
<ToggleButton
|
||||
key={id}
|
||||
// raised={(state.view.layout as any)[id]}
|
||||
checked={(state.view.layout as any)[id]}
|
||||
// label={label}
|
||||
onLabel={label}
|
||||
offLabel={label}
|
||||
onIcon={icon}
|
||||
offIcon={icon}
|
||||
// icon={icon}
|
||||
disabled={id === 'customizer'}
|
||||
// onClick={() => model.changeMultiVisibility(id, !(state.view.layout as any)[id])}
|
||||
onChange={e => model.changeMultiVisibility(id, e.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
: <TabMenu
|
||||
style={{
|
||||
flex: 1,
|
||||
// justifyContent: 'center'
|
||||
}}
|
||||
model={singleTargets.map(({icon, label, id}) =>
|
||||
({icon, label, command: () => model.changeSingleVisibility(id)}))} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={mode === 'multi' ? 'flex flex-row' : 'flex flex-column'}
|
||||
style={mode === 'multi' ? {flex: 1} : {
|
||||
flex: 1,
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* {position: 'relative'}}> */}
|
||||
|
||||
<EditorPanel className={`
|
||||
opacity-animated
|
||||
${layout.mode === 'single' && layout.focus !== 'editor' ? 'opacity-0' : ''}
|
||||
${layout.mode === 'single' ? 'absolute-fill' : ''}
|
||||
`} style={getPanelStyle('editor')} />
|
||||
<ViewerPanel className={layout.mode === 'single' ? `absolute-fill` : ''} style={getPanelStyle('viewer')} />
|
||||
{/* <CustomizerPanel className={`${getPanelClasses('customizer')} absolute-fill`} style={getPanelStyle('customizer')} /> */}
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</FSContext.Provider>
|
||||
</ModelContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// <Splitter style={{ flex: 1 }}>
|
||||
// <SplitterPanel className="flex flex-column align-items-center justify-content-center">
|
||||
// <EditorPanel />
|
||||
// </SplitterPanel>
|
||||
// <SplitterPanel className="flex flex-column align-items-center justify-content-center">
|
||||
// <ViewerPanel/>
|
||||
// </SplitterPanel>
|
||||
// </Splitter>
|
||||
// <Footer />
|
||||
3
src/BrowserFS.d.ts
vendored
3
src/BrowserFS.d.ts
vendored
|
|
@ -3,9 +3,12 @@
|
|||
// import { FS } from "./filesystem";
|
||||
|
||||
declare interface FS {
|
||||
writeFile(path: string, content: string): void;
|
||||
readdir(path: string, cb: (err: any, files: string[]) => void): void;
|
||||
readdirSync(path: string): string[];
|
||||
symlink(target: string, source: string): void;
|
||||
readFileSync(path: string): BufferSource;
|
||||
lstatSync(path: string): {isDirectory(): boolean};
|
||||
}
|
||||
|
||||
declare interface EmscriptenFS extends FS {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { getParentDir } from './filesystem';
|
||||
import { spawnOpenSCAD } from "./openscad-runner";
|
||||
import { processMergedOutputs } from "./output-parser";
|
||||
import { AbortablePromise, turnIntoDelayableExecution } from './utils';
|
||||
|
|
@ -9,15 +10,15 @@ const syntaxDelay = 300;
|
|||
|
||||
type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[]};
|
||||
export const checkSyntax =
|
||||
turnIntoDelayableExecution(syntaxDelay, (source: string) => {
|
||||
turnIntoDelayableExecution(syntaxDelay, (source: string, sourcePath: string) => {
|
||||
// const timestamp = Date.now();
|
||||
|
||||
source = '$preview=true;\n' + source;
|
||||
const sourceFile = 'input.scad';
|
||||
|
||||
const job = spawnOpenSCAD({
|
||||
inputs: [[sourceFile, source + '\n']],
|
||||
args: [sourceFile, "-o", "out.ast"],
|
||||
workingDir: getParentDir(sourcePath),
|
||||
inputs: [[sourcePath, source + '\n']],
|
||||
args: [sourcePath, "-o", "out.ast"],
|
||||
});
|
||||
|
||||
return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
|
||||
|
|
@ -25,7 +26,7 @@ export const checkSyntax =
|
|||
try {
|
||||
const result = await job;
|
||||
// console.log(result);
|
||||
res(processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {[sourceFile]: 1}}));
|
||||
res(processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {[sourcePath]: 1}}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
rej(e);
|
||||
|
|
@ -40,15 +41,15 @@ export type RenderOutput = {stlFile: File, logText: string, markers: monaco.edit
|
|||
|
||||
export type RenderArgs = {
|
||||
source: string,
|
||||
sourcePath: string,
|
||||
features?: string[],
|
||||
extraArgs?: string[],
|
||||
isPreview: boolean
|
||||
}
|
||||
export const render =
|
||||
turnIntoDelayableExecution(renderDelay, (params: RenderArgs) => {
|
||||
const inputFile = 'input.scad';
|
||||
const args = [
|
||||
inputFile,
|
||||
params.sourcePath,
|
||||
"-o", "out.stl",
|
||||
"--export-format=binstl",
|
||||
...(params.features ?? []).map(f => `--enable=${f}`),
|
||||
|
|
@ -63,7 +64,8 @@ export const render =
|
|||
|
||||
const job = spawnOpenSCAD({
|
||||
// wasmMemory,
|
||||
inputs: [['input.scad', source]],
|
||||
workingDir: getParentDir(params.sourcePath),
|
||||
inputs: [[params.sourcePath, source]],
|
||||
args,
|
||||
outputPaths: ['out.stl']
|
||||
});
|
||||
|
|
@ -75,7 +77,7 @@ export const render =
|
|||
console.log(result);
|
||||
|
||||
const {logText, markers} = processMergedOutputs(result.mergedOutputs, {
|
||||
shiftSourceLines: {[inputFile]: prefixLines.length}
|
||||
shiftSourceLines: {[params.sourcePath]: prefixLines.length}
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -83,7 +85,10 @@ export const render =
|
|||
}
|
||||
|
||||
const [output] = result.outputs ?? [];
|
||||
if (!output) throw 'No output from runner!'
|
||||
if (!output) {
|
||||
reject(new Error('No output from runner!'));
|
||||
return;
|
||||
}
|
||||
const [filePath, content] = output;
|
||||
const filePathFragments = filePath.split('/');
|
||||
const fileName = filePathFragments[filePathFragments.length - 1];
|
||||
|
|
|
|||
|
|
@ -5,11 +5,26 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
|||
import React from 'react';
|
||||
import { Model } from "./model"
|
||||
|
||||
export type MultiLayoutComponentId = 'editor' | 'viewer' | 'customizer';
|
||||
export type SingleLayoutComponentId = MultiLayoutComponentId;
|
||||
|
||||
export interface State {
|
||||
params: {
|
||||
sourcePath: string,
|
||||
source: string,
|
||||
features: string[],
|
||||
},
|
||||
|
||||
view: {
|
||||
logs?: boolean,
|
||||
layout: {
|
||||
mode: 'single',
|
||||
focus: SingleLayoutComponentId,
|
||||
} | ({
|
||||
mode: 'multi',
|
||||
} & { [K in MultiLayoutComponentId]: boolean })
|
||||
}
|
||||
|
||||
lastCheckerRun?: {
|
||||
logText: string,
|
||||
markers: monaco.editor.IMarkerData[]
|
||||
|
|
@ -18,6 +33,7 @@ export interface State {
|
|||
previewing?: boolean,
|
||||
checkingSyntax?: boolean,
|
||||
|
||||
error?: string,
|
||||
output?: {
|
||||
isPreview: boolean,
|
||||
stlFile: File,
|
||||
|
|
@ -28,12 +44,8 @@ export interface State {
|
|||
},
|
||||
};
|
||||
|
||||
export const ModelContext = React.createContext(new Model(
|
||||
{
|
||||
params: {
|
||||
source: '',
|
||||
features: ['manifold', 'lazy-union'],
|
||||
}
|
||||
export const ModelContext = React.createContext<Model | null>(null);
|
||||
|
||||
},
|
||||
() => { throw new Error('Not implemented'); }
|
||||
));
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ function collectObjectEntriesDeeply(o: KVObject, out: KVEntriesMap = new Map()):
|
|||
}
|
||||
|
||||
function bubbleChangesUp(o: KVObject, allOriginalEntries: KVEntriesMap) {
|
||||
if (o == null || typeof o !== 'object') {
|
||||
return o;
|
||||
}
|
||||
const entries = Object.entries(o);
|
||||
const originalEntries = allOriginalEntries.get(o);
|
||||
if (!originalEntries) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export type FSMounts = {
|
|||
|
||||
export type Symlinks = {[alias: string]: string};
|
||||
|
||||
export const getParentDir = (path: string) => path.split('/').slice(0, -1).join('/');
|
||||
|
||||
export function readDirAsArray(fs: FS, path: string): Promise<string[] | undefined> {
|
||||
return new Promise((res, rej) => fs.readdir(path, (err, files) => err ? rej(err) : res(files)));
|
||||
}
|
||||
|
|
@ -70,7 +72,7 @@ function configureAndInstallFS(windowOrSelf: Window, options: any) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function createEditorFS(workingDir='/home') {
|
||||
export async function createEditorFS(workingDir='/home'): Promise<FS> {
|
||||
const archiveNames = Object.keys(zipArchives);
|
||||
const librariesMounts = await getBrowserFSLibrariesMounts(archiveNames);
|
||||
const allMounts: FSMounts = {};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { State } from "./app-state";
|
||||
import { validateArray, validateString } from "./utils";
|
||||
import { validateArray, validateBoolean, validateString, validateStringEnum } from "./utils";
|
||||
|
||||
export function writeStateInFragment(state: State) {
|
||||
window.location.hash = encodeURIComponent(JSON.stringify(state.params));
|
||||
window.location.hash = encodeURIComponent(JSON.stringify({
|
||||
params: state.params,
|
||||
view: state.view
|
||||
}));
|
||||
}
|
||||
export function readStateFromFragment(): State | null {
|
||||
if (window.location.hash.startsWith('#') && window.location.hash.length > 1) {
|
||||
try {
|
||||
const params = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
const {params, view} = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
return {
|
||||
params: {
|
||||
sourcePath: validateString(params?.sourcePath),
|
||||
source: validateString(params?.source),
|
||||
features: validateArray(params?.features, validateString),
|
||||
},
|
||||
view: {
|
||||
layout: {
|
||||
mode: validateStringEnum(view?.layout?.mode, ['multi', 'single']),
|
||||
focus: validateStringEnum(view?.layout?.focus, ['editor', 'viewer', 'customizer'], s => false),
|
||||
editor: validateBoolean(view?.layout['editor']),
|
||||
viewer: validateBoolean(view?.layout['viewer']),
|
||||
customizer: validateBoolean(view?.layout['customizer']),
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {readStateFromFragment} from './fragment-state'
|
|||
import { State } from './app-state';
|
||||
import defaultScad from './default-scad'
|
||||
import './index.css';
|
||||
// import "react-widgets/styles.css";
|
||||
|
||||
import debug from 'debug';
|
||||
const log = debug('app:log');
|
||||
|
|
@ -24,16 +25,54 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
(async () => {
|
||||
|
||||
const workingDir = '/home';
|
||||
const fs = await createEditorFS(workingDir);
|
||||
const fs = await createEditorFS(workingDir)!;
|
||||
await registerOpenSCADLanguage(fs, workingDir, zipArchives);
|
||||
|
||||
const initialState: State = readStateFromFragment() ?? {
|
||||
type Mode = State['view']['layout']['mode'];
|
||||
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
||||
? 'multi' : 'single';
|
||||
|
||||
const defaultSourcePath = '/home/playground.scad';
|
||||
const initialState: State = {
|
||||
params: {
|
||||
sourcePath: defaultSourcePath,
|
||||
source: defaultScad,
|
||||
features: [],
|
||||
}
|
||||
},
|
||||
view: {
|
||||
layout: {
|
||||
mode: 'multi',
|
||||
editor: true,
|
||||
viewer: true,
|
||||
customizer: false,
|
||||
} as any
|
||||
},
|
||||
...(readStateFromFragment() ?? {})
|
||||
};
|
||||
|
||||
if (initialState.view.layout.mode != mode) {
|
||||
if (mode === 'multi' && initialState.view.layout.mode === 'single') {
|
||||
initialState.view.layout = {
|
||||
mode,
|
||||
editor: true,
|
||||
viewer: true,
|
||||
customizer: initialState.view.layout.focus == 'customizer'
|
||||
}
|
||||
} else if (mode === 'single' && initialState.view.layout.mode === 'multi') {
|
||||
initialState.view.layout = {
|
||||
mode,
|
||||
focus: initialState.view.layout.viewer ? 'viewer'
|
||||
: initialState.view.layout.customizer ? 'customizer'
|
||||
: 'editor'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFile(initialState.params.sourcePath, initialState.params.source);
|
||||
if (initialState.params.sourcePath !== defaultSourcePath) {
|
||||
fs.writeFile(defaultSourcePath, defaultScad);
|
||||
}
|
||||
|
||||
const defaultFeatures = ['manifold', 'fast-csg', 'lazy-union'];
|
||||
defaultFeatures.forEach(f => {
|
||||
if (initialState.params.features.indexOf(f) < 0)
|
||||
|
|
@ -45,7 +84,7 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App initialState={initialState} />
|
||||
<App initialState={initialState} fs={fs} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
|||
|
||||
export default {
|
||||
lineNumbers: 'on',
|
||||
automaticLayout: true,
|
||||
// automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 12,
|
||||
language: 'openscad',
|
||||
|
|
|
|||
76
src/model.ts
76
src/model.ts
|
|
@ -1,12 +1,12 @@
|
|||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||
|
||||
import { checkSyntax, render, RenderArgs, RenderOutput } from "./actions";
|
||||
import { State } from "./app-state";
|
||||
import { MultiLayoutComponentId, SingleLayoutComponentId, State } from "./app-state";
|
||||
import { bubbleUpDeepMutations } from "./deep-mutate";
|
||||
import { writeStateInFragment } from "./fragment-state";
|
||||
|
||||
export class Model {
|
||||
constructor(public state: State, private setStateCallback?: (state: State) => void) {
|
||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void) {
|
||||
}
|
||||
|
||||
init() {
|
||||
|
|
@ -34,6 +34,55 @@ export class Model {
|
|||
return false;
|
||||
}
|
||||
|
||||
set logsVisible(value: boolean) {
|
||||
this.mutate(s => s.view.logs = value);
|
||||
}
|
||||
|
||||
changeLayout(mode: 'multi' | 'single') {
|
||||
if (this.state.view.layout.mode === mode) return;
|
||||
this.mutate(s => {
|
||||
s.view.layout = s.view.layout.mode === 'multi'
|
||||
? {
|
||||
mode: 'single',
|
||||
focus: s.view.layout.editor ? 'editor' : s.view.layout.viewer ? 'viewer' : 'customizer'
|
||||
}
|
||||
: {
|
||||
mode: 'multi',
|
||||
editor: s.view.layout.focus === 'editor',
|
||||
viewer: s.view.layout.focus === 'viewer',
|
||||
customizer: s.view.layout.focus === 'customizer',
|
||||
}
|
||||
});
|
||||
}
|
||||
changeSingleVisibility(focus: SingleLayoutComponentId) {
|
||||
this.mutate(s => {
|
||||
if (s.view.layout.mode !== 'single') throw new Error('Wrong mode');
|
||||
s.view.layout.focus = focus;
|
||||
});
|
||||
}
|
||||
|
||||
changeMultiVisibility(target: MultiLayoutComponentId, visible: boolean) {
|
||||
this.mutate(s => {
|
||||
if (s.view.layout.mode !== 'multi') throw new Error('Wrong mode');
|
||||
s.view.layout[target] = visible
|
||||
if ((s.view.layout.customizer ? 1 : 0) + (s.view.layout.editor ? 1 : 0) + (s.view.layout.viewer ? 1 : 0) == 0) {
|
||||
// Select at least one panel
|
||||
// s.view.layout.editor = true;
|
||||
s.view.layout[target] = !visible;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
openFile(path: string) {
|
||||
// alert(`TODO: open ${path}`);
|
||||
if (this.mutate(s => {
|
||||
s.params.source = new TextDecoder("utf-8").decode(this.fs.readFileSync(path));
|
||||
s.params.sourcePath = path;
|
||||
})) {
|
||||
this.processSource();
|
||||
}
|
||||
}
|
||||
|
||||
set source(source: string) {
|
||||
if (this.mutate(s => { s.params.source = source; })) {
|
||||
this.processSource();
|
||||
|
|
@ -41,14 +90,20 @@ export class Model {
|
|||
}
|
||||
|
||||
private processSource() {
|
||||
const params = this.state.params;
|
||||
// this.fs.writeFile(params.sourcePath, params.source);
|
||||
this.checkSyntax();
|
||||
this.render({isPreview: true, now: false});
|
||||
}
|
||||
checkSyntax() {
|
||||
this.mutate(s => s.checkingSyntax = true);
|
||||
checkSyntax(this.state.params.source)({now: false, callback: checkerRun => this.mutate(s => {
|
||||
checkSyntax(this.state.params.source, this.state.params.sourcePath)({now: false, callback: (checkerRun, err) => this.mutate(s => {
|
||||
if (err != null) {
|
||||
console.error('Error while checking syntax:', err)
|
||||
} else {
|
||||
s.lastCheckerRun = checkerRun;
|
||||
s.checkingSyntax = false;
|
||||
}
|
||||
})});
|
||||
}
|
||||
|
||||
|
|
@ -62,13 +117,16 @@ export class Model {
|
|||
}
|
||||
this.mutate(s => setRendering(s, true));
|
||||
|
||||
const source = this.state.params.source;
|
||||
const features = this.state.params.features;
|
||||
const renderArgs = {source, features, extraArgs: [], isPreview};
|
||||
// console.log('renderArgs', renderArgs);
|
||||
const {source, sourcePath, features} = this.state.params;
|
||||
|
||||
render(renderArgs)({now, callback: output => {
|
||||
render({source, sourcePath, features, extraArgs: [], isPreview})({now, callback: (output, err) => {
|
||||
this.mutate(s => {
|
||||
setRendering(s, false);
|
||||
if (err != null) {
|
||||
console.error('Error while doing ' + (isPreview ? 'preview' : 'rendering') + ':', err)
|
||||
s.error = `${err}`;
|
||||
} else if (output) {
|
||||
s.error = undefined;
|
||||
s.lastCheckerRun = {
|
||||
logText: output.logText,
|
||||
markers: output.markers,
|
||||
|
|
@ -85,7 +143,7 @@ export class Model {
|
|||
formattedElapsedMillis: formatMillis(output.elapsedMillis),
|
||||
formattedStlFileSize: formatBytes(output.stlFile.size),
|
||||
};
|
||||
setRendering(s, false);
|
||||
}
|
||||
});
|
||||
}})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function createWasmMemory({maximumMegabytes, maximumBytes}: {maximumMegab
|
|||
// Output is {outputs: [name, content][], mergedOutputs: [{(stderr|stdout|error)?: string}], exitCode: number}
|
||||
export type OpenSCADInvocation = {
|
||||
wasmMemory?: WebAssembly.Memory,
|
||||
workingDir: string,
|
||||
inputs?: [string, string][],
|
||||
args: string[],
|
||||
outputPaths?: string[],
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
import OpenSCAD from "./wasm/openscad.js";
|
||||
|
||||
import { getBrowserFSLibrariesMounts, symlinkLibraries } from "./filesystem";
|
||||
import { getBrowserFSLibrariesMounts, getParentDir, symlinkLibraries } from "./filesystem";
|
||||
import { OpenSCADInvocation, OpenSCADInvocationResults } from "./openscad-runner";
|
||||
import { zipArchives } from "./zip-archives";
|
||||
declare var BrowserFS: BrowserFSInterface
|
||||
|
||||
// importScripts("browserfs.min.js");
|
||||
importScripts("https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.min.js");
|
||||
importScripts("browserfs.min.js");
|
||||
// importScripts("https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.min.js");
|
||||
|
||||
const allArchiveNames = Object.keys(zipArchives)
|
||||
const allZipMountsPromise = getBrowserFSLibrariesMounts(allArchiveNames);
|
||||
|
|
@ -17,7 +17,7 @@ export type MergedOutputs = {stdout?: string, stderr?: string, error?: string}[]
|
|||
|
||||
addEventListener('message', async (e) => {
|
||||
|
||||
const { inputs, args, outputPaths, wasmMemory } = e.data as OpenSCADInvocation;
|
||||
const { inputs, args, outputPaths, wasmMemory, workingDir } = e.data as OpenSCADInvocation;
|
||||
|
||||
const mergedOutputs: MergedOutputs = [];
|
||||
try {
|
||||
|
|
@ -33,8 +33,14 @@ addEventListener('message', async (e) => {
|
|||
// console.log('stderr: ' + text);
|
||||
mergedOutputs.push({ stderr: text })
|
||||
},
|
||||
ENV: {
|
||||
OPENSCADPATH: '/libraries:/home'
|
||||
}
|
||||
});
|
||||
|
||||
// console.error('INSTANCE')
|
||||
// console.error(Object.keys(instance))
|
||||
|
||||
// await browserFSInit;
|
||||
await new Promise(async (resolve, reject) => {
|
||||
BrowserFS.install(self);
|
||||
|
|
@ -50,7 +56,8 @@ addEventListener('message', async (e) => {
|
|||
const archiveNames = allArchiveNames;
|
||||
|
||||
// instance.FS.mkdir('tmp');
|
||||
instance.FS.mkdir('/tmp/run');
|
||||
// instance.FS.mkdir('/home');
|
||||
// instance.FS.mkdir('/tmp/run');
|
||||
instance.FS.mkdir('/libraries');
|
||||
|
||||
// await browserFSInit;
|
||||
|
|
@ -64,9 +71,13 @@ addEventListener('message', async (e) => {
|
|||
}, instance.ERRNO_CODES ?? {});
|
||||
instance.FS.mount(BFS, {root: '/'}, '/libraries');
|
||||
|
||||
await symlinkLibraries(archiveNames, instance.FS, '/libraries', '/tmp/run');
|
||||
// await symlinkLibraries(archiveNames, instance.FS, '/libraries', '/tmp/run');
|
||||
await symlinkLibraries(archiveNames, instance.FS, '/libraries', '/home');
|
||||
|
||||
instance.FS.chdir('/tmp/run');
|
||||
// const
|
||||
// instance.FS.chdir('/tmp/run');
|
||||
instance.FS.chdir('/home');
|
||||
// instance.FS.chdir(workingDir);
|
||||
|
||||
// console.log('.', instance.FS.readdir('.'));
|
||||
// console.log('fonts', instance.FS.readdir('fonts'));
|
||||
|
|
@ -74,11 +85,16 @@ addEventListener('message', async (e) => {
|
|||
|
||||
if (inputs) {
|
||||
for (const [path, content] of inputs) {
|
||||
try {
|
||||
// const parent = getParentDir(path);
|
||||
instance.FS.writeFile(path, content);
|
||||
} catch (e) {
|
||||
console.error(`Error while trying to write ${path}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('Calling main ', args)
|
||||
console.error('Calling main ', args)
|
||||
const start = performance.now();
|
||||
const exitCode = instance.callMain(args);
|
||||
const end = performance.now();
|
||||
|
|
@ -94,8 +110,7 @@ addEventListener('message', async (e) => {
|
|||
|
||||
postMessage(result);
|
||||
} catch (e) {
|
||||
|
||||
console.error(e);
|
||||
console.trace(e);//, e instanceof Error ? e.stack : '');
|
||||
const error = `${e}`;
|
||||
mergedOutputs.push({ error });
|
||||
postMessage({
|
||||
|
|
|
|||
10
src/utils.ts
10
src/utils.ts
|
|
@ -29,7 +29,7 @@ export function turnIntoDelayableExecution<T extends any[], R>(
|
|||
let pendingId: number | null;
|
||||
let runningJobKillSignal: (() => void) | null;
|
||||
|
||||
return (...args: T) => async ({now, callback}: {now: boolean, callback: (result: R) => void}) => {
|
||||
return (...args: T) => async ({now, callback}: {now: boolean, callback: (result?: R, error?: any) => void}) => {
|
||||
const doExecute = async () => {
|
||||
if (runningJobKillSignal) {
|
||||
runningJobKillSignal();
|
||||
|
|
@ -39,6 +39,8 @@ export function turnIntoDelayableExecution<T extends any[], R>(
|
|||
runningJobKillSignal = abortablePromise.kill;
|
||||
try {
|
||||
callback(await abortablePromise);
|
||||
} catch (e) {
|
||||
callback(undefined, e);
|
||||
} finally {
|
||||
runningJobKillSignal = null;
|
||||
}
|
||||
|
|
@ -55,6 +57,12 @@ export function turnIntoDelayableExecution<T extends any[], R>(
|
|||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ module.exports = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|gif|woff|woff2|eot|ttf|svg)$/,
|
||||
loader: "url-loader?limit=100000"
|
||||
},
|
||||
// {
|
||||
// test: /\.(png|gif|woff|woff2|eot|ttf|svg)$/,
|
||||
// loader: "url-loader?limit=100000"
|
||||
// },
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue