@selenite/graph-editor
Version:
A graph editor for visual programming, based on rete and svelte.
711 lines (710 loc) • 22.9 kB
JavaScript
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
};
}
}