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,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
|
+
);
|