@leafer-in/linker 2.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023-present, Chao (Leafer) Wan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @leafer-in/linker
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@leafer-in/linker",
3
+ "version": "2.3.0",
4
+ "description": "@leafer-in/linker",
5
+ "author": "Chao (Leafer) Wan",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/linker.esm.js",
9
+ "exports": {
10
+ "import": "./dist/linker.esm.js",
11
+ "require": "./dist/linker.cjs",
12
+ "types": "./types/index.d.ts"
13
+ },
14
+ "types": "types/index.d.ts",
15
+ "unpkg": "dist/linker.js",
16
+ "jsdelivr": "dist/linker.js",
17
+ "files": [
18
+ "src",
19
+ "types",
20
+ "dist"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/leaferjs/leafer-in.git"
25
+ },
26
+ "homepage": "https://github.com/leaferjs/leafer-in/tree/main/packages/linker",
27
+ "bugs": "https://github.com/leaferjs/leafer-in/issues",
28
+ "keywords": [
29
+ "leafer linker",
30
+ "leafer-linker",
31
+ "leafer-in",
32
+ "linker",
33
+ "leafer-ui",
34
+ "leaferjs"
35
+ ],
36
+ "peerDependencies": {
37
+ "@leafer-ui/draw": "^2.0.0",
38
+ "@leafer-ui/interface": "^2.0.0"
39
+ }
40
+ }
package/src/Linker.ts ADDED
@@ -0,0 +1,355 @@
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'
3
+
4
+ import { LinkerData } from './data/LinkerData'
5
+ import { LinkerHelper } from './helper/LinkerHelper'
6
+ import { linkerPointType } from './decorator'
7
+
8
+
9
+ const { moveTo, bezierCurveTo, drawPoints } = PathCommandDataHelper
10
+ const { isEmpty, getPoint } = BoundsHelper
11
+ const { getDistance } = PointHelper
12
+ const { getCtrlPoint, getDirectionForFromPoint, isOppositeDirection } = LinkerHelper
13
+ const { max, min, abs } = Math
14
+
15
+
16
+ @registerUI()
17
+ export class Linker extends Line implements ILinker {
18
+
19
+ public get __tag() { return 'Linker' }
20
+
21
+ @dataProcessor(LinkerData)
22
+ declare public __: ILinkerData
23
+
24
+ @linkerPointType()
25
+ public startPoint?: ILinkerPointData
26
+
27
+ @linkerPointType()
28
+ public endPoint?: ILinkerPointData
29
+
30
+ @pathType({ type: 'C', value: 0.5 } as IPointsCurveData)
31
+ declare public curve?: IPointsCurve
32
+
33
+ public startNode: IUI
34
+ public endNode: IUI
35
+
36
+ public startData: ILinkerComputedPointData
37
+ public endData: ILinkerComputedPointData
38
+
39
+
40
+ constructor(data?: ILinkerInputData) {
41
+ super(data)
42
+ this.onStartNodeChange = this.onStartNodeChange.bind(this)
43
+ this.onEndNodeChange = this.onEndNodeChange.bind(this)
44
+ }
45
+
46
+ public __updatePath(): void {
47
+
48
+ const layout = this.__layout
49
+
50
+ const { startNode, endNode } = this
51
+
52
+ if (layout.linkerChanged) {
53
+ this.updateLinkerNode('start')
54
+ this.updateLinkerNode('end')
55
+
56
+ this.updateLinkerPoint('start')
57
+ this.updateLinkerPoint('end')
58
+
59
+ if (startNode !== this.startNode) this.listenLinkerNode(this.startNode, startNode, 'start')
60
+ if (endNode !== this.endNode) this.listenLinkerNode(this.endNode, endNode, 'end')
61
+
62
+ layout.linkerChanged = false
63
+ }
64
+
65
+ this.updateLinkerPath()
66
+ }
67
+
68
+ public listenLinkerNode(newNode: IUI, oldNode: IUI, type: ILinkerPointType): void {
69
+ if (oldNode) {
70
+ oldNode.off(BoundsEvent.WORLD, type === 'end' ? this.onEndNodeChange : this.onStartNodeChange)
71
+ const list = type === 'end' ? oldNode.endLinker : oldNode.startLinker
72
+ if (list) {
73
+ const index = list.indexOf(this)
74
+ if (index > -1) list.splice(index, 1)
75
+ }
76
+ }
77
+ if (newNode) {
78
+ newNode.on(BoundsEvent.WORLD, type === 'end' ? this.onEndNodeChange : this.onStartNodeChange)
79
+ const list = type === 'end' ? (newNode.endLinker || (newNode.endLinker = [])) : (newNode.startLinker || (newNode.startLinker = []))
80
+ list.push(this)
81
+ }
82
+ }
83
+
84
+ public onStartNodeChange(): void {
85
+ this.onLinkerNodeChange('start')
86
+ }
87
+
88
+ public onEndNodeChange(): void {
89
+ this.onLinkerNodeChange('end')
90
+ }
91
+
92
+ public onLinkerNodeChange(type: ILinkerPointType): void {
93
+ if (this.leaferIsCreated) {
94
+ const { layouter, zoomLayer } = this.leafer
95
+ if (layouter.__updatedList.has(zoomLayer)) return // 节流,画布缩放平移不用更新
96
+ layouter.addExtra(this, true)
97
+ }
98
+
99
+ const { startData, endData } = this
100
+ if (type == 'start') {
101
+ this.updateLinkerPoint('start')
102
+ if (endData && endData.auto) this.updateLinkerPoint('end')
103
+ } else {
104
+ if (startData && startData.auto) this.updateLinkerPoint('start')
105
+ this.updateLinkerPoint('end')
106
+ }
107
+
108
+ this.__layout.boxChange()
109
+ LeafHelper.updateBounds(this)
110
+ }
111
+
112
+ public updateLinkerNode(type: ILinkerPointType) {
113
+ const { startPoint, endPoint } = this
114
+ const nowPoint = type === 'end' ? endPoint : startPoint
115
+ if (nowPoint) {
116
+ const { id } = nowPoint
117
+ const ui = id && (isString(id) ? this.app.findId(id) : id)
118
+ type === 'end' ? this.endNode = ui : this.startNode = ui
119
+ }
120
+ }
121
+
122
+ public getLinkerNodeCenter(type: ILinkerPointType): IPointData {
123
+ const { startPoint, endPoint, startNode, endNode, startData, endData } = this
124
+ const node = type === 'end' ? endNode : startNode
125
+ const data = type === 'end' ? (endData || endPoint) : (startData || startPoint)
126
+ if (node) {
127
+ const { worldRenderBounds } = node
128
+ if (isEmpty(worldRenderBounds)) LeafHelper.updateBounds(node) // 防止线条位于元素底部,先于元素布局
129
+ return getPoint(node.worldRenderBounds, 'center')
130
+ } else {
131
+ return data && isObject(data.point) && this.getWorldPointByLocal(data.point)
132
+ }
133
+ }
134
+
135
+ public updateLinkerPoint(type: ILinkerPointType) {
136
+ const { startPoint, endPoint } = this
137
+ const nowPoint = type === 'end' ? endPoint : startPoint
138
+ if (nowPoint) {
139
+ const data = {} as ILinkerComputedPointData
140
+ const { id, offset } = nowPoint
141
+ let { direction, point } = nowPoint
142
+
143
+ if (direction === 'center' || (!direction && id && !point)) {
144
+ const start = this.getLinkerNodeCenter('start')
145
+ const end = this.getLinkerNodeCenter('end')
146
+ const from = type === 'end' ? end : start
147
+ const to = type === 'end' ? start : end
148
+ if (from && to) direction = getDirectionForFromPoint(from, to)
149
+ data.auto = true
150
+ }
151
+
152
+
153
+ if (!direction && isString(point)) direction = point as any as IDirection4
154
+
155
+
156
+ data.direction = direction
157
+
158
+ const ui = type === 'end' ? this.endNode : this.startNode
159
+
160
+ let p: IPointData
161
+
162
+ if (ui) {
163
+ if (!point) point = direction
164
+ if (point) {
165
+ const { boxBounds } = ui
166
+
167
+ if (isEmpty(boxBounds)) LeafHelper.updateBounds(ui) // 防止线条位于元素底部,先于元素布局
168
+ p = getPoint(boxBounds, point, true)
169
+ ui.getWorldPointByBox(p, this, false, true)
170
+ }
171
+ } else {
172
+ if (isObject(point)) p = point
173
+ }
174
+
175
+ if (p && offset) {
176
+ const o = { ...offset } as IPointData
177
+ if (!offset.x) o.x = 0
178
+ if (!offset.y) o.y = 0
179
+
180
+ if (ui) ui.getWorldPoint(o, this, true, true)
181
+ PointHelper.move(p, o)
182
+ }
183
+
184
+ data.point = p
185
+
186
+ type === 'end' ? this.endData = p && data : this.startData = p && data
187
+
188
+ }
189
+ }
190
+
191
+
192
+ public updateLinkerPath() {
193
+ const data = this.__
194
+ const path: number[] = data.path = []
195
+
196
+ const hasPoints = data.points && data.points.length
197
+
198
+ const { startData, endData, startNode, endNode } = this
199
+ if (!startData || !endData) {
200
+ return
201
+ }
202
+
203
+ const startPoint = startData.point, endPoint = endData.point
204
+ const startDirection = startData.direction, endDirection = endData.direction
205
+
206
+ const half: IPointData = {
207
+ x: abs((startPoint.x - endPoint.x) / 2),
208
+ y: abs((startPoint.y - endPoint.y) / 2)
209
+ }
210
+
211
+
212
+ const arrowWidth = this.__.strokeWidth * 8
213
+
214
+ let minCtrl: number
215
+
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
+ }
222
+
223
+ if (endDirection) {
224
+
225
+ if (isOppositeDirection(startDirection, endDirection) && startDirection === getDirectionForFromPoint(startPoint, endPoint)) // 正好处于相反的位置, 不需要太长的控制手柄
226
+ {
227
+ minCtrl = arrowWidth * 2
228
+ }
229
+
230
+ }
231
+
232
+ half.x = max(minCtrl, half.x)
233
+ half.y = max(minCtrl, half.y)
234
+
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)
239
+
240
+ if (startDirection) {
241
+ const handleX = path[4]
242
+ const handleY = path[5]
243
+
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
+ }
250
+
251
+ const len = path.length
252
+ if (endDirection) {
253
+
254
+ const handleX = path[len - 4]
255
+ const handleY = path[len - 3]
256
+
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
+ }
266
+
267
+ } else {
268
+
269
+ const startCtrlPoint: IPointData = startDirection ? getCtrlPoint(this, startNode, startPoint, startDirection, half) : startPoint
270
+ const endCtrlPoint: IPointData = endDirection ? getCtrlPoint(this, endNode, endPoint, endDirection, half) : endPoint
271
+
272
+ moveTo(path, startPoint.x, startPoint.y)
273
+ bezierCurveTo(path, startCtrlPoint.x, startCtrlPoint.y, endCtrlPoint.x, endCtrlPoint.y, endPoint.x, endPoint.y)
274
+ }
275
+
276
+
277
+ }
278
+
279
+ public createStartPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData {
280
+ const data = this.createLinkerPoint(worldPoint, node, options)
281
+ this.startPoint = data
282
+ return data
283
+ }
284
+
285
+ public createEndPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData {
286
+ const data = this.createLinkerPoint(worldPoint, node, options)
287
+ this.endPoint = data
288
+ return data
289
+ }
290
+
291
+ public createLinkerPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData {
292
+ const data: ILinkerPointData = {}
293
+ if (node && node.id) {
294
+ const point = node.getBoxPoint(worldPoint)
295
+ const { boxBounds } = node
296
+
297
+ const mode = options && options.mode
298
+
299
+ const edge = mode === 'edge' || !mode
300
+ const four = mode === 'four'
301
+ const auto = mode === 'auto'
302
+ const free = mode === 'free'
303
+
304
+ if (!auto) {
305
+
306
+ if (four) {
307
+ data.point = LinkerHelper.getDirection(point, boxBounds, four)
308
+ } else {
309
+ data.direction = LinkerHelper.getDirection(point, boxBounds, edge)
310
+ data.point = LinkerHelper.getPointDirection(point, boxBounds, edge) || data.direction
311
+ if (data.point === data.direction) delete data.direction
312
+
313
+ if (free || edge) {
314
+ const snapPoint = BoundsHelper.getPoint(boxBounds, data.point)
315
+
316
+ const offsetX = point.x - snapPoint.x
317
+ const offsetY = point.y - snapPoint.y
318
+
319
+ const snapRadius = (options && options.snapRadius) || 10
320
+
321
+ if (Math.abs(offsetX) > snapRadius || Math.abs(offsetY) > snapRadius) {
322
+ const offset = data.offset = {
323
+ x: Math.abs(offsetX) > snapRadius ? offsetX : 0,
324
+ y: Math.abs(offsetY) > snapRadius ? offsetY : 0
325
+ }
326
+
327
+ if (edge && (offset.x || offset.y)) {
328
+ switch (data.direction || data.point) {
329
+ case 'top':
330
+ case 'bottom':
331
+ offset.y = 0; break
332
+ case 'right':
333
+ case 'left':
334
+ offset.x = 0; break
335
+ default:
336
+ if (offset.x > offset.y) offset.y = 0
337
+ else offset.x = 0
338
+ }
339
+
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ }
346
+
347
+ data.id = node.id
348
+ } else {
349
+ data.point = this.getLocalPoint(worldPoint)
350
+ }
351
+
352
+ return data
353
+ }
354
+
355
+ }
@@ -0,0 +1,11 @@
1
+ import { ILinkerData } from "@leafer-ui/interface"
2
+ import { LineData } from '@leafer-ui/draw'
3
+
4
+
5
+ export class LinkerData extends LineData implements ILinkerData {
6
+
7
+ public get __usePathBox(): boolean {
8
+ return ((this as ILinkerData).path || (this as ILinkerData).__pathInputed) as any as boolean
9
+ }
10
+
11
+ }
@@ -0,0 +1,18 @@
1
+ import { IValue } from '@leafer-ui/interface'
2
+ import { decorateLeafAttr, attr, doBoundsType, doStrokeType } from '@leafer-ui/draw'
3
+
4
+
5
+ export function linkerPointType(defaultValue?: IValue) {
6
+ return decorateLeafAttr(defaultValue, (key: string) => attr({
7
+ set(value: IValue) {
8
+ if (this.__setAttr(key, value)) {
9
+ const layout = this.__layout
10
+ if (!layout.linkerChanged) {
11
+ doBoundsType(this)
12
+ doStrokeType(this)
13
+ layout.linkerChanged = true
14
+ }
15
+ }
16
+ }
17
+ }))
18
+ }
@@ -0,0 +1,207 @@
1
+ import { IBoundsData, IDirection, IDirection4, ILinker, IPointData, IUI } from '@leafer-ui/interface'
2
+ import { AroundHelper, BoundsHelper, Direction9, PointHelper } from '@leafer-ui/draw'
3
+
4
+
5
+ const { directionData } = AroundHelper
6
+
7
+ const directionNames: IDirection[] = [
8
+ 'top-left',
9
+ 'top',
10
+ 'top-right',
11
+ 'right',
12
+ 'bottom-right',
13
+ 'bottom',
14
+ 'bottom-left',
15
+ 'left',
16
+ 'center'
17
+ ]
18
+
19
+ export const LinkerHelper = {
20
+
21
+ /**
22
+ * 根据箭头方向获取控制点
23
+ */
24
+ getCtrlPoint(linker: ILinker, node: IUI, point: IPointData, direction: IDirection, ctrl: IPointData, offsetAngle?: number): IPointData {
25
+
26
+ if (node) {
27
+ point = node.getBoxPoint(point, linker)
28
+ // half 需要 / startNode.scale
29
+ }
30
+
31
+ let ctrlPoint
32
+ switch (direction) {
33
+ case 'left':
34
+ case 'top-left':
35
+ case 'bottom-left':
36
+ ctrlPoint = {
37
+ x: point.x - ctrl.x,
38
+ y: point.y
39
+ }
40
+ break
41
+ case 'top':
42
+ ctrlPoint = {
43
+ x: point.x,
44
+ y: point.y - ctrl.y
45
+ }
46
+ break
47
+ case 'right':
48
+ case 'top-right':
49
+ case 'bottom-right':
50
+ ctrlPoint = {
51
+ x: point.x + ctrl.x,
52
+ y: point.y
53
+ }
54
+ break
55
+ case 'bottom':
56
+ ctrlPoint = {
57
+ x: point.x,
58
+ y: point.y + ctrl.y
59
+ }
60
+ break
61
+ default:
62
+ ctrlPoint = {
63
+ x: point.x,
64
+ y: point.y
65
+ }
66
+ }
67
+ if (offsetAngle) { // 旋转角度
68
+ offsetAngle = offsetAngle / 180 * Math.PI
69
+ ctrlPoint = {
70
+ x: (ctrlPoint.x - point.x) * Math.cos(offsetAngle) - (ctrlPoint.y - point.y) * Math.sin(offsetAngle) + point.x,
71
+ y: (ctrlPoint.x - point.x) * Math.sin(offsetAngle) + (ctrlPoint.y - point.y) * Math.cos(offsetAngle) + point.y
72
+ }
73
+ }
74
+
75
+ if (node) node.getWorldPointByBox(ctrlPoint, linker, undefined, true)
76
+
77
+ return ctrlPoint
78
+ },
79
+
80
+ // 是否为相反方向
81
+ isOppositeDirection(from: IDirection, to: IDirection): boolean {
82
+ switch (from) {
83
+ case 'left':
84
+ return to == 'right'
85
+ case 'top':
86
+ return to == 'bottom'
87
+ case 'right':
88
+ return to == 'left'
89
+ case 'bottom':
90
+ return to == 'top'
91
+ case 'center':
92
+ return to == 'center'
93
+ default:
94
+ return false
95
+ }
96
+ },
97
+
98
+ /**
99
+ * 获取to 相对于 from 的方向
100
+ * @param from
101
+ * @param to
102
+ * @returns
103
+ */
104
+ getDirectionForFromPoint(from: IPointData, to: IPointData): IDirection4 {
105
+ const a = from.x - to.x
106
+ const b = from.y - to.y
107
+
108
+ let direction: IDirection
109
+ let angle = (180 * Math.atan2(b, a)) / Math.PI
110
+
111
+ if (angle < 0) angle = 360 + angle
112
+
113
+ if (angle > 315 || angle < 45) {
114
+ direction = 'left'
115
+ } else if (angle > 45 && angle < 135) {
116
+ direction = 'top'
117
+ } else if (angle > 135 && angle < 225) {
118
+ direction = 'right'
119
+ } else {
120
+ direction = 'bottom'
121
+ }
122
+
123
+ return direction
124
+ },
125
+
126
+ getRotateDirection(direction: IDirection, rotation: number, totalDirection = 8): IDirection {
127
+ let directionIndex = Direction9[direction]
128
+ directionIndex = (directionIndex + Math.round(rotation / (360 / totalDirection))) % totalDirection
129
+ if (directionIndex < 0) directionIndex += totalDirection
130
+ return directionNames[directionIndex]
131
+ },
132
+
133
+ getDirection,
134
+
135
+ getPointDirection,
136
+
137
+ autoRotatePoint(point: IPointData, origin: IPointData, ui: IUI): void {
138
+ const { rotation, scaleX, scaleY } = ui
139
+ const oneFlip = scaleX * scaleY < 0
140
+ if (rotation || oneFlip) PointHelper.rotate(point, oneFlip ? -rotation : rotation, origin)
141
+ }
142
+
143
+ }
144
+
145
+
146
+
147
+ function getPointDirection(boxPoint: IPointData, bounds: IBoundsData, eight?: boolean): IDirection {
148
+ const { width, height } = bounds
149
+ const hitRadiusX = width / 4
150
+ const hitRadiusY = height / 4
151
+
152
+ for (let i = 0; i < directionData.length; i++) {
153
+ const unit = directionData[i]
154
+ // 百分比坐标转像素
155
+ const px = unit.x * width
156
+ const py = unit.y * height
157
+
158
+ const dx = boxPoint.x - px
159
+ const dy = boxPoint.y - py
160
+
161
+ if (Math.abs(dx) <= hitRadiusX && Math.abs(dy) <= hitRadiusY) {
162
+ if (eight) {
163
+ if (directionNames[i] !== 'center') return directionNames[i]
164
+ } else return directionNames[i]
165
+ }
166
+ }
167
+
168
+ return undefined
169
+ }
170
+
171
+
172
+ function getDirection(boxPoint: IPointData, bounds: IBoundsData, four?: boolean): IDirection4 | 'center' {
173
+ const { width, height } = bounds
174
+ const center = BoundsHelper.getPoint(bounds, 'center', true)
175
+ // 将元素沿着对角线切成4块,匹配对应上下左右四个方向
176
+ // 转为以中心为原点的坐标
177
+ const dx = boxPoint.x - center.x
178
+ const dy = boxPoint.y - center.y
179
+
180
+ if (!four) {
181
+ // 中心点
182
+ if (Math.abs(dx) < width / 4 && Math.abs(dy) < height / 4) return 'center'
183
+ }
184
+
185
+ // ----------------------------------------
186
+ // 将矩形按比例缩放到正方形空间
187
+ //
188
+ // 等价于比较:
189
+ // |dx| / width 和 |dy| / height
190
+ //
191
+ // 但为了避免除法,改为:
192
+ // |dx| * height 和 |dy| * width
193
+ //
194
+ // 这样可以严格按矩形的两条对角线划分区域
195
+ // ----------------------------------------
196
+
197
+ const ax = Math.abs(dx) * height
198
+ const ay = Math.abs(dy) * width
199
+
200
+ // 左右区域
201
+ if (ax > ay) {
202
+ return dx > 0 ? 'right' : 'left'
203
+ }
204
+
205
+ // 上下区域
206
+ return dy > 0 ? 'bottom' : 'top'
207
+ }
@@ -0,0 +1,283 @@
1
+ import { IPointData, IPathCommandData } from '@leafer-ui/interface'
2
+
3
+ import { PathCommandMap, PointHelper } from '@leafer-ui/core'
4
+
5
+ const { toNumberPoints } = PointHelper
6
+ const { M, L, C, Z } = PathCommandMap
7
+
8
+
9
+ /**
10
+ * 使用角平分线自动生成平滑 Bezier 曲线
11
+ *
12
+ * 算法特点:
13
+ *
14
+ * 1. Handle 方向:
15
+ * 使用前后方向的角平分线
16
+ *
17
+ * 2. Handle 长度:
18
+ * 左右分别根据相邻边长度计算
19
+ *
20
+ * in = AB * curve
21
+ * out = BC * curve
22
+ *
23
+ * 3. 自动曲率控制:
24
+ *
25
+ * - 接近平角:
26
+ * 降低 handle,避免直线中间产生鼓包
27
+ *
28
+ * - 接近尖角:
29
+ * 降低 handle,避免曲线过冲
30
+ *
31
+ * 4. 性能:
32
+ *
33
+ * - O(n)
34
+ * - 无对象创建
35
+ * - 无临时数组
36
+ * - 适合大规模路径
37
+ *
38
+ *
39
+ * curve:
40
+ *
41
+ * 0 直线
42
+ * 0.5 推荐
43
+ * 1 最大平滑
44
+ */
45
+
46
+ /**
47
+ * 使用角平分线法线生成自动 Bezier 曲线
48
+ *
49
+ * 算法:
50
+ *
51
+ * 1. 通过 BA / BC 求角平分方向
52
+ * 2. 旋转 90° 得到曲线切线方向
53
+ * 3. 左右 handle 长度分别根据两侧距离计算
54
+ *
55
+ * handle:
56
+ *
57
+ * C2
58
+ * |
59
+ * |
60
+ * A ---- B ---- C
61
+ * |
62
+ * |
63
+ * C1
64
+ *
65
+ *
66
+ * curve:
67
+ *
68
+ * 0 直线
69
+ * 0.5 推荐
70
+ * 1 最大曲率
71
+ */
72
+ export function drawPoints(data: IPathCommandData, originPoints: number[] | IPointData[], curve?: boolean | number, close?: boolean): void {
73
+ const points = toNumberPoints(originPoints)
74
+ const count = points.length >> 1
75
+
76
+ if (count < 2) return
77
+
78
+ data.push(M, points[0], points[1])
79
+
80
+ if (!curve || count < 3) {
81
+
82
+ for (let i = 1; i < count; i++) {
83
+
84
+ data.push(L, points[i * 2], points[i * 2 + 1])
85
+ }
86
+
87
+ if (close) data.push(Z)
88
+ return
89
+ }
90
+
91
+ let tension = curve === true ? 0.5 : curve as number
92
+
93
+ if (tension < 0) tension = 0
94
+ else if (tension > 1) tension = 1
95
+
96
+ const end = close ? count : count - 1
97
+
98
+ let lastOutX = points[0]
99
+ let lastOutY = points[1]
100
+
101
+ /**
102
+ * 非闭合路径第一个点:
103
+ * 使用第一条边方向作为初始切线
104
+ */
105
+ {
106
+
107
+ const dx = points[2] - points[0]
108
+ const dy = points[3] - points[1]
109
+
110
+ const len = Math.sqrt(dx * dx + dy * dy)
111
+
112
+ if (len) {
113
+ lastOutX = points[0] + dx / len * len * tension
114
+ lastOutY = points[1] + dy / len * len * tension
115
+ }
116
+ }
117
+
118
+
119
+ for (let i = 0; i < end; i++) {
120
+ const index = i + 1 === count ? 0 : i + 1
121
+
122
+ const bx = points[index * 2]
123
+ const by = points[index * 2 + 1]
124
+
125
+ const prev = index === 0 ? count - 1 : index - 1
126
+ const next = index + 1 === count ? 0 : index + 1
127
+
128
+ const ax = points[prev * 2]
129
+ const ay = points[prev * 2 + 1]
130
+
131
+ const cx = points[next * 2]
132
+ const cy = points[next * 2 + 1]
133
+
134
+ /**
135
+ * BA
136
+ */
137
+ let bax = ax - bx
138
+ let bay = ay - by
139
+
140
+ /**
141
+ * BC
142
+ */
143
+ let bcx = cx - bx
144
+ let bcy = cy - by
145
+
146
+ const ab = Math.sqrt(bax * bax + bay * bay)
147
+ const bc = Math.sqrt(bcx * bcx + bcy * bcy)
148
+
149
+ let inX = bx
150
+ let inY = by
151
+
152
+ let outX = bx
153
+ let outY = by
154
+
155
+ if (ab || bc) {
156
+
157
+ /**
158
+ * 计算角平分方向
159
+ */
160
+ let ux = 0
161
+ let uy = 0
162
+
163
+ if (ab) {
164
+ ux += bax / ab
165
+ uy += bay / ab
166
+ }
167
+
168
+ if (bc) {
169
+ ux += bcx / bc
170
+ uy += bcy / bc
171
+ }
172
+
173
+ /**
174
+ * 角平分线长度
175
+ */
176
+ let uLen = Math.sqrt(ux * ux + uy * uy)
177
+
178
+ /**
179
+ * 180度:
180
+ * 两个方向抵消
181
+ *
182
+ * 使用 BC 方向
183
+ */
184
+ if (!uLen) {
185
+
186
+ if (bc) {
187
+ ux = bcx / bc
188
+ uy = bcy / bc
189
+
190
+ } else {
191
+ ux = bax / ab
192
+ uy = bay / ab
193
+ }
194
+
195
+
196
+ uLen = 1
197
+ }
198
+
199
+ ux /= uLen
200
+ uy /= uLen
201
+
202
+ /**
203
+ * 角平分线旋转90度
204
+ *
205
+ * 得到曲线切线方向
206
+ */
207
+ let tx = -uy
208
+ let ty = ux
209
+
210
+ /**
211
+ * 保证方向朝向下一点
212
+ */
213
+ if (bc) {
214
+ if (tx * bcx + ty * bcy < 0) {
215
+ tx = -tx
216
+ ty = -ty
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 计算夹角
222
+ */
223
+ let cos = 0
224
+
225
+ if (ab && bc) {
226
+ cos = (bax * bcx + bay * bcy) / (ab * bc)
227
+ if (cos > 1) cos = 1
228
+ else if (cos < -1) cos = -1
229
+ }
230
+
231
+ /**
232
+ * 曲率限制:
233
+ *
234
+ * 平角降低
235
+ * 尖角降低
236
+ */
237
+ let angleFactor = Math.sqrt(Math.sqrt(Math.max(0, 1 - cos * cos)))
238
+
239
+ /**
240
+ * 保留少量曲率,
241
+ * 避免完全退化
242
+ */
243
+ if (angleFactor < 0.15) angleFactor = 0.15
244
+
245
+ let inLength = ab * tension * angleFactor
246
+ let outLength = bc * tension * angleFactor
247
+
248
+ /**
249
+ * 防止 handle 过长
250
+ */
251
+ const maxLength = Math.min(ab || bc, bc || ab) * 0.5
252
+
253
+ if (inLength > maxLength) inLength = maxLength
254
+ if (outLength > maxLength) outLength = maxLength
255
+
256
+ inX = bx - tx * inLength
257
+ inY = by - ty * inLength
258
+
259
+ outX = bx + tx * outLength
260
+ outY = by + ty * outLength
261
+ }
262
+
263
+ data.push(C,
264
+
265
+ // 上一个节点 handle out
266
+ lastOutX,
267
+ lastOutY,
268
+
269
+ // 当前节点 handle in
270
+ inX,
271
+ inY,
272
+
273
+ // 当前节点
274
+ bx,
275
+ by
276
+ )
277
+
278
+ lastOutX = outX
279
+ lastOutY = outY
280
+ }
281
+
282
+ if (close) data.push(Z)
283
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { Linker } from './Linker'
2
+ export { LinkerData } from './data/LinkerData'
3
+
4
+ import { Plugin, registerPointsCurve } from '@leafer-ui/draw'
5
+ import { drawPoints } from './helper/drawPoints'
6
+
7
+
8
+ Plugin.add('linker')
9
+
10
+ registerPointsCurve('C', drawPoints)
@@ -0,0 +1,33 @@
1
+ import { ILinker, ILinkerData, ILinkerPointData, IPointsCurve, IUI, ILinkerComputedPointData, ILinkerInputData, ILinkerPointType, IPointData, ILeaf, ILinkerPointOptions } from '@leafer-ui/interface';
2
+ import { Line, LineData } from '@leafer-ui/draw';
3
+
4
+ declare class Linker extends Line implements ILinker {
5
+ get __tag(): string;
6
+ __: ILinkerData;
7
+ startPoint?: ILinkerPointData;
8
+ endPoint?: ILinkerPointData;
9
+ curve?: IPointsCurve;
10
+ startNode: IUI;
11
+ endNode: IUI;
12
+ startData: ILinkerComputedPointData;
13
+ endData: ILinkerComputedPointData;
14
+ constructor(data?: ILinkerInputData);
15
+ __updatePath(): void;
16
+ listenLinkerNode(newNode: IUI, oldNode: IUI, type: ILinkerPointType): void;
17
+ onStartNodeChange(): void;
18
+ onEndNodeChange(): void;
19
+ onLinkerNodeChange(type: ILinkerPointType): void;
20
+ updateLinkerNode(type: ILinkerPointType): void;
21
+ getLinkerNodeCenter(type: ILinkerPointType): IPointData;
22
+ updateLinkerPoint(type: ILinkerPointType): void;
23
+ updateLinkerPath(): void;
24
+ createStartPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData;
25
+ createEndPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData;
26
+ createLinkerPoint(worldPoint: IPointData, node?: ILeaf, options?: ILinkerPointOptions): ILinkerPointData;
27
+ }
28
+
29
+ declare class LinkerData extends LineData implements ILinkerData {
30
+ get __usePathBox(): boolean;
31
+ }
32
+
33
+ export { Linker, LinkerData };