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
@@ -170,6 +170,165 @@ class PictServiceFlowLayout extends libFableServiceProviderBase
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * Auto-layout a subset of nodes, positioning them to the right of
175
+ * any fixed (already-positioned) nodes.
176
+ *
177
+ * Uses the same topological sort approach as autoLayout, but only
178
+ * repositions the nodes in pNodesToLayout. The pFixedNodes are used
179
+ * to compute a bounding box that the new layout avoids.
180
+ *
181
+ * @param {Array} pNodesToLayout - Nodes that need new positions
182
+ * @param {Array} pFixedNodes - Nodes that already have positions (read-only)
183
+ * @param {Array} pConnections - All connections in the flow
184
+ */
185
+ autoLayoutSubset(pNodesToLayout, pFixedNodes, pConnections)
186
+ {
187
+ if (!pNodesToLayout || pNodesToLayout.length === 0) return;
188
+
189
+ // Compute the starting X position to the right of all fixed nodes
190
+ let tmpStartX = this._StartX;
191
+ let tmpStartY = this._StartY;
192
+
193
+ if (pFixedNodes && pFixedNodes.length > 0)
194
+ {
195
+ let tmpMaxX = -Infinity;
196
+
197
+ for (let i = 0; i < pFixedNodes.length; i++)
198
+ {
199
+ let tmpRight = pFixedNodes[i].X + (pFixedNodes[i].Width || 180);
200
+ if (tmpRight > tmpMaxX)
201
+ {
202
+ tmpMaxX = tmpRight;
203
+ }
204
+ }
205
+
206
+ // Place unmatched nodes to the right of all fixed nodes
207
+ tmpStartX = tmpMaxX + this._HorizontalSpacing;
208
+ }
209
+
210
+ // Build a set of nodes we are laying out for quick lookup
211
+ let tmpNodeSet = {};
212
+ for (let i = 0; i < pNodesToLayout.length; i++)
213
+ {
214
+ tmpNodeSet[pNodesToLayout[i].Hash] = true;
215
+ }
216
+
217
+ // Build adjacency information only for nodes in the subset
218
+ let tmpNodeMap = {};
219
+ let tmpInDegree = {};
220
+ let tmpOutEdges = {};
221
+
222
+ for (let i = 0; i < pNodesToLayout.length; i++)
223
+ {
224
+ let tmpNode = pNodesToLayout[i];
225
+ tmpNodeMap[tmpNode.Hash] = tmpNode;
226
+ tmpInDegree[tmpNode.Hash] = 0;
227
+ tmpOutEdges[tmpNode.Hash] = [];
228
+ }
229
+
230
+ // Only count edges between nodes in the subset
231
+ for (let i = 0; i < pConnections.length; i++)
232
+ {
233
+ let tmpConn = pConnections[i];
234
+ let tmpSourceInSubset = tmpNodeSet[tmpConn.SourceNodeHash];
235
+ let tmpTargetInSubset = tmpNodeSet[tmpConn.TargetNodeHash];
236
+
237
+ if (tmpSourceInSubset && tmpTargetInSubset)
238
+ {
239
+ tmpInDegree[tmpConn.TargetNodeHash]++;
240
+ tmpOutEdges[tmpConn.SourceNodeHash].push(tmpConn.TargetNodeHash);
241
+ }
242
+ }
243
+
244
+ // Topological sort (Kahn's algorithm)
245
+ let tmpLayers = [];
246
+ let tmpQueue = [];
247
+ let tmpAssigned = {};
248
+
249
+ for (let tmpHash in tmpInDegree)
250
+ {
251
+ if (tmpInDegree[tmpHash] === 0)
252
+ {
253
+ tmpQueue.push(tmpHash);
254
+ }
255
+ }
256
+
257
+ while (tmpQueue.length > 0)
258
+ {
259
+ let tmpCurrentLayer = [];
260
+ let tmpNextQueue = [];
261
+
262
+ for (let i = 0; i < tmpQueue.length; i++)
263
+ {
264
+ let tmpNodeHash = tmpQueue[i];
265
+ if (tmpAssigned[tmpNodeHash]) continue;
266
+
267
+ tmpAssigned[tmpNodeHash] = true;
268
+ tmpCurrentLayer.push(tmpNodeHash);
269
+
270
+ let tmpEdges = tmpOutEdges[tmpNodeHash] || [];
271
+ for (let j = 0; j < tmpEdges.length; j++)
272
+ {
273
+ let tmpTargetHash = tmpEdges[j];
274
+ tmpInDegree[tmpTargetHash]--;
275
+ if (tmpInDegree[tmpTargetHash] <= 0 && !tmpAssigned[tmpTargetHash])
276
+ {
277
+ tmpNextQueue.push(tmpTargetHash);
278
+ }
279
+ }
280
+ }
281
+
282
+ if (tmpCurrentLayer.length > 0)
283
+ {
284
+ tmpLayers.push(tmpCurrentLayer);
285
+ }
286
+
287
+ tmpQueue = tmpNextQueue;
288
+ }
289
+
290
+ // Handle remaining unassigned nodes (cycles or disconnected)
291
+ let tmpRemainingNodes = [];
292
+ for (let i = 0; i < pNodesToLayout.length; i++)
293
+ {
294
+ if (!tmpAssigned[pNodesToLayout[i].Hash])
295
+ {
296
+ tmpRemainingNodes.push(pNodesToLayout[i].Hash);
297
+ }
298
+ }
299
+ if (tmpRemainingNodes.length > 0)
300
+ {
301
+ tmpLayers.push(tmpRemainingNodes);
302
+ }
303
+
304
+ // Assign positions based on layers, starting from tmpStartX
305
+ let tmpCurrentX = tmpStartX;
306
+
307
+ for (let tmpLayerIndex = 0; tmpLayerIndex < tmpLayers.length; tmpLayerIndex++)
308
+ {
309
+ let tmpLayer = tmpLayers[tmpLayerIndex];
310
+ let tmpMaxWidth = 0;
311
+ let tmpCurrentY = tmpStartY;
312
+
313
+ for (let i = 0; i < tmpLayer.length; i++)
314
+ {
315
+ let tmpNode = tmpNodeMap[tmpLayer[i]];
316
+ if (!tmpNode) continue;
317
+
318
+ tmpNode.X = tmpCurrentX;
319
+ tmpNode.Y = tmpCurrentY;
320
+
321
+ let tmpWidth = tmpNode.Width || 180;
322
+ let tmpHeight = tmpNode.Height || 80;
323
+
324
+ tmpMaxWidth = Math.max(tmpMaxWidth, tmpWidth);
325
+ tmpCurrentY += tmpHeight + this._VerticalSpacing;
326
+ }
327
+
328
+ tmpCurrentX += tmpMaxWidth + this._HorizontalSpacing;
329
+ }
330
+ }
331
+
173
332
  /**
174
333
  * Center all nodes around a given point
175
334
  * @param {Array} pNodes
@@ -0,0 +1,199 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * PictService-Flow-PathGenerator
5
+ *
6
+ * Centralizes SVG path generation for the flow diagram.
7
+ * Provides shared building blocks used by both the ConnectionRenderer
8
+ * (port-to-port connections) and the TetherService (panel-to-node tethers).
9
+ *
10
+ * Responsibilities:
11
+ * - Departure/approach point calculation from anchors
12
+ * - Auto orthogonal corner computation for right-angle paths
13
+ * - Cubic bezier evaluation at arbitrary parameter t
14
+ * - SVG path string assembly (bezier, split-bezier, orthogonal)
15
+ */
16
+ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
17
+ {
18
+ constructor(pFable, pOptions, pServiceHash)
19
+ {
20
+ super(pFable, pOptions, pServiceHash);
21
+
22
+ this.serviceType = 'PictServiceFlowPathGenerator';
23
+
24
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
25
+ }
26
+
27
+ // ---- Departure / Approach Calculation ----
28
+
29
+ /**
30
+ * Compute departure and approach points from start/end anchors.
31
+ * The departure point extends outward from the start in its side direction,
32
+ * and the approach point extends outward from the end in its side direction.
33
+ *
34
+ * @param {{x: number, y: number, side: string}} pFrom - Start anchor with side
35
+ * @param {{x: number, y: number, side: string}} pTo - End anchor with side
36
+ * @param {number} pDepartDist - Distance for departure/approach straight segments
37
+ * @returns {{departX: number, departY: number, approachX: number, approachY: number, fromDir: {dx: number, dy: number}, toDir: {dx: number, dy: number}}}
38
+ */
39
+ computeDepartApproach(pFrom, pTo, pDepartDist)
40
+ {
41
+ let tmpGeometry = this._FlowView._GeometryProvider;
42
+
43
+ let tmpFromDir = tmpGeometry.sideDirection(pFrom.side || 'right');
44
+ let tmpToDir = tmpGeometry.sideDirection(pTo.side || 'left');
45
+
46
+ return {
47
+ departX: pFrom.x + tmpFromDir.dx * pDepartDist,
48
+ departY: pFrom.y + tmpFromDir.dy * pDepartDist,
49
+ approachX: pTo.x + tmpToDir.dx * pDepartDist,
50
+ approachY: pTo.y + tmpToDir.dy * pDepartDist,
51
+ fromDir: tmpFromDir,
52
+ toDir: tmpToDir
53
+ };
54
+ }
55
+
56
+ // ---- Orthogonal Corner Calculation ----
57
+
58
+ /**
59
+ * Compute auto orthogonal corners for an L-shaped or Z-shaped path.
60
+ * Determines corner placement based on departure/approach directions.
61
+ *
62
+ * Used by both connection and tether renderers for right-angle paths.
63
+ *
64
+ * @param {number} pDepartX
65
+ * @param {number} pDepartY
66
+ * @param {number} pApproachX
67
+ * @param {number} pApproachY
68
+ * @param {{dx: number, dy: number}} pFromDir - Departure direction vector
69
+ * @param {{dx: number, dy: number}} pToDir - Approach direction vector
70
+ * @param {number} pMidOffset - Offset for the corridor midpoint
71
+ * @returns {{corner1: {x: number, y: number}, corner2: {x: number, y: number}, midpoint: {x: number, y: number}}}
72
+ */
73
+ computeAutoOrthogonalCorners(pDepartX, pDepartY, pApproachX, pApproachY, pFromDir, pToDir, pMidOffset)
74
+ {
75
+ let tmpOffset = pMidOffset || 0;
76
+ let tmpFromHoriz = Math.abs(pFromDir.dx) > 0;
77
+ let tmpToHoriz = Math.abs(pToDir.dx) > 0;
78
+
79
+ let tmpCorner1, tmpCorner2, tmpMidpoint;
80
+
81
+ if (tmpFromHoriz && tmpToHoriz)
82
+ {
83
+ // Both horizontal departure/approach: corridor is vertical
84
+ let tmpMidX = (pDepartX + pApproachX) / 2 + tmpOffset;
85
+ tmpCorner1 = { x: tmpMidX, y: pDepartY };
86
+ tmpCorner2 = { x: tmpMidX, y: pApproachY };
87
+ tmpMidpoint = { x: tmpMidX, y: (pDepartY + pApproachY) / 2 };
88
+ }
89
+ else if (!tmpFromHoriz && !tmpToHoriz)
90
+ {
91
+ // Both vertical: corridor is horizontal
92
+ let tmpMidY = (pDepartY + pApproachY) / 2 + tmpOffset;
93
+ tmpCorner1 = { x: pDepartX, y: tmpMidY };
94
+ tmpCorner2 = { x: pApproachX, y: tmpMidY };
95
+ tmpMidpoint = { x: (pDepartX + pApproachX) / 2, y: tmpMidY };
96
+ }
97
+ else if (tmpFromHoriz && !tmpToHoriz)
98
+ {
99
+ // Horizontal→Vertical: single L-bend
100
+ tmpCorner1 = { x: pApproachX + tmpOffset, y: pDepartY };
101
+ tmpCorner2 = { x: pApproachX + tmpOffset, y: pApproachY };
102
+ tmpMidpoint = { x: pApproachX + tmpOffset, y: (pDepartY + pApproachY) / 2 };
103
+ }
104
+ else
105
+ {
106
+ // Vertical→Horizontal: single L-bend
107
+ tmpCorner1 = { x: pDepartX, y: pApproachY + tmpOffset };
108
+ tmpCorner2 = { x: pApproachX, y: pApproachY + tmpOffset };
109
+ tmpMidpoint = { x: (pDepartX + pApproachX) / 2, y: pApproachY + tmpOffset };
110
+ }
111
+
112
+ return { corner1: tmpCorner1, corner2: tmpCorner2, midpoint: tmpMidpoint };
113
+ }
114
+
115
+ // ---- Bezier Evaluation ----
116
+
117
+ /**
118
+ * Evaluate a cubic bezier curve at parameter t.
119
+ * B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
120
+ *
121
+ * @param {{x: number, y: number}} pP0 - Start point
122
+ * @param {{x: number, y: number}} pP1 - First control point
123
+ * @param {{x: number, y: number}} pP2 - Second control point
124
+ * @param {{x: number, y: number}} pP3 - End point
125
+ * @param {number} pT - Parameter in range [0, 1]
126
+ * @returns {{x: number, y: number}}
127
+ */
128
+ evaluateCubicBezier(pP0, pP1, pP2, pP3, pT)
129
+ {
130
+ let tmpOMT = 1 - pT;
131
+ let tmpOMT2 = tmpOMT * tmpOMT;
132
+ let tmpOMT3 = tmpOMT2 * tmpOMT;
133
+ let tmpT2 = pT * pT;
134
+ let tmpT3 = tmpT2 * pT;
135
+
136
+ return {
137
+ x: tmpOMT3 * pP0.x + 3 * tmpOMT2 * pT * pP1.x + 3 * tmpOMT * tmpT2 * pP2.x + tmpT3 * pP3.x,
138
+ y: tmpOMT3 * pP0.y + 3 * tmpOMT2 * pT * pP1.y + 3 * tmpOMT * tmpT2 * pP2.y + tmpT3 * pP3.y
139
+ };
140
+ }
141
+
142
+ // ---- SVG Path String Assembly ----
143
+
144
+ /**
145
+ * Build an SVG bezier path string.
146
+ * Pattern: M start L depart C cp1, cp2, approach L end
147
+ *
148
+ * @param {{x: number, y: number}} pStart - Start point
149
+ * @param {{x: number, y: number}} pDepart - Departure point after straight segment
150
+ * @param {{x: number, y: number}} pCP1 - First control point
151
+ * @param {{x: number, y: number}} pCP2 - Second control point
152
+ * @param {{x: number, y: number}} pApproach - Approach point before final straight segment
153
+ * @param {{x: number, y: number}} pEnd - End point
154
+ * @returns {string} SVG path d attribute
155
+ */
156
+ buildBezierPathString(pStart, pDepart, pCP1, pCP2, pApproach, pEnd)
157
+ {
158
+ return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} C ${pCP1.x} ${pCP1.y}, ${pCP2.x} ${pCP2.y}, ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
159
+ }
160
+
161
+ /**
162
+ * Build an SVG split bezier path string (two cubic segments through a handle point).
163
+ * Pattern: M start L depart C cp1a, cp1b, handle C cp2a, cp2b, approach L end
164
+ *
165
+ * @param {{x: number, y: number}} pStart
166
+ * @param {{x: number, y: number}} pDepart
167
+ * @param {{x: number, y: number}} pCP1a - First segment's first control point
168
+ * @param {{x: number, y: number}} pCP1b - First segment's second control point
169
+ * @param {{x: number, y: number}} pHandle - Handle point where the two segments meet
170
+ * @param {{x: number, y: number}} pCP2a - Second segment's first control point
171
+ * @param {{x: number, y: number}} pCP2b - Second segment's second control point
172
+ * @param {{x: number, y: number}} pApproach
173
+ * @param {{x: number, y: number}} pEnd
174
+ * @returns {string} SVG path d attribute
175
+ */
176
+ buildSplitBezierPathString(pStart, pDepart, pCP1a, pCP1b, pHandle, pCP2a, pCP2b, pApproach, pEnd)
177
+ {
178
+ return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} C ${pCP1a.x} ${pCP1a.y}, ${pCP1b.x} ${pCP1b.y}, ${pHandle.x} ${pHandle.y} C ${pCP2a.x} ${pCP2a.y}, ${pCP2b.x} ${pCP2b.y}, ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
179
+ }
180
+
181
+ /**
182
+ * Build an SVG orthogonal (right-angle) path string.
183
+ * Pattern: M start L depart L corner1 L corner2 L approach L end
184
+ *
185
+ * @param {{x: number, y: number}} pStart
186
+ * @param {{x: number, y: number}} pDepart
187
+ * @param {{x: number, y: number}} pCorner1
188
+ * @param {{x: number, y: number}} pCorner2
189
+ * @param {{x: number, y: number}} pApproach
190
+ * @param {{x: number, y: number}} pEnd
191
+ * @returns {string} SVG path d attribute
192
+ */
193
+ buildOrthogonalPathString(pStart, pDepart, pCorner1, pCorner2, pApproach, pEnd)
194
+ {
195
+ return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} L ${pCorner1.x} ${pCorner1.y} L ${pCorner2.x} ${pCorner2.y} L ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
196
+ }
197
+ }
198
+
199
+ module.exports = PictServiceFlowPathGenerator;