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
@@ -320,6 +320,30 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
320
320
  return tmpElement;
321
321
  }
322
322
 
323
+ /**
324
+ * Create a complete drag handle element with all data attributes set and
325
+ * append it to the given layer. Unifies the handle-creation logic that
326
+ * was previously duplicated in ConnectionRenderer and TetherService.
327
+ *
328
+ * @param {SVGGElement} pLayer - The SVG group to append the handle to
329
+ * @param {string} pOwnerHash - Connection hash or panel hash
330
+ * @param {string} pHandleType - e.g. 'ortho-corner1', 'bezier-midpoint'
331
+ * @param {number} pX
332
+ * @param {number} pY
333
+ * @param {string} pShapeKey - Shape config key ('connection-handle', 'tether-handle-midpoint', etc.)
334
+ * @param {string} pElementType - data-element-type value ('connection-handle' or 'tether-handle')
335
+ * @param {string} pOwnerAttrName - data attribute name for the owner ('data-connection-hash' or 'data-panel-hash')
336
+ * @returns {SVGElement}
337
+ */
338
+ createFullHandle(pLayer, pOwnerHash, pHandleType, pX, pY, pShapeKey, pElementType, pOwnerAttrName)
339
+ {
340
+ let tmpHandle = this.createHandleElement(pOwnerHash, pHandleType, pX, pY, pShapeKey);
341
+ tmpHandle.setAttribute('data-element-type', pElementType);
342
+ tmpHandle.setAttribute(pOwnerAttrName, pOwnerHash);
343
+ pLayer.appendChild(tmpHandle);
344
+ return tmpHandle;
345
+ }
346
+
323
347
  /**
324
348
  * Create a temporary drag connection path element.
325
349
  * @param {string} pPath - The SVG path d-string
@@ -114,9 +114,12 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
114
114
  * Calculate a port's local position relative to node origin.
115
115
  *
116
116
  * Supports 12 positions (3 zones per edge). For left/right edges,
117
- * the body area below the title bar is divided into three vertical zones
118
- * (start/middle/end). For top/bottom edges, the full width is divided
119
- * into three horizontal zones.
117
+ * the body area below the title bar is divided into vertical zones.
118
+ * For top/bottom edges, the full width is divided into horizontal zones.
119
+ *
120
+ * When pPortCountsBySide is provided, zone fractions are computed
121
+ * proportionally based on actual port counts (adaptive zones).
122
+ * Otherwise, fixed 1/3 zones are used for backward compatibility.
120
123
  *
121
124
  * Multiple ports sharing the same Side value distribute evenly within
122
125
  * their zone.
@@ -127,12 +130,21 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
127
130
  * @param {number} pWidth - Node width
128
131
  * @param {number} pHeight - Node height
129
132
  * @param {number} pTitleBarHeight - Height of the node title bar
133
+ * @param {Object} [pPortCountsBySide] - Optional map of Side → port count
134
+ * for all ports on the node. Enables adaptive zone sizing.
130
135
  * @returns {{x: number, y: number}}
131
136
  */
