前端图形学← 返回文章列表
流程图自定义连线算法与吸附计算:基于 React Flow 的 SVG 自定义 Edge 渲染
从起点终点与端口朝向推导贝塞尔控制点,结合 React Flow 的 EdgeLabelRenderer 实现带交互按钮的自定义连线。
LJ
· 8 分钟
本页目录展开 / 收起
读完你能做什么
- 从起点、终点和方向向量推导贝塞尔曲线控制点。
- 把几何计算与 React Flow 的 Edge 渲染组件分开实现和测试。
- 在节点拖动时保持路径更新,并为标签、删除按钮留出交互层。
实现时先保证“同一组坐标总能得到同一路径”,再接入拖拽与交互。这样几何错误和 React 状态错误不会混在一起排查。
做流程图或拓扑图时,连线(Edge)往往比节点本身更考验细节:两个节点错位排列时,连线怎么拐弯才自然?节点拖动时连线会不会滞后?更麻烦的是,如果想在连线中间加一个“删除按钮”或“分支条件输入框”,直接在 SVG 里面放 HTML 标签很容易碰到事件穿透和缩放计算问题。这篇文章梳理三次贝塞尔曲线控制点的计算,并用 React Flow 实现带交互的自定义连线。
连线算法核心:贝塞尔曲线控制点计算
在二维平面中,从源节点坐标 $(x_1, y_1)$ 到目标节点坐标 $(x_2, y_2)$,平滑的 S 型或弧形连线通常采用三次贝塞尔曲线:
三次贝塞尔公式:
B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃, t ∈ [0, 1]
SVG 路径指令:
M sourceX sourceY C controlX1 controlY1, controlX2 controlY2, targetX targetY贝塞尔控制点拓扑图:
(Source: P₀) ──────► [Control Point 1: P₁]
│
▼
(Target: P₃) ◄────── [Control Point 2: P₂]- 水平流向:$P_1 = (x_1 + \Delta x / 2, y_1)$,$P_2 = (x_2 - \Delta x / 2, y_2)$;
- 垂直流向:$P_1 = (x_1, y_1 + \Delta y / 2)$,$P_2 = (x_2, y_2 - \Delta y / 2)$。
核心实现:React Flow 自定义连线组件
通过实现一个带有“删除按钮”和“条件标签”的 Custom Edge:
// components/edges/CustomFlowEdge.tsx
'use client';
import React from 'react';
import {
type EdgeProps,
getBezierPath,
EdgeLabelRenderer,
BaseEdge,
} from '@xyflow/react';
export function CustomFlowEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data,
}: EdgeProps) {
// 1. 计算平滑贝塞尔 SVG Path 与中点坐标
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
{/* 基础 SVG 连线渲染 */}
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: 2,
stroke: '#3b82f6',
...style,
}}
/>
{/* 使用 EdgeLabelRenderer 渲染脱离 SVG 坐标系的绝对定位 DOM 按钮 */}
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
}}
className="nodrag nopan flex items-center gap-1 bg-white px-2 py-1 border rounded-md shadow-sm text-xs"
>
<span>{data?.label || '流转条件'}</span>
<button
onClick={(e) => {
e.stopPropagation();
data?.onDelete?.(id);
}}
className="w-4 h-4 rounded-full bg-red-100 text-red-600 hover:bg-red-200 flex items-center justify-center text-[10px]"
>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}注册与在 React Flow 中使用
import { ReactFlow, type EdgeTypes } from '@xyflow/react';
import { CustomFlowEdge } from './components/edges/CustomFlowEdge';
// 必须在组件外定义 edgeTypes,避免重复渲染
const edgeTypes: EdgeTypes = {
custom: CustomFlowEdge,
};
export function FlowEditor() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={initialNodes}
edges={initialEdges}
edgeTypes={edgeTypes}
fitView
/>
</div>
);
}官方资料
实现要点
- SVG 与 HTML 交互隔离:连线本身用 SVG
<path>绘制性能最高,但连线上的按钮或输入框推荐使用EdgeLabelRenderer渲染为绝对定位的 HTML DOM,这样既能正常接收鼠标点击事件,又不会受 SVG 内部foreignObject的兼容性限制。 - 避免在组件体内创建 edgeTypes:React Flow 要求
edgeTypes对象定义在组件外部或通过useMemo缓存。如果在父组件内每次 render 都声明一个新字面量,会导致画布上所有连线在每次状态变动时全部被销毁重建。