state/model files refactoring

This commit is contained in:
ochafik 2023-03-26 15:28:05 +01:00
commit 05fb68ed14
11 changed files with 209 additions and 89 deletions

View file

@ -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";

View file

@ -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: {

View file

@ -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

View 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 {}

View file

@ -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'); }
));

View file

@ -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])

View file

@ -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
})); }));

View 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;
}

View file

@ -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`;
}

View file

@ -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`;
}

View file

@ -24,8 +24,8 @@
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": [
"src/*.ts", "src/**/*.ts",
"src/*.tsx", "src/**/*.tsx",
// "public" // "public"
] ]
} }