UNPKG

@selenite/graph-editor

Version:

A graph editor for visual programming, based on rete and svelte.

711 lines (710 loc) 22.9 kB
import { ClassicPreset } from 'rete'; import { Stack } from '../../types/Stack'; import { PythonNodeComponent, R_SocketSelection_NC } from '../components'; import { Socket, ExecSocket, Output, Input, Control, InputControl, assignControl } from '../socket'; import { ErrorWNotif } from '../../global/todo.svelte'; import { structures } from 'rete-structures'; import { getLeavesFromOutput } from './utils'; import { capitalize, Rect, uuidv4 } from '@selenite/commons'; import { SvelteMap } from 'svelte/reactivity'; /** * A map of node classes indexed by their id. */ export const nodeRegistry = new SvelteMap(); /** * Registers a node class. */ export function registerNode(id, type) { // this is the decorator factory, it sets up // the returned decorator function return function (target) { target.id = id; if (type === 'abstract') target.visible = false; if (nodeRegistry.has(id)) { console.warn('Node already registered', id); } console.debug('Registering node', target.id); nodeRegistry.set(target.id, target); }; } /** * Registered converter nodes, from source to target. */ export const converters = new Map(); /** * Registers a converter node. */ export function registerConverter(source, target) { return function (nodeClass) { console.debug(`Registering converter from ${source} to ${target}`); if (!converters.has(source)) converters.set(source, new Map()); converters.get(source).set(target, nodeClass); }; } export function hidden(nodeClass) { nodeClass.visible = false; } function sortedByIndex(entries) { return entries.toSorted((a, b) => (a[1].index ?? 0) - (b[1].index ?? 0)); } /** * Decorator that adds a path to a node. */ export function path(...path) { return function (target) { target.path = path; }; } /** * Decorator that adds tags to a node */ export function tags(...tags) { return function (target) { target.tags = tags; }; } /** * Decorator that adds a description to a node. */ export function description(description) { return function (target) { target.description = description; }; } const r = 'a'; export class Node { pos = $state({ x: 0, y: 0 }); #width = $state(100); #height = $state(50); get rect() { const n = this; return { get x() { return n.pos.x; }, get y() { return n.pos.y; }, get width() { return n.width; }, get height() { return n.height; }, get right() { return n.pos.x + n.width; }, get bottom() { return n.pos.y + n.height; } }; } visible = $state(true); get width() { return this.#width; } get height() { return this.#height; } set height(h) { this.#height = h; this.emitResized(); } set width(w) { this.#width = w; this.emitResized(); } async emitResized() { this.area?.emit({ type: 'noderesized', data: { id: this.id, size: { height: this.height, width: this.width } } }); } // updateConnections() { // console.debug("update conns") // for (const conn of this.getConnections()) { // this.updateElement('connection', conn.id) // } // } get editor() { return this.factory?.getEditor(); } get area() { return this.factory?.getArea(); } get view() { return this.area?.nodeViews.get(this.id); } static description = ''; static visible = true; // static inputTypes?: string[]; // static outputTypes?: string[]; components = []; static activeFactory; inEditor = $derived(this.editor?.hasNode(this) ?? false); outData = {}; resolveEndExecutes = new Stack(); naturalFlowExec = 'exec'; factory = $state(); params = {}; static id; static nodeCounts = 0; state = $state({}); get name() { return this.state.name; } set name(n) { if (n.trim() === '') { this.state.name = undefined; return; } this.state.name = n; } description = $state(); inputs = $state({}); outputs = $state({}); controls = $state({}); needsProcessing = $state(false); sortedInputs = $derived(sortedByIndex(Object.entries(this.inputs))); sortedOutputs = $derived(sortedByIndex(Object.entries(this.outputs))); selectedInputs = $derived(Object.entries(this.inputs).filter(([_, i]) => i?.socket.selected)); selectedOutputs = $derived(Object.entries(this.outputs).filter(([_, o]) => o?.socket.selected)); sortedControls = $derived(sortedByIndex(Object.entries(this.controls))); pythonComponent; socketSelectionComponent; ingoingDataConnections = $state({}); ingoingExecConnections = $state({}); outgoingDataConnections = $state({}); outgoingExecConnections = $state({}); onRemoveIngoingConnection; initializePromise; initialValues; afterInitialize; getFactory() { return this.factory; } getState() { return this.state; } addInput(key, input) { this.inputs[key] = input; } addOutput(key, output) { this.outputs[key] = output; } getConnections() { return [ ...Object.values(this.ingoingDataConnections), ...Object.values(this.ingoingExecConnections), ...Object.values(this.outgoingDataConnections), ...Object.values(this.outgoingExecConnections) ].flat(); } outConnections = $derived({ ...this.outgoingDataConnections, ...this.outgoingExecConnections }); inConnections = $derived({ ...this.ingoingDataConnections, ...this.ingoingExecConnections }); constructor(params = {}) { const { label = '', factory, height = 0, width = 0 } = params; this.#id = params.id ?? uuidv4(); this.label = label; if (params.name) { this.name = params.name; } this.initialValues = params.initialValues === undefined ? undefined : 'inputs' in params.initialValues ? params.initialValues : { inputs: params.initialValues, controls: {} }; this.pythonComponent = this.addComponentByClass(PythonNodeComponent); this.socketSelectionComponent = this.addComponentByClass(R_SocketSelection_NC); if (params.state) { this.state = { ...this.state, ...params.state }; } Node.nodeCounts++; if (params.params && 'factory' in params.params) { delete params.params['factory']; } this.params = params.params || {}; this.description = params.description; this.factory = factory; // if (factory === undefined) { // throw new Error(name + ': Factory is undefined'); // } // format.subscribe((_) => (this.label = _(label))); this.width = width; this.height = height; } label = $state(''); #id; get id() { return this.#id; } get previewed() { return this.factory?.previewedNodes.has(this) ?? false; } set previewed(previewed) { if (previewed) { this.factory?.previewedNodes.clear(); this.factory?.previewedNodes.add(this); this.factory?.runDataflowEngines(); } else { this.factory?.previewedNodes.delete(this); } } get selected() { return this.factory?.selector.isSelected(this) ?? false; } get picked() { return this.factory ? this.factory.selector.picked === this : false; } hasInput(key) { return key in this.inputs; } removeInput(key) { delete this.inputs[key]; if (key in this.inConnections) { for (const conn of this.inConnections[key]) { this.editor?.removeConnection(conn); } } } hasOutput(key) { return key in this.outputs; } removeOutput(key) { delete this.outputs[key]; } hasControl(key) { return key in this.controls; } addControl(key, control) { this.controls[key] = control; } removeControl(key) { throw new Error('Method not implemented.'); } setState(state) { this.state = state; } getOutgoers(key) { if (key in this.outgoingExecConnections) { return this.outgoingExecConnections[key] .map((conn) => this.getEditor().getNode(conn.target)) .filter((n) => n !== undefined); } else if (key in this.outgoingDataConnections) { return this.outgoingDataConnections[key] .map((conn) => this.getEditor().getNode(conn.target)) .filter((n) => n !== undefined); } return null; } addComponentByClass(componentClass, params) { const component = new componentClass({ owner: this, ...params }); this.components.push(component); return component; } getPosition() { return this.getArea()?.nodeViews.get(this.id)?.position; } applyState() { //to be overriden } toJSON() { if (this.constructor.id === undefined) { console.error('Node missing in registry', this); throw new ErrorWNotif(`A node can't be saved as it's missing in the node registry. Node : ${this.label}`); } // console.debug('Saving', this); const inputControlValues = { inputs: {}, controls: {} }; const selectedInputs = []; const selectedOutputs = []; for (const key in this.inputs) { const value = this.getData(key); if (value !== undefined) { inputControlValues.inputs[key] = value; } if (this.inputs[key]?.socket.selected) selectedInputs.push(key); } for (const key in this.outputs) { if (this.outputs[key]?.socket.selected) selectedOutputs.push(key); } for (const key in this.controls) { const control = this.controls[key]; if (!(control instanceof InputControl)) continue; inputControlValues.controls[key] = control.value; } return { id: this.id, type: this.constructor.id, params: this.params, state: $state.snapshot(this.state), position: this.getArea()?.nodeViews.get(this.id)?.position, inputControlValues: $state.snapshot(inputControlValues), selectedInputs, selectedOutputs }; } static async fromJSON(data, { factory }) { const nodeClass = nodeRegistry.get(data.type); if (!nodeClass) { throw new Error(`Node class ${data.type} not found`); } const node = new nodeClass({ ...data.params, factory, initialValues: data.inputControlValues, state: data.state }); node.id = data.id; if (node.initializePromise) { await node.initializePromise; if (node.afterInitialize) node.afterInitialize(); } node.applyState(); for (const key of data.selectedInputs) { node.selectInput(key); } for (const key of data.selectedOutputs) { node.selectOutput(key); } return node; } selectInput(key) { this.inputs[key]?.socket.select(); } deselectInput(key) { this.inputs[key]?.socket.deselect(); } selectOutput(key) { this.outputs[key]?.socket.select(); } deselectOutput(key) { this.outputs[key]?.socket.deselect(); } setNaturalFlow(outExec) { this.naturalFlowExec = outExec; } getNaturalFlow() { return this.naturalFlowExec; } async fetchInputs() { if (!this.factory) { throw new Error("Can't fetch inputs, node factory is undefined"); } try { return (await this.factory.dataflowEngine.fetchInputs(this.id)); } catch (e) { if (e && e.message === 'cancelled') { console.log('gracefully cancelled Node.fetchInputs'); return {}; } else throw e; } } getDataflowEngine() { return this.factory?.dataflowEngine; } getEditor() { return this.factory?.getEditor(); } setFactory(nodeFactory) { this.factory = nodeFactory; } getArea() { return this.factory?.getArea(); } // Callback called at the end of execute onEndExecute() { // if (!this.resolveEndExecutes.isEmpty()) { while (!this.resolveEndExecutes.isEmpty()) { const resolve = this.resolveEndExecutes.pop(); if (resolve) { resolve(); } } } waitForEndExecutePromise() { return new Promise((resolve) => { this.resolveEndExecutes.push(resolve); }); } inputTypes = $derived.by(() => { const res = {}; for (const k of Object.keys(this.inputs)) { const socket = this.inputs[k]?.socket; if (socket) { res[k] = { type: socket.type, datastructure: socket.datastructure }; } } return res; }); outputTypes = $derived.by(() => { const res = {}; for (const k of Object.keys(this.outputs)) { const socket = this.outputs[k]?.socket; if (socket) { res[k] = { type: socket.type, datastructure: socket.datastructure }; } } return res; }); async getWaitForChildrenPromises(output) { const leavesFromLoopExec = getLeavesFromOutput(this, output); const promises = this.getWaitPromises(leavesFromLoopExec); await Promise.all(promises); } execute(input, forward, forwardExec = true) { this.needsProcessing = true; if (forwardExec && this.outputs.exec) { forward('exec'); } this.onEndExecute(); setTimeout(() => { this.needsProcessing = false; }); } addInExec(name = 'exec', displayName = '') { const input = new Input({ index: -1, socket: new ExecSocket({ name: displayName, node: this }), isRequired: true }); this.addInput(name, input); } addOutData(key, params) { if (key in this.outputs) { throw new Error(`Output ${String(key)} already exists`); } const output = new Output({ socket: new Socket({ name: params.label ?? key, datastructure: params.datastructure ?? 'scalar', type: params.type, node: this, displayLabel: params.showLabel }), index: params.index, label: (params.showLabel ?? true) ? (params.label ?? (key !== 'value' && key !== 'result' ? capitalize(key) : undefined)) : undefined, description: params.description }); this.addOutput(key, output); return output.socket; } oldAddOutData({ name = 'data', displayName = '', socketLabel = '', displayLabel = true, isArray = false, type = 'any' }) { const output = new Output({ socket: new Socket({ name: socketLabel, datastructure: isArray ? 'array' : 'scalar', type: type, node: this, displayLabel }), label: displayName }); this.addOutput(name, output); } addInData(key, params) { if (key in this.inputs) { throw new Error(`Input ${String(key)} already exists`); } const input = new Input({ socket: new Socket({ node: this, displayLabel: params?.alwaysShowLabel, datastructure: params?.datastructure, type: params?.type ?? 'any' }), hideLabel: params?.hideLabel, alwaysShowLabel: params?.alwaysShowLabel, index: params?.index, description: params?.description, multipleConnections: params?.datastructure === 'array' || (params?.type?.startsWith('xmlElement') && params.datastructure === 'array'), isRequired: params?.isRequired, label: params?.label ?? (key !== 'value' && key !== 'result' && !key.includes('¤') ? key : undefined) }); this.addInput(key, input); const type = params?.type ?? 'any'; let controlType = params?.options ? 'select' : assignControl(params?.type ?? 'any'); let options = params?.options; if (!options && type.startsWith('geos_')) { const geosSchema = this.factory?.xmlSchemas.get('geos'); const simpleType = geosSchema?.simpleTypeMap.get(type); if (simpleType) { controlType = 'select'; options = simpleType.options; } } if (controlType) { const inputControl = this.makeInputControl({ type: controlType, ...params?.control, props: params?.props, options, socketType: params?.type ?? 'any', datastructure: params?.datastructure ?? 'scalar', initial: this.initialValues?.inputs?.[key] ?? params?.initial, changeType: params?.changeType }); input.addControl(inputControl); } return input; } oldAddInData({ name = 'data', displayName = '', socketLabel = '', control = undefined, isArray = false, isRequired = false, type = 'any', index = undefined }) { return this.addInData(name, { label: socketLabel === '' ? displayName : socketLabel, type, datastructure: isArray ? 'array' : 'scalar', isRequired, index, control }); } makeInputControl(params) { return new InputControl({ ...params, onChange: (v) => { this.processDataflow(); if (params.onChange) { params.onChange(v); } } }); } addInputControl(key, params) { const inputControl = this.makeInputControl({ ...params, datastructure: params.datastructure ?? 'scalar', initial: this.initialValues?.controls[key] ?? params.initial }); this.addControl(key, inputControl); return inputControl; } addOutExec(name = 'exec', displayName = '', isNaturalFlow = false) { if (isNaturalFlow) this.naturalFlowExec = name; const output = new Output({ socket: new ExecSocket({ name: displayName, node: this }), label: displayName }); output.index = -1; this.addOutput(name, output); } processDataflow = () => { if (!this.editor) return; // console.log("cache", Array.from(f.dataflowCache.keys())); // this.needsProcessing = true; try { for (const n of structures(this.editor).successors(this.id).nodes()) { n.needsProcessing = true; } this.factory?.resetDataflow(this); } catch (e) { console.warn('Dataflow processing cancelled', e); } }; getWaitPromises(nodes) { return nodes.map((node) => node.waitForEndExecutePromise()); } async getDataWithInputs(key) { const inputs = await this.fetchInputs(); return this.getData(key, inputs); } getData(key, inputs) { if (inputs && key in inputs) { const isArray = this.inputs[key]?.socket.datastructure === 'array'; const checkedInputs = inputs; // Data is an array because there can be multiple connections const data = checkedInputs[key]; // console.log(checkedInputs); // console.log("get0", checkedInputs[key][0]); if (data.length > 1) { return data.flat(); } const firstData = data[0]; return (isArray && !Array.isArray(firstData) ? [data[0]] : data[0]); } const inputControl = this.inputs[key]?.control; if (inputControl) { return inputControl.value; } return undefined; } // @ts-expect-error data(inputs) { const res = {}; for (const key in this.outputs) { if (!(this.outputs[key]?.socket instanceof ExecSocket)) res[key] = undefined; } return { ...res, ...this.getOutData() }; } setData(key, value) { this.outData[key] = value; // this.getDataflowEngine().reset(this.id); // this.processDataflow(); } getOutData() { return this.outData; } updateElement(type = 'node', id) { return; if (id === undefined) id = this.id; const area = this.getArea(); if (area) { area.update(type, id); } } } export class Connection extends ClassicPreset.Connection { // constructor(source: A, sourceOutput: keyof A['outputs'], target: B, targetInput: keyof B['inputs']) { // super(source, sourceOutput, target, targetInput); // } visible = $derived.by(() => { const source = this.factory?.editor.getNode(this.source); const target = this.factory?.editor.getNode(this.target); if (!source || !target) return true; return source.visible && target.visible; }); get selected() { return this.factory ? this.factory.selector.isSelected(this) : false; } get picked() { return this.factory ? this.factory.selector.isPicked(this) : false; } factory; toJSON() { return { id: this.id, source: this.source, target: this.target, sourceOutput: this.sourceOutput, targetInput: this.targetInput }; } }