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