Shuffle files around

This commit is contained in:
ochafik 2023-03-26 15:57:09 +01:00
commit cfda844339
18 changed files with 704 additions and 738 deletions

View file

@ -1,45 +0,0 @@
/* .App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.logs-container {
overflow-y: scroll;
height: calc(min(200px, 30vh));
position: relative;
} */

View file

@ -1,679 +0,0 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
import {MultiLayoutComponentId, SingleLayoutComponentId, State} from './state/app-state'
import Editor, { loader, Monaco } from '@monaco-editor/react';
// import './App.css';
import openscadEditorOptions from './language/openscad-editor-options';
import { Model, ModelContext } from './state/model';
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 { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { Toast } from 'primereact/toast';
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 { buildUrlForStateParams } from './state/fragment-state';
import { blankProjectState } from './state/initial-state';
// import "primereact/resources/themes/lara-light-indigo/theme.css";
// import "primereact/resources/primereact.min.css";
// import "primeicons/primeicons.css";
let monacoInstance: Monaco
loader.init().then(mi => monacoInstance = mi);
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 menu = useRef<Menu>(null);
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 = state.lastCheckerRun;
const editorModel = editor.getModel();
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",
label: "Render OpenSCAD",
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
monaco.KeyCode.F6,
],
run: () => model.render({isPreview: false, now: true})
});
editor.addAction({
id: "openscad-preview",
label: "Preview OpenSCAD",
keybindings: [monaco.KeyCode.F5],
run: () => model.render({isPreview: true, now: true})
});
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 ${className ?? ''}`} style={{
// maxWidth: '5 0vw',
display: 'flex',
flexDirection: 'column',
// position: 'relative',
// width: '100%', height: '100%',
...(style ?? {})
}}>
{/* <BreadCrumb model={breadCrumbsItems} home={breadCrumbsHome}/> */}
<div className='flex flex-row gap-2' style={{
margin: '5px',
}}>
<Menu model={[
{
label: "New project",
icon: 'pi pi-plus-circle',
// disabled: true,
command: () => window.open(buildUrlForStateParams(blankProjectState), '_blank'),
},
{
// TODO: share text, title and rendering image
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
label: 'Share project',
icon: 'pi pi-share-alt',
disabled: true,
command: () => window.open('http://openscad.org/cheatsheet/', '_blank'),
},
{
separator: true
},
{
// TODO: popup to ask for file name
label: "New file",
icon: 'pi pi-plus',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: "Copy to new file",
icon: 'pi pi-clone',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: "Upload file(s)",
icon: 'pi pi-upload',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: 'Download sources',
icon: 'pi pi-download',
disabled: true,
command: () => window.open('https://github.com/revarbat/BOSL2/wiki/CheatSheet', '_blank'),
},
{
separator: true
},
// https://vscode-docs.readthedocs.io/en/stable/customization/keybindings/
// {
// label: 'Undo',
// icon: 'pi pi-undo',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.undoAction', null),
// },
// {
// label: 'Redo',
// icon: 'pi pi-reply',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.redoAction', null),
// },
{
separator: true
},
// {
// label: 'Copy',
// icon: 'pi pi-copy',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCopyAction', null),
// },
// {
// label: 'Cut',
// icon: 'pi pi-eraser',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCutAction', null),
// },
// {
// label: 'Paste',
// icon: 'pi pi-images',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardPasteAction', null),
// },
{
label: 'Select All',
icon: 'pi pi-info-circle',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'editor.action.selectAll', null),
},
{
separator: true
},
{
label: 'Find',
icon: 'pi pi-search',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'actions.find', null),
},
] 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
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>
{/* </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');
const state = model.state;
return (
<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}
orbitControls
shadows
modelProps={{
color: '#f9d72c',
}}
url={state.output?.stlFileURL ?? ''}
/>
}
</div>
)
}
function downloadOutput(state: State) {
if (!state.output) return;
const fileName = state.output!.isPreview ? 'preview.stl' : 'render.stl';
const doDownload = () => {
const a = document.createElement('a')
a.href = state.output!.stlFileURL
a.download = fileName;
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
};
if (state.output.isPreview && state.params.source.indexOf('$preview') >= 0) {
confirmDialog({
message: "This model references the $preview variable but hasn't been rendered (F6), or its rendering is stale. You're about to download the preview result itself, which may not have the intended refinement of the render. Sure you want to proceed?",
header: 'Preview vs. Render',
icon: 'pi pi-exclamation-triangle',
accept: () => doDownload,
acceptLabel: `Download ${fileName}`,
rejectLabel: 'Cancel'
// reject: () => {}
});
} else {
doDownload();
}
}
function Footer({style}: {style?: CSSProperties}) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const menu = 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'],
[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"
title="Render the model (F6 / Ctrl+Enter). Models can test $preview to enable more detail in renders only."
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>
<ConfirmDialog />
<Toast ref={toast} />
{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-sliders-h', label: 'Customize' },
// { 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]),
}
}
}
const isIPhone = /iPhone/.test(navigator.userAgent)
return (
<ModelContext.Provider value={model}>
<FSContext.Provider value={fs}>
<div className='flex flex-column' style={{
// height: '100vh'
// maxHeight: '-webkit-fill-available',
height: isIPhone ? "calc(100vh - 80px - env(safe-area-inset-bottom) - env(safe-area-inset-top))" : '100vh'
}}>
{isIPhone && <Footer />}
<div className="">
<div className='flex flex-row' style={{
margin: '5px',
}}>
{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
activeIndex={singleTargets.map(t => t.id).indexOf(state.view.layout.focus)}
style={{
flex: 1,
// justifyContent: 'center'
}}
model={singleTargets.map(({icon, label, id}) =>
({icon, label, disabled: id === 'customizer', 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>
{!isIPhone && <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 />

159
src/components/App.tsx Normal file
View file

@ -0,0 +1,159 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
import {MultiLayoutComponentId, SingleLayoutComponentId, State} from '../state/app-state'
import { Model, ModelContext } from '../state/model';
import { TabMenu } from 'primereact/tabmenu';
import { ToggleButton } from 'primereact/togglebutton';
import EditorPanel from './EditorPanel';
import ViewerPanel from './ViewerPanel';
import Footer from './Footer';
import { FSContext } from '../fs/filesystem';
// import "primereact/resources/themes/lara-light-indigo/theme.css";
// import "primereact/resources/primereact.min.css";
// import "primeicons/primeicons.css";
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 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-sliders-h', label: 'Customize' },
// { id: 'customizer', icon: 'pi pi-cog', label: 'Customize' },
];
const multiTargets = singleTargets;
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]),
}
}
}
// TODO: test this again:
// body {
// min-height: 100vh;
// min-height: fill-available;
// min-height: -webkit-fill-available;
// }
// html {
// height: fill-available;
// height: -webkit-fill-available;
// }
const isIPhone = /iPhone/.test(navigator.userAgent)
return (
<ModelContext.Provider value={model}>
<FSContext.Provider value={fs}>
<div className='flex flex-column' style={{
// height: '100vh'
// maxHeight: '-webkit-fill-available',
height: isIPhone ? "calc(100vh - 80px - env(safe-area-inset-bottom) - env(safe-area-inset-top))" : '100vh'
}}>
{isIPhone && <Footer />}
<div className="">
<div className='flex flex-row' style={{
margin: '5px',
}}>
{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
activeIndex={singleTargets.map(t => t.id).indexOf(state.view.layout.focus)}
style={{
flex: 1,
// justifyContent: 'center'
}}
model={singleTargets.map(({icon, label, id}) =>
({icon, label, disabled: id === 'customizer', 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>
{!isIPhone && <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 />

View file

@ -0,0 +1,289 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React, { CSSProperties, useContext, useRef, useState } from 'react';
import Editor, { loader, Monaco } from '@monaco-editor/react';
import openscadEditorOptions from '../language/openscad-editor-options';
import { ModelContext } from '../state/model';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { Button } from 'primereact/button';
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 { buildUrlForStateParams } from '../state/fragment-state';
import { blankProjectState } from '../state/initial-state';
// import "primereact/resources/themes/lara-light-indigo/theme.css";
// import "primereact/resources/primereact.min.css";
// import "primeicons/primeicons.css";
let monacoInstance: Monaco
loader.init().then(mi => monacoInstance = mi);
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;
}
export default function EditorPanel({className, style}: {className?: string, style?: CSSProperties}) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const menu = useRef<Menu>(null);
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 = state.lastCheckerRun;
const editorModel = editor.getModel();
if (editorModel) {
if (checkerRun) {
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', checkerRun.markers);
}
}
}
const onMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
editor.addAction({
id: "openscad-render",
label: "Render OpenSCAD",
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
monaco.KeyCode.F6,
],
run: () => model.render({isPreview: false, now: true})
});
editor.addAction({
id: "openscad-preview",
label: "Preview OpenSCAD",
keybindings: [monaco.KeyCode.F5],
run: () => model.render({isPreview: true, now: true})
});
setEditor(editor)
}
const fsItems = fs && listFilesAsNodes(fs, '/home') || [];
return (
<div className={`editor-panel ${className ?? ''}`} style={{
// maxWidth: '5 0vw',
display: 'flex',
flexDirection: 'column',
// position: 'relative',
// width: '100%', height: '100%',
...(style ?? {})
}}>
{/* <BreadCrumb model={breadCrumbsItems} home={breadCrumbsHome}/> */}
<div className='flex flex-row gap-2' style={{
margin: '5px',
}}>
<Menu model={[
{
label: "New project",
icon: 'pi pi-plus-circle',
// disabled: true,
command: () => window.open(buildUrlForStateParams(blankProjectState), '_blank'),
},
{
// TODO: share text, title and rendering image
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
label: 'Share project',
icon: 'pi pi-share-alt',
disabled: true,
command: () => window.open('http://openscad.org/cheatsheet/', '_blank'),
},
{
separator: true
},
{
// TODO: popup to ask for file name
label: "New file",
icon: 'pi pi-plus',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: "Copy to new file",
icon: 'pi pi-clone',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: "Upload file(s)",
icon: 'pi pi-upload',
disabled: true,
command: () => window.open('https://github.com/openscad/openscad-playground/tree/rewrite1', '_blank'),
},
{
label: 'Download sources',
icon: 'pi pi-download',
disabled: true,
command: () => window.open('https://github.com/revarbat/BOSL2/wiki/CheatSheet', '_blank'),
},
{
separator: true
},
// https://vscode-docs.readthedocs.io/en/stable/customization/keybindings/
// {
// label: 'Undo',
// icon: 'pi pi-undo',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.undoAction', null),
// },
// {
// label: 'Redo',
// icon: 'pi pi-reply',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.redoAction', null),
// },
{
separator: true
},
// {
// label: 'Copy',
// icon: 'pi pi-copy',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCopyAction', null),
// },
// {
// label: 'Cut',
// icon: 'pi pi-eraser',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardCutAction', null),
// },
// {
// label: 'Paste',
// icon: 'pi pi-images',
// // disabled: true,
// command: () => editor?.trigger(state.params.sourcePath, 'editor.action.clipboardPasteAction', null),
// },
{
label: 'Select All',
icon: 'pi pi-info-circle',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'editor.action.selectAll', null),
},
{
separator: true
},
{
label: 'Find',
icon: 'pi pi-search',
// disabled: true,
command: () => editor?.trigger(state.params.sourcePath, 'actions.find', null),
},
] 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
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>
{/* </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>
)
}

195
src/components/Footer.tsx Normal file
View file

@ -0,0 +1,195 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { CSSProperties, useContext, useRef } from 'react';
import { State } from '../state/app-state'
import { ModelContext } from '../state/model';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { Button } from 'primereact/button';
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';
function downloadOutput(state: State) {
if (!state.output) return;
const fileName = state.output!.isPreview ? 'preview.stl' : 'render.stl';
const doDownload = () => {
const a = document.createElement('a')
a.href = state.output!.stlFileURL
a.download = fileName;
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
};
if (state.output.isPreview && state.params.source.indexOf('$preview') >= 0) {
confirmDialog({
message: "This model references the $preview variable but hasn't been rendered (F6), or its rendering is stale. You're about to download the preview result itself, which may not have the intended refinement of the render. Sure you want to proceed?",
header: 'Preview vs. Render',
icon: 'pi pi-exclamation-triangle',
accept: () => doDownload,
acceptLabel: `Download ${fileName}`,
rejectLabel: 'Cancel'
// reject: () => {}
});
} else {
doDownload();
}
}
export default function Footer({style}: {style?: CSSProperties}) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const menu = 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'],
[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"
title="Render the model (F6 / Ctrl+Enter). Models can test $preview to enable more detail in renders only."
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>
<ConfirmDialog />
<Toast ref={toast} />
{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>
</>
}

View file

@ -0,0 +1,42 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
import { ModelContext } from '../state/model';
import { StlViewer} from "react-stl-viewer";
export default function ViewerPanel({className, style}: {className?: string, style?: CSSProperties}) {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const state = model.state;
return (
<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}
orbitControls
shadows
modelProps={{
color: '#f9d72c',
}}
url={state.output?.stlFileURL ?? ''}
/>
}
</div>
)
}

View file

@ -1,9 +1,12 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React from "react";
import { zipArchives } from "./zip-archives";
declare var BrowserFS: BrowserFSInterface
export const FSContext = React.createContext<FS | undefined>(undefined);
export type FSMounts = {
[n: string]: {fs: string, options: {zipData: Buffer}}
}

View file

@ -2,10 +2,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import {App} from './App';
import { createEditorFS } from './filesystem';
import {App} from './components/App';
import { createEditorFS } from './fs/filesystem';
import { registerOpenSCADLanguage } from './language/openscad-register-language';
import { zipArchives } from './zip-archives';
import { zipArchives } from './fs/zip-archives';
import {readStateFromFragment} from './state/fragment-state'
import { createInitialState } from './state/initial-state';
import './index.css';

View file

@ -1,11 +1,11 @@
// 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 { readDirAsArray, Symlinks } from '../filesystem';
import { readDirAsArray, Symlinks } from '../fs/filesystem';
import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser';
import builtinSignatures from './openscad-builtins'
import { mapObject } from '../utils';
import { ZipArchives } from '../zip-archives';
import { ZipArchives } from '../fs/zip-archives';
import openscadLanguage from './openscad-language';
function makeFunctionoidSuggestion(name: string, mod: ParsedFunctionoidDef) {

View file

@ -1,7 +1,7 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import loader from '@monaco-editor/loader';
import { ZipArchives } from '../zip-archives';
import { ZipArchives } from '../fs/zip-archives';
import { buildOpenSCADCompletionItemProvider } from './openscad-completions';
import openscadLanguage from './openscad-language';
// import jsLanguage from 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'

View file

@ -1,10 +1,10 @@
// 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 { getParentDir } from '../fs/filesystem';
import { spawnOpenSCAD } from "./openscad-runner";
import { processMergedOutputs } from "./output-parser";
import { AbortablePromise, turnIntoDelayableExecution } from './utils';
import { AbortablePromise, turnIntoDelayableExecution } from '../utils';
const syntaxDelay = 300;

View file

@ -1,7 +1,7 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { MergedOutputs } from "./openscad-worker";
import { AbortablePromise } from "./utils";
import { AbortablePromise } from "../utils";
export function createWasmMemory({maximumMegabytes, maximumBytes}: {maximumMegabytes: number, maximumBytes: number}) {
const pageSize = 64 * 1024; // 64KB

View file

@ -1,10 +1,10 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import OpenSCAD from "./wasm/openscad.js";
import OpenSCAD from "../wasm/openscad.js";
import { createEditorFS, getBrowserFSLibrariesMounts, getParentDir, symlinkLibraries } from "./filesystem";
import { createEditorFS, getBrowserFSLibrariesMounts, getParentDir, symlinkLibraries } from "../fs/filesystem";
import { OpenSCADInvocation, OpenSCADInvocationResults } from "./openscad-runner";
import { zipArchives } from "./zip-archives";
import { zipArchives } from "../fs/zip-archives";
declare var BrowserFS: BrowserFSInterface
importScripts("browserfs.min.js");
@ -49,6 +49,8 @@ addEventListener('message', async (e) => {
}, instance.ERRNO_CODES ?? {});
instance.FS.mount(BFS, {root: '/home'}, '/home');
// await symlinkLibraries(allArchiveNames, instance.FS, '/home/libraries', '/home');
instance.FS.chdir('/home');
if (inputs) {

View file

@ -1,9 +1,9 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React from "react"
import { checkSyntax, render, RenderArgs, RenderOutput } from "../actions";
import { checkSyntax, render, RenderArgs, RenderOutput } from "../runner/actions";
import { MultiLayoutComponentId, SingleLayoutComponentId, State } from "./app-state";
import { bubbleUpDeepMutations } from "../deep-mutate";
import { bubbleUpDeepMutations } from "./deep-mutate";
import { writeStateInFragment } from "./fragment-state";
import { formatBytes, formatMillis } from '../utils'