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.
Files changed (88) hide show
  1. package/.claude/launch.json +1 -1
  2. package/README.md +176 -0
  3. package/docs/.nojekyll +0 -0
  4. package/docs/Architecture.md +303 -0
  5. package/docs/Custom-Styling.md +275 -0
  6. package/docs/Data_Model.md +158 -0
  7. package/docs/Event_System.md +156 -0
  8. package/docs/Getting_Started.md +237 -0
  9. package/docs/Implementation_Reference.md +528 -0
  10. package/docs/Layout_Persistence.md +117 -0
  11. package/docs/README.md +115 -52
  12. package/docs/_cover.md +11 -0
  13. package/docs/_sidebar.md +52 -0
  14. package/docs/_topbar.md +8 -0
  15. package/docs/api/PictFlowCard.md +216 -0
  16. package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
  17. package/docs/api/addConnection.md +101 -0
  18. package/docs/api/addNode.md +137 -0
  19. package/docs/api/autoLayout.md +77 -0
  20. package/docs/api/getFlowData.md +112 -0
  21. package/docs/api/marshalToView.md +95 -0
  22. package/docs/api/openPanel.md +128 -0
  23. package/docs/api/registerHandler.md +174 -0
  24. package/docs/api/registerNodeType.md +142 -0
  25. package/docs/api/removeConnection.md +57 -0
  26. package/docs/api/removeNode.md +80 -0
  27. package/docs/api/saveLayout.md +152 -0
  28. package/docs/api/screenToSVGCoords.md +68 -0
  29. package/docs/api/selectNode.md +116 -0
  30. package/docs/api/setTheme.md +168 -0
  31. package/docs/api/setZoom.md +97 -0
  32. package/docs/api/toggleFullscreen.md +68 -0
  33. package/docs/card-help/EACH.md +19 -0
  34. package/docs/card-help/FREAD.md +24 -0
  35. package/docs/card-help/FWRITE.md +24 -0
  36. package/docs/card-help/GET.md +22 -0
  37. package/docs/card-help/ITE.md +23 -0
  38. package/docs/card-help/LOG.md +23 -0
  39. package/docs/card-help/NOTE.md +17 -0
  40. package/docs/card-help/PREV.md +18 -0
  41. package/docs/card-help/SET.md +27 -0
  42. package/docs/card-help/SPKL.md +22 -0
  43. package/docs/card-help/STAT.md +23 -0
  44. package/docs/card-help/SW.md +25 -0
  45. package/docs/css/docuserve.css +73 -0
  46. package/docs/index.html +39 -0
  47. package/docs/retold-catalog.json +169 -0
  48. package/docs/retold-keyword-index.json +13942 -0
  49. package/example_applications/simple_cards/package.json +1 -0
  50. package/example_applications/simple_cards/source/card-help-content.js +16 -0
  51. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
  52. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
  53. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
  54. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
  55. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
  56. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
  57. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
  58. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
  59. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
  60. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
  61. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
  62. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
  63. package/package.json +11 -7
  64. package/scripts/generate-card-help.js +214 -0
  65. package/source/Pict-Section-Flow.js +4 -0
  66. package/source/PictFlowCard.js +3 -1
  67. package/source/providers/PictProvider-Flow-CSS.js +245 -152
  68. package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
  69. package/source/providers/PictProvider-Flow-Geometry.js +195 -38
  70. package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
  71. package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
  72. package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
  73. package/source/services/PictService-Flow-DataManager.js +338 -0
  74. package/source/services/PictService-Flow-InteractionManager.js +165 -7
  75. package/source/services/PictService-Flow-PathGenerator.js +282 -0
  76. package/source/services/PictService-Flow-PortRenderer.js +269 -0
  77. package/source/services/PictService-Flow-RenderManager.js +281 -0
  78. package/source/services/PictService-Flow-Tether.js +6 -42
  79. package/source/views/PictView-Flow-Node.js +2 -220
  80. package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
  81. package/source/views/PictView-Flow.js +130 -882
  82. package/test/ConnectionHandleManager_tests.js +717 -0
  83. package/test/ConnectionRenderer_tests.js +591 -0
  84. package/test/DataManager_tests.js +859 -0
  85. package/test/Geometry_tests.js +767 -0
  86. package/test/PathGenerator_tests.js +978 -0
  87. package/test/PortRenderer_tests.js +367 -0
  88. package/test/RenderManager_tests.js +756 -0
