pict-section-flow 0.0.16 → 0.0.18

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 (84) hide show
  1. package/README.md +18 -18
  2. package/docs/Architecture.md +1 -1
  3. package/docs/Data_Model.md +2 -2
  4. package/docs/Getting_Started.md +5 -5
  5. package/docs/Implementation_Reference.md +6 -6
  6. package/docs/Layout_Persistence.md +3 -3
  7. package/docs/README.md +12 -12
  8. package/docs/_cover.md +1 -1
  9. package/docs/_sidebar.md +6 -6
  10. package/docs/_version.json +7 -0
  11. package/docs/api/PictFlowCard.md +6 -6
  12. package/docs/api/PictFlowCardPropertiesPanel.md +2 -2
  13. package/docs/api/addConnection.md +4 -4
  14. package/docs/api/addNode.md +6 -6
  15. package/docs/api/autoLayout.md +2 -2
  16. package/docs/api/getFlowData.md +5 -5
  17. package/docs/api/marshalToView.md +3 -3
  18. package/docs/api/openPanel.md +2 -2
  19. package/docs/api/registerHandler.md +3 -3
  20. package/docs/api/registerNodeType.md +3 -3
  21. package/docs/api/removeConnection.md +5 -5
  22. package/docs/api/removeNode.md +6 -6
  23. package/docs/api/saveLayout.md +2 -2
  24. package/docs/api/screenToSVGCoords.md +2 -2
  25. package/docs/api/selectNode.md +3 -3
  26. package/docs/api/setTheme.md +2 -2
  27. package/docs/api/setZoom.md +3 -3
  28. package/docs/api/toggleFullscreen.md +2 -2
  29. package/docs/card-help/EACH.md +3 -3
  30. package/docs/card-help/FREAD.md +5 -5
  31. package/docs/card-help/FWRITE.md +5 -5
  32. package/docs/card-help/GET.md +2 -2
  33. package/docs/card-help/ITE.md +3 -3
  34. package/docs/card-help/LOG.md +4 -4
  35. package/docs/card-help/NOTE.md +1 -1
  36. package/docs/card-help/PREV.md +2 -2
  37. package/docs/card-help/SET.md +5 -5
  38. package/docs/card-help/SPKL.md +2 -2
  39. package/docs/card-help/STAT.md +3 -3
  40. package/docs/card-help/SW.md +4 -4
  41. package/docs/css/docuserve.css +277 -23
  42. package/docs/index.html +2 -2
  43. package/docs/retold-catalog.json +1 -1
  44. package/docs/retold-keyword-index.json +1 -1
  45. package/example_applications/simple_cards/css/flowexample.css +2 -2
  46. package/example_applications/simple_cards/source/card-help-content.js +12 -12
  47. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +1 -1
  48. package/example_applications/simple_cards/source/sample-flows.js +410 -0
  49. package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +5 -5
  50. package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +5 -5
  51. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +4 -4
  52. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +141 -8
  53. package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +2 -2
  54. package/package.json +3 -2
  55. package/source/Pict-Section-Flow.js +26 -0
  56. package/source/providers/PictProvider-Flow-CSS.js +244 -14
  57. package/source/providers/PictProvider-Flow-Theme.js +7 -7
  58. package/source/providers/edges/Edge-Bezier.js +41 -0
  59. package/source/providers/edges/Edge-Orthogonal.js +37 -0
  60. package/source/providers/edges/Edge-OrthogonalSnap.js +72 -0
  61. package/source/providers/edges/Edge-Perimeter-Linear.js +31 -0
  62. package/source/providers/edges/Edge-Perimeter-Orthogonal.js +39 -0
  63. package/source/providers/edges/Edge-Perimeter.js +48 -0
  64. package/source/providers/edges/Edge-PerimeterMath.js +92 -0
  65. package/source/providers/edges/Edge-Straight.js +24 -0
  66. package/source/providers/layouts/Layout-Circular.js +203 -0
  67. package/source/providers/layouts/Layout-Coerce.js +40 -0
  68. package/source/providers/layouts/Layout-Columnar.js +134 -0
  69. package/source/providers/layouts/Layout-Custom.js +27 -0
  70. package/source/providers/layouts/Layout-ForcedFromCenter.js +256 -0
  71. package/source/providers/layouts/Layout-Grid.js +134 -0
  72. package/source/providers/layouts/Layout-Layered.js +209 -0
  73. package/source/providers/layouts/Layout-Tabular.js +94 -0
  74. package/source/services/PictService-Flow-ConnectionRenderer.js +532 -28
  75. package/source/services/PictService-Flow-DataManager.js +12 -1
  76. package/source/services/PictService-Flow-Layout.js +305 -121
  77. package/source/services/PictService-Flow-PortRenderer.js +122 -26
  78. package/source/services/PictService-Flow-RenderManager.js +41 -11
  79. package/source/views/PictView-Flow-FloatingToolbar.js +3 -3
  80. package/source/views/PictView-Flow-Node.js +28 -0
  81. package/source/views/PictView-Flow-Toolbar.js +715 -10
  82. package/source/views/PictView-Flow.js +272 -5
  83. package/test/Layout_tests.js +1400 -0
  84. package/test/PortRenderer_tests.js +11 -2
