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,338 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-DataManager
5
+ *
6
+ * Manages flow data lifecycle: serialization, deserialization, and CRUD
7
+ * operations for nodes and connections.
8
+ *
9
+ * Extracted from PictView-Flow.js to keep the view focused on
10
+ * coordination and lifecycle rather than data manipulation.
11
+ */
12
+ class PictServiceFlowDataManager extends libFableServiceProviderBase
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+
18
+ this.serviceType = 'PictServiceFlowDataManager';
19
+
20
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
21
+ }
22
+
23
+ // ---- Marshaling ----
24
+
25
+ /**
26
+ * Marshal data from AppData into the flow view
27
+ */
28
+ marshalToView()
29
+ {
30
+ if (!this._FlowView) return;
31
+
32
+ if (this._FlowView.options.FlowDataAddress)
33
+ {
34
+ const tmpAddressSpace =
35
+ {
36
+ Fable: this._FlowView.fable,
37
+ Pict: this._FlowView.pict || this._FlowView.fable,
38
+ AppData: this._FlowView.pict ? this._FlowView.pict.AppData : this._FlowView.fable.AppData,
39
+ Bundle: this._FlowView.Bundle,
40
+ Options: this._FlowView.options
41
+ };
42
+ let tmpData = this._FlowView.fable.manifest.getValueByHash(tmpAddressSpace, this._FlowView.options.FlowDataAddress);
43
+ if (typeof tmpData === 'object' && tmpData !== null)
44
+ {
45
+ this.setFlowData(tmpData);
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Marshal data from the flow view back to AppData
52
+ */
53
+ marshalFromView()
54
+ {
55
+ if (!this._FlowView) return;
56
+
57
+ if (this._FlowView.options.FlowDataAddress)
58
+ {
59
+ const tmpAddressSpace =
60
+ {
61
+ Fable: this._FlowView.fable,
62
+ Pict: this._FlowView.pict || this._FlowView.fable,
63
+ AppData: this._FlowView.pict ? this._FlowView.pict.AppData : this._FlowView.fable.AppData,
64
+ Bundle: this._FlowView.Bundle,
65
+ Options: this._FlowView.options
66
+ };
67
+ this._FlowView.fable.manifest.setValueByHash(tmpAddressSpace, this._FlowView.options.FlowDataAddress, JSON.parse(JSON.stringify(this._FlowView._FlowData)));
68
+ }
69
+ }
70
+
71
+ // ---- Flow Data Get/Set ----
72
+
73
+ /**
74
+ * Get the complete flow data object
75
+ * @returns {Object} The flow data including nodes, connections, and view state
76
+ */
77
+ getFlowData()
78
+ {
79
+ if (!this._FlowView) return {};
80
+ return JSON.parse(JSON.stringify(this._FlowView._FlowData));
81
+ }
82
+
83
+ /**
84
+ * Set the complete flow data object and re-render
85
+ * @param {Object} pFlowData - The flow data to set
86
+ */
87
+ setFlowData(pFlowData)
88
+ {
89
+ if (!this._FlowView) return;
90
+
91
+ if (typeof pFlowData !== 'object' || pFlowData === null)
92
+ {
93
+ this._FlowView.log.warn('PictSectionFlow setFlowData received invalid data');
94
+ return;
95
+ }
96
+
97
+ this._FlowView._FlowData = {
98
+ Nodes: Array.isArray(pFlowData.Nodes) ? pFlowData.Nodes : [],
99
+ Connections: Array.isArray(pFlowData.Connections) ? pFlowData.Connections : [],
100
+ OpenPanels: Array.isArray(pFlowData.OpenPanels) ? pFlowData.OpenPanels : [],
101
+ SavedLayouts: Array.isArray(pFlowData.SavedLayouts) ? pFlowData.SavedLayouts : [],
102
+ ViewState: Object.assign(
103
+ { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null, SelectedTetherHash: null },
104
+ pFlowData.ViewState || {}
105
+ )
106
+ };
107
+
108
+ // Merge any browser-persisted layouts into the newly loaded data
109
+ if (this._FlowView._LayoutProvider)
110
+ {
111
+ this._FlowView._LayoutProvider.loadPersistedLayouts();
112
+ }
113
+
114
+ if (this._FlowView.initialRenderComplete)
115
+ {
116
+ this._FlowView.renderFlow();
117
+ }
118
+ }
119
+
120
+ // ---- Node CRUD ----
121
+
122
+ /**
123
+ * Add a new node to the flow
124
+ * @param {string} pType - The node type hash
125
+ * @param {number} pX - X position
126
+ * @param {number} pY - Y position
127
+ * @param {string} [pTitle] - Optional title
128
+ * @param {Object} [pData] - Optional additional data
129
+ * @returns {Object} The created node
130
+ */
131
+ addNode(pType, pX, pY, pTitle, pData)
132
+ {
133
+ if (!this._FlowView) return null;
134
+
135
+ let tmpType = pType || this._FlowView.options.DefaultNodeType;
136
+ let tmpNodeTypeConfig = this._FlowView._NodeTypeProvider.getNodeType(tmpType);
137
+
138
+ let tmpNodeHash = `node-${this._FlowView.fable.getUUID()}`;
139
+ let tmpNode =
140
+ {
141
+ Hash: tmpNodeHash,
142
+ Type: tmpType,
143
+ X: pX || 100,
144
+ Y: pY || 100,
145
+ Width: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultWidth) || this._FlowView.options.DefaultNodeWidth,
146
+ Height: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultHeight) || this._FlowView.options.DefaultNodeHeight,
147
+ Title: pTitle || (tmpNodeTypeConfig && tmpNodeTypeConfig.Label) || 'New Node',
148
+ Ports: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultPorts)
149
+ ? JSON.parse(JSON.stringify(tmpNodeTypeConfig.DefaultPorts))
150
+ : [
151
+ { Hash: `port-in-${this._FlowView.fable.getUUID()}`, Direction: 'input', Side: 'left', Label: 'In' },
152
+ { Hash: `port-out-${this._FlowView.fable.getUUID()}`, Direction: 'output', Side: 'right', Label: 'Out' }
153
+ ],
154
+ Data: pData || {}
155
+ };
156
+
157
+ // Ensure each port has a unique hash
158
+ for (let i = 0; i < tmpNode.Ports.length; i++)
159
+ {
160
+ if (!tmpNode.Ports[i].Hash)
161
+ {
162
+ tmpNode.Ports[i].Hash = `port-${tmpNode.Ports[i].Direction}-${this._FlowView.fable.getUUID()}`;
163
+ }
164
+ }
165
+
166
+ this._FlowView._FlowData.Nodes.push(tmpNode);
167
+ this._FlowView.renderFlow();
168
+ this.marshalFromView();
169
+
170
+ if (this._FlowView._EventHandlerProvider)
171
+ {
172
+ this._FlowView._EventHandlerProvider.fireEvent('onNodeAdded', tmpNode);
173
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
174
+ }
175
+
176
+ return tmpNode;
177
+ }
178
+
179
+ /**
180
+ * Remove a node and all its connections
181
+ * @param {string} pNodeHash - The hash of the node to remove
182
+ * @returns {boolean} Whether the node was removed
183
+ */
184
+ removeNode(pNodeHash)
185
+ {
186
+ if (!this._FlowView) return false;
187
+
188
+ let tmpNodeIndex = this._FlowView._FlowData.Nodes.findIndex((pNode) => pNode.Hash === pNodeHash);
189
+ if (tmpNodeIndex < 0)
190
+ {
191
+ this._FlowView.log.warn(`PictSectionFlow removeNode: node ${pNodeHash} not found`);
192
+ return false;
193
+ }
194
+
195
+ let tmpRemovedNode = this._FlowView._FlowData.Nodes.splice(tmpNodeIndex, 1)[0];
196
+
197
+ // Remove all connections involving this node
198
+ this._FlowView._FlowData.Connections = this._FlowView._FlowData.Connections.filter((pConnection) =>
199
+ {
200
+ return pConnection.SourceNodeHash !== pNodeHash && pConnection.TargetNodeHash !== pNodeHash;
201
+ });
202
+
203
+ // Close any open panels for this node
204
+ this._FlowView.closePanelForNode(pNodeHash);
205
+
206
+ // Clear selection if this node was selected
207
+ if (this._FlowView._FlowData.ViewState.SelectedNodeHash === pNodeHash)
208
+ {
209
+ this._FlowView._FlowData.ViewState.SelectedNodeHash = null;
210
+ }
211
+
212
+ this._FlowView.renderFlow();
213
+ this.marshalFromView();
214
+
215
+ if (this._FlowView._EventHandlerProvider)
216
+ {
217
+ this._FlowView._EventHandlerProvider.fireEvent('onNodeRemoved', tmpRemovedNode);
218
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
219
+ }
220
+
221
+ return true;
222
+ }
223
+
224
+ // ---- Connection CRUD ----
225
+
226
+ /**
227
+ * Add a connection between two ports
228
+ * @param {string} pSourceNodeHash
229
+ * @param {string} pSourcePortHash
230
+ * @param {string} pTargetNodeHash
231
+ * @param {string} pTargetPortHash
232
+ * @param {Object} [pData] - Optional additional data
233
+ * @returns {Object|false} The created connection, or false if invalid
234
+ */
235
+ addConnection(pSourceNodeHash, pSourcePortHash, pTargetNodeHash, pTargetPortHash, pData)
236
+ {
237
+ if (!this._FlowView) return false;
238
+
239
+ // Validate that both nodes and ports exist
240
+ let tmpSourceNode = this._FlowView._FlowData.Nodes.find((pNode) => pNode.Hash === pSourceNodeHash);
241
+ let tmpTargetNode = this._FlowView._FlowData.Nodes.find((pNode) => pNode.Hash === pTargetNodeHash);
242
+
243
+ if (!tmpSourceNode || !tmpTargetNode)
244
+ {
245
+ this._FlowView.log.warn('PictSectionFlow addConnection: source or target node not found');
246
+ return false;
247
+ }
248
+
249
+ let tmpSourcePort = tmpSourceNode.Ports.find((pPort) => pPort.Hash === pSourcePortHash);
250
+ let tmpTargetPort = tmpTargetNode.Ports.find((pPort) => pPort.Hash === pTargetPortHash);
251
+
252
+ if (!tmpSourcePort || !tmpTargetPort)
253
+ {
254
+ this._FlowView.log.warn('PictSectionFlow addConnection: source or target port not found');
255
+ return false;
256
+ }
257
+
258
+ // Prevent self-connections
259
+ if (pSourceNodeHash === pTargetNodeHash)
260
+ {
261
+ this._FlowView.log.warn('PictSectionFlow addConnection: cannot connect a node to itself');
262
+ return false;
263
+ }
264
+
265
+ // Check for duplicate connections
266
+ let tmpDuplicate = this._FlowView._FlowData.Connections.find((pConn) =>
267
+ {
268
+ return pConn.SourceNodeHash === pSourceNodeHash
269
+ && pConn.SourcePortHash === pSourcePortHash
270
+ && pConn.TargetNodeHash === pTargetNodeHash
271
+ && pConn.TargetPortHash === pTargetPortHash;
272
+ });
273
+ if (tmpDuplicate)
274
+ {
275
+ this._FlowView.log.warn('PictSectionFlow addConnection: duplicate connection');
276
+ return false;
277
+ }
278
+
279
+ let tmpConnection =
280
+ {
281
+ Hash: `conn-${this._FlowView.fable.getUUID()}`,
282
+ SourceNodeHash: pSourceNodeHash,
283
+ SourcePortHash: pSourcePortHash,
284
+ TargetNodeHash: pTargetNodeHash,
285
+ TargetPortHash: pTargetPortHash,
286
+ Data: pData || {}
287
+ };
288
+
289
+ this._FlowView._FlowData.Connections.push(tmpConnection);
290
+ this._FlowView.renderFlow();
291
+ this.marshalFromView();
292
+
293
+ if (this._FlowView._EventHandlerProvider)
294
+ {
295
+ this._FlowView._EventHandlerProvider.fireEvent('onConnectionCreated', tmpConnection);
296
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
297
+ }
298
+
299
+ return tmpConnection;
300
+ }
301
+
302
+ /**
303
+ * Remove a connection
304
+ * @param {string} pConnectionHash - The hash of the connection to remove
305
+ * @returns {boolean} Whether the connection was removed
306
+ */
307
+ removeConnection(pConnectionHash)
308
+ {
309
+ if (!this._FlowView) return false;
310
+
311
+ let tmpConnectionIndex = this._FlowView._FlowData.Connections.findIndex((pConn) => pConn.Hash === pConnectionHash);
312
+ if (tmpConnectionIndex < 0)
313
+ {
314
+ this._FlowView.log.warn(`PictSectionFlow removeConnection: connection ${pConnectionHash} not found`);
315
+ return false;
316
+ }
317
+
318
+ let tmpRemovedConnection = this._FlowView._FlowData.Connections.splice(tmpConnectionIndex, 1)[0];
319
+
320
+ if (this._FlowView._FlowData.ViewState.SelectedConnectionHash === pConnectionHash)
321
+ {
322
+ this._FlowView._FlowData.ViewState.SelectedConnectionHash = null;
323
+ }
324
+
325
+ this._FlowView.renderFlow();
326
+ this.marshalFromView();
327
+
328
+ if (this._FlowView._EventHandlerProvider)
329
+ {
330
+ this._FlowView._EventHandlerProvider.fireEvent('onConnectionRemoved', tmpRemovedConnection);
331
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
332
+ }
333
+
334
+ return true;
335
+ }
336
+ }
337
+
338
+ module.exports = PictServiceFlowDataManager;
@@ -10,7 +10,8 @@ const INTERACTION_STATES =
10
10
  DRAGGING_PANEL: 'dragging-panel',
11
11
  DRAGGING_HANDLE: 'dragging-handle',
12
12
  CONNECTING: 'connecting',
13
- PANNING: 'panning'
13
+ PANNING: 'panning',
14
+ RESIZING_PANEL: 'resizing-panel'
14
15
  };
15
16
 
16
17
  class PictServiceFlowInteractionManager extends libFableServiceProviderBase
@@ -65,6 +66,15 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
65
66
  this._LastClickNodeHash = null;
66
67
  this._DoubleClickThreshold = 400;
67
68
 
69
+ // Panel resize state
70
+ this._ResizePanelHash = null;
71
+ this._ResizeStartY = 0;
72
+ this._ResizePanelStartHeight = 0;
73
+
74
+ // Double-click detection for connections
75
+ this._LastConnectionClickTime = 0;
76
+ this._LastConnectionClickHash = null;
77
+
68
78
  // Double-click detection for handles
69
79
  this._LastHandleClickTime = 0;
70
80
  this._LastHandleClickHash = null;
@@ -100,10 +110,25 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
100
110
  // Keyboard events for delete
101
111
  document.addEventListener('keydown', this._boundOnKeyDown);
102
112
 
103
- // Prevent context menu on right-click
113
+ // Handle right-click: add/remove bezier handles on connections
104
114
  this._SVGElement.addEventListener('contextmenu', (pEvent) =>
105
115
  {
106
116
  pEvent.preventDefault();
117
+
118
+ let tmpTarget = pEvent.target;
119
+ let tmpElementType = this._getElementType(tmpTarget);
120
+
121
+ switch (tmpElementType)
122
+ {
123
+ case 'connection':
124
+ case 'connection-hitarea':
125
+ this._addBezierHandle(tmpTarget, pEvent);
126
+ break;
127
+
128
+ case 'connection-handle':
129
+ this._removeBezierHandle(tmpTarget);
130
+ break;
131
+ }
107
132
  });
