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.
- package/docs/README.md +19 -0
- package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
- package/example_applications/simple_cards/package.json +43 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
- package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
- package/package.json +6 -6
- package/source/Pict-Section-Flow.js +19 -0
- package/source/PictFlowCard.js +207 -0
- package/source/PictFlowCardPropertiesPanel.js +105 -0
- package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
- package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
- package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
- package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
- package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
- package/source/providers/PictProvider-Flow-Geometry.js +64 -0
- package/source/providers/PictProvider-Flow-Layouts.js +284 -0
- package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
- package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
- package/source/services/PictService-Flow-InteractionManager.js +399 -75
- package/source/services/PictService-Flow-Layout.js +159 -0
- package/source/services/PictService-Flow-PathGenerator.js +199 -0
- package/source/services/PictService-Flow-Tether.js +544 -0
- package/source/views/PictView-Flow-Node.js +95 -18
- package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
- package/source/views/PictView-Flow-Toolbar.js +491 -5
- package/source/views/PictView-Flow.js +830 -8
- package/example_application/package.json +0 -41
- package/example_application/source/Pict-Application-FlowExample.js +0 -241
- package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
- /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
- /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
- /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;
|