pict-section-flow 0.0.13 → 0.0.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-flow",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -223,6 +223,9 @@ class PictServiceFlowConnectionHandleManager extends libFableServiceProviderBase
223
223
  if (tmpConn.Data && tmpConn.Data.HandleCustomized)
224
224
  {
225
225
  tmpConn.Data.HandleCustomized = false;
226
+ // Clear multi-handle array (current format)
227
+ tmpConn.Data.BezierHandles = [];
228
+ // Clear legacy single-handle fields
226
229
  tmpConn.Data.BezierHandleX = null;
227
230
  tmpConn.Data.BezierHandleY = null;
228
231
  tmpConn.Data.OrthoCorner1X = null;
@@ -75,6 +75,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
75
75
  this._LastConnectionClickTime = 0;
76
76
  this._LastConnectionClickHash = null;
77
77
 
78
+ // Double-click detection for tethers
79
+ this._LastTetherClickTime = 0;
80
+ this._LastTetherClickHash = null;
81
+
78
82
  // Double-click detection for handles
79
83
  this._LastHandleClickTime = 0;
80
84
  this._LastHandleClickHash = null;
@@ -128,6 +132,15 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
128
132
  case 'connection-handle':
129
133
  this._removeBezierHandle(tmpTarget);
130
134
  break;
135
+
136
+ case 'tether':
137
+ case 'tether-hitarea':
138
+ this._addTetherBezierHandle(tmpTarget, pEvent);
139
+ break;
140
+
141
+ case 'tether-handle':
142
+ this._removeTetherBezierHandle(tmpTarget);
143
+ break;
131
144
  }
132
145
  });
133
146
  }
@@ -252,8 +265,26 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
252
265
 
253
266
  case 'tether':
254
267
  case 'tether-hitarea':
255
- this._selectTether(tmpTarget);
268
+ {
269
+ let tmpPanelHash = this._getPanelHash(tmpTarget);
270
+ let tmpNow = Date.now();
271
+
272
+ // Check for double-click on same tether to add a handle
273
+ if (tmpPanelHash && tmpPanelHash === this._LastTetherClickHash
274
+ && (tmpNow - this._LastTetherClickTime) < this._DoubleClickThreshold)
275
+ {
276
+ this._LastTetherClickTime = 0;
277
+ this._LastTetherClickHash = null;
278
+ this._addTetherBezierHandle(tmpTarget, pEvent);
279
+ }
280
+ else
281
+ {
282
+ this._LastTetherClickTime = tmpNow;
283
+ this._LastTetherClickHash = tmpPanelHash;
284
+ this._selectTether(tmpTarget);
285
+ }
256
286
  break;
287
+ }
257
288
 
258
289
  case 'tether-handle':
259
290
  {
@@ -758,6 +789,41 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
758
789
  this._FlowView.removeConnectionHandle(tmpConnectionHash, tmpIndex);
759
790
  }
760
791
 
792
+ /**
793
+ * Add a bezier handle to a tether.
794
+ * @param {Element} pTarget - The tether SVG element that was right-clicked or double-clicked
795
+ * @param {Event} pEvent - The mouse event (for coordinate extraction)
796
+ */
797
+ _addTetherBezierHandle(pTarget, pEvent)
798
+ {
799
+ let tmpPanelHash = this._getPanelHash(pTarget);
800
+ if (!tmpPanelHash) return;
801
+
802
+ // Select the tether so handles render after re-render
803
+ this._FlowView.selectTether(tmpPanelHash);
804
+
805
+ let tmpCoords = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
806
+ this._FlowView.addTetherHandle(tmpPanelHash, tmpCoords.x, tmpCoords.y);
807
+ }
808
+
809
+ /**
810
+ * Remove a bezier handle from a tether.
811
+ * @param {Element} pTarget - The tether handle SVG element that was right-clicked
812
+ */
813
+ _removeTetherBezierHandle(pTarget)
814
+ {
815
+ let tmpPanelHash = this._getPanelHash(pTarget);
816
+ if (!tmpPanelHash) return;
817
+
818
+ let tmpHandleType = pTarget.getAttribute('data-handle-type');
819
+ if (!tmpHandleType || !tmpHandleType.startsWith('bezier-handle-')) return;
820
+
821
+ let tmpIndex = parseInt(tmpHandleType.replace('bezier-handle-', ''), 10);
822
+ if (isNaN(tmpIndex)) return;
823
+
824
+ this._FlowView.removeTetherHandle(tmpPanelHash, tmpIndex);
825
+ }
826
+
761
827
  // ---- Line Mode Toggling ----
