@ngroznykh/papirus 0.9.12 → 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 (52) 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 +15 -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 +5 -0
  12. package/dist/core/InteractionManager.d.ts.map +1 -1
  13. package/dist/core/NavigationManager.d.ts +4 -0
  14. package/dist/core/NavigationManager.d.ts.map +1 -1
  15. package/dist/core/SearchManager.d.ts.map +1 -1
  16. package/dist/core/SelectionManager.d.ts +1 -0
  17. package/dist/core/SelectionManager.d.ts.map +1 -1
  18. package/dist/core/overlays/GridOverlay.d.ts.map +1 -1
  19. package/dist/core/overlays/MiniMap.d.ts +8 -0
  20. package/dist/core/overlays/MiniMap.d.ts.map +1 -1
  21. package/dist/core/overlays/RulersOverlay.d.ts +4 -0
  22. package/dist/core/overlays/RulersOverlay.d.ts.map +1 -1
  23. package/dist/elements/Edge.d.ts.map +1 -1
  24. package/dist/elements/Group.d.ts.map +1 -1
  25. package/dist/elements/TextLabel.d.ts.map +1 -1
  26. package/dist/elements/composite/CText.d.ts.map +1 -1
  27. package/dist/elements/composite/CompositeNode.d.ts.map +1 -1
  28. package/dist/elements/composite/FlexLayout.d.ts.map +1 -1
  29. package/dist/elements/edge/EdgeLabelRenderer.d.ts.map +1 -1
  30. package/dist/elements/nodes/CircleNode.d.ts.map +1 -1
  31. package/dist/elements/nodes/DiamondNode.d.ts.map +1 -1
  32. package/dist/elements/paths/BezierPathStrategy.d.ts.map +1 -1
  33. package/dist/events/EventEmitter.d.ts +2 -3
  34. package/dist/events/EventEmitter.d.ts.map +1 -1
  35. package/dist/events/InputHandler.d.ts +11 -2
  36. package/dist/events/InputHandler.d.ts.map +1 -1
  37. package/dist/papirus.js +665 -245
  38. package/dist/papirus.js.map +1 -1
  39. package/dist/styles/StyleManager.d.ts.map +1 -1
  40. package/dist/types.d.ts +17 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/utils/ImageExporter.d.ts.map +1 -1
  43. package/dist/utils/Serializer.d.ts +1 -0
  44. package/dist/utils/Serializer.d.ts.map +1 -1
  45. package/dist/utils/SvgExporter.d.ts.map +1 -1
  46. package/dist/utils/adaptiveGridStep.d.ts +10 -0
  47. package/dist/utils/adaptiveGridStep.d.ts.map +1 -0
  48. package/dist/utils/pathViewport.d.ts +12 -0
  49. package/dist/utils/pathViewport.d.ts.map +1 -0
  50. package/dist/utils/screenText.d.ts +4 -0
  51. package/dist/utils/screenText.d.ts.map +1 -0
  52. 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
  */
@@ -3220,6 +3316,20 @@ function measureTextLines(ctx, lines) {
3220
3316
  for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
3221
3317
  return maxWidth;
3222
3318
  }
3319
+ function readScale(ctx) {
3320
+ if (typeof ctx.getTransform !== "function") return null;
3321
+ const m = ctx.getTransform();
3322
+ if (m === null || typeof m.a !== "number") return null;
3323
+ return {
3324
+ a: m.a,
3325
+ b: typeof m.b === "number" ? m.b : 0
3326
+ };
3327
+ }
3328
+ function isScreenFontReadable(ctx, fontSize) {
3329
+ const m = readScale(ctx);
3330
+ if (m === null) return true;
3331
+ return fontSize * Math.hypot(m.a, m.b) >= 7;
3332
+ }
3223
3333
  //#endregion
3224
3334
  //#region src/elements/TextLabel.ts
3225
3335
  var DEFAULT_INSET = 8;
@@ -3477,10 +3587,10 @@ var TextLabel = class {
3477
3587
  default: startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
3478
3588
  }
3479
3589
  ctx.fillStyle = this._style.color ?? "#000000";
3590
+ if (!isScreenFontReadable(ctx, this._style.fontSize ?? 14)) return;
3480
3591
  for (let i = 0; i < this._lines.length; i++) {
3481
- const line = this._lines[i];
3482
3592
  const y = startY + i * lineHeight;
3483
- ctx.fillText(line, x, y);
3593
+ ctx.fillText(this._lines[i], x, y);
3484
3594
  }
3485
3595
  }
