pict-section-flow 1.4.0 → 2.0.0

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 (164) hide show
  1. package/package.json +7 -2
  2. package/source/Pict-Section-Flow.js +20 -14
  3. package/source/providers/PictProvider-Flow-Background.js +303 -0
  4. package/source/providers/PictProvider-Flow-CSS.js +73 -7
  5. package/source/providers/PictProvider-Flow-Geometry.js +11 -421
  6. package/source/providers/PictProvider-Flow-Icons.js +12 -0
  7. package/source/providers/PictProvider-Flow-Layouts.js +107 -0
  8. package/source/services/PictService-Flow-ConnectionRenderer.js +1 -1
  9. package/source/services/PictService-Flow-CursorManager.js +113 -0
  10. package/source/services/PictService-Flow-InteractionManager.js +439 -59
  11. package/source/services/PictService-Flow-Layout.js +21 -16
  12. package/source/services/PictService-Flow-PathGenerator.js +30 -417
  13. package/source/services/PictService-Flow-RenderManager.js +9 -1
  14. package/source/services/PictService-Flow-ViewportManager.js +102 -0
  15. package/source/views/PictView-Flow-FloatingToolbar.js +5 -1
  16. package/source/views/PictView-Flow-Node.js +29 -0
  17. package/source/views/PictView-Flow-Toolbar.js +50 -3
  18. package/source/views/PictView-Flow.js +591 -2
  19. package/.claude/launch.json +0 -11
  20. package/docs/.nojekyll +0 -0
  21. package/docs/Architecture.md +0 -163
  22. package/docs/Custom-Styling.md +0 -275
  23. package/docs/Data_Model.md +0 -149
  24. package/docs/Event_System.md +0 -156
  25. package/docs/Getting_Started.md +0 -237
  26. package/docs/Implementation_Reference.md +0 -528
  27. package/docs/Layout_Persistence.md +0 -117
  28. package/docs/README.md +0 -103
  29. package/docs/Theme_Integration.md +0 -150
  30. package/docs/_brand.json +0 -18
  31. package/docs/_cover.md +0 -17
  32. package/docs/_playground.json +0 -24
  33. package/docs/_sidebar.md +0 -57
  34. package/docs/_topbar.md +0 -8
  35. package/docs/_version.json +0 -7
  36. package/docs/api/PictFlowCard.md +0 -216
  37. package/docs/api/PictFlowCardPropertiesPanel.md +0 -235
  38. package/docs/api/addConnection.md +0 -101
  39. package/docs/api/addNode.md +0 -137
  40. package/docs/api/autoLayout.md +0 -77
  41. package/docs/api/getFlowData.md +0 -112
  42. package/docs/api/marshalToView.md +0 -95
  43. package/docs/api/openPanel.md +0 -128
  44. package/docs/api/registerHandler.md +0 -174
  45. package/docs/api/registerNodeType.md +0 -142
  46. package/docs/api/removeConnection.md +0 -57
  47. package/docs/api/removeNode.md +0 -80
  48. package/docs/api/saveLayout.md +0 -152
  49. package/docs/api/screenToSVGCoords.md +0 -68
  50. package/docs/api/selectNode.md +0 -116
  51. package/docs/api/setTheme.md +0 -168
  52. package/docs/api/setZoom.md +0 -97
  53. package/docs/api/toggleFullscreen.md +0 -68
  54. package/docs/card-help/EACH.md +0 -19
  55. package/docs/card-help/FREAD.md +0 -24
  56. package/docs/card-help/FWRITE.md +0 -24
  57. package/docs/card-help/GET.md +0 -22
  58. package/docs/card-help/ITE.md +0 -23
  59. package/docs/card-help/LOG.md +0 -23
  60. package/docs/card-help/NOTE.md +0 -17
  61. package/docs/card-help/PREV.md +0 -18
  62. package/docs/card-help/SET.md +0 -27
  63. package/docs/card-help/SPKL.md +0 -22
  64. package/docs/card-help/STAT.md +0 -23
  65. package/docs/card-help/SW.md +0 -25
  66. package/docs/diagrams/architecture-at-a-glance.excalidraw +0 -4270
  67. package/docs/diagrams/architecture-at-a-glance.mmd +0 -30
  68. package/docs/diagrams/architecture-at-a-glance.svg +0 -2
  69. package/docs/diagrams/data-flow.excalidraw +0 -1451
  70. package/docs/diagrams/data-flow.mmd +0 -17
  71. package/docs/diagrams/data-flow.svg +0 -2
  72. package/docs/diagrams/high-level-design.excalidraw +0 -5767
  73. package/docs/diagrams/high-level-design.mmd +0 -86
  74. package/docs/diagrams/high-level-design.svg +0 -2
  75. package/docs/diagrams/relationships.excalidraw +0 -3852
  76. package/docs/diagrams/relationships.mmd +0 -9
  77. package/docs/diagrams/relationships.svg +0 -2
  78. package/docs/diagrams/service-initialization-sequence.excalidraw +0 -1466
  79. package/docs/diagrams/service-initialization-sequence.mmd +0 -19
  80. package/docs/diagrams/service-initialization-sequence.svg +0 -2
  81. package/docs/diagrams/svg-layer-structure.excalidraw +0 -1060
  82. package/docs/diagrams/svg-layer-structure.mmd +0 -18
  83. package/docs/diagrams/svg-layer-structure.svg +0 -2
  84. package/docs/examples/README.md +0 -9
  85. package/docs/examples/simple_cards/README.md +0 -677
  86. package/docs/examples/simple_cards/css/flowexample.css +0 -65
  87. package/docs/examples/simple_cards/index.html +0 -32
  88. package/docs/examples/simple_cards/js/pict.min.js +0 -12
  89. package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +0 -1
  90. package/docs/index.html +0 -38
  91. package/docs/playground/app.json +0 -6
  92. package/docs/playground/appdata.json +0 -85
  93. package/docs/playground/application.js +0 -23
  94. package/docs/playground/pict.json +0 -17
  95. package/docs/playground/runtime/pict-application.min.js +0 -2
  96. package/docs/playground/runtime/pict-section-flow.min.js +0 -2
  97. package/docs/playground/runtime/pict-section-modal.min.js +0 -2
  98. package/docs/playground/runtime/pict.min.js +0 -12
  99. package/docs/retold-catalog.json +0 -244
  100. package/docs/retold-keyword-index.json +0 -26028
  101. package/example_applications/simple_cards/css/flowexample.css +0 -65
  102. package/example_applications/simple_cards/html/index.html +0 -32
  103. package/example_applications/simple_cards/package.json +0 -52
  104. package/example_applications/simple_cards/source/Pict-Application-FlowExample-Configuration.json +0 -15
  105. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +0 -539
  106. package/example_applications/simple_cards/source/card-help-content.js +0 -16
  107. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +0 -38
  108. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +0 -44
  109. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +0 -38
  110. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +0 -56
  111. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +0 -50
  112. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +0 -37
  113. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +0 -49
  114. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +0 -55
  115. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +0 -97
  116. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +0 -100
  117. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +0 -46
  118. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +0 -39
  119. package/example_applications/simple_cards/source/providers/PictRouter-FlowExample-Configuration.json +0 -22
  120. package/example_applications/simple_cards/source/sample-flows.js +0 -410
  121. package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +0 -184
  122. package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +0 -77
  123. package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +0 -325
  124. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +0 -59
  125. package/example_applications/simple_cards/source/views/PictView-FlowExample-Layout.js +0 -90
  126. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +0 -453
  127. package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +0 -95
  128. package/scripts/generate-card-help.js +0 -214
  129. package/source/providers/edges/Edge-Bezier.js +0 -41
  130. package/source/providers/edges/Edge-Orthogonal.js +0 -37
  131. package/source/providers/edges/Edge-OrthogonalSnap.js +0 -72
  132. package/source/providers/edges/Edge-Perimeter-Linear.js +0 -31
  133. package/source/providers/edges/Edge-Perimeter-Orthogonal.js +0 -39
  134. package/source/providers/edges/Edge-Perimeter.js +0 -48
  135. package/source/providers/edges/Edge-PerimeterMath.js +0 -92
  136. package/source/providers/edges/Edge-Straight.js +0 -24
  137. package/source/providers/layouts/Layout-Circular.js +0 -203
  138. package/source/providers/layouts/Layout-Coerce.js +0 -40
  139. package/source/providers/layouts/Layout-Columnar.js +0 -134
  140. package/source/providers/layouts/Layout-Custom.js +0 -27
  141. package/source/providers/layouts/Layout-ForcedFromCenter.js +0 -256
  142. package/source/providers/layouts/Layout-Grid.js +0 -134
  143. package/source/providers/layouts/Layout-Layered.js +0 -155
  144. package/source/providers/layouts/Layout-Rank.js +0 -141
  145. package/source/providers/layouts/Layout-Staggered.js +0 -131
  146. package/source/providers/layouts/Layout-Tabular.js +0 -94
  147. package/test/CardPalette_tests.js +0 -43
  148. package/test/ConnectionHandleManager_tests.js +0 -717
  149. package/test/ConnectionRenderer_tests.js +0 -591
  150. package/test/ConnectionStyle_tests.js +0 -90
  151. package/test/DataManager_tests.js +0 -859
  152. package/test/Geometry_tests.js +0 -767
  153. package/test/InteractionManager_tests.js +0 -279
  154. package/test/Layout_tests.js +0 -1604
  155. package/test/NodeView_tests.js +0 -66
  156. package/test/PanelManager_tests.js +0 -172
  157. package/test/PathGenerator_tests.js +0 -978
  158. package/test/PortRenderer_tests.js +0 -376
  159. package/test/RenderManager_tests.js +0 -756
  160. package/test/Renderer_tests.js +0 -133
  161. package/test/SelectionManager_tests.js +0 -185
  162. package/test/StylePresets_tests.js +0 -153
  163. package/test/ToolbarExtraButtons_tests.js +0 -138
  164. package/test/UndirectedConnections_tests.js +0 -70
