pict-section-flow 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/docs/README.md +19 -0
  2. package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
  3. package/example_applications/simple_cards/package.json +43 -0
  4. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
  8. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
  9. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
  10. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
  11. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
  12. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
  13. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
  14. package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
  15. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
  16. package/package.json +6 -6
  17. package/source/Pict-Section-Flow.js +19 -0
  18. package/source/PictFlowCard.js +207 -0
  19. package/source/PictFlowCardPropertiesPanel.js +105 -0
  20. package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
  21. package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
  22. package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
  23. package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
  24. package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
  25. package/source/providers/PictProvider-Flow-Geometry.js +64 -0
  26. package/source/providers/PictProvider-Flow-Layouts.js +284 -0
  27. package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
  28. package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
  29. package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
  30. package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
  31. package/source/services/PictService-Flow-InteractionManager.js +399 -75
  32. package/source/services/PictService-Flow-Layout.js +159 -0
  33. package/source/services/PictService-Flow-PathGenerator.js +199 -0
  34. package/source/services/PictService-Flow-Tether.js +544 -0
  35. package/source/views/PictView-Flow-Node.js +95 -18
  36. package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
  37. package/source/views/PictView-Flow-Toolbar.js +491 -5
  38. package/source/views/PictView-Flow.js +830 -8
  39. package/example_application/package.json +0 -41
  40. package/example_application/source/Pict-Application-FlowExample.js +0 -241
  41. package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
  42. /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
  43. /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
  44. /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
  45. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
  46. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
  47. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
  48. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-TopBar.js +0 -0
@@ -2,13 +2,26 @@ const libPictView = require('pict-view');
2
2
 
3
3
  const libPictServiceFlowInteractionManager = require('../services/PictService-Flow-InteractionManager.js');
4
4
  const libPictServiceFlowConnectionRenderer = require('../services/PictService-Flow-ConnectionRenderer.js');
5
+ const libPictServiceFlowTether = require('../services/PictService-Flow-Tether.js');
5
6
  const libPictServiceFlowLayout = require('../services/PictService-Flow-Layout.js');
7
+ const libPictServiceFlowPathGenerator = require('../services/PictService-Flow-PathGenerator.js');
6
8
 
7
9
  const libPictProviderFlowNodeTypes = require('../providers/PictProvider-Flow-NodeTypes.js');
8
10
  const libPictProviderFlowEventHandler = require('../providers/PictProvider-Flow-EventHandler.js');
11
+ const libPictProviderFlowLayouts = require('../providers/PictProvider-Flow-Layouts.js');
12
+ const libPictProviderFlowSVGHelpers = require('../providers/PictProvider-Flow-SVGHelpers.js');
13
+ const libPictProviderFlowGeometry = require('../providers/PictProvider-Flow-Geometry.js');
14
+ const libPictProviderFlowPanelChrome = require('../providers/PictProvider-Flow-PanelChrome.js');
9
15
 
10
16
  const libPictViewFlowNode = require('./PictView-Flow-Node.js');
11
17
  const libPictViewFlowToolbar = require('./PictView-Flow-Toolbar.js');
18
+ const libPictViewFlowPropertiesPanel = require('./PictView-Flow-PropertiesPanel.js');
19
+
20
+ const libPictFlowCardPropertiesPanel = require('../PictFlowCardPropertiesPanel.js');
21
+ const libPanelTemplate = require('../panels/FlowCardPropertiesPanel-Template.js');
22
+ const libPanelMarkdown = require('../panels/FlowCardPropertiesPanel-Markdown.js');
23
+ const libPanelForm = require('../panels/FlowCardPropertiesPanel-Form.js');
24
+ const libPanelView = require('../panels/FlowCardPropertiesPanel-View.js');
12
25
 
13
26
  const _DefaultConfiguration =
14
27
  {
@@ -49,11 +62,17 @@ const _DefaultConfiguration =
49
62
  background-color: #fafafa;
50
63
  border: 1px solid #e0e0e0;
51
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;
52
72
  }