3486
3596
  /**
@@ -6044,12 +6154,18 @@ var BezierPathStrategy = class {
6044
6154
  hitTest(point, path, tolerance) {
6045
6155
  if (path.length < 4) return false;
6046
6156
  const samples = 20;
6047
- const points = [];
6048
- for (let i = 0; i <= samples; i++) {
6049
- const t = i / samples;
6050
- 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
+ }
6051
6168
  }
6052
- for (let i = 1; i < points.length; i++) if (distanceToSegment(point, points[i - 1], points[i]) <= tolerance) return true;
6053
6169
  return false;
6054
6170
  }
6055
6171
  };
@@ -6411,6 +6527,7 @@ function drawLegacyArrowHead(ctx, from, to) {
6411
6527
  function renderEdgeLabel(ctx, options) {
6412
6528
  const { label, path, background } = options;
6413
6529
  if (!label || path.length < 2) return;
6530
+ if (!isScreenFontReadable(ctx, label.style.fontSize ?? 14)) return;
6414
6531
  const labelCenter = getEdgeLabelPosition(options);
6415
6532
  if (!labelCenter) return;
6416
6533
  const rotation = getEdgeLabelRotation(options);
@@ -7060,7 +7177,7 @@ var Edge = class extends Element {
7060
7177
  }
7061
7178
  return pts;
7062
7179
  };
7063
- if (!(this._label !== void 0 && this._labelLineGap)) {
7180
+ if (!(this._label !== void 0 && this._labelLineGap && isScreenFontReadable(ctx, this._label.style.fontSize ?? 14))) {
7064
7181
  const start = path[0];
7065
7182
  const startNext = path[1];
7066
7183
  const startAngle = Math.atan2(startNext.y - start.y, startNext.x - start.x);
@@ -7547,6 +7664,7 @@ var InteractionManager = class {
7547
7664
  this.handledScrollbarMouseDown = false;
7548
7665
  this.overlayDragSession = null;
7549
7666
  this.handledOverlayMouseDown = false;
7667
+ this.editHoverSuspendedForPan = false;
7550
7668
  this.renderer = options.renderer;
7551
7669
  this.navigationOnly = options.navigationOnly ?? false;
7552
7670
  this.inputHandler = new InputHandler({
@@ -7714,6 +7832,13 @@ var InteractionManager = class {
7714
7832
  }
7715
7833
  destroy() {
7716
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();
7717
7842
  if (this.overlayDragSession) {
7718
7843
  this.renderer.endOverlayDrag(this.overlayDragSession);
7719
7844
  this.overlayDragSession = null;
@@ -7730,7 +7855,7 @@ var InteractionManager = class {
7730
7855
  setupEvents() {
7731
7856
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
7732
7857
  this.selectionManager.renderSelectionRect(ctx);
7733
- if (!this.navigationOnly) {
7858
+ if (!this.navigationOnly && !this.navigationManager.panning) {
7734
7859
  this.dragManager.renderAlignmentGuides(ctx);
7735
7860
  this.connectionManager.renderPreview(ctx);
7736
7861
  this.connectionManager.renderHoverAnchors(ctx);
@@ -7901,7 +8026,7 @@ var InteractionManager = class {
7901
8026
  if (event.button === 0) {
7902
8027
  if (this.navigationOnly) {
7903
8028
  this.selectionManager.clearSelection();
7904
- this.navigationManager.startPan(event);
8029
+ this.beginBackgroundPan(event);
7905
8030
  return;
7906
8031
  }
7907
8032
  if (hitElement === void 0) {
@@ -7910,11 +8035,14 @@ var InteractionManager = class {
7910
8035
  return;
7911
8036
  }
7912
8037
  this.selectionManager.clearSelection();
7913
- this.navigationManager.startPan(event);
8038
+ this.beginBackgroundPan(event);
7914
8039
  return;
7915
8040
  }
7916
8041
  }
7917
- if (this.navigationManager.handleMouseDown(event)) return;
8042
+ if (this.navigationManager.handleMouseDown(event)) {
8043
+ this.suspendEditHover();
8044
+ return;
8045
+ }
7918
8046
  if (this.navigationOnly) return;
7919
8047
  if (this.dragManager.handleMouseDown(event)) return;
7920
8048
  }
@@ -7928,7 +8056,11 @@ var InteractionManager = class {
7928
8056
  this.renderer.setScrollbarActiveAxis(null);
7929
8057
  this.scrollbarDragState = null;
7930
8058
  }
7931
- if (this.navigationManager.panning) this.navigationManager.handleMouseUp(event);
8059
+ if (this.navigationManager.panning) this.finishBackgroundPan(event);
8060
+ }
8061
+ if (this.navigationManager.panning) {
8062
+ this.navigationManager.handleMouseMove(event);
8063
+ return;
7932
8064
  }
7933
8065
  const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
7934
8066
  const overOverlayPointerSink = this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY);
@@ -7990,7 +8122,7 @@ var InteractionManager = class {
7990
8122
  this.selectionManager.endSelectionRect();
7991
8123
  return;
7992
8124
  }
7993
- this.navigationManager.handleMouseUp(event);
8125
+ this.finishBackgroundPan(event);
7994
8126
  }
7995
8127
  handleClick(event) {
7996
8128
  if (this.handledOverlayMouseDown) {
@@ -8094,6 +8226,24 @@ var InteractionManager = class {
8094
8226
  }
8095
8227
  return closestEdge;
8096
8228
  }
8229
+ suspendEditHover() {
8230
+ if (this.navigationOnly || this.editHoverSuspendedForPan) return;
8231
+ this.connectionManager.disableHover();
8232
+ this.editHoverSuspendedForPan = true;
8233
+ }
8234
+ resumeEditHover() {
8235
+ if (!this.editHoverSuspendedForPan) return;
8236
+ this.connectionManager.enableHover();
8237
+ this.editHoverSuspendedForPan = false;
8238
+ }
8239
+ beginBackgroundPan(event) {
8240
+ this.suspendEditHover();
8241
+ this.navigationManager.startPan(event);
8242
+ }
8243
+ finishBackgroundPan(event) {
8244
+ this.navigationManager.handleMouseUp(event);
8245
+ this.resumeEditHover();
8246
+ }
8097
8247
  handleWheel(event) {
8098
8248
  if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
8099
8249
  this.navigationManager.handleWheel(event);
@@ -8132,6 +8282,7 @@ var InteractionManager = class {
8132
8282
  }
8133
8283
  handleKeyUp(event) {
8134
8284
  this.navigationManager.handleKeyUp(event);
8285
+ if (!this.navigationManager.panning) this.resumeEditHover();
8135
8286
  }
8136
8287
  handleViewportNavigationKey(event) {
8137
8288
  switch (event.key) {
@@ -8316,6 +8467,66 @@ var InteractionManager = class {
8316
8467
  }
8317
8468
  };
8318
8469
  //#endregion
8470
+ //#region src/utils/pathViewport.ts
8471
+ /**
8472
+ * True when the drawn edge path meets the viewport.
8473
+ * AABB-only cull keeps fat bezier control boxes on the walk even when the
8474
+ * curve never enters the view. Empty path stays visible for the first layout.
8475
+ */
8476
+ function edgePathIntersectsViewport(input, viewport) {
8477
+ const path = input.path;
8478
+ if (path.length === 0) return true;
8479
+ const aabb = boundsFromPoints(path);
8480
+ if (aabb === null || !rectsIntersect(aabb, viewport)) return false;
8481
+ if (input.type === "bezier" && path.length >= 4) return polylineIntersectsRect(sampleBezier(path), viewport);
8482
+ return polylineIntersectsRect(path, viewport);
8483
+ }
8484
+ function boundsFromPoints(points) {
8485
+ if (points.length === 0) return null;
8486
+ let minX = Infinity;
8487
+ let minY = Infinity;
8488
+ let maxX = -Infinity;
8489
+ let maxY = -Infinity;
8490
+ for (const point of points) {
8491
+ minX = Math.min(minX, point.x);
8492
+ minY = Math.min(minY, point.y);
8493
+ maxX = Math.max(maxX, point.x);
8494
+ maxY = Math.max(maxY, point.y);
8495
+ }
8496
+ return {
8497
+ x: minX,
8498
+ y: minY,
8499
+ width: maxX - minX,
8500
+ height: maxY - minY
8501
+ };
8502
+ }
8503
+ function polylineIntersectsRect(points, rect) {
8504
+ if (points.length === 1) return pointInRect(points[0], rect);
8505
+ for (let i = 0; i < points.length - 1; i++) {
8506
+ const start = points[i];
8507
+ const end = points[i + 1];
8508
+ if (pointInRect(start, rect) || pointInRect(end, rect)) return true;
8509
+ if (segmentRectIntersections(start, end, rect).length > 0) return true;
8510
+ }
8511
+ return false;
8512
+ }
8513
+ function sampleBezier(path) {
8514
+ const samples = [];
8515
+ const steps = 20;
8516
+ for (let i = 1; i + 2 < path.length; i += 3) {
8517
+ const p0 = path[i - 1];
8518
+ const p1 = path[i];
8519
+ const p2 = path[i + 1];
8520
+ const p3 = path[i + 2];
8521
+ for (let s = 0; s <= steps; s++) {
8522
+ const t = s / steps;
8523
+ if (samples.length > 0 && t === 0) continue;
8524
+ samples.push(bezierPoint(p0, p1, p2, p3, t));
8525
+ }
8526
+ }
8527
+ return samples;
8528
+ }
8529
+ //#endregion
8319
8530
  //#region src/core/AnimationManager.ts