@@ -1,1604 +0,0 @@
1
- const libFable = require('fable');
2
- const libChai = require('chai');
3
- const libExpect = libChai.expect;
4
-
5
- const libLayoutService = require('../source/services/PictService-Flow-Layout.js');
6
-
7
- const libLayoutCustom = require('../source/providers/layouts/Layout-Custom.js');
8
- const libLayoutLayered = require('../source/providers/layouts/Layout-Layered.js');
9
- const libLayoutStaggered = require('../source/providers/layouts/Layout-Staggered.js');
10
- const libLayoutRank = require('../source/providers/layouts/Layout-Rank.js');
11
- const libLayoutForcedFromCenter = require('../source/providers/layouts/Layout-ForcedFromCenter.js');
12
- const libLayoutGrid = require('../source/providers/layouts/Layout-Grid.js');
13
- const libLayoutCircular = require('../source/providers/layouts/Layout-Circular.js');
14
- const libLayoutTabular = require('../source/providers/layouts/Layout-Tabular.js');
15
- const libLayoutColumnar = require('../source/providers/layouts/Layout-Columnar.js');
16
-
17
- const libEdgeBezier = require('../source/providers/edges/Edge-Bezier.js');
18
- const libEdgeOrthogonal = require('../source/providers/edges/Edge-Orthogonal.js');
19
- const libEdgeStraight = require('../source/providers/edges/Edge-Straight.js');
20
- const libEdgeOrthogonalSnap = require('../source/providers/edges/Edge-OrthogonalSnap.js');
21
- const libEdgePerimeter = require('../source/providers/edges/Edge-Perimeter.js');
22
-
23
- function makeNodes(pCount)
24
- {
25
- let tmpNodes = [];
26
- for (let i = 0; i < pCount; i++)
27
- {
28
- tmpNodes.push({
29
- Hash: `n-${i}`,
30
- Title: `Node ${i}`,
31
- X: 0,
32
- Y: 0,
33
- Width: 180,
34
- Height: 80
35
- });
36
- }
37
- return tmpNodes;
38
- }
39
-
40
- function makeChain(pCount)
41
- {
42
- let tmpConns = [];
43
- for (let i = 0; i < pCount - 1; i++)
44
- {
45
- tmpConns.push({
46
- Hash: `c-${i}`,
47
- SourceNodeHash: `n-${i}`,
48
- TargetNodeHash: `n-${i + 1}`
49
- });
50
- }
51
- return tmpConns;
52
- }
53
-
54
- suite
55
- (
56
- 'PictService-Flow-Layout',
57
- function ()
58
- {
59
- let _Fable;
60
- let _LayoutService;
61
-
62
- setup
63
- (
64
- function ()
65
- {
66
- _Fable = new libFable({});
67
- _LayoutService = new libLayoutService(_Fable, {}, 'Layout-Test');
68
- }
69
- );
70
-
71
- // ── Service constructor ───────────────────────────────────────
72
-
73
- suite
74
- (
75
- 'Constructor',
76
- function ()
77
- {
78
- test
79
- (
80
- 'should instantiate with correct serviceType',
81
- function (fDone)
82
- {
83
- libExpect(_LayoutService).to.be.an('object');
84
- libExpect(_LayoutService.serviceType).to.equal('PictServiceFlowLayout');
85
- fDone();
86
- }
87
- );
88
-
89
- test
90
- (
91
- 'should register all eight built-in algorithms by default',
92
- function (fDone)
93
- {
94
- let tmpNames = _LayoutService.getAlgorithmNames();
95
- libExpect(tmpNames).to.include.members([
96
- 'Custom', 'Layered', 'Staggered', 'ForcedFromCenter',
97
- 'Grid', 'Circular', 'Tabular', 'Columnar'
98
- ]);
99
- libExpect(tmpNames.length).to.equal(8);
100
- fDone();
101
- }
102
- );
103
- }
104
- );
105
-
106
- // ── Registry ──────────────────────────────────────────────────
107
-
108
- suite
109
- (
110
- 'Registry',
111
- function ()
112
- {
113
- test
114
- (
115
- 'getAlgorithm returns null for unknown name',
116
- function (fDone)
117
- {
118
- libExpect(_LayoutService.getAlgorithm('NopeNope')).to.equal(null);
119
- fDone();
120
- }
121
- );
122
-
123
- test
124
- (
125
- 'getAlgorithm returns a descriptor with required fields',
126
- function (fDone)
127
- {
128
- let tmpAlgo = _LayoutService.getAlgorithm('Layered');
129
- libExpect(tmpAlgo).to.be.an('object');
130
- libExpect(tmpAlgo.Name).to.equal('Layered');
131
- libExpect(tmpAlgo.Apply).to.be.a('function');
132
- libExpect(tmpAlgo.DefaultParameters).to.be.an('object');
133
- libExpect(tmpAlgo.ParameterSchema).to.be.an('object');
134
- fDone();
135
- }
136
- );
137
-
138
- test
139
- (
140
- 'registerAlgorithm round-trips a custom descriptor',
141
- function (fDone)
142
- {
143
- let tmpCustom =
144
- {
145
- Name: 'TestStub',
146
- Label: 'Test Stub',
147
- Apply: function (pNodes)
148
- {
149
- for (let i = 0; i < pNodes.length; i++)
150
- {
151
- pNodes[i].X = 999;
152
- pNodes[i].Y = i;
153
- }
154
- },
155
- DefaultParameters: {},
156
- ParameterSchema: {}
157
- };
158
- let tmpResult = _LayoutService.registerAlgorithm(tmpCustom);
159
- libExpect(tmpResult).to.equal(true);
160
- libExpect(_LayoutService.getAlgorithm('TestStub')).to.equal(tmpCustom);
161
-
162
- let tmpNodes = makeNodes(3);
163
- _LayoutService.applyLayout(tmpNodes, [], 'TestStub', {});
164
- libExpect(tmpNodes[0].X).to.equal(999);
165
- libExpect(tmpNodes[2].Y).to.equal(2);
166
- fDone();
167
- }
168
- );
169
-
170
- test
171
- (
172
- 'registerAlgorithm rejects invalid descriptors',
173
- function (fDone)
174
- {
175
- libExpect(_LayoutService.registerAlgorithm(null)).to.equal(false);
176
- libExpect(_LayoutService.registerAlgorithm({})).to.equal(false);
177
- libExpect(_LayoutService.registerAlgorithm({ Name: 'X' })).to.equal(false);
178
- libExpect(_LayoutService.registerAlgorithm({ Apply: function () {} })).to.equal(false);
179
- fDone();
180
- }
181
- );
182
-
183
- test
184
- (
185
- 'listAlgorithms returns descriptors for all registered algorithms',
186
- function (fDone)
187
- {
188
- let tmpAll = _LayoutService.listAlgorithms();
189
- libExpect(tmpAll.length).to.equal(8);
190
- let tmpNames = tmpAll.map((pA) => pA.Name);
191
- libExpect(tmpNames).to.include('Custom');
192
- libExpect(tmpNames).to.include('Staggered');
193
- libExpect(tmpNames).to.include('ForcedFromCenter');
194
- fDone();
195
- }
196
- );
197
- }
198
- );
199
-
200
- // ── Backwards compatibility ───────────────────────────────────
201
-
202
- suite
203
- (
204
- 'Backwards compatibility',
205
- function ()
206
- {
207
- test
208
- (
209
- 'autoLayout(nodes, connections) — 2-arg legacy form dispatches to Layered',
210
- function (fDone)
211
- {
212
- // Build a 3-node chain n-0 -> n-1 -> n-2 and assert Layered positions
213
- let tmpNodes = makeNodes(3);
214
- let tmpConns = makeChain(3);
215
-
216
- _LayoutService.autoLayout(tmpNodes, tmpConns);
217
-
218
- // Layered defaults: HorizontalSpacing 250, VerticalSpacing 120, Start (100, 100)
219
- // Expected: each node in its own layer, X advances by node-width (180) + 250
220
- libExpect(tmpNodes[0].X).to.equal(100);
221
- libExpect(tmpNodes[0].Y).to.equal(100);
222
- libExpect(tmpNodes[1].X).to.equal(100 + 180 + 250);
223
- libExpect(tmpNodes[1].Y).to.equal(100);
224
- libExpect(tmpNodes[2].X).to.equal(100 + (180 + 250) * 2);
225
- libExpect(tmpNodes[2].Y).to.equal(100);
226
- fDone();
227
- }
228
- );
229
-
230
- test
231
- (
232
- 'autoLayout — empty nodes is a no-op',
233
- function (fDone)
234
- {
235
- _LayoutService.autoLayout([], []);
236
- _LayoutService.autoLayout(null, []);
237
- fDone();
238
- }
239
- );
240
-
241
- test
242
- (
243
- 'autoLayoutSubset — places orphans to the right of fixed nodes (always Layered)',
244
- function (fDone)
245
- {
246
- let tmpFixed = [
247
- { Hash: 'fix-0', X: 0, Y: 0, Width: 100, Height: 50 },
248
- { Hash: 'fix-1', X: 200, Y: 0, Width: 100, Height: 50 }
249
- ];
250
- let tmpOrphans = makeNodes(2);
251
- _LayoutService.autoLayoutSubset(tmpOrphans, tmpFixed, []);
252
- // Right-edge of fix-1 is 300; +HorizontalSpacing(250) = 550
253
- libExpect(tmpOrphans[0].X).to.equal(550);
254
- fDone();
255
- }
256
- );
257
-
258
- test
259
- (
260
- 'snapToGrid rounds to grid size',
261
- function (fDone)
262
- {
263
- libExpect(_LayoutService.snapToGrid(13, 10)).to.equal(10);
264
- libExpect(_LayoutService.snapToGrid(16, 10)).to.equal(20);
265
- libExpect(_LayoutService.snapToGrid(50, 0)).to.equal(50); // disabled
266
- fDone();
267
- }
268
- );
269
- }
270
- );
271
-
272
- // ── Layered ───────────────────────────────────────────────────
273
-
274
- suite
275
- (
276
- 'Layered algorithm',
277
- function ()
278
- {
279
- test
280
- (
281
- 'one node lands at StartX/StartY',
282
- function (fDone)
283
- {
284
- let tmpNodes = makeNodes(1);
285
- libLayoutLayered.Apply(tmpNodes, [], libLayoutLayered.DefaultParameters);
286
- libExpect(tmpNodes[0].X).to.equal(100);
287
- libExpect(tmpNodes[0].Y).to.equal(100);
288
- fDone();
289
- }
290
- );
291
-
292
- test
293
- (
294
- 'parallel siblings stack vertically',
295
- function (fDone)
296
- {
297
- // Two roots, no connections — they end up in the same layer
298
- let tmpNodes = makeNodes(2);
299
- libLayoutLayered.Apply(tmpNodes, [], libLayoutLayered.DefaultParameters);
300
- libExpect(tmpNodes[0].X).to.equal(tmpNodes[1].X);
301
- libExpect(tmpNodes[1].Y).to.equal(tmpNodes[0].Y + 80 + 120);
302
- fDone();
303
- }
304
- );
305
-
306
- test
307
- (
308
- 'caller-supplied params override defaults',
309
- function (fDone)
310
- {
311
- let tmpNodes = makeNodes(2);
312
- let tmpConns = makeChain(2);
313
- libLayoutLayered.Apply(tmpNodes, tmpConns, { HorizontalSpacing: 50, VerticalSpacing: 30, StartX: 0, StartY: 0 });
314
- libExpect(tmpNodes[0].X).to.equal(0);
315
- libExpect(tmpNodes[0].Y).to.equal(0);
316
- libExpect(tmpNodes[1].X).to.equal(180 + 50);
317
- libExpect(tmpNodes[1].Y).to.equal(0);
318
- fDone();
319
- }
320
- );
321
-
322
- test
323
- (
324
- 'a back-edge cycle does NOT collapse into one column (regression)',
325
- function (fDone)
326
- {
327
- // n0 -> n1 -> n2 -> n3 -> n4 with a back-edge n4 -> n1.
328
- // Plain Kahn's would place n0, then dump n1..n4 into a single
329
- // trailing layer (one tall column). The cycle-tolerant ranker
330
- // must spread them across columns instead.
331
- let tmpNodes = makeNodes(5);
332
- let tmpConns = makeChain(5);
333
- tmpConns.push({ Hash: 'c-back', SourceNodeHash: 'n-4', TargetNodeHash: 'n-1' });
334
-
335
- libLayoutLayered.Apply(tmpNodes, tmpConns, libLayoutLayered.DefaultParameters);
336
-
337
- let tmpColumns = {};
338
- let tmpMaxPerColumn = 0;
339
- for (let i = 0; i < tmpNodes.length; i++)
340
- {
341
- let tmpX = tmpNodes[i].X;
342
- tmpColumns[tmpX] = (tmpColumns[tmpX] || 0) + 1;
343
- tmpMaxPerColumn = Math.max(tmpMaxPerColumn, tmpColumns[tmpX]);
344
- }
345
- // Five distinct columns, one node each — no tower.
346
- libExpect(Object.keys(tmpColumns).length).to.equal(5);
347
- libExpect(tmpMaxPerColumn).to.equal(1);
348
- fDone();
349
- }
350
- );
351
-
352
- test
353
- (
354
- 'a self-loop does not strand a node in a trailing column',
355
- function (fDone)
356
- {
357
- // n0 -> n1 -> n2 with a self-loop on n1.
358
- let tmpNodes = makeNodes(3);
359
- let tmpConns = makeChain(3);
360
- tmpConns.push({ Hash: 'c-self', SourceNodeHash: 'n-1', TargetNodeHash: 'n-1' });
361
-
362
- libLayoutLayered.Apply(tmpNodes, tmpConns, libLayoutLayered.DefaultParameters);
363
-
364
- // Clean chain: three columns left to right, one node each.
365
- libExpect(tmpNodes[0].X).to.be.below(tmpNodes[1].X);
366
- libExpect(tmpNodes[1].X).to.be.below(tmpNodes[2].X);
367
- fDone();
368
- }
369
- );
370
- }
371
- );
372
-
373
- // ── Rank (shared ranker) ──────────────────────────────────────
374
-
375
- suite
376
- (
377
- 'Layout-Rank ranker',
378
- function ()
379
- {
380
- test
381
- (
382
- 'a chain ranks one node per rank, in order',
383
- function (fDone)
384
- {
385
- let tmpNodes = makeNodes(4);
386
- let tmpRanks = libLayoutRank.toRanks(tmpNodes, makeChain(4));
387
- libExpect(tmpRanks.length).to.equal(4);
388
- libExpect(tmpRanks[0]).to.deep.equal(['n-0']);
389
- libExpect(tmpRanks[3]).to.deep.equal(['n-3']);
390
- fDone();
391
- }
392
- );
393
-
394
- test
395
- (
396
- 'unconnected nodes share the first rank',
397
- function (fDone)
398
- {
399
- let tmpNodes = makeNodes(3);
400
- let tmpRanks = libLayoutRank.toRanks(tmpNodes, []);
401
- libExpect(tmpRanks.length).to.equal(1);
402
- libExpect(tmpRanks[0].length).to.equal(3);
403
- fDone();
404
- }
405
- );
406
-
407
- test
408
- (
409
- 'toOrder visits every node exactly once even with a cycle',
410
- function (fDone)
411
- {
412
- let tmpNodes = makeNodes(5);
413
- let tmpConns = makeChain(5);
414
- tmpConns.push({ Hash: 'c-back', SourceNodeHash: 'n-4', TargetNodeHash: 'n-1' });
415
- let tmpOrder = libLayoutRank.toOrder(tmpNodes, tmpConns);
416
- libExpect(tmpOrder.length).to.equal(5);
417
- let tmpSeen = {};
418
- for (let i = 0; i < tmpOrder.length; i++) tmpSeen[tmpOrder[i]] = true;
419
- libExpect(Object.keys(tmpSeen).length).to.equal(5);
420
- fDone();
421
- }
422
- );
423
-
424
- test
425
- (
426
- 'empty input returns an empty rank list',
427
- function (fDone)
428
- {
429
- libExpect(libLayoutRank.toRanks([], [])).to.deep.equal([]);
430
- libExpect(libLayoutRank.toOrder(null, null)).to.deep.equal([]);
431
- fDone();
432
- }
433
- );
434
- }
435
- );
436
-
437
- // ── Staggered ─────────────────────────────────────────────────
438
-
439
- suite
440
- (
441
- 'Staggered algorithm',
442
- function ()
443
- {
444
- test
445
- (
446
- 'two rows zigzag: X strictly increases, Y alternates',
447
- function (fDone)
448
- {
449
- let tmpNodes = makeNodes(4);
450
- let tmpConns = makeChain(4);
451
- libLayoutStaggered.Apply(tmpNodes, tmpConns, { Rows: 2, ColumnSpacing: 80, RowOffset: 150, StartX: 0, StartY: 0 });
452
-
453
- // Topological order is n0..n3; column pitch = 180 + 80 = 260.
454
- libExpect(tmpNodes[0].X).to.equal(0);
455
- libExpect(tmpNodes[1].X).to.equal(260);
456
- libExpect(tmpNodes[2].X).to.equal(520);
457
- libExpect(tmpNodes[3].X).to.equal(780);
458
- // Rows=2 → row pattern 0,1,0,1 → Y 0,150,0,150.
459
- libExpect(tmpNodes[0].Y).to.equal(0);
460
- libExpect(tmpNodes[1].Y).to.equal(150);
461
- libExpect(tmpNodes[2].Y).to.equal(0);
462
- libExpect(tmpNodes[3].Y).to.equal(150);
463
- fDone();
464
- }
465
- );
466
-
467
- test
468
- (
469
- 'three rows make a triangle-wave stairstep (down then up)',
470
- function (fDone)
471
- {
472
- let tmpNodes = makeNodes(6);
473
- let tmpConns = makeChain(6);
474
- libLayoutStaggered.Apply(tmpNodes, tmpConns, { Rows: 3, RowOffset: 100, StartX: 0, StartY: 0 });
475
-
476
- // period = 4 → row phases 0,1,2,1,0,1 → Y 0,100,200,100,0,100.
477
- let tmpRows = tmpNodes.map((pN) => pN.Y / 100);
478
- libExpect(tmpRows).to.deep.equal([0, 1, 2, 1, 0, 1]);
479
- fDone();
480
- }
481
- );
482
-
483
- test
484
- (
485
- 'column pitch follows the widest node',
486
- function (fDone)
487
- {
488
- let tmpNodes = makeNodes(3);
489
- tmpNodes[1].Width = 400; // widest
490
- libLayoutStaggered.Apply(tmpNodes, makeChain(3), { ColumnSpacing: 50, StartX: 0 });
491
- // pitch = 400 + 50 = 450
492
- libExpect(tmpNodes[1].X).to.equal(450);
493
- libExpect(tmpNodes[2].X).to.equal(900);
494
- fDone();
495
- }
496
- );
497
-
498
- test
499
- (
500
- 'Rows=1 places every node on a single row',
501
- function (fDone)
502
- {
503
- let tmpNodes = makeNodes(4);
504
- libLayoutStaggered.Apply(tmpNodes, makeChain(4), { Rows: 1, StartY: 42 });
505
- for (let i = 0; i < tmpNodes.length; i++)
506
- {
507
- libExpect(tmpNodes[i].Y).to.equal(42);
508
- }
509
- fDone();
510
- }
511
- );
512
-
513
- test
514
- (
515
- 'empty node list does not throw',
516
- function (fDone)
517
- {
518
- libExpect(function () { libLayoutStaggered.Apply([], [], {}); }).to.not.throw();
519
- fDone();
520
- }
521
- );
522
- }
523
- );
524
-
525
- // ── ForcedFromCenter ──────────────────────────────────────────
526
-
527
- suite
528
- (
529
- 'ForcedFromCenter algorithm',
530
- function ()
531
- {
532
- test
533
- (
534
- 'is deterministic for a fixed seed',
535
- function (fDone)
536
- {
537
- let tmpNodesA = makeNodes(5);
538
- let tmpNodesB = makeNodes(5);
539
- let tmpConns = makeChain(5);
540
-
541
- let tmpParams = Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 12345, Iterations: 50 });
542
- libLayoutForcedFromCenter.Apply(tmpNodesA, tmpConns, tmpParams);
543
- libLayoutForcedFromCenter.Apply(tmpNodesB, tmpConns, tmpParams);
544
-
545
- for (let i = 0; i < 5; i++)
546
- {
547
- libExpect(tmpNodesA[i].X).to.equal(tmpNodesB[i].X);
548
- libExpect(tmpNodesA[i].Y).to.equal(tmpNodesB[i].Y);
549
- }
550
- fDone();
551
- }
552
- );
553
-
554
- test
555
- (
556
- 'different seeds produce different positions',
557
- function (fDone)
558
- {
559
- let tmpNodesA = makeNodes(5);
560
- let tmpNodesB = makeNodes(5);
561
- let tmpConns = makeChain(5);
562
-
563
- libLayoutForcedFromCenter.Apply(tmpNodesA, tmpConns, Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 1, Iterations: 50 }));
564
- libLayoutForcedFromCenter.Apply(tmpNodesB, tmpConns, Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 2, Iterations: 50 }));
565
-
566
- let tmpDifferent = false;
567
- for (let i = 0; i < 5; i++)
568
- {
569
- if (tmpNodesA[i].X !== tmpNodesB[i].X || tmpNodesA[i].Y !== tmpNodesB[i].Y)
570
- {
571
- tmpDifferent = true;
572
- break;
573
- }
574
- }
575
- libExpect(tmpDifferent).to.equal(true);
576
- fDone();
577
- }
578
- );
579
-
580
- test
581
- (
582
- 'PreservePositions=true keeps placed nodes',
583
- function (fDone)
584
- {
585
- let tmpNodes = makeNodes(2);
586
- tmpNodes[0].X = 12345;
587
- tmpNodes[0].Y = 67890;
588
- // One iteration, no forces should move much
589
- libLayoutForcedFromCenter.Apply(tmpNodes, [], Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, {
590
- Iterations: 0, PreservePositions: true
591
- }));
592
- libExpect(tmpNodes[0].X).to.equal(12345);
593
- libExpect(tmpNodes[0].Y).to.equal(67890);
594
- fDone();
595
- }
596
- );
597
-
598
- test
599
- (
600
- 'rounds final positions to integers',
601
- function (fDone)
602
- {
603
- let tmpNodes = makeNodes(3);
604
- libLayoutForcedFromCenter.Apply(tmpNodes, makeChain(3), Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Iterations: 20 }));
605
- for (let i = 0; i < 3; i++)
606
- {
607
- libExpect(tmpNodes[i].X).to.equal(Math.round(tmpNodes[i].X));
608
- libExpect(tmpNodes[i].Y).to.equal(Math.round(tmpNodes[i].Y));
609
- }
610
- fDone();
611
- }
612
- );
613
- }
614
- );
615
-
616
- // ── Grid ──────────────────────────────────────────────────────
617
-
618
- suite
619
- (
620
- 'Grid algorithm',
621
- function ()
622
- {
623
- test
624
- (
625
- 'auto columns = ceil(sqrt(n))',
626
- function (fDone)
627
- {
628
- let tmpNodes = makeNodes(9);
629
- libLayoutGrid.Apply(tmpNodes, [], libLayoutGrid.DefaultParameters);
630
- // 9 nodes -> 3 cols, 3 rows
631
- libExpect(tmpNodes[0].X).to.equal(100);
632
- libExpect(tmpNodes[0].Y).to.equal(100);
633
- libExpect(tmpNodes[2].X).to.equal(100 + 2 * (180 + 40));
634
- libExpect(tmpNodes[3].X).to.equal(100); // wraps
635
- libExpect(tmpNodes[3].Y).to.equal(100 + (80 + 40));
636
- fDone();
637
- }
638
- );
639
-
640
- test
641
- (
642
- 'explicit Columns: 4',
643
- function (fDone)
644
- {
645
- let tmpNodes = makeNodes(8);
646
- libLayoutGrid.Apply(tmpNodes, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 4 }));
647
- libExpect(tmpNodes[3].X).to.equal(100 + 3 * (180 + 40));
648
- libExpect(tmpNodes[3].Y).to.equal(100);
649
- libExpect(tmpNodes[4].X).to.equal(100); // wraps after col 4
650
- libExpect(tmpNodes[4].Y).to.equal(100 + (80 + 40));
651
- fDone();
652
- }
653
- );
654
-
655
- test
656
- (
657
- 'OrderBy: hash sorts before placing',
658
- function (fDone)
659
- {
660
- let tmpNodes = [
661
- { Hash: 'b', X: 0, Y: 0, Width: 180, Height: 80 },
662
- { Hash: 'a', X: 0, Y: 0, Width: 180, Height: 80 }
663
- ];
664
- libLayoutGrid.Apply(tmpNodes, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 2, OrderBy: 'hash' }));
665
- // 'a' should land at column 0, 'b' at column 1
666
- let tmpA = tmpNodes.find((pN) => pN.Hash === 'a');
667
- let tmpB = tmpNodes.find((pN) => pN.Hash === 'b');
668
- libExpect(tmpA.X).to.equal(100);
669
- libExpect(tmpB.X).to.equal(100 + (180 + 40));
670
- fDone();
671
- }
672
- );
673
- }
674
- );
675
-
676
- // ── Circular ─────────────────────────────────────────────────
677
-
678
- suite
679
- (
680
- 'Circular algorithm',
681
- function ()
682
- {
683
- test
684
- (
685
- 'no connections — single ring with everyone',
686
- function (fDone)
687
- {
688
- let tmpNodes = makeNodes(4);
689
- libLayoutCircular.Apply(tmpNodes, [], libLayoutCircular.DefaultParameters);
690
- // All on ring 0 (radius 0 + 0*220 = 0) — but ring index 0 has radius 0,
691
- // so the single-root center fast-path doesn't apply when ring has more than 1.
692
- // They all end up centered around (CenterX, CenterY).
693
- let tmpCx = 1000, tmpCy = 750;
694
- for (let i = 0; i < 4; i++)
695
- {
696
- let tmpDist = Math.hypot(
697
- tmpNodes[i].X + 90 - tmpCx,
698
- tmpNodes[i].Y + 40 - tmpCy
699
- );
700
- libExpect(tmpDist).to.be.lessThan(1); // all at radius 0
701
- }
702
- fDone();
703
- }
704
- );
705
-
706
- test
707
- (
708
- 'with connections — root node at center',
709
- function (fDone)
710
- {
711
- let tmpNodes = makeNodes(3);
712
- let tmpConns = makeChain(3);
713
- libLayoutCircular.Apply(tmpNodes, tmpConns, libLayoutCircular.DefaultParameters);
714
- let tmpRoot = tmpNodes[0]; // n-0 has in-degree 0
715
- // Root at center: X = CenterX - W/2, Y = CenterY - H/2
716
- libExpect(tmpRoot.X).to.equal(1000 - 90);
717
- libExpect(tmpRoot.Y).to.equal(750 - 40);
718
- fDone();
719
- }
720
- );
721
- }
722
- );
723
-
724
- // ── Tabular ──────────────────────────────────────────────────
725
-
726
- suite
727
- (
728
- 'Tabular algorithm',
729
- function ()
730
- {
731
- test
732
- (
733
- 'stacks nodes vertically',
734
- function (fDone)
735
- {
736
- let tmpNodes = makeNodes(3);
737
- libLayoutTabular.Apply(tmpNodes, [], libLayoutTabular.DefaultParameters);
738
- libExpect(tmpNodes[0].X).to.equal(100);
739
- libExpect(tmpNodes[0].Y).to.equal(100);
740
- libExpect(tmpNodes[1].Y).to.equal(100 + 80 + 40);
741
- libExpect(tmpNodes[2].Y).to.equal(100 + (80 + 40) * 2);
742
- // All same X
743
- libExpect(tmpNodes[1].X).to.equal(100);
744
- libExpect(tmpNodes[2].X).to.equal(100);
745
- fDone();
746
- }
747
- );
748
- }
749
- );
750
-
751
- // ── Columnar ─────────────────────────────────────────────────
752
-
753
- suite
754
- (
755
- 'Columnar algorithm',
756
- function ()
757
- {
758
- test
759
- (
760
- 'fills row-first across N columns',
761
- function (fDone)
762
- {
763
- let tmpNodes = makeNodes(7);
764
- libLayoutColumnar.Apply(tmpNodes, [], Object.assign({}, libLayoutColumnar.DefaultParameters, { Columns: 3 }));
765
- libExpect(tmpNodes[0].X).to.equal(100);
766
- libExpect(tmpNodes[0].Y).to.equal(100);
767
- libExpect(tmpNodes[1].X).to.equal(100 + (180 + 40));
768
- libExpect(tmpNodes[2].X).to.equal(100 + (180 + 40) * 2);
769
- libExpect(tmpNodes[3].X).to.equal(100); // wraps to row 1
770
- libExpect(tmpNodes[3].Y).to.equal(100 + (80 + 40));
771
- fDone();
772
- }
773
- );
774
-
775
- test
776
- (
777
- 'FillOrder: column flows column-first',
778
- function (fDone)
779
- {
780
- let tmpNodes = makeNodes(6);
781
- libLayoutColumnar.Apply(tmpNodes, [], Object.assign({}, libLayoutColumnar.DefaultParameters, { Columns: 3, FillOrder: 'column' }));
782
- // 6 nodes / 3 cols = 2 rows. Column-first: n-0 (0,0), n-1 (0,1), n-2 (1,0), ...
783
- libExpect(tmpNodes[0].X).to.equal(100);
784
- libExpect(tmpNodes[0].Y).to.equal(100);
785
- libExpect(tmpNodes[1].X).to.equal(100);
786
- libExpect(tmpNodes[1].Y).to.equal(100 + (80 + 40));
787
- libExpect(tmpNodes[2].X).to.equal(100 + (180 + 40));
788
- libExpect(tmpNodes[2].Y).to.equal(100);
789
- fDone();
790
- }
791
- );
792
- }
793
- );
794
-
795
- // ── PreciseNumber string parameters ──────────────────────────
796
-
797
- suite
798
- (
799
- 'PreciseNumber string parameters (big.js compatibility)',
800
- function ()
801
- {
802
- test
803
- (
804
- 'Layered: string parameters produce identical positions to numbers',
805
- function (fDone)
806
- {
807
- let tmpA = makeNodes(3);
808
- let tmpB = makeNodes(3);
809
- let tmpConns = makeChain(3);
810
- libLayoutLayered.Apply(tmpA, tmpConns, { HorizontalSpacing: 250, VerticalSpacing: 120, StartX: 100, StartY: 100 });
811
- libLayoutLayered.Apply(tmpB, tmpConns, { HorizontalSpacing: '250', VerticalSpacing: '120', StartX: '100', StartY: '100' });
812
- for (let i = 0; i < 3; i++)
813
- {
814
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
815
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
816
- }
817
- fDone();
818
- }
819
- );
820
-
821
- test
822
- (
823
- 'ForcedFromCenter: string PreciseNumber params match numeric params',
824
- function (fDone)
825
- {
826
- let tmpA = makeNodes(4);
827
- let tmpB = makeNodes(4);
828
- let tmpConns = makeChain(4);
829
- let tmpNumeric = { Iterations: 30, Seed: 7, CenterX: 500, CenterY: 500, SpringLength: 150, SpringStiffness: 0.05, Repulsion: 8000, CenterAttraction: 0.01, CoolingFactor: 0.95, InitialTemperature: 100, InitialSpread: 400 };
830
- let tmpStringy = { Iterations: 30, Seed: 7, CenterX: '500', CenterY: '500', SpringLength: '150', SpringStiffness: '0.05', Repulsion: '8000', CenterAttraction: '0.01', CoolingFactor: '0.95', InitialTemperature: '100', InitialSpread: '400' };
831
- libLayoutForcedFromCenter.Apply(tmpA, tmpConns, tmpNumeric);
832
- libLayoutForcedFromCenter.Apply(tmpB, tmpConns, tmpStringy);
833
- for (let i = 0; i < 4; i++)
834
- {
835
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
836
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
837
- }
838
- fDone();
839
- }
840
- );
841
-
842
- test
843
- (
844
- 'Grid: Columns as string-int matches Columns as int',
845
- function (fDone)
846
- {
847
- let tmpA = makeNodes(8);
848
- let tmpB = makeNodes(8);
849
- libLayoutGrid.Apply(tmpA, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 4 }));
850
- libLayoutGrid.Apply(tmpB, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: '4' }));
851
- for (let i = 0; i < 8; i++)
852
- {
853
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
854
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
855
- }
856
- fDone();
857
- }
858
- );
859
-
860
- test
861
- (
862
- 'Circular: string CenterX/Y/RingSpacing match numeric',
863
- function (fDone)
864
- {
865
- let tmpA = makeNodes(5);
866
- let tmpB = makeNodes(5);
867
- libLayoutCircular.Apply(tmpA, [], { CenterX: 1000, CenterY: 750, RingSpacing: 220, InnerRadius: 100, StartAngle: -90 });
868
- libLayoutCircular.Apply(tmpB, [], { CenterX: '1000', CenterY: '750', RingSpacing: '220', InnerRadius: '100', StartAngle: '-90' });
869
- for (let i = 0; i < 5; i++)
870
- {
871
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
872
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
873
- }
874
- fDone();
875
- }
876
- );
877
-
878
- test
879
- (
880
- 'Tabular: string spacing matches numeric',
881
- function (fDone)
882
- {
883
- let tmpA = makeNodes(4);
884
- let tmpB = makeNodes(4);
885
- libLayoutTabular.Apply(tmpA, [], { StartX: 100, StartY: 100, VerticalSpacing: 40 });
886
- libLayoutTabular.Apply(tmpB, [], { StartX: '100', StartY: '100', VerticalSpacing: '40' });
887
- for (let i = 0; i < 4; i++)
888
- {
889
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
890
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
891
- }
892
- fDone();
893
- }
894
- );
895
-
896
- test
897
- (
898
- 'Columnar: mixed string and number params match all-number params',
899
- function (fDone)
900
- {
901
- let tmpA = makeNodes(7);
902
- let tmpB = makeNodes(7);
903
- libLayoutColumnar.Apply(tmpA, [], { Columns: 3, ColumnSpacing: 40, RowSpacing: 40, StartX: 100, StartY: 100 });
904
- libLayoutColumnar.Apply(tmpB, [], { Columns: '3', ColumnSpacing: '40', RowSpacing: '40', StartX: '100', StartY: '100' });
905
- for (let i = 0; i < 7; i++)
906
- {
907
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
908
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
909
- }
910
- fDone();
911
- }
912
- );
913
-
914
- test
915
- (
916
- 'Empty string parameters fall back to defaults gracefully',
917
- function (fDone)
918
- {
919
- let tmpA = makeNodes(3);
920
- let tmpB = makeNodes(3);
921
- let tmpConns = makeChain(3);
922
- libLayoutLayered.Apply(tmpA, tmpConns, {});
923
- libLayoutLayered.Apply(tmpB, tmpConns, { HorizontalSpacing: '', VerticalSpacing: null, StartX: undefined });
924
- for (let i = 0; i < 3; i++)
925
- {
926
- libExpect(tmpA[i].X).to.equal(tmpB[i].X);
927
- libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
928
- }
929
- fDone();
930
- }
931
- );
932
- }
933
- );
934
-
935
- // ── Custom (no-op) ───────────────────────────────────────────
936
-
937
- suite
938
- (
939
- 'Custom algorithm',
940
- function ()
941
- {
942
- test
943
- (
944
- 'preserves existing X/Y',
945
- function (fDone)
946
- {
947
- let tmpNodes = [
948
- { Hash: 'a', X: 42, Y: 99, Width: 180, Height: 80 },
949
- { Hash: 'b', X: 314, Y: 271, Width: 180, Height: 80 }
950
- ];
951
- libLayoutCustom.Apply(tmpNodes, [], {});
952
- libExpect(tmpNodes[0].X).to.equal(42);
953
- libExpect(tmpNodes[0].Y).to.equal(99);
954
- libExpect(tmpNodes[1].X).to.equal(314);
955
- libExpect(tmpNodes[1].Y).to.equal(271);
956
- fDone();
957
- }
958
- );
959
- }
960
- );
961
-
962
- // ── applyLayout dispatch ─────────────────────────────────────
963
-
964
- suite
965
- (
966
- 'applyLayout dispatch',
967
- function ()
968
- {
969
- test
970
- (
971
- 'falls back to Layered for unknown algorithm name',
972
- function (fDone)
973
- {
974
- let tmpNodes = makeNodes(2);
975
- let tmpConns = makeChain(2);
976
- _LayoutService.applyLayout(tmpNodes, tmpConns, 'TotallyMadeUp', {});
977
- // Should match Layered output (chain → 2 layers)
978
- libExpect(tmpNodes[0].X).to.equal(100);
979
- libExpect(tmpNodes[1].X).to.equal(100 + 180 + 250);
980
- fDone();
981
- }
982
- );
983
-
984
- test
985
- (
986
- 'merges DefaultParameters with caller overrides',
987
- function (fDone)
988
- {
989
- let tmpNodes = makeNodes(1);
990
- _LayoutService.applyLayout(tmpNodes, [], 'Layered', { StartX: 500 });
991
- // StartY left as default (100), StartX overridden
992
- libExpect(tmpNodes[0].X).to.equal(500);
993
- libExpect(tmpNodes[0].Y).to.equal(100);
994
- fDone();
995
- }
996
- );
997
-
998
- test
999
- (
1000
- 'getMergedParameters returns defaults merged with overrides',
1001
- function (fDone)
1002
- {
1003
- let tmpMerged = _LayoutService.getMergedParameters('Layered', { StartX: 999 });
1004
- libExpect(tmpMerged.StartX).to.equal(999);
1005
- libExpect(tmpMerged.StartY).to.equal(100); // default preserved
1006
- libExpect(tmpMerged.HorizontalSpacing).to.equal(250);
1007
- fDone();
1008
- }
1009
- );
1010
- }
1011
- );
1012
-
1013
- // ── Spacing multiplier ───────────────────────────────────────
1014
-
1015
- suite
1016
- (
1017
- 'Spacing multiplier',
1018
- function ()
1019
- {
1020
- test
1021
- (
1022
- 'Layered: Spacing=2 doubles the gap between layers',
1023
- function (fDone)
1024
- {
1025
- let tmpA = makeNodes(2);
1026
- let tmpB = makeNodes(2);
1027
- let tmpConns = makeChain(2);
1028
- libLayoutLayered.Apply(tmpA, tmpConns, { Spacing: 1.0 });
1029
- libLayoutLayered.Apply(tmpB, tmpConns, { Spacing: 2.0 });
1030
- // First node lands at StartX both times.
1031
- libExpect(tmpA[0].X).to.equal(tmpB[0].X);
1032
- // Second node Δx with Spacing=2 should be twice the Δx with Spacing=1.
1033
- let tmpDxA = tmpA[1].X - tmpA[0].X;
1034
- let tmpDxB = tmpB[1].X - tmpB[0].X;
1035
- // Δx = nodeWidth + (HorizontalSpacing * Spacing). With nodeWidth=180, HSpace=250:
1036
- // Spacing=1 → 430; Spacing=2 → 680. Difference is HorizontalSpacing (250).
1037
- libExpect(tmpDxB - tmpDxA).to.equal(250);
1038
- fDone();
1039
- }
1040
- );
1041
-
1042
- test
1043
- (
1044
- 'Tabular: Spacing scales VerticalSpacing',
1045
- function (fDone)
1046
- {
1047
- let tmpA = makeNodes(3);
1048
- let tmpB = makeNodes(3);
1049
- libLayoutTabular.Apply(tmpA, [], { Spacing: 1.0 });
1050
- libLayoutTabular.Apply(tmpB, [], { Spacing: 0.5 });
1051
- let tmpDyA = tmpA[1].Y - tmpA[0].Y; // height(80) + 40*1 = 120
1052
- let tmpDyB = tmpB[1].Y - tmpB[0].Y; // height(80) + 40*0.5 = 100
1053
- libExpect(tmpDyA).to.equal(120);
1054
- libExpect(tmpDyB).to.equal(100);
1055
- fDone();
1056
- }
1057
- );
1058
-
1059
- test
1060
- (
1061
- 'Circular: Spacing scales RingSpacing (and InnerRadius)',
1062
- function (fDone)
1063
- {
1064
- let tmpA = makeNodes(3);
1065
- let tmpB = makeNodes(3);
1066
- let tmpConns = makeChain(3);
1067
- libLayoutCircular.Apply(tmpA, tmpConns, { Spacing: 1.0 });
1068
- libLayoutCircular.Apply(tmpB, tmpConns, { Spacing: 2.0 });
1069
- // Root sits at center for both. Second node is on ring 1 (radius = RingSpacing*Spacing).
1070
- let tmpRadiusA = Math.hypot((tmpA[1].X + 90) - 1000, (tmpA[1].Y + 40) - 750);
1071
- let tmpRadiusB = Math.hypot((tmpB[1].X + 90) - 1000, (tmpB[1].Y + 40) - 750);
1072
- libExpect(Math.round(tmpRadiusB / tmpRadiusA)).to.equal(2);
1073
- fDone();
1074
- }
1075
- );
1076
- }
1077
- );
1078
-
1079
- // ── Edge-theme registry ──────────────────────────────────────
1080
-
1081
- suite
1082
- (
1083
- 'Edge-theme registry',
1084
- function ()
1085
- {
1086
- test
1087
- (
1088
- 'all four built-in themes are registered',
1089
- function (fDone)
1090
- {
1091
- let tmpNames = _LayoutService.getEdgeThemeNames();
1092
- libExpect(tmpNames).to.include.members(['Bezier', 'Orthogonal', 'Straight', 'OrthogonalSnap']);
1093
- fDone();
1094
- }
1095
- );
1096
-
1097
- test
1098
- (
1099
- 'getEdgeTheme returns null for unknown name',
1100
- function (fDone)
1101
- {
1102
- libExpect(_LayoutService.getEdgeTheme('NotARealTheme')).to.equal(null);
1103
- fDone();
1104
- }
1105
- );
1106
-
1107
- test
1108
- (
1109
- 'registerEdgeTheme rejects descriptors missing GeneratePath',
1110
- function (fDone)
1111
- {
1112
- libExpect(_LayoutService.registerEdgeTheme({ Name: 'X' })).to.equal(false);
1113
- libExpect(_LayoutService.registerEdgeTheme({ GeneratePath: function () {} })).to.equal(false);
1114
- fDone();
1115
- }
1116
- );
1117
-
1118
- test
1119
- (
1120
- 'registerEdgeTheme + dispatch via resolveActiveEdgeTheme',
1121
- function (fDone)
1122
- {
1123
- let tmpStub =
1124
- {
1125
- Name: 'StubEdge',
1126
- GeneratePath: function () { return 'M 0 0 L 10 10'; }
1127
- };
1128
- libExpect(_LayoutService.registerEdgeTheme(tmpStub)).to.equal(true);
1129
- let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { EdgeTheme: 'StubEdge' } });
1130
- libExpect(tmpResolved).to.equal(tmpStub);
1131
- fDone();
1132
- }
1133
- );
1134
-
1135
- test
1136
- (
1137
- 'resolveActiveEdgeTheme — flow-level EdgeTheme overrides layout default',
1138
- function (fDone)
1139
- {
1140
- _LayoutService._FlowView = { _FlowData: { EdgeTheme: 'Straight', LayoutAlgorithm: 'Layered' } };
1141
- let tmpResolved = _LayoutService.resolveActiveEdgeTheme({});
1142
- libExpect(tmpResolved.Name).to.equal('Straight');
1143
- _LayoutService._FlowView = null;
1144
- fDone();
1145
- }
1146
- );
1147
-
1148
- test
1149
- (
1150
- 'resolveActiveEdgeTheme — falls back to layout DefaultEdgeTheme when no flow override',
1151
- function (fDone)
1152
- {
1153
- _LayoutService._FlowView = { _FlowData: { EdgeTheme: null, LayoutAlgorithm: 'Layered' } };
1154
- let tmpResolved = _LayoutService.resolveActiveEdgeTheme({});
1155
- // Layered's DefaultEdgeTheme is 'Orthogonal'
1156
- libExpect(tmpResolved.Name).to.equal('Orthogonal');
1157
- _LayoutService._FlowView = null;
1158
- fDone();
1159
- }
1160
- );
1161
-
1162
- test
1163
- (
1164
- 'resolveActiveEdgeTheme — connection-level EdgeTheme beats flow-level',
1165
- function (fDone)
1166
- {
1167
- _LayoutService._FlowView = { _FlowData: { EdgeTheme: 'Straight', LayoutAlgorithm: 'Layered' } };
1168
- let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { EdgeTheme: 'Bezier' } });
1169
- libExpect(tmpResolved.Name).to.equal('Bezier');
1170
- _LayoutService._FlowView = null;
1171
- fDone();
1172
- }
1173
- );
1174
-
1175
- test
1176
- (
1177
- 'resolveActiveEdgeTheme — legacy LineMode=orthogonal maps to Orthogonal theme',
1178
- function (fDone)
1179
- {
1180
- let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { LineMode: 'orthogonal' } });
1181
- libExpect(tmpResolved.Name).to.equal('Orthogonal');
1182
- fDone();
1183
- }
1184
- );
1185
- }
1186
- );
1187
-
1188
- // ── Edge themes — path generation ────────────────────────────
1189
-
1190
- suite
1191
- (
1192
- 'Edge themes — GeneratePath',
1193
- function ()
1194
- {
1195
- let _Helpers;
1196
-
1197
- setup
1198
- (
1199
- function ()
1200
- {
1201
- _Helpers =
1202
- {
1203
- generateBezier: function (s, e) { return `BEZ ${s.x},${s.y}→${e.x},${e.y}`; },
1204
- generateMultiBezier: function (s, e, h) { return `MBEZ ${s.x},${s.y} hs=${h.length} →${e.x},${e.y}`; },
1205
- generateOrthogonal: function (s, e, c, m) { return `ORT ${s.x},${s.y}→${e.x},${e.y} m=${m}`; },
1206
- getBezierHandles: function (d) { return (d && d.HandleCustomized && d.BezierHandles) || []; }
1207
- };
1208
- }
1209
- );
1210
-
1211
- test
1212
- (
1213
- 'Bezier: emits straight-bezier when no custom handles',
1214
- function (fDone)
1215
- {
1216
- let tmpPath = libEdgeBezier.GeneratePath({
1217
- Source: { x: 0, y: 0, side: 'right' },
1218
- Target: { x: 100, y: 50, side: 'left' },
1219
- Connection: { Data: {} },
1220
- Helpers: _Helpers, Parameters: {}
1221
- });
1222
- libExpect(tmpPath).to.equal('BEZ 0,0→100,50');
1223
- fDone();
1224
- }
1225
- );
1226
-
1227
- test
1228
- (
1229
- 'Bezier: honors per-connection custom handles',
1230
- function (fDone)
1231
- {
1232
- let tmpPath = libEdgeBezier.GeneratePath({
1233
- Source: { x: 0, y: 0 }, Target: { x: 100, y: 50 },
1234
- Connection: { Data: { HandleCustomized: true, BezierHandles: [{ x: 50, y: 25 }, { x: 70, y: 30 }] } },
1235
- Helpers: _Helpers, Parameters: {}
1236
- });
1237
- libExpect(tmpPath.indexOf('MBEZ')).to.equal(0);
1238
- libExpect(tmpPath.indexOf('hs=2')).to.be.above(0);
1239
- fDone();
1240
- }
1241
- );
1242
-
1243
- test
1244
- (
1245
- 'Orthogonal: emits orthogonal path; respects custom corners',
1246
- function (fDone)
1247
- {
1248
- let tmpPath = libEdgeOrthogonal.GeneratePath({
1249
- Source: { x: 0, y: 0 }, Target: { x: 100, y: 50 },
1250
- Connection: { Data: { OrthoMidOffset: 7 } },
1251
- Helpers: _Helpers, Parameters: {}
1252
- });
1253
- libExpect(tmpPath).to.equal('ORT 0,0→100,50 m=7');
1254
- fDone();
1255
- }
1256
- );
1257
-
1258
- test
1259
- (
1260
- 'Straight: literal M..L path',
1261
- function (fDone)
1262
- {
1263
- let tmpPath = libEdgeStraight.GeneratePath({
1264
- Source: { x: 12, y: 34 }, Target: { x: 56, y: 78 },
1265
- Connection: { Data: {} }, Helpers: _Helpers, Parameters: {}
1266
- });
1267
- libExpect(tmpPath).to.equal('M 12 34 L 56 78');
1268
- fDone();
1269
- }
1270
- );
1271
-
1272
- test
1273
- (
1274
- 'OrthogonalSnap: AdjustLayout snaps node positions to grid',
1275
- function (fDone)
1276
- {
1277
- let tmpNodes =
1278
- [
1279
- { X: 13, Y: 27, Width: 100, Height: 50 },
1280
- { X: 86, Y: 199, Width: 100, Height: 50 }
1281
- ];
1282
- libEdgeOrthogonalSnap.AdjustLayout(tmpNodes, [], { GridSize: 20 });
1283
- libExpect(tmpNodes[0].X).to.equal(20);
1284
- libExpect(tmpNodes[0].Y).to.equal(20);
1285
- libExpect(tmpNodes[1].X).to.equal(80);
1286
- libExpect(tmpNodes[1].Y).to.equal(200);
1287
- fDone();
1288
- }
1289
- );
1290
-
1291
- test
1292
- (
1293
- 'OrthogonalSnap: AdjustLayout no-op when GridSize<=0',
1294
- function (fDone)
1295
- {
1296
- let tmpNodes = [{ X: 13, Y: 27, Width: 100, Height: 50 }];
1297
- libEdgeOrthogonalSnap.AdjustLayout(tmpNodes, [], { GridSize: 0 });
1298
- libExpect(tmpNodes[0].X).to.equal(13);
1299
- libExpect(tmpNodes[0].Y).to.equal(27);
1300
- fDone();
1301
- }
1302
- );
1303
- }
1304
- );
1305
-
1306
- // ── Edge-Perimeter (ResolveAttachment) ───────────────────────
1307
-
1308
- suite
1309
- (
1310
- 'Edge-Perimeter — perimeter-routing attachment',
1311
- function ()
1312
- {
1313
- let _Hub;
1314
-
1315
- setup
1316
- (
1317
- function ()
1318
- {
1319
- // 200×100 node at (400, 300) → center (500, 350)
1320
- _Hub = { Hash: 'hub', X: 400, Y: 300, Width: 200, Height: 100 };
1321
- }
1322
- );
1323
-
1324
- test
1325
- (
1326
- 'aim due east → exits right edge at center-Y',
1327
- function (fDone)
1328
- {
1329
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1330
- Node: _Hub,
1331
- OtherNode: { X: 1000, Y: 300, Width: 200, Height: 100 } // center (1100, 350)
1332
- });
1333
- libExpect(tmpAttach.x).to.equal(600); // hub right edge
1334
- libExpect(tmpAttach.y).to.equal(350); // hub center Y
1335
- libExpect(tmpAttach.side).to.equal('right');
1336
- fDone();
1337
- }
1338
- );
1339
-
1340
- test
1341
- (
1342
- 'aim due north → exits top edge at center-X',
1343
- function (fDone)
1344
- {
1345
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1346
- Node: _Hub,
1347
- OtherNode: { X: 400, Y: -100, Width: 200, Height: 100 } // center (500, -50)
1348
- });
1349
- libExpect(tmpAttach.x).to.equal(500); // hub center X
1350
- libExpect(tmpAttach.y).to.equal(300); // hub top edge
1351
- libExpect(tmpAttach.side).to.equal('top');
1352
- fDone();
1353
- }
1354
- );
1355
-
1356
- test
1357
- (
1358
- 'aim due south → exits bottom edge at center-X',
1359
- function (fDone)
1360
- {
1361
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1362
- Node: _Hub,
1363
- OtherNode: { X: 400, Y: 700, Width: 200, Height: 100 } // center (500, 750)
1364
- });
1365
- libExpect(tmpAttach.x).to.equal(500);
1366
- libExpect(tmpAttach.y).to.equal(400); // hub bottom edge
1367
- libExpect(tmpAttach.side).to.equal('bottom');
1368
- fDone();
1369
- }
1370
- );
1371
-
1372
- test
1373
- (
1374
- 'aim diagonally up-right → exits whichever edge the line hits first',
1375
- function (fDone)
1376
- {
1377
- // Aim at (700, 100). dx=200, dy=-250.
1378
- // halfW=100 → tx = 100/200 = 0.5
1379
- // halfH=50 → ty = 50/250 = 0.2 ← smaller; top edge wins
1380
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1381
- Node: _Hub,
1382
- OtherNode: { X: 600, Y: 50, Width: 200, Height: 100 } // center (700, 100)
1383
- });
1384
- libExpect(tmpAttach.side).to.equal('top');
1385
- libExpect(tmpAttach.y).to.equal(300);
1386
- // At t=0.2 from (500,350) in direction (200,-250) → (540, 300).
1387
- libExpect(tmpAttach.x).to.equal(540);
1388
- fDone();
1389
- }
1390
- );
1391
-
1392
- test
1393
- (
1394
- '8 spokes around a hub each get a unique exit point',
1395
- function (fDone)
1396
- {
1397
- let tmpExits = {};
1398
- for (let i = 0; i < 8; i++)
1399
- {
1400
- let tmpAngle = (i / 8) * 2 * Math.PI;
1401
- let tmpSpoke = {
1402
- X: 500 + Math.cos(tmpAngle) * 400 - 70,
1403
- Y: 350 + Math.sin(tmpAngle) * 400 - 35,
1404
- Width: 140, Height: 70
1405
- };
1406
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1407
- Node: _Hub,
1408
- OtherNode: tmpSpoke
1409
- });
1410
- let tmpKey = `${Math.round(tmpAttach.x)},${Math.round(tmpAttach.y)}`;
1411
- tmpExits[tmpKey] = (tmpExits[tmpKey] || 0) + 1;
1412
- }
1413
- let tmpUniqueExits = Object.keys(tmpExits).length;
1414
- libExpect(tmpUniqueExits).to.equal(8);
1415
- fDone();
1416
- }
1417
- );
1418
-
1419
- test
1420
- (
1421
- 'identical centers fall back to DefaultPosition',
1422
- function (fDone)
1423
- {
1424
- let tmpDefault = { x: 600, y: 350, side: 'right' };
1425
- let tmpAttach = libEdgePerimeter.ResolveAttachment({
1426
- Node: _Hub,
1427
- OtherNode: { X: 400, Y: 300, Width: 200, Height: 100 }, // identical center
1428
- DefaultPosition: tmpDefault
1429
- });
1430
- libExpect(tmpAttach).to.equal(tmpDefault);
1431
- fDone();
1432
- }
1433
- );
1434
-
1435
- test
1436
- (
1437
- 'returns null when node geometry is missing',
1438
- function (fDone)
1439
- {
1440
- libExpect(libEdgePerimeter.ResolveAttachment({ Node: null })).to.equal(null);
1441
- libExpect(libEdgePerimeter.ResolveAttachment({ Node: { X: 0 } })).to.equal(null);
1442
- fDone();
1443
- }
1444
- );
1445
- }
1446
- );
1447
-
1448
- // ── centerNodes ──────────────────────────────────────────────
1449
-
1450
- suite
1451
- (
1452
- 'centerNodes',
1453
- function ()
1454
- {
1455
- test
1456
- (
1457
- 'translates the bounding box center to a target',
1458
- function (fDone)
1459
- {
1460
- let tmpNodes = [
1461
- { Hash: 'a', X: 0, Y: 0, Width: 100, Height: 50 },
1462
- { Hash: 'b', X: 200, Y: 0, Width: 100, Height: 50 }
1463
- ];
1464
- _LayoutService.centerNodes(tmpNodes, 1000, 500);
1465
- // Original bounding box: (0,0) to (300,50), center (150,25). Offset (850, 475).
1466
- libExpect(tmpNodes[0].X).to.equal(850);
1467
- libExpect(tmpNodes[0].Y).to.equal(475);
1468
- libExpect(tmpNodes[1].X).to.equal(1050);
1469
- libExpect(tmpNodes[1].Y).to.equal(475);
1470
- fDone();
1471
- }
1472
- );
1473
- }
1474
- );
1475
-
1476
- // ── Auto-apply hookup (simulated) ────────────────────────────
1477
-
1478
- suite
1479
- (
1480
- 'Auto-apply event hookup',
1481
- function ()
1482
- {
1483
- // Build a minimal mock FlowView that exposes just enough
1484
- // to exercise the auto-apply handler logic in PictView-Flow.
1485
- // We don't pull the full PictView-Flow because it requires a
1486
- // pict-view runtime; instead we replicate the handler shape.
1487
- function makeMockFlowView(pAlgorithm, pAutoApply)
1488
- {
1489
- let tmpHandlers = {};
1490
- return {
1491
- _FlowData: {
1492
- Nodes: makeNodes(3),
1493
- Connections: makeChain(3),
1494
- LayoutAlgorithm: pAlgorithm,
1495
- LayoutParameters: {},
1496
- LayoutAutoApply: pAutoApply
1497
- },
1498
- _AutoApplyInProgress: false,
1499
- _AutoApplyHandlerHashes: [],
1500
- applyCalls: 0,
1501
- _LayoutService: _LayoutService,
1502
- applyCurrentLayout: function ()
1503
- {
1504
- this.applyCalls++;
1505
- let tmpAlgo = this._FlowData.LayoutAlgorithm;
1506
- if (tmpAlgo === 'Custom') return;
1507
- this._AutoApplyInProgress = true;
1508
- try
1509
- {
1510
- this._LayoutService.applyLayout(
1511
- this._FlowData.Nodes,
1512
- this._FlowData.Connections,
1513
- tmpAlgo,
1514
- this._FlowData.LayoutParameters
1515
- );
1516
- }
1517
- finally
1518
- {
1519
- this._AutoApplyInProgress = false;
1520
- }
1521
- },
1522
- fireMockEvent: function (pName)
1523
- {
1524
- let tmpHandler = tmpHandlers[pName];
1525
- if (tmpHandler) tmpHandler();
1526
- },
1527
- subscribe: function ()
1528
- {
1529
- let tmpEvents = ['onNodeAdded', 'onNodeRemoved', 'onConnectionCreated', 'onConnectionRemoved'];
1530
- for (let i = 0; i < tmpEvents.length; i++)
1531
- {
1532
- ((pEvent) =>
1533
- {
1534
- tmpHandlers[pEvent] = () =>
1535
- {
1536
- if (this._AutoApplyInProgress) return;
1537
- if (!this._FlowData.LayoutAutoApply) return;
1538
- if (!this._FlowData.LayoutAlgorithm || this._FlowData.LayoutAlgorithm === 'Custom') return;
1539
- this.applyCurrentLayout();
1540
- };
1541
- })(tmpEvents[i]);
1542
- }
1543
- }
1544
- };
1545
- }
1546
-
1547
- test
1548
- (
1549
- 'fires applyCurrentLayout on onNodeAdded when AutoApply is true and algorithm is non-Custom',
1550
- function (fDone)
1551
- {
1552
- let tmpMock = makeMockFlowView('Grid', true);
1553
- tmpMock.subscribe();
1554
- tmpMock.fireMockEvent('onNodeAdded');
1555
- libExpect(tmpMock.applyCalls).to.equal(1);
1556
- fDone();
1557
- }
1558
- );
1559
-
1560
- test
1561
- (
1562
- 'does NOT fire when AutoApply is false',
1563
- function (fDone)
1564
- {
1565
- let tmpMock = makeMockFlowView('Grid', false);
1566
- tmpMock.subscribe();
1567
- tmpMock.fireMockEvent('onNodeAdded');
1568
- libExpect(tmpMock.applyCalls).to.equal(0);
1569
- fDone();
1570
- }
1571
- );
1572
-
1573
- test
1574
- (
1575
- 'does NOT fire when algorithm is Custom',
1576
- function (fDone)
1577
- {
1578
- let tmpMock = makeMockFlowView('Custom', true);
1579
- tmpMock.subscribe();
1580
- tmpMock.fireMockEvent('onNodeAdded');
1581
- libExpect(tmpMock.applyCalls).to.equal(0);
1582
- fDone();
1583
- }
1584
- );
1585
-
1586
- test
1587
- (
1588
- 'fires on each of the four structural events',
1589
- function (fDone)
1590
- {
1591
- let tmpMock = makeMockFlowView('Grid', true);
1592
- tmpMock.subscribe();
1593
- tmpMock.fireMockEvent('onNodeAdded');
1594
- tmpMock.fireMockEvent('onNodeRemoved');
1595
- tmpMock.fireMockEvent('onConnectionCreated');
1596
- tmpMock.fireMockEvent('onConnectionRemoved');
1597
- libExpect(tmpMock.applyCalls).to.equal(4);
1598
- fDone();
1599
- }
1600
- );
1601
- }
1602
- );
1603
- }
1604
- );