pict-section-flow 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.claude/launch.json +1 -1
  2. package/README.md +176 -0
  3. package/docs/.nojekyll +0 -0
  4. package/docs/Architecture.md +303 -0
  5. package/docs/Custom-Styling.md +275 -0
  6. package/docs/Data_Model.md +158 -0
  7. package/docs/Event_System.md +156 -0
  8. package/docs/Getting_Started.md +237 -0
  9. package/docs/Implementation_Reference.md +528 -0
  10. package/docs/Layout_Persistence.md +117 -0
  11. package/docs/README.md +115 -52
  12. package/docs/_cover.md +11 -0
  13. package/docs/_sidebar.md +52 -0
  14. package/docs/_topbar.md +8 -0
  15. package/docs/api/PictFlowCard.md +216 -0
  16. package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
  17. package/docs/api/addConnection.md +101 -0
  18. package/docs/api/addNode.md +137 -0
  19. package/docs/api/autoLayout.md +77 -0
  20. package/docs/api/getFlowData.md +112 -0
  21. package/docs/api/marshalToView.md +95 -0
  22. package/docs/api/openPanel.md +128 -0
  23. package/docs/api/registerHandler.md +174 -0
  24. package/docs/api/registerNodeType.md +142 -0
  25. package/docs/api/removeConnection.md +57 -0
  26. package/docs/api/removeNode.md +80 -0
  27. package/docs/api/saveLayout.md +152 -0
  28. package/docs/api/screenToSVGCoords.md +68 -0
  29. package/docs/api/selectNode.md +116 -0
  30. package/docs/api/setTheme.md +168 -0
  31. package/docs/api/setZoom.md +97 -0
  32. package/docs/api/toggleFullscreen.md +68 -0
  33. package/docs/card-help/EACH.md +19 -0
  34. package/docs/card-help/FREAD.md +24 -0
  35. package/docs/card-help/FWRITE.md +24 -0
  36. package/docs/card-help/GET.md +22 -0
  37. package/docs/card-help/ITE.md +23 -0
  38. package/docs/card-help/LOG.md +23 -0
  39. package/docs/card-help/NOTE.md +17 -0
  40. package/docs/card-help/PREV.md +18 -0
  41. package/docs/card-help/SET.md +27 -0
  42. package/docs/card-help/SPKL.md +22 -0
  43. package/docs/card-help/STAT.md +23 -0
  44. package/docs/card-help/SW.md +25 -0
  45. package/docs/css/docuserve.css +73 -0
  46. package/docs/index.html +39 -0
  47. package/docs/retold-catalog.json +169 -0
  48. package/docs/retold-keyword-index.json +13942 -0
  49. package/example_applications/simple_cards/package.json +1 -0
  50. package/example_applications/simple_cards/source/card-help-content.js +16 -0
  51. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
  52. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
  53. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
  54. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
  55. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
  56. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
  57. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
  58. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
  59. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
  60. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
  61. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
  62. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
  63. package/package.json +11 -7
  64. package/scripts/generate-card-help.js +214 -0
  65. package/source/Pict-Section-Flow.js +4 -0
  66. package/source/PictFlowCard.js +3 -1
  67. package/source/providers/PictProvider-Flow-CSS.js +245 -152
  68. package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
  69. package/source/providers/PictProvider-Flow-Geometry.js +195 -38
  70. package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
  71. package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
  72. package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
  73. package/source/services/PictService-Flow-DataManager.js +338 -0
  74. package/source/services/PictService-Flow-InteractionManager.js +165 -7
  75. package/source/services/PictService-Flow-PathGenerator.js +282 -0
  76. package/source/services/PictService-Flow-PortRenderer.js +269 -0
  77. package/source/services/PictService-Flow-RenderManager.js +281 -0
  78. package/source/services/PictService-Flow-Tether.js +6 -42
  79. package/source/views/PictView-Flow-Node.js +2 -220
  80. package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
  81. package/source/views/PictView-Flow.js +130 -882
  82. package/test/ConnectionHandleManager_tests.js +717 -0
  83. package/test/ConnectionRenderer_tests.js +591 -0
  84. package/test/DataManager_tests.js +859 -0
  85. package/test/Geometry_tests.js +767 -0
  86. package/test/PathGenerator_tests.js +978 -0
  87. package/test/PortRenderer_tests.js +367 -0
  88. package/test/RenderManager_tests.js +756 -0
