Split into more components

This commit is contained in:
ochafik 2023-03-26 19:29:24 +01:00
commit 0e97a288e1
11 changed files with 276 additions and 160 deletions

View file

@ -9,10 +9,11 @@ import { MenuItem } from 'primereact/menuitem';
import { TreeSelect } from 'primereact/treeselect';
import TreeNode from 'primereact/treenode';
import { Menu } from 'primereact/menu';
import { getParentDir } from '../fs/filesystem';
import { getParentDir, librariesFolder } from '../fs/filesystem';
import { buildUrlForStateParams } from '../state/fragment-state';
import { blankProjectState } from '../state/initial-state';
import { ModelContext, FSContext } from './contexts';
import FilePicker, { isFileWritable } from './FilePicker';
// import "primereact/resources/themes/lara-light-indigo/theme.css";
// import "primereact/resources/primereact.min.css";
@ -21,42 +22,45 @@ import { ModelContext, FSContext } from './contexts';
let monacoInstance: Monaco
loader.init().then(mi => monacoInstance = mi);
const isFileWritable = (path: string) => getParentDir(path) === '/home'
// const isFileWritable = (path: string) => getParentDir(path) === '/home'
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)));
// function listFilesAsNodes(fs: FS, path: string): TreeNode[] {
// const files: [string, string][] = []
// const dirs: [string, string][] = []
// for (const name of fs.readdirSync(path)) {
// if (name.startsWith('.')) {
// continue;
// }
// 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;
}
// 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;
// }
export default function EditorPanel({className, style}: {className?: string, style?: CSSProperties}) {
@ -67,7 +71,7 @@ export default function EditorPanel({className, style}: {className?: string, sty
const state = model.state;
const fs = useContext(FSContext);
// const fs = useContext(FSContext);
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
// const [modelFile, setModelFile] = useState(state.params.sourcePath);
@ -100,8 +104,6 @@ export default function EditorPanel({className, style}: {className?: string, sty
setEditor(editor)
}
const fsItems = fs && listFilesAsNodes(fs, '/home') || [];
return (
<div className={`editor-panel ${className ?? ''}`} style={{
// maxWidth: '5 0vw',
@ -111,7 +113,6 @@ export default function EditorPanel({className, style}: {className?: string, sty
// width: '100%', height: '100%',
...(style ?? {})
}}>
{/* <BreadCrumb model={breadCrumbsItems} home={breadCrumbsHome}/> */}
<div className='flex flex-row gap-2' style={{
margin: '5px',
}}>
@ -214,73 +215,39 @@ export default function EditorPanel({className, style}: {className?: string, sty
] as MenuItem[]} popup ref={menu} />
<Button title="Editor menu" rounded text icon="pi pi-ellipsis-h" onClick={(e) => menu.current && menu.current.toggle(e)} />
<TreeSelect
title='OpenSCAD Playground Files'
value={state.params.sourcePath}
onChange={(e) => model.openFile(String(e.value))}
// dropdownIcon="pi pi-folder-open"
filter
<FilePicker
style={{
flex: 1,
}}
options={fsItems} />
}}/>
</div>
{/* <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 absolute-fill"
defaultLanguage="openscad"
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,
readOnly: !isFileWritable(state.params.sourcePath)
}}
// height="100%"
// width="100%"
// height="80vh"
/>
</div>
<div style={{
position: 'relative',
flex: 1
}}>
<Editor
className="openscad-editor absolute-fill"
defaultLanguage="openscad"
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,
readOnly: !isFileWritable(state.params.sourcePath)
}}
/>
</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 style={{
display: state.view.logs ? undefined : 'none',
overflowY: 'scroll',
height: 'calc(min(200px, 30vh))',
}}>
<pre><code id="logs" style={{
}}>{state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
</div>
</div>
)

View file

@ -0,0 +1,85 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { CSSProperties, useContext } from 'react';
import { TreeSelect } from 'primereact/treeselect';
import TreeNode from 'primereact/treenode';
import { getParentDir, librariesFolder } from '../fs/filesystem';
import { ModelContext, FSContext } from './contexts';
export const isFileWritable = (path: string) => getParentDir(path) === '/home'
function listFilesAsNodes(fs: FS, path: string): TreeNode[] {
const files: [string, string][] = []
const dirs: [string, string][] = []
for (const name of fs.readdirSync(path)) {
if (name.startsWith('.')) {
continue;
}
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;
}
export default function FilePicker({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 fsItems = fs && [
{
icon: 'pi pi-home',
label: 'User files',
key: '/home',
children: listFilesAsNodes(fs, '/home'),
selectable: false
},
{
icon: 'pi pi-database',
label: 'Builtin libraries',
key: librariesFolder,
children: listFilesAsNodes(fs, librariesFolder),
selectable: false
},
] || [];
return (
<TreeSelect
className={className}
title='OpenSCAD Playground Files'
value={state.params.sourcePath}
onChange={(e) => model.openFile(String(e.value))}
// dropdownIcon="pi pi-folder-open"
filter
style={style}
// style={{style}}
options={fsItems} />
)
}

View file

@ -9,9 +9,10 @@ import { ProgressBar } from 'primereact/progressbar';
import { MenuItem } from 'primereact/menuitem';
import { Badge } from 'primereact/badge';
import { Menu } from 'primereact/menu';
import { ToggleButton } from 'primereact/togglebutton';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { Toast } from 'primereact/toast';
import SettingsMenu from './SettingsMenu';
import HelpMenu from './HelpMenu';
function downloadOutput(state: State) {
if (!state.output) return;
@ -46,11 +47,11 @@ function downloadOutput(state: State) {
export default function Footer({style}: {style?: CSSProperties}) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const state = model.state;
const menu = useRef<Menu>(null);
const helpMenu = useRef<Menu>(null);
const toast = useRef<Toast>(null);
const state = model.state;
const severityByMarkerSeverity = new Map<monaco.MarkerSeverity, 'danger' | 'warning' | 'info'>([
[monaco.MarkerSeverity.Error, 'danger'],
@ -147,49 +148,9 @@ export default function Footer({style}: {style?: CSSProperties}) {
// </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')} />
<SettingsMenu />
<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)} />
<HelpMenu />
</div>
</>
}

View file

@ -0,0 +1,48 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { useContext, useRef } from 'react';
import { Button } from 'primereact/button';
import { MenuItem } from 'primereact/menuitem';
import { Menu } from 'primereact/menu';
import { ModelContext } from './contexts';
export default function HelpMenu() {
const menuRef = useRef<Menu>(null);
return (
<>
<Menu model={[
{
label: "openscad-playground",
icon: 'pi pi-github',
url: 'https://github.com/openscad/openscad-playground/tree/rewrite1',
target: '_blank'
},
{
label: 'LICENSES',
icon: 'pi pi-info-circle',
url: 'https://github.com/openscad/openscad-playground/blob/rewrite1/LICENSE.md',
target: '_blank'
},
{
label: 'OpenSCAD Docs',
icon: 'pi pi-book',
url: 'http://openscad.org/documentation.html',
target: '_blank'
},
{
label: 'OpenSCAD Cheatsheet',
icon: 'pi pi-palette',
url: 'http://openscad.org/cheatsheet/',
target: '_blank'
},
{
label: 'BOSL2 Cheatsheet',
icon: 'pi pi-palette',
url: 'https://github.com/revarbat/BOSL2/wiki/CheatSheet',
target: '_blank'
},
] as MenuItem[]} popup ref={menuRef} />
<Button title="Help & Licenses" rounded icon="pi pi-question-circle" onClick={(e) => menuRef.current && menuRef.current.toggle(e)} />
</>
);
}

View file

@ -0,0 +1,45 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { useContext, useRef } from 'react';
import { Button } from 'primereact/button';
import { MenuItem } from 'primereact/menuitem';
import { Menu } from 'primereact/menu';
import { ModelContext } from './contexts';
export default function SettingsMenu() {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const state = model.state;
const settingsMenu = useRef<Menu>(null);
return (
<>
<Menu model={[
{
label: state.view.layout.mode === 'multi'
? 'Switch to single panel mode'
: "Switch to side-by-side mode",
icon: 'pi pi-table',
// disabled: true,
command: () => model.changeLayout(state.view.layout.mode === 'multi' ? 'single' : 'multi'),
},
{
separator: true
},
{
label: state.view.showAxes ? 'Hide axes' : 'Show axes',
icon: 'pi pi-box',
// disabled: true,
command: () => model.mutate(s => s.view.showAxes = !s.view.showAxes)
},
{
label: state.view.showShadows ? 'Hide shadows' : 'Add shadows',
icon: 'pi pi-box',
// disabled: true,
command: () => model.mutate(s => s.view.showShadows = !s.view.showShadows)
},
] as MenuItem[]} popup ref={settingsMenu} />
<Button title="Settings menu" rounded text icon="pi pi-cog" onClick={(e) => settingsMenu.current && settingsMenu.current.toggle(e)} />
</>
);
}

View file

@ -26,9 +26,9 @@ export default function ViewerPanel({className, style}: {className?: string, sty
// width: '100%'
}}
// ref={stlModelRef}
showAxes={true}
showAxes={state.view.showAxes}
orbitControls
shadows
shadows={state.view.showShadows}
modelProps={{
color: '#f9d72c',

View file

@ -31,7 +31,7 @@ export type OpenSCADInvocationResults = {
elapsedMillis: number,
}
export function spawnOpenSCAD({inputs, args, outputPaths}: OpenSCADInvocation): AbortablePromise<OpenSCADInvocationResults> {
export function spawnOpenSCAD(invocation: OpenSCADInvocation): AbortablePromise<OpenSCADInvocationResults> {
var worker: Worker | null;
var rejection: (err: any) => void;
@ -55,7 +55,7 @@ export function spawnOpenSCAD({inputs, args, outputPaths}: OpenSCADInvocation):
resolve(e.data);
terminate();
}
worker.postMessage({inputs, args, outputPaths})
worker.postMessage(invocation)
return () => {
// rejection({error: 'Terminated'});

View file

@ -2,7 +2,7 @@
import OpenSCAD from "../wasm/openscad.js";
import { createEditorFS, getBrowserFSLibrariesMounts, symlinkLibraries } from "../fs/filesystem";
import { createEditorFS, getBrowserFSLibrariesMounts, librariesFolder, symlinkLibraries } from "../fs/filesystem";
import { OpenSCADInvocation, OpenSCADInvocationResults } from "./openscad-runner";
import { zipArchives } from "../fs/zip-archives";
declare var BrowserFS: BrowserFSInterface
@ -33,12 +33,14 @@ addEventListener('message', async (e) => {
console.debug('stderr: ' + text);
mergedOutputs.push({ stderr: text })
},
ENV: {
OPENSCADPATH: '/home'
}
// ENV: {
// OPENSCADPATH: '/home'
// }
});
const fs = await createEditorFS('/home');
// const librariesFolder = '/home/.local/share/OpenSCAD/libraries'
const fs = await createEditorFS();
await symlinkLibraries(allArchiveNames, fs, librariesFolder, '/home');//'/home', '/home');
// https://github.com/emscripten-core/emscripten/issues/10061
const BFS = new BrowserFS.EmscriptenFS(
@ -49,7 +51,7 @@ addEventListener('message', async (e) => {
}, instance.ERRNO_CODES ?? {});
instance.FS.mount(BFS, {root: '/home'}, '/home');
// await symlinkLibraries(allArchiveNames, instance.FS, '/home/libraries', '/home');
//await symlinkLibraries(allArchiveNames, instance.FS, '/home/libraries', '/home');
instance.FS.chdir('/home');
@ -65,7 +67,7 @@ addEventListener('message', async (e) => {
}
}
console.log('Calling main ', args)
console.log('Invoking OpenSCAD from ', workingDir, args)
const start = performance.now();
const exitCode = instance.callMain(args);
const end = performance.now();

View file

@ -20,6 +20,9 @@ export interface State {
} | ({
mode: 'multi',
} & { [K in MultiLayoutComponentId]: boolean })
showAxes?: boolean,
showShadows?: boolean,
}
lastCheckerRun?: {

View file

@ -32,7 +32,9 @@ export function readStateFromFragment(): State | null {
editor: validateBoolean(view?.layout['editor']),
viewer: validateBoolean(view?.layout['viewer']),
customizer: validateBoolean(view?.layout['customizer']),
}
},
showAxes: validateBoolean(view?.layout?.showAxis),
showShadows: validateBoolean(view?.layout?.showShadow),
}
};
} catch (e) {

View file

@ -60,6 +60,9 @@ export function createInitialState(fs: any, state: State | null) {
}
}
initialState.view.showAxes ??= true
initialState.view.showShadows ??= true
fs.writeFile(initialState.params.sourcePath, initialState.params.source);
if (initialState.params.sourcePath !== defaultSourcePath) {
fs.writeFile(defaultSourcePath, defaultScad);