import React from "react"; import { computed, observable, makeObservable, toJS } from "mobx"; import { ClassInfo, EezObject, getParent, IEezObject, isAncestor, isSubclassOf, PropertyInfo, PropertyType, registerClass, SerializedData, setParent } from "project-editor/core/object"; import { visitObjects } from "project-editor/core/search"; import { getProjectStore, getAncestorOfType, ProjectStore, createObject, getClass, objectToClipboardData } from "project-editor/store"; import { ActionComponent, Component, Widget } from "project-editor/flow/component"; import type { IFlowContext } from "project-editor/flow/flow-interfaces"; import { Rect } from "eez-studio-shared/geometry"; import { deleteObject, updateObject } from "project-editor/store"; import { ContainerWidget, SelectWidget } from "project-editor/flow/components/widgets"; import { Variable } from "project-editor/features/variable/variable"; import { ValueType } from "project-editor/features/variable/value-type"; import { EndActionComponent, InputActionComponent, OutputActionComponent, StartActionComponent } from "project-editor/flow/components/actions"; import { ProjectEditor } from "project-editor/project-editor-interface"; import type { LVGLWidget } from "project-editor/lvgl/widgets"; import { ConnectionLine } from "project-editor/flow/connection-line"; import { userPropertiesProperty, UserProperty } from "project-editor/flow/user-property"; //////////////////////////////////////////////////////////////////////////////// export abstract class Flow extends EezObject { static classInfo: ClassInfo = { properties: [ { name: "components", type: PropertyType.Array, typeClass: Component, hideInPropertyGrid: true }, { name: "connectionLines", type: PropertyType.Array, typeClass: ConnectionLine, hideInPropertyGrid: true }, { name: "localVariables", type: PropertyType.Array, typeClass: Variable, hideInPropertyGrid: true }, userPropertiesProperty ], findPastePlaceInside: ( flow: Flow, classInfo: ClassInfo, isSingleObject: boolean ): IEezObject | PropertyInfo | undefined => { if (flow) { if (isSubclassOf(classInfo, Component.classInfo)) { return flow.components; } else if (classInfo === FlowFragment.classInfo) { return flow; } } return undefined; }, beforeLoadHook: (object: IEezObject, jsObject: any) => { if (!jsObject.localVariables) { jsObject.localVariables = []; } if (jsObject.components) { for (const component of jsObject.components) { if (component.type === "DeclareVariableActionComponent") { component.type = "SetVariableActionComponent"; let localVariable = jsObject.localVariables.find( (localVariable: any) => localVariable.name === component.variable ); if (!localVariable) { let value; try { value = JSON.parse(component.value); } catch (err) {} let type: ValueType | undefined; if (typeof value === "number") { type = "float"; } else if (typeof value === "boolean") { type = "boolean"; } else if (typeof value === "string") { type = "string"; } jsObject.localVariables.push({ name: component.variable, type }); } } } } if (jsObject.connectionLines) { for (let i = 1; i < jsObject.connectionLines.length; i++) { for (let j = 0; j < i; j++) { const a = jsObject.connectionLines[i]; const b = jsObject.connectionLines[j]; if ( a.source == b.source && a.output == b.output && a.target == b.target && a.input == b.input ) { console.log("duplicate", a); } } } } if (!jsObject.userProperties) { jsObject.userProperties = []; } } }; components: Component[] = []; connectionLines: ConnectionLine[] = []; localVariables: Variable[] = []; userProperties: UserProperty[]; get userPropertiesAndLocalVariables() { return [...this.userProperties, ...this.localVariables]; } constructor() { super(); makeObservable(this, { actionComponents: computed, startComponent: computed, endComponent: computed, inputComponents: computed, outputComponents: computed, userPropertiesAndLocalVariables: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { components: observable, connectionLines: observable, localVariables: observable, userProperties: observable }); } get actionComponents() { const components = []; for (const component of visitObjects(this.components)) { if (component instanceof ActionComponent) { components.push(component); } } return components; } objectsToClipboardData(objects: IEezObject[]) { const flowFragment = new FlowFragment(); flowFragment.addObjects(this, objects); setParent(flowFragment, this); return objectToClipboardData( ProjectEditor.getProjectStore(this), flowFragment ); } deleteConnectionLines(component: Component) { this.connectionLines .filter( connectionLine => isAncestor(connectionLine.sourceComponent, component) || isAncestor(connectionLine.targetComponent, component) ) .forEach(connectionLine => deleteObject(connectionLine)); } deleteConnectionLinesToInput(component: Component, input: string) { this.connectionLines .filter( connectionLine => connectionLine.targetComponent == component && connectionLine.input == input ) .forEach(connectionLine => deleteObject(connectionLine)); } deleteConnectionLinesFromOutput(component: Component, output: string) { this.connectionLines .filter( connectionLine => connectionLine.sourceComponent == component && connectionLine.output == output ) .forEach(connectionLine => deleteObject(connectionLine)); } rerouteConnectionLinesInput( component: Component, inputBefore: string, inputAfter: string ) { this.connectionLines .filter( connectionLine => connectionLine.targetComponent == component && connectionLine.input == inputBefore ) .forEach(connectionLine => updateObject(connectionLine, { input: inputAfter }) ); } rerouteConnectionLinesOutput( component: Component, outputBefore: string, outputAfter: string ) { this.connectionLines .filter( connectionLine => connectionLine.sourceComponent == component && connectionLine.output == outputBefore ) .forEach(connectionLine => updateObject(connectionLine, { output: outputAfter }) ); } abstract get pageRect(): Rect; get startComponent() { return this.components.find( component => component instanceof StartActionComponent ); } get endComponent() { return this.components.find( component => component instanceof EndActionComponent ); } get inputComponents() { return this.components .filter(component => component instanceof InputActionComponent) .sort((a, b) => a.top - b.top) as InputActionComponent[]; } get outputComponents() { return this.components .filter(component => component instanceof OutputActionComponent) .sort((a, b) => a.top - b.top) as OutputActionComponent[]; } abstract renderWidgetComponents(flowContext: IFlowContext): React.ReactNode; abstract renderActionComponents(flowContext: IFlowContext): React.ReactNode; } //////////////////////////////////////////////////////////////////////////////// export class FlowFragment extends EezObject { components: Component[]; connectionLines: ConnectionLine[]; static classInfo: ClassInfo = { icon: ( ), properties: [ { name: "components", type: PropertyType.Array, typeClass: Component }, { name: "connectionLines", type: PropertyType.Array, typeClass: ConnectionLine } ], beforeLoadHook: (object: IEezObject, jsObject: any) => { if (jsObject.widgets) { jsObject.components = jsObject.widgets; delete jsObject.widgets; } }, pasteItemHook: ( object: IEezObject, clipboardData: { serializedData: SerializedData; pastePlace: EezObject; } ) => { const flow = ProjectEditor.getFlow(clipboardData.pastePlace); const pasteFlowFragment = clipboardData.serializedData .object as FlowFragment; const projectStore = getProjectStore(flow); return FlowFragment.paste( projectStore, flow, pasteFlowFragment, object ); } }; static paste( projectStore: ProjectStore, flow: Flow, pasteFlowFragment: FlowFragment, object: IEezObject ) { const flowFragment = createObject( projectStore, pasteFlowFragment, FlowFragment, undefined, false ); let closeCombineCommands = false; if (!projectStore.undoManager.combineCommands) { projectStore.undoManager.setCombineCommands(true); closeCombineCommands = true; } let components: EezObject[] = []; if (flowFragment.connectionLines.length > 0) { flowFragment.connectionLines.forEach(connectionLine => projectStore.addObject(flow.connectionLines, connectionLine) ); flowFragment.components.forEach(component => { components.push( projectStore.addObject(flow.components, component) ); }); } else { if ( (object instanceof Widget || object instanceof ProjectEditor.LVGLWidgetClass) && flowFragment.components.every( component => component instanceof Widget ) ) { let containerAncestor: | ContainerWidget | SelectWidget | LVGLWidget | undefined = getAncestorOfType( getParent(getParent(object)), ContainerWidget.classInfo ) as ContainerWidget | undefined; if (!containerAncestor) { containerAncestor = getAncestorOfType( getParent(getParent(object)), SelectWidget.classInfo ) as SelectWidget | undefined; if (!containerAncestor) { containerAncestor = getAncestorOfType( getParent(getParent(object)), ProjectEditor.LVGLWidgetClass.classInfo ) as LVGLWidget | undefined; } } if (containerAncestor) { const parent = containerAncestor instanceof ProjectEditor.LVGLWidgetClass ? containerAncestor.children : containerAncestor.widgets; flowFragment.components.forEach(component => { components.push( projectStore.addObject(parent, component) ); }); } else { flowFragment.components.forEach(component => { components.push( projectStore.addObject(flow.components, component) ); }); } } else { flowFragment.components.forEach(component => { components.push( projectStore.addObject(flow.components, component) ); }); } } if (closeCombineCommands) { projectStore.undoManager.setCombineCommands(false); } return components; } addObjects(flow: Flow, objects: IEezObject[]) { this.components = []; this.connectionLines = []; const projectStore = getProjectStore(flow); const objIDMap = new Set(); function cloneObject( projectStore: ProjectStore, obj: T ) { return createObject( projectStore, toJS(obj), getClass(obj), undefined, false // createNewObjectobjIDs ); } objects.forEach((object: Component) => { if (!(object instanceof Component)) { return; } const clone = cloneObject(projectStore, object) as Component; this.components.push(clone); objIDMap.add(object.objID); for (const object2 of visitObjects(object)) { if (object2 != object && object2 instanceof Component) { objIDMap.add(object2.objID); } } }); flow.connectionLines.forEach(connectionLine => { if ( objIDMap.has(connectionLine.source) && objIDMap.has(connectionLine.target) ) { const clone = cloneObject( projectStore, connectionLine ) as ConnectionLine; this.connectionLines.push(clone); } }); } } registerClass("FlowFragment", FlowFragment);