add customizer
This commit is contained in:
parent
9929ffe324
commit
76492bc903
10 changed files with 341 additions and 16 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -5,4 +5,5 @@ public/libraries
|
||||||
public/openscad.js
|
public/openscad.js
|
||||||
public/openscad.wasm
|
public/openscad.wasm
|
||||||
src/wasm
|
src/wasm
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
.DS_Store
|
||||||
|
|
@ -9,6 +9,7 @@ import Footer from './Footer';
|
||||||
import { ModelContext, FSContext } from './contexts';
|
import { ModelContext, FSContext } from './contexts';
|
||||||
import PanelSwitcher from './PanelSwitcher';
|
import PanelSwitcher from './PanelSwitcher';
|
||||||
import { ConfirmDialog } from 'primereact/confirmdialog';
|
import { ConfirmDialog } from 'primereact/confirmdialog';
|
||||||
|
import CustomizerPanel from './CustomizerPanel';
|
||||||
|
|
||||||
|
|
||||||
// import "primereact/resources/themes/lara-light-indigo/theme.css";
|
// import "primereact/resources/themes/lara-light-indigo/theme.css";
|
||||||
|
|
@ -30,11 +31,11 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
|
||||||
viewer: {
|
viewer: {
|
||||||
editor: 2,
|
editor: 2,
|
||||||
viewer: 3,
|
viewer: 3,
|
||||||
customizer: 2,
|
customizer: 1,
|
||||||
},
|
},
|
||||||
customizer: {
|
customizer: {
|
||||||
editor: 0,
|
editor: 0,
|
||||||
viewer: 0,
|
viewer: 1,
|
||||||
customizer: 3,
|
customizer: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +79,11 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
|
||||||
${layout.mode === 'single' ? 'absolute-fill' : ''}
|
${layout.mode === 'single' ? 'absolute-fill' : ''}
|
||||||
`} style={getPanelStyle('editor')} />
|
`} style={getPanelStyle('editor')} />
|
||||||
<ViewerPanel className={layout.mode === 'single' ? `absolute-fill` : ''} style={getPanelStyle('viewer')} />
|
<ViewerPanel className={layout.mode === 'single' ? `absolute-fill` : ''} style={getPanelStyle('viewer')} />
|
||||||
{/* <CustomizerPanel className={`${getPanelClasses('customizer')} absolute-fill`} style={getPanelStyle('customizer')} /> */}
|
<CustomizerPanel className={`
|
||||||
|
opacity-animated
|
||||||
|
${layout.mode === 'single' && layout.focus !== 'customizer' ? 'opacity-0' : ''}
|
||||||
|
${layout.mode === 'single' ? `absolute-fill` : ''}
|
||||||
|
`} style={getPanelStyle('customizer')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
219
src/components/CustomizerPanel.tsx
Normal file
219
src/components/CustomizerPanel.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import { CSSProperties, useContext } from 'react';
|
||||||
|
import { ModelContext, FSContext } from './contexts';
|
||||||
|
|
||||||
|
import { Dropdown } from 'primereact/dropdown';
|
||||||
|
import { Slider } from 'primereact/slider';
|
||||||
|
import { Checkbox } from 'primereact/checkbox';
|
||||||
|
import { InputNumber } from 'primereact/inputnumber';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { Fieldset } from 'primereact/fieldset';
|
||||||
|
import { Parameter } from '../state/customizer-types';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
|
||||||
|
export default function CustomizerPanel({className, style}: {className?: string, style?: CSSProperties}) {
|
||||||
|
|
||||||
|
const model = useContext(ModelContext);
|
||||||
|
if (!model) throw new Error('No model');
|
||||||
|
|
||||||
|
const state = model.state;
|
||||||
|
|
||||||
|
const handleChange = (name: string, value: any) => {
|
||||||
|
model.setVar(name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupedParameters = (state.parameterSet?.parameters ?? []).reduce((acc, param) => {
|
||||||
|
if (!acc[param.group]) {
|
||||||
|
acc[param.group] = [];
|
||||||
|
}
|
||||||
|
acc[param.group].push(param);
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: any[] });
|
||||||
|
|
||||||
|
const groups = Object.entries(groupedParameters);
|
||||||
|
const collapsedTabSet = new Set(state.view.collapsedCustomizerTabs ?? []);
|
||||||
|
const setTabOpen = (name: string, open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
collapsedTabSet.delete(name);
|
||||||
|
} else {
|
||||||
|
collapsedTabSet.add(name)
|
||||||
|
}
|
||||||
|
model.mutate(s => s.view.collapsedCustomizerTabs = Array.from(collapsedTabSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'scroll',
|
||||||
|
...style,
|
||||||
|
bottom: 'unset',
|
||||||
|
}}>
|
||||||
|
{groups.map(([group, params]) => (
|
||||||
|
<Fieldset
|
||||||
|
style={{
|
||||||
|
margin: '5px 10px 5px 10px',
|
||||||
|
// backgroundColor: 'transparent',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||||
|
}}
|
||||||
|
onCollapse={() => setTabOpen(group, false)}
|
||||||
|
onExpand={() => setTabOpen(group, true)}
|
||||||
|
collapsed={collapsedTabSet.has(group)}
|
||||||
|
key={group}
|
||||||
|
legend={group}
|
||||||
|
toggleable={true}>
|
||||||
|
{params.map((param) => (
|
||||||
|
<ParameterInput
|
||||||
|
key={param.name}
|
||||||
|
value={(state.params.vars ?? {})[param.name]}
|
||||||
|
param={param}
|
||||||
|
handleChange={handleChange} />
|
||||||
|
))}
|
||||||
|
</Fieldset>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ParameterInput({param, value, className, style, handleChange}: {param: Parameter, value: any, className?: string, style?: CSSProperties, handleChange: (key: string, value: any) => void}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// ref={ref}
|
||||||
|
// onClick={state.view.layout.mode === 'single' ? handleClick : undefined}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
...style,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
margin: '10px -10px 10px 5px',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
// onClick={swallowClick}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<label><b>{param.name}</b></label>
|
||||||
|
<div>{param.caption}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
// onClick={swallowClick}
|
||||||
|
style={{
|
||||||
|
// flex: 1,
|
||||||
|
// margin: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
{param.type === 'number' && 'options' in param && (
|
||||||
|
<Dropdown
|
||||||
|
style={{flex: 1}}
|
||||||
|
value={value || param.initial}
|
||||||
|
options={param.options}
|
||||||
|
onChange={(e) => handleChange(param.name, e.value)}
|
||||||
|
optionLabel="name"
|
||||||
|
optionValue="value"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{param.type === 'string' && param.options && (
|
||||||
|
<Dropdown
|
||||||
|
// style={{flex: 1}}
|
||||||
|
value={value || param.initial}
|
||||||
|
options={param.options}
|
||||||
|
onChange={(e) => handleChange(param.name, e.value)}
|
||||||
|
optionLabel="name"
|
||||||
|
optionValue="value"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{param.type === 'boolean' && (
|
||||||
|
<Checkbox
|
||||||
|
// style={{flex: 1}}
|
||||||
|
checked={value ?? param.initial}
|
||||||
|
onChange={(e) => handleChange(param.name, e.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!Array.isArray(param.initial) && param.type === 'number' && !('options' in param) && (
|
||||||
|
<InputNumber
|
||||||
|
// style={{flex: 1}}
|
||||||
|
value={value || param.initial}
|
||||||
|
showButtons
|
||||||
|
size={5}
|
||||||
|
// buttonLayout='horizontal'
|
||||||
|
onValueChange={(e) => handleChange(param.name, e.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{param.type === 'string' && !param.options && (
|
||||||
|
<InputText
|
||||||
|
style={{flex: 1}}
|
||||||
|
value={value || param.initial}
|
||||||
|
onChange={(e) => handleChange(param.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{Array.isArray(param.initial) && 'min' in param && (
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
}}>
|
||||||
|
{param.initial.map((_, index) => (
|
||||||
|
<InputNumber
|
||||||
|
style={{flex: 1}}
|
||||||
|
key={index}
|
||||||
|
value={value?.[index] ?? (param.initial as any)[index]}
|
||||||
|
min={param.min}
|
||||||
|
max={param.max}
|
||||||
|
showButtons
|
||||||
|
// buttonLayout='horizontal'
|
||||||
|
size={5}
|
||||||
|
step={param.step}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
const newArray = [...(value ?? param.initial)];
|
||||||
|
newArray[index] = e.value;
|
||||||
|
handleChange(param.name, newArray);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => handleChange(param.name, param.initial)}
|
||||||
|
style={{
|
||||||
|
marginRight: '0',
|
||||||
|
visibility: value === undefined || (JSON.stringify(value) === JSON.stringify(param.initial)) ? 'hidden' : 'visible',
|
||||||
|
}}
|
||||||
|
// tooltip={`Reset to default value (${JSON.stringify(param.initial)})`}
|
||||||
|
tooltipOptions={{position: 'left'}}
|
||||||
|
icon='pi pi-refresh'
|
||||||
|
className='p-button-text'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!Array.isArray(param.initial) && param.type === 'number' && param.min !== undefined && (
|
||||||
|
<Slider
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: '5px',
|
||||||
|
margin: '5px 40px 5px 5px',
|
||||||
|
}}
|
||||||
|
value={value || param.initial}
|
||||||
|
min={param.min}
|
||||||
|
max={param.max}
|
||||||
|
step={param.step}
|
||||||
|
onChange={(e) => handleChange(param.name, e.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,13 @@ export default function HelpMenu() {
|
||||||
url: 'http://openscad.org/documentation.html',
|
url: 'http://openscad.org/documentation.html',
|
||||||
target: '_blank'
|
target: '_blank'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Customizer Syntax',
|
||||||
|
icon: 'pi pi-book',
|
||||||
|
url: 'https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Customizer',
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'OpenSCAD Cheatsheet',
|
label: 'OpenSCAD Cheatsheet',
|
||||||
icon: 'pi pi-palette',
|
icon: 'pi pi-palette',
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ export default function PanelSwitcher() {
|
||||||
const singleTargets: {id: SingleLayoutComponentId, icon: string, label: string}[] = [
|
const singleTargets: {id: SingleLayoutComponentId, icon: string, label: string}[] = [
|
||||||
{ id: 'editor', icon: 'pi pi-pencil', label: 'Edit' },
|
{ id: 'editor', icon: 'pi pi-pencil', label: 'Edit' },
|
||||||
{ id: 'viewer', icon: 'pi pi-box', label: 'View' },
|
{ id: 'viewer', icon: 'pi pi-box', label: 'View' },
|
||||||
{ id: 'customizer', icon: 'pi pi-sliders-h', label: 'Customize' },
|
|
||||||
];
|
];
|
||||||
|
if ((state.parameterSet?.parameters?.length ?? 0) > 0) {
|
||||||
|
singleTargets.push({ id: 'customizer', icon: 'pi pi-sliders-h', label: 'Customize' });
|
||||||
|
}
|
||||||
const multiTargets = singleTargets;
|
const multiTargets = singleTargets;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -45,7 +47,6 @@ export default function PanelSwitcher() {
|
||||||
onIcon={icon}
|
onIcon={icon}
|
||||||
offIcon={icon}
|
offIcon={icon}
|
||||||
// icon={icon}
|
// icon={icon}
|
||||||
disabled={id === 'customizer'}
|
|
||||||
// onClick={() => model.changeMultiVisibility(id, !(state.view.layout as any)[id])}
|
// onClick={() => model.changeMultiVisibility(id, !(state.view.layout as any)[id])}
|
||||||
onChange={e => model.changeMultiVisibility(id, e.value)}
|
onChange={e => model.changeMultiVisibility(id, e.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -59,7 +60,7 @@ export default function PanelSwitcher() {
|
||||||
// justifyContent: 'center'
|
// justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
model={singleTargets.map(({icon, label, id}) =>
|
model={singleTargets.map(({icon, label, id}) =>
|
||||||
({icon, label, disabled: id === 'customizer', command: () => model.changeSingleVisibility(id)}))} />
|
({icon, label/*, disabled: id === 'customizer' && state?.parameterSet == null*/, command: () => model.changeSingleVisibility(id)}))} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<SettingsMenu style={{
|
<SettingsMenu style={{
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,23 @@ import { getParentDir } from '../fs/filesystem';
|
||||||
import { spawnOpenSCAD } from "./openscad-runner";
|
import { spawnOpenSCAD } from "./openscad-runner";
|
||||||
import { processMergedOutputs } from "./output-parser";
|
import { processMergedOutputs } from "./output-parser";
|
||||||
import { AbortablePromise, turnIntoDelayableExecution } from '../utils';
|
import { AbortablePromise, turnIntoDelayableExecution } from '../utils';
|
||||||
|
import { ParameterSet } from '../state/customizer-types';
|
||||||
|
|
||||||
const syntaxDelay = 300;
|
const syntaxDelay = 300;
|
||||||
|
|
||||||
type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[]};
|
type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[], parameterSet?: ParameterSet};
|
||||||
export const checkSyntax =
|
export const checkSyntax =
|
||||||
turnIntoDelayableExecution(syntaxDelay, (source: string, sourcePath: string) => {
|
turnIntoDelayableExecution(syntaxDelay, (source: string, sourcePath: string) => {
|
||||||
// const timestamp = Date.now();
|
// const timestamp = Date.now();
|
||||||
|
|
||||||
source = '$preview=true;\n' + source;
|
source = '$preview=true;\n' + source;
|
||||||
|
|
||||||
|
const outFile = 'out.json';
|
||||||
const job = spawnOpenSCAD({
|
const job = spawnOpenSCAD({
|
||||||
inputs: [[sourcePath, source + '\n']],
|
inputs: [[sourcePath, source + '\n']],
|
||||||
args: [sourcePath, "-o", "out.ast"],
|
args: [sourcePath, "-o", outFile, "--export-format=param"],
|
||||||
// workingDir: sourcePath.startsWith('/') ? getParentDir(sourcePath) : '/home'
|
// workingDir: sourcePath.startsWith('/') ? getParentDir(sourcePath) : '/home'
|
||||||
|
outputPaths: [outFile],
|
||||||
});
|
});
|
||||||
|
|
||||||
return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
|
return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
|
||||||
|
|
@ -26,10 +29,28 @@ export const checkSyntax =
|
||||||
try {
|
try {
|
||||||
const result = await job;
|
const result = await job;
|
||||||
// console.log(result);
|
// console.log(result);
|
||||||
res(processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {
|
|
||||||
sourcePath,
|
let parameterSet: ParameterSet | undefined = undefined;
|
||||||
skipLines: 1,
|
if (result.outputs && result.outputs.length == 1) {
|
||||||
}}));
|
let [[, content]] = result.outputs;
|
||||||
|
content = new TextDecoder().decode(content as any);
|
||||||
|
try {
|
||||||
|
parameterSet = JSON.parse(content)
|
||||||
|
// console.log('PARAMETER SET', JSON.stringify(parameterSet, null, 2))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error while parsing parameter set: ${e}\n${content}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('No output from runner!');
|
||||||
|
}
|
||||||
|
|
||||||
|
res({
|
||||||
|
...processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {
|
||||||
|
sourcePath,
|
||||||
|
skipLines: 1,
|
||||||
|
}}),
|
||||||
|
parameterSet,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
rej(e);
|
rej(e);
|
||||||
|
|
@ -45,12 +66,23 @@ export type RenderOutput = {stlFile: File, logText: string, markers: monaco.edit
|
||||||
export type RenderArgs = {
|
export type RenderArgs = {
|
||||||
source: string,
|
source: string,
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
|
vars?: {[name: string]: any},
|
||||||
features?: string[],
|
features?: string[],
|
||||||
extraArgs?: string[],
|
extraArgs?: string[],
|
||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatValue(any: any): string {
|
||||||
|
if (typeof any === 'string') {
|
||||||
|
return `"${any}"`;
|
||||||
|
} else if (any instanceof Array) {
|
||||||
|
return `[${any.map(formatValue).join(', ')}]`;
|
||||||
|
} else {
|
||||||
|
return `${any}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
export const render =
|
export const render =
|
||||||
turnIntoDelayableExecution(renderDelay, ({sourcePath, source, isPreview, features, extraArgs}: RenderArgs) => {
|
turnIntoDelayableExecution(renderDelay, ({sourcePath, source, isPreview, vars, features, extraArgs}: RenderArgs) => {
|
||||||
|
|
||||||
const prefixLines: string[] = [];
|
const prefixLines: string[] = [];
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
|
|
@ -62,6 +94,7 @@ export const render =
|
||||||
sourcePath,
|
sourcePath,
|
||||||
"-o", "out.stl",
|
"-o", "out.stl",
|
||||||
"--export-format=binstl",
|
"--export-format=binstl",
|
||||||
|
...(Object.entries(vars ?? {}).flatMap(([k, v]) => [`-D${k}=${formatValue(v)}`])),
|
||||||
...(features ?? []).map(f => `--enable=${f}`),
|
...(features ?? []).map(f => `--enable=${f}`),
|
||||||
...(extraArgs ?? [])
|
...(extraArgs ?? [])
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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 * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
import { ParameterSet } from './customizer-types';
|
||||||
|
|
||||||
export type MultiLayoutComponentId = 'editor' | 'viewer' | 'customizer';
|
export type MultiLayoutComponentId = 'editor' | 'viewer' | 'customizer';
|
||||||
export type SingleLayoutComponentId = MultiLayoutComponentId;
|
export type SingleLayoutComponentId = MultiLayoutComponentId;
|
||||||
|
|
@ -9,6 +10,7 @@ export interface State {
|
||||||
params: {
|
params: {
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
source: string,
|
source: string,
|
||||||
|
vars?: {[name: string]: any},
|
||||||
features: string[],
|
features: string[],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -20,6 +22,8 @@ export interface State {
|
||||||
} | ({
|
} | ({
|
||||||
mode: 'multi',
|
mode: 'multi',
|
||||||
} & { [K in MultiLayoutComponentId]: boolean })
|
} & { [K in MultiLayoutComponentId]: boolean })
|
||||||
|
|
||||||
|
collapsedCustomizerTabs?: string[],
|
||||||
|
|
||||||
color: string,
|
color: string,
|
||||||
showAxes?: boolean,
|
showAxes?: boolean,
|
||||||
|
|
@ -35,6 +39,7 @@ export interface State {
|
||||||
previewing?: boolean,
|
previewing?: boolean,
|
||||||
checkingSyntax?: boolean,
|
checkingSyntax?: boolean,
|
||||||
|
|
||||||
|
parameterSet?: ParameterSet,
|
||||||
error?: string,
|
error?: string,
|
||||||
output?: {
|
output?: {
|
||||||
isPreview: boolean,
|
isPreview: boolean,
|
||||||
|
|
|
||||||
46
src/state/customizer-types.ts
Normal file
46
src/state/customizer-types.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
export type ParameterOption = {
|
||||||
|
name: string;
|
||||||
|
value: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseParameter = {
|
||||||
|
caption: string;
|
||||||
|
group: string;
|
||||||
|
name: string;
|
||||||
|
type: 'number' | 'string' | 'boolean';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NumberParameter = BaseParameter & {
|
||||||
|
type: 'number';
|
||||||
|
initial: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
options?: ParameterOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StringParameter = BaseParameter & {
|
||||||
|
type: 'string';
|
||||||
|
initial: string;
|
||||||
|
options?: ParameterOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BooleanParameter = BaseParameter & {
|
||||||
|
type: 'boolean';
|
||||||
|
initial: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VectorParameter = BaseParameter & {
|
||||||
|
type: 'number';
|
||||||
|
initial: number[];
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Parameter = NumberParameter | StringParameter | BooleanParameter | VectorParameter;
|
||||||
|
|
||||||
|
export type ParameterSet = {
|
||||||
|
parameters: Parameter[];
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
@ -63,6 +63,7 @@ export async function readStateFromFragment(): Promise<State | null> {
|
||||||
sourcePath: validateString(params?.sourcePath),
|
sourcePath: validateString(params?.sourcePath),
|
||||||
source: validateString(params?.source),
|
source: validateString(params?.source),
|
||||||
features: validateArray(params?.features, validateString),
|
features: validateArray(params?.features, validateString),
|
||||||
|
vars: params?.vars, // TODO: validate!
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
layout: {
|
layout: {
|
||||||
|
|
@ -72,6 +73,7 @@ export async function readStateFromFragment(): Promise<State | null> {
|
||||||
viewer: validateBoolean(view?.layout['viewer']),
|
viewer: validateBoolean(view?.layout['viewer']),
|
||||||
customizer: validateBoolean(view?.layout['customizer']),
|
customizer: validateBoolean(view?.layout['customizer']),
|
||||||
},
|
},
|
||||||
|
collapsedCustomizerTabs: validateArray(view?.collapsedCustomizerTabs, validateString),
|
||||||
color: validateString(view?.color, () => defaultModelColor),
|
color: validateString(view?.color, () => defaultModelColor),
|
||||||
showAxes: validateBoolean(view?.layout?.showAxis, () => true),
|
showAxes: validateBoolean(view?.layout?.showAxis, () => true),
|
||||||
showShadows: validateBoolean(view?.layout?.showShadow, () => true),
|
showShadows: validateBoolean(view?.layout?.showShadow, () => true),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ export class Model {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVar(name: string, value: any) {
|
||||||
|
this.mutate(s => s.params.vars = {...s.params.vars ?? {}, [name]: value});
|
||||||
|
this.render({isPreview: true, now: false});
|
||||||
|
}
|
||||||
|
|
||||||
set logsVisible(value: boolean) {
|
set logsVisible(value: boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (this.state.view.layout.mode === 'single') {
|
if (this.state.view.layout.mode === 'single') {
|
||||||
|
|
@ -131,6 +136,7 @@ export class Model {
|
||||||
console.error('Error while checking syntax:', err)
|
console.error('Error while checking syntax:', err)
|
||||||
} else {
|
} else {
|
||||||
s.lastCheckerRun = checkerRun;
|
s.lastCheckerRun = checkerRun;
|
||||||
|
s.parameterSet = checkerRun?.parameterSet;
|
||||||
s.checkingSyntax = false;
|
s.checkingSyntax = false;
|
||||||
}
|
}
|
||||||
})});
|
})});
|
||||||
|
|
@ -146,9 +152,9 @@ export class Model {
|
||||||
}
|
}
|
||||||
this.mutate(s => setRendering(s, true));
|
this.mutate(s => setRendering(s, true));
|
||||||
|
|
||||||
const {source, sourcePath, features} = this.state.params;
|
const {source, sourcePath, vars, features} = this.state.params;
|
||||||
|
|
||||||
render({source, sourcePath, features, extraArgs: [], isPreview})({now, callback: (output, err) => {
|
render({source, sourcePath, vars, features, extraArgs: [], isPreview})({now, callback: (output, err) => {
|
||||||
this.mutate(s => {
|
this.mutate(s => {
|
||||||
setRendering(s, false);
|
setRendering(s, false);
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue