pict-section-flow 0.0.1 → 0.0.2

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 (48) hide show
  1. package/docs/README.md +19 -0
  2. package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
  3. package/example_applications/simple_cards/package.json +43 -0
  4. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
  8. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
  9. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
  10. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
  11. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
  12. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
  13. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
  14. package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
  15. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
  16. package/package.json +6 -6
  17. package/source/Pict-Section-Flow.js +19 -0
  18. package/source/PictFlowCard.js +207 -0
  19. package/source/PictFlowCardPropertiesPanel.js +105 -0
  20. package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
  21. package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
  22. package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
  23. package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
  24. package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
  25. package/source/providers/PictProvider-Flow-Geometry.js +64 -0
  26. package/source/providers/PictProvider-Flow-Layouts.js +284 -0
  27. package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
  28. package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
  29. package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
  30. package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
  31. package/source/services/PictService-Flow-InteractionManager.js +399 -75
  32. package/source/services/PictService-Flow-Layout.js +159 -0
  33. package/source/services/PictService-Flow-PathGenerator.js +199 -0
  34. package/source/services/PictService-Flow-Tether.js +544 -0
  35. package/source/views/PictView-Flow-Node.js +95 -18
  36. package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
  37. package/source/views/PictView-Flow-Toolbar.js +491 -5
  38. package/source/views/PictView-Flow.js +830 -8
  39. package/example_application/package.json +0 -41
  40. package/example_application/source/Pict-Application-FlowExample.js +0 -241
  41. package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
  42. /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
  43. /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
  44. /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
  45. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
  46. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
  47. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
  48. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-TopBar.js +0 -0