53
73
  .pict-flow-svg {
54
74
  width: 100%;
55
75
  height: 100%;
56
- min-height: 400px;
57
76
  cursor: grab;
58
77
  user-select: none;
59
78
  -webkit-user-select: none;
@@ -178,16 +197,231 @@ const _DefaultConfiguration =
178
197
  rx: 25;
179
198
  ry: 25;
180
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
+ }
181
411
  `,
182
412
 
183
413
  Templates:
184
414
  [
415
+ {
416
+ 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>`
418
+ },
185
419
  {
186
420
  Hash: 'Flow-Container-Template',
187
421
  Template: /*html*/`
188
422
  <div class="pict-flow-container" id="Flow-Wrapper-{~D:Record.ViewIdentifier~}">
189
423
  <div id="Flow-Toolbar-{~D:Record.ViewIdentifier~}"></div>
190
- <div id="Flow-SVG-Container-{~D:Record.ViewIdentifier~}">
424
+ <div class="pict-flow-svg-container" id="Flow-SVG-Container-{~D:Record.ViewIdentifier~}">
191
425
  <svg class="pict-flow-svg"
192
426
  id="Flow-SVG-{~D:Record.ViewIdentifier~}"
193
427
  xmlns="http://www.w3.org/2000/svg">
@@ -204,6 +438,12 @@ const _DefaultConfiguration =
204
438
  orient="auto" markerUnits="strokeWidth">
205
439
  <polygon points="0 0, 5 3.5, 0 7" fill="#3498db" />
206
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>
207
447
  <pattern id="flow-grid-{~D:Record.ViewIdentifier~}"
208
448
  width="20" height="20" patternUnits="userSpaceOnUse">
209
449
  <line x1="20" y1="0" x2="20" y2="20" class="pict-flow-grid-pattern" />
@@ -216,6 +456,8 @@ const _DefaultConfiguration =
216
456
  <g class="pict-flow-viewport" id="Flow-Viewport-{~D:Record.ViewIdentifier~}">
217
457
  <g class="pict-flow-connections-layer" id="Flow-Connections-{~D:Record.ViewIdentifier~}"></g>
218
458
  <g class="pict-flow-nodes-layer" id="Flow-Nodes-{~D:Record.ViewIdentifier~}"></g>
459
+ <g class="pict-flow-tethers-layer" id="Flow-Tethers-{~D:Record.ViewIdentifier~}"></g>
460
+ <g class="pict-flow-panels-layer" id="Flow-Panels-{~D:Record.ViewIdentifier~}"></g>
219
461
  </g>
220
462
  </svg>
221
463
  </div>
@@ -253,10 +495,30 @@ class PictViewFlow extends libPictView
253
495
  {
254
496
  this.fable.addServiceType('PictServiceFlowConnectionRenderer', libPictServiceFlowConnectionRenderer);
255
497
  }
498
+ if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowTether'))
499
+ {
500
+ this.fable.addServiceType('PictServiceFlowTether', libPictServiceFlowTether);
501
+ }
256
502
  if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowLayout'))
257
503
  {
258
504
  this.fable.addServiceType('PictServiceFlowLayout', libPictServiceFlowLayout);
259
505
  }
506
+ if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowPathGenerator'))
507
+ {
508
+ this.fable.addServiceType('PictServiceFlowPathGenerator', libPictServiceFlowPathGenerator);
509
+ }
510
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowSVGHelpers'))
511
+ {
512
+ this.fable.addServiceType('PictProviderFlowSVGHelpers', libPictProviderFlowSVGHelpers);
513
+ }
514
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowGeometry'))
515
+ {
516
+ this.fable.addServiceType('PictProviderFlowGeometry', libPictProviderFlowGeometry);
517
+ }
518
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowPanelChrome'))
519
+ {
520
+ this.fable.addServiceType('PictProviderFlowPanelChrome', libPictProviderFlowPanelChrome);
521
+ }
260
522
  if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowNodeTypes'))
261
523
  {
262
524
  this.fable.addServiceType('PictProviderFlowNodeTypes', libPictProviderFlowNodeTypes);
@@ -265,6 +527,10 @@ class PictViewFlow extends libPictView
265
527
  {
266
528
  this.fable.addServiceType('PictProviderFlowEventHandler', libPictProviderFlowEventHandler);
267
529
  }
530
+ if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowLayouts'))
531
+ {
532
+ this.fable.addServiceType('PictProviderFlowLayouts', libPictProviderFlowLayouts);
533
+ }
268
534
  if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowNode'))
269
535
  {
270
536
  this.fable.addServiceType('PictViewFlowNode', libPictViewFlowNode);
@@ -273,17 +539,44 @@ class PictViewFlow extends libPictView
273
539
  {
274
540
  this.fable.addServiceType('PictViewFlowToolbar', libPictViewFlowToolbar);
275
541
  }
542
+ if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowPropertiesPanel'))
543
+ {
544
+ this.fable.addServiceType('PictViewFlowPropertiesPanel', libPictViewFlowPropertiesPanel);
545
+ }
546
+ if (!this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel'))
547
+ {
548
+ this.fable.addServiceType('PictFlowCardPropertiesPanel', libPictFlowCardPropertiesPanel);
549
+ }
550
+ if (!this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel-Template'))
551
+ {
552
+ this.fable.addServiceType('PictFlowCardPropertiesPanel-Template', libPanelTemplate);
553
+ }
554
+ if (!this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel-Markdown'))
555
+ {
556
+ this.fable.addServiceType('PictFlowCardPropertiesPanel-Markdown', libPanelMarkdown);
557
+ }
558
+ if (!this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel-Form'))
559
+ {
560
+ this.fable.addServiceType('PictFlowCardPropertiesPanel-Form', libPanelForm);
561
+ }
562
+ if (!this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel-View'))
563
+ {
564
+ this.fable.addServiceType('PictFlowCardPropertiesPanel-View', libPanelView);
565
+ }
276
566
 
277
567
  // Internal state
278
568
  this._FlowData = {
279
569
  Nodes: [],
280
570
  Connections: [],
571
+ OpenPanels: [],
572
+ SavedLayouts: [],
281
573
  ViewState: {
282
574
  PanX: 0,
283
575
  PanY: 0,
284
576
  Zoom: 1,
285
577
  SelectedNodeHash: null,
286
- SelectedConnectionHash: null
578
+ SelectedConnectionHash: null,
579
+ SelectedTetherHash: null
287
580
  }
288
581
  };
289
582
 
@@ -291,14 +584,25 @@ class PictViewFlow extends libPictView
291
584
  this._ViewportElement = null;
292
585
  this._NodesLayer = null;
293
586
  this._ConnectionsLayer = null;
587
+ this._TethersLayer = null;
588
+ this._PanelsLayer = null;
294
589
 
295
590
  this._InteractionManager = null;
296
591
  this._ConnectionRenderer = null;
592
+ this._TetherService = null;
297
593
  this._LayoutService = null;
594
+ this._PathGenerator = null;
595
+ this._SVGHelperProvider = null;
596
+ this._GeometryProvider = null;
597
+ this._PanelChromeProvider = null;
298
598
  this._NodeTypeProvider = null;
599
+ this._LayoutProvider = null;
299
600
  this._EventHandlerProvider = null;
300
601
  this._NodeView = null;
301
602
  this._ToolbarView = null;
603
+ this._PropertiesPanelView = null;
604
+
605
+ this._IsFullscreen = false;
302
606
 
303
607
  this.initialRenderComplete = false;
304
608
  }
@@ -336,14 +640,22 @@ class PictViewFlow extends libPictView
336
640
  {
337
641
  super.onBeforeInitialize();
338
642
 
339
- // Register services
643
+ // Instantiate shared utility providers first (used by services below)
644
+ this._SVGHelperProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowSVGHelpers');
645
+ this._GeometryProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowGeometry');
646
+ this._PathGenerator = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowPathGenerator', { FlowView: this });
647
+ this._PanelChromeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowPanelChrome', { FlowView: this });
648
+
649
+ // Instantiate services
340
650
  this._InteractionManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowInteractionManager', { FlowView: this });
341
651
  this._ConnectionRenderer = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowConnectionRenderer', { FlowView: this });
652
+ this._TetherService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowTether', { FlowView: this });
342
653
  this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
343
654
 
344
- // Register providers
345
- this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this });
655
+ // Instantiate providers, passing any additional node types from view options
656
+ this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this, AdditionalNodeTypes: this.options.NodeTypes });
346
657
  this._EventHandlerProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowEventHandler', { FlowView: this });
658
+ this._LayoutProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowLayouts', { FlowView: this });
347
659
 
348
660
  return super.onBeforeInitialize();
349
661
  }
@@ -389,6 +701,36 @@ class PictViewFlow extends libPictView
389
701
  this._ConnectionsLayer = tmpConnectionsElements[0];
390
702
  }
391
703
 
704
+ let tmpTethersElements = this.pict.ContentAssignment.getElement(`#Flow-Tethers-${tmpViewIdentifier}`);
705
+ if (tmpTethersElements.length > 0)
706
+ {
707
+ this._TethersLayer = tmpTethersElements[0];
708
+ }
709
+
710
+ let tmpPanelsElements = this.pict.ContentAssignment.getElement(`#Flow-Panels-${tmpViewIdentifier}`);
711
+ if (tmpPanelsElements.length > 0)
712
+ {
713
+ this._PanelsLayer = tmpPanelsElements[0];
714
+ }
715
+
716
+ // Initialize shared utility providers (used by services below)
717
+ if (!this._SVGHelperProvider)
718
+ {
719
+ this._SVGHelperProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowSVGHelpers');
720
+ }
721
+ if (!this._GeometryProvider)
722
+ {
723
+ this._GeometryProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowGeometry');
724
+ }
725
+ if (!this._PathGenerator)
726
+ {
727
+ this._PathGenerator = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowPathGenerator', { FlowView: this });
728
+ }
729
+ if (!this._PanelChromeProvider)
730
+ {
731
+ this._PanelChromeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowPanelChrome', { FlowView: this });
732
+ }
733
+
392
734
  // Initialize services with references
393
735
  if (!this._InteractionManager)
394
736
  {
@@ -398,18 +740,26 @@ class PictViewFlow extends libPictView
398
740
  {
399
741
  this._ConnectionRenderer = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowConnectionRenderer', { FlowView: this });
400
742
  }
743
+ if (!this._TetherService)
744
+ {
745
+ this._TetherService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowTether', { FlowView: this });
746
+ }
401
747
  if (!this._LayoutService)
402
748
  {
403
749
  this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
404
750
  }
405
751
  if (!this._NodeTypeProvider)
406
752
  {
407
- this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this });
753
+ this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this, AdditionalNodeTypes: this.options.NodeTypes });
408
754
  }