132
- getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight)
137
+ getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight, pPortCountsBySide)
133
138
  {
134
139
  let tmpEdge = this.getEdgeFromSide(pSide);
135
- let tmpZone = this._getZoneFromSide(pSide);
140
+ let tmpZone = pPortCountsBySide
141
+ ? this._computeAdaptiveZone(pSide, pPortCountsBySide)
142
+ : this._getZoneFromSide(pSide);
143
+
144
+ // Use the fixed zone to decide alignment intent (start/center/end)
145
+ // because adaptive zones shift boundaries when neighbouring zones
146
+ // are empty, which would break alignment decisions.
147
+ let tmpFixedZone = this._getZoneFromSide(pSide);
136
148
 
137
149
  // Minimum spacing between port centers (px)
138
150
  let tmpMinSpacing = 16;
@@ -142,18 +154,49 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
142
154
  // and always leave a visible gap above the node bottom edge.
143
155
  let tmpBottomPad = 16;
144
156
 
157
+ // Determine alignment from the fixed zone position:
158
+ // start zone (0.000 – 0.333) → start-align (offset 0)
159
+ // middle zone (0.333 – 0.667) → center
160
+ // end zone (0.667 – 1.000) → end-align
161
+ let tmpAlignment = 'start';
162
+ if (tmpFixedZone.start >= 0.5)
163
+ {
164
+ tmpAlignment = 'end';
165
+ }
166
+ else if (tmpFixedZone.start >= 0.17)
167
+ {
168
+ tmpAlignment = 'center';
169
+ }
170
+
145
171
  if (tmpEdge === 'left' || tmpEdge === 'right')
146
172
  {
147
173
  let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
148
174
  let tmpBodyHeight = pHeight - pTitleBarHeight - tmpBottomPad;
149
175
  let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
150
176
  let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
151
- let tmpSpacing = tmpZoneHeight / (pTotal + 1);
152
- if (tmpSpacing < tmpMinSpacing)
177
+
178
+ // Use fixed spacing so port gaps stay consistent across cards
179
+ // even when one edge drives the card height beyond what the
180
+ // other needs.
181
+ let tmpSpacing = tmpMinSpacing;
182
+ let tmpGroupHeight = tmpSpacing * (pTotal + 1);
183
+ let tmpSlack = tmpZoneHeight - tmpGroupHeight;
184
+ if (tmpSlack < 0)
185
+ {
186
+ tmpSlack = 0;
187
+ }
188
+
189
+ let tmpAlignOffset = 0;
190
+ if (tmpAlignment === 'end')
153
191
  {
154
- tmpSpacing = tmpMinSpacing;
192
+ tmpAlignOffset = tmpSlack;
155
193
  }
156
- let tmpY = tmpZoneStart + tmpSpacing * (pIndex + 1);
194
+ else if (tmpAlignment === 'center')
195
+ {
196
+ tmpAlignOffset = tmpSlack / 2;
197
+ }
198
+
199
+ let tmpY = tmpZoneStart + tmpAlignOffset + tmpSpacing * (pIndex + 1);
157
200
  return { x: tmpX, y: tmpY };
158
201
  }
159
202
 
@@ -161,12 +204,26 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
161
204
  let tmpY = (tmpEdge === 'top') ? 0 : pHeight;
162
205
  let tmpZoneStart = pWidth * tmpZone.start;
163
206
  let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
164
- let tmpSpacing = tmpZoneWidth / (pTotal + 1);
165
- if (tmpSpacing < tmpMinSpacing)
207
+
208
+ let tmpSpacing = tmpMinSpacing;
209
+ let tmpGroupWidth = tmpSpacing * (pTotal + 1);
210
+ let tmpSlack = tmpZoneWidth - tmpGroupWidth;
211
+ if (tmpSlack < 0)
166
212
  {
167
- tmpSpacing = tmpMinSpacing;
213
+ tmpSlack = 0;
168
214
  }
169
- let tmpX = tmpZoneStart + tmpSpacing * (pIndex + 1);
215
+
216
+ let tmpAlignOffset = 0;
217
+ if (tmpAlignment === 'end')
218
+ {
219
+ tmpAlignOffset = tmpSlack;
220
+ }
221
+ else if (tmpAlignment === 'center')
222
+ {
223
+ tmpAlignOffset = tmpSlack / 2;
224
+ }
225
+
226
+ let tmpX = tmpZoneStart + tmpAlignOffset + tmpSpacing * (pIndex + 1);
170
227
  return { x: tmpX, y: tmpY };
171
228
  }
172
229
 
@@ -178,6 +235,8 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
178
235
  * middle: 0.333 — 0.667
179
236
  * end: 0.667 — 1.0
180
237
  *
238
+ * Used as fallback when adaptive zones are not available.
239
+ *
181
240
  * @param {string} pSide
182
241
  * @returns {{start: number, end: number}}
183
242
  */
@@ -209,13 +268,116 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
209
268
  default: return { start: 0.0, end: 1.0 };
210
269
  }
211
270
  }