@@ -1,5 +1,63 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
2
 
3
+ const libLayoutCustom = require('../providers/layouts/Layout-Custom.js');
4
+ const libLayoutLayered = require('../providers/layouts/Layout-Layered.js');
5
+ const libLayoutForcedFromCenter = require('../providers/layouts/Layout-ForcedFromCenter.js');
6
+ const libLayoutGrid = require('../providers/layouts/Layout-Grid.js');
7
+ const libLayoutCircular = require('../providers/layouts/Layout-Circular.js');
8
+ const libLayoutTabular = require('../providers/layouts/Layout-Tabular.js');
9
+ const libLayoutColumnar = require('../providers/layouts/Layout-Columnar.js');
10
+
11
+ const libEdgeBezier = require('../providers/edges/Edge-Bezier.js');
12
+ const libEdgeOrthogonal = require('../providers/edges/Edge-Orthogonal.js');
13
+ const libEdgeStraight = require('../providers/edges/Edge-Straight.js');
14
+ const libEdgeOrthogonalSnap = require('../providers/edges/Edge-OrthogonalSnap.js');
15
+ const libEdgePerimeter = require('../providers/edges/Edge-Perimeter.js');
16
+ const libEdgePerimeterLinear = require('../providers/edges/Edge-Perimeter-Linear.js');
17
+ const libEdgePerimeterOrthogonal = require('../providers/edges/Edge-Perimeter-Orthogonal.js');
18
+
19
+ const _BUILTIN_ALGORITHMS =
20
+ [
21
+ libLayoutCustom,
22
+ libLayoutLayered,
23
+ libLayoutForcedFromCenter,
24
+ libLayoutGrid,
25
+ libLayoutCircular,
26
+ libLayoutTabular,
27
+ libLayoutColumnar
28
+ ];
29
+
30
+ const _BUILTIN_EDGE_THEMES =
31
+ [
32
+ libEdgeBezier,
33
+ libEdgeOrthogonal,
34
+ libEdgeStraight,
35
+ libEdgeOrthogonalSnap,
36
+ libEdgePerimeter,
37
+ libEdgePerimeterLinear,
38
+ libEdgePerimeterOrthogonal
39
+ ];
40
+
41
+ const _LEGACY_ALGORITHM = 'Layered';
42
+ const _DEFAULT_EDGE_THEME = 'Bezier';
43
+
44
+ /**
45
+ * PictServiceFlowLayout
46
+ *
47
+ * Layout-algorithm registry and dispatcher. Holds plain descriptor
48
+ * objects (`{ Name, Label, Apply, DefaultParameters, ParameterSchema }`)
49
+ * for every registered layout algorithm, plus the small helpers
50
+ * `snapToGrid` and `centerNodes` and the special-case `autoLayoutSubset`.
51
+ *
52
+ * Algorithms are pure functions over `(nodes, connections, parameters)`
53
+ * that mutate node X/Y in place. Third parties extend the registry via
54
+ * `registerAlgorithm(pDescriptor)`.
55
+ *
56
+ * Note: `_LayoutProvider` (PictProvider-Flow-Layouts.js) is a different
57
+ * concept — it manages **saved layout snapshots** (named position
58
+ * captures the user can restore later). Don't confuse "layouts" (saved
59
+ * snapshots) with "layout algorithms" (procedural arrangement).
60
+ */
3
61
  class PictServiceFlowLayout extends libFableServiceProviderBase
