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,72 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictProvider-Flow-PanelChrome
5
+ *
6
+ * Template-based provider for creating the panel chrome (the foreignObject
7
+ * wrapper, title bar, close button, and body container) for properties panels.
8
+ *
9
+ * Replaces the raw DOM API approach with a configuration template
10
+ * (Flow-PanelChrome-Template) registered in the FlowView's template set.
11
+ */
12
+ class PictProviderFlowPanelChrome extends libFableServiceProviderBase
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+
18
+ this.serviceType = 'PictProviderFlowPanelChrome';
19
+
20
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
21
+ }
22
+
23
+ /**
24
+ * Create a foreignObject containing the panel chrome and empty body container.
25
+ *
26
+ * Uses the Flow-PanelChrome-Template registered in the FlowView configuration
27
+ * to render the inner HTML (title bar, close button, body container), then
28
+ * attaches event isolation listeners so pointer and wheel events inside the
29
+ * panel body do not propagate to the SVG interaction layer.
30
+ *
31
+ * @param {Object} pPanelData - Panel data from OpenPanels (Hash, NodeHash, X, Y, Width, Height, Title)
32
+ * @param {SVGGElement} pPanelsLayer - The SVG <g> for panel foreignObjects
33
+ * @returns {HTMLDivElement} The panel body container element for content rendering
34
+ */
35
+ createPanelForeignObject(pPanelData, pPanelsLayer)
36
+ {
37
+ let tmpSVGHelper = this._FlowView._SVGHelperProvider;
38
+
39
+ // Create the SVG foreignObject wrapper
40
+ let tmpFO = tmpSVGHelper.createSVGElement('foreignObject');
41
+ tmpFO.setAttribute('class', 'pict-flow-panel-foreign-object');
42
+ tmpFO.setAttribute('data-panel-hash', pPanelData.Hash);
43
+ tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash);
44
+ tmpFO.setAttribute('x', String(pPanelData.X));
45
+ tmpFO.setAttribute('y', String(pPanelData.Y));
46
+ tmpFO.setAttribute('width', String(pPanelData.Width));
47
+ tmpFO.setAttribute('height', String(pPanelData.Height));
48
+
49
+ // Render the panel chrome from the configuration template
50
+ let tmpPict = this._FlowView.pict || this._FlowView.fable;
51
+ let tmpTitle = pPanelData.Title || 'Properties';
52
+ let tmpChromeHTML = tmpPict.parseTemplateByHash('Flow-PanelChrome-Template',
53
+ { Hash: pPanelData.Hash, Title: tmpTitle });
54
+
55
+ tmpFO.innerHTML = tmpChromeHTML;
56
+
57
+ // Attach event isolation to the panel body so pointer/wheel events
58
+ // inside the panel content do not trigger SVG interactions
59
+ let tmpBody = tmpFO.querySelector('.pict-flow-panel-body');
60
+ if (tmpBody)
61
+ {
62
+ tmpBody.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
63
+ tmpBody.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
64
+ }
65
+
66
+ pPanelsLayer.appendChild(tmpFO);
67
+
68
+ return tmpBody;
69
+ }
70
+ }
71
+
72
+ module.exports = PictProviderFlowPanelChrome;
@@ -0,0 +1,30 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictProvider-Flow-SVGHelpers
5
+ *
6
+ * Shared SVG element creation utility used by all flow components
7
+ * that need to create SVG namespace elements (nodes, connections, tethers).
8
+ */
9
+ class PictProviderFlowSVGHelpers extends libFableServiceProviderBase
10
+ {
11
+ constructor(pFable, pOptions, pServiceHash)
12
+ {
13
+ super(pFable, pOptions, pServiceHash);
14
+
15
+ this.serviceType = 'PictProviderFlowSVGHelpers';
16
+ }
17
+
18
+ /**
19
+ * Create an SVG namespace element.
20
+ *
21
+ * @param {string} pTagName - The SVG element tag name (e.g., 'path', 'circle', 'g', 'rect', 'text')
22
+ * @returns {SVGElement}
23
+ */
24
+ createSVGElement(pTagName)
25
+ {
26
+ return document.createElementNS('http://www.w3.org/2000/svg', pTagName);
27
+ }
28
+ }
29
+
30
+ module.exports = PictProviderFlowSVGHelpers;
@@ -12,17 +12,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
12
12
  }
13
13
 
