Skip to content

Commit 1bec243

Browse files
committed
fix: 修复折线边拖拽过程中偶现斜线问题&圆角旁边出现突出线段问题
1 parent f0e1d5e commit 1bec243

File tree

3 files changed

+90
-38
lines changed

3 files changed

+90
-38
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,9 @@ export class PolylineEdgeModel extends BaseEdgeModel {
504504
this.offset || 0,
505505
)
506506
this.pointsList = this.orthogonalizePath(pointsList)
507-
this.points = pointsList.map((point) => `${point.x},${point.y}`).join(' ')
507+
this.points = this.pointsList
508+
.map((point) => `${point.x},${point.y}`)
509+
.join(' ')
508510
}
509511

510512
@action

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,
@@ -377,7 +386,11 @@ export const isSegmentCrossingBBox = (
377386
)
378387
}
379388

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

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

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

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

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