Skip to content

Commit 95bba08

Browse files
authored
Merge pull request #2368 from didi/bugfix/round1-in-2026-Jan
fix: 修复来自内部反馈的边问题
2 parents 2bda6f7 + be975be commit 95bba08

3 files changed

Lines changed: 223 additions & 53 deletions

File tree

packages/core/src/model/edge/PolylineEdgeModel.ts

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export class PolylineEdgeModel extends BaseEdgeModel {
4242
? providedOffset
4343
: this.getDefaultOffset()
4444
if (data.pointsList) {
45-
this.pointsList = data.pointsList
45+
const corrected = this.orthogonalizePath(data.pointsList)
46+
;(data as any).pointsList = corrected
47+
this.pointsList = corrected
4648
}
4749
super.initEdgeData(data)
4850
}
@@ -55,6 +57,120 @@ export class PolylineEdgeModel extends BaseEdgeModel {
5557
}
5658
}
5759

60+
orthogonalizePath(points: Point[]): Point[] {
61+
// 输入非法或不足两点时直接返回副本
62+
if (!Array.isArray(points) || points.length < 2) {
63+
return points
64+
}
65+
// pushUnique: 向数组中添加唯一点,避免重复
66+
const pushUnique = (arr: Point[], p: Point) => {
67+
const last = arr[arr.length - 1]
68+
if (!last || last.x !== p.x || last.y !== p.y) {
69+
arr.push({ x: p.x, y: p.y })
70+
}
71+
}
72+
// isAxisAligned: 检查两点是否在同一条轴上
73+
const isAxisAligned = (a: Point, b: Point) => a.x === b.x || a.y === b.y
74+
// manhattanDistance: 计算两点在曼哈顿距离上的距离
75+
const manhattanDistance = (a: Point, b: Point) =>
76+
Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
77+
78+
// 1) 生成严格正交路径,尽量延续前一段方向以减少折点
79+
const orthogonal: Point[] = []
80+
pushUnique(orthogonal, points[0])
81+
// previousDirection: 记录前一段的方向,用于判断当前段的PreferredCorner
82+
let previousDirection: SegmentDirection | undefined
83+
// 遍历所有点对,生成正交路径
84+
for (let i = 0; i < points.length - 1; i++) {
85+
const current = orthogonal[orthogonal.length - 1]
86+
const next = points[i + 1]
87+
if (!current || !next) continue
88+
89+
if (isAxisAligned(current, next)) {
90+
pushUnique(orthogonal, next)
91+
previousDirection =
92+
current.x === next.x
93+
? SegmentDirection.VERTICAL
94+
: SegmentDirection.HORIZONTAL
95+
continue
96+
}
97+
98+
const cornerHV: Point = { x: next.x, y: current.y }
99+
const cornerVH: Point = { x: current.x, y: next.y }
100+
101+
// 根据前一段的方向,优先选择能延续该方向的拐角点,以减少折点数量;
102+
// 若前一段为垂直方向,则优先选择垂直-水平拐角(cornerVH);
103+
// 若前一段为水平方向,则优先选择水平-垂直拐角(cornerHV);
104+
// 若前一段无方向(初始情况),则比较两个拐角的曼哈顿距离,选更近者。
105+
const preferredCorner =
106+
previousDirection === SegmentDirection.VERTICAL
107+
? cornerVH
108+
: previousDirection === SegmentDirection.HORIZONTAL
109+
? cornerHV
110+
: manhattanDistance(current, cornerHV) <=
111+
manhattanDistance(current, cornerVH)
112+
? cornerHV
113+
: cornerVH
114+
115+
if (preferredCorner.x !== current.x || preferredCorner.y !== current.y) {
116+
pushUnique(orthogonal, preferredCorner)
117+
}
118+
pushUnique(orthogonal, next)
119+
120+
const a = orthogonal[orthogonal.length - 2]
121+
const b = orthogonal[orthogonal.length - 1]
122+
previousDirection =
123+
a && b
124+
? a.x === b.x
125+
? SegmentDirection.VERTICAL
126+
: SegmentDirection.HORIZONTAL
127+
: previousDirection
128+
}
129+
130+
// 2) 去除冗余共线中间点
131+
const simplified: Point[] = []
132+
for (let i = 0; i < orthogonal.length; i++) {
133+
const prev = orthogonal[i - 1]
134+
const curr = orthogonal[i]
135+
const next = orthogonal[i + 1]
136+
// 如果当前点与前一个点和后一个点在同一条水平线或垂直线上,则跳过该点,去除冗余的共线中间点
137+
if (
138+
prev &&
139+
curr &&
140+
next &&
141+
((prev.x === curr.x && curr.x === next.x) || // 水平共线
142+
(prev.y === curr.y && curr.y === next.y)) // 垂直共线
143+
) {
144+
continue
145+
}
146+
pushUnique(simplified, curr)
147+
}
148+
149+
// 3) 保留原始起点与终点位置
150+
if (simplified.length >= 2) {
151+
simplified[0] = { x: points[0].x, y: points[0].y }
152+
simplified[simplified.length - 1] = {
153+
x: points[points.length - 1].x,
154+
y: points[points.length - 1].y,
155+
}
156+
}
157+
158+
// 4) 结果校验:任意相邻段都必须为水平/垂直;失败则退化为起止两点
159+
const isOrthogonal =
160+
simplified.length < 2 ||
161+
simplified.every((_, idx, arr) => {
162+
if (idx === 0) return true
163+
return isAxisAligned(arr[idx - 1], arr[idx])
164+
})
165+
166+
return isOrthogonal
167+
? simplified
168+
: [
169+
{ x: points[0].x, y: points[0].y },
170+
{ x: points[points.length - 1].x, y: points[points.length - 1].y },
171+
]
172+
}
173+
58174
/**
59175
* 计算默认 offset:箭头与折线重叠长度 + 5
60176
* 重叠长度采用箭头样式中的 offset(沿边方向的长度)
@@ -344,7 +460,7 @@ export class PolylineEdgeModel extends BaseEdgeModel {
344460
}
345461

346462
updatePath(pointList: Point[]) {
347-
this.pointsList = pointList
463+
this.pointsList = this.orthogonalizePath(pointList)
348464
this.points = this.getPath(this.pointsList)
349465
}
350466

@@ -387,8 +503,10 @@ export class PolylineEdgeModel extends BaseEdgeModel {
387503
this.targetNode,
388504
this.offset || 0,
389505
)
390-
this.pointsList = pointsList
391-
this.points = pointsList.map((point) => `${point.x},${point.y}`).join(' ')
506+
this.pointsList = this.orthogonalizePath(pointsList)
507+
this.points = this.pointsList
508+
.map((point) => `${point.x},${point.y}`)
509+
.join(' ')
392510
}
393511

394512
@action
@@ -676,18 +794,20 @@ export class PolylineEdgeModel extends BaseEdgeModel {
676794
sourceNode: BaseNodeModel
677795
targetNode: BaseNodeModel
678796
}) {
679-
this.pointsList = getPolylinePoints(
680-
{
681-
x: startPoint.x,
682-
y: startPoint.y,
683-
},
684-
{
685-
x: endPoint.x,
686-
y: endPoint.y,
687-
},
688-
sourceNode,
689-
targetNode,
690-
this.offset || 0,
797+
this.pointsList = this.orthogonalizePath(
798+
getPolylinePoints(
799+
{
800+
x: startPoint.x,
801+
y: startPoint.y,
802+
},
803+
{
804+
x: endPoint.x,
805+
y: endPoint.y,
806+
},
807+
sourceNode,
808+
targetNode,
809+
this.offset || 0,
810+
),
691811
)
692812

693813
this.initPoints()

packages/core/src/util/edge.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ export const pointDirection = (point: Point, bbox: BoxBounds): Direction => {
107107
}
108108

109109
/* 获取扩展图形上的点,即起始终点相邻的点,上一个或者下一个节点 */
110+
/**
111+
* 计算扩展包围盒上的相邻点(起点或终点的下一个/上一个拐点)
112+
* - 使用原始节点 bbox 来判定点相对中心的方向,避免 offset 扩展后宽高改变导致方向误判
113+
* - 若 start 相对中心为水平方向,则返回扩展盒在 x 上的边界,y 保持不变
114+
* - 若为垂直方向,则返回扩展盒在 y 上的边界,x 保持不变
115+
* @param expendBBox 扩展后的包围盒(包含 offset)
116+
* @param bbox 原始节点包围盒(用于正确的方向判定)
117+
* @param point 起点或终点坐标
118+
*/
110119
export const getExpandedBBoxPoint = (
111120
expendBBox: BoxBounds,
112121
bbox: BoxBounds,
@@ -378,7 +387,11 @@ export const isSegmentCrossingBBox = (
378387
)
379388
}
380389

