跳到主要内容

边数据结构

基础接口

interface EdgeData {
id: string; // 唯一标识
shape: string; // 形状类型(line、bezier、orthogonal、polyline)
source: string; // 源节点ID
target: string; // 目标节点ID
sourcePortId?: string; // 源锚点ID(可选)
targetPortId?: string; // 目标锚点ID(可选)
points?: Point[]; // 控制点(用于 polyline)
selected?: boolean; // 是否选中(默认 false)
visible?: boolean; // 是否可见(默认 true)
selectable?: boolean; // 是否可选中(默认 true)
zIndex?: number; // 层级(默认 0)
data?: EdgeCustomData; // 自定义数据
}

interface EdgeCustomData {
label?: string; // 边标签文本
style?: { // 样式配置
stroke?: string; // 描边色
lineWidth?: number; // 线宽
lineDash?: number[]; // 虚线样式(用于基础边线,如 [5, 5])
alpha?: number; // 整体透明度
sourceArrowType?: 'solid' | 'hollow' | 'none'; // 源端箭头类型
targetArrowType?: 'solid' | 'hollow' | 'none'; // 目标端箭头类型
arrowSize?: number; // 箭头大小
label?: { // 标签样式
color?: string; // 文字颜色
fontSize?: number; // 字体大小
background?: string; // 背景色
padding?: number; // 内边距
position?: number; // 位置(0-1)
};
pipeline?: {
outerWidth?: number; // 外层宽度
innerWidth?: number; // 内层宽度
gap?: number; // 内外层之间的间隙
cornerRadius?: number; // 拐角圆角半径
stub?: number; // 起始或结束的延伸长度
};
flow?: { // 流动动画
enabled?: boolean; // 是否启用
speed?: number; // 流动速度
color?: string;
direction?: 'forward' | 'reverse'; // 流动方向
};
[key: string]: any; // 其他形状特定样式
};
custom?: Record<string, any>; // 完全自定义的业务数据
[key: string]: any; // 其他扩展字段
}

interface Point {
x: number;
y: number;
}

属性说明

EdgeData 核心属性

属性名类型默认值必选描述
idstring-边唯一标识
shapestring-形状类型(line、bezier、orthogonal、polyline)
sourcestring-源节点ID
targetstring-目标节点ID
sourcePortIdstring--源锚点ID,不指定则连接到节点中心
targetPortIdstring--目标锚点ID,不指定则连接到节点中心
pointsPoint[]--控制点列表(用于 polyline)
selectedbooleanfalse-是否选中
visiblebooleantrue-是否可见
selectablebooleantrue-是否可选中
zIndexnumber0-层级,数值越大越靠前
dataEdgeCustomData--自定义数据(样式、标签等)

EdgeCustomData 扩展属性

属性名类型默认值必选描述
labelstring--边标签文本
styleobject--样式配置对象
customobject--完全自定义的业务数据

style 样式属性(常用)

属性名类型默认值描述
strokestring'#64748b'描边色
lineWidthnumber2线宽(像素)
lineDashnumber[]-虚线样式(用于基础边线及流动动画,其中边流动和管道同时启动默认为[12, 10])
alphanumber1整体透明度(0-1)
sourceArrowTypestring'none'源端箭头类型(solid/hollow/none)
targetArrowTypestring'solid'目标端箭头类型(solid/hollow/none)
arrowSizenumber10箭头大小(像素)
labelLabelStyle-标签样式配置(见下表)
flowFlowStyle-流动动画配置(见下表)

label 标签样式属性

属性名类型默认值必选描述
colorstring'#000000'-文字颜色
fontSizenumber12-字体大小(像素)
backgroundstring'#ffffff'-背景色
paddingnumber4-内边距(像素)
positionnumber0.5-标签位置(0-1,0.5为中点)

flow 流动动画配置

属性名类型默认值必选描述
enabledbooleanfalse-是否启用流动效果
speednumber160-流动速度px/s
colorstring'#3B82F6'-颜色

创建边

基础边

const edge = {
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2"
};

engine.graph.addEdge(edge);

带锚点的边

const edge = {
"id": "edge-2",
"shape": "line",
"source": "node-1",
"target": "node-2",
"sourcePortId": "port-right",
"targetPortId": "port-left"
};

engine.graph.addEdge(edge);

带控制点的边

当前仅对折线 polyline 支持拐点控制。

const edge = {
"id": "edge-3",
"shape": "polyline",
"source": "node-1",
"target": "node-2",
"points": [
{ "x": 150, "y": 150 },
{ "x": 200, "y": 200 },
{ "x": 250, "y": 150 }
]
};

engine.graph.addEdge(edge);

内置边类型

