pict-section-flow 0.0.2 → 0.0.3

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 (38) hide show
  1. package/.claude/launch.json +11 -0
  2. package/docs/README.md +51 -0
  3. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
  4. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
  8. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
  9. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
  10. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
  11. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
  12. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
  13. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
  14. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
  15. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
  16. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
  17. package/package.json +2 -2
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +49 -1
  20. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  21. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  22. package/source/providers/PictProvider-Flow-Geometry.js +43 -0
  23. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  24. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  25. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  26. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  27. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  28. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  29. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  30. package/source/services/PictService-Flow-PanelManager.js +188 -0
  31. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  32. package/source/services/PictService-Flow-Tether.js +52 -25
  33. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  34. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  35. package/source/views/PictView-Flow-Node.js +654 -169
  36. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  37. package/source/views/PictView-Flow-Toolbar.js +846 -379
  38. package/source/views/PictView-Flow.js +279 -671
@@ -5,6 +5,9 @@ const libPictServiceFlowConnectionRenderer = require('../services/PictService-Fl
5
5
  const libPictServiceFlowTether = require('../services/PictService-Flow-Tether.js');
6
6
  const libPictServiceFlowLayout = require('../services/PictService-Flow-Layout.js');
7
7
  const libPictServiceFlowPathGenerator = require('../services/PictService-Flow-PathGenerator.js');
8
+ const libPictServiceFlowViewportManager = require('../services/PictService-Flow-ViewportManager.js');
9
+ const libPictServiceFlowSelectionManager = require('../services/PictService-Flow-SelectionManager.js');
10
+ const libPictServiceFlowPanelManager = require('../services/PictService-Flow-PanelManager.js');
8
11
 
9
12
  const libPictProviderFlowNodeTypes = require('../providers/PictProvider-Flow-NodeTypes.js');
10
13
  const libPictProviderFlowEventHandler = require('../providers/PictProvider-Flow-EventHandler.js');
@@ -12,9 +15,15 @@ const libPictProviderFlowLayouts = require('../providers/PictProvider-Flow-Layou
12
15
  const libPictProviderFlowSVGHelpers = require('../providers/PictProvider-Flow-SVGHelpers.js');
13
16
  const libPictProviderFlowGeometry = require('../providers/PictProvider-Flow-Geometry.js');
14
17
  const libPictProviderFlowPanelChrome = require('../providers/PictProvider-Flow-PanelChrome.js');
18
+ const libPictProviderFlowCSS = require('../providers/PictProvider-Flow-CSS.js');
19
+ const libPictProviderFlowIcons = require('../providers/PictProvider-Flow-Icons.js');
20
+ const libPictProviderFlowConnectorShapes = require('../providers/PictProvider-Flow-ConnectorShapes.js');
21
+ const libPictProviderFlowTheme = require('../providers/PictProvider-Flow-Theme.js');
22
+ const libPictProviderFlowNoise = require('../providers/PictProvider-Flow-Noise.js');
15
23
 
16
24
  const libPictViewFlowNode = require('./PictView-Flow-Node.js');
17
25
  const libPictViewFlowToolbar = require('./PictView-Flow-Toolbar.js');
26
+ const libPictViewFlowFloatingToolbar = require('./PictView-Flow-FloatingToolbar.js');
18
27
  const libPictViewFlowPropertiesPanel = require('./PictView-Flow-PropertiesPanel.js');
19
28
 
20
29
  const libPictFlowCardPropertiesPanel = require('../PictFlowCardPropertiesPanel.js');
@@ -52,398 +61,25 @@ const _DefaultConfiguration =
52
61
  DefaultNodeWidth: 180,
53
62
  DefaultNodeHeight: 80,
54
63
 
55
- CSS: /*css*/`
56
- .pict-flow-container {
57
- position: relative;
58
- width: 100%;
59
- height: 100%;
60
- min-height: 400px;
61
- overflow: hidden;
62
- background-color: #fafafa;
63
- border: 1px solid #e0e0e0;
64
- border-radius: 4px;
65
- display: flex;
66
- flex-direction: column;
67
- }
68
- .pict-flow-svg-container {
69
- flex: 1;
70
- min-height: 0;
71
- position: relative;
72
- }
73
- .pict-flow-svg {
74
- width: 100%;
75
- height: 100%;
76
- cursor: grab;
77
- user-select: none;
78
- -webkit-user-select: none;
79
- }
80
- .pict-flow-svg.panning {
81
- cursor: grabbing;
82
- }
83
- .pict-flow-svg.connecting {
84
- cursor: crosshair;
85
- }
86
- .pict-flow-grid-pattern line {
87
- stroke: #e8e8e8;
88
- stroke-width: 0.5;
89
- }
90
- .pict-flow-node {
91
- cursor: pointer;
92
- }
93
- .pict-flow-node:hover .pict-flow-node-body {
94
- filter: brightness(0.97);
95
- }
96
- .pict-flow-node.selected .pict-flow-node-body {
97
- stroke: #3498db;
98
- stroke-width: 2.5;
99
- }
100
- .pict-flow-node.dragging {
101
- opacity: 0.85;
102
- cursor: grabbing;
103
- }
104
- .pict-flow-node-body {
105
- fill: #ffffff;
106
- stroke: #bdc3c7;
107
- stroke-width: 1.5;
108
- rx: 6;
109
- ry: 6;
110
- transition: filter 0.15s;
111
- }
112
- .pict-flow-node-title-bar {
113
- fill: #2c3e50;
114
- rx: 6;
115
- ry: 6;
116
- }
117
- .pict-flow-node-title-bar-bottom {
118
- fill: #2c3e50;
119
- }
120
- .pict-flow-node-title {
121
- fill: #ffffff;
122
- font-size: 12px;
123
- font-weight: 700;
124
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
125
- pointer-events: none;
126
- }
127
- .pict-flow-node-type-label {
128
- fill: #95a5a6;
129
- font-size: 10px;
130
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
131
- pointer-events: none;
132
- }
133
- .pict-flow-port {
134
- cursor: crosshair;
135
- transition: r 0.15s;
136
- }
137
- .pict-flow-port.input {
138
- fill: #3498db;
139
- stroke: #2980b9;
140
- stroke-width: 1.5;
141
- }
142
- .pict-flow-port.output {
143
- fill: #2ecc71;
144
- stroke: #27ae60;
145
- stroke-width: 1.5;
146
- }
147
- .pict-flow-port:hover {
148
- r: 7;
149
- }
150
- .pict-flow-port-label {
151
- fill: #7f8c8d;
152
- font-size: 9px;
153
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
154
- pointer-events: none;
155
- }
156
- .pict-flow-connection {
157
- fill: none;
158
- stroke: #95a5a6;
159
- stroke-width: 2;
160
- cursor: pointer;
161
- transition: stroke 0.15s;
162
- }
163
- .pict-flow-connection:hover {
164
- stroke: #7f8c8d;
165
- stroke-width: 3;
166
- }
167
- .pict-flow-connection.selected {
168
- stroke: #3498db;
169
- stroke-width: 3;
170
- }
171
- .pict-flow-connection-hitarea {
172
- fill: none;
173
- stroke: transparent;
174
- stroke-width: 12;
175
- cursor: pointer;
176
- }
177
- .pict-flow-drag-connection {
178
- fill: none;
179
- stroke: #3498db;
180
- stroke-width: 2;
181
- stroke-dasharray: 6 3;
182
- pointer-events: none;
183
- }
184
- .pict-flow-node-decision .pict-flow-node-body {
185
- fill: #fff9e6;
186
- stroke: #f39c12;
187
- }
188
- .pict-flow-node-start .pict-flow-node-body {
189
- fill: #eafaf1;
190
- stroke: #27ae60;
191
- rx: 25;
192
- ry: 25;
193
- }
194
- .pict-flow-node-end .pict-flow-node-body {
195
- fill: #fdedec;
196
- stroke: #e74c3c;
197
- rx: 25;
198
- ry: 25;
199
- }
200
- .pict-flow-connection-handle {
201
- fill: #ffffff;
202
- stroke: #3498db;
203
- stroke-width: 2;
204
- cursor: grab;
205
- transition: r 0.15s;
206
- filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
207
- }
208
- .pict-flow-connection-handle:hover {
209
- r: 8;
210
- stroke-width: 2.5;
211
- }
212
- .pict-flow-connection-handle-midpoint {
213
- fill: #ffffff;
214
- stroke: #e67e22;
215
- stroke-width: 2;
216
- cursor: grab;
217
- transition: r 0.15s;
218
- filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
219
- }
220
- .pict-flow-connection-handle-midpoint:hover {
221
- r: 8;
222
- stroke-width: 2.5;
223
- }
224
- .pict-flow-tether-line {
225
- fill: none;
226
- stroke: #95a5a6;
227
- stroke-width: 1.5;
228
- stroke-dasharray: 6 4;
229
- pointer-events: visibleStroke;
230
- cursor: pointer;
231
- }
232
- .pict-flow-tether-line.selected {
233
- stroke: #3498db;
234
- stroke-width: 2;
235
- }
236
- .pict-flow-tether-hitarea {
237
- fill: none;
238
- stroke: transparent;
239
- stroke-width: 10;
240
- cursor: pointer;
241
- }
242
- .pict-flow-tether-handle {
243
- fill: #ffffff;
244
- stroke: #3498db;
245
- stroke-width: 2;
246
- cursor: grab;
247
- transition: r 0.15s;
248
- filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
249
- }
250
- .pict-flow-tether-handle:hover {
251
- r: 8;
252
- stroke-width: 2.5;
253
- }
254
- .pict-flow-tether-handle-midpoint {
255
- fill: #ffffff;
256
- stroke: #e67e22;
257
- stroke-width: 2;
258
- cursor: grab;
259
- transition: r 0.15s;
260
- filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
261
- }
262
- .pict-flow-tether-handle-midpoint:hover {
263
- r: 8;
264
- stroke-width: 2.5;
265
- }
266
- .pict-flow-node-panel-indicator {
267
- fill: #3498db;
268
- stroke: #2980b9;
269
- stroke-width: 1;
270
- cursor: pointer;
271
- }
272
- .pict-flow-node-panel-indicator:hover {
273
- fill: #2980b9;
274
- }
275
- .pict-flow-panel-foreign-object {
276
- overflow: visible;
277
- }
278
- .pict-flow-panel {
279
- background: #ffffff;
280
- border: 1px solid #bdc3c7;
281
- border-radius: 6px;
282
- box-shadow: 0 2px 8px rgba(0,0,0,0.12);
283
- display: flex;
284
- flex-direction: column;
285
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
286
- font-size: 13px;
287
- overflow: hidden;
288
- width: 100%;
289
- height: 100%;
290
- box-sizing: border-box;
291
- }
292
- .pict-flow-panel-titlebar {
293
- display: flex;
294
- align-items: center;
295
- justify-content: space-between;
296
- padding: 6px 10px;
297
- background: #ecf0f1;
298
- border-bottom: 1px solid #d5dbdb;
299
- cursor: grab;
300
- user-select: none;
301
- -webkit-user-select: none;
302
- flex-shrink: 0;
303
- }
304
- .pict-flow-panel-titlebar.dragging {
305
- cursor: grabbing;
306
- }
307
- .pict-flow-panel-title-text {
308
- font-weight: 600;
309
- font-size: 12px;
310
- color: #2c3e50;
311
- white-space: nowrap;
312
- overflow: hidden;
313
- text-overflow: ellipsis;
314
- }
315
- .pict-flow-panel-close-btn {
316
- cursor: pointer;
317
- color: #95a5a6;
318
- font-size: 14px;
319
- line-height: 1;
320
- padding: 2px 4px;
321
- border: none;
322
- background: none;
323
- }
324
- .pict-flow-panel-close-btn:hover {
325
- color: #e74c3c;
326
- }
327
- .pict-flow-panel-body {
328
- flex: 1;
329
- overflow: auto;
330
- padding: 8px;
331
- }
332
- .pict-flow-fullscreen {
333
- position: fixed;
334
- top: 0;
335
- left: 0;
336
- width: 100vw;
337
- height: 100vh;
338
- z-index: 9999;
339
- border-radius: 0;
340
- border: none;
341
- min-height: 100vh;
342
- }
343
- .pict-flow-fullscreen .pict-flow-svg {
344
- min-height: calc(100vh - 50px);
345
- }
346
- .pict-flow-info-panel {
347
- padding: 4px;
348
- font-size: 12px;
349
- line-height: 1.5;
350
- color: #2c3e50;
351
- }
352
- .pict-flow-info-panel-header {
353
- font-size: 14px;
354
- font-weight: 600;
355
- margin-bottom: 4px;
356
- }
357
- .pict-flow-info-panel-header.with-icon {
358
- font-size: 16px;
359
- }
360
- .pict-flow-info-panel-description {
361
- font-size: 11px;
362
- color: #7f8c8d;
363
- margin-bottom: 8px;
364
- }
365
- .pict-flow-info-panel-badges {
366
- margin-bottom: 8px;
367
- }
368
- .pict-flow-info-panel-badge {
369
- display: inline-block;
370
- padding: 1px 6px;
371
- border-radius: 3px;
372
- font-size: 10px;
373
- margin-right: 4px;
374
- }
375
- .pict-flow-info-panel-badge.category {
376
- background: #ecf0f1;
377
- color: #7f8c8d;
378
- }
379
- .pict-flow-info-panel-badge.code {
380
- background: #eaf2f8;
381
- color: #2980b9;
382
- font-family: monospace;
383
- }
384
- .pict-flow-info-panel-section {
385
- margin-bottom: 6px;
386
- }
387
- .pict-flow-info-panel-section-title {
388
- font-size: 10px;
389
- font-weight: 700;
390
- text-transform: uppercase;
391
- letter-spacing: 0.5px;
392
- color: #95a5a6;
393
- margin-bottom: 2px;
394
- }
395
- .pict-flow-info-panel-port {
396
- padding: 2px 6px;
397
- background: #f8f9fa;
398
- margin-bottom: 2px;
399
- font-size: 11px;
400
- }
401
- .pict-flow-info-panel-port.input {
402
- border-left: 3px solid #3498db;
403
- }
404
- .pict-flow-info-panel-port.output {
405
- border-left: 3px solid #2ecc71;
406
- }
407
- .pict-flow-info-panel-port-constraint {
408
- color: #95a5a6;
409
- font-size: 10px;
410
- }
411
- `,
64
+ CSS: false,
412
65
 
413
66
  Templates:
414
67
  [
415
68
  {
416
69
  Hash: 'Flow-PanelChrome-Template',
417
- Template: /*html*/`<div class="pict-flow-panel" xmlns="http://www.w3.org/1999/xhtml"><div class="pict-flow-panel-titlebar" data-element-type="panel-titlebar" data-panel-hash="{~D:Record.Hash~}"><span class="pict-flow-panel-title-text">{~D:Record.Title~}</span><span class="pict-flow-panel-close-btn" data-element-type="panel-close" data-panel-hash="{~D:Record.Hash~}">\u2715</span></div><div class="pict-flow-panel-body" data-panel-hash="{~D:Record.Hash~}"></div></div>`
70
+ Template: /*html*/`<div class="pict-flow-panel" xmlns="http://www.w3.org/1999/xhtml"><div class="pict-flow-panel-titlebar" data-element-type="panel-titlebar" data-panel-hash="{~D:Record.Hash~}"><span class="pict-flow-panel-title-text">{~D:Record.Title~}</span><span class="pict-flow-panel-close-btn" data-element-type="panel-close" data-panel-hash="{~D:Record.Hash~}"><span class="pict-flow-panel-close-icon"></span></span></div><div class="pict-flow-panel-body" data-panel-hash="{~D:Record.Hash~}"></div><div class="pict-flow-panel-node-props" data-panel-hash="{~D:Record.Hash~}"><div class="pict-flow-panel-node-props-header" data-element-type="node-props-toggle" data-panel-hash="{~D:Record.Hash~}"><span class="pict-flow-panel-node-props-chevron">&#9654;</span><span class="pict-flow-panel-node-props-title">Node Properties</span></div><div class="pict-flow-panel-node-props-body" style="display:none;"></div></div></div>`
418
71
  },
419
72
  {
420
73
  Hash: 'Flow-Container-Template',
421
74
  Template: /*html*/`
422
75
  <div class="pict-flow-container" id="Flow-Wrapper-{~D:Record.ViewIdentifier~}">
423
76
  <div id="Flow-Toolbar-{~D:Record.ViewIdentifier~}"></div>
77
+ <div id="Flow-FloatingToolbar-Container-{~D:Record.ViewIdentifier~}" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100;"></div>
424
78
  <div class="pict-flow-svg-container" id="Flow-SVG-Container-{~D:Record.ViewIdentifier~}">
425
79
  <svg class="pict-flow-svg"
426
80
  id="Flow-SVG-{~D:Record.ViewIdentifier~}"
427
81
  xmlns="http://www.w3.org/2000/svg">
428
82
  <defs>
429
- <marker id="flow-arrowhead-{~D:Record.ViewIdentifier~}"
430
- markerWidth="5" markerHeight="7"
431
- refX="7.5" refY="3.5"
432
- orient="auto" markerUnits="strokeWidth">
433
- <polygon points="0 0, 5 3.5, 0 7" fill="#95a5a6" />
434
- </marker>
435
- <marker id="flow-arrowhead-selected-{~D:Record.ViewIdentifier~}"
436
- markerWidth="5" markerHeight="7"
437
- refX="7.5" refY="3.5"
438
- orient="auto" markerUnits="strokeWidth">
439
- <polygon points="0 0, 5 3.5, 0 7" fill="#3498db" />
440
- </marker>
441
- <marker id="flow-tether-arrowhead-{~D:Record.ViewIdentifier~}"
442
- markerWidth="4" markerHeight="6"
443
- refX="6" refY="3"
444
- orient="auto" markerUnits="strokeWidth">
445
- <polygon points="0 0, 4 3, 0 6" fill="#95a5a6" />
446
- </marker>
447
83
  <pattern id="flow-grid-{~D:Record.ViewIdentifier~}"
448
84
  width="20" height="20" patternUnits="userSpaceOnUse">
449
85
  <line x1="20" y1="0" x2="20" y2="20" class="pict-flow-grid-pattern" />
@@ -507,6 +143,18 @@ class PictViewFlow extends libPictView
507
143
  {
508
144
  this.fable.addServiceType('PictServiceFlowPathGenerator', libPictServiceFlowPathGenerator);
509
145
  }
146
+ if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowViewportManager'))
147
+ {
148
+ this.fable.addServiceType('PictServiceFlowViewportManager', libPictServiceFlowViewportManager);
149
+ }
150
+ if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowSelectionManager'))
151
+ {
152
+ this.fable.addServiceType('PictServiceFlowSelectionManager', libPictServiceFlowSelectionManager);
153
+ }
154
+ if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowPanelManager'))
155
+ {
156
+ this.fable.addServiceType('PictServiceFlowPanelManager', libPictServiceFlowPanelManager);
157
+ }
510
158
  if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowSVGHelpers'))
511
159
  {
512
160
  this.fable.addServiceType('PictProviderFlowSVGHelpers', libPictProviderFlowSVGHelpers);
@@ -519,6 +167,26 @@ class PictViewFlow extends libPictView
519
167
  {
520
168
  this.fable.addServiceType('PictProviderFlowPanelChrome', libPictProviderFlowPanelChrome);
521
169
  }
170
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowCSS'))
171
+ {
172
+ this.fable.addServiceType('PictProviderFlowCSS', libPictProviderFlowCSS);
173
+ }
174
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowIcons'))
175
+ {
176
+ this.fable.addServiceType('PictProviderFlowIcons', libPictProviderFlowIcons);
177
+ }
178
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowConnectorShapes'))
179
+ {
180
+ this.fable.addServiceType('PictProviderFlowConnectorShapes', libPictProviderFlowConnectorShapes);
181
+ }
182
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowTheme'))
183
+ {
184
+ this.fable.addServiceType('PictProviderFlowTheme', libPictProviderFlowTheme);
185
+ }
186
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowNoise'))
187
+ {
188
+ this.fable.addServiceType('PictProviderFlowNoise', libPictProviderFlowNoise);
189
+ }
522
190
  if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowNodeTypes'))
523
191
  {
524
192
  this.fable.addServiceType('PictProviderFlowNodeTypes', libPictProviderFlowNodeTypes);
@@ -539,6 +207,10 @@ class PictViewFlow extends libPictView
539
207
  {
540
208
  this.fable.addServiceType('PictViewFlowToolbar', libPictViewFlowToolbar);
541
209
  }
210
+ if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowFloatingToolbar'))
211
+ {
212
+ this.fable.addServiceType('PictViewFlowFloatingToolbar', libPictViewFlowFloatingToolbar);
213
+ }
542
214
  if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowPropertiesPanel'))
543
215
  {
544
216
  this.fable.addServiceType('PictViewFlowPropertiesPanel', libPictViewFlowPropertiesPanel);
@@ -592,6 +264,14 @@ class PictViewFlow extends libPictView
592
264
  this._TetherService = null;
593
265
  this._LayoutService = null;
594
266
  this._PathGenerator = null;
267
+ this._ViewportManager = null;
268
+ this._SelectionManager = null;
269
+ this._PanelManager = null;
270
+ this._CSSProvider = null;
271
+ this._IconProvider = null;
272
+ this._ConnectorShapesProvider = null;
273
+ this._ThemeProvider = null;
274
+ this._NoiseProvider = null;
595
275
  this._SVGHelperProvider = null;
596
276
  this._GeometryProvider = null;
597
277
  this._PanelChromeProvider = null;
@@ -602,8 +282,6 @@ class PictViewFlow extends libPictView
602
282
  this._ToolbarView = null;
603
283
  this._PropertiesPanelView = null;
604
284
 
605
- this._IsFullscreen = false;
606
-
607
285
  this.initialRenderComplete = false;
608
286
  }
609
287
 
@@ -617,6 +295,12 @@ class PictViewFlow extends libPictView
617
295
  return this._FlowData.ViewState;
618
296
  }
619
297
 
298
+ // Backward-compatible getter for InteractionManager direct access
299
+ get _IsFullscreen()
300
+ {
301
+ return this._ViewportManager ? this._ViewportManager._IsFullscreen : false;
302
+ }
303
+
620
304
  /**
621
305
  * Override render to pass view options as the template record,
622
306
  * so template expressions like {~D:Record.ViewIdentifier~} resolve correctly.
@@ -640,6 +324,31 @@ class PictViewFlow extends libPictView
640
324
  {
641
325
  super.onBeforeInitialize();
642
326
 
327
+ // Instantiate theme and noise providers (before CSS so theme state is available)
328
+ this._ThemeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowTheme', { FlowView: this });
329
+ this._NoiseProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNoise');
330
+
331
+ // Apply initial theme from options
332
+ if (this.options.Theme)
333
+ {
334
+ this._ThemeProvider.setTheme(this.options.Theme);
335
+ }
336
+ if (typeof this.options.NoiseLevel === 'number')
337
+ {
338
+ this._ThemeProvider.setNoiseLevel(this.options.NoiseLevel);
339
+ }
340
+
341
+ // Instantiate and register the centralized CSS provider
342
+ this._CSSProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowCSS', { FlowView: this });
343
+ this._CSSProvider.registerCSS();
344
+
345
+ // Instantiate the SVG icon provider
346
+ this._IconProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowIcons', { FlowView: this });
347
+ this._IconProvider.registerIconTemplates();
348
+
349
+ // Instantiate the connector shapes provider
350
+ this._ConnectorShapesProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowConnectorShapes', { FlowView: this });
351
+
643
352
  // Instantiate shared utility providers first (used by services below)
644
353
  this._SVGHelperProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowSVGHelpers');
645
354
  this._GeometryProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowGeometry');
@@ -651,11 +360,15 @@ class PictViewFlow extends libPictView
651
360
  this._ConnectionRenderer = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowConnectionRenderer', { FlowView: this });
652
361
  this._TetherService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowTether', { FlowView: this });
653
362
  this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
363
+ this._ViewportManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowViewportManager', { FlowView: this });
364
+ this._SelectionManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowSelectionManager', { FlowView: this });
365
+ this._PanelManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowPanelManager', { FlowView: this });
654
366
 
655
367
  // Instantiate providers, passing any additional node types from view options
656
368
  this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this, AdditionalNodeTypes: this.options.NodeTypes });
657
369
  this._EventHandlerProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowEventHandler', { FlowView: this });
658
370
  this._LayoutProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowLayouts', { FlowView: this });
371
+ this._LayoutProvider.loadPersistedLayouts();
659
372
 
660
373
  return super.onBeforeInitialize();
661
374
  }
@@ -713,6 +426,36 @@ class PictViewFlow extends libPictView
713
426
  this._PanelsLayer = tmpPanelsElements[0];
714
427
  }
715
428
 
429
+ // Initialize theme and noise providers (fallback if not already created)
430
+ if (!this._ThemeProvider)
431
+ {
432
+ this._ThemeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowTheme', { FlowView: this });
433
+ }
434
+ if (!this._NoiseProvider)
435
+ {
436
+ this._NoiseProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNoise');
437
+ }
438
+
439
+ // Initialize CSS provider (fallback if not already created)
440
+ if (!this._CSSProvider)
441
+ {
442
+ this._CSSProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowCSS', { FlowView: this });
443
+ this._CSSProvider.registerCSS();
444
+ }
445
+
446
+ // Initialize icon provider (fallback if not already created)
447
+ if (!this._IconProvider)
448
+ {
449
+ this._IconProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowIcons', { FlowView: this });
450
+ this._IconProvider.registerIconTemplates();
451
+ }
452
+
453
+ // Initialize connector shapes provider (fallback if not already created)
454
+ if (!this._ConnectorShapesProvider)
455
+ {
456
+ this._ConnectorShapesProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowConnectorShapes', { FlowView: this });
457
+ }
458
+
716
459
  // Initialize shared utility providers (used by services below)
717
460
  if (!this._SVGHelperProvider)
718
461
  {
@@ -748,6 +491,18 @@ class PictViewFlow extends libPictView
748
491
  {
749
492
  this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
750
493
  }
494
+ if (!this._ViewportManager)
495
+ {
496
+ this._ViewportManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowViewportManager', { FlowView: this });
497
+ }
498
+ if (!this._SelectionManager)
499
+ {
500
+ this._SelectionManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowSelectionManager', { FlowView: this });
501
+ }
502
+ if (!this._PanelManager)
503
+ {
504
+ this._PanelManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowPanelManager', { FlowView: this });
505
+ }
751
506
  if (!this._NodeTypeProvider)
752
507
  {
753
508
  this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this, AdditionalNodeTypes: this.options.NodeTypes });
@@ -759,6 +514,25 @@ class PictViewFlow extends libPictView
759
514
  if (!this._LayoutProvider)
760
515
  {
761
516
  this._LayoutProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowLayouts', { FlowView: this });
517
+ this._LayoutProvider.loadPersistedLayouts();
518
+ }
519
+
520
+ // Inject marker defs via the connector shapes provider
521
+ // Note: insertAdjacentHTML does not work on SVG elements (wrong namespace),
522
+ // so we parse via a temporary <svg> element to ensure SVG namespace.
523
+ if (this._ConnectorShapesProvider && this._SVGElement)
524
+ {
525
+ let tmpDefs = this._SVGElement.querySelector('defs');
526
+ if (tmpDefs)
527
+ {
528
+ let tmpMarkerMarkup = this._ConnectorShapesProvider.generateMarkerDefs(tmpViewIdentifier);
529
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
530
+ tmpTempSVG.innerHTML = tmpMarkerMarkup;
531
+ while (tmpTempSVG.firstChild)
532
+ {
533
+ tmpDefs.appendChild(tmpTempSVG.firstChild);
534
+ }
535
+ }
762
536
  }
763
537
 
764
538
  // Setup the toolbar if enabled
@@ -890,6 +664,12 @@ class PictViewFlow extends libPictView
890
664
  )
891
665
  };
892
666
 
667
+ // Merge any browser-persisted layouts into the newly loaded data
668
+ if (this._LayoutProvider)
669
+ {
670
+ this._LayoutProvider.loadPersistedLayouts();
671
+ }
672
+
893
673
  if (this.initialRenderComplete)
894
674
  {
895
675
  this.renderFlow();
@@ -1107,18 +887,7 @@ class PictViewFlow extends libPictView
1107
887
  */
1108
888
  selectNode(pNodeHash)
1109
889
  {
1110
- let tmpPreviousSelection = this._FlowData.ViewState.SelectedNodeHash;
1111
- this._FlowData.ViewState.SelectedNodeHash = pNodeHash;
1112
- this._FlowData.ViewState.SelectedConnectionHash = null;
1113
- this._FlowData.ViewState.SelectedTetherHash = null;
1114
-
1115
- this.renderFlow();
1116
-
1117
- if (this._EventHandlerProvider && pNodeHash !== tmpPreviousSelection)
1118
- {
1119
- let tmpNode = pNodeHash ? this._FlowData.Nodes.find((pNode) => pNode.Hash === pNodeHash) : null;
1120
- this._EventHandlerProvider.fireEvent('onNodeSelected', tmpNode);
1121
- }
890
+ return this._SelectionManager.selectNode(pNodeHash);
1122
891
  }
1123
892
 
1124
893
  /**
@@ -1127,18 +896,7 @@ class PictViewFlow extends libPictView
1127
896
  */
1128
897
  selectConnection(pConnectionHash)
1129
898
  {
1130
- let tmpPreviousSelection = this._FlowData.ViewState.SelectedConnectionHash;
1131
- this._FlowData.ViewState.SelectedConnectionHash = pConnectionHash;
1132
- this._FlowData.ViewState.SelectedNodeHash = null;
1133
- this._FlowData.ViewState.SelectedTetherHash = null;
1134
-
1135
- this.renderFlow();
1136
-
1137
- if (this._EventHandlerProvider && pConnectionHash !== tmpPreviousSelection)
1138
- {
1139
- let tmpConnection = pConnectionHash ? this._FlowData.Connections.find((pConn) => pConn.Hash === pConnectionHash) : null;
1140
- this._EventHandlerProvider.fireEvent('onConnectionSelected', tmpConnection);
1141
- }
899
+ return this._SelectionManager.selectConnection(pConnectionHash);
1142
900
  }
1143
901
 
1144
902
  /**
@@ -1146,10 +904,7 @@ class PictViewFlow extends libPictView
1146
904
  */
1147
905
  deselectAll()
1148
906
  {
1149
- this._FlowData.ViewState.SelectedNodeHash = null;
1150
- this._FlowData.ViewState.SelectedConnectionHash = null;
1151
- this._FlowData.ViewState.SelectedTetherHash = null;
1152
- this.renderFlow();
907
+ return this._SelectionManager.deselectAll();
1153
908
  }
1154
909
 
1155
910
  /**
@@ -1158,15 +913,7 @@ class PictViewFlow extends libPictView
1158
913
  */
1159
914
  deleteSelected()
1160
915
  {
1161
- if (this._FlowData.ViewState.SelectedNodeHash)
1162
- {
1163
- return this.removeNode(this._FlowData.ViewState.SelectedNodeHash);
1164
- }
1165
- if (this._FlowData.ViewState.SelectedConnectionHash)
1166
- {
1167
- return this.removeConnection(this._FlowData.ViewState.SelectedConnectionHash);
1168
- }
1169
- return false;
916
+ return this._SelectionManager.deleteSelected();
1170
917
  }
1171
918
 
1172
919
  /**
@@ -1174,11 +921,7 @@ class PictViewFlow extends libPictView
1174
921
  */
1175
922
  updateViewportTransform()
1176
923
  {
1177
- if (!this._ViewportElement) return;
1178
- let tmpVS = this._FlowData.ViewState;
1179
- this._ViewportElement.setAttribute('transform',
1180
- `translate(${tmpVS.PanX}, ${tmpVS.PanY}) scale(${tmpVS.Zoom})`
1181
- );
924
+ return this._ViewportManager.updateViewportTransform();
1182
925
  }
1183
926
 
1184
927
  /**
@@ -1189,19 +932,7 @@ class PictViewFlow extends libPictView
1189
932
  */
1190
933
  setZoom(pZoom, pFocusX, pFocusY)
1191
934
  {
1192
- let tmpNewZoom = Math.max(this.options.MinZoom, Math.min(this.options.MaxZoom, pZoom));
1193
- let tmpOldZoom = this._FlowData.ViewState.Zoom;
1194
-
1195
- if (typeof pFocusX === 'number' && typeof pFocusY === 'number')
1196
- {
1197
- // Zoom toward focus point
1198
- let tmpVS = this._FlowData.ViewState;
1199
- tmpVS.PanX = pFocusX - (pFocusX - tmpVS.PanX) * (tmpNewZoom / tmpOldZoom);
1200
- tmpVS.PanY = pFocusY - (pFocusY - tmpVS.PanY) * (tmpNewZoom / tmpOldZoom);
1201
- }
1202
-
1203
- this._FlowData.ViewState.Zoom = tmpNewZoom;
1204
- this.updateViewportTransform();
935
+ return this._ViewportManager.setZoom(pZoom, pFocusX, pFocusY);
1205
936
  }
1206
937
 
1207
938
  /**
@@ -1209,39 +940,7 @@ class PictViewFlow extends libPictView
1209
940
  */
1210
941
  zoomToFit()
1211
942
  {
1212
- if (this._FlowData.Nodes.length === 0) return;
1213
- if (!this._SVGElement) return;
1214
-
1215
- let tmpMinX = Infinity, tmpMinY = Infinity;
1216
- let tmpMaxX = -Infinity, tmpMaxY = -Infinity;
1217
-
1218
- for (let i = 0; i < this._FlowData.Nodes.length; i++)
1219
- {
1220
- let tmpNode = this._FlowData.Nodes[i];
1221
- tmpMinX = Math.min(tmpMinX, tmpNode.X);
1222
- tmpMinY = Math.min(tmpMinY, tmpNode.Y);
1223
- tmpMaxX = Math.max(tmpMaxX, tmpNode.X + tmpNode.Width);
1224
- tmpMaxY = Math.max(tmpMaxY, tmpNode.Y + tmpNode.Height);
1225
- }
1226
-
1227
- let tmpPadding = 50;
1228
- let tmpFlowWidth = tmpMaxX - tmpMinX + tmpPadding * 2;
1229
- let tmpFlowHeight = tmpMaxY - tmpMinY + tmpPadding * 2;
1230
-
1231
- let tmpSVGRect = this._SVGElement.getBoundingClientRect();
1232
- let tmpScaleX = tmpSVGRect.width / tmpFlowWidth;
1233
- let tmpScaleY = tmpSVGRect.height / tmpFlowHeight;
1234
- let tmpZoom = Math.min(tmpScaleX, tmpScaleY, 1.0); // Don't zoom in past 1.0
1235
- tmpZoom = Math.max(this.options.MinZoom, Math.min(this.options.MaxZoom, tmpZoom));
1236
-
1237
- let tmpCenterX = (tmpMinX + tmpMaxX) / 2;
1238
- let tmpCenterY = (tmpMinY + tmpMaxY) / 2;
1239
-
1240
- this._FlowData.ViewState.Zoom = tmpZoom;
1241
- this._FlowData.ViewState.PanX = (tmpSVGRect.width / 2) - (tmpCenterX * tmpZoom);
1242
- this._FlowData.ViewState.PanY = (tmpSVGRect.height / 2) - (tmpCenterY * tmpZoom);
1243
-
1244
- this.updateViewportTransform();
943
+ return this._ViewportManager.zoomToFit();
1245
944
  }
1246
945
 
1247
946
  /**
@@ -1269,41 +968,129 @@ class PictViewFlow extends libPictView
1269
968
  */
1270
969
  toggleFullscreen()
1271
970
  {
1272
- let tmpViewIdentifier = this.options.ViewIdentifier;
1273
- let tmpContainerElements = this.pict.ContentAssignment.getElement(`#Flow-Wrapper-${tmpViewIdentifier}`);
1274
- if (tmpContainerElements.length < 1) return this._IsFullscreen;
971
+ return this._ViewportManager.toggleFullscreen();
972
+ }
1275
973
 
1276
- let tmpContainer = tmpContainerElements[0];
974
+ /**
975
+ * Exit fullscreen mode if currently active.
976
+ */
977
+ exitFullscreen()
978
+ {
979
+ return this._ViewportManager.exitFullscreen();
980
+ }
1277
981
 
1278
- this._IsFullscreen = !this._IsFullscreen;
982
+ // ── Theme API ────────────────────────────────────────────────────────
1279
983
 
1280
- if (this._IsFullscreen)
984
+ /**
985
+ * Switch the active theme and re-render.
986
+ * @param {string} pThemeKey - Theme key (e.g. 'default', 'sketch', 'blueprint', 'mono', 'retro-80s', 'retro-90s')
987
+ */
988
+ setTheme(pThemeKey)
989
+ {
990
+ if (!this._ThemeProvider)
991
+ {
992
+ this.log.warn('PictSectionFlow setTheme: ThemeProvider not available');
993
+ return;
994
+ }
995
+
996
+ let tmpApplied = this._ThemeProvider.setTheme(pThemeKey);
997
+ if (!tmpApplied) return;
998
+
999
+ // Re-register CSS with the new theme overrides
1000
+ if (this._CSSProvider)
1281
1001
  {
1282
- tmpContainer.classList.add('pict-flow-fullscreen');
1002
+ this._CSSProvider.registerCSS();
1283
1003
  }
1284
- else
1004
+
1005
+ // Re-inject marker defs (arrowhead colors may have changed)
1006
+ this._reinjectMarkerDefs();
1007
+
1008
+ // Full re-render
1009
+ if (this.initialRenderComplete)
1285
1010
  {
1286
- tmpContainer.classList.remove('pict-flow-fullscreen');
1011
+ this.renderFlow();
1287
1012
  }
1288
1013
 
1289
- return this._IsFullscreen;
1014
+ if (this._EventHandlerProvider)
1015
+ {
1016
+ this._EventHandlerProvider.fireEvent('onThemeChanged', pThemeKey);
1017
+ }
1290
1018
  }
1291
1019
 
1292
1020
  /**
1293
- * Exit fullscreen mode if currently active.
1021
+ * Set the noise level (0 to 1) and re-render.
1022
+ * @param {number} pLevel - 0 = precise, 1 = maximum wobble
1294
1023
  */
1295
- exitFullscreen()
1024
+ setNoiseLevel(pLevel)
1025
+ {
1026
+ if (!this._ThemeProvider)
1027
+ {
1028
+ this.log.warn('PictSectionFlow setNoiseLevel: ThemeProvider not available');
1029
+ return;
1030
+ }
1031
+
1032
+ this._ThemeProvider.setNoiseLevel(pLevel);
1033
+
1034
+ // Full re-render to apply new noise
1035
+ if (this.initialRenderComplete)
1036
+ {
1037
+ this.renderFlow();
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Get the current noise level (0 to 1).
1043
+ * @returns {number}
1044
+ */
1045
+ getNoiseLevel()
1046
+ {
1047
+ if (this._ThemeProvider)
1048
+ {
1049
+ return this._ThemeProvider.getNoiseLevel();
1050
+ }
1051
+ return 0;
1052
+ }
1053
+
1054
+ /**
1055
+ * Get the active theme key.
1056
+ * @returns {string}
1057
+ */
1058
+ getThemeKey()
1059
+ {
1060
+ if (this._ThemeProvider)
1061
+ {
1062
+ return this._ThemeProvider.getActiveThemeKey();
1063
+ }
1064
+ return 'default';
1065
+ }
1066
+
1067
+ /**
1068
+ * Re-inject SVG marker definitions (arrowheads).
1069
+ * Called after a theme switch to update arrowhead colors.
1070
+ */
1071
+ _reinjectMarkerDefs()
1296
1072
  {
1297
- if (!this._IsFullscreen) return;
1073
+ if (!this._ConnectorShapesProvider || !this._SVGElement) return;
1298
1074
 
1299
1075
  let tmpViewIdentifier = this.options.ViewIdentifier;
1300
- let tmpContainerElements = this.pict.ContentAssignment.getElement(`#Flow-Wrapper-${tmpViewIdentifier}`);
1301
- if (tmpContainerElements.length > 0)
1076
+ let tmpDefs = this._SVGElement.querySelector('defs');
1077
+ if (!tmpDefs) return;
1078
+
1079
+ // Remove existing marker elements
1080
+ let tmpExistingMarkers = tmpDefs.querySelectorAll('marker');
1081
+ for (let i = 0; i < tmpExistingMarkers.length; i++)
1302
1082
  {
1303
- tmpContainerElements[0].classList.remove('pict-flow-fullscreen');
1083
+ tmpExistingMarkers[i].remove();
1304
1084
  }
1305
1085
 
1306
- this._IsFullscreen = false;
1086
+ // Re-generate and inject
1087
+ let tmpMarkerMarkup = this._ConnectorShapesProvider.generateMarkerDefs(tmpViewIdentifier);
1088
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1089
+ tmpTempSVG.innerHTML = tmpMarkerMarkup;
1090
+ while (tmpTempSVG.firstChild)
1091
+ {
1092
+ tmpDefs.appendChild(tmpTempSVG.firstChild);
1093
+ }
1307
1094
  }
1308
1095
 
1309
1096
  /**
@@ -1332,18 +1119,7 @@ class PictViewFlow extends libPictView
1332
1119
  */
1333
1120
  selectTether(pPanelHash)
1334
1121
  {
1335
- let tmpPreviousSelection = this._FlowData.ViewState.SelectedTetherHash;
1336
- this._FlowData.ViewState.SelectedTetherHash = pPanelHash;
1337
- this._FlowData.ViewState.SelectedNodeHash = null;
1338
- this._FlowData.ViewState.SelectedConnectionHash = null;
1339
-
1340
- this.renderFlow();
1341
-
1342
- if (this._EventHandlerProvider && pPanelHash !== tmpPreviousSelection)
1343
- {
1344
- let tmpPanel = pPanelHash ? this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash) : null;
1345
- this._EventHandlerProvider.fireEvent('onTetherSelected', tmpPanel);
1346
- }
1122
+ return this._SelectionManager.selectTether(pPanelHash);
1347
1123
  }
1348
1124
 
1349
1125
  /**
@@ -1552,40 +1328,9 @@ class PictViewFlow extends libPictView
1552
1328
 
1553
1329
  let tmpTitleBarHeight = (this._NodeView && this._NodeView.options.NodeTitleBarHeight) || 28;
1554
1330
 
1555
- let tmpX, tmpY;
1331
+ let tmpLocal = this._GeometryProvider.getPortLocalPosition(tmpPort.Side, tmpPortIndex, tmpPortCount, tmpNode.Width, tmpNode.Height, tmpTitleBarHeight);
1556
1332
 
1557
- switch (tmpPort.Side)
1558
- {
1559
- case 'left':
1560
- {
1561
- // Distribute ports in the body area below the title bar
1562
- let tmpBodyHeight = tmpNode.Height - tmpTitleBarHeight;
1563
- tmpX = tmpNode.X;
1564
- tmpY = tmpNode.Y + tmpTitleBarHeight + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpBodyHeight;
1565
- break;
1566
- }
1567
- case 'right':
1568
- {
1569
- let tmpBodyHeight = tmpNode.Height - tmpTitleBarHeight;
1570
- tmpX = tmpNode.X + tmpNode.Width;
1571
- tmpY = tmpNode.Y + tmpTitleBarHeight + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpBodyHeight;
1572
- break;
1573
- }
1574
- case 'top':
1575
- tmpX = tmpNode.X + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpNode.Width;
1576
- tmpY = tmpNode.Y;
1577
- break;
1578
- case 'bottom':
1579
- tmpX = tmpNode.X + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpNode.Width;
1580
- tmpY = tmpNode.Y + tmpNode.Height;
1581
- break;
1582
- default:
1583
- tmpX = tmpNode.X + tmpNode.Width;
1584
- tmpY = tmpNode.Y + tmpNode.Height / 2;
1585
- break;
1586
- }
1587
-
1588
- return { x: tmpX, y: tmpY, side: tmpPort.Side || 'right' };
1333
+ return { x: tmpNode.X + tmpLocal.x, y: tmpNode.Y + tmpLocal.y, side: tmpPort.Side || 'right' };
1589
1334
  }
1590
1335
 
1591
1336
  /**
@@ -1596,29 +1341,7 @@ class PictViewFlow extends libPictView
1596
1341
  */
1597
1342
  screenToSVGCoords(pScreenX, pScreenY)
1598
1343
  {
1599
- if (!this._SVGElement)
1600
- {
1601
- return { x: pScreenX, y: pScreenY };
1602
- }
1603
-
1604
- let tmpPoint = this._SVGElement.createSVGPoint();
1605
- tmpPoint.x = pScreenX;
1606
- tmpPoint.y = pScreenY;
1607
-
1608
- let tmpCTM = this._SVGElement.getScreenCTM();
1609
- if (tmpCTM)
1610
- {
1611
- let tmpInverse = tmpCTM.inverse();
1612
- let tmpTransformed = tmpPoint.matrixTransform(tmpInverse);
1613
- // Account for viewport pan/zoom
1614
- let tmpVS = this._FlowData.ViewState;
1615
- return {
1616
- x: (tmpTransformed.x - tmpVS.PanX) / tmpVS.Zoom,
1617
- y: (tmpTransformed.y - tmpVS.PanY) / tmpVS.Zoom
1618
- };
1619
- }
1620
-
1621
- return { x: pScreenX, y: pScreenY };
1344
+ return this._ViewportManager.screenToSVGCoords(pScreenX, pScreenY);
1622
1345
  }
1623
1346
 
1624
1347
  /**
@@ -1773,59 +1496,7 @@ class PictViewFlow extends libPictView
1773
1496
  */
1774
1497
  openPanel(pNodeHash)
1775
1498
  {
1776
- let tmpNode = this.getNode(pNodeHash);
1777
- if (!tmpNode) return false;
1778
-
1779
- let tmpNodeTypeConfig = this._NodeTypeProvider.getNodeType(tmpNode.Type);
1780
- if (!tmpNodeTypeConfig) return false;
1781
-
1782
- // Check if a panel is already open for this node
1783
- let tmpExisting = this._FlowData.OpenPanels.find((pPanel) => pPanel.NodeHash === pNodeHash);
1784
- if (tmpExisting) return tmpExisting;
1785
-
1786
- let tmpPanelConfig = tmpNodeTypeConfig.PropertiesPanel;
1787
- let tmpPanelHash = `panel-${this.fable.getUUID()}`;
1788
- let tmpWidth, tmpHeight, tmpPanelType, tmpTitle;
1789
-
1790
- if (tmpPanelConfig)
1791
- {
1792
- tmpWidth = tmpPanelConfig.DefaultWidth || 300;
1793
- tmpHeight = tmpPanelConfig.DefaultHeight || 200;
1794
- tmpPanelType = tmpPanelConfig.PanelType || 'Base';
1795
- tmpTitle = tmpPanelConfig.Title || tmpNodeTypeConfig.Label || 'Properties';
1796
- }
1797
- else
1798
- {
1799
- // No PropertiesPanel configured — open an auto-generated info panel
1800
- tmpWidth = 240;
1801
- tmpHeight = 180;
1802
- tmpPanelType = 'Info';
1803
- tmpTitle = tmpNodeTypeConfig.Label || tmpNode.Title || 'Node Info';
1804
- }
1805
-
1806
- let tmpPanelData =
1807
- {
1808
- Hash: tmpPanelHash,
1809
- NodeHash: pNodeHash,
1810
- PanelType: tmpPanelType,
1811
- Title: tmpTitle,
1812
- X: tmpNode.X + tmpNode.Width + 30,
1813
- Y: tmpNode.Y,
1814
- Width: tmpWidth,
1815
- Height: tmpHeight
1816
- };
1817
-
1818
- this._FlowData.OpenPanels.push(tmpPanelData);
1819
- this.renderFlow();
1820
- this.marshalFromView();
1821
-
1822
- if (this._EventHandlerProvider)
1823
- {
1824
- this._EventHandlerProvider.fireEvent('onPanelOpened', tmpPanelData);
1825
- this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
1826
- }
1827
-
1828
- return tmpPanelData;
1499
+ return this._PanelManager.openPanel(pNodeHash);
1829
1500
  }
1830
1501
 
1831
1502
  /**
@@ -1835,27 +1506,7 @@ class PictViewFlow extends libPictView
1835
1506
  */
1836
1507
  closePanel(pPanelHash)
1837
1508
  {
1838
- let tmpIndex = this._FlowData.OpenPanels.findIndex((pPanel) => pPanel.Hash === pPanelHash);
1839
- if (tmpIndex < 0) return false;
1840
-
1841
- let tmpRemovedPanel = this._FlowData.OpenPanels.splice(tmpIndex, 1)[0];
1842
-
1843
- // Clean up the panel instance
1844
- if (this._PropertiesPanelView)
1845
- {
1846
- this._PropertiesPanelView.destroyPanel(pPanelHash);
1847
- }
1848
-
1849
- this.renderFlow();
1850
- this.marshalFromView();
1851
-
1852
- if (this._EventHandlerProvider)
1853
- {
1854
- this._EventHandlerProvider.fireEvent('onPanelClosed', tmpRemovedPanel);
1855
- this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
1856
- }
1857
-
1858
- return true;
1509
+ return this._PanelManager.closePanel(pPanelHash);
1859
1510
  }
1860
1511
 
1861
1512
  /**
@@ -1865,23 +1516,7 @@ class PictViewFlow extends libPictView
1865
1516
  */
1866
1517
  closePanelForNode(pNodeHash)
1867
1518
  {
1868
- let tmpPanelsToClose = this._FlowData.OpenPanels.filter((pPanel) => pPanel.NodeHash === pNodeHash);
1869
- if (tmpPanelsToClose.length === 0) return false;
1870
-
1871
- for (let i = 0; i < tmpPanelsToClose.length; i++)
1872
- {
1873
- let tmpIndex = this._FlowData.OpenPanels.indexOf(tmpPanelsToClose[i]);
1874
- if (tmpIndex >= 0)
1875
- {
1876
- this._FlowData.OpenPanels.splice(tmpIndex, 1);
1877
- }
1878
- if (this._PropertiesPanelView)
1879
- {
1880
- this._PropertiesPanelView.destroyPanel(tmpPanelsToClose[i].Hash);
1881
- }
1882
- }
1883
-
1884
- return true;
1519
+ return this._PanelManager.closePanelForNode(pNodeHash);
1885
1520
  }
1886
1521
 
1887
1522
  /**
@@ -1891,13 +1526,7 @@ class PictViewFlow extends libPictView
1891
1526
  */
1892
1527
  togglePanel(pNodeHash)
1893
1528
  {
1894
- let tmpExisting = this._FlowData.OpenPanels.find((pPanel) => pPanel.NodeHash === pNodeHash);
1895
- if (tmpExisting)
1896
- {
1897
- this.closePanel(tmpExisting.Hash);
1898
- return false;
1899
- }
1900
- return this.openPanel(pNodeHash);
1529
+ return this._PanelManager.togglePanel(pNodeHash);
1901
1530
  }
1902
1531
 
1903
1532
  /**
@@ -1908,28 +1537,7 @@ class PictViewFlow extends libPictView
1908
1537
  */
1909
1538
  updatePanelPosition(pPanelHash, pX, pY)
1910
1539
  {
1911
- let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
1912
- if (!tmpPanel) return;
1913
-
1914
- tmpPanel.X = pX;
1915
- tmpPanel.Y = pY;
1916
-
1917
- // Reset tether handle positions when panel moves
1918
- this._resetHandlesForPanel(pPanelHash);
1919
-
1920
- // Update the foreignObject position directly for smooth dragging
1921
- if (this._PanelsLayer)
1922
- {
1923
- let tmpFO = this._PanelsLayer.querySelector(`[data-panel-hash="${pPanelHash}"]`);
1924
- if (tmpFO)
1925
- {
1926
- tmpFO.setAttribute('x', String(pX));
1927
- tmpFO.setAttribute('y', String(pY));
1928
- }
1929
- }
1930
-
1931
- // Update the tether for this panel
1932
- this._renderTethersForNode(tmpPanel.NodeHash);
1540
+ return this._PanelManager.updatePanelPosition(pPanelHash, pX, pY);
1933
1541
  }
1934
1542
  }
1935
1543