pict-section-flow 1.4.0 → 2.0.1

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 (164) 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 +73 -7
  5. package/source/providers/PictProvider-Flow-Geometry.js +11 -421
  6. package/source/providers/PictProvider-Flow-Icons.js +12 -0
  7. package/source/providers/PictProvider-Flow-Layouts.js +107 -0
  8. package/source/services/PictService-Flow-ConnectionRenderer.js +1 -1
  9. package/source/services/PictService-Flow-CursorManager.js +113 -0
  10. package/source/services/PictService-Flow-InteractionManager.js +439 -59
  11. package/source/services/PictService-Flow-Layout.js +21 -16
  12. package/source/services/PictService-Flow-PathGenerator.js +30 -417
  13. package/source/services/PictService-Flow-RenderManager.js +23 -3
  14. package/source/services/PictService-Flow-ViewportManager.js +102 -0
  15. package/source/views/PictView-Flow-FloatingToolbar.js +5 -1
  16. package/source/views/PictView-Flow-Node.js +29 -0
  17. package/source/views/PictView-Flow-Toolbar.js +50 -3
  18. package/source/views/PictView-Flow.js +591 -2
  19. package/.claude/launch.json +0 -11
  20. package/docs/.nojekyll +0 -0
  21. package/docs/Architecture.md +0 -163
  22. package/docs/Custom-Styling.md +0 -275
  23. package/docs/Data_Model.md +0 -149
  24. package/docs/Event_System.md +0 -156
  25. package/docs/Getting_Started.md +0 -237
  26. package/docs/Implementation_Reference.md +0 -528
  27. package/docs/Layout_Persistence.md +0 -117
  28. package/docs/README.md +0 -103
  29. package/docs/Theme_Integration.md +0 -150
  30. package/docs/_brand.json +0 -18
  31. package/docs/_cover.md +0 -17
  32. package/docs/_playground.json +0 -24
  33. package/docs/_sidebar.md +0 -57
  34. package/docs/_topbar.md +0 -8
  35. package/docs/_version.json +0 -7
  36. package/docs/api/PictFlowCard.md +0 -216
  37. package/docs/api/PictFlowCardPropertiesPanel.md +0 -235
  38. package/docs/api/addConnection.md +0 -101
  39. package/docs/api/addNode.md +0 -137
  40. package/docs/api/autoLayout.md +0 -77
  41. package/docs/api/getFlowData.md +0 -112
  42. package/docs/api/marshalToView.md +0 -95
  43. package/docs/api/openPanel.md +0 -128
  44. package/docs/api/registerHandler.md +0 -174
  45. package/docs/api/registerNodeType.md +0 -142
  46. package/docs/api/removeConnection.md +0 -57
  47. package/docs/api/removeNode.md +0 -80
  48. package/docs/api/saveLayout.md +0 -152
  49. package/docs/api/screenToSVGCoords.md +0 -68
  50. package/docs/api/selectNode.md +0 -116
  51. package/docs/api/setTheme.md +0 -168
  52. package/docs/api/setZoom.md +0 -97
  53. package/docs/api/toggleFullscreen.md +0 -68
  54. package/docs/card-help/EACH.md +0 -19
  55. package/docs/card-help/FREAD.md +0 -24
  56. package/docs/card-help/FWRITE.md +0 -24
  57. package/docs/card-help/GET.md +0 -22
  58. package/docs/card-help/ITE.md +0 -23
  59. package/docs/card-help/LOG.md +0 -23
  60. package/docs/card-help/NOTE.md +0 -17
  61. package/docs/card-help/PREV.md +0 -18
  62. package/docs/card-help/SET.md +0 -27
  63. package/docs/card-help/SPKL.md +0 -22
  64. package/docs/card-help/STAT.md +0 -23
  65. package/docs/card-help/SW.md +0 -25
  66. package/docs/diagrams/architecture-at-a-glance.excalidraw +0 -4270
  67. package/docs/diagrams/architecture-at-a-glance.mmd +0 -30
  68. package/docs/diagrams/architecture-at-a-glance.svg +0 -2
  69. package/docs/diagrams/data-flow.excalidraw +0 -1451
  70. package/docs/diagrams/data-flow.mmd +0 -17
  71. package/docs/diagrams/data-flow.svg +0 -2
  72. package/docs/diagrams/high-level-design.excalidraw +0 -5767
  73. package/docs/diagrams/high-level-design.mmd +0 -86
  74. package/docs/diagrams/high-level-design.svg +0 -2
  75. package/docs/diagrams/relationships.excalidraw +0 -3852
  76. package/docs/diagrams/relationships.mmd +0 -9
  77. package/docs/diagrams/relationships.svg +0 -2
  78. package/docs/diagrams/service-initialization-sequence.excalidraw +0 -1466
  79. package/docs/diagrams/service-initialization-sequence.mmd +0 -19
  80. package/docs/diagrams/service-initialization-sequence.svg +0 -2
  81. package/docs/diagrams/svg-layer-structure.excalidraw +0 -1060
  82. package/docs/diagrams/svg-layer-structure.mmd +0 -18
  83. package/docs/diagrams/svg-layer-structure.svg +0 -2
  84. package/docs/examples/README.md +0 -9
  85. package/docs/examples/simple_cards/README.md +0 -677
  86. package/docs/examples/simple_cards/css/flowexample.css +0 -65
  87. package/docs/examples/simple_cards/index.html +0 -32
  88. package/docs/examples/simple_cards/js/pict.min.js +0 -12
  89. package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +0 -1
  90. package/docs/index.html +0 -38
  91. package/docs/playground/app.json +0 -6
  92. package/docs/playground/appdata.json +0 -85
  93. package/docs/playground/application.js +0 -23
  94. package/docs/playground/pict.json +0 -17
  95. package/docs/playground/runtime/pict-application.min.js +0 -2
  96. package/docs/playground/runtime/pict-section-flow.min.js +0 -2
  97. package/docs/playground/runtime/pict-section-modal.min.js +0 -2
  98. package/docs/playground/runtime/pict.min.js +0 -12
  99. package/docs/retold-catalog.json +0 -244
  100. package/docs/retold-keyword-index.json +0 -26028
  101. package/example_applications/simple_cards/css/flowexample.css +0 -65
  102. package/example_applications/simple_cards/html/index.html +0 -32
  103. package/example_applications/simple_cards/package.json +0 -52
  104. package/example_applications/simple_cards/source/Pict-Application-FlowExample-Configuration.json +0 -15
  105. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +0 -539
  106. package/example_applications/simple_cards/source/card-help-content.js +0 -16
  107. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +0 -38
  108. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +0 -44
  109. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +0 -38
  110. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +0 -56
  111. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +0 -50
  112. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +0 -37
  113. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +0 -49
  114. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +0 -55
  115. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +0 -97
  116. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +0 -100
  117. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +0 -46
  118. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +0 -39
  119. package/example_applications/simple_cards/source/providers/PictRouter-FlowExample-Configuration.json +0 -22
  120. package/example_applications/simple_cards/source/sample-flows.js +0 -410
  121. package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +0 -184
  122. package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +0 -77
  123. package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +0 -325
  124. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +0 -59
  125. package/example_applications/simple_cards/source/views/PictView-FlowExample-Layout.js +0 -90
  126. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +0 -453
  127. package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +0 -95
  128. package/scripts/generate-card-help.js +0 -214
  129. package/source/providers/edges/Edge-Bezier.js +0 -41
  130. package/source/providers/edges/Edge-Orthogonal.js +0 -37
  131. package/source/providers/edges/Edge-OrthogonalSnap.js +0 -72
  132. package/source/providers/edges/Edge-Perimeter-Linear.js +0 -31
  133. package/source/providers/edges/Edge-Perimeter-Orthogonal.js +0 -39
  134. package/source/providers/edges/Edge-Perimeter.js +0 -48
  135. package/source/providers/edges/Edge-PerimeterMath.js +0 -92
  136. package/source/providers/edges/Edge-Straight.js +0 -24
  137. package/source/providers/layouts/Layout-Circular.js +0 -203
  138. package/source/providers/layouts/Layout-Coerce.js +0 -40
  139. package/source/providers/layouts/Layout-Columnar.js +0 -134
  140. package/source/providers/layouts/Layout-Custom.js +0 -27
  141. package/source/providers/layouts/Layout-ForcedFromCenter.js +0 -256
  142. package/source/providers/layouts/Layout-Grid.js +0 -134
  143. package/source/providers/layouts/Layout-Layered.js +0 -155
  144. package/source/providers/layouts/Layout-Rank.js +0 -141
  145. package/source/providers/layouts/Layout-Staggered.js +0 -131
  146. package/source/providers/layouts/Layout-Tabular.js +0 -94
  147. package/test/CardPalette_tests.js +0 -43
  148. package/test/ConnectionHandleManager_tests.js +0 -717
  149. package/test/ConnectionRenderer_tests.js +0 -591
  150. package/test/ConnectionStyle_tests.js +0 -90
  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
  163. package/test/ToolbarExtraButtons_tests.js +0 -138
  164. package/test/UndirectedConnections_tests.js +0 -70