Canvas 引擎内置了多种边类型,通过设置边的 shape 属性来指定边类型。

边类型shape 值描述适用场景
直线line两点间直线连接简单关系图
贝塞尔曲线bezier平滑曲线连接关系图、脑图
正交线orthogonal直角折线连接流程图、UML图
折线polyline多点折线连接自定义路径

line - 直线

最基础的边类型,源节点和目标节点之间的直线连接:

{
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2",
"data": {
"style": {
"stroke": "#64748b",
"lineWidth": 2,
"targetArrowType": "solid" // 目标端箭头: 'solid' | 'hollow' | 'none'
}
}
}

polyline - 折线

带控制点的折线连接:

{
"id": "edge-2",
"shape": "polyline",
"source": "node-1",
"target": "node-2",
"points": [
{ "x": 200, "y": 150 },
{ "x": 250, "y": 200 },
{ "x": 300, "y": 150 }
],
"data": {
"style": {
"stroke": "#3b82f6",
"lineWidth": 2,
"targetArrowType": "solid"
}
}
}

bezier - 贝塞尔曲线

平滑的贝塞尔曲线连接:

{
"id": "edge-3",
"shape": "bezier",
"source": "node-1",
"target": "node-2",
"data": {
"style": {
"stroke": "#10b981",
"lineWidth": 2,
"targetArrowType": "solid"
}
}
}

orthogonal - 正交线

直角折线连接(常用于流程图):

{
"id": "edge-4",
"shape": "orthogonal",
"source": "node-1",
"target": "node-2",
"sourcePortId": "port-right",
"targetPortId": "port-left",
"data": {
"style": {
"stroke": "#f59e0b",
"lineWidth": 2,
"targetArrowType": "solid"
}
}
}

边样式

基础样式

{
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2",
"data": {
"style": {
"stroke": "#3b82f6", // 颜色
"lineWidth": 2, // 宽度
"lineDash": [5, 5], // 虚线 [实线长度, 间隙长度]
"alpha": 0.8 // 透明度
}
}
}

箭头

// 目标端实心箭头
{
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2",
"data": {
"style": {
"targetArrowType": "solid", // 目标端箭头类型
"arrowSize": 10 // 箭头大小
}
}
}

// 双向箭头
{
"data": {
"style": {
"sourceArrowType": "solid", // 源端箭头
"targetArrowType": "solid" // 目标端箭头
}
}
}

// 空心箭头
{
"data": {
"style": {
"targetArrowType": "hollow" // 空心箭头
}
}
}

// 无箭头
{
"data": {
"style": {
"sourceArrowType": "none",
"targetArrowType": "none"
}
}
}

标签

{
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2",
"data": {
"label": "连接",
"style": {
"label": {
"position": 0.5, // 标签位置 (0-1, 0.5为中点)
"fontSize": 12,
"color": "#64748b",
"background": "#ffffff",
"padding": 4
}
}
}
}

流动效果

{
"id": "edge-1",
"shape": "line",
"source": "node-1",
"target": "node-2",
"data": {
"style": {
"flow": {
"enabled": true, // 启用流动效果
"speed": 160 // 流动速度px/s
},
"lineDash": [5, 5] // 需配合虚线使用,流动动画将沿此虚线样式进行
}
}
}

虚线样式说明

lineDash 用于设置虚线样式,同时适用于基础边线和流动动画:

{
"data": {
"style": {
"lineDash": [5, 5], // 基础边线和流动动画均使用此虚线样式
"flow": {
"enabled": true,
"speed": 160
}
}
}
}

边操作

警告

目前引擎层面仅提供最基础的方法,其他便捷方式适时评估必要性扩展(如:updateEdge、removeEdges、selectEdge、deselectEdge、toggleEdgeSelection)。

添加边

// 单个添加
engine.graph.addEdge(edge);

// 批量添加
[edge1, edge2, edge3].forEach(edge => engine.graph.addEdge(edge));

获取边

// 通过ID获取
const edge = engine.graph.getEdge('edge-1');

// 获取所有边
const edges = engine.graph.getEdges();

// 获取选中的边
const selected = engine.graph.getEdges().filter(e => e.selected);

// 获取连接到指定节点的边
const connectedEdges = engine.graph.connectedEdges('node-1');

// 获取从指定节点出发的边
const outgoingEdges = engine.graph.getEdges().filter(e => e.source === 'node-1');

// 获取指向指定节点的边
const incomingEdges = engine.graph.getEdges().filter(e => e.target === 'node-1');

更新边

// 更新样式
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.data = {
...edge.data,
style: {
...edge.data?.style,
stroke: '#10b981',
lineWidth: 3
}
};
engine.graph.markDirty('style');
}

