widgets, file size and elapsed time
This commit is contained in:
parent
8cdf0e29b9
commit
f996bbc1fb
11 changed files with 118 additions and 57 deletions
|
|
@ -15,6 +15,8 @@
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"monaco-editor": "^0.36.1",
|
"monaco-editor": "^0.36.1",
|
||||||
|
"primeicons": "^6.0.1",
|
||||||
|
"primereact": "^9.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-stl-viewer": "^2.2.5",
|
"react-stl-viewer": "^2.2.5",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/primeicons/primeicons.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/primereact/resources/themes/lara-light-indigo/theme.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/primereact/resources/primereact.min.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/primeflex@3.2.1/primeflex.min.css" />
|
||||||
|
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="OpenSCAD Playground: edit .scad models and render them to STL from the browser (WebAssembly build OpenSCAD, with Monaco editor and STL Viewer)"
|
content="OpenSCAD Playground: edit .scad models and render them to STL from the browser (WebAssembly build OpenSCAD, with Monaco editor and STL Viewer)"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
.App {
|
/* .App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,4 +42,4 @@
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: calc(min(200px, 30vh));
|
height: calc(min(200px, 30vh));
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
} */
|
||||||
79
src/App.tsx
79
src/App.tsx
|
|
@ -1,13 +1,19 @@
|
||||||
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import {ModelContext, State} from './app-state'
|
import {ModelContext, State} from './app-state'
|
||||||
import Editor, { loader, Monaco } from '@monaco-editor/react';
|
import Editor, { loader, Monaco } from '@monaco-editor/react';
|
||||||
import './App.css';
|
// import './App.css';
|
||||||
import openscadEditorOptions from './language/openscad-editor-options';
|
import openscadEditorOptions from './language/openscad-editor-options';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import {StlViewer} from "react-stl-viewer";
|
import {StlViewer} from "react-stl-viewer";
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { ProgressBar } from 'primereact/progressbar';
|
||||||
|
|
||||||
|
// import "primereact/resources/themes/lara-light-indigo/theme.css";
|
||||||
|
// import "primereact/resources/primereact.min.css";
|
||||||
|
// import "primeicons/primeicons.css";
|
||||||
|
|
||||||
let monacoInstance: Monaco
|
let monacoInstance: Monaco
|
||||||
loader.init().then(mi => monacoInstance = mi);
|
loader.init().then(mi => monacoInstance = mi);
|
||||||
|
|
@ -31,13 +37,13 @@ function EditorPanel() {
|
||||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||||
monaco.KeyCode.F6,
|
monaco.KeyCode.F6,
|
||||||
],
|
],
|
||||||
run: () => model.render({isPreview: false})
|
run: () => model.render({isPreview: false, now: true})
|
||||||
});
|
});
|
||||||
editor.addAction({
|
editor.addAction({
|
||||||
id: "openscad-preview",
|
id: "openscad-preview",
|
||||||
label: "Preview OpenSCAD",
|
label: "Preview OpenSCAD",
|
||||||
keybindings: [monaco.KeyCode.F5],
|
keybindings: [monaco.KeyCode.F5],
|
||||||
run: () => model.render({isPreview: false})
|
run: () => model.render({isPreview: true, now: true})
|
||||||
});
|
});
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +85,7 @@ export function App({initialState}: {initialState: State}) {
|
||||||
const model = new Model(state, setState);
|
const model = new Model(state, setState);
|
||||||
useEffect(() => model.init());
|
useEffect(() => model.init());
|
||||||
|
|
||||||
const stlModelRef = useRef(null);
|
// const stlModelRef = forwardRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelContext.Provider value={model}>
|
<ModelContext.Provider value={model}>
|
||||||
|
|
@ -88,41 +94,50 @@ export function App({initialState}: {initialState: State}) {
|
||||||
<div style={{width: "50vw"}}>
|
<div style={{width: "50vw"}}>
|
||||||
<EditorPanel/>
|
<EditorPanel/>
|
||||||
</div>
|
</div>
|
||||||
{state.output?.stlFileURL &&
|
<div style={{display: 'flex', flexDirection: 'column', flex: 1, position: 'relative'}}>
|
||||||
<StlViewer
|
{(state.rendering || state.previewing) &&
|
||||||
style={{
|
<ProgressBar mode="indeterminate" style={{ height: '6px' }}></ProgressBar>}
|
||||||
flex: 1
|
{state.output?.stlFileURL &&
|
||||||
}}
|
<StlViewer
|
||||||
ref={stlModelRef}
|
style={{
|
||||||
showAxes={true}
|
flex: 1
|
||||||
orbitControls
|
}}
|
||||||
shadows
|
// ref={stlModelRef}
|
||||||
modelProps={{
|
showAxes={true}
|
||||||
color: '#f9d72c',
|
orbitControls
|
||||||
|
shadows
|
||||||
|
modelProps={{
|
||||||
|
color: '#f9d72c',
|
||||||
|
|
||||||
|
}}
|
||||||
|
url={state.output?.stlFileURL ?? ''}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
}}
|
|
||||||
url={state.output?.stlFileURL ?? ''}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{display: 'flex', flexDirection: 'row'}}>
|
</div>
|
||||||
<button onClick={() => model.render({isPreview: false})}>Render</button>
|
<div style={{display: 'flex', flexDirection: 'row', gap: '10px'}}>
|
||||||
{model.state.previewing && 'previewing... '}
|
<Button onClick={() => model.render({isPreview: false, now: true})}>Render</Button>
|
||||||
{model.state.rendering && 'rendering... '}
|
{state.output && state.output.formattedElapsedMillis}
|
||||||
{model.state.checkingSyntax && 'checking syntax... '}
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
<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/documentation.html">Docs</a> |
|
||||||
<a target="_blank" className="text-fragment" href="http://openscad.org/cheatsheet/">Cheatsheet</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>
|
<a target="_blank" href="https://github.com/openscad/openscad-playground/blob/rewrite1/LICENSE.md">LICENSES</a>
|
||||||
|
|
||||||
{state.output?.stlFileURL && (
|
|
||||||
<a href={state.output?.stlFileURL}
|
|
||||||
title="STL Download">
|
|
||||||
Download Model
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModelContext.Provider>
|
</ModelContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const checkSyntax =
|
||||||
});
|
});
|
||||||
|
|
||||||
var renderDelay = 1000;
|
var renderDelay = 1000;
|
||||||
export type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis?: number}
|
export type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis: number}
|
||||||
|
|
||||||
export type RenderArgs = {
|
export type RenderArgs = {
|
||||||
source: string,
|
source: string,
|
||||||
|
|
@ -45,23 +45,26 @@ export type RenderArgs = {
|
||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
}
|
}
|
||||||
export const render =
|
export const render =
|
||||||
turnIntoDelayableExecution(renderDelay, (args: RenderArgs) => {
|
turnIntoDelayableExecution(renderDelay, (params: RenderArgs) => {
|
||||||
|
const inputFile = 'input.scad';
|
||||||
|
const args = [
|
||||||
|
inputFile,
|
||||||
|
"-o", "out.stl",
|
||||||
|
"--export-format=binstl",
|
||||||
|
...(params.features ?? []).map(f => `--enable=${f}`),
|
||||||
|
...(params.extraArgs ?? [])
|
||||||
|
]
|
||||||
|
|
||||||
const prefixLines: string[] = [];
|
const prefixLines: string[] = [];
|
||||||
if (args.isPreview) prefixLines.push('$preview=false;');
|
if (params.isPreview) {
|
||||||
const source = args.isPreview ? [...prefixLines, args.source].join('\n') : args.source
|
prefixLines.push('$preview=true;');
|
||||||
const inputFile = 'input.scad';
|
}
|
||||||
|
const source = [...prefixLines, params.source].join('\n');
|
||||||
|
|
||||||
const job = spawnOpenSCAD({
|
const job = spawnOpenSCAD({
|
||||||
// wasmMemory,
|
// wasmMemory,
|
||||||
inputs: [['input.scad', source]],
|
inputs: [['input.scad', source]],
|
||||||
args: [
|
args,
|
||||||
inputFile,
|
|
||||||
"-o", "out.stl",
|
|
||||||
"--export-format=binstl",
|
|
||||||
...(args.features ?? []).map(f => `--enable=${f}`),
|
|
||||||
...(args.extraArgs ?? [])
|
|
||||||
],
|
|
||||||
outputPaths: ['out.stl']
|
outputPaths: ['out.stl']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export interface State {
|
||||||
isPreview: boolean,
|
isPreview: boolean,
|
||||||
stlFile: File,
|
stlFile: File,
|
||||||
stlFileURL: string,
|
stlFileURL: string,
|
||||||
|
elapsedMillis: number,
|
||||||
|
formattedElapsedMillis: string,
|
||||||
|
formattedStlFileSize: string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ translate([10, 10, 10])
|
||||||
//
|
//
|
||||||
// include <BOSL2/std.scad>
|
// include <BOSL2/std.scad>
|
||||||
// translate([-100, 0, 0])
|
// translate([-100, 0, 0])
|
||||||
spheroid(d=100, style="icosa", $fn=20);
|
// spheroid(d=100, style="icosa", $fn=20);
|
||||||
`
|
`
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: (--font-family);
|
||||||
|
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif; */
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
src/model.ts
32
src/model.ts
|
|
@ -42,7 +42,7 @@ export class Model {
|
||||||
|
|
||||||
private processSource() {
|
private processSource() {
|
||||||
this.checkSyntax();
|
this.checkSyntax();
|
||||||
this.render({isPreview: true});
|
this.render({isPreview: true, now: false});
|
||||||
}
|
}
|
||||||
checkSyntax() {
|
checkSyntax() {
|
||||||
this.mutate(s => s.checkingSyntax = true);
|
this.mutate(s => s.checkingSyntax = true);
|
||||||
|
|
@ -52,7 +52,7 @@ export class Model {
|
||||||
})});
|
})});
|
||||||
}
|
}
|
||||||
|
|
||||||
render({isPreview}: {isPreview: boolean}) {
|
render({isPreview, now}: {isPreview: boolean, now: boolean}) {
|
||||||
const setRendering = (s: State, value: boolean) => {
|
const setRendering = (s: State, value: boolean) => {
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
s.previewing = value;
|
s.previewing = value;
|
||||||
|
|
@ -64,9 +64,10 @@ export class Model {
|
||||||
|
|
||||||
const source = this.state.params.source;
|
const source = this.state.params.source;
|
||||||
const features = this.state.params.features;
|
const features = this.state.params.features;
|
||||||
const renderArgs = {source, features, extraArgs: ['-D$preview=true'], isPreview};
|
const renderArgs = {source, features, extraArgs: [], isPreview};
|
||||||
|
// console.log('renderArgs', renderArgs);
|
||||||
|
|
||||||
render(renderArgs)({now: !isPreview, callback: output => {
|
render(renderArgs)({now, callback: output => {
|
||||||
this.mutate(s => {
|
this.mutate(s => {
|
||||||
s.lastCheckerRun = {
|
s.lastCheckerRun = {
|
||||||
logText: output.logText,
|
logText: output.logText,
|
||||||
|
|
@ -75,10 +76,14 @@ export class Model {
|
||||||
if (s.output?.stlFileURL) {
|
if (s.output?.stlFileURL) {
|
||||||
URL.revokeObjectURL(s.output.stlFileURL);
|
URL.revokeObjectURL(s.output.stlFileURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
s.output = {
|
s.output = {
|
||||||
isPreview: isPreview,
|
isPreview: isPreview,
|
||||||
stlFile: output.stlFile,
|
stlFile: output.stlFile,
|
||||||
stlFileURL: URL.createObjectURL(output.stlFile),
|
stlFileURL: URL.createObjectURL(output.stlFile),
|
||||||
|
elapsedMillis: output.elapsedMillis,
|
||||||
|
formattedElapsedMillis: formatMillis(output.elapsedMillis),
|
||||||
|
formattedStlFileSize: formatBytes(output.stlFile.size),
|
||||||
};
|
};
|
||||||
setRendering(s, false);
|
setRendering(s, false);
|
||||||
});
|
});
|
||||||
|
|
@ -86,3 +91,22 @@ export class Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMillis(n: number) {
|
||||||
|
if (n < 1000)
|
||||||
|
return `${Math.floor(n)} millis`;
|
||||||
|
|
||||||
|
return `${Math.floor(n / 100) / 10} sec`;
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ export type OpenSCADInvocationResults = {
|
||||||
error?: string,
|
error?: string,
|
||||||
outputs?: [string, string][],
|
outputs?: [string, string][],
|
||||||
mergedOutputs: MergedOutputs,
|
mergedOutputs: MergedOutputs,
|
||||||
elapsedMillis?: number,
|
elapsedMillis: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnOpenSCAD({inputs, args, outputPaths}: OpenSCADInvocation): AbortablePromise<OpenSCADInvocationResults> {
|
export function spawnOpenSCAD({inputs, args, outputPaths}: OpenSCADInvocation): AbortablePromise<OpenSCADInvocationResults> {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,16 @@ module.exports = {
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
use: [
|
use: [
|
||||||
{loader: 'css-loader', options:{url: false}}
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options:{url: false},
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|gif|woff|woff2|eot|ttf|svg)$/,
|
||||||
|
loader: "url-loader?limit=100000"
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue