@ngroznykh/papirus 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/papirus.js CHANGED
@@ -1047,19 +1047,21 @@ var DragManager = class extends EventEmitter {
1047
1047
  const nodeBindings = /* @__PURE__ */ new Map();
1048
1048
  for (const edge of this.renderer.edges.values()) {
1049
1049
  if (!edge.hasEditableControlPoints()) continue;
1050
- const isFromDragged = draggedIds.has(edge.from.nodeId);
1051
- const isToDragged = draggedIds.has(edge.to.nodeId);
1050
+ const fromId = edge.from.nodeId;
1051
+ const toId = edge.to.nodeId;
1052
+ const isFromDragged = !!fromId && draggedIds.has(fromId);
1053
+ const isToDragged = !!toId && draggedIds.has(toId);
1052
1054
  if (isFromDragged && isToDragged) {
1053
1055
  this.editablePolylineFullyConnectedEdges.add(edge.id);
1054
1056
  this.initialControlPointsForFullyConnected.set(edge.id, clonePoints(edge.controlPoints));
1055
1057
  continue;
1056
1058
  }
1057
1059
  for (const node of this.draggedNodes) {
1058
- const isFrom = edge.from.nodeId === node.id;
1059
- const isTo = edge.to.nodeId === node.id;
1060
+ const isFrom = fromId === node.id;
1061
+ const isTo = toId === node.id;
1060
1062
  if (!isFrom && !isTo) continue;
1061
- const oppositeNodeId = isFrom ? edge.to.nodeId : edge.from.nodeId;
1062
- if (draggedIds.has(oppositeNodeId)) continue;
1063
+ const oppositeNodeId = isFrom ? toId : fromId;
1064
+ if (!oppositeNodeId || draggedIds.has(oppositeNodeId)) continue;
1063
1065
  const controlPointIndex = isFrom ? 0 : edge.controlPoints.length - 1;
1064
1066
  const nearestPoint = edge.controlPoints[controlPointIndex];
1065
1067
  const endpoint = isFrom ? edge.startPoint : edge.endPoint;
@@ -1734,6 +1736,8 @@ var ConnectionManager = class extends EventEmitter {
1734
1736
  this.sourceOutlineParam = null;
1735
1737
  this.previewTargetOutlineParam = null;
1736
1738
  this.previewTargetNodeId = null;
1739
+ this.previewTargetEdgeId = null;
1740
+ this.previewTargetPathParam = null;
1737
1741
  this.isReconnecting = false;
1738
1742
  this.reconnectingEdge = null;
1739
1743
  this.reconnectingEndpoint = null;
@@ -1912,8 +1916,8 @@ var ConnectionManager = class extends EventEmitter {
1912
1916
  updateReconnectingEdge(point) {
1913
1917
  if (!this.reconnectingEdge) return;
1914
1918
  const edge = this.reconnectingEdge;
1915
- const fromNode = this.renderer.getNode(edge.from.nodeId);
1916
- const toNode = this.renderer.getNode(edge.to.nodeId);
1919
+ const fromNode = edge.from.nodeId ? this.renderer.getNode(edge.from.nodeId) : void 0;
1920
+ const toNode = edge.to.nodeId ? this.renderer.getNode(edge.to.nodeId) : void 0;
1917
1921
  let snappedPoint = point;
1918
1922
  let snappedDir;
1919
1923
  let targetNode = this.getNodeAtPoint(point, true);
@@ -2056,6 +2060,8 @@ var ConnectionManager = class extends EventEmitter {
2056
2060
  this.previewTargetAnchorId = null;
2057
2061
  this.previewTargetOutlineParam = null;
2058
2062
  this.previewTargetNodeId = null;
2063
+ this.previewTargetEdgeId = null;
2064
+ this.previewTargetPathParam = null;
2059
2065
  let targetNode = this.getNodeAtPoint(cursorPoint, false);
2060
2066
  if (this.attachToOutline) {
2061
2067
  const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
@@ -2099,6 +2105,24 @@ var ConnectionManager = class extends EventEmitter {
2099
2105
  }
2100
2106
  }
2101
2107
  }
2108
+ const edgeDrop = this.findEdgeDropTarget(cursorPoint);
2109
+ if (edgeDrop) {
2110
+ let nodeDistance = Infinity;
2111
+ if (targetNode) {
2112
+ const dx = cursorPoint.x - snappedPoint.x;
2113
+ const dy = cursorPoint.y - snappedPoint.y;
2114
+ nodeDistance = Math.sqrt(dx * dx + dy * dy);
2115
+ }
2116
+ if (!targetNode || edgeDrop.distance < nodeDistance) {
2117
+ targetNode = null;
2118
+ this.previewTargetNodeId = null;
2119
+ this.previewTargetOutlineParam = null;
2120
+ this.previewTargetAnchorId = null;
2121
+ snappedPoint = edgeDrop.point;
2122
+ this.previewTargetEdgeId = edgeDrop.edge.id;
2123
+ this.previewTargetPathParam = edgeDrop.pathParam;
2124
+ }
2125
+ }
2102
2126
  if (targetNode) {
2103
2127
  const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
2104
2128
  this.setCursor(forbidden ? "not-allowed" : "crosshair");
@@ -2199,9 +2223,29 @@ var ConnectionManager = class extends EventEmitter {
2199
2223
  y: event.worldY
2200
2224
  };
2201
2225
  let createdEdge = null;
2226
+ const edgeDrop = this.previewTargetEdgeId != null && this.previewTargetPathParam != null ? {
2227
+ edgeId: this.previewTargetEdgeId,
2228
+ pathParam: this.previewTargetPathParam,
2229
+ point
2230
+ } : this.findEdgeDropTarget(point);
2202
2231
  let targetNode = this.getNodeAtPoint(point, false);
2203
2232
  if (!targetNode && this.attachToOutline && this.previewTargetNodeId) targetNode = this.renderer.getNode(this.previewTargetNodeId) ?? null;
2204
- if (targetNode) {
2233
+ if (edgeDrop && this.sourceNode && (!targetNode || this.previewTargetEdgeId != null)) {
2234
+ const from = this.attachToOutline ? {
2235
+ nodeId: this.sourceNode.id,
2236
+ outlineParam: this.sourceOutlineParam ?? void 0
2237
+ } : {
2238
+ nodeId: this.sourceNode.id,
2239
+ portId: this.sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${this.sourceAnchorId}` : void 0
2240
+ };
2241
+ const to = {
2242
+ edgeId: edgeDrop.edgeId,
2243
+ pathParam: edgeDrop.pathParam
2244
+ };
2245
+ createdEdge = this.createEdge(from, to);
2246
+ this.addEdge(createdEdge);
2247
+ this.emit("connect", createdEdge);
2248
+ } else if (targetNode) {
2205
2249
  if (!this._connectionValidator || this._connectionValidator(this.sourceNode.id, targetNode.id)) {
2206
2250
  let from;
2207
2251
  let to;
@@ -2338,8 +2382,36 @@ var ConnectionManager = class extends EventEmitter {
2338
2382
  this.sourceOutlineParam = null;
2339
2383
  this.previewTargetOutlineParam = null;
2340
2384
  this.previewTargetNodeId = null;
2385
+ this.previewTargetEdgeId = null;
2386
+ this.previewTargetPathParam = null;
2341
2387
  this.setCursor("");
2342
2388
  }
2389
+ /**
2390
+ * Find the nearest edge under the cursor for junction / note→relation drops.
2391
+ */
2392
+ findEdgeDropTarget(point) {
2393
+ const tolerance = Math.max(14, 20 / Math.max(this.renderer.zoom, 1e-4));
2394
+ let best = null;
2395
+ for (const edge of this.renderer.edges.values()) {
2396
+ if (!edge.visible || edge.path.length < 2) continue;
2397
+ const closest = edge.getClosestPointOnPath(point);
2398
+ if (!closest || closest.distance > tolerance) continue;
2399
+ if (!best || closest.distance < best.distance) best = {
2400
+ edge,
2401
+ pathParam: closest.pathParam,
2402
+ point: closest.point,
2403
+ distance: closest.distance
2404
+ };
2405
+ }
2406
+ if (!best) return null;
2407
+ return {
2408
+ edge: best.edge,
2409
+ edgeId: best.edge.id,
2410
+ pathParam: best.pathParam,
2411
+ point: best.point,
2412
+ distance: best.distance
2413
+ };
2414
+ }
2343
2415
  resetReconnection() {
2344
2416
  this.isReconnecting = false;
2345
2417
  if (this.reconnectingEdge) this.reconnectingEdge.autoUpdateEndpoints = true;
@@ -3588,6 +3660,109 @@ var Element = class {
3588
3660
  }
3589
3661
  };
3590
3662
  //#endregion
3663
+ //#region src/utils/edgePath.ts
3664
+ /**
3665
+ * Point and tangent angle at a normalized position along an edge path (0..1 by length).
3666
+ */
3667
+ function getPathPointAt(path, type, position) {
3668
+ if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
3669
+ const samples = [];
3670
+ const steps = 20;
3671
+ for (let i = 1; i + 2 < path.length; i += 3) {
3672
+ const start = path[i - 1];
3673
+ const control1 = path[i];
3674
+ const control2 = path[i + 1];
3675
+ const end = path[i + 2];
3676
+ for (let sample = 0; sample <= steps; sample++) {
3677
+ const t = sample / steps;
3678
+ if (samples.length > 0 && t === 0) continue;
3679
+ samples.push(bezierPoint(start, control1, control2, end, t));
3680
+ }
3681
+ }
3682
+ return getPointAlongPolyline(samples, position);
3683
+ }
3684
+ function getPointAlongPolyline(path, position) {
3685
+ if (path.length === 0) return {
3686
+ point: {
3687
+ x: 0,
3688
+ y: 0
3689
+ },
3690
+ angle: 0
3691
+ };
3692
+ if (path.length === 1) return {
3693
+ point: path[0],
3694
+ angle: 0
3695
+ };
3696
+ let totalLength = 0;
3697
+ const segments = [];
3698
+ for (let i = 1; i < path.length; i++) {
3699
+ const start = path[i - 1];
3700
+ const end = path[i];
3701
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
3702
+ segments.push({
3703
+ start,
3704
+ end,
3705
+ length
3706
+ });
3707
+ totalLength += length;
3708
+ }
3709
+ const targetLength = totalLength * Math.max(0, Math.min(1, position));
3710
+ let accumulated = 0;
3711
+ for (const segment of segments) {
3712
+ if (accumulated + segment.length >= targetLength) {
3713
+ const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
3714
+ return {
3715
+ point: {
3716
+ x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
3717
+ y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
3718
+ },
3719
+ angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
3720
+ };
3721
+ }
3722
+ accumulated += segment.length;
3723
+ }
3724
+ const lastSegment = segments[segments.length - 1];
3725
+ return {
3726
+ point: lastSegment.end,
3727
+ angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
3728
+ };
3729
+ }
3730
+ /** Map tangent angle to a cardinal direction used by polyline routing. */
3731
+ function directionFromAngle(angle) {
3732
+ const twoPi = Math.PI * 2;
3733
+ const a = (angle % twoPi + twoPi) % twoPi;
3734
+ if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
3735
+ if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
3736
+ if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
3737
+ return "right";
3738
+ }
3739
+ /**
3740
+ * Nearest point on an edge path to `point`, by sampling along normalized length.
3741
+ */
3742
+ function getClosestPointOnPath(path, type, point, samples = 48) {
3743
+ if (path.length < 2) return null;
3744
+ let bestPoint = getPathPointAt(path, type, 0).point;
3745
+ let bestParam = 0;
3746
+ let bestDistSq = Infinity;
3747
+ for (let i = 0; i <= samples; i++) {
3748
+ const pathParam = i / samples;
3749
+ const at = getPathPointAt(path, type, pathParam).point;
3750
+ const dx = at.x - point.x;
3751
+ const dy = at.y - point.y;
3752
+ const distSq = dx * dx + dy * dy;
3753
+ if (distSq < bestDistSq) {
3754
+ bestDistSq = distSq;
3755
+ bestPoint = at;
3756
+ bestParam = pathParam;
3757
+ }
3758
+ }
3759
+ return {
3760
+ point: bestPoint,
3761
+ pathParam: bestParam,
3762
+ distance: Math.sqrt(bestDistSq)
3763
+ };
3764
+ }
3765
+ //#endregion
3591
3766
  //#region src/elements/paths/StraightPathStrategy.ts
3592
3767
  /**
3593
3768
  * Straight line path strategy
@@ -4696,69 +4871,6 @@ function getEdgeLabelRotation(options) {
4696
4871
  if (angle < -Math.PI / 2) angle += Math.PI;
4697
4872
  return angle;
4698
4873
  }
4699
- function getPathPointAt(path, type, position) {
4700
- if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
4701
- const samples = [];
4702
- const steps = 20;
4703
- for (let i = 1; i + 2 < path.length; i += 3) {
4704
- const start = path[i - 1];
4705
- const control1 = path[i];
4706
- const control2 = path[i + 1];
4707
- const end = path[i + 2];
4708
- for (let sample = 0; sample <= steps; sample++) {
4709
- const t = sample / steps;
4710
- if (samples.length > 0 && t === 0) continue;
4711
- samples.push(bezierPoint(start, control1, control2, end, t));
4712
- }
4713
- }
4714
- return getPointAlongPolyline(samples, position);
4715
- }
4716
- function getPointAlongPolyline(path, position) {
4717
- if (path.length === 0) return {
4718
- point: {
4719
- x: 0,
4720
- y: 0
4721
- },
4722
- angle: 0
4723
- };
4724
- if (path.length === 1) return {
4725
- point: path[0],
4726
- angle: 0
4727
- };
4728
- let totalLength = 0;
4729
- const segments = [];
4730
- for (let i = 1; i < path.length; i++) {
4731
- const start = path[i - 1];
4732
- const end = path[i];
4733
- const length = Math.hypot(end.x - start.x, end.y - start.y);
4734
- segments.push({
4735
- start,
4736
- end,
4737
- length
4738
- });
4739
- totalLength += length;
4740
- }
4741
- const targetLength = totalLength * Math.max(0, Math.min(1, position));
4742
- let accumulated = 0;
4743
- for (const segment of segments) {
4744
- if (accumulated + segment.length >= targetLength) {
4745
- const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
4746
- return {
4747
- point: {
4748
- x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
4749
- y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
4750
- },
4751
- angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
4752
- };
4753
- }
4754
- accumulated += segment.length;
4755
- }
4756
- const lastSegment = segments[segments.length - 1];
4757
- return {
4758
- point: lastSegment.end,
4759
- angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
4760
- };
4761
- }
4762
4874
  //#endregion
4763
4875
  //#region src/elements/Edge.ts
4764
4876
  var DEFAULT_EDGE_STYLE = {
@@ -4825,6 +4937,7 @@ var Edge = class extends Element {
4825
4937
  }
4826
4938
  set from(value) {
4827
4939
  this._from = value;
4940
+ this._bindingListener?.();
4828
4941
  this.markDirty();
4829
4942
  }
4830
4943
  /**
@@ -4835,9 +4948,17 @@ var Edge = class extends Element {
4835
4948
  }
4836
4949
  set to(value) {
4837
4950
  this._to = value;
4951
+ this._bindingListener?.();
4838
4952
  this.markDirty();
4839
4953
  }
4840
4954
  /**
4955
+ * Notified when from/to binding changes (ports / outline params / node ids).
4956
+ * Used by DiagramRenderer to resync endpoints without doing so on every pan frame.
4957
+ */
4958
+ setBindingListener(listener) {
4959
+ this._bindingListener = listener;
4960
+ }
4961
+ /**
4841
4962
  * Edge type
4842
4963
  */
4843
4964
  get type() {
@@ -4848,7 +4969,7 @@ var Edge = class extends Element {
4848
4969
  this._type = value;
4849
4970
  this._pathStrategy = this.getPathStrategy(value);
4850
4971
  if (value !== "editable-polyline" && this._controlPoints?.length) this.controlPoints = void 0;
4851
- this.markDirty();
4972
+ else this.recalculatePath();
4852
4973
  }
4853
4974
  }
4854
4975
  /**
@@ -5102,6 +5223,10 @@ var Edge = class extends Element {
5102
5223
  * @param toDir Direction at end point (top, right, bottom, left)
5103
5224
  */
5104
5225
  updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
5226
+ if (this._path.length > 0 && this._fromPoint.x === fromPoint.x && this._fromPoint.y === fromPoint.y && this._toPoint.x === toPoint.x && this._toPoint.y === toPoint.y && this._fromDir === fromDir && this._toDir === toDir) {
5227
+ this._pathOptions = options;
5228
+ return;
5229
+ }
5105
5230
  this._fromPoint = fromPoint;
5106
5231
  this._toPoint = toPoint;
5107
5232
  this._fromDir = fromDir;
@@ -5398,6 +5523,19 @@ var Edge = class extends Element {
5398
5523
  });
5399
5524
  }
5400
5525
  /**
5526
+ * Point and tangent on the current path at a normalized length position (0..1).
5527
+ */
5528
+ getPointAt(pathParam = .5) {
5529
+ if (this._path.length < 2) return null;
5530
+ return getPathPointAt(this._path, this._type, pathParam);
5531
+ }
5532
+ /**
5533
+ * Nearest point on this edge's path to `point` (world coordinates).
5534
+ */
5535
+ getClosestPointOnPath(point) {
5536
+ return getClosestPointOnPath(this._path, this._type, point);
5537
+ }
5538
+ /**
5401
5539
  * Get world position of label center along path.
5402
5540
  */
5403
5541
  getLabelPosition() {
@@ -5592,22 +5730,27 @@ var ClipboardManager = class {
5592
5730
  nodeIds.add(node.id);
5593
5731
  }
5594
5732
  }
5595
- for (const edge of this.renderer.edges.values()) if (nodeIds.has(edge.from.nodeId) && nodeIds.has(edge.to.nodeId)) edges.push({
5596
- id: edge.id,
5597
- from: edge.from,
5598
- to: edge.to,
5599
- type: edge.type,
5600
- controlPoints: edge.controlPoints,
5601
- arrowType: edge.arrowType,
5602
- style: edge.style,
5603
- styleClass: edge.styleClass,
5604
- label: edge.label?.text,
5605
- labelStyleClass: edge.label?.styleClass,
5606
- labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
5607
- labelBackground: edge.labelBackground,
5608
- labelLineGap: edge.labelLineGap ? true : void 0,
5609
- data: Object.keys(edge.data).length > 0 ? edge.data : void 0
5610
- });
5733
+ for (const edge of this.renderer.edges.values()) {
5734
+ const fromId = edge.from.nodeId;
5735
+ const toId = edge.to.nodeId;
5736
+ if (!fromId || !toId || !nodeIds.has(fromId) || !nodeIds.has(toId)) continue;
5737
+ edges.push({
5738
+ id: edge.id,
5739
+ from: edge.from,
5740
+ to: edge.to,
5741
+ type: edge.type,
5742
+ controlPoints: edge.controlPoints,
5743
+ arrowType: edge.arrowType,
5744
+ style: edge.style,
5745
+ styleClass: edge.styleClass,
5746
+ label: edge.label?.text,
5747
+ labelStyleClass: edge.label?.styleClass,
5748
+ labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
5749
+ labelBackground: edge.labelBackground,
5750
+ labelLineGap: edge.labelLineGap ? true : void 0,
5751
+ data: Object.keys(edge.data).length > 0 ? edge.data : void 0
5752
+ });
5753
+ }
5611
5754
  this.clipboard = {
5612
5755
  nodes,
5613
5756
  edges
@@ -5634,8 +5777,11 @@ var ClipboardManager = class {
5634
5777
  newNodes.push(this.nodeFactory(copyData));
5635
5778
  }
5636
5779
  for (const edgeData of this.clipboard.edges) {
5637
- const fromNodeId = idMap.get(edgeData.from.nodeId);
5638
- const toNodeId = idMap.get(edgeData.to.nodeId);
5780
+ const fromKey = edgeData.from.nodeId;
5781
+ const toKey = edgeData.to.nodeId;
5782
+ if (!fromKey || !toKey) continue;
5783
+ const fromNodeId = idMap.get(fromKey);
5784
+ const toNodeId = idMap.get(toKey);
5639
5785
  if (!fromNodeId || !toNodeId) continue;
5640
5786
  const newId = `${edgeData.id}_copy_${Date.now()}`;
5641
5787
  const copyEdge = {
@@ -5837,7 +5983,9 @@ var InteractionManager = class {
5837
5983
  const idSet = new Set(nodeIds);
5838
5984
  for (const edge of this.renderer.edges.values()) {
5839
5985
  if (!edge.hasEditableControlPoints()) continue;
5840
- if (!idSet.has(edge.from.nodeId) && !idSet.has(edge.to.nodeId)) continue;
5986
+ const fromId = edge.from.nodeId;
5987
+ const toId = edge.to.nodeId;
5988
+ if ((!fromId || !idSet.has(fromId)) && (!toId || !idSet.has(toId))) continue;
5841
5989
  if (this.dragStartEditablePolylinePoints.has(edge.id)) continue;
5842
5990
  this.dragStartEditablePolylinePoints.set(edge.id, clonePoints(edge.controlPoints));
5843
5991
  }
@@ -6385,10 +6533,14 @@ var InteractionManager = class {
6385
6533
  nodeGroupIds.set(child.id, list);
6386
6534
  }
6387
6535
  }
6388
- for (const edge of this.renderer.edges.values()) if (nodeIds.has(edge.from.nodeId) || nodeIds.has(edge.to.nodeId)) {
6389
- if (!edgeIds.has(edge.id)) {
6390
- edgeIds.add(edge.id);
6391
- edges.push(edge);
6536
+ for (const edge of this.renderer.edges.values()) {
6537
+ const fromId = edge.from.nodeId;
6538
+ const toId = edge.to.nodeId;
6539
+ if (fromId && nodeIds.has(fromId) || toId && nodeIds.has(toId)) {
6540
+ if (!edgeIds.has(edge.id)) {
6541
+ edgeIds.add(edge.id);
6542
+ edges.push(edge);
6543
+ }
6392
6544
  }
6393
6545
  }
6394
6546
  return new CompositeCommand([{
@@ -6411,8 +6563,15 @@ var InteractionManager = class {
6411
6563
  }]);
6412
6564
  }
6413
6565
  endpointsEqual(a, b) {
6414
- if (a.nodeId !== b.nodeId) return false;
6566
+ if ((a.edgeId ?? null) !== (b.edgeId ?? null)) return false;
6567
+ if ((a.nodeId ?? null) !== (b.nodeId ?? null)) return false;
6415
6568
  if ((a.portId ?? null) !== (b.portId ?? null)) return false;
6569
+ const aPath = a.pathParam;
6570
+ const bPath = b.pathParam;
6571
+ if (aPath !== void 0 || bPath !== void 0) {
6572
+ if (aPath === void 0 || bPath === void 0) return false;
6573
+ if (Math.abs(aPath - bPath) >= 1e-9) return false;
6574
+ }
6416
6575
  const aParam = a.outlineParam;
6417
6576
  const bParam = b.outlineParam;
6418
6577
  if (aParam === void 0 && bParam === void 0) return true;
@@ -6812,7 +6971,16 @@ var ContextMenuManager = class extends EventEmitter {
6812
6971
  }
6813
6972
  };
6814
6973
  //#endregion
6974
+ //#region src/types.ts
6975
+ function isNodeEdgeEndpoint(endpoint) {
6976
+ return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
6977
+ }
6978
+ function isEdgeEdgeEndpoint(endpoint) {
6979
+ return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
6980
+ }
6981
+ //#endregion
6815
6982
  //#region src/core/EdgeEndpointUpdater.ts
6983
+ var EDGE_ATTACH_MAX_PASSES = 3;
6816
6984
  var EdgeEndpointUpdater = class {
6817
6985
  constructor(host) {
6818
6986
  this.host = host;
@@ -6826,26 +6994,71 @@ var EdgeEndpointUpdater = class {
6826
6994
  update(includeRoutingObstacles) {
6827
6995
  const obstacles = includeRoutingObstacles ? this.host.getNodeObstacles() : void 0;
6828
6996
  const nodes = this.host.getNodes();
6829
- for (const edge of this.host.getEdges().values()) {
6997
+ const edges = this.host.getEdges();
6998
+ const nodeNode = [];
6999
+ const edgeAttached = [];
7000
+ for (const edge of edges.values()) {
6830
7001
  if (!edge.autoUpdateEndpoints) continue;
6831
- const fromNode = nodes.get(edge.from.nodeId);
6832
- const toNode = nodes.get(edge.to.nodeId);
6833
- if (!fromNode || !toNode) continue;
6834
- if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
6835
- const from = this.resolveEndpoint(fromNode, edge.from.portId, edge.from.outlineParam);
6836
- const to = this.resolveEndpoint(toNode, edge.to.portId, edge.to.outlineParam);
6837
- edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
6838
- }
7002
+ if (isEdgeEdgeEndpoint(edge.from) || isEdgeEdgeEndpoint(edge.to)) edgeAttached.push(edge);
7003
+ else nodeNode.push(edge);
7004
+ }
7005
+ for (const edge of nodeNode) this.updateNodeNodeEdge(edge, nodes, includeRoutingObstacles, obstacles);
7006
+ for (let pass = 0; pass < EDGE_ATTACH_MAX_PASSES; pass++) {
7007
+ let changed = false;
7008
+ for (const edge of edgeAttached) if (this.updateEdgeAttachedEdge(edge, nodes, edges, obstacles)) changed = true;
7009
+ if (!changed) break;
7010
+ }
7011
+ }
7012
+ updateNodeNodeEdge(edge, nodes, includeRoutingObstacles, obstacles) {
7013
+ if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
7014
+ const fromNode = nodes.get(edge.from.nodeId);
7015
+ const toNode = nodes.get(edge.to.nodeId);
7016
+ if (!fromNode || !toNode) return;
7017
+ if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
7018
+ const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
7019
+ const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
7020
+ edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
7021
+ }
7022
+ /**
7023
+ * @returns true when the resolved geometry changed enough to warrant another pass
7024
+ */
7025
+ updateEdgeAttachedEdge(edge, nodes, edges, obstacles) {
7026
+ const fromEdge = isEdgeEdgeEndpoint(edge.from) ? this.resolveEdgeEndpoint(edge.from, edges) : null;
7027
+ const toEdge = isEdgeEdgeEndpoint(edge.to) ? this.resolveEdgeEndpoint(edge.to, edges) : null;
7028
+ const from = fromEdge ?? (isNodeEdgeEndpoint(edge.from) ? this.resolveNodeEnd(edge.from, nodes, toEdge?.point, edge.lockAnchors) : null);
7029
+ const to = toEdge ?? (isNodeEdgeEndpoint(edge.to) ? this.resolveNodeEnd(edge.to, nodes, from?.point, edge.lockAnchors) : null);
7030
+ if (!from || !to) return false;
7031
+ const before = edge.path;
7032
+ const beforeKey = before.length >= 2 ? `${before[0].x},${before[0].y}:${before[before.length - 1].x},${before[before.length - 1].y}` : "";
7033
+ edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
7034
+ const after = edge.path;
7035
+ return beforeKey !== (after.length >= 2 ? `${after[0].x},${after[0].y}:${after[after.length - 1].x},${after[after.length - 1].y}` : "");
7036
+ }
7037
+ resolveNodeEnd(endpoint, nodes, toward, lockAnchors) {
7038
+ const node = nodes.get(endpoint.nodeId);
7039
+ if (!node) return null;
7040
+ return this.resolveNodeEndpoint(node, endpoint, toward, lockAnchors);
7041
+ }
7042
+ resolveEdgeEndpoint(endpoint, edges) {
7043
+ const host = edges.get(endpoint.edgeId);
7044
+ if (!host) return null;
7045
+ const pathParam = endpoint.pathParam ?? .5;
7046
+ const at = host.getPointAt(pathParam);
7047
+ if (!at) return null;
7048
+ return {
7049
+ point: at.point,
7050
+ direction: directionFromAngle(at.angle)
7051
+ };
6839
7052
  }
6840
7053
  lockAnchors(edge, fromNode, toNode) {
6841
- if (edge.lockAnchors && edge.from.outlineParam === void 0 && !edge.from.portId) {
7054
+ if (isNodeEdgeEndpoint(edge.from) && edge.lockAnchors && edge.from.outlineParam === void 0 && !edge.from.portId) {
6842
7055
  const anchor = fromNode.getNearestAnchor(toNode.getCenter());
6843
7056
  if (anchor) edge.from = {
6844
7057
  ...edge.from,
6845
7058
  portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
6846
7059
  };
6847
7060
  }
6848
- if (edge.lockAnchors && edge.to.outlineParam === void 0 && !edge.to.portId) {
7061
+ if (isNodeEdgeEndpoint(edge.to) && edge.lockAnchors && edge.to.outlineParam === void 0 && !edge.to.portId) {
6849
7062
  const anchor = toNode.getNearestAnchor(fromNode.getCenter());
6850
7063
  if (anchor) edge.to = {
6851
7064
  ...edge.to,
@@ -6853,21 +7066,36 @@ var EdgeEndpointUpdater = class {
6853
7066
  };
6854
7067
  }
6855
7068
  }
6856
- resolveEndpoint(node, portId, outlineParam) {
6857
- if (portId) {
6858
- const anchorId = portId.slice(ANCHOR_PORT_PREFIX.length);
7069
+ /**
7070
+ * Resolve a node endpoint:
7071
+ * - lockAnchors + portId → fixed side port (wins over leftover outlineParam)
7072
+ * - outlineParam → fixed point on outline (attach-to-outline)
7073
+ * - otherwise → floating nearest port/outline toward the other end
7074
+ */
7075
+ resolveNodeEndpoint(node, endpoint, toward, lockAnchors) {
7076
+ if (lockAnchors && isNodeEdgeEndpoint(endpoint) && endpoint.portId) {
7077
+ const anchorId = endpoint.portId.slice(ANCHOR_PORT_PREFIX.length);
6859
7078
  const point = node.getAnchorPointById(anchorId);
6860
- return point ? {
7079
+ if (point) return {
6861
7080
  point,
6862
7081
  direction: anchorId.split(":")[0]
6863
- } : { point: node.getCenter() };
7082
+ };
6864
7083
  }
6865
- if (outlineParam !== void 0) return {
6866
- point: node.getConnectionPointAtOutlineParam(outlineParam),
6867
- direction: getDirectionFromOutlineParam(outlineParam)
7084
+ if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0) return {
7085
+ point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
7086
+ direction: getDirectionFromOutlineParam(endpoint.outlineParam)
6868
7087
  };
7088
+ if (toward) return this.resolveFloatingEndpoint(node, toward);
6869
7089
  return { point: node.getCenter() };
6870
7090
  }
7091
+ resolveFloatingEndpoint(node, toward) {
7092
+ const anchor = node.getNearestAnchor(toward);
7093
+ if (anchor) return {
7094
+ point: anchor.point,
7095
+ direction: anchor.id.split(":")[0]
7096
+ };
7097
+ return { point: node.getConnectionPoint(toward) };
7098
+ }
6871
7099
  };
6872
7100
  //#endregion
6873
7101
  //#region src/core/ScrollbarController.ts
@@ -7232,6 +7460,7 @@ var DiagramRenderer = class extends EventEmitter {
7232
7460
  this._offsetY = 0;
7233
7461
  this._dirty = true;
7234
7462
  this._styleDirty = true;
7463
+ this._contentDirty = true;
7235
7464
  this._destroyed = false;
7236
7465
  this._canvasRect = null;
7237
7466
  this._nodeObstaclesCache = null;
@@ -7536,12 +7765,11 @@ var DiagramRenderer = class extends EventEmitter {
7536
7765
  * Add a node to the diagram
7537
7766
  */
7538
7767
  addNode(node) {
7539
- node.setDirtyListener(() => this.markDirty());
7768
+ node.setDirtyListener(() => this.markContentDirty());
7540
7769
  node.setAttachToOutlineGetter(() => this._attachToOutline);
7541
7770
  this._nodes.set(node.id, node);
7542
- this._nodeObstaclesCache = null;
7543
7771
  this.animationManager.registerEnter(node.id);
7544
- this.markDirty();
7772
+ this.markContentDirty();
7545
7773
  this.emit("nodeAdd", node);
7546
7774
  }
7547
7775
  /**
@@ -7572,11 +7800,12 @@ var DiagramRenderer = class extends EventEmitter {
7572
7800
  */
7573
7801
  addEdge(edge) {
7574
7802
  edge.setDirtyListener(() => this.markDirty());
7803
+ edge.setBindingListener(() => this.markContentDirty());
7575
7804
  this._edges.set(edge.id, edge);
7576
- this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
7577
- this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
7805
+ if (edge.from.nodeId) this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
7806
+ if (edge.to.nodeId) this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
7578
7807
  this.animationManager.registerEnter(edge.id);
7579
- this.markDirty();
7808
+ this.markContentDirty();
7580
7809
  this.emit("edgeAdd", edge);
7581
7810
  }
7582
7811
  /**
@@ -7615,21 +7844,27 @@ var DiagramRenderer = class extends EventEmitter {
7615
7844
  if (edgesToRemove) for (const edgeId of Array.from(edgesToRemove)) this.removeEdgeImmediate(edgeId);
7616
7845
  for (const group of this._groups.values()) group.removeChild(nodeId);
7617
7846
  this._nodes.delete(nodeId);
7618
- this._nodeObstaclesCache = null;
7619
7847
  node.setDirtyListener(void 0);
7620
7848
  node.setAttachToOutlineGetter(void 0);
7621
- this.markDirty();
7849
+ this.markContentDirty();
7622
7850
  this.emit("nodeRemove", node);
7623
7851
  return true;
7624
7852
  }
7625
7853
  removeEdgeImmediate(edgeId) {
7626
7854
  const edge = this._edges.get(edgeId);
7627
7855
  if (edge === void 0) return false;
7628
- this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
7629
- this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
7856
+ const dependents = [];
7857
+ for (const other of this._edges.values()) {
7858
+ if (other.id === edgeId) continue;
7859
+ if (other.from.edgeId === edgeId || other.to.edgeId === edgeId) dependents.push(other.id);
7860
+ }
7861
+ for (const dependentId of dependents) this.removeEdgeImmediate(dependentId);
7862
+ if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
7863
+ if (edge.to.nodeId) this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
7630
7864
  this._edges.delete(edgeId);
7631
7865
  edge.setDirtyListener(void 0);
7632
- this.markDirty();
7866
+ edge.setBindingListener(void 0);
7867
+ this.markContentDirty();
7633
7868
  this.emit("edgeRemove", edge);
7634
7869
  return true;
7635
7870
  }
@@ -7709,12 +7944,22 @@ var DiagramRenderer = class extends EventEmitter {
7709
7944
  for (const n of this._nodes.values()) n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
7710
7945
  }
7711
7946
  /**
7712
- * Mark the diagram as needing re-render
7947
+ * Mark the diagram as needing re-render (viewport / cosmetics).
7948
+ * Does not resync edge endpoints — use {@link markContentDirty} after layout changes.
7713
7949
  */
7714
7950
  markDirty() {
7715
7951
  this._dirty = true;
7716
7952
  }
7717
7953
  /**
7954
+ * Mark that node/edge layout changed and edge endpoints must be recalculated.
7955
+ * Pan/zoom should call {@link markDirty} only so path routing is not redone every frame.
7956
+ */
7957
+ markContentDirty() {
7958
+ this._contentDirty = true;
7959
+ this._nodeObstaclesCache = null;
7960
+ this._dirty = true;
7961
+ }
7962
+ /**
7718
7963
  * Force an immediate render
7719
7964
  */
7720
7965
  render() {
@@ -7792,7 +8037,6 @@ var DiagramRenderer = class extends EventEmitter {
7792
8037
  }
7793
8038
  renderFrame(now) {
7794
8039
  this.frameTime = now;
7795
- this._nodeObstaclesCache = null;
7796
8040
  const ctx = this.ctx;
7797
8041
  const { width, height, backgroundColor } = this.options;
7798
8042
  ctx.setTransform(this.devicePixelRatio, 0, 0, this.devicePixelRatio, 0, 0);
@@ -7810,7 +8054,10 @@ var DiagramRenderer = class extends EventEmitter {
7810
8054
  this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
7811
8055
  group.clearDirty();
7812
8056
  }
7813
- this.edgeEndpointUpdater.updateAll();
8057
+ if (this._contentDirty) {
8058
+ this.edgeEndpointUpdater.updateAll();
8059
+ this._contentDirty = false;
8060
+ }
7814
8061
  for (const node of this._nodes.values()) if (node.visible) {
7815
8062
  this.renderElementWithAnimation(ctx, node, () => node.render(ctx));
7816
8063
  node.clearDirty();
@@ -8054,8 +8301,10 @@ var SearchManager = class {
8054
8301
  for (const edge of this.renderer.edges.values()) {
8055
8302
  if (scope === "nodes") continue;
8056
8303
  const matchesEdge = this.matchesEdge(edge, options);
8057
- const fromVisible = this.renderer.getNode(edge.from.nodeId)?.visible ?? true;
8058
- const toVisible = this.renderer.getNode(edge.to.nodeId)?.visible ?? true;
8304
+ const fromId = edge.from.nodeId;
8305
+ const toId = edge.to.nodeId;
8306
+ const fromVisible = fromId ? this.renderer.getNode(fromId)?.visible ?? true : true;
8307
+ const toVisible = toId ? this.renderer.getNode(toId)?.visible ?? true : true;
8059
8308
  edge.visible = matchesEdge && fromVisible && toVisible;
8060
8309
  }
8061
8310
  this.renderer.markDirty();
@@ -8414,7 +8663,7 @@ var svgTextCache = new LRUCache(100);
8414
8663
  function fetchSvgText(url) {
8415
8664
  let promise = svgTextCache.get(url);
8416
8665
  if (!promise) {
8417
- promise = fetch(url).then((r) => r.ok ? r.text() : "");
8666
+ promise = fetch(url).then((r) => r.ok ? r.text() : "").catch(() => "");
8418
8667
  svgTextCache.set(url, promise);
8419
8668
  }
8420
8669
  return promise;
@@ -8432,6 +8681,32 @@ function readSvgFromUrlSync(url) {
8432
8681
  } catch {}
8433
8682
  return null;
8434
8683
  }
8684
+ /**
8685
+ * Resolve a source string to an href suitable for Image/SVG (async, with tint).
8686
+ */
8687
+ async function resolveTintedSvgDataUrl(source, options = {}) {
8688
+ const { strokeColor, fillColor } = options;
8689
+ if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, strokeColor, fillColor));
8690
+ if (isSvgUrl(source) || source.toLowerCase().endsWith(".svg")) {
8691
+ const svgText = await fetchSvgText(source);
8692
+ if (!svgText) return null;
8693
+ return svgToDataUrl(tintSvg(svgText, strokeColor, fillColor));
8694
+ }
8695
+ return null;
8696
+ }
8697
+ /**
8698
+ * Resolve a source to a tinted data URL synchronously (SvgExporter path).
8699
+ */
8700
+ function resolveTintedSvgDataUrlSync(source, options = {}) {
8701
+ const { strokeColor, fillColor } = options;
8702
+ if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, strokeColor, fillColor));
8703
+ if (source.toLowerCase().endsWith(".svg")) {
8704
+ const svgText = readSvgFromUrlSync(source);
8705
+ if (!svgText) return null;
8706
+ return svgToDataUrl(tintSvg(svgText, strokeColor, fillColor));
8707
+ }
8708
+ return null;
8709
+ }
8435
8710
  //#endregion
8436
8711
  //#region src/utils/iconLayout.ts
8437
8712
  /**
@@ -8616,22 +8891,20 @@ var NodeImage = class {
8616
8891
  if (source instanceof HTMLImageElement) return;
8617
8892
  const version = ++this._sourceVersion;
8618
8893
  if (isSvgMarkup(source)) {
8619
- const tinted = tintSvg(source, this._options.strokeColor, this._options.fillColor);
8894
+ const resolvedSource = resolveTintedSvgDataUrlSync(source, {
8895
+ strokeColor: this._options.strokeColor,
8896
+ fillColor: this._options.fillColor
8897
+ });
8620
8898
  if (version !== this._sourceVersion) return;
8621
- this._image.src = svgToDataUrl(tinted);
8899
+ this._image.src = resolvedSource ?? source;
8622
8900
  return;
8623
8901
  }
8624
- if ((!!this._options.strokeColor || !!this._options.fillColor) && source.toLowerCase().endsWith(".svg")) {
8625
- const svgText = await fetchSvgText(source);
8626
- if (version !== this._sourceVersion) return;
8627
- if (svgText) {
8628
- const tinted = tintSvg(svgText, this._options.strokeColor, this._options.fillColor);
8629
- this._image.src = svgToDataUrl(tinted);
8630
- return;
8631
- }
8632
- }
8902
+ const resolvedSource = await resolveTintedSvgDataUrl(source, {
8903
+ strokeColor: this._options.strokeColor,
8904
+ fillColor: this._options.fillColor
8905
+ });
8633
8906
  if (version !== this._sourceVersion) return;
8634
- this._image.src = source;
8907
+ this._image.src = resolvedSource ?? source;
8635
8908
  }
8636
8909
  attachHandlers() {
8637
8910
  if (this._image.complete && this._image.naturalWidth > 0) {
@@ -10703,16 +10976,8 @@ var CIcon = class extends CompositeComponentBase {
10703
10976
  }
10704
10977
  }
10705
10978
  async loadSource(source) {
10706
- if (isSvgMarkup(source)) {
10707
- const tinted = this._fillColor ? tintSvg(source, void 0, this._fillColor) : source;
10708
- this._image.src = svgToDataUrl(tinted);
10709
- } else if (isSvgUrl(source) && this._fillColor) try {
10710
- const tinted = tintSvg(await fetchSvgText(source), void 0, this._fillColor);
10711
- this._image.src = svgToDataUrl(tinted);
10712
- } catch {
10713
- this._image.src = source;
10714
- }
10715
- else this._image.src = source;
10979
+ const resolvedSource = await resolveTintedSvgDataUrl(source, { fillColor: this._fillColor });
10980
+ this._image.src = resolvedSource ?? source;
10716
10981
  }
10717
10982
  measure(_ctx) {
10718
10983
  return {
@@ -10761,7 +11026,8 @@ var CIcon = class extends CompositeComponentBase {
10761
11026
  const drawHeight = Math.min(this._height, bounds.height);
10762
11027
  const dx = bounds.x + (bounds.width - drawWidth) / 2;
10763
11028
  const dy = bounds.y + (bounds.height - drawHeight) / 2;
10764
- svg += `<image href="${escapeXmlAttr(this._source)}" x="${dx}" y="${dy}" width="${drawWidth}" height="${drawHeight}" />`;
11029
+ const source = resolveTintedSvgDataUrlSync(this._source, { fillColor: this._fillColor }) ?? this._source;
11030
+ svg += `<image href="${escapeXmlAttr(source)}" x="${dx}" y="${dy}" width="${drawWidth}" height="${drawHeight}" />`;
10765
11031
  return svg;
10766
11032
  }
10767
11033
  };
@@ -12180,9 +12446,14 @@ var Serializer = class {
12180
12446
  if (!Array.isArray(data.nodes)) throw new SerializerValidationError("Invalid or missing nodes array");
12181
12447
  if (!Array.isArray(data.edges)) throw new SerializerValidationError("Invalid or missing edges array");
12182
12448
  const nodeIds = new Set(data.nodes.map((n) => n.id));
12449
+ const edgeIds = new Set(data.edges.map((e) => e.id));
12183
12450
  for (const edge of data.edges) {
12184
- if (!nodeIds.has(edge.from.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent source node "${edge.from.nodeId}"`);
12185
- if (!nodeIds.has(edge.to.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent target node "${edge.to.nodeId}"`);
12451
+ if (edge.from.edgeId) {
12452
+ if (!edgeIds.has(edge.from.edgeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent host edge "${edge.from.edgeId}"`);
12453
+ } else if (!edge.from.nodeId || !nodeIds.has(edge.from.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent source node "${edge.from.nodeId}"`);
12454
+ if (edge.to.edgeId) {
12455
+ if (!edgeIds.has(edge.to.edgeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent host edge "${edge.to.edgeId}"`);
12456
+ } else if (!edge.to.nodeId || !nodeIds.has(edge.to.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent target node "${edge.to.nodeId}"`);
12186
12457
  }
12187
12458
  }
12188
12459
  /**
@@ -12965,12 +13236,10 @@ var SvgExporter = class {
12965
13236
  const source = opts.source;
12966
13237
  if (source instanceof HTMLImageElement) return source.src;
12967
13238
  if (!source) return "";
12968
- if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, opts.strokeColor, opts.fillColor));
12969
- if (source.toLowerCase().endsWith(".svg")) {
12970
- const svgText = readSvgFromUrlSync(source);
12971
- if (svgText) return svgToDataUrl(tintSvg(svgText, opts.strokeColor, opts.fillColor));
12972
- }
12973
- return source;
13239
+ return resolveTintedSvgDataUrlSync(source, {
13240
+ strokeColor: opts.strokeColor,
13241
+ fillColor: opts.fillColor
13242
+ }) ?? source;
12974
13243
  }
12975
13244
  escapeAttribute(value) {
12976
13245
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -13027,8 +13296,11 @@ var AutoRouting = class {
13027
13296
  const type = options.type ?? "polyline";
13028
13297
  for (const edge of renderer.edges.values()) {
13029
13298
  edge.type = type;
13030
- const fromNode = renderer.getNode(edge.from.nodeId);
13031
- const toNode = renderer.getNode(edge.to.nodeId);
13299
+ const fromId = edge.from.nodeId;
13300
+ const toId = edge.to.nodeId;
13301
+ if (!fromId || !toId) continue;
13302
+ const fromNode = renderer.getNode(fromId);
13303
+ const toNode = renderer.getNode(toId);
13032
13304
  if (!fromNode || !toNode) continue;
13033
13305
  const fromPoint = edge.from.portId !== void 0 ? fromNode.getPortPosition(edge.from.portId) ?? fromNode.getCenter() : fromNode.getCenter();
13034
13306
  const toPoint = edge.to.portId !== void 0 ? toNode.getPortPosition(edge.to.portId) ?? toNode.getCenter() : toNode.getCenter();
@@ -13503,6 +13775,6 @@ function applyEdgeStyle(ctx, style) {
13503
13775
  return baseOpacity * strokeOpacity;
13504
13776
  }
13505
13777
  //#endregion
13506
- export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, icon, isCornerPlacement, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
13778
+ export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, directionFromAngle, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, getClosestPointOnPath, getPathPointAt, icon, isCornerPlacement, isEdgeEdgeEndpoint, isNodeEdgeEndpoint, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
13507
13779
 
13508
13780
  //# sourceMappingURL=papirus.js.map