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,4 +1,27 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+ const libPerimeterMath = require('../providers/edges/Edge-PerimeterMath.js');
3
+
4
+ // Chip (port-label badge) geometry — must mirror PortRenderer's badge
5
+ // dimensions exactly so hint paths land on the chip's actual outer
6
+ // edge instead of where we *guess* it might be.
7
+ const _CHIP_HEIGHT = 12;
8
+ const _CHIP_PAD_H = 5;
9
+ const _CHIP_EDGE_PAD = 1;
10
+ const _CHIP_PORT_RADIUS = 5;
11
+ const _CHIP_BORDER_WIDTH = 2;
12
+ const _CHIP_PER_CHAR_PX = 5;
13
+
14
+ // Port-type → stroke color map. Mirrors the table PortRenderer uses for
15
+ // badge borders so the hint bezier matches its port's affinity color.
16
+ const PORT_TYPE_COLORS =
17
+ {
18
+ 'event-in': '#3498db',
19
+ 'event-out': '#2ecc71',
20
+ 'setting': '#e67e22',
21
+ 'value': '#f1c40f',
22
+ 'error': '#e74c3c'
23
+ };
24
+ const PORT_TYPE_DEFAULT_COLOR = '#95a5a6';
2
25
 
3
26
  class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
4
27
  {
@@ -24,6 +47,16 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
24
47
  let tmpSourcePos = this._FlowView.getPortPosition(pConnection.SourceNodeHash, pConnection.SourcePortHash);
25
48
  let tmpTargetPos = this._FlowView.getPortPosition(pConnection.TargetNodeHash, pConnection.TargetPortHash);
26
49
 
50
+ // Let the active edge theme reshape the attachment points (e.g. a
51
+ // "perimeter" theme that exits whichever side of the node faces the
52
+ // other end). The static port positions are still computed above so
53
+ // themes that don't override fall through unchanged.
54
+ let tmpStaticSourcePos = tmpSourcePos;
55
+ let tmpStaticTargetPos = tmpTargetPos;
56
+ let tmpAttachOverride = this._resolveAttachmentsViaEdgeTheme(pConnection, tmpSourcePos, tmpTargetPos);
57
+ if (tmpAttachOverride.source) tmpSourcePos = tmpAttachOverride.source;
58
+ if (tmpAttachOverride.target) tmpTargetPos = tmpAttachOverride.target;
59
+
27
60
  // Look up the source port's PortType for connection coloring
28
61
  let tmpSourcePortType = null;
29
62
  let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
@@ -42,34 +75,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
42
75
  if (!tmpSourcePos || !tmpTargetPos) return;
43
76
 
44
77
  let tmpData = pConnection.Data || {};
45
- let tmpLineMode = tmpData.LineMode || 'bezier';
46
- let tmpPath;
47
-
48
- if (tmpLineMode === 'orthogonal')
49
- {
50
- let tmpCorners = null;
51
- if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
52
- {
53
- tmpCorners =
54
- {
55
- corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
56
- corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
57
- };
58
- }
59
- tmpPath = this._generateOrthogonalPath(tmpSourcePos, tmpTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
60
- }
61
- else
62
- {
63
- let tmpHandles = this._getBezierHandles(tmpData);
64
- if (tmpHandles.length > 0)
65
- {
66
- tmpPath = this._generateMultiHandleBezierPath(tmpSourcePos, tmpTargetPos, tmpHandles);
67
- }
68
- else
69
- {
70
- tmpPath = this._generateDirectionalPath(tmpSourcePos, tmpTargetPos);
71
- }
72
- }
78
+ let tmpPath = this._generatePathViaEdgeTheme(pConnection, tmpSourcePos, tmpTargetPos, tmpData);
73
79
 
74
80
  // Apply theme noise post-processing to the path
75
81
  if (this._FlowView._ThemeProvider)
@@ -154,6 +160,26 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
154
160
  pConnectionsLayer.appendChild(tmpPathElement);
155
161
  }
156
162
 
163
+ // Render the colored endpoint dot at each end of the connection
164
+ // into the dedicated endpoints layer (sits *above* the nodes
165
+ // layer so the dot doesn't get hidden under the card chrome
166
+ // when an edge theme places it on the node's perimeter).
167
+ // PortRenderer suppresses its own circle for any port that
168
+ // participates in a connection — we own the dot here.
169
+ let tmpEndpointsLayer = this._FlowView._EndpointsLayer || pConnectionsLayer;
170
+ this._renderEndpointDots(pConnection, tmpEndpointsLayer, tmpSourcePos, tmpTargetPos);
171
+
172
+ // When the resolved attachment differs from the card-defined port
173
+ // position (e.g. a Perimeter theme moved the dot to the edge of the
174
+ // card), paint a hidden hint bezier connecting the badge to the
175
+ // dot. CSS reveals it on hover so users can see "this 'In' chip
176
+ // corresponds to that dot up at the top".
177
+ if (this._FlowView._PortHintsLayer)
178
+ {
179
+ this._renderPortHints(pConnection, this._FlowView._PortHintsLayer,
180
+ tmpStaticSourcePos, tmpSourcePos, tmpStaticTargetPos, tmpTargetPos);
181
+ }
182
+
157
183
  // Render drag handles when selected
158
184
  if (pIsSelected)
159
185
  {
@@ -161,6 +187,284 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
161
187
  }
162
188
  }