@@ -178,6 +178,105 @@ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
178
178
  return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} C ${pCP1a.x} ${pCP1a.y}, ${pCP1b.x} ${pCP1b.y}, ${pHandle.x} ${pHandle.y} C ${pCP2a.x} ${pCP2a.y}, ${pCP2b.x} ${pCP2b.y}, ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
179
179
  }
180
180
 
181
+ /**
182
+ * Build an SVG multi-segment bezier path string.
183
+ * Generates N+1 cubic bezier segments through N handle points.
184
+ *
185
+ * Pattern: M start L depart C cp,cp,handle[0] C cp,cp,handle[1] ... C cp,cp,approach L end
186
+ *
187
+ * Control points are computed using Catmull-Rom-to-Bezier conversion
188
+ * for C1 (smooth tangent) continuity at every handle.
189
+ *
190
+ * @param {{x: number, y: number}} pStart - Port anchor start
191
+ * @param {{x: number, y: number}} pDepart - Departure point after straight segment
192
+ * @param {Array<{x: number, y: number}>} pHandles - Ordered handle waypoints
193
+ * @param {{x: number, y: number}} pApproach - Approach point before final straight segment
194
+ * @param {{x: number, y: number}} pEnd - Port anchor end
195
+ * @param {{dx: number, dy: number}} pStartDir - Departure direction unit vector
196
+ * @param {{dx: number, dy: number}} pEndDir - Approach direction unit vector
197
+ * @returns {string} SVG path d attribute
198
+ */
199
+ buildMultiBezierPathString(pStart, pDepart, pHandles, pApproach, pEnd, pStartDir, pEndDir)
200
+ {
201
+ // Build the full list of waypoints: depart, handle[0..N-1], approach
202
+ let tmpWaypoints = [pDepart];
203
+ for (let i = 0; i < pHandles.length; i++)
204
+ {
205
+ tmpWaypoints.push(pHandles[i]);
206
+ }
207
+ tmpWaypoints.push(pApproach);
208
+
209
+ let tmpPath = `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y}`;
210
+
211
+ for (let i = 0; i < tmpWaypoints.length - 1; i++)
212
+ {
213
+ let tmpFrom = tmpWaypoints[i];
214
+ let tmpTo = tmpWaypoints[i + 1];
215
+
216
+ let tmpSegDX = tmpTo.x - tmpFrom.x;
217
+ let tmpSegDY = tmpTo.y - tmpFrom.y;
218
+ let tmpSegLen = Math.sqrt(tmpSegDX * tmpSegDX + tmpSegDY * tmpSegDY);
219
+ if (tmpSegLen < 1)
220
+ {
221
+ tmpSegLen = 1;
222
+ }
223
+ let tmpScale = tmpSegLen * 0.35;
224
+
225
+ // Tangent at tmpFrom
226
+ let tmpTanFromX, tmpTanFromY;
227
+ if (i === 0)
228
+ {
229
+ // First segment: use the port departure direction
230
+ tmpTanFromX = pStartDir.dx;
231
+ tmpTanFromY = pStartDir.dy;
232
+ }
233
+ else
234
+ {
235
+ // Interior handle: tangent points from previous toward next waypoint
236
+ let tmpPrev = tmpWaypoints[i - 1];
237
+ let tmpNext = tmpWaypoints[i + 1];
238
+ tmpTanFromX = tmpNext.x - tmpPrev.x;
239
+ tmpTanFromY = tmpNext.y - tmpPrev.y;
240
+ let tmpTanLen = Math.sqrt(tmpTanFromX * tmpTanFromX + tmpTanFromY * tmpTanFromY);
241
+ if (tmpTanLen < 1) tmpTanLen = 1;
242
+ tmpTanFromX /= tmpTanLen;
243
+ tmpTanFromY /= tmpTanLen;
244
+ }
245
+
246
+ // Tangent at tmpTo
247
+ let tmpTanToX, tmpTanToY;
248
+ if (i === tmpWaypoints.length - 2)
249
+ {
250
+ // Last segment: use the port approach direction (reversed for incoming)
251
+ tmpTanToX = -pEndDir.dx;
252
+ tmpTanToY = -pEndDir.dy;
253
+ }
254
+ else
255
+ {
256
+ // Interior handle: tangent points from previous toward next waypoint
257
+ let tmpPrev = tmpWaypoints[i];
258
+ let tmpNext = tmpWaypoints[i + 2];
259
+ tmpTanToX = tmpNext.x - tmpPrev.x;
260
+ tmpTanToY = tmpNext.y - tmpPrev.y;
261
+ let tmpTanLen = Math.sqrt(tmpTanToX * tmpTanToX + tmpTanToY * tmpTanToY);
262
+ if (tmpTanLen < 1) tmpTanLen = 1;
263
+ tmpTanToX /= tmpTanLen;
264
+ tmpTanToY /= tmpTanLen;
265
+ }
266
+
267
+ let tmpCP1X = tmpFrom.x + tmpTanFromX * tmpScale;
268
+ let tmpCP1Y = tmpFrom.y + tmpTanFromY * tmpScale;
269
+ let tmpCP2X = tmpTo.x - tmpTanToX * tmpScale;
270
+ let tmpCP2Y = tmpTo.y - tmpTanToY * tmpScale;
271
+
272
+ tmpPath += ` C ${tmpCP1X} ${tmpCP1Y}, ${tmpCP2X} ${tmpCP2Y}, ${tmpTo.x} ${tmpTo.y}`;
273
+ }
274
+
275
+ tmpPath += ` L ${pEnd.x} ${pEnd.y}`;
276
+
277
+ return tmpPath;
278
+ }
279
+
181
280
  /**
182
281
  * Build an SVG orthogonal (right-angle) path string.
183
282
  * Pattern: M start L depart L corner1 L corner2 L approach L end
@@ -194,6 +293,189 @@ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
194
293
  {
195
294
  return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} L ${pCorner1.x} ${pCorner1.y} L ${pCorner2.x} ${pCorner2.y} L ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
196
295
  }
296
+
297
+ // ---- Directional Geometry ----
298
+
299
+ /**
300
+ * Compute full directional geometry between two port anchors, including
301
+ * departure/approach points and bezier control points.
302
+ *
303
+ * Uses sophisticated facing detection: when ports face each other the
304
+ * curve offset scales with inline distance; when ports are on the same
305
+ * axis but not facing, a wider offset prevents the path from collapsing;
306
+ * perpendicular exits use a moderate offset.
307
+ *
308
+ * @param {{x: number, y: number, side: string}} pStart
309
+ * @param {{x: number, y: number, side: string}} pEnd
310
+ * @returns {{departX: number, departY: number, approachX: number, approachY: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, startDir: {dx: number, dy: number}, endDir: {dx: number, dy: number}}}
311
+ */
312
+ computeDirectionalGeometry(pStart, pEnd)
313
+ {
314
+ let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
315
+ let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
316
+
317
+ let tmpStraightLen = 20;
318
+
319
+ let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
320
+ let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
321
+
322
+ let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
323
+ let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
324
+
325
+ let tmpDX = Math.abs(tmpApproachX - tmpDepartX);
326
+ let tmpDY = Math.abs(tmpApproachY - tmpDepartY);
327
+ let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
328
+
329
+ let tmpBaseOffset = Math.max(Math.min(tmpDist * 0.4, 180), 30);
330
+
331
+ let tmpSameAxis = (tmpStartDir.dx !== 0 && tmpEndDir.dx !== 0) ||
332
+ (tmpStartDir.dy !== 0 && tmpEndDir.dy !== 0);
333
+
334
+ let tmpFacingEachOther = false;
335
+ if (tmpSameAxis)
336
+ {
337
+ if (tmpStartDir.dx === 1 && tmpEndDir.dx === -1 && pEnd.x >= pStart.x)
338
+ {
339
+ tmpFacingEachOther = true;
340
+ }
341
+ else if (tmpStartDir.dx === -1 && tmpEndDir.dx === 1 && pEnd.x <= pStart.x)
342
+ {
343
+ tmpFacingEachOther = true;
344
+ }
345
+ else if (tmpStartDir.dy === 1 && tmpEndDir.dy === -1 && pEnd.y >= pStart.y)
346
+ {
347
+ tmpFacingEachOther = true;
348
+ }
349
+ else if (tmpStartDir.dy === -1 && tmpEndDir.dy === 1 && pEnd.y <= pStart.y)
350
+ {
351
+ tmpFacingEachOther = true;
352
+ }
353
+ }
354
+
355
+ let tmpCurveOffset;
356
+
357
+ if (tmpFacingEachOther)
358
+ {
359
+ let tmpInlineDist = (tmpStartDir.dx !== 0) ? tmpDX : tmpDY;
360
+ tmpCurveOffset = Math.max(tmpInlineDist * 0.35, 30);
361
+ }
362
+ else if (tmpSameAxis)
363
+ {
364
+ tmpCurveOffset = Math.max(tmpBaseOffset, 60);
365
+ }
366
+ else
367
+ {
368
+ tmpCurveOffset = Math.max(tmpBaseOffset * 0.8, 40);
369
+ }
370
+
371
+ let tmpCP1X = tmpDepartX + tmpStartDir.dx * tmpCurveOffset;
372
+ let tmpCP1Y = tmpDepartY + tmpStartDir.dy * tmpCurveOffset;
373
+ let tmpCP2X = tmpApproachX + tmpEndDir.dx * tmpCurveOffset;
374
+ let tmpCP2Y = tmpApproachY + tmpEndDir.dy * tmpCurveOffset;
375
+
376
+ return {
377
+ departX: tmpDepartX, departY: tmpDepartY,
378
+ approachX: tmpApproachX, approachY: tmpApproachY,
379
+ cp1X: tmpCP1X, cp1Y: tmpCP1Y,
380
+ cp2X: tmpCP2X, cp2Y: tmpCP2Y,
381
+ startDir: tmpStartDir, endDir: tmpEndDir
382
+ };
383
+ }
384
+
385
+ // ---- Distance Utilities ----
386
+
387
+ /**
388
+ * Distance from point (pPX, pPY) to line segment (pAX, pAY)-(pBX, pBY).
389
+ * Pure math utility, no state.
390
+ *
391
+ * @param {number} pPX
392
+ * @param {number} pPY
393
+ * @param {number} pAX
394
+ * @param {number} pAY
395
+ * @param {number} pBX
396
+ * @param {number} pBY
397
+ * @returns {number}
398
+ */
399
+ distanceToSegment(pPX, pPY, pAX, pAY, pBX, pBY)
400
+ {
401
+ let tmpDX = pBX - pAX;
402
+ let tmpDY = pBY - pAY;
403
+ let tmpLenSq = tmpDX * tmpDX + tmpDY * tmpDY;
404
+
405
+ if (tmpLenSq < 0.001)
406
+ {
407
+ // Degenerate segment
408
+ let tmpDPX = pPX - pAX;
409
+ let tmpDPY = pPY - pAY;
410
+ return Math.sqrt(tmpDPX * tmpDPX + tmpDPY * tmpDPY);
411
+ }
412
+
413
+ // Project point onto segment, clamped to [0, 1]
414
+ let tmpT = ((pPX - pAX) * tmpDX + (pPY - pAY) * tmpDY) / tmpLenSq;
415
+ if (tmpT < 0) tmpT = 0;
416
+ if (tmpT > 1) tmpT = 1;
417
+
418
+ let tmpClosestX = pAX + tmpT * tmpDX;
419
+ let tmpClosestY = pAY + tmpT * tmpDY;
420
+ let tmpDistX = pPX - tmpClosestX;
421
+ let tmpDistY = pPY - tmpClosestY;
422
+ return Math.sqrt(tmpDistX * tmpDistX + tmpDistY * tmpDistY);
423
+ }
424
+
425
+ // ---- Auto Midpoint Calculation ----
426
+
427
+ /**
428
+ * Get the auto-calculated midpoint of the default bezier curve between
429
+ * two port anchors, using the full directional geometry (facing detection,
430
+ * adaptive curve offsets). Evaluates the cubic bezier at t=0.5.
431
+ *
432
+ * Used by ConnectionRenderer for connection midpoints.
433
+ *
434
+ * @param {{x: number, y: number, side: string}} pStart
435
+ * @param {{x: number, y: number, side: string}} pEnd
436
+ * @returns {{x: number, y: number}}
437
+ */
438
+ getAutoMidpoint(pStart, pEnd)
439
+ {
440
+ let tmpGeo = this.computeDirectionalGeometry(pStart, pEnd);
441
+
442
+ return this.evaluateCubicBezier(
443
+ { x: tmpGeo.departX, y: tmpGeo.departY },
444
+ { x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
445
+ { x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
446
+ { x: tmpGeo.approachX, y: tmpGeo.approachY },
447
+ 0.5
448
+ );
449
+ }
450
+
451
+ /**
452
+ * Get the auto-calculated midpoint using simple span-based control points.
453
+ * Uses computeDepartApproach for basic geometry, then span * 0.4 for
454
+ * control point distance. Evaluates the cubic bezier at t=0.5.
455
+ *
456
+ * Used by TetherService for tether midpoints.
457
+ *
458
+ * @param {{x: number, y: number, side: string}} pFrom
459
+ * @param {{x: number, y: number, side: string}} pTo
460
+ * @param {number} pDepartDist - Departure/approach distance
461
+ * @returns {{x: number, y: number}}
462
+ */
463
+ getAutoMidpointSimple(pFrom, pTo, pDepartDist)
464
+ {
465
+ let tmpDA = this.computeDepartApproach(pFrom, pTo, pDepartDist);
466
+
467
+ let tmpSpanX = Math.abs(tmpDA.approachX - tmpDA.departX);
468
+ let tmpSpanY = Math.abs(tmpDA.approachY - tmpDA.departY);
469
+ let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
470
+ let tmpCPDist = tmpSpan * 0.4;
471
+
472
+ let tmpP0 = { x: tmpDA.departX, y: tmpDA.departY };
473
+ let tmpP1 = { x: tmpDA.departX + tmpDA.fromDir.dx * tmpCPDist, y: tmpDA.departY + tmpDA.fromDir.dy * tmpCPDist };
474
+ let tmpP2 = { x: tmpDA.approachX + tmpDA.toDir.dx * tmpCPDist, y: tmpDA.approachY + tmpDA.toDir.dy * tmpCPDist };
475
+ let tmpP3 = { x: tmpDA.approachX, y: tmpDA.approachY };
476
+
477
+ return this.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
478
+ }
197
479
  }
198
480
 
199
481
  module.exports = PictServiceFlowPathGenerator;
@@ -0,0 +1,269 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-PortRenderer
5
+ *
6
+ * Renders port circles, labels, and badges for flow diagram nodes.
7
+ *
8
+ * Extracted from PictView-Flow-Node.js to isolate port rendering logic
9
+ * from node body rendering and layout.
10
+ *
11
+ * Dependencies (all accessed via this._FlowView):
12
+ * - _GeometryProvider — for getEdgeFromSide, getPortLocalPosition
13
+ * - _ConnectorShapesProvider — for createPortElement
14
+ * - _SVGHelperProvider — for createSVGElement
15
+ */
16
+ class PictServiceFlowPortRenderer extends libFableServiceProviderBase
17
+ {
18
+ constructor(pFable, pOptions, pServiceHash)
19
+ {
20
+ super(pFable, pOptions, pServiceHash);
21
+
22
+ this.serviceType = 'PictServiceFlowPortRenderer';
23
+
24
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
25
+ }
26
+
27
+ /**
28
+ * Render ports for a node.
29
+ * @param {Object} pNodeData
30
+ * @param {SVGGElement} pGroup - The node's SVG group
31
+ * @param {number} pWidth
32
+ * @param {number} pHeight
33
+ * @param {Object} [pNodeTypeConfig] - Node type configuration (for label display options)
34
+ * @param {number} pNodeTitleBarHeight - Title bar height (for port position offset)
35
+ */
36
+ renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig, pNodeTitleBarHeight)
37
+ {
38
+ if (!this._FlowView) return;
39
+ if (!pNodeData.Ports || !Array.isArray(pNodeData.Ports)) return;
40
+
41
+ let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
42
+ let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
43
+ let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
44
+ let tmpGeometryProvider = this._FlowView._GeometryProvider;
45
+
46
+ // Group ports by their Side value (supports all 12 positions)
47
+ let tmpPortsBySide = {};
48
+ for (let i = 0; i < pNodeData.Ports.length; i++)
49
+ {
50
+ let tmpPort = pNodeData.Ports[i];
51
+ let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
52
+ if (!tmpPortsBySide[tmpSide])
53
+ {
54
+ tmpPortsBySide[tmpSide] = [];
55
+ }
56
+ tmpPortsBySide[tmpSide].push(tmpPort);
57
+ }
58
+
59
+ // Build port counts map for adaptive zone sizing
60
+ let tmpPortCountsBySide = {};
61
+ for (let tmpKey in tmpPortsBySide)
62
+ {
63
+ tmpPortCountsBySide[tmpKey] = tmpPortsBySide[tmpKey].length;
64
+ }
65
+
66
+ for (let tmpSide in tmpPortsBySide)
67
+ {
68
+ let tmpPorts = tmpPortsBySide[tmpSide];
69
+ // Determine the edge for label positioning
70
+ let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
71
+
72
+ for (let i = 0; i < tmpPorts.length; i++)
73
+ {
74
+ let tmpPort = tmpPorts[i];
75
+ let tmpPosition = this.getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight, pNodeTitleBarHeight, tmpPortCountsBySide);
76
+
77
+ // Port label badge — flush against the node edge with no
78
+ // border on the edge side; rendered before the port circle
79
+ // so the circle visually sits on top of the badge
80
+ let tmpLabelElement = null;
81
+ if (tmpPort.Label)
82
+ {
83
+ let tmpPortTypeColorMap =
84
+ {
85
+ 'event-in': '#3498db',
86
+ 'event-out': '#2ecc71',
87
+ 'setting': '#e67e22',
88
+ 'value': '#f1c40f',
89
+ 'error': '#e74c3c'
90
+ };
91
+ let tmpBorderColor = tmpPort.PortType ? (tmpPortTypeColorMap[tmpPort.PortType] || '#95a5a6') : '#95a5a6';
92
+
93
+ let tmpBadgeHeight = 12;
94
+ let tmpBadgePadH = 5;
95
+ let tmpBadgeBorderW = 2;
96
+ let tmpEdgePad = 1;
97
+ let tmpPortRadius = 5;
98
+
99
+ let tmpTextLen = tmpPort.Label.length * 5;
100
+ let tmpBadgeX, tmpBadgeY, tmpBadgeWidth;
101
+ let tmpTextX, tmpTextAnchor;
102
+ let tmpStripeX, tmpStripeY, tmpStripeW, tmpStripeH;
103
+ let tmpBorderPath;
104
+
105
+ if (tmpEdge === 'left')
106
+ {
107
+ tmpBadgeWidth = tmpPortRadius + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpBadgeBorderW;
108
+ tmpBadgeX = tmpEdgePad;
109
+ tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
110
+ tmpTextX = tmpBadgeX + tmpPortRadius + tmpBadgePadH;
111
+ tmpTextAnchor = 'start';
112
+ tmpStripeX = tmpBadgeX + tmpBadgeWidth - tmpBadgeBorderW;
113
+ tmpStripeY = tmpBadgeY;
114
+ tmpStripeW = tmpBadgeBorderW;
115
+ tmpStripeH = tmpBadgeHeight;
116
+ tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
117
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
118
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
119
+ + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight);
120
+ }
121
+ else if (tmpEdge === 'right')
122
+ {
123
+ tmpBadgeWidth = tmpBadgeBorderW + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpPortRadius;
124
+ tmpBadgeX = pWidth - tmpBadgeWidth - tmpEdgePad;
125
+ tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
126
+ tmpTextX = tmpBadgeX + tmpBadgeBorderW + tmpBadgePadH;
127
+ tmpTextAnchor = 'start';
128
+ tmpStripeX = tmpBadgeX;
129
+ tmpStripeY = tmpBadgeY;
130
+ tmpStripeW = tmpBadgeBorderW;
131
+ tmpStripeH = tmpBadgeHeight;
132
+ tmpBorderPath = 'M ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
133
+ + ' L ' + tmpBadgeX + ' ' + tmpBadgeY
134
+ + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
135
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
136
+ }
137
+ else if (tmpEdge === 'top')
138
+ {
139
+ tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
140
+ tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
141
+ tmpBadgeY = tmpEdgePad;
142
+ tmpTextX = tmpPosition.x;
143
+ tmpTextAnchor = 'middle';
144
+ tmpStripeX = tmpBadgeX;
145
+ tmpStripeY = tmpBadgeY + tmpBadgeHeight - tmpBadgeBorderW;
146
+ tmpStripeW = tmpBadgeWidth;
147
+ tmpStripeH = tmpBadgeBorderW;
148
+ tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
149
+ + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
150
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
151
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY;
152
+ }
153
+ else
154
+ {
155
+ tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
156
+ tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
157
+ tmpBadgeY = pHeight - tmpBadgeHeight - tmpEdgePad;
158
+ tmpTextX = tmpPosition.x;
159
+ tmpTextAnchor = 'middle';
160
+ tmpStripeX = tmpBadgeX;
161
+ tmpStripeY = tmpBadgeY;
162
+ tmpStripeW = tmpBadgeWidth;
163
+ tmpStripeH = tmpBadgeBorderW;
164
+ tmpBorderPath = 'M ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
165
+ + ' L ' + tmpBadgeX + ' ' + tmpBadgeY
166
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
167
+ + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
168
+ }
169
+
170
+ // Background rect (cream, no stroke — border drawn separately)
171
+ let tmpBgRect = this._FlowView._SVGHelperProvider.createSVGElement('rect');
172
+ tmpBgRect.setAttribute('class', 'pict-flow-port-label-bg');
173
+ tmpBgRect.setAttribute('x', String(tmpBadgeX));
174
+ tmpBgRect.setAttribute('y', String(tmpBadgeY));
175
+ tmpBgRect.setAttribute('width', String(tmpBadgeWidth));
176
+ tmpBgRect.setAttribute('height', String(tmpBadgeHeight));
177
+ tmpBgRect.setAttribute('fill', 'var(--pf-port-label-bg, rgba(255, 253, 240, 0.5))');
178
+ pGroup.appendChild(tmpBgRect);
179
+
180
+ // 3-sided border path (open on the edge-facing side)
181
+ let tmpBorderPathEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
182
+ tmpBorderPathEl.setAttribute('class', 'pict-flow-port-label-bg');
183
+ tmpBorderPathEl.setAttribute('d', tmpBorderPath);
184
+ tmpBorderPathEl.setAttribute('fill', 'none');
185
+ tmpBorderPathEl.setAttribute('stroke', tmpBorderColor);
186
+ tmpBorderPathEl.setAttribute('stroke-width', '0.75');
187
+ pGroup.appendChild(tmpBorderPathEl);
188
+
189
+ // Colored stripe on the inner side
190
+ let tmpStripe = this._FlowView._SVGHelperProvider.createSVGElement('rect');
191
+ tmpStripe.setAttribute('class', 'pict-flow-port-label-bg');
192
+ tmpStripe.setAttribute('x', String(tmpStripeX));
193
+ tmpStripe.setAttribute('y', String(tmpStripeY));
194
+ tmpStripe.setAttribute('width', String(tmpStripeW));
195
+ tmpStripe.setAttribute('height', String(tmpStripeH));
196
+ tmpStripe.setAttribute('fill', tmpBorderColor);
197
+ pGroup.appendChild(tmpStripe);
198
+
199
+ // Text label — appended after circle for z-order
200
+ tmpLabelElement = this._FlowView._SVGHelperProvider.createSVGElement('text');
201
+ tmpLabelElement.setAttribute('class', 'pict-flow-port-label');
202
+ tmpLabelElement.setAttribute('fill', 'var(--pf-port-label-text, #2c3e50)');
203
+ tmpLabelElement.textContent = tmpPort.Label;
204
+ tmpLabelElement.setAttribute('x', String(tmpTextX));
205
+ tmpLabelElement.setAttribute('y', String(tmpBadgeY + tmpBadgeHeight / 2));
206
+ tmpLabelElement.setAttribute('text-anchor', tmpTextAnchor);
207
+ tmpLabelElement.setAttribute('dominant-baseline', 'central');
208
+ }
209
+
210
+ // Port circle (rendered on top of badge background)
211
+ let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
212
+ let tmpCircle;
213
+ if (tmpShapeProvider)
214
+ {
215
+ tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
216
+ }
217
+ else
218
+ {
219
+ tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
220
+ let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
221
+ if (tmpPort.PortType)
222
+ {
223
+ tmpPortClass += ` port-type-${tmpPort.PortType}`;
224
+ }
225
+ tmpCircle.setAttribute('class', tmpPortClass);
226
+ tmpCircle.setAttribute('cx', String(tmpPosition.x));
227
+ tmpCircle.setAttribute('cy', String(tmpPosition.y));
228
+ tmpCircle.setAttribute('r', '5');
229
+ tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
230
+ tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
231
+ tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
232
+ if (tmpPort.PortType)
233
+ {
234
+ tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
235
+ }
236
+ tmpCircle.setAttribute('data-element-type', 'port');
237
+ }
238
+ pGroup.appendChild(tmpCircle);
239
+
240
+ // Port label text (on top of everything)
241
+ if (tmpLabelElement)
242
+ {
243
+ pGroup.appendChild(tmpLabelElement);
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Calculate port position relative to node origin.
251
+ *
252
+ * Delegates to the geometry provider's getPortLocalPosition method.
253
+ *
254
+ * @param {string} pSide - 'left', 'right', 'top', 'bottom' (or compound sides)
255
+ * @param {number} pIndex - Index of this port on its side
256
+ * @param {number} pTotal - Total ports on this side
257
+ * @param {number} pWidth - Node width
258
+ * @param {number} pHeight - Node height
259
+ * @param {number} pNodeTitleBarHeight - Title bar height
260
+ * @param {Object} [pPortCountsBySide] - Optional map of Side → count for adaptive zones
261
+ * @returns {{x: number, y: number}}
262
+ */
263
+ getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pNodeTitleBarHeight, pPortCountsBySide)
264
+ {
265
+ return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pNodeTitleBarHeight, pPortCountsBySide);
266
+ }
267
+ }
268
+
269
+ module.exports = PictServiceFlowPortRenderer;