pict-section-flow 1.3.0 → 2.0.0

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 (162) hide show
  1. package/package.json +7 -2
  2. package/source/Pict-Section-Flow.js +20 -14
  3. package/source/providers/PictProvider-Flow-Background.js +303 -0
  4. package/source/providers/PictProvider-Flow-CSS.js +99 -7
  5. package/source/providers/PictProvider-Flow-ConnectorShapes.js +8 -0
  6. package/source/providers/PictProvider-Flow-Geometry.js +11 -421
  7. package/source/providers/PictProvider-Flow-Icons.js +20 -0
  8. package/source/providers/PictProvider-Flow-Layouts.js +107 -0
  9. package/source/services/PictService-Flow-ConnectionRenderer.js +77 -5
  10. package/source/services/PictService-Flow-CursorManager.js +113 -0
  11. package/source/services/PictService-Flow-InteractionManager.js +443 -61
  12. package/source/services/PictService-Flow-Layout.js +21 -16
  13. package/source/services/PictService-Flow-PathGenerator.js +30 -417
  14. package/source/services/PictService-Flow-RenderManager.js +9 -1
  15. package/source/services/PictService-Flow-ViewportManager.js +102 -0
  16. package/source/views/PictView-Flow-FloatingToolbar.js +57 -0
  17. package/source/views/PictView-Flow-Node.js +36 -0
  18. package/source/views/PictView-Flow-PropertiesPanel.js +27 -5
  19. package/source/views/PictView-Flow-Toolbar.js +148 -13
  20. package/source/views/PictView-Flow.js +628 -3
  21. package/.claude/launch.json +0 -11
  22. package/docs/.nojekyll +0 -0
  23. package/docs/Architecture.md +0 -163
  24. package/docs/Custom-Styling.md +0 -275
  25. package/docs/Data_Model.md +0 -149
  26. package/docs/Event_System.md +0 -156
  27. package/docs/Getting_Started.md +0 -237
  28. package/docs/Implementation_Reference.md +0 -528
  29. package/docs/Layout_Persistence.md +0 -117
  30. package/docs/README.md +0 -103
  31. package/docs/Theme_Integration.md +0 -150
  32. package/docs/_brand.json +0 -18
  33. package/docs/_cover.md +0 -17
  34. package/docs/_playground.json +0 -24
  35. package/docs/_sidebar.md +0 -57
  36. package/docs/_topbar.md +0 -8
  37. package/docs/_version.json +0 -7
  38. package/docs/api/PictFlowCard.md +0 -216
  39. package/docs/api/PictFlowCardPropertiesPanel.md +0 -235
  40. package/docs/api/addConnection.md +0 -101
  41. package/docs/api/addNode.md +0 -137
  42. package/docs/api/autoLayout.md +0 -77
  43. package/docs/api/getFlowData.md +0 -112
  44. package/docs/api/marshalToView.md +0 -95
  45. package/docs/api/openPanel.md +0 -128
  46. package/docs/api/registerHandler.md +0 -174
  47. package/docs/api/registerNodeType.md +0 -142
  48. package/docs/api/removeConnection.md +0 -57
  49. package/docs/api/removeNode.md +0 -80
  50. package/docs/api/saveLayout.md +0 -152
  51. package/docs/api/screenToSVGCoords.md +0 -68
  52. package/docs/api/selectNode.md +0 -116
  53. package/docs/api/setTheme.md +0 -168
  54. package/docs/api/setZoom.md +0 -97
  55. package/docs/api/toggleFullscreen.md +0 -68
  56. package/docs/card-help/EACH.md +0 -19
  57. package/docs/card-help/FREAD.md +0 -24
  58. package/docs/card-help/FWRITE.md +0 -24
  59. package/docs/card-help/GET.md +0 -22
  60. package/docs/card-help/ITE.md +0 -23
  61. package/docs/card-help/LOG.md +0 -23
  62. package/docs/card-help/NOTE.md +0 -17
  63. package/docs/card-help/PREV.md +0 -18
  64. package/docs/card-help/SET.md +0 -27
  65. package/docs/card-help/SPKL.md +0 -22
  66. package/docs/card-help/STAT.md +0 -23
  67. package/docs/card-help/SW.md +0 -25
  68. package/docs/diagrams/architecture-at-a-glance.excalidraw +0 -4270
  69. package/docs/diagrams/architecture-at-a-glance.mmd +0 -30
  70. package/docs/diagrams/architecture-at-a-glance.svg +0 -2
  71. package/docs/diagrams/data-flow.excalidraw +0 -1451
  72. package/docs/diagrams/data-flow.mmd +0 -17
  73. package/docs/diagrams/data-flow.svg +0 -2
  74. package/docs/diagrams/high-level-design.excalidraw +0 -5767
  75. package/docs/diagrams/high-level-design.mmd +0 -86
  76. package/docs/diagrams/high-level-design.svg +0 -2
  77. package/docs/diagrams/relationships.excalidraw +0 -3852
  78. package/docs/diagrams/relationships.mmd +0 -9
  79. package/docs/diagrams/relationships.svg +0 -2
  80. package/docs/diagrams/service-initialization-sequence.excalidraw +0 -1466
  81. package/docs/diagrams/service-initialization-sequence.mmd +0 -19
  82. package/docs/diagrams/service-initialization-sequence.svg +0 -2
  83. package/docs/diagrams/svg-layer-structure.excalidraw +0 -1060
  84. package/docs/diagrams/svg-layer-structure.mmd +0 -18
  85. package/docs/diagrams/svg-layer-structure.svg +0 -2
  86. package/docs/examples/README.md +0 -9
  87. package/docs/examples/simple_cards/README.md +0 -677
  88. package/docs/examples/simple_cards/css/flowexample.css +0 -65
  89. package/docs/examples/simple_cards/index.html +0 -32
  90. package/docs/examples/simple_cards/js/pict.min.js +0 -12
  91. package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +0 -1
  92. package/docs/index.html +0 -38
  93. package/docs/playground/app.json +0 -6
  94. package/docs/playground/appdata.json +0 -85
  95. package/docs/playground/application.js +0 -23
  96. package/docs/playground/pict.json +0 -17
  97. package/docs/playground/runtime/pict-application.min.js +0 -2
  98. package/docs/playground/runtime/pict-section-flow.min.js +0 -2
  99. package/docs/playground/runtime/pict-section-modal.min.js +0 -2
  100. package/docs/playground/runtime/pict.min.js +0 -12
  101. package/docs/retold-catalog.json +0 -244
  102. package/docs/retold-keyword-index.json +0 -26028
  103. package/example_applications/simple_cards/css/flowexample.css +0 -65
  104. package/example_applications/simple_cards/html/index.html +0 -32
  105. package/example_applications/simple_cards/package.json +0 -52
  106. package/example_applications/simple_cards/source/Pict-Application-FlowExample-Configuration.json +0 -15
  107. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +0 -539
  108. package/example_applications/simple_cards/source/card-help-content.js +0 -16
  109. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +0 -38
  110. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +0 -44
  111. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +0 -38
  112. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +0 -56
  113. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +0 -50
  114. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +0 -37
  115. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +0 -49
  116. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +0 -55
  117. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +0 -97
  118. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +0 -100
  119. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +0 -46
  120. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +0 -39
  121. package/example_applications/simple_cards/source/providers/PictRouter-FlowExample-Configuration.json +0 -22
  122. package/example_applications/simple_cards/source/sample-flows.js +0 -410
  123. package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +0 -184
  124. package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +0 -77
  125. package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +0 -325
  126. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +0 -59
  127. package/example_applications/simple_cards/source/views/PictView-FlowExample-Layout.js +0 -90
  128. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +0 -453
  129. package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +0 -95
  130. package/scripts/generate-card-help.js +0 -214
  131. package/source/providers/edges/Edge-Bezier.js +0 -41
  132. package/source/providers/edges/Edge-Orthogonal.js +0 -37
  133. package/source/providers/edges/Edge-OrthogonalSnap.js +0 -72
  134. package/source/providers/edges/Edge-Perimeter-Linear.js +0 -31
  135. package/source/providers/edges/Edge-Perimeter-Orthogonal.js +0 -39
  136. package/source/providers/edges/Edge-Perimeter.js +0 -48
  137. package/source/providers/edges/Edge-PerimeterMath.js +0 -92
  138. package/source/providers/edges/Edge-Straight.js +0 -24
  139. package/source/providers/layouts/Layout-Circular.js +0 -203
  140. package/source/providers/layouts/Layout-Coerce.js +0 -40
  141. package/source/providers/layouts/Layout-Columnar.js +0 -134
  142. package/source/providers/layouts/Layout-Custom.js +0 -27
  143. package/source/providers/layouts/Layout-ForcedFromCenter.js +0 -256
  144. package/source/providers/layouts/Layout-Grid.js +0 -134
  145. package/source/providers/layouts/Layout-Layered.js +0 -155
  146. package/source/providers/layouts/Layout-Rank.js +0 -141
  147. package/source/providers/layouts/Layout-Staggered.js +0 -131
  148. package/source/providers/layouts/Layout-Tabular.js +0 -94
  149. package/test/ConnectionHandleManager_tests.js +0 -717
  150. package/test/ConnectionRenderer_tests.js +0 -591
  151. package/test/DataManager_tests.js +0 -859
  152. package/test/Geometry_tests.js +0 -767
  153. package/test/InteractionManager_tests.js +0 -279
  154. package/test/Layout_tests.js +0 -1604
  155. package/test/NodeView_tests.js +0 -66
  156. package/test/PanelManager_tests.js +0 -172
  157. package/test/PathGenerator_tests.js +0 -978
  158. package/test/PortRenderer_tests.js +0 -376
  159. package/test/RenderManager_tests.js +0 -756
  160. package/test/Renderer_tests.js +0 -133
  161. package/test/SelectionManager_tests.js +0 -185
  162. package/test/StylePresets_tests.js +0 -153