163
189
 
190
+ /**
191
+ * Append the colored endpoint dots for both ends of a connection
192
+ * onto the destination layer (absolute coords). Reuses
193
+ * `ConnectorShapesProvider.createPortElement` so the styling
194
+ * matches the static port circle exactly.
195
+ *
196
+ * @param {Object} pConnection
197
+ * @param {SVGGElement} pLayer - the endpoints layer (or fallback)
198
+ * @param {{x: number, y: number}} pSourcePos
199
+ * @param {{x: number, y: number}} pTargetPos
200
+ */
201
+ _renderEndpointDots(pConnection, pLayer, pSourcePos, pTargetPos)
202
+ {
203
+ let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
204
+ if (!tmpShapeProvider) return;
205
+
206
+ let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
207
+ let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
208
+ let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
209
+ let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
210
+
211
+ if (tmpSourcePort && pSourcePos)
212
+ {
213
+ let tmpDot = tmpShapeProvider.createPortElement(tmpSourcePort, pSourcePos, pConnection.SourceNodeHash);
214
+ tmpDot.setAttribute('data-connection-hash', pConnection.Hash);
215
+ tmpDot.setAttribute('data-connection-end', 'source');
216
+ pLayer.appendChild(tmpDot);
217
+ }
218
+ if (tmpTargetPort && pTargetPos)
219
+ {
220
+ let tmpDot = tmpShapeProvider.createPortElement(tmpTargetPort, pTargetPos, pConnection.TargetNodeHash);
221
+ tmpDot.setAttribute('data-connection-hash', pConnection.Hash);
222
+ tmpDot.setAttribute('data-connection-end', 'target');
223
+ pLayer.appendChild(tmpDot);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Render the per-end "where did the dot go" hint paths. Drawn into
229
+ * the port-hints layer (above nodes) but kept opacity:0 by CSS;
230
+ * PortRenderer/NodeView toggle a `data-active` attribute on hover.
231
+ *
232
+ * Skipped when the resolved attachment matches the static port
233
+ * position (no theme rerouting → no need for a hint).
234
+ *
235
+ * @param {Object} pConnection
236
+ * @param {SVGGElement} pLayer
237
+ * @param {{x: number, y: number, side?: string}} pStaticSrc - card-defined port position
238
+ * @param {{x: number, y: number, side?: string}} pSrc - resolved attachment
239
+ * @param {{x: number, y: number, side?: string}} pStaticTgt
240
+ * @param {{x: number, y: number, side?: string}} pTgt
241
+ */
242
+ _renderPortHints(pConnection, pLayer, pStaticSrc, pSrc, pStaticTgt, pTgt)
243
+ {
244
+ let tmpPositionsDiffer = function (pA, pB)
245
+ {
246
+ if (!pA || !pB) return false;
247
+ return (Math.abs(pA.x - pB.x) > 0.5) || (Math.abs(pA.y - pB.y) > 0.5);
248
+ };
249
+
250
+ let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
251
+ let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
252
+ let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
253
+ let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
254
+
255
+ // Hints are colored by the *other* end's node identity — looking
256
+ // at a hub with 8 hints fanning out, each hint is the color of
257
+ // the spoke that connection terminates at, so you can tell at a
258
+ // glance which line goes where without tracing it. (Mirrors the
259
+ // Ultravisor card-category color model: gray=flow-control,
260
+ // purple=core, orange=data, teal=llm, etc.)
261
+ // Anchor each hint at the chip's outer edge facing the dot
262
+ // (not the port's static position, which sits on the node's
263
+ // edge — visually inside the chip's stripe). The hint ends at
264
+ // the dot but its tail "points at" the chip's center.
265
+ if (pStaticSrc && tmpPositionsDiffer(pStaticSrc, pSrc))
266
+ {
267
+ let tmpChipExit = this._chipEdgeAimingAt(tmpSourcePort, pStaticSrc, pSrc);
268
+ pLayer.appendChild(this._buildPortHintPath(
269
+ tmpChipExit, pSrc,
270
+ pConnection.SourceNodeHash, pConnection.SourcePortHash, pConnection.Hash, 'source',
271
+ this._hintColor(tmpSourcePort, tmpTargetNode)));
272
+ }
273
+ if (pStaticTgt && tmpPositionsDiffer(pStaticTgt, pTgt))
274
+ {
275
+ let tmpChipExit = this._chipEdgeAimingAt(tmpTargetPort, pStaticTgt, pTgt);
276
+ pLayer.appendChild(this._buildPortHintPath(
277
+ tmpChipExit, pTgt,
278
+ pConnection.TargetNodeHash, pConnection.TargetPortHash, pConnection.Hash, 'target',
279
+ this._hintColor(tmpTargetPort, tmpSourceNode)));
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Compute the chip (port-label badge) bounding box for a given
285
+ * port. The chip is a small rectangle that lives just inside the
286
+ * node's outer edge, anchored to the port's static position. We
287
+ * mirror PortRenderer's badge geometry exactly so the hint path
288
+ * lands on real chip-edge pixels.
289
+ *
290
+ * Returns null when we don't have enough info (no label or
291
+ * unknown side) — caller should fall back to the static position.
292
+ *
293
+ * @param {Object|null} pPort
294
+ * @param {{x: number, y: number, side?: string}} pStaticPos - the
295
+ * port's nominal position (returned by getPortPosition); the
296
+ * chip extends *inward* from here.
297
+ * @returns {{X: number, Y: number, Width: number, Height: number}|null}
298
+ */
299
+ _chipBoundsFor(pPort, pStaticPos)
300
+ {
301
+ if (!pPort || !pStaticPos) return null;
302
+ let tmpSide = pStaticPos.side || pPort.Side || (pPort.Direction === 'input' ? 'left' : 'right');
303
+ let tmpLabel = pPort.Label || '';
304
+ // PortRenderer skips badge rendering entirely when there is no
305
+ // label, so there's nothing for the hint to anchor against.
306
+ if (tmpLabel === '') return null;
307
+
308
+ let tmpTextPx = tmpLabel.length * _CHIP_PER_CHAR_PX;
309
+ let tmpWidth = _CHIP_PORT_RADIUS + _CHIP_PAD_H + tmpTextPx + _CHIP_PAD_H + _CHIP_BORDER_WIDTH;
310
+ let tmpHeight = _CHIP_HEIGHT;
311
+
312
+ // Chip center sits halfway along the line from the port's static
313
+ // position toward the inside of the node, offset by half the
314
+ // chip's extent in that direction (plus the 1px edge padding
315
+ // PortRenderer leaves between the chip and the node edge).
316
+ let tmpCx, tmpCy;
317
+ if (tmpSide.indexOf('left') === 0)
318
+ {
319
+ tmpCx = pStaticPos.x + tmpWidth / 2 + _CHIP_EDGE_PAD;
320
+ tmpCy = pStaticPos.y;
321
+ }
322
+ else if (tmpSide.indexOf('right') === 0)
323
+ {
324
+ tmpCx = pStaticPos.x - tmpWidth / 2 - _CHIP_EDGE_PAD;
325
+ tmpCy = pStaticPos.y;
326
+ }
327
+ else if (tmpSide.indexOf('top') === 0)
328
+ {
329
+ tmpCx = pStaticPos.x;
330
+ tmpCy = pStaticPos.y + tmpHeight / 2 + _CHIP_EDGE_PAD;
331
+ }
332
+ else if (tmpSide.indexOf('bottom') === 0)
333
+ {
334
+ tmpCx = pStaticPos.x;
335
+ tmpCy = pStaticPos.y - tmpHeight / 2 - _CHIP_EDGE_PAD;
336
+ }
337
+ else
338
+ {
339
+ return null;
340
+ }
341
+
342
+ return { X: tmpCx - tmpWidth / 2, Y: tmpCy - tmpHeight / 2, Width: tmpWidth, Height: tmpHeight };
343
+ }
344
+
345
+ /**
346
+ * Resolve the point on the chip's outer perimeter where a line
347
+ * from the chip's center toward the dot would exit the chip. Used
348
+ * as the hint's start point so the line visually emerges from the
349
+ * chip's edge facing the dot, aimed at the chip's center.
350
+ *
351
+ * Falls back to the static port position when chip geometry can't
352
+ * be computed (no label, unknown side).
353
+ *
354
+ * @param {Object|null} pPort
355
+ * @param {{x: number, y: number, side?: string}} pStaticPos
356
+ * @param {{x: number, y: number}} pDotPos
357
+ * @returns {{x: number, y: number}}
358
+ */
359
+ _chipEdgeAimingAt(pPort, pStaticPos, pDotPos)
360
+ {
361
+ let tmpBounds = this._chipBoundsFor(pPort, pStaticPos);
362
+ if (!tmpBounds) return pStaticPos;
363
+ let tmpExit = libPerimeterMath.resolvePerimeterAttachment({
364
+ Node: tmpBounds,
365
+ OtherDefaultPosition: pDotPos
366
+ });
367
+ if (!tmpExit) return pStaticPos;
368
+ return tmpExit;
369
+ }
370
+
371
+ /**
372
+ * Resolve a node's identity color. Looks at the inline node fields
373
+ * first (per-node overrides win), then the registered node-type
374
+ * config from the NodeTypeProvider. Returns null when the node has
375
+ * no opinion — caller should fall back to port-type color.
376
+ *
377
+ * @param {Object|null} pNode
378
+ * @returns {string|null}
379
+ */
380
+ _resolveNodeIdentityColor(pNode)
381
+ {
382
+ if (!pNode) return null;
383
+ if (pNode.TitleBarColor) return pNode.TitleBarColor;
384
+ if (pNode.BodyStyle && pNode.BodyStyle.stroke) return pNode.BodyStyle.stroke;
385
+ let tmpProvider = this._FlowView ? this._FlowView._NodeTypeProvider : null;
386
+ if (tmpProvider && typeof tmpProvider.getNodeType === 'function' && pNode.Type)
387
+ {
388
+ let tmpType = tmpProvider.getNodeType(pNode.Type);
389
+ if (tmpType)
390
+ {
391
+ if (tmpType.TitleBarColor) return tmpType.TitleBarColor;
392
+ if (tmpType.BodyStyle && tmpType.BodyStyle.stroke) return tmpType.BodyStyle.stroke;
393
+ }
394
+ }
395
+ return null;
396
+ }
397
+
398
+ /**
399
+ * Resolve the hint stroke color. Priority chain:
400
+ * 1. The OTHER end's node identity color (TitleBarColor /
401
+ * BodyStyle.stroke / NodeTypeConfig color) — the strongest
402
+ * signal: "this hint terminates at *that* card".
403
+ * 2. This end's PortType color — semantic-role fallback.
404
+ * 3. Direction-based default that matches the visible dot color.
405
+ * 4. Neutral gray.
406
+ *
407
+ * @param {Object|null} pPort - this end's port
408
+ * @param {Object|null} pOtherNode - the node at the other end
409
+ * @returns {string}
410
+ */
411
+ _hintColor(pPort, pOtherNode)
412
+ {
413
+ let tmpOtherColor = this._resolveNodeIdentityColor(pOtherNode);
414
+ if (tmpOtherColor) return tmpOtherColor;
415
+ if (pPort && pPort.PortType && PORT_TYPE_COLORS[pPort.PortType])
416
+ {
417
+ return PORT_TYPE_COLORS[pPort.PortType];
418
+ }
419
+ if (pPort && pPort.Direction === 'input') return PORT_TYPE_COLORS['event-in'];
420
+ if (pPort && pPort.Direction === 'output') return PORT_TYPE_COLORS['event-out'];
421
+ return PORT_TYPE_DEFAULT_COLOR;
422
+ }
423
+
424
+ /**
425
+ * Build a single hint-path SVG element: a soft S-curve from the
426
+ * card-defined port position to the resolved attachment position.
427
+ * Tagged with port + node + connection hashes so hover handlers can
428
+ * find the right hint by attribute.
429
+ *
430
+ * @param {{x: number, y: number}} pStart - badge / chip position
431
+ * @param {{x: number, y: number}} pEnd - actual dot position
432
+ * @returns {SVGPathElement}
433
+ */
434
+ _buildPortHintPath(pStart, pEnd, pNodeHash, pPortHash, pConnectionHash, pEndLabel, pStrokeColor)
435
+ {
436
+ let tmpEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
437
+
438
+ // Soft S-curve: control points pulled along the perpendicular bisector
439
+ let tmpDX = pEnd.x - pStart.x;
440
+ let tmpDY = pEnd.y - pStart.y;
441
+ let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
442
+ let tmpBend = Math.min(60, tmpDist * 0.35);
443
+ // Perpendicular unit vector
444
+ let tmpPX = (tmpDist > 0) ? -tmpDY / tmpDist : 0;
445
+ let tmpPY = (tmpDist > 0) ? tmpDX / tmpDist : 0;
446
+ let tmpCp1X = pStart.x + tmpDX * 0.35 + tmpPX * tmpBend * 0.3;
447
+ let tmpCp1Y = pStart.y + tmpDY * 0.35 + tmpPY * tmpBend * 0.3;
448
+ let tmpCp2X = pStart.x + tmpDX * 0.65 + tmpPX * tmpBend * 0.3;
449
+ let tmpCp2Y = pStart.y + tmpDY * 0.65 + tmpPY * tmpBend * 0.3;
450
+ let tmpD = `M ${pStart.x} ${pStart.y} C ${tmpCp1X} ${tmpCp1Y} ${tmpCp2X} ${tmpCp2Y} ${pEnd.x} ${pEnd.y}`;
451
+
452
+ tmpEl.setAttribute('class', 'pict-flow-port-hint');
453
+ tmpEl.setAttribute('d', tmpD);
454
+ tmpEl.setAttribute('data-node-hash', pNodeHash);
455
+ tmpEl.setAttribute('data-port-hash', pPortHash);
456
+ tmpEl.setAttribute('data-connection-hash', pConnectionHash);
457
+ tmpEl.setAttribute('data-connection-end', pEndLabel);
458
+ // Affinity color from port type (matches the badge border color).
459
+ // Inline so PortRenderer's color map stays the single source of
460
+ // truth without dragging CSS variables around per port type.
461
+ if (pStrokeColor)
462
+ {
463
+ tmpEl.setAttribute('stroke', pStrokeColor);
464
+ }
465
+ return tmpEl;
466
+ }
467
+
164
468
  /**
165
469
  * Compute the departure and approach points plus control points
166
470
  * for a direction-aware bezier between two ports.
@@ -206,6 +510,206 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
206
510
  );
207
511
  }
208
512
 
513
+ /**
514
+ * Dispatch to an edge theme to generate the path string. Falls back
515
+ * to the renderer's built-in bezier / orthogonal helpers when no
516
+ * edge-theme registry is available (older host integrations) or
517
+ * the resolved theme errors out.
518
+ *
519
+ * @param {Object} pConnection
520
+ * @param {{x: number, y: number, side?: string}} pSourcePos
521
+ * @param {{x: number, y: number, side?: string}} pTargetPos
522
+ * @param {Object} pData - connection data (LineMode, custom handles, etc.)
523
+ * @returns {string} SVG path 'd' attribute
524
+ */
525
+ _generatePathViaEdgeTheme(pConnection, pSourcePos, pTargetPos, pData)
526
+ {
527
+ let tmpLayoutService = this._FlowView ? this._FlowView._LayoutService : null;
528
+ let tmpTheme = (tmpLayoutService && typeof tmpLayoutService.resolveActiveEdgeTheme === 'function')
529
+ ? tmpLayoutService.resolveActiveEdgeTheme(pConnection)
530
+ : null;
531
+
532
+ if (!tmpTheme || typeof tmpTheme.GeneratePath !== 'function')
533
+ {
534
+ return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
535
+ }
536
+
537
+ let tmpContext =
538
+ {
539
+ Source: pSourcePos,
540
+ Target: pTargetPos,
541
+ Connection: pConnection,
542
+ AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
543
+ AllConnections: this._FlowView._FlowData ? this._FlowView._FlowData.Connections : [],
544
+ FlowView: this._FlowView,
545
+ Helpers: this._buildEdgeThemeHelpers(),
546
+ Parameters: tmpLayoutService.getMergedEdgeThemeParameters(
547
+ tmpTheme.Name,
548
+ (this._FlowView._FlowData && this._FlowView._FlowData.EdgeThemeParameters) || {}
549
+ )
550
+ };
551
+
552
+ try
553
+ {
554
+ let tmpPath = tmpTheme.GeneratePath(tmpContext);
555
+ if (typeof tmpPath !== 'string' || tmpPath === '')
556
+ {
557
+ return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
558
+ }
559
+ return tmpPath;
560
+ }
561
+ catch (pError)
562
+ {
563
+ this._FlowView.log.warn(`PictServiceFlowConnectionRenderer edge theme '${tmpTheme.Name}' threw: ${pError.message}`);
564
+ return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Resolve per-end attachment overrides via the active edge theme's
570
+ * optional `ResolveAttachment(context)` hook. The theme can return
571
+ * `{ x, y, side }` for either end (or null to fall through). Used by
572
+ * themes like Perimeter that route through the node's bounding box
573
+ * rather than the static port position.
574
+ *
575
+ * @param {Object} pConnection
576
+ * @param {{x: number, y: number, side?: string}} pSourcePos - default port position
577
+ * @param {{x: number, y: number, side?: string}} pTargetPos - default port position
578
+ * @returns {{source: ?Object, target: ?Object}}
579
+ */
580
+ _resolveAttachmentsViaEdgeTheme(pConnection, pSourcePos, pTargetPos)
581
+ {
582
+ let tmpEmpty = { source: null, target: null };
583
+ let tmpLayoutService = this._FlowView ? this._FlowView._LayoutService : null;
584
+ let tmpTheme = (tmpLayoutService && typeof tmpLayoutService.resolveActiveEdgeTheme === 'function')
585
+ ? tmpLayoutService.resolveActiveEdgeTheme(pConnection)
586
+ : null;
587
+ if (!tmpTheme || typeof tmpTheme.ResolveAttachment !== 'function') return tmpEmpty;
588
+
589
+ let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
590
+ let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
591
+ let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
592
+ let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
593
+
594
+ let tmpParams = tmpLayoutService.getMergedEdgeThemeParameters(
595
+ tmpTheme.Name,
596
+ (this._FlowView._FlowData && this._FlowView._FlowData.EdgeThemeParameters) || {}
597
+ );
598
+
599
+ let tmpSrc = null, tmpTgt = null;
600
+ try
601
+ {
602
+ tmpSrc = tmpTheme.ResolveAttachment({
603
+ Node: tmpSourceNode, Port: tmpSourcePort,
604
+ DefaultPosition: pSourcePos,
605
+ Connection: pConnection, IsSource: true,
606
+ OtherNode: tmpTargetNode, OtherPort: tmpTargetPort,
607
+ OtherDefaultPosition: pTargetPos,
608
+ AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
609
+ FlowView: this._FlowView,
610
+ Parameters: tmpParams
611
+ });
612
+ tmpTgt = tmpTheme.ResolveAttachment({
613
+ Node: tmpTargetNode, Port: tmpTargetPort,
614
+ DefaultPosition: pTargetPos,
615
+ Connection: pConnection, IsSource: false,
616
+ OtherNode: tmpSourceNode, OtherPort: tmpSourcePort,
617
+ OtherDefaultPosition: pSourcePos,
618
+ AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
619
+ FlowView: this._FlowView,
620
+ Parameters: tmpParams
621
+ });
622
+ }
623
+ catch (pError)
624
+ {
625
+ this._FlowView.log.warn(`PictServiceFlowConnectionRenderer edge theme '${tmpTheme.Name}' ResolveAttachment threw: ${pError.message}`);
626
+ return tmpEmpty;
627
+ }
628
+
629
+ return { source: tmpSrc || null, target: tmpTgt || null };
630
+ }
631
+
632
+ /**
633
+ * Find a port by hash on a node. Returns null if missing.
634
+ * @param {Object} pNode
635
+ * @param {string} pPortHash
636
+ * @returns {Object|null}
637
+ */
638
+ _findPort(pNode, pPortHash)
639
+ {
640
+ if (!pNode || !Array.isArray(pNode.Ports)) return null;
641
+ for (let i = 0; i < pNode.Ports.length; i++)
642
+ {
643
+ if (pNode.Ports[i].Hash === pPortHash) return pNode.Ports[i];
644
+ }
645
+ return null;
646
+ }
647
+
648
+ /**
649
+ * Path-generation helpers exposed to edge themes via the GeneratePath
650
+ * context object. Themes can compose these to derive their own routing.
651
+ *
652
+ * @returns {Object}
653
+ */
654
+ _buildEdgeThemeHelpers()
655
+ {
656
+ let tmpRenderer = this;
657
+ return {
658
+ generateBezier: function (pStart, pEnd)
659
+ {
660
+ return tmpRenderer._generateDirectionalPath(pStart, pEnd);
661
+ },
662
+ generateMultiBezier: function (pStart, pEnd, pHandles)
663
+ {
664
+ return tmpRenderer._generateMultiHandleBezierPath(pStart, pEnd, pHandles);
665
+ },
666
+ generateOrthogonal: function (pStart, pEnd, pCorners, pMidOffset)
667
+ {
668
+ return tmpRenderer._generateOrthogonalPath(pStart, pEnd, pCorners || null, pMidOffset || 0);
669
+ },
670
+ getBezierHandles: function (pData)
671
+ {
672
+ return tmpRenderer._getBezierHandles(pData);
673
+ }
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Built-in fallback when no edge theme is registered. Mirrors the
679
+ * pre-edge-theme dispatch (LineMode → bezier or orthogonal).
680
+ *
681
+ * @param {{x: number, y: number, side?: string}} pSourcePos
682
+ * @param {{x: number, y: number, side?: string}} pTargetPos
683
+ * @param {Object} pData
684
+ * @returns {string}
685
+ */
686
+ _builtInPathFallback(pSourcePos, pTargetPos, pData)
687
+ {
688
+ let tmpData = pData || {};
689
+ let tmpLineMode = tmpData.LineMode || 'bezier';
690
+
691
+ if (tmpLineMode === 'orthogonal')
692
+ {
693
+ let tmpCorners = null;
694
+ if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
695
+ {
696
+ tmpCorners =
697
+ {
698
+ corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
699
+ corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
700
+ };
701
+ }
702
+ return this._generateOrthogonalPath(pSourcePos, pTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
703
+ }
704
+
705
+ let tmpHandles = this._getBezierHandles(tmpData);
706
+ if (tmpHandles.length > 0)
707
+ {
708
+ return this._generateMultiHandleBezierPath(pSourcePos, pTargetPos, tmpHandles);
709
+ }
710
+ return this._generateDirectionalPath(pSourcePos, pTargetPos);
711
+ }
712
+
209
713
  /**
210
714
  * Get the bezier handles array from connection data, with backward
211
715
  * compatibility for the legacy BezierHandleX/Y single-handle format.
@@ -94,6 +94,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
94
94
  return;
95
95
  }
96
96
 
97
+ let tmpDefaultAlgorithm = (this._FlowView.options && this._FlowView.options.DefaultLayoutAlgorithm) || 'Custom';
98
+ let tmpDefaultParameters = (this._FlowView.options && this._FlowView.options.DefaultLayoutParameters) || {};
99
+ let tmpDefaultAutoApply = !!(this._FlowView.options && this._FlowView.options.DefaultLayoutAutoApply);
100
+ let tmpDefaultEdgeTheme = (this._FlowView.options && this._FlowView.options.DefaultEdgeTheme) || null;
101
+ let tmpDefaultEdgeThemeParameters = (this._FlowView.options && this._FlowView.options.DefaultEdgeThemeParameters) || {};
102
+
97
103
  this._FlowView._FlowData = {
98
104
  Nodes: Array.isArray(pFlowData.Nodes) ? pFlowData.Nodes : [],
99
105
  Connections: Array.isArray(pFlowData.Connections) ? pFlowData.Connections : [],
@@ -102,7 +108,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
102
108
  ViewState: Object.assign(
103
109
  { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null, SelectedTetherHash: null },
104
110
  pFlowData.ViewState || {}
105
- )
111
+ ),
112
+ LayoutAlgorithm: (typeof pFlowData.LayoutAlgorithm === 'string' && pFlowData.LayoutAlgorithm !== '') ? pFlowData.LayoutAlgorithm : tmpDefaultAlgorithm,
113
+ LayoutParameters: (pFlowData.LayoutParameters && typeof pFlowData.LayoutParameters === 'object') ? JSON.parse(JSON.stringify(pFlowData.LayoutParameters)) : JSON.parse(JSON.stringify(tmpDefaultParameters)),
114
+ LayoutAutoApply: (typeof pFlowData.LayoutAutoApply === 'boolean') ? pFlowData.LayoutAutoApply : tmpDefaultAutoApply,
115
+ EdgeTheme: (typeof pFlowData.EdgeTheme === 'string' && pFlowData.EdgeTheme !== '') ? pFlowData.EdgeTheme : tmpDefaultEdgeTheme,
116
+ EdgeThemeParameters: (pFlowData.EdgeThemeParameters && typeof pFlowData.EdgeThemeParameters === 'object') ? JSON.parse(JSON.stringify(pFlowData.EdgeThemeParameters)) : JSON.parse(JSON.stringify(tmpDefaultEdgeThemeParameters))
106
117
  };
107
118
 
108
119
  // Merge any browser-persisted layouts into the newly loaded data