762
828
 
763
829
  _toggleConnectionLineMode(pConnectionHash)
@@ -298,12 +298,74 @@ class PictServiceFlowTether extends libFableServiceProviderBase
298
298
  }
299
299
  else
300
300
  {
301
+ // Check for multi-handle array first
302
+ let tmpHandles = this._getTetherBezierHandles(pPanelData);
303
+ if (tmpHandles.length > 0)
304
+ {
305
+ return this.generateMultiBezierPath(pFrom, pTo, tmpHandles);
306
+ }
307
+
308
+ // Single-handle legacy path
301
309
  let tmpHandleX = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null) ? pPanelData.TetherBezierHandleX : null;
302
310
  let tmpHandleY = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleY != null) ? pPanelData.TetherBezierHandleY : null;
303
311
  return this.generateBezierPath(pFrom, pTo, tmpHandleX, tmpHandleY);
304
312
  }
305
313
  }
306
314
 
315
+ /**
316
+ * Get the bezier handles array for a tether, respecting the customized flag.
317
+ * @param {Object} pPanelData
318
+ * @returns {Array<{x: number, y: number}>}
319
+ */
320
+ _getTetherBezierHandles(pPanelData)
321
+ {
322
+ if (!pPanelData || !pPanelData.TetherHandleCustomized)
323
+ {
324
+ return [];
325
+ }
326
+
327
+ // Multi-handle format
328
+ if (Array.isArray(pPanelData.TetherBezierHandles) && pPanelData.TetherBezierHandles.length > 0)
329
+ {
330
+ return pPanelData.TetherBezierHandles;
331
+ }
332
+
333
+ // Legacy single-handle format
334
+ if (pPanelData.TetherBezierHandleX != null && pPanelData.TetherBezierHandleY != null)
335
+ {
336
+ return [{ x: pPanelData.TetherBezierHandleX, y: pPanelData.TetherBezierHandleY }];
337
+ }
338
+
339
+ return [];
340
+ }
341
+
342
+ /**
343
+ * Generate a multi-handle bezier path for a tether.
344
+ * Delegates to PathGenerator.buildMultiBezierPathString.
345
+ * @param {Object} pFrom - {x, y, side}
346
+ * @param {Object} pTo - {x, y, side}
347
+ * @param {Array<{x: number, y: number}>} pHandles
348
+ * @returns {string} SVG path d attribute
349
+ */
350
+ generateMultiBezierPath(pFrom, pTo, pHandles)
351
+ {
352
+ let tmpDepartDist = 20;
353
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
354
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
355
+
356
+ let tmpDepart = {
357
+ x: pFrom.x + tmpFromDir.dx * tmpDepartDist,
358
+ y: pFrom.y + tmpFromDir.dy * tmpDepartDist
359
+ };
360
+ let tmpApproach = {
361
+ x: pTo.x + tmpToDir.dx * tmpDepartDist,
362
+ y: pTo.y + tmpToDir.dy * tmpDepartDist
363
+ };
364
+
365
+ return this._FlowView._PathGenerator.buildMultiBezierPathString(
366
+ pFrom, tmpDepart, pFrom.side, tmpApproach, pTo.side, pTo, pHandles);
367
+ }
368
+
307
369
  // ---- Handle State Management ----
308
370
 
