UNPKG

@selenite/graph-editor

Version:

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

209 lines (208 loc) 9.14 kB
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; };