@@ -1,23 +1,20 @@
1
- const libFableServiceProviderBase = require('fable-serviceproviderbase');
1
+ const libPictProviderGraphGeometry = require('pict-provider-graphgeometry');
2
2
 
3
3
  /**
4
4
  * PictProvider-Flow-Geometry
5
5
  *
6
- * Shared geometry utilities for the flow diagram.
7
- * Provides direction vectors and edge center calculations used by
8
- * connections, tethers, and other flow components.
6
+ * Backwards-compatible shim. As of the flow 2.0 plan (Phase 1a) the geometry
7
+ * math lives in the standalone, dependency-free pict-provider-graphgeometry
8
+ * module so it can be reused and unit tested on its own. This subclass keeps the
9
+ * historical `PictProviderFlowGeometry` export and serviceType so the flow
10
+ * service registry, every caller (PortRenderer, Node view, ConnectionRenderer,
11
+ * Tether), and the existing Geometry_tests.js are unaffected. All methods are
12
+ * inherited and delegate to the GraphGeometry core.
9
13
  *
10
- * Port Side values (12 positions):
11
- *
12
- * Top edge: 'top-left' 'top' 'top-right'
13
- * Left edge: 'left-top' 'left' 'left-bottom'
14
- * Right edge: 'right-top' 'right' 'right-bottom'
15
- * Bottom edge: 'bottom-left' 'bottom' 'bottom-right'
16
- *
17
- * The old 4-value sides ('left', 'right', 'top', 'bottom') map to
18
- * the middle position on each edge for backward compatibility.
14
+ * Port Side values (12 positions) and the full method set are documented in
15
+ * pict-provider-graphgeometry/source/GraphGeometry-Core.js.
19
16
  */
