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
@@ -0,0 +1,281 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-RenderManager
5
+ *
6
+ * Orchestrates rendering of the flow diagram: nodes, connections,
7
+ * tethers, panels, SVG marker definitions, and node position updates.
8
+ *
9
+ * Extracted from PictView-Flow.js to isolate rendering orchestration
10
+ * from data management and interaction handling.
11
+ */
12
+ class PictServiceFlowRenderManager extends libFableServiceProviderBase
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+
18
+ this.serviceType = 'PictServiceFlowRenderManager';
19
+
20
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
21
+ }
22
+
23
+ /**
24
+ * Render the complete flow diagram
25
+ */
26
+ renderFlow()
27
+ {
28
+ if (!this._FlowView) return;
29
+ if (!this._FlowView._NodesLayer || !this._FlowView._ConnectionsLayer) return;
30
+
31
+ // Clear existing SVG content
32
+ while (this._FlowView._NodesLayer.firstChild)
33
+ {
34
+ this._FlowView._NodesLayer.removeChild(this._FlowView._NodesLayer.firstChild);
35
+ }
36
+ while (this._FlowView._ConnectionsLayer.firstChild)
37
+ {
38
+ this._FlowView._ConnectionsLayer.removeChild(this._FlowView._ConnectionsLayer.firstChild);
39
+ }
40
+
41
+ // Render connections first (behind nodes)
42
+ for (let i = 0; i < this._FlowView._FlowData.Connections.length; i++)
43
+ {
44
+ let tmpConnection = this._FlowView._FlowData.Connections[i];
45
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === tmpConnection.Hash);
46
+
47
+ this._FlowView._ConnectionRenderer.renderConnection(
48
+ tmpConnection,
49
+ this._FlowView._ConnectionsLayer,
50
+ tmpIsSelected
51
+ );
52
+ }
53
+
54
+ // Render nodes
55
+ for (let i = 0; i < this._FlowView._FlowData.Nodes.length; i++)
56
+ {
57
+ let tmpNode = this._FlowView._FlowData.Nodes[i];
58
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedNodeHash === tmpNode.Hash);
59
+ let tmpNodeTypeConfig = this._FlowView._NodeTypeProvider.getNodeType(tmpNode.Type);
60
+
61
+ // Enrich saved port data with metadata from the node type's DefaultPorts.
62
+ // Saved flow data may not include PortType or may have stale Side values,
63
+ // so we match each port to its DefaultPort counterpart by Label and Direction,
64
+ // then copy over PortType and Side from the authoritative node type definition.
65
+ if (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultPorts && tmpNode.Ports)
66
+ {
67
+ for (let p = 0; p < tmpNode.Ports.length; p++)
68
+ {
69
+ let tmpPort = tmpNode.Ports[p];
70
+ for (let d = 0; d < tmpNodeTypeConfig.DefaultPorts.length; d++)
71
+ {
72
+ let tmpDefault = tmpNodeTypeConfig.DefaultPorts[d];
73
+ if (tmpDefault.Label === tmpPort.Label && tmpDefault.Direction === tmpPort.Direction)
74
+ {
75
+ if (tmpDefault.PortType)
76
+ {
77
+ tmpPort.PortType = tmpDefault.PortType;
78
+ }
79
+ if (tmpDefault.Side)
80
+ {
81
+ tmpPort.Side = tmpDefault.Side;
82
+ }
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ this._FlowView._NodeView.renderNode(tmpNode, this._FlowView._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
90
+ }
91
+
92
+ // Render properties panels and tethers
93
+ if (this._FlowView._PropertiesPanelView && this._FlowView._PanelsLayer && this._FlowView._TethersLayer)
94
+ {
95
+ this._FlowView._PropertiesPanelView.renderPanels(
96
+ this._FlowView._FlowData.OpenPanels,
97
+ this._FlowView._PanelsLayer,
98
+ this._FlowView._TethersLayer,
99
+ this._FlowView._FlowData.ViewState.SelectedTetherHash
100
+ );
101
+ }
102
+
103
+ // Update viewport transform
104
+ this._FlowView.updateViewportTransform();
105
+ }
106
+
107
+ /**
108
+ * Re-render a single connection (remove and re-add) for smooth drag performance.
109
+ * @param {string} pConnectionHash
110
+ */
111
+ renderSingleConnection(pConnectionHash)
112
+ {
113
+ if (!this._FlowView || !this._FlowView._ConnectionsLayer) return;
114
+
115
+ // Remove existing elements for this connection
116
+ let tmpExisting = this._FlowView._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${pConnectionHash}"]`);
117
+ for (let i = 0; i < tmpExisting.length; i++)
118
+ {
119
+ tmpExisting[i].remove();
120
+ }
121
+
122
+ let tmpConnection = this._FlowView.getConnection(pConnectionHash);
123
+ if (!tmpConnection) return;
124
+
125
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === pConnectionHash);
126
+ this._FlowView._ConnectionRenderer.renderConnection(tmpConnection, this._FlowView._ConnectionsLayer, tmpIsSelected);
127
+ }
128
+
129
+ /**
130
+ * Re-render a single tether (remove and re-add) for smooth drag performance.
131
+ * @param {string} pPanelHash
132
+ */
133
+ renderSingleTether(pPanelHash)
134
+ {
135
+ if (!this._FlowView || !this._FlowView._TethersLayer || !this._FlowView._TetherService) return;
136
+
137
+ // Remove existing tether elements for this panel
138
+ let tmpExisting = this._FlowView._TethersLayer.querySelectorAll(`[data-panel-hash="${pPanelHash}"]`);
139
+ for (let i = 0; i < tmpExisting.length; i++)
140
+ {
141
+ tmpExisting[i].remove();
142
+ }
143
+
144
+ let tmpPanel = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
145
+ if (!tmpPanel) return;
146
+
147
+ let tmpNodeData = this._FlowView.getNode(tmpPanel.NodeHash);
148
+ if (!tmpNodeData) return;
149
+
150
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedTetherHash === pPanelHash);
151
+ this._FlowView._TetherService.renderTether(tmpPanel, tmpNodeData, this._FlowView._TethersLayer, tmpIsSelected, this._FlowView.options.ViewIdentifier);
152
+ }
153
+
154
+ /**
155
+ * Update a single node's position in the SVG without full re-render (for drag performance)
156
+ * @param {string} pNodeHash
157
+ * @param {number} pX
158
+ * @param {number} pY
159
+ */
160
+ updateNodePosition(pNodeHash, pX, pY)
161
+ {
162
+ if (!this._FlowView) return;
163
+
164
+ let tmpNode = this._FlowView.getNode(pNodeHash);
165
+ if (!tmpNode) return;
166
+
167
+ if (this._FlowView.options.EnableGridSnap)
168
+ {
169
+ pX = this._FlowView._LayoutService.snapToGrid(pX, this._FlowView.options.GridSnapSize);
170
+ pY = this._FlowView._LayoutService.snapToGrid(pY, this._FlowView.options.GridSnapSize);
171
+ }
172
+
173
+ tmpNode.X = pX;
174
+ tmpNode.Y = pY;
175
+
176
+ // Reset customized handle positions for connections/tethers involving this node
177
+ this._FlowView._resetHandlesForNode(pNodeHash);
178
+
179
+ // Update the node's SVG group transform for smooth dragging
180
+ let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${pNodeHash}"]`);
181
+ if (tmpNodeGroup)
182
+ {
183
+ tmpNodeGroup.setAttribute('transform', `translate(${pX}, ${pY})`);
184
+ }
185
+
186
+ // Re-render connections that involve this node
187
+ this.renderConnectionsForNode(pNodeHash);
188
+
189
+ // Update tethers for any panels attached to this node
190
+ this.renderTethersForNode(pNodeHash);
191
+ }
192
+
193
+ /**
194
+ * Re-render only connections that involve a specific node (for drag performance)
195
+ * @param {string} pNodeHash
196
+ */
197
+ renderConnectionsForNode(pNodeHash)
198
+ {
199
+ if (!this._FlowView || !this._FlowView._ConnectionsLayer) return;
200
+
201
+ let tmpAffectedConnections = this._FlowView._FlowData.Connections.filter((pConn) =>
202
+ {
203
+ return pConn.SourceNodeHash === pNodeHash || pConn.TargetNodeHash === pNodeHash;
204
+ });
205
+
206
+ for (let i = 0; i < tmpAffectedConnections.length; i++)
207
+ {
208
+ let tmpConn = tmpAffectedConnections[i];
209
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === tmpConn.Hash);
210
+
211
+ // Remove existing connection SVG elements
212
+ let tmpExisting = this._FlowView._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${tmpConn.Hash}"]`);
213
+ for (let j = 0; j < tmpExisting.length; j++)
214
+ {
215
+ tmpExisting[j].remove();
216
+ }
217
+
218
+ // Re-render this connection
219
+ this._FlowView._ConnectionRenderer.renderConnection(tmpConn, this._FlowView._ConnectionsLayer, tmpIsSelected);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Re-render tethers for panels attached to a specific node (for drag performance).
225
+ * @param {string} pNodeHash
226
+ */
227
+ renderTethersForNode(pNodeHash)
228
+ {
229
+ if (!this._FlowView || !this._FlowView._TethersLayer || !this._FlowView._TetherService) return;
230
+
231
+ let tmpAffectedPanels = this._FlowView._FlowData.OpenPanels.filter((pPanel) => pPanel.NodeHash === pNodeHash);
232
+ if (tmpAffectedPanels.length === 0) return;
233
+
234
+ // Remove existing tethers for these panels and re-render via TetherService
235
+ for (let i = 0; i < tmpAffectedPanels.length; i++)
236
+ {
237
+ let tmpExisting = this._FlowView._TethersLayer.querySelectorAll(`[data-panel-hash="${tmpAffectedPanels[i].Hash}"]`);
238
+ for (let j = 0; j < tmpExisting.length; j++)
239
+ {
240
+ tmpExisting[j].remove();
241
+ }
242
+
243
+ let tmpNodeData = this._FlowView.getNode(tmpAffectedPanels[i].NodeHash);
244
+ if (!tmpNodeData) continue;
245
+
246
+ let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedTetherHash === tmpAffectedPanels[i].Hash);
247
+ this._FlowView._TetherService.renderTether(tmpAffectedPanels[i], tmpNodeData, this._FlowView._TethersLayer, tmpIsSelected, this._FlowView.options.ViewIdentifier);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Re-inject SVG marker definitions (arrowheads).
253
+ * Called after a theme switch to update arrowhead colors.
254
+ */
255
+ reinjectMarkerDefs()
256
+ {
257
+ if (!this._FlowView || !this._FlowView._ConnectorShapesProvider || !this._FlowView._SVGElement) return;
258
+
259
+ let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
260
+ let tmpDefs = this._FlowView._SVGElement.querySelector('defs');
261
+ if (!tmpDefs) return;
262
+
263
+ // Remove existing marker elements
264
+ let tmpExistingMarkers = tmpDefs.querySelectorAll('marker');
265
+ for (let i = 0; i < tmpExistingMarkers.length; i++)
266
+ {
267
+ tmpExistingMarkers[i].remove();
268
+ }
269
+
270
+ // Re-generate and inject
271
+ let tmpMarkerMarkup = this._FlowView._ConnectorShapesProvider.generateMarkerDefs(tmpViewIdentifier);
272
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
273
+ tmpTempSVG.innerHTML = tmpMarkerMarkup;
274
+ while (tmpTempSVG.firstChild)
275
+ {
276
+ tmpDefs.appendChild(tmpTempSVG.firstChild);
277
+ }
278
+ }
279
+ }
280
+
281
+ module.exports = PictServiceFlowRenderManager;
@@ -224,27 +224,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
224
224
  */
225
225
  getAutoMidpoint(pFrom, pTo)
226
226
  {
227
- let tmpDepartDist = 20;
228
- let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
229
- let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
230
-
231
- let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
232
- let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
233
- let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
234
- let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
235
-
236
- let tmpSpanX = Math.abs(tmpApproachX - tmpDepartX);
237
- let tmpSpanY = Math.abs(tmpApproachY - tmpDepartY);
238
- let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
239
- let tmpCPDist = tmpSpan * 0.4;
240
-
241
- let tmpP0 = { x: tmpDepartX, y: tmpDepartY };
242
- let tmpP1 = { x: tmpDepartX + tmpFromDir.dx * tmpCPDist, y: tmpDepartY + tmpFromDir.dy * tmpCPDist };
243
- let tmpP2 = { x: tmpApproachX + tmpToDir.dx * tmpCPDist, y: tmpApproachY + tmpToDir.dy * tmpCPDist };
244
- let tmpP3 = { x: tmpApproachX, y: tmpApproachY };
245
-
246
- // Evaluate cubic bezier at t=0.5
247
- return this._FlowView._PathGenerator.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
227
+ return this._FlowView._PathGenerator.getAutoMidpointSimple(pFrom, pTo, 20);
248
228
  }
249
229
 
250
230
  /**
@@ -541,30 +521,14 @@ class PictServiceFlowTether extends libFableServiceProviderBase
541
521
  */
542
522
  _createHandle(pLayer, pPanelHash, pHandleType, pX, pY, pClassName)
543
523
  {
544
- let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
524
+ if (!this._FlowView._ConnectorShapesProvider) return;
525
+
545
526
  let tmpShapeKey = (pClassName === 'pict-flow-tether-handle-midpoint')
546
527
  ? 'tether-handle-midpoint' : 'tether-handle';
547
528
 
548
- if (tmpShapeProvider)
549
- {
550
- let tmpHandle = tmpShapeProvider.createHandleElement(
551
- pPanelHash, pHandleType, pX, pY, tmpShapeKey);
552
- tmpHandle.setAttribute('data-element-type', 'tether-handle');
553
- tmpHandle.setAttribute('data-panel-hash', pPanelHash);
554
- pLayer.appendChild(tmpHandle);
555
- }
556
- else
557
- {
558
- let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
559
- tmpCircle.setAttribute('class', pClassName);
560
- tmpCircle.setAttribute('cx', String(pX));
561
- tmpCircle.setAttribute('cy', String(pY));
562
- tmpCircle.setAttribute('r', '6');
563
- tmpCircle.setAttribute('data-element-type', 'tether-handle');
564
- tmpCircle.setAttribute('data-panel-hash', pPanelHash);
565
- tmpCircle.setAttribute('data-handle-type', pHandleType);
566
- pLayer.appendChild(tmpCircle);
567
- }
529
+ this._FlowView._ConnectorShapesProvider.createFullHandle(
530
+ pLayer, pPanelHash, pHandleType, pX, pY,
531
+ tmpShapeKey, 'tether-handle', 'data-panel-hash');
568
532
  }
569
533
  }
570
534
 
@@ -296,7 +296,7 @@ class PictViewFlowNode extends libPictView
296
296
  }
