@leafer-in/linker 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Linker.ts CHANGED
@@ -1,12 +1,13 @@
1
- import { ILinker, ILinkerInputData, ILinkerData, ILinkerPointData, ILinkerPointOptions, ILinkerComputedPointData, ILinkerPointType, IPointData, IUI, IDirection4, ILeaf, IPointsCurve, IPointsCurveData } from '@leafer-ui/interface'
2
- import { registerUI, dataProcessor, PathCommandDataHelper, pathType, isString, BoundsEvent, Line, PointHelper, BoundsHelper, LeafHelper, isObject } from '@leafer-ui/draw'
1
+ import { ILinker, ILinkerInputData, ILinkerData, ILineType, ILinkerPointData, ILinkerPointOptions, ILinkerComputedPointData, ILinkerPointType, IPointData, IUI, IDirection4, ILeaf, IPointsCurve, IPointsCurveData } from '@leafer-ui/interface'
2
+ import { registerUI, dataProcessor, PathCommandDataHelper, pathType, isString, BoundsEvent, Line, PointHelper, BoundsHelper, LeafHelper, isObject, boundsType } from '@leafer-ui/draw'
3
3
 
4
4
  import { LinkerData } from './data/LinkerData'
5
5
  import { LinkerHelper } from './helper/LinkerHelper'
6
6
  import { linkerPointType } from './decorator'
7
+ import { drawPolyline } from './helper/drawPolyline'
7
8
 
8
9
 
9
- const { moveTo, bezierCurveTo, drawPoints } = PathCommandDataHelper
10
+ const { moveTo, lineTo, bezierCurveTo, drawPoints } = PathCommandDataHelper
10
11
  const { isEmpty, getPoint } = BoundsHelper
11
12
  const { getDistance } = PointHelper
12
13
  const { getCtrlPoint, getDirectionForFromPoint, isOppositeDirection } = LinkerHelper
