From ab40307fc9250878b1df8fde61f47dc226030575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20J=C3=BCrisoo?= Date: Wed, 29 Jan 2025 10:32:37 +0100 Subject: [PATCH 1/8] Reconnect after middle node delete --- .../components/DataStory/DataStoryCanvas.tsx | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index d8e52a4e..b84834e0 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -213,7 +213,36 @@ const Flow = ({ onEdgesChange(changes); if (onChange) onChange(toDiagram()) }} - onNodesDelete={() => { + onNodesDelete={(nodesToDelete) => { + console.log('onNodesDelete', nodesToDelete); + + nodesToDelete.forEach(node => { + const store = reactFlowStore.getState(); + const { edges } = store; + + // Find all incoming and outgoing edges for this node + const incomingEdges = edges.filter(e => e.target === node.id); + const outgoingEdges = edges.filter(e => e.source === node.id); + + console.log({ + incomingEdges, + outgoingEdges, + }); + + // For each incoming edge, connect it to all outgoing edges + incomingEdges.forEach(inEdge => { + outgoingEdges.forEach(outEdge => { + // Create a connection that will be handled by the store's connect method + connect({ + source: inEdge.source, + sourceHandle: inEdge.sourceHandle ?? null, + target: outEdge.target, + targetHandle: outEdge.targetHandle ?? null, + }); + }); + }); + }); + // focus on the diagram after node deletion to enhance hotkey usage focusOnFlow(); }} From c61a5cc0faa5b07dbfa98e0346ed91b0cdcfb177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20J=C3=BCrisoo?= Date: Wed, 29 Jan 2025 11:48:00 +0100 Subject: [PATCH 2/8] Draggable working --- packages/core/src/Diagram.ts | 6 ++ .../components/DataStory/DataStoryCanvas.tsx | 80 ++++++++++++++++++- .../DataStory/modals/addNodeForm.tsx | 5 ++ .../src/components/DataStory/store/store.tsx | 7 ++ packages/ui/src/components/DataStory/types.ts | 1 + 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Diagram.ts b/packages/core/src/Diagram.ts index c2ca6321..5a1a4a41 100644 --- a/packages/core/src/Diagram.ts +++ b/packages/core/src/Diagram.ts @@ -47,6 +47,12 @@ export class Diagram { return this } + disconnect(linkId: LinkId) { + this.links = this.links.filter(l => l.id !== linkId) + + return this + } + linksAtInputPortId(id: PortId | undefined): Link[] { return this.links.filter(link => link.targetPortId === id) } diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index b84834e0..14ed8cf3 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -3,6 +3,7 @@ import React, { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useS import { Background, BackgroundVariant, + Edge, EdgeChange, NodeChange, ReactFlow, @@ -67,16 +68,19 @@ const Flow = ({ onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, connect: state.connect, + disconnect: state.disconnect, onInit: state.onInit, onRun: state.onRun, addNodeFromDescription: state.addNodeFromDescription, toDiagram: state.toDiagram, updateEdgeCounts: state.updateEdgeCounts, updateEdgeStatus: state.updateEdgeStatus, + setEdges: state.setEdges, }); const { connect, + disconnect, nodes, edges, onNodesChange, @@ -87,6 +91,7 @@ const Flow = ({ toDiagram, updateEdgeCounts, updateEdgeStatus, + setEdges, } = useStore(selector, shallow); const id = useId() @@ -179,6 +184,52 @@ const Flow = ({ useEscapeKey(() => setSidebarKey!(''), flowRef); + const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); + + const onNodeDrag = useCallback((event: any, node: any) => { + console.log('onNodeDrag...') + // Find any edges that the node is being dragged over + const edgeElements = document.elementsFromPoint(event.clientX, event.clientY) + .filter(el => el.classList.contains('react-flow__edge') || + el.classList.contains('react-flow__edge-path') || + el.classList.contains('react-flow__edge-text')); + + if(!edgeElements.length) return; + // Get the closest edge element + const edgeElement = edgeElements[0].closest('.react-flow__edge'); + if (edgeElement) { + const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); + const { edges } = reactFlowStore.getState(); + const edge = edges.find(e => e.id === edgeId); + if (edge) { + setDraggedNode({ node, droppedOnEdge: edge }); + return; + } + } + + setDraggedNode(null); + }, []); + + const onNodeDragStop = useCallback((event: any, node: any) => { + if(!draggedNode?.droppedOnEdge) return; + const { droppedOnEdge } = draggedNode; + + connect({ + source: droppedOnEdge.source, + target: node.id, + sourceHandle: `${droppedOnEdge.source}.output`, + targetHandle: `${node.id}.input`, + }); + connect({ + source: node.id, + target: droppedOnEdge.target, + sourceHandle: `${node.id}.output`, + targetHandle: `${droppedOnEdge.target}.input`, + }); + disconnect(droppedOnEdge.id) + setDraggedNode(null); + }, [draggedNode, connect, setEdges]); + return ( <> { event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; + // Allow dropping on edges + const target = event.target as HTMLElement; + const isEdge = target.closest('.react-flow__edge'); + const hasNodeType = event.dataTransfer.types.includes('application/reactflow'); + + if (isEdge && hasNodeType) { + event.dataTransfer.dropEffect = 'copy'; + } else { + event.dataTransfer.dropEffect = 'move'; + } }, [])} onDrop={ useCallback((event) => { diff --git a/packages/ui/src/components/DataStory/modals/addNodeForm.tsx b/packages/ui/src/components/DataStory/modals/addNodeForm.tsx index 5ca11051..1164e765 100644 --- a/packages/ui/src/components/DataStory/modals/addNodeForm.tsx +++ b/packages/ui/src/components/DataStory/modals/addNodeForm.tsx @@ -84,6 +84,11 @@ export const AddNodeFormContent = (props: AddNodeModalContentProps) => { )} key={nodeDescription.name} onClick={() => doAddNode(nodeDescription)} + draggable="true" + onDragStart={(event) => { + event.dataTransfer.setData('application/reactflow', nodeDescription.name); + event.dataTransfer.effectAllowed = 'move'; + }} >
{nodeDescription.category || 'Core'}:: diff --git a/packages/ui/src/components/DataStory/store/store.tsx b/packages/ui/src/components/DataStory/store/store.tsx index d22df91b..6443b7b1 100644 --- a/packages/ui/src/components/DataStory/store/store.tsx +++ b/packages/ui/src/components/DataStory/store/store.tsx @@ -66,6 +66,13 @@ export const createStore = () => createWithEqualityFn((set, get) => // Update the diagram get().updateDiagram(diagram) }, + disconnect: (linkId: string) => { + const diagram = get().toDiagram() + + diagram.disconnect(linkId) + + get().updateDiagram(diagram) + }, addNode: (node: ReactFlowNode) => { set({ nodes: [ diff --git a/packages/ui/src/components/DataStory/types.ts b/packages/ui/src/components/DataStory/types.ts index 791c7407..b137f7a6 100644 --- a/packages/ui/src/components/DataStory/types.ts +++ b/packages/ui/src/components/DataStory/types.ts @@ -113,6 +113,7 @@ export type StoreSchema = { updateEdgeStatus: (edgeStatus: { nodeId: NodeId, status: NodeStatus }[]) => void setEdges: (edges: Edge[]) => void; connect: OnConnect; + disconnect: (linkId: string) => void; /** Global Params */ params: Param[], From a539f4f38df3ad9896614ddf88c47c1b3d65f946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20J=C3=BCrisoo?= Date: Wed, 29 Jan 2025 12:23:25 +0100 Subject: [PATCH 3/8] Drag onto link --- .../ui/src/components/DataStory/DataStoryCanvas.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index 14ed8cf3..1263cf51 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -187,16 +187,24 @@ const Flow = ({ const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); const onNodeDrag = useCallback((event: any, node: any) => { - console.log('onNodeDrag...') // Find any edges that the node is being dragged over const edgeElements = document.elementsFromPoint(event.clientX, event.clientY) .filter(el => el.classList.contains('react-flow__edge') || el.classList.contains('react-flow__edge-path') || el.classList.contains('react-flow__edge-text')); - if(!edgeElements.length) return; + if(!edgeElements.length) { + setDraggedNode(null); + return; + } // Get the closest edge element const edgeElement = edgeElements[0].closest('.react-flow__edge'); + const distance = Math.sqrt( + Math.pow(edgeElements[0].getBoundingClientRect().x, 2) + + Math.pow(edgeElements[0].getBoundingClientRect().y, 2), + ) + + console.log({ distance }); if (edgeElement) { const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); const { edges } = reactFlowStore.getState(); From 10508921a9cb345676da6a4552a7703e10f3aeac Mon Sep 17 00:00:00 2001 From: stone Date: Sun, 2 Feb 2025 18:53:26 +0800 Subject: [PATCH 4/8] feat: extract the useDragNode hook --- .../components/DataStory/DataStoryCanvas.tsx | 56 +------------ .../src/components/DataStory/useDragNode.tsx | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 packages/ui/src/components/DataStory/useDragNode.tsx diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index 1263cf51..1c068c9b 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -26,6 +26,7 @@ import { useEscapeKey } from './hooks/useEscapeKey'; import { keyManager } from './keyManager'; import { getNodesWithNewSelection } from './getNodesWithNewSelection'; import { createDataStoryId, LinkCount, LinkId, NodeStatus, RequestObserverType } from '@data-story/core'; +import { useDragNode } from './useDragNode'; const nodeTypes = { commentNodeComponent: CommentNodeComponent, @@ -183,60 +184,7 @@ const Flow = ({ }); useEscapeKey(() => setSidebarKey!(''), flowRef); - - const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); - - const onNodeDrag = useCallback((event: any, node: any) => { - // Find any edges that the node is being dragged over - const edgeElements = document.elementsFromPoint(event.clientX, event.clientY) - .filter(el => el.classList.contains('react-flow__edge') || - el.classList.contains('react-flow__edge-path') || - el.classList.contains('react-flow__edge-text')); - - if(!edgeElements.length) { - setDraggedNode(null); - return; - } - // Get the closest edge element - const edgeElement = edgeElements[0].closest('.react-flow__edge'); - const distance = Math.sqrt( - Math.pow(edgeElements[0].getBoundingClientRect().x, 2) + - Math.pow(edgeElements[0].getBoundingClientRect().y, 2), - ) - - console.log({ distance }); - if (edgeElement) { - const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); - const { edges } = reactFlowStore.getState(); - const edge = edges.find(e => e.id === edgeId); - if (edge) { - setDraggedNode({ node, droppedOnEdge: edge }); - return; - } - } - - setDraggedNode(null); - }, []); - - const onNodeDragStop = useCallback((event: any, node: any) => { - if(!draggedNode?.droppedOnEdge) return; - const { droppedOnEdge } = draggedNode; - - connect({ - source: droppedOnEdge.source, - target: node.id, - sourceHandle: `${droppedOnEdge.source}.output`, - targetHandle: `${node.id}.input`, - }); - connect({ - source: node.id, - target: droppedOnEdge.target, - sourceHandle: `${node.id}.output`, - targetHandle: `${droppedOnEdge.target}.input`, - }); - disconnect(droppedOnEdge.id) - setDraggedNode(null); - }, [draggedNode, connect, setEdges]); + const { draggedNode, onNodeDragStop, onNodeDrag } = useDragNode(); return ( <> diff --git a/packages/ui/src/components/DataStory/useDragNode.tsx b/packages/ui/src/components/DataStory/useDragNode.tsx new file mode 100644 index 00000000..47d22e3c --- /dev/null +++ b/packages/ui/src/components/DataStory/useDragNode.tsx @@ -0,0 +1,79 @@ +import { useCallback, useState } from 'react'; +import { useStoreApi } from '@xyflow/react'; +import { useStore } from './store/store'; +import { shallow } from 'zustand/shallow'; +import { StoreSchema } from './types'; + +export function useDragNode() { + const reactFlowStore = useStoreApi(); + const selector = (state: StoreSchema) => ({ + connect: state.connect, + disconnect: state.disconnect, + setEdges: state.setEdges, + }); + const { + connect, + disconnect, + setEdges, + } = useStore(selector, shallow); + + const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); + + const onNodeDrag = useCallback((event: any, node: any) => { + // Find any edges that the node is being dragged over + const edgeElements = document.elementsFromPoint(event.clientX, event.clientY) + .filter(el => el.classList.contains('react-flow__edge') || + el.classList.contains('react-flow__edge-path') || + el.classList.contains('react-flow__edge-text')); + + if (!edgeElements.length) { + setDraggedNode(null); + return; + } + // Get the closest edge element + const edgeElement = edgeElements[0].closest('.react-flow__edge'); + const distance = Math.sqrt( + Math.pow(edgeElements[0].getBoundingClientRect().x, 2) + + Math.pow(edgeElements[0].getBoundingClientRect().y, 2), + ) + + console.log({ distance }); + if (edgeElement) { + const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); + const { edges } = reactFlowStore.getState(); + const edge = edges.find(e => e.id === edgeId); + if (edge) { + setDraggedNode({ node, droppedOnEdge: edge }); + return; + } + } + + setDraggedNode(null); + }, []); + + const onNodeDragStop = useCallback((event: any, node: any) => { + if (!draggedNode?.droppedOnEdge) return; + const { droppedOnEdge } = draggedNode; + + connect({ + source: droppedOnEdge.source, + target: node.id, + sourceHandle: `${droppedOnEdge.source}.output`, + targetHandle: `${node.id}.input`, + }); + connect({ + source: node.id, + target: droppedOnEdge.target, + sourceHandle: `${node.id}.output`, + targetHandle: `${droppedOnEdge.target}.input`, + }); + disconnect(droppedOnEdge.id) + setDraggedNode(null); + }, [draggedNode, connect, setEdges]); + + return { + onNodeDrag, + onNodeDragStop, + draggedNode, + } +} \ No newline at end of file From de3b695491060a196df4799ca52a7a277602cb62 Mon Sep 17 00:00:00 2001 From: stone Date: Sun, 2 Feb 2025 21:24:35 +0800 Subject: [PATCH 5/8] feat:ensure node works with port names other than input/output --- .../src/components/DataStory/useDragNode.tsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/DataStory/useDragNode.tsx b/packages/ui/src/components/DataStory/useDragNode.tsx index 47d22e3c..7bbaef18 100644 --- a/packages/ui/src/components/DataStory/useDragNode.tsx +++ b/packages/ui/src/components/DataStory/useDragNode.tsx @@ -3,6 +3,14 @@ import { useStoreApi } from '@xyflow/react'; import { useStore } from './store/store'; import { shallow } from 'zustand/shallow'; import { StoreSchema } from './types'; +import { ReactFlowNode } from '../Node/ReactFlowNode'; + +/** + * todo: + * 1. 添加如 clone node, output input port 不为标准的端口名的支持 -get + * 2. can't disconnect successfully + * 3. BNode can't 顺滑的添加到 edge 上 + */ export function useDragNode() { const reactFlowStore = useStoreApi(); @@ -51,22 +59,25 @@ export function useDragNode() { setDraggedNode(null); }, []); - const onNodeDragStop = useCallback((event: any, node: any) => { + const onNodeDragStop = useCallback((event: any, node: ReactFlowNode, nodes: ReactFlowNode[]) => { if (!draggedNode?.droppedOnEdge) return; - const { droppedOnEdge } = draggedNode; + const { node: node1, droppedOnEdge } = draggedNode; + const nodeOutputId = node.data.outputs[0].id; + const nodeInputId = node.data.inputs[0].id; connect({ source: droppedOnEdge.source, target: node.id, - sourceHandle: `${droppedOnEdge.source}.output`, - targetHandle: `${node.id}.input`, + sourceHandle: droppedOnEdge.sourceHandle, + targetHandle: nodeInputId, }); connect({ source: node.id, target: droppedOnEdge.target, - sourceHandle: `${node.id}.output`, - targetHandle: `${droppedOnEdge.target}.input`, + sourceHandle: nodeOutputId, + targetHandle: droppedOnEdge.targetHandle, }); + disconnect(droppedOnEdge.id) setDraggedNode(null); }, [draggedNode, connect, setEdges]); From 508a6e69ed5cdce086a6cce3722240030e75d370 Mon Sep 17 00:00:00 2001 From: stone Date: Mon, 3 Feb 2025 12:05:33 +0800 Subject: [PATCH 6/8] fix: resolve smooth addition of a node to the edge between two nodes --- .../components/DataStory/DataStoryCanvas.tsx | 17 ++- .../src/components/DataStory/useDragNode.tsx | 112 ++++++++++-------- 2 files changed, 75 insertions(+), 54 deletions(-) diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index 1c068c9b..649c24cb 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -184,7 +184,12 @@ const Flow = ({ }); useEscapeKey(() => setSidebarKey!(''), flowRef); - const { draggedNode, onNodeDragStop, onNodeDrag } = useDragNode(); + const { draggedNode, onNodeDragStop, onNodeDrag } = useDragNode({ + connect, + disconnect, + setEdges, + edges, + }); return ( <> @@ -237,7 +242,7 @@ const Flow = ({ if (onChange) onChange(toDiagram()) }} onNodesDelete={(nodesToDelete) => { - console.log('onNodesDelete', nodesToDelete); + // console.log('onNodesDelete', nodesToDelete); nodesToDelete.forEach(node => { const store = reactFlowStore.getState(); @@ -247,10 +252,10 @@ const Flow = ({ const incomingEdges = edges.filter(e => e.target === node.id); const outgoingEdges = edges.filter(e => e.source === node.id); - console.log({ - incomingEdges, - outgoingEdges, - }); + // console.log({ + // incomingEdges, + // outgoingEdges, + // }); // For each incoming edge, connect it to all outgoing edges incomingEdges.forEach(inEdge => { diff --git a/packages/ui/src/components/DataStory/useDragNode.tsx b/packages/ui/src/components/DataStory/useDragNode.tsx index 7bbaef18..d817adf8 100644 --- a/packages/ui/src/components/DataStory/useDragNode.tsx +++ b/packages/ui/src/components/DataStory/useDragNode.tsx @@ -1,63 +1,79 @@ -import { useCallback, useState } from 'react'; -import { useStoreApi } from '@xyflow/react'; -import { useStore } from './store/store'; -import { shallow } from 'zustand/shallow'; +import { type MouseEvent as ReactMouseEvent, useCallback, useState } from 'react'; +import { Edge } from '@xyflow/react'; import { StoreSchema } from './types'; import { ReactFlowNode } from '../Node/ReactFlowNode'; -/** - * todo: - * 1. 添加如 clone node, output input port 不为标准的端口名的支持 -get - * 2. can't disconnect successfully - * 3. BNode can't 顺滑的添加到 edge 上 - */ - -export function useDragNode() { - const reactFlowStore = useStoreApi(); - const selector = (state: StoreSchema) => ({ - connect: state.connect, - disconnect: state.disconnect, - setEdges: state.setEdges, - }); - const { - connect, - disconnect, - setEdges, - } = useStore(selector, shallow); +interface IntersectionResult { + isIntersecting: boolean; + edge?: Edge; + edgeElement?: SVGPathElement; +} +export function useDragNode({ + connect, + disconnect, + setEdges, + edges, +}: { + connect: StoreSchema['connect']; + disconnect: StoreSchema['disconnect']; + setEdges: StoreSchema['setEdges']; + edges: StoreSchema['edges']; +}) { const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); - const onNodeDrag = useCallback((event: any, node: any) => { - // Find any edges that the node is being dragged over - const edgeElements = document.elementsFromPoint(event.clientX, event.clientY) - .filter(el => el.classList.contains('react-flow__edge') || - el.classList.contains('react-flow__edge-path') || - el.classList.contains('react-flow__edge-text')); + const checkNodeEdgeIntersection = useCallback(( + dragNodeRect: DOMRect, + threshold: number = 0, + ): IntersectionResult => { + // 遍历所有边,检查是否有相交 + for (const edge of edges) { + // 获取边的 DOM 元素 + const edgeElement = document.querySelector( + `[data-id="${edge.id}"]`, + ) as SVGPathElement; - if (!edgeElements.length) { - setDraggedNode(null); - return; + console.log(edgeElement, 'edgeElement'); + if (!edgeElement) continue; + + // 获取边的边界矩形 + const edgeRect = edgeElement.getBoundingClientRect(); + + // 检查矩形是否相交 + const isIntersecting = !( + dragNodeRect.right < edgeRect.left - threshold || + dragNodeRect.left > edgeRect.right + threshold || + dragNodeRect.bottom < edgeRect.top - threshold || + dragNodeRect.top > edgeRect.bottom + threshold + ); + + if (isIntersecting) { + return { + isIntersecting: true, + edge, + edgeElement: edgeElement, + }; + } } - // Get the closest edge element - const edgeElement = edgeElements[0].closest('.react-flow__edge'); - const distance = Math.sqrt( - Math.pow(edgeElements[0].getBoundingClientRect().x, 2) + - Math.pow(edgeElements[0].getBoundingClientRect().y, 2), - ) - - console.log({ distance }); - if (edgeElement) { + + return { isIntersecting: false }; + }, [edges]); + + const onNodeDrag = useCallback((event: ReactMouseEvent, node: ReactFlowNode) => { + // @ts-ignore + const nodeRect = event.target!.getBoundingClientRect() as unknown as DOMRect; + + const { edgeElement, edge, isIntersecting } = checkNodeEdgeIntersection(nodeRect); + + if (edgeElement && edge) { const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); - const { edges } = reactFlowStore.getState(); - const edge = edges.find(e => e.id === edgeId); - if (edge) { - setDraggedNode({ node, droppedOnEdge: edge }); - return; - } + console.log(edgeId, 'edgeId'); + setDraggedNode({ node, droppedOnEdge: edge }); + return; } setDraggedNode(null); - }, []); + }, [checkNodeEdgeIntersection]); const onNodeDragStop = useCallback((event: any, node: ReactFlowNode, nodes: ReactFlowNode[]) => { if (!draggedNode?.droppedOnEdge) return; From bd6caeb809bb57ec9eef6a22f245cd8a2d7a5db6 Mon Sep 17 00:00:00 2001 From: stone Date: Mon, 3 Feb 2025 18:56:16 +0800 Subject: [PATCH 7/8] feat: optimize algorithm for adding a node to the edge between two nodes --- .../src/components/DataStory/useDragNode.tsx | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/DataStory/useDragNode.tsx b/packages/ui/src/components/DataStory/useDragNode.tsx index d817adf8..6bbc13da 100644 --- a/packages/ui/src/components/DataStory/useDragNode.tsx +++ b/packages/ui/src/components/DataStory/useDragNode.tsx @@ -9,6 +9,40 @@ interface IntersectionResult { edgeElement?: SVGPathElement; } +function isIntersecting( + edgeRect: DOMRect, + nodeRect: DOMRect, + threshold: number = 0.33, +): boolean { + // 首先检查是否有重叠 + if ( + edgeRect.left > nodeRect.right || + edgeRect.right < nodeRect.left || + edgeRect.top > nodeRect.bottom || + edgeRect.bottom < nodeRect.top + ) { + return false; + } + + // 计算重叠区域 + const overlapLeft = Math.max(edgeRect.left, nodeRect.left); + const overlapRight = Math.min(edgeRect.right, nodeRect.right); + const overlapTop = Math.max(edgeRect.top, nodeRect.top); + const overlapBottom = Math.min(edgeRect.bottom, nodeRect.bottom); + + // 计算重叠面积 + const overlapArea = + (overlapRight - overlapLeft) * (overlapBottom - overlapTop); + + // 计算节点总面积 + const nodeArea = nodeRect.width * nodeRect.height; + + // 计算重叠比例 + const overlapRatio = overlapArea / nodeArea; + + return overlapRatio > threshold; +} + export function useDragNode({ connect, disconnect, @@ -24,30 +58,21 @@ export function useDragNode({ const checkNodeEdgeIntersection = useCallback(( dragNodeRect: DOMRect, - threshold: number = 0, ): IntersectionResult => { // 遍历所有边,检查是否有相交 for (const edge of edges) { - // 获取边的 DOM 元素 const edgeElement = document.querySelector( `[data-id="${edge.id}"]`, ) as SVGPathElement; - console.log(edgeElement, 'edgeElement'); if (!edgeElement) continue; // 获取边的边界矩形 const edgeRect = edgeElement.getBoundingClientRect(); - // 检查矩形是否相交 - const isIntersecting = !( - dragNodeRect.right < edgeRect.left - threshold || - dragNodeRect.left > edgeRect.right + threshold || - dragNodeRect.bottom < edgeRect.top - threshold || - dragNodeRect.top > edgeRect.bottom + threshold - ); + const isEdgeCrossingNode =isIntersecting(edgeRect, dragNodeRect); - if (isIntersecting) { + if (isEdgeCrossingNode) { return { isIntersecting: true, edge, @@ -57,7 +82,7 @@ export function useDragNode({ } return { isIntersecting: false }; - }, [edges]); + }, [edges.length]); const onNodeDrag = useCallback((event: ReactMouseEvent, node: ReactFlowNode) => { // @ts-ignore @@ -96,7 +121,7 @@ export function useDragNode({ disconnect(droppedOnEdge.id) setDraggedNode(null); - }, [draggedNode, connect, setEdges]); + }, [draggedNode, connect, disconnect]); return { onNodeDrag, From 366ff6a57446ee90ec56d87f823b6f7260d2a8d9 Mon Sep 17 00:00:00 2001 From: stone Date: Mon, 3 Feb 2025 19:12:41 +0800 Subject: [PATCH 8/8] feat: add connectivity requirement for nodes to have inputs and outputs --- .../components/DataStory/DataStoryCanvas.tsx | 1 - .../src/components/DataStory/useDragNode.tsx | 24 +++++++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx index 649c24cb..9a36d35d 100644 --- a/packages/ui/src/components/DataStory/DataStoryCanvas.tsx +++ b/packages/ui/src/components/DataStory/DataStoryCanvas.tsx @@ -187,7 +187,6 @@ const Flow = ({ const { draggedNode, onNodeDragStop, onNodeDrag } = useDragNode({ connect, disconnect, - setEdges, edges, }); diff --git a/packages/ui/src/components/DataStory/useDragNode.tsx b/packages/ui/src/components/DataStory/useDragNode.tsx index 6bbc13da..53968e2c 100644 --- a/packages/ui/src/components/DataStory/useDragNode.tsx +++ b/packages/ui/src/components/DataStory/useDragNode.tsx @@ -14,7 +14,7 @@ function isIntersecting( nodeRect: DOMRect, threshold: number = 0.33, ): boolean { - // 首先检查是否有重叠 + // check if there is any overlap. if ( edgeRect.left > nodeRect.right || edgeRect.right < nodeRect.left || @@ -24,20 +24,17 @@ function isIntersecting( return false; } - // 计算重叠区域 + // calculate the overlap area const overlapLeft = Math.max(edgeRect.left, nodeRect.left); const overlapRight = Math.min(edgeRect.right, nodeRect.right); const overlapTop = Math.max(edgeRect.top, nodeRect.top); const overlapBottom = Math.min(edgeRect.bottom, nodeRect.bottom); - // 计算重叠面积 const overlapArea = (overlapRight - overlapLeft) * (overlapBottom - overlapTop); - - // 计算节点总面积 const nodeArea = nodeRect.width * nodeRect.height; - // 计算重叠比例 + // calculate the overlap ratio const overlapRatio = overlapArea / nodeArea; return overlapRatio > threshold; @@ -46,12 +43,10 @@ function isIntersecting( export function useDragNode({ connect, disconnect, - setEdges, edges, }: { connect: StoreSchema['connect']; disconnect: StoreSchema['disconnect']; - setEdges: StoreSchema['setEdges']; edges: StoreSchema['edges']; }) { const [draggedNode, setDraggedNode] = useState<{ node: any, droppedOnEdge: any } | null>(null); @@ -59,7 +54,6 @@ export function useDragNode({ const checkNodeEdgeIntersection = useCallback(( dragNodeRect: DOMRect, ): IntersectionResult => { - // 遍历所有边,检查是否有相交 for (const edge of edges) { const edgeElement = document.querySelector( `[data-id="${edge.id}"]`, @@ -67,10 +61,8 @@ export function useDragNode({ if (!edgeElement) continue; - // 获取边的边界矩形 const edgeRect = edgeElement.getBoundingClientRect(); - - const isEdgeCrossingNode =isIntersecting(edgeRect, dragNodeRect); + const isEdgeCrossingNode = isIntersecting(edgeRect, dragNodeRect); if (isEdgeCrossingNode) { return { @@ -90,9 +82,11 @@ export function useDragNode({ const { edgeElement, edge, isIntersecting } = checkNodeEdgeIntersection(nodeRect); - if (edgeElement && edge) { - const edgeId = edgeElement.getAttribute('data-testid')?.replace('rf__edge-', ''); - console.log(edgeId, 'edgeId'); + // The node must have inputs and outputs that can be connected + const isConnected = node.data.inputs.length > 0 && node.data.outputs.length > 0; + + if (isConnected && isIntersecting) { + edgeElement!.getAttribute('data-testid')?.replace('rf__edge-', ''); setDraggedNode({ node, droppedOnEdge: edge }); return; }