20
- class PictProviderFlowGeometry extends libFableServiceProviderBase
17
+ class PictProviderFlowGeometry extends libPictProviderGraphGeometry
21
18
  {
22
19
  constructor(pFable, pOptions, pServiceHash)
23
20
  {
@@ -25,413 +22,6 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
25
22
 
26
23
  this.serviceType = 'PictProviderFlowGeometry';
27
24
  }
28
-
29
- /**
30
- * Extract the edge name from a Side value.
31
- *
32
- * Maps all 12 positions (and the 4 legacy values) back to
33
- * the edge they sit on: 'left', 'right', 'top', or 'bottom'.
34
- *
35
- * @param {string} pSide - Any valid Side value
36
- * @returns {string} The edge: 'left', 'right', 'top', or 'bottom'
37
- */
38
- getEdgeFromSide(pSide)
39
- {
40
- switch (pSide)
41
- {
42
- case 'left-top':
43
- case 'left':
44
- case 'left-bottom':
45
- return 'left';
46
-
47
- case 'right-top':
48
- case 'right':
49
- case 'right-bottom':
50
- return 'right';
51
-
52
- case 'top-left':
53
- case 'top':
54
- case 'top-right':
55
- return 'top';
56
-
57
- case 'bottom-left':
58
- case 'bottom':
59
- case 'bottom-right':
60
- return 'bottom';
61
-
62
- default:
63
- return 'right';
64
- }
65
- }
66
-
67
- /**
68
- * Get the outward unit direction vector for a given side.
69
- *
70
- * All positions on the same edge share the same direction vector.
71
- *
72
- * @param {string} pSide - Any valid Side value (12 positions or 4 legacy)
73
- * @returns {{dx: number, dy: number}}
74
- */
75
- sideDirection(pSide)
76
- {
77
- switch (this.getEdgeFromSide(pSide))
78
- {
79
- case 'left': return { dx: -1, dy: 0 };
80
- case 'right': return { dx: 1, dy: 0 };
81
- case 'top': return { dx: 0, dy: -1 };
82
- case 'bottom': return { dx: 0, dy: 1 };
83
- default: return { dx: 1, dy: 0 };
84
- }
85
- }
86
-
87
- /**
88
- * Get the center point of a rectangle's edge.
89
- * Works for any object with X, Y, Width, Height properties
90
- * (nodes, panels, or any rectangular element).
91
- *
92
- * @param {Object} pRectData - Object with X, Y, Width, Height
93
- * @param {string} pSide - 'left', 'right', 'top', 'bottom'
94
- * @returns {{x: number, y: number}}
95
- */
96
- getEdgeCenter(pRectData, pSide)
97
- {
98
- switch (pSide)
99
- {
100
- case 'left':
101
- return { x: pRectData.X, y: pRectData.Y + pRectData.Height / 2 };
102
- case 'right':
103
- return { x: pRectData.X + pRectData.Width, y: pRectData.Y + pRectData.Height / 2 };
104
- case 'top':
105
- return { x: pRectData.X + pRectData.Width / 2, y: pRectData.Y };
106
- case 'bottom':
107
- return { x: pRectData.X + pRectData.Width / 2, y: pRectData.Y + pRectData.Height };
108
- default:
109
- return { x: pRectData.X + pRectData.Width, y: pRectData.Y + pRectData.Height / 2 };
110
- }
111
- }
112
-
113
- /**
114
- * Calculate a port's local position relative to node origin.
115
- *
116
- * Supports 12 positions (3 zones per edge). For left/right edges,
117
- * the body area below the title bar is divided into vertical zones.
118
- * For top/bottom edges, the full width is divided into horizontal zones.
119
- *
120
- * When pPortCountsBySide is provided, zone fractions are computed
121
- * proportionally based on actual port counts (adaptive zones).
122
- * Otherwise, fixed 1/3 zones are used for backward compatibility.
123
- *
124
- * Multiple ports sharing the same Side value distribute evenly within
125
- * their zone.
126
- *
127
- * @param {string} pSide - Side value (any of 12 positions or 4 legacy)
128
- * @param {number} pIndex - Index of this port within its Side group
129
- * @param {number} pTotal - Total ports with this Side value
130
- * @param {number} pWidth - Node width
131
- * @param {number} pHeight - Node height
132
- * @param {number} pTitleBarHeight - Height of the node title bar
133
- * @param {Object} [pPortCountsBySide] - Optional map of Side → port count
134
- * for all ports on the node. Enables adaptive zone sizing.
135
- * @returns {{x: number, y: number}}
136
- */
137
- getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight, pPortCountsBySide)
138
- {
139
- let tmpEdge = this.getEdgeFromSide(pSide);
140
- let tmpZone = pPortCountsBySide
141
- ? this._computeAdaptiveZone(pSide, pPortCountsBySide)
142
- : this._getZoneFromSide(pSide);
143
-
144
- // Use the fixed zone to decide alignment intent (start/center/end)
145
- // because adaptive zones shift boundaries when neighbouring zones
146
- // are empty, which would break alignment decisions.
147
- let tmpFixedZone = this._getZoneFromSide(pSide);
148
-
149
- // Minimum spacing between port centers (px)
150
- let tmpMinSpacing = 16;
151
-
152
- // Reserve space at the bottom of the body so that port badges
153
- // never overlap the panel-indicator icon (10×10 rect at bottom-right)
154
- // and always leave a visible gap above the node bottom edge.
155
- let tmpBottomPad = 16;
156
-
157
- // Determine alignment from the fixed zone position:
158
- // start zone (0.000 – 0.333) → start-align (offset 0)
159
- // middle zone (0.333 – 0.667) → center
160
- // end zone (0.667 – 1.000) → end-align
161
- let tmpAlignment = 'start';
162
- if (tmpFixedZone.start >= 0.5)
163
- {
164
- tmpAlignment = 'end';
165
- }
166
- else if (tmpFixedZone.start >= 0.17)
167
- {
168
- tmpAlignment = 'center';
169
- }
170
-
171
- if (tmpEdge === 'left' || tmpEdge === 'right')
172
- {
173
- let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
174
- let tmpBodyHeight = pHeight - pTitleBarHeight - tmpBottomPad;
175
- let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
176
- let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
177
-
178
- // Use fixed spacing so port gaps stay consistent across cards
179
- // even when one edge drives the card height beyond what the
180
- // other needs.
181
- let tmpSpacing = tmpMinSpacing;
182
- let tmpGroupHeight = tmpSpacing * (pTotal + 1);
183
- let tmpSlack = tmpZoneHeight - tmpGroupHeight;
184
- if (tmpSlack < 0)
185
- {
186
- tmpSlack = 0;
187
- }
188
-
189
- let tmpAlignOffset = 0;
190
- if (tmpAlignment === 'end')
191
- {
192
- tmpAlignOffset = tmpSlack;
193
- }
194
- else if (tmpAlignment === 'center')
195
- {
196
- tmpAlignOffset = tmpSlack / 2;
197
- }
198
-
199
- let tmpY = tmpZoneStart + tmpAlignOffset + tmpSpacing * (pIndex + 1);
200
- return { x: tmpX, y: tmpY };
201
- }
202
-
203
- // top or bottom
204
- let tmpY = (tmpEdge === 'top') ? 0 : pHeight;
205
- let tmpZoneStart = pWidth * tmpZone.start;
206
- let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
207
-
208
- let tmpSpacing = tmpMinSpacing;
209
- let tmpGroupWidth = tmpSpacing * (pTotal + 1);
210
- let tmpSlack = tmpZoneWidth - tmpGroupWidth;
211
- if (tmpSlack < 0)
212
- {
213
- tmpSlack = 0;
214
- }
215
-
216
- let tmpAlignOffset = 0;
217
- if (tmpAlignment === 'end')
218
- {
219
- tmpAlignOffset = tmpSlack;
220
- }
221
- else if (tmpAlignment === 'center')
222
- {
223
- tmpAlignOffset = tmpSlack / 2;
224
- }
225
-
226
- let tmpX = tmpZoneStart + tmpAlignOffset + tmpSpacing * (pIndex + 1);
227
- return { x: tmpX, y: tmpY };
228
- }
229
-
230
- /**
231
- * Get the zone fraction (start, end) for a Side value.
232
- *
233
- * Each edge is divided into three zones of equal size:
234
- * start: 0.0 — 0.333
235
- * middle: 0.333 — 0.667
236
- * end: 0.667 — 1.0
237
- *
238
- * Used as fallback when adaptive zones are not available.
239
- *
240
- * @param {string} pSide
241
- * @returns {{start: number, end: number}}
242
- */
243
- _getZoneFromSide(pSide)
244
- {
245
- switch (pSide)
246
- {
247
- // Left edge: top, middle, bottom
248
- case 'left-top': return { start: 0.0, end: 0.333 };
249
- case 'left': return { start: 0.333, end: 0.667 };
250
- case 'left-bottom': return { start: 0.667, end: 1.0 };
251
-
252
- // Right edge: top, middle, bottom
253
- case 'right-top': return { start: 0.0, end: 0.333 };
254
- case 'right': return { start: 0.333, end: 0.667 };
255
- case 'right-bottom': return { start: 0.667, end: 1.0 };
256
-
257
- // Top edge: left, middle, right
258
- case 'top-left': return { start: 0.0, end: 0.333 };
259
- case 'top': return { start: 0.333, end: 0.667 };
260
- case 'top-right': return { start: 0.667, end: 1.0 };
261
-
262
- // Bottom edge: left, middle, right
263
- case 'bottom-left': return { start: 0.0, end: 0.333 };
264
- case 'bottom': return { start: 0.333, end: 0.667 };
265
- case 'bottom-right': return { start: 0.667, end: 1.0 };
266
-
267
- // Fallback: full range (legacy behavior)
268
- default: return { start: 0.0, end: 1.0 };
269
- }
270
- }
271
-
272
- /**
273
- * Get the three zone Side keys for a given edge, in order.
274
- *
275
- * @param {string} pEdge - 'left', 'right', 'top', or 'bottom'
276
- * @returns {Array<string>} Three Side keys in start-to-end order
277
- */
278
- _getZoneKeysForEdge(pEdge)
279
- {
280
- switch (pEdge)
281
- {
282
- case 'left': return ['left-top', 'left', 'left-bottom'];
283
- case 'right': return ['right-top', 'right', 'right-bottom'];
284
- case 'top': return ['top-left', 'top', 'top-right'];
285
- case 'bottom': return ['bottom-left', 'bottom', 'bottom-right'];
286
- default: return ['right-top', 'right', 'right-bottom'];
287
- }
288
- }
289
-
290
- /**
291
- * Compute an adaptive zone fraction for a Side value based on the
292
- * actual port distribution across all zones on the same edge.
293
- *
294
- * Instead of fixed 1/3 splits, zones are sized proportionally to the
295
- * space each zone needs (minSpacing * (portCount + 1)). Zones with
296
- * zero ports collapse to zero, giving occupied zones more room.
297
- *
298
- * @param {string} pSide - The Side value to compute a zone for
299
- * @param {Object} pPortCountsBySide - Map of Side → number of ports
300
- * @returns {{start: number, end: number}}
301
- */
302
- _computeAdaptiveZone(pSide, pPortCountsBySide)
303
- {
304
- let tmpEdge = this.getEdgeFromSide(pSide);
305
- let tmpZoneKeys = this._getZoneKeysForEdge(tmpEdge);
306
-
307
- let tmpMinSpacing = 16;
308
-
309
- // Compute the space each zone needs: minSpacing * (count + 1)
310
- // The +1 provides padding at both ends of the zone.
311
- let tmpTotalSpace = 0;
312
- let tmpSpaceByZone = {};
313
- for (let i = 0; i < tmpZoneKeys.length; i++)
314
- {
315
- let tmpKey = tmpZoneKeys[i];
316
- let tmpCount = pPortCountsBySide[tmpKey] || 0;
317
- let tmpSpace = (tmpCount > 0) ? (tmpMinSpacing * (tmpCount + 1)) : 0;
318
- tmpSpaceByZone[tmpKey] = tmpSpace;
319
- tmpTotalSpace += tmpSpace;
320
- }
321
-
322
- // If no ports on this edge at all, fall back to fixed zones
323
- if (tmpTotalSpace === 0)
324
- {
325
- return this._getZoneFromSide(pSide);
326
- }
327
-
328
- // Compute proportional start/end for the requested zone
329
- let tmpCumulativeStart = 0;
330
- for (let i = 0; i < tmpZoneKeys.length; i++)
331
- {
332
- let tmpKey = tmpZoneKeys[i];
333
- let tmpFraction = tmpSpaceByZone[tmpKey] / tmpTotalSpace;
334
- if (tmpKey === pSide)
335
- {
336
- return { start: tmpCumulativeStart, end: tmpCumulativeStart + tmpFraction };
337
- }
338
- tmpCumulativeStart += tmpFraction;
339
- }
340
-
341
- // Should not reach here; fall back to fixed zones
342
- return this._getZoneFromSide(pSide);
343
- }
344
-
345
- /**
346
- * Build a map of Side → port count from an array of port objects.
347
- *
348
- * Convenience method for callers that need to pass port counts
349
- * to getPortLocalPosition or computeMinimumNodeHeight.
350
- *
351
- * @param {Array} pPorts - Array of port objects with Side, Direction
352
- * @returns {Object} Map of Side value → count
353
- */
354
- buildPortCountsBySide(pPorts)
355
- {
356
- let tmpCounts = {};
357
- if (!pPorts || !Array.isArray(pPorts))
358
- {
359
- return tmpCounts;
360
- }
361
- for (let i = 0; i < pPorts.length; i++)
362
- {
363
- let tmpSide = pPorts[i].Side || (pPorts[i].Direction === 'input' ? 'left' : 'right');
364
- if (!tmpCounts[tmpSide])
365
- {
366
- tmpCounts[tmpSide] = 0;
367
- }
368
- tmpCounts[tmpSide]++;
369
- }
370
- return tmpCounts;
371
- }
372
-
373
- /**
374
- * Compute the minimum node height required so that all ports
375
- * (with their badges) fit within the node boundary.
376
- *
377
- * Uses adaptive zone sizing: instead of assuming each zone gets
378
- * a fixed 1/3 of the body, sums the space needed by all occupied
379
- * zones on each left/right edge. This produces compact cards
380
- * whose height scales linearly with total port count.
381
- *
382
- * @param {Array} pPorts - Array of port objects with Side, Direction
383
- * @param {number} pTitleBarHeight - Height of the title bar
384
- * @returns {number} Minimum node height in pixels (0 if no ports)
385
- */
386
- computeMinimumNodeHeight(pPorts, pTitleBarHeight)
387
- {
388
- if (!pPorts || !Array.isArray(pPorts) || pPorts.length === 0)
389
- {
390
- return 0;
391
- }
392
-
393
- let tmpMinSpacing = 16;
394
- let tmpBottomPad = 16;
395
-
396
- // Count ports per Side value
397
- let tmpCountBySide = this.buildPortCountsBySide(pPorts);
398
-
399
- // Sum the space needed per edge (left, right) across all zones.
400
- // Each zone needs minSpacing * (count + 1) pixels.
401
- let tmpSpacePerEdge = {};
402
- for (let tmpSide in tmpCountBySide)
403
- {
404
- let tmpEdge = this.getEdgeFromSide(tmpSide);
405
-
406
- // Only left/right edge zones affect required height
407
- if (tmpEdge !== 'left' && tmpEdge !== 'right')
408
- {
409
- continue;
410
- }
411
-
412
- let tmpCount = tmpCountBySide[tmpSide];
413
- let tmpZoneSpace = tmpMinSpacing * (tmpCount + 1);
414
-
415
- if (!tmpSpacePerEdge[tmpEdge])
416
- {
417
- tmpSpacePerEdge[tmpEdge] = 0;
418
- }
419
- tmpSpacePerEdge[tmpEdge] += tmpZoneSpace;
420
- }
421
-
422
- // The minimum height is titleBar + bottomPad + max edge space
423
- let tmpMinHeight = 0;
424
- for (let tmpEdge in tmpSpacePerEdge)
425
- {
426
- let tmpRequired = pTitleBarHeight + tmpBottomPad + tmpSpacePerEdge[tmpEdge];
427
- if (tmpRequired > tmpMinHeight)
428
- {
429
- tmpMinHeight = tmpRequired;
430
- }
431
- }
432
-
433
- return Math.ceil(tmpMinHeight);
434
- }
435
25
  }
