pict-section-flow 0.0.10 → 0.0.13
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/.claude/launch.json +1 -1
- package/README.md +176 -0
- package/docs/.nojekyll +0 -0
- package/docs/Architecture.md +303 -0
- package/docs/Custom-Styling.md +275 -0
- package/docs/Data_Model.md +158 -0
- package/docs/Event_System.md +156 -0
- package/docs/Getting_Started.md +237 -0
- package/docs/Implementation_Reference.md +528 -0
- package/docs/Layout_Persistence.md +117 -0
- package/docs/README.md +115 -52
- package/docs/_cover.md +11 -0
- package/docs/_sidebar.md +52 -0
- package/docs/_topbar.md +8 -0
- package/docs/api/PictFlowCard.md +216 -0
- package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
- package/docs/api/addConnection.md +101 -0
- package/docs/api/addNode.md +137 -0
- package/docs/api/autoLayout.md +77 -0
- package/docs/api/getFlowData.md +112 -0
- package/docs/api/marshalToView.md +95 -0
- package/docs/api/openPanel.md +128 -0
- package/docs/api/registerHandler.md +174 -0
- package/docs/api/registerNodeType.md +142 -0
- package/docs/api/removeConnection.md +57 -0
- package/docs/api/removeNode.md +80 -0
- package/docs/api/saveLayout.md +152 -0
- package/docs/api/screenToSVGCoords.md +68 -0
- package/docs/api/selectNode.md +116 -0
- package/docs/api/setTheme.md +168 -0
- package/docs/api/setZoom.md +97 -0
- package/docs/api/toggleFullscreen.md +68 -0
- package/docs/card-help/EACH.md +19 -0
- package/docs/card-help/FREAD.md +24 -0
- package/docs/card-help/FWRITE.md +24 -0
- package/docs/card-help/GET.md +22 -0
- package/docs/card-help/ITE.md +23 -0
- package/docs/card-help/LOG.md +23 -0
- package/docs/card-help/NOTE.md +17 -0
- package/docs/card-help/PREV.md +18 -0
- package/docs/card-help/SET.md +27 -0
- package/docs/card-help/SPKL.md +22 -0
- package/docs/card-help/STAT.md +23 -0
- package/docs/card-help/SW.md +25 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +169 -0
- package/docs/retold-keyword-index.json +13942 -0
- package/example_applications/simple_cards/package.json +1 -0
- package/example_applications/simple_cards/source/card-help-content.js +16 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
- package/package.json +11 -7
- package/scripts/generate-card-help.js +214 -0
- package/source/Pict-Section-Flow.js +4 -0
- package/source/PictFlowCard.js +3 -1
- package/source/providers/PictProvider-Flow-CSS.js +245 -152
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
- package/source/providers/PictProvider-Flow-Geometry.js +195 -38
- package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
- package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
- package/source/services/PictService-Flow-DataManager.js +338 -0
- package/source/services/PictService-Flow-InteractionManager.js +165 -7
- package/source/services/PictService-Flow-PathGenerator.js +282 -0
- package/source/services/PictService-Flow-PortRenderer.js +269 -0
- package/source/services/PictService-Flow-RenderManager.js +281 -0
- package/source/services/PictService-Flow-Tether.js +6 -42
- package/source/views/PictView-Flow-Node.js +2 -220
- package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
- package/source/views/PictView-Flow.js +130 -882
- package/test/ConnectionHandleManager_tests.js +717 -0
- package/test/ConnectionRenderer_tests.js +591 -0
- package/test/DataManager_tests.js +859 -0
- package/test/Geometry_tests.js +767 -0
- package/test/PathGenerator_tests.js +978 -0
- package/test/PortRenderer_tests.js +367 -0
- package/test/RenderManager_tests.js +756 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libPathGenerator = require('../source/services/PictService-Flow-PathGenerator.js');
|
|
6
|
+
|
|
7
|
+
suite
|
|
8
|
+
(
|
|
9
|
+
'PictService-Flow-PathGenerator',
|
|
10
|
+
function ()
|
|
11
|
+
{
|
|
12
|
+
let _Fable;
|
|
13
|
+
let _PathGenerator;
|
|
14
|
+
|
|
15
|
+
// Minimal mock FlowView with a GeometryProvider
|
|
16
|
+
let _MockFlowView;
|
|
17
|
+
|
|
18
|
+
setup
|
|
19
|
+
(
|
|
20
|
+
function ()
|
|
21
|
+
{
|
|
22
|
+
_Fable = new libFable({});
|
|
23
|
+
|
|
24
|
+
_MockFlowView =
|
|
25
|
+
{
|
|
26
|
+
_GeometryProvider:
|
|
27
|
+
{
|
|
28
|
+
sideDirection: function (pSide)
|
|
29
|
+
{
|
|
30
|
+
switch (pSide)
|
|
31
|
+
{
|
|
32
|
+
case 'left': return { dx: -1, dy: 0 };
|
|
33
|
+
case 'right': return { dx: 1, dy: 0 };
|
|
34
|
+
case 'top': return { dx: 0, dy: -1 };
|
|
35
|
+
case 'bottom': return { dx: 0, dy: 1 };
|
|
36
|
+
default: return { dx: 1, dy: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
_PathGenerator = new libPathGenerator(_Fable, { FlowView: _MockFlowView }, 'PathGen-Test');
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// ---- Constructor ----
|
|
47
|
+
|
|
48
|
+
suite
|
|
49
|
+
(
|
|
50
|
+
'Constructor',
|
|
51
|
+
function ()
|
|
52
|
+
{
|
|
53
|
+
test
|
|
54
|
+
(
|
|
55
|
+
'should instantiate with correct serviceType',
|
|
56
|
+
function (fDone)
|
|
57
|
+
{
|
|
58
|
+
libExpect(_PathGenerator).to.be.an('object');
|
|
59
|
+
libExpect(_PathGenerator.serviceType).to.equal('PictServiceFlowPathGenerator');
|
|
60
|
+
fDone();
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
test
|
|
65
|
+
(
|
|
66
|
+
'should store FlowView reference from options',
|
|
67
|
+
function (fDone)
|
|
68
|
+
{
|
|
69
|
+
libExpect(_PathGenerator._FlowView).to.equal(_MockFlowView);
|
|
70
|
+
fDone();
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
test
|
|
75
|
+
(
|
|
76
|
+
'should handle missing FlowView in options',
|
|
77
|
+
function (fDone)
|
|
78
|
+
{
|
|
79
|
+
let tmpGen = new libPathGenerator(_Fable, {}, 'NoView');
|
|
80
|
+
libExpect(tmpGen._FlowView).to.be.null;
|
|
81
|
+
fDone();
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// ---- computeDepartApproach ----
|
|
88
|
+
|
|
89
|
+
suite
|
|
90
|
+
(
|
|
91
|
+
'computeDepartApproach',
|
|
92
|
+
function ()
|
|
93
|
+
{
|
|
94
|
+
test
|
|
95
|
+
(
|
|
96
|
+
'should compute departure and approach for right-to-left',
|
|
97
|
+
function (fDone)
|
|
98
|
+
{
|
|
99
|
+
let tmpResult = _PathGenerator.computeDepartApproach(
|
|
100
|
+
{ x: 100, y: 50, side: 'right' },
|
|
101
|
+
{ x: 300, y: 50, side: 'left' },
|
|
102
|
+
20
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
libExpect(tmpResult.departX).to.equal(120);
|
|
106
|
+
libExpect(tmpResult.departY).to.equal(50);
|
|
107
|
+
libExpect(tmpResult.approachX).to.equal(280);
|
|
108
|
+
libExpect(tmpResult.approachY).to.equal(50);
|
|
109
|
+
libExpect(tmpResult.fromDir).to.deep.equal({ dx: 1, dy: 0 });
|
|
110
|
+
libExpect(tmpResult.toDir).to.deep.equal({ dx: -1, dy: 0 });
|
|
111
|
+
fDone();
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
test
|
|
116
|
+
(
|
|
117
|
+
'should compute departure for top-to-bottom',
|
|
118
|
+
function (fDone)
|
|
119
|
+
{
|
|
120
|
+
let tmpResult = _PathGenerator.computeDepartApproach(
|
|
121
|
+
{ x: 100, y: 50, side: 'top' },
|
|
122
|
+
{ x: 100, y: 200, side: 'bottom' },
|
|
123
|
+
30
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
libExpect(tmpResult.departX).to.equal(100);
|
|
127
|
+
libExpect(tmpResult.departY).to.equal(20);
|
|
128
|
+
libExpect(tmpResult.approachX).to.equal(100);
|
|
129
|
+
libExpect(tmpResult.approachY).to.equal(230);
|
|
130
|
+
fDone();
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
test
|
|
135
|
+
(
|
|
136
|
+
'should default to right/left when side is missing',
|
|
137
|
+
function (fDone)
|
|
138
|
+
{
|
|
139
|
+
let tmpResult = _PathGenerator.computeDepartApproach(
|
|
140
|
+
{ x: 0, y: 0 },
|
|
141
|
+
{ x: 100, y: 0 },
|
|
142
|
+
10
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Default right → dx=1, default left → dx=-1
|
|
146
|
+
libExpect(tmpResult.departX).to.equal(10);
|
|
147
|
+
libExpect(tmpResult.approachX).to.equal(90);
|
|
148
|
+
fDone();
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ---- computeAutoOrthogonalCorners ----
|
|
155
|
+
|
|
156
|
+
suite
|
|
157
|
+
(
|
|
158
|
+
'computeAutoOrthogonalCorners',
|
|
159
|
+
function ()
|
|
160
|
+
{
|
|
161
|
+
test
|
|
162
|
+
(
|
|
163
|
+
'should compute Z-shaped corridor for both-horizontal',
|
|
164
|
+
function (fDone)
|
|
165
|
+
{
|
|
166
|
+
// Both horizontal: corridor is vertical at midpoint X
|
|
167
|
+
let tmpResult = _PathGenerator.computeAutoOrthogonalCorners(
|
|
168
|
+
100, 50, // depart
|
|
169
|
+
300, 150, // approach
|
|
170
|
+
{ dx: 1, dy: 0 }, // from horizontal
|
|
171
|
+
{ dx: -1, dy: 0 }, // to horizontal
|
|
172
|
+
0
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
libExpect(tmpResult.corner1.x).to.equal(200); // midX
|
|
176
|
+
libExpect(tmpResult.corner1.y).to.equal(50); // departY
|
|
177
|
+
libExpect(tmpResult.corner2.x).to.equal(200); // midX
|
|
178
|
+
libExpect(tmpResult.corner2.y).to.equal(150); // approachY
|
|
179
|
+
libExpect(tmpResult.midpoint.x).to.equal(200);
|
|
180
|
+
libExpect(tmpResult.midpoint.y).to.equal(100); // avg Y
|
|
181
|
+
fDone();
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
test
|
|
186
|
+
(
|
|
187
|
+
'should compute Z-shaped corridor for both-vertical',
|
|
188
|
+
function (fDone)
|
|
189
|
+
{
|
|
190
|
+
let tmpResult = _PathGenerator.computeAutoOrthogonalCorners(
|
|
191
|
+
50, 100,
|
|
192
|
+
150, 300,
|
|
193
|
+
{ dx: 0, dy: 1 }, // from vertical (downward)
|
|
194
|
+
{ dx: 0, dy: -1 }, // to vertical (upward)
|
|
195
|
+
0
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
libExpect(tmpResult.corner1.x).to.equal(50);
|
|
199
|
+
libExpect(tmpResult.corner1.y).to.equal(200); // midY
|
|
200
|
+
libExpect(tmpResult.corner2.x).to.equal(150);
|
|
201
|
+
libExpect(tmpResult.corner2.y).to.equal(200); // midY
|
|
202
|
+
fDone();
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
test
|
|
207
|
+
(
|
|
208
|
+
'should compute L-bend for horizontal-to-vertical',
|
|
209
|
+
function (fDone)
|
|
210
|
+
{
|
|
211
|
+
let tmpResult = _PathGenerator.computeAutoOrthogonalCorners(
|
|
212
|
+
100, 50,
|
|
213
|
+
300, 200,
|
|
214
|
+
{ dx: 1, dy: 0 }, // from horizontal
|
|
215
|
+
{ dx: 0, dy: -1 }, // to vertical
|
|
216
|
+
0
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// H→V: corner at (approachX, departY)
|
|
220
|
+
libExpect(tmpResult.corner1.x).to.equal(300);
|
|
221
|
+
libExpect(tmpResult.corner1.y).to.equal(50);
|
|
222
|
+
fDone();
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
test
|
|
227
|
+
(
|
|
228
|
+
'should compute L-bend for vertical-to-horizontal',
|
|
229
|
+
function (fDone)
|
|
230
|
+
{
|
|
231
|
+
let tmpResult = _PathGenerator.computeAutoOrthogonalCorners(
|
|
232
|
+
100, 50,
|
|
233
|
+
300, 200,
|
|
234
|
+
{ dx: 0, dy: 1 }, // from vertical
|
|
235
|
+
{ dx: -1, dy: 0 }, // to horizontal
|
|
236
|
+
0
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// V→H: corner at (departX, approachY)
|
|
240
|
+
libExpect(tmpResult.corner1.x).to.equal(100);
|
|
241
|
+
libExpect(tmpResult.corner1.y).to.equal(200);
|
|
242
|
+
fDone();
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
test
|
|
247
|
+
(
|
|
248
|
+
'should apply midOffset for both-horizontal',
|
|
249
|
+
function (fDone)
|
|
250
|
+
{
|
|
251
|
+
let tmpResult = _PathGenerator.computeAutoOrthogonalCorners(
|
|
252
|
+
100, 50,
|
|
253
|
+
300, 150,
|
|
254
|
+
{ dx: 1, dy: 0 },
|
|
255
|
+
{ dx: -1, dy: 0 },
|
|
256
|
+
25 // offset
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
libExpect(tmpResult.corner1.x).to.equal(225); // 200 + 25
|
|
260
|
+
libExpect(tmpResult.corner2.x).to.equal(225);
|
|
261
|
+
fDone();
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// ---- evaluateCubicBezier ----
|
|
268
|
+
|
|
269
|
+
suite
|
|
270
|
+
(
|
|
271
|
+
'evaluateCubicBezier',
|
|
272
|
+
function ()
|
|
273
|
+
{
|
|
274
|
+
test
|
|
275
|
+
(
|
|
276
|
+
'should return start point at t=0',
|
|
277
|
+
function (fDone)
|
|
278
|
+
{
|
|
279
|
+
let tmpResult = _PathGenerator.evaluateCubicBezier(
|
|
280
|
+
{ x: 10, y: 20 },
|
|
281
|
+
{ x: 40, y: 80 },
|
|
282
|
+
{ x: 60, y: 80 },
|
|
283
|
+
{ x: 90, y: 20 },
|
|
284
|
+
0
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
libExpect(tmpResult.x).to.be.closeTo(10, 0.001);
|
|
288
|
+
libExpect(tmpResult.y).to.be.closeTo(20, 0.001);
|
|
289
|
+
fDone();
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
test
|
|
294
|
+
(
|
|
295
|
+
'should return end point at t=1',
|
|
296
|
+
function (fDone)
|
|
297
|
+
{
|
|
298
|
+
let tmpResult = _PathGenerator.evaluateCubicBezier(
|
|
299
|
+
{ x: 10, y: 20 },
|
|
300
|
+
{ x: 40, y: 80 },
|
|
301
|
+
{ x: 60, y: 80 },
|
|
302
|
+
{ x: 90, y: 20 },
|
|
303
|
+
1
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
libExpect(tmpResult.x).to.be.closeTo(90, 0.001);
|
|
307
|
+
libExpect(tmpResult.y).to.be.closeTo(20, 0.001);
|
|
308
|
+
fDone();
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
test
|
|
313
|
+
(
|
|
314
|
+
'should return midpoint at t=0.5 for a straight-line bezier',
|
|
315
|
+
function (fDone)
|
|
316
|
+
{
|
|
317
|
+
// When all control points are collinear, the midpoint
|
|
318
|
+
// should be on the line
|
|
319
|
+
let tmpResult = _PathGenerator.evaluateCubicBezier(
|
|
320
|
+
{ x: 0, y: 0 },
|
|
321
|
+
{ x: 33.33, y: 0 },
|
|
322
|
+
{ x: 66.67, y: 0 },
|
|
323
|
+
{ x: 100, y: 0 },
|
|
324
|
+
0.5
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
libExpect(tmpResult.x).to.be.closeTo(50, 0.1);
|
|
328
|
+
libExpect(tmpResult.y).to.be.closeTo(0, 0.1);
|
|
329
|
+
fDone();
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
test
|
|
334
|
+
(
|
|
335
|
+
'should compute intermediate points for symmetric S-curve',
|
|
336
|
+
function (fDone)
|
|
337
|
+
{
|
|
338
|
+
let tmpResult = _PathGenerator.evaluateCubicBezier(
|
|
339
|
+
{ x: 0, y: 0 },
|
|
340
|
+
{ x: 0, y: 100 },
|
|
341
|
+
{ x: 100, y: -100 },
|
|
342
|
+
{ x: 100, y: 0 },
|
|
343
|
+
0.5
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// At t=0.5, x should be exactly 50 for this symmetric curve
|
|
347
|
+
libExpect(tmpResult.x).to.be.closeTo(50, 0.001);
|
|
348
|
+
// y should be 0 for this symmetric arrangement
|
|
349
|
+
libExpect(tmpResult.y).to.be.closeTo(0, 0.001);
|
|
350
|
+
fDone();
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// ---- buildBezierPathString ----
|
|
357
|
+
|
|
358
|
+
suite
|
|
359
|
+
(
|
|
360
|
+
'buildBezierPathString',
|
|
361
|
+
function ()
|
|
362
|
+
{
|
|
363
|
+
test
|
|
364
|
+
(
|
|
365
|
+
'should produce valid SVG path string',
|
|
366
|
+
function (fDone)
|
|
367
|
+
{
|
|
368
|
+
let tmpResult = _PathGenerator.buildBezierPathString(
|
|
369
|
+
{ x: 0, y: 0 },
|
|
370
|
+
{ x: 20, y: 0 },
|
|
371
|
+
{ x: 50, y: 0 },
|
|
372
|
+
{ x: 80, y: 0 },
|
|
373
|
+
{ x: 100, y: 0 },
|
|
374
|
+
{ x: 120, y: 0 }
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
libExpect(tmpResult).to.be.a('string');
|
|
378
|
+
libExpect(tmpResult).to.match(/^M /);
|
|
379
|
+
libExpect(tmpResult).to.contain('L');
|
|
380
|
+
libExpect(tmpResult).to.contain('C');
|
|
381
|
+
fDone();
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
test
|
|
386
|
+
(
|
|
387
|
+
'should include all coordinate values',
|
|
388
|
+
function (fDone)
|
|
389
|
+
{
|
|
390
|
+
let tmpResult = _PathGenerator.buildBezierPathString(
|
|
391
|
+
{ x: 10, y: 20 },
|
|
392
|
+
{ x: 30, y: 20 },
|
|
393
|
+
{ x: 50, y: 40 },
|
|
394
|
+
{ x: 70, y: 40 },
|
|
395
|
+
{ x: 90, y: 20 },
|
|
396
|
+
{ x: 110, y: 20 }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
libExpect(tmpResult).to.contain('M 10 20');
|
|
400
|
+
libExpect(tmpResult).to.contain('L 30 20');
|
|
401
|
+
libExpect(tmpResult).to.contain('C 50 40');
|
|
402
|
+
libExpect(tmpResult).to.contain('L 110 20');
|
|
403
|
+
fDone();
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// ---- buildSplitBezierPathString ----
|
|
410
|
+
|
|
411
|
+
suite
|
|
412
|
+
(
|
|
413
|
+
'buildSplitBezierPathString',
|
|
414
|
+
function ()
|
|
415
|
+
{
|
|
416
|
+
test
|
|
417
|
+
(
|
|
418
|
+
'should produce SVG path with two cubic segments',
|
|
419
|
+
function (fDone)
|
|
420
|
+
{
|
|
421
|
+
let tmpResult = _PathGenerator.buildSplitBezierPathString(
|
|
422
|
+
{ x: 0, y: 0 }, // start
|
|
423
|
+
{ x: 20, y: 0 }, // depart
|
|
424
|
+
{ x: 40, y: 10 }, // cp1a
|
|
425
|
+
{ x: 45, y: 20 }, // cp1b
|
|
426
|
+
{ x: 50, y: 30 }, // handle
|
|
427
|
+
{ x: 55, y: 20 }, // cp2a
|
|
428
|
+
{ x: 60, y: 10 }, // cp2b
|
|
429
|
+
{ x: 80, y: 0 }, // approach
|
|
430
|
+
{ x: 100, y: 0 } // end
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
libExpect(tmpResult).to.be.a('string');
|
|
434
|
+
// Should have M, L, two C commands, and final L
|
|
435
|
+
let tmpCCount = (tmpResult.match(/C /g) || []).length;
|
|
436
|
+
libExpect(tmpCCount).to.equal(2);
|
|
437
|
+
fDone();
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// ---- buildOrthogonalPathString ----
|
|
444
|
+
|
|
445
|
+
suite
|
|
446
|
+
(
|
|
447
|
+
'buildOrthogonalPathString',
|
|
448
|
+
function ()
|
|
449
|
+
{
|
|
450
|
+
test
|
|
451
|
+
(
|
|
452
|
+
'should produce SVG path with only L commands (no curves)',
|
|
453
|
+
function (fDone)
|
|
454
|
+
{
|
|
455
|
+
let tmpResult = _PathGenerator.buildOrthogonalPathString(
|
|
456
|
+
{ x: 0, y: 50 },
|
|
457
|
+
{ x: 20, y: 50 },
|
|
458
|
+
{ x: 60, y: 50 },
|
|
459
|
+
{ x: 60, y: 100 },
|
|
460
|
+
{ x: 80, y: 100 },
|
|
461
|
+
{ x: 100, y: 100 }
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
libExpect(tmpResult).to.match(/^M /);
|
|
465
|
+
libExpect(tmpResult).to.not.contain('C');
|
|
466
|
+
// Should have M + 5 L commands
|
|
467
|
+
let tmpLCount = (tmpResult.match(/L /g) || []).length;
|
|
468
|
+
libExpect(tmpLCount).to.equal(5);
|
|
469
|
+
fDone();
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// ---- computeDirectionalGeometry ----
|
|
476
|
+
|
|
477
|
+
suite
|
|
478
|
+
(
|
|
479
|
+
'computeDirectionalGeometry',
|
|
480
|
+
function ()
|
|
481
|
+
{
|
|
482
|
+
test
|
|
483
|
+
(
|
|
484
|
+
'should compute departure and approach for right-to-left facing',
|
|
485
|
+
function (fDone)
|
|
486
|
+
{
|
|
487
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
488
|
+
{ x: 100, y: 50, side: 'right' },
|
|
489
|
+
{ x: 400, y: 50, side: 'left' }
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
libExpect(tmpGeo.departX).to.equal(120);
|
|
493
|
+
libExpect(tmpGeo.departY).to.equal(50);
|
|
494
|
+
libExpect(tmpGeo.approachX).to.equal(380);
|
|
495
|
+
libExpect(tmpGeo.approachY).to.equal(50);
|
|
496
|
+
fDone();
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
test
|
|
501
|
+
(
|
|
502
|
+
'should use facing-each-other offset when ports face each other',
|
|
503
|
+
function (fDone)
|
|
504
|
+
{
|
|
505
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
506
|
+
{ x: 100, y: 50, side: 'right' },
|
|
507
|
+
{ x: 400, y: 50, side: 'left' }
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
// Facing: offset = max(inlineDist*0.35, 30)
|
|
511
|
+
// inlineDist = |380-120| = 260, offset = 91
|
|
512
|
+
let tmpOffset = tmpGeo.cp1X - tmpGeo.departX;
|
|
513
|
+
libExpect(tmpOffset).to.be.closeTo(91, 1);
|
|
514
|
+
fDone();
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
test
|
|
519
|
+
(
|
|
520
|
+
'should handle perpendicular exits',
|
|
521
|
+
function (fDone)
|
|
522
|
+
{
|
|
523
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
524
|
+
{ x: 100, y: 50, side: 'right' },
|
|
525
|
+
{ x: 300, y: 200, side: 'top' }
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
libExpect(tmpGeo.startDir).to.deep.equal({ dx: 1, dy: 0 });
|
|
529
|
+
libExpect(tmpGeo.endDir).to.deep.equal({ dx: 0, dy: -1 });
|
|
530
|
+
libExpect(tmpGeo.cp1X).to.be.greaterThan(tmpGeo.departX);
|
|
531
|
+
libExpect(tmpGeo.cp2Y).to.be.lessThan(tmpGeo.approachY);
|
|
532
|
+
fDone();
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
test
|
|
537
|
+
(
|
|
538
|
+
'should handle vertical facing ports',
|
|
539
|
+
function (fDone)
|
|
540
|
+
{
|
|
541
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
542
|
+
{ x: 100, y: 50, side: 'bottom' },
|
|
543
|
+
{ x: 100, y: 250, side: 'top' }
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
libExpect(tmpGeo.departY).to.equal(70);
|
|
547
|
+
libExpect(tmpGeo.approachY).to.equal(230);
|
|
548
|
+
fDone();
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
test
|
|
553
|
+
(
|
|
554
|
+
'should return direction vectors',
|
|
555
|
+
function (fDone)
|
|
556
|
+
{
|
|
557
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
558
|
+
{ x: 0, y: 0, side: 'bottom' },
|
|
559
|
+
{ x: 200, y: 200, side: 'left' }
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
libExpect(tmpGeo.startDir).to.deep.equal({ dx: 0, dy: 1 });
|
|
563
|
+
libExpect(tmpGeo.endDir).to.deep.equal({ dx: -1, dy: 0 });
|
|
564
|
+
fDone();
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
test
|
|
569
|
+
(
|
|
570
|
+
'should use same-axis-not-facing offset when both face same direction',
|
|
571
|
+
function (fDone)
|
|
572
|
+
{
|
|
573
|
+
// Right port facing right, left port facing left — reversed so not facing
|
|
574
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(
|
|
575
|
+
{ x: 400, y: 50, side: 'right' },
|
|
576
|
+
{ x: 100, y: 50, side: 'left' }
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Same axis, not facing: max(baseOffset, 60)
|
|
580
|
+
let tmpOffset = tmpGeo.cp1X - tmpGeo.departX;
|
|
581
|
+
libExpect(tmpOffset).to.be.at.least(60);
|
|
582
|
+
fDone();
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// ---- distanceToSegment ----
|
|
589
|
+
|
|
590
|
+
suite
|
|
591
|
+
(
|
|
592
|
+
'distanceToSegment',
|
|
593
|
+
function ()
|
|
594
|
+
{
|
|
595
|
+
test
|
|
596
|
+
(
|
|
597
|
+
'should return 0 for point on the segment',
|
|
598
|
+
function (fDone)
|
|
599
|
+
{
|
|
600
|
+
let tmpDist = _PathGenerator.distanceToSegment(50, 0, 0, 0, 100, 0);
|
|
601
|
+
libExpect(tmpDist).to.be.closeTo(0, 0.001);
|
|
602
|
+
fDone();
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
test
|
|
607
|
+
(
|
|
608
|
+
'should compute perpendicular distance',
|
|
609
|
+
function (fDone)
|
|
610
|
+
{
|
|
611
|
+
let tmpDist = _PathGenerator.distanceToSegment(50, 30, 0, 0, 100, 0);
|
|
612
|
+
libExpect(tmpDist).to.be.closeTo(30, 0.001);
|
|
613
|
+
fDone();
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
test
|
|
618
|
+
(
|
|
619
|
+
'should compute distance to endpoint when past segment',
|
|
620
|
+
function (fDone)
|
|
621
|
+
{
|
|
622
|
+
let tmpDist = _PathGenerator.distanceToSegment(110, 0, 0, 0, 100, 0);
|
|
623
|
+
libExpect(tmpDist).to.be.closeTo(10, 0.001);
|
|
624
|
+
fDone();
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
test
|
|
629
|
+
(
|
|
630
|
+
'should compute distance to start when before segment',
|
|
631
|
+
function (fDone)
|
|
632
|
+
{
|
|
633
|
+
let tmpDist = _PathGenerator.distanceToSegment(-20, 0, 0, 0, 100, 0);
|
|
634
|
+
libExpect(tmpDist).to.be.closeTo(20, 0.001);
|
|
635
|
+
fDone();
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
test
|
|
640
|
+
(
|
|
641
|
+
'should handle degenerate (zero-length) segment',
|
|
642
|
+
function (fDone)
|
|
643
|
+
{
|
|
644
|
+
let tmpDist = _PathGenerator.distanceToSegment(30, 40, 0, 0, 0, 0);
|
|
645
|
+
libExpect(tmpDist).to.be.closeTo(50, 0.001);
|
|
646
|
+
fDone();
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
test
|
|
651
|
+
(
|
|
652
|
+
'should handle diagonal segments',
|
|
653
|
+
function (fDone)
|
|
654
|
+
{
|
|
655
|
+
let tmpDist = _PathGenerator.distanceToSegment(0, 100, 0, 0, 100, 100);
|
|
656
|
+
libExpect(tmpDist).to.be.closeTo(70.71, 0.1);
|
|
657
|
+
fDone();
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// ---- getAutoMidpoint ----
|
|
664
|
+
|
|
665
|
+
suite
|
|
666
|
+
(
|
|
667
|
+
'getAutoMidpoint',
|
|
668
|
+
function ()
|
|
669
|
+
{
|
|
670
|
+
test
|
|
671
|
+
(
|
|
672
|
+
'should return a point between start and end',
|
|
673
|
+
function (fDone)
|
|
674
|
+
{
|
|
675
|
+
let tmpMid = _PathGenerator.getAutoMidpoint(
|
|
676
|
+
{ x: 0, y: 50, side: 'right' },
|
|
677
|
+
{ x: 400, y: 50, side: 'left' }
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
libExpect(tmpMid.x).to.be.greaterThan(0);
|
|
681
|
+
libExpect(tmpMid.x).to.be.lessThan(400);
|
|
682
|
+
fDone();
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
test
|
|
687
|
+
(
|
|
688
|
+
'should be near horizontal midpoint for symmetric endpoints',
|
|
689
|
+
function (fDone)
|
|
690
|
+
{
|
|
691
|
+
let tmpMid = _PathGenerator.getAutoMidpoint(
|
|
692
|
+
{ x: 0, y: 50, side: 'right' },
|
|
693
|
+
{ x: 400, y: 50, side: 'left' }
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
libExpect(tmpMid.x).to.be.closeTo(200, 20);
|
|
697
|
+
libExpect(tmpMid.y).to.be.closeTo(50, 5);
|
|
698
|
+
fDone();
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
test
|
|
703
|
+
(
|
|
704
|
+
'should handle vertical endpoints',
|
|
705
|
+
function (fDone)
|
|
706
|
+
{
|
|
707
|
+
let tmpMid = _PathGenerator.getAutoMidpoint(
|
|
708
|
+
{ x: 100, y: 0, side: 'bottom' },
|
|
709
|
+
{ x: 100, y: 400, side: 'top' }
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
libExpect(tmpMid.y).to.be.greaterThan(0);
|
|
713
|
+
libExpect(tmpMid.y).to.be.lessThan(400);
|
|
714
|
+
libExpect(tmpMid.x).to.be.closeTo(100, 5);
|
|
715
|
+
fDone();
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
test
|
|
720
|
+
(
|
|
721
|
+
'should match manual evaluateCubicBezier at t=0.5',
|
|
722
|
+
function (fDone)
|
|
723
|
+
{
|
|
724
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
725
|
+
let tmpEnd = { x: 400, y: 150, side: 'left' };
|
|
726
|
+
|
|
727
|
+
let tmpMid = _PathGenerator.getAutoMidpoint(tmpStart, tmpEnd);
|
|
728
|
+
let tmpGeo = _PathGenerator.computeDirectionalGeometry(tmpStart, tmpEnd);
|
|
729
|
+
|
|
730
|
+
let tmpManual = _PathGenerator.evaluateCubicBezier(
|
|
731
|
+
{ x: tmpGeo.departX, y: tmpGeo.departY },
|
|
732
|
+
{ x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
|
|
733
|
+
{ x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
|
|
734
|
+
{ x: tmpGeo.approachX, y: tmpGeo.approachY },
|
|
735
|
+
0.5
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
libExpect(tmpMid.x).to.be.closeTo(tmpManual.x, 0.001);
|
|
739
|
+
libExpect(tmpMid.y).to.be.closeTo(tmpManual.y, 0.001);
|
|
740
|
+
fDone();
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// ---- getAutoMidpointSimple ----
|
|
747
|
+
|
|
748
|
+
suite
|
|
749
|
+
(
|
|
750
|
+
'getAutoMidpointSimple',
|
|
751
|
+
function ()
|
|
752
|
+
{
|
|
753
|
+
test
|
|
754
|
+
(
|
|
755
|
+
'should return a point between from and to',
|
|
756
|
+
function (fDone)
|
|
757
|
+
{
|
|
758
|
+
let tmpMid = _PathGenerator.getAutoMidpointSimple(
|
|
759
|
+
{ x: 0, y: 50, side: 'right' },
|
|
760
|
+
{ x: 400, y: 50, side: 'left' },
|
|
761
|
+
20
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
libExpect(tmpMid.x).to.be.greaterThan(0);
|
|
765
|
+
libExpect(tmpMid.x).to.be.lessThan(400);
|
|
766
|
+
fDone();
|
|
767
|
+
}
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
test
|
|
771
|
+
(
|
|
772
|
+
'should be near horizontal midpoint for symmetric endpoints',
|
|
773
|
+
function (fDone)
|
|
774
|
+
{
|
|
775
|
+
let tmpMid = _PathGenerator.getAutoMidpointSimple(
|
|
776
|
+
{ x: 0, y: 50, side: 'right' },
|
|
777
|
+
{ x: 400, y: 50, side: 'left' },
|
|
778
|
+
20
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
libExpect(tmpMid.x).to.be.closeTo(200, 20);
|
|
782
|
+
libExpect(tmpMid.y).to.be.closeTo(50, 5);
|
|
783
|
+
fDone();
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
test
|
|
788
|
+
(
|
|
789
|
+
'should handle vertical endpoints',
|
|
790
|
+
function (fDone)
|
|
791
|
+
{
|
|
792
|
+
let tmpMid = _PathGenerator.getAutoMidpointSimple(
|
|
793
|
+
{ x: 100, y: 0, side: 'bottom' },
|
|
794
|
+
{ x: 100, y: 400, side: 'top' },
|
|
795
|
+
20
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
libExpect(tmpMid.y).to.be.greaterThan(0);
|
|
799
|
+
libExpect(tmpMid.y).to.be.lessThan(400);
|
|
800
|
+
libExpect(tmpMid.x).to.be.closeTo(100, 5);
|
|
801
|
+
fDone();
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
test
|
|
806
|
+
(
|
|
807
|
+
'should use span-based control points (different strategy than getAutoMidpoint)',
|
|
808
|
+
function (fDone)
|
|
809
|
+
{
|
|
810
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
811
|
+
let tmpEnd = { x: 400, y: 200, side: 'left' };
|
|
812
|
+
|
|
813
|
+
let tmpSimple = _PathGenerator.getAutoMidpointSimple(tmpStart, tmpEnd, 20);
|
|
814
|
+
let tmpFull = _PathGenerator.getAutoMidpoint(tmpStart, tmpEnd);
|
|
815
|
+
|
|
816
|
+
// Both should be valid midpoints
|
|
817
|
+
libExpect(tmpSimple.x).to.be.a('number');
|
|
818
|
+
libExpect(tmpSimple.y).to.be.a('number');
|
|
819
|
+
libExpect(tmpFull.x).to.be.a('number');
|
|
820
|
+
libExpect(tmpFull.y).to.be.a('number');
|
|
821
|
+
fDone();
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// ---- buildMultiBezierPathString ----
|
|
828
|
+
|
|
829
|
+
suite
|
|
830
|
+
(
|
|
831
|
+
'buildMultiBezierPathString',
|
|
832
|
+
function ()
|
|
833
|
+
{
|
|
834
|
+
test
|
|
835
|
+
(
|
|
836
|
+
'should produce path with N+1 cubic segments for N handles',
|
|
837
|
+
function (fDone)
|
|
838
|
+
{
|
|
839
|
+
let tmpHandles = [
|
|
840
|
+
{ x: 150, y: 80 },
|
|
841
|
+
{ x: 250, y: 120 }
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
845
|
+
{ x: 0, y: 50 }, // start
|
|
846
|
+
{ x: 20, y: 50 }, // depart
|
|
847
|
+
tmpHandles,
|
|
848
|
+
{ x: 380, y: 50 }, // approach
|
|
849
|
+
{ x: 400, y: 50 }, // end
|
|
850
|
+
{ dx: 1, dy: 0 }, // startDir
|
|
851
|
+
{ dx: -1, dy: 0 } // endDir
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
libExpect(tmpResult).to.be.a('string');
|
|
855
|
+
// 2 handles → 3 segments → 3 C commands
|
|
856
|
+
let tmpCCount = (tmpResult.match(/C /g) || []).length;
|
|
857
|
+
libExpect(tmpCCount).to.equal(3);
|
|
858
|
+
fDone();
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
test
|
|
863
|
+
(
|
|
864
|
+
'should produce path with 2 cubic segments for 1 handle',
|
|
865
|
+
function (fDone)
|
|
866
|
+
{
|
|
867
|
+
let tmpHandles = [{ x: 200, y: 100 }];
|
|
868
|
+
|
|
869
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
870
|
+
{ x: 0, y: 50 },
|
|
871
|
+
{ x: 20, y: 50 },
|
|
872
|
+
tmpHandles,
|
|
873
|
+
{ x: 380, y: 50 },
|
|
874
|
+
{ x: 400, y: 50 },
|
|
875
|
+
{ dx: 1, dy: 0 },
|
|
876
|
+
{ dx: -1, dy: 0 }
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
let tmpCCount = (tmpResult.match(/C /g) || []).length;
|
|
880
|
+
libExpect(tmpCCount).to.equal(2);
|
|
881
|
+
fDone();
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
test
|
|
886
|
+
(
|
|
887
|
+
'should produce single segment for zero handles',
|
|
888
|
+
function (fDone)
|
|
889
|
+
{
|
|
890
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
891
|
+
{ x: 0, y: 50 },
|
|
892
|
+
{ x: 20, y: 50 },
|
|
893
|
+
[],
|
|
894
|
+
{ x: 380, y: 50 },
|
|
895
|
+
{ x: 400, y: 50 },
|
|
896
|
+
{ dx: 1, dy: 0 },
|
|
897
|
+
{ dx: -1, dy: 0 }
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
let tmpCCount = (tmpResult.match(/C /g) || []).length;
|
|
901
|
+
libExpect(tmpCCount).to.equal(1);
|
|
902
|
+
fDone();
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
test
|
|
907
|
+
(
|
|
908
|
+
'should start with M and end with L',
|
|
909
|
+
function (fDone)
|
|
910
|
+
{
|
|
911
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
912
|
+
{ x: 10, y: 20 },
|
|
913
|
+
{ x: 30, y: 20 },
|
|
914
|
+
[{ x: 100, y: 80 }],
|
|
915
|
+
{ x: 170, y: 20 },
|
|
916
|
+
{ x: 190, y: 20 },
|
|
917
|
+
{ dx: 1, dy: 0 },
|
|
918
|
+
{ dx: -1, dy: 0 }
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
libExpect(tmpResult).to.match(/^M 10 20/);
|
|
922
|
+
libExpect(tmpResult).to.match(/L 190 20$/);
|
|
923
|
+
fDone();
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
test
|
|
928
|
+
(
|
|
929
|
+
'should handle vertical start/end directions',
|
|
930
|
+
function (fDone)
|
|
931
|
+
{
|
|
932
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
933
|
+
{ x: 100, y: 0 },
|
|
934
|
+
{ x: 100, y: 20 },
|
|
935
|
+
[{ x: 200, y: 100 }],
|
|
936
|
+
{ x: 100, y: 180 },
|
|
937
|
+
{ x: 100, y: 200 },
|
|
938
|
+
{ dx: 0, dy: 1 }, // downward departure
|
|
939
|
+
{ dx: 0, dy: -1 } // upward approach
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
libExpect(tmpResult).to.be.a('string');
|
|
943
|
+
libExpect(tmpResult).to.contain('C ');
|
|
944
|
+
fDone();
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
test
|
|
949
|
+
(
|
|
950
|
+
'should handle many handles (5+)',
|
|
951
|
+
function (fDone)
|
|
952
|
+
{
|
|
953
|
+
let tmpHandles = [];
|
|
954
|
+
for (let i = 0; i < 5; i++)
|
|
955
|
+
{
|
|
956
|
+
tmpHandles.push({ x: 50 + i * 60, y: 50 + (i % 2) * 80 });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
let tmpResult = _PathGenerator.buildMultiBezierPathString(
|
|
960
|
+
{ x: 0, y: 50 },
|
|
961
|
+
{ x: 20, y: 50 },
|
|
962
|
+
tmpHandles,
|
|
963
|
+
{ x: 380, y: 50 },
|
|
964
|
+
{ x: 400, y: 50 },
|
|
965
|
+
{ dx: 1, dy: 0 },
|
|
966
|
+
{ dx: -1, dy: 0 }
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// 5 handles → 6 segments → 6 C commands
|
|
970
|
+
let tmpCCount = (tmpResult.match(/C /g) || []).length;
|
|
971
|
+
libExpect(tmpCCount).to.equal(6);
|
|
972
|
+
fDone();
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
);
|