// 更新控制点
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.points = [
{ x: 200, y: 200 },
{ x: 300, y: 250 }
];
engine.graph.markDirty('structure');
}

// 更新连接(通过命令)
import { ReconnectEdgeCommand } from '@fnt-agilejs/core';
const edge = engine.graph.getEdge('edge-1');
if (edge) {
engine.history.execute(
new ReconnectEdgeCommand(
engine.graph,
'edge-1',
{ source: edge.source, target: edge.target },
{ source: 'node-3', target: 'node-4' }
)
);
}

删除边

// 删除单条边
engine.graph.removeEdge('edge-1');

// 删除多条边
['edge-1', 'edge-2'].forEach(id => engine.graph.removeEdge(id));

// 删除节点时自动删除相关边
engine.graph.removeNode('node-1'); // 自动删除连接到 node-1 的所有边

选中边

提示

默认情况下边选中的轮廓为红色 #dc2626,如果有单独设置轮廓颜色或流动颜色其将会被覆盖。

// 选中边
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.selected = true;
engine.graph.markDirty('style');
}

// 取消选中
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.selected = false;
engine.graph.markDirty('style');
}

// 切换选中状态
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.selected = !edge.selected;
engine.graph.markDirty('style');
}

// 清除所有边的选中
engine.graph.getEdges().forEach(e => e.selected = false);
engine.graph.markDirty('style');

边路由

自动路由

对于 orthogonal(正交线)边,引擎会自动计算路径:

{
id: 'edge-1',
shape: 'orthogonal',
source: 'node-1',
target: 'node-2',
sourcePortId: 'port-right',
targetPortId: 'port-left',
// 路径自动计算
}

手动路由

对于 polyline 边,可以手动指定路径点:

{
id: 'edge-1',
shape: 'polyline',
source: 'node-1',
target: 'node-2',
points: [
{ x: 200, y: 150 },
{ x: 250, y: 200 },
{ x: 300, y: 150 }
]
}

动态更新路径

// 添加控制点
const edge = engine.graph.getEdge('edge-1');
if (edge) {
const newPoints = [
...(edge.points || []),
{ x: 350, y: 200 }
];
edge.points = newPoints;
engine.graph.markDirty('structure');
}

// 移除控制点
const edge = engine.graph.getEdge('edge-1');
if (edge && edge.points) {
const updatedPoints = edge.points.filter((_, index) => index !== 1);
edge.points = updatedPoints;
engine.graph.markDirty('structure');
}

// 清除所有控制点
const edge = engine.graph.getEdge('edge-1');
if (edge) {
edge.points = [];
engine.graph.markDirty('structure');
}

高级功能

警告

以下功能主要基于画布引擎基础接口实现,并非完全由引擎内置。

边的碰撞检测

function isPointOnEdge(
point: Point,
edge: EdgeData,
threshold: number = 5
): boolean {
const source = engine.graph.getNode(edge.source);
const target = engine.graph.getNode(edge.target);

if (!source || !target) return false;

// 简化:直线距离检测
const dist = distanceToLine(
point,
source.position,
target.position
);

return dist <= threshold;
}