8320
8531
  var DEFAULT_OPTIONS$3 = {
8321
8532
  enabled: false,
@@ -8369,6 +8580,15 @@ var AnimationManager = class {
8369
8580
  hasActive() {
8370
8581
  return this.enter.size > 0 || this.exit.size > 0 || this.highlight.size > 0;
8371
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
+ }
8372
8592
  update(now = performance.now()) {
8373
8593
  if (!this.options.enabled) return false;
8374
8594
  let changed = false;
@@ -9294,6 +9514,9 @@ var DiagramRenderer = class extends EventEmitter {
9294
9514
  this._dirty = true;
9295
9515
  this._styleDirty = true;
9296
9516
  this._contentDirty = true;
9517
+ this._contentRevision = 0;
9518
+ this._contentBoundsCache = null;
9519
+ this._edgeIndexEndpoints = /* @__PURE__ */ new Map();
9297
9520
  this._destroyed = false;
9298
9521
  this._contextLost = false;
9299
9522
  this._canvasRect = null;
@@ -9650,10 +9873,12 @@ var DiagramRenderer = class extends EventEmitter {
9650
9873
  */
9651
9874
  addEdge(edge) {
9652
9875
  edge.setDirtyListener(() => this.markDirty());
9653
- edge.setBindingListener(() => this.markContentDirty());
9876
+ edge.setBindingListener(() => {
9877
+ this.syncEdgeNodeIndex(edge);
9878
+ this.markContentDirty();
9879
+ });
9654
9880
  this._edges.set(edge.id, edge);
9655
- if (edge.from.nodeId) this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
9656
- if (edge.to.nodeId) this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
9881
+ this.syncEdgeNodeIndex(edge);
9657
9882
  this.animationManager.registerEnter(edge.id);
9658
9883
  this.markContentDirty();
9659
9884
  this.emit("edgeAdd", edge);
@@ -9674,6 +9899,22 @@ var DiagramRenderer = class extends EventEmitter {
9674
9899
  }
9675
9900
  }
9676
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
+ /**
9677
9918
  * Remove an edge from the diagram
9678
9919
  */
9679
9920
  removeEdge(edgeId) {
@@ -9709,8 +9950,12 @@ var DiagramRenderer = class extends EventEmitter {
9709
9950
  if (other.from.edgeId === edgeId || other.to.edgeId === edgeId) dependents.push(other.id);
9710
9951
  }
9711
9952
  for (const dependentId of dependents) this.removeEdgeImmediate(dependentId);
9712
- if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
9713
- 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);
9714
9959
  this._edges.delete(edgeId);
9715
9960
  edge.setDirtyListener(void 0);
9716
9961
  edge.setBindingListener(void 0);
@@ -9730,7 +9975,7 @@ var DiagramRenderer = class extends EventEmitter {
9730
9975
  addGroup(group) {
9731
9976
  group.setDirtyListener(() => this.markDirty());
9732
9977
  this._groups.set(group.id, group);
9733
- this.markDirty();
9978
+ this.markContentDirty();
9734
9979
  }
9735
9980
  /**
9736
9981
  * Remove a group from the diagram
@@ -9740,7 +9985,7 @@ var DiagramRenderer = class extends EventEmitter {
9740
9985
  if (group === void 0) return false;
9741
9986
  this._groups.delete(groupId);
9742
9987
  group.setDirtyListener(void 0);
9743
- this.markDirty();
9988
+ this.markContentDirty();
9744
9989
  return true;
9745
9990
  }
9746
9991
  /**
@@ -9814,10 +10059,18 @@ var DiagramRenderer = class extends EventEmitter {
9814
10059
  */
9815
10060
  markContentDirty() {
9816
10061
  this._contentDirty = true;
10062
+ this._contentRevision += 1;
9817
10063
  this._nodeObstaclesCache = null;
9818
10064
  this._dirty = true;
9819
10065
  }
9820
10066
  /**
10067
+ * Monotonic counter bumped when nodes/edges/layout change (not on pan/zoom).
10068
+ * Used by MiniMap to cache its content bake across viewport moves.
10069
+ */
10070
+ getContentRevision() {
10071
+ return this._contentRevision;
10072
+ }
10073
+ /**
9821
10074
  * Visible world rectangle plus padding so labels and arrow markers are not culled early.
9822
10075
  */
9823
10076
  getViewportWorldBounds() {
@@ -9835,10 +10088,13 @@ var DiagramRenderer = class extends EventEmitter {
9835
10088
  }
9836
10089
  /**
9837
10090
  * Edges without a computed path stay drawable so the first layout pass can paint them.
10091
+ * Bezier control-box AABB is not enough — walk the actual path.
9838
10092
  */
9839
10093
  edgeIntersectsViewport(edge, viewport) {
9840
- if (edge.path.length === 0) return true;
9841
- return this.intersectsViewport(edge.getBounds(), viewport);
10094
+ return edgePathIntersectsViewport({
10095
+ path: edge.path,
10096
+ type: edge.type
10097
+ }, viewport);
9842
10098
  }
9843
10099
  /**
9844
10100
  * Force an immediate render
@@ -9869,19 +10125,25 @@ var DiagramRenderer = class extends EventEmitter {
9869
10125
  this._edges.clear();
9870
10126
  this._groups.clear();
9871
10127
  this._nodeEdgeIndex.clear();
10128
+ this._edgeIndexEndpoints.clear();
9872
10129
  }
9873
10130
  /**
9874
10131
  * Clear all elements from the diagram
9875
10132
  */
9876
10133
  clear() {
9877
10134
  for (const node of this._nodes.values()) node.setDirtyListener(void 0);
9878
- 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
+ }
9879
10139
  for (const group of this._groups.values()) group.setDirtyListener(void 0);
9880
10140
  this._nodes.clear();
9881
10141
  this._edges.clear();
9882
10142
  this._groups.clear();
9883
10143
  this._nodeEdgeIndex.clear();
9884
- this.markDirty();
10144
+ this._edgeIndexEndpoints.clear();
10145
+ this.animationManager.clear();
10146
+ this.markContentDirty();
9885
10147
  }
9886
10148
  nativePixelRatio() {
9887
10149
  return this.options.retina ? window.devicePixelRatio || 1 : 1;
@@ -9938,13 +10200,13 @@ var DiagramRenderer = class extends EventEmitter {
9938
10200
  applyStyleManagerToElements(this.styleManager, this._groups.values(), this._edges.values(), this._nodes.values());
9939
10201
  this._styleDirty = false;
9940
10202
  }
9941
- for (const group of this._groups.values()) group.recalculateBounds();
9942
10203
  const viewport = this.getViewportWorldBounds();
9943
10204
  for (const group of this._groups.values()) if (group.visible && this.intersectsViewport(group.getBounds(), viewport)) {
9944
10205
  this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
9945
10206
  group.clearDirty();
9946
10207
  }
9947
10208
  if (this._contentDirty) {
10209
+ for (const group of this._groups.values()) group.recalculateBounds();
9948
10210
  this.edgeEndpointUpdater.updateAll();
9949
10211
  this._contentDirty = false;
9950
10212
  }
@@ -10007,11 +10269,17 @@ var DiagramRenderer = class extends EventEmitter {
10007
10269
  return this._nodeObstaclesCache;
10008
10270
  }
10009
10271
  getContentBounds() {
10010
- return getContentBounds({
10272
+ if (this._contentBoundsCache?.revision === this._contentRevision) return this._contentBoundsCache.bounds;
10273
+ const bounds = getContentBounds({
10011
10274
  nodes: this._nodes.values(),
10012
10275
  edges: this._edges.values(),
10013
10276
  groups: this._groups.values()
10014
10277
  });
10278
+ this._contentBoundsCache = {
10279
+ revision: this._contentRevision,
10280
+ bounds
10281
+ };
10282
+ return bounds;
10015
10283
  }
10016
10284
  notifyScrollbarInteraction() {
10017
10285
  this.scrollbarController.notifyInteraction();
@@ -10106,6 +10374,7 @@ var SearchManager = class {
10106
10374
  this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => this.renderHighlights(ctx));
10107
10375
  }
10108
10376
  destroy() {
10377
+ if (this.filterActive) this.clearFilter();
10109
10378
  this.overlayCleanup?.();
10110
10379
  this.overlayCleanup = null;
10111
10380
  this.highlightIds.clear();
@@ -11894,7 +12163,7 @@ var Group = class Group extends Element {
11894
12163
  }
11895
12164
  applyStyleManager(styleManager) {
11896
12165
  const mergedStyle = {
11897
- ...styleManager.getGroupStyle(false, this._styleClass),
12166
+ ...styleManager.getGroupStyle(this._state === "selected", this._styleClass),
11898
12167
  ...this._style
11899
12168
  };
11900
12169
  if (!shallowEqual(this._groupStyle, mergedStyle)) {
@@ -11947,6 +12216,7 @@ var Group = class Group extends Element {
11947
12216
  */
11948
12217
  recalculateBounds() {
11949
12218
  if (this._children.length === 0) {
12219
+ if (this._x === 0 && this._y === 0 && this._width === 0 && this._height === 0) return;
11950
12220
  this._x = 0;
11951
12221
  this._y = 0;
11952
12222
  this._width = 0;
@@ -11965,10 +12235,15 @@ var Group = class Group extends Element {
11965
12235
  maxX = Math.max(maxX, bounds.x + bounds.width);
11966
12236
  maxY = Math.max(maxY, bounds.y + bounds.height);
11967
12237
  }
11968
- this._x = minX - this._padding;
11969
- this._y = minY - this._padding;
11970
- this._width = maxX - minX + this._padding * 2;
11971
- this._height = maxY - minY + this._padding * 2;
12238
+ const nextX = minX - this._padding;
12239
+ const nextY = minY - this._padding;
12240
+ const nextWidth = maxX - minX + this._padding * 2;
12241
+ const nextHeight = maxY - minY + this._padding * 2;
12242
+ if (this._x === nextX && this._y === nextY && this._width === nextWidth && this._height === nextHeight) return;
12243
+ this._x = nextX;
12244
+ this._y = nextY;
12245
+ this._width = nextWidth;
12246
+ this._height = nextHeight;
11972
12247
  this.markDirty();
11973
12248
  }
11974
12249
  hitTest(point) {
@@ -11996,7 +12271,6 @@ var Group = class Group extends Element {
11996
12271
  ctx.fillText(this._label, x + 8, y + 4);
11997
12272
  }
11998
12273
  ctx.globalAlpha = 1;
11999
- for (const child of this._children) if (child.visible) child.render(ctx);
12000
12274
  }
12001
12275
  getBounds() {
12002
12276
  return {
@@ -12133,11 +12407,12 @@ var CircleNode = class extends Node {
12133
12407
  };
12134
12408
  }
12135
12409
  getLabelContainerBounds(bounds) {
12136
- const width = bounds.width / Math.SQRT2;
12137
- 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;
12138
12413
  return {
12139
- x: bounds.x + (bounds.width - width) / 2,
12140
- y: bounds.y + (bounds.height - height) / 2,
12414
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12415
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12141
12416
  width,
12142
12417
  height
12143
12418
  };
@@ -12294,11 +12569,12 @@ var DiamondNode = class extends Node {
12294
12569
  };
12295
12570
  }
12296
12571
  getLabelContainerBounds(bounds) {
12297
- const width = bounds.width / 2;
12298
- const height = bounds.height / 2;
12572
+ const insetBounds = super.getLabelContainerBounds(bounds);
12573
+ const width = insetBounds.width / 2;
12574
+ const height = insetBounds.height / 2;
12299
12575
  return {
12300
- x: bounds.x + (bounds.width - width) / 2,
12301
- y: bounds.y + (bounds.height - height) / 2,
12576
+ x: insetBounds.x + (insetBounds.width - width) / 2,
12577
+ y: insetBounds.y + (insetBounds.height - height) / 2,
12302
12578
  width,
12303
12579
  height
12304
12580
  };
@@ -12856,6 +13132,7 @@ var CText = class extends CompositeComponentBase {
12856
13132
  if (this.style.visible === false) return;
12857
13133
  const opacity = this.style.opacity ?? 1;
12858
13134
  if (opacity <= 0) return;
13135
+ if (!isScreenFontReadable(ctx, this._fontSize)) return;
12859
13136
  ctx.save();
12860
13137
  ctx.globalAlpha *= opacity;
12861
13138
  ctx.font = this.getFont();
@@ -12908,8 +13185,14 @@ var CText = class extends CompositeComponentBase {
12908
13185
  ctx.rotate(this._rotation * Math.PI / 180);
12909
13186
  const offsetX = xBase - (textBounds.x + textBounds.width / 2);
12910
13187
  const offsetY = yStart - (textBounds.y + textBounds.height / 2);
12911
- for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], offsetX, offsetY + i * lineHeightPx);
12912
- } else for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
13188
+ for (let i = 0; i < lines.length; i++) {
13189
+ const y = offsetY + i * lineHeightPx;
13190
+ ctx.fillText(lines[i], offsetX, y);
13191
+ }
13192
+ } else for (let i = 0; i < lines.length; i++) {
13193
+ const y = yStart + i * lineHeightPx;
13194
+ ctx.fillText(lines[i], xBase, y);
13195
+ }
12913
13196
  ctx.restore();
12914
13197
  }
12915
13198
  serialize() {
@@ -13309,16 +13592,33 @@ function flexLayout(container, config, children) {
13309
13592
  finalSizes[i] = finalSizes[i] + remaining * child.flexGrow / totalGrow;
13310
13593
  }
13311
13594
  } else if (remaining < 0) {
13312
- let totalShrink = 0;
13313
- for (let i = 0; i < children.length; i++) totalShrink += children[i].flexShrink * baseSizes[i];
13314
- if (totalShrink > 0) {
13315
- const deficit = -remaining;
13316
- 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) {
13317
13609
  const child = children[i];
13318
13610
  const shrinkRatio = child.flexShrink * baseSizes[i] / totalShrink;
13319
13611
  const minMain = isRow ? child.minSize.width : child.minSize.height;
13320
- 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
+ }
13321
13619
  }
13620
+ if (distributed <= epsilon) break;
13621
+ deficit -= distributed;
13322
13622
  }
13323
13623
  }
13324
13624
  let actualTotal = totalGaps + totalMargins;
@@ -14007,13 +14307,24 @@ var CompositeNode = class extends Node {
14007
14307
  const fillOpacity = style.fillOpacity ?? 1;
14008
14308
  const strokeOpacity = style.strokeOpacity ?? 1;
14009
14309
  this.applyStyle(ctx);
14010
- ctx.beginPath();
14011
- this.buildShapePath(ctx, x, y, width, height);
14012
- ctx.closePath();
14013
- ctx.globalAlpha = baseOpacity * fillOpacity;
14014
- ctx.fill();
14015
- ctx.globalAlpha = baseOpacity * strokeOpacity;
14016
- 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
+ }
14017
14328
  ctx.globalAlpha = 1;
14018
14329
  this.renderContents(ctx);
14019
14330
  }
@@ -14035,15 +14346,6 @@ var CompositeNode = class extends Node {
14035
14346
  break;
14036
14347
  }
14037
14348
  case "custom":
14038
- if (this._pathFactory) {
14039
- const path = this._pathFactory(w, h);
14040
- ctx.save();
14041
- ctx.translate(x, y);
14042
- ctx.fill(path);
14043
- ctx.stroke(path);
14044
- ctx.restore();
14045
- return;
14046
- }
14047
14349
  this.buildRectPath(ctx, x, y, w, h);
14048
14350
  break;
14049
14351
  default:
@@ -14081,35 +14383,40 @@ var CompositeNode = class extends Node {
14081
14383
  }
14082
14384
  applyAutoSize(ctx) {
14083
14385
  const contentSize = this._content.measure(ctx);
14084
- 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);
14085
14391
  const neededWidth = Math.max(this._minWidth, contentSize.width + inset.left + inset.right);
14086
14392
  const neededHeight = Math.max(this._minHeight, contentSize.height + inset.top + inset.bottom);
14087
14393
  if (neededWidth > this._width) this._width = neededWidth;
14088
14394
  if (neededHeight > this._height) this._height = neededHeight;
14089
14395
  }
14090
14396
  getLabelContainerBounds(bounds) {
14397
+ const insetBounds = super.getLabelContainerBounds(bounds);
14091
14398
  if (this._shapeType === "circle") {
14092
14399
  const factor = 1 / Math.SQRT2;
14093
- const w = bounds.width * factor;
14094
- const h = bounds.height * factor;
14400
+ const w = insetBounds.width * factor;
14401
+ const h = insetBounds.height * factor;
14095
14402
  return {
14096
- x: bounds.x + (bounds.width - w) / 2,
14097
- y: bounds.y + (bounds.height - h) / 2,
14403
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14404
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14098
14405
  width: w,
14099
14406
  height: h
14100
14407
  };
14101
14408
  }
14102
14409
  if (this._shapeType === "diamond") {
14103
- const w = bounds.width / 2;
14104
- const h = bounds.height / 2;
14410
+ const w = insetBounds.width / 2;
14411
+ const h = insetBounds.height / 2;
14105
14412
  return {
14106
- x: bounds.x + (bounds.width - w) / 2,
14107
- y: bounds.y + (bounds.height - h) / 2,
14413
+ x: insetBounds.x + (insetBounds.width - w) / 2,
14414
+ y: insetBounds.y + (insetBounds.height - h) / 2,
14108
14415
  width: w,
14109
14416
  height: h
14110
14417
  };
14111
14418
  }
14112
- return super.getLabelContainerBounds(bounds);
14419
+ return insetBounds;
14113
14420
  }
14114
14421
  /**
14115
14422
  * Get the minimum content size needed for the content tree.
@@ -14317,16 +14624,22 @@ var DARK_THEME = {
14317
14624
  }
14318
14625
  }
14319
14626
  };
14627
+ function cloneTheme(theme) {
14628
+ return structuredClone(theme);
14629
+ }
14320
14630
  /**
14321
14631
  * Manages styles and themes
14322
14632
  */
14323
14633
  var StyleManager = class {
14324
14634
  constructor(theme) {
14325
14635
  this._classes = /* @__PURE__ */ new Map();
14326
- this._builtInThemes = new Map([["default", DEFAULT_THEME], ["dark", DARK_THEME]]);
14327
- if (theme === void 0) this._theme = DEFAULT_THEME;
14328
- else if (typeof theme === "string") this._theme = this._builtInThemes.get(theme) ?? DEFAULT_THEME;
14329
- 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);
14330
14643
  }
14331
14644
  /**
14332
14645
  * Current theme
@@ -14340,14 +14653,15 @@ var StyleManager = class {
14340
14653
  setTheme(theme) {
14341
14654
  if (typeof theme === "string") {
14342
14655
  const builtIn = this._builtInThemes.get(theme);
14343
- if (builtIn !== void 0) this._theme = builtIn;
14344
- } 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);
14345
14659
  }
14346
14660
  /**
14347
14661
  * Register a custom theme
14348
14662
  */
14349
14663
  registerTheme(theme) {
14350
- this._builtInThemes.set(theme.name, theme);
14664
+ this._builtInThemes.set(theme.name, cloneTheme(theme));
14351
14665
  }
14352
14666
  /**
14353
14667
  * Get all available theme names
@@ -14462,10 +14776,17 @@ var StyleManager = class {
14462
14776
  }
14463
14777
  }
14464
14778
  getBaseEdgeStyle(state) {
14779
+ const defaultStyle = this._theme.edge.default;
14465
14780
  switch (state) {
14466
- case "hover": return this._theme.edge.hover;
14467
- case "selected": return this._theme.edge.selected;
14468
- 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;
14469
14790
  }
14470
14791
  }
14471
14792
  };
@@ -14637,36 +14958,7 @@ var Serializer = class {
14637
14958
  position: port.position,
14638
14959
  styleClass: port.styleClass
14639
14960
  }));
14640
- let label;
14641
- if (node.label) {
14642
- const ins = node.label.inset;
14643
- const defaultInset = 8;
14644
- 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 : {
14645
- top: ins.top,
14646
- right: ins.right,
14647
- bottom: ins.bottom,
14648
- left: ins.left
14649
- };
14650
- if (hasNonDefaultValues({
14651
- text: node.label.text,
14652
- style: node.label.style,
14653
- maxWidth: node.label.maxWidth,
14654
- inset: serializedInset ?? void 0,
14655
- styleClass: node.label.styleClass
14656
- }, {
14657
- inset: void 0,
14658
- style: {},
14659
- maxWidth: void 0,
14660
- styleClass: void 0
14661
- })) label = omitEmptyValues({
14662
- text: node.label.text,
14663
- style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
14664
- maxWidth: node.label.maxWidth,
14665
- inset: serializedInset,
14666
- styleClass: node.label.styleClass
14667
- });
14668
- else label = node.label.text;
14669
- }
14961
+ const label = node.label ? this.serializeTextLabel(node.label) : void 0;
14670
14962
  let icon;
14671
14963
  if (node.icon) {
14672
14964
  const opts = node.icon.options;
@@ -14699,6 +14991,8 @@ var Serializer = class {
14699
14991
  if (hasNonDefaultValues(anchorData, anchorDefaults)) anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
14700
14992
  const contentInset = node.contentInset;
14701
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;
14702
14996
  const base = omitEmptyValues({
14703
14997
  id: node.id,
14704
14998
  type: node.typeName,
@@ -14706,14 +15000,16 @@ var Serializer = class {
14706
15000
  y: node.y,
14707
15001
  width: node.width,
14708
15002
  height: node.height,
14709
- style: node.style,
15003
+ style: { ...node.styleOverrides },
14710
15004
  styleClass: node.styleClass,
14711
15005
  label,
14712
- labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
14713
15006
  labelPlacement: node.labelPlacement !== "center" ? node.labelPlacement : void 0,
14714
15007
  labelGap: node.labelGap !== 4 ? node.labelGap : void 0,
14715
15008
  icon,
14716
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,
14717
15013
  anchorPoints,
14718
15014
  ports: ports.length > 0 ? ports : void 0,
14719
15015
  data: Object.keys(node.data).length > 0 ? node.data : void 0
@@ -14740,14 +15036,14 @@ var Serializer = class {
14740
15036
  arrowType: edge.arrowType,
14741
15037
  startMarker: edge.startMarker,
14742
15038
  endMarker: edge.endMarker,
14743
- style: edge.style,
15039
+ style: { ...edge.styleOverrides },
14744
15040
  styleClass: edge.styleClass,
14745
- label: edge.label?.text,
14746
- labelStyleClass: edge.label?.styleClass,
15041
+ label: edge.label ? this.serializeTextLabel(edge.label) : void 0,
14747
15042
  labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
14748
15043
  labelPosition: edge.labelPosition !== .5 ? edge.labelPosition : void 0,
14749
15044
  labelFollowPath: edge.labelFollowPath ? true : void 0,
14750
15045
  labelBackground: edge.labelBackground,
15046
+ lockAnchors: edge.lockAnchors ? void 0 : false,
14751
15047
  labelLineGap: edge.labelLineGap ? true : void 0,
14752
15048
  data: Object.keys(edge.data).length > 0 ? edge.data : void 0
14753
15049
  };
@@ -14756,12 +15052,31 @@ var Serializer = class {
14756
15052
  return {
14757
15053
  id: group.id,
14758
15054
  childIds: group.children.map((c) => c.id),
14759
- style: group.style,
15055
+ style: { ...group.styleOverrides },
14760
15056
  styleClass: group.styleClass,
14761
15057
  label: group.label,
15058
+ padding: group.padding !== 20 ? group.padding : void 0,
14762
15059
  data: Object.keys(group.data).length > 0 ? group.data : void 0
14763
15060
  };
14764
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
+ }
14765
15080
  };
14766
15081
  //#endregion
14767
15082
  //#region src/utils/download.ts
@@ -14843,7 +15158,11 @@ var ImageExporter = class {
14843
15158
  const padding = options.padding ?? 20;
14844
15159
  const format = options.format ?? "png";
14845
15160
  const quality = options.quality ?? .92;
14846
- 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
+ });
14847
15166
  if (bounds === null) return this.createBlankImage(100, 100, backgroundColor, format, quality);
14848
15167
  const width = (bounds.width + padding * 2) * scale;
14849
15168
  const height = (bounds.height + padding * 2) * scale;
@@ -14895,6 +15214,7 @@ var SvgExporter = class {
14895
15214
  const padding = options.padding ?? 20;
14896
15215
  const includeBackground = options.includeBackground ?? true;
14897
15216
  const backgroundColor = options.backgroundColor ?? "#ffffff";
15217
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
14898
15218
  const edgeLabelOffset = options.edgeLabelOffset;
14899
15219
  const bounds = getContentBounds({
14900
15220
  nodes: this.renderer.nodes.values(),
@@ -14911,7 +15231,7 @@ var SvgExporter = class {
14911
15231
  const renderedEdges = [];
14912
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}">`);
14913
15233
  for (const edge of this.renderer.edges.values()) if (edge.visible) renderedEdges.push(this.renderEdge(edge, edgeLabelOffset));
14914
- if (includeBackground) parts.push(`<rect width="100%" height="100%" fill="${backgroundColor}"/>`);
15234
+ if (includeBackground) parts.push(`<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>`);
14915
15235
  parts.push(`<g transform="translate(${offsetX}, ${offsetY})">`);
14916
15236
  for (const group of this.renderer.groups.values()) if (group.visible) parts.push(this.renderGroup(group));
14917
15237
  for (const node of this.renderer.nodes.values()) if (node.visible) parts.push(this.renderNode(node));
@@ -14930,8 +15250,8 @@ var SvgExporter = class {
14930
15250
  renderGroup(group) {
14931
15251
  const bounds = group.getBounds();
14932
15252
  const style = group.style;
14933
- const fill = style.fillColor ?? "rgba(200, 200, 200, 0.2)";
14934
- 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");
14935
15255
  const strokeWidth = style.strokeWidth ?? 1;
14936
15256
  const opacity = style.opacity ?? 1;
14937
15257
  const label = group.label ? `<text x="${bounds.x + 8}" y="${bounds.y + 14}" fill="#666666" font-size="12">${this.escapeText(group.label)}</text>` : "";
@@ -14945,8 +15265,8 @@ var SvgExporter = class {
14945
15265
  renderNode(node) {
14946
15266
  const bounds = node.getBounds();
14947
15267
  const style = node.style;
14948
- const fill = style.fillColor ?? "#ffffff";
14949
- const stroke = style.strokeColor ?? "#333333";
15268
+ const fill = this.escapeAttribute(style.fillColor ?? "#ffffff");
15269
+ const stroke = this.escapeAttribute(style.strokeColor ?? "#333333");
14950
15270
  const strokeWidth = style.strokeWidth ?? 2;
14951
15271
  const baseOpacity = style.opacity ?? 1;
14952
15272
  const fillOpacity = (style.fillOpacity ?? 1) * baseOpacity;
@@ -15043,14 +15363,15 @@ var SvgExporter = class {
15043
15363
  if (edge.path.length < 2) return "";
15044
15364
  const style = edge.style;
15045
15365
  const stroke = style.strokeColor ?? "#666666";
15366
+ const escapedStroke = this.escapeAttribute(stroke);
15046
15367
  const strokeWidth = style.strokeWidth ?? 2;
15047
15368
  const strokeOpacity = (style.strokeOpacity ?? 1) * (style.opacity ?? 1);
15048
- const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
15049
- 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)}"` : "";
15050
15371
  const dashValues = style.flowDash ?? style.lineDash;
15051
15372
  const dash = dashValues ? ` stroke-dasharray="${dashValues.join(" ")}"` : "";
15052
15373
  const dashOffset = style.lineDashOffset !== void 0 ? ` stroke-dashoffset="${style.lineDashOffset}"` : "";
15053
- 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)}`;
15054
15375
  }
15055
15376
  renderEdgeLabel(edge, edgeLabelOffset) {
15056
15377
  if (!edge.label) return "";
@@ -15062,7 +15383,7 @@ var SvgExporter = class {
15062
15383
  renderEdgeLabelBackground(edge, point, rotation = 0) {
15063
15384
  if (!edge.label) return "";
15064
15385
  const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
15065
- const bgColor = edge.labelBackground?.color ?? "#ffffff";
15386
+ const bgColor = this.escapeAttribute(edge.labelBackground?.color ?? "#ffffff");
15066
15387
  const bgOpacity = edge.labelBackground?.opacity ?? 1;
15067
15388
  const bgRadius = edge.labelBackground?.borderRadius ?? 2;
15068
15389
  const x = point.x - metrics.width / 2;
@@ -15143,7 +15464,11 @@ var SvgExporter = class {
15143
15464
  return calculateMarkerPoints(edge.path, position, edge.type);
15144
15465
  }
15145
15466
  renderMarkerShape(marker, from, to, edgeStroke) {
15146
- 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));
15147
15472
  }
15148
15473
  buildPath(edge) {
15149
15474
  const path = edge.path;
@@ -15213,11 +15538,11 @@ var SvgExporter = class {
15213
15538
  };
15214
15539
  }
15215
15540
  renderTextLabel(text, point, style = {}, rotation = 0) {
15216
- const fill = style.color ?? "#000000";
15541
+ const fill = this.escapeAttribute(style.color ?? "#000000");
15217
15542
  const fontSize = style.fontSize ?? 14;
15218
- const fontFamily = style.fontFamily ?? "sans-serif";
15219
- const fontWeight = style.fontWeight ?? "normal";
15220
- 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");
15221
15546
  const fontStyleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
15222
15547
  const opacity = style.opacity ?? 1;
15223
15548
  const anchor = style.align === "left" ? "start" : style.align === "right" ? "end" : "middle";
@@ -15413,9 +15738,10 @@ var SvgExporter = class {
15413
15738
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15414
15739
  }
15415
15740
  createEmptySvg(width, height, backgroundColor, includeBackground) {
15741
+ const escapedBackgroundColor = this.escapeAttribute(backgroundColor);
15416
15742
  return [
15417
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}">`,
15418
- includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
15744
+ includeBackground ? `<rect width="100%" height="100%" fill="${escapedBackgroundColor}"/>` : "",
15419
15745
  "</svg>"
15420
15746
  ].join("");
15421
15747
  }
@@ -15546,6 +15872,24 @@ function distributeNodes(nodes, type) {
15546
15872
  }
15547
15873
  }
15548
15874
  }
15875
+ /**
15876
+ * World step that keeps the visible grid cell in a Miro-like range.
15877
+ * The base `gridSize` is doubled or halved so the screen size stays ~16–32 px.
15878
+ */
15879
+ function adaptiveGridStep(gridSize, zoom) {
15880
+ if (!Number.isFinite(gridSize) || !Number.isFinite(zoom) || gridSize <= 0 || zoom <= 0) return gridSize;
15881
+ let step = gridSize;
15882
+ let screen = step * zoom;
15883
+ while (screen < 16) {
15884
+ step *= 2;
15885
+ screen *= 2;
15886
+ }
15887
+ while (screen > 32) {
15888
+ step /= 2;
15889
+ screen /= 2;
15890
+ }
15891
+ return step;
15892
+ }
15549
15893
  //#endregion
15550
15894
  //#region src/core/overlays/BaseOverlay.ts
15551
15895
  /**
@@ -15584,20 +15928,20 @@ var GridOverlay = class extends BaseOverlay {
15584
15928
  if (!this.enabled) return;
15585
15929
  const width = renderer.width;
15586
15930
  const height = renderer.height;
15587
- const gridSize = this.gridSize;
15588
- const color = this.color;
15589
- const startX = Math.floor(-renderer.offsetX / renderer.zoom / gridSize) * gridSize;
15590
- const startY = Math.floor(-renderer.offsetY / renderer.zoom / gridSize) * gridSize;
15591
- const endX = Math.ceil((width - renderer.offsetX) / renderer.zoom / gridSize) * gridSize;
15592
- const endY = Math.ceil((height - renderer.offsetY) / renderer.zoom / gridSize) * gridSize;
15593
- ctx.strokeStyle = color;
15594
- ctx.lineWidth = 1 / renderer.zoom;
15931
+ const zoom = renderer.zoom;
15932
+ const step = adaptiveGridStep(this.gridSize, zoom);
15933
+ const startX = Math.floor(-renderer.offsetX / zoom / step) * step;
15934
+ const startY = Math.floor(-renderer.offsetY / zoom / step) * step;
15935
+ const endX = Math.ceil((width - renderer.offsetX) / zoom / step) * step;
15936
+ const endY = Math.ceil((height - renderer.offsetY) / zoom / step) * step;
15937
+ ctx.strokeStyle = this.color;
15938
+ ctx.lineWidth = 1 / zoom;
15595
15939
  ctx.beginPath();
15596
- for (let x = startX; x <= endX; x += gridSize) {
15940
+ for (let x = startX; x <= endX; x += step) {
15597
15941
  ctx.moveTo(x, startY);
15598
15942
  ctx.lineTo(x, endY);
15599
15943
  }
15600
- for (let y = startY; y <= endY; y += gridSize) {
15944
+ for (let y = startY; y <= endY; y += step) {
15601
15945
  ctx.moveTo(startX, y);
15602
15946
  ctx.lineTo(endX, y);
15603
15947
  }
@@ -15610,51 +15954,114 @@ var GridOverlay = class extends BaseOverlay {
15610
15954
  var RulersOverlay = class extends BaseOverlay {
15611
15955
  constructor(options = {}) {
15612
15956
  super(options.enabled ?? true);
15957
+ this.tickBake = null;
15958
+ this.tickBakeGeneration = 0;
15613
15959
  this.thickness = options.thickness ?? 20;
15614
15960
  this.backgroundColor = options.backgroundColor ?? "#f4f4f5";
15615
15961
  this.textColor = options.textColor ?? "#52525b";
15616
15962
  this.tickColor = options.tickColor ?? "#a1a1aa";
15617
15963
  }
15964
+ getTickBakeGeneration() {
15965
+ return this.tickBakeGeneration;
15966
+ }
15618
15967
  install(renderer) {
15619
15968
  this.removeOverlay = renderer.addOverlayRenderer((ctx) => {
15620
15969
  if (!this.enabled) return;
15621
15970
  const thickness = this.thickness;
15971
+ const zoom = renderer.zoom;
15972
+ const worldLeft = -renderer.offsetX / zoom;
15973
+ const worldTop = -renderer.offsetY / zoom;
15622
15974
  ctx.save();
15623
15975
  ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
15624
15976
  ctx.fillStyle = this.backgroundColor;
15625
15977
  ctx.fillRect(0, 0, renderer.width, thickness);
15626
15978
  ctx.fillRect(0, 0, thickness, renderer.height);
15627
- ctx.strokeStyle = this.tickColor;
15628
- ctx.fillStyle = this.textColor;
15629
- ctx.font = "10px sans-serif";
15630
- ctx.textAlign = "left";
15631
- ctx.textBaseline = "top";
15632
- const step = this.getTickStep(renderer.zoom);
15633
- const worldLeft = -renderer.offsetX / renderer.zoom;
15634
- const worldTop = -renderer.offsetY / renderer.zoom;
15635
- const worldRight = worldLeft + renderer.width / renderer.zoom;
15636
- const worldBottom = worldTop + renderer.height / renderer.zoom;
15637
- ctx.beginPath();
15638
- for (let x = Math.floor(worldLeft / step) * step; x <= worldRight; x += step) {
15639
- const screenX = x * renderer.zoom + renderer.offsetX;
15640
- ctx.moveTo(screenX, thickness);
15641
- ctx.lineTo(screenX, thickness - 6);
15642
- ctx.fillText(`${Math.round(x)}`, screenX + 2, 2);
15643
- }
15644
- for (let y = Math.floor(worldTop / step) * step; y <= worldBottom; y += step) {
15645
- const screenY = y * renderer.zoom + renderer.offsetY;
15646
- ctx.moveTo(thickness, screenY);
15647
- ctx.lineTo(thickness - 6, screenY);
15648
- ctx.save();
15649
- ctx.translate(2, screenY + 2);
15650
- ctx.rotate(-Math.PI / 2);
15651
- ctx.fillText(`${Math.round(y)}`, 0, 0);
15652
- ctx.restore();
15653
- }
15654
- ctx.stroke();
15979
+ const bake = this.ensureTickBake(renderer);
15980
+ const srcX = (worldLeft - bake.worldLeft) * zoom;
15981
+ const srcY = (worldTop - bake.worldTop) * zoom;
15982
+ const pr = renderer.pixelRatio;
15983
+ ctx.drawImage(bake.horizontal, srcX * pr, 0, renderer.width * pr, thickness * pr, 0, 0, renderer.width, thickness);
15984
+ ctx.drawImage(bake.vertical, 0, srcY * pr, thickness * pr, renderer.height * pr, 0, 0, thickness, renderer.height);
15655
15985
  ctx.restore();
15656
15986
  });
15657
15987
  }
15988
+ ensureTickBake(renderer) {
15989
+ const zoom = renderer.zoom;
15990
+ const worldLeft = -renderer.offsetX / zoom;
15991
+ const worldTop = -renderer.offsetY / zoom;
15992
+ const worldRight = worldLeft + renderer.width / zoom;
15993
+ const worldBottom = worldTop + renderer.height / zoom;
15994
+ if (this.tickBake !== null && this.tickBake.zoom === zoom && this.tickBake.pixelRatio === renderer.pixelRatio && worldLeft >= this.tickBake.worldLeft && worldRight <= this.tickBake.worldRight && worldTop >= this.tickBake.worldTop && worldBottom <= this.tickBake.worldBottom) return this.tickBake;
15995
+ const extraX = renderer.width / zoom;
15996
+ const extraY = renderer.height / zoom;
15997
+ const bakeLeft = worldLeft - extraX;
15998
+ const bakeTop = worldTop - extraY;
15999
+ const bakeRight = worldRight + extraX;
16000
+ const bakeBottom = worldBottom + extraY;
16001
+ const pr = Math.max(1, renderer.pixelRatio);
16002
+ const thickness = this.thickness;
16003
+ const step = this.getTickStep(zoom);
16004
+ const horizontal = this.tickBake?.horizontal ?? document.createElement("canvas");
16005
+ const vertical = this.tickBake?.vertical ?? document.createElement("canvas");
16006
+ const hWidth = Math.max(1, Math.ceil((bakeRight - bakeLeft) * zoom));
16007
+ const vHeight = Math.max(1, Math.ceil((bakeBottom - bakeTop) * zoom));
16008
+ horizontal.width = Math.max(1, Math.ceil(hWidth * pr));
16009
+ horizontal.height = Math.max(1, Math.ceil(thickness * pr));
16010
+ vertical.width = Math.max(1, Math.ceil(thickness * pr));
16011
+ vertical.height = Math.max(1, Math.ceil(vHeight * pr));
16012
+ const hCtx = horizontal.getContext("2d");
16013
+ const vCtx = vertical.getContext("2d");
16014
+ if (hCtx !== null) {
16015
+ hCtx.setTransform(pr, 0, 0, pr, 0, 0);
16016
+ hCtx.clearRect(0, 0, hWidth, thickness);
16017
+ hCtx.strokeStyle = this.tickColor;
16018
+ hCtx.fillStyle = this.textColor;
16019
+ hCtx.font = "10px sans-serif";
16020
+ hCtx.textAlign = "left";
16021
+ hCtx.textBaseline = "top";
16022
+ hCtx.beginPath();
16023
+ for (let x = Math.floor(bakeLeft / step) * step; x <= bakeRight; x += step) {
16024
+ const screenX = (x - bakeLeft) * zoom;
16025
+ hCtx.moveTo(screenX, thickness);
16026
+ hCtx.lineTo(screenX, thickness - 6);
16027
+ hCtx.fillText(`${Math.round(x)}`, screenX + 2, 2);
16028
+ }
16029
+ hCtx.stroke();
16030
+ }
16031
+ if (vCtx !== null) {
16032
+ vCtx.setTransform(pr, 0, 0, pr, 0, 0);
16033
+ vCtx.clearRect(0, 0, thickness, vHeight);
16034
+ vCtx.strokeStyle = this.tickColor;
16035
+ vCtx.fillStyle = this.textColor;
16036
+ vCtx.font = "10px sans-serif";
16037
+ vCtx.textAlign = "left";
16038
+ vCtx.textBaseline = "top";
16039
+ vCtx.beginPath();
16040
+ for (let y = Math.floor(bakeTop / step) * step; y <= bakeBottom; y += step) {
16041
+ const screenY = (y - bakeTop) * zoom;
16042
+ vCtx.moveTo(thickness, screenY);
16043
+ vCtx.lineTo(thickness - 6, screenY);
16044
+ vCtx.save();
16045
+ vCtx.translate(2, screenY + 2);
16046
+ vCtx.rotate(-Math.PI / 2);
16047
+ vCtx.fillText(`${Math.round(y)}`, 0, 0);
16048
+ vCtx.restore();
16049
+ }
16050
+ vCtx.stroke();
16051
+ }
16052
+ this.tickBakeGeneration += 1;
16053
+ this.tickBake = {
16054
+ zoom,
16055
+ pixelRatio: renderer.pixelRatio,
16056
+ worldLeft: bakeLeft,
16057
+ worldTop: bakeTop,
16058
+ worldRight: bakeRight,
16059
+ worldBottom: bakeBottom,
16060
+ horizontal,
16061
+ vertical
16062
+ };
16063
+ return this.tickBake;
16064
+ }
15658
16065
  getTickStep(zoom) {
15659
16066
  const rawStep = 80 / zoom;
15660
16067
  const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
@@ -15710,6 +16117,7 @@ var GuidesOverlay = class extends BaseOverlay {
15710
16117
  var MiniMap = class extends BaseOverlay {
15711
16118
  constructor(options = {}) {
15712
16119
  super(options.enabled ?? true);
16120
+ this.contentBake = null;
15713
16121
  this.options = {
15714
16122
  width: options.width ?? 180,
15715
16123
  height: options.height ?? 120,
@@ -15721,12 +16129,16 @@ var MiniMap = class extends BaseOverlay {
15721
16129
  anchor: options.anchor ?? "bottom-right"
15722
16130
  };
15723
16131
  }
16132
+ /** Cached bake projection; stable across pan if only the viewport moved. */
16133
+ getContentBakeProjectionKey() {
16134
+ return this.contentBake?.projectionKey ?? null;
16135
+ }
15724
16136
  install(renderer) {
15725
16137
  this.removeOverlay = renderer.addTopOverlayRenderer((ctx) => {
15726
16138
  if (!this.enabled) return;
15727
16139
  const layout = this.computeLayout(renderer);
15728
16140
  if (!layout) return;
15729
- const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY, viewRect } = layout;
16141
+ const { x, y, width, height, viewRect } = layout;
15730
16142
  ctx.save();
15731
16143
  ctx.setTransform(renderer.pixelRatio, 0, 0, renderer.pixelRatio, 0, 0);
15732
16144
  ctx.setLineDash([]);
@@ -15736,42 +16148,61 @@ var MiniMap = class extends BaseOverlay {
15736
16148
  ctx.strokeStyle = this.options.borderColor;
15737
16149
  ctx.lineWidth = 1;
15738
16150
  ctx.strokeRect(x, y, width, height);
15739
- ctx.save();
15740
- ctx.translate(mapOffsetX, mapOffsetY);
15741
- ctx.scale(scale, scale);
15742
- ctx.translate(-bounds.x, -bounds.y);
15743
- ctx.setLineDash([]);
15744
- ctx.lineDashOffset = 0;
15745
- ctx.strokeStyle = "#94a3b8";
15746
- ctx.lineWidth = 1 / scale;
15747
- for (const edge of renderer.edges.values()) {
15748
- if (!edge.visible) continue;
15749
- const path = edge.path;
15750
- if (path.length < 2) continue;
15751
- ctx.beginPath();
15752
- ctx.moveTo(path[0].x, path[0].y);
15753
- for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y);
15754
- ctx.stroke();
16151
+ const bake = this.ensureContentBake(renderer, layout);
16152
+ ctx.drawImage(bake, x, y, width, height);
16153
+ if (viewRect.width >= 0 && viewRect.height >= 0) {
16154
+ ctx.strokeStyle = this.options.viewportColor;
16155
+ ctx.lineWidth = 1.5;
16156
+ ctx.setLineDash([]);
16157
+ ctx.lineDashOffset = 0;
16158
+ ctx.strokeRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
15755
16159
  }
15756
- for (const node of renderer.nodes.values()) {
15757
- if (!node.visible) continue;
15758
- const b = node.getBounds();
15759
- ctx.fillStyle = "#e2e8f0";
15760
- ctx.fillRect(b.x, b.y, b.width, b.height);
15761
- }
15762
- ctx.restore();
15763
- if (viewRect.width < 0 || viewRect.height < 0) {
15764
- ctx.restore();
15765
- return;
15766
- }
15767
- ctx.strokeStyle = this.options.viewportColor;
15768
- ctx.lineWidth = 1.5;
15769
- ctx.setLineDash([]);
15770
- ctx.lineDashOffset = 0;
15771
- ctx.strokeRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
15772
16160
  ctx.restore();
15773
16161
  });
15774
16162
  }
16163
+ ensureContentBake(renderer, layout) {
16164
+ const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY } = layout;
16165
+ const revision = renderer.getContentRevision();
16166
+ const projectionKey = [
16167
+ width,
16168
+ height,
16169
+ Math.round(bounds.x),
16170
+ Math.round(bounds.y),
16171
+ Math.round(bounds.width),
16172
+ Math.round(bounds.height),
16173
+ scale.toFixed(5),
16174
+ Math.round(mapOffsetX),
16175
+ Math.round(mapOffsetY)
16176
+ ].join("|");
16177
+ if (this.contentBake !== null && this.contentBake.revision === revision && this.contentBake.projectionKey === projectionKey) return this.contentBake.canvas;
16178
+ const canvas = this.contentBake?.canvas ?? document.createElement("canvas");
16179
+ const pixelRatio = Math.max(1, Math.min(2, renderer.pixelRatio));
16180
+ canvas.width = Math.max(1, Math.ceil(width * pixelRatio));
16181
+ canvas.height = Math.max(1, Math.ceil(height * pixelRatio));
16182
+ const ctx = canvas.getContext("2d");
16183
+ if (ctx === null) return canvas;
16184
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
16185
+ ctx.clearRect(0, 0, width, height);
16186
+ ctx.save();
16187
+ ctx.translate(mapOffsetX - x, mapOffsetY - y);
16188
+ ctx.scale(scale, scale);
16189
+ ctx.translate(-bounds.x, -bounds.y);
16190
+ ctx.setLineDash([]);
16191
+ ctx.lineDashOffset = 0;
16192
+ for (const node of renderer.nodes.values()) {
16193
+ if (!node.visible) continue;
16194
+ const b = node.getBounds();
16195
+ ctx.fillStyle = "#e2e8f0";
16196
+ ctx.fillRect(b.x, b.y, b.width, b.height);
16197
+ }
16198
+ ctx.restore();
16199
+ this.contentBake = {
16200
+ revision,
16201
+ projectionKey,
16202
+ canvas
16203
+ };
16204
+ return canvas;
16205
+ }
15775
16206
  beginOverlayDrag(renderer, screenX, screenY) {
15776
16207
  const hit = this.hitTestMiniMap(renderer, screenX, screenY);
15777
16208
  if (!hit) return null;
@@ -15833,12 +16264,11 @@ var MiniMap = class extends BaseOverlay {
15833
16264
  computeLayout(renderer) {
15834
16265
  const rawBounds = getContentBounds({
15835
16266
  nodes: renderer.nodes.values(),
15836
- edges: renderer.edges.values(),
15837
16267
  groups: renderer.groups.values()
15838
16268
  });
15839
16269
  if (!rawBounds) return null;
15840
16270
  const margin = this.options.contentMargin;
15841
- const expandedContentBounds = {
16271
+ const bounds = {
15842
16272
  x: rawBounds.x - margin,
15843
16273
  y: rawBounds.y - margin,
15844
16274
  width: rawBounds.width + 2 * margin,
@@ -15848,17 +16278,7 @@ var MiniMap = class extends BaseOverlay {
15848
16278
  const x = anchor === "bottom-left" || anchor === "top-left" ? padding : renderer.width - width - padding;
15849
16279
  const y = anchor === "top-left" || anchor === "top-right" ? padding : renderer.height - height - padding;
15850
16280
  const viewportBounds = this.getViewportBounds(renderer);
15851
- const unionMinX = Math.min(expandedContentBounds.x, viewportBounds.x);
15852
- const unionMinY = Math.min(expandedContentBounds.y, viewportBounds.y);
15853
- const unionMaxX = Math.max(expandedContentBounds.x + expandedContentBounds.width, viewportBounds.x + viewportBounds.width);
15854
- const unionMaxY = Math.max(expandedContentBounds.y + expandedContentBounds.height, viewportBounds.y + viewportBounds.height);
15855
- const bounds = {
15856
- x: unionMinX,
15857
- y: unionMinY,
15858
- width: unionMaxX - unionMinX,
15859
- height: unionMaxY - unionMinY
15860
- };
15861
- const scale = Math.min(width / bounds.width, height / bounds.height);
16281
+ const scale = Math.min(width / Math.max(bounds.width, 1), height / Math.max(bounds.height, 1));
15862
16282
  const mapOffsetX = x + (width - bounds.width * scale) / 2;
15863
16283
  const mapOffsetY = y + (height - bounds.height * scale) / 2;
15864
16284
  let viewX = mapOffsetX + (viewportBounds.x - bounds.x) * scale;