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.
- package/.claude/launch.json +1 -1
- package/README.md +176 -0
- package/docs/.nojekyll +0 -0
- package/docs/Architecture.md +303 -0
- package/docs/Custom-Styling.md +275 -0
- package/docs/Data_Model.md +158 -0
- package/docs/Event_System.md +156 -0
- package/docs/Getting_Started.md +237 -0
- package/docs/Implementation_Reference.md +528 -0
- package/docs/Layout_Persistence.md +117 -0
- package/docs/README.md +115 -52
- package/docs/_cover.md +11 -0
- package/docs/_sidebar.md +52 -0
- package/docs/_topbar.md +8 -0
- package/docs/api/PictFlowCard.md +216 -0
- package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
- package/docs/api/addConnection.md +101 -0
- package/docs/api/addNode.md +137 -0
- package/docs/api/autoLayout.md +77 -0
- package/docs/api/getFlowData.md +112 -0
- package/docs/api/marshalToView.md +95 -0
- package/docs/api/openPanel.md +128 -0
- package/docs/api/registerHandler.md +174 -0
- package/docs/api/registerNodeType.md +142 -0
- package/docs/api/removeConnection.md +57 -0
- package/docs/api/removeNode.md +80 -0
- package/docs/api/saveLayout.md +152 -0
- package/docs/api/screenToSVGCoords.md +68 -0
- package/docs/api/selectNode.md +116 -0
- package/docs/api/setTheme.md +168 -0
- package/docs/api/setZoom.md +97 -0
- package/docs/api/toggleFullscreen.md +68 -0
- package/docs/card-help/EACH.md +19 -0
- package/docs/card-help/FREAD.md +24 -0
- package/docs/card-help/FWRITE.md +24 -0
- package/docs/card-help/GET.md +22 -0
- package/docs/card-help/ITE.md +23 -0
- package/docs/card-help/LOG.md +23 -0
- package/docs/card-help/NOTE.md +17 -0
- package/docs/card-help/PREV.md +18 -0
- package/docs/card-help/SET.md +27 -0
- package/docs/card-help/SPKL.md +22 -0
- package/docs/card-help/STAT.md +23 -0
- package/docs/card-help/SW.md +25 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +169 -0
- package/docs/retold-keyword-index.json +13942 -0
- package/example_applications/simple_cards/package.json +1 -0
- package/example_applications/simple_cards/source/card-help-content.js +16 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
- package/package.json +11 -7
- package/scripts/generate-card-help.js +214 -0
- package/source/Pict-Section-Flow.js +4 -0
- package/source/PictFlowCard.js +3 -1
- package/source/providers/PictProvider-Flow-CSS.js +245 -152
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
- package/source/providers/PictProvider-Flow-Geometry.js +195 -38
- package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
- package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
- package/source/services/PictService-Flow-DataManager.js +338 -0
- package/source/services/PictService-Flow-InteractionManager.js +165 -7
- package/source/services/PictService-Flow-PathGenerator.js +282 -0
- package/source/services/PictService-Flow-PortRenderer.js +269 -0
- package/source/services/PictService-Flow-RenderManager.js +281 -0
- package/source/services/PictService-Flow-Tether.js +6 -42
- package/source/views/PictView-Flow-Node.js +2 -220
- package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
- package/source/views/PictView-Flow.js +130 -882
- package/test/ConnectionHandleManager_tests.js +717 -0
- package/test/ConnectionRenderer_tests.js +591 -0
- package/test/DataManager_tests.js +859 -0
- package/test/Geometry_tests.js +767 -0
- package/test/PathGenerator_tests.js +978 -0
- package/test/PortRenderer_tests.js +367 -0
- 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
|
-
//
|
|
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
|
|
139
|
-
if (tmpTarget.closest && tmpTarget.closest('.pict-flow-panel-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|