@ngroznykh/papirus 0.9.13 → 0.9.15

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 (51) 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/ResizeManager.d.ts +5 -0
  15. package/dist/core/ResizeManager.d.ts.map +1 -1
  16. package/dist/core/SearchManager.d.ts.map +1 -1
  17. package/dist/core/SelectionManager.d.ts +1 -0
  18. package/dist/core/SelectionManager.d.ts.map +1 -1
  19. package/dist/elements/Group.d.ts.map +1 -1
  20. package/dist/elements/Node.d.ts +4 -2
  21. package/dist/elements/Node.d.ts.map +1 -1
  22. package/dist/elements/composite/CComponent.d.ts +4 -2
  23. package/dist/elements/composite/CComponent.d.ts.map +1 -1
  24. package/dist/elements/composite/CContainer.d.ts +1 -1
  25. package/dist/elements/composite/CContainer.d.ts.map +1 -1
  26. package/dist/elements/composite/CShape.d.ts +1 -1
  27. package/dist/elements/composite/CShape.d.ts.map +1 -1
  28. package/dist/elements/composite/CText.d.ts +1 -1
  29. package/dist/elements/composite/CText.d.ts.map +1 -1
  30. package/dist/elements/composite/CompositeComponentBase.d.ts +1 -1
  31. package/dist/elements/composite/CompositeComponentBase.d.ts.map +1 -1
  32. package/dist/elements/composite/CompositeNode.d.ts +5 -1
  33. package/dist/elements/composite/CompositeNode.d.ts.map +1 -1
  34. package/dist/elements/composite/FlexLayout.d.ts.map +1 -1
  35. package/dist/elements/nodes/CircleNode.d.ts.map +1 -1
  36. package/dist/elements/nodes/DiamondNode.d.ts.map +1 -1
  37. package/dist/elements/paths/BezierPathStrategy.d.ts.map +1 -1
  38. package/dist/events/EventEmitter.d.ts +2 -3
  39. package/dist/events/EventEmitter.d.ts.map +1 -1
  40. package/dist/events/InputHandler.d.ts +11 -2
  41. package/dist/events/InputHandler.d.ts.map +1 -1
  42. package/dist/papirus.js +408 -167
  43. package/dist/papirus.js.map +1 -1
  44. package/dist/styles/StyleManager.d.ts.map +1 -1
  45. package/dist/types.d.ts +17 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/utils/ImageExporter.d.ts.map +1 -1
  48. package/dist/utils/Serializer.d.ts +1 -0
  49. package/dist/utils/Serializer.d.ts.map +1 -1
  50. package/dist/utils/SvgExporter.d.ts.map +1 -1
  51. 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);
@@ -1317,10 +1381,17 @@ var ResizeManager = class extends EventEmitter {
1317
1381
  this.handle = hit;
1318
1382
  this.startPoint = point;
1319
1383
  this.startBounds = node.getBounds();
1320
- const contentMinSize = node.getContentMinSize(this.renderer.getContext());
1384
+ this.updateMinSize(node, this.startBounds.width);
1385
+ return true;
1386
+ }
1387
+ /**
1388
+ * Recompute content minimums at the given width: as the node narrows, text
1389
+ * wraps and the height floor grows to fit the wrapped lines.
1390
+ */
1391
+ updateMinSize(node, width) {
1392
+ const contentMinSize = node.getContentMinSize(this.renderer.getContext(), width);
1321
1393
  this.minWidth = Math.max(this.minSize, contentMinSize.width);
1322
1394
  this.minHeight = Math.max(this.minSize, contentMinSize.height);
1323
- return true;
1324
1395
  }
1325
1396
  /**
1326
1397
  * Set canvas cursor when the pointer is over a selected node's resize handle.
@@ -1350,14 +1421,19 @@ var ResizeManager = class extends EventEmitter {
1350
1421
  node.state = "dragging";
1351
1422
  this.emit("resizeStart", node.id, this.handle, this.startBounds);
1352
1423
  }
1424
+ const dx = point.x - this.startPoint.x;
1425
+ const prospectiveWidth = this.handle.includes("w") ? this.startBounds.width - dx : this.startBounds.width + dx;
1426
+ this.updateMinSize(node, prospectiveWidth);
1353
1427
  const bounds = this.calculateBounds(point);
1354
- node.x = bounds.x;
1355
- node.y = bounds.y;
1356
- node.width = bounds.width;
1357
- node.height = bounds.height;
1358
- this.emit("resize", node.id, bounds, {
1359
- x: bounds.x - this.startBounds.x,
1360
- y: bounds.y - this.startBounds.y
1428
+ this.updateMinSize(node, bounds.width);
1429
+ const clamped = this.applyMinSize(bounds.x, bounds.y, bounds.width, bounds.height, this.startBounds, this.handle);
1430
+ node.x = clamped.x;
1431
+ node.y = clamped.y;
1432
+ node.width = clamped.width;
1433
+ node.height = clamped.height;
1434
+ this.emit("resize", node.id, clamped, {
1435
+ x: clamped.x - this.startBounds.x,
1436
+ y: clamped.y - this.startBounds.y
1361
1437
  });
1362
1438
  this.renderer.markDirty();
1363
1439
  return true;
@@ -1512,9 +1588,23 @@ var NavigationManager = class extends EventEmitter {
1512
1588
  this.isPanning = false;
1513
1589
  this.panStart = null;
1514
1590
  this.spacePressed = false;
1591
+ this.boundBlur = () => this.handleWindowBlur();
1592
+ this.boundVisibilityChange = () => {
1593
+ if (document.visibilityState === "hidden") this.handleWindowBlur();
1594
+ };
1515
1595
  this.renderer = options.renderer;
1516
1596
  this.zoomSensitivity = options.zoomSensitivity ?? .001;
1517
1597
  this.panButton = options.panButton ?? 1;
1598
+ window.addEventListener("blur", this.boundBlur);
1599
+ document.addEventListener("visibilitychange", this.boundVisibilityChange);
1600
+ }
1601
+ destroy() {
1602
+ window.removeEventListener("blur", this.boundBlur);
1603
+ document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1604
+ }
1605
+ handleWindowBlur() {
1606
+ this.spacePressed = false;
1607
+ if (this.isPanning) this.endPan();
1518
1608
  }
1519
1609
  /**
1520
1610
  * Check if currently panning
@@ -1633,7 +1723,7 @@ var NavigationManager = class extends EventEmitter {
1633
1723
  const actualNewZoom = this.renderer.zoom;
1634
1724
  this.renderer.offsetX = oldOffsetX + worldPos.x * (oldZoom - actualNewZoom);
1635
1725
  this.renderer.offsetY = oldOffsetY + worldPos.y * (oldZoom - actualNewZoom);
1636
- this.emit("zoomChange", this.renderer.zoom, centerPoint);
1726
+ this.emit("zoomChange", this.renderer.zoom, worldPos);
1637
1727
  }
1638
1728
  /**
1639
1729
  * Fit all content in view
@@ -1650,6 +1740,7 @@ var NavigationManager = class extends EventEmitter {
1650
1740
  * Zoom to fit a specific rectangle
1651
1741
  */
