@ngroznykh/papirus 0.9.13 → 0.9.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.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/README.ru.md +2 -1
  3. package/dist/core/AnimationManager.d.ts +5 -0
  4. package/dist/core/AnimationManager.d.ts.map +1 -1
  5. package/dist/core/ConnectionManager.d.ts +5 -0
  6. package/dist/core/ConnectionManager.d.ts.map +1 -1
  7. package/dist/core/DiagramRenderer.d.ts +6 -0
  8. package/dist/core/DiagramRenderer.d.ts.map +1 -1
  9. package/dist/core/DragManager.d.ts +2 -1
  10. package/dist/core/DragManager.d.ts.map +1 -1
  11. package/dist/core/InteractionManager.d.ts.map +1 -1
  12. package/dist/core/NavigationManager.d.ts +4 -0
  13. package/dist/core/NavigationManager.d.ts.map +1 -1
  14. package/dist/core/SearchManager.d.ts.map +1 -1
  15. package/dist/core/SelectionManager.d.ts +1 -0
  16. package/dist/core/SelectionManager.d.ts.map +1 -1
  17. package/dist/elements/Group.d.ts.map +1 -1
  18. package/dist/elements/composite/CompositeNode.d.ts.map +1 -1
  19. package/dist/elements/composite/FlexLayout.d.ts.map +1 -1
  20. package/dist/elements/nodes/CircleNode.d.ts.map +1 -1
  21. package/dist/elements/nodes/DiamondNode.d.ts.map +1 -1
  22. package/dist/elements/paths/BezierPathStrategy.d.ts.map +1 -1
  23. package/dist/events/EventEmitter.d.ts +2 -3
  24. package/dist/events/EventEmitter.d.ts.map +1 -1
  25. package/dist/events/InputHandler.d.ts +11 -2
  26. package/dist/events/InputHandler.d.ts.map +1 -1
  27. package/dist/papirus.js +332 -140
  28. package/dist/papirus.js.map +1 -1
  29. package/dist/styles/StyleManager.d.ts.map +1 -1
  30. package/dist/types.d.ts +17 -1
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/utils/ImageExporter.d.ts.map +1 -1
  33. package/dist/utils/Serializer.d.ts +1 -0
  34. package/dist/utils/Serializer.d.ts.map +1 -1
  35. package/dist/utils/SvgExporter.d.ts.map +1 -1
  36. package/package.json +2 -2
package/dist/papirus.js CHANGED
@@ -1,10 +1,8 @@
1
1
  //#region src/events/EventEmitter.ts
2
- /**
3
- * Type-safe event emitter with support for typed event maps
4
- */
5
2
  var EventEmitter = class {
6
3
  constructor() {
7
4
  this.listeners = /* @__PURE__ */ new Map();
5
+ this.onceWrappers = /* @__PURE__ */ new Map();
8
6
  }
9
7
  /**
10
8
  * Subscribe to an event
@@ -25,8 +23,20 @@ var EventEmitter = class {
25
23
  * @param listener - Callback function to remove
26
24
  */
27
25
  off(event, listener) {
26
+ const eventListener = listener;
28
27
  const listeners = this.listeners.get(event);
29
- if (listeners !== void 0) listeners.delete(listener);
28
+ listeners?.delete(eventListener);
29
+ const wrappersByListener = this.onceWrappers.get(event);
30
+ const wrappers = wrappersByListener?.get(eventListener);
31
+ if (wrappers !== void 0) {
32
+ for (const wrapper of wrappers) listeners?.delete(wrapper);
33
+ wrappersByListener?.delete(eventListener);
34
+ } else if (wrappersByListener !== void 0) for (const [original, originalWrappers] of wrappersByListener) {
35
+ originalWrappers.delete(eventListener);
36
+ if (originalWrappers.size === 0) wrappersByListener.delete(original);
37
+ }
38
+ if (listeners?.size === 0) this.listeners.delete(event);
39
+ if (wrappersByListener?.size === 0) this.onceWrappers.delete(event);
30
40
  }
31
41
  /**
32
42
  * Subscribe to an event for a single emission
@@ -39,6 +49,19 @@ var EventEmitter = class {
39
49
  this.off(event, wrapper);
40
50
  listener(...args);
41
51
  };
52
+ const originalListener = listener;
53
+ const eventWrapper = wrapper;
54
+ let wrappersByListener = this.onceWrappers.get(event);
55
+ if (wrappersByListener === void 0) {
56
+ wrappersByListener = /* @__PURE__ */ new Map();
57
+ this.onceWrappers.set(event, wrappersByListener);
58
+ }
59
+ let wrappers = wrappersByListener.get(originalListener);
60
+ if (wrappers === void 0) {
61
+ wrappers = /* @__PURE__ */ new Set();
62
+ wrappersByListener.set(originalListener, wrappers);
63
+ }
64
+ wrappers.add(eventWrapper);
42
65
  return this.on(event, wrapper);
43
66
  }
44
67
  /**
@@ -48,15 +71,20 @@ var EventEmitter = class {
48
71
  */
49
72
  emit(event, ...args) {
50
73
  const listeners = this.listeners.get(event);
51
- if (listeners !== void 0) for (const listener of listeners) listener(...args);
74
+ if (listeners !== void 0) for (const listener of Array.from(listeners)) listener(...args);
52
75
  }
53
76
  /**
54
77
  * Remove all listeners for an event or all events
55
78
  * @param event - Optional event name. If not provided, removes all listeners
56
79
  */
57
80
  removeAllListeners(event) {
58
- if (event !== void 0) this.listeners.delete(event);
59
- else this.listeners.clear();
81
+ if (event !== void 0) {
82
+ this.listeners.delete(event);
83
+ this.onceWrappers.delete(event);
84
+ } else {
85
+ this.listeners.clear();
86
+ this.onceWrappers.clear();
87
+ }
60
88
  }
61
89
  /**
62
90
  * Get the number of listeners for an event
@@ -79,6 +107,9 @@ var InputHandler = class extends EventEmitter {
79
107
  this.boundHandlers = /* @__PURE__ */ new Map();
80
108
  this.lastTouchDistance = null;
81
109
  this.lastTouchCenter = null;
110
+ this.windowPointerActive = false;
111
+ this.boundWindowMouseMove = (e) => this.handleWindowMouseMove(e);
112
+ this.boundWindowMouseUp = (e) => this.handleWindowMouseUp(e);
82
113
  this.canvas = options.canvas;
83
114
  this.screenToWorld = options.screenToWorld;
84
115
  this.setupEventListeners();
