pict-section-flow 0.0.2 → 0.0.5

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 (39) hide show
  1. package/.claude/launch.json +11 -0
  2. package/docs/README.md +51 -0
  3. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
  4. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
  8. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
  9. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
  10. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
  11. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
  12. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
  13. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
  14. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
  15. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
  16. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
  17. package/package.json +7 -5
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +52 -1
  20. package/source/panels/FlowCardPropertiesPanel-Template.js +1 -1
  21. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  22. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  23. package/source/providers/PictProvider-Flow-Geometry.js +136 -2
  24. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  25. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  26. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  27. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  28. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  29. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  30. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  31. package/source/services/PictService-Flow-PanelManager.js +188 -0
  32. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  33. package/source/services/PictService-Flow-Tether.js +52 -25
  34. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  35. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  36. package/source/views/PictView-Flow-Node.js +668 -174
  37. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  38. package/source/views/PictView-Flow-Toolbar.js +846 -379
  39. package/source/views/PictView-Flow.js +279 -671
@@ -0,0 +1,413 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+
9
+ this.serviceType = 'PictProviderFlowConnectorShapes';
10
+
11
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
12
+
13
+ // Default shape configurations — each keyed by a shape identifier
14
+ // _OriginalShapes stores a deep copy for reset when switching themes.
15
+ this._DefaultShapes =
16
+ {
17
+ 'port':
18
+ {
19
+ ElementType: 'circle',
20
+ Attributes: { r: '5' },
21
+ ClassName: 'pict-flow-port'
22
+ },
23
+ 'panel-indicator':
24
+ {
25
+ ElementType: 'rect',
26
+ Attributes: { rx: '2', ry: '2' },
27
+ ClassName: 'pict-flow-node-panel-indicator'
28
+ },
29
+ 'connection-path':
30
+ {
31
+ ElementType: 'path',
32
+ Attributes: {},
33
+ ClassName: 'pict-flow-connection'
34
+ },
35
+ 'connection-hitarea':
36
+ {
37
+ ElementType: 'path',
38
+ Attributes: {},
39
+ ClassName: 'pict-flow-connection-hitarea'
40
+ },
41
+ 'connection-handle':
42
+ {
43
+ ElementType: 'circle',
44
+ Attributes: { r: '6' },
45
+ ClassName: 'pict-flow-connection-handle'
46
+ },
47
+ 'connection-handle-midpoint':
48
+ {
49
+ ElementType: 'circle',
50
+ Attributes: { r: '6' },
51
+ ClassName: 'pict-flow-connection-handle-midpoint'
52
+ },
53
+ 'drag-connection':
54
+ {
55
+ ElementType: 'path',
56
+ Attributes: {},
57
+ ClassName: 'pict-flow-drag-connection'
58
+ },
59
+ 'tether-path':
60
+ {
61
+ ElementType: 'path',
62
+ Attributes: {},
63
+ ClassName: 'pict-flow-tether-line'
64
+ },
65
+ 'tether-hitarea':
66
+ {
67
+ ElementType: 'path',
68
+ Attributes: {},
69
+ ClassName: 'pict-flow-tether-hitarea'
70
+ },
71
+ 'tether-handle':
72
+ {
73
+ ElementType: 'circle',
74
+ Attributes: { r: '6' },
75
+ ClassName: 'pict-flow-tether-handle'
76
+ },
77
+ 'tether-handle-midpoint':
78
+ {
79
+ ElementType: 'circle',
80
+ Attributes: { r: '6' },
81
+ ClassName: 'pict-flow-tether-handle-midpoint'
82
+ },
83
+ 'arrowhead-connection':
84
+ {
85
+ MarkerWidth: '5',
86
+ MarkerHeight: '7',
87
+ RefX: '7.5',
88
+ RefY: '3.5',
89
+ Points: '0 0, 5 3.5, 0 7',
90
+ Fill: '#95a5a6'
91
+ },
92
+ 'arrowhead-connection-selected':
93
+ {
94
+ MarkerWidth: '5',
95
+ MarkerHeight: '7',
96
+ RefX: '7.5',
97
+ RefY: '3.5',
98
+ Points: '0 0, 5 3.5, 0 7',
99
+ Fill: '#3498db'
100
+ },
101
+ 'arrowhead-tether':
102
+ {
103
+ MarkerWidth: '4',
104
+ MarkerHeight: '6',
105
+ RefX: '6',
106
+ RefY: '3',
107
+ Points: '0 0, 4 3, 0 6',
108
+ Fill: '#95a5a6'
109
+ }
110
+ };
111
+
112
+ // Store a deep copy for resetting when switching themes
113
+ this._OriginalShapes = JSON.parse(JSON.stringify(this._DefaultShapes));
114
+ }
115
+
116
+ // ── Theme Override Methods ─────────────────────────────────────────────
117
+
118
+ /**
119
+ * Apply theme-specific shape overrides.
120
+ * Merges the overrides into the active shape configs.
121
+ * @param {Object} pOverrides - Map of shape key to partial config
122
+ */
123
+ applyThemeOverrides(pOverrides)
124
+ {
125
+ if (!pOverrides || typeof pOverrides !== 'object')
126
+ {
127
+ return;
128
+ }
129
+ for (let tmpKey in pOverrides)
130
+ {
131
+ if (this._DefaultShapes.hasOwnProperty(tmpKey))
132
+ {
133
+ Object.assign(this._DefaultShapes[tmpKey], pOverrides[tmpKey]);
134
+ }
135
+ else
136
+ {
137
+ this._DefaultShapes[tmpKey] = pOverrides[tmpKey];
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Reset all shape configs to their original defaults.
144
+ * Called before applying new theme overrides to prevent accumulation.
145
+ */
146
+ resetToDefaults()
147
+ {
148
+ this._DefaultShapes = JSON.parse(JSON.stringify(this._OriginalShapes));
149
+ }
150
+
151
+ /**
152
+ * Get a shape configuration by key.
153
+ * @param {string} pShapeKey
154
+ * @returns {Object|null}
155
+ */
156
+ getShapeConfig(pShapeKey)
157
+ {
158
+ if (this._DefaultShapes.hasOwnProperty(pShapeKey))
159
+ {
160
+ return this._DefaultShapes[pShapeKey];
161
+ }
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Override or add a shape configuration.
167
+ * Consumers can call this to customize connector shapes without subclassing.
168
+ * @param {string} pShapeKey
169
+ * @param {Object} pConfig
170
+ */
171
+ setShapeConfig(pShapeKey, pConfig)
172
+ {
173
+ this._DefaultShapes[pShapeKey] = pConfig;
174
+ }
175
+
176
+ /**
177
+ * Get all registered shape keys.
178
+ * @returns {string[]}
179
+ */
180
+ getShapeKeys()
181
+ {
182
+ return Object.keys(this._DefaultShapes);
183
+ }
184
+
185
+ // ---- Factory Methods ----
186
+
187
+ /**
188
+ * Create a port SVG element.
189
+ * @param {Object} pPortData - Port data with Hash, Direction
190
+ * @param {{x: number, y: number}} pPosition - Local position within the node group
191
+ * @param {string} pNodeHash - The owning node hash
192
+ * @returns {SVGCircleElement}
193
+ */
194
+ createPortElement(pPortData, pPosition, pNodeHash)
195
+ {
196
+ let tmpConfig = this._DefaultShapes['port'];
197
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
198
+ tmpElement.setAttribute('class', tmpConfig.ClassName + ' ' + pPortData.Direction);
199
+ tmpElement.setAttribute('cx', String(pPosition.x));
200
+ tmpElement.setAttribute('cy', String(pPosition.y));
201
+ // Apply config attributes (r, etc.)
202
+ for (let tmpKey in tmpConfig.Attributes)
203
+ {
204
+ tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
205
+ }
206
+ tmpElement.setAttribute('data-port-hash', pPortData.Hash);
207
+ tmpElement.setAttribute('data-node-hash', pNodeHash);
208
+ tmpElement.setAttribute('data-port-direction', pPortData.Direction);
209
+ tmpElement.setAttribute('data-element-type', 'port');
210
+ return tmpElement;
211
+ }
212
+
213
+ /**
214
+ * Create a panel indicator SVG element.
215
+ * @param {string} pNodeHash
216
+ * @param {number} pX
217
+ * @param {number} pY
218
+ * @param {number} pWidth
219
+ * @param {number} pHeight
220
+ * @returns {SVGRectElement}
221
+ */
222
+ createPanelIndicatorElement(pNodeHash, pX, pY, pWidth, pHeight)
223
+ {
224
+ let tmpConfig = this._DefaultShapes['panel-indicator'];
225
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
226
+ tmpElement.setAttribute('class', tmpConfig.ClassName);
227
+ tmpElement.setAttribute('x', String(pX));
228
+ tmpElement.setAttribute('y', String(pY));
229
+ tmpElement.setAttribute('width', String(pWidth));
230
+ tmpElement.setAttribute('height', String(pHeight));
231
+ // Apply config attributes (rx, ry, etc.)
232
+ for (let tmpKey in tmpConfig.Attributes)
233
+ {
234
+ tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
235
+ }
236
+ tmpElement.setAttribute('data-node-hash', pNodeHash);
237
+ tmpElement.setAttribute('data-element-type', 'panel-indicator');
238
+ return tmpElement;
239
+ }
240
+
241
+ /**
242
+ * Create a visible connection path SVG element.
243
+ * @param {string} pPath - The SVG path d-string
244
+ * @param {string} pConnectionHash
245
+ * @param {boolean} pIsSelected
246
+ * @param {string} pViewIdentifier
247
+ * @returns {SVGPathElement}
248
+ */
249
+ createConnectionPathElement(pPath, pConnectionHash, pIsSelected, pViewIdentifier)
250
+ {
251
+ let tmpConfig = this._DefaultShapes['connection-path'];
252
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
253
+ tmpElement.setAttribute('class', tmpConfig.ClassName + (pIsSelected ? ' selected' : ''));
254
+ tmpElement.setAttribute('d', pPath);
255
+ tmpElement.setAttribute('data-connection-hash', pConnectionHash);
256
+ tmpElement.setAttribute('data-element-type', 'connection');
257
+
258
+ // Arrow marker
259
+ let tmpMarkerConfig = pIsSelected
260
+ ? this._DefaultShapes['arrowhead-connection-selected']
261
+ : this._DefaultShapes['arrowhead-connection'];
262
+ // The marker id follows the naming convention used in generateMarkerDefs
263
+ let tmpMarkerId = pIsSelected
264
+ ? ('flow-arrowhead-selected-' + pViewIdentifier)
265
+ : ('flow-arrowhead-' + pViewIdentifier);
266
+ tmpElement.setAttribute('marker-end', 'url(#' + tmpMarkerId + ')');
267
+
268
+ return tmpElement;
269
+ }
270
+
271
+ /**
272
+ * Create a connection hit area SVG element (wider invisible path for click targeting).
273
+ * @param {string} pPath - The SVG path d-string
274
+ * @param {string} pConnectionHash
275
+ * @returns {SVGPathElement}
276
+ */
277
+ createConnectionHitAreaElement(pPath, pConnectionHash)
278
+ {
279
+ let tmpConfig = this._DefaultShapes['connection-hitarea'];
280
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
281
+ tmpElement.setAttribute('class', tmpConfig.ClassName);
282
+ tmpElement.setAttribute('d', pPath);
283
+ tmpElement.setAttribute('data-connection-hash', pConnectionHash);
284
+ tmpElement.setAttribute('data-element-type', 'connection-hitarea');
285
+ return tmpElement;
286
+ }
287
+
288
+ /**
289
+ * Create a drag handle circle element.
290
+ * Works for both connection handles and tether handles.
291
+ * @param {string} pOwnerHash - Connection hash or panel hash
292
+ * @param {string} pHandleType - e.g. 'ortho-corner1', 'bezier-midpoint'
293
+ * @param {number} pX
294
+ * @param {number} pY
295
+ * @param {string} pShapeKey - 'connection-handle', 'connection-handle-midpoint', 'tether-handle', 'tether-handle-midpoint'
296
+ * @returns {SVGCircleElement}
297
+ */
298
+ createHandleElement(pOwnerHash, pHandleType, pX, pY, pShapeKey)
299
+ {
300
+ let tmpConfig = this._DefaultShapes[pShapeKey] || this._DefaultShapes['connection-handle'];
301
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
302
+ tmpElement.setAttribute('class', tmpConfig.ClassName);
303
+ tmpElement.setAttribute('cx', String(pX));
304
+ tmpElement.setAttribute('cy', String(pY));
305
+ // Apply config attributes (r, etc.)
306
+ for (let tmpKey in tmpConfig.Attributes)
307
+ {
308
+ tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
309
+ }
310
+ tmpElement.setAttribute('data-handle-type', pHandleType);
311
+ return tmpElement;
312
+ }
313
+
314
+ /**
315
+ * Create a temporary drag connection path element.
316
+ * @param {string} pPath - The SVG path d-string
317
+ * @returns {SVGPathElement}
318
+ */
319
+ createDragConnectionElement(pPath)
320
+ {
321
+ let tmpConfig = this._DefaultShapes['drag-connection'];
322
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
323
+ tmpElement.setAttribute('class', tmpConfig.ClassName);
324
+ tmpElement.setAttribute('d', pPath);
325
+ return tmpElement;
326
+ }
327
+
328
+ /**
329
+ * Create a visible tether path SVG element.
330
+ * @param {string} pPath - The SVG path d-string
331
+ * @param {string} pPanelHash
332
+ * @param {boolean} pIsSelected
333
+ * @param {string} pViewIdentifier
334
+ * @returns {SVGPathElement}
335
+ */
336
+ createTetherPathElement(pPath, pPanelHash, pIsSelected, pViewIdentifier)
337
+ {
338
+ let tmpConfig = this._DefaultShapes['tether-path'];
339
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
340
+ tmpElement.setAttribute('class', tmpConfig.ClassName + (pIsSelected ? ' selected' : ''));
341
+ tmpElement.setAttribute('d', pPath);
342
+ tmpElement.setAttribute('marker-end', 'url(#flow-tether-arrowhead-' + pViewIdentifier + ')');
343
+ tmpElement.setAttribute('data-element-type', 'tether');
344
+ tmpElement.setAttribute('data-panel-hash', pPanelHash);
345
+ return tmpElement;
346
+ }
347
+
348
+ /**
349
+ * Create a tether hit area SVG element.
350
+ * @param {string} pPath - The SVG path d-string
351
+ * @param {string} pPanelHash
352
+ * @returns {SVGPathElement}
353
+ */
354
+ createTetherHitAreaElement(pPath, pPanelHash)
355
+ {
356
+ let tmpConfig = this._DefaultShapes['tether-hitarea'];
357
+ let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
358
+ tmpElement.setAttribute('class', tmpConfig.ClassName);
359
+ tmpElement.setAttribute('d', pPath);
360
+ tmpElement.setAttribute('data-element-type', 'tether-hitarea');
361
+ tmpElement.setAttribute('data-panel-hash', pPanelHash);
362
+ return tmpElement;
363
+ }
364
+
365
+ /**
366
+ * Generate SVG marker definition markup for all arrowhead types.
367
+ * Returns raw SVG markup to be injected into the <defs> section.
368
+ * @param {string} pViewIdentifier
369
+ * @returns {string}
370
+ */
371
+ generateMarkerDefs(pViewIdentifier)
372
+ {
373
+ let tmpConnectionMarker = this._DefaultShapes['arrowhead-connection'];
374
+ let tmpSelectedMarker = this._DefaultShapes['arrowhead-connection-selected'];
375
+ let tmpTetherMarker = this._DefaultShapes['arrowhead-tether'];
376
+
377
+ let tmpMarkup = '';
378
+
379
+ // Normal connection arrowhead
380
+ tmpMarkup += '<marker id="flow-arrowhead-' + pViewIdentifier + '"'
381
+ + ' markerWidth="' + tmpConnectionMarker.MarkerWidth + '"'
382
+ + ' markerHeight="' + tmpConnectionMarker.MarkerHeight + '"'
383
+ + ' refX="' + tmpConnectionMarker.RefX + '"'
384
+ + ' refY="' + tmpConnectionMarker.RefY + '"'
385
+ + ' orient="auto" markerUnits="strokeWidth">'
386
+ + '<polygon points="' + tmpConnectionMarker.Points + '" fill="' + tmpConnectionMarker.Fill + '" />'
387
+ + '</marker>';
388
+
389
+ // Selected connection arrowhead
390
+ tmpMarkup += '<marker id="flow-arrowhead-selected-' + pViewIdentifier + '"'
391
+ + ' markerWidth="' + tmpSelectedMarker.MarkerWidth + '"'
392
+ + ' markerHeight="' + tmpSelectedMarker.MarkerHeight + '"'
393
+ + ' refX="' + tmpSelectedMarker.RefX + '"'
394
+ + ' refY="' + tmpSelectedMarker.RefY + '"'
395
+ + ' orient="auto" markerUnits="strokeWidth">'
396
+ + '<polygon points="' + tmpSelectedMarker.Points + '" fill="' + tmpSelectedMarker.Fill + '" />'
397
+ + '</marker>';
398
+
399
+ // Tether arrowhead
400
+ tmpMarkup += '<marker id="flow-tether-arrowhead-' + pViewIdentifier + '"'
401
+ + ' markerWidth="' + tmpTetherMarker.MarkerWidth + '"'
402
+ + ' markerHeight="' + tmpTetherMarker.MarkerHeight + '"'
403
+ + ' refX="' + tmpTetherMarker.RefX + '"'
404
+ + ' refY="' + tmpTetherMarker.RefY + '"'
405
+ + ' orient="auto" markerUnits="strokeWidth">'
406
+ + '<polygon points="' + tmpTetherMarker.Points + '" fill="' + tmpTetherMarker.Fill + '" />'
407
+ + '</marker>';
408
+
409
+ return tmpMarkup;
410
+ }
411
+ }
412
+
413
+ module.exports = PictProviderFlowConnectorShapes;
@@ -6,6 +6,16 @@ const libFableServiceProviderBase = require('fable-serviceproviderbase');
6
6
  * Shared geometry utilities for the flow diagram.
7
7
  * Provides direction vectors and edge center calculations used by
8
8
  * connections, tethers, and other flow components.
9
+ *
10
+ * Port Side values (12 positions):
11
+ *
12
+ * Top edge: 'top-left' 'top' 'top-right'
13
+ * Left edge: 'left-top' 'left' 'left-bottom'
14
+ * Right edge: 'right-top' 'right' 'right-bottom'
15
+ * Bottom edge: 'bottom-left' 'bottom' 'bottom-right'
16
+ *
17
+ * The old 4-value sides ('left', 'right', 'top', 'bottom') map to
18
+ * the middle position on each edge for backward compatibility.
9
19
  */
10
20
  class PictProviderFlowGeometry extends libFableServiceProviderBase
11
21
  {
@@ -16,15 +26,55 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
16
26
  this.serviceType = 'PictProviderFlowGeometry';
17
27
  }
18
28
 
29
+ /**
30
+ * Extract the edge name from a Side value.
31
+ *
32
+ * Maps all 12 positions (and the 4 legacy values) back to
33
+ * the edge they sit on: 'left', 'right', 'top', or 'bottom'.
34
+ *
35
+ * @param {string} pSide - Any valid Side value
36
+ * @returns {string} The edge: 'left', 'right', 'top', or 'bottom'
37
+ */
38
+ getEdgeFromSide(pSide)
39
+ {
40
+ switch (pSide)
41
+ {
42
+ case 'left-top':
43
+ case 'left':
44
+ case 'left-bottom':
45
+ return 'left';
46
+
47
+ case 'right-top':
48
+ case 'right':
49
+ case 'right-bottom':
50
+ return 'right';
51
+
52
+ case 'top-left':
53
+ case 'top':
54
+ case 'top-right':
55
+ return 'top';
56
+
57
+ case 'bottom-left':
58
+ case 'bottom':
59
+ case 'bottom-right':
60
+ return 'bottom';
61
+
62
+ default:
63
+ return 'right';
64
+ }
65
+ }
66
+
19
67
  /**
20
68
  * Get the outward unit direction vector for a given side.
21
69
  *
22
- * @param {string} pSide - 'left', 'right', 'top', or 'bottom'
70
+ * All positions on the same edge share the same direction vector.
71
+ *
72
+ * @param {string} pSide - Any valid Side value (12 positions or 4 legacy)
23
73
  * @returns {{dx: number, dy: number}}
24
74
  */
25
75
  sideDirection(pSide)
26
76
  {
27
- switch (pSide)
77
+ switch (this.getEdgeFromSide(pSide))
28
78
  {
29
79
  case 'left': return { dx: -1, dy: 0 };
30
80
  case 'right': return { dx: 1, dy: 0 };
@@ -59,6 +109,90 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
59
109
  return { x: pRectData.X + pRectData.Width, y: pRectData.Y + pRectData.Height / 2 };
60
110
  }
61
111
  }
112
+
113
+ /**
114
+ * Calculate a port's local position relative to node origin.
115
+ *
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.
120
+ *
121
+ * Multiple ports sharing the same Side value distribute evenly within
122
+ * their zone.
123
+ *
124
+ * @param {string} pSide - Side value (any of 12 positions or 4 legacy)
125
+ * @param {number} pIndex - Index of this port within its Side group
126
+ * @param {number} pTotal - Total ports with this Side value
127
+ * @param {number} pWidth - Node width
128
+ * @param {number} pHeight - Node height
129
+ * @param {number} pTitleBarHeight - Height of the node title bar
130
+ * @returns {{x: number, y: number}}
131
+ */
132
+ getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight)
133
+ {
134
+ let tmpEdge = this.getEdgeFromSide(pSide);
135
+ let tmpZone = this._getZoneFromSide(pSide);
136
+
137
+ if (tmpEdge === 'left' || tmpEdge === 'right')
138
+ {
139
+ let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
140
+ let tmpBodyHeight = pHeight - pTitleBarHeight;
141
+ let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
142
+ let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
143
+ let tmpSpacing = tmpZoneHeight / (pTotal + 1);
144
+ let tmpY = tmpZoneStart + tmpSpacing * (pIndex + 1);
145
+ return { x: tmpX, y: tmpY };
146
+ }
147
+
148
+ // top or bottom
149
+ let tmpY = (tmpEdge === 'top') ? 0 : pHeight;
150
+ let tmpZoneStart = pWidth * tmpZone.start;
151
+ let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
152
+ let tmpSpacing = tmpZoneWidth / (pTotal + 1);
153
+ let tmpX = tmpZoneStart + tmpSpacing * (pIndex + 1);
154
+ return { x: tmpX, y: tmpY };
155
+ }
156
+
157
+ /**
158
+ * Get the zone fraction (start, end) for a Side value.
159
+ *
160
+ * Each edge is divided into three zones of equal size:
161
+ * start: 0.0 — 0.333
162
+ * middle: 0.333 — 0.667
163
+ * end: 0.667 — 1.0
164
+ *
165
+ * @param {string} pSide
166
+ * @returns {{start: number, end: number}}
167
+ */
168
+ _getZoneFromSide(pSide)
169
+ {
170
+ switch (pSide)
171
+ {
172
+ // Left edge: top, middle, bottom
173
+ case 'left-top': return { start: 0.0, end: 0.333 };
174
+ case 'left': return { start: 0.333, end: 0.667 };
175
+ case 'left-bottom': return { start: 0.667, end: 1.0 };
176
+
177
+ // Right edge: top, middle, bottom
178
+ case 'right-top': return { start: 0.0, end: 0.333 };
179
+ case 'right': return { start: 0.333, end: 0.667 };
180
+ case 'right-bottom': return { start: 0.667, end: 1.0 };
181
+
182
+ // Top edge: left, middle, right
183
+ case 'top-left': return { start: 0.0, end: 0.333 };
184
+ case 'top': return { start: 0.333, end: 0.667 };
185
+ case 'top-right': return { start: 0.667, end: 1.0 };
186
+
187
+ // Bottom edge: left, middle, right
188
+ case 'bottom-left': return { start: 0.0, end: 0.333 };
189
+ case 'bottom': return { start: 0.333, end: 0.667 };
190
+ case 'bottom-right': return { start: 0.667, end: 1.0 };
191
+
192
+ // Fallback: full range (legacy behavior)
193
+ default: return { start: 0.0, end: 1.0 };
194
+ }
195
+ }
62
196
  }
63
197
 
64
198
  module.exports = PictProviderFlowGeometry;