Skip to content

Commit be975be

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

3 files changed

Lines changed: 90 additions & 38 deletions

File tree

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,
@@ -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)