diff --git a/modules/lstream/base.js b/modules/lstream/base.js index 207690d7..ca189e76 100644 --- a/modules/lstream/base.js +++ b/modules/lstream/base.js @@ -14,8 +14,8 @@ export class StreamBase { return new PairwiseStream(this, first); } - scan(initAccumulator) { - return new ScanStream(this, initAccumulator); + scan(initAccumulator, scanFunc) { + return new ScanStream(this, initAccumulator, scanFunc); } remember(initialValue, usingStream) { diff --git a/modules/lstream/index.d.ts b/modules/lstream/index.d.ts index d9dedf3f..9f86a360 100644 --- a/modules/lstream/index.d.ts +++ b/modules/lstream/index.d.ts @@ -9,11 +9,11 @@ interface Stream extends Observable { filter(predicate: (T) => boolean): Stream; - pairwise(first: T): Stream<[T, T]>; + pairwise(first?: T): Stream<[T, T]>; - scan(initAccumulator: any): Stream; + scan(seed: T, scanFn: (accum: T, current: T) => T): Stream; - remember(initialValue: T, usingStream: any): StateStream + remember(initialValue: T, usingStream?: any): StateStream distinct(): Stream; @@ -42,7 +42,7 @@ export function stream(): Emitter; export function eventStream(): Emitter; -export function combine(...streams: Stream[]): Stream; +export function combine(...streams: Stream[]): Stream; export function merge(...streams: Stream[]): Stream; diff --git a/modules/lstream/scan.js b/modules/lstream/scan.js index 10a3810c..c58c8af5 100644 --- a/modules/lstream/scan.js +++ b/modules/lstream/scan.js @@ -2,13 +2,17 @@ import {StreamBase} from './base'; export class ScanStream extends StreamBase { - constructor(stream, initAccumulator) { + constructor(stream, seed, scanFunc) { super(); this.stream = stream; - this.acc = initAccumulator; + this.value = seed; + this.scanFunc = scanFunc; } attach(observer) { - return this.stream.attach(v => this.acc = observer(this.acc, v)); + return this.stream.attach(v => { + this.value = this.scanFunc(this.value, v); + observer(this.value); + }); } } diff --git a/modules/ui/components/GenericWizard.tsx b/modules/ui/components/GenericWizard.tsx index 58f1a800..a9830d21 100644 --- a/modules/ui/components/GenericWizard.tsx +++ b/modules/ui/components/GenericWizard.tsx @@ -13,7 +13,8 @@ export function GenericWizard({topicId, title, left, className, children, onCanc left?: number, onCancel: () => any, onOK: () => any, - infoText: string + onKeyDown: (e) => any, + infoText: any } & WindowProps ) { return { + if (opt.value === value) { + needsInclusion = false; + } + }); + if (needsInclusion) { + nonExistent = {value ? value+'' : ''}; + } + } + return } diff --git a/modules/ui/errorBoundary.js b/modules/ui/errorBoundary.js deleted file mode 100644 index 9f2d8da4..00000000 --- a/modules/ui/errorBoundary.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import context from 'context'; - -export default function errorBoundary(message, fix, resetOn) { - return function(Comp) { - class ErrorBoundary extends React.Component { - - state = { - hasError: false, - fixAttempt: false - }; - - componentDidCatch() { - this.setState({hasError: true}); - if (!this.state.fixAttempt) { - if (fix) { - fix(this.props); - this.setState({hasError: false, fixAttempt: true}); - } - } - if (resetOn) { - let stream = resetOn(context.streams); - if (stream) { - this.attcahing = true; - this.detacher = stream.attach(this.reset); - this.attcahing = false; - } - } - } - - reset = () => { - if (this.attcahing) { - return; - } - this.setState({hasError: false, fixAttempt: false}); - if (this.detacher) { - this.detacher(); - } - }; - - render() { - if (this.state.hasError) { - return message || null; - } - return ; - } - } - return ErrorBoundary; - } -} \ No newline at end of file diff --git a/modules/ui/errorBoundary.tsx b/modules/ui/errorBoundary.tsx new file mode 100644 index 00000000..692094d0 --- /dev/null +++ b/modules/ui/errorBoundary.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import context from 'context'; +import {Stream} from "lstream"; +import {useStream} from "ui/effects"; + +type ErrorBoundaryProps = { + message: any; + children: any; +}; + +export function HealingErrorBoundary({resetOn, ...props}: ErrorBoundaryProps & { + resetOn: (ctx) => Stream, +}) { + + const key = useStream(ctx => resetOn(ctx).scan(0, (acc, curr) => acc + curr)); + + return ; +} + +export class ErrorBoundary extends React.Component { + + state = { + hasError: false, + }; + + componentDidCatch(error:Error, errorInfo:React.ErrorInfo) { + console.error(error); + this.setState({hasError: true}); + } + + render() { + if (this.state.hasError) { + return this.props.message || null; + } + return this.props.children; + } +} + diff --git a/package-lock.json b/package-lock.json index f404c149..905437de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "classnames": "2.2.5", "clipper-lib": "6.2.1", "earcut": "2.1.1", + "immer": "^9.0.12", "less": "^3.11.1", "libtess": "1.2.2", "lodash": "^4.17.15", @@ -7512,6 +7513,15 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", + "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -21428,6 +21438,11 @@ "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", "optional": true }, + "immer": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz", + "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==" + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", diff --git a/package.json b/package.json index f903f221..a1db187c 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "classnames": "2.2.5", "clipper-lib": "6.2.1", "earcut": "2.1.1", + "immer": "^9.0.12", "less": "^3.11.1", "libtess": "1.2.2", "lodash": "^4.17.15", diff --git a/web/app/cad/assembly/assemblyPlugin.ts b/web/app/cad/assembly/assemblyPlugin.ts index b6fe0bed..fab7819e 100644 --- a/web/app/cad/assembly/assemblyPlugin.ts +++ b/web/app/cad/assembly/assemblyPlugin.ts @@ -49,7 +49,7 @@ export function activate(ctx: ApplicationContext) { } function solveAssembly(): void { - if (ctx.craftService.isEditingHistory()) { + if (ctx.craftService.isEditingHistory) { console.log('skipping assembly resolve request in the history mode'); return; } diff --git a/web/app/cad/craft/craftPlugin.ts b/web/app/cad/craft/craftPlugin.ts index 4867ccf7..17711ebf 100644 --- a/web/app/cad/craft/craftPlugin.ts +++ b/web/app/cad/craft/craftPlugin.ts @@ -1,4 +1,4 @@ -import {addModification, stepOverriding} from './craftHistoryUtils'; +import {addModification, finishHistoryEditing, stepOverriding} from './craftHistoryUtils'; import {Emitter, state, StateStream, stream} from 'lstream'; import materializeParams from './schema/materializeParams'; import CadError from '../../utils/errors'; @@ -103,15 +103,16 @@ export function activate(ctx: CoreContext) { } } - function isEditingHistory() { - const mods = this.modifications$.value; - return mods && mods.pointer !== mods.history.length - 1; - } - ctx.craftService = { - modify, modifyInHistoryAndStep, reset, rebuild, runRequest, runPipeline, + + get isEditingHistory() { + const mods = this.modifications$.value; + return mods && mods.pointer !== mods.history.length - 1; + }, + + modify, modifyInHistoryAndStep, reset, rebuild, runRequest, runPipeline, historyTravel: historyTravel(modifications$), - modifications$, models$, update$, isEditingHistory, pipelineFailure$ + modifications$, models$, update$, pipelineFailure$ }; // @ts-ignore @@ -244,9 +245,16 @@ export interface OperationResult { } -interface CraftHistory { +export interface CraftHistory { history: OperationRequest[]; pointer: number; + hints?: CraftHints; +} + +export interface CraftHints { + + noWizardFocus?: boolean; + } interface CraftService { @@ -256,9 +264,9 @@ interface CraftService { update$: Emitter; pipelineFailure$: StateStream - modify(request: OperationRequest, onAccepted: () => void, onError: () => Error); + modify(request: OperationRequest, onAccepted: () => void, onError: (error) => void); - modifyInHistoryAndStep(request: OperationRequest, onAccepted: () => void, onError: () => Error); + modifyInHistoryAndStep(request: OperationRequest, onAccepted: () => void, onError: (error) => void); reset(modifications: OperationRequest[]); @@ -270,13 +278,13 @@ interface CraftService { runPipeline(history: OperationRequest[], beginIndex: number, endIndex: number): Promise; - isEditingHistory(): boolean; + isEditingHistory: boolean; } interface HistoryTravel { setPointer(pointer, hints:any); - begin(hints: any); - end(hints: any); + begin(hints?: any); + end(hints?: any); forward(hints: any); backward(hints: any); } diff --git a/web/app/cad/craft/e0/occCommandInterface.ts b/web/app/cad/craft/e0/occCommandInterface.ts index e5e67c32..9c0f5104 100644 --- a/web/app/cad/craft/e0/occCommandInterface.ts +++ b/web/app/cad/craft/e0/occCommandInterface.ts @@ -8,6 +8,9 @@ const pushedModels = new Set(); export const OCI: OCCCommandInterface = new Proxy({}, { get: function (target, prop: string, receiver) { return prop in target ? target[prop] : function() { + if (typeof prop !== 'string') { + return undefined; + } prop = prop.replace(/^_/, ''); const args = Array.from(arguments).map(arg => { const type = typeof arg; diff --git a/web/app/cad/craft/operationPlugin.ts b/web/app/cad/craft/operationPlugin.ts index 23be7cea..cc3e666c 100644 --- a/web/app/cad/craft/operationPlugin.ts +++ b/web/app/cad/craft/operationPlugin.ts @@ -94,7 +94,7 @@ export interface Operation extends OperationDescriptor{ icon: string|IconType; }; schemaIndex: SchemaIndex; - form: () => React.ReactNode; + form: React.FunctionComponent; schema: OperationSchema; } @@ -107,7 +107,8 @@ export interface OperationDescriptor { run: (request: R, opContext: CoreContext) => OperationResult | Promise; paramsInfo: (params: R) => string, previewGeomProvider?: (params: R) => OperationGeometryProvider, - form: FormDefinition | (() => React.ReactNode), + previewer?: any, + form: FormDefinition | React.FunctionComponent, schema?: OperationSchema, onParamsUpdate?: (params, name, value) => void, } diff --git a/web/app/cad/craft/schema/initializeBySchema.ts b/web/app/cad/craft/schema/initializeBySchema.ts index ff96323b..d2a27362 100644 --- a/web/app/cad/craft/schema/initializeBySchema.ts +++ b/web/app/cad/craft/schema/initializeBySchema.ts @@ -1,8 +1,8 @@ -import {TypeRegistry, Types} from "cad/craft/schema/types"; -import {OperationSchema, SchemaField} from "cad/craft/schema/schema"; -import {ApplicationContext} from "context"; +import {Types} from "cad/craft/schema/types"; +import {isValueNotProvided, OperationSchema, SchemaField} from "cad/craft/schema/schema"; +import {CoreContext} from "context"; -export default function initializeBySchema(schema: OperationSchema, context: ApplicationContext) { +export default function initializeBySchema(schema: OperationSchema, context: CoreContext) { let fields = Object.keys(schema); let obj = {}; for (let field of fields) { @@ -42,3 +42,33 @@ export default function initializeBySchema(schema: OperationSchema, context: App } return obj; } + + +export function fillUpMissingFields(params: any, schema: OperationSchema, context: CoreContext) { + let fields = Object.keys(schema); + for (let field of fields) { + const md = schema[field] as SchemaField; + + if (md.optional) { + continue; + } + + let val = params[field]; + + const isPrimitive = + md.type !== Types.array + && md.type !== Types.object + && md.type !== Types.entity; + + if (isPrimitive && isValueNotProvided(val)) { + params[field] = md.defaultValue; + } else if (md.type === Types.object) { + if (!val) { + val = {}; + params[field] = val; + } + fillUpMissingFields(val, md.schema, context); + } + } + +} \ No newline at end of file diff --git a/web/app/cad/craft/schema/materializeParams.ts b/web/app/cad/craft/schema/materializeParams.ts index b1a3b758..d0885081 100644 --- a/web/app/cad/craft/schema/materializeParams.ts +++ b/web/app/cad/craft/schema/materializeParams.ts @@ -1,6 +1,7 @@ import {TypeRegistry} from "cad/craft/schema/types"; import {CoreContext} from "context"; import { + isValueNotProvided, OperationParams, OperationParamsError, OperationParamsErrorReporter, @@ -33,14 +34,13 @@ function materializeParamsImpl(ctx: CoreContext, result: any, parentReportError: OperationParamsErrorReporter) { - const notProvided = value => value === undefined || value === null || value === ''; for (let field of Object.keys(schema)) { const reportError = parentReportError.dot(field); const md = schema[field]; let value = params[field]; - if (notProvided(value)) { + if (isValueNotProvided(value)) { if (!md.optional) { reportError('required'); } @@ -52,7 +52,7 @@ function materializeParamsImpl(ctx: CoreContext, value = md.resolve( ctx, value, md as any, reportError ) - if (notProvided(value) && !md.optional) { + if (isValueNotProvided(value) && !md.optional) { reportError('required'); } } diff --git a/web/app/cad/craft/schema/schema.ts b/web/app/cad/craft/schema/schema.ts index 693f60cf..3c4e181c 100644 --- a/web/app/cad/craft/schema/schema.ts +++ b/web/app/cad/craft/schema/schema.ts @@ -93,4 +93,6 @@ export function unwrapMetadata(fieldMd: SchemaField) { ); } return fieldMd; -} \ No newline at end of file +} + +export const isValueNotProvided = value => value === undefined || value === null || value === ''; diff --git a/web/app/cad/craft/ui/HistoryTimeline.jsx b/web/app/cad/craft/ui/HistoryTimeline.jsx index d5956704..401d528b 100644 --- a/web/app/cad/craft/ui/HistoryTimeline.jsx +++ b/web/app/cad/craft/ui/HistoryTimeline.jsx @@ -16,7 +16,7 @@ import {aboveElement} from 'ui/positionUtils'; .map(([modifications, operationRegistry, insertOperationReq]) => ({ ...modifications, operationRegistry, - inProgressOperation: insertOperationReq.type, + inProgressOperation: !!insertOperationReq, getOperation: type => operationRegistry[type]||EMPTY_OBJECT }))) @mapContext(({streams, services}) => ({ diff --git a/web/app/cad/craft/wizard/components/Wizard.jsx b/web/app/cad/craft/wizard/components/Wizard.jsx deleted file mode 100644 index b4abd940..00000000 --- a/web/app/cad/craft/wizard/components/Wizard.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import Stack from 'ui/components/Stack'; -import Button from 'ui/components/controls/Button'; -import ButtonGroup from 'ui/components/controls/ButtonGroup'; - -import ls from './Wizard.less'; -import CadError from '../../../../utils/errors'; -import {FormContext, FormContextData} from './form/Form'; -import connect from 'ui/connect'; -import {combine} from 'lstream'; -import {GenericWizard} from "ui/components/GenericWizard"; -import * as PropTypes from "prop-types"; -import {useStream} from "ui/effects"; - -@connect((streams, props) => combine(props.context.workingRequest$, props.context.state$) - .map(([workingRequest, state]) => ({ - ...workingRequest, - activeParam: state.activeParam, - error: state.error - }))) -export default class Wizard extends React.Component { - - state = { - hasInternalError: false, - }; - - updateParam = (name, value) => { - this.props.context.updateParam(name, value); - }; - - componentDidCatch() { - this.setState({hasInternalError: true}); - } - - render() { - if (this.state.hasInternalError) { - return operation error; - } - - let {left, type, params, state, context} = this.props; - let operation = context.operation; - - let title = (operation.label || type).toUpperCase(); - - let Form = operation.form; - - const error = this.props.error; - return - {error && } - - } - > - -
- - - ; - } - - onKeyDown = e => { - switch (e.keyCode) { - case 27 : - this.cancel(); - break; - case 13 : - this.onOK(); - break; - } - }; - - focusFirstInput = el => { - if (this.props.noFocus) { - return; - } - let toFocus = el.querySelector('input, select'); - if (!toFocus) { - toFocus = el; - } - toFocus.focus(); - }; - - cancel = () => { - this.props.onCancel(); - }; - - onOK = () => { - this.props.onOK(); - }; -} -function PipelineError() { - const pipelineFailure = useStream(ctx => ctx.craftService.pipelineFailure$); - if (!pipelineFailure) { - return null; - } - return -} - -function ErrorPrinter({error}) { - return
- {CadError.ALGORITHM_ERROR_KINDS.includes(error.kind) && - performing operation with current parameters leads to an invalid object - (self-intersecting / zero-thickness / complete degeneration or unsupported cases) - } - {error.code &&
{error.code}
} - {error.userMessage &&
{error.userMessage}
} - {!error.userMessage &&
internal error processing operation, check the log
} -
-} diff --git a/web/app/cad/craft/wizard/components/Wizard.tsx b/web/app/cad/craft/wizard/components/Wizard.tsx new file mode 100644 index 00000000..e48f0f29 --- /dev/null +++ b/web/app/cad/craft/wizard/components/Wizard.tsx @@ -0,0 +1,122 @@ +import React, {useContext} from 'react'; + +import ls from './Wizard.less'; +import CadError from '../../../../utils/errors'; +import {FormParamsContext, FormPathContext, FormStateContext} from './form/Form'; +import {GenericWizard} from "ui/components/GenericWizard"; +import {useStream} from "ui/effects"; +import {AppContext} from "cad/dom/components/AppContext"; + +interface WizardProps { + noFocus: boolean; + + left?: number; + + onCancel(): void; + + onOK(): void; +} + +export default function Wizard(props: WizardProps) { + + const ctx = useContext(AppContext); + const state = useStream(ctx => ctx.wizardService.state$); + const workingRequest = useStream(ctx => ctx.wizardService.workingRequest$); + + if (!workingRequest) { + return; + } + + const operation = ctx.operationService.get(workingRequest.type); + + if (!operation) { + return; + } + + const error = state.error; + + const onKeyDown = e => { + switch (e.keyCode) { + case 27 : + cancel(); + break; + case 13 : + onOK(); + break; + } + }; + + const focusFirstInput = el => { + if (props.noFocus) { + return; + } + let toFocus = el.querySelector('input, select'); + if (!toFocus) { + toFocus = el; + } + toFocus.focus(); + }; + + const cancel = () => { + props.onCancel(); + }; + + const onOK = () => { + props.onOK(); + }; + + let {left} = props; + let wizardService = ctx.wizardService; + + let title = (operation.label || operation.id).toUpperCase(); + + let Form = operation.form; + + return + {error && } + + } + > + + + + + + + + + + ; +} + + +function PipelineError() { + const pipelineFailure = useStream(ctx => ctx.craftService.pipelineFailure$); + if (!pipelineFailure) { + return null; + } + return +} + +function ErrorPrinter({error}) { + return
+ {CadError.ALGORITHM_ERROR_KINDS.includes(error.kind) && + performing operation with current parameters leads to an invalid object + (self-intersecting / zero-thickness / complete degeneration or unsupported cases) + } + {error.code &&
{error.code}
} + {error.userMessage &&
{error.userMessage}
} + {!error.userMessage &&
internal error processing operation, check the log
} +
+} diff --git a/web/app/cad/craft/wizard/components/WizardManager.jsx b/web/app/cad/craft/wizard/components/WizardManager.jsx deleted file mode 100644 index 97fdd810..00000000 --- a/web/app/cad/craft/wizard/components/WizardManager.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import Wizard from './Wizard'; -import connect from 'ui/connect'; -import decoratorChain from 'ui/decoratorChain'; -import mapContext from 'ui/mapContext'; -import {finishHistoryEditing} from '../../craftHistoryUtils'; - -function WizardManager({wizardContext, type, cancel, cancelHistoryEdit, applyWorkingRequest}) { - if (!wizardContext) { - return null; - } - return -} - -export default decoratorChain( - connect(streams => streams.wizard.wizardContext.map(wizardContext => ({wizardContext}))), - mapContext(ctx => ({ - cancel: ctx.services.wizard.cancel, - cancelHistoryEdit: () => ctx.streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)), - applyWorkingRequest: ctx.services.wizard.applyWorkingRequest - })) -) -(WizardManager); diff --git a/web/app/cad/craft/wizard/components/WizardManager.tsx b/web/app/cad/craft/wizard/components/WizardManager.tsx new file mode 100644 index 00000000..ac60cfa8 --- /dev/null +++ b/web/app/cad/craft/wizard/components/WizardManager.tsx @@ -0,0 +1,22 @@ +import React, {useContext} from 'react'; +import Wizard from './Wizard'; +import {useStream} from "ui/effects"; +import {AppContext} from "cad/dom/components/AppContext"; +import {ErrorBoundary} from "ui/errorBoundary"; + +export default function WizardManager() { + + const ctx = useContext(AppContext); + const workingRequest = useStream(ctx => ctx.wizardService.workingRequest$); + + if (!workingRequest) { + return null; + } + + return operation error}> + + +} diff --git a/web/app/cad/craft/wizard/components/form/Form.tsx b/web/app/cad/craft/wizard/components/form/Form.tsx index adccd7a2..81bcde3a 100644 --- a/web/app/cad/craft/wizard/components/form/Form.tsx +++ b/web/app/cad/craft/wizard/components/form/Form.tsx @@ -1,43 +1,17 @@ -import React from 'react'; +import React, {useContext} from 'react'; import Label from 'ui/components/controls/Label'; import Field from 'ui/components/controls/Field'; import Stack from 'ui/components/Stack'; import {camelCaseSplitToStr} from 'gems/camelCaseSplit'; -import {FlattenPath, ParamsPath, ParamsPathSegment, WizardContext} from "cad/craft/wizard/wizardTypes"; -import {flattenPath, OperationParamValue} from "cad/craft/schema/schema"; +import {ParamsPath, ParamsPathSegment, WizardState} from "cad/craft/wizard/wizardTypes"; +import {OperationParams, OperationParamValue} from "cad/craft/schema/schema"; +import {AppContext} from "cad/dom/components/AppContext"; +import _ from "lodash"; -export const FormContext: React.Context = React.createContext(null); +export const FormStateContext: React.Context = React.createContext(null); +export const FormParamsContext: React.Context = React.createContext(null); +export const FormPathContext: React.Context = React.createContext([]); -export class FormContextData { - - wizardContext: WizardContext; - prefix: ParamsPath; - - constructor(wizardContext: WizardContext, prefix: ParamsPath) { - this.wizardContext = wizardContext; - this.prefix = prefix; - } - - updateParam(segment: ParamsPathSegment, value: OperationParamValue): void { - this.wizardContext.updateParam([...this.prefix, segment], value); - } - - readParam(segment: ParamsPathSegment): OperationParamValue { - return this.wizardContext.readParam([...this.prefix, segment]); - } - - dot(segment: ParamsPathSegment): FormContextData { - return new FormContextData(this.wizardContext, [...this.prefix, segment]); - } - - setActiveParam = (path: FlattenPath) => { - this.wizardContext.updateState(state => state.activeParam = path); - } - - get activeParam(): FlattenPath { - return this.wizardContext.state$.value.activeParam; - } -} export function Group({children}) { return @@ -61,36 +35,42 @@ interface FormFieldProps { children?: any } -export function attachToForm(Control) { +export function attachToForm(Control): any { + return function FormField({name, ...props}: FormFieldProps) { - return - { - (ctx: FormContextData) => { - const fullPath = flattenPath([...ctx.prefix, name]); - const onChange = val => ctx.updateParam(name, val); - const setActive = val => ctx.setActiveParam(val ? fullPath : undefined); - return - - ; - } - } - ; + + const ctx = useContext(AppContext); + const formPath = useContext(FormPathContext); + const formState = useContext(FormStateContext); + const params = useContext(FormParamsContext); + + const fullPath = [...formPath, name]; + const fullPathFlatten = fullPath.join('.'); + const onChange = value => ctx.wizardService.updateParam(fullPath, value); + const setActive = (isActive) => ctx.wizardService.updateState(state => { + state.activeParam = isActive ? fullPathFlatten : null; + }); + + const value = _.get(params, fullPath); + + return + + ; }; } export function SubForm(props: {name: ParamsPathSegment, children: any}) { - return - { - (ctx: FormContextData) => { - return - {props.children} - - } - } - + const formState = useContext(FormStateContext); + const formPath = useContext(FormPathContext); + + return + + {props.children} + + ; } \ No newline at end of file diff --git a/web/app/cad/craft/wizard/wizardPlugin.ts b/web/app/cad/craft/wizard/wizardPlugin.ts index 05c7f491..ac47ba87 100644 --- a/web/app/cad/craft/wizard/wizardPlugin.ts +++ b/web/app/cad/craft/wizard/wizardPlugin.ts @@ -1,163 +1,181 @@ -import {state} from 'lstream'; -import initializeBySchema from '../schema/initializeBySchema'; -import {clone, EMPTY_OBJECT} from 'gems/objects'; +import {combine, state, StateStream} from 'lstream'; +import initializeBySchema, {fillUpMissingFields} from '../schema/initializeBySchema'; +import {clone} from 'gems/objects'; import materializeParams from '../schema/materializeParams'; import {createFunctionList} from 'gems/func'; -import {OperationRequest} from "cad/craft/craftPlugin"; -import {ParamsPath, WizardContext, WizardState} from "cad/craft/wizard/wizardTypes"; +import {CraftHints, CraftHistory, OperationRequest} from "cad/craft/craftPlugin"; +import {NewOperationCall, ParamsPath, WizardService, WizardState} from "cad/craft/wizard/wizardTypes"; import _ from "lodash"; import {OperationParamValue} from "cad/craft/schema/schema"; +import {ApplicationContext} from "context"; +import {Operation} from "cad/craft/operationPlugin"; +import produce from "immer" -export function activate(ctx) { +type WorkingRequest = OperationRequest & { + hints?: CraftHints, + requestKey: number +} + +export function activate(ctx: ApplicationContext) { let {streams, services} = ctx; - streams.wizard = {}; - - streams.wizard.insertOperation = state(EMPTY_OBJECT); + const insertOperation$ = state(null); - streams.wizard.effectiveOperation = state(EMPTY_OBJECT); + let REQUEST_COUNTER = 1; - streams.wizard.insertOperation.attach(insertOperationReq => { - if (insertOperationReq.type) { - let type = insertOperationReq.type; - let operation = ctx.services.operation.get(type); - streams.wizard.effectiveOperation.value = { - type: operation.id, - initialOverrides: insertOperationReq.initialOverrides, - changingHistory: false - }; - } - }); - - function gotoEditHistoryModeIfNeeded({pointer, history, hints}) { - if (pointer !== history.length - 1) { - let {type, params} = history[pointer + 1]; - streams.wizard.effectiveOperation.value = { - type, - params, - noWizardFocus: hints && hints.noWizardFocus, - changingHistory: true - }; - } else { - streams.wizard.effectiveOperation.value = EMPTY_OBJECT; - } + const workingRequest$: StateStream = combine<[NewOperationCall, CraftHistory]>( + insertOperation$, + ctx.craftService.modifications$ + ).map(([insertOperationReq, mods]) => { - } - - streams.craft.modifications.attach(mod => { - if (streams.wizard.insertOperation.value.type) { - return; - } - gotoEditHistoryModeIfNeeded(mod); - }); + let operation; + let params; - streams.wizard.wizardContext = streams.wizard.effectiveOperation.map((opRequest: OperationRequest) => { - let wizCtx: WizardContext = null; - if (opRequest.type) { - - let operation = ctx.services.operation.get(opRequest.type); - - let params; - let {changingHistory, noWizardFocus} = opRequest; - if (changingHistory) { - params = clone(opRequest.params) - } else { - params = initializeBySchema(operation.schema, ctx); - if (opRequest.initialOverrides) { - applyOverrides(params, opRequest.initialOverrides); - } + if (insertOperationReq !== null) { + operation = ctx.operationService.get(insertOperationReq.type); + params = initializeBySchema(operation.schema, ctx); + if (insertOperationReq.initialOverrides) { + applyOverrides(params, insertOperationReq.initialOverrides); } - let workingRequest$ = state({ - type: opRequest.type, - params - }); - - let materializedWorkingRequest$ = workingRequest$.map(req => { - let params = {}; - let errors = []; - materializeParams(ctx, req.params, operation.schema, params, errors); - if (errors.length !== 0) { - return INVALID_REQUEST; - } + return { + type: operation.id, + params, + requestKey: REQUEST_COUNTER++ + } + } else { + const {pointer, history, hints} = mods + if (pointer !== history.length - 1) { + let {type, params} = history[pointer + 1]; return { - type: req.type, - params + type, + params: clone(params), + hints, + requestKey: REQUEST_COUNTER++ }; - }).remember(INVALID_REQUEST).filter(r => r !== INVALID_REQUEST).throttle(500); - const state$ = state({ - activeParam: null - }); - const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params)); - const updateState = mutator => state$.mutate(state => mutator(state)); - const updateParam = (path: ParamsPath, value: OperationParamValue) => { - updateParams(params => { - // if (operation.onParamsUpdate) { - // operation.onParamsUpdate(params, name, value, params[name]); - // } - if (!Array.isArray(path)) { - path = [path] - } - _.set(params, path, value); - }); - }; - - const readParam = (path: ParamsPath) => { - return _.get(params, path); - }; - - const disposerList = createFunctionList(); - wizCtx = { - workingRequest$, materializedWorkingRequest$, state$, - updateParams, updateParam, readParam, updateState, - operation, changingHistory, noWizardFocus, - addDisposer: disposerList.add, - dispose: disposerList.call, - ID: ++REQUEST_COUNTER, - }; + } else { + return null; + } } - return wizCtx; + }).remember(null); - streams.wizard.wizardContext.pairwise().attach(([oldContext, newContext]) => { - if (oldContext) { - oldContext.dispose(); - } - }); - - services.wizard = { + const materializedWorkingRequest$ = workingRequest$.map(req => { + if (req == null) { + return null; + } + let params = {}; + let errors = []; + let operation = ctx.services.operation.get(req.type); - open: (type, initialOverrides) => { - streams.wizard.insertOperation.value = { + materializeParams(ctx, req.params, operation.schema, params, errors); + if (errors.length !== 0) { + return null; + } + return { + type: req.type, + params + }; + }).remember(null).filter(r => r !== null).throttle(500); + + const state$ = state({}); + let disposerList = createFunctionList(); + + // reset effect + workingRequest$.pairwise().attach(([old, curr]) => { + if (old !== null && old.requestKey !== curr?.requestKey) { + console.log("=========> DISPOSE") + disposerList.call(); + disposerList = createFunctionList(); + state$.next({}); + } + }) + + const updateParams = mutator => workingRequest$.update((req: WorkingRequest) => produce(req, draft => mutator(draft.params))); + const updateState = mutator => state$.update((state: WizardState) => produce(state, mutator)); + const updateParam = (path: ParamsPath, value: OperationParamValue) => { + updateParams(params => { + // if (operation.onParamsUpdate) { + // operation.onParamsUpdate(params, name, value, params[name]); + // } + if (!Array.isArray(path)) { + path = [path] + } + _.set(params, path, value); + }); + }; + + const readParam = (path: ParamsPath) => { + return _.get(workingRequest$.value.params, path); + }; + + const getWorkingRequest = () => workingRequest$.value; + + //legacy + streams.wizard = { + insertOperation: insertOperation$, + }; + + const cancel = () => { + insertOperation$.next(null); + }; + + const wizardService: WizardService = { + + open: (type: string, initialOverrides: NewOperationCall) => { + streams.wizard.insertOperation.next({ type, initialOverrides - }; + }); }, - cancel: () => { - streams.wizard.insertOperation.value = EMPTY_OBJECT; - gotoEditHistoryModeIfNeeded(streams.craft.modifications.value); - }, + cancel, applyWorkingRequest: () => { - let {type, params} = streams.wizard.wizardContext.value.workingRequest$.value; + let {type, params} = getWorkingRequest(); let request = clone({type, params}); - const setError = error => streams.wizard.wizardContext.mutate(ctx => ctx.state$.mutate(state => state.error = error)); - if (streams.wizard.insertOperation.value.type) { - ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT, setError ); + const setError = error => state$.mutate(state => state.error = error); + if (insertOperation$.value) { + ctx.craftService.modify(request, cancel, setError); } else { - ctx.services.craft.modifyInHistoryAndStep(request, () => streams.wizard.effectiveOperation.value = EMPTY_OBJECT, setError); + ctx.craftService.modifyInHistoryAndStep(request, () => {}, setError); } }, - isInProgress: () => streams.wizard.wizardContext.value !== null + isInProgress: () => getWorkingRequest() !== null, + + get workingRequest() { + return getWorkingRequest(); + }, + + get materializedWorkingRequest() { + return materializedWorkingRequest$.value; + }, + + get operation(): Operation { + const req = getWorkingRequest(); + if (!req) { + return null; + } + return ctx.operationService.get(req.type); + }, + + workingRequest$, materializedWorkingRequest$, state$, + updateParams, updateParam, readParam, updateState, + addDisposer: disposerList.add }; + + ctx.wizardService = services.wizard = wizardService; } +declare module 'context' { + interface ApplicationContext { + wizardService: WizardService + } +} + + function applyOverrides(params, initialOverrides) { Object.assign(params, initialOverrides); } - -const INVALID_REQUEST = {}; -let REQUEST_COUNTER = 0; \ No newline at end of file diff --git a/web/app/cad/craft/wizard/wizardSelectionPlugin.ts b/web/app/cad/craft/wizard/wizardSelectionPlugin.ts index 5879aa1a..58b510f8 100644 --- a/web/app/cad/craft/wizard/wizardSelectionPlugin.ts +++ b/web/app/cad/craft/wizard/wizardSelectionPlugin.ts @@ -1,37 +1,36 @@ -import {FACE, SHELL} from '../../model/entities'; +import {FACE, SHELL} from 'cad/model/entities'; import {memoize} from "lodash/function"; -import {Types} from "cad/craft/schema/types"; import {OperationRequest} from "cad/craft/craftPlugin"; -import {FlattenPath, ParamsPath, WizardContext} from "cad/craft/wizard/wizardTypes"; -import {OperationParamValue, SchemaField} from "cad/craft/schema/schema"; -import {EntityReference, SchemaIndexField} from "cad/craft/operationPlugin"; +import {FlattenPath, ParamsPath, WizardService} from "cad/craft/wizard/wizardTypes"; +import {OperationParamValue} from "cad/craft/schema/schema"; +import {EntityReference} from "cad/craft/operationPlugin"; +import {ApplicationContext} from "context"; -export function activate(ctx) { - ctx.streams.wizard.wizardContext.attach((wizCtx: WizardContext) => { +export function activate(ctx: ApplicationContext) { + const wizardService = ctx.wizardService; + wizardService.workingRequest$.attach((opRequest: OperationRequest) => { ctx.services.marker.clear(); - if (wizCtx) { - const wizardPickHandler = createPickHandlerFromSchema(wizCtx); + if (opRequest) { + const wizardPickHandler = createPickHandlerFromSchema(wizardService); ctx.services.pickControl.setPickHandler(wizardPickHandler); - wizCtx.workingRequest$.attach(({type, params}: OperationRequest) => { - const marker = ctx.services.marker; - marker.startSession(); - let {schemaIndex} = wizCtx.operation; - schemaIndex.entities.forEach(entityRef => { - //TODO: move to uiDefinition - let color = entityRef.metadata.markColor; + const marker = ctx.services.marker; + marker.startSession(); + let {schemaIndex} = wizardService.operation; + schemaIndex.entities.forEach(entityRef => { + //TODO: move to uiDefinition + let color = entityRef.metadata.markColor; - let val = wizCtx.readParam(entityRef.field.path); + let val = wizardService.readParam(entityRef.field.path); - if (Array.isArray(val)) { - val.forEach(id => marker.mark(id, color)); - } else { - if (val) { - marker.mark(val, color); - } + if (Array.isArray(val)) { + val.forEach(id => marker.mark(id, color)); + } else { + if (val) { + marker.mark(val, color); } - }); - marker.commit(); + } }); + marker.commit(); } else { ctx.services.pickControl.setPickHandler(null); @@ -52,20 +51,20 @@ const arrayValue = (id, arr) => { const getEntityParams = memoize(schema => Object.keys(schema).filter(key => schema[key].type === 'entity')); -function createPickHandlerFromSchema(wizCtx: WizardContext) { +function createPickHandlerFromSchema(wizardService: WizardService) { function update(param: ParamsPath, value: OperationParamValue, paramToMakeActive: FlattenPath) { - wizCtx.updateParam(param, value); - wizCtx.updateState(state => { + wizardService.updateParam(param, value); + wizardService.updateState(state => { state.activeParam = paramToMakeActive; }); } return model => { const modelType = model.TYPE; - let {schemaIndex} = wizCtx.operation; + let {schemaIndex} = wizardService.operation; let activeEntityRef = () => { - const state = wizCtx.state$.value; + const state = wizardService.state$.value; return schemaIndex.entitiesByFlattenedPaths[state.activeParam]; } @@ -83,7 +82,7 @@ function createPickHandlerFromSchema(wizCtx: WizardContext) { const param = entityRef.field; const valueGetter = entityRef.isArray ? arrayValue : singleValue; let paramToMakeActive = getNextActiveParam(entityRef); - const currentValue = wizCtx.readParam(param.path); + const currentValue = wizardService.readParam(param.path); update(param.path, valueGetter(id, currentValue), paramToMakeActive.field.flattenedPath); } @@ -111,7 +110,7 @@ function createPickHandlerFromSchema(wizCtx: WizardContext) { function deselectIfNeeded(id) { for (let entityRef of schemaIndex.entities) { - let val = wizCtx.readParam(entityRef.field.path); + let val = wizardService.readParam(entityRef.field.path); if (val === id) { update(entityRef.field.path, undefined, entityRef.field.flattenedPath); return true; diff --git a/web/app/cad/craft/wizard/wizardTypes.ts b/web/app/cad/craft/wizard/wizardTypes.ts index 32db8be1..6a052ece 100644 --- a/web/app/cad/craft/wizard/wizardTypes.ts +++ b/web/app/cad/craft/wizard/wizardTypes.ts @@ -1,5 +1,5 @@ import {StateStream} from "lstream"; -import {OperationRequest} from "cad/craft/craftPlugin"; +import {CraftHints, OperationRequest} from "cad/craft/craftPlugin"; import {MaterializedOperationParams, OperationParamValue, OperationParams} from "cad/craft/schema/schema"; import {Operation} from "cad/craft/operationPlugin"; @@ -10,17 +10,30 @@ export type ParamsPath = ParamsPathSegment[]; export type FlattenPath = string; export type WizardState = { - activeParam: FlattenPath + activeParam?: FlattenPath + error?: any }; -export interface WizardContext { +export interface WizardService { - workingRequest$: StateStream; + workingRequest$: StateStream; materializedWorkingRequest$: StateStream; state$: StateStream; + open(type: string, initialOverrides: NewOperationCall); + + cancel(); + + applyWorkingRequest(); + + isInProgress(): boolean; + + workingRequest: OperationRequest; + + materializedWorkingRequest: any; + updateParams: (mutator: (params: OperationParams) => void) => void; updateParam: (path: ParamsPath, value: OperationParamValue) => void; @@ -31,13 +44,11 @@ export interface WizardContext { operation: Operation; - changingHistory: boolean; - - noWizardFocus: boolean; - addDisposer: (disposer: () => any|void) => void; - dispose: () => void; +} - ID: number; +export interface NewOperationCall { + type: string; + initialOverrides: OperationParams; } \ No newline at end of file diff --git a/web/app/cad/mdf/ui/ChoiceWidget.tsx b/web/app/cad/mdf/ui/ChoiceWidget.tsx index 62af6f6d..d6bda270 100644 --- a/web/app/cad/mdf/ui/ChoiceWidget.tsx +++ b/web/app/cad/mdf/ui/ChoiceWidget.tsx @@ -1,10 +1,8 @@ -import {ComboBoxField, NumberField} from "cad/craft/wizard/components/form/Fields"; +import {ComboBoxField} from "cad/craft/wizard/components/form/Fields"; import React from "react"; -import {flattenPath, OperationSchema} from "cad/craft/schema/schema"; import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field"; import {Types} from "cad/craft/schema/types"; import {ComboBoxOption} from "ui/components/controls/ComboBoxControl"; -import {FormContext, FormContextData} from "cad/craft/wizard/components/form/Form"; export interface ChoiceWidgetProps extends FieldBasicProps { @@ -19,21 +17,10 @@ export interface ChoiceWidgetProps extends FieldBasicProps { export function ChoiceWidget(props: ChoiceWidgetProps) { if (!props.style || props.style === 'dropdown') { - return - { - (ctx: FormContextData) => { - const value = ctx.readParam(props.name); - let nonExistent = null; - if (!props.values.includes(value as any)) { - nonExistent = {value ? value+'' : ''} - } - return - {nonExistent} - {props.values.map(value => {value})} - ; - } - } - + return + {props.values.map(value => {value})} + ; + } else { throw 'implement me'; } diff --git a/web/app/cad/preview/previewPlugin.js b/web/app/cad/preview/previewPlugin.js deleted file mode 100644 index 1c39b57b..00000000 --- a/web/app/cad/preview/previewPlugin.js +++ /dev/null @@ -1,40 +0,0 @@ -import {createPreviewer} from './scenePreviewer'; - -export function activate(ctx) { - let {streams, services} = ctx; - - streams.wizard.wizardContext.attach(wizCtx => { - if (!wizCtx) { - return; - } - let {operation, materializedWorkingRequest$} = wizCtx; - if (operation.previewGeomProvider || operation.previewer) { - let previewer = null; - materializedWorkingRequest$.attach(({type, params}) => { - if (previewer === null) { - try { - if (operation.previewGeomProvider) { - previewer = createPreviewer(operation.previewGeomProvider, services, params); - } else if (operation.previewer) { - previewer = operation.previewer(ctx, params, wizCtx.updateParams); - } - } catch (e) { - console.error(e); - return; - } - wizCtx.addDisposer(() => { - previewer.dispose(); - ctx.services.viewer.requestRender(); - }); - } else { - try { - previewer.update(params); - } catch (e) { - console.error(e); - } - } - ctx.services.viewer.requestRender(); - }); - } - }); -} \ No newline at end of file diff --git a/web/app/cad/preview/previewPlugin.ts b/web/app/cad/preview/previewPlugin.ts new file mode 100644 index 00000000..ebc671ad --- /dev/null +++ b/web/app/cad/preview/previewPlugin.ts @@ -0,0 +1,42 @@ +import {createPreviewer} from './scenePreviewer'; +import {ApplicationContext} from "context"; + +export function activate(ctx: ApplicationContext) { + let previewer = null; + ctx.wizardService.materializedWorkingRequest$.attach(materializedWorkingRequest => { + if (!materializedWorkingRequest) { + previewer = null; + return; + } + const {type, params} = materializedWorkingRequest; + const operation = ctx.wizardService.operation; + if (operation.previewGeomProvider || operation.previewer) { + if (previewer === null) { + let newPreviewer; + try { + if (operation.previewGeomProvider) { + newPreviewer = createPreviewer(operation.previewGeomProvider, ctx.services, params); + } else if (operation.previewer) { + newPreviewer = operation.previewer(ctx, params, ctx.wizardService.updateParams); + } + previewer = newPreviewer; + } catch (e) { + console.error(e); + return; + } + ctx.wizardService.addDisposer(() => { + newPreviewer.dispose(); + previewer = null; + ctx.services.viewer.requestRender(); + }); + } else { + try { + previewer.update(params); + } catch (e) { + console.error(e); + } + } + ctx.services.viewer.requestRender(); + } + }); +} \ No newline at end of file diff --git a/web/app/cad/projectPlugin.ts b/web/app/cad/projectPlugin.ts index e6a7dd3f..e9ffdc6c 100644 --- a/web/app/cad/projectPlugin.ts +++ b/web/app/cad/projectPlugin.ts @@ -6,6 +6,7 @@ import {SketchFormat_V3} from "../sketcher/io"; import {OperationRequest} from "./craft/craftPlugin"; import {ProjectModel} from "./projectManager/projectManagerPlugin"; import {DebugMode$} from "debugger/Debugger"; +import {fillUpMissingFields} from "cad/craft/schema/initializeBySchema"; export const STORAGE_GLOBAL_PREFIX = 'TCAD'; export const PROJECTS_PREFIX = `${STORAGE_GLOBAL_PREFIX}.projects.`; @@ -54,6 +55,7 @@ export function initProjectService(ctx: CoreContext, id: string, hints: any) { let dataStr = ctx.storageService.get(ctx.projectService.projectStorageKey()); if (dataStr) { let data = JSON.parse(dataStr); + upgradeIfNeeded(data); loadData(data); } } catch (e) { @@ -61,6 +63,16 @@ export function initProjectService(ctx: CoreContext, id: string, hints: any) { } } + function upgradeIfNeeded(data: ProjectModel) { + if (data.history) { + data.history.forEach(req => { + const operation = ctx.operationService.get(req.type); + if (operation) { + fillUpMissingFields(req.params, operation.schema, ctx); + } + }); + } + } function loadData(data: ProjectModel) { if (data.expressions) {