@@ -1,21 +1,26 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
2
 
3
- const libLayoutCustom = require('../providers/layouts/Layout-Custom.js');
4
- const libLayoutLayered = require('../providers/layouts/Layout-Layered.js');
5
- const libLayoutStaggered = require('../providers/layouts/Layout-Staggered.js');
6
- const libLayoutForcedFromCenter = require('../providers/layouts/Layout-ForcedFromCenter.js');
7
- const libLayoutGrid = require('../providers/layouts/Layout-Grid.js');
8
- const libLayoutCircular = require('../providers/layouts/Layout-Circular.js');
9
- const libLayoutTabular = require('../providers/layouts/Layout-Tabular.js');
10
- const libLayoutColumnar = require('../providers/layouts/Layout-Columnar.js');
11
-
12
- const libEdgeBezier = require('../providers/edges/Edge-Bezier.js');
13
- const libEdgeOrthogonal = require('../providers/edges/Edge-Orthogonal.js');
14
- const libEdgeStraight = require('../providers/edges/Edge-Straight.js');
15
- const libEdgeOrthogonalSnap = require('../providers/edges/Edge-OrthogonalSnap.js');
16
- const libEdgePerimeter = require('../providers/edges/Edge-Perimeter.js');
17
- const libEdgePerimeterLinear = require('../providers/edges/Edge-Perimeter-Linear.js');
18
- const libEdgePerimeterOrthogonal = require('../providers/edges/Edge-Perimeter-Orthogonal.js');
3
+ // Layout algorithm and edge-theme descriptors now live in the standalone
4
+ // pict-provider-graphlayout module (flow 2.0 Phase 1b). The variable names below
5
+ // are unchanged so the registration logic that follows is untouched.
6
+ const libGraphLayout = require('pict-provider-graphlayout');
7
+
8
+ const libLayoutCustom = libGraphLayout.Layouts.Custom;
9
+ const libLayoutLayered = libGraphLayout.Layouts.Layered;
10
+ const libLayoutStaggered = libGraphLayout.Layouts.Staggered;
11
+ const libLayoutForcedFromCenter = libGraphLayout.Layouts.ForcedFromCenter;
12
+ const libLayoutGrid = libGraphLayout.Layouts.Grid;
13
+ const libLayoutCircular = libGraphLayout.Layouts.Circular;
14
+ const libLayoutTabular = libGraphLayout.Layouts.Tabular;
15
+ const libLayoutColumnar = libGraphLayout.Layouts.Columnar;
16
+
17
+ const libEdgeBezier = libGraphLayout.Edges.Bezier;
18
+ const libEdgeOrthogonal = libGraphLayout.Edges.Orthogonal;
19
+ const libEdgeStraight = libGraphLayout.Edges.Straight;
20
+ const libEdgeOrthogonalSnap = libGraphLayout.Edges.OrthogonalSnap;
21
+ const libEdgePerimeter = libGraphLayout.Edges.Perimeter;
22
+ const libEdgePerimeterLinear = libGraphLayout.Edges.PerimeterLinear;
23
+ const libEdgePerimeterOrthogonal = libGraphLayout.Edges.PerimeterOrthogonal;
19
24
 
