@selenite/graph-editor
Version:
A graph editor for visual programming, based on rete and svelte.
209 lines (208 loc) • 9.14 kB
JavaScript
import { SetupClass } from './Setup';
import { BidirectFlow, ClassicFlow, ConnectionPlugin as BaseConnectionPlugin, Presets } from 'rete-connection-plugin';
import { isConnectionInvalid } from '../plugins/typed-sockets';
import { findSocket } from '../socket/utils';
import { XmlNode } from '../nodes/XML';
import { isEqual } from 'lodash-es';
export class ConnectionPlugin extends BaseConnectionPlugin {
factory;
area;
picked = false;
lastClickedSocket = false;
lastPickedSockedData;
/** Last picked connection. */
lastConn;
constructor(factory, area) {
super();
this.factory = factory;
this.area = area;
}
setParent(scope) {
// super.setParent(scope);
// @ts-expect-error: Access private field
this.areaPlugin = this.area;
// @ts-expect-error: Access private field
this.editor = this.factory.editor;
const pointerdownSocket = (e) => {
this.pick(e, 'down');
};
// eslint-disable-next-line max-statements
this.addPipe((context) => {
if (!context || typeof context !== 'object' || !('type' in context))
return context;
if (context.type === 'pointermove') {
this.update();
}
else if (context.type === 'pointerup') {
this.pick(context.data.event, 'up');
}
else if (context.type === 'render') {
if (context.data.type === 'socket') {
const { element } = context.data;
element.addEventListener('pointerdown', pointerdownSocket);
// @ts-expect-error: Access private field
this.socketsCache.set(element, context.data);
}
}
else if (context.type === 'unmount') {
const { element } = context.data;
element.removeEventListener('pointerdown', pointerdownSocket);
// @ts-expect-error: Access private field
this.socketsCache.delete(element);
}
return context;
});
}
/**
* Handles pointer down and up events to control interactive connection creation.
* @param event
* @param type
*/
async pick(event, type) {
const pointedElements = document.elementsFromPoint(event.clientX, event.clientY);
// @ts-expect-error: Access private field
const socketData = findSocket(this.socketsCache, pointedElements);
if (event.button == 0) {
if (type === 'down') {
if (socketData === undefined)
return;
console.debug('Pick connection');
this.lastPickedSockedData = socketData;
this.picked = true;
const node = this.factory.editor.getNode(socketData.nodeId);
if (node) {
this.lastConn =
socketData.side === 'output'
? node.outConnections[socketData.key]?.at(0)
: node.inConnections[socketData.key]?.at(0);
}
}
if (type === 'up' && this.picked && this.lastPickedSockedData) {
this.picked = false;
if (!socketData) {
this.emit({
type: 'connectiondrop',
data: {
created: false,
initial: this.lastPickedSockedData,
socket: socketData ?? null,
event
}
});
return;
}
// Fix for the case where the user drops the connection on the same socket
// it was picked from
else {
if (this.lastConn && isEqual(this.lastPickedSockedData, socketData)) {
await this.factory.editor.addConnection(this.lastConn);
this.drop();
return;
}
}
}
}
// select socket on right click
if (event.button == 2) {
if (type === 'up')
return;
if (!socketData)
return;
// pickedSocket.selected = !pickedSocket.selected;
// @ts-expect-error: Access private field
const node = this.editor.getNode(socketData.nodeId);
const socket = (socketData.side === 'input' ? node.inputs[socketData.key] : node.outputs[socketData.key])?.socket;
if (socket === undefined)
throw new Error(`Socket not found for node ${node.id} and key ${socketData.key}`);
this.lastClickedSocket = true;
event.preventDefault();
event.stopPropagation();
socket.toggleSelection();
node.updateElement();
return;
}
await super.pick(event, type);
}
}
export class ConnectionSetup extends SetupClass {
setup(editor, area, factory) {
setupConnections({ editor, area, factory });
}
}
export const setupConnections = (params) => {
console.log('Setting up connection plugin');
const { factory, editor, area } = params;
if (!area) {
console.warn("Area plugin is not defined, can't setup connections plugin.");
return params;
}
const connectionPlugin = new ConnectionPlugin(factory, area);
Presets.classic.setup();
// @ts-expect-error: Ignore type error
connectionPlugin.addPreset((socketData) => {
// console.log("connectionPlugin", socketData)
const params = {
makeConnection(from, to, context) {
const forward = from.side === 'output' && to.side === 'input';
const backward = from.side === 'input' && to.side === 'output';
const [source, target] = forward ? [from, to] : backward ? [to, from] : [];
if (!source || !target)
return false;
const sourceNode = editor.getNode(source.nodeId);
const targetNode = editor.getNode(target.nodeId);
if (!sourceNode || !targetNode) {
console.warn("Can't find source or target node in makeConnection function");
return false;
}
editor.addNewConnection(sourceNode, source.key, targetNode, target.key);
return true;
},
// @ts-expect-error: Ignore payload missing in parent method types
canMakeConnection(from, to) {
connectionPlugin.drop();
const forward = from.side === 'output' && to.side === 'input';
const backward = from.side === 'input' && to.side === 'output';
const [source, target] = forward ? [from, to] : backward ? [to, from] : [];
if (!source || !target)
return false;
const sourceNode = editor.getNode(source.nodeId);
const targetNode = editor.getNode(target.nodeId);
if (!sourceNode || !targetNode) {
console.warn("Can't find source or target node in canMakeConnection function");
return false;
}
const conns = source.key in sourceNode.outgoingDataConnections
? sourceNode.outgoingDataConnections[source.key]
: source.key in sourceNode.outgoingExecConnections
? sourceNode.outgoingExecConnections[source.key]
: undefined;
if (conns) {
if (conns.some((conn) => conn.target === target.nodeId && conn.targetInput === target.key)) {
console.log('Connection already exists');
return false;
}
}
// this function checks if the old connection should be removed
if (isConnectionInvalid(from.payload, to.payload)) {
console.log(`Connection between ${from.nodeId} and ${to.nodeId} is not allowed. From socket type is ${from.payload.type} and to socket type is ${to.payload.type}`);
factory.notifications.show({
title: 'Erreur',
message: `Connection invalide entre types "${from.payload.type}" et "${to.payload.type}" !`,
color: 'red'
});
return false;
}
else
return true;
}
};
return new (socketData.payload.datastructure === 'array'
// socketData.payload.node instanceof XmlNode &&
// socketData.key === 'children'
? BidirectFlow
: ClassicFlow)(params);
});
area.use(connectionPlugin);
factory.connectionPlugin = connectionPlugin;
return params;
};