409
755
  if (!this._EventHandlerProvider)
410
756
  {
411
757
  this._EventHandlerProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowEventHandler', { FlowView: this });
412
758
  }
759
+ if (!this._LayoutProvider)
760
+ {
761
+ this._LayoutProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowLayouts', { FlowView: this });
762
+ }
413
763
 
414
764
  // Setup the toolbar if enabled
415
765
  if (this.options.EnableToolbar)
@@ -442,6 +792,17 @@ class PictViewFlow extends libPictView
442
792
  ));
443
793
  this._NodeView._FlowView = this;
444
794
 
795
+ // Setup the properties panel renderer
796
+ this._PropertiesPanelView = this.fable.instantiateServiceProviderWithoutRegistration('PictViewFlowPropertiesPanel',
797
+ Object.assign({},
798
+ libPictViewFlowPropertiesPanel.default_configuration,
799
+ {
800
+ ViewIdentifier: `Flow-PropertiesPanel-${tmpViewIdentifier}`,
801
+ AutoRender: false
802
+ }
803
+ ));
804
+ this._PropertiesPanelView._FlowView = this;
805
+
445
806
  // Bind interaction events
446
807
  this._InteractionManager.initialize(this._SVGElement, this._ViewportElement);
447
808
 
@@ -521,8 +882,10 @@ class PictViewFlow extends libPictView
521
882
  this._FlowData = {
522
883
  Nodes: Array.isArray(pFlowData.Nodes) ? pFlowData.Nodes : [],
523
884
  Connections: Array.isArray(pFlowData.Connections) ? pFlowData.Connections : [],
885
+ OpenPanels: Array.isArray(pFlowData.OpenPanels) ? pFlowData.OpenPanels : [],
886
+ SavedLayouts: Array.isArray(pFlowData.SavedLayouts) ? pFlowData.SavedLayouts : [],
524
887
  ViewState: Object.assign(
525
- { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null },
888
+ { PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null, SelectedTetherHash: null },
526
889
  pFlowData.ViewState || {}
527
890
  )
528
891
  };