4
62
  {
5
63
  constructor(pFable, pOptions, pServiceHash)
@@ -10,173 +68,301 @@ class PictServiceFlowLayout extends libFableServiceProviderBase
10
68
 
11
69
  this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
12
70
 
13
- // Layout configuration
71
+ // Legacy spacing fields kept for `autoLayoutSubset` and any
72
+ // external consumer that read them. The Layered algorithm itself
73
+ // pulls spacing from its DefaultParameters.
14
74
  this._HorizontalSpacing = 250;
15
75
  this._VerticalSpacing = 120;
16
76
  this._StartX = 100;
17
77
  this._StartY = 100;
78
+
79
+ // Algorithm registry: Name → descriptor
80
+ this._Algorithms = {};
81
+
82
+ // Edge-theme registry: Name → descriptor
83
+ this._EdgeThemes = {};
84
+
85
+ for (let i = 0; i < _BUILTIN_ALGORITHMS.length; i++)
86
+ {
87
+ this.registerAlgorithm(_BUILTIN_ALGORITHMS[i]);
88
+ }
89
+ for (let i = 0; i < _BUILTIN_EDGE_THEMES.length; i++)
90
+ {
91
+ this.registerEdgeTheme(_BUILTIN_EDGE_THEMES[i]);
92
+ }
18
93
  }
19
94
 
95
+ // ── Algorithm Registry ────────────────────────────────────────────────
96
+
20
97
  /**
21
- * Snap a coordinate to the nearest grid point
22
- * @param {number} pValue - The coordinate value
23
- * @param {number} pGridSize - The grid size
24
- * @returns {number}
98
+ * Register a layout algorithm.
99
+ * @param {Object} pDescriptor - { Name, Label, Apply, DefaultParameters, ParameterSchema }
100
+ * @returns {boolean} true if registered, false if invalid
25
101
  */
26
- snapToGrid(pValue, pGridSize)
102
+ registerAlgorithm(pDescriptor)
27
103
  {
28
- if (!pGridSize || pGridSize <= 0) return pValue;
29
- return Math.round(pValue / pGridSize) * pGridSize;
104
+ if (!pDescriptor || typeof pDescriptor !== 'object')
105
+ {
106
+ this.log.warn('PictServiceFlowLayout registerAlgorithm: descriptor must be an object');
107
+ return false;
108
+ }
109
+ if (typeof pDescriptor.Name !== 'string' || pDescriptor.Name === '')
110
+ {
111
+ this.log.warn('PictServiceFlowLayout registerAlgorithm: descriptor.Name is required');
112
+ return false;
113
+ }
114
+ if (typeof pDescriptor.Apply !== 'function')
115
+ {
116
+ this.log.warn(`PictServiceFlowLayout registerAlgorithm: descriptor.Apply for '${pDescriptor.Name}' must be a function`);
117
+ return false;
118
+ }
119
+
120
+ this._Algorithms[pDescriptor.Name] = pDescriptor;
121
+ return true;
30
122
  }
31
123
 
32
124
  /**
33
- * Auto-layout nodes using a simple left-to-right topological approach
34
- * @param {Array} pNodes - Array of node data objects
35
- * @param {Array} pConnections - Array of connection data objects
125
+ * Look up a registered algorithm by name.
126
+ * @param {string} pName
127
+ * @returns {Object|null}
36
128
  */
37
- autoLayout(pNodes, pConnections)
129
+ getAlgorithm(pName)
38
130
  {
39
- if (!pNodes || pNodes.length === 0) return;
131
+ return this._Algorithms[pName] || null;
132
+ }
40
133
 