297
297
 
298
298
  /**
299
- * Render ports for a node
299
+ * Render ports for a node — delegates to the PortRenderer service.
300
300
  * @param {Object} pNodeData
301
301
  * @param {SVGGElement} pGroup - The node's SVG group
302
302
  * @param {number} pWidth
@@ -305,225 +305,7 @@ class PictViewFlowNode extends libPictView
305
305
  */
306
306
  _renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig)
307
307
  {
308
- if (!pNodeData.Ports || !Array.isArray(pNodeData.Ports)) return;
309
-
310
- let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
311
- let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
312
- let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
313
- let tmpGeometryProvider = this._FlowView._GeometryProvider;
314
-
315
- // Group ports by their Side value (supports all 12 positions)
316
- let tmpPortsBySide = {};
317
- for (let i = 0; i < pNodeData.Ports.length; i++)
318
- {
319
- let tmpPort = pNodeData.Ports[i];
320
- let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
321
- if (!tmpPortsBySide[tmpSide])
322
- {
323
- tmpPortsBySide[tmpSide] = [];
324
- }
325
- tmpPortsBySide[tmpSide].push(tmpPort);
326
- }
327
-
328
- for (let tmpSide in tmpPortsBySide)
329
- {
330
- let tmpPorts = tmpPortsBySide[tmpSide];
331
- // Determine the edge for label positioning
332
- let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
333
-
334
- for (let i = 0; i < tmpPorts.length; i++)
335
- {
336
- let tmpPort = tmpPorts[i];
337
- let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
338
-
339
- // Port label badge — flush against the node edge with no
340
- // border on the edge side; rendered before the port circle
341
- // so the circle visually sits on top of the badge
342
- let tmpLabelElement = null;
343
- if (tmpPort.Label)
344
- {
345
- let tmpPortTypeColorMap =
346
- {
347
- 'event-in': '#3498db',
348
- 'event-out': '#2ecc71',
349
- 'setting': '#e67e22',
350
- 'value': '#f1c40f',
351
- 'error': '#e74c3c'
352
- };
353
- let tmpBorderColor = tmpPort.PortType ? (tmpPortTypeColorMap[tmpPort.PortType] || '#95a5a6') : '#95a5a6';
354
-
355
- let tmpBadgeHeight = 12;
356
- let tmpBadgePadH = 5;
357
- let tmpBadgeBorderW = 2;
358
- let tmpEdgePad = 1;
359
- let tmpPortRadius = 5;
360
-
361
- let tmpTextLen = tmpPort.Label.length * 5;
362
- let tmpBadgeX, tmpBadgeY, tmpBadgeWidth;
363
- let tmpTextX, tmpTextAnchor;
364
- let tmpStripeX, tmpStripeY, tmpStripeW, tmpStripeH;
365
- let tmpBorderPath;
366
-
367
- if (tmpEdge === 'left')
368
- {
369
- tmpBadgeWidth = tmpPortRadius + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpBadgeBorderW;
370
- tmpBadgeX = tmpEdgePad;
371
- tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
372
- tmpTextX = tmpBadgeX + tmpPortRadius + tmpBadgePadH;
373
- tmpTextAnchor = 'start';
374
- tmpStripeX = tmpBadgeX + tmpBadgeWidth - tmpBadgeBorderW;
375
- tmpStripeY = tmpBadgeY;
376
- tmpStripeW = tmpBadgeBorderW;
377
- tmpStripeH = tmpBadgeHeight;
378
- tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
379
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
380
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
381
- + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight);
382
- }
383
- else if (tmpEdge === 'right')
384
- {
385
- tmpBadgeWidth = tmpBadgeBorderW + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpPortRadius;
386
- tmpBadgeX = pWidth - tmpBadgeWidth - tmpEdgePad;
387
- tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
388
- tmpTextX = tmpBadgeX + tmpBadgeBorderW + tmpBadgePadH;
389
- tmpTextAnchor = 'start';
390
- tmpStripeX = tmpBadgeX;
391
- tmpStripeY = tmpBadgeY;
392
- tmpStripeW = tmpBadgeBorderW;
393
- tmpStripeH = tmpBadgeHeight;
394
- tmpBorderPath = 'M ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
395
- + ' L ' + tmpBadgeX + ' ' + tmpBadgeY
396
- + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
397
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
398
- }
399
- else if (tmpEdge === 'top')
400
- {
401
- tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
402
- tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
403
- tmpBadgeY = tmpEdgePad;
404
- tmpTextX = tmpPosition.x;
405
- tmpTextAnchor = 'middle';
406
- tmpStripeX = tmpBadgeX;
407
- tmpStripeY = tmpBadgeY + tmpBadgeHeight - tmpBadgeBorderW;
408
- tmpStripeW = tmpBadgeWidth;
409
- tmpStripeH = tmpBadgeBorderW;
410
- tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
411
- + ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
412
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
413
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY;
414
- }
415
- else
416
- {
417
- tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
418
- tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
419
- tmpBadgeY = pHeight - tmpBadgeHeight - tmpEdgePad;
420
- tmpTextX = tmpPosition.x;
421
- tmpTextAnchor = 'middle';
422
- tmpStripeX = tmpBadgeX;
423
- tmpStripeY = tmpBadgeY;
424
- tmpStripeW = tmpBadgeWidth;
425
- tmpStripeH = tmpBadgeBorderW;
426
- tmpBorderPath = 'M ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
427
- + ' L ' + tmpBadgeX + ' ' + tmpBadgeY
428
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
429
- + ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
430
- }
431
-
432
- // Background rect (cream, no stroke — border drawn separately)
433
- let tmpBgRect = this._FlowView._SVGHelperProvider.createSVGElement('rect');
434
- tmpBgRect.setAttribute('class', 'pict-flow-port-label-bg');
435
- tmpBgRect.setAttribute('x', String(tmpBadgeX));
436
- tmpBgRect.setAttribute('y', String(tmpBadgeY));
437
- tmpBgRect.setAttribute('width', String(tmpBadgeWidth));
438
- tmpBgRect.setAttribute('height', String(tmpBadgeHeight));
439
- tmpBgRect.setAttribute('fill', 'var(--pf-port-label-bg, rgba(255, 253, 240, 0.5))');
440
- pGroup.appendChild(tmpBgRect);
441
-
442
- // 3-sided border path (open on the edge-facing side)
443
- let tmpBorderPathEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
444
- tmpBorderPathEl.setAttribute('class', 'pict-flow-port-label-bg');
445
- tmpBorderPathEl.setAttribute('d', tmpBorderPath);
446
- tmpBorderPathEl.setAttribute('fill', 'none');
447
- tmpBorderPathEl.setAttribute('stroke', tmpBorderColor);
448
- tmpBorderPathEl.setAttribute('stroke-width', '0.75');
449
- pGroup.appendChild(tmpBorderPathEl);
450
-
451
- // Colored stripe on the inner side
452
- let tmpStripe = this._FlowView._SVGHelperProvider.createSVGElement('rect');
453
- tmpStripe.setAttribute('class', 'pict-flow-port-label-bg');
454
- tmpStripe.setAttribute('x', String(tmpStripeX));
455
- tmpStripe.setAttribute('y', String(tmpStripeY));
456
- tmpStripe.setAttribute('width', String(tmpStripeW));
457
- tmpStripe.setAttribute('height', String(tmpStripeH));
458
- tmpStripe.setAttribute('fill', tmpBorderColor);
459
- pGroup.appendChild(tmpStripe);
460
-
461
- // Text label — appended after circle for z-order
462
- tmpLabelElement = this._FlowView._SVGHelperProvider.createSVGElement('text');
463
- tmpLabelElement.setAttribute('class', 'pict-flow-port-label');
464
- tmpLabelElement.setAttribute('fill', 'var(--pf-port-label-text, #2c3e50)');
465
- tmpLabelElement.textContent = tmpPort.Label;
466
- tmpLabelElement.setAttribute('x', String(tmpTextX));
467
- tmpLabelElement.setAttribute('y', String(tmpBadgeY + tmpBadgeHeight / 2));
468
- tmpLabelElement.setAttribute('text-anchor', tmpTextAnchor);
469
- tmpLabelElement.setAttribute('dominant-baseline', 'central');
470
- }
471
-
472
- // Port circle (rendered on top of badge background)
473
- let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
474
- let tmpCircle;
475
- if (tmpShapeProvider)
476
- {
477
- tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
478
- }
479
- else
480
- {
481
- tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
482
- let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
483
- if (tmpPort.PortType)
484
- {
485
- tmpPortClass += ` port-type-${tmpPort.PortType}`;
486
- }
487
- tmpCircle.setAttribute('class', tmpPortClass);
488
- tmpCircle.setAttribute('cx', String(tmpPosition.x));
489
- tmpCircle.setAttribute('cy', String(tmpPosition.y));
490
- tmpCircle.setAttribute('r', '5');
491
- tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
492
- tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
493
- tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
494
- if (tmpPort.PortType)
495
- {
496
- tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
497
- }
498
- tmpCircle.setAttribute('data-element-type', 'port');
499
- }
500
- pGroup.appendChild(tmpCircle);
501
-
502
- // Port label text (on top of everything)
503
- if (tmpLabelElement)
504
- {
505
- pGroup.appendChild(tmpLabelElement);
506
- }
507
- }
508
- }
509
- }
510
-
511
- /**
512
- * Calculate port position relative to node origin.
513
- *
514
- * For left and right side ports, positioning is offset below the title bar
515
- * so that ports never overlap the header area.
516
- *
517
- * @param {string} pSide - 'left', 'right', 'top', 'bottom'
518
- * @param {number} pIndex - Index of this port on its side
519
- * @param {number} pTotal - Total ports on this side
520
- * @param {number} pWidth - Node width
521
- * @param {number} pHeight - Node height
522
- * @returns {{x: number, y: number}}
523
- */
524
- _getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight)
525
- {
526
- return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, this.options.NodeTitleBarHeight);
308
+ this._FlowView._PortRenderer.renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig, this.options.NodeTitleBarHeight);
527
309
  }
528
310
 
529
311
  /**