From d068927b53cadabb7024dbb2bea3427b195049c8 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 4 Aug 2026 09:22:36 +0800 Subject: [PATCH] feat(workflow): add floating directed edges --- .../src/components/workflow/ConditionEdge.tsx | 111 +++++++++++++++++- .../components/workflow/WorkflowCanvas.tsx | 12 +- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/workflow/ConditionEdge.tsx b/frontend/src/components/workflow/ConditionEdge.tsx index cc1807c..f088d26 100644 --- a/frontend/src/components/workflow/ConditionEdge.tsx +++ b/frontend/src/components/workflow/ConditionEdge.tsx @@ -8,7 +8,10 @@ import { BaseEdge, EdgeLabelRenderer, getBezierPath, + Position, type EdgeProps, + type InternalNode, + useInternalNode, } from "@xyflow/react"; import { Trash2 } from "lucide-react"; import { useContext } from "react"; @@ -16,8 +19,77 @@ import { useContext } from "react"; import { cn } from "@/lib/utils"; import { EdgeActionContext } from "./context"; +type EdgeEndpoint = { + x: number; + y: number; + position: Position; +}; + +function getNodeCenter(node: InternalNode) { + const width = node.measured.width ?? node.width ?? 0; + const height = node.measured.height ?? node.height ?? 0; + + return { + x: node.internals.positionAbsolute.x + width / 2, + y: node.internals.positionAbsolute.y + height / 2, + width, + height, + }; +} + +/** + * 计算一条从节点中心朝外的射线与节点矩形边界的交点。 + * 这样边只依赖节点的位置和尺寸,不会被创建连接时使用的底部 Handle 限制。 + */ +function getNodeBoundaryPoint( + node: InternalNode, + direction: { x: number; y: number }, + fallbackPosition: Position, +): EdgeEndpoint | null { + const center = getNodeCenter(node); + if (!center.width || !center.height) return null; + + const { x: dx } = direction; + let { y: dy } = direction; + if (dx === 0 && dy === 0) { + dy = fallbackPosition === Position.Top ? -1 : 1; + } + + const halfWidth = center.width / 2; + const halfHeight = center.height / 2; + const normalizedX = dx / halfWidth; + const normalizedY = dy / halfHeight; + const scale = 1 / Math.max(Math.abs(normalizedX), Math.abs(normalizedY)); + const hitsVerticalSide = Math.abs(normalizedX) > Math.abs(normalizedY); + + if (hitsVerticalSide) { + // 避开圆角,让吸附点始终落在可见的直边上。 + const cornerInset = Math.min(16, halfHeight); + return { + x: center.x + dx * scale, + y: Math.min( + center.y + halfHeight - cornerInset, + Math.max(center.y - halfHeight + cornerInset, center.y + dy * scale), + ), + position: dx > 0 ? Position.Right : Position.Left, + }; + } + + const cornerInset = Math.min(16, halfWidth); + return { + x: Math.min( + center.x + halfWidth - cornerInset, + Math.max(center.x - halfWidth + cornerInset, center.x + dx * scale), + ), + y: center.y + dy * scale, + position: dy > 0 ? Position.Bottom : Position.Top, + }; +} + export function ConditionEdge({ id, + source, + target, sourceX, sourceY, targetX, @@ -26,16 +98,42 @@ export function ConditionEdge({ targetPosition, data, selected, + markerEnd, }: EdgeProps) { const actions = useContext(EdgeActionContext); + const sourceNode = useInternalNode(source); + const targetNode = useInternalNode(target); + + const sourceCenter = sourceNode ? getNodeCenter(sourceNode) : null; + const targetCenter = targetNode ? getNodeCenter(targetNode) : null; + const sourceEndpoint = sourceNode && sourceCenter && targetCenter + ? getNodeBoundaryPoint( + sourceNode, + { + x: targetCenter.x - sourceCenter.x, + y: targetCenter.y - sourceCenter.y, + }, + Position.Bottom, + ) + : null; + const targetEndpoint = targetNode && targetCenter && sourceCenter + ? getNodeBoundaryPoint( + targetNode, + { + x: sourceCenter.x - targetCenter.x, + y: sourceCenter.y - targetCenter.y, + }, + Position.Top, + ) + : null; const [path, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, + sourceX: sourceEndpoint?.x ?? sourceX, + sourceY: sourceEndpoint?.y ?? sourceY, + sourcePosition: sourceEndpoint?.position ?? sourcePosition, + targetX: targetEndpoint?.x ?? targetX, + targetY: targetEndpoint?.y ?? targetY, + targetPosition: targetEndpoint?.position ?? targetPosition, curvature: 0.28, }); @@ -55,6 +153,7 @@ export function ConditionEdge({