41
- // Build adjacency information
42
- let tmpNodeMap = {};
43
- let tmpInDegree = {};
44
- let tmpOutEdges = {};
134
+ /**
135
+ * List all registered algorithm names in registration order.
136
+ * @returns {string[]}
137
+ */
138
+ getAlgorithmNames()
139
+ {
140
+ return Object.keys(this._Algorithms);
141
+ }
45
142
 
46
- for (let i = 0; i < pNodes.length; i++)
143
+ /**
144
+ * List all registered algorithm descriptors.
145
+ * @returns {Object[]}
146
+ */
147
+ listAlgorithms()
148
+ {
149
+ let tmpKeys = Object.keys(this._Algorithms);
150
+ let tmpResult = [];
151
+ for (let i = 0; i < tmpKeys.length; i++)
47
152
  {
48
- let tmpNode = pNodes[i];
49
- tmpNodeMap[tmpNode.Hash] = tmpNode;
50
- tmpInDegree[tmpNode.Hash] = 0;
51
- tmpOutEdges[tmpNode.Hash] = [];
153
+ tmpResult.push(this._Algorithms[tmpKeys[i]]);
52
154
  }
155
+ return tmpResult;
156
+ }
53
157
 
54
- for (let i = 0; i < pConnections.length; i++)
55
- {
56
- let tmpConn = pConnections[i];
57
- if (tmpInDegree.hasOwnProperty(tmpConn.TargetNodeHash))
58
- {
59
- tmpInDegree[tmpConn.TargetNodeHash]++;
60
- }
61
- if (tmpOutEdges.hasOwnProperty(tmpConn.SourceNodeHash))
62
- {
63
- tmpOutEdges[tmpConn.SourceNodeHash].push(tmpConn.TargetNodeHash);
64
- }
65
- }
158
+ /**
159
+ * Build the merged parameter set for an algorithm: default values
160
+ * with caller-supplied overrides on top.
161
+ * @param {string} pName
162
+ * @param {Object} [pOverrides]
163
+ * @returns {Object}
164
+ */
165
+ getMergedParameters(pName, pOverrides)
166
+ {
167
+ let tmpAlgo = this.getAlgorithm(pName);
168
+ if (!tmpAlgo) return Object.assign({}, pOverrides || {});
169
+ return Object.assign({}, tmpAlgo.DefaultParameters || {}, pOverrides || {});
170
+ }
66
171
 
67
- // Topological sort (Kahn's algorithm)
68
- let tmpLayers = [];
69
- let tmpQueue = [];
70
- let tmpAssigned = {};
172
+ // ── Edge-Theme Registry ──────────────────────────────────────────────
71
173
 
72
- // Start with nodes that have no incoming edges
73
- for (let tmpHash in tmpInDegree)
174
+ /**
175
+ * Register an edge theme.
176
+ * @param {Object} pDescriptor - { Name, Label, Description, GeneratePath, AdjustLayout?, DefaultParameters?, ParameterSchema? }
177
+ * @returns {boolean} true if registered, false if invalid
178
+ */
179
+ registerEdgeTheme(pDescriptor)
180
+ {
181
+ if (!pDescriptor || typeof pDescriptor !== 'object')
74
182
  {
75
- if (tmpInDegree[tmpHash] === 0)
76
- {
77
- tmpQueue.push(tmpHash);
78
- }
183
+ this.log.warn('PictServiceFlowLayout registerEdgeTheme: descriptor must be an object');
184
+ return false;
79
185
  }
80
-
81
- while (tmpQueue.length > 0)
186
+ if (typeof pDescriptor.Name !== 'string' || pDescriptor.Name === '')
82
187
  {
83
- let tmpCurrentLayer = [];
188
+ this.log.warn('PictServiceFlowLayout registerEdgeTheme: descriptor.Name is required');
189
+ return false;
190
+ }
191
+ if (typeof pDescriptor.GeneratePath !== 'function')
192
+ {
193
+ this.log.warn(`PictServiceFlowLayout registerEdgeTheme: descriptor.GeneratePath for '${pDescriptor.Name}' must be a function`);
194
+ return false;
195
+ }
196
+ this._EdgeThemes[pDescriptor.Name] = pDescriptor;
197
+ return true;
198
+ }
84
199
 
