From c3e7d61cb485e7bb154c6604b227ab87d56c6604 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 13 Aug 2024 17:30:35 +0200 Subject: [PATCH] replaced handle lookup with ad-hoc way of searching for closest handles --- .../react/src/components/Handle/index.tsx | 2 + packages/system/src/xyhandle/XYHandle.ts | 31 ++-- packages/system/src/xyhandle/types.ts | 2 +- packages/system/src/xyhandle/utils.ts | 136 ++++++++---------- 4 files changed, 75 insertions(+), 96 deletions(-) diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 10f34731..2d3a8738 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -159,6 +159,7 @@ function HandleComponent( isValidConnection: isValidConnectionStore, lib, rfId: flowId, + nodeLookup, } = store.getState(); if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) { @@ -187,6 +188,7 @@ function HandleComponent( flowId, doc, lib, + nodeLookup, }); if (isValid && connection) { diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 0136479d..c7baa728 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -15,7 +15,7 @@ import { type Connection, } from '../types'; -import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils'; +import { getClosestHandle, isConnectionValid, getHandleType, getHandle } from './utils'; import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types'; const alwaysValid = () => true; @@ -61,19 +61,17 @@ function onPointerDown( return; } + const fromHandleInternal = getHandle(nodeId, handleType, handleId, nodeLookup); + if (!fromHandleInternal) { + return; + } + let position = getEventPosition(event, containerBounds); let autoPanStarted = false; let connection: Connection | null = null; let isValid: boolean | null = false; let handleDomNode: Element | null = null; - const [handleLookup, fromHandleInternal] = getHandleLookup({ - nodeLookup, - nodeId, - handleId, - handleType, - }); - // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas function autoPan(): void { if (!autoPanOnConnect || !containerBounds) { @@ -128,7 +126,8 @@ function onPointerDown( closestHandle = getClosestHandle( pointToRendererPoint(position, transform, false, [1, 1]), connectionRadius, - handleLookup + nodeLookup, + fromHandle ); if (!autoPanStarted) { @@ -146,7 +145,7 @@ function onPointerDown( doc, lib, flowId, - handleLookup, + nodeLookup, }); handleDomNode = result.handleDomNode; @@ -233,7 +232,7 @@ function isValidHandle( lib, flowId, isValidConnection = alwaysValid, - handleLookup, + nodeLookup, }: IsValidParams ) { const isTarget = fromType === 'target'; @@ -261,7 +260,7 @@ function isValidHandle( const connectable = handleToCheck.classList.contains('connectable'); const connectableEnd = handleToCheck.classList.contains('connectableend'); - if (!handleNodeId) { + if (!handleNodeId || !handleType) { return result; } @@ -284,13 +283,7 @@ function isValidHandle( result.isValid = isValid && isValidConnection(connection); - const toHandle = handleLookup?.get(`${handleNodeId}-${handleType}-${handleId}`); - - if (toHandle) { - result.toHandle = { - ...toHandle, - }; - } + result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, false); } return result; diff --git a/packages/system/src/xyhandle/types.ts b/packages/system/src/xyhandle/types.ts index ac1b6e23..799f68c3 100644 --- a/packages/system/src/xyhandle/types.ts +++ b/packages/system/src/xyhandle/types.ts @@ -48,7 +48,7 @@ export type IsValidParams = { doc: Document | ShadowRoot; lib: string; flowId: string | null; - handleLookup?: Map; + nodeLookup: NodeLookup; }; export type XYHandleInstance = { diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index 04099bcf..285d8fc3 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -1,52 +1,62 @@ -import { getHandlePosition } from '../utils'; -import { - type HandleType, - type NodeHandleBounds, - type XYPosition, - type Handle, - InternalNodeBase, - NodeLookup, -} from '../types'; +import { getHandlePosition, getOverlappingArea, nodeToRect } from '../utils'; +import type { HandleType, XYPosition, Handle, InternalNodeBase, NodeLookup } from '../types'; -// this functions collects all handles and adds an absolute position -// so that we can later find the closest handle to the mouse position -function getHandles( - node: InternalNodeBase, - handleBounds: NodeHandleBounds, - type: HandleType, - currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType } -): [Handle[], Handle | null] { - let excludedHandle = null; - const handles = (handleBounds[type] || []).reduce((res, handle) => { - if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) { - excludedHandle = handle; - } else { - const handleXY = getHandlePosition(node, handle, handle.position, true); - res.push({ ...handle, ...handleXY }); +function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, distance: number): InternalNodeBase[] { + const nodes: InternalNodeBase[] = []; + + for (const node of nodeLookup.values()) { + const rect = { + x: position.x - distance, + y: position.y - distance, + width: distance * 2, + height: distance * 2, + }; + + const overlappingArea = getOverlappingArea(rect, nodeToRect(node)); + + if (overlappingArea > 0) { + nodes.push(node); } - return res; - }, []); - return [handles, excludedHandle]; + } + + return nodes; } +const ADDITIONAL_DISTANCE = 250; export function getClosestHandle( - pos: XYPosition, + position: XYPosition, connectionRadius: number, - handleLookup: Map + nodeLookup: NodeLookup, + fromHandle: { nodeId: string; type: HandleType; id?: string | null } ): Handle | null { let closestHandles: Handle[] = []; let minDistance = Infinity; - for (const handle of handleLookup.values()) { - const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2)); - if (distance <= connectionRadius) { + const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE); + for (const node of closeNodes) { + const allHandles = [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])]; + + for (const handle of allHandles) { + // if the handle is the same as the fromHandle we skip it + if (fromHandle.nodeId === handle.nodeId && fromHandle.type === handle.type && fromHandle.id === handle.id) { + continue; + } + + // determine absolute position of the handle + const { x, y } = getHandlePosition(node, handle, handle.position, true); + + const distance = Math.sqrt(Math.pow(x - position.x, 2) + Math.pow(y - position.y, 2)); + if (distance > connectionRadius) { + continue; + } + if (distance < minDistance) { - closestHandles = [handle]; + closestHandles = [{ ...handle, x, y }]; + minDistance = distance; } else if (distance === minDistance) { // when multiple handles are on the same distance we collect all of them - closestHandles.push(handle); + closestHandles.push({ ...handle, x, y }); } - minDistance = distance; } } @@ -60,50 +70,24 @@ export function getClosestHandle( closestHandles.find((handle) => handle.type === 'target') || closestHandles[0]; } -type GetHandleLookupParams = { - nodeLookup: NodeLookup; - nodeId: string; - handleId: string | null; - handleType: HandleType; -}; - -export function getHandleLookup({ - nodeLookup, - nodeId, - handleId, - handleType, -}: GetHandleLookupParams): [Map, Handle] { - const connectionHandles: Map = new Map(); - const currentHandle = { nodeId, handleId, handleType }; - let matchingHandle: Handle | null = null; - - for (const node of nodeLookup.values()) { - if (node.internals.handleBounds) { - const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle); - const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle); - - matchingHandle = matchingHandle ? matchingHandle : excludedSource ?? excludedTarget; - - [...sourceHandles, ...targetHandles].forEach((handle) => - connectionHandles.set(`${handle.nodeId}-${handle.type}-${handle.id}`, handle) - ); - } +export function getHandle( + nodeId: string, + handleType: HandleType, + handleId: string | null, + nodeLookup: NodeLookup, + withAbsolutePosition = false +): Handle | null { + const node = nodeLookup.get(nodeId); + if (!node) { + return null; } - // if the user only works with handles that are type="source" + connectionMode="loose" - // it happens that we can't find a matching handle. The reason for this is, that the - // edge don't know about the handles and always assumes that there is source and a target. - // In this case we need to find the matching handle by switching the handleType - if (!matchingHandle) { - const node = nodeLookup.get(nodeId); - if (node?.internals.handleBounds) { - currentHandle.handleType = handleType === 'source' ? 'target' : 'source'; - const [, excluded] = getHandles(node, node.internals.handleBounds, currentHandle.handleType, currentHandle); - matchingHandle = excluded; - } - } + const handles = node.internals.handleBounds?.[handleType]; + const handle = (handleId ? handles?.find((h) => h.id === handleId) : handles?.[0]) ?? null; - return [connectionHandles, matchingHandle!]; + return handle && withAbsolutePosition + ? { ...handle, ...getHandlePosition(node, handle, handle.position, true) } + : handle; } export function getHandleType(