@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/dist/linker.cjs +458 -49
- package/dist/linker.esm.js +459 -50
- package/dist/linker.esm.min.js +1 -1
- package/dist/linker.esm.min.js.map +1 -1
- package/dist/linker.js +430 -49
- package/dist/linker.min.cjs +1 -1
- package/dist/linker.min.cjs.map +1 -1
- package/dist/linker.min.js +1 -1
- package/dist/linker.min.js.map +1 -1
- package/package.json +1 -1
- package/src/Linker.ts +72 -57
- package/src/helper/drawPolyline.ts +456 -0
- package/types/index.d.ts +2 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
half.y = max(minCtrl, half.y)
|
|
244
|
+
}
|
|
234
245
|
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
265
|
+
const len = path.length
|
|
266
|
+
if (endDirection) {
|
|
256
267
|
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
const endCtrlPoint: IPointData = endDirection ? getCtrlPoint(this, endNode, endPoint, endDirection, half) : endPoint
|
|
281
|
+
} else {
|
|
271
282
|
|
|
272
|
-
|
|
273
|
-
|
|
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;
|