@@ -93,6 +124,7 @@ var InputHandler = class extends EventEmitter {
93
124
  * Clean up event listeners
94
125
  */
95
126
  destroy() {
127
+ this.teardownWindowPointerListeners();
96
128
  for (const [eventName, handler] of this.boundHandlers) if (eventName.startsWith("key")) window.removeEventListener(eventName, handler);
97
129
  else this.canvas.removeEventListener(eventName, handler);
98
130
  this.boundHandlers.clear();
@@ -102,8 +134,8 @@ var InputHandler = class extends EventEmitter {
102
134
  this.addCanvasListener("click", this.handleClick.bind(this));
103
135
  this.addCanvasListener("dblclick", this.handleDblClick.bind(this));
104
136
  this.addCanvasListener("mousedown", this.handleMouseDown.bind(this));
105
- this.addCanvasListener("mouseup", this.handleMouseUp.bind(this));
106
- this.addCanvasListener("mousemove", this.handleMouseMove.bind(this));
137
+ this.addCanvasListener("mouseup", this.handleCanvasMouseUp.bind(this));
138
+ this.addCanvasListener("mousemove", this.handleCanvasMouseMove.bind(this));
107
139
  this.addCanvasListener("wheel", this.handleWheel.bind(this), { passive: false });
108
140
  this.addCanvasListener("touchstart", this.handleTouchStart.bind(this));
109
141
  this.addCanvasListener("touchend", this.handleTouchEnd.bind(this));
@@ -120,6 +152,18 @@ var InputHandler = class extends EventEmitter {
120
152
  window.addEventListener(eventName, handler);
121
153
  this.boundHandlers.set(eventName, handler);
122
154
  }
155
+ ensureWindowPointerListeners() {
156
+ if (this.windowPointerActive) return;
157
+ this.windowPointerActive = true;
158
+ window.addEventListener("mousemove", this.boundWindowMouseMove);
159
+ window.addEventListener("mouseup", this.boundWindowMouseUp);
160
+ }
161
+ teardownWindowPointerListeners() {
162
+ if (!this.windowPointerActive) return;
163
+ this.windowPointerActive = false;
164
+ window.removeEventListener("mousemove", this.boundWindowMouseMove);
165
+ window.removeEventListener("mouseup", this.boundWindowMouseUp);
166
+ }
123
167
  handleClick(e) {
124
168
  const event = this.normalizeMouseEvent(e);
125
169
  this.emit("click", event);
@@ -130,16 +174,30 @@ var InputHandler = class extends EventEmitter {
130
174
  }
131
175
  handleMouseDown(e) {
132
176
  const event = this.normalizeMouseEvent(e);
177
+ this.ensureWindowPointerListeners();
133
178
  this.emit("mousedown", event);
134
179
  }
135
- handleMouseUp(e) {
180
+ handleCanvasMouseMove(e) {
136
181
  const event = this.normalizeMouseEvent(e);
137
- this.emit("mouseup", event);
182
+ this.emit("mousemove", event);
138
183
  }
139
- handleMouseMove(e) {
184
+ handleWindowMouseMove(e) {
185
+ if (e.target === this.canvas || this.canvas.contains(e.target)) return;
140
186
  const event = this.normalizeMouseEvent(e);
141
187
  this.emit("mousemove", event);
142
188
  }
189
+ handleCanvasMouseUp(e) {
190
+ this.finishPointerUp(e);
191
+ }
192
+ handleWindowMouseUp(e) {
193
+ if (e.target === this.canvas || this.canvas.contains(e.target)) return;
194
+ this.finishPointerUp(e);
195
+ }
196
+ finishPointerUp(mouseEvent) {
197
+ const event = this.normalizeMouseEvent(mouseEvent);
198
+ this.teardownWindowPointerListeners();
199
+ this.emit("mouseup", event);
200
+ }
143
201
  handleWheel(e) {
144
202
  e.preventDefault();
145
203
  const wheelEvent = e;
@@ -748,6 +806,7 @@ var SelectionManager = class extends EventEmitter {
748
806
  const rect = this.selectionRect;
749
807
  const selected = [];
750
808
  for (const node of this.renderer.nodes.values()) if (rectsIntersect(rect, node.getBounds())) selected.push(node.id);
809
+ for (const edge of this.renderer.edges.values()) if (rectsIntersect(rect, edge.getBounds())) selected.push(edge.id);
751
810
  for (const group of this.renderer.groups.values()) if (rectsIntersect(rect, group.getBounds())) selected.push(group.id);
752
811
  if (selected.length > 0) this.selectMultiple(selected);
753
812
  this.emit("selectionEnd", rect);
@@ -779,6 +838,7 @@ var SelectionManager = class extends EventEmitter {
779
838
  ctx.setLineDash([]);
780
839
  }
781
840
  updateElementStates() {
841
+ this.pruneStaleSelection();
782
842
  const updateState = (elements) => {
783
843
  for (const element of elements) if (this._selectedIds.has(element.id)) element.state = "selected";
784
844
  else if (element.state === "selected") element.state = "normal";
@@ -789,6 +849,9 @@ var SelectionManager = class extends EventEmitter {
789
849
  this.renderer.markDirty();
790
850
  this.renderer.markStyleDirty();
791
851
  }
852
+ pruneStaleSelection() {
853
+ for (const id of Array.from(this._selectedIds)) if (this.renderer.getNode(id) === void 0 && this.renderer.getEdge(id) === void 0 && this.renderer.getGroup(id) === void 0) this._selectedIds.delete(id);
854
+ }
792
855
  emitSelect() {
793
856
  this.emit("select", Array.from(this._selectedIds));
794
857
  this.renderer.emit("select", Array.from(this._selectedIds));
@@ -810,9 +873,9 @@ var DragManager = class extends EventEmitter {
810
873
  this.editableBendFollow = /* @__PURE__ */ new Map();
811
874
  this.editablePolylineFullyConnectedEdges = /* @__PURE__ */ new Set();
812
875
  this.initialControlPointsForFullyConnected = /* @__PURE__ */ new Map();
876
+ this.initialControlPointsForBendFollow = /* @__PURE__ */ new Map();
813
877
  this.alignmentGuides = [];
814
878
  this._handledMouseDown = false;
815
- this.draggedGroupSelection = false;
816
879
  this.renderer = options.renderer;
817
880
  this.selectionManager = options.selectionManager;
818
881
  this.snapToGrid = options.snapToGrid ?? false;
@@ -890,7 +953,6 @@ var DragManager = class extends EventEmitter {
890
953
  this._handledMouseDown = true;
891
954
  this.dragStartPoint = point;
892
955
  this.lastDragPoint = point;
893
- this.draggedGroupSelection = false;
894
956
  this.draggedNodes = [];
895
957
  this.initialPositions.clear();
896
958
  for (const id of this.selectionManager.selectedIds) {
@@ -907,7 +969,6 @@ var DragManager = class extends EventEmitter {
907
969
  }
908
970
  this.dragStartPoint = point;
909
971
  this.lastDragPoint = point;
910
- this.draggedGroupSelection = false;
911
972
  this.draggedNodes = [];
912
973
  for (const id of this.selectionManager.selectedIds) {
913
974
  const selectedNode = this.renderer.getNode(id);
@@ -916,12 +977,9 @@ var DragManager = class extends EventEmitter {
916
977
  continue;
917
978
  }
918
979
  const selectedGroup = this.renderer.getGroup(id);
919
- if (selectedGroup !== void 0) {
920
- this.draggedGroupSelection = true;
921
- for (const child of selectedGroup.getAllChildren()) {
922
- const childNode = this.renderer.getNode(child.id);
923
- if (childNode) this.addDraggedNode(childNode);
924
- }
980
+ if (selectedGroup !== void 0) for (const child of selectedGroup.getAllChildren()) {
981
+ const childNode = this.renderer.getNode(child.id);
982
+ if (childNode) this.addDraggedNode(childNode);
925
983
  }
926
984
  }
927
985
  return true;
@@ -977,6 +1035,7 @@ var DragManager = class extends EventEmitter {
977
1035
  }
978
1036
  if (firstDelta !== null) this.followEditableBendForFullyConnectedEdges(firstDelta.x, firstDelta.y);
979
1037
  this.emit("drag", this.draggedNodes.map((n) => n.id), point, delta);
1038
+ this.lastDragPoint = point;
980
1039
  this.renderer.markDirty();
981
1040
  return true;
982
1041
  }
@@ -993,7 +1052,6 @@ var DragManager = class extends EventEmitter {
993
1052
  if (this.isDragging) {
994
1053
  for (const node of this.draggedNodes) node.state = "selected";
995
1054
  this.emit("dragend", this.draggedNodes.map((n) => n.id), point);
996
- if (this.draggedGroupSelection) this.selectionManager.clearSelection();
997
1055
  }
998
1056
  this.reset();
999
1057
  this.renderer.markDirty();
@@ -1016,6 +1074,10 @@ var DragManager = class extends EventEmitter {
1016
1074
  const edge = this.renderer.getEdge(edgeId);
1017
1075
  if (edge) edge.controlPoints = clonePoints(points);
1018
1076
  }
1077
+ for (const [edgeId, points] of this.initialControlPointsForBendFollow) {
1078
+ const edge = this.renderer.getEdge(edgeId);
1079
+ if (edge) edge.controlPoints = clonePoints(points);
1080
+ }
1019
1081
  }
1020
1082
  this.reset();
1021
1083
  this.renderer.markDirty();
@@ -1037,13 +1099,14 @@ var DragManager = class extends EventEmitter {
1037
1099
  this.editableBendFollow.clear();
1038
1100
  this.editablePolylineFullyConnectedEdges.clear();
1039
1101
  this.initialControlPointsForFullyConnected.clear();
1102
+ this.initialControlPointsForBendFollow.clear();
1040
1103
  this.alignmentGuides = [];
1041
- this.draggedGroupSelection = false;
1042
1104
  }
1043
1105
  buildEditableBendFollowBindings() {
1044
1106
  this.editableBendFollow.clear();
1045
1107
  this.editablePolylineFullyConnectedEdges.clear();
1046
1108
  this.initialControlPointsForFullyConnected.clear();
1109
+ this.initialControlPointsForBendFollow.clear();
1047
1110
  const draggedIds = new Set(this.draggedNodes.map((node) => node.id));
1048
1111
  const nodeBindings = /* @__PURE__ */ new Map();
1049
1112
  for (const edge of this.renderer.edges.values()) {
@@ -1080,6 +1143,7 @@ var DragManager = class extends EventEmitter {
1080
1143
  axis
1081
1144
  });
1082
1145
  nodeBindings.set(node.id, bindings);
1146
+ if (!this.initialControlPointsForBendFollow.has(edge.id)) this.initialControlPointsForBendFollow.set(edge.id, clonePoints(edge.controlPoints));
1083
1147
  }
1084
1148
  }
1085
1149
  for (const [nodeId, bindings] of nodeBindings) if (bindings.length > 0) this.editableBendFollow.set(nodeId, bindings);
@@ -1512,9 +1576,23 @@ var NavigationManager = class extends EventEmitter {
1512
1576
  this.isPanning = false;
1513
1577
  this.panStart = null;
1514
1578
  this.spacePressed = false;
1579
+ this.boundBlur = () => this.handleWindowBlur();
1580
+ this.boundVisibilityChange = () => {
1581
+ if (document.visibilityState === "hidden") this.handleWindowBlur();
1582
+ };
1515
1583
  this.renderer = options.renderer;
1516
1584
  this.zoomSensitivity = options.zoomSensitivity ?? .001;
1517
1585
  this.panButton = options.panButton ?? 1;
1586
+ window.addEventListener("blur", this.boundBlur);
1587
+ document.addEventListener("visibilitychange", this.boundVisibilityChange);
1588
+ }
1589
+ destroy() {
1590
+ window.removeEventListener("blur", this.boundBlur);
1591
+ document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1592
+ }
1593
+ handleWindowBlur() {
1594
+ this.spacePressed = false;
1595
+ if (this.isPanning) this.endPan();
1518
1596
  }
1519
1597
  /**
1520
1598
  * Check if currently panning
@@ -1633,7 +1711,7 @@ var NavigationManager = class extends EventEmitter {
1633
1711
  const actualNewZoom = this.renderer.zoom;
1634
1712
  this.renderer.offsetX = oldOffsetX + worldPos.x * (oldZoom - actualNewZoom);
1635
1713
  this.renderer.offsetY = oldOffsetY + worldPos.y * (oldZoom - actualNewZoom);
1636
- this.emit("zoomChange", this.renderer.zoom, centerPoint);
1714
+ this.emit("zoomChange", this.renderer.zoom, worldPos);
1637
1715
  }
1638
1716
  /**
1639
1717
  * Fit all content in view
@@ -1650,6 +1728,7 @@ var NavigationManager = class extends EventEmitter {
1650
1728
  * Zoom to fit a specific rectangle
1651
1729
  */
1652
1730
  zoomToRect(rect, padding = 50) {
1731
+ if (rect.width <= 0 || rect.height <= 0) return;
1653
1732
  const viewWidth = this.renderer.width - padding * 2;
1654
1733
  const viewHeight = this.renderer.height - padding * 2;
1655
1734
  const scaleX = viewWidth / rect.width;
@@ -2626,12 +2705,24 @@ var ConnectionManager = class extends EventEmitter {
2626
2705
  * Cancel current connection
2627
2706
  */
2628
2707
  cancelConnection() {
2708
+ if (this.isReconnecting) {
2709
+ this.restoreReconnectingEndpoint();
2710
+ this.resetReconnection();
2711
+ this.renderer.markDirty();
2712
+ return;
2713
+ }
2629
2714
  if (!this.isConnecting) return;
2630
2715
  this.emit("connectionEnd", null);
2631
2716
  this.reset();
2632
2717
  this.renderer.markDirty();
2633
2718
  }
2634
2719
  /**
2720
+ * Cancel any in-flight connect or reconnect gesture and restore edge auto-updates.
2721
+ */
2722
+ cancelAll() {
2723
+ this.cancelConnection();
2724
+ }
2725
+ /**
2635
2726
  * Render connection preview
2636
2727
  */
2637
2728
  renderPreview(ctx) {
@@ -2767,6 +2858,11 @@ var ConnectionManager = class extends EventEmitter {
2767
2858
  this.reconnectPoint = null;
2768
2859
  this.setCursor("");
2769
2860
  }
2861
+ restoreReconnectingEndpoint() {
2862
+ if (!this.reconnectingEdge || !this.originalEdgeEndpoint || !this.reconnectingEndpoint) return;
2863
+ if (this.reconnectingEndpoint === "start") this.reconnectingEdge.from = this.originalEdgeEndpoint;
2864
+ else this.reconnectingEdge.to = this.originalEdgeEndpoint;
2865
+ }
2770
2866
  /**
2771
2867
  * Render anchors when hovering a node (not connecting)
2772
2868
  */
@@ -6058,12 +6154,18 @@ var BezierPathStrategy = class {
6058
6154
  hitTest(point, path, tolerance) {
6059
6155
  if (path.length < 4) return false;
6060
6156
  const samples = 20;
6061
- const points = [];
6062
- for (let i = 0; i <= samples; i++) {
6063
- const t = i / samples;
6064
- points.push(bezierPoint(path[0], path[1], path[2], path[3], t));
6157
+ for (let i = 1; i + 2 < path.length; i += 3) {
6158
+ const start = path[i - 1];
6159
+ const control1 = path[i];
6160
+ const control2 = path[i + 1];
6161
+ const end = path[i + 2];
6162
+ let previous = start;
6163
+ for (let sample = 1; sample <= samples; sample++) {
6164
+ const current = bezierPoint(start, control1, control2, end, sample / samples);
6165
+ if (distanceToSegment(point, previous, current) <= tolerance) return true;
6166
+ previous = current;
6167
+ }
6065
6168
  }
6066
- for (let i = 1; i < points.length; i++) if (distanceToSegment(point, points[i - 1], points[i]) <= tolerance) return true;
6067
6169
  return false;
6068
6170
  }
6069
6171
  };
@@ -7730,6 +7832,13 @@ var InteractionManager = class {
7730
7832
  }
7731
7833
  destroy() {
7732
7834
  this.labelEditor.finish(false);
7835
+ this.propertyChangeBatcher.flush();
7836
+ this.dragManager.cancelDrag();
7837
+ this.resizeManager.cancelResize();
7838
+ this.connectionManager.cancelAll();
7839
+ this.selectionManager.cancelSelectionRect();
7840
+ this.navigationManager.destroy();
7841
+ this.resumeEditHover();
7733
7842
  if (this.overlayDragSession) {
7734
7843
  this.renderer.endOverlayDrag(this.overlayDragSession);
7735
7844
  this.overlayDragSession = null;
@@ -8471,6 +8580,15 @@ var AnimationManager = class {
8471
8580
  hasActive() {
8472
8581
  return this.enter.size > 0 || this.exit.size > 0 || this.highlight.size > 0;
8473
8582
  }
8583
+ /**
8584
+ * Drop all in-flight enter/exit/highlight timers without running exit callbacks.
8585
+ * Used by {@link DiagramRenderer.clear} so removed ids do not keep the render loop dirty.
8586
+ */
8587
+ clear() {
8588
+ this.enter.clear();
8589
+ this.exit.clear();
8590
+ this.highlight.clear();
8591
+ }
8474
8592
  update(now = performance.now()) {
8475
8593
  if (!this.options.enabled) return false;
8476
8594
  let changed = false;
@@ -9398,6 +9516,7 @@ var DiagramRenderer = class extends EventEmitter {
9398
9516
  this._contentDirty = true;
9399
9517
  this._contentRevision = 0;
9400
9518
  this._contentBoundsCache = null;
9519
+ this._edgeIndexEndpoints = /* @__PURE__ */ new Map();
9401
9520
  this._destroyed = false;
9402
9521
  this._contextLost = false;
9403
9522
  this._canvasRect = null;
@@ -9754,10 +9873,12 @@ var DiagramRenderer = class extends EventEmitter {
9754
9873
  */
9755
9874
  addEdge(edge) {
9756
9875
  edge.setDirtyListener(() => this.markDirty());
9757
- edge.setBindingListener(() => this.markContentDirty());
9876
+ edge.setBindingListener(() => {
9877
+ this.syncEdgeNodeIndex(edge);
9878
+ this.markContentDirty();
9879
+ });
9758
9880
  this._edges.set(edge.id, edge);
9759
- if (edge.from.nodeId) this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
9760
- if (edge.to.nodeId) this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
9881
+ this.syncEdgeNodeIndex(edge);
9761
9882
  this.animationManager.registerEnter(edge.id);
9762
9883
  this.markContentDirty();
9763
9884
  this.emit("edgeAdd", edge);
@@ -9778,6 +9899,22 @@ var DiagramRenderer = class extends EventEmitter {
9778
9899
  }
9779
9900
  }
9780
9901
  /**
9902
+ * Keep `_nodeEdgeIndex` accurate when an edge is added or its from/to node ids change.
9903
+ */
9904
+ syncEdgeNodeIndex(edge) {
9905
+ const prev = this._edgeIndexEndpoints.get(edge.id);
9906
+ const nextFrom = edge.from.nodeId;
9907
+ const nextTo = edge.to.nodeId;
9908
+ if (prev?.from !== void 0 && prev.from !== nextFrom) this._updateEdgeIndex(edge.id, prev.from, false);
9909
+ if (prev?.to !== void 0 && prev.to !== nextTo) this._updateEdgeIndex(edge.id, prev.to, false);
9910
+ if (nextFrom !== void 0 && nextFrom !== prev?.from) this._updateEdgeIndex(edge.id, nextFrom, true);
9911
+ if (nextTo !== void 0 && nextTo !== prev?.to) this._updateEdgeIndex(edge.id, nextTo, true);
9912
+ this._edgeIndexEndpoints.set(edge.id, {
9913
+ from: nextFrom,
9914
+ to: nextTo
9915
+ });
9916
+ }
9917
+ /**
9781
9918
  * Remove an edge from the diagram
9782
9919
  */
9783
9920
  removeEdge(edgeId) {
@@ -9813,8 +9950,12 @@ var DiagramRenderer = class extends EventEmitter {
9813
9950
  if (other.from.edgeId === edgeId || other.to.edgeId === edgeId) dependents.push(other.id);
9814
9951
  }
9815
9952
  for (const dependentId of dependents) this.removeEdgeImmediate(dependentId);
9816
- if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
9817
- if (edge.to.nodeId) this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
9953
+ const indexed = this._edgeIndexEndpoints.get(edgeId);
9954
+ if (indexed?.from !== void 0) this._updateEdgeIndex(edgeId, indexed.from, false);
9955
+ else if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
9956
+ if (indexed?.to !== void 0) this._updateEdgeIndex(edgeId, indexed.to, false);
9957
+ else if (edge.to.nodeId) this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
9958
+ this._edgeIndexEndpoints.delete(edgeId);
9818
9959
  this._edges.delete(edgeId);
9819
9960
  edge.setDirtyListener(void 0);
9820
9961
  edge.setBindingListener(void 0);
@@ -9984,19 +10125,25 @@ var DiagramRenderer = class extends EventEmitter {
9984
10125
  this._edges.clear();
9985
10126
  this._groups.clear();
9986
10127
  this._nodeEdgeIndex.clear();
10128
+ this._edgeIndexEndpoints.clear();
9987
10129
  }
9988
10130
  /**
9989
10131
  * Clear all elements from the diagram
9990
10132
  */
9991
10133
  clear() {
9992
10134
  for (const node of this._nodes.values()) node.setDirtyListener(void 0);
9993
- for (const edge of this._edges.values()) edge.setDirtyListener(void 0);
10135
+ for (const edge of this._edges.values()) {
10136
+ edge.setDirtyListener(void 0);
10137
+ edge.setBindingListener(void 0);
10138
+ }
9994
10139
  for (const group of this._groups.values()) group.setDirtyListener(void 0);
9995
10140
  this._nodes.clear();
9996
10141
  this._edges.clear();
9997
10142
  this._groups.clear();
9998
10143
  this._nodeEdgeIndex.clear();
9999
- this.markDirty();
10144
+ this._edgeIndexEndpoints.clear();
10145
+ this.animationManager.clear();
10146
+ this.markContentDirty();
10000
10147
  }
10001
10148
  nativePixelRatio() {
10002
10149
  return this.options.retina ? window.devicePixelRatio || 1 : 1;
@@ -10227,6 +10374,7 @@ var SearchManager = class {
10227
10374
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => this.renderHighlights(ctx));
10228
10375
  }
10229
10376
  destroy() {
10377
+ if (this.filterActive) this.clearFilter();
10230
10378
  this.overlayCleanup?.();
10231
10379
  this.overlayCleanup = null;
10232
10380
  this.highlightIds.clear();
@@ -12015,7 +12163,7 @@ var Group = class Group extends Element {
12015
12163
  }
12016
12164
  applyStyleManager(styleManager) {
12017
12165
  const mergedStyle = {
12018
- ...styleManager.getGroupStyle(false, this._styleClass),
12166
+ ...styleManager.getGroupStyle(this._state === "selected", this._styleClass),
12019
12167
  ...this._style
12020
12168
  };
12021
12169
  if (!shallowEqual(this._groupStyle, mergedStyle)) {
@@ -12123,7 +12271,6 @@ var Group = class Group extends Element {
12123
12271
  ctx.fillText(this._label, x + 8, y + 4);
12124
12272
  }
12125
12273
  ctx.globalAlpha = 1;
12126
- for (const child of this._children) if (child.visible) child.render(ctx);
12127
12274
  }
12128
12275
  getBounds() {
12129
12276
  return {
@@ -12260,11 +12407,12 @@ var CircleNode = class extends Node {
12260
12407
  };
12261
12408
  }
12262
12409
  getLabelContainerBounds(bounds) {
12263
- const width = bounds.width / Math.SQRT2;
12264
- const height = bounds.height / Math.SQRT2;
12410
+ const insetBounds = super.getLabelContainerBounds(bounds);
12411
+ const width = insetBounds.width / Math.SQRT2;
12412
+ const height = insetBounds.height / Math.SQRT2;
12265
12413
  return {
12266
- x: bounds.x + (bounds.width - width) / 2,
12267
- y: bounds.y + (bounds.height - height) / 2,
12414
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12415
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12268
12416
  width,
12269
12417
  height
12270
12418
  };
@@ -12421,11 +12569,12 @@ var DiamondNode = class extends Node {
12421
12569
  };
12422
12570
  }
12423
12571
  getLabelContainerBounds(bounds) {
12424
- const width = bounds.width / 2;
12425
- const height = bounds.height / 2;
12572
+ const insetBounds = super.getLabelContainerBounds(bounds);
12573
+ const width = insetBounds.width / 2;
12574
+ const height = insetBounds.height / 2;
12426
12575
  return {
12427
- x: bounds.x + (bounds.width - width) / 2,
12428
- y: bounds.y + (bounds.height - height) / 2,
12576
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12577
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12429
12578
  width,
12430
12579
  height
12431
12580
  };
@@ -13443,16 +13592,33 @@ function flexLayout(container, config, children) {
13443
13592
  finalSizes[i] = finalSizes[i] + remaining * child.flexGrow / totalGrow;
13444
13593
  }
13445
13594
  } else if (remaining < 0) {
13446
- let totalShrink = 0;
13447
- for (let i = 0; i < children.length; i++) totalShrink += children[i].flexShrink * baseSizes[i];
13448
- if (totalShrink > 0) {
13449
- const deficit = -remaining;
13450
- for (let i = 0; i < children.length; i++) {
13595
+ const active = /* @__PURE__ */ new Set();
13596
+ for (let i = 0; i < children.length; i++) {
13597
+ const child = children[i];
13598
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
13599
+ if (child.flexShrink > 0 && finalSizes[i] > minMain) active.add(i);
13600
+ }
13601
+ let deficit = -remaining;
13602
+ const epsilon = 1e-9;
13603
+ while (deficit > epsilon && active.size > 0) {
13604
+ let totalShrink = 0;
13605
+ for (const i of active) totalShrink += children[i].flexShrink * baseSizes[i];
13606
+ if (totalShrink <= 0) break;
13607
+ let distributed = 0;
13608
+ for (const i of active) {
13451
13609
  const child = children[i];
13452
13610
  const shrinkRatio = child.flexShrink * baseSizes[i] / totalShrink;
13453
13611
  const minMain = isRow ? child.minSize.width : child.minSize.height;
13454
- finalSizes[i] = Math.max(minMain, finalSizes[i] - deficit * shrinkRatio);
13612
+ const shrink = Math.min(deficit * shrinkRatio, finalSizes[i] - minMain);
13613
+ finalSizes[i] = finalSizes[i] - shrink;
13614
+ distributed += shrink;
13615
+ if (finalSizes[i] - minMain <= epsilon) {
13616
+ finalSizes[i] = minMain;
13617
+ active.delete(i);
13618
+ }
13455
13619
  }
13620
+ if (distributed <= epsilon) break;
13621
+ deficit -= distributed;
13456
13622
  }
13457
13623
  }
13458
13624
  let actualTotal = totalGaps + totalMargins;
@@ -14141,13 +14307,24 @@ var CompositeNode = class extends Node {
14141
14307
  const fillOpacity = style.fillOpacity ?? 1;
14142
14308
  const strokeOpacity = style.strokeOpacity ?? 1;
14143
14309
  this.applyStyle(ctx);
14144
- ctx.beginPath();
14145
- this.buildShapePath(ctx, x, y, width, height);
14146
- ctx.closePath();
14147
- ctx.globalAlpha = baseOpacity * fillOpacity;
14148
- ctx.fill();
14149
- ctx.globalAlpha = baseOpacity * strokeOpacity;
14150
- ctx.stroke();
14310
+ if (this._shapeType === "custom" && this._pathFactory) {
14311
+ const path = this._pathFactory(width, height);
14312
+ ctx.save();
14313
+ ctx.translate(x, y);
14314
+ ctx.globalAlpha = baseOpacity * fillOpacity;
14315
+ ctx.fill(path);
14316
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
14317
+ ctx.stroke(path);
14318
+ ctx.restore();
14319
+ } else {
14320
+ ctx.beginPath();
14321
+ this.buildShapePath(ctx, x, y, width, height);
14322
+ ctx.closePath();
14323
+ ctx.globalAlpha = baseOpacity * fillOpacity;
14324
+ ctx.fill();
14325
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
14326
+ ctx.stroke();
14327
+ }
14151
14328
  ctx.globalAlpha = 1;
14152
14329
  this.renderContents(ctx);
14153
14330
  }
@@ -14169,15 +14346,6 @@ var CompositeNode = class extends Node {
14169
14346
  break;
14170
14347
  }
14171
14348
  case "custom":
14172
- if (this._pathFactory) {
14173
- const path = this._pathFactory(w, h);
14174
- ctx.save();
14175
- ctx.translate(x, y);
14176
- ctx.fill(path);
14177
- ctx.stroke(path);
14178
- ctx.restore();
14179
- return;
14180
- }
14181
14349
  this.buildRectPath(ctx, x, y, w, h);
14182
14350
  break;
14183
14351
  default:
@@ -14215,35 +14383,40 @@ var CompositeNode = class extends Node {
14215
14383
  }
14216
14384
  applyAutoSize(ctx) {
14217
14385
  const contentSize = this._content.measure(ctx);
14218
- const inset = this.contentInset;
14386
+ const bounds = this.getBounds();
14387
+ const inset = resolveContentInset(this.contentInset, this.contentInsetScale, {
14388
+ width: bounds.width,
14389
+ height: bounds.height
14390
+ }, this.contentInsetBaseSize);
14219
14391
  const neededWidth = Math.max(this._minWidth, contentSize.width + inset.left + inset.right);
14220
14392
  const neededHeight = Math.max(this._minHeight, contentSize.height + inset.top + inset.bottom);
14221
14393
  if (neededWidth > this._width) this._width = neededWidth;
14222
14394
  if (neededHeight > this._height) this._height = neededHeight;
14223
14395
  }
14224
14396
  getLabelContainerBounds(bounds) {
14397
+ const insetBounds = super.getLabelContainerBounds(bounds);
14225
14398
  if (this._shapeType === "circle") {
14226
14399
  const factor = 1 / Math.SQRT2;
14227
- const w = bounds.width * factor;
14228
- const h = bounds.height * factor;
14400
+ const w = insetBounds.width * factor;
14401
+ const h = insetBounds.height * factor;
14229
14402
  return {
14230
- x: bounds.x + (bounds.width - w) / 2,
14231
- y: bounds.y + (bounds.height - h) / 2,
14403
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14404
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14232
14405
  width: w,
14233
14406
  height: h
14234
14407
  };
14235
14408
  }
14236
14409
  if (this._shapeType === "diamond") {
14237
- const w = bounds.width / 2;
14238
- const h = bounds.height / 2;
14410
+ const w = insetBounds.width / 2;
14411
+ const h = insetBounds.height / 2;
14239
14412
  return {
14240
- x: bounds.x + (bounds.width - w) / 2,
14241
- y: bounds.y + (bounds.height - h) / 2,
14413
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14414
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14242
14415
  width: w,
14243
14416
  height: h
14244
14417
  };
14245
14418
  }
14246
- return super.getLabelContainerBounds(bounds);
14419
+ return insetBounds;
14247
14420
  }
14248
14421
  /**
14249
14422
  * Get the minimum content size needed for the content tree.
@@ -14451,16 +14624,22 @@ var DARK_THEME = {
14451
14624
  }
14452
14625
  }
14453
14626
  };
14627
+ function cloneTheme(theme) {
14628
+ return structuredClone(theme);
14629
+ }
14454
14630
  /**
14455
14631
  * Manages styles and themes
14456
14632
  */
14457
14633
  var StyleManager = class {
14458
14634
  constructor(theme) {
14459
14635
  this._classes = /* @__PURE__ */ new Map();
14460
- this._builtInThemes = new Map([["default", DEFAULT_THEME], ["dark", DARK_THEME]]);
14461
- if (theme === void 0) this._theme = DEFAULT_THEME;
14462
- else if (typeof theme === "string") this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
14463
- else this._theme = theme;
14636
+ this._builtInThemes = new Map([["default", cloneTheme(DEFAULT_THEME)], ["dark", cloneTheme(DARK_THEME)]]);
14637
+ const initialTheme = theme ?? "default";
14638
+ if (typeof initialTheme === "string") {
14639
+ const registeredTheme = this._builtInThemes.get(initialTheme);
14640
+ if (registeredTheme === void 0) throw new Error(`Unknown theme: ${initialTheme}`);
14641
+ this._theme = cloneTheme(registeredTheme);
14642
+ } else this._theme = cloneTheme(initialTheme);
14464
14643
  }
14465
14644
  /**
14466
14645
  * Current theme
@@ -14474,14 +14653,15 @@ var StyleManager = class {
14474
14653
  setTheme(theme) {
14475
14654
  if (typeof theme === "string") {
14476
14655
  const builtIn = this._builtInThemes.get(theme);
14477
- if (builtIn !== void 0) this._theme = builtIn;
14478
- } else this._theme = theme;
14656
+ if (builtIn === void 0) throw new Error(`Unknown theme: ${theme}`);
14657
+ this._theme = cloneTheme(builtIn);
14658
+ } else this._theme = cloneTheme(theme);
14479
14659
  }
14480
14660
  /**
14481
14661
  * Register a custom theme
14482
14662
  */
14483
14663
  registerTheme(theme) {
14484
- this._builtInThemes.set(theme.name, theme);
14664
+ this._builtInThemes.set(theme.name, cloneTheme(theme));
14485
14665
  }
14486
14666
  /**
14487
14667
  * Get all available theme names
@@ -14596,10 +14776,17 @@ var StyleManager = class {
14596
14776
  }
14597
14777
  }
14598
14778
  getBaseEdgeStyle(state) {
14779
+ const defaultStyle = this._theme.edge.default;
14599
14780
  switch (state) {
14600
- case "hover": return this._theme.edge.hover;
14601
- case "selected": return this._theme.edge.selected;
14602
- default: return this._theme.edge.default;
14781
+ case "hover": return {
14782
+ ...defaultStyle,
14783
+ ...this._theme.edge.hover
14784
+ };
14785
+ case "selected": return {
14786
+ ...defaultStyle,
14787
+ ...this._theme.edge.selected
14788
+ };
14789
+ default: return defaultStyle;
14603
14790
  }
14604
14791
  }
14605
14792
  };
@@ -14771,36 +14958,7 @@ var Serializer = class {
14771
14958
  position: port.position,
14772
14959
  styleClass: port.styleClass
14773
14960
  }));
14774
- let label;
14775
- if (node.label) {
14776
- const ins = node.label.inset;
14777
- const defaultInset = 8;
14778
- const serializedInset = ins.top === defaultInset && ins.right === defaultInset && ins.bottom === defaultInset && ins.left === defaultInset ? void 0 : ins.top === ins.right && ins.top === ins.bottom && ins.top === ins.left ? ins.top : {
14779
- top: ins.top,
14780
- right: ins.right,
14781
- bottom: ins.bottom,
14782
- left: ins.left
14783
- };
14784
- if (hasNonDefaultValues({
14785
- text: node.label.text,
14786
- style: node.label.style,
14787
- maxWidth: node.label.maxWidth,
14788
- inset: serializedInset ?? void 0,
14789
- styleClass: node.label.styleClass
14790
- }, {
14791
- inset: void 0,
14792
- style: {},
14793
- maxWidth: void 0,
14794
- styleClass: void 0
14795
- })) label = omitEmptyValues({
14796
- text: node.label.text,
14797
- style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
14798
- maxWidth: node.label.maxWidth,
14799
- inset: serializedInset,
14800
- styleClass: node.label.styleClass
14801
- });
14802
- else label = node.label.text;
14803
- }
14961
+ const label = node.label ? this.serializeTextLabel(node.label) : void 0;
14804
14962
  let icon;
14805
14963
  if (node.icon) {
14806
14964
  const opts = node.icon.options;
@@ -14833,6 +14991,8 @@ var Serializer = class {
14833
14991
  if (hasNonDefaultValues(anchorData, anchorDefaults)) anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
14834
14992
  const contentInset = node.contentInset;
14835
14993
  const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
14994
+ const contentInsetScale = node.contentInsetScale;
14995
+ const hasContentInsetScale = Object.keys(contentInsetScale).length > 0;
14836
14996
  const base = omitEmptyValues({
14837
14997
  id: node.id,
14838
14998
  type: node.typeName,
@@ -14840,14 +15000,16 @@ var Serializer = class {
14840
15000
  y: node.y,
14841
15001
  width: node.width,
14842
15002
  height: node.height,
14843
- style: node.style,
15003
+ style: { ...node.styleOverrides },
14844
15004
  styleClass: node.styleClass,
14845
15005
  label,
14846
- labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
14847
15006
  labelPlacement: node.labelPlacement !== "center" ? node.labelPlacement : void 0,
14848
15007
  labelGap: node.labelGap !== 4 ? node.labelGap : void 0,
14849
15008
  icon,
14850
15009
  contentInset: hasContentInset ? contentInset : void 0,
15010
+ contentInsetScale: hasContentInsetScale ? contentInsetScale : void 0,
15011
+ contentInsetBaseSize: node.contentInsetBaseSize,
15012
+ badges: node.badges.length > 0 ? node.badges.map((badge) => ({ ...badge })) : void 0,
14851
15013
  anchorPoints,
14852
15014
  ports: ports.length > 0 ? ports : void 0,
14853
15015
  data: Object.keys(node.data).length > 0 ? node.data : void 0
@@ -14874,14 +15036,14 @@ var Serializer = class {
14874
15036
  arrowType: edge.arrowType,
14875
15037
  startMarker: edge.startMarker,
14876
15038
  endMarker: edge.endMarker,
14877
- style: edge.style,
15039
+ style: { ...edge.styleOverrides },
14878
15040
  styleClass: edge.styleClass,
14879
- label: edge.label?.text,
14880
- labelStyleClass: edge.label?.styleClass,
15041
+ label: edge.label ? this.serializeTextLabel(edge.label) : void 0,
14881
15042
  labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
14882
15043
  labelPosition: edge.labelPosition !== .5 ? edge.labelPosition : void 0,
14883
15044
  labelFollowPath: edge.labelFollowPath ? true : void 0,
14884
15045
  labelBackground: edge.labelBackground,
15046
+ lockAnchors: edge.lockAnchors ? void 0 : false,
14885
15047
  labelLineGap: edge.labelLineGap ? true : void 0,
14886
15048
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
14887
15049
  };
@@ -14890,12 +15052,31 @@ var Serializer = class {
14890
15052
  return {
14891
15053
  id: group.id,
14892
15054
  childIds: group.children.map((c) => c.id),
14893
- style: group.style,
15055
+ style: { ...group.styleOverrides },
14894
15056
  styleClass: group.styleClass,
14895
15057
  label: group.label,
15058
+ padding: group.padding !== 20 ? group.padding : void 0,
14896
15059
  data: Object.keys(group.data).length > 0 ? group.data : void 0
14897
15060
  };
14898
15061
  }
15062
+ serializeTextLabel(label) {
15063
+ const inset = label.inset;
15064
+ const serializedInset = inset.top === 8 && inset.right === 8 && inset.bottom === 8 && inset.left === 8 ? void 0 : inset.top === inset.right && inset.top === inset.bottom && inset.top === inset.left ? inset.top : {
15065
+ top: inset.top,
15066
+ right: inset.right,
15067
+ bottom: inset.bottom,
15068
+ left: inset.left
15069
+ };
15070
+ const style = label.styleOverrides;
15071
+ if (!(Object.keys(style).length > 0 || label.maxWidth !== void 0 || serializedInset !== void 0 || label.styleClass !== void 0)) return label.text;
15072
+ return omitEmptyValues({
15073
+ text: label.text,
15074
+ style: Object.keys(style).length > 0 ? style : void 0,
15075
+ maxWidth: label.maxWidth,
15076
+ inset: serializedInset,
15077
+ styleClass: label.styleClass
15078
+ });
15079
+ }
14899
15080
  };
14900
15081
  //#endregion
14901
15082
  //#region src/utils/download.ts
@@ -14977,7 +15158,11 @@ var ImageExporter = class {
14977
15158
  const padding = options.padding ?? 20;
14978
15159
  const format = options.format ?? "png";
14979
15160
  const quality = options.quality ?? .92;
14980
- const bounds = getContentBounds({ nodes: this.renderer.nodes.values() });
15161
+ const bounds = getContentBounds({
15162
+ nodes: this.renderer.nodes.values(),
15163
+ edges: this.renderer.edges.values(),
15164
+ groups: this.renderer.groups.values()
15165
+ });
14981
15166
  if (bounds === null) return this.createBlankImage(100, 100, backgroundColor, format, quality);
14982
15167
  const width = (bounds.width + padding * 2) * scale;
14983
15168
  const height = (bounds.height + padding * 2) * scale;
@@ -15029,6 +15214,7 @@ var SvgExporter = class {
15029
15214
  const padding = options.padding ?? 20;
15030
15215
  const includeBackground = options.includeBackground ?? true;
15031
15216
  const backgroundColor = options.backgroundColor ?? "#ffffff";
15217
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
15032
15218
  const edgeLabelOffset = options.edgeLabelOffset;
15033
15219
  const bounds = getContentBounds({
15034
15220
  nodes: this.renderer.nodes.values(),
@@ -15045,7 +15231,7 @@ var SvgExporter = class {
15045
15231
  const renderedEdges = [];
15046
15232
  parts.push(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`);
15047
15233
  for (const edge of this.renderer.edges.values()) if (edge.visible) renderedEdges.push(this.renderEdge(edge, edgeLabelOffset));
15048
- if (includeBackground) parts.push(`<rect width="100%" height="100%" fill="${backgroundColor}"/>`);
15234
+ if (includeBackground) parts.push(`<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>`);
15049
15235
  parts.push(`<g transform="translate(${offsetX}, ${offsetY})">`);
15050
15236
  for (const group of this.renderer.groups.values()) if (group.visible) parts.push(this.renderGroup(group));
15051
15237
  for (const node of this.renderer.nodes.values()) if (node.visible) parts.push(this.renderNode(node));
@@ -15064,8 +15250,8 @@ var SvgExporter = class {
15064
15250
  renderGroup(group) {
15065
15251
  const bounds = group.getBounds();
15066
15252
  const style = group.style;
15067
- const fill = style.fillColor ?? "rgba(200, 200, 200, 0.2)";
15068
- const stroke = style.strokeColor ?? "#999999";
15253
+ const fill = this.escapeAttribute(style.fillColor ?? "rgba(200, 200, 200, 0.2)");
15254
+ const stroke = this.escapeAttribute(style.strokeColor ?? "#999999");
15069
15255
  const strokeWidth = style.strokeWidth ?? 1;
15070
15256
  const opacity = style.opacity ?? 1;
15071
15257
  const label = group.label ? `<text x="${bounds.x + 8}" y="${bounds.y + 14}" fill="#666666" font-size="12">${this.escapeText(group.label)}</text>` : "";
@@ -15079,8 +15265,8 @@ var SvgExporter = class {
15079
15265
  renderNode(node) {
15080
15266
  const bounds = node.getBounds();
15081
15267
  const style = node.style;
15082
- const fill = style.fillColor ?? "#ffffff";
15083
- const stroke = style.strokeColor ?? "#333333";
15268
+ const fill = this.escapeAttribute(style.fillColor ?? "#ffffff");
15269
+ const stroke = this.escapeAttribute(style.strokeColor ?? "#333333");
15084
15270
  const strokeWidth = style.strokeWidth ?? 2;
15085
15271
  const baseOpacity = style.opacity ?? 1;
15086
15272
  const fillOpacity = (style.fillOpacity ?? 1) * baseOpacity;
@@ -15177,14 +15363,15 @@ var SvgExporter = class {
15177
15363
  if (edge.path.length < 2) return "";
15178
15364
  const style = edge.style;
15179
15365
  const stroke = style.strokeColor ?? "#666666";
15366
+ const escapedStroke = this.escapeAttribute(stroke);
15180
15367
  const strokeWidth = style.strokeWidth ?? 2;
15181
15368
  const strokeOpacity = (style.strokeOpacity ?? 1) * (style.opacity ?? 1);
15182
- const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
15183
- const lineJoin = style.lineJoin ? ` stroke-linejoin="${style.lineJoin}"` : "";
15369
+ const lineCap = style.lineCap ? ` stroke-linecap="${this.escapeAttribute(style.lineCap)}"` : "";
15370
+ const lineJoin = style.lineJoin ? ` stroke-linejoin="${this.escapeAttribute(style.lineJoin)}"` : "";
15184
15371
  const dashValues = style.flowDash ?? style.lineDash;
15185
15372
  const dash = dashValues ? ` stroke-dasharray="${dashValues.join(" ")}"` : "";
15186
15373
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
15187
- return `<path d="${this.buildEdgePath(edge, edgeLabelOffset)}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}" color="${stroke}"${lineCap}${lineJoin}${dash}${dashOffset}/>${this.renderEdgeMarkers(edge, stroke)}${this.renderEdgeLabel(edge, edgeLabelOffset)}`;
15374
+ return `<path d="${this.buildEdgePath(edge, edgeLabelOffset)}" fill="none" stroke="${escapedStroke}" stroke-width="${strokeWidth}" stroke-opacity="${strokeOpacity}" color="${escapedStroke}"${lineCap}${lineJoin}${dash}${dashOffset}/>${this.renderEdgeMarkers(edge, stroke)}${this.renderEdgeLabel(edge, edgeLabelOffset)}`;
15188
15375
  }
15189
15376
  renderEdgeLabel(edge, edgeLabelOffset) {
15190
15377
  if (!edge.label) return "";
@@ -15196,7 +15383,7 @@ var SvgExporter = class {
15196
15383
  renderEdgeLabelBackground(edge, point, rotation = 0) {
15197
15384
  if (!edge.label) return "";
15198
15385
  const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
15199
- const bgColor = edge.labelBackground?.color ?? "#ffffff";
15386
+ const bgColor = this.escapeAttribute(edge.labelBackground?.color ?? "#ffffff");
15200
15387
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
15201
15388
  const bgRadius = edge.labelBackground?.borderRadius ?? 2;
15202
15389
  const x = point.x - metrics.width / 2;
@@ -15277,7 +15464,11 @@ var SvgExporter = class {
15277
15464
  return calculateMarkerPoints(edge.path, position, edge.type);
15278
15465
  }
15279
15466
  renderMarkerShape(marker, from, to, edgeStroke) {
15280
- return generateSvgMarker(marker, from, to, edgeStroke);
15467
+ return generateSvgMarker({
15468
+ ...marker,
15469
+ strokeColor: marker.strokeColor !== void 0 ? this.escapeAttribute(marker.strokeColor) : void 0,
15470
+ fillColor: marker.fillColor !== void 0 ? this.escapeAttribute(marker.fillColor) : void 0
15471
+ }, from, to, this.escapeAttribute(edgeStroke));
15281
15472
  }
15282
15473
  buildPath(edge) {
15283
15474
  const path = edge.path;
@@ -15347,11 +15538,11 @@ var SvgExporter = class {
15347
15538
  };
15348
15539
  }
15349
15540
  renderTextLabel(text, point, style = {}, rotation = 0) {
15350
- const fill = style.color ?? "#000000";
15541
+ const fill = this.escapeAttribute(style.color ?? "#000000");
15351
15542
  const fontSize = style.fontSize ?? 14;
15352
- const fontFamily = style.fontFamily ?? "sans-serif";
15353
- const fontWeight = style.fontWeight ?? "normal";
15354
- const fontStyle = style.fontStyle ?? "normal";
15543
+ const fontFamily = this.escapeAttribute(style.fontFamily ?? "sans-serif");
15544
+ const fontWeight = this.escapeAttribute(style.fontWeight ?? "normal");
15545
+ const fontStyle = this.escapeAttribute(style.fontStyle ?? "normal");
15355
15546
  const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
15356
15547
  const opacity = style.opacity ?? 1;
15357
15548
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
@@ -15547,9 +15738,10 @@ var SvgExporter = class {
15547
15738
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15548
15739
  }
15549
15740
  createEmptySvg(width, height, backgroundColor, includeBackground) {
15741
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
15550
15742
  return [
15551
15743
  `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
15552
- includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
15744
+ includeBackground ? `<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>` : "",
15553
15745
  "</svg>"
15554
15746
  ].join("");
15555
15747
  }