@@ -610,6 +973,9 @@ class PictViewFlow extends libPictView
610
973
  return pConnection.SourceNodeHash !== pNodeHash && pConnection.TargetNodeHash !== pNodeHash;
611
974
  });
612
975
 
976
+ // Close any open panels for this node
977
+ this.closePanelForNode(pNodeHash);
978
+
613
979
  // Clear selection if this node was selected
614
980
  if (this._FlowData.ViewState.SelectedNodeHash === pNodeHash)
615
981
  {
@@ -744,6 +1110,7 @@ class PictViewFlow extends libPictView
744
1110
  let tmpPreviousSelection = this._FlowData.ViewState.SelectedNodeHash;
745
1111
  this._FlowData.ViewState.SelectedNodeHash = pNodeHash;
746
1112
  this._FlowData.ViewState.SelectedConnectionHash = null;
1113
+ this._FlowData.ViewState.SelectedTetherHash = null;
747
1114
 
748
1115
  this.renderFlow();
749
1116
 
@@ -763,6 +1130,7 @@ class PictViewFlow extends libPictView
763
1130
  let tmpPreviousSelection = this._FlowData.ViewState.SelectedConnectionHash;
764
1131
  this._FlowData.ViewState.SelectedConnectionHash = pConnectionHash;
765
1132
  this._FlowData.ViewState.SelectedNodeHash = null;
1133
+ this._FlowData.ViewState.SelectedTetherHash = null;
766
1134
 
767
1135
  this.renderFlow();
768
1136
 
@@ -780,6 +1148,7 @@ class PictViewFlow extends libPictView
780
1148
  {
781
1149
  this._FlowData.ViewState.SelectedNodeHash = null;
782
1150
  this._FlowData.ViewState.SelectedConnectionHash = null;
1151
+ this._FlowData.ViewState.SelectedTetherHash = null;
783
1152
  this.renderFlow();
784
1153
  }
785
1154
 
@@ -893,6 +1262,50 @@ class PictViewFlow extends libPictView
893
1262
  }