14
14
  /**
15
- * Create an SVG namespace element
16
- * @param {string} pTagName
17
- * @returns {SVGElement}
18
- */
19
- _createSVGElement(pTagName)
20
- {
21
- return document.createElementNS('http://www.w3.org/2000/svg', pTagName);
22
- }
23
-
24
- /**
25
- * Render a connection as an SVG path with hit area
15
+ * Render a connection as an SVG path with hit area and optional handles.
26
16
  * @param {Object} pConnection - The connection data
27
17
  * @param {SVGGElement} pConnectionsLayer - The SVG group to append to
28
18
  * @param {boolean} pIsSelected - Whether this connection is selected
@@ -36,11 +26,34 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
36
26
 
37
27
  if (!tmpSourcePos || !tmpTargetPos) return;
38
28
 
39
- let tmpPath = this._generateDirectionalPath(tmpSourcePos, tmpTargetPos);
29
+ let tmpData = pConnection.Data || {};
30
+ let tmpLineMode = tmpData.LineMode || 'bezier';
31
+ let tmpPath;
32
+
33
+ if (tmpLineMode === 'orthogonal')
34
+ {
35
+ let tmpCorners = null;
36
+ if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
37
+ {
38
+ tmpCorners =
39
+ {
40
+ corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
41
+ corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
42
+ };
43
+ }
44
+ tmpPath = this._generateOrthogonalPath(tmpSourcePos, tmpTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
45
+ }
46
+ else
47
+ {
48
+ let tmpHandleX = (tmpData.HandleCustomized && tmpData.BezierHandleX != null) ? tmpData.BezierHandleX : null;
49
+ let tmpHandleY = (tmpData.HandleCustomized && tmpData.BezierHandleY != null) ? tmpData.BezierHandleY : null;
50
+ tmpPath = this._generateBezierPathWithHandle(tmpSourcePos, tmpTargetPos, tmpHandleX, tmpHandleY);
51
+ }
52
+
40
53
  let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
41
54
 
42
55
  // Hit area (wider invisible path for easier selection)
43
- let tmpHitArea = this._createSVGElement('path');
56
+ let tmpHitArea = this._FlowView._SVGHelperProvider.createSVGElement('path');
44
57
  tmpHitArea.setAttribute('class', 'pict-flow-connection-hitarea');
45
58
  tmpHitArea.setAttribute('d', tmpPath);
46
59
  tmpHitArea.setAttribute('data-connection-hash', pConnection.Hash);
@@ -48,7 +61,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
48
61
  pConnectionsLayer.appendChild(tmpHitArea);
49
62
 
50
63
  // Visible connection path
51
- let tmpPathElement = this._createSVGElement('path');
64
+ let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
52
65
  tmpPathElement.setAttribute('class', `pict-flow-connection ${pIsSelected ? 'selected' : ''}`);
53
66
  tmpPathElement.setAttribute('d', tmpPath);
54
67
  tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
@@ -65,77 +78,47 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
65
78
  }
66
79
 
67
80
  pConnectionsLayer.appendChild(tmpPathElement);
68
- }
69
81
 
70
- /**
71
- * Get the outward unit direction vector for a given port side.
72
- *
73
- * @param {string} pSide - 'left', 'right', 'top', or 'bottom'
74
- * @returns {{dx: number, dy: number}}
75
- */
76
- _sideDirection(pSide)
77
- {
78
- switch (pSide)
82
+ // Render drag handles when selected
83
+ if (pIsSelected)
79
84
  {
80
- case 'left': return { dx: -1, dy: 0 };
81
- case 'right': return { dx: 1, dy: 0 };
82
- case 'top': return { dx: 0, dy: -1 };
83
- case 'bottom': return { dx: 0, dy: 1 };
84
- default: return { dx: 1, dy: 0 };
85
+ this._renderHandles(pConnection, pConnectionsLayer, tmpSourcePos, tmpTargetPos);
85
86
  }
86
87
  }
87
88
 
88
89
  /**
89
- * Generate a direction-aware path between two ports.
90
- *
91
- * The path is composed of three segments:
92
- * 1. A short straight "departure" segment leaving the source port
93
- * in its outward direction.
94
- * 2. A cubic bezier curve connecting the departure point to an
95
- * "approach" point near the target port.
96
- * 3. A short straight "approach" segment arriving at the target
97
- * port aligned with its inward direction.
90
+ * Compute the departure and approach points plus control points
91
+ * for a direction-aware bezier between two ports.
98
92
  *
99
- * The straight approach segment at the end ensures the arrowhead
100
- * marker is always oriented correctly (pointing straight into the
101
- * port) regardless of the overall curve shape.
93
+ * This extracts the intermediate geometry from _generateDirectionalPath
94
+ * so it can be reused by _getAutoMidpoint and _generateBezierPathWithHandle.
102
95
  *
103
- * @param {{x: number, y: number, side: string}} pStart - Start port position + side
104
- * @param {{x: number, y: number, side: string}} pEnd - End port position + side
105
- * @returns {string} SVG path d attribute
96
+ * @param {{x: number, y: number, side: string}} pStart
97
+ * @param {{x: number, y: number, side: string}} pEnd
98
+ * @returns {{departX, departY, approachX, approachY, cp1X, cp1Y, cp2X, cp2Y, startDir, endDir}}
106
99
  */