85
- let tmpNextQueue = [];
86
- for (let i = 0; i < tmpQueue.length; i++)
87
- {
88
- let tmpNodeHash = tmpQueue[i];
89
- if (tmpAssigned[tmpNodeHash]) continue;
200
+ /**
201
+ * Look up a registered edge theme by name.
202
+ * @param {string} pName
203
+ * @returns {Object|null}
204
+ */
205
+ getEdgeTheme(pName)
206
+ {
207
+ return this._EdgeThemes[pName] || null;
208
+ }
90
209
 
91
- tmpAssigned[tmpNodeHash] = true;
92
- tmpCurrentLayer.push(tmpNodeHash);
210
+ /**
211
+ * List all registered edge-theme names in registration order.
212
+ * @returns {string[]}
213
+ */
214
+ getEdgeThemeNames()
215
+ {
216
+ return Object.keys(this._EdgeThemes);
217
+ }
93
218
 
94
- // Process outgoing edges
95
- let tmpEdges = tmpOutEdges[tmpNodeHash] || [];
96
- for (let j = 0; j < tmpEdges.length; j++)
97
- {
98
- let tmpTargetHash = tmpEdges[j];
99
- tmpInDegree[tmpTargetHash]--;
100
- if (tmpInDegree[tmpTargetHash] <= 0 && !tmpAssigned[tmpTargetHash])
101
- {
102
- tmpNextQueue.push(tmpTargetHash);
103
- }
104
- }
105
- }
219
+ /**
220
+ * List all registered edge-theme descriptors.
221
+ * @returns {Object[]}
222
+ */
223
+ listEdgeThemes()
224
+ {
225
+ let tmpKeys = Object.keys(this._EdgeThemes);
226
+ let tmpResult = [];
227
+ for (let i = 0; i < tmpKeys.length; i++)
228
+ {
229
+ tmpResult.push(this._EdgeThemes[tmpKeys[i]]);
230
+ }
231
+ return tmpResult;
232
+ }
106
233
 
107
- if (tmpCurrentLayer.length > 0)
108
- {
109
- tmpLayers.push(tmpCurrentLayer);
110
- }
234
+ /**
235
+ * Resolve which edge theme should render a given connection. Order:
236
+ * 1. `Connection.Data.EdgeTheme` (per-connection override)
237
+ * 2. Per-connection `Connection.Data.LineMode === 'orthogonal'`
238
+ * → 'Orthogonal' (back-compat for the existing line-mode flag)
239
+ * 3. `_FlowData.EdgeTheme` (flow-level)
240
+ * 4. Active layout's `DefaultEdgeTheme`
241
+ * 5. The hard fallback ('Bezier')
242
+ *
243
+ * Returns null only if even the fallback isn't registered, in which
244
+ * case the renderer should use its built-in path generators.
245
+ *
246
+ * @param {Object} pConnection
247
+ * @returns {Object|null}
248
+ */
249
+ resolveActiveEdgeTheme(pConnection)
250
+ {
251
+ let tmpData = (pConnection && pConnection.Data) || {};
252
+ let tmpFlowView = this._FlowView;
253
+ let tmpFlowData = tmpFlowView ? tmpFlowView._FlowData : null;
111
254
 
112
- tmpQueue = tmpNextQueue;
255
+ if (tmpData.EdgeTheme)
256
+ {
257
+ let tmpTheme = this.getEdgeTheme(tmpData.EdgeTheme);
258
+ if (tmpTheme) return tmpTheme;
113
259
  }
114
-
115
- // Handle any remaining unassigned nodes (cycles or disconnected)
116
- let tmpRemainingNodes = [];
117
- for (let i = 0; i < pNodes.length; i++)
260
+ if (tmpData.LineMode === 'orthogonal' && !tmpData.EdgeTheme)
118
261
  {
119
- if (!tmpAssigned[pNodes[i].Hash])
120
- {
121
- tmpRemainingNodes.push(pNodes[i].Hash);
122
- }
262
+ let tmpTheme = this.getEdgeTheme('Orthogonal');
263
+ if (tmpTheme) return tmpTheme;
123
264
  }
124
- if (tmpRemainingNodes.length > 0)
265
+ if (tmpFlowData && tmpFlowData.EdgeTheme)
125
266
  {
126
- tmpLayers.push(tmpRemainingNodes);
267
+ let tmpTheme = this.getEdgeTheme(tmpFlowData.EdgeTheme);
268
+ if (tmpTheme) return tmpTheme;
127
269
  }
128
-
129
- // Assign positions based on layers
130
- let tmpCurrentX = this._StartX;
131
-
132
- for (let tmpLayerIndex = 0; tmpLayerIndex < tmpLayers.length; tmpLayerIndex++)
270
+ if (tmpFlowData && tmpFlowData.LayoutAlgorithm)
133
271
  {
134
- let tmpLayer = tmpLayers[tmpLayerIndex];
135
- let tmpMaxWidth = 0;
136
-
137
- // Calculate the total height for this layer to center vertically
138
- let tmpTotalHeight = 0;
139
- for (let i = 0; i < tmpLayer.length; i++)
272
+ let tmpAlgo = this.getAlgorithm(tmpFlowData.LayoutAlgorithm);
273
+ if (tmpAlgo && tmpAlgo.DefaultEdgeTheme)
140
274
  {
141
- let tmpNode = tmpNodeMap[tmpLayer[i]];
142
- if (tmpNode)
143
- {
144
- tmpTotalHeight += tmpNode.Height || 80;
145
- if (i < tmpLayer.length - 1)
146
- {
147
- tmpTotalHeight += this._VerticalSpacing;
148
- }
149
- }
275
+ let tmpTheme = this.getEdgeTheme(tmpAlgo.DefaultEdgeTheme);
276
+ if (tmpTheme) return tmpTheme;
150
277
  }
278
+ }
279
+ return this.getEdgeTheme(_DEFAULT_EDGE_THEME);
280
+ }
151
281
 