20
25
  const _BUILTIN_ALGORITHMS =
21
26
  [
@@ -1,17 +1,19 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
2
 
3
+ const libPathGeneratorCore = require('pict-provider-graphlayout').PathGenerator;
4
+
3
5
  /**
4
6
  * PictService-Flow-PathGenerator
5
7
  *
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).
8
+ * Backwards-compatible shim. The SVG path-generation math now lives in the
9
+ * standalone pict-provider-graphlayout module as a geometry-injected core (flow
10
+ * 2.0 Phase 1b). This service keeps the historical serviceType and the
11
+ * `_FlowView` property, and delegates every method to the core. The core gets a
12
+ * resolver so it reads the live FlowView geometry provider at call time, exactly
13
+ * as the original implementation did.
9
14
  *
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
+ * Used by the ConnectionRenderer (port-to-port connections) and the
16
+ * TetherService (panel-to-node tethers).
15
17
  */
16
18
  class PictServiceFlowPathGenerator extends libFableServiceProviderBase
17
19
  {
@@ -22,459 +24,70 @@ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
22
24
  this.serviceType = 'PictServiceFlowPathGenerator';
23
25
 
24
26
  this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
25
- }
26
27
 
27
- // ---- Departure / Approach Calculation ----
28
+ let tmpSelf = this;
29
+ this._Core = new libPathGeneratorCore(
30
+ {
31
+ geometryResolver: function ()
32
+ {
33
+ return tmpSelf._FlowView ? tmpSelf._FlowView._GeometryProvider : null;
34
+ }
35
+ });
36
+ }
28
37
 
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
38
  computeDepartApproach(pFrom, pTo, pDepartDist)