436
26
 
437
27
  module.exports = PictProviderFlowGeometry;
@@ -53,6 +53,18 @@ const _DefaultIcons =
53
53
 
54
54
  'connect': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M8 8l8 8" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/><circle cx="6" cy="6" r="3" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/><circle cx="18" cy="18" r="3" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/></svg>',
55
55
 
56
+ // Pan / navigate "hand" tool — read-only surfaces use this to toggle pan + zoom on the canvas.
57
+ 'pan': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6.5a1.5 1.5 0 0 0-3 0M15 6.5V4.5a1.5 1.5 0 0 0-3 0V6M12 6V5a1.5 1.5 0 0 0-3 0v7M9 12V8.5a1.5 1.5 0 0 0-3 0V14a6 6 0 0 0 6 6h1a5 5 0 0 0 5-5v-4a1.5 1.5 0 0 0-3 0" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/></svg>',
58
+
59
+ // Crop / view-area frame — a host (e.g. a moodboard) uses this to toggle the content-frame drag handles.
60
+ 'frame': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2v16a1 1 0 0 0 1 1h16"/><path d="M2 6h16a1 1 0 0 1 1 1v16"/><rect x="9" y="9" width="6" height="5" rx="1" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="none"/></svg>',
61
+
62
+ // Display-style toggles for a presentation surface (moodboard): canvas (a free board of cards),
63
+ // jumbotron (a hero band across the top), background (a full-width backdrop behind content).
64
+ 'display-canvas': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><rect x="6" y="7" width="6" height="5" rx="1" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="none"/><rect x="14" y="11" width="5" height="6" rx="1" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="none"/></svg>',
65
+ 'display-jumbotron': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><rect x="3" y="4" width="18" height="7" rx="2" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="none"/><path d="M6 15h8M6 18h12"/></svg>',
66
+ 'display-background': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2" fill="var(--theme-color-background-secondary, #d5e8f7)"/><rect x="8" y="8" width="8" height="8" rx="1" fill="var(--theme-color-background-panel, #ffffff)"/></svg>',
67
+
56
68
  'fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',
57
69
 
58
70
  'exit-fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>',
@@ -67,6 +67,20 @@ class PictProviderFlowLayouts extends libPictProvider
67
67
  }