@@ -0,0 +1,544 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-Tether
5
+ *
6
+ * Centralizes all tether geometry, path generation, handle state management,
7
+ * and SVG rendering for the lines that connect properties panels to their nodes.
8
+ *
9
+ * Delegates to shared providers for:
10
+ * - SVG element creation (_FlowView._SVGHelperProvider)
11
+ * - Geometry calculations (_FlowView._GeometryProvider)
12
+ * - Path string building (_FlowView._PathGenerator)
13
+ *
14
+ * Responsibilities:
15
+ * - Smart 4-quadrant anchor detection (which edge of the node/panel to connect)
16
+ * - Bezier and orthogonal path generation
17
+ * - Auto-midpoint and auto-corner calculations
18
+ * - Handle position updates during drag
19
+ * - Handle reset when nodes or panels move
20
+ * - Line mode toggling (bezier <-> orthogonal)
21
+ * - SVG element creation for tether lines, hit areas, and drag handles
22
+ */
23
+ class PictServiceFlowTether extends libFableServiceProviderBase
24
+ {
25
+ constructor(pFable, pOptions, pServiceHash)
26
+ {
27
+ super(pFable, pOptions, pServiceHash);
28
+
29
+ this.serviceType = 'PictServiceFlowTether';
30
+
31
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
32
+ }
33
+
34
+ // ---- Anchor Calculation ----
35
+
36
+ /**
37
+ * Determine which node edge and panel edge to connect based on 4-quadrant detection.
38
+ * Uses the relative position of the panel center to the node center.
39
+ *
40
+ * @param {Object} pPanelData - Panel data with X, Y, Width, Height
41
+ * @param {Object} pNodeData - Node data with X, Y, Width, Height
42
+ * @returns {{nodeAnchor: {x,y,side}, panelAnchor: {x,y,side}}}
43
+ */
44
+ getSmartAnchors(pPanelData, pNodeData)
45
+ {
46
+ let tmpNodeCX = pNodeData.X + pNodeData.Width / 2;
47
+ let tmpNodeCY = pNodeData.Y + pNodeData.Height / 2;
48
+ let tmpPanelCX = pPanelData.X + pPanelData.Width / 2;
49
+ let tmpPanelCY = pPanelData.Y + pPanelData.Height / 2;
50
+
51
+ let tmpDX = tmpPanelCX - tmpNodeCX;
52
+ let tmpDY = tmpPanelCY - tmpNodeCY;
53
+
54
+ let tmpNodeSide, tmpPanelSide;
55
+
56
+ if (Math.abs(tmpDX) >= Math.abs(tmpDY))
57
+ {
58
+ // Panel is primarily to the left or right
59
+ if (tmpDX >= 0)
60
+ {
61
+ tmpNodeSide = 'right';
62
+ tmpPanelSide = 'left';
63
+ }
64
+ else
65
+ {
66
+ tmpNodeSide = 'left';
67
+ tmpPanelSide = 'right';
68
+ }
69
+ }
70
+ else
71
+ {
72
+ // Panel is primarily above or below
73
+ if (tmpDY >= 0)
74
+ {
75
+ tmpNodeSide = 'bottom';
76
+ tmpPanelSide = 'top';
77
+ }
78
+ else
79
+ {
80
+ tmpNodeSide = 'top';
81
+ tmpPanelSide = 'bottom';
82
+ }
83
+ }
84
+
85
+ let tmpNodeAnchor = this._FlowView._GeometryProvider.getEdgeCenter(pNodeData, tmpNodeSide);
86
+ let tmpPanelAnchor = this._FlowView._GeometryProvider.getEdgeCenter(pPanelData, tmpPanelSide);
87
+
88
+ return {
89
+ nodeAnchor: Object.assign(tmpNodeAnchor, { side: tmpNodeSide }),
90
+ panelAnchor: Object.assign(tmpPanelAnchor, { side: tmpPanelSide })
91
+ };
92
+ }
93
+
94
+ // ---- Path Generation ----
95
+
96
+ /**
97
+ * Generate a bezier path between two anchor points with directional departure/approach.
98
+ * @param {Object} pFrom - {x, y, side}
99
+ * @param {Object} pTo - {x, y, side}
100
+ * @param {number|null} pHandleX - User-set handle X or null for auto
101
+ * @param {number|null} pHandleY - User-set handle Y or null for auto
102
+ * @returns {string} SVG path d attribute
103
+ */
104
+ generateBezierPath(pFrom, pTo, pHandleX, pHandleY)
105
+ {
106
+ let tmpDepartDist = 20;
107
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
108
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
109
+
110
+ let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
111
+ let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
112
+ let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
113
+ let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
114
+
115
+ if (pHandleX == null || pHandleY == null)
116
+ {
117
+ // Auto bezier: simple cubic from depart to approach
118
+ let tmpSpanX = Math.abs(tmpApproachX - tmpDepartX);
119
+ let tmpSpanY = Math.abs(tmpApproachY - tmpDepartY);
120
+ let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
121
+ let tmpCPDist = tmpSpan * 0.4;
122
+
123
+ let tmpCP1X = tmpDepartX + tmpFromDir.dx * tmpCPDist;
124
+ let tmpCP1Y = tmpDepartY + tmpFromDir.dy * tmpCPDist;
125
+ let tmpCP2X = tmpApproachX + tmpToDir.dx * tmpCPDist;
126
+ let tmpCP2Y = tmpApproachY + tmpToDir.dy * tmpCPDist;
127
+
128
+ return this._FlowView._PathGenerator.buildBezierPathString(
129
+ { x: pFrom.x, y: pFrom.y },
130
+ { x: tmpDepartX, y: tmpDepartY },
131
+ { x: tmpCP1X, y: tmpCP1Y },
132
+ { x: tmpCP2X, y: tmpCP2Y },
133
+ { x: tmpApproachX, y: tmpApproachY },
134
+ { x: pTo.x, y: pTo.y }
135
+ );
136
+ }
137
+
138
+ // User-set handle: split bezier into two segments through handle
139
+ let tmpCP1aDist = 30;
140
+ let tmpCP1aX = tmpDepartX + tmpFromDir.dx * tmpCP1aDist;
141
+ let tmpCP1aY = tmpDepartY + tmpFromDir.dy * tmpCP1aDist;
142
+
143
+ let tmpCP2aDist = 30;
144
+ let tmpCP2aX = tmpApproachX + tmpToDir.dx * tmpCP2aDist;
145
+ let tmpCP2aY = tmpApproachY + tmpToDir.dy * tmpCP2aDist;
146
+
147
+ // Tangent at the handle — direction from first segment end to second segment start
148
+ let tmpTangentX = tmpApproachX - tmpDepartX;
149
+ let tmpTangentY = tmpApproachY - tmpDepartY;
150
+ let tmpTangentLen = Math.sqrt(tmpTangentX * tmpTangentX + tmpTangentY * tmpTangentY) || 1;
151
+ tmpTangentX /= tmpTangentLen;
152
+ tmpTangentY /= tmpTangentLen;
153
+ let tmpTangentDist = 25;
154
+
155
+ let tmpCP1bX = pHandleX - tmpTangentX * tmpTangentDist;
156
+ let tmpCP1bY = pHandleY - tmpTangentY * tmpTangentDist;
157
+ let tmpCP2bX = pHandleX + tmpTangentX * tmpTangentDist;
158
+ let tmpCP2bY = pHandleY + tmpTangentY * tmpTangentDist;
159
+
160
+ return this._FlowView._PathGenerator.buildSplitBezierPathString(
161
+ { x: pFrom.x, y: pFrom.y },
162
+ { x: tmpDepartX, y: tmpDepartY },
163
+ { x: tmpCP1aX, y: tmpCP1aY },
164
+ { x: tmpCP1bX, y: tmpCP1bY },
165
+ { x: pHandleX, y: pHandleY },
166
+ { x: tmpCP2bX, y: tmpCP2bY },
167
+ { x: tmpCP2aX, y: tmpCP2aY },
168
+ { x: tmpApproachX, y: tmpApproachY },
169
+ { x: pTo.x, y: pTo.y }
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Generate an orthogonal (90-degree) path between two anchor points.
175
+ * @param {Object} pFrom - {x, y, side}
176
+ * @param {Object} pTo - {x, y, side}
177
+ * @param {Object|null} pCorners - {corner1: {x,y}, corner2: {x,y}} or null for auto
178
+ * @param {number} pMidOffset - Offset for the corridor midpoint
179
+ * @returns {string} SVG path d attribute
180
+ */
181
+ generateOrthogonalPath(pFrom, pTo, pCorners, pMidOffset)
182
+ {
183
+ let tmpDepartDist = 20;
184
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
185
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
186
+
187
+ let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
188
+ let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
189
+ let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
190
+ let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
191
+
192
+ let tmpCorner1, tmpCorner2;
193
+
194
+ if (pCorners && pCorners.corner1 && pCorners.corner2)
195
+ {
196
+ tmpCorner1 = pCorners.corner1;
197
+ tmpCorner2 = pCorners.corner2;
198
+ }
199
+ else
200
+ {
201
+ // Auto-calculate corners based on direction
202
+ let tmpAutoCorners = this._FlowView._PathGenerator.computeAutoOrthogonalCorners(tmpDepartX, tmpDepartY, tmpApproachX, tmpApproachY, tmpFromDir, tmpToDir, pMidOffset);
203
+ tmpCorner1 = tmpAutoCorners.corner1;
204
+ tmpCorner2 = tmpAutoCorners.corner2;
205
+ }
206
+
207
+ return this._FlowView._PathGenerator.buildOrthogonalPathString(
208
+ { x: pFrom.x, y: pFrom.y },
209
+ { x: tmpDepartX, y: tmpDepartY },
210
+ { x: tmpCorner1.x, y: tmpCorner1.y },
211
+ { x: tmpCorner2.x, y: tmpCorner2.y },
212
+ { x: tmpApproachX, y: tmpApproachY },
213
+ { x: pTo.x, y: pTo.y }
214
+ );
215
+ }
216
+
217
+ // ---- Handle Position Computation ----
218
+
219
+ /**
220
+ * Get auto-calculated bezier midpoint for a tether at t=0.5 on the curve.
221
+ * @param {Object} pFrom - {x, y, side}
222
+ * @param {Object} pTo - {x, y, side}
223
+ * @returns {{x: number, y: number}}
224
+ */
225
+ getAutoMidpoint(pFrom, pTo)
226
+ {
227
+ let tmpDepartDist = 20;
228
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
229
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
230
+
231
+ let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
232
+ let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
233
+ let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
234
+ let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
235
+
236
+ let tmpSpanX = Math.abs(tmpApproachX - tmpDepartX);
237
+ let tmpSpanY = Math.abs(tmpApproachY - tmpDepartY);
238
+ let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
239
+ let tmpCPDist = tmpSpan * 0.4;
240
+
241
+ let tmpP0 = { x: tmpDepartX, y: tmpDepartY };
242
+ let tmpP1 = { x: tmpDepartX + tmpFromDir.dx * tmpCPDist, y: tmpDepartY + tmpFromDir.dy * tmpCPDist };
243
+ let tmpP2 = { x: tmpApproachX + tmpToDir.dx * tmpCPDist, y: tmpApproachY + tmpToDir.dy * tmpCPDist };
244
+ let tmpP3 = { x: tmpApproachX, y: tmpApproachY };
245
+
246
+ // Evaluate cubic bezier at t=0.5
247
+ return this._FlowView._PathGenerator.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
248
+ }
249
+
250
+ /**
251
+ * Get full orthogonal geometry including corners and midpoint for handle rendering.
252
+ * @param {Object} pFrom - {x, y, side}
253
+ * @param {Object} pTo - {x, y, side}
254
+ * @param {Object} pPanelData - Panel data with tether handle properties
255
+ * @returns {{corner1: {x,y}, corner2: {x,y}, midpoint: {x,y}}}
256
+ */
257
+ getOrthoGeometry(pFrom, pTo, pPanelData)
258
+ {
259
+ let tmpDepartDist = 20;
260
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
261
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
262
+
263
+ let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
264
+ let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
265
+ let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
266
+ let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
267
+
268
+ let tmpCorners;
269
+ if (pPanelData.TetherHandleCustomized && pPanelData.TetherOrthoCorner1X != null)
270
+ {
271
+ tmpCorners = this._FlowView._PathGenerator.computeAutoOrthogonalCorners(tmpDepartX, tmpDepartY, tmpApproachX, tmpApproachY, tmpFromDir, tmpToDir, pPanelData.TetherOrthoMidOffset || 0);
272
+ tmpCorners.corner1 = { x: pPanelData.TetherOrthoCorner1X, y: pPanelData.TetherOrthoCorner1Y };
273
+ tmpCorners.corner2 = { x: pPanelData.TetherOrthoCorner2X, y: pPanelData.TetherOrthoCorner2Y };
274
+ }
275
+ else
276
+ {
277
+ tmpCorners = this._FlowView._PathGenerator.computeAutoOrthogonalCorners(tmpDepartX, tmpDepartY, tmpApproachX, tmpApproachY, tmpFromDir, tmpToDir, pPanelData.TetherOrthoMidOffset || 0);
278
+ }
279
+
280
+ let tmpMidpoint =
281
+ {
282
+ x: (tmpCorners.corner1.x + tmpCorners.corner2.x) / 2,
283
+ y: (tmpCorners.corner1.y + tmpCorners.corner2.y) / 2
284
+ };
285
+
286
+ return {
287
+ corner1: tmpCorners.corner1,
288
+ corner2: tmpCorners.corner2,
289
+ midpoint: tmpMidpoint
290
+ };
291
+ }
292
+
293
+ // ---- Path Generation (high-level) ----
294
+
295
+ /**
296
+ * Generate the SVG path string for a tether based on its panel data and anchors.
297
+ * @param {Object} pPanelData - Panel data with tether handle properties
298
+ * @param {Object} pFrom - {x, y, side} panel anchor
299
+ * @param {Object} pTo - {x, y, side} node anchor
300
+ * @returns {string} SVG path d attribute
301
+ */
302
+ generatePath(pPanelData, pFrom, pTo)
303
+ {
304
+ let tmpLineMode = pPanelData.TetherLineMode || 'bezier';
305
+
306
+ if (tmpLineMode === 'orthogonal')
307
+ {
308
+ let tmpCorners = null;
309
+ if (pPanelData.TetherHandleCustomized && pPanelData.TetherOrthoCorner1X != null)
310
+ {
311
+ tmpCorners =
312
+ {
313
+ corner1: { x: pPanelData.TetherOrthoCorner1X, y: pPanelData.TetherOrthoCorner1Y },
314
+ corner2: { x: pPanelData.TetherOrthoCorner2X, y: pPanelData.TetherOrthoCorner2Y }
315
+ };
316
+ }
317
+ return this.generateOrthogonalPath(pFrom, pTo, tmpCorners, pPanelData.TetherOrthoMidOffset || 0);
318
+ }
319
+ else
320
+ {
321
+ let tmpHandleX = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null) ? pPanelData.TetherBezierHandleX : null;
322
+ let tmpHandleY = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleY != null) ? pPanelData.TetherBezierHandleY : null;
323
+ return this.generateBezierPath(pFrom, pTo, tmpHandleX, tmpHandleY);
324
+ }
325
+ }
326
+
327
+ // ---- Handle State Management ----
328
+
329
+ /**
330
+ * Update a tether handle position during drag.
331
+ * @param {Object} pPanelData - Panel data to update
332
+ * @param {string} pHandleType - 'bezier-midpoint', 'ortho-corner1', 'ortho-corner2', 'ortho-midpoint'
333
+ * @param {number} pX
334
+ * @param {number} pY
335
+ */
336
+ updateHandlePosition(pPanelData, pHandleType, pX, pY)
337
+ {
338
+ pPanelData.TetherHandleCustomized = true;
339
+
340
+ switch (pHandleType)
341
+ {
342
+ case 'bezier-midpoint':
343
+ pPanelData.TetherBezierHandleX = pX;
344
+ pPanelData.TetherBezierHandleY = pY;
345
+ break;
346
+
347
+ case 'ortho-corner1':
348
+ pPanelData.TetherOrthoCorner1X = pX;
349
+ pPanelData.TetherOrthoCorner1Y = pY;
350
+ break;
351
+
352
+ case 'ortho-corner2':
353
+ pPanelData.TetherOrthoCorner2X = pX;
354
+ pPanelData.TetherOrthoCorner2Y = pY;
355
+ break;
356
+
357
+ case 'ortho-midpoint':
358
+ // Store the desired position for offset computation
359
+ pPanelData.TetherOrthoMidOffset = (pPanelData.TetherOrthoMidOffset || 0);
360
+ pPanelData._TetherMidDragX = pX;
361
+ pPanelData._TetherMidDragY = pY;
362
+ break;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Reset tether handle positions to auto for a single panel.
368
+ * Preserves TetherLineMode but clears all handle coordinates.
369
+ * @param {Object} pPanelData - Panel data to reset
370
+ */
371
+ resetHandlePositions(pPanelData)
372
+ {
373
+ if (pPanelData.TetherHandleCustomized)
374
+ {
375
+ pPanelData.TetherHandleCustomized = false;
376
+ pPanelData.TetherBezierHandleX = null;
377
+ pPanelData.TetherBezierHandleY = null;
378
+ pPanelData.TetherOrthoCorner1X = null;
379
+ pPanelData.TetherOrthoCorner1Y = null;
380
+ pPanelData.TetherOrthoCorner2X = null;
381
+ pPanelData.TetherOrthoCorner2Y = null;
382
+ pPanelData.TetherOrthoMidOffset = 0;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Reset tether handle positions for all panels attached to a node.
388
+ * Called when a node moves.
389
+ * @param {Array} pOpenPanels - Array of all open panel data objects
390
+ * @param {string} pNodeHash - The node hash whose panels should be reset
391
+ */
392
+ resetHandlesForNode(pOpenPanels, pNodeHash)
393
+ {
394
+ for (let i = 0; i < pOpenPanels.length; i++)
395
+ {
396
+ let tmpPanel = pOpenPanels[i];
397
+ if (tmpPanel.NodeHash === pNodeHash)
398
+ {
399
+ this.resetHandlePositions(tmpPanel);
400
+ }
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Toggle tether line mode between bezier and orthogonal.
406
+ * Resets handle positions on toggle.
407
+ * @param {Object} pPanelData - Panel data to toggle
408
+ * @returns {string} The new line mode ('bezier' or 'orthogonal')
409
+ */
410
+ toggleLineMode(pPanelData)
411
+ {
412
+ let tmpCurrentMode = pPanelData.TetherLineMode || 'bezier';
413
+ pPanelData.TetherLineMode = (tmpCurrentMode === 'bezier') ? 'orthogonal' : 'bezier';
414
+
415
+ pPanelData.TetherHandleCustomized = false;
416
+ pPanelData.TetherBezierHandleX = null;
417
+ pPanelData.TetherBezierHandleY = null;
418
+ pPanelData.TetherOrthoCorner1X = null;
419
+ pPanelData.TetherOrthoCorner1Y = null;
420
+ pPanelData.TetherOrthoCorner2X = null;
421
+ pPanelData.TetherOrthoCorner2Y = null;
422
+ pPanelData.TetherOrthoMidOffset = 0;
423
+
424
+ return pPanelData.TetherLineMode;
425
+ }
426
+
427
+ // ---- SVG Rendering ----
428
+
429
+ /**
430
+ * Render a tether from a panel to its node.
431
+ * Creates SVG path elements for the line and hit area, plus drag handles when selected.
432
+ *
433
+ * @param {Object} pPanelData - Panel data with position and tether properties
434
+ * @param {Object} pNodeData - Node data with position
435
+ * @param {SVGGElement} pTethersLayer - SVG group to append elements to
436
+ * @param {boolean} pIsSelected - Whether this tether is currently selected
437
+ * @param {string} pViewIdentifier - The flow view identifier (for marker URL)
438
+ */
439
+ renderTether(pPanelData, pNodeData, pTethersLayer, pIsSelected, pViewIdentifier)
440
+ {
441
+ if (!pNodeData) return;
442
+
443
+ let tmpAnchors = this.getSmartAnchors(pPanelData, pNodeData);
444
+ let tmpFrom = tmpAnchors.panelAnchor;
445
+ let tmpTo = tmpAnchors.nodeAnchor;
446
+
447
+ let tmpPath = this.generatePath(pPanelData, tmpFrom, tmpTo);
448
+
449
+ // Hit area (wider invisible path for easier click targeting)
450
+ let tmpHitArea = this._FlowView._SVGHelperProvider.createSVGElement('path');
451
+ tmpHitArea.setAttribute('class', 'pict-flow-tether-hitarea');
452
+ tmpHitArea.setAttribute('d', tmpPath);
453
+ tmpHitArea.setAttribute('data-element-type', 'tether-hitarea');
454
+ tmpHitArea.setAttribute('data-panel-hash', pPanelData.Hash);
455
+ pTethersLayer.appendChild(tmpHitArea);
456
+
457
+ // Visible tether path
458
+ let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
459
+ tmpPathElement.setAttribute('class', `pict-flow-tether-line${pIsSelected ? ' selected' : ''}`);
460
+ tmpPathElement.setAttribute('d', tmpPath);
461
+ tmpPathElement.setAttribute('marker-end', `url(#flow-tether-arrowhead-${pViewIdentifier})`);
462
+ tmpPathElement.setAttribute('data-element-type', 'tether');
463
+ tmpPathElement.setAttribute('data-panel-hash', pPanelData.Hash);
464
+ pTethersLayer.appendChild(tmpPathElement);
465
+
466
+ // Render drag handles when selected
467
+ if (pIsSelected)
468
+ {
469
+ this._renderHandles(pPanelData, pTethersLayer, tmpFrom, tmpTo);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Render drag handles for a selected tether.
475
+ * @param {Object} pPanelData
476
+ * @param {SVGGElement} pTethersLayer
477
+ * @param {Object} pFrom - Panel anchor {x, y, side}
478
+ * @param {Object} pTo - Node anchor {x, y, side}
479
+ */
480
+ _renderHandles(pPanelData, pTethersLayer, pFrom, pTo)
481
+ {
482
+ let tmpLineMode = pPanelData.TetherLineMode || 'bezier';
483
+
484
+ if (tmpLineMode === 'orthogonal')
485
+ {
486
+ let tmpGeom = this.getOrthoGeometry(pFrom, pTo, pPanelData);
487
+
488
+ // Corner 1 handle
489
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'ortho-corner1',
490
+ tmpGeom.corner1.x, tmpGeom.corner1.y, 'pict-flow-tether-handle');
491
+
492
+ // Midpoint handle
493
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'ortho-midpoint',
494
+ tmpGeom.midpoint.x, tmpGeom.midpoint.y, 'pict-flow-tether-handle-midpoint');
495
+
496
+ // Corner 2 handle
497
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'ortho-corner2',
498
+ tmpGeom.corner2.x, tmpGeom.corner2.y, 'pict-flow-tether-handle');
499
+ }
500
+ else
501
+ {
502
+ // Bezier: single midpoint handle
503
+ let tmpMidX, tmpMidY;
504
+ if (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null)
505
+ {
506
+ tmpMidX = pPanelData.TetherBezierHandleX;
507
+ tmpMidY = pPanelData.TetherBezierHandleY;
508
+ }
509
+ else
510
+ {
511
+ let tmpMid = this.getAutoMidpoint(pFrom, pTo);
512
+ tmpMidX = tmpMid.x;
513
+ tmpMidY = tmpMid.y;
514
+ }
515
+
516
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-midpoint',
517
+ tmpMidX, tmpMidY, 'pict-flow-tether-handle-midpoint');
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Create a single tether drag handle circle.
523
+ * @param {SVGGElement} pLayer
524
+ * @param {string} pPanelHash
525
+ * @param {string} pHandleType
526
+ * @param {number} pX
527
+ * @param {number} pY
528
+ * @param {string} pClassName
529
+ */
530
+ _createHandle(pLayer, pPanelHash, pHandleType, pX, pY, pClassName)
531
+ {
532
+ let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
533
+ tmpCircle.setAttribute('class', pClassName);
534
+ tmpCircle.setAttribute('cx', String(pX));
535
+ tmpCircle.setAttribute('cy', String(pY));
536
+ tmpCircle.setAttribute('r', '6');
537
+ tmpCircle.setAttribute('data-element-type', 'tether-handle');
538
+ tmpCircle.setAttribute('data-panel-hash', pPanelHash);
539
+ tmpCircle.setAttribute('data-handle-type', pHandleType);
540
+ pLayer.appendChild(tmpCircle);
541
+ }
542
+ }
543
+
544
+ module.exports = PictServiceFlowTether;