40
39
  {
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
- };
40
+ return this._Core.computeDepartApproach(pFrom, pTo, pDepartDist);
54
41
  }
55
42
 
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
43
  computeAutoOrthogonalCorners(pDepartX, pDepartY, pApproachX, pApproachY, pFromDir, pToDir, pMidOffset)
74
44
  {
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 };
45
+ return this._Core.computeAutoOrthogonalCorners(pDepartX, pDepartY, pApproachX, pApproachY, pFromDir, pToDir, pMidOffset);
113
46
  }
114
47
 
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
48
  evaluateCubicBezier(pP0, pP1, pP2, pP3, pT)
129
49
  {
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
- };
50
+ return this._Core.evaluateCubicBezier(pP0, pP1, pP2, pP3, pT);
140
51
  }
141
52
 
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
53
  buildBezierPathString(pStart, pDepart, pCP1, pCP2, pApproach, pEnd)
157
54
  {
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}`;
55
+ return this._Core.buildBezierPathString(pStart, pDepart, pCP1, pCP2, pApproach, pEnd);
159
56
  }
160
57
 
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
58
  buildSplitBezierPathString(pStart, pDepart, pCP1a, pCP1b, pHandle, pCP2a, pCP2b, pApproach, pEnd)
177
59
  {
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}`;
60
+ return this._Core.buildSplitBezierPathString(pStart, pDepart, pCP1a, pCP1b, pHandle, pCP2a, pCP2b, pApproach, pEnd);
179
61
  }
180
62
 
181
- /**
182
- * Build an SVG multi-segment bezier path string.
183
- * Generates N+1 cubic bezier segments through N handle points.
184
- *
185
- * Pattern: M start L depart C cp,cp,handle[0] C cp,cp,handle[1] ... C cp,cp,approach L end
186
- *
187
- * Control points are computed using Catmull-Rom-to-Bezier conversion
188
- * for C1 (smooth tangent) continuity at every handle.
189
- *
190
- * @param {{x: number, y: number}} pStart - Port anchor start
191
- * @param {{x: number, y: number}} pDepart - Departure point after straight segment
192
- * @param {Array<{x: number, y: number}>} pHandles - Ordered handle waypoints
193
- * @param {{x: number, y: number}} pApproach - Approach point before final straight segment
194
- * @param {{x: number, y: number}} pEnd - Port anchor end
195
- * @param {{dx: number, dy: number}} pStartDir - Departure direction unit vector
196
- * @param {{dx: number, dy: number}} pEndDir - Approach direction unit vector
197
- * @returns {string} SVG path d attribute
198
- */
199
63
  buildMultiBezierPathString(pStart, pDepart, pHandles, pApproach, pEnd, pStartDir, pEndDir)