@@ -0,0 +1,767 @@
1
+ const libFable = require('fable');
2
+ const libChai = require('chai');
3
+ const libExpect = libChai.expect;
4
+
5
+ const libGeometry = require('../source/providers/PictProvider-Flow-Geometry.js');
6
+
7
+ suite
8
+ (
9
+ 'PictProvider-Flow-Geometry',
10
+ function ()
11
+ {
12
+ let _Fable;
13
+ let _Geometry;
14
+
15
+ setup
16
+ (
17
+ function ()
18
+ {
19
+ _Fable = new libFable({});
20
+ _Geometry = new libGeometry(_Fable, {}, 'Geometry-Test');
21
+ }
22
+ );
23
+
24
+ // ---- Constructor ----
25
+
26
+ suite
27
+ (
28
+ 'Constructor',
29
+ function ()
30
+ {
31
+ test
32
+ (
33
+ 'should instantiate with correct serviceType',
34
+ function (fDone)
35
+ {
36
+ libExpect(_Geometry).to.be.an('object');
37
+ libExpect(_Geometry.serviceType).to.equal('PictProviderFlowGeometry');
38
+ fDone();
39
+ }
40
+ );
41
+ }
42
+ );
43
+
44
+ // ---- getEdgeFromSide ----
45
+
46
+ suite
47
+ (
48
+ 'getEdgeFromSide',
49
+ function ()
50
+ {
51
+ test
52
+ (
53
+ 'should return left for left-side variants',
54
+ function (fDone)
55
+ {
56
+ libExpect(_Geometry.getEdgeFromSide('left')).to.equal('left');
57
+ libExpect(_Geometry.getEdgeFromSide('left-top')).to.equal('left');
58
+ libExpect(_Geometry.getEdgeFromSide('left-bottom')).to.equal('left');
59
+ fDone();
60
+ }
61
+ );
62
+
63
+ test
64
+ (
65
+ 'should return right for right-side variants',
66
+ function (fDone)
67
+ {
68
+ libExpect(_Geometry.getEdgeFromSide('right')).to.equal('right');
69
+ libExpect(_Geometry.getEdgeFromSide('right-top')).to.equal('right');
70
+ libExpect(_Geometry.getEdgeFromSide('right-bottom')).to.equal('right');
71
+ fDone();
72
+ }
73
+ );
74
+
75
+ test
76
+ (
77
+ 'should return top for top-side variants',
78
+ function (fDone)
79
+ {
80
+ libExpect(_Geometry.getEdgeFromSide('top')).to.equal('top');
81
+ libExpect(_Geometry.getEdgeFromSide('top-left')).to.equal('top');
82
+ libExpect(_Geometry.getEdgeFromSide('top-right')).to.equal('top');
83
+ fDone();
84
+ }
85
+ );
86
+
87
+ test
88
+ (
89
+ 'should return bottom for bottom-side variants',
90
+ function (fDone)
91
+ {
92
+ libExpect(_Geometry.getEdgeFromSide('bottom')).to.equal('bottom');
93
+ libExpect(_Geometry.getEdgeFromSide('bottom-left')).to.equal('bottom');
94
+ libExpect(_Geometry.getEdgeFromSide('bottom-right')).to.equal('bottom');
95
+ fDone();
96
+ }
97
+ );
98
+
99
+ test
100
+ (
101
+ 'should default to right for unknown side',
102
+ function (fDone)
103
+ {
104
+ libExpect(_Geometry.getEdgeFromSide('unknown')).to.equal('right');
105
+ libExpect(_Geometry.getEdgeFromSide('')).to.equal('right');
106
+ fDone();
107
+ }
108
+ );
109
+ }
110
+ );
111
+
112
+ // ---- sideDirection ----
113
+
114
+ suite
115
+ (
116
+ 'sideDirection',
117
+ function ()
118
+ {
119
+ test
120
+ (
121
+ 'should return correct direction vectors',
122
+ function (fDone)
123
+ {
124
+ libExpect(_Geometry.sideDirection('left')).to.deep.equal({ dx: -1, dy: 0 });
125
+ libExpect(_Geometry.sideDirection('right')).to.deep.equal({ dx: 1, dy: 0 });
126
+ libExpect(_Geometry.sideDirection('top')).to.deep.equal({ dx: 0, dy: -1 });
127
+ libExpect(_Geometry.sideDirection('bottom')).to.deep.equal({ dx: 0, dy: 1 });
128
+ fDone();
129
+ }
130
+ );
131
+
132
+ test
133
+ (
134
+ 'should map compound sides to their edge direction',
135
+ function (fDone)
136
+ {
137
+ // left-top is on the left edge → dx=-1
138
+ libExpect(_Geometry.sideDirection('left-top')).to.deep.equal({ dx: -1, dy: 0 });
139
+ // right-bottom is on the right edge → dx=1
140
+ libExpect(_Geometry.sideDirection('right-bottom')).to.deep.equal({ dx: 1, dy: 0 });
141
+ // top-left is on the top edge → dy=-1
142
+ libExpect(_Geometry.sideDirection('top-left')).to.deep.equal({ dx: 0, dy: -1 });
143
+ // bottom-right is on the bottom edge → dy=1
144
+ libExpect(_Geometry.sideDirection('bottom-right')).to.deep.equal({ dx: 0, dy: 1 });
145
+ fDone();
146
+ }
147
+ );
148
+ }
149
+ );
150
+
151
+ // ---- getEdgeCenter ----
152
+
153
+ suite
154
+ (
155
+ 'getEdgeCenter',
156
+ function ()
157
+ {
158
+ let tmpRect;
159
+
160
+ setup
161
+ (
162
+ function ()
163
+ {
164
+ tmpRect = { X: 100, Y: 200, Width: 160, Height: 80 };
165
+ }
166
+ );
167
+
168
+ test
169
+ (
170
+ 'should compute left edge center',
171
+ function (fDone)
172
+ {
173
+ let tmpResult = _Geometry.getEdgeCenter(tmpRect, 'left');
174
+ libExpect(tmpResult.x).to.equal(100);
175
+ libExpect(tmpResult.y).to.equal(240); // 200 + 80/2
176
+ fDone();
177
+ }
178
+ );
179
+
180
+ test
181
+ (
182
+ 'should compute right edge center',
183
+ function (fDone)
184
+ {
185
+ let tmpResult = _Geometry.getEdgeCenter(tmpRect, 'right');
186
+ libExpect(tmpResult.x).to.equal(260); // 100 + 160
187
+ libExpect(tmpResult.y).to.equal(240);
188
+ fDone();
189
+ }
190
+ );
191
+
192
+ test
193
+ (
194
+ 'should compute top edge center',
195
+ function (fDone)
196
+ {
197
+ let tmpResult = _Geometry.getEdgeCenter(tmpRect, 'top');
198
+ libExpect(tmpResult.x).to.equal(180); // 100 + 160/2
199
+ libExpect(tmpResult.y).to.equal(200);
200
+ fDone();
201
+ }
202
+ );
203
+
204
+ test
205
+ (
206
+ 'should compute bottom edge center',
207
+ function (fDone)
208
+ {
209
+ let tmpResult = _Geometry.getEdgeCenter(tmpRect, 'bottom');
210
+ libExpect(tmpResult.x).to.equal(180);
211
+ libExpect(tmpResult.y).to.equal(280); // 200 + 80
212
+ fDone();
213
+ }
214
+ );
215
+
216
+ test
217
+ (
218
+ 'should default to right for unknown side',
219
+ function (fDone)
220
+ {
221
+ let tmpResult = _Geometry.getEdgeCenter(tmpRect, 'bogus');
222
+ libExpect(tmpResult.x).to.equal(260);
223
+ libExpect(tmpResult.y).to.equal(240);
224
+ fDone();
225
+ }
226
+ );
227
+ }
228
+ );
229
+
230
+ // ---- _getZoneFromSide ----
231
+
232
+ suite
233
+ (
234
+ '_getZoneFromSide',
235
+ function ()
236
+ {
237
+ test
238
+ (
239
+ 'should return start zone for -top and -left positions',
240
+ function (fDone)
241
+ {
242
+ let tmpZone = _Geometry._getZoneFromSide('left-top');
243
+ libExpect(tmpZone.start).to.equal(0.0);
244
+ libExpect(tmpZone.end).to.equal(0.333);
245
+
246
+ tmpZone = _Geometry._getZoneFromSide('top-left');
247
+ libExpect(tmpZone.start).to.equal(0.0);
248
+ libExpect(tmpZone.end).to.equal(0.333);
249
+ fDone();
250
+ }
251
+ );
252
+
253
+ test
254
+ (
255
+ 'should return middle zone for plain edge names',
256
+ function (fDone)
257
+ {
258
+ let tmpZone = _Geometry._getZoneFromSide('left');
259
+ libExpect(tmpZone.start).to.equal(0.333);
260
+ libExpect(tmpZone.end).to.equal(0.667);
261
+
262
+ tmpZone = _Geometry._getZoneFromSide('bottom');
263
+ libExpect(tmpZone.start).to.equal(0.333);
264
+ libExpect(tmpZone.end).to.equal(0.667);
265
+ fDone();
266
+ }
267
+ );
268
+
269
+ test
270
+ (
271
+ 'should return end zone for -bottom and -right positions',
272
+ function (fDone)
273
+ {
274
+ let tmpZone = _Geometry._getZoneFromSide('left-bottom');
275
+ libExpect(tmpZone.start).to.equal(0.667);
276
+ libExpect(tmpZone.end).to.equal(1.0);
277
+
278
+ tmpZone = _Geometry._getZoneFromSide('top-right');
279
+ libExpect(tmpZone.start).to.equal(0.667);
280
+ libExpect(tmpZone.end).to.equal(1.0);
281
+ fDone();
282
+ }
283
+ );
284
+
285
+ test
286
+ (
287
+ 'should return full range for unknown side',
288
+ function (fDone)
289
+ {
290
+ let tmpZone = _Geometry._getZoneFromSide('nonsense');
291
+ libExpect(tmpZone.start).to.equal(0.0);
292
+ libExpect(tmpZone.end).to.equal(1.0);
293
+ fDone();
294
+ }
295
+ );
296
+ }
297
+ );
298
+
299
+ // ---- _getZoneKeysForEdge ----
300
+
301
+ suite
302
+ (
303
+ '_getZoneKeysForEdge',
304
+ function ()
305
+ {
306
+ test
307
+ (
308
+ 'should return three zone keys in order for each edge',
309
+ function (fDone)
310
+ {
311
+ libExpect(_Geometry._getZoneKeysForEdge('left')).to.deep.equal(
312
+ ['left-top', 'left', 'left-bottom']);
313
+ libExpect(_Geometry._getZoneKeysForEdge('right')).to.deep.equal(
314
+ ['right-top', 'right', 'right-bottom']);
315
+ libExpect(_Geometry._getZoneKeysForEdge('top')).to.deep.equal(
316
+ ['top-left', 'top', 'top-right']);
317
+ libExpect(_Geometry._getZoneKeysForEdge('bottom')).to.deep.equal(
318
+ ['bottom-left', 'bottom', 'bottom-right']);
319
+ fDone();
320
+ }
321
+ );
322
+ }
323
+ );
324
+
325
+ // ---- _computeAdaptiveZone ----
326
+
327
+ suite
328
+ (
329
+ '_computeAdaptiveZone',
330
+ function ()
331
+ {
332
+ test
333
+ (
334
+ 'should fall back to fixed zones when no ports on edge',
335
+ function (fDone)
336
+ {
337
+ // No ports on left edge at all
338
+ let tmpResult = _Geometry._computeAdaptiveZone('left-top', {});
339
+ let tmpFixed = _Geometry._getZoneFromSide('left-top');
340
+ libExpect(tmpResult.start).to.equal(tmpFixed.start);
341
+ libExpect(tmpResult.end).to.equal(tmpFixed.end);
342
+ fDone();
343
+ }
344
+ );
345
+
346
+ test
347
+ (
348
+ 'should give full range to single occupied zone',
349
+ function (fDone)
350
+ {
351
+ // Only left-top has ports
352
+ let tmpResult = _Geometry._computeAdaptiveZone('left-top',
353
+ { 'left-top': 3 });
354
+ libExpect(tmpResult.start).to.equal(0.0);
355
+ libExpect(tmpResult.end).to.equal(1.0);
356
+ fDone();
357
+ }
358
+ );
359
+
360
+ test
361
+ (
362
+ 'should split proportionally between two occupied zones',
363
+ function (fDone)
364
+ {
365
+ // left-top has 2 ports (needs 16*(2+1) = 48),
366
+ // left has 1 port (needs 16*(1+1) = 32)
367
+ // total = 80, fractions: 48/80 = 0.6, 32/80 = 0.4
368
+ let tmpResult1 = _Geometry._computeAdaptiveZone('left-top',
369
+ { 'left-top': 2, 'left': 1 });
370
+ libExpect(tmpResult1.start).to.equal(0.0);
371
+ libExpect(tmpResult1.end).to.be.closeTo(0.6, 0.001);
372
+
373
+ let tmpResult2 = _Geometry._computeAdaptiveZone('left',
374
+ { 'left-top': 2, 'left': 1 });
375
+ libExpect(tmpResult2.start).to.be.closeTo(0.6, 0.001);
376
+ libExpect(tmpResult2.end).to.be.closeTo(1.0, 0.001);
377
+ fDone();
378
+ }
379
+ );
380
+
381
+ test
382
+ (
383
+ 'should handle all three zones occupied',
384
+ function (fDone)
385
+ {
386
+ // Each zone has 1 port → each needs 16*(1+1) = 32
387
+ // All equal → each gets 1/3
388
+ let tmpCounts = { 'right-top': 1, 'right': 1, 'right-bottom': 1 };
389
+
390
+ let tmpZone1 = _Geometry._computeAdaptiveZone('right-top', tmpCounts);
391
+ let tmpZone2 = _Geometry._computeAdaptiveZone('right', tmpCounts);
392
+ let tmpZone3 = _Geometry._computeAdaptiveZone('right-bottom', tmpCounts);
393
+
394
+ libExpect(tmpZone1.start).to.be.closeTo(0.0, 0.001);
395
+ libExpect(tmpZone1.end).to.be.closeTo(0.333, 0.001);
396
+ libExpect(tmpZone2.start).to.be.closeTo(0.333, 0.001);
397
+ libExpect(tmpZone2.end).to.be.closeTo(0.667, 0.001);
398
+ libExpect(tmpZone3.start).to.be.closeTo(0.667, 0.001);
399
+ libExpect(tmpZone3.end).to.be.closeTo(1.0, 0.001);
400
+ fDone();
401
+ }
402
+ );
403
+
404
+ test
405
+ (
406
+ 'should collapse empty zone to zero width',
407
+ function (fDone)
408
+ {
409
+ // Only right-top and right-bottom are occupied;
410
+ // 'right' has no ports → its adaptive zone should be zero width
411
+ let tmpCounts = { 'right-top': 2, 'right-bottom': 2 };
412
+
413
+ let tmpMid = _Geometry._computeAdaptiveZone('right', tmpCounts);
414
+ // With 0 ports, the middle zone gets 0 space
415
+ // But since it's not in the counts, its space is 0
416
+ // Actually _computeAdaptiveZone for 'right' with 0 ports:
417
+ // right zone's space = 0 (no ports), so its fraction = 0/total
418
+ // This means start == end for this zone
419
+ libExpect(tmpMid.end - tmpMid.start).to.be.closeTo(0, 0.001);
420
+ fDone();
421
+ }
422
+ );
423
+ }
424
+ );
425
+
426
+ // ---- buildPortCountsBySide ----
427
+
428
+ suite
429
+ (
430
+ 'buildPortCountsBySide',
431
+ function ()
432
+ {
433
+ test
434
+ (
435
+ 'should count ports by Side value',
436
+ function (fDone)
437
+ {
438
+ let tmpPorts = [
439
+ { Side: 'left-top', Direction: 'input' },
440
+ { Side: 'left-top', Direction: 'input' },
441
+ { Side: 'right-top', Direction: 'output' },
442
+ { Side: 'bottom', Direction: 'output' }
443
+ ];
444
+
445
+ let tmpResult = _Geometry.buildPortCountsBySide(tmpPorts);
446
+ libExpect(tmpResult['left-top']).to.equal(2);
447
+ libExpect(tmpResult['right-top']).to.equal(1);
448
+ libExpect(tmpResult['bottom']).to.equal(1);
449
+ fDone();
450
+ }
451
+ );
452
+
453
+ test
454
+ (
455
+ 'should fall back to left/right based on Direction when Side is missing',
456
+ function (fDone)
457
+ {
458
+ let tmpPorts = [
459
+ { Direction: 'input' },
460
+ { Direction: 'output' },
461
+ { Direction: 'input' }
462
+ ];
463
+
464
+ let tmpResult = _Geometry.buildPortCountsBySide(tmpPorts);
465
+ libExpect(tmpResult['left']).to.equal(2);
466
+ libExpect(tmpResult['right']).to.equal(1);
467
+ fDone();
468
+ }
469
+ );
470
+
471
+ test
472
+ (
473
+ 'should return empty object for null/empty input',
474
+ function (fDone)
475
+ {
476
+ libExpect(_Geometry.buildPortCountsBySide(null)).to.deep.equal({});
477
+ libExpect(_Geometry.buildPortCountsBySide([])).to.deep.equal({});
478
+ libExpect(_Geometry.buildPortCountsBySide(undefined)).to.deep.equal({});
479
+ fDone();
480
+ }
481
+ );
482
+ }
483
+ );
484
+
485
+ // ---- getPortLocalPosition ----
486
+
487
+ suite
488
+ (
489
+ 'getPortLocalPosition',
490
+ function ()
491
+ {
492
+ test
493
+ (
494
+ 'should place left port at x=0',
495
+ function (fDone)
496
+ {
497
+ let tmpResult = _Geometry.getPortLocalPosition(
498
+ 'left-top', 0, 1, 200, 120, 30);
499
+ libExpect(tmpResult.x).to.equal(0);
500
+ fDone();
501
+ }
502
+ );
503
+
504
+ test
505
+ (
506
+ 'should place right port at x=width',
507
+ function (fDone)
508
+ {
509
+ let tmpResult = _Geometry.getPortLocalPosition(
510
+ 'right-top', 0, 1, 200, 120, 30);
511
+ libExpect(tmpResult.x).to.equal(200);
512
+ fDone();
513
+ }
514
+ );
515
+
516
+ test
517
+ (
518
+ 'should place top port at y=0',
519
+ function (fDone)
520
+ {
521
+ let tmpResult = _Geometry.getPortLocalPosition(
522
+ 'top', 0, 1, 200, 120, 30);
523
+ libExpect(tmpResult.y).to.equal(0);
524
+ fDone();
525
+ }
526
+ );
527
+
528
+ test
529
+ (
530
+ 'should place bottom port at y=height',
531
+ function (fDone)
532
+ {
533
+ let tmpResult = _Geometry.getPortLocalPosition(
534
+ 'bottom', 0, 1, 200, 120, 30);
535
+ libExpect(tmpResult.y).to.equal(120);
536
+ fDone();
537
+ }
538
+ );
539
+
540
+ test
541
+ (
542
+ 'should use fixed spacing for consistent port gaps',
543
+ function (fDone)
544
+ {
545
+ // Two ports in the same zone
546
+ let tmpPos0 = _Geometry.getPortLocalPosition(
547
+ 'left-top', 0, 2, 200, 200, 30);
548
+ let tmpPos1 = _Geometry.getPortLocalPosition(
549
+ 'left-top', 1, 2, 200, 200, 30);
550
+
551
+ // Spacing should be 16px (the minSpacing constant)
552
+ libExpect(tmpPos1.y - tmpPos0.y).to.equal(16);
553
+ fDone();
554
+ }
555
+ );
556
+
557
+ test
558
+ (
559
+ 'should maintain same spacing on a taller card',
560
+ function (fDone)
561
+ {
562
+ // Same port config but larger card height
563
+ let tmpPos0Short = _Geometry.getPortLocalPosition(
564
+ 'left-top', 0, 2, 200, 100, 30);
565
+ let tmpPos1Short = _Geometry.getPortLocalPosition(
566
+ 'left-top', 1, 2, 200, 100, 30);
567
+
568
+ let tmpPos0Tall = _Geometry.getPortLocalPosition(
569
+ 'left-top', 0, 2, 200, 400, 30);
570
+ let tmpPos1Tall = _Geometry.getPortLocalPosition(
571
+ 'left-top', 1, 2, 200, 400, 30);
572
+
573
+ // Spacing should be same regardless of card height
574
+ let tmpSpacingShort = tmpPos1Short.y - tmpPos0Short.y;
575
+ let tmpSpacingTall = tmpPos1Tall.y - tmpPos0Tall.y;
576
+ libExpect(tmpSpacingShort).to.equal(tmpSpacingTall);
577
+ libExpect(tmpSpacingShort).to.equal(16);
578
+ fDone();
579
+ }
580
+ );
581
+
582
+ test
583
+ (
584
+ 'should top-align start zone ports',
585
+ function (fDone)
586
+ {
587
+ // left-top is in the start zone (0.0–0.333)
588
+ // With only 1 port, alignment='start' means offset=0
589
+ let tmpResult = _Geometry.getPortLocalPosition(
590
+ 'left-top', 0, 1, 200, 200, 30);
591
+
592
+ // zoneStart = titleBar + bodyHeight * 0.0 = 30
593
+ // alignOffset = 0 (start alignment)
594
+ // y = 30 + 0 + 16*(0+1) = 46
595
+ libExpect(tmpResult.y).to.equal(46);
596
+ fDone();
597
+ }
598
+ );
599
+
600
+ test
601
+ (
602
+ 'should bottom-align end zone ports',
603
+ function (fDone)
604
+ {
605
+ // left-bottom is in the end zone (0.667–1.0)
606
+ // alignment='end' means offset = slack
607
+ let tmpResult = _Geometry.getPortLocalPosition(
608
+ 'left-bottom', 0, 1, 200, 200, 30);
609
+
610
+ // bodyHeight = 200 - 30 - 16 = 154
611
+ // zoneStart = 30 + 154 * 0.667 = 132.718
612
+ // zoneHeight = 154 * (1.0 - 0.667) = 51.282
613
+ // groupHeight = 16 * (1+1) = 32
614
+ // slack = 51.282 - 32 = 19.282
615
+ // alignOffset = slack (end alignment) = 19.282
616
+ // y = 132.718 + 19.282 + 16*1 = 168
617
+ libExpect(tmpResult.y).to.be.closeTo(168, 0.5);
618
+ fDone();
619
+ }
620
+ );
621
+
622
+ test
623
+ (
624
+ 'should center-align middle zone ports (including bottom edge error port)',
625
+ function (fDone)
626
+ {
627
+ // 'bottom' is in the middle zone (0.333–0.667)
628
+ // This is the error port use case
629
+ let tmpResult = _Geometry.getPortLocalPosition(
630
+ 'bottom', 0, 1, 200, 120, 30);
631
+
632
+ // On bottom edge: y = height = 120
633
+ libExpect(tmpResult.y).to.equal(120);
634
+
635
+ // zoneStart = 200 * 0.333 = 66.6
636
+ // zoneWidth = 200 * (0.667 - 0.333) = 66.8
637
+ // groupWidth = 16 * 2 = 32
638
+ // slack = 66.8 - 32 = 34.8
639
+ // center offset = 34.8 / 2 = 17.4
640
+ // x = 66.6 + 17.4 + 16*1 = 100
641
+ libExpect(tmpResult.x).to.be.closeTo(100, 0.5);
642
+ fDone();
643
+ }
644
+ );
645
+
646
+ test
647
+ (
648
+ 'should center error port with adaptive zones',
649
+ function (fDone)
650
+ {
651
+ // The critical case: error port is the only port on bottom edge
652
+ // Adaptive zone gives it {start:0, end:1} (full width)
653
+ // But alignment must still be 'center' based on fixed zone
654
+ let tmpPortCounts = { 'bottom': 1 };
655
+
656
+ let tmpResult = _Geometry.getPortLocalPosition(
657
+ 'bottom', 0, 1, 200, 120, 30, tmpPortCounts);
658
+
659
+ // y = height = 120 (bottom edge)
660
+ libExpect(tmpResult.y).to.equal(120);
661
+
662
+ // With adaptive zone {0, 1}: zoneStart=0, zoneWidth=200
663
+ // groupWidth = 16*2 = 32
664
+ // slack = 200 - 32 = 168
665
+ // center offset = 168 / 2 = 84
666
+ // x = 0 + 84 + 16 = 100 (centered!)
667
+ libExpect(tmpResult.x).to.be.closeTo(100, 0.5);
668
+ fDone();
669
+ }
670
+ );
671
+ }
672
+ );
673
+
674
+ // ---- computeMinimumNodeHeight ----
675
+
676
+ suite
677
+ (
678
+ 'computeMinimumNodeHeight',
679
+ function ()
680
+ {
681
+ test
682
+ (
683
+ 'should return 0 for no ports',
684
+ function (fDone)
685
+ {
686
+ libExpect(_Geometry.computeMinimumNodeHeight([], 30)).to.equal(0);
687
+ libExpect(_Geometry.computeMinimumNodeHeight(null, 30)).to.equal(0);
688
+ fDone();
689
+ }
690
+ );
691
+
692
+ test
693
+ (
694
+ 'should compute height for single left port',
695
+ function (fDone)
696
+ {
697
+ let tmpPorts = [{ Side: 'left-top', Direction: 'input' }];
698
+ let tmpHeight = _Geometry.computeMinimumNodeHeight(tmpPorts, 30);
699
+
700
+ // 1 port on left-top: space = 16 * (1+1) = 32
701
+ // height = titleBar(30) + bottomPad(16) + 32 = 78
702
+ libExpect(tmpHeight).to.equal(78);
703
+ fDone();
704
+ }
705
+ );
706
+
707
+ test
708
+ (
709
+ 'should take the max of left and right edges',
710
+ function (fDone)
711
+ {
712
+ // 3 ports on left, 1 port on right
713
+ let tmpPorts = [
714
+ { Side: 'left-top', Direction: 'input' },
715
+ { Side: 'left-top', Direction: 'input' },
716
+ { Side: 'left-top', Direction: 'input' },
717
+ { Side: 'right-top', Direction: 'output' }
718
+ ];
719
+ let tmpHeight = _Geometry.computeMinimumNodeHeight(tmpPorts, 30);
720
+
721
+ // Left: 16 * (3+1) = 64. Right: 16 * (1+1) = 32.
722
+ // Max edge = 64. Height = 30 + 16 + 64 = 110
723
+ libExpect(tmpHeight).to.equal(110);
724
+ fDone();
725
+ }
726
+ );
727
+
728
+ test
729
+ (
730
+ 'should sum across zones on the same edge',
731
+ function (fDone)
732
+ {
733
+ // Ports spread across left-top, left, and left-bottom
734
+ let tmpPorts = [
735
+ { Side: 'left-top', Direction: 'input' },
736
+ { Side: 'left', Direction: 'input' },
737
+ { Side: 'left-bottom', Direction: 'input' }
738
+ ];
739
+ let tmpHeight = _Geometry.computeMinimumNodeHeight(tmpPorts, 30);
740
+
741
+ // Each zone: 16*(1+1) = 32. Total = 96.
742
+ // Height = 30 + 16 + 96 = 142
743
+ libExpect(tmpHeight).to.equal(142);
744
+ fDone();
745
+ }
746
+ );
747
+
748
+ test
749
+ (
750
+ 'should ignore top/bottom edge ports for height calculation',
751
+ function (fDone)
752
+ {
753
+ let tmpPorts = [
754
+ { Side: 'bottom', Direction: 'output' },
755
+ { Side: 'top', Direction: 'input' }
756
+ ];
757
+ let tmpHeight = _Geometry.computeMinimumNodeHeight(tmpPorts, 30);
758
+
759
+ // No left/right ports → height = 0
760
+ libExpect(tmpHeight).to.equal(0);
761
+ fDone();
762
+ }
763
+ );
764
+ }
765
+ );
766
+ }
767
+ );