108
133
  }
109
134
 
@@ -135,14 +160,18 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
135
160
  let tmpTarget = pEvent.target;
136
161
  let tmpElementType = this._getElementType(tmpTarget);
137
162
 
138
- // Check if click is inside a panel body — let HTML handle its own events
139
- if (tmpTarget.closest && tmpTarget.closest('.pict-flow-panel-body'))
163
+ // Check if click is inside a panel content area — let HTML handle its own events
164
+ if (tmpTarget.closest && tmpTarget.closest('.pict-flow-panel-content'))
140
165
  {
141
166
  return;
142
167
  }
143
168
 
144
- // Capture pointer for smooth dragging
145
- this._SVGElement.setPointerCapture(pEvent.pointerId);
169
+ // Capture pointer for smooth dragging (left-click only — right-click
170
+ // needs the real target for contextmenu handle add/remove)
171
+ if (pEvent.button === 0)
172
+ {
173
+ this._SVGElement.setPointerCapture(pEvent.pointerId);
174
+ }
146
175
 
147
176
  switch (tmpElementType)
148
177
  {
@@ -180,6 +209,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
180
209
  this._startPanelDrag(pEvent, tmpTarget);
181
210
  break;
182
211
 
212
+ case 'panel-resize':
213
+ this._startPanelResize(pEvent, tmpTarget);
214
+ break;
215
+
183
216
  case 'panel-close':
184
217
  {
185
218
  let tmpPanelHash = this._getPanelHash(tmpTarget);
@@ -251,8 +284,26 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
251
284
 
252
285
  case 'connection':
253
286
  case 'connection-hitarea':
254
- this._selectConnection(tmpTarget);
287
+ {
288
+ let tmpConnectionHash = this._getConnectionHash(tmpTarget);
289
+ let tmpNow = Date.now();
290
+
291
+ // Check for double-click on same connection to add a handle
292
+ if (tmpConnectionHash && tmpConnectionHash === this._LastConnectionClickHash
293
+ && (tmpNow - this._LastConnectionClickTime) < this._DoubleClickThreshold)
294
+ {
295
+ this._LastConnectionClickTime = 0;
296
+ this._LastConnectionClickHash = null;
297
+ this._addBezierHandle(tmpTarget, pEvent);
298
+ }
299
+ else
300
+ {
301
+ this._LastConnectionClickTime = tmpNow;
302
+ this._LastConnectionClickHash = tmpConnectionHash;
303
+ this._selectConnection(tmpTarget);
304
+ }
255
305
  break;
306
+ }
256
307
 
257
308
  default:
258
309
  // Click on background - start panning or deselect
@@ -290,6 +341,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
290
341
  this._onConnectionDrag(pEvent);
291
342
  break;
292
343
 
344
+ case INTERACTION_STATES.RESIZING_PANEL:
345
+ this._onPanelResize(pEvent);
346
+ break;
347
+
293
348
  case INTERACTION_STATES.PANNING:
294
349
  this._onPan(pEvent);
295
350
  break;
@@ -324,6 +379,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
324
379
  this._endHandleDrag(pEvent);
325
380
  break;
326
381
 
382
+ case INTERACTION_STATES.RESIZING_PANEL:
383
+ this._endPanelResize(pEvent);
384
+ break;
385
+
327
386
  case INTERACTION_STATES.CONNECTING:
328
387
  this._endConnection(pEvent);
329
388
  break;
@@ -527,6 +586,67 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
527
586
  this._DragPanelHash = null;
528
587
  }
529
588
 
589
+ // ---- Panel Resizing ----
590
+
591
+ _startPanelResize(pEvent, pTarget)
592
+ {
593
+ let tmpPanelHash = this._getPanelHash(pTarget);
594
+ if (!tmpPanelHash) return;
595
+
596
+ let tmpPanel = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === tmpPanelHash);
597
+ if (!tmpPanel) return;
598
+
599
+ this._State = INTERACTION_STATES.RESIZING_PANEL;
600
+ this._ResizePanelHash = tmpPanelHash;
601
+ this._ResizeStartY = pEvent.clientY;
602
+ this._ResizePanelStartHeight = tmpPanel.Height;
603
+
604
+ this._SVGElement.classList.add('panning');
605
+ }
606
+
607
+ _onPanelResize(pEvent)
608
+ {
609
+ if (!this._ResizePanelHash) return;
610
+
611
+ let tmpVS = this._FlowView.viewState;
612
+ let tmpDY = (pEvent.clientY - this._ResizeStartY) / tmpVS.Zoom;
613
+ let tmpNewHeight = Math.max(120, this._ResizePanelStartHeight + tmpDY);
614
+
615
+ // Update the panel data
616
+ let tmpPanel = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === this._ResizePanelHash);
617
+ if (tmpPanel)
618
+ {
619
+ tmpPanel.Height = tmpNewHeight;
620
+ }
621
+
622
+ // Update the foreignObject height directly for smooth resizing
623
+ if (this._FlowView._PanelsLayer)
624
+ {
625
+ let tmpFO = this._FlowView._PanelsLayer.querySelector('[data-panel-hash="' + this._ResizePanelHash + '"]');
626
+ if (tmpFO)
627
+ {
628
+ tmpFO.setAttribute('height', String(tmpNewHeight));
629
+ }
630
+ }
631
+ }
632
+
633
+ _endPanelResize(pEvent)
634
+ {
635
+ this._SVGElement.classList.remove('panning');
636
+
637
+ // Re-render to sync tethers
638
+ this._FlowView.renderFlow();
639
+ this._FlowView.marshalFromView();
640
+
641
+ if (this._FlowView._EventHandlerProvider)
642
+ {
643
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView.flowData);
644
+ }
645
+
646
+ this._State = INTERACTION_STATES.IDLE;
647
+ this._ResizePanelHash = null;
648
+ }
649
+
530
650
  // ---- Handle Dragging ----