152
- let tmpCurrentY = this._StartY;
282
+ /**
283
+ * Build the merged parameter set for an edge theme.
284
+ * @param {string} pName
285
+ * @param {Object} [pOverrides]
286
+ * @returns {Object}
287
+ */
288
+ getMergedEdgeThemeParameters(pName, pOverrides)
289
+ {
290
+ let tmpTheme = this.getEdgeTheme(pName);
291
+ if (!tmpTheme) return Object.assign({}, pOverrides || {});
292
+ return Object.assign({}, tmpTheme.DefaultParameters || {}, pOverrides || {});
293
+ }
153
294
 
154
- for (let i = 0; i < tmpLayer.length; i++)
155
- {
156
- let tmpNode = tmpNodeMap[tmpLayer[i]];
157
- if (!tmpNode) continue;
295
+ // ── Dispatch ──────────────────────────────────────────────────────────
158
296
 
159
- tmpNode.X = tmpCurrentX;
160
- tmpNode.Y = tmpCurrentY;
297
+ /**
298
+ * Apply a registered layout algorithm. Mutates node X/Y in place.
299
+ * @param {Array} pNodes
300
+ * @param {Array} pConnections
301
+ * @param {string} pAlgorithmName
302
+ * @param {Object} [pParameters]
303
+ */
304
+ applyLayout(pNodes, pConnections, pAlgorithmName, pParameters)
305
+ {
306
+ let tmpAlgo = this.getAlgorithm(pAlgorithmName);
307
+ if (!tmpAlgo)
308
+ {
309
+ this.log.warn(`PictServiceFlowLayout applyLayout: unknown algorithm '${pAlgorithmName}', falling back to '${_LEGACY_ALGORITHM}'`);
310
+ tmpAlgo = this.getAlgorithm(_LEGACY_ALGORITHM);
311
+ if (!tmpAlgo) return;
312
+ }
313
+ let tmpMerged = Object.assign({}, tmpAlgo.DefaultParameters || {}, pParameters || {});
314
+ tmpAlgo.Apply(pNodes, pConnections, tmpMerged);
315
+ }
161
316
 
162
- let tmpWidth = tmpNode.Width || 180;
163
- let tmpHeight = tmpNode.Height || 80;
317
+ /**
318
+ * Auto-layout entry point.
319
+ *
320
+ * - Legacy 2-arg form `autoLayout(nodes, connections)` dispatches to
321
+ * the Layered algorithm and is byte-for-byte identical to the
322
+ * pre-subsystem behavior.
323
+ * - 3- or 4-arg form `autoLayout(nodes, connections, name, params)`
324
+ * dispatches to the named algorithm.
325
+ *
326
+ * @param {Array} pNodes
327
+ * @param {Array} pConnections
328
+ * @param {string} [pAlgorithmName] - defaults to the Layered algorithm
329
+ * @param {Object} [pParameters]
330
+ */
331
+ autoLayout(pNodes, pConnections, pAlgorithmName, pParameters)
332
+ {
333
+ if (!pNodes || pNodes.length === 0) return;
334
+ let tmpName = (typeof pAlgorithmName === 'string' && pAlgorithmName !== '') ? pAlgorithmName : _LEGACY_ALGORITHM;
335
+ this.applyLayout(pNodes, pConnections, tmpName, pParameters);
336
+ }
164
337
 
165
- tmpMaxWidth = Math.max(tmpMaxWidth, tmpWidth);
166
- tmpCurrentY += tmpHeight + this._VerticalSpacing;
167
- }
338
+ // ── Helpers ──────────────────────────────────────────────────────────
168
339
 