200
64
  {
201
- // Build the full list of waypoints: depart, handle[0..N-1], approach
202
- let tmpWaypoints = [pDepart];
203
- for (let i = 0; i < pHandles.length; i++)
204
- {
205
- tmpWaypoints.push(pHandles[i]);
206
- }
207
- tmpWaypoints.push(pApproach);
208
-
209
- let tmpPath = `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y}`;
210
-
211
- for (let i = 0; i < tmpWaypoints.length - 1; i++)
212
- {
213
- let tmpFrom = tmpWaypoints[i];
214
- let tmpTo = tmpWaypoints[i + 1];
215
-
216
- let tmpSegDX = tmpTo.x - tmpFrom.x;
217
- let tmpSegDY = tmpTo.y - tmpFrom.y;
218
- let tmpSegLen = Math.sqrt(tmpSegDX * tmpSegDX + tmpSegDY * tmpSegDY);
219
- if (tmpSegLen < 1)
220
- {
221
- tmpSegLen = 1;
222
- }
223
- let tmpScale = tmpSegLen * 0.35;
224
-
225
- // Tangent at tmpFrom
226
- let tmpTanFromX, tmpTanFromY;
227
- if (i === 0)
228
- {
229
- // First segment: use the port departure direction
230
- tmpTanFromX = pStartDir.dx;
231
- tmpTanFromY = pStartDir.dy;
232
- }
233
- else
234
- {
235
- // Interior handle: tangent points from previous toward next waypoint
236
- let tmpPrev = tmpWaypoints[i - 1];
237
- let tmpNext = tmpWaypoints[i + 1];
238
- tmpTanFromX = tmpNext.x - tmpPrev.x;
239
- tmpTanFromY = tmpNext.y - tmpPrev.y;
240
- let tmpTanLen = Math.sqrt(tmpTanFromX * tmpTanFromX + tmpTanFromY * tmpTanFromY);
241
- if (tmpTanLen < 1) tmpTanLen = 1;
242
- tmpTanFromX /= tmpTanLen;
243
- tmpTanFromY /= tmpTanLen;
244
- }
245
-
246
- // Tangent at tmpTo
247
- let tmpTanToX, tmpTanToY;
248
- if (i === tmpWaypoints.length - 2)
249
- {
250
- // Last segment: use the port approach direction (reversed for incoming)
251
- tmpTanToX = -pEndDir.dx;
252
- tmpTanToY = -pEndDir.dy;
253
- }
254
- else
255
- {
256
- // Interior handle: tangent points from previous toward next waypoint
257
- let tmpPrev = tmpWaypoints[i];
258
- let tmpNext = tmpWaypoints[i + 2];
259
- tmpTanToX = tmpNext.x - tmpPrev.x;
260
- tmpTanToY = tmpNext.y - tmpPrev.y;
261
- let tmpTanLen = Math.sqrt(tmpTanToX * tmpTanToX + tmpTanToY * tmpTanToY);
262
- if (tmpTanLen < 1) tmpTanLen = 1;
263
- tmpTanToX /= tmpTanLen;
264
- tmpTanToY /= tmpTanLen;
265
- }
266
-
267
- let tmpCP1X = tmpFrom.x + tmpTanFromX * tmpScale;
268
- let tmpCP1Y = tmpFrom.y + tmpTanFromY * tmpScale;
269
- let tmpCP2X = tmpTo.x - tmpTanToX * tmpScale;
270
- let tmpCP2Y = tmpTo.y - tmpTanToY * tmpScale;
271
-
272
- tmpPath += ` C ${tmpCP1X} ${tmpCP1Y}, ${tmpCP2X} ${tmpCP2Y}, ${tmpTo.x} ${tmpTo.y}`;
273
- }
274
-
275
- tmpPath += ` L ${pEnd.x} ${pEnd.y}`;
276
-
277
- return tmpPath;
65
+ return this._Core.buildMultiBezierPathString(pStart, pDepart, pHandles, pApproach, pEnd, pStartDir, pEndDir);
278
66
  }
279
67
 
280
- /**
281
- * Build an SVG orthogonal (right-angle) path string.
282
- * Pattern: M start L depart L corner1 L corner2 L approach L end
283
- *
284
- * @param {{x: number, y: number}} pStart
285
- * @param {{x: number, y: number}} pDepart
286
- * @param {{x: number, y: number}} pCorner1
287
- * @param {{x: number, y: number}} pCorner2
288
- * @param {{x: number, y: number}} pApproach
289
- * @param {{x: number, y: number}} pEnd
290
- * @returns {string} SVG path d attribute
291
- */
292
68
  buildOrthogonalPathString(pStart, pDepart, pCorner1, pCorner2, pApproach, pEnd)