107
- _generateDirectionalPath(pStart, pEnd)
100
+ _computeDirectionalGeometry(pStart, pEnd)
108
101
  {
109
- let tmpStartDir = this._sideDirection(pStart.side || 'right');
110
- let tmpEndDir = this._sideDirection(pEnd.side || 'left');
102
+ let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
103
+ let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
111
104
 
112
- // Length of the straight segments leaving/entering each port.
113
- // The arrowhead is ~20px (10 marker * 2 stroke-width), so
114
- // the approach segment needs to be at least that long to look
115
- // clean. We use 20px as a minimum.
116
105
  let tmpStraightLen = 20;
117
106
 
118
- // Departure point: offset from source port along its outward direction
119
107
  let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
120
108
  let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
121
109
 
122
- // Approach point: offset from target port along its OUTWARD direction
123
- // (i.e. away from the port, so the line segment goes inward toward the port)
124
110
  let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
125
111
  let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
126
112
 
127
- // Now compute the bezier between the departure and approach points
128
113
  let tmpDX = Math.abs(tmpApproachX - tmpDepartX);
129
114
  let tmpDY = Math.abs(tmpApproachY - tmpDepartY);
130
115
  let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
131
116
 
132
- // Base offset for control points
133
117
  let tmpBaseOffset = Math.max(Math.min(tmpDist * 0.4, 180), 30);
134
118
 
135
119
  let tmpSameAxis = (tmpStartDir.dx !== 0 && tmpEndDir.dx !== 0) ||
136
120
  (tmpStartDir.dy !== 0 && tmpEndDir.dy !== 0);
137
121
 
138
- // Check if the ports are "facing each other" on the same axis
139
122
  let tmpFacingEachOther = false;
140
123
  if (tmpSameAxis)
141
124
  {
@@ -173,18 +156,293 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
173
156
  tmpCurveOffset = Math.max(tmpBaseOffset * 0.8, 40);
174
157
  }
175
158
 
176
- // Control points extend outward from the departure/approach points
177
159
  let tmpCP1X = tmpDepartX + tmpStartDir.dx * tmpCurveOffset;
178
160
  let tmpCP1Y = tmpDepartY + tmpStartDir.dy * tmpCurveOffset;
179
161
  let tmpCP2X = tmpApproachX + tmpEndDir.dx * tmpCurveOffset;
180
162
  let tmpCP2Y = tmpApproachY + tmpEndDir.dy * tmpCurveOffset;
181
163
 
182
- // Build the composite path:
183
- // 1. Move to source port
184
- // 2. Line to departure point (straight out from port)
185
- // 3. Bezier curve to approach point
186
- // 4. Line to target port (straight into port — arrowhead aligns here)
187
- return `M ${pStart.x} ${pStart.y} L ${tmpDepartX} ${tmpDepartY} C ${tmpCP1X} ${tmpCP1Y}, ${tmpCP2X} ${tmpCP2Y}, ${tmpApproachX} ${tmpApproachY} L ${pEnd.x} ${pEnd.y}`;
164
+ return {
165
+ departX: tmpDepartX, departY: tmpDepartY,
166
+ approachX: tmpApproachX, approachY: tmpApproachY,
167
+ cp1X: tmpCP1X, cp1Y: tmpCP1Y,
168
+ cp2X: tmpCP2X, cp2Y: tmpCP2Y,
169
+ startDir: tmpStartDir, endDir: tmpEndDir
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Generate a direction-aware path between two ports.
175
+ *
176
+ * The path is composed of three segments:
177
+ * 1. A short straight "departure" segment leaving the source port
178
+ * in its outward direction.
179
+ * 2. A cubic bezier curve connecting the departure point to an
180
+ * "approach" point near the target port.
181
+ * 3. A short straight "approach" segment arriving at the target
182
+ * port aligned with its inward direction.
183
+ *
184
+ * @param {{x: number, y: number, side: string}} pStart - Start port position + side
185
+ * @param {{x: number, y: number, side: string}} pEnd - End port position + side
186
+ * @returns {string} SVG path d attribute
187
+ */
188
+ _generateDirectionalPath(pStart, pEnd)
189
+ {
190
+ let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
191
+
192
+ return this._FlowView._PathGenerator.buildBezierPathString(
193
+ { x: pStart.x, y: pStart.y },
194
+ { x: tmpGeo.departX, y: tmpGeo.departY },
195
+ { x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
196
+ { x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
197
+ { x: tmpGeo.approachX, y: tmpGeo.approachY },
198
+ { x: pEnd.x, y: pEnd.y }
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Generate a bezier path with an optional user-controlled midpoint handle.
204
+ *
205
+ * If handle is null, uses the standard auto-calculated bezier.
206
+ * If handle is set, splits into two cubic bezier segments passing
207
+ * through the handle point for a smooth S-curve.
208
+ *
209
+ * @param {{x: number, y: number, side: string}} pStart
210
+ * @param {{x: number, y: number, side: string}} pEnd
211
+ * @param {number|null} pHandleX
212
+ * @param {number|null} pHandleY
213
+ * @returns {string} SVG path d attribute
214
+ */
215
+ _generateBezierPathWithHandle(pStart, pEnd, pHandleX, pHandleY)
216
+ {
217
+ if (pHandleX == null || pHandleY == null)
218
+ {
219
+ return this._generateDirectionalPath(pStart, pEnd);
220
+ }
221
+
222
+ let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
223
+
224
+ // Split into two cubic bezier segments through the handle point.
225
+ // First segment: depart -> handle
226
+ // Second segment: handle -> approach
227
+ // Control points are computed to ensure smooth tangent at the handle.
228
+ let tmpCP1aX = tmpGeo.departX + tmpGeo.startDir.dx * ((Math.abs(pHandleX - tmpGeo.departX) + Math.abs(pHandleY - tmpGeo.departY)) * 0.4);
229
+ let tmpCP1aY = tmpGeo.departY + tmpGeo.startDir.dy * ((Math.abs(pHandleX - tmpGeo.departX) + Math.abs(pHandleY - tmpGeo.departY)) * 0.4);
230
+
231
+ // The tangent at the handle should be smooth: the control points on
232
+ // either side of the handle should be collinear through it.
233
+ // Use the direction from depart to approach as the tangent direction.
234
+ let tmpTangentX = tmpGeo.approachX - tmpGeo.departX;
235
+ let tmpTangentY = tmpGeo.approachY - tmpGeo.departY;
236
+ let tmpTangentLen = Math.sqrt(tmpTangentX * tmpTangentX + tmpTangentY * tmpTangentY);
237
+ if (tmpTangentLen < 1) tmpTangentLen = 1;
238
+ let tmpTangentNX = tmpTangentX / tmpTangentLen;
239
+ let tmpTangentNY = tmpTangentY / tmpTangentLen;
240
+
241
+ let tmpTangentScale = tmpTangentLen * 0.2;
242
+
243
+ let tmpCP1bX = pHandleX - tmpTangentNX * tmpTangentScale;
244
+ let tmpCP1bY = pHandleY - tmpTangentNY * tmpTangentScale;
245
+ let tmpCP2aX = pHandleX + tmpTangentNX * tmpTangentScale;
246
+ let tmpCP2aY = pHandleY + tmpTangentNY * tmpTangentScale;
247
+
248
+ let tmpCP2bX = tmpGeo.approachX + tmpGeo.endDir.dx * ((Math.abs(pHandleX - tmpGeo.approachX) + Math.abs(pHandleY - tmpGeo.approachY)) * 0.4);
249
+ let tmpCP2bY = tmpGeo.approachY + tmpGeo.endDir.dy * ((Math.abs(pHandleX - tmpGeo.approachX) + Math.abs(pHandleY - tmpGeo.approachY)) * 0.4);
250
+
251
+ return this._FlowView._PathGenerator.buildSplitBezierPathString(
252
+ { x: pStart.x, y: pStart.y },
253
+ { x: tmpGeo.departX, y: tmpGeo.departY },
254
+ { x: tmpCP1aX, y: tmpCP1aY },
255
+ { x: tmpCP1bX, y: tmpCP1bY },
256
+ { x: pHandleX, y: pHandleY },
257
+ { x: tmpCP2aX, y: tmpCP2aY },
258
+ { x: tmpCP2bX, y: tmpCP2bY },
259
+ { x: tmpGeo.approachX, y: tmpGeo.approachY },
260
+ { x: pEnd.x, y: pEnd.y }
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Get the auto-calculated midpoint of the default bezier curve between two ports.
266
+ * Evaluates the cubic bezier at t=0.5.
267
+ *
268
+ * @param {{x: number, y: number, side: string}} pStart
269
+ * @param {{x: number, y: number, side: string}} pEnd
270
+ * @returns {{x: number, y: number}}
271
+ */
272
+ getAutoMidpoint(pStart, pEnd)
273
+ {
274
+ let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
275
+
276
+ return this._FlowView._PathGenerator.evaluateCubicBezier(
277
+ { x: tmpGeo.departX, y: tmpGeo.departY },
278
+ { x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
279
+ { x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
280
+ { x: tmpGeo.approachX, y: tmpGeo.approachY },
281
+ 0.5
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Generate an orthogonal (90-degree angles only) path between two ports.
287
+ *
288
+ * Path format: M start L depart L corner1 L corner2 L approach L end
289
+ *
290
+ * @param {{x: number, y: number, side: string}} pStart
291
+ * @param {{x: number, y: number, side: string}} pEnd
292
+ * @param {Object|null} pCorners - { corner1: {x,y}, corner2: {x,y} } or null for auto
293
+ * @param {number} pMidOffset - Offset for the auto-calculated corridor position
294
+ * @returns {string} SVG path d attribute
295
+ */
296
+ _generateOrthogonalPath(pStart, pEnd, pCorners, pMidOffset)
297
+ {
298
+ let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
299
+ let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
300
+
301
+ let tmpStraightLen = 20;
302
+
303
+ let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
304
+ let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
305
+
306
+ let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
307
+ let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
308
+
309
+ let tmpCorner1, tmpCorner2;
310
+
311
+ if (pCorners && pCorners.corner1 && pCorners.corner2)
312
+ {
313
+ tmpCorner1 = pCorners.corner1;
314
+ tmpCorner2 = pCorners.corner2;
315
+ }
316
+ else
317
+ {
318
+ let tmpAutoCorners = this._FlowView._PathGenerator.computeAutoOrthogonalCorners(
319
+ tmpDepartX, tmpDepartY,
320
+ tmpApproachX, tmpApproachY,
321
+ tmpStartDir, tmpEndDir,
322
+ pMidOffset || 0
323
+ );
324
+ tmpCorner1 = tmpAutoCorners.corner1;
325
+ tmpCorner2 = tmpAutoCorners.corner2;
326
+ }
327
+
328
+ return this._FlowView._PathGenerator.buildOrthogonalPathString(
329
+ { x: pStart.x, y: pStart.y },
330
+ { x: tmpDepartX, y: tmpDepartY },
331
+ tmpCorner1,
332
+ tmpCorner2,
333
+ { x: tmpApproachX, y: tmpApproachY },
334
+ { x: pEnd.x, y: pEnd.y }
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Get the full orthogonal geometry for a connection (for handle positioning).
340
+ *
341
+ * @param {{x: number, y: number, side: string}} pStart
342
+ * @param {{x: number, y: number, side: string}} pEnd
343
+ * @param {Object} pData - Connection.Data
344
+ * @returns {{corner1: {x,y}, corner2: {x,y}, midpoint: {x,y}}}
345
+ */
346
+ getOrthogonalGeometry(pStart, pEnd, pData)
347
+ {
348
+ let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
349
+ let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
350
+
351
+ let tmpStraightLen = 20;
352
+
353
+ let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
354
+ let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
355
+ let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
356
+ let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
357
+
358
+ if (pData && pData.HandleCustomized && pData.OrthoCorner1X != null)
359
+ {
360
+ let tmpCorner1 = { x: pData.OrthoCorner1X, y: pData.OrthoCorner1Y };
361
+ let tmpCorner2 = { x: pData.OrthoCorner2X, y: pData.OrthoCorner2Y };
362
+ let tmpMidpoint =
363
+ {
364
+ x: (tmpCorner1.x + tmpCorner2.x) / 2,
365
+ y: (tmpCorner1.y + tmpCorner2.y) / 2
366
+ };
367
+ return { corner1: tmpCorner1, corner2: tmpCorner2, midpoint: tmpMidpoint };
368
+ }
369
+
370
+ return this._FlowView._PathGenerator.computeAutoOrthogonalCorners(
371
+ tmpDepartX, tmpDepartY,
372
+ tmpApproachX, tmpApproachY,
373
+ tmpStartDir, tmpEndDir,
374
+ (pData && pData.OrthoMidOffset) || 0
375
+ );
376
+ }
377
+
378
+ /**
379
+ * Render drag handles for the selected connection.
380
+ *
381
+ * @param {Object} pConnection
382
+ * @param {SVGGElement} pLayer
383
+ * @param {{x, y, side}} pStart - Source port position
384
+ * @param {{x, y, side}} pEnd - Target port position
385
+ */
386
+ _renderHandles(pConnection, pLayer, pStart, pEnd)
387
+ {
388
+ let tmpData = pConnection.Data || {};
389
+ let tmpLineMode = tmpData.LineMode || 'bezier';
390
+
391
+ if (tmpLineMode === 'orthogonal')
392
+ {
393
+ let tmpGeometry = this.getOrthogonalGeometry(pStart, pEnd, tmpData);
394
+
395
+ // Corner 1 handle
396
+ this._createHandle(pLayer, pConnection.Hash, 'ortho-corner1',
397
+ tmpGeometry.corner1.x, tmpGeometry.corner1.y, 'pict-flow-connection-handle');
398
+
399
+ // Midpoint handle (between corners)
400
+ this._createHandle(pLayer, pConnection.Hash, 'ortho-midpoint',
401
+ tmpGeometry.midpoint.x, tmpGeometry.midpoint.y, 'pict-flow-connection-handle-midpoint');
402
+
403
+ // Corner 2 handle
404
+ this._createHandle(pLayer, pConnection.Hash, 'ortho-corner2',
405
+ tmpGeometry.corner2.x, tmpGeometry.corner2.y, 'pict-flow-connection-handle');
406
+ }
407
+ else
408
+ {
409
+ // Bezier midpoint handle
410
+ let tmpMidpoint;
411
+ if (tmpData.HandleCustomized && tmpData.BezierHandleX != null)
412
+ {
413
+ tmpMidpoint = { x: tmpData.BezierHandleX, y: tmpData.BezierHandleY };
414
+ }
415
+ else
416
+ {
417
+ tmpMidpoint = this.getAutoMidpoint(pStart, pEnd);
418
+ }
419
+
420
+ this._createHandle(pLayer, pConnection.Hash, 'bezier-midpoint',
421
+ tmpMidpoint.x, tmpMidpoint.y, 'pict-flow-connection-handle');
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Create a single SVG circle handle element.
427
+ *
428
+ * @param {SVGGElement} pLayer
429
+ * @param {string} pConnectionHash
430
+ * @param {string} pHandleType
431
+ * @param {number} pX
432
+ * @param {number} pY
433
+ * @param {string} pClassName
434
+ */
435
+ _createHandle(pLayer, pConnectionHash, pHandleType, pX, pY, pClassName)
436
+ {
437
+ let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
438
+ tmpCircle.setAttribute('class', pClassName);
439
+ tmpCircle.setAttribute('cx', String(pX));
440
+ tmpCircle.setAttribute('cy', String(pY));
441
+ tmpCircle.setAttribute('r', '6');
442
+ tmpCircle.setAttribute('data-element-type', 'connection-handle');
443
+ tmpCircle.setAttribute('data-connection-hash', pConnectionHash);
444
+ tmpCircle.setAttribute('data-handle-type', pHandleType);
445
+ pLayer.appendChild(tmpCircle);
188
446
  }
189
447
 
190
448
  /**
@@ -195,7 +453,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
195
453
  */
196
454
  _generateBezierPath(pStart, pEnd)
197
455
  {
198
- // During drag operations we may not have side info; default to rightleft
456
+ // During drag operations we may not have side info; default to right->left
199
457
  let tmpStart = { x: pStart.x, y: pStart.y, side: pStart.side || 'right' };
200
458
  let tmpEnd = { x: pEnd.x, y: pEnd.y, side: pEnd.side || 'left' };
201
459
  return this._generateDirectionalPath(tmpStart, tmpEnd);
@@ -218,7 +476,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
218
476
  { x: pEndX, y: pEndY, side: 'left' }
219
477
  );
220
478
 
221
- let tmpPathElement = this._createSVGElement('path');
479
+ let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
222
480
  tmpPathElement.setAttribute('class', 'pict-flow-drag-connection');
223
481
  tmpPathElement.setAttribute('d', tmpPath);
224
482
  pLayer.appendChild(tmpPathElement);