68
68
  }
69
69
 
70
+ /**
71
+ * Resolve a host-supplied storage backend, if one was passed as the flow's
72
+ * `LayoutStorage` option: `{ read(cb), write(layouts, cb), delete(cb) }` with
73
+ * Node-style callbacks. Lets a host wire a REST API / IndexedDB by config
74
+ * instead of overriding the three storage hooks. Returns null to fall back to
75
+ * localStorage.
76
+ * @returns {Object|null}
77
+ */
78
+ _resolveLayoutStorage()
79
+ {
80
+ let tmpStorage = (this._FlowView && this._FlowView.options) ? this._FlowView.options.LayoutStorage : null;
81
+ return (tmpStorage && typeof tmpStorage === 'object') ? tmpStorage : null;
82
+ }
83
+
70
84
  // ── Storage Hooks ─────────────────────────────────────────────────────
71
85
  // These three methods form the persistence contract. The default
72
86
  // implementation uses localStorage. Override them on the instance or
@@ -84,6 +98,11 @@ class PictProviderFlowLayouts extends libPictProvider
84
98
  */
85
99
  storageWrite(pLayouts, fCallback)
86
100
  {
101
+ let tmpStorage = this._resolveLayoutStorage();
102
+ if (tmpStorage && typeof tmpStorage.write === 'function')
103
+ {
104
+ return tmpStorage.write(pLayouts, fCallback);
105
+ }
87
106
  if (this._StorageKey === false)
88
107
  {
89
108
  return fCallback(null);
@@ -113,6 +132,11 @@ class PictProviderFlowLayouts extends libPictProvider
113
132
  */
114
133
  storageRead(fCallback)
115
134
  {
135
+ let tmpStorage = this._resolveLayoutStorage();
136
+ if (tmpStorage && typeof tmpStorage.read === 'function')
137
+ {
138
+ return tmpStorage.read(fCallback);
139
+ }
116
140
  if (this._StorageKey === false)
117
141
  {
118
142
  return fCallback(null, []);
@@ -149,6 +173,11 @@ class PictProviderFlowLayouts extends libPictProvider
149
173
  */
150
174
  storageDelete(fCallback)
151
175
  {
176
+ let tmpStorage = this._resolveLayoutStorage();
177
+ if (tmpStorage && typeof tmpStorage.delete === 'function')
178
+ {
179
+ return tmpStorage.delete(fCallback);
180
+ }
152
181
  if (this._StorageKey === false)
153
182
  {
154
183
  return fCallback(null);
@@ -489,6 +518,84 @@ class PictProviderFlowLayouts extends libPictProvider
489
518
  (pLayout) => pLayout.Hash === pLayoutHash
490
519
  ) || null;
491
520
  }
521
+
522
+ // ── Default layout ─────────────────────────────────────────────────────
523
+ // One saved layout can be marked the default arrangement for a graph. The
524
+ // mark is an IsDefault flag on the layout object, so it rides inside
525
+ // SavedLayouts and persists through both the storage hooks and
526
+ // getFlowData/setFlowData with no storage-shape change.
527
+
528
+ /**
529
+ * Mark a saved layout as the default (clearing any previous default). Pass a
530
+ * falsy hash to clear the default entirely. Persists the change.
531
+ * @param {string} pLayoutHash
532
+ * @returns {boolean} true on success; false if a given hash was not found
533
+ */
534
+ setDefaultLayout(pLayoutHash)
535
+ {
536
+ if (!this._FlowView)
537
+ {
538
+ this.log.warn('PictProviderFlowLayouts setDefaultLayout: no FlowView reference');
539
+ return false;
540
+ }
541
+
542
+ let tmpFlowData = this._FlowView._FlowData;
543
+ let tmpFound = false;
544
+ for (let i = 0; i < tmpFlowData.SavedLayouts.length; i++)
545
+ {
546
+ let tmpIsMatch = (!!pLayoutHash && tmpFlowData.SavedLayouts[i].Hash === pLayoutHash);
547
+ tmpFlowData.SavedLayouts[i].IsDefault = tmpIsMatch;
548
+ if (tmpIsMatch) { tmpFound = true; }
549
+ }
550
+
551
+ // A falsy hash clears the default (every IsDefault now false); a non-matching hash is an error.
552
+ if (pLayoutHash && !tmpFound)
553
+ {
554
+ this.log.warn(`PictProviderFlowLayouts setDefaultLayout: layout '${pLayoutHash}' not found`);
555
+ return false;
556
+ }
557
+
558
+ this._FlowView.marshalFromView();
559
+ this.storageWrite(tmpFlowData.SavedLayouts, (pError) =>
560
+ {
561
+ if (pError)
562
+ {
563
+ this.log.warn(`PictProviderFlowLayouts: failed to persist default: ${pError.message}`);
564
+ }
565
+ });
566
+
567
+ if (this._FlowView._EventHandlerProvider)
568
+ {
569
+ this._FlowView._EventHandlerProvider.fireEvent('onLayoutDefaultChanged', this.getDefaultLayout());
570
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', tmpFlowData);
571
+ }
572
+
573
+ return true;
574
+ }
575
+
576
+ /**
577
+ * The layout currently marked default, or null if none.
578
+ * @returns {Object|null}
579
+ */
580
+ getDefaultLayout()
581
+ {
582
+ if (!this._FlowView) return null;
583
+ return this._FlowView._FlowData.SavedLayouts.find((pLayout) => pLayout.IsDefault === true) || null;
584
+ }
585
+
586
+ /**
587
+ * Restore the default layout if one is set.
588
+ * @returns {boolean} true if a default existed and was restored
589
+ */
590
+ applyDefaultLayout()
591
+ {
592
+ let tmpDefault = this.getDefaultLayout();
593
+ if (!tmpDefault)
594
+ {
595
+ return false;
596
+ }
597
+ return this.restoreLayout(tmpDefault.Hash);
598
+ }
492
599
  }
493
600
 
494
601
  module.exports = PictProviderFlowLayouts;
@@ -1,5 +1,5 @@
1
1
  const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
- const libPerimeterMath = require('../providers/edges/Edge-PerimeterMath.js');
2
+ const libPerimeterMath = require('pict-provider-graphlayout').Edges.PerimeterMath;
3
3
 
4
4
  // Chip (port-label badge) geometry — must mirror PortRenderer's badge
5
5
  // dimensions exactly so hint paths land on the chip's actual outer