w-flow-vue 1.0.21 → 1.0.23
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/README.md +1 -1
- package/dist/w-flow-vue.umd.js +3 -3
- package/dist/w-flow-vue.umd.js.map +1 -1
- package/docs/components_WFlowVue.vue.html +61 -6
- package/docs/components_ui_Controls.vue.html +1 -1
- package/docs/examples/app.html +1 -1
- package/docs/examples/app.umd.js +4 -4
- package/docs/examples/app.umd.js.map +1 -1
- package/docs/examples/ex-AppBasic.html +1 -1
- package/docs/examples/ex-AppConnectivity.html +1 -1
- package/docs/global.html +1 -1
- package/docs/index.html +1 -1
- package/docs/js_defaults.mjs.html +1 -1
- package/docs/js_edgePath.mjs.html +1 -1
- package/docs/js_fixSvgNs.mjs.html +1 -1
- package/docs/js_geometry.mjs.html +1 -1
- package/docs/js_graph.mjs.html +1 -1
- package/docs/js_stepRouting.mjs.html +1 -1
- package/docs/module-WFlowVue.html +1 -1
- package/package.json +1 -1
- package/src/components/WFlowVue.vue +60 -5
- package/src/components/nodes/NodeRenderer.vue +1 -0
- package/src/components/nodes/NodeWrapper.vue +81 -19
- package/test/pics/flow-E2E-011-node-settings-form.png +0 -0
- package/test/pics/flow-E2E-012-popup-exclusive.png +0 -0
- package/test/unit-WFlowVue-features.test.mjs +10 -8
- package/test/unit-WFlowVue.test.mjs +432 -421
- package/test/unit-components-canvas.test.mjs +104 -102
- package/test/unit-components-edge.test.mjs +157 -147
- package/test/unit-components-node.test.mjs +27 -5
- package/test/unit-geometry.test.mjs +248 -236
- package/test/unit-graph.test.mjs +72 -72
- package/test/unit-node-drag-gesture.test.mjs +254 -0
|
@@ -1,236 +1,248 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
expect(pos.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(pos.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(pos.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
expect(pos.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expect(pos.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(pos.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(pos.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
test('
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
expect(p.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(p.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
expect(p.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
expect(
|
|
220
|
-
})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
describe('
|
|
224
|
-
test('
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
//getDiamondEdgePoint / getEllipseEdgePoint / getTriangleEdgePoint / rectsOverlap 現為 geometry.mjs 之內部函式
|
|
2
|
+
//(仍存在且仍被使用, 只是不再 export), 故改由公開入口 getHandlePosition / getOverlappingNodes 驗證其行為
|
|
3
|
+
import {
|
|
4
|
+
getHandlePosition,
|
|
5
|
+
getOverlappingNodes,
|
|
6
|
+
clampPosition, snapPosition
|
|
7
|
+
} from '../src/js/geometry'
|
|
8
|
+
|
|
9
|
+
describe('geometry', () => {
|
|
10
|
+
const node = { id: '1', position: { x: 100, y: 50 }, width: 150, height: 40 }
|
|
11
|
+
|
|
12
|
+
describe('getHandlePosition', () => {
|
|
13
|
+
test('top handle is at top center', () => {
|
|
14
|
+
const pos = getHandlePosition(node, 'top', {})
|
|
15
|
+
expect(pos).toEqual({ x: 175, y: 50 })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('bottom handle is at bottom center', () => {
|
|
19
|
+
const pos = getHandlePosition(node, 'bottom', {})
|
|
20
|
+
expect(pos).toEqual({ x: 175, y: 90 })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('left handle is at left center', () => {
|
|
24
|
+
const pos = getHandlePosition(node, 'left', {})
|
|
25
|
+
expect(pos).toEqual({ x: 100, y: 70 })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('right handle is at right center', () => {
|
|
29
|
+
const pos = getHandlePosition(node, 'right', {})
|
|
30
|
+
expect(pos).toEqual({ x: 250, y: 70 })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('uses nodeInternals dimensions if provided', () => {
|
|
34
|
+
const pos = getHandlePosition(
|
|
35
|
+
{ id: '1', position: { x: 0, y: 0 } },
|
|
36
|
+
'bottom',
|
|
37
|
+
{ width: 200, height: 60 }
|
|
38
|
+
)
|
|
39
|
+
expect(pos).toEqual({ x: 100, y: 60 })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('defaults to 150x40 if no dimensions', () => {
|
|
43
|
+
const pos = getHandlePosition({ id: '1', position: { x: 0, y: 0 } }, 'bottom', {})
|
|
44
|
+
expect(pos).toEqual({ x: 75, y: 40 })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('getHandlePosition — diamond', () => {
|
|
49
|
+
const diamond = { id: 'd', position: { x: 0, y: 0 }, width: 100, height: 100, shape: 'diamond', type: 'input' }
|
|
50
|
+
|
|
51
|
+
test('top handle is at top vertex', () => {
|
|
52
|
+
const pos = getHandlePosition(diamond, 'top', {})
|
|
53
|
+
expect(pos.x).toBe(50)
|
|
54
|
+
expect(pos.y).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('bottom handle is at bottom vertex', () => {
|
|
58
|
+
const pos = getHandlePosition(diamond, 'bottom', {})
|
|
59
|
+
expect(pos.x).toBe(50)
|
|
60
|
+
expect(pos.y).toBe(100)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('getHandlePosition — ellipse', () => {
|
|
65
|
+
const ellipse = { id: 'e', position: { x: 0, y: 0 }, width: 200, height: 100, shape: 'ellipse', type: 'input' }
|
|
66
|
+
|
|
67
|
+
test('right handle is at rightmost point', () => {
|
|
68
|
+
const pos = getHandlePosition(ellipse, 'right', {})
|
|
69
|
+
expect(pos.x).toBeCloseTo(200, 0)
|
|
70
|
+
expect(pos.y).toBeCloseTo(50, 0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('left handle is at leftmost point', () => {
|
|
74
|
+
const pos = getHandlePosition(ellipse, 'left', {})
|
|
75
|
+
expect(pos.x).toBeCloseTo(0, 0)
|
|
76
|
+
expect(pos.y).toBeCloseTo(50, 0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('top handle is at topmost point', () => {
|
|
80
|
+
const pos = getHandlePosition(ellipse, 'top', {})
|
|
81
|
+
expect(pos.x).toBeCloseTo(100, 0)
|
|
82
|
+
expect(pos.y).toBeCloseTo(0, 0)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('getHandlePosition — triangle', () => {
|
|
87
|
+
const tri = { id: 't', position: { x: 0, y: 0 }, width: 100, height: 100, shape: 'triangle', type: 'input' }
|
|
88
|
+
|
|
89
|
+
test('top handle (apex) is at top center', () => {
|
|
90
|
+
const pos = getHandlePosition(tri, 'top', {})
|
|
91
|
+
expect(pos.x).toBeCloseTo(50, 0)
|
|
92
|
+
expect(pos.y).toBeCloseTo(0, 0)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('bottom handle (base) is at bottom midpoint', () => {
|
|
96
|
+
const pos = getHandlePosition(tri, 'bottom', {})
|
|
97
|
+
expect(pos.x).toBe(50)
|
|
98
|
+
expect(pos.y).toBe(100)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
//以下四組原本直接呼叫已內部化之 getDiamondEdgePoint / getEllipseEdgePoint /
|
|
103
|
+
//getTriangleEdgePoint / rectsOverlap, 改為經公開入口驗證同一批行為。
|
|
104
|
+
//註: 舊測試之 ratio 0 與 ratio 1 為公開 API 無法產生之值
|
|
105
|
+
//(getHandlePosition 僅產生 0.5, 或 sameSide 時之 0.33/0.67), 該兩組斷言不可達, 故不保留。
|
|
106
|
+
|
|
107
|
+
describe('getHandlePosition — diamond same-side (內部 getDiamondEdgePoint)', () => {
|
|
108
|
+
//diamond 需 sameSide(type='basic' 且 toPosition===fromPosition)才會走菱形邊緣運算
|
|
109
|
+
const diamond = {
|
|
110
|
+
id: 'd', shape: 'diamond', type: 'basic',
|
|
111
|
+
position: { x: 0, y: 0 }, width: 100, height: 100,
|
|
112
|
+
toPosition: 'top', fromPosition: 'top',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test('target 與 source 落在同側之不同點', () => {
|
|
116
|
+
const pt = getHandlePosition(diamond, 'top', {}, 'target')
|
|
117
|
+
const ps = getHandlePosition(diamond, 'top', {}, 'source')
|
|
118
|
+
expect(pt).not.toEqual(ps)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('兩點皆落在菱形邊上(到中心之曼哈頓距離為定值)', () => {
|
|
122
|
+
//菱形邊之方程式: |x-cx|/(w/2) + |y-cy|/(h/2) = 1
|
|
123
|
+
const onEdge = (p) => Math.abs(p.x - 50) / 50 + Math.abs(p.y - 50) / 50
|
|
124
|
+
expect(onEdge(getHandlePosition(diamond, 'top', {}, 'target'))).toBeCloseTo(1, 5)
|
|
125
|
+
expect(onEdge(getHandlePosition(diamond, 'top', {}, 'source'))).toBeCloseTo(1, 5)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('getHandlePosition — ellipse (內部 getEllipseEdgePoint)', () => {
|
|
130
|
+
const ellipse = { id: 'e', shape: 'ellipse', position: { x: 0, y: 0 }, width: 200, height: 100 }
|
|
131
|
+
|
|
132
|
+
test('top 為橢圓頂點', () => {
|
|
133
|
+
const p = getHandlePosition(ellipse, 'top', {})
|
|
134
|
+
expect(p.x).toBeCloseTo(100, 0)
|
|
135
|
+
expect(p.y).toBeCloseTo(0, 0)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('right 為橢圓右頂點', () => {
|
|
139
|
+
const p = getHandlePosition(ellipse, 'right', {})
|
|
140
|
+
expect(p.x).toBeCloseTo(200, 0)
|
|
141
|
+
expect(p.y).toBeCloseTo(50, 0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('各方位之點皆落在橢圓邊界上', () => {
|
|
145
|
+
//橢圓方程式: ((x-cx)/rx)^2 + ((y-cy)/ry)^2 = 1
|
|
146
|
+
const cx = 100; const cy = 50; const rx = 100; const ry = 50
|
|
147
|
+
;['top', 'bottom', 'left', 'right'].forEach((side) => {
|
|
148
|
+
const p = getHandlePosition(ellipse, side, {})
|
|
149
|
+
const dx = (p.x - cx) / rx
|
|
150
|
+
const dy = (p.y - cy) / ry
|
|
151
|
+
expect(dx * dx + dy * dy).toBeCloseTo(1, 1)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('getHandlePosition — triangle 各朝向 (內部 getTriangleEdgePoint)', () => {
|
|
157
|
+
const tri = (shape) => ({ id: 't', shape, position: { x: 0, y: 0 }, width: 100, height: 100 })
|
|
158
|
+
|
|
159
|
+
test('triangle(朝上): top 側為頂點', () => {
|
|
160
|
+
const p = getHandlePosition(tri('triangle'), 'top', {})
|
|
161
|
+
expect(p.x).toBeCloseTo(50, 0)
|
|
162
|
+
expect(p.y).toBeCloseTo(0, 0)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('triangle(朝上): bottom 側為底邊中點', () => {
|
|
166
|
+
const p = getHandlePosition(tri('triangle'), 'bottom', {})
|
|
167
|
+
expect(p.x).toBeCloseTo(50, 0)
|
|
168
|
+
expect(p.y).toBeCloseTo(100, 0)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('triangle-right: right 側為右頂點', () => {
|
|
172
|
+
const p = getHandlePosition(tri('triangle-right'), 'right', {})
|
|
173
|
+
expect(p.x).toBeCloseTo(100, 0)
|
|
174
|
+
expect(p.y).toBeCloseTo(50, 0)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('triangle-down: bottom 側為下頂點', () => {
|
|
178
|
+
const p = getHandlePosition(tri('triangle-down'), 'bottom', {})
|
|
179
|
+
expect(p.x).toBeCloseTo(50, 0)
|
|
180
|
+
expect(p.y).toBeCloseTo(100, 0)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('triangle-left: left 側為左頂點', () => {
|
|
184
|
+
const p = getHandlePosition(tri('triangle-left'), 'left', {})
|
|
185
|
+
expect(p.x).toBeCloseTo(0, 0)
|
|
186
|
+
expect(p.y).toBeCloseTo(50, 0)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('矩形重疊判定 (內部 rectsOverlap, 經 getOverlappingNodes 驗證)', () => {
|
|
191
|
+
const nodeAt = (x, y, w, h) => ({ id: 'n', position: { x, y }, width: w, height: h })
|
|
192
|
+
|
|
193
|
+
test('相交者回傳該節點', () => {
|
|
194
|
+
const rect = { x: 50, y: 50, width: 100, height: 100 }
|
|
195
|
+
expect(getOverlappingNodes(rect, [nodeAt(0, 0, 100, 100)], {})).toHaveLength(1)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('不相交者不回傳', () => {
|
|
199
|
+
const rect = { x: 100, y: 100, width: 50, height: 50 }
|
|
200
|
+
expect(getOverlappingNodes(rect, [nodeAt(0, 0, 50, 50)], {})).toHaveLength(0)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('僅邊緣相接不算重疊', () => {
|
|
204
|
+
const rect = { x: 50, y: 0, width: 50, height: 50 }
|
|
205
|
+
expect(getOverlappingNodes(rect, [nodeAt(0, 0, 50, 50)], {})).toHaveLength(0)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('getOverlappingNodes', () => {
|
|
210
|
+
const nodes = [
|
|
211
|
+
{ id: '1', position: { x: 0, y: 0 }, width: 100, height: 50 },
|
|
212
|
+
{ id: '2', position: { x: 200, y: 200 }, width: 100, height: 50 },
|
|
213
|
+
{ id: '3', position: { x: 50, y: 20 }, width: 100, height: 50 },
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
test('returns overlapping nodes', () => {
|
|
217
|
+
const rect = { x: 10, y: 10, width: 80, height: 80 }
|
|
218
|
+
const result = getOverlappingNodes(rect, nodes, {})
|
|
219
|
+
expect(result.map(n => n.id)).toEqual(['1', '3'])
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('clampPosition', () => {
|
|
224
|
+
test('clamps to extent', () => {
|
|
225
|
+
const extent = [[0, 0], [100, 100]]
|
|
226
|
+
expect(clampPosition({ x: -10, y: 50 }, extent)).toEqual({ x: 0, y: 50 })
|
|
227
|
+
expect(clampPosition({ x: 50, y: 150 }, extent)).toEqual({ x: 50, y: 100 })
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('returns original if no extent', () => {
|
|
231
|
+
expect(clampPosition({ x: -10, y: 50 }, null)).toEqual({ x: -10, y: 50 })
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('snapPosition', () => {
|
|
236
|
+
test('snaps to grid', () => {
|
|
237
|
+
expect(snapPosition({ x: 17, y: 23 }, 15)).toEqual({ x: 15, y: 30 })
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('returns original if no grid', () => {
|
|
241
|
+
expect(snapPosition({ x: 17, y: 23 }, null)).toEqual({ x: 17, y: 23 })
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('snaps negative positions', () => {
|
|
245
|
+
expect(snapPosition({ x: -7, y: -22 }, 10)).toEqual({ x: -10, y: -20 })
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
})
|