169
- tmpCurrentX += tmpMaxWidth + this._HorizontalSpacing;
170
- }
340
+ /**
341
+ * Snap a coordinate to the nearest grid point.
342
+ * @param {number} pValue
343
+ * @param {number} pGridSize
344
+ * @returns {number}
345
+ */
346
+ snapToGrid(pValue, pGridSize)
347
+ {
348
+ if (!pGridSize || pGridSize <= 0) return pValue;
349
+ return Math.round(pValue / pGridSize) * pGridSize;
171
350
  }
172
351
 
173
352
  /**
174
353
  * Auto-layout a subset of nodes, positioning them to the right of
175
354
  * any fixed (already-positioned) nodes.
176
355
  *
177
- * Uses the same topological sort approach as autoLayout, but only
178
- * repositions the nodes in pNodesToLayout. The pFixedNodes are used
179
- * to compute a bounding box that the new layout avoids.
356
+ * Used by the saved-layout restore flow (`PictProvider-Flow-Layouts`)
357
+ * to position nodes that exist in the current flow but were absent
358
+ * from the saved snapshot.
359
+ *
360
+ * **Always runs the Layered algorithm**, regardless of the flow's
361
+ * configured `LayoutAlgorithm`. Running ForcedFromCenter or another
362
+ * algorithm on a partial node set would jitter the matched nodes
363
+ * (which are intentionally fixed). The contract here is "place the
364
+ * orphans in a sensible default arrangement," not "obey the user's
365
+ * full-graph algorithm."
180
366
  *
181
367
  * @param {Array} pNodesToLayout - Nodes that need new positions
182
368
  * @param {Array} pFixedNodes - Nodes that already have positions (read-only)
@@ -203,7 +389,6 @@ class PictServiceFlowLayout extends libFableServiceProviderBase
203
389
  }
204
390
  }
205
391
 
206
- // Place unmatched nodes to the right of all fixed nodes
207
392
  tmpStartX = tmpMaxX + this._HorizontalSpacing;
208
393
  }
209
394
 
@@ -227,7 +412,6 @@ class PictServiceFlowLayout extends libFableServiceProviderBase
227
412
  tmpOutEdges[tmpNode.Hash] = [];
228
413
  }
229
414
 
230
- // Only count edges between nodes in the subset
231
415
  for (let i = 0; i < pConnections.length; i++)
232
416
  {
233
417
  let tmpConn = pConnections[i];
@@ -330,7 +514,7 @@ class PictServiceFlowLayout extends libFableServiceProviderBase
330
514
  }
331
515
 
332
516
  /**
333
- * Center all nodes around a given point
517
+ * Center all nodes around a given point.
334
518
  * @param {Array} pNodes
335
519
  * @param {number} pCenterX
336
520
  * @param {number} pCenterY