state/model files refactoring
This commit is contained in:
parent
4350b67f80
commit
05fb68ed14
11 changed files with 209 additions and 89 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
// 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, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
import React, { CSSProperties, forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import {ModelContext, MultiLayoutComponentId, SingleLayoutComponentId, State} from './app-state'
|
import {MultiLayoutComponentId, SingleLayoutComponentId, State} from './state/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, ModelContext } from './state/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 { Button } from 'primereact/button';
|
||||||
|
|
@ -18,11 +18,14 @@ import { Badge } from 'primereact/badge';
|
||||||
import { Menu } from 'primereact/menu';
|
import { Menu } from 'primereact/menu';
|
||||||
import { ToggleButton } from 'primereact/togglebutton';
|
import { ToggleButton } from 'primereact/togglebutton';
|
||||||
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
||||||
|
import { Toast } from 'primereact/toast';
|
||||||
import { getParentDir } from './filesystem';
|
import { getParentDir } from './filesystem';
|
||||||
import { BreadCrumb } from 'primereact/breadcrumb';
|
import { BreadCrumb } from 'primereact/breadcrumb';
|
||||||
import { ScrollPanel } from 'primereact/scrollpanel';
|
import { ScrollPanel } from 'primereact/scrollpanel';
|
||||||
import { Splitter, SplitterPanel } from 'primereact/splitter';
|
import { Splitter, SplitterPanel } from 'primereact/splitter';
|
||||||
import { Toolbar } from 'primereact/toolbar';
|
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/themes/lara-light-indigo/theme.css";
|
||||||
// import "primereact/resources/primereact.min.css";
|
// import "primereact/resources/primereact.min.css";
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export async function createEditorFS(workingDir='/home'): Promise<FS> {
|
||||||
allMounts[`${workingDir}/${n}`] = librariesMounts[n];
|
allMounts[`${workingDir}/${n}`] = librariesMounts[n];
|
||||||
}
|
}
|
||||||
|
|
||||||
await configureAndInstallFS(window, {
|
await configureAndInstallFS(typeof window === 'object' && window || self, {
|
||||||
fs: "OverlayFS",
|
fs: "OverlayFS",
|
||||||
options: {
|
options: {
|
||||||
readable: {
|
readable: {
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,9 @@ import {App} from './App';
|
||||||
import { createEditorFS } from './filesystem';
|
import { createEditorFS } from './filesystem';
|
||||||
import { registerOpenSCADLanguage } from './language/openscad-register-language';
|
import { registerOpenSCADLanguage } from './language/openscad-register-language';
|
||||||
import { zipArchives } from './zip-archives';
|
import { zipArchives } from './zip-archives';
|
||||||
import {readStateFromFragment} from './fragment-state'
|
import {readStateFromFragment} from './state/fragment-state'
|
||||||
import { State } from './app-state';
|
import { createInitialState } from './state/initial-state';
|
||||||
import defaultScad from './default-scad'
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
// import "react-widgets/styles.css";
|
|
||||||
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
const log = debug('app:log');
|
const log = debug('app:log');
|
||||||
|
|
@ -28,56 +26,11 @@ if (process.env.NODE_ENV !== 'production') {
|
||||||
const fs = await createEditorFS(workingDir)!;
|
const fs = await createEditorFS(workingDir)!;
|
||||||
await registerOpenSCADLanguage(fs, workingDir, zipArchives);
|
await registerOpenSCADLanguage(fs, workingDir, zipArchives);
|
||||||
|
|
||||||
type Mode = State['view']['layout']['mode'];
|
// type Mode = State['view']['layout']['mode'];
|
||||||
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
// const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
||||||
? 'multi' : 'single';
|
// ? 'multi' : 'single';
|
||||||
|
|
||||||
const defaultSourcePath = '/home/playground.scad';
|
const initialState = createInitialState(fs, readStateFromFragment());
|
||||||
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)
|
|
||||||
initialState.params.features.push(f);
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
|
|
|
||||||
80
src/state/app-state-future.ts
Normal file
80
src/state/app-state-future.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
export type CustomizerValues = {name: string, value: string|number|boolean}[]
|
||||||
|
export type CustomizerSchema = {
|
||||||
|
presets: {
|
||||||
|
// No name = default preset
|
||||||
|
name?: string,
|
||||||
|
values: CustomizerValues
|
||||||
|
},
|
||||||
|
variables: ({
|
||||||
|
type: 'string'
|
||||||
|
default: string
|
||||||
|
} | {
|
||||||
|
type: 'boolean'
|
||||||
|
default: boolean
|
||||||
|
} | {
|
||||||
|
type: 'enum',
|
||||||
|
default: number | string
|
||||||
|
values: number[] | string[]
|
||||||
|
labels?: string[]
|
||||||
|
} | {
|
||||||
|
type: 'range',
|
||||||
|
default: number,
|
||||||
|
start?: number, // Thingiverse supports foo = 10; // [50]
|
||||||
|
end: number,
|
||||||
|
step?: number
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SourceLocation {
|
||||||
|
path: string,
|
||||||
|
row?: number,
|
||||||
|
column?: number,
|
||||||
|
}
|
||||||
|
export interface PlaygroundState {
|
||||||
|
fs: {
|
||||||
|
inputRoot: string,
|
||||||
|
resources: {
|
||||||
|
path: string,
|
||||||
|
lastModified: number,
|
||||||
|
type: 'file' | 'mounted-zip',
|
||||||
|
mimeType: string,
|
||||||
|
// If mimeType ends w/ ;base64, this is a base64 encoded string
|
||||||
|
content: string,
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
fileExplorerPath?: string,
|
||||||
|
scadInputPath?: string,
|
||||||
|
outputLogs?: {
|
||||||
|
// sourceLastModified: number,
|
||||||
|
stdout: string,
|
||||||
|
stderr: string,
|
||||||
|
errors: {
|
||||||
|
message: string,
|
||||||
|
loc: SourceLocation
|
||||||
|
}[],
|
||||||
|
warnings: {
|
||||||
|
message: string,
|
||||||
|
loc: SourceLocation
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
|
||||||
|
viewer: {
|
||||||
|
mode: 'normal' | 'maximized' | 'full-screen'
|
||||||
|
// TODO: camera, zoom...
|
||||||
|
},
|
||||||
|
|
||||||
|
output?: {
|
||||||
|
path: string,
|
||||||
|
timestamp: number,
|
||||||
|
sizeBytes: number,
|
||||||
|
formattedSize: string,
|
||||||
|
},
|
||||||
|
|
||||||
|
customizer: {
|
||||||
|
visible?: boolean,
|
||||||
|
schema?: CustomizerSchema,
|
||||||
|
values: CustomizerValues,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
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 MultiLayoutComponentId = 'editor' | 'viewer' | 'customizer';
|
||||||
export type SingleLayoutComponentId = MultiLayoutComponentId;
|
export type SingleLayoutComponentId = MultiLayoutComponentId;
|
||||||
|
|
||||||
|
|
@ -41,12 +38,11 @@ export interface State {
|
||||||
elapsedMillis: number,
|
elapsedMillis: number,
|
||||||
formattedElapsedMillis: string,
|
formattedElapsedMillis: string,
|
||||||
formattedStlFileSize: string,
|
formattedStlFileSize: string,
|
||||||
|
// path: string,
|
||||||
|
// timestamp: number,
|
||||||
|
// sizeBytes: number,
|
||||||
|
// formattedSize: string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelContext = React.createContext<Model | null>(null);
|
export {}
|
||||||
|
|
||||||
},
|
|
||||||
() => { throw new Error('Not implemented'); }
|
|
||||||
));
|
|
||||||
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
export default `
|
export default `
|
||||||
rotate([90, 0, 0]) {
|
rotate([90, 0, 0]) {
|
||||||
translate([-50, 0, 50])
|
translate([-50, 0, 50])
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
// 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 { State } from "./app-state";
|
import { State } from "./app-state";
|
||||||
import { validateArray, validateBoolean, validateString, validateStringEnum } from "./utils";
|
import { validateArray, validateBoolean, validateString, validateStringEnum } from "../utils";
|
||||||
|
|
||||||
|
export function buildUrlForStateParams(state: State) {//partialState: {params: State['params'], view: State['view']}) {
|
||||||
|
return `${location.protocol}//${location.host}${location.pathname}#${encodeStateParamsAsFragment(state)}`;
|
||||||
|
}
|
||||||
export function writeStateInFragment(state: State) {
|
export function writeStateInFragment(state: State) {
|
||||||
window.location.hash = encodeURIComponent(JSON.stringify({
|
window.location.hash = encodeStateParamsAsFragment(state);
|
||||||
|
}
|
||||||
|
export function encodeStateParamsAsFragment(state: State) {
|
||||||
|
return encodeURIComponent(JSON.stringify({
|
||||||
params: state.params,
|
params: state.params,
|
||||||
view: state.view
|
view: state.view
|
||||||
}));
|
}));
|
||||||
76
src/state/initial-state.ts
Normal file
76
src/state/initial-state.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import defaultScad from './default-scad';
|
||||||
|
import { State } from './app-state';
|
||||||
|
|
||||||
|
export const defaultSourcePath = '/home/playground.scad';
|
||||||
|
|
||||||
|
export const blankProjectState: State = {
|
||||||
|
params: {
|
||||||
|
sourcePath: defaultSourcePath,
|
||||||
|
source: '',
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
layout: {
|
||||||
|
mode: 'single',
|
||||||
|
focus: 'editor'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createInitialState(fs: any, state: State | null) {
|
||||||
|
|
||||||
|
type Mode = State['view']['layout']['mode'];
|
||||||
|
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
||||||
|
? 'multi' : 'single';
|
||||||
|
|
||||||
|
const initialState: State = {
|
||||||
|
params: {
|
||||||
|
sourcePath: defaultSourcePath,
|
||||||
|
source: defaultScad,
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
layout: {
|
||||||
|
mode: 'multi',
|
||||||
|
editor: true,
|
||||||
|
viewer: true,
|
||||||
|
customizer: false,
|
||||||
|
} as any
|
||||||
|
},
|
||||||
|
...(state ?? {})
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
initialState.params.features.push(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
return initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
// 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 { checkSyntax, render, RenderArgs, RenderOutput } from "./actions";
|
import React from "react"
|
||||||
|
import { checkSyntax, render, RenderArgs, RenderOutput } from "../actions";
|
||||||
import { MultiLayoutComponentId, SingleLayoutComponentId, State } from "./app-state";
|
import { MultiLayoutComponentId, SingleLayoutComponentId, State } from "./app-state";
|
||||||
import { bubbleUpDeepMutations } from "./deep-mutate";
|
import { bubbleUpDeepMutations } from "../deep-mutate";
|
||||||
import { writeStateInFragment } from "./fragment-state";
|
import { writeStateInFragment } from "./fragment-state";
|
||||||
|
import { formatBytes, formatMillis } from '../utils'
|
||||||
|
|
||||||
export class Model {
|
export class Model {
|
||||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void) {
|
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void) {
|
||||||
|
|
@ -160,24 +162,7 @@ export class Model {
|
||||||
});
|
});
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(n: number) {
|
export const ModelContext = React.createContext<Model | null>(null);
|
||||||
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`;
|
|
||||||
}
|
|
||||||
19
src/utils.ts
19
src/utils.ts
|
|
@ -68,3 +68,22 @@ export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orEl
|
||||||
if (!(a instanceof Array)) return orElse();
|
if (!(a instanceof Array)) return orElse();
|
||||||
return a.map(validateElement);
|
return a.map(validateElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatBytes(n: number) {
|
||||||
|
if (n < 1024) {
|
||||||
|
return `${Math.floor(n)} bytes`;
|
||||||
|
}
|
||||||
|
n /= 1024;
|
||||||
|
if (n < 1024) {
|
||||||
|
return `${Math.floor(n * 10) / 10} kB`;
|
||||||
|
}
|
||||||
|
n /= 1024;
|
||||||
|
return `${Math.floor(n * 10) / 10} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMillis(n: number) {
|
||||||
|
if (n < 1000)
|
||||||
|
return `${Math.floor(n)} millis`;
|
||||||
|
|
||||||
|
return `${Math.floor(n / 100) / 10} sec`;
|
||||||
|
}
|
||||||
|
|
@ -24,8 +24,8 @@
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/*.ts",
|
"src/**/*.ts",
|
||||||
"src/*.tsx",
|
"src/**/*.tsx",
|
||||||
// "public"
|
// "public"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue