@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.
- package/README.md +2 -1
- package/README.ru.md +2 -1
- package/dist/core/AnimationManager.d.ts +5 -0
- package/dist/core/AnimationManager.d.ts.map +1 -1
- package/dist/core/ConnectionManager.d.ts +5 -0
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts +15 -0
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/DragManager.d.ts +2 -1
- package/dist/core/DragManager.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +5 -0
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/NavigationManager.d.ts +4 -0
- package/dist/core/NavigationManager.d.ts.map +1 -1
- package/dist/core/SearchManager.d.ts.map +1 -1
- package/dist/core/SelectionManager.d.ts +1 -0
- package/dist/core/SelectionManager.d.ts.map +1 -1
- package/dist/core/overlays/GridOverlay.d.ts.map +1 -1
- package/dist/core/overlays/MiniMap.d.ts +8 -0
- package/dist/core/overlays/MiniMap.d.ts.map +1 -1
- package/dist/core/overlays/RulersOverlay.d.ts +4 -0
- package/dist/core/overlays/RulersOverlay.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/Group.d.ts.map +1 -1
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/elements/composite/CText.d.ts.map +1 -1
- package/dist/elements/composite/CompositeNode.d.ts.map +1 -1
- package/dist/elements/composite/FlexLayout.d.ts.map +1 -1
- package/dist/elements/edge/EdgeLabelRenderer.d.ts.map +1 -1
- package/dist/elements/nodes/CircleNode.d.ts.map +1 -1
- package/dist/elements/nodes/DiamondNode.d.ts.map +1 -1
- package/dist/elements/paths/BezierPathStrategy.d.ts.map +1 -1
- package/dist/events/EventEmitter.d.ts +2 -3
- package/dist/events/EventEmitter.d.ts.map +1 -1
- package/dist/events/InputHandler.d.ts +11 -2
- package/dist/events/InputHandler.d.ts.map +1 -1
- package/dist/papirus.js +665 -245
- package/dist/papirus.js.map +1 -1
- package/dist/styles/StyleManager.d.ts.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ImageExporter.d.ts.map +1 -1
- package/dist/utils/Serializer.d.ts +1 -0
- package/dist/utils/Serializer.d.ts.map +1 -1
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/dist/utils/adaptiveGridStep.d.ts +10 -0
- package/dist/utils/adaptiveGridStep.d.ts.map +1 -0
- package/dist/utils/pathViewport.d.ts +12 -0
- package/dist/utils/pathViewport.d.ts.map +1 -0
- package/dist/utils/screenText.d.ts +4 -0
- package/dist/utils/screenText.d.ts.map +1 -0
- 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
|
-
|
|
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)
|
|
59
|
-
|
|
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.
|
|
106
|
-
this.addCanvasListener("mousemove", 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
|
-
|
|
180
|
+
handleCanvasMouseMove(e) {
|
|
136
181
|
const event = this.normalizeMouseEvent(e);
|
|
137
|
-
this.emit("
|
|
182
|
+
this.emit("mousemove", event);
|
|
138
183
|
}
|
|
139
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
6048
|
-
|
|
6049
|
-
const
|
|
6050
|
-
|
|
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.
|
|
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.
|
|
8038
|
+
this.beginBackgroundPan(event);
|
|
7914
8039
|
return;
|
|
7915
8040
|
}
|
|
7916
8041
|
}
|
|
7917
|
-
if (this.navigationManager.handleMouseDown(event))
|
|
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.
|
|
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.
|
|
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(() =>
|
|
9876
|
+
edge.setBindingListener(() => {
|
|
9877
|
+
this.syncEdgeNodeIndex(edge);
|
|
9878
|
+
this.markContentDirty();
|
|
9879
|
+
});
|
|
9654
9880
|
this._edges.set(edge.id, edge);
|
|
9655
|
-
|
|
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
|
-
|
|
9713
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
9841
|
-
|
|
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())
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
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
|
|
12137
|
-
const
|
|
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:
|
|
12140
|
-
y:
|
|
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
|
|
12298
|
-
const
|
|
12572
|
+
const insetBounds = super.getLabelContainerBounds(bounds);
|
|
12573
|
+
const width = insetBounds.width / 2;
|
|
12574
|
+
const height = insetBounds.height / 2;
|
|
12299
12575
|
return {
|
|
12300
|
-
x:
|
|
12301
|
-
y:
|
|
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++)
|
|
12912
|
-
|
|
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
|
-
|
|
13313
|
-
for (let i = 0; i < children.length; i++)
|
|
13314
|
-
|
|
13315
|
-
const
|
|
13316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14011
|
-
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
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
|
|
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 =
|
|
14094
|
-
const h =
|
|
14400
|
+
const w = insetBounds.width * factor;
|
|
14401
|
+
const h = insetBounds.height * factor;
|
|
14095
14402
|
return {
|
|
14096
|
-
x:
|
|
14097
|
-
y:
|
|
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 =
|
|
14104
|
-
const h =
|
|
14410
|
+
const w = insetBounds.width / 2;
|
|
14411
|
+
const h = insetBounds.height / 2;
|
|
14105
14412
|
return {
|
|
14106
|
-
x:
|
|
14107
|
-
y:
|
|
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
|
|
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
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
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
|
|
14344
|
-
|
|
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
|
|
14467
|
-
|
|
14468
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
15039
|
+
style: { ...edge.styleOverrides },
|
|
14744
15040
|
styleClass: edge.styleClass,
|
|
14745
|
-
label: edge.label
|
|
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.
|
|
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({
|
|
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="${
|
|
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="${
|
|
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(
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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="${
|
|
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
|
|
15588
|
-
const
|
|
15589
|
-
const startX = Math.floor(-renderer.offsetX /
|
|
15590
|
-
const startY = Math.floor(-renderer.offsetY /
|
|
15591
|
-
const endX = Math.ceil((width - renderer.offsetX) /
|
|
15592
|
-
const endY = Math.ceil((height - renderer.offsetY) /
|
|
15593
|
-
ctx.strokeStyle = color;
|
|
15594
|
-
ctx.lineWidth = 1 /
|
|
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 +=
|
|
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 +=
|
|
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
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
ctx.
|
|
15632
|
-
|
|
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,
|
|
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
|
-
|
|
15740
|
-
ctx.
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
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
|
|
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
|
|
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;
|