381-
/* 获取下一个相邻的点 */
390+
/**
391+
* 基于轴对齐规则获取某点的相邻可连通点(不穿越节点)
392+
* - 仅考虑 x 或 y 相同的候选点,保证严格水平/垂直
393+
* - 使用 isSegmentCrossingBBox 校验线段不穿越源/目标节点
394+
*/
382395
export const getNextNeighborPoints = (
383396
points: Point[],
384397
point: Point,
@@ -401,9 +414,12 @@ export const getNextNeighborPoints = (
401414
return filterRepeatPoints(neighbors)
402415
}
403416

404-
/* 路径查找,AStar查找+曼哈顿距离
405-
* 算法wiki:https://zh.wikipedia.org/wiki/A*%E6%90%9C%E5%B0%8B%E6%BC%94%E7%AE%97%E6%B3%95
406-
* 方法无法复用,且调用了很多polyline相关的方法,暂不抽离到src/algorithm中
417+
/**
418+
* 使用 A* + 曼哈顿启发式在候选点图上查找正交路径
419+
* - 开放集/关闭集管理遍历
420+
* - gScore 为累计实际代价,fScore = gScore + 启发式
421+
* - 邻居仅为与当前点 x 或 y 相同且不穿越节点的点
422+
* 参考:https://zh.wikipedia.org/wiki/A*%E6%90%9C%E5%B0%8B%E6%BC%94%E7%AE%97%E6%B3%95
407423
*/
408424
export const pathFinder = (
409425
points: Point[],
@@ -475,8 +491,9 @@ export const pathFinder = (
475491
}
476492

477493
if (current?.id && neighbor?.id) {
494+
// 修复:累计代价应基于 gScore[current] 而非 fScore[current]
478495
const tentativeGScore =
479-
fScore[current.id] + estimateDistance(current, neighbor)
496+
(gScore[current.id] ?? 0) + estimateDistance(current, neighbor)
480497
if (gScore[neighbor.id] && tentativeGScore >= gScore[neighbor.id]) {
481498
return
482499
}
@@ -495,7 +512,10 @@ export const pathFinder = (
495512
export const getBoxByOriginNode = (node: BaseNodeModel): BoxBounds => {
496513
return getNodeBBox(node)
497514
}
498-
/* 保证一条直线上只有2个节点: 删除x/y相同的中间节点 */
515+
/**
516+
* 去除共线冗余中间点,保持每条直线段仅保留两端点
517+
* - 若三点在同一水平线或同一垂直线,移除中间点
518+
*/
499519
export const pointFilter = (points: Point[]): Point[] => {
500520
let i = 1
501521
while (i < points.length - 1) {
@@ -514,7 +534,16 @@ export const pointFilter = (points: Point[]): Point[] => {
514534
return points
515535
}
516536

517-
/* 计算折线点 */
537+
/**
538+
* 计算折线点(正交候选点 + A* 路径)
539+
* 步骤:
540+
* 1) 取源/目标节点的扩展包围盒与相邻点 sPoint/tPoint
541+
* 2) 若两个扩展盒重合,使用简单路径 getSimplePoints
542+
* 3) 构造 lineBBox/sMixBBox/tMixBBox,并收集其角点与中心交点
543+
* 4) 过滤掉落在两个扩展盒内部的点,形成 connectPoints
544+
* 5) 以 sPoint/tPoint 为起止,用 A* 查找路径
545+
* 6) 拼入原始 start/end,并用 pointFilter 去除冗余共线点
546+
*/
518547
export const getPolylinePoints = (
519548
start: Point,
520549
end: Point,
@@ -700,6 +729,10 @@ export const points2PointsList = (points: string): Point[] => {
700729
return pointsList
701730
}
702731

732+
/**
733+
* 当扩展 bbox 重合时的简化拐点计算
734+
* - 根据起止段的方向(水平/垂直)插入 1~2 个中间点,避免折线重合与穿越
735+
*/
703736
export const getSimplePoints = (
704737
start: Point,
705738
end: Point,

packages/extension/src/materials/curved-edge/index.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -97,50 +97,67 @@ function getMidPoints(
9797
}
9898
}
9999

100+
/**
101+
* 生成局部路径片段(包含圆角)
102+
* - 输入为上一个顶点、当前拐点、下一个顶点,计算方向组合并选择圆弧象限
103+
* - 将圆角半径限制在相邻两段长度的一半以内,避免过度弯曲
104+
* @param prevPoint 上一个顶点
105+
* @param cornerPoint 当前拐点(圆角所在拐点)
106+
* @param nextPoint 下一个顶点
107+
* @param cornerRadius 圆角半径上限
108+
* @returns 局部 path 字符串(包含 L/Q 操作)
109+
*/
100110
function getPartialPath(
101-
prev: PointTuple,
102-
cur: PointTuple,
103-
next: PointTuple,
104-
radius: number,
111+
prevPoint: PointTuple,
112+
cornerPoint: PointTuple,
113+
nextPoint: PointTuple,
114+
cornerRadius: number,
105115
): string {
106-
// 定义误差容错变量
107-
const tolerance = 1
108-
109-
let dir1: DirectionType = ''
110-
let dir2: DirectionType = ''
111-
112-
if (Math.abs(prev[0] - cur[0]) <= tolerance) {
113-
// 垂直方向
114-
dir1 = prev[1] > cur[1] ? 't' : 'b'
115-
} else if (Math.abs(prev[1] - cur[1]) <= tolerance) {
116-
// 水平方向
117-
dir1 = prev[0] > cur[0] ? 'l' : 'r'
116+
// 轴对齐容差(像素),用于消除微小误差
117+
const epsilon = 1
118+
119+
const resolveDir = (a: PointTuple, b: PointTuple): DirectionType => {
120+
const dx = b[0] - a[0]
121+
const dy = b[1] - a[1]
122+
const adx = Math.abs(dx)
123+
const ady = Math.abs(dy)
124+
if (ady <= epsilon && adx > epsilon) {
125+
return dx < 0 ? 'l' : 'r'
126+
}
127+
if (adx <= epsilon && ady > epsilon) {
128+
return dy < 0 ? 't' : 'b'
129+
}
130+
if (adx <= epsilon && ady <= epsilon) {
131+
return ''
132+
}
133+
// 非严格对齐时,选择更接近的轴
134+
return adx < ady ? (dx < 0 ? 'l' : 'r') : dy < 0 ? 't' : 'b'
118135
}
119136

120-
if (Math.abs(cur[0] - next[0]) <= tolerance) {
121-
dir2 = cur[1] > next[1] ? 't' : 'b'
122-
} else if (Math.abs(cur[1] - next[1]) <= tolerance) {
123-
dir2 = cur[0] > next[0] ? 'l' : 'r'
124-
}
137+
const dir1: DirectionType = resolveDir(prevPoint, cornerPoint)
138+
const dir2: DirectionType = resolveDir(cornerPoint, nextPoint)
125139

126140
const r =
127141
Math.min(
128-
Math.hypot(cur[0] - prev[0], cur[1] - prev[1]) / 2,
129-
Math.hypot(next[0] - cur[0], next[1] - cur[1]) / 2,
130-
radius,
131-
) || (1 / 5) * radius
142+
Math.hypot(cornerPoint[0] - prevPoint[0], cornerPoint[1] - prevPoint[1]) /
143+
2,
144+
Math.hypot(nextPoint[0] - cornerPoint[0], nextPoint[1] - cornerPoint[1]) /
145+
2,
146+
cornerRadius,
147+
) || (1 / 5) * cornerRadius
132148

133149
const key = `${dir1}${dir2}`
134150
const orientation: ArcQuadrantType = directionMap[key] || '-'
135-
let path = `L ${prev[0]} ${prev[1]}`
151+
let path = ''
136152

137153
if (orientation === '-') {
138-
path += `L ${cur[0]} ${cur[1]} L ${next[0]} ${next[1]}`
154+
// 仅移动到当前拐点,由下一次迭代决定如何从拐点继续(直线或圆角)
155+
path += `L ${cornerPoint[0]} ${cornerPoint[1]}`
139156
} else {
140-
const [mid1, mid2] = getMidPoints(cur, key, orientation, r)
157+
const [mid1, mid2] = getMidPoints(cornerPoint, key, orientation, r)
141158
if (mid1 && mid2) {
142-
path += `L ${mid1[0]} ${mid1[1]} Q ${cur[0]} ${cur[1]} ${mid2[0]} ${mid2[1]}`
143-
;[cur[0], cur[1]] = mid2
159+
path += `L ${mid1[0]} ${mid1[1]} Q ${cornerPoint[0]} ${cornerPoint[1]} ${mid2[0]} ${mid2[1]}`
160+
;[cornerPoint[0], cornerPoint[1]] = mid2
144161
}
145162
}
146163
return path

0 commit comments

Comments
 (0)