293
69
  {
294
- 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}`;
70
+ return this._Core.buildOrthogonalPathString(pStart, pDepart, pCorner1, pCorner2, pApproach, pEnd);
295
71
  }
296
72
 
297
- // ---- Directional Geometry ----
298
-
299
- /**
300
- * Compute full directional geometry between two port anchors, including
301
- * departure/approach points and bezier control points.
302
- *
303
- * Uses sophisticated facing detection: when ports face each other the
304
- * curve offset scales with inline distance; when ports are on the same
305
- * axis but not facing, a wider offset prevents the path from collapsing;
306
- * perpendicular exits use a moderate offset.
307
- *
308
- * @param {{x: number, y: number, side: string}} pStart
309
- * @param {{x: number, y: number, side: string}} pEnd
310
- * @returns {{departX: number, departY: number, approachX: number, approachY: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, startDir: {dx: number, dy: number}, endDir: {dx: number, dy: number}}}
311
- */
312
73
  computeDirectionalGeometry(pStart, pEnd)
313
74
  {
314
- let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
315
- let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
316
-
317
- let tmpStraightLen = 20;
318
-
319
- let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
320
- let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
321
-
322
- let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
323
- let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
324
-
325
- let tmpDX = Math.abs(tmpApproachX - tmpDepartX);
326
- let tmpDY = Math.abs(tmpApproachY - tmpDepartY);
327
- let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
328
-
329
- let tmpBaseOffset = Math.max(Math.min(tmpDist * 0.4, 180), 30);
330
-
331
- let tmpSameAxis = (tmpStartDir.dx !== 0 && tmpEndDir.dx !== 0) ||
332
- (tmpStartDir.dy !== 0 && tmpEndDir.dy !== 0);
333
-
334
- let tmpFacingEachOther = false;
335
- if (tmpSameAxis)
336
- {
337
- if (tmpStartDir.dx === 1 && tmpEndDir.dx === -1 && pEnd.x >= pStart.x)
338
- {
339
- tmpFacingEachOther = true;
340
- }
341
- else if (tmpStartDir.dx === -1 && tmpEndDir.dx === 1 && pEnd.x <= pStart.x)
342
- {
343
- tmpFacingEachOther = true;
344
- }
345
- else if (tmpStartDir.dy === 1 && tmpEndDir.dy === -1 && pEnd.y >= pStart.y)
346
- {
347
- tmpFacingEachOther = true;
348
- }
349
- else if (tmpStartDir.dy === -1 && tmpEndDir.dy === 1 && pEnd.y <= pStart.y)
350
- {
351
- tmpFacingEachOther = true;
352
- }
353
- }
354
-
355
- let tmpCurveOffset;
356
-
357
- if (tmpFacingEachOther)
358
- {
359
- let tmpInlineDist = (tmpStartDir.dx !== 0) ? tmpDX : tmpDY;
360
- tmpCurveOffset = Math.max(tmpInlineDist * 0.35, 30);
361
- }
362
- else if (tmpSameAxis)
363
- {
364
- tmpCurveOffset = Math.max(tmpBaseOffset, 60);
365
- }
366
- else
367
- {
368
- tmpCurveOffset = Math.max(tmpBaseOffset * 0.8, 40);
369
- }
370
-
371
- let tmpCP1X = tmpDepartX + tmpStartDir.dx * tmpCurveOffset;
372
- let tmpCP1Y = tmpDepartY + tmpStartDir.dy * tmpCurveOffset;
373
- let tmpCP2X = tmpApproachX + tmpEndDir.dx * tmpCurveOffset;
374
- let tmpCP2Y = tmpApproachY + tmpEndDir.dy * tmpCurveOffset;
375
-
376
- return {
377
- departX: tmpDepartX, departY: tmpDepartY,
378
- approachX: tmpApproachX, approachY: tmpApproachY,
379
- cp1X: tmpCP1X, cp1Y: tmpCP1Y,
380
- cp2X: tmpCP2X, cp2Y: tmpCP2Y,
381
- startDir: tmpStartDir, endDir: tmpEndDir
382
- };
75
+ return this._Core.computeDirectionalGeometry(pStart, pEnd);
383
76
  }
384
77
 
385
- // ---- Distance Utilities ----
386
-
387
- /**
388
- * Distance from point (pPX, pPY) to line segment (pAX, pAY)-(pBX, pBY).
389
- * Pure math utility, no state.
390
- *
391
- * @param {number} pPX
392
- * @param {number} pPY
393
- * @param {number} pAX
394
- * @param {number} pAY
395
- * @param {number} pBX
396
- * @param {number} pBY
397
- * @returns {number}
398
- */
399
78
  distanceToSegment(pPX, pPY, pAX, pAY, pBX, pBY)
400
79
  {
401
- let tmpDX = pBX - pAX;
402
- let tmpDY = pBY - pAY;
403
- let tmpLenSq = tmpDX * tmpDX + tmpDY * tmpDY;
404
-
405
- if (tmpLenSq < 0.001)
406
- {
407
- // Degenerate segment
408
- let tmpDPX = pPX - pAX;
409
- let tmpDPY = pPY - pAY;
410
- return Math.sqrt(tmpDPX * tmpDPX + tmpDPY * tmpDPY);
411
- }
412
-
413
- // Project point onto segment, clamped to [0, 1]
414
- let tmpT = ((pPX - pAX) * tmpDX + (pPY - pAY) * tmpDY) / tmpLenSq;
415
- if (tmpT < 0) tmpT = 0;
416
- if (tmpT > 1) tmpT = 1;
417
-
418
- let tmpClosestX = pAX + tmpT * tmpDX;
419
- let tmpClosestY = pAY + tmpT * tmpDY;
420
- let tmpDistX = pPX - tmpClosestX;
421
- let tmpDistY = pPY - tmpClosestY;
422
- return Math.sqrt(tmpDistX * tmpDistX + tmpDistY * tmpDistY);
80
+ return this._Core.distanceToSegment(pPX, pPY, pAX, pAY, pBX, pBY);
423
81
  }
424
82
 
425
- // ---- Auto Midpoint Calculation ----
426
-
427
- /**
428
- * Get the auto-calculated midpoint of the default bezier curve between
429
- * two port anchors, using the full directional geometry (facing detection,
430
- * adaptive curve offsets). Evaluates the cubic bezier at t=0.5.
431
- *
432
- * Used by ConnectionRenderer for connection midpoints.
433
- *
434
- * @param {{x: number, y: number, side: string}} pStart
435
- * @param {{x: number, y: number, side: string}} pEnd
436
- * @returns {{x: number, y: number}}
437
- */
438
83
  getAutoMidpoint(pStart, pEnd)
439
84
  {
440
- let tmpGeo = this.computeDirectionalGeometry(pStart, pEnd);
441
-
442
- return this.evaluateCubicBezier(
443
- { x: tmpGeo.departX, y: tmpGeo.departY },
444
- { x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
445
- { x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
446
- { x: tmpGeo.approachX, y: tmpGeo.approachY },
447
- 0.5
448
- );
85
+ return this._Core.getAutoMidpoint(pStart, pEnd);
449
86
  }
450
87
 
451
- /**
452
- * Get the auto-calculated midpoint using simple span-based control points.
453
- * Uses computeDepartApproach for basic geometry, then span * 0.4 for
454
- * control point distance. Evaluates the cubic bezier at t=0.5.
455
- *
456
- * Used by TetherService for tether midpoints.
457
- *
458
- * @param {{x: number, y: number, side: string}} pFrom
459
- * @param {{x: number, y: number, side: string}} pTo
460
- * @param {number} pDepartDist - Departure/approach distance
461
- * @returns {{x: number, y: number}}
462
- */
463
88
  getAutoMidpointSimple(pFrom, pTo, pDepartDist)
464
89
  {
465
- let tmpDA = this.computeDepartApproach(pFrom, pTo, pDepartDist);
466
-
467
- let tmpSpanX = Math.abs(tmpDA.approachX - tmpDA.departX);
468
- let tmpSpanY = Math.abs(tmpDA.approachY - tmpDA.departY);
469
- let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
470
- let tmpCPDist = tmpSpan * 0.4;
471
-
472
- let tmpP0 = { x: tmpDA.departX, y: tmpDA.departY };
473
- let tmpP1 = { x: tmpDA.departX + tmpDA.fromDir.dx * tmpCPDist, y: tmpDA.departY + tmpDA.fromDir.dy * tmpCPDist };
474
- let tmpP2 = { x: tmpDA.approachX + tmpDA.toDir.dx * tmpCPDist, y: tmpDA.approachY + tmpDA.toDir.dy * tmpCPDist };
475
- let tmpP3 = { x: tmpDA.approachX, y: tmpDA.approachY };
476
-
477
- return this.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
90
+ return this._Core.getAutoMidpointSimple(pFrom, pTo, pDepartDist);
478
91
  }
479
92
  }
480
93
 
@@ -96,7 +96,15 @@ class PictServiceFlowRenderManager extends libFableServiceProviderBase
96
96
  }
97
97
  }
98
98
 
99
- this._FlowView._NodeView.renderNode(tmpNode, this._FlowView._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
99
+ // Dispatch through the renderable-renderer registry. The default
100
+ // 'card' renderer is the node view, so existing diagrams (and
101
+ // ultravisor) render identically; a node type can name a different
102
+ // renderer via its RenderableType field. Fall back to the node view
103
+ // directly when no registry is present (e.g. a minimal test harness).
104
+ let tmpRenderer = (typeof this._FlowView.resolveRenderableRenderer === 'function')
105
+ ? this._FlowView.resolveRenderableRenderer(tmpNode, tmpNodeTypeConfig)
106
+ : this._FlowView._NodeView;
107
+ tmpRenderer.renderNode(tmpNode, this._FlowView._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
100
108
  }
101
109
 
102
110
  // Render properties panels and tethers
@@ -32,6 +32,20 @@ class PictServiceFlowViewportManager extends libFableServiceProviderBase
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * Pan the viewport by a delta in screen pixels (added to the current pan).
37
+ * Used by wheel-pan and any consumer that wants to nudge the canvas.
38
+ * @param {number} pDX
39
+ * @param {number} pDY
40
+ */
41
+ panBy(pDX, pDY)
42
+ {
43
+ let tmpVS = this._FlowView._FlowData.ViewState;
44
+ tmpVS.PanX += pDX;
45
+ tmpVS.PanY += pDY;
46
+ this.updateViewportTransform();
47
+ }
48
+
35
49
  /**
36
50
  * Set zoom level
37
51
  * @param {number} pZoom - The zoom level
@@ -95,6 +109,94 @@ class PictServiceFlowViewportManager extends libFableServiceProviderBase
95
109
  this.updateViewportTransform();
96
110
  }
97
111
 
112
+ /**
113
+ * Fit the viewport to a content frame box (origin + size in content space),
114
+ * centering it with a little padding. Mirrors zoomToFit but for a fixed box
115
+ * rather than the node bounds.
116
+ * @param {Object} pFrame - { X, Y, Width, Height }
117
+ * @returns {boolean}
118
+ */
119
+ fitToFrame(pFrame)
120
+ {
121
+ if (!pFrame || !pFrame.Width || !pFrame.Height) return false;
122
+ if (!this._FlowView._SVGElement) return false;
123
+
124
+ let tmpPadding = 20;
125
+ let tmpFrameWidth = pFrame.Width + tmpPadding * 2;
126
+ let tmpFrameHeight = pFrame.Height + tmpPadding * 2;
127
+
128
+ let tmpSVGRect = this._FlowView._SVGElement.getBoundingClientRect();
129
+ let tmpScaleX = tmpSVGRect.width / tmpFrameWidth;
130
+ let tmpScaleY = tmpSVGRect.height / tmpFrameHeight;
131
+ let tmpZoom = Math.min(tmpScaleX, tmpScaleY);
132
+ tmpZoom = Math.max(this._FlowView.options.MinZoom, Math.min(this._FlowView.options.MaxZoom, tmpZoom));
133
+
134
+ let tmpCenterX = (pFrame.X || 0) + pFrame.Width / 2;
135
+ let tmpCenterY = (pFrame.Y || 0) + pFrame.Height / 2;
136
+
137
+ this._FlowView._FlowData.ViewState.Zoom = tmpZoom;
138
+ this._FlowView._FlowData.ViewState.PanX = (tmpSVGRect.width / 2) - (tmpCenterX * tmpZoom);
139
+ this._FlowView._FlowData.ViewState.PanY = (tmpSVGRect.height / 2) - (tmpCenterY * tmpZoom);
140
+
141
+ this.updateViewportTransform();
142
+ return true;
143
+ }
144
+
145
+ /**
146
+ * Fit the frame's WIDTH to the container width (vs fitToFrame, which contains the whole
147
+ * frame and centers it), anchoring the frame's top-left at the container's top-left plus
148
+ * an optional top margin. Content outside the frame bleeds past the edges on purpose;
149
+ * vertical overflow is the host's call (a jumbotron clips to the frame height, a
150
+ * fullscreen / background view lets the user scroll down).
151
+ * @param {Object} pFrame - { X, Y, Width }
152
+ * @param {Object} [pOptions] - { TopMargin }
153
+ * @returns {boolean}
154
+ */
155
+ fitToFrameWidth(pFrame, pOptions)
156
+ {
157
+ if (!pFrame || !pFrame.Width) return false;
158
+ if (!this._FlowView._SVGElement) return false;
159
+
160
+ let tmpRect = this._FlowView._SVGElement.getBoundingClientRect();
161
+ let tmpResult = PictServiceFlowViewportManager.computeFitToWidth(pFrame, tmpRect.width,
162
+ {
163
+ TopMargin: (pOptions && pOptions.TopMargin) || 0,
164
+ MinZoom: this._FlowView.options.MinZoom,
165
+ MaxZoom: this._FlowView.options.MaxZoom
166
+ });
167
+
168
+ this._FlowView._FlowData.ViewState.Zoom = tmpResult.Zoom;
169
+ this._FlowView._FlowData.ViewState.PanX = tmpResult.PanX;
170
+ this._FlowView._FlowData.ViewState.PanY = tmpResult.PanY;
171
+
172
+ this.updateViewportTransform();
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * Pure fit-to-width math: zoom so the frame's width equals the container width (clamped to
178
+ * the view's zoom bounds), and pan so the frame's top-left sits at (0, TopMargin). No DOM,
179
+ * so it is unit tested directly.
180
+ * @param {Object} pFrame - { X, Y, Width }
181
+ * @param {number} pContainerWidth
182
+ * @param {Object} [pOptions] - { TopMargin, MinZoom, MaxZoom }
183
+ * @returns {{Zoom:number, PanX:number, PanY:number}}
184
+ */
185
+ static computeFitToWidth(pFrame, pContainerWidth, pOptions)
186
+ {
187
+ let tmpOptions = pOptions || {};
188
+ let tmpZoom = (pFrame && pFrame.Width > 0 && pContainerWidth > 0) ? (pContainerWidth / pFrame.Width) : 1;
189
+ let tmpMin = (typeof tmpOptions.MinZoom === 'number') ? tmpOptions.MinZoom : 0.05;
190
+ let tmpMax = (typeof tmpOptions.MaxZoom === 'number') ? tmpOptions.MaxZoom : 8;
191
+ tmpZoom = Math.max(tmpMin, Math.min(tmpMax, tmpZoom));
192
+ let tmpTopMargin = tmpOptions.TopMargin || 0;
193
+ return {
194
+ Zoom: tmpZoom,
195
+ PanX: -(((pFrame && pFrame.X) || 0) * tmpZoom),
196
+ PanY: tmpTopMargin - (((pFrame && pFrame.Y) || 0) * tmpZoom)
197
+ };
198
+ }
199
+
98
200
  /**
99
201
  * Convert screen coordinates to SVG viewport coordinates
100
202
  * @param {number} pScreenX