function distanceToLine(
point: Point,
lineStart: Point,
lineEnd: Point
): number {
const dx = lineEnd.x - lineStart.x;
const dy = lineEnd.y - lineStart.y;
const length = Math.sqrt(dx * dx + dy * dy);

if (length === 0) {
return Math.sqrt(
(point.x - lineStart.x) ** 2 +
(point.y - lineStart.y) ** 2
);
}

const t = Math.max(0, Math.min(1,
((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (length * length)
));

const projX = lineStart.x + t * dx;
const projY = lineStart.y + t * dy;

return Math.sqrt(
(point.x - projX) ** 2 +
(point.y - projY) ** 2
);
}

查找两节点间的边

function findEdgesBetween(
sourceId: string,
targetId: string
): EdgeData[] {
return engine.graph.getEdges().filter(edge =>
(edge.source === sourceId && edge.target === targetId) ||
(edge.source === targetId && edge.target === sourceId)
);
}

// 使用
const edges = findEdgesBetween('node-1', 'node-2');
console.log(`Found ${edges.length} edges`);

获取节点的度数

function getNodeDegree(nodeId: string): {
in: number;
out: number;
total: number;
} {
const edges = engine.graph.getEdges();

let inDegree = 0;
let outDegree = 0;

edges.forEach(edge => {
if (edge.target === nodeId) inDegree++;
if (edge.source === nodeId) outDegree++;
});

return {
in: inDegree,
out: outDegree,
total: inDegree + outDegree
};
}

// 使用
const degree = getNodeDegree('node-1');
console.log(`入度: ${degree.in}, 出度: ${degree.out}`);

检测环路

function hasCycle(): boolean {
const edges = engine.graph.getEdges();
const visited = new Set<string>();
const recursionStack = new Set<string>();

function dfs(nodeId: string): boolean {
visited.add(nodeId);
recursionStack.add(nodeId);

const outgoing = edges.filter(e => e.source === nodeId);

for (const edge of outgoing) {
if (!visited.has(edge.target)) {
if (dfs(edge.target)) return true;
} else if (recursionStack.has(edge.target)) {
return true;
}
}

recursionStack.delete(nodeId);
return false;
}

const nodes = engine.graph.getNodes();
for (const node of nodes) {
if (!visited.has(node.id)) {
if (dfs(node.id)) return true;
}
}

return false;
}

// 使用
if (hasCycle()) {
console.warn('图中存在环路');
}

拓扑排序

function topologicalSort(): string[] | null {
const edges = engine.graph.getEdges();
const nodes = engine.graph.getNodes();
const inDegree = new Map<string, number>();
const result: string[] = [];
const queue: string[] = [];

// 初始化入度
nodes.forEach(node => {
inDegree.set(node.id, 0);
});

edges.forEach(edge => {
inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
});

// 找到所有入度为0的节点
inDegree.forEach((degree, nodeId) => {
if (degree === 0) {
queue.push(nodeId);
}
});

// BFS
while (queue.length > 0) {
const nodeId = queue.shift()!;
result.push(nodeId);

edges.filter(e => e.source === nodeId).forEach(edge => {
const targetDegree = inDegree.get(edge.target)! - 1;
inDegree.set(edge.target, targetDegree);

if (targetDegree === 0) {
queue.push(edge.target);
}
});
}

// 如果结果数量不等于节点数量,说明有环
return result.length === nodes.length ? result : null;
}

// 使用
const sorted = topologicalSort();
if (sorted) {
console.log('拓扑排序结果:', sorted);
} else {
console.log('图中存在环,无法进行拓扑排序');
}

性能优化

边缓存

对于大量边的场景,使用边快照模式:

const engine = new CanvasEngine({
container,
edgeSnapshot: 'always' // 始终使用快照
});

视口裁剪

边会自动参与视口裁剪,只渲染可见的边:

// 自动优化,无需手动处理
// 引擎会根据视口范围和节点位置判断边是否可见

降质渲染

在拖拽或大规模场景中,边会自动降质:

const engine = new CanvasEngine({
container,
dragEdgeRenderThreshold: {
nodes: 400,
edges: 800
}
});

最佳实践

1. 使用锚点连接

// ✅ 推荐:使用锚点连接
{
id: 'edge-1',
shape: 'line',
source: 'node-1',
target: 'node-2',
sourcePortId: 'port-right',
targetPortId: 'port-left'
}

// ❌ 避免:不指定锚点(连接到节点中心)
{
id: 'edge-1',
shape: 'line',
source: 'node-1',
target: 'node-2'
}

2. 选择合适的边类型

// 流程图:使用正交线
{
shape: 'orthogonal'
}

// 关系图:使用曲线
{
shape: 'curve'
}

// 简单连接:使用直线
{
shape: 'line'
}

3. 避免创建重复边

// ✅ 检查边是否已存在
function addEdgeIfNotExists(edge: EdgeData): void {
const existing = engine.graph.getEdges().find(e =>
e.source === edge.source &&
e.target === edge.target &&
e.sourcePortId === edge.sourcePortId &&
e.targetPortId === edge.targetPortId
);

if (!existing) {
engine.graph.addEdge(edge);
}
}

4. 清理孤立边

// ✅ 定期清理孤立边
function cleanOrphanEdges(): void {
const nodes = new Set(engine.graph.getNodes().map(n => n.id));
const orphanEdges = engine.graph.getEdges().filter(edge =>
!nodes.has(edge.source) || !nodes.has(edge.target)
);

orphanEdges.forEach(edge => {
engine.graph.removeEdge(edge.id);
});
}

5. 批量操作

// ✅ 批量添加边
engine.graph.beginUpdate();
edges.forEach(edge => engine.graph.addEdge(edge));
engine.graph.endUpdate();

// ❌ 避免:逐个添加导致多次重绘
edges.forEach(edge => engine.graph.addEdge(edge));

常见问题

Q: 如何让边始终在节点下方?

边总是在节点下方渲染,这是引擎的默认行为。

Q: 如何实现自定义箭头?

需要创建自定义边渲染器,参考 自定义渲染器 文档。

Q: 边的标签如何自动避让节点?

使用 labelOffset 微调标签位置,或使用自定义标签渲染逻辑。

Q: 如何实现动画边?

使用 FlowDashPlugin 或在自定义渲染器中实现动画效果。