@@ -21,6 +22,9 @@ export class Linker extends Line implements ILinker {
21
22
  @dataProcessor(LinkerData)
22
23
  declare public __: ILinkerData
23
24
 
25
+ @boundsType('curve')
26
+ public lineType?: ILineType
27
+
24
28
  @linkerPointType()
25
29
  public startPoint?: ILinkerPointData
26
30
 
@@ -133,7 +137,7 @@ export class Linker extends Line implements ILinker {
133
137
  }
134
138
 
135
139
  public updateLinkerPoint(type: ILinkerPointType) {
136
- const { startPoint, endPoint } = this
140
+ const { startPoint, endPoint, lineType } = this
137
141
  const nowPoint = type === 'end' ? endPoint : startPoint
138
142
  if (nowPoint) {
139
143
  const data = {} as ILinkerComputedPointData
@@ -167,6 +171,7 @@ export class Linker extends Line implements ILinker {
167
171
  if (isEmpty(boxBounds)) LeafHelper.updateBounds(ui) // 防止线条位于元素底部,先于元素布局
168
172
  p = getPoint(boxBounds, point, true)
169
173
  ui.getWorldPointByBox(p, this, false, true)
174
+ if (lineType === 'polyline') data.bounds = ui.getBounds('box', this.parent)
170
175
  }
171
176
  } else {
172
177
  if (isObject(point)) p = point
@@ -203,77 +208,87 @@ export class Linker extends Line implements ILinker {
203
208
  const startPoint = startData.point, endPoint = endData.point
204
209
  const startDirection = startData.direction, endDirection = endData.direction
205
210
 
206
- const half: IPointData = {
207
- x: abs((startPoint.x - endPoint.x) / 2),
208
- y: abs((startPoint.y - endPoint.y) / 2)
209
- }
210
-
211
-
212
211
  const arrowWidth = this.__.strokeWidth * 8
213
212
 
214
- let minCtrl: number
213
+ switch (data.lineType) {
214
+ case 'straight':
215
+ moveTo(path, startPoint.x, startPoint.y)
216
+ lineTo(path, endPoint.x, endPoint.y)
217
+ break
218
+ case 'polyline':
219
+ drawPolyline(path, startPoint, endPoint, startDirection, endDirection, startData.bounds, endData.bounds, arrowWidth, hasPoints && PointHelper.toNumberPoints(data.points))
220
+ break
221
+ case 'curve':
222
+ default:
223
+ const half: IPointData = {
224
+ x: abs((startPoint.x - endPoint.x) / 2),
225
+ y: abs((startPoint.y - endPoint.y) / 2)
226
+ }
215
227
 
216
- if (this.__.__useArrow) {
217
- const ratio = min(getDistance(startPoint, endPoint) / (arrowWidth * 25), 1) // 控制add 手柄的长度与两点间的距离有关
218
- minCtrl = max(arrowWidth * ratio * 12, arrowWidth * 3,) // 控制手柄最少不能低于3个箭头的宽度
219
- } else {
220
- minCtrl = arrowWidth * 2
221
- }
228
+ let minCtrl: number
222
229
 
223
- if (endDirection) {
230
+ if (this.__.__useArrow) {
231
+ const ratio = min(getDistance(startPoint, endPoint) / (arrowWidth * 25), 1) // 控制add 手柄的长度与两点间的距离有关
232
+ minCtrl = max(arrowWidth * ratio * 12, arrowWidth * 3,) // 控制手柄最少不能低于3个箭头的宽度
233
+ } else {
234
+ minCtrl = arrowWidth * 2
235
+ }
224
236
 
225
- if (isOppositeDirection(startDirection, endDirection) && startDirection === getDirectionForFromPoint(startPoint, endPoint)) // 正好处于相反的位置, 不需要太长的控制手柄
226
- {
227
- minCtrl = arrowWidth * 2
228
- }
237
+ if (endDirection) {
229
238
 
230
- }
239
+ if (isOppositeDirection(startDirection, endDirection) && startDirection === getDirectionForFromPoint(startPoint, endPoint)) // 正好处于相反的位置, 不需要太长的控制手柄
240
+ {
241
+ minCtrl = arrowWidth * 2
242
+ }
231
243
 
232
- half.x = max(minCtrl, half.x)
233
- half.y = max(minCtrl, half.y)
244
+ }
234
245
 
235
- if (hasPoints) {
236
- minCtrl *= 0.6
237
- const points = PointHelper.toNumberPoints(data.points)
238
- drawPoints(path, [startPoint.x, startPoint.y, ...points, endPoint.x, endPoint.y], data.curve, false, data)
246
+ half.x = max(minCtrl, half.x)
247
+ half.y = max(minCtrl, half.y)
239
248
 
240
- if (startDirection) {
241
- const handleX = path[4]
242
- const handleY = path[5]
249
+ if (hasPoints) {
250
+ minCtrl *= 0.6
251
+ const points = PointHelper.toNumberPoints(data.points)
252
+ drawPoints(path, [startPoint.x, startPoint.y, ...points, endPoint.x, endPoint.y], data.curve, false, data)
243
253
 
244
- half.x = max(minCtrl, abs(handleX - startPoint.x))
245
- half.y = max(minCtrl, abs(handleY - startPoint.y))
246
- const startCtrlPoint: IPointData = getCtrlPoint(this, startNode, startPoint, startDirection, half)
247
- path[4] = startCtrlPoint.x
248
- path[5] = startCtrlPoint.y
249
- }
254
+ if (startDirection) {
255
+ const handleX = path[4]
256
+ const handleY = path[5]
250
257
 
251
- const len = path.length
252
- if (endDirection) {
258
+ half.x = max(minCtrl, abs(handleX - startPoint.x))
259
+ half.y = max(minCtrl, abs(handleY - startPoint.y))
260
+ const startCtrlPoint: IPointData = getCtrlPoint(this, startNode, startPoint, startDirection, half)
261
+ path[4] = startCtrlPoint.x
262
+ path[5] = startCtrlPoint.y
263
+ }
253
264
 
254
- const handleX = path[len - 4]
255
- const handleY = path[len - 3]
265
+ const len = path.length
266
+ if (endDirection) {
256
267
 
257
- half.x = max(minCtrl, abs(handleX - endPoint.x))
258
- half.y = max(minCtrl, abs(handleY - endPoint.y))
259
- const endCtrlPoint: IPointData = getCtrlPoint(this, endNode, endPoint, endDirection, half)
260
- path[len - 4] = endCtrlPoint.x
261
- path[len - 3] = endCtrlPoint.y
262
- } else {
263
- path[len - 4] = endPoint.x
264
- path[len - 3] = endPoint.y
265
- }
268
+ const handleX = path[len - 4]
269
+ const handleY = path[len - 3]
266
270
 
267
- } else {
271
+ half.x = max(minCtrl, abs(handleX - endPoint.x))
272
+ half.y = max(minCtrl, abs(handleY - endPoint.y))
273
+ const endCtrlPoint: IPointData = getCtrlPoint(this, endNode, endPoint, endDirection, half)
274
+ path[len - 4] = endCtrlPoint.x
275
+ path[len - 3] = endCtrlPoint.y
276
+ } else {
277
+ path[len - 4] = endPoint.x
278
+ path[len - 3] = endPoint.y
279
+ }
268
280
 
269
- const startCtrlPoint: IPointData = startDirection ? getCtrlPoint(this, startNode, startPoint, startDirection, half) : startPoint
270
- const endCtrlPoint: IPointData = endDirection ? getCtrlPoint(this, endNode, endPoint, endDirection, half) : endPoint
281
+ } else {
271
282
 
272
- moveTo(path, startPoint.x, startPoint.y)
273
- bezierCurveTo(path, startCtrlPoint.x, startCtrlPoint.y, endCtrlPoint.x, endCtrlPoint.y, endPoint.x, endPoint.y)
274
- }
283
+ const startCtrlPoint: IPointData = startDirection ? getCtrlPoint(this, startNode, startPoint, startDirection, half) : startPoint
284
+ const endCtrlPoint: IPointData = endDirection ? getCtrlPoint(this, endNode, endPoint, endDirection, half) : endPoint
285
+
286
+ moveTo(path, startPoint.x, startPoint.y)
287
+ bezierCurveTo(path, startCtrlPoint.x, startCtrlPoint.y, endCtrlPoint.x, endCtrlPoint.y, endPoint.x, endPoint.y)
288
+ }
275
289
 
276
290
 
291
+ }
277
292
  }
278
293
 
279
294
  public createStartPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData {
@@ -0,0 +1,456 @@
1
+ import { IDirection, IPointData, IBoundsData } from '@leafer-ui/interface'
2
+ import { AroundHelper, PathCommandDataHelper } from '@leafer-ui/draw'
3
+
4
+ const { directionData } = AroundHelper
5
+ const { moveTo, lineTo } = PathCommandDataHelper
6
+
7
+ const POLYLINE_X = 0, POLYLINE_Y = 1
8
+ const POLYLINE_EPSILON = 0.001
9
+ const POLYLINE_MIN_SEGMENT = 4
10
+ const POLYLINE_TURN_WEIGHT = 1.5
11
+
12
+ function getPolylineDirectionIndex(direction: IDirection): number {
13
+ switch (direction) {
14
+ case 'top-left': return 0
15
+ case 'top': return 1
16
+ case 'top-right': return 2
17
+ case 'right': return 3
18
+ case 'bottom-right': return 4
19
+ case 'bottom': return 5
20
+ case 'bottom-left': return 6
21
+ case 'left': return 7
22
+ default: return 8
23
+ }
24
+ }
25
+
26
+ function getPolylinePorts(direction: IDirection, point: IPointData, target: IPointData, bounds?: IBoundsData, targetBounds?: IBoundsData): number[] {
27
+ const data = directionData[getPolylineDirectionIndex(direction)]
28
+ const dx = Math.sign(data.x - 0.5), dy = Math.sign(data.y - 0.5)
29
+ const cx = bounds ? bounds.x + bounds.width / 2 : point.x
30
+ const cy = bounds ? bounds.y + bounds.height / 2 : point.y
31
+ const tx = targetBounds ? targetBounds.x + targetBounds.width / 2 : target.x
32
+ const ty = targetBounds ? targetBounds.y + targetBounds.height / 2 : target.y
33
+ const deltaX = tx - cx, deltaY = ty - cy
34
+ const xPort = (dx || (deltaX >= 0 ? 1 : -1)) > 0 ? 1 : 0
35
+ const yPort = (dy || (deltaY >= 0 ? 1 : -1)) > 0 ? 3 : 2
36
+
37
+ if (dx && !dy) return [xPort]
38
+ if (dy && !dx) return [yPort]
39
+ return Math.abs(deltaX) >= Math.abs(deltaY) ? [xPort, yPort] : [yPort, xPort]
40
+ }
41
+
42
+ function getPolylineExit(point: IPointData, bounds: IBoundsData | undefined, port: number, clearance: number): IPointData {
43
+ const axis = port >> 1, sign = port & 1 ? 1 : -1
44
+
45
+ if (axis === POLYLINE_X) {
46
+ const x = bounds ? bounds.x + (sign > 0 ? bounds.width : 0) : point.x
47
+ return { x: x + sign * clearance, y: point.y }
48
+ }
49
+
50
+ const y = bounds ? bounds.y + (sign > 0 ? bounds.height : 0) : point.y
51
+ return { x: point.x, y: y + sign * clearance }
52
+ }
53
+
54
+ function isPolylinePointBlocked(x: number, y: number, bounds: IBoundsData | undefined, clearance: number): boolean {
55
+ if (!bounds) return false
56
+ const left = bounds.x - clearance, right = bounds.x + bounds.width + clearance
57
+ const top = bounds.y - clearance, bottom = bounds.y + bounds.height + clearance
58
+ return x > left + POLYLINE_EPSILON && x < right - POLYLINE_EPSILON && y > top + POLYLINE_EPSILON && y < bottom - POLYLINE_EPSILON
59
+ }
60
+
61
+ function isPolylineSegmentBlocked(x1: number, y1: number, x2: number, y2: number, bounds: IBoundsData | undefined, clearance: number): boolean {
62
+ if (!bounds) return false
63
+ const left = bounds.x - clearance, right = bounds.x + bounds.width + clearance
64
+ const top = bounds.y - clearance, bottom = bounds.y + bounds.height + clearance
65
+
66
+ if (Math.abs(y1 - y2) <= POLYLINE_EPSILON) {
67
+ return y1 > top + POLYLINE_EPSILON && y1 < bottom - POLYLINE_EPSILON &&
68
+ Math.max(Math.min(x1, x2), left + POLYLINE_EPSILON) < Math.min(Math.max(x1, x2), right - POLYLINE_EPSILON)
69
+ }
70
+
71
+ if (Math.abs(x1 - x2) <= POLYLINE_EPSILON) {
72
+ return x1 > left + POLYLINE_EPSILON && x1 < right - POLYLINE_EPSILON &&
73
+ Math.max(Math.min(y1, y2), top + POLYLINE_EPSILON) < Math.min(Math.max(y1, y2), bottom - POLYLINE_EPSILON)
74
+ }
75
+
76
+ return true
77
+ }
78
+
79
+ function addPolylineBoundsCoordinates(xs: number[], ys: number[], bounds: IBoundsData | undefined, clearance: number, margin: number): void {
80
+ if (!bounds) return
81
+ const left = bounds.x - clearance, right = bounds.x + bounds.width + clearance
82
+ const top = bounds.y - clearance, bottom = bounds.y + bounds.height + clearance
83
+ xs.push(left, right, left - margin, right + margin)
84
+ ys.push(top, bottom, top - margin, bottom + margin)
85
+ }
86
+
87
+ function sortPolylineCoordinates(values: number[]): void {
88
+ values.sort((a, b) => a - b)
89
+ let write = 0
90
+
91
+ for (let i = 0; i < values.length; i++) {
92
+ if (!write || Math.abs(values[i] - values[write - 1]) > POLYLINE_EPSILON) values[write++] = values[i]
93
+ }
94
+
95
+ values.length = write
96
+ }
97
+
98
+ function getPolylineCoordinateIndex(values: number[], value: number): number {
99
+ for (let i = 0; i < values.length; i++) {
100
+ if (Math.abs(values[i] - value) <= POLYLINE_EPSILON) return i
101
+ }
102
+ return -1
103
+ }
104
+
105
+ // 根据两端连接方向确定中点转折轴
106
+ function getPolylineMidpointAxis(startPort: number, endPort: number, sx: number, sy: number, ex: number, ey: number): number {
107
+ const startAxis = startPort >> 1, endAxis = endPort >> 1
108
+ const startSign = startPort & 1 ? 1 : -1, endSign = endPort & 1 ? 1 : -1
109
+
110
+ if (startAxis === endAxis && startSign === -endSign) return startAxis
111
+ return Math.abs(ex - sx) >= Math.abs(ey - sy) ? POLYLINE_X : POLYLINE_Y
112
+ }
113
+
114
+ function isPolylinePortAligned(port: number, stub: number, dx: number, dy: number): boolean {
115
+ const offset = (port >> 1) === POLYLINE_X ? dx : dy
116
+ const sign = port & 1 ? 1 : -1
117
+ return Math.abs(offset) <= POLYLINE_EPSILON ? stub > POLYLINE_EPSILON : offset * sign > 0
118
+ }
119
+
120
+ function isPolylineMidpointAligned(port: number | undefined, stub: number, dx: number, dy: number, axis: number): boolean {
121
+ if (port === undefined) return true
122
+
123
+ // 取中点路径中实际存在的第一段,忽略重合的出口和转折点
124
+ if (axis === POLYLINE_X && Math.abs(dx / 2) > POLYLINE_EPSILON) return isPolylinePortAligned(port, stub, dx, 0)
125
+ if (axis === POLYLINE_Y && Math.abs(dy / 2) > POLYLINE_EPSILON) return isPolylinePortAligned(port, stub, 0, dy)
126
+ if (Math.abs(dx) <= POLYLINE_EPSILON && Math.abs(dy) <= POLYLINE_EPSILON) return true
127
+ return isPolylinePortAligned(port, stub, dx, dy)
128
+ }
129
+
130
+ // 检查实际接入方向,允许顺向接入或垂直转向,禁止与后续线段原路折返
131
+ function isPolylineApproachAligned(points: number[], port?: number): boolean {
132
+ if (port === undefined) return true
133
+
134
+ const last = points.length - 2, x = points[last], y = points[last + 1]
135
+
136
+ for (let i = last - 2; i >= 0; i -= 2) {
137
+ const dx = points[i] - x, dy = points[i + 1] - y
138
+ if (Math.abs(dx) > POLYLINE_EPSILON || Math.abs(dy) > POLYLINE_EPSILON) return isPolylinePortAligned(port, 1, dx, dy)
139
+ }
140
+
141
+ return true
142
+ }
143
+
144
+ function getPolylineRoute(result: number[], startPoint: IPointData, endPoint: IPointData, startPort: number, endPort: number | undefined, startBounds: IBoundsData | undefined, endBounds: IBoundsData | undefined, clearance: number, strictPorts: boolean, approachPort?: number): number {
145
+ const startExit = getPolylineExit(startPoint, startBounds, startPort, clearance)
146
+ const endExit = endPort === undefined ? endPoint : getPolylineExit(endPoint, endBounds, endPort, clearance)
147
+ const { x: sx, y: sy } = startExit, { x: ex, y: ey } = endExit
148
+ const startStub = Math.abs(sx - startPoint.x) + Math.abs(sy - startPoint.y)
149
+ const endStub = Math.abs(ex - endPoint.x) + Math.abs(ey - endPoint.y)
150
+
151
+ if (isPolylineSegmentBlocked(startPoint.x, startPoint.y, sx, sy, endBounds, clearance) ||
152
+ isPolylineSegmentBlocked(endPoint.x, endPoint.y, ex, ey, startBounds, clearance)) return Infinity
153
+
154
+ if (endPort === undefined && (isPolylinePointBlocked(ex, ey, startBounds, clearance) ||
155
+ isPolylinePointBlocked(ex, ey, endBounds, clearance))) return Infinity
156
+
157
+ const axis = endPort === undefined ? startPort >> 1 : getPolylineMidpointAxis(startPort, endPort, sx, sy, ex, ey)
158
+ const dx = ex - sx, dy = ey - sy
159
+ result.length = 0
160
+ result.push(startPoint.x, startPoint.y, sx, sy)
161
+
162
+ if ((!strictPorts || (isPolylineMidpointAligned(startPort, startStub, dx, dy, axis) &&
163
+ isPolylineMidpointAligned(endPort, endStub, -dx, -dy, axis))) &&
164
+ isPolylineMidpointAligned(approachPort, 1, -dx, -dy, axis)) {
165
+ const mx = (sx + ex) / 2, my = (sy + ey) / 2
166
+ if (axis === POLYLINE_X) result.push(mx, sy, mx, ey)
167
+ else result.push(sx, my, ex, my)
168
+ result.push(ex, ey, endPoint.x, endPoint.y)
169
+
170
+ // 两端的出线段可以进入各自所属元素
171
+ for (let i = 4; i < result.length - 2; i += 2) {
172
+ if (isPolylineSegmentBlocked(result[i - 2], result[i - 1], result[i], result[i + 1], startBounds, clearance) ||
173
+ isPolylineSegmentBlocked(result[i - 2], result[i - 1], result[i], result[i + 1], endBounds, clearance)) {
174
+ result.length = 4
175
+ break
176
+ }
177
+ }
178
+
179
+ // 兼顾出口与途径点重合的情况
180
+ if (result.length > 4 && isPolylineApproachAligned(result, approachPort)) return getPolylineScore(result, clearance)
181
+ result.length = 4
182
+ }
183
+
184
+ const score = findPolylineGridRoute(result, startExit, endExit, startPort, endPort, startBounds, endBounds, clearance, axis, startStub, endStub, strictPorts, approachPort)
185
+ if (score === Infinity) return score
186
+
187
+ result.push(endPoint.x, endPoint.y)
188
+ return isPolylineApproachAligned(result, approachPort) ? score + startStub + endStub : Infinity
189
+ }
190
+
191
+ function findPolylineGridRoute(result: number[], startExit: IPointData, endExit: IPointData, startPort: number, endPort: number | undefined, startBounds: IBoundsData | undefined, endBounds: IBoundsData | undefined, clearance: number, midpointAxis: number, startStub: number, endStub: number, strictPorts: boolean, approachPort?: number): number {
192
+ const { x: sx, y: sy } = startExit, { x: ex, y: ey } = endExit
193
+ const margin = Math.max(clearance, POLYLINE_MIN_SEGMENT) * 2
194
+ const xs: number[] = [sx, ex, (sx + ex) / 2], ys: number[] = [sy, ey, (sy + ey) / 2]
195
+
196
+ addPolylineBoundsCoordinates(xs, ys, startBounds, clearance, margin)
197
+ addPolylineBoundsCoordinates(xs, ys, endBounds, clearance, margin)
198
+
199
+ xs.push(Math.min(...xs) - margin * 2, Math.max(...xs) + margin * 2)
200
+ ys.push(Math.min(...ys) - margin * 2, Math.max(...ys) + margin * 2)
201
+
202
+ sortPolylineCoordinates(xs)
203
+ sortPolylineCoordinates(ys)
204
+
205
+ const nx = xs.length, ny = ys.length
206
+ const startXIndex = getPolylineCoordinateIndex(xs, sx), startYIndex = getPolylineCoordinateIndex(ys, sy)
207
+ const endXIndex = getPolylineCoordinateIndex(xs, ex), endYIndex = getPolylineCoordinateIndex(ys, ey)
208
+
209
+ if (startXIndex < 0 || startYIndex < 0 || endXIndex < 0 || endYIndex < 0) return Infinity
210
+
211
+ const startNode = startYIndex * nx + startXIndex, endNode = endYIndex * nx + endXIndex
212
+ const total = nx * ny * 3
213
+ const distance = new Float64Array(total), previous = new Int32Array(total), visited = new Uint8Array(total)
214
+
215
+ distance.fill(Infinity)
216
+ previous.fill(-1)
217
+
218
+ const startState = startNode * 3
219
+ distance[startState] = 0
220
+ const pending = [startState]
221
+
222
+ const turnCost = Math.max(POLYLINE_MIN_SEGMENT, clearance * POLYLINE_TURN_WEIGHT)
223
+ const preferredCoordinate = midpointAxis === POLYLINE_X ? (sx + ex) / 2 : (sy + ey) / 2
224
+ let currentState = -1, currentNode = -1, currentAxis = 0
225
+ let currentX = 0, currentY = 0, currentDistance = 0
226
+
227
+ const relax = (nextXIndex: number, nextYIndex: number, axis: number): void => {
228
+ const nextX = xs[nextXIndex], nextY = ys[nextYIndex]
229
+ const nextNode = nextYIndex * nx + nextXIndex
230
+ const dx = nextX - currentX, dy = nextY - currentY
231
+
232
+ if (nextNode === startNode) return
233
+
234
+ // 接入约束独立于端口约束,放宽端口时仍禁止在途径点处折返
235
+ if (approachPort !== undefined && nextNode === endNode && !isPolylinePortAligned(approachPort, 1, -dx, -dy)) return
236
+
237
+ if (strictPorts && ((currentState === startState && !isPolylinePortAligned(startPort, startStub, dx, dy)) ||
238
+ (endPort !== undefined && nextNode === endNode && !isPolylinePortAligned(endPort, endStub, -dx, -dy)))) return
239
+
240
+ if (isPolylinePointBlocked(nextX, nextY, startBounds, clearance) ||
241
+ isPolylinePointBlocked(nextX, nextY, endBounds, clearance) ||
242
+ isPolylineSegmentBlocked(currentX, currentY, nextX, nextY, startBounds, clearance) ||
243
+ isPolylineSegmentBlocked(currentX, currentY, nextX, nextY, endBounds, clearance)) return
244
+
245
+ const nextState = nextNode * 3 + axis
246
+ if (visited[nextState]) return
247
+
248
+ let cost = currentDistance + (Math.abs(dx) + Math.abs(dy))
249
+
250
+ if (currentAxis && currentAxis !== axis) {
251
+ cost += turnCost
252
+ const turnCoordinate = currentAxis === POLYLINE_X + 1 ? currentX : currentY
253
+ if (Math.abs(turnCoordinate - preferredCoordinate) > POLYLINE_EPSILON) cost += turnCost * 2
254
+ }
255
+
256
+ if (cost < distance[nextState]) {
257
+ if (distance[nextState] === Infinity) pending.push(nextState)
258
+ distance[nextState] = cost
259
+ previous[nextState] = currentState
260
+ }
261
+ }
262
+
263
+ let bestState = -1
264
+
265
+ while (pending.length) {
266
+ let index = 0, state = pending[0]
267
+ for (let i = 1; i < pending.length; i++) {
268
+ const next = pending[i]
269
+ if (distance[next] < distance[state] || (distance[next] === distance[state] && next < state)) {
270
+ index = i
271
+ state = next
272
+ }
273
+ }
274
+
275
+ pending[index] = pending[pending.length - 1]
276
+ pending.pop()
277
+ currentState = state
278
+ currentNode = Math.floor(state / 3)
279
+ currentAxis = state % 3
280
+ currentDistance = distance[state]
281
+ visited[state] = 1
282
+
283
+ if (currentNode === endNode) {
284
+ bestState = state
285
+ break
286
+ }
287
+
288
+ const xIndex = currentNode % nx, yIndex = Math.floor(currentNode / nx)
289
+ currentX = xs[xIndex]
290
+ currentY = ys[yIndex]
291
+
292
+ if (xIndex > 0) relax(xIndex - 1, yIndex, POLYLINE_X + 1)
293
+ if (xIndex < nx - 1) relax(xIndex + 1, yIndex, POLYLINE_X + 1)
294
+ if (yIndex > 0) relax(xIndex, yIndex - 1, POLYLINE_Y + 1)
295
+ if (yIndex < ny - 1) relax(xIndex, yIndex + 1, POLYLINE_Y + 1)
296
+ }
297
+
298
+ if (bestState < 0) return Infinity
299
+
300
+ const reverse: number[] = []
301
+ let state = bestState
302
+
303
+ while (state >= 0) {
304
+ const node = Math.floor(state / 3)
305
+ reverse.push(xs[node % nx], ys[Math.floor(node / nx)])
306
+ if (state === startState) break
307
+ state = previous[state]
308
+ }
309
+
310
+ if (state !== startState) return Infinity
311
+
312
+ for (let i = reverse.length - 4; i >= 0; i -= 2) result.push(reverse[i], reverse[i + 1])
313
+ return distance[bestState]
314
+ }
315
+
316
+ // 锚点或途径点被元素覆盖时,保留一条尽量少穿过元素的临时接线
317
+ function getPolylineFallback(start: IPointData, end: IPointData, port: number, startBounds?: IBoundsData, endBounds?: IBoundsData): number[] {
318
+ const { x: sx, y: sy } = start, { x: ex, y: ey } = end
319
+ if (sx === ex || sy === ey) return [sx, sy, ex, ey]
320
+
321
+ const horizontal = (port >> 1) === POLYLINE_X
322
+ const crosses = (x: number, y: number): number => {
323
+ let count = 0
324
+ if (isPolylineSegmentBlocked(sx, sy, x, y, startBounds, 0)) count++
325
+ if (isPolylineSegmentBlocked(x, y, ex, ey, startBounds, 0)) count++
326
+ if (isPolylineSegmentBlocked(sx, sy, x, y, endBounds, 0)) count++
327
+ if (isPolylineSegmentBlocked(x, y, ex, ey, endBounds, 0)) count++
328
+ return count
329
+ }
330
+ const xy = crosses(ex, sy), yx = crosses(sx, ey)
331
+ return xy < yx || (xy === yx && horizontal) ? [sx, sy, ex, sy, ex, ey] : [sx, sy, sx, ey, ex, ey]
332
+ }
333
+
334
+ function getBestPolyline(startPoint: IPointData, endPoint: IPointData, startDirection: IDirection, endDirection: IDirection | undefined, startBounds: IBoundsData | undefined, endBounds: IBoundsData | undefined, clearance: number, approachPort?: number): number[] {
335
+ const startPorts = getPolylinePorts(startDirection, startPoint, endPoint, startBounds, endDirection === undefined ? undefined : endBounds)
336
+ const endPorts = endDirection === undefined ? [undefined] : getPolylinePorts(endDirection, endPoint, startPoint, endBounds, startBounds)
337
+ let candidate: number[] = [], best: number[] = [], bestScore = Infinity
338
+
339
+ // 先保留方向并减小间距,再允许贴边转向
340
+ for (let pass = clearance ? 0 : 1; pass < 3 && !best.length; pass++) {
341
+ const gap = pass ? 0 : clearance
342
+ for (let i = 0; i < startPorts.length; i++) {
343
+ for (let j = 0; j < endPorts.length; j++) {
344
+ const score = getPolylineRoute(candidate, startPoint, endPoint, startPorts[i], endPorts[j], startBounds, endBounds, gap, pass < 2, approachPort)
345
+ if (score < bestScore) {
346
+ bestScore = score
347
+ const previous = best
348
+ best = candidate
349
+ candidate = previous
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ if (!best.length) return getPolylineFallback(startPoint, endPoint, startPorts[0], startBounds, endBounds)
356
+ simplifyPolyline(best)
357
+ const last = best.length - 2
358
+ if (best[last] !== endPoint.x || best[last + 1] !== endPoint.y) best.push(endPoint.x, endPoint.y)
359
+ return best
360
+ }
361
+
362
+ function getPolylineEditSide(point: IPointData, waypoint: IPointData, direction: IDirection, bounds: IBoundsData | undefined, otherBounds: IBoundsData | undefined, clearance: number, points: number[], index: number, step: number): number[] {
363
+ if (point.x === waypoint.x && point.y === waypoint.y) return []
364
+
365
+ let approachPort: number | undefined
366
+
367
+ // 跳过重合途径点,根据后续线段确定接入方向
368
+ for (let i = index + step; i >= 0 && i < points.length; i += step) {
369
+ const dx = points[i] - waypoint.x, dy = points[i + 1] - waypoint.y
370
+ if (Math.abs(dx) <= POLYLINE_EPSILON && Math.abs(dy) <= POLYLINE_EPSILON) continue
371
+
372
+ if (Math.abs(dy) <= POLYLINE_EPSILON) approachPort = dx > 0 ? 0 : 1
373
+ else if (Math.abs(dx) <= POLYLINE_EPSILON) approachPort = dy > 0 ? 2 : 3
374
+ break
375
+ }
376
+
377
+ const side = getBestPolyline(point, waypoint, direction, undefined, bounds, otherBounds, clearance, approachPort)
378
+ side.length -= 2
379
+ return side
380
+ }
381
+
382
+ function drawPolylineEditPath(path: number[], startPoint: IPointData, endPoint: IPointData, startDirection: IDirection, endDirection: IDirection, startBounds: IBoundsData | undefined, endBounds: IBoundsData | undefined, clearance: number, points: number[]): boolean {
383
+ const length = points.length, last = length - 2
384
+ if (length % 2) return false
385
+ for (let i = 0; i < length; i++) if (!Number.isFinite(points[i])) return false
386
+
387
+ const start = getPolylineEditSide(startPoint, { x: points[0], y: points[1] }, startDirection, startBounds, endBounds, clearance, points, 0, 2)
388
+ const end = getPolylineEditSide(endPoint, { x: points[last], y: points[last + 1] }, endDirection, endBounds, startBounds, clearance, points, last, -2)
389
+
390
+ // 只拼接两端生成的接线,中间顶点按原坐标写入
391
+ const first = start.length ? start : points
392
+ moveTo(path, first[0], first[1])
393
+ for (let i = 2; i < start.length; i += 2) lineTo(path, start[i], start[i + 1])
394
+ for (let i = start.length ? 0 : 2; i < length; i += 2) lineTo(path, points[i], points[i + 1])
395
+ for (let i = end.length - 2; i >= 0; i -= 2) lineTo(path, end[i], end[i + 1])
396
+ return true
397
+ }
398
+
399
+ function getPolylineScore(points: number[], clearance: number): number {
400
+ let length = 0, turns = 0, previousAxis = -1
401
+
402
+ for (let i = 2; i < points.length; i += 2) {
403
+ const dx = points[i] - points[i - 2], dy = points[i + 1] - points[i - 1]
404
+ if (Math.abs(dx) <= POLYLINE_EPSILON && Math.abs(dy) <= POLYLINE_EPSILON) continue
405
+
406
+ const axis = Math.abs(dx) > POLYLINE_EPSILON ? POLYLINE_X : POLYLINE_Y
407
+ length += Math.abs(dx) + Math.abs(dy)
408
+ if (previousAxis !== -1 && previousAxis !== axis) turns++
409
+ previousAxis = axis
410
+ }
411
+
412
+ return length + turns * Math.max(clearance, POLYLINE_MIN_SEGMENT) * POLYLINE_TURN_WEIGHT
413
+ }
414
+
415
+ function simplifyPolyline(points: number[]): void {
416
+ let write = 2
417
+
418
+ for (let read = 2; read < points.length; read += 2) {
419
+ const x = points[read], y = points[read + 1]
420
+ const px = points[write - 2], py = points[write - 1]
421
+
422
+ if (Math.abs(x - px) <= POLYLINE_EPSILON && Math.abs(y - py) <= POLYLINE_EPSILON) continue
423
+
424
+ if (write >= 4) {
425
+ const ax = points[write - 4], ay = points[write - 3]
426
+ const horizontal = Math.abs(ay - py) <= POLYLINE_EPSILON && Math.abs(py - y) <= POLYLINE_EPSILON && (px - ax) * (x - px) >= 0
427
+ const vertical = Math.abs(ax - px) <= POLYLINE_EPSILON && Math.abs(px - x) <= POLYLINE_EPSILON && (py - ay) * (y - py) >= 0
428
+
429
+ if (horizontal || vertical) {
430
+ points[write - 2] = x
431
+ points[write - 1] = y
432
+ continue
433
+ }
434
+ }
435
+
436
+ points[write++] = x
437
+ points[write++] = y
438
+ }
439
+
440
+ points.length = write
441
+ }
442
+
443
+ export function drawPolyline(path: number[], startPoint: IPointData, endPoint: IPointData, startDirection: IDirection, endDirection: IDirection, startBounds?: IBoundsData, endBounds?: IBoundsData, clearance = 8, points?: number[]): boolean {
444
+ if (!Number.isFinite(startPoint.x) || !Number.isFinite(startPoint.y) || !Number.isFinite(endPoint.x) || !Number.isFinite(endPoint.y)) return false
445
+
446
+ if (startBounds && (!Number.isFinite(startBounds.x) || !Number.isFinite(startBounds.y) || !Number.isFinite(startBounds.width) || !Number.isFinite(startBounds.height) || startBounds.width < 0 || startBounds.height < 0)) return false
447
+ if (endBounds && (!Number.isFinite(endBounds.x) || !Number.isFinite(endBounds.y) || !Number.isFinite(endBounds.width) || !Number.isFinite(endBounds.height) || endBounds.width < 0 || endBounds.height < 0)) return false
448
+
449
+ clearance = Number.isFinite(clearance) ? Math.max(0, clearance) : 8
450
+ if (points?.length) return drawPolylineEditPath(path, startPoint, endPoint, startDirection, endDirection, startBounds, endBounds, clearance, points)
451
+
452
+ const best = getBestPolyline(startPoint, endPoint, startDirection, endDirection, startBounds, endBounds, clearance)
453
+ moveTo(path, best[0], best[1])
454
+ for (let i = 2; i < best.length; i += 2) lineTo(path, best[i], best[i + 1])
455
+ return true
456
+ }
package/types/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { ILinker, ILinkerData, ILinkerPointData, IPointsCurve, IUI, ILinkerComputedPointData, ILinkerInputData, ILinkerPointType, IPointData, ILeaf, ILinkerPointOptions } from '@leafer-ui/interface';
1
+ import { ILinker, ILinkerData, ILineType, ILinkerPointData, IPointsCurve, IUI, ILinkerComputedPointData, ILinkerInputData, ILinkerPointType, IPointData, ILeaf, ILinkerPointOptions } from '@leafer-ui/interface';
2
2
  import { Line, LineData } from '@leafer-ui/draw';
3
3
 
4
4
  declare class Linker extends Line implements ILinker {
5
5
  get __tag(): string;
6
6
  __: ILinkerData;
7
+ lineType?: ILineType;
7
8
  startPoint?: ILinkerPointData;
8
9
  endPoint?: ILinkerPointData;
9
10
  curve?: IPointsCurve;