531
651
 
532
652
  _startHandleDrag(pEvent, pConnectionHash, pPanelHash, pHandleType, pIsTether)
@@ -601,6 +721,43 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
601
721
  this._DragHandleIsTether = false;
602
722
  }
603
723
 
724
+ // ---- Right-Click Handle Add/Remove ----
725
+
726
+ /**
727
+ * Add a bezier handle to a connection at the right-click position.
728
+ * @param {Element} pTarget - The SVG element that was right-clicked
729
+ * @param {MouseEvent} pEvent - The contextmenu event
730
+ */
731
+ _addBezierHandle(pTarget, pEvent)
732
+ {
733
+ let tmpConnectionHash = this._getConnectionHash(pTarget);
734
+ if (!tmpConnectionHash) return;
735
+
736
+ // Select the connection so handle circles are rendered after the re-render
737
+ this._FlowView.selectConnection(tmpConnectionHash);
738
+
739
+ let tmpCoords = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
740
+ this._FlowView.addConnectionHandle(tmpConnectionHash, tmpCoords.x, tmpCoords.y);
741
+ }
742
+
743
+ /**
744
+ * Remove a bezier handle from a connection.
745
+ * @param {Element} pTarget - The handle SVG element that was right-clicked
746
+ */
747
+ _removeBezierHandle(pTarget)
748
+ {
749
+ let tmpConnectionHash = this._getConnectionHash(pTarget);
750
+ if (!tmpConnectionHash) return;
751
+
752
+ let tmpHandleType = pTarget.getAttribute('data-handle-type');
753
+ if (!tmpHandleType || !tmpHandleType.startsWith('bezier-handle-')) return;
754
+
755
+ let tmpIndex = parseInt(tmpHandleType.replace('bezier-handle-', ''), 10);
756
+ if (isNaN(tmpIndex)) return;
757
+
758
+ this._FlowView.removeConnectionHandle(tmpConnectionHash, tmpIndex);
759
+ }
760
+
604
761
  // ---- Line Mode Toggling ----
605
762
 
606
763
  _toggleConnectionLineMode(pConnectionHash)
@@ -615,6 +772,7 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
615
772
 
616
773
  // Reset handle positions when switching modes
617
774
  tmpConnection.Data.HandleCustomized = false;
775
+ tmpConnection.Data.BezierHandles = [];
618
776
  tmpConnection.Data.BezierHandleX = null;
619
777
  tmpConnection.Data.BezierHandleY = null;
620
778
  tmpConnection.Data.OrthoCorner1X = null;