271
+
272
+ /**
273
+ * Get the three zone Side keys for a given edge, in order.
274
+ *
275
+ * @param {string} pEdge - 'left', 'right', 'top', or 'bottom'
276
+ * @returns {Array<string>} Three Side keys in start-to-end order
277
+ */
278
+ _getZoneKeysForEdge(pEdge)
279
+ {
280
+ switch (pEdge)
281
+ {
282
+ case 'left': return ['left-top', 'left', 'left-bottom'];
283
+ case 'right': return ['right-top', 'right', 'right-bottom'];
284
+ case 'top': return ['top-left', 'top', 'top-right'];
285
+ case 'bottom': return ['bottom-left', 'bottom', 'bottom-right'];
286
+ default: return ['right-top', 'right', 'right-bottom'];
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Compute an adaptive zone fraction for a Side value based on the
292
+ * actual port distribution across all zones on the same edge.
293
+ *
294
+ * Instead of fixed 1/3 splits, zones are sized proportionally to the
295
+ * space each zone needs (minSpacing * (portCount + 1)). Zones with
296
+ * zero ports collapse to zero, giving occupied zones more room.
297
+ *
298
+ * @param {string} pSide - The Side value to compute a zone for
299
+ * @param {Object} pPortCountsBySide - Map of Side → number of ports
300
+ * @returns {{start: number, end: number}}
301
+ */
302
+ _computeAdaptiveZone(pSide, pPortCountsBySide)
303
+ {
304
+ let tmpEdge = this.getEdgeFromSide(pSide);
305
+ let tmpZoneKeys = this._getZoneKeysForEdge(tmpEdge);
306
+
307
+ let tmpMinSpacing = 16;
308
+
309
+ // Compute the space each zone needs: minSpacing * (count + 1)
310
+ // The +1 provides padding at both ends of the zone.
311
+ let tmpTotalSpace = 0;
312
+ let tmpSpaceByZone = {};
313
+ for (let i = 0; i < tmpZoneKeys.length; i++)
314
+ {
315
+ let tmpKey = tmpZoneKeys[i];
316
+ let tmpCount = pPortCountsBySide[tmpKey] || 0;
317
+ let tmpSpace = (tmpCount > 0) ? (tmpMinSpacing * (tmpCount + 1)) : 0;
318
+ tmpSpaceByZone[tmpKey] = tmpSpace;
319
+ tmpTotalSpace += tmpSpace;
320
+ }
321
+
322
+ // If no ports on this edge at all, fall back to fixed zones
323
+ if (tmpTotalSpace === 0)
324
+ {
325
+ return this._getZoneFromSide(pSide);
326
+ }
327
+
328
+ // Compute proportional start/end for the requested zone
329
+ let tmpCumulativeStart = 0;
330
+ for (let i = 0; i < tmpZoneKeys.length; i++)
331
+ {
332
+ let tmpKey = tmpZoneKeys[i];
333
+ let tmpFraction = tmpSpaceByZone[tmpKey] / tmpTotalSpace;
334
+ if (tmpKey === pSide)
335
+ {
336
+ return { start: tmpCumulativeStart, end: tmpCumulativeStart + tmpFraction };
337
+ }
338
+ tmpCumulativeStart += tmpFraction;
339
+ }
340
+
341
+ // Should not reach here; fall back to fixed zones
342
+ return this._getZoneFromSide(pSide);
343
+ }
344
+
345
+ /**
346
+ * Build a map of Side → port count from an array of port objects.
347
+ *
348
+ * Convenience method for callers that need to pass port counts
349
+ * to getPortLocalPosition or computeMinimumNodeHeight.
350
+ *
351
+ * @param {Array} pPorts - Array of port objects with Side, Direction
352
+ * @returns {Object} Map of Side value → count
353
+ */
354
+ buildPortCountsBySide(pPorts)
355
+ {
356
+ let tmpCounts = {};
357
+ if (!pPorts || !Array.isArray(pPorts))
358
+ {
359
+ return tmpCounts;
360
+ }
361
+ for (let i = 0; i < pPorts.length; i++)
362
+ {
363
+ let tmpSide = pPorts[i].Side || (pPorts[i].Direction === 'input' ? 'left' : 'right');
364
+ if (!tmpCounts[tmpSide])
365
+ {
366
+ tmpCounts[tmpSide] = 0;
367
+ }
368
+ tmpCounts[tmpSide]++;
369
+ }
370
+ return tmpCounts;
371
+ }
372
+
212
373
  /**
213
374
  * Compute the minimum node height required so that all ports
214
375
  * (with their badges) fit within the node boundary.
215
376
  *
216
- * Uses the same zone system and minimum spacing as getPortLocalPosition.
217
- * For each left/right zone, calculates where the last port would land
218
- * and ensures the node is tall enough to contain it plus badge clearance.
377
+ * Uses adaptive zone sizing: instead of assuming each zone gets
378
+ * a fixed 1/3 of the body, sums the space needed by all occupied
379
+ * zones on each left/right edge. This produces compact cards
380
+ * whose height scales linearly with total port count.
219
381
  *
220
382
  * @param {Array} pPorts - Array of port objects with Side, Direction
221
383
  * @param {number} pTitleBarHeight - Height of the title bar
@@ -229,26 +391,16 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
229
391
  }
230
392
 
231
393
  let tmpMinSpacing = 16;
232
- let tmpBadgeHalfHeight = 6;
233
394
  let tmpBottomPad = 16;
234
395
 
235
396
  // Count ports per Side value
236
- let tmpCountBySide = {};
237
- for (let i = 0; i < pPorts.length; i++)
238
- {
239
- let tmpSide = pPorts[i].Side || (pPorts[i].Direction === 'input' ? 'left' : 'right');
240
- if (!tmpCountBySide[tmpSide])
241
- {
242
- tmpCountBySide[tmpSide] = 0;
243
- }
244
- tmpCountBySide[tmpSide]++;
245
- }
246
-
247
- let tmpMinHeight = 0;
397
+ let tmpCountBySide = this.buildPortCountsBySide(pPorts);
248
398
 
399
+ // Sum the space needed per edge (left, right) across all zones.
400
+ // Each zone needs minSpacing * (count + 1) pixels.
401
+ let tmpSpacePerEdge = {};
249
402
  for (let tmpSide in tmpCountBySide)
250
403
  {
251
- let tmpCount = tmpCountBySide[tmpSide];
252
404
  let tmpEdge = this.getEdgeFromSide(tmpSide);
253
405
 
254
406
  // Only left/right edge zones affect required height
@@ -257,16 +409,21 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
257
409
  continue;
258
410
  }
259
411
 
260
- let tmpZone = this._getZoneFromSide(tmpSide);
412
+ let tmpCount = tmpCountBySide[tmpSide];
413
+ let tmpZoneSpace = tmpMinSpacing * (tmpCount + 1);
261
414
 
262
- // With bottomPad reserving space at the bottom:
263
- // bodyHeight = H - titleBar - bottomPad
264
- // lastPortY = titleBar + bodyHeight * zone.start + minSpacing * count
265
- // Need: lastPortY + badgeHalfHeight <= H - bottomPad
266
- // Solving for H:
267
- // H >= titleBar + bottomPad + (minSpacing * count + badgeHalfHeight) / (1 - zone.start)
268
- let tmpRequired = pTitleBarHeight + tmpBottomPad + (tmpMinSpacing * tmpCount + tmpBadgeHalfHeight) / (tmpZone.end - tmpZone.start);
415
+ if (!tmpSpacePerEdge[tmpEdge])
416
+ {
417
+ tmpSpacePerEdge[tmpEdge] = 0;
418
+ }
419
+ tmpSpacePerEdge[tmpEdge] += tmpZoneSpace;
420
+ }
269
421
 
422
+ // The minimum height is titleBar + bottomPad + max edge space
423
+ let tmpMinHeight = 0;
424
+ for (let tmpEdge in tmpSpacePerEdge)
425
+ {
426
+ let tmpRequired = pTitleBarHeight + tmpBottomPad + tmpSpacePerEdge[tmpEdge];
270
427
  if (tmpRequired > tmpMinHeight)
271
428
  {
272
429
  tmpMinHeight = tmpRequired;
@@ -65,26 +65,28 @@ class PictProviderFlowPanelChrome extends libFableServiceProviderBase
65
65
  tmpCloseIcon.textContent = '\u2715';
66
66
  }
67
67
 
68
- // Attach event isolation to the panel body so pointer/wheel events
69
- // inside the panel content do not trigger SVG interactions
70
- let tmpBody = tmpFO.querySelector('.pict-flow-panel-body');
71
- if (tmpBody)
68
+ // Attach event isolation to the scrollable content area so
69
+ // pointer/wheel events inside the panel do not trigger SVG interactions
70
+ let tmpContent = tmpFO.querySelector('.pict-flow-panel-content');
71
+ if (tmpContent)
72
72
  {
73
- tmpBody.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
74
- tmpBody.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
73
+ tmpContent.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
74
+ tmpContent.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
75
75
  }
76
76
 
77
- // Isolate events on the collapsible node properties editor section
78
- let tmpNodeProps = tmpFO.querySelector('.pict-flow-panel-node-props');
79
- if (tmpNodeProps)
77
+ // Isolate events on the tab bar
78
+ let tmpTabbar = tmpFO.querySelector('.pict-flow-panel-tabbar');
79
+ if (tmpTabbar)
80
80
  {
81
- tmpNodeProps.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
82
- tmpNodeProps.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
81
+ tmpTabbar.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
82
+ tmpTabbar.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
83
83
  }
84
84
 
85
85
  pPanelsLayer.appendChild(tmpFO);
86
86
 
87
- return tmpBody;
87
+ // Return the properties tab pane as the body container for content rendering
88
+ let tmpPropertiesPane = tmpFO.querySelector('.pict-flow-panel-tab-pane[data-tab="properties"]');
89
+ return tmpPropertiesPane;
88
90
  }
89
91
  }
90
92
 
@@ -0,0 +1,263 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-ConnectionHandleManager
5
+ *
6
+ * Manages connection handle lifecycle: dragging, adding, removing,
7
+ * and resetting bezier/orthogonal handles on connections and tethers.
8
+ *
9
+ * Extracted from PictView-Flow.js to isolate handle CRUD operations
10
+ * from the main view.
11
+ */
12
+ class PictServiceFlowConnectionHandleManager extends libFableServiceProviderBase
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+
18
+ this.serviceType = 'PictServiceFlowConnectionHandleManager';
19
+
20
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
21
+ }
22
+
23
+ /**
24
+ * Update a connection handle position during drag (for real-time feedback).
25
+ * @param {string} pConnectionHash
26
+ * @param {string} pHandleType - 'bezier-midpoint', 'bezier-handle-N', 'ortho-corner1', 'ortho-corner2', 'ortho-midpoint'
27
+ * @param {number} pX
28
+ * @param {number} pY
29
+ */
30
+ updateConnectionHandle(pConnectionHash, pHandleType, pX, pY)
31
+ {
32
+ if (!this._FlowView) return;
33
+
34
+ let tmpConnection = this._FlowView.getConnection(pConnectionHash);
35
+ if (!tmpConnection) return;
36
+
37
+ if (!tmpConnection.Data) tmpConnection.Data = {};
38
+ tmpConnection.Data.HandleCustomized = true;
39
+
40
+ // Multi-handle bezier: handle type is 'bezier-handle-N'
41
+ if (pHandleType && pHandleType.startsWith('bezier-handle-'))
42
+ {
43
+ let tmpIndex = parseInt(pHandleType.replace('bezier-handle-', ''), 10);
44
+ if (!isNaN(tmpIndex) && Array.isArray(tmpConnection.Data.BezierHandles)
45
+ && tmpIndex < tmpConnection.Data.BezierHandles.length)
46
+ {
47
+ tmpConnection.Data.BezierHandles[tmpIndex].x = pX;
48
+ tmpConnection.Data.BezierHandles[tmpIndex].y = pY;
49
+ }
50
+ this._FlowView._renderSingleConnection(pConnectionHash);
51
+ return;
52
+ }
53
+
54
+ switch (pHandleType)
55
+ {
56
+ case 'bezier-midpoint':
57
+ // Legacy single-handle: migrate to BezierHandles array
58
+ if (!Array.isArray(tmpConnection.Data.BezierHandles)
59
+ || tmpConnection.Data.BezierHandles.length === 0)
60
+ {
61
+ tmpConnection.Data.BezierHandles = [{ x: pX, y: pY }];
62
+ }
63
+ else
64
+ {
65
+ tmpConnection.Data.BezierHandles[0].x = pX;
66
+ tmpConnection.Data.BezierHandles[0].y = pY;
67
+ }
68
+ // Keep legacy fields in sync for backward compat
69
+ tmpConnection.Data.BezierHandleX = pX;
70
+ tmpConnection.Data.BezierHandleY = pY;
71
+ break;
72
+
73
+ case 'ortho-corner1':
74
+ tmpConnection.Data.OrthoCorner1X = pX;
75
+ tmpConnection.Data.OrthoCorner1Y = pY;
76
+ break;
77
+
78
+ case 'ortho-corner2':
79
+ tmpConnection.Data.OrthoCorner2X = pX;
80
+ tmpConnection.Data.OrthoCorner2Y = pY;
81
+ break;
82
+
83
+ case 'ortho-midpoint':
84
+ {
85
+ // Midpoint drag shifts the corridor offset
86
+ let tmpSourcePos = this._FlowView.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
87
+ let tmpTargetPos = this._FlowView.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
88
+ if (tmpSourcePos && tmpTargetPos)
89
+ {
90
+ let tmpGeom = this._FlowView._ConnectionRenderer._computeDirectionalGeometry(tmpSourcePos, tmpTargetPos);
91
+ let tmpStartDir = tmpGeom.startDir;
92
+
93
+ // Compute offset along the corridor axis
94
+ if (Math.abs(tmpStartDir.dx) > Math.abs(tmpStartDir.dy))
95
+ {
96
+ // Horizontal departure — corridor is vertical, shift is along X
97
+ let tmpAutoMidX = (tmpGeom.departX + tmpGeom.approachX) / 2;
98
+ tmpConnection.Data.OrthoMidOffset = pX - tmpAutoMidX;
99
+ }
100
+ else
101
+ {
102
+ // Vertical departure — corridor is horizontal, shift is along Y
103
+ let tmpAutoMidY = (tmpGeom.departY + tmpGeom.approachY) / 2;
104
+ tmpConnection.Data.OrthoMidOffset = pY - tmpAutoMidY;
105
+ }
106
+ }
107
+ break;
108
+ }
109
+ }
110
+
111
+ this._FlowView._renderSingleConnection(pConnectionHash);
112
+ }
113
+
114
+ /**
115
+ * Add a bezier handle to a connection at the specified position.
116
+ * The handle is inserted at the correct index based on which
117
+ * segment of the curve the click point is closest to.
118
+ *
119
+ * @param {string} pConnectionHash
120
+ * @param {number} pX
121
+ * @param {number} pY
122
+ */
123
+ addConnectionHandle(pConnectionHash, pX, pY)
124
+ {
125
+ if (!this._FlowView) return;
126
+
127
+ let tmpConnection = this._FlowView.getConnection(pConnectionHash);
128
+ if (!tmpConnection) return;
129
+
130
+ if (!tmpConnection.Data) tmpConnection.Data = {};
131
+
132
+ // Ensure BezierHandles array exists (migrate from legacy if needed)
133
+ if (!Array.isArray(tmpConnection.Data.BezierHandles))
134
+ {
135
+ tmpConnection.Data.BezierHandles = [];
136
+ if (tmpConnection.Data.BezierHandleX != null && tmpConnection.Data.BezierHandleY != null)
137
+ {
138
+ tmpConnection.Data.BezierHandles.push({
139
+ x: tmpConnection.Data.BezierHandleX,
140
+ y: tmpConnection.Data.BezierHandleY
141
+ });
142
+ }
143
+ }
144
+
145
+ // Ensure bezier mode
146
+ tmpConnection.Data.LineMode = 'bezier';
147
+
148
+ let tmpSourcePos = this._FlowView.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
149
+ let tmpTargetPos = this._FlowView.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
150
+
151
+ let tmpInsertIndex = 0;
152
+ if (tmpSourcePos && tmpTargetPos && this._FlowView._ConnectionRenderer)
153
+ {
154
+ tmpInsertIndex = this._FlowView._ConnectionRenderer.computeInsertionIndex(
155
+ tmpConnection.Data.BezierHandles,
156
+ { x: pX, y: pY },
157
+ tmpSourcePos,
158
+ tmpTargetPos
159
+ );
160
+ }
161
+
162
+ tmpConnection.Data.BezierHandles.splice(tmpInsertIndex, 0, { x: pX, y: pY });
163
+ tmpConnection.Data.HandleCustomized = true;
164
+
165
+ this._FlowView.renderFlow();
166
+ this._FlowView.marshalFromView();
167
+
168
+ if (this._FlowView._EventHandlerProvider)
169
+ {
170
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Remove a bezier handle from a connection by index.
176
+ *
177
+ * @param {string} pConnectionHash
178
+ * @param {number} pIndex - Index in the BezierHandles array
179
+ */
180
+ removeConnectionHandle(pConnectionHash, pIndex)
181
+ {
182
+ if (!this._FlowView) return;
183
+
184
+ let tmpConnection = this._FlowView.getConnection(pConnectionHash);
185
+ if (!tmpConnection || !tmpConnection.Data) return;
186
+
187
+ if (!Array.isArray(tmpConnection.Data.BezierHandles)) return;
188
+ if (pIndex < 0 || pIndex >= tmpConnection.Data.BezierHandles.length) return;
189
+
190
+ tmpConnection.Data.BezierHandles.splice(pIndex, 1);
191
+
192
+ if (tmpConnection.Data.BezierHandles.length === 0)
193
+ {
194
+ tmpConnection.Data.HandleCustomized = false;
195
+ tmpConnection.Data.BezierHandleX = null;
196
+ tmpConnection.Data.BezierHandleY = null;
197
+ }
198
+
199
+ this._FlowView.renderFlow();
200
+ this._FlowView.marshalFromView();
201
+
202
+ if (this._FlowView._EventHandlerProvider)
203
+ {
204
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Reset handle positions for all connections/tethers involving a node.
210
+ * Called when a node moves. Preserves LineMode but resets handle coordinates to auto.
211
+ * @param {string} pNodeHash
212
+ */
213
+ resetHandlesForNode(pNodeHash)
214
+ {
215
+ if (!this._FlowView) return;
216
+
217
+ // Reset connection handles
218
+ for (let i = 0; i < this._FlowView._FlowData.Connections.length; i++)
219
+ {
220
+ let tmpConn = this._FlowView._FlowData.Connections[i];
221
+ if (tmpConn.SourceNodeHash === pNodeHash || tmpConn.TargetNodeHash === pNodeHash)
222
+ {
223
+ if (tmpConn.Data && tmpConn.Data.HandleCustomized)
224
+ {
225
+ tmpConn.Data.HandleCustomized = false;
226
+ tmpConn.Data.BezierHandleX = null;
227
+ tmpConn.Data.BezierHandleY = null;
228
+ tmpConn.Data.OrthoCorner1X = null;
229
+ tmpConn.Data.OrthoCorner1Y = null;
230
+ tmpConn.Data.OrthoCorner2X = null;
231
+ tmpConn.Data.OrthoCorner2Y = null;
232
+ tmpConn.Data.OrthoMidOffset = 0;
233
+ }
234
+ }
235
+ }
236
+
237
+ // Reset tether handles for panels attached to this node
238
+ if (this._FlowView._TetherService)
239
+ {
240
+ this._FlowView._TetherService.resetHandlesForNode(this._FlowView._FlowData.OpenPanels, pNodeHash);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Reset tether handle positions for a specific panel.
246
+ * Called when a panel is dragged.
247
+ * @param {string} pPanelHash
248
+ */
249
+ resetHandlesForPanel(pPanelHash)
250
+ {
251
+ if (!this._FlowView) return;
252
+
253
+ let tmpPanel = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
254
+ if (!tmpPanel) return;
255
+
256
+ if (this._FlowView._TetherService)
257
+ {
258
+ this._FlowView._TetherService.resetHandlePositions(tmpPanel);
259
+ }
260
+ }
261
+ }
262
+
263
+ module.exports = PictServiceFlowConnectionHandleManager;