309
371
  /**
@@ -317,9 +379,34 @@ class PictServiceFlowTether extends libFableServiceProviderBase
317
379
  {
318
380
  pPanelData.TetherHandleCustomized = true;
319
381
 
382
+ // Multi-handle bezier: handle type is 'bezier-handle-N'
383
+ if (pHandleType && pHandleType.startsWith('bezier-handle-'))
384
+ {
385
+ let tmpIndex = parseInt(pHandleType.replace('bezier-handle-', ''), 10);
386
+ if (!isNaN(tmpIndex) && Array.isArray(pPanelData.TetherBezierHandles)
387
+ && tmpIndex < pPanelData.TetherBezierHandles.length)
388
+ {
389
+ pPanelData.TetherBezierHandles[tmpIndex].x = pX;
390
+ pPanelData.TetherBezierHandles[tmpIndex].y = pY;
391
+ }
392
+ return;
393
+ }
394
+
320
395
  switch (pHandleType)
321
396
  {
322
397
  case 'bezier-midpoint':
398
+ // Migrate to multi-handle array
399
+ if (!Array.isArray(pPanelData.TetherBezierHandles)
400
+ || pPanelData.TetherBezierHandles.length === 0)
401
+ {
402
+ pPanelData.TetherBezierHandles = [{ x: pX, y: pY }];
403
+ }
404
+ else
405
+ {
406
+ pPanelData.TetherBezierHandles[0].x = pX;
407
+ pPanelData.TetherBezierHandles[0].y = pY;
408
+ }
409
+ // Keep legacy fields in sync
323
410
  pPanelData.TetherBezierHandleX = pX;
324
411
  pPanelData.TetherBezierHandleY = pY;
325
412
  break;
@@ -353,6 +440,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
353
440
  if (pPanelData.TetherHandleCustomized)
354
441
  {
355
442
  pPanelData.TetherHandleCustomized = false;
443
+ pPanelData.TetherBezierHandles = [];
356
444
  pPanelData.TetherBezierHandleX = null;
357
445
  pPanelData.TetherBezierHandleY = null;
358
446
  pPanelData.TetherOrthoCorner1X = null;
@@ -381,6 +469,68 @@ class PictServiceFlowTether extends libFableServiceProviderBase
381
469
  }
382
470
  }
383
471
 
472
+ /**
473
+ * Add a bezier handle to a tether at the specified position.
474
+ * @param {Object} pPanelData - Panel data
475
+ * @param {number} pX
476
+ * @param {number} pY
477
+ * @param {Object} pFrom - Panel anchor {x, y, side}
478
+ * @param {Object} pTo - Node anchor {x, y, side}
479
+ */
480
+ addHandle(pPanelData, pX, pY, pFrom, pTo)
481
+ {
482
+ // Ensure bezier mode and multi-handle array
483
+ pPanelData.TetherLineMode = 'bezier';
484
+
485
+ if (!Array.isArray(pPanelData.TetherBezierHandles))
486
+ {
487
+ pPanelData.TetherBezierHandles = [];
488
+ // Migrate legacy single-handle if present
489
+ if (pPanelData.TetherBezierHandleX != null && pPanelData.TetherBezierHandleY != null)
490
+ {
491
+ pPanelData.TetherBezierHandles.push({
492
+ x: pPanelData.TetherBezierHandleX,
493
+ y: pPanelData.TetherBezierHandleY
494
+ });
495
+ }
496
+ }
497
+
498
+ // Compute insertion index
499
+ let tmpInsertIndex = 0;
500
+ if (this._FlowView._ConnectionRenderer && pFrom && pTo)
501
+ {
502
+ tmpInsertIndex = this._FlowView._ConnectionRenderer.computeInsertionIndex(
503
+ pPanelData.TetherBezierHandles,
504
+ { x: pX, y: pY },
505
+ pFrom,
506
+ pTo
507
+ );
508
+ }
509
+
510
+ pPanelData.TetherBezierHandles.splice(tmpInsertIndex, 0, { x: pX, y: pY });
511
+ pPanelData.TetherHandleCustomized = true;
512
+ }
513
+
514
+ /**
515
+ * Remove a bezier handle from a tether by index.
516
+ * @param {Object} pPanelData - Panel data
517
+ * @param {number} pIndex - Index in TetherBezierHandles array
518
+ */
519
+ removeHandle(pPanelData, pIndex)
520
+ {
521
+ if (!Array.isArray(pPanelData.TetherBezierHandles)) return;
522
+ if (pIndex < 0 || pIndex >= pPanelData.TetherBezierHandles.length) return;
523
+
524
+ pPanelData.TetherBezierHandles.splice(pIndex, 1);
525
+
526
+ if (pPanelData.TetherBezierHandles.length === 0)
527
+ {
528
+ pPanelData.TetherHandleCustomized = false;
529
+ pPanelData.TetherBezierHandleX = null;
530
+ pPanelData.TetherBezierHandleY = null;
531
+ }
532
+ }
533
+
384
534
  /**
385
535
  * Toggle tether line mode between bezier and orthogonal.
386
536
  * Resets handle positions on toggle.
@@ -393,6 +543,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
393
543
  pPanelData.TetherLineMode = (tmpCurrentMode === 'bezier') ? 'orthogonal' : 'bezier';
394
544
 
395
545
  pPanelData.TetherHandleCustomized = false;
546
+ pPanelData.TetherBezierHandles = [];
396
547
  pPanelData.TetherBezierHandleX = null;
397
548
  pPanelData.TetherBezierHandleY = null;
398
549
  pPanelData.TetherOrthoCorner1X = null;
@@ -491,22 +642,25 @@ class PictServiceFlowTether extends libFableServiceProviderBase
491
642
  }
492
643
  else
493
644
  {
494
- // Bezier: single midpoint handle
495
- let tmpMidX, tmpMidY;
496
- if (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null)
645
+ // Bezier handles
646
+ let tmpHandles = this._getTetherBezierHandles(pPanelData);
647
+
648
+ if (tmpHandles.length > 0)
497
649
  {
498
- tmpMidX = pPanelData.TetherBezierHandleX;
499
- tmpMidY = pPanelData.TetherBezierHandleY;
650
+ // Multi-handle: render each handle as a draggable circle
651
+ for (let i = 0; i < tmpHandles.length; i++)
652
+ {
653
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-handle-' + i,
654
+ tmpHandles[i].x, tmpHandles[i].y, 'pict-flow-tether-handle');
655
+ }
500
656
  }
501
657
  else
502
658
  {
659
+ // No custom handles: show auto-computed midpoint
503
660
  let tmpMid = this.getAutoMidpoint(pFrom, pTo);
504
- tmpMidX = tmpMid.x;
505
- tmpMidY = tmpMid.y;
661
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-midpoint',
662
+ tmpMid.x, tmpMid.y, 'pict-flow-tether-handle-midpoint');
506
663
  }
507
-
508
- this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-midpoint',
509
- tmpMidX, tmpMidY, 'pict-flow-tether-handle-midpoint');
510
664
  }
511
665
  }
512
666
 
@@ -689,6 +689,54 @@ class PictViewFlow extends libPictView
689
689
  _resetHandlesForNode(pNodeHash) { return this._ConnectionHandleManager.resetHandlesForNode(pNodeHash); }
690
690
  _resetHandlesForPanel(pPanelHash) { return this._ConnectionHandleManager.resetHandlesForPanel(pPanelHash); }
691
691
 
692
+ /**
693
+ * Add a bezier handle to a tether at the specified SVG position.
694
+ * @param {string} pPanelHash
695
+ * @param {number} pX
696
+ * @param {number} pY
697
+ */
698
+ addTetherHandle(pPanelHash, pX, pY)
699
+ {
700
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
701
+ if (!tmpPanel || !this._TetherService) return;
702
+
703
+ let tmpNode = this.getNode(tmpPanel.NodeHash);
704
+ if (!tmpNode) return;
705
+
706
+ let tmpAnchors = this._TetherService.getSmartAnchors(tmpPanel, tmpNode);
707
+
708
+ this._TetherService.addHandle(tmpPanel, pX, pY, tmpAnchors.panelAnchor, tmpAnchors.nodeAnchor);
709
+
710
+ this.renderFlow();
711
+ this.marshalFromView();
712
+
713
+ if (this._EventHandlerProvider)
714
+ {
715
+ this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Remove a bezier handle from a tether by index.
721
+ * @param {string} pPanelHash
722
+ * @param {number} pIndex
723
+ */
724
+ removeTetherHandle(pPanelHash, pIndex)
725
+ {
726
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
727
+ if (!tmpPanel || !this._TetherService) return;
728
+
729
+ this._TetherService.removeHandle(tmpPanel, pIndex);
730
+
731
+ this.renderFlow();
732
+ this.marshalFromView();
733
+
734
+ if (this._EventHandlerProvider)
735
+ {
736
+ this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
737
+ }
738
+ }
739
+
692
740
  /**
693
741
  * Update a tether handle position during drag (for real-time feedback).
694
742
  * Delegates state update to the TetherService.