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,591 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libConnectionRenderer = require('../source/services/PictService-Flow-ConnectionRenderer.js');
|
|
6
|
+
const libPathGenerator = require('../source/services/PictService-Flow-PathGenerator.js');
|
|
7
|
+
const libGeometry = require('../source/providers/PictProvider-Flow-Geometry.js');
|
|
8
|
+
|
|
9
|
+
suite
|
|
10
|
+
(
|
|
11
|
+
'PictService-Flow-ConnectionRenderer',
|
|
12
|
+
function ()
|
|
13
|
+
{
|
|
14
|
+
let _Fable;
|
|
15
|
+
let _Renderer;
|
|
16
|
+
let _GeometryProvider;
|
|
17
|
+
let _PathGenerator;
|
|
18
|
+
|
|
19
|
+
setup
|
|
20
|
+
(
|
|
21
|
+
function ()
|
|
22
|
+
{
|
|
23
|
+
_Fable = new libFable({});
|
|
24
|
+
|
|
25
|
+
_GeometryProvider = new libGeometry(_Fable, {}, 'Geometry-Test');
|
|
26
|
+
_PathGenerator = new libPathGenerator(_Fable, {}, 'PathGen-Test');
|
|
27
|
+
|
|
28
|
+
// Build a mock FlowView with real providers
|
|
29
|
+
let tmpMockFlowView =
|
|
30
|
+
{
|
|
31
|
+
_GeometryProvider: _GeometryProvider,
|
|
32
|
+
_PathGenerator: _PathGenerator,
|
|
33
|
+
_SVGHelperProvider: null,
|
|
34
|
+
_ConnectorShapesProvider: null,
|
|
35
|
+
_ThemeProvider: null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Wire up PathGenerator's FlowView reference
|
|
39
|
+
_PathGenerator._FlowView = tmpMockFlowView;
|
|
40
|
+
|
|
41
|
+
_Renderer = new libConnectionRenderer(
|
|
42
|
+
_Fable, { FlowView: tmpMockFlowView }, 'ConnRend-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(_Renderer.serviceType).to.equal('PictServiceFlowConnectionRenderer');
|
|
59
|
+
fDone();
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ---- _getBezierHandles ----
|
|
66
|
+
|
|
67
|
+
suite
|
|
68
|
+
(
|
|
69
|
+
'_getBezierHandles',
|
|
70
|
+
function ()
|
|
71
|
+
{
|
|
72
|
+
test
|
|
73
|
+
(
|
|
74
|
+
'should return empty array when no data',
|
|
75
|
+
function (fDone)
|
|
76
|
+
{
|
|
77
|
+
libExpect(_Renderer._getBezierHandles(null)).to.deep.equal([]);
|
|
78
|
+
libExpect(_Renderer._getBezierHandles({})).to.deep.equal([]);
|
|
79
|
+
fDone();
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
test
|
|
84
|
+
(
|
|
85
|
+
'should return empty array when HandleCustomized is false',
|
|
86
|
+
function (fDone)
|
|
87
|
+
{
|
|
88
|
+
let tmpData = { HandleCustomized: false, BezierHandleX: 100, BezierHandleY: 50 };
|
|
89
|
+
libExpect(_Renderer._getBezierHandles(tmpData)).to.deep.equal([]);
|
|
90
|
+
fDone();
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
test
|
|
95
|
+
(
|
|
96
|
+
'should migrate legacy BezierHandleX/Y to single-element array',
|
|
97
|
+
function (fDone)
|
|
98
|
+
{
|
|
99
|
+
let tmpData =
|
|
100
|
+
{
|
|
101
|
+
HandleCustomized: true,
|
|
102
|
+
BezierHandleX: 150,
|
|
103
|
+
BezierHandleY: 75
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let tmpResult = _Renderer._getBezierHandles(tmpData);
|
|
107
|
+
libExpect(tmpResult).to.have.length(1);
|
|
108
|
+
libExpect(tmpResult[0].x).to.equal(150);
|
|
109
|
+
libExpect(tmpResult[0].y).to.equal(75);
|
|
110
|
+
fDone();
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
test
|
|
115
|
+
(
|
|
116
|
+
'should return BezierHandles array when present',
|
|
117
|
+
function (fDone)
|
|
118
|
+
{
|
|
119
|
+
let tmpData =
|
|
120
|
+
{
|
|
121
|
+
HandleCustomized: true,
|
|
122
|
+
BezierHandles: [
|
|
123
|
+
{ x: 100, y: 50 },
|
|
124
|
+
{ x: 200, y: 80 },
|
|
125
|
+
{ x: 300, y: 50 }
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let tmpResult = _Renderer._getBezierHandles(tmpData);
|
|
130
|
+
libExpect(tmpResult).to.have.length(3);
|
|
131
|
+
libExpect(tmpResult[1].x).to.equal(200);
|
|
132
|
+
fDone();
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
test
|
|
137
|
+
(
|
|
138
|
+
'should prefer BezierHandles over legacy format',
|
|
139
|
+
function (fDone)
|
|
140
|
+
{
|
|
141
|
+
let tmpData =
|
|
142
|
+
{
|
|
143
|
+
HandleCustomized: true,
|
|
144
|
+
BezierHandles: [{ x: 100, y: 50 }],
|
|
145
|
+
BezierHandleX: 999,
|
|
146
|
+
BezierHandleY: 999
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
let tmpResult = _Renderer._getBezierHandles(tmpData);
|
|
150
|
+
libExpect(tmpResult).to.have.length(1);
|
|
151
|
+
libExpect(tmpResult[0].x).to.equal(100);
|
|
152
|
+
fDone();
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
test
|
|
157
|
+
(
|
|
158
|
+
'should return empty for empty BezierHandles with legacy fallback',
|
|
159
|
+
function (fDone)
|
|
160
|
+
{
|
|
161
|
+
let tmpData =
|
|
162
|
+
{
|
|
163
|
+
HandleCustomized: true,
|
|
164
|
+
BezierHandles: [],
|
|
165
|
+
BezierHandleX: 150,
|
|
166
|
+
BezierHandleY: 75
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Empty array → falls through to legacy
|
|
170
|
+
let tmpResult = _Renderer._getBezierHandles(tmpData);
|
|
171
|
+
libExpect(tmpResult).to.have.length(1);
|
|
172
|
+
libExpect(tmpResult[0].x).to.equal(150);
|
|
173
|
+
fDone();
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ---- _distanceToSegment ----
|
|
180
|
+
|
|
181
|
+
suite
|
|
182
|
+
(
|
|
183
|
+
'_distanceToSegment',
|
|
184
|
+
function ()
|
|
185
|
+
{
|
|
186
|
+
test
|
|
187
|
+
(
|
|
188
|
+
'should return 0 for point on the segment',
|
|
189
|
+
function (fDone)
|
|
190
|
+
{
|
|
191
|
+
// Point at midpoint of horizontal segment
|
|
192
|
+
let tmpDist = _Renderer._distanceToSegment(50, 0, 0, 0, 100, 0);
|
|
193
|
+
libExpect(tmpDist).to.be.closeTo(0, 0.001);
|
|
194
|
+
fDone();
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
test
|
|
199
|
+
(
|
|
200
|
+
'should compute perpendicular distance',
|
|
201
|
+
function (fDone)
|
|
202
|
+
{
|
|
203
|
+
// Point 30 units above the midpoint of a horizontal segment
|
|
204
|
+
let tmpDist = _Renderer._distanceToSegment(50, 30, 0, 0, 100, 0);
|
|
205
|
+
libExpect(tmpDist).to.be.closeTo(30, 0.001);
|
|
206
|
+
fDone();
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
test
|
|
211
|
+
(
|
|
212
|
+
'should compute distance to endpoint when past segment',
|
|
213
|
+
function (fDone)
|
|
214
|
+
{
|
|
215
|
+
// Point beyond the end of a horizontal segment
|
|
216
|
+
let tmpDist = _Renderer._distanceToSegment(110, 0, 0, 0, 100, 0);
|
|
217
|
+
libExpect(tmpDist).to.be.closeTo(10, 0.001);
|
|
218
|
+
fDone();
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
test
|
|
223
|
+
(
|
|
224
|
+
'should compute distance to start when before segment',
|
|
225
|
+
function (fDone)
|
|
226
|
+
{
|
|
227
|
+
// Point before the start of a horizontal segment
|
|
228
|
+
let tmpDist = _Renderer._distanceToSegment(-20, 0, 0, 0, 100, 0);
|
|
229
|
+
libExpect(tmpDist).to.be.closeTo(20, 0.001);
|
|
230
|
+
fDone();
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
test
|
|
235
|
+
(
|
|
236
|
+
'should handle degenerate (zero-length) segment',
|
|
237
|
+
function (fDone)
|
|
238
|
+
{
|
|
239
|
+
let tmpDist = _Renderer._distanceToSegment(30, 40, 0, 0, 0, 0);
|
|
240
|
+
libExpect(tmpDist).to.be.closeTo(50, 0.001); // sqrt(900+1600)
|
|
241
|
+
fDone();
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
test
|
|
246
|
+
(
|
|
247
|
+
'should handle diagonal segments',
|
|
248
|
+
function (fDone)
|
|
249
|
+
{
|
|
250
|
+
// Diagonal from (0,0) to (100,100), point at (0,100) (perpendicular)
|
|
251
|
+
let tmpDist = _Renderer._distanceToSegment(0, 100, 0, 0, 100, 100);
|
|
252
|
+
// Distance from (0,100) to the line y=x is |0-100|/sqrt(2) ≈ 70.71
|
|
253
|
+
libExpect(tmpDist).to.be.closeTo(70.71, 0.1);
|
|
254
|
+
fDone();
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// ---- computeInsertionIndex ----
|
|
261
|
+
|
|
262
|
+
suite
|
|
263
|
+
(
|
|
264
|
+
'computeInsertionIndex',
|
|
265
|
+
function ()
|
|
266
|
+
{
|
|
267
|
+
test
|
|
268
|
+
(
|
|
269
|
+
'should return 0 for click near the only segment (no handles)',
|
|
270
|
+
function (fDone)
|
|
271
|
+
{
|
|
272
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
273
|
+
let tmpEnd = { x: 400, y: 50, side: 'left' };
|
|
274
|
+
|
|
275
|
+
let tmpIndex = _Renderer.computeInsertionIndex(
|
|
276
|
+
[], { x: 200, y: 50 }, tmpStart, tmpEnd);
|
|
277
|
+
|
|
278
|
+
libExpect(tmpIndex).to.equal(0);
|
|
279
|
+
fDone();
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
test
|
|
284
|
+
(
|
|
285
|
+
'should return 0 for click before first handle',
|
|
286
|
+
function (fDone)
|
|
287
|
+
{
|
|
288
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
289
|
+
let tmpEnd = { x: 400, y: 50, side: 'left' };
|
|
290
|
+
let tmpHandles = [{ x: 250, y: 100 }];
|
|
291
|
+
|
|
292
|
+
// Click near the start, before the handle
|
|
293
|
+
let tmpIndex = _Renderer.computeInsertionIndex(
|
|
294
|
+
tmpHandles, { x: 60, y: 50 }, tmpStart, tmpEnd);
|
|
295
|
+
|
|
296
|
+
libExpect(tmpIndex).to.equal(0);
|
|
297
|
+
fDone();
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
test
|
|
302
|
+
(
|
|
303
|
+
'should return 1 for click after first handle',
|
|
304
|
+
function (fDone)
|
|
305
|
+
{
|
|
306
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
307
|
+
let tmpEnd = { x: 400, y: 50, side: 'left' };
|
|
308
|
+
let tmpHandles = [{ x: 100, y: 100 }];
|
|
309
|
+
|
|
310
|
+
// Click near the end, after the handle
|
|
311
|
+
let tmpIndex = _Renderer.computeInsertionIndex(
|
|
312
|
+
tmpHandles, { x: 340, y: 50 }, tmpStart, tmpEnd);
|
|
313
|
+
|
|
314
|
+
libExpect(tmpIndex).to.equal(1);
|
|
315
|
+
fDone();
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
test
|
|
320
|
+
(
|
|
321
|
+
'should pick correct segment among multiple handles',
|
|
322
|
+
function (fDone)
|
|
323
|
+
{
|
|
324
|
+
let tmpStart = { x: 0, y: 50, side: 'right' };
|
|
325
|
+
let tmpEnd = { x: 600, y: 50, side: 'left' };
|
|
326
|
+
let tmpHandles = [
|
|
327
|
+
{ x: 150, y: 100 },
|
|
328
|
+
{ x: 300, y: 50 },
|
|
329
|
+
{ x: 450, y: 100 }
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
// Click between handle[1] and handle[2]
|
|
333
|
+
let tmpIndex = _Renderer.computeInsertionIndex(
|
|
334
|
+
tmpHandles, { x: 375, y: 75 }, tmpStart, tmpEnd);
|
|
335
|
+
|
|
336
|
+
libExpect(tmpIndex).to.equal(2);
|
|
337
|
+
fDone();
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// ---- _computeDirectionalGeometry ----
|
|
344
|
+
|
|
345
|
+
suite
|
|
346
|
+
(
|
|
347
|
+
'_computeDirectionalGeometry',
|
|
348
|
+
function ()
|
|
349
|
+
{
|
|
350
|
+
test
|
|
351
|
+
(
|
|
352
|
+
'should compute departure and approach with straight segments',
|
|
353
|
+
function (fDone)
|
|
354
|
+
{
|
|
355
|
+
let tmpGeo = _Renderer._computeDirectionalGeometry(
|
|
356
|
+
{ x: 100, y: 50, side: 'right' },
|
|
357
|
+
{ x: 400, y: 150, side: 'left' }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Departure: x + dx*20 = 100 + 1*20 = 120
|
|
361
|
+
libExpect(tmpGeo.departX).to.equal(120);
|
|
362
|
+
libExpect(tmpGeo.departY).to.equal(50);
|
|
363
|
+
// Approach: x + dx*20 = 400 + (-1)*20 = 380
|
|
364
|
+
libExpect(tmpGeo.approachX).to.equal(380);
|
|
365
|
+
libExpect(tmpGeo.approachY).to.equal(150);
|
|
366
|
+
fDone();
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
test
|
|
371
|
+
(
|
|
372
|
+
'should compute control points extending in port direction',
|
|
373
|
+
function (fDone)
|
|
374
|
+
{
|
|
375
|
+
let tmpGeo = _Renderer._computeDirectionalGeometry(
|
|
376
|
+
{ x: 100, y: 50, side: 'right' },
|
|
377
|
+
{ x: 400, y: 50, side: 'left' }
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// CP1 should be to the right of depart (dx=1)
|
|
381
|
+
libExpect(tmpGeo.cp1X).to.be.greaterThan(tmpGeo.departX);
|
|
382
|
+
libExpect(tmpGeo.cp1Y).to.equal(tmpGeo.departY);
|
|
383
|
+
|
|
384
|
+
// CP2 should be to the left of approach (dx=-1, so approach + endDir*offset)
|
|
385
|
+
libExpect(tmpGeo.cp2X).to.be.lessThan(tmpGeo.approachX);
|
|
386
|
+
libExpect(tmpGeo.cp2Y).to.equal(tmpGeo.approachY);
|
|
387
|
+
fDone();
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
test
|
|
392
|
+
(
|
|
393
|
+
'should handle vertical port directions',
|
|
394
|
+
function (fDone)
|
|
395
|
+
{
|
|
396
|
+
let tmpGeo = _Renderer._computeDirectionalGeometry(
|
|
397
|
+
{ x: 100, y: 50, side: 'bottom' },
|
|
398
|
+
{ x: 100, y: 250, side: 'top' }
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Bottom port: dy=1, so depart is below start
|
|
402
|
+
libExpect(tmpGeo.departY).to.equal(70); // 50 + 1*20
|
|
403
|
+
// Top port: dy=-1, so approach is above end
|
|
404
|
+
libExpect(tmpGeo.approachY).to.equal(230); // 250 + (-1)*20
|
|
405
|
+
fDone();
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
test
|
|
410
|
+
(
|
|
411
|
+
'should return direction vectors',
|
|
412
|
+
function (fDone)
|
|
413
|
+
{
|
|
414
|
+
let tmpGeo = _Renderer._computeDirectionalGeometry(
|
|
415
|
+
{ x: 100, y: 50, side: 'right' },
|
|
416
|
+
{ x: 400, y: 150, side: 'top' }
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
libExpect(tmpGeo.startDir).to.deep.equal({ dx: 1, dy: 0 });
|
|
420
|
+
libExpect(tmpGeo.endDir).to.deep.equal({ dx: 0, dy: -1 });
|
|
421
|
+
fDone();
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// ---- _generateDirectionalPath ----
|
|
428
|
+
|
|
429
|
+
suite
|
|
430
|
+
(
|
|
431
|
+
'_generateDirectionalPath',
|
|
432
|
+
function ()
|
|
433
|
+
{
|
|
434
|
+
test
|
|
435
|
+
(
|
|
436
|
+
'should produce valid SVG path string',
|
|
437
|
+
function (fDone)
|
|
438
|
+
{
|
|
439
|
+
let tmpPath = _Renderer._generateDirectionalPath(
|
|
440
|
+
{ x: 0, y: 50, side: 'right' },
|
|
441
|
+
{ x: 300, y: 50, side: 'left' }
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
libExpect(tmpPath).to.be.a('string');
|
|
445
|
+
libExpect(tmpPath).to.match(/^M /);
|
|
446
|
+
libExpect(tmpPath).to.contain('C');
|
|
447
|
+
libExpect(tmpPath).to.contain('L');
|
|
448
|
+
fDone();
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// ---- _generateMultiHandleBezierPath ----
|
|
455
|
+
|
|
456
|
+
suite
|
|
457
|
+
(
|
|
458
|
+
'_generateMultiHandleBezierPath',
|
|
459
|
+
function ()
|
|
460
|
+
{
|
|
461
|
+
test
|
|
462
|
+
(
|
|
463
|
+
'should produce path with correct segment count for 1 handle',
|
|
464
|
+
function (fDone)
|
|
465
|
+
{
|
|
466
|
+
let tmpPath = _Renderer._generateMultiHandleBezierPath(
|
|
467
|
+
{ x: 0, y: 50, side: 'right' },
|
|
468
|
+
{ x: 400, y: 50, side: 'left' },
|
|
469
|
+
[{ x: 200, y: 100 }]
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
libExpect(tmpPath).to.be.a('string');
|
|
473
|
+
// 1 handle → 2 cubic segments
|
|
474
|
+
let tmpCCount = (tmpPath.match(/C /g) || []).length;
|
|
475
|
+
libExpect(tmpCCount).to.equal(2);
|
|
476
|
+
fDone();
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
test
|
|
481
|
+
(
|
|
482
|
+
'should produce path with 3 segments for 2 handles',
|
|
483
|
+
function (fDone)
|
|
484
|
+
{
|
|
485
|
+
let tmpPath = _Renderer._generateMultiHandleBezierPath(
|
|
486
|
+
{ x: 0, y: 50, side: 'right' },
|
|
487
|
+
{ x: 400, y: 50, side: 'left' },
|
|
488
|
+
[{ x: 120, y: 80 }, { x: 280, y: 80 }]
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
let tmpCCount = (tmpPath.match(/C /g) || []).length;
|
|
492
|
+
libExpect(tmpCCount).to.equal(3);
|
|
493
|
+
fDone();
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// ---- getAutoMidpoint ----
|
|
500
|
+
|
|
501
|
+
suite
|
|
502
|
+
(
|
|
503
|
+
'getAutoMidpoint',
|
|
504
|
+
function ()
|
|
505
|
+
{
|
|
506
|
+
test
|
|
507
|
+
(
|
|
508
|
+
'should return a point between start and end',
|
|
509
|
+
function (fDone)
|
|
510
|
+
{
|
|
511
|
+
let tmpMid = _Renderer.getAutoMidpoint(
|
|
512
|
+
{ x: 0, y: 50, side: 'right' },
|
|
513
|
+
{ x: 400, y: 50, side: 'left' }
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
libExpect(tmpMid.x).to.be.greaterThan(0);
|
|
517
|
+
libExpect(tmpMid.x).to.be.lessThan(400);
|
|
518
|
+
fDone();
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
test
|
|
523
|
+
(
|
|
524
|
+
'should be near horizontal midpoint for same-y endpoints',
|
|
525
|
+
function (fDone)
|
|
526
|
+
{
|
|
527
|
+
let tmpMid = _Renderer.getAutoMidpoint(
|
|
528
|
+
{ x: 0, y: 50, side: 'right' },
|
|
529
|
+
{ x: 400, y: 50, side: 'left' }
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// For symmetric right→left facing each other, midpoint x should
|
|
533
|
+
// be near 200, and y should stay near 50
|
|
534
|
+
libExpect(tmpMid.x).to.be.closeTo(200, 20);
|
|
535
|
+
libExpect(tmpMid.y).to.be.closeTo(50, 5);
|
|
536
|
+
fDone();
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// ---- _generateOrthogonalPath ----
|
|
543
|
+
|
|
544
|
+
suite
|
|
545
|
+
(
|
|
546
|
+
'_generateOrthogonalPath',
|
|
547
|
+
function ()
|
|
548
|
+
{
|
|
549
|
+
test
|
|
550
|
+
(
|
|
551
|
+
'should produce orthogonal path with no curves',
|
|
552
|
+
function (fDone)
|
|
553
|
+
{
|
|
554
|
+
let tmpPath = _Renderer._generateOrthogonalPath(
|
|
555
|
+
{ x: 0, y: 50, side: 'right' },
|
|
556
|
+
{ x: 300, y: 150, side: 'left' },
|
|
557
|
+
null, 0
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
libExpect(tmpPath).to.be.a('string');
|
|
561
|
+
libExpect(tmpPath).to.not.contain('C');
|
|
562
|
+
fDone();
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
test
|
|
567
|
+
(
|
|
568
|
+
'should use provided corners when given',
|
|
569
|
+
function (fDone)
|
|
570
|
+
{
|
|
571
|
+
let tmpCorners =
|
|
572
|
+
{
|
|
573
|
+
corner1: { x: 100, y: 50 },
|
|
574
|
+
corner2: { x: 100, y: 150 }
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
let tmpPath = _Renderer._generateOrthogonalPath(
|
|
578
|
+
{ x: 0, y: 50, side: 'right' },
|
|
579
|
+
{ x: 300, y: 150, side: 'left' },
|
|
580
|
+
tmpCorners, 0
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
libExpect(tmpPath).to.contain('100 50');
|
|
584
|
+
libExpect(tmpPath).to.contain('100 150');
|
|
585
|
+
fDone();
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
);
|