894
1263
  }
895
1264
 
1265
+ /**
1266
+ * Toggle fullscreen mode on the flow editor container.
1267
+ * Uses a CSS fixed-position overlay instead of the Fullscreen API.
1268
+ * @returns {boolean} The new fullscreen state
1269
+ */
1270
+ toggleFullscreen()
1271
+ {
1272
+ let tmpViewIdentifier = this.options.ViewIdentifier;
1273
+ let tmpContainerElements = this.pict.ContentAssignment.getElement(`#Flow-Wrapper-${tmpViewIdentifier}`);
1274
+ if (tmpContainerElements.length < 1) return this._IsFullscreen;
1275
+
1276
+ let tmpContainer = tmpContainerElements[0];
1277
+
1278
+ this._IsFullscreen = !this._IsFullscreen;
1279
+
1280
+ if (this._IsFullscreen)
1281
+ {
1282
+ tmpContainer.classList.add('pict-flow-fullscreen');
1283
+ }
1284
+ else
1285
+ {
1286
+ tmpContainer.classList.remove('pict-flow-fullscreen');
1287
+ }
1288
+
1289
+ return this._IsFullscreen;
1290
+ }
1291
+
1292
+ /**
1293
+ * Exit fullscreen mode if currently active.
1294
+ */
1295
+ exitFullscreen()
1296
+ {
1297
+ if (!this._IsFullscreen) return;
1298
+
1299
+ let tmpViewIdentifier = this.options.ViewIdentifier;
1300
+ let tmpContainerElements = this.pict.ContentAssignment.getElement(`#Flow-Wrapper-${tmpViewIdentifier}`);
1301
+ if (tmpContainerElements.length > 0)
1302
+ {
1303
+ tmpContainerElements[0].classList.remove('pict-flow-fullscreen');
1304
+ }
1305
+
1306
+ this._IsFullscreen = false;
1307
+ }
1308
+
896
1309
  /**
897
1310
  * Get a node by hash
898
1311
  * @param {string} pNodeHash
@@ -913,6 +1326,207 @@ class PictViewFlow extends libPictView
913
1326
  return this._FlowData.Connections.find((pConn) => pConn.Hash === pConnectionHash) || null;
914
1327
  }
915
1328
 
1329
+ /**
1330
+ * Select a tether by its panel hash.
1331
+ * @param {string|null} pPanelHash - Hash of the panel whose tether to select, or null to deselect
1332
+ */
1333
+ selectTether(pPanelHash)
1334
+ {
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
+ }
1347
+ }
1348
+
1349
+ /**
1350
+ * Update a connection handle position during drag (for real-time feedback).
1351
+ * @param {string} pConnectionHash
1352
+ * @param {string} pHandleType - 'bezier-midpoint', 'ortho-corner1', 'ortho-corner2', 'ortho-midpoint'
1353
+ * @param {number} pX
1354
+ * @param {number} pY
1355
+ */
1356
+ updateConnectionHandle(pConnectionHash, pHandleType, pX, pY)
1357
+ {
1358
+ let tmpConnection = this.getConnection(pConnectionHash);
1359
+ if (!tmpConnection) return;
1360
+
1361
+ if (!tmpConnection.Data) tmpConnection.Data = {};
1362
+ tmpConnection.Data.HandleCustomized = true;
1363
+
1364
+ switch (pHandleType)
1365
+ {
1366
+ case 'bezier-midpoint':
1367
+ tmpConnection.Data.BezierHandleX = pX;
1368
+ tmpConnection.Data.BezierHandleY = pY;
1369
+ break;
1370
+
1371
+ case 'ortho-corner1':
1372
+ tmpConnection.Data.OrthoCorner1X = pX;
1373
+ tmpConnection.Data.OrthoCorner1Y = pY;
1374
+ break;
1375
+
1376
+ case 'ortho-corner2':
1377
+ tmpConnection.Data.OrthoCorner2X = pX;
1378
+ tmpConnection.Data.OrthoCorner2Y = pY;
1379
+ break;
1380
+
1381
+ case 'ortho-midpoint':
1382
+ {
1383
+ // Midpoint drag shifts the corridor offset
1384
+ let tmpSourcePos = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
1385
+ let tmpTargetPos = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
1386
+ if (tmpSourcePos && tmpTargetPos)
1387
+ {
1388
+ let tmpGeom = this._ConnectionRenderer._computeDirectionalGeometry(tmpSourcePos, tmpTargetPos);
1389
+ let tmpStartDir = tmpGeom.startDir;
1390
+
1391
+ // Compute offset along the corridor axis
1392
+ if (Math.abs(tmpStartDir.dx) > Math.abs(tmpStartDir.dy))
1393
+ {
1394
+ // Horizontal departure — corridor is vertical, shift is along X
1395
+ let tmpAutoMidX = (tmpGeom.departX + tmpGeom.approachX) / 2;
1396
+ tmpConnection.Data.OrthoMidOffset = pX - tmpAutoMidX;
1397
+ }
1398
+ else
1399
+ {
1400
+ // Vertical departure — corridor is horizontal, shift is along Y
1401
+ let tmpAutoMidY = (tmpGeom.departY + tmpGeom.approachY) / 2;
1402
+ tmpConnection.Data.OrthoMidOffset = pY - tmpAutoMidY;
1403
+ }
1404
+ }
1405
+ break;
1406
+ }
1407
+ }
1408
+
1409
+ this._renderSingleConnection(pConnectionHash);
1410
+ }
1411
+
1412
+ /**
1413
+ * Update a tether handle position during drag (for real-time feedback).
1414
+ * Delegates state update to the TetherService.
1415
+ * @param {string} pPanelHash
1416
+ * @param {string} pHandleType - 'bezier-midpoint', 'ortho-corner1', 'ortho-corner2', 'ortho-midpoint'
1417
+ * @param {number} pX
1418
+ * @param {number} pY
1419
+ */
1420
+ updateTetherHandle(pPanelHash, pHandleType, pX, pY)
1421
+ {
1422
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
1423
+ if (!tmpPanel) return;
1424
+
1425
+ if (this._TetherService)
1426
+ {
1427
+ this._TetherService.updateHandlePosition(tmpPanel, pHandleType, pX, pY);
1428
+ }
1429
+
1430
+ this._renderSingleTether(pPanelHash);
1431
+ }
1432
+
1433
+ /**
1434
+ * Re-render a single connection (remove and re-add) for smooth drag performance.
1435
+ * @param {string} pConnectionHash
1436
+ */
1437
+ _renderSingleConnection(pConnectionHash)
1438
+ {
1439
+ if (!this._ConnectionsLayer) return;
1440
+
1441
+ // Remove existing elements for this connection
1442
+ let tmpExisting = this._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${pConnectionHash}"]`);
1443
+ for (let i = 0; i < tmpExisting.length; i++)
1444
+ {
1445
+ tmpExisting[i].remove();
1446
+ }
1447
+
1448
+ let tmpConnection = this.getConnection(pConnectionHash);
1449
+ if (!tmpConnection) return;
1450
+
1451
+ let tmpIsSelected = (this._FlowData.ViewState.SelectedConnectionHash === pConnectionHash);
1452
+ this._ConnectionRenderer.renderConnection(tmpConnection, this._ConnectionsLayer, tmpIsSelected);
1453
+ }
1454
+
1455
+ /**
1456
+ * Re-render a single tether (remove and re-add) for smooth drag performance.
1457
+ * @param {string} pPanelHash
1458
+ */
1459
+ _renderSingleTether(pPanelHash)
1460
+ {
1461
+ if (!this._TethersLayer || !this._TetherService) return;
1462
+
1463
+ // Remove existing tether elements for this panel
1464
+ let tmpExisting = this._TethersLayer.querySelectorAll(`[data-panel-hash="${pPanelHash}"]`);
1465
+ for (let i = 0; i < tmpExisting.length; i++)
1466
+ {
1467
+ tmpExisting[i].remove();
1468
+ }
1469
+
1470
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
1471
+ if (!tmpPanel) return;
1472
+
1473
+ let tmpNodeData = this.getNode(tmpPanel.NodeHash);
1474
+ if (!tmpNodeData) return;
1475
+
1476
+ let tmpIsSelected = (this._FlowData.ViewState.SelectedTetherHash === pPanelHash);
1477
+ this._TetherService.renderTether(tmpPanel, tmpNodeData, this._TethersLayer, tmpIsSelected, this.options.ViewIdentifier);
1478
+ }
1479
+
1480
+ /**
1481
+ * Reset handle positions for all connections/tethers involving a node.
1482
+ * Called when a node moves. Preserves LineMode but resets handle coordinates to auto.
1483
+ * @param {string} pNodeHash
1484
+ */
1485
+ _resetHandlesForNode(pNodeHash)
1486
+ {
1487
+ // Reset connection handles
1488
+ for (let i = 0; i < this._FlowData.Connections.length; i++)
1489
+ {
1490
+ let tmpConn = this._FlowData.Connections[i];
1491
+ if (tmpConn.SourceNodeHash === pNodeHash || tmpConn.TargetNodeHash === pNodeHash)
1492
+ {
1493
+ if (tmpConn.Data && tmpConn.Data.HandleCustomized)
1494
+ {
1495
+ tmpConn.Data.HandleCustomized = false;
1496
+ tmpConn.Data.BezierHandleX = null;
1497
+ tmpConn.Data.BezierHandleY = null;
1498
+ tmpConn.Data.OrthoCorner1X = null;
1499
+ tmpConn.Data.OrthoCorner1Y = null;
1500
+ tmpConn.Data.OrthoCorner2X = null;
1501
+ tmpConn.Data.OrthoCorner2Y = null;
1502
+ tmpConn.Data.OrthoMidOffset = 0;
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ // Reset tether handles for panels attached to this node
1508
+ if (this._TetherService)
1509
+ {
1510
+ this._TetherService.resetHandlesForNode(this._FlowData.OpenPanels, pNodeHash);
1511
+ }
1512
+ }
1513
+
1514
+ /**
1515
+ * Reset tether handle positions for a specific panel.
1516
+ * Called when a panel is dragged.
1517
+ * @param {string} pPanelHash
1518
+ */
1519
+ _resetHandlesForPanel(pPanelHash)
1520
+ {
1521
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
1522
+ if (!tmpPanel) return;
1523
+
1524
+ if (this._TetherService)
1525
+ {
1526
+ this._TetherService.resetHandlePositions(tmpPanel);
1527
+ }
1528
+ }
1529
+
916
1530
  /**
917
1531
  * Get a port's absolute position in SVG coordinates.
918
1532
  *
@@ -1047,6 +1661,12 @@ class PictViewFlow extends libPictView
1047
1661
  this._NodeView.renderNode(tmpNode, this._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
1048
1662
  }
1049
1663
 
1664
+ // Render properties panels and tethers
1665
+ if (this._PropertiesPanelView && this._PanelsLayer && this._TethersLayer)
1666
+ {
1667
+ this._PropertiesPanelView.renderPanels(this._FlowData.OpenPanels, this._PanelsLayer, this._TethersLayer, this._FlowData.ViewState.SelectedTetherHash);
1668
+ }
1669
+
1050
1670
  // Update viewport transform
1051
1671
  this.updateViewportTransform();
1052
1672
  }
@@ -1071,6 +1691,9 @@ class PictViewFlow extends libPictView
1071
1691
  tmpNode.X = pX;
1072
1692
  tmpNode.Y = pY;
1073
1693
 
1694
+ // Reset customized handle positions for connections/tethers involving this node
1695
+ this._resetHandlesForNode(pNodeHash);
1696
+
1074
1697
  // Update the node's SVG group transform for smooth dragging
1075
1698
  let tmpNodeGroup = this._NodesLayer.querySelector(`[data-node-hash="${pNodeHash}"]`);
1076
1699
  if (tmpNodeGroup)
@@ -1080,6 +1703,9 @@ class PictViewFlow extends libPictView
1080
1703
 
1081
1704
  // Re-render connections that involve this node
1082
1705
  this._renderConnectionsForNode(pNodeHash);
1706
+
1707
+ // Update tethers for any panels attached to this node
1708
+ this._renderTethersForNode(pNodeHash);
1083
1709
  }
1084
1710
 
1085
1711
  /**
@@ -1109,6 +1735,202 @@ class PictViewFlow extends libPictView
1109
1735
  this._ConnectionRenderer.renderConnection(tmpConn, this._ConnectionsLayer, tmpIsSelected);
1110
1736
  }
1111
1737
  }
1738
+
1739
+ /**
1740
+ * Re-render tethers for panels attached to a specific node (for drag performance).
1741
+ * @param {string} pNodeHash
1742
+ */
1743
+ _renderTethersForNode(pNodeHash)
1744
+ {
1745
+ if (!this._TethersLayer || !this._TetherService) return;
1746
+
1747
+ let tmpAffectedPanels = this._FlowData.OpenPanels.filter((pPanel) => pPanel.NodeHash === pNodeHash);
1748
+ if (tmpAffectedPanels.length === 0) return;
1749
+
1750
+ // Remove existing tethers for these panels and re-render via TetherService
1751
+ for (let i = 0; i < tmpAffectedPanels.length; i++)
1752
+ {
1753
+ let tmpExisting = this._TethersLayer.querySelectorAll(`[data-panel-hash="${tmpAffectedPanels[i].Hash}"]`);
1754
+ for (let j = 0; j < tmpExisting.length; j++)
1755
+ {
1756
+ tmpExisting[j].remove();
1757
+ }
1758
+
1759
+ let tmpNodeData = this.getNode(tmpAffectedPanels[i].NodeHash);
1760
+ if (!tmpNodeData) continue;
1761
+
1762
+ let tmpIsSelected = (this._FlowData.ViewState.SelectedTetherHash === tmpAffectedPanels[i].Hash);
1763
+ this._TetherService.renderTether(tmpAffectedPanels[i], tmpNodeData, this._TethersLayer, tmpIsSelected, this.options.ViewIdentifier);
1764
+ }
1765
+ }
1766
+
1767
+ // ---- Properties Panel Management ----
1768
+
1769
+ /**
1770
+ * Open a properties panel for a node.
1771
+ * @param {string} pNodeHash - The hash of the node to open a panel for
1772
+ * @returns {Object|false} The panel data, or false if the node has no PropertiesPanel config
1773
+ */
1774
+ openPanel(pNodeHash)
1775
+ {
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;
1829
+ }
1830
+
1831
+ /**
1832
+ * Close a properties panel by panel hash.
1833
+ * @param {string} pPanelHash
1834
+ * @returns {boolean}
1835
+ */
1836
+ closePanel(pPanelHash)
1837
+ {
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;
1859
+ }
1860
+
1861
+ /**
1862
+ * Close all panels for a given node.
1863
+ * @param {string} pNodeHash
1864
+ * @returns {boolean}
1865
+ */
1866
+ closePanelForNode(pNodeHash)
1867
+ {
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;
1885
+ }
1886
+
1887
+ /**
1888
+ * Toggle a properties panel for a node (open if closed, close if open).
1889
+ * @param {string} pNodeHash
1890
+ * @returns {Object|false}
1891
+ */
1892
+ togglePanel(pNodeHash)
1893
+ {
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);
1901
+ }
1902
+
1903
+ /**
1904
+ * Update a panel's position (for drag).
1905
+ * @param {string} pPanelHash
1906
+ * @param {number} pX
1907
+ * @param {number} pY
1908
+ */
1909
+ updatePanelPosition(pPanelHash, pX, pY)
1910
+ {
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);
1933
+ }
1112
1934
  }
1113
1935
 
1114
1936
  module.exports = PictViewFlow;