1652
1742
  zoomToRect(rect, padding = 50) {
1743
+ if (rect.width <= 0 || rect.height <= 0) return;
1653
1744
  const viewWidth = this.renderer.width - padding * 2;
1654
1745
  const viewHeight = this.renderer.height - padding * 2;
1655
1746
  const scaleX = viewWidth / rect.width;
@@ -2626,12 +2717,24 @@ var ConnectionManager = class extends EventEmitter {
2626
2717
  * Cancel current connection
2627
2718
  */
2628
2719
  cancelConnection() {
2720
+ if (this.isReconnecting) {
2721
+ this.restoreReconnectingEndpoint();
2722
+ this.resetReconnection();
2723
+ this.renderer.markDirty();
2724
+ return;
2725
+ }
2629
2726
  if (!this.isConnecting) return;
2630
2727
  this.emit("connectionEnd", null);
2631
2728
  this.reset();
2632
2729
  this.renderer.markDirty();
2633
2730
  }
2634
2731
  /**
2732
+ * Cancel any in-flight connect or reconnect gesture and restore edge auto-updates.
2733
+ */
2734
+ cancelAll() {
2735
+ this.cancelConnection();
2736
+ }
2737
+ /**
2635
2738
  * Render connection preview
2636
2739
  */
2637
2740
  renderPreview(ctx) {
@@ -2767,6 +2870,11 @@ var ConnectionManager = class extends EventEmitter {
2767
2870
  this.reconnectPoint = null;
2768
2871
  this.setCursor("");
2769
2872
  }
2873
+ restoreReconnectingEndpoint() {
2874
+ if (!this.reconnectingEdge || !this.originalEdgeEndpoint || !this.reconnectingEndpoint) return;
2875
+ if (this.reconnectingEndpoint === "start") this.reconnectingEdge.from = this.originalEdgeEndpoint;
2876
+ else this.reconnectingEdge.to = this.originalEdgeEndpoint;
2877
+ }
2770
2878
  /**
2771
2879
  * Render anchors when hovering a node (not connecting)
2772
2880
  */
@@ -6058,12 +6166,18 @@ var BezierPathStrategy = class {
6058
6166
  hitTest(point, path, tolerance) {
6059
6167
  if (path.length < 4) return false;
6060
6168
  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));
6169
+ for (let i = 1; i + 2 < path.length; i += 3) {
6170
+ const start = path[i - 1];
6171
+ const control1 = path[i];
6172
+ const control2 = path[i + 1];
6173
+ const end = path[i + 2];
6174
+ let previous = start;
6175
+ for (let sample = 1; sample <= samples; sample++) {
6176
+ const current = bezierPoint(start, control1, control2, end, sample / samples);
6177
+ if (distanceToSegment(point, previous, current) <= tolerance) return true;
6178
+ previous = current;
6179
+ }
6065
6180
  }
6066
- for (let i = 1; i < points.length; i++) if (distanceToSegment(point, points[i - 1], points[i]) <= tolerance) return true;
6067
6181
  return false;
6068
6182
  }
6069
6183
  };
@@ -7730,6 +7844,13 @@ var InteractionManager = class {
7730
7844
  }
7731
7845
  destroy() {
7732
7846
  this.labelEditor.finish(false);
7847
+ this.propertyChangeBatcher.flush();
7848
+ this.dragManager.cancelDrag();
7849
+ this.resizeManager.cancelResize();
7850
+ this.connectionManager.cancelAll();
7851
+ this.selectionManager.cancelSelectionRect();
7852
+ this.navigationManager.destroy();
7853
+ this.resumeEditHover();
7733
7854
  if (this.overlayDragSession) {
7734
7855
  this.renderer.endOverlayDrag(this.overlayDragSession);
7735
7856
  this.overlayDragSession = null;
@@ -8471,6 +8592,15 @@ var AnimationManager = class {
8471
8592
  hasActive() {
8472
8593
  return this.enter.size > 0 || this.exit.size > 0 || this.highlight.size > 0;
8473
8594
  }
8595
+ /**
8596
+ * Drop all in-flight enter/exit/highlight timers without running exit callbacks.
8597
+ * Used by {@link DiagramRenderer.clear} so removed ids do not keep the render loop dirty.
8598
+ */
8599
+ clear() {
8600
+ this.enter.clear();
8601
+ this.exit.clear();
8602
+ this.highlight.clear();
8603
+ }
8474
8604
  update(now = performance.now()) {
8475
8605
  if (!this.options.enabled) return false;
8476
8606
  let changed = false;
@@ -9398,6 +9528,7 @@ var DiagramRenderer = class extends EventEmitter {
9398
9528
  this._contentDirty = true;
9399
9529
  this._contentRevision = 0;
9400
9530
  this._contentBoundsCache = null;
9531
+ this._edgeIndexEndpoints = /* @__PURE__ */ new Map();
9401
9532
  this._destroyed = false;
9402
9533
  this._contextLost = false;
9403
9534
  this._canvasRect = null;
@@ -9754,10 +9885,12 @@ var DiagramRenderer = class extends EventEmitter {
9754
9885
  */
9755
9886
  addEdge(edge) {
9756
9887
  edge.setDirtyListener(() => this.markDirty());
9757
- edge.setBindingListener(() => this.markContentDirty());
9888
+ edge.setBindingListener(() => {
9889
+ this.syncEdgeNodeIndex(edge);
9890
+ this.markContentDirty();
9891
+ });
9758
9892
  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);
9893
+ this.syncEdgeNodeIndex(edge);
9761
9894
  this.animationManager.registerEnter(edge.id);
9762
9895
  this.markContentDirty();
9763
9896
  this.emit("edgeAdd", edge);
@@ -9778,6 +9911,22 @@ var DiagramRenderer = class extends EventEmitter {
9778
9911
  }
9779
9912
  }
9780
9913
  /**
9914
+ * Keep `_nodeEdgeIndex` accurate when an edge is added or its from/to node ids change.
9915
+ */
9916
+ syncEdgeNodeIndex(edge) {
9917
+ const prev = this._edgeIndexEndpoints.get(edge.id);
9918
+ const nextFrom = edge.from.nodeId;
9919
+ const nextTo = edge.to.nodeId;
9920
+ if (prev?.from !== void 0 && prev.from !== nextFrom) this._updateEdgeIndex(edge.id, prev.from, false);
9921
+ if (prev?.to !== void 0 && prev.to !== nextTo) this._updateEdgeIndex(edge.id, prev.to, false);
9922
+ if (nextFrom !== void 0 && nextFrom !== prev?.from) this._updateEdgeIndex(edge.id, nextFrom, true);
9923
+ if (nextTo !== void 0 && nextTo !== prev?.to) this._updateEdgeIndex(edge.id, nextTo, true);
9924
+ this._edgeIndexEndpoints.set(edge.id, {
9925
+ from: nextFrom,
9926
+ to: nextTo
9927
+ });
9928
+ }
9929
+ /**
9781
9930
  * Remove an edge from the diagram
9782
9931
  */
9783
9932
  removeEdge(edgeId) {
@@ -9813,8 +9962,12 @@ var DiagramRenderer = class extends EventEmitter {
9813
9962
  if (other.from.edgeId === edgeId || other.to.edgeId === edgeId) dependents.push(other.id);
9814
9963
  }
9815
9964
  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);
9965
+ const indexed = this._edgeIndexEndpoints.get(edgeId);
9966
+ if (indexed?.from !== void 0) this._updateEdgeIndex(edgeId, indexed.from, false);
9967
+ else if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
9968
+ if (indexed?.to !== void 0) this._updateEdgeIndex(edgeId, indexed.to, false);
9969
+ else if (edge.to.nodeId) this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
9970
+ this._edgeIndexEndpoints.delete(edgeId);
9818
9971
  this._edges.delete(edgeId);
9819
9972
  edge.setDirtyListener(void 0);
9820
9973
  edge.setBindingListener(void 0);
@@ -9984,19 +10137,25 @@ var DiagramRenderer = class extends EventEmitter {
9984
10137
  this._edges.clear();
9985
10138
  this._groups.clear();
9986
10139
  this._nodeEdgeIndex.clear();
10140
+ this._edgeIndexEndpoints.clear();
9987
10141
  }
9988
10142
  /**
9989
10143
  * Clear all elements from the diagram
9990
10144
  */
9991
10145
  clear() {
9992
10146
  for (const node of this._nodes.values()) node.setDirtyListener(void 0);
9993
- for (const edge of this._edges.values()) edge.setDirtyListener(void 0);
10147
+ for (const edge of this._edges.values()) {
10148
+ edge.setDirtyListener(void 0);
10149
+ edge.setBindingListener(void 0);
10150
+ }
9994
10151
  for (const group of this._groups.values()) group.setDirtyListener(void 0);
9995
10152
  this._nodes.clear();
9996
10153
  this._edges.clear();
9997
10154
  this._groups.clear();
9998
10155
  this._nodeEdgeIndex.clear();
9999
- this.markDirty();
10156
+ this._edgeIndexEndpoints.clear();
10157
+ this.animationManager.clear();
10158
+ this.markContentDirty();
10000
10159
  }
10001
10160
  nativePixelRatio() {
10002
10161
  return this.options.retina ? window.devicePixelRatio || 1 : 1;
@@ -10227,6 +10386,7 @@ var SearchManager = class {
10227
10386
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => this.renderHighlights(ctx));
10228
10387
  }
10229
10388
  destroy() {
10389
+ if (this.filterActive) this.clearFilter();
10230
10390
  this.overlayCleanup?.();
10231
10391
  this.overlayCleanup = null;
10232
10392
  this.highlightIds.clear();
@@ -11490,9 +11650,11 @@ var Node = class extends Element {
11490
11650
  }
11491
11651
  }
11492
11652
  /**
11493
- * Minimal size required to fit current contents
11653
+ * Minimal size required to fit current contents.
11654
+ * Optional availableWidth enables width-constrained measurement
11655
+ * (composite content wraps text to fit the given width).
11494
11656
  */
11495
- getContentMinSize(ctx) {
11657
+ getContentMinSize(ctx, _availableWidth) {
11496
11658
  const bounds = this.getBounds();
11497
11659
  const labelSize = this._label ? this._label.measure(ctx) : void 0;
11498
11660
  const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
@@ -12015,7 +12177,7 @@ var Group = class Group extends Element {
12015
12177
  }
12016
12178
  applyStyleManager(styleManager) {
12017
12179
  const mergedStyle = {
12018
- ...styleManager.getGroupStyle(false, this._styleClass),
12180
+ ...styleManager.getGroupStyle(this._state === "selected", this._styleClass),
12019
12181
  ...this._style
12020
12182
  };
12021
12183
  if (!shallowEqual(this._groupStyle, mergedStyle)) {
@@ -12123,7 +12285,6 @@ var Group = class Group extends Element {
12123
12285
  ctx.fillText(this._label, x + 8, y + 4);
12124
12286
  }
12125
12287
  ctx.globalAlpha = 1;
12126
- for (const child of this._children) if (child.visible) child.render(ctx);
12127
12288
  }
12128
12289
  getBounds() {
12129
12290
  return {
@@ -12260,11 +12421,12 @@ var CircleNode = class extends Node {
12260
12421
  };
12261
12422
  }
12262
12423
  getLabelContainerBounds(bounds) {
12263
- const width = bounds.width / Math.SQRT2;
12264
- const height = bounds.height / Math.SQRT2;
12424
+ const insetBounds = super.getLabelContainerBounds(bounds);
12425
+ const width = insetBounds.width / Math.SQRT2;
12426
+ const height = insetBounds.height / Math.SQRT2;
12265
12427
  return {
12266
- x: bounds.x + (bounds.width - width) / 2,
12267
- y: bounds.y + (bounds.height - height) / 2,
12428
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12429
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12268
12430
  width,
12269
12431
  height
12270
12432
  };
@@ -12421,11 +12583,12 @@ var DiamondNode = class extends Node {
12421
12583
  };
12422
12584
  }
12423
12585
  getLabelContainerBounds(bounds) {
12424
- const width = bounds.width / 2;
12425
- const height = bounds.height / 2;
12586
+ const insetBounds = super.getLabelContainerBounds(bounds);
12587
+ const width = insetBounds.width / 2;
12588
+ const height = insetBounds.height / 2;
12426
12589
  return {
12427
- x: bounds.x + (bounds.width - width) / 2,
12428
- y: bounds.y + (bounds.height - height) / 2,
12590
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12591
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12429
12592
  width,
12430
12593
  height
12431
12594
  };
@@ -12961,14 +13124,30 @@ var CText = class extends CompositeComponentBase {
12961
13124
  appendEllipsis: true
12962
13125
  });
12963
13126
  }
12964
- measure(ctx) {
13127
+ measure(ctx, maxWidth) {
12965
13128
  ctx.font = this.getFont();
12966
13129
  const lineHeightPx = this.getLineHeightPx();
13130
+ if (maxWidth !== void 0) {
13131
+ const wrapped = wrapMeasuredText(ctx, this._text, maxWidth, {
13132
+ maxLines: this._maxLines,
13133
+ appendEllipsis: true
13134
+ });
13135
+ const width = measureTextLines(ctx, wrapped);
13136
+ const height = wrapped.length * lineHeightPx;
13137
+ if (this._rotation === 90 || this._rotation === -90) return {
13138
+ width: height,
13139
+ height: width
13140
+ };
13141
+ return {
13142
+ width,
13143
+ height
13144
+ };
13145
+ }
12967
13146
  const rawLines = this._text.split("\n");
12968
- const maxWidth = measureTextLines(ctx, rawLines);
13147
+ const oneLineWidth = measureTextLines(ctx, rawLines);
12969
13148
  let lineCount = rawLines.length;
12970
13149
  if (this._maxLines !== void 0 && lineCount > this._maxLines) lineCount = this._maxLines;
12971
- const width = maxWidth;
13150
+ const width = oneLineWidth;
12972
13151
  const height = lineCount * lineHeightPx;
12973
13152
  if (this._rotation === 90 || this._rotation === -90) return {
12974
13153
  width: height,
@@ -13443,16 +13622,33 @@ function flexLayout(container, config, children) {
13443
13622
  finalSizes[i] = finalSizes[i] + remaining * child.flexGrow / totalGrow;
13444
13623
  }
13445
13624
  } 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++) {
13625
+ const active = /* @__PURE__ */ new Set();
13626
+ for (let i = 0; i < children.length; i++) {
13627
+ const child = children[i];
13628
+ const minMain = isRow ? child.minSize.width : child.minSize.height;
13629
+ if (child.flexShrink > 0 && finalSizes[i] > minMain) active.add(i);
13630
+ }
13631
+ let deficit = -remaining;
13632
+ const epsilon = 1e-9;
13633
+ while (deficit > epsilon && active.size > 0) {
13634
+ let totalShrink = 0;
13635
+ for (const i of active) totalShrink += children[i].flexShrink * baseSizes[i];
13636
+ if (totalShrink <= 0) break;
13637
+ let distributed = 0;
13638
+ for (const i of active) {
13451
13639
  const child = children[i];
13452
13640
  const shrinkRatio = child.flexShrink * baseSizes[i] / totalShrink;
13453
13641
  const minMain = isRow ? child.minSize.width : child.minSize.height;
13454
- finalSizes[i] = Math.max(minMain, finalSizes[i] - deficit * shrinkRatio);
13642
+ const shrink = Math.min(deficit * shrinkRatio, finalSizes[i] - minMain);
13643
+ finalSizes[i] = finalSizes[i] - shrink;
13644
+ distributed += shrink;
13645
+ if (finalSizes[i] - minMain <= epsilon) {
13646
+ finalSizes[i] = minMain;
13647
+ active.delete(i);
13648
+ }
13455
13649
  }
13650
+ if (distributed <= epsilon) break;
13651
+ deficit -= distributed;
13456
13652
  }
13457
13653
  }
13458
13654
  let actualTotal = totalGaps + totalMargins;
@@ -13641,11 +13837,13 @@ var CContainer = class extends CompositeComponentBase {
13641
13837
  padding: this._padding
13642
13838
  };
13643
13839
  }
13644
- buildFlexChildren(ctx, children = this.getLayoutChildren()) {
13840
+ buildFlexChildren(ctx, availableWidth, children = this.getLayoutChildren()) {
13841
+ const pad = normalizeSides(this._padding);
13842
+ const innerWidth = availableWidth !== void 0 && this._direction === "column" ? Math.max(0, availableWidth - pad.left - pad.right) : void 0;
13645
13843
  return children.map((child) => {
13646
13844
  const s = child.style;
13647
13845
  return {
13648
- measure: child.measure(ctx),
13846
+ measure: child.measure(ctx, innerWidth),
13649
13847
  minSize: {
13650
13848
  width: 0,
13651
13849
  height: 0
@@ -13658,8 +13856,8 @@ var CContainer = class extends CompositeComponentBase {
13658
13856
  };
13659
13857
  });
13660
13858
  }
13661
- measure(ctx) {
13662
- const flexChildren = this.buildFlexChildren(ctx);
13859
+ measure(ctx, availableWidth) {
13860
+ const flexChildren = this.buildFlexChildren(ctx, availableWidth);
13663
13861
  return flexLayout({
13664
13862
  width: 1e5,
13665
13863
  height: 1e5
@@ -13676,7 +13874,7 @@ var CContainer = class extends CompositeComponentBase {
13676
13874
  render(ctx, bounds) {
13677
13875
  if (this.style.visible === false) return;
13678
13876
  const layoutChildren = this.getLayoutChildren();
13679
- const flexChildren = this.buildFlexChildren(ctx, layoutChildren);
13877
+ const flexChildren = this.buildFlexChildren(ctx, void 0, layoutChildren);
13680
13878
  const result = flexLayout({
13681
13879
  width: bounds.width,
13682
13880
  height: bounds.height
@@ -13851,11 +14049,12 @@ var CShape = class extends CompositeComponentBase {
13851
14049
  get content() {
13852
14050
  return this._content;
13853
14051
  }
13854
- measure(ctx) {
14052
+ measure(ctx, maxWidth) {
13855
14053
  const pad = normalizeSides(this._padding);
13856
14054
  const bw = this._borderWidth;
13857
14055
  if (this._content) {
13858
- const contentSize = this._content.measure(ctx);
14056
+ const innerWidth = maxWidth === void 0 ? void 0 : Math.max(0, maxWidth - pad.left - pad.right - bw * 2);
14057
+ const contentSize = this._content.measure(ctx, innerWidth);
13859
14058
  return {
13860
14059
  width: contentSize.width + pad.left + pad.right + bw * 2,
13861
14060
  height: contentSize.height + pad.top + pad.bottom + bw * 2
@@ -14141,13 +14340,24 @@ var CompositeNode = class extends Node {
14141
14340
  const fillOpacity = style.fillOpacity ?? 1;
14142
14341
  const strokeOpacity = style.strokeOpacity ?? 1;
14143
14342
  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();
14343
+ if (this._shapeType === "custom" && this._pathFactory) {
14344
+ const path = this._pathFactory(width, height);
14345
+ ctx.save();
14346
+ ctx.translate(x, y);
14347
+ ctx.globalAlpha = baseOpacity * fillOpacity;
14348
+ ctx.fill(path);
14349
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
14350
+ ctx.stroke(path);
14351
+ ctx.restore();
14352
+ } else {
14353
+ ctx.beginPath();
14354
+ this.buildShapePath(ctx, x, y, width, height);
14355
+ ctx.closePath();
14356
+ ctx.globalAlpha = baseOpacity * fillOpacity;
14357
+ ctx.fill();
14358
+ ctx.globalAlpha = baseOpacity * strokeOpacity;
14359
+ ctx.stroke();
14360
+ }
14151
14361
  ctx.globalAlpha = 1;
14152
14362
  this.renderContents(ctx);
14153
14363
  }
@@ -14169,15 +14379,6 @@ var CompositeNode = class extends Node {
14169
14379
  break;
14170
14380
  }
14171
14381
  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
14382
  this.buildRectPath(ctx, x, y, w, h);
14182
14383
  break;
14183
14384
  default:
@@ -14214,43 +14415,64 @@ var CompositeNode = class extends Node {
14214
14415
  this.renderPorts(ctx);
14215
14416
  }
14216
14417
  applyAutoSize(ctx) {
14217
- const contentSize = this._content.measure(ctx);
14218
- const inset = this.contentInset;
14219
- const neededWidth = Math.max(this._minWidth, contentSize.width + inset.left + inset.right);
14220
- const neededHeight = Math.max(this._minHeight, contentSize.height + inset.top + inset.bottom);
14418
+ const bounds = this.getBounds();
14419
+ const inset = resolveContentInset(this.contentInset, this.contentInsetScale, {
14420
+ width: bounds.width,
14421
+ height: bounds.height
14422
+ }, this.contentInsetBaseSize);
14423
+ const floorContentWidth = this._content.measure(ctx, 0).width;
14424
+ const neededWidth = Math.max(this._minWidth, floorContentWidth + inset.left + inset.right);
14221
14425
  if (neededWidth > this._width) this._width = neededWidth;
14426
+ const innerWidth = Math.max(0, this._width - inset.left - inset.right);
14427
+ const contentSize = this._content.measure(ctx, innerWidth);
14428
+ const neededHeight = Math.max(this._minHeight, contentSize.height + inset.top + inset.bottom);
14222
14429
  if (neededHeight > this._height) this._height = neededHeight;
14223
14430
  }
14224
14431
  getLabelContainerBounds(bounds) {
14432
+ const insetBounds = super.getLabelContainerBounds(bounds);
14225
14433
  if (this._shapeType === "circle") {
14226
14434
  const factor = 1 / Math.SQRT2;
14227
- const w = bounds.width * factor;
14228
- const h = bounds.height * factor;
14435
+ const w = insetBounds.width * factor;
14436
+ const h = insetBounds.height * factor;
14229
14437
  return {
14230
- x: bounds.x + (bounds.width - w) / 2,
14231
- y: bounds.y + (bounds.height - h) / 2,
14438
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14439
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14232
14440
  width: w,
14233
14441
  height: h
14234
14442
  };
14235
14443
  }
14236
14444
  if (this._shapeType === "diamond") {
14237
- const w = bounds.width / 2;
14238
- const h = bounds.height / 2;
14445
+ const w = insetBounds.width / 2;
14446
+ const h = insetBounds.height / 2;
14239
14447
  return {
14240
- x: bounds.x + (bounds.width - w) / 2,
14241
- y: bounds.y + (bounds.height - h) / 2,
14448
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14449
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14242
14450
  width: w,
14243
14451
  height: h
14244
14452
  };
14245
14453
  }
14246
- return super.getLabelContainerBounds(bounds);
14454
+ return insetBounds;
14247
14455
  }
14248
14456
  /**
14249
14457
  * Get the minimum content size needed for the content tree.
14250
14458
  * Useful for external auto-sizing logic.
14251
- */
14252
- getContentMinSize(ctx) {
14253
- return this._content.measure(ctx);
14459
+ *
14460
+ * With `availableWidth`, text wraps to the given node width: the returned
14461
+ * width is the longest-word floor (content can never be narrower than its
14462
+ * widest unbreakable word), the height is the wrapped height at that width.
14463
+ */
14464
+ getContentMinSize(ctx, availableWidth) {
14465
+ if (availableWidth === void 0) return this._content.measure(ctx);
14466
+ const inset = resolveContentInset(this.contentInset, this.contentInsetScale, {
14467
+ width: availableWidth,
14468
+ height: this._height
14469
+ }, this.contentInsetBaseSize);
14470
+ const innerWidth = Math.max(0, availableWidth - inset.left - inset.right);
14471
+ const wrapped = this._content.measure(ctx, innerWidth);
14472
+ return {
14473
+ width: this._content.measure(ctx, 0).width + inset.left + inset.right,
14474
+ height: wrapped.height + inset.top + inset.bottom
14475
+ };
14254
14476
  }
14255
14477
  };
14256
14478
  //#endregion
@@ -14451,16 +14673,22 @@ var DARK_THEME = {
14451
14673
  }
14452
14674
  }
14453
14675
  };
14676
+ function cloneTheme(theme) {
14677
+ return structuredClone(theme);
14678
+ }
14454
14679
  /**
14455
14680
  * Manages styles and themes
14456
14681
  */
14457
14682
  var StyleManager = class {
14458
14683
  constructor(theme) {
14459
14684
  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;
14685
+ this._builtInThemes = new Map([["default", cloneTheme(DEFAULT_THEME)], ["dark", cloneTheme(DARK_THEME)]]);
14686
+ const initialTheme = theme ?? "default";
14687
+ if (typeof initialTheme === "string") {
14688
+ const registeredTheme = this._builtInThemes.get(initialTheme);
14689
+ if (registeredTheme === void 0) throw new Error(`Unknown theme: ${initialTheme}`);
14690
+ this._theme = cloneTheme(registeredTheme);
14691
+ } else this._theme = cloneTheme(initialTheme);
14464
14692
  }
14465
14693
  /**
14466
14694
  * Current theme
@@ -14474,14 +14702,15 @@ var StyleManager = class {
14474
14702
  setTheme(theme) {
14475
14703
  if (typeof theme === "string") {
14476
14704
  const builtIn = this._builtInThemes.get(theme);
14477
- if (builtIn !== void 0) this._theme = builtIn;
14478
- } else this._theme = theme;
14705
+ if (builtIn === void 0) throw new Error(`Unknown theme: ${theme}`);
14706
+ this._theme = cloneTheme(builtIn);
14707
+ } else this._theme = cloneTheme(theme);
14479
14708
  }
14480
14709
  /**
14481
14710
  * Register a custom theme
14482
14711
  */
14483
14712
  registerTheme(theme) {
14484
- this._builtInThemes.set(theme.name, theme);
14713
+ this._builtInThemes.set(theme.name, cloneTheme(theme));
14485
14714
  }
14486
14715
  /**
14487
14716
  * Get all available theme names
@@ -14596,10 +14825,17 @@ var StyleManager = class {
14596
14825
  }
14597
14826
  }
14598
14827
  getBaseEdgeStyle(state) {
14828
+ const defaultStyle = this._theme.edge.default;
14599
14829
  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;
14830
+ case "hover": return {
14831
+ ...defaultStyle,
14832
+ ...this._theme.edge.hover
14833
+ };
14834
+ case "selected": return {
14835
+ ...defaultStyle,
14836
+ ...this._theme.edge.selected
14837
+ };
14838
+ default: return defaultStyle;
14603
14839
  }
14604
14840
  }
14605
14841
  };
@@ -14771,36 +15007,7 @@ var Serializer = class {
14771
15007
  position: port.position,
14772
15008
  styleClass: port.styleClass
14773
15009
  }));
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
- }
15010
+ const label = node.label ? this.serializeTextLabel(node.label) : void 0;
14804
15011
  let icon;
14805
15012
  if (node.icon) {
14806
15013
  const opts = node.icon.options;
@@ -14833,6 +15040,8 @@ var Serializer = class {
14833
15040
  if (hasNonDefaultValues(anchorData, anchorDefaults)) anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
14834
15041
  const contentInset = node.contentInset;
14835
15042
  const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
15043
+ const contentInsetScale = node.contentInsetScale;
15044
+ const hasContentInsetScale = Object.keys(contentInsetScale).length > 0;
14836
15045
  const base = omitEmptyValues({
14837
15046
  id: node.id,
14838
15047
  type: node.typeName,
@@ -14840,14 +15049,16 @@ var Serializer = class {
14840
15049
  y: node.y,
14841
15050
  width: node.width,
14842
15051
  height: node.height,
14843
- style: node.style,
15052
+ style: { ...node.styleOverrides },
14844
15053
  styleClass: node.styleClass,
14845
15054
  label,
14846
- labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
14847
15055
  labelPlacement: node.labelPlacement !== "center" ? node.labelPlacement : void 0,
14848
15056
  labelGap: node.labelGap !== 4 ? node.labelGap : void 0,
14849
15057
  icon,
14850
15058
  contentInset: hasContentInset ? contentInset : void 0,
15059
+ contentInsetScale: hasContentInsetScale ? contentInsetScale : void 0,
15060
+ contentInsetBaseSize: node.contentInsetBaseSize,
15061
+ badges: node.badges.length > 0 ? node.badges.map((badge) => ({ ...badge })) : void 0,
14851
15062
  anchorPoints,
14852
15063
  ports: ports.length > 0 ? ports : void 0,
14853
15064
  data: Object.keys(node.data).length > 0 ? node.data : void 0
@@ -14874,14 +15085,14 @@ var Serializer = class {
14874
15085
  arrowType: edge.arrowType,
14875
15086
  startMarker: edge.startMarker,
14876
15087
  endMarker: edge.endMarker,
14877
- style: edge.style,
15088
+ style: { ...edge.styleOverrides },
14878
15089
  styleClass: edge.styleClass,
14879
- label: edge.label?.text,
14880
- labelStyleClass: edge.label?.styleClass,
15090
+ label: edge.label ? this.serializeTextLabel(edge.label) : void 0,
14881
15091
  labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
14882
15092
  labelPosition: edge.labelPosition !== .5 ? edge.labelPosition : void 0,
14883
15093
  labelFollowPath: edge.labelFollowPath ? true : void 0,
14884
15094
  labelBackground: edge.labelBackground,
15095
+ lockAnchors: edge.lockAnchors ? void 0 : false,
14885
15096
  labelLineGap: edge.labelLineGap ? true : void 0,
14886
15097
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
14887
15098
  };
@@ -14890,12 +15101,31 @@ var Serializer = class {
14890
15101
  return {
14891
15102
  id: group.id,
14892
15103
  childIds: group.children.map((c) => c.id),
14893
- style: group.style,
15104
+ style: { ...group.styleOverrides },
14894
15105
  styleClass: group.styleClass,
14895
15106
  label: group.label,
15107
+ padding: group.padding !== 20 ? group.padding : void 0,
14896
15108
  data: Object.keys(group.data).length > 0 ? group.data : void 0
14897
15109
  };
14898
15110
  }
15111
+ serializeTextLabel(label) {
15112
+ const inset = label.inset;
15113
+ 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 : {
15114
+ top: inset.top,
15115
+ right: inset.right,
15116
+ bottom: inset.bottom,
15117
+ left: inset.left
15118
+ };
15119
+ const style = label.styleOverrides;
15120
+ if (!(Object.keys(style).length > 0 || label.maxWidth !== void 0 || serializedInset !== void 0 || label.styleClass !== void 0)) return label.text;
15121
+ return omitEmptyValues({
15122
+ text: label.text,
15123
+ style: Object.keys(style).length > 0 ? style : void 0,
15124
+ maxWidth: label.maxWidth,
15125
+ inset: serializedInset,
15126
+ styleClass: label.styleClass
15127
+ });
15128
+ }
14899
15129
  };
14900
15130
  //#endregion
14901
15131
  //#region src/utils/download.ts
@@ -14977,7 +15207,11 @@ var ImageExporter = class {
14977
15207
  const padding = options.padding ?? 20;
14978
15208
  const format = options.format ?? "png";
14979
15209
  const quality = options.quality ?? .92;
14980
- const bounds = getContentBounds({ nodes: this.renderer.nodes.values() });
15210
+ const bounds = getContentBounds({
15211
+ nodes: this.renderer.nodes.values(),
15212
+ edges: this.renderer.edges.values(),
15213
+ groups: this.renderer.groups.values()
15214
+ });
14981
15215
  if (bounds === null) return this.createBlankImage(100, 100, backgroundColor, format, quality);
14982
15216
  const width = (bounds.width + padding * 2) * scale;
14983
15217
  const height = (bounds.height + padding * 2) * scale;
@@ -15029,6 +15263,7 @@ var SvgExporter = class {
15029
15263
  const padding = options.padding ?? 20;
15030
15264
  const includeBackground = options.includeBackground ?? true;
15031
15265
  const backgroundColor = options.backgroundColor ?? "#ffffff";
15266
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
15032
15267
  const edgeLabelOffset = options.edgeLabelOffset;
15033
15268
  const bounds = getContentBounds({
15034
15269
  nodes: this.renderer.nodes.values(),
@@ -15045,7 +15280,7 @@ var SvgExporter = class {
15045
15280
  const renderedEdges = [];
15046
15281
  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
15282
  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}"/>`);
15283
+ if (includeBackground) parts.push(`<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>`);
15049
15284
  parts.push(`<g transform="translate(${offsetX}, ${offsetY})">`);
15050
15285
  for (const group of this.renderer.groups.values()) if (group.visible) parts.push(this.renderGroup(group));
15051
15286
  for (const node of this.renderer.nodes.values()) if (node.visible) parts.push(this.renderNode(node));
@@ -15064,8 +15299,8 @@ var SvgExporter = class {
15064
15299
  renderGroup(group) {
15065
15300
  const bounds = group.getBounds();
15066
15301
  const style = group.style;
15067
- const fill = style.fillColor ?? "rgba(200, 200, 200, 0.2)";
15068
- const stroke = style.strokeColor ?? "#999999";
15302
+ const fill = this.escapeAttribute(style.fillColor ?? "rgba(200, 200, 200, 0.2)");
15303
+ const stroke = this.escapeAttribute(style.strokeColor ?? "#999999");
15069
15304
  const strokeWidth = style.strokeWidth ?? 1;
15070
15305
  const opacity = style.opacity ?? 1;
15071
15306
  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 +15314,8 @@ var SvgExporter = class {
15079
15314
  renderNode(node) {
15080
15315
  const bounds = node.getBounds();
15081
15316
  const style = node.style;
15082
- const fill = style.fillColor ?? "#ffffff";
15083
- const stroke = style.strokeColor ?? "#333333";
15317
+ const fill = this.escapeAttribute(style.fillColor ?? "#ffffff");
15318
+ const stroke = this.escapeAttribute(style.strokeColor ?? "#333333");
15084
15319
  const strokeWidth = style.strokeWidth ?? 2;
15085
15320
  const baseOpacity = style.opacity ?? 1;
15086
15321
  const fillOpacity = (style.fillOpacity ?? 1) * baseOpacity;
@@ -15177,14 +15412,15 @@ var SvgExporter = class {
15177
15412
  if (edge.path.length < 2) return "";
15178
15413
  const style = edge.style;
15179
15414
  const stroke = style.strokeColor ?? "#666666";
15415
+ const escapedStroke = this.escapeAttribute(stroke);
15180
15416
  const strokeWidth = style.strokeWidth ?? 2;
15181
15417
  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}"` : "";
15418
+ const lineCap = style.lineCap ? ` stroke-linecap="${this.escapeAttribute(style.lineCap)}"` : "";
15419
+ const lineJoin = style.lineJoin ? ` stroke-linejoin="${this.escapeAttribute(style.lineJoin)}"` : "";
15184
15420
  const dashValues = style.flowDash ?? style.lineDash;
15185
15421
  const dash = dashValues ? ` stroke-dasharray="${dashValues.join(" ")}"` : "";
15186
15422
  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)}`;
15423
+ 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
15424
  }
15189
15425
  renderEdgeLabel(edge, edgeLabelOffset) {
15190
15426
  if (!edge.label) return "";
@@ -15196,7 +15432,7 @@ var SvgExporter = class {
15196
15432
  renderEdgeLabelBackground(edge, point, rotation = 0) {
15197
15433
  if (!edge.label) return "";
15198
15434
  const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
15199
- const bgColor = edge.labelBackground?.color ?? "#ffffff";
15435
+ const bgColor = this.escapeAttribute(edge.labelBackground?.color ?? "#ffffff");
15200
15436
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
15201
15437
  const bgRadius = edge.labelBackground?.borderRadius ?? 2;
15202
15438
  const x = point.x - metrics.width / 2;
@@ -15277,7 +15513,11 @@ var SvgExporter = class {
15277
15513
  return calculateMarkerPoints(edge.path, position, edge.type);
15278
15514
  }
15279
15515
  renderMarkerShape(marker, from, to, edgeStroke) {
15280
- return generateSvgMarker(marker, from, to, edgeStroke);
15516
+ return generateSvgMarker({
15517
+ ...marker,
15518
+ strokeColor: marker.strokeColor !== void 0 ? this.escapeAttribute(marker.strokeColor) : void 0,
15519
+ fillColor: marker.fillColor !== void 0 ? this.escapeAttribute(marker.fillColor) : void 0
15520
+ }, from, to, this.escapeAttribute(edgeStroke));
15281
15521
  }
15282
15522
  buildPath(edge) {
15283
15523
  const path = edge.path;
@@ -15347,11 +15587,11 @@ var SvgExporter = class {
15347
15587
  };
15348
15588
  }
15349
15589
  renderTextLabel(text, point, style = {}, rotation = 0) {
15350
- const fill = style.color ?? "#000000";
15590
+ const fill = this.escapeAttribute(style.color ?? "#000000");
15351
15591
  const fontSize = style.fontSize ?? 14;
15352
- const fontFamily = style.fontFamily ?? "sans-serif";
15353
- const fontWeight = style.fontWeight ?? "normal";
15354
- const fontStyle = style.fontStyle ?? "normal";
15592
+ const fontFamily = this.escapeAttribute(style.fontFamily ?? "sans-serif");
15593
+ const fontWeight = this.escapeAttribute(style.fontWeight ?? "normal");
15594
+ const fontStyle = this.escapeAttribute(style.fontStyle ?? "normal");
15355
15595
  const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
15356
15596
  const opacity = style.opacity ?? 1;
15357
15597
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
@@ -15547,9 +15787,10 @@ var SvgExporter = class {
15547
15787
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15548
15788
  }
15549
15789
  createEmptySvg(width, height, backgroundColor, includeBackground) {
15790
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
15550
15791
  return [
15551
15792
  `<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}"/>` : "",
15793
+ includeBackground ? `<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>` : "",
15553
15794
  "</svg>"
15554
15795
  ].join("");
15555
15796
  }