pict-section-flow 0.0.2 → 0.0.3

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 (38) 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 +2 -2
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +49 -1
  20. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  21. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  22. package/source/providers/PictProvider-Flow-Geometry.js +43 -0
  23. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  24. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  25. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  26. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  27. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  28. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  29. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  30. package/source/services/PictService-Flow-PanelManager.js +188 -0
  31. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  32. package/source/services/PictService-Flow-Tether.js +52 -25
  33. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  34. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  35. package/source/views/PictView-Flow-Node.js +654 -169
  36. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  37. package/source/views/PictView-Flow-Toolbar.js +846 -379
  38. 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;
@@ -59,6 +59,49 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
59
59
  return { x: pRectData.X + pRectData.Width, y: pRectData.Y + pRectData.Height / 2 };
60
60
  }
61
61
  }
62
+
63
+ /**
64
+ * Calculate a port's local position relative to node origin.
65
+ *
66
+ * For left and right side ports, positioning is offset below the title bar
67
+ * so that ports never overlap the header area.
68
+ *
69
+ * @param {string} pSide - 'left', 'right', 'top', 'bottom'
70
+ * @param {number} pIndex - Index of this port on its side
71
+ * @param {number} pTotal - Total ports on this side
72
+ * @param {number} pWidth - Node width
73
+ * @param {number} pHeight - Node height
74
+ * @param {number} pTitleBarHeight - Height of the node title bar
75
+ * @returns {{x: number, y: number}}
76
+ */
77
+ getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight)
78
+ {
79
+ let tmpSpacing;
80
+
81
+ switch (pSide)
82
+ {
83
+ case 'left':
84
+ {
85
+ let tmpBodyHeight = pHeight - pTitleBarHeight;
86
+ tmpSpacing = tmpBodyHeight / (pTotal + 1);
87
+ return { x: 0, y: pTitleBarHeight + tmpSpacing * (pIndex + 1) };
88
+ }
89
+ case 'right':
90
+ {
91
+ let tmpBodyHeight = pHeight - pTitleBarHeight;
92
+ tmpSpacing = tmpBodyHeight / (pTotal + 1);
93
+ return { x: pWidth, y: pTitleBarHeight + tmpSpacing * (pIndex + 1) };
94
+ }
95
+ case 'top':
96
+ tmpSpacing = pWidth / (pTotal + 1);
97
+ return { x: tmpSpacing * (pIndex + 1), y: 0 };
98
+ case 'bottom':
99
+ tmpSpacing = pWidth / (pTotal + 1);
100
+ return { x: tmpSpacing * (pIndex + 1), y: pHeight };
101
+ default:
102
+ return { x: pWidth, y: pHeight / 2 };
103
+ }
104
+ }
62
105
  }
63
106
 
64
107
  module.exports = PictProviderFlowGeometry;