@ngroznykh/papirus 0.6.4 → 0.7.0
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 +13 -2
- package/README.ru.md +13 -2
- package/dist/core/ClipboardManager.d.ts +25 -0
- package/dist/core/ClipboardManager.d.ts.map +1 -0
- package/dist/core/ConnectionManager.d.ts +8 -3
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts +20 -29
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/DiagramSurface.d.ts +63 -0
- package/dist/core/DiagramSurface.d.ts.map +1 -0
- package/dist/core/DragManager.d.ts +2 -2
- package/dist/core/DragManager.d.ts.map +1 -1
- package/dist/core/EdgeEndpointUpdater.d.ts +25 -0
- package/dist/core/EdgeEndpointUpdater.d.ts.map +1 -0
- package/dist/core/HistoryManager.d.ts +0 -1
- package/dist/core/HistoryManager.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +4 -10
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/NavigationManager.d.ts +2 -2
- package/dist/core/NavigationManager.d.ts.map +1 -1
- package/dist/core/PropertyChangeBatcher.d.ts +21 -0
- package/dist/core/PropertyChangeBatcher.d.ts.map +1 -0
- package/dist/core/ResizeManager.d.ts +2 -2
- package/dist/core/ResizeManager.d.ts.map +1 -1
- package/dist/core/ScrollbarController.d.ts +50 -0
- package/dist/core/ScrollbarController.d.ts.map +1 -0
- package/dist/core/SearchManager.d.ts.map +1 -1
- package/dist/core/SelectionManager.d.ts +2 -2
- package/dist/core/SelectionManager.d.ts.map +1 -1
- package/dist/core/overlays/MiniMap.d.ts +4 -7
- package/dist/core/overlays/MiniMap.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts +20 -20
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/Element.d.ts +1 -0
- package/dist/elements/Element.d.ts.map +1 -1
- package/dist/elements/Node.d.ts.map +1 -1
- package/dist/elements/NodeImage.d.ts.map +1 -1
- package/dist/elements/Port.d.ts +1 -0
- package/dist/elements/Port.d.ts.map +1 -1
- package/dist/elements/TextLabel.d.ts +0 -1
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/elements/composite/CComponent.d.ts +3 -2
- package/dist/elements/composite/CComponent.d.ts.map +1 -1
- package/dist/elements/composite/CContainer.d.ts +2 -3
- package/dist/elements/composite/CContainer.d.ts.map +1 -1
- package/dist/elements/composite/CDivider.d.ts +3 -8
- package/dist/elements/composite/CDivider.d.ts.map +1 -1
- package/dist/elements/composite/CIcon.d.ts +4 -10
- package/dist/elements/composite/CIcon.d.ts.map +1 -1
- package/dist/elements/composite/CShape.d.ts +4 -5
- package/dist/elements/composite/CShape.d.ts.map +1 -1
- package/dist/elements/composite/CText.d.ts +3 -8
- package/dist/elements/composite/CText.d.ts.map +1 -1
- package/dist/elements/composite/CompositeComponentBase.d.ts +25 -0
- package/dist/elements/composite/CompositeComponentBase.d.ts.map +1 -0
- package/dist/elements/edge/EdgeLabelRenderer.d.ts +17 -0
- package/dist/elements/edge/EdgeLabelRenderer.d.ts.map +1 -0
- package/dist/elements/edge/EdgeMarkerRenderer.d.ts +11 -0
- package/dist/elements/edge/EdgeMarkerRenderer.d.ts.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/papirus.js +1875 -1776
- package/dist/papirus.js.map +1 -1
- package/dist/types.d.ts +15 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/AutoRouting.d.ts.map +1 -1
- package/dist/utils/Serializer.d.ts.map +1 -1
- package/dist/utils/SvgExporter.d.ts +0 -8
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/dist/utils/direction.d.ts +4 -0
- package/dist/utils/direction.d.ts.map +1 -1
- package/dist/utils/edgePath.d.ts +21 -0
- package/dist/utils/edgePath.d.ts.map +1 -0
- package/dist/utils/iconLayout.d.ts +34 -0
- package/dist/utils/iconLayout.d.ts.map +1 -0
- package/dist/utils/keymap.d.ts +11 -0
- package/dist/utils/keymap.d.ts.map +1 -0
- package/dist/utils/svgAssetLoader.d.ts +23 -0
- package/dist/utils/svgAssetLoader.d.ts.map +1 -0
- package/dist/utils/textMeasurer.d.ts +13 -0
- package/dist/utils/textMeasurer.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -1047,19 +1047,21 @@ var DragManager = class extends EventEmitter {
|
|
|
1047
1047
|
const nodeBindings = /* @__PURE__ */ new Map();
|
|
1048
1048
|
for (const edge of this.renderer.edges.values()) {
|
|
1049
1049
|
if (!edge.hasEditableControlPoints()) continue;
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1050
|
+
const fromId = edge.from.nodeId;
|
|
1051
|
+
const toId = edge.to.nodeId;
|
|
1052
|
+
const isFromDragged = !!fromId && draggedIds.has(fromId);
|
|
1053
|
+
const isToDragged = !!toId && draggedIds.has(toId);
|
|
1052
1054
|
if (isFromDragged && isToDragged) {
|
|
1053
1055
|
this.editablePolylineFullyConnectedEdges.add(edge.id);
|
|
1054
1056
|
this.initialControlPointsForFullyConnected.set(edge.id, clonePoints(edge.controlPoints));
|
|
1055
1057
|
continue;
|
|
1056
1058
|
}
|
|
1057
1059
|
for (const node of this.draggedNodes) {
|
|
1058
|
-
const isFrom =
|
|
1059
|
-
const isTo =
|
|
1060
|
+
const isFrom = fromId === node.id;
|
|
1061
|
+
const isTo = toId === node.id;
|
|
1060
1062
|
if (!isFrom && !isTo) continue;
|
|
1061
|
-
const oppositeNodeId = isFrom ?
|
|
1062
|
-
if (draggedIds.has(oppositeNodeId)) continue;
|
|
1063
|
+
const oppositeNodeId = isFrom ? toId : fromId;
|
|
1064
|
+
if (!oppositeNodeId || draggedIds.has(oppositeNodeId)) continue;
|
|
1063
1065
|
const controlPointIndex = isFrom ? 0 : edge.controlPoints.length - 1;
|
|
1064
1066
|
const nearestPoint = edge.controlPoints[controlPointIndex];
|
|
1065
1067
|
const endpoint = isFrom ? edge.startPoint : edge.endPoint;
|
|
@@ -1668,6 +1670,51 @@ var NavigationManager = class extends EventEmitter {
|
|
|
1668
1670
|
}
|
|
1669
1671
|
};
|
|
1670
1672
|
//#endregion
|
|
1673
|
+
//#region src/utils/direction.ts
|
|
1674
|
+
/**
|
|
1675
|
+
* Map outline param [0,1) to cardinal direction (top → right → bottom → left).
|
|
1676
|
+
*/
|
|
1677
|
+
function getDirectionFromOutlineParam(param) {
|
|
1678
|
+
const p = (param % 1 + 1) % 1;
|
|
1679
|
+
if (p < .25) return "top";
|
|
1680
|
+
if (p < .5) return "right";
|
|
1681
|
+
if (p < .75) return "bottom";
|
|
1682
|
+
return "left";
|
|
1683
|
+
}
|
|
1684
|
+
function isHorizontal(dir) {
|
|
1685
|
+
return dir === "left" || dir === "right";
|
|
1686
|
+
}
|
|
1687
|
+
function isVertical(dir) {
|
|
1688
|
+
return dir === "top" || dir === "bottom";
|
|
1689
|
+
}
|
|
1690
|
+
function isOppositeDirections(fromDir, toDir) {
|
|
1691
|
+
return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
|
|
1692
|
+
}
|
|
1693
|
+
function directionToVector(dir) {
|
|
1694
|
+
switch (dir) {
|
|
1695
|
+
case "top": return {
|
|
1696
|
+
x: 0,
|
|
1697
|
+
y: -1
|
|
1698
|
+
};
|
|
1699
|
+
case "bottom": return {
|
|
1700
|
+
x: 0,
|
|
1701
|
+
y: 1
|
|
1702
|
+
};
|
|
1703
|
+
case "left": return {
|
|
1704
|
+
x: -1,
|
|
1705
|
+
y: 0
|
|
1706
|
+
};
|
|
1707
|
+
case "right": return {
|
|
1708
|
+
x: 1,
|
|
1709
|
+
y: 0
|
|
1710
|
+
};
|
|
1711
|
+
default: return {
|
|
1712
|
+
x: 0,
|
|
1713
|
+
y: 0
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
//#endregion
|
|
1671
1718
|
//#region src/core/ConnectionManager.ts
|
|
1672
1719
|
/**
|
|
1673
1720
|
* Manages port-to-port connection creation
|
|
@@ -1689,6 +1736,8 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1689
1736
|
this.sourceOutlineParam = null;
|
|
1690
1737
|
this.previewTargetOutlineParam = null;
|
|
1691
1738
|
this.previewTargetNodeId = null;
|
|
1739
|
+
this.previewTargetEdgeId = null;
|
|
1740
|
+
this.previewTargetPathParam = null;
|
|
1692
1741
|
this.isReconnecting = false;
|
|
1693
1742
|
this.reconnectingEdge = null;
|
|
1694
1743
|
this.reconnectingEndpoint = null;
|
|
@@ -1867,8 +1916,8 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1867
1916
|
updateReconnectingEdge(point) {
|
|
1868
1917
|
if (!this.reconnectingEdge) return;
|
|
1869
1918
|
const edge = this.reconnectingEdge;
|
|
1870
|
-
const fromNode = this.renderer.getNode(edge.from.nodeId);
|
|
1871
|
-
const toNode = this.renderer.getNode(edge.to.nodeId);
|
|
1919
|
+
const fromNode = edge.from.nodeId ? this.renderer.getNode(edge.from.nodeId) : void 0;
|
|
1920
|
+
const toNode = edge.to.nodeId ? this.renderer.getNode(edge.to.nodeId) : void 0;
|
|
1872
1921
|
let snappedPoint = point;
|
|
1873
1922
|
let snappedDir;
|
|
1874
1923
|
let targetNode = this.getNodeAtPoint(point, true);
|
|
@@ -1897,14 +1946,14 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1897
1946
|
snappedPoint = bestResult.point;
|
|
1898
1947
|
this.reconnectingOutlineParam = bestResult.param;
|
|
1899
1948
|
this.reconnectingTargetNodeId = bestResult.node.id;
|
|
1900
|
-
snappedDir =
|
|
1949
|
+
snappedDir = getDirectionFromOutlineParam(bestResult.param);
|
|
1901
1950
|
if (this.reconnectingEndpoint === "start" ? toNode : fromNode) {
|
|
1902
1951
|
const alignRef = edge.isEditablePolyline() ? this.getNearestBendPointForAxisAlignment(edge, this.reconnectingEndpoint) : this.reconnectingEndpoint === "start" ? this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint) : this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1903
1952
|
snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, alignRef);
|
|
1904
1953
|
const reprojected = targetNode.getClosestPointOnOutline(snappedPoint);
|
|
1905
1954
|
snappedPoint = reprojected.point;
|
|
1906
1955
|
this.reconnectingOutlineParam = reprojected.param;
|
|
1907
|
-
snappedDir =
|
|
1956
|
+
snappedDir = getDirectionFromOutlineParam(reprojected.param);
|
|
1908
1957
|
}
|
|
1909
1958
|
} else {
|
|
1910
1959
|
this.reconnectingOutlineParam = null;
|
|
@@ -1926,12 +1975,12 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1926
1975
|
if (this.reconnectingEndpoint === "start") {
|
|
1927
1976
|
if (toNode) {
|
|
1928
1977
|
const target = this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint);
|
|
1929
|
-
const toDir = this.getTargetDirForReconnect(edge,
|
|
1978
|
+
const toDir = this.getTargetDirForReconnect(edge, "to");
|
|
1930
1979
|
edge.updateEndpoints(snappedPoint, target, snappedDir, toDir);
|
|
1931
1980
|
}
|
|
1932
1981
|
} else if (fromNode) {
|
|
1933
1982
|
const start = this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1934
|
-
const fromDir = this.getTargetDirForReconnect(edge,
|
|
1983
|
+
const fromDir = this.getTargetDirForReconnect(edge, "from");
|
|
1935
1984
|
edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir);
|
|
1936
1985
|
}
|
|
1937
1986
|
this.renderer.markDirty();
|
|
@@ -1943,18 +1992,11 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1943
1992
|
if (anchorId) return node.getAnchorPointById(anchorId) ?? node.getConnectionPoint(fallbackPoint);
|
|
1944
1993
|
return node.getConnectionPoint(fallbackPoint);
|
|
1945
1994
|
}
|
|
1946
|
-
getTargetDirForReconnect(edge,
|
|
1995
|
+
getTargetDirForReconnect(edge, endpoint) {
|
|
1947
1996
|
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1948
|
-
if (this.attachToOutline && ep.outlineParam !== void 0) return
|
|
1997
|
+
if (this.attachToOutline && ep.outlineParam !== void 0) return getDirectionFromOutlineParam(ep.outlineParam);
|
|
1949
1998
|
return (ep.portId?.replace(ANCHOR_PORT_PREFIX, ""))?.split(":")[0];
|
|
1950
1999
|
}
|
|
1951
|
-
getDirectionFromOutlineParam(param, _node) {
|
|
1952
|
-
const p = (param % 1 + 1) % 1;
|
|
1953
|
-
if (p < .25) return "top";
|
|
1954
|
-
if (p < .5) return "right";
|
|
1955
|
-
if (p < .75) return "bottom";
|
|
1956
|
-
return "left";
|
|
1957
|
-
}
|
|
1958
2000
|
/**
|
|
1959
2001
|
* For editable polyline: return the nearest bend point for axis alignment.
|
|
1960
2002
|
* When reconnecting 'end', use last control point (or start). When reconnecting 'start', use first control point (or end).
|
|
@@ -2018,6 +2060,8 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2018
2060
|
this.previewTargetAnchorId = null;
|
|
2019
2061
|
this.previewTargetOutlineParam = null;
|
|
2020
2062
|
this.previewTargetNodeId = null;
|
|
2063
|
+
this.previewTargetEdgeId = null;
|
|
2064
|
+
this.previewTargetPathParam = null;
|
|
2021
2065
|
let targetNode = this.getNodeAtPoint(cursorPoint, false);
|
|
2022
2066
|
if (this.attachToOutline) {
|
|
2023
2067
|
const tolerance = 40 / Math.max(this.renderer.zoom, 1e-4);
|
|
@@ -2061,6 +2105,24 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2061
2105
|
}
|
|
2062
2106
|
}
|
|
2063
2107
|
}
|
|
2108
|
+
const edgeDrop = this.findEdgeDropTarget(cursorPoint);
|
|
2109
|
+
if (edgeDrop) {
|
|
2110
|
+
let nodeDistance = Infinity;
|
|
2111
|
+
if (targetNode) {
|
|
2112
|
+
const dx = cursorPoint.x - snappedPoint.x;
|
|
2113
|
+
const dy = cursorPoint.y - snappedPoint.y;
|
|
2114
|
+
nodeDistance = Math.sqrt(dx * dx + dy * dy);
|
|
2115
|
+
}
|
|
2116
|
+
if (!targetNode || edgeDrop.distance < nodeDistance) {
|
|
2117
|
+
targetNode = null;
|
|
2118
|
+
this.previewTargetNodeId = null;
|
|
2119
|
+
this.previewTargetOutlineParam = null;
|
|
2120
|
+
this.previewTargetAnchorId = null;
|
|
2121
|
+
snappedPoint = edgeDrop.point;
|
|
2122
|
+
this.previewTargetEdgeId = edgeDrop.edge.id;
|
|
2123
|
+
this.previewTargetPathParam = edgeDrop.pathParam;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2064
2126
|
if (targetNode) {
|
|
2065
2127
|
const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
|
|
2066
2128
|
this.setCursor(forbidden ? "not-allowed" : "crosshair");
|
|
@@ -2161,9 +2223,29 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2161
2223
|
y: event.worldY
|
|
2162
2224
|
};
|
|
2163
2225
|
let createdEdge = null;
|
|
2226
|
+
const edgeDrop = this.previewTargetEdgeId != null && this.previewTargetPathParam != null ? {
|
|
2227
|
+
edgeId: this.previewTargetEdgeId,
|
|
2228
|
+
pathParam: this.previewTargetPathParam,
|
|
2229
|
+
point
|
|
2230
|
+
} : this.findEdgeDropTarget(point);
|
|
2164
2231
|
let targetNode = this.getNodeAtPoint(point, false);
|
|
2165
2232
|
if (!targetNode && this.attachToOutline && this.previewTargetNodeId) targetNode = this.renderer.getNode(this.previewTargetNodeId) ?? null;
|
|
2166
|
-
if (targetNode) {
|
|
2233
|
+
if (edgeDrop && this.sourceNode && (!targetNode || this.previewTargetEdgeId != null)) {
|
|
2234
|
+
const from = this.attachToOutline ? {
|
|
2235
|
+
nodeId: this.sourceNode.id,
|
|
2236
|
+
outlineParam: this.sourceOutlineParam ?? void 0
|
|
2237
|
+
} : {
|
|
2238
|
+
nodeId: this.sourceNode.id,
|
|
2239
|
+
portId: this.sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${this.sourceAnchorId}` : void 0
|
|
2240
|
+
};
|
|
2241
|
+
const to = {
|
|
2242
|
+
edgeId: edgeDrop.edgeId,
|
|
2243
|
+
pathParam: edgeDrop.pathParam
|
|
2244
|
+
};
|
|
2245
|
+
createdEdge = this.createEdge(from, to);
|
|
2246
|
+
this.addEdge(createdEdge);
|
|
2247
|
+
this.emit("connect", createdEdge);
|
|
2248
|
+
} else if (targetNode) {
|
|
2167
2249
|
if (!this._connectionValidator || this._connectionValidator(this.sourceNode.id, targetNode.id)) {
|
|
2168
2250
|
let from;
|
|
2169
2251
|
let to;
|
|
@@ -2227,8 +2309,8 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2227
2309
|
ctx.lineTo(end.x, end.y);
|
|
2228
2310
|
ctx.stroke();
|
|
2229
2311
|
} else {
|
|
2230
|
-
const fromDir = this.sourceOutlineParam !== null ?
|
|
2231
|
-
const toDir = this.previewTargetOutlineParam !== null ?
|
|
2312
|
+
const fromDir = this.sourceOutlineParam !== null ? getDirectionFromOutlineParam(this.sourceOutlineParam) : this.sourceAnchorId?.split(":")[0];
|
|
2313
|
+
const toDir = this.previewTargetOutlineParam !== null ? getDirectionFromOutlineParam(this.previewTargetOutlineParam) : this.previewTargetAnchorId?.split(":")[0];
|
|
2232
2314
|
this.drawBezierPreview(ctx, start, end, fromDir, toDir);
|
|
2233
2315
|
}
|
|
2234
2316
|
ctx.setLineDash([]);
|
|
@@ -2300,8 +2382,36 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2300
2382
|
this.sourceOutlineParam = null;
|
|
2301
2383
|
this.previewTargetOutlineParam = null;
|
|
2302
2384
|
this.previewTargetNodeId = null;
|
|
2385
|
+
this.previewTargetEdgeId = null;
|
|
2386
|
+
this.previewTargetPathParam = null;
|
|
2303
2387
|
this.setCursor("");
|
|
2304
2388
|
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Find the nearest edge under the cursor for junction / note→relation drops.
|
|
2391
|
+
*/
|
|
2392
|
+
findEdgeDropTarget(point) {
|
|
2393
|
+
const tolerance = Math.max(14, 20 / Math.max(this.renderer.zoom, 1e-4));
|
|
2394
|
+
let best = null;
|
|
2395
|
+
for (const edge of this.renderer.edges.values()) {
|
|
2396
|
+
if (!edge.visible || edge.path.length < 2) continue;
|
|
2397
|
+
const closest = edge.getClosestPointOnPath(point);
|
|
2398
|
+
if (!closest || closest.distance > tolerance) continue;
|
|
2399
|
+
if (!best || closest.distance < best.distance) best = {
|
|
2400
|
+
edge,
|
|
2401
|
+
pathParam: closest.pathParam,
|
|
2402
|
+
point: closest.point,
|
|
2403
|
+
distance: closest.distance
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
if (!best) return null;
|
|
2407
|
+
return {
|
|
2408
|
+
edge: best.edge,
|
|
2409
|
+
edgeId: best.edge.id,
|
|
2410
|
+
pathParam: best.pathParam,
|
|
2411
|
+
point: best.point,
|
|
2412
|
+
distance: best.distance
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2305
2415
|
resetReconnection() {
|
|
2306
2416
|
this.isReconnecting = false;
|
|
2307
2417
|
if (this.reconnectingEdge) this.reconnectingEdge.autoUpdateEndpoints = true;
|
|
@@ -2577,6 +2687,25 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2577
2687
|
}
|
|
2578
2688
|
};
|
|
2579
2689
|
//#endregion
|
|
2690
|
+
//#region src/utils/keymap.ts
|
|
2691
|
+
var DEFAULT_HISTORY_SHORTCUT_KEYS = {
|
|
2692
|
+
undoKey: "z",
|
|
2693
|
+
redoKey: "y"
|
|
2694
|
+
};
|
|
2695
|
+
/**
|
|
2696
|
+
* Resolves a letter key independently of the active keyboard layout.
|
|
2697
|
+
*/
|
|
2698
|
+
function getShortcutKey(event) {
|
|
2699
|
+
return event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
|
|
2700
|
+
}
|
|
2701
|
+
function getHistoryShortcut(event, keys = DEFAULT_HISTORY_SHORTCUT_KEYS) {
|
|
2702
|
+
if (!event.ctrlKey && !event.metaKey) return null;
|
|
2703
|
+
const key = getShortcutKey(event);
|
|
2704
|
+
if (key === keys.undoKey.toLowerCase() && !event.shiftKey) return "undo";
|
|
2705
|
+
if (key === keys.redoKey.toLowerCase() || key === keys.undoKey.toLowerCase() && event.shiftKey) return "redo";
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
//#endregion
|
|
2580
2709
|
//#region src/utils/style.ts
|
|
2581
2710
|
/**
|
|
2582
2711
|
* Shallow compare two objects
|
|
@@ -2608,6 +2737,44 @@ function applyStyleManagerToElements(styleManager, groups, edges, nodes) {
|
|
|
2608
2737
|
for (const node of nodes) node.applyStyleManager(styleManager);
|
|
2609
2738
|
}
|
|
2610
2739
|
//#endregion
|
|
2740
|
+
//#region src/utils/textMeasurer.ts
|
|
2741
|
+
/**
|
|
2742
|
+
* Wrap text by words while preserving explicit newline boundaries.
|
|
2743
|
+
*/
|
|
2744
|
+
function wrapMeasuredText(ctx, text, maxWidth, options = {}) {
|
|
2745
|
+
const lines = [];
|
|
2746
|
+
for (const paragraph of text.split("\n")) {
|
|
2747
|
+
const words = paragraph.split(" ");
|
|
2748
|
+
let currentLine = "";
|
|
2749
|
+
for (const word of words) {
|
|
2750
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
2751
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine) {
|
|
2752
|
+
lines.push(currentLine);
|
|
2753
|
+
currentLine = word;
|
|
2754
|
+
} else currentLine = testLine;
|
|
2755
|
+
}
|
|
2756
|
+
lines.push(currentLine);
|
|
2757
|
+
}
|
|
2758
|
+
const maxLines = options.maxLines;
|
|
2759
|
+
if (maxLines !== void 0 && lines.length > maxLines) {
|
|
2760
|
+
const truncated = lines.slice(0, Math.max(0, maxLines));
|
|
2761
|
+
if (options.appendEllipsis && truncated.length > 0) {
|
|
2762
|
+
const lastIndex = truncated.length - 1;
|
|
2763
|
+
truncated[lastIndex] = `${truncated[lastIndex]}…`;
|
|
2764
|
+
}
|
|
2765
|
+
return truncated;
|
|
2766
|
+
}
|
|
2767
|
+
return lines;
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Return the widest measured line.
|
|
2771
|
+
*/
|
|
2772
|
+
function measureTextLines(ctx, lines) {
|
|
2773
|
+
let maxWidth = 0;
|
|
2774
|
+
for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
|
|
2775
|
+
return maxWidth;
|
|
2776
|
+
}
|
|
2777
|
+
//#endregion
|
|
2611
2778
|
//#region src/elements/TextLabel.ts
|
|
2612
2779
|
var DEFAULT_INSET = 8;
|
|
2613
2780
|
function normalizeTextInset(value, fallback) {
|
|
@@ -2815,16 +2982,9 @@ var TextLabel = class {
|
|
|
2815
2982
|
const lineHeight = (this._style.fontSize ?? 14) * 1.2;
|
|
2816
2983
|
const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
|
|
2817
2984
|
const text = this._text ?? "";
|
|
2818
|
-
if (effectiveMaxWidth !== void 0)
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
} else this._lines = text.split("\n");
|
|
2822
|
-
let maxLineWidth = 0;
|
|
2823
|
-
for (const line of this._lines) {
|
|
2824
|
-
const metrics = ctx.measureText(line);
|
|
2825
|
-
maxLineWidth = Math.max(maxLineWidth, metrics.width);
|
|
2826
|
-
}
|
|
2827
|
-
this._measuredWidth = maxLineWidth + this._inset.left + this._inset.right;
|
|
2985
|
+
if (effectiveMaxWidth !== void 0) this._lines = wrapMeasuredText(ctx, text, Math.max(0, effectiveMaxWidth - this._inset.left - this._inset.right));
|
|
2986
|
+
else this._lines = text.split("\n");
|
|
2987
|
+
this._measuredWidth = measureTextLines(ctx, this._lines) + this._inset.left + this._inset.right;
|
|
2828
2988
|
this._measuredHeight = this._lines.length * lineHeight + this._inset.top + this._inset.bottom;
|
|
2829
2989
|
this._measureDirty = false;
|
|
2830
2990
|
return {
|
|
@@ -2896,23 +3056,6 @@ var TextLabel = class {
|
|
|
2896
3056
|
ctx.textAlign = this._style.align ?? "center";
|
|
2897
3057
|
ctx.textBaseline = this._style.baseline ?? "middle";
|
|
2898
3058
|
}
|
|
2899
|
-
wrapText(ctx, text, maxWidth) {
|
|
2900
|
-
const lines = [];
|
|
2901
|
-
const paragraphs = text.split("\n");
|
|
2902
|
-
for (const paragraph of paragraphs) {
|
|
2903
|
-
const words = paragraph.split(" ");
|
|
2904
|
-
let currentLine = "";
|
|
2905
|
-
for (const word of words) {
|
|
2906
|
-
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
2907
|
-
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
2908
|
-
lines.push(currentLine);
|
|
2909
|
-
currentLine = word;
|
|
2910
|
-
} else currentLine = testLine;
|
|
2911
|
-
}
|
|
2912
|
-
if (currentLine.length > 0) lines.push(currentLine);
|
|
2913
|
-
}
|
|
2914
|
-
return lines.length > 0 ? lines : [""];
|
|
2915
|
-
}
|
|
2916
3059
|
};
|
|
2917
3060
|
//#endregion
|
|
2918
3061
|
//#region src/core/history/commands.ts
|
|
@@ -3282,16 +3425,14 @@ var HistoryManager = class extends EventEmitter {
|
|
|
3282
3425
|
}
|
|
3283
3426
|
/**
|
|
3284
3427
|
* Handle keyboard shortcuts.
|
|
3285
|
-
* Uses event.code for letter keys to work with any keyboard layout (e.g. Russian).
|
|
3286
3428
|
*/
|
|
3287
3429
|
handleKeyDown(event) {
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
if (isCtrlOrMeta && key === "z" && !event.shiftKey) {
|
|
3430
|
+
const shortcut = getHistoryShortcut(event);
|
|
3431
|
+
if (shortcut === "undo") {
|
|
3291
3432
|
event.preventDefault();
|
|
3292
3433
|
return this.undo();
|
|
3293
3434
|
}
|
|
3294
|
-
if (
|
|
3435
|
+
if (shortcut === "redo") {
|
|
3295
3436
|
event.preventDefault();
|
|
3296
3437
|
return this.redo();
|
|
3297
3438
|
}
|
|
@@ -3312,6 +3453,7 @@ function generateId(prefix = "el") {
|
|
|
3312
3453
|
}
|
|
3313
3454
|
/**
|
|
3314
3455
|
* Reset ID counter (for testing)
|
|
3456
|
+
* @internal
|
|
3315
3457
|
*/
|
|
3316
3458
|
function resetIdCounter() {
|
|
3317
3459
|
nextId = 1;
|
|
@@ -3518,6 +3660,109 @@ var Element = class {
|
|
|
3518
3660
|
}
|
|
3519
3661
|
};
|
|
3520
3662
|
//#endregion
|
|
3663
|
+
//#region src/utils/edgePath.ts
|
|
3664
|
+
/**
|
|
3665
|
+
* Point and tangent angle at a normalized position along an edge path (0..1 by length).
|
|
3666
|
+
*/
|
|
3667
|
+
function getPathPointAt(path, type, position) {
|
|
3668
|
+
if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
|
|
3669
|
+
const samples = [];
|
|
3670
|
+
const steps = 20;
|
|
3671
|
+
for (let i = 1; i + 2 < path.length; i += 3) {
|
|
3672
|
+
const start = path[i - 1];
|
|
3673
|
+
const control1 = path[i];
|
|
3674
|
+
const control2 = path[i + 1];
|
|
3675
|
+
const end = path[i + 2];
|
|
3676
|
+
for (let sample = 0; sample <= steps; sample++) {
|
|
3677
|
+
const t = sample / steps;
|
|
3678
|
+
if (samples.length > 0 && t === 0) continue;
|
|
3679
|
+
samples.push(bezierPoint(start, control1, control2, end, t));
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
return getPointAlongPolyline(samples, position);
|
|
3683
|
+
}
|
|
3684
|
+
function getPointAlongPolyline(path, position) {
|
|
3685
|
+
if (path.length === 0) return {
|
|
3686
|
+
point: {
|
|
3687
|
+
x: 0,
|
|
3688
|
+
y: 0
|
|
3689
|
+
},
|
|
3690
|
+
angle: 0
|
|
3691
|
+
};
|
|
3692
|
+
if (path.length === 1) return {
|
|
3693
|
+
point: path[0],
|
|
3694
|
+
angle: 0
|
|
3695
|
+
};
|
|
3696
|
+
let totalLength = 0;
|
|
3697
|
+
const segments = [];
|
|
3698
|
+
for (let i = 1; i < path.length; i++) {
|
|
3699
|
+
const start = path[i - 1];
|
|
3700
|
+
const end = path[i];
|
|
3701
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
3702
|
+
segments.push({
|
|
3703
|
+
start,
|
|
3704
|
+
end,
|
|
3705
|
+
length
|
|
3706
|
+
});
|
|
3707
|
+
totalLength += length;
|
|
3708
|
+
}
|
|
3709
|
+
const targetLength = totalLength * Math.max(0, Math.min(1, position));
|
|
3710
|
+
let accumulated = 0;
|
|
3711
|
+
for (const segment of segments) {
|
|
3712
|
+
if (accumulated + segment.length >= targetLength) {
|
|
3713
|
+
const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
|
|
3714
|
+
return {
|
|
3715
|
+
point: {
|
|
3716
|
+
x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
|
|
3717
|
+
y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
|
|
3718
|
+
},
|
|
3719
|
+
angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
accumulated += segment.length;
|
|
3723
|
+
}
|
|
3724
|
+
const lastSegment = segments[segments.length - 1];
|
|
3725
|
+
return {
|
|
3726
|
+
point: lastSegment.end,
|
|
3727
|
+
angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
/** Map tangent angle to a cardinal direction used by polyline routing. */
|
|
3731
|
+
function directionFromAngle(angle) {
|
|
3732
|
+
const twoPi = Math.PI * 2;
|
|
3733
|
+
const a = (angle % twoPi + twoPi) % twoPi;
|
|
3734
|
+
if (a >= Math.PI * .25 && a < Math.PI * .75) return "bottom";
|
|
3735
|
+
if (a >= Math.PI * .75 && a < Math.PI * 1.25) return "left";
|
|
3736
|
+
if (a >= Math.PI * 1.25 && a < Math.PI * 1.75) return "top";
|
|
3737
|
+
return "right";
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Nearest point on an edge path to `point`, by sampling along normalized length.
|
|
3741
|
+
*/
|
|
3742
|
+
function getClosestPointOnPath(path, type, point, samples = 48) {
|
|
3743
|
+
if (path.length < 2) return null;
|
|
3744
|
+
let bestPoint = getPathPointAt(path, type, 0).point;
|
|
3745
|
+
let bestParam = 0;
|
|
3746
|
+
let bestDistSq = Infinity;
|
|
3747
|
+
for (let i = 0; i <= samples; i++) {
|
|
3748
|
+
const pathParam = i / samples;
|
|
3749
|
+
const at = getPathPointAt(path, type, pathParam).point;
|
|
3750
|
+
const dx = at.x - point.x;
|
|
3751
|
+
const dy = at.y - point.y;
|
|
3752
|
+
const distSq = dx * dx + dy * dy;
|
|
3753
|
+
if (distSq < bestDistSq) {
|
|
3754
|
+
bestDistSq = distSq;
|
|
3755
|
+
bestPoint = at;
|
|
3756
|
+
bestParam = pathParam;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
return {
|
|
3760
|
+
point: bestPoint,
|
|
3761
|
+
pathParam: bestParam,
|
|
3762
|
+
distance: Math.sqrt(bestDistSq)
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
//#endregion
|
|
3521
3766
|
//#region src/elements/paths/StraightPathStrategy.ts
|
|
3522
3767
|
/**
|
|
3523
3768
|
* Straight line path strategy
|
|
@@ -3532,41 +3777,6 @@ var StraightPathStrategy = class {
|
|
|
3532
3777
|
}
|
|
3533
3778
|
};
|
|
3534
3779
|
//#endregion
|
|
3535
|
-
//#region src/utils/direction.ts
|
|
3536
|
-
function isHorizontal(dir) {
|
|
3537
|
-
return dir === "left" || dir === "right";
|
|
3538
|
-
}
|
|
3539
|
-
function isVertical(dir) {
|
|
3540
|
-
return dir === "top" || dir === "bottom";
|
|
3541
|
-
}
|
|
3542
|
-
function isOppositeDirections(fromDir, toDir) {
|
|
3543
|
-
return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
|
|
3544
|
-
}
|
|
3545
|
-
function directionToVector(dir) {
|
|
3546
|
-
switch (dir) {
|
|
3547
|
-
case "top": return {
|
|
3548
|
-
x: 0,
|
|
3549
|
-
y: -1
|
|
3550
|
-
};
|
|
3551
|
-
case "bottom": return {
|
|
3552
|
-
x: 0,
|
|
3553
|
-
y: 1
|
|
3554
|
-
};
|
|
3555
|
-
case "left": return {
|
|
3556
|
-
x: -1,
|
|
3557
|
-
y: 0
|
|
3558
|
-
};
|
|
3559
|
-
case "right": return {
|
|
3560
|
-
x: 1,
|
|
3561
|
-
y: 0
|
|
3562
|
-
};
|
|
3563
|
-
default: return {
|
|
3564
|
-
x: 0,
|
|
3565
|
-
y: 0
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
//#endregion
|
|
3570
3780
|
//#region src/elements/paths/PolylinePathStrategy.ts
|
|
3571
3781
|
var MIN_SEGMENT_LENGTH = 20;
|
|
3572
3782
|
var SELF_LOOP_MIN_DISTANCE$1 = 1;
|
|
@@ -4298,85 +4508,458 @@ var BezierPathStrategy = class {
|
|
|
4298
4508
|
}
|
|
4299
4509
|
};
|
|
4300
4510
|
//#endregion
|
|
4301
|
-
//#region src/
|
|
4302
|
-
var DEFAULT_EDGE_STYLE = {
|
|
4303
|
-
strokeColor: "#666666",
|
|
4304
|
-
strokeWidth: 2,
|
|
4305
|
-
opacity: 1
|
|
4306
|
-
};
|
|
4511
|
+
//#region src/utils/markers.ts
|
|
4307
4512
|
/**
|
|
4308
|
-
*
|
|
4513
|
+
* Calculate marker points based on edge path
|
|
4309
4514
|
*/
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
this._endMarker = options.endMarker;
|
|
4338
|
-
this._edgeStyle = {
|
|
4339
|
-
...DEFAULT_EDGE_STYLE,
|
|
4340
|
-
...options.style
|
|
4515
|
+
function calculateMarkerPoints(path, position, type) {
|
|
4516
|
+
if (path.length < 2) return null;
|
|
4517
|
+
if (type === "bezier" && path.length >= 4) {
|
|
4518
|
+
const epsilon = .001;
|
|
4519
|
+
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
4520
|
+
if (position === "end") {
|
|
4521
|
+
const endIndex = path.length - 1;
|
|
4522
|
+
const endPoint = path[endIndex];
|
|
4523
|
+
let from = path[endIndex - 1];
|
|
4524
|
+
if (isSame(from, endPoint)) {
|
|
4525
|
+
from = path[endIndex - 2];
|
|
4526
|
+
if (isSame(from, endPoint)) from = path[0];
|
|
4527
|
+
}
|
|
4528
|
+
return {
|
|
4529
|
+
from,
|
|
4530
|
+
to: endPoint
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
const start = path[0];
|
|
4534
|
+
let next = path[1];
|
|
4535
|
+
if (isSame(next, start)) {
|
|
4536
|
+
next = path[2];
|
|
4537
|
+
if (isSame(next, start)) next = path[path.length - 1];
|
|
4538
|
+
}
|
|
4539
|
+
return {
|
|
4540
|
+
from: next,
|
|
4541
|
+
to: start
|
|
4341
4542
|
};
|
|
4342
|
-
this._pathStrategy = this.getPathStrategy(this._type);
|
|
4343
|
-
this._lockAnchors = options.lockAnchors ?? true;
|
|
4344
|
-
this._labelOffset = options.labelOffset ?? 0;
|
|
4345
|
-
this._labelPosition = options.labelPosition ?? .5;
|
|
4346
|
-
this._labelFollowPath = options.labelFollowPath ?? false;
|
|
4347
|
-
this._labelBackground = options.labelBackground;
|
|
4348
|
-
this._labelLineGap = options.labelLineGap ?? false;
|
|
4349
|
-
if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
|
|
4350
|
-
text: options.label,
|
|
4351
|
-
onChange: () => this.markDirty()
|
|
4352
|
-
});
|
|
4353
|
-
else this._label = new TextLabel({
|
|
4354
|
-
...options.label,
|
|
4355
|
-
onChange: () => this.markDirty()
|
|
4356
|
-
});
|
|
4357
|
-
}
|
|
4358
|
-
/**
|
|
4359
|
-
* Source endpoint
|
|
4360
|
-
*/
|
|
4361
|
-
get from() {
|
|
4362
|
-
return this._from;
|
|
4363
4543
|
}
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4544
|
+
if (position === "end") return {
|
|
4545
|
+
from: path[path.length - 2],
|
|
4546
|
+
to: path[path.length - 1]
|
|
4547
|
+
};
|
|
4548
|
+
return {
|
|
4549
|
+
from: path[1],
|
|
4550
|
+
to: path[0]
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* Calculate arrow marker vertex points
|
|
4555
|
+
*/
|
|
4556
|
+
function calculateArrowMarkerPoints(to, angle, size) {
|
|
4557
|
+
return {
|
|
4558
|
+
tip: to,
|
|
4559
|
+
left: {
|
|
4560
|
+
x: to.x - size * Math.cos(angle - ARROW_ANGLE),
|
|
4561
|
+
y: to.y - size * Math.sin(angle - ARROW_ANGLE)
|
|
4562
|
+
},
|
|
4563
|
+
right: {
|
|
4564
|
+
x: to.x - size * Math.cos(angle + ARROW_ANGLE),
|
|
4565
|
+
y: to.y - size * Math.sin(angle + ARROW_ANGLE)
|
|
4566
|
+
}
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* Calculate diamond marker vertex points
|
|
4571
|
+
*/
|
|
4572
|
+
function calculateDiamondMarkerPoints(to, angle, size) {
|
|
4573
|
+
const halfLength = size / 2;
|
|
4574
|
+
const halfWidth = size * .3;
|
|
4575
|
+
const cos = Math.cos(angle);
|
|
4576
|
+
const sin = Math.sin(angle);
|
|
4577
|
+
return [
|
|
4578
|
+
{
|
|
4579
|
+
x: to.x,
|
|
4580
|
+
y: to.y
|
|
4581
|
+
},
|
|
4582
|
+
{
|
|
4583
|
+
x: to.x - halfLength * cos + halfWidth * sin,
|
|
4584
|
+
y: to.y - halfLength * sin - halfWidth * cos
|
|
4585
|
+
},
|
|
4586
|
+
{
|
|
4587
|
+
x: to.x - size * cos,
|
|
4588
|
+
y: to.y - size * sin
|
|
4589
|
+
},
|
|
4590
|
+
{
|
|
4591
|
+
x: to.x - halfLength * cos - halfWidth * sin,
|
|
4592
|
+
y: to.y - halfLength * sin + halfWidth * cos
|
|
4593
|
+
}
|
|
4594
|
+
];
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Calculate circle marker center
|
|
4598
|
+
*/
|
|
4599
|
+
function calculateCircleMarkerCenter(to, angle, size) {
|
|
4600
|
+
return {
|
|
4601
|
+
x: to.x - size * Math.cos(angle),
|
|
4602
|
+
y: to.y - size * Math.sin(angle)
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
/**
|
|
4606
|
+
* Generate SVG path for arrow marker
|
|
4607
|
+
*/
|
|
4608
|
+
function generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4609
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4610
|
+
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} L ${right.x} ${right.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4611
|
+
}
|
|
4612
|
+
/**
|
|
4613
|
+
* Generate SVG path for open arrow marker
|
|
4614
|
+
*/
|
|
4615
|
+
function generateSvgOpenArrowMarker(to, angle, size, stroke) {
|
|
4616
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4617
|
+
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} M ${to.x} ${to.y} L ${right.x} ${right.y}" fill="none" stroke="${stroke}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
4618
|
+
}
|
|
4619
|
+
/**
|
|
4620
|
+
* Generate SVG path for diamond marker
|
|
4621
|
+
*/
|
|
4622
|
+
function generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4623
|
+
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
4624
|
+
return `<path d="M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y} L ${points[2].x} ${points[2].y} L ${points[3].x} ${points[3].y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4625
|
+
}
|
|
4626
|
+
/**
|
|
4627
|
+
* Generate SVG circle marker
|
|
4628
|
+
*/
|
|
4629
|
+
function generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4630
|
+
const { x: cx, y: cy } = calculateCircleMarkerCenter(to, angle, size);
|
|
4631
|
+
return `<circle cx="${cx}" cy="${cy}" r="${size}" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Square marker: front edge centered at `to`, depth 2×size along the edge toward `from`.
|
|
4635
|
+
*/
|
|
4636
|
+
function generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4637
|
+
const cos = Math.cos(angle);
|
|
4638
|
+
const sin = Math.sin(angle);
|
|
4639
|
+
const px = -sin;
|
|
4640
|
+
const py = cos;
|
|
4641
|
+
const p1 = {
|
|
4642
|
+
x: to.x + size * px,
|
|
4643
|
+
y: to.y + size * py
|
|
4644
|
+
};
|
|
4645
|
+
const p2 = {
|
|
4646
|
+
x: to.x - size * px,
|
|
4647
|
+
y: to.y - size * py
|
|
4648
|
+
};
|
|
4649
|
+
const bx = 2 * size * cos;
|
|
4650
|
+
const by = 2 * size * sin;
|
|
4651
|
+
const p3 = {
|
|
4652
|
+
x: p2.x - bx,
|
|
4653
|
+
y: p2.y - by
|
|
4654
|
+
};
|
|
4655
|
+
const p4 = {
|
|
4656
|
+
x: p1.x - bx,
|
|
4657
|
+
y: p1.y - by
|
|
4658
|
+
};
|
|
4659
|
+
return `<path d="M ${p1.x} ${p1.y} L ${p2.x} ${p2.y} L ${p3.x} ${p3.y} L ${p4.x} ${p4.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* Generate SVG path for any marker type
|
|
4663
|
+
*/
|
|
4664
|
+
function generateSvgMarker(marker, from, to, edgeStroke) {
|
|
4665
|
+
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
4666
|
+
const size = marker.size ?? 12;
|
|
4667
|
+
const stroke = marker.strokeColor ?? edgeStroke;
|
|
4668
|
+
const fill = marker.fillColor ?? stroke;
|
|
4669
|
+
const fillOpacity = marker.fillOpacity ?? 1;
|
|
4670
|
+
switch (marker.type) {
|
|
4671
|
+
case "open": return generateSvgOpenArrowMarker(to, angle, size, stroke);
|
|
4672
|
+
case "diamond": return generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4673
|
+
case "circle": return generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4674
|
+
case "square": return generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4675
|
+
default: return generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
//#endregion
|
|
4679
|
+
//#region src/elements/edge/EdgeMarkerRenderer.ts
|
|
4680
|
+
function getCanvasMarkerLength(config) {
|
|
4681
|
+
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
4682
|
+
switch (config.type) {
|
|
4683
|
+
case "arrow": return size * Math.cos(ARROW_ANGLE);
|
|
4684
|
+
case "open": return 0;
|
|
4685
|
+
case "diamond": return size;
|
|
4686
|
+
case "circle":
|
|
4687
|
+
case "square": return size * 2;
|
|
4688
|
+
default: return 0;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
function renderEdgeMarkers(ctx, options) {
|
|
4692
|
+
const { path, type, arrowType, startMarker, endMarker } = options;
|
|
4693
|
+
if (path.length < 2) return;
|
|
4694
|
+
if (startMarker !== void 0 || endMarker !== void 0) {
|
|
4695
|
+
renderConfiguredMarker(ctx, path, type, "end", endMarker);
|
|
4696
|
+
renderConfiguredMarker(ctx, path, type, "start", startMarker);
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
if (arrowType === "none") return;
|
|
4700
|
+
const endPoints = calculateMarkerPoints(path, "end", type);
|
|
4701
|
+
if (endPoints) drawLegacyArrowHead(ctx, endPoints.from, endPoints.to);
|
|
4702
|
+
if (arrowType === "double") {
|
|
4703
|
+
const startPoints = calculateMarkerPoints(path, "start", type);
|
|
4704
|
+
if (startPoints) drawLegacyArrowHead(ctx, startPoints.from, startPoints.to);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
function renderConfiguredMarker(ctx, path, type, position, config) {
|
|
4708
|
+
if (!config || config.type === "none") return;
|
|
4709
|
+
const points = calculateMarkerPoints(path, position, type);
|
|
4710
|
+
if (points) drawMarker(ctx, points.from, points.to, config);
|
|
4711
|
+
}
|
|
4712
|
+
function drawMarker(ctx, from, to, config) {
|
|
4713
|
+
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
4714
|
+
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
4715
|
+
const strokeColor = config.strokeColor ?? ctx.strokeStyle;
|
|
4716
|
+
const fillColor = config.fillColor ?? strokeColor;
|
|
4717
|
+
const fillOpacity = config.fillOpacity ?? 1;
|
|
4718
|
+
ctx.save();
|
|
4719
|
+
ctx.setLineDash([]);
|
|
4720
|
+
ctx.lineDashOffset = 0;
|
|
4721
|
+
ctx.strokeStyle = strokeColor;
|
|
4722
|
+
switch (config.type) {
|
|
4723
|
+
case "arrow":
|
|
4724
|
+
drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4725
|
+
break;
|
|
4726
|
+
case "open":
|
|
4727
|
+
drawOpenArrowMarker(ctx, to, angle, size);
|
|
4728
|
+
break;
|
|
4729
|
+
case "diamond":
|
|
4730
|
+
drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4731
|
+
break;
|
|
4732
|
+
case "circle":
|
|
4733
|
+
drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4734
|
+
break;
|
|
4735
|
+
case "square":
|
|
4736
|
+
drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4737
|
+
break;
|
|
4738
|
+
}
|
|
4739
|
+
ctx.restore();
|
|
4740
|
+
}
|
|
4741
|
+
function drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4742
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4743
|
+
ctx.beginPath();
|
|
4744
|
+
ctx.moveTo(to.x, to.y);
|
|
4745
|
+
ctx.lineTo(left.x, left.y);
|
|
4746
|
+
ctx.lineTo(right.x, right.y);
|
|
4747
|
+
ctx.closePath();
|
|
4748
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4749
|
+
}
|
|
4750
|
+
function drawOpenArrowMarker(ctx, to, angle, size) {
|
|
4751
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4752
|
+
ctx.beginPath();
|
|
4753
|
+
ctx.moveTo(to.x, to.y);
|
|
4754
|
+
ctx.lineTo(left.x, left.y);
|
|
4755
|
+
ctx.moveTo(to.x, to.y);
|
|
4756
|
+
ctx.lineTo(right.x, right.y);
|
|
4757
|
+
ctx.stroke();
|
|
4758
|
+
}
|
|
4759
|
+
function drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4760
|
+
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
4761
|
+
ctx.beginPath();
|
|
4762
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
4763
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
4764
|
+
ctx.closePath();
|
|
4765
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4766
|
+
}
|
|
4767
|
+
function drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4768
|
+
const center = calculateCircleMarkerCenter(to, angle, size);
|
|
4769
|
+
ctx.beginPath();
|
|
4770
|
+
ctx.arc(center.x, center.y, size, 0, Math.PI * 2);
|
|
4771
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4772
|
+
}
|
|
4773
|
+
function drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4774
|
+
const cos = Math.cos(angle);
|
|
4775
|
+
const sin = Math.sin(angle);
|
|
4776
|
+
const perpendicularX = -sin;
|
|
4777
|
+
const perpendicularY = cos;
|
|
4778
|
+
const frontLeft = {
|
|
4779
|
+
x: to.x + size * perpendicularX,
|
|
4780
|
+
y: to.y + size * perpendicularY
|
|
4781
|
+
};
|
|
4782
|
+
const frontRight = {
|
|
4783
|
+
x: to.x - size * perpendicularX,
|
|
4784
|
+
y: to.y - size * perpendicularY
|
|
4785
|
+
};
|
|
4786
|
+
const depthX = 2 * size * cos;
|
|
4787
|
+
const depthY = 2 * size * sin;
|
|
4788
|
+
const backRight = {
|
|
4789
|
+
x: frontRight.x - depthX,
|
|
4790
|
+
y: frontRight.y - depthY
|
|
4791
|
+
};
|
|
4792
|
+
const backLeft = {
|
|
4793
|
+
x: frontLeft.x - depthX,
|
|
4794
|
+
y: frontLeft.y - depthY
|
|
4795
|
+
};
|
|
4796
|
+
ctx.beginPath();
|
|
4797
|
+
ctx.moveTo(frontLeft.x, frontLeft.y);
|
|
4798
|
+
ctx.lineTo(frontRight.x, frontRight.y);
|
|
4799
|
+
ctx.lineTo(backRight.x, backRight.y);
|
|
4800
|
+
ctx.lineTo(backLeft.x, backLeft.y);
|
|
4801
|
+
ctx.closePath();
|
|
4802
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4803
|
+
}
|
|
4804
|
+
function fillAndStroke(ctx, fillColor, fillOpacity) {
|
|
4805
|
+
ctx.globalAlpha = fillOpacity;
|
|
4806
|
+
ctx.fillStyle = fillColor;
|
|
4807
|
+
ctx.fill();
|
|
4808
|
+
ctx.globalAlpha = 1;
|
|
4809
|
+
ctx.stroke();
|
|
4810
|
+
}
|
|
4811
|
+
function drawLegacyArrowHead(ctx, from, to) {
|
|
4812
|
+
const { left, right } = calculateArrowMarkerPoints(to, Math.atan2(to.y - from.y, to.x - from.x), 10);
|
|
4813
|
+
ctx.save();
|
|
4814
|
+
ctx.setLineDash([]);
|
|
4815
|
+
ctx.lineDashOffset = 0;
|
|
4816
|
+
ctx.beginPath();
|
|
4817
|
+
ctx.moveTo(to.x, to.y);
|
|
4818
|
+
ctx.lineTo(left.x, left.y);
|
|
4819
|
+
ctx.moveTo(to.x, to.y);
|
|
4820
|
+
ctx.lineTo(right.x, right.y);
|
|
4821
|
+
ctx.stroke();
|
|
4822
|
+
ctx.restore();
|
|
4823
|
+
}
|
|
4824
|
+
//#endregion
|
|
4825
|
+
//#region src/elements/edge/EdgeLabelRenderer.ts
|
|
4826
|
+
function renderEdgeLabel(ctx, options) {
|
|
4827
|
+
const { label, path, background } = options;
|
|
4828
|
+
if (!label || path.length < 2) return;
|
|
4829
|
+
const labelCenter = getEdgeLabelPosition(options);
|
|
4830
|
+
if (!labelCenter) return;
|
|
4831
|
+
const rotation = getEdgeLabelRotation(options);
|
|
4832
|
+
const labelOpacity = label.style.opacity ?? 1;
|
|
4833
|
+
label.measure(ctx);
|
|
4834
|
+
const labelWidth = label.measuredWidth;
|
|
4835
|
+
const labelHeight = label.measuredHeight;
|
|
4836
|
+
const backgroundColor = background?.color ?? "#ffffff";
|
|
4837
|
+
const backgroundOpacity = background?.opacity ?? 1;
|
|
4838
|
+
const backgroundRadius = background?.borderRadius ?? 2;
|
|
4839
|
+
ctx.save();
|
|
4840
|
+
if (rotation !== 0) {
|
|
4841
|
+
ctx.translate(labelCenter.x, labelCenter.y);
|
|
4842
|
+
ctx.rotate(rotation);
|
|
4843
|
+
ctx.translate(-labelCenter.x, -labelCenter.y);
|
|
4844
|
+
}
|
|
4845
|
+
const backgroundX = labelCenter.x - labelWidth / 2;
|
|
4846
|
+
const backgroundY = labelCenter.y - labelHeight / 2;
|
|
4847
|
+
ctx.fillStyle = backgroundColor;
|
|
4848
|
+
ctx.globalAlpha = backgroundOpacity;
|
|
4849
|
+
if (backgroundRadius > 0) {
|
|
4850
|
+
drawRoundedRectPath(ctx, backgroundX, backgroundY, labelWidth, labelHeight, backgroundRadius);
|
|
4851
|
+
ctx.fill();
|
|
4852
|
+
} else ctx.fillRect(backgroundX, backgroundY, labelWidth, labelHeight);
|
|
4853
|
+
ctx.globalAlpha = labelOpacity;
|
|
4854
|
+
label.renderAt(ctx, labelCenter);
|
|
4855
|
+
ctx.globalAlpha = 1;
|
|
4856
|
+
ctx.restore();
|
|
4857
|
+
}
|
|
4858
|
+
function getEdgeLabelPosition(options) {
|
|
4859
|
+
if (options.path.length < 2) return null;
|
|
4860
|
+
const { point, angle } = getPathPointAt(options.path, options.type, options.position);
|
|
4861
|
+
const perpendicularAngle = options.followPath ? angle + Math.PI / 2 : Math.PI / 2;
|
|
4862
|
+
return {
|
|
4863
|
+
x: point.x + options.offset * Math.cos(perpendicularAngle),
|
|
4864
|
+
y: point.y + options.offset * Math.sin(perpendicularAngle)
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
function getEdgeLabelRotation(options) {
|
|
4868
|
+
if (!options.followPath || options.path.length < 2) return 0;
|
|
4869
|
+
let angle = getPathPointAt(options.path, options.type, options.position).angle;
|
|
4870
|
+
if (angle > Math.PI / 2) angle -= Math.PI;
|
|
4871
|
+
if (angle < -Math.PI / 2) angle += Math.PI;
|
|
4872
|
+
return angle;
|
|
4873
|
+
}
|
|
4874
|
+
//#endregion
|
|
4875
|
+
//#region src/elements/Edge.ts
|
|
4876
|
+
var DEFAULT_EDGE_STYLE = {
|
|
4877
|
+
strokeColor: "#666666",
|
|
4878
|
+
strokeWidth: 2,
|
|
4879
|
+
opacity: 1
|
|
4880
|
+
};
|
|
4881
|
+
/**
|
|
4882
|
+
* Connection edge between nodes
|
|
4883
|
+
*/
|
|
4884
|
+
var Edge = class extends Element {
|
|
4885
|
+
constructor(options) {
|
|
4886
|
+
super({
|
|
4887
|
+
id: options.id ?? generateId("edge"),
|
|
4888
|
+
x: 0,
|
|
4889
|
+
y: 0,
|
|
4890
|
+
width: 0,
|
|
4891
|
+
height: 0,
|
|
4892
|
+
style: options.style,
|
|
4893
|
+
styleClass: options.styleClass
|
|
4894
|
+
});
|
|
4895
|
+
this._path = [];
|
|
4896
|
+
this._autoUpdateEndpoints = true;
|
|
4897
|
+
this._fromPoint = {
|
|
4898
|
+
x: 0,
|
|
4899
|
+
y: 0
|
|
4900
|
+
};
|
|
4901
|
+
this._toPoint = {
|
|
4902
|
+
x: 0,
|
|
4903
|
+
y: 0
|
|
4904
|
+
};
|
|
4905
|
+
this._from = options.from;
|
|
4906
|
+
this._to = options.to;
|
|
4907
|
+
this._type = options.type ?? "straight";
|
|
4908
|
+
this._arrowType = options.arrowType ?? "single";
|
|
4909
|
+
this._controlPoints = options.controlPoints;
|
|
4910
|
+
this._startMarker = options.startMarker;
|
|
4911
|
+
this._endMarker = options.endMarker;
|
|
4912
|
+
this._edgeStyle = {
|
|
4913
|
+
...DEFAULT_EDGE_STYLE,
|
|
4914
|
+
...options.style
|
|
4915
|
+
};
|
|
4916
|
+
this._pathStrategy = this.getPathStrategy(this._type);
|
|
4917
|
+
this._lockAnchors = options.lockAnchors ?? true;
|
|
4918
|
+
this._labelOffset = options.labelOffset ?? 0;
|
|
4919
|
+
this._labelPosition = options.labelPosition ?? .5;
|
|
4920
|
+
this._labelFollowPath = options.labelFollowPath ?? false;
|
|
4921
|
+
this._labelBackground = options.labelBackground;
|
|
4922
|
+
this._labelLineGap = options.labelLineGap ?? false;
|
|
4923
|
+
if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
|
|
4924
|
+
text: options.label,
|
|
4925
|
+
onChange: () => this.markDirty()
|
|
4926
|
+
});
|
|
4927
|
+
else this._label = new TextLabel({
|
|
4928
|
+
...options.label,
|
|
4929
|
+
onChange: () => this.markDirty()
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4932
|
+
/**
|
|
4933
|
+
* Source endpoint
|
|
4934
|
+
*/
|
|
4935
|
+
get from() {
|
|
4936
|
+
return this._from;
|
|
4937
|
+
}
|
|
4938
|
+
set from(value) {
|
|
4939
|
+
this._from = value;
|
|
4940
|
+
this._bindingListener?.();
|
|
4941
|
+
this.markDirty();
|
|
4942
|
+
}
|
|
4943
|
+
/**
|
|
4944
|
+
* Target endpoint
|
|
4945
|
+
*/
|
|
4946
|
+
get to() {
|
|
4947
|
+
return this._to;
|
|
4948
|
+
}
|
|
4949
|
+
set to(value) {
|
|
4950
|
+
this._to = value;
|
|
4951
|
+
this._bindingListener?.();
|
|
4952
|
+
this.markDirty();
|
|
4953
|
+
}
|
|
4954
|
+
/**
|
|
4955
|
+
* Notified when from/to binding changes (ports / outline params / node ids).
|
|
4956
|
+
* Used by DiagramRenderer to resync endpoints without doing so on every pan frame.
|
|
4957
|
+
*/
|
|
4958
|
+
setBindingListener(listener) {
|
|
4959
|
+
this._bindingListener = listener;
|
|
4960
|
+
}
|
|
4961
|
+
/**
|
|
4962
|
+
* Edge type
|
|
4380
4963
|
*/
|
|
4381
4964
|
get type() {
|
|
4382
4965
|
return this._type;
|
|
@@ -4640,6 +5223,10 @@ var Edge = class extends Element {
|
|
|
4640
5223
|
* @param toDir Direction at end point (top, right, bottom, left)
|
|
4641
5224
|
*/
|
|
4642
5225
|
updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
|
|
5226
|
+
if (this._path.length > 0 && this._fromPoint.x === fromPoint.x && this._fromPoint.y === fromPoint.y && this._toPoint.x === toPoint.x && this._toPoint.y === toPoint.y && this._fromDir === fromDir && this._toDir === toDir) {
|
|
5227
|
+
this._pathOptions = options;
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
4643
5230
|
this._fromPoint = fromPoint;
|
|
4644
5231
|
this._toPoint = toPoint;
|
|
4645
5232
|
this._fromDir = fromDir;
|
|
@@ -4737,8 +5324,8 @@ var Edge = class extends Element {
|
|
|
4737
5324
|
renderPath(ctx) {
|
|
4738
5325
|
const path = this._path;
|
|
4739
5326
|
if (path.length < 2) return;
|
|
4740
|
-
const startOffset = this._startMarker && this._startMarker.type !== "none" ?
|
|
4741
|
-
const endOffset = this._endMarker && this._endMarker.type !== "none" ?
|
|
5327
|
+
const startOffset = this._startMarker && this._startMarker.type !== "none" ? getCanvasMarkerLength(this._startMarker) : 0;
|
|
5328
|
+
const endOffset = this._endMarker && this._endMarker.type !== "none" ? getCanvasMarkerLength(this._endMarker) : 0;
|
|
4742
5329
|
const buildPolyline = () => {
|
|
4743
5330
|
if (this._type === "bezier" && path.length >= 4) {
|
|
4744
5331
|
const samples = [];
|
|
@@ -4920,363 +5507,53 @@ var Edge = class extends Element {
|
|
|
4920
5507
|
ctx.stroke();
|
|
4921
5508
|
}
|
|
4922
5509
|
renderArrows(ctx) {
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
const points = this.getMarkerPoints("start");
|
|
4931
|
-
if (points) this.drawMarker(ctx, points.from, points.to, this._startMarker);
|
|
4932
|
-
}
|
|
4933
|
-
} else {
|
|
4934
|
-
if (this._arrowType === "none") return;
|
|
4935
|
-
const endPoints = this.getMarkerPoints("end");
|
|
4936
|
-
if (endPoints) this.drawArrowHead(ctx, endPoints.from, endPoints.to);
|
|
4937
|
-
if (this._arrowType === "double") {
|
|
4938
|
-
const startPoints = this.getMarkerPoints("start");
|
|
4939
|
-
if (startPoints) this.drawArrowHead(ctx, startPoints.from, startPoints.to);
|
|
4940
|
-
}
|
|
4941
|
-
}
|
|
5510
|
+
renderEdgeMarkers(ctx, {
|
|
5511
|
+
path: this._path,
|
|
5512
|
+
type: this._type,
|
|
5513
|
+
arrowType: this._arrowType,
|
|
5514
|
+
startMarker: this._startMarker,
|
|
5515
|
+
endMarker: this._endMarker
|
|
5516
|
+
});
|
|
4942
5517
|
}
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
const cp2 = path[2];
|
|
4950
|
-
const end = path[3];
|
|
4951
|
-
const epsilon = .001;
|
|
4952
|
-
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
4953
|
-
if (position === "end") {
|
|
4954
|
-
const endIndex = path.length - 1;
|
|
4955
|
-
const endPoint = path[endIndex];
|
|
4956
|
-
let from = path[endIndex - 1] ?? cp2;
|
|
4957
|
-
if (isSame(from, endPoint)) {
|
|
4958
|
-
from = path[endIndex - 2] ?? cp1;
|
|
4959
|
-
if (isSame(from, endPoint)) from = start;
|
|
4960
|
-
}
|
|
4961
|
-
return {
|
|
4962
|
-
from,
|
|
4963
|
-
to: endPoint
|
|
4964
|
-
};
|
|
4965
|
-
}
|
|
4966
|
-
let next = cp1;
|
|
4967
|
-
if (isSame(next, start)) {
|
|
4968
|
-
next = cp2;
|
|
4969
|
-
if (isSame(next, start)) next = end;
|
|
4970
|
-
}
|
|
4971
|
-
return {
|
|
4972
|
-
from: next,
|
|
4973
|
-
to: start
|
|
4974
|
-
};
|
|
4975
|
-
}
|
|
4976
|
-
if (position === "end") return {
|
|
4977
|
-
from: path[path.length - 2],
|
|
4978
|
-
to: path[path.length - 1]
|
|
4979
|
-
};
|
|
4980
|
-
return {
|
|
4981
|
-
from: path[1],
|
|
4982
|
-
to: path[0]
|
|
4983
|
-
};
|
|
5518
|
+
renderLabel(ctx) {
|
|
5519
|
+
renderEdgeLabel(ctx, {
|
|
5520
|
+
...this.getLabelLayoutOptions(),
|
|
5521
|
+
label: this._label,
|
|
5522
|
+
background: this._labelBackground
|
|
5523
|
+
});
|
|
4984
5524
|
}
|
|
4985
5525
|
/**
|
|
4986
|
-
*
|
|
5526
|
+
* Point and tangent on the current path at a normalized length position (0..1).
|
|
4987
5527
|
*/
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
case "arrow": return size * Math.cos(ARROW_ANGLE);
|
|
4992
|
-
case "open": return 0;
|
|
4993
|
-
case "diamond": return size;
|
|
4994
|
-
case "circle": return size * 2;
|
|
4995
|
-
case "square": return size * 2;
|
|
4996
|
-
default: return 0;
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
4999
|
-
drawMarker(ctx, from, to, config) {
|
|
5000
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
5001
|
-
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
5002
|
-
const strokeColor = config.strokeColor ?? ctx.strokeStyle;
|
|
5003
|
-
const fillColor = config.fillColor ?? strokeColor;
|
|
5004
|
-
const fillOpacity = config.fillOpacity ?? 1;
|
|
5005
|
-
ctx.save();
|
|
5006
|
-
ctx.setLineDash([]);
|
|
5007
|
-
ctx.lineDashOffset = 0;
|
|
5008
|
-
ctx.strokeStyle = strokeColor;
|
|
5009
|
-
switch (config.type) {
|
|
5010
|
-
case "arrow":
|
|
5011
|
-
this.drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5012
|
-
break;
|
|
5013
|
-
case "open":
|
|
5014
|
-
this.drawOpenArrowMarker(ctx, to, angle, size);
|
|
5015
|
-
break;
|
|
5016
|
-
case "diamond":
|
|
5017
|
-
this.drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5018
|
-
break;
|
|
5019
|
-
case "circle":
|
|
5020
|
-
this.drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5021
|
-
break;
|
|
5022
|
-
case "square":
|
|
5023
|
-
this.drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5024
|
-
break;
|
|
5025
|
-
}
|
|
5026
|
-
ctx.restore();
|
|
5027
|
-
}
|
|
5028
|
-
drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5029
|
-
const x1 = to.x - size * Math.cos(angle - ARROW_ANGLE);
|
|
5030
|
-
const y1 = to.y - size * Math.sin(angle - ARROW_ANGLE);
|
|
5031
|
-
const x2 = to.x - size * Math.cos(angle + ARROW_ANGLE);
|
|
5032
|
-
const y2 = to.y - size * Math.sin(angle + ARROW_ANGLE);
|
|
5033
|
-
ctx.beginPath();
|
|
5034
|
-
ctx.moveTo(to.x, to.y);
|
|
5035
|
-
ctx.lineTo(x1, y1);
|
|
5036
|
-
ctx.lineTo(x2, y2);
|
|
5037
|
-
ctx.closePath();
|
|
5038
|
-
ctx.globalAlpha = fillOpacity;
|
|
5039
|
-
ctx.fillStyle = fillColor;
|
|
5040
|
-
ctx.fill();
|
|
5041
|
-
ctx.globalAlpha = 1;
|
|
5042
|
-
ctx.stroke();
|
|
5043
|
-
}
|
|
5044
|
-
drawOpenArrowMarker(ctx, to, angle, size) {
|
|
5045
|
-
const x1 = to.x - size * Math.cos(angle - ARROW_ANGLE);
|
|
5046
|
-
const y1 = to.y - size * Math.sin(angle - ARROW_ANGLE);
|
|
5047
|
-
const x2 = to.x - size * Math.cos(angle + ARROW_ANGLE);
|
|
5048
|
-
const y2 = to.y - size * Math.sin(angle + ARROW_ANGLE);
|
|
5049
|
-
ctx.beginPath();
|
|
5050
|
-
ctx.moveTo(to.x, to.y);
|
|
5051
|
-
ctx.lineTo(x1, y1);
|
|
5052
|
-
ctx.moveTo(to.x, to.y);
|
|
5053
|
-
ctx.lineTo(x2, y2);
|
|
5054
|
-
ctx.stroke();
|
|
5055
|
-
}
|
|
5056
|
-
drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5057
|
-
const halfLength = size / 2;
|
|
5058
|
-
const halfWidth = size * .3;
|
|
5059
|
-
const cos = Math.cos(angle);
|
|
5060
|
-
const sin = Math.sin(angle);
|
|
5061
|
-
const points = [
|
|
5062
|
-
{
|
|
5063
|
-
x: to.x,
|
|
5064
|
-
y: to.y
|
|
5065
|
-
},
|
|
5066
|
-
{
|
|
5067
|
-
x: to.x - halfLength * cos + halfWidth * sin,
|
|
5068
|
-
y: to.y - halfLength * sin - halfWidth * cos
|
|
5069
|
-
},
|
|
5070
|
-
{
|
|
5071
|
-
x: to.x - size * cos,
|
|
5072
|
-
y: to.y - size * sin
|
|
5073
|
-
},
|
|
5074
|
-
{
|
|
5075
|
-
x: to.x - halfLength * cos - halfWidth * sin,
|
|
5076
|
-
y: to.y - halfLength * sin + halfWidth * cos
|
|
5077
|
-
}
|
|
5078
|
-
];
|
|
5079
|
-
ctx.beginPath();
|
|
5080
|
-
ctx.moveTo(points[0].x, points[0].y);
|
|
5081
|
-
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
5082
|
-
ctx.closePath();
|
|
5083
|
-
ctx.globalAlpha = fillOpacity;
|
|
5084
|
-
ctx.fillStyle = fillColor;
|
|
5085
|
-
ctx.fill();
|
|
5086
|
-
ctx.globalAlpha = 1;
|
|
5087
|
-
ctx.stroke();
|
|
5088
|
-
}
|
|
5089
|
-
drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5090
|
-
const cx = to.x - size * Math.cos(angle);
|
|
5091
|
-
const cy = to.y - size * Math.sin(angle);
|
|
5092
|
-
ctx.beginPath();
|
|
5093
|
-
ctx.arc(cx, cy, size, 0, Math.PI * 2);
|
|
5094
|
-
ctx.globalAlpha = fillOpacity;
|
|
5095
|
-
ctx.fillStyle = fillColor;
|
|
5096
|
-
ctx.fill();
|
|
5097
|
-
ctx.globalAlpha = 1;
|
|
5098
|
-
ctx.stroke();
|
|
5099
|
-
}
|
|
5100
|
-
drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5101
|
-
const cos = Math.cos(angle);
|
|
5102
|
-
const sin = Math.sin(angle);
|
|
5103
|
-
const px = -sin;
|
|
5104
|
-
const py = cos;
|
|
5105
|
-
const p1 = {
|
|
5106
|
-
x: to.x + size * px,
|
|
5107
|
-
y: to.y + size * py
|
|
5108
|
-
};
|
|
5109
|
-
const p2 = {
|
|
5110
|
-
x: to.x - size * px,
|
|
5111
|
-
y: to.y - size * py
|
|
5112
|
-
};
|
|
5113
|
-
const bx = 2 * size * cos;
|
|
5114
|
-
const by = 2 * size * sin;
|
|
5115
|
-
const p3 = {
|
|
5116
|
-
x: p2.x - bx,
|
|
5117
|
-
y: p2.y - by
|
|
5118
|
-
};
|
|
5119
|
-
const p4 = {
|
|
5120
|
-
x: p1.x - bx,
|
|
5121
|
-
y: p1.y - by
|
|
5122
|
-
};
|
|
5123
|
-
ctx.beginPath();
|
|
5124
|
-
ctx.moveTo(p1.x, p1.y);
|
|
5125
|
-
ctx.lineTo(p2.x, p2.y);
|
|
5126
|
-
ctx.lineTo(p3.x, p3.y);
|
|
5127
|
-
ctx.lineTo(p4.x, p4.y);
|
|
5128
|
-
ctx.closePath();
|
|
5129
|
-
ctx.globalAlpha = fillOpacity;
|
|
5130
|
-
ctx.fillStyle = fillColor;
|
|
5131
|
-
ctx.fill();
|
|
5132
|
-
ctx.globalAlpha = 1;
|
|
5133
|
-
ctx.stroke();
|
|
5134
|
-
}
|
|
5135
|
-
drawArrowHead(ctx, from, to) {
|
|
5136
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
5137
|
-
ctx.save();
|
|
5138
|
-
ctx.setLineDash([]);
|
|
5139
|
-
ctx.lineDashOffset = 0;
|
|
5140
|
-
ctx.beginPath();
|
|
5141
|
-
ctx.moveTo(to.x, to.y);
|
|
5142
|
-
ctx.lineTo(to.x - 10 * Math.cos(angle - ARROW_ANGLE), to.y - 10 * Math.sin(angle - ARROW_ANGLE));
|
|
5143
|
-
ctx.moveTo(to.x, to.y);
|
|
5144
|
-
ctx.lineTo(to.x - 10 * Math.cos(angle + ARROW_ANGLE), to.y - 10 * Math.sin(angle + ARROW_ANGLE));
|
|
5145
|
-
ctx.stroke();
|
|
5146
|
-
ctx.restore();
|
|
5147
|
-
}
|
|
5148
|
-
renderLabel(ctx) {
|
|
5149
|
-
if (this._label === void 0 || this._path.length < 2) return;
|
|
5150
|
-
const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
|
|
5151
|
-
const rotation = this.getLabelRotation();
|
|
5152
|
-
const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
|
|
5153
|
-
const labelCenter = {
|
|
5154
|
-
x: point.x + this._labelOffset * Math.cos(perpAngle),
|
|
5155
|
-
y: point.y + this._labelOffset * Math.sin(perpAngle)
|
|
5156
|
-
};
|
|
5157
|
-
const labelOpacity = this._label.style.opacity ?? 1;
|
|
5158
|
-
this._label.measure(ctx);
|
|
5159
|
-
const labelWidth = this._label.measuredWidth;
|
|
5160
|
-
const labelHeight = this._label.measuredHeight;
|
|
5161
|
-
const bgColor = this._labelBackground?.color ?? "#ffffff";
|
|
5162
|
-
const bgOpacity = this._labelBackground?.opacity ?? 1;
|
|
5163
|
-
const bgRadius = this._labelBackground?.borderRadius ?? 2;
|
|
5164
|
-
ctx.save();
|
|
5165
|
-
if (rotation !== 0) {
|
|
5166
|
-
ctx.translate(labelCenter.x, labelCenter.y);
|
|
5167
|
-
ctx.rotate(rotation);
|
|
5168
|
-
ctx.translate(-labelCenter.x, -labelCenter.y);
|
|
5169
|
-
}
|
|
5170
|
-
const bgX = labelCenter.x - labelWidth / 2;
|
|
5171
|
-
const bgY = labelCenter.y - labelHeight / 2;
|
|
5172
|
-
ctx.fillStyle = bgColor;
|
|
5173
|
-
ctx.globalAlpha = bgOpacity;
|
|
5174
|
-
if (bgRadius > 0) {
|
|
5175
|
-
this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
|
|
5176
|
-
ctx.fill();
|
|
5177
|
-
} else ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
|
|
5178
|
-
ctx.globalAlpha = labelOpacity;
|
|
5179
|
-
this._label.renderAt(ctx, labelCenter);
|
|
5180
|
-
ctx.globalAlpha = 1;
|
|
5181
|
-
ctx.restore();
|
|
5182
|
-
}
|
|
5183
|
-
drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
5184
|
-
drawRoundedRectPath(ctx, x, y, width, height, radius);
|
|
5528
|
+
getPointAt(pathParam = .5) {
|
|
5529
|
+
if (this._path.length < 2) return null;
|
|
5530
|
+
return getPathPointAt(this._path, this._type, pathParam);
|
|
5185
5531
|
}
|
|
5186
5532
|
/**
|
|
5187
|
-
*
|
|
5188
|
-
* Also returns the tangent angle in radians at that point.
|
|
5533
|
+
* Nearest point on this edge's path to `point` (world coordinates).
|
|
5189
5534
|
*/
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
let samples;
|
|
5193
|
-
if (this._type === "bezier" && path.length >= 4) {
|
|
5194
|
-
samples = [];
|
|
5195
|
-
const steps = 20;
|
|
5196
|
-
for (let i = 1; i + 2 < path.length; i += 3) {
|
|
5197
|
-
const p0 = path[i - 1];
|
|
5198
|
-
const p1 = path[i];
|
|
5199
|
-
const p2 = path[i + 1];
|
|
5200
|
-
const p3 = path[i + 2];
|
|
5201
|
-
for (let s = 0; s <= steps; s++) {
|
|
5202
|
-
const st = s / steps;
|
|
5203
|
-
if (samples.length > 0 && st === 0) continue;
|
|
5204
|
-
samples.push(bezierPoint(p0, p1, p2, p3, st));
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
} else samples = path;
|
|
5208
|
-
return this.getPointAlongPolyline(samples, t);
|
|
5535
|
+
getClosestPointOnPath(point) {
|
|
5536
|
+
return getClosestPointOnPath(this._path, this._type, point);
|
|
5209
5537
|
}
|
|
5210
5538
|
/**
|
|
5211
5539
|
* Get world position of label center along path.
|
|
5212
5540
|
*/
|
|
5213
5541
|
getLabelPosition() {
|
|
5214
|
-
|
|
5215
|
-
const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
|
|
5216
|
-
const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
|
|
5217
|
-
return {
|
|
5218
|
-
x: point.x + this._labelOffset * Math.cos(perpAngle),
|
|
5219
|
-
y: point.y + this._labelOffset * Math.sin(perpAngle)
|
|
5220
|
-
};
|
|
5542
|
+
return getEdgeLabelPosition(this.getLabelLayoutOptions());
|
|
5221
5543
|
}
|
|
5222
5544
|
/**
|
|
5223
5545
|
* Get label rotation angle in radians (if labelFollowPath is true).
|
|
5224
5546
|
*/
|
|
5225
5547
|
getLabelRotation() {
|
|
5226
|
-
|
|
5227
|
-
const { angle } = this.getPathPointAt(this._labelPosition);
|
|
5228
|
-
let a = angle;
|
|
5229
|
-
if (a > Math.PI / 2) a -= Math.PI;
|
|
5230
|
-
if (a < -Math.PI / 2) a += Math.PI;
|
|
5231
|
-
return a;
|
|
5548
|
+
return getEdgeLabelRotation(this.getLabelLayoutOptions());
|
|
5232
5549
|
}
|
|
5233
|
-
|
|
5234
|
-
* Get point and tangent angle at parameter t along a polyline.
|
|
5235
|
-
*/
|
|
5236
|
-
getPointAlongPolyline(path, t) {
|
|
5237
|
-
if (path.length === 0) return {
|
|
5238
|
-
point: {
|
|
5239
|
-
x: 0,
|
|
5240
|
-
y: 0
|
|
5241
|
-
},
|
|
5242
|
-
angle: 0
|
|
5243
|
-
};
|
|
5244
|
-
if (path.length === 1) return {
|
|
5245
|
-
point: path[0],
|
|
5246
|
-
angle: 0
|
|
5247
|
-
};
|
|
5248
|
-
let totalLength = 0;
|
|
5249
|
-
const segments = [];
|
|
5250
|
-
for (let i = 1; i < path.length; i++) {
|
|
5251
|
-
const start = path[i - 1];
|
|
5252
|
-
const end = path[i];
|
|
5253
|
-
const length = Math.sqrt((end.x - start.x) ** 2 + (end.y - start.y) ** 2);
|
|
5254
|
-
segments.push({
|
|
5255
|
-
start,
|
|
5256
|
-
end,
|
|
5257
|
-
length
|
|
5258
|
-
});
|
|
5259
|
-
totalLength += length;
|
|
5260
|
-
}
|
|
5261
|
-
const targetLength = totalLength * Math.max(0, Math.min(1, t));
|
|
5262
|
-
let accumulated = 0;
|
|
5263
|
-
for (const seg of segments) {
|
|
5264
|
-
if (accumulated + seg.length >= targetLength) {
|
|
5265
|
-
const segT = seg.length > 0 ? (targetLength - accumulated) / seg.length : 0;
|
|
5266
|
-
return {
|
|
5267
|
-
point: {
|
|
5268
|
-
x: seg.start.x + segT * (seg.end.x - seg.start.x),
|
|
5269
|
-
y: seg.start.y + segT * (seg.end.y - seg.start.y)
|
|
5270
|
-
},
|
|
5271
|
-
angle: Math.atan2(seg.end.y - seg.start.y, seg.end.x - seg.start.x)
|
|
5272
|
-
};
|
|
5273
|
-
}
|
|
5274
|
-
accumulated += seg.length;
|
|
5275
|
-
}
|
|
5276
|
-
const lastSeg = segments[segments.length - 1];
|
|
5550
|
+
getLabelLayoutOptions() {
|
|
5277
5551
|
return {
|
|
5278
|
-
|
|
5279
|
-
|
|
5552
|
+
path: this._path,
|
|
5553
|
+
type: this._type,
|
|
5554
|
+
position: this._labelPosition,
|
|
5555
|
+
offset: this._labelOffset,
|
|
5556
|
+
followPath: this._labelFollowPath
|
|
5280
5557
|
};
|
|
5281
5558
|
}
|
|
5282
5559
|
getPathStrategy(type) {
|
|
@@ -5412,6 +5689,179 @@ var LabelEditor = class {
|
|
|
5412
5689
|
}
|
|
5413
5690
|
};
|
|
5414
5691
|
//#endregion
|
|
5692
|
+
//#region src/core/ClipboardManager.ts
|
|
5693
|
+
var ClipboardManager = class {
|
|
5694
|
+
constructor(options) {
|
|
5695
|
+
this.clipboard = null;
|
|
5696
|
+
this.renderer = options.renderer;
|
|
5697
|
+
this.selectionManager = options.selectionManager;
|
|
5698
|
+
this.historyManager = options.historyManager;
|
|
5699
|
+
this.nodeFactory = options.nodeFactory;
|
|
5700
|
+
this.edgeFactory = options.edgeFactory;
|
|
5701
|
+
}
|
|
5702
|
+
copySelection() {
|
|
5703
|
+
const selectedIds = new Set(this.selectionManager.selectedIds);
|
|
5704
|
+
if (selectedIds.size === 0) return;
|
|
5705
|
+
const nodes = [];
|
|
5706
|
+
const edges = [];
|
|
5707
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
5708
|
+
for (const id of selectedIds) {
|
|
5709
|
+
const node = this.renderer.getNode(id);
|
|
5710
|
+
if (node) {
|
|
5711
|
+
nodes.push({
|
|
5712
|
+
id: node.id,
|
|
5713
|
+
type: node.typeName,
|
|
5714
|
+
x: node.x,
|
|
5715
|
+
y: node.y,
|
|
5716
|
+
width: node.width,
|
|
5717
|
+
height: node.height,
|
|
5718
|
+
style: node.style,
|
|
5719
|
+
styleClass: node.styleClass,
|
|
5720
|
+
label: node.label?.text,
|
|
5721
|
+
labelStyleClass: node.label?.styleClass,
|
|
5722
|
+
ports: node.ports.map((port) => ({
|
|
5723
|
+
id: port.id,
|
|
5724
|
+
type: port.type,
|
|
5725
|
+
position: port.position,
|
|
5726
|
+
styleClass: port.styleClass
|
|
5727
|
+
})),
|
|
5728
|
+
data: Object.keys(node.data).length > 0 ? node.data : void 0
|
|
5729
|
+
});
|
|
5730
|
+
nodeIds.add(node.id);
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
for (const edge of this.renderer.edges.values()) {
|
|
5734
|
+
const fromId = edge.from.nodeId;
|
|
5735
|
+
const toId = edge.to.nodeId;
|
|
5736
|
+
if (!fromId || !toId || !nodeIds.has(fromId) || !nodeIds.has(toId)) continue;
|
|
5737
|
+
edges.push({
|
|
5738
|
+
id: edge.id,
|
|
5739
|
+
from: edge.from,
|
|
5740
|
+
to: edge.to,
|
|
5741
|
+
type: edge.type,
|
|
5742
|
+
controlPoints: edge.controlPoints,
|
|
5743
|
+
arrowType: edge.arrowType,
|
|
5744
|
+
style: edge.style,
|
|
5745
|
+
styleClass: edge.styleClass,
|
|
5746
|
+
label: edge.label?.text,
|
|
5747
|
+
labelStyleClass: edge.label?.styleClass,
|
|
5748
|
+
labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
|
|
5749
|
+
labelBackground: edge.labelBackground,
|
|
5750
|
+
labelLineGap: edge.labelLineGap ? true : void 0,
|
|
5751
|
+
data: Object.keys(edge.data).length > 0 ? edge.data : void 0
|
|
5752
|
+
});
|
|
5753
|
+
}
|
|
5754
|
+
this.clipboard = {
|
|
5755
|
+
nodes,
|
|
5756
|
+
edges
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
5759
|
+
pasteSelection() {
|
|
5760
|
+
if (!this.clipboard || !this.nodeFactory || !this.edgeFactory) return;
|
|
5761
|
+
const offset = {
|
|
5762
|
+
x: 20,
|
|
5763
|
+
y: 20
|
|
5764
|
+
};
|
|
5765
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
5766
|
+
const newNodes = [];
|
|
5767
|
+
const newEdges = [];
|
|
5768
|
+
for (const nodeData of this.clipboard.nodes) {
|
|
5769
|
+
const newId = `${nodeData.id}_copy_${Date.now()}`;
|
|
5770
|
+
idMap.set(nodeData.id, newId);
|
|
5771
|
+
const copyData = {
|
|
5772
|
+
...nodeData,
|
|
5773
|
+
id: newId,
|
|
5774
|
+
x: nodeData.x + offset.x,
|
|
5775
|
+
y: nodeData.y + offset.y
|
|
5776
|
+
};
|
|
5777
|
+
newNodes.push(this.nodeFactory(copyData));
|
|
5778
|
+
}
|
|
5779
|
+
for (const edgeData of this.clipboard.edges) {
|
|
5780
|
+
const fromKey = edgeData.from.nodeId;
|
|
5781
|
+
const toKey = edgeData.to.nodeId;
|
|
5782
|
+
if (!fromKey || !toKey) continue;
|
|
5783
|
+
const fromNodeId = idMap.get(fromKey);
|
|
5784
|
+
const toNodeId = idMap.get(toKey);
|
|
5785
|
+
if (!fromNodeId || !toNodeId) continue;
|
|
5786
|
+
const newId = `${edgeData.id}_copy_${Date.now()}`;
|
|
5787
|
+
const copyEdge = {
|
|
5788
|
+
...edgeData,
|
|
5789
|
+
id: newId,
|
|
5790
|
+
from: {
|
|
5791
|
+
...edgeData.from,
|
|
5792
|
+
nodeId: fromNodeId
|
|
5793
|
+
},
|
|
5794
|
+
to: {
|
|
5795
|
+
...edgeData.to,
|
|
5796
|
+
nodeId: toNodeId
|
|
5797
|
+
}
|
|
5798
|
+
};
|
|
5799
|
+
newEdges.push(this.edgeFactory(copyEdge));
|
|
5800
|
+
}
|
|
5801
|
+
if (newNodes.length === 0 && newEdges.length === 0) return;
|
|
5802
|
+
this.historyManager.execute({
|
|
5803
|
+
execute: () => {
|
|
5804
|
+
for (const node of newNodes) this.renderer.addNode(node);
|
|
5805
|
+
for (const edge of newEdges) this.renderer.addEdge(edge);
|
|
5806
|
+
},
|
|
5807
|
+
undo: () => {
|
|
5808
|
+
for (const edge of newEdges) this.renderer.removeEdge(edge.id);
|
|
5809
|
+
for (const node of newNodes) this.renderer.removeNode(node.id);
|
|
5810
|
+
}
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5813
|
+
};
|
|
5814
|
+
//#endregion
|
|
5815
|
+
//#region src/core/PropertyChangeBatcher.ts
|
|
5816
|
+
var PropertyChangeBatcher = class {
|
|
5817
|
+
constructor(options) {
|
|
5818
|
+
this.pendingChanges = /* @__PURE__ */ new Map();
|
|
5819
|
+
this.renderer = options.renderer;
|
|
5820
|
+
this.historyManager = options.historyManager;
|
|
5821
|
+
this.debounceMs = options.debounceMs ?? 350;
|
|
5822
|
+
}
|
|
5823
|
+
queue(kind, id, before, after) {
|
|
5824
|
+
const key = `${kind}:${id}`;
|
|
5825
|
+
const existing = this.pendingChanges.get(key);
|
|
5826
|
+
if (existing) {
|
|
5827
|
+
existing.after = after;
|
|
5828
|
+
window.clearTimeout(existing.timerId);
|
|
5829
|
+
existing.timerId = window.setTimeout(() => this.flushOne(key), this.debounceMs);
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
const timerId = window.setTimeout(() => this.flushOne(key), this.debounceMs);
|
|
5833
|
+
this.pendingChanges.set(key, {
|
|
5834
|
+
kind,
|
|
5835
|
+
id,
|
|
5836
|
+
before,
|
|
5837
|
+
after,
|
|
5838
|
+
timerId
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
flush() {
|
|
5842
|
+
for (const key of Array.from(this.pendingChanges.keys())) this.flushOne(key);
|
|
5843
|
+
}
|
|
5844
|
+
flushOne(key) {
|
|
5845
|
+
const pending = this.pendingChanges.get(key);
|
|
5846
|
+
if (!pending) return;
|
|
5847
|
+
window.clearTimeout(pending.timerId);
|
|
5848
|
+
this.pendingChanges.delete(key);
|
|
5849
|
+
if (shallowEqual(pending.before, pending.after)) return;
|
|
5850
|
+
switch (pending.kind) {
|
|
5851
|
+
case "node":
|
|
5852
|
+
this.historyManager.execute(new ChangeNodePropertiesCommand((id) => this.renderer.getNode(id), pending.id, pending.before, pending.after));
|
|
5853
|
+
break;
|
|
5854
|
+
case "edge":
|
|
5855
|
+
this.historyManager.execute(new ChangeEdgePropertiesCommand((id) => this.renderer.getEdge(id), pending.id, pending.before, pending.after));
|
|
5856
|
+
break;
|
|
5857
|
+
case "group":
|
|
5858
|
+
this.historyManager.execute(new ChangeGroupPropertiesCommand((id) => this.renderer.getGroup(id), pending.id, pending.before, pending.after));
|
|
5859
|
+
break;
|
|
5860
|
+
}
|
|
5861
|
+
this.renderer.markStyleDirty();
|
|
5862
|
+
}
|
|
5863
|
+
};
|
|
5864
|
+
//#endregion
|
|
5415
5865
|
//#region src/core/InteractionManager.ts
|
|
5416
5866
|
var DEFAULT_KEYMAP = {
|
|
5417
5867
|
deleteKeys: ["Delete", "Backspace"],
|
|
@@ -5426,9 +5876,6 @@ var InteractionManager = class {
|
|
|
5426
5876
|
this.dragStartPositions = /* @__PURE__ */ new Map();
|
|
5427
5877
|
this.dragStartEditablePolylinePoints = /* @__PURE__ */ new Map();
|
|
5428
5878
|
this.reconnectOrigins = /* @__PURE__ */ new Map();
|
|
5429
|
-
this.clipboard = null;
|
|
5430
|
-
this.pendingPropertyChanges = /* @__PURE__ */ new Map();
|
|
5431
|
-
this.propertyChangeDebounceMs = 350;
|
|
5432
5879
|
this.labelEditor = new LabelEditor();
|
|
5433
5880
|
this.scrollbarDragState = null;
|
|
5434
5881
|
this.handledScrollbarMouseDown = false;
|
|
@@ -5457,6 +5904,17 @@ var InteractionManager = class {
|
|
|
5457
5904
|
});
|
|
5458
5905
|
this.navigationManager = new NavigationManager({ renderer: this.renderer });
|
|
5459
5906
|
this.historyManager = new HistoryManager();
|
|
5907
|
+
this.clipboardManager = new ClipboardManager({
|
|
5908
|
+
renderer: this.renderer,
|
|
5909
|
+
selectionManager: this.selectionManager,
|
|
5910
|
+
historyManager: this.historyManager,
|
|
5911
|
+
nodeFactory: options.nodeFactory,
|
|
5912
|
+
edgeFactory: options.edgeFactory
|
|
5913
|
+
});
|
|
5914
|
+
this.propertyChangeBatcher = new PropertyChangeBatcher({
|
|
5915
|
+
renderer: this.renderer,
|
|
5916
|
+
historyManager: this.historyManager
|
|
5917
|
+
});
|
|
5460
5918
|
this.keymap = {
|
|
5461
5919
|
...DEFAULT_KEYMAP,
|
|
5462
5920
|
...options.keymap
|
|
@@ -5484,7 +5942,7 @@ var InteractionManager = class {
|
|
|
5484
5942
|
});
|
|
5485
5943
|
}
|
|
5486
5944
|
});
|
|
5487
|
-
this.setupEvents(
|
|
5945
|
+
this.setupEvents();
|
|
5488
5946
|
}
|
|
5489
5947
|
get selection() {
|
|
5490
5948
|
return this.selectionManager;
|
|
@@ -5525,7 +5983,9 @@ var InteractionManager = class {
|
|
|
5525
5983
|
const idSet = new Set(nodeIds);
|
|
5526
5984
|
for (const edge of this.renderer.edges.values()) {
|
|
5527
5985
|
if (!edge.hasEditableControlPoints()) continue;
|
|
5528
|
-
|
|
5986
|
+
const fromId = edge.from.nodeId;
|
|
5987
|
+
const toId = edge.to.nodeId;
|
|
5988
|
+
if ((!fromId || !idSet.has(fromId)) && (!toId || !idSet.has(toId))) continue;
|
|
5529
5989
|
if (this.dragStartEditablePolylinePoints.has(edge.id)) continue;
|
|
5530
5990
|
this.dragStartEditablePolylinePoints.set(edge.id, clonePoints(edge.controlPoints));
|
|
5531
5991
|
}
|
|
@@ -5547,7 +6007,7 @@ var InteractionManager = class {
|
|
|
5547
6007
|
const after = createNodeSnapshot(node);
|
|
5548
6008
|
if (shallowEqual(before, after)) return;
|
|
5549
6009
|
this.renderer.markStyleDirty();
|
|
5550
|
-
this.
|
|
6010
|
+
this.propertyChangeBatcher.queue("node", nodeId, before, after);
|
|
5551
6011
|
}
|
|
5552
6012
|
changeEdgeProperties(edgeId, apply) {
|
|
5553
6013
|
const edge = this.renderer.getEdge(edgeId);
|
|
@@ -5557,7 +6017,7 @@ var InteractionManager = class {
|
|
|
5557
6017
|
const after = createEdgeSnapshot(edge);
|
|
5558
6018
|
if (shallowEqual(before, after)) return;
|
|
5559
6019
|
this.renderer.markStyleDirty();
|
|
5560
|
-
this.
|
|
6020
|
+
this.propertyChangeBatcher.queue("edge", edgeId, before, after);
|
|
5561
6021
|
}
|
|
5562
6022
|
changeGroupProperties(groupId, apply) {
|
|
5563
6023
|
const group = this.renderer.getGroup(groupId);
|
|
@@ -5567,7 +6027,7 @@ var InteractionManager = class {
|
|
|
5567
6027
|
const after = createGroupSnapshot(group);
|
|
5568
6028
|
if (shallowEqual(before, after)) return;
|
|
5569
6029
|
this.renderer.markStyleDirty();
|
|
5570
|
-
this.
|
|
6030
|
+
this.propertyChangeBatcher.queue("group", groupId, before, after);
|
|
5571
6031
|
}
|
|
5572
6032
|
removeNodeFromGroups(nodeId, groupIds) {
|
|
5573
6033
|
if (!this.renderer.getNode(nodeId)) return;
|
|
@@ -5601,7 +6061,7 @@ var InteractionManager = class {
|
|
|
5601
6061
|
this.overlayCleanup?.();
|
|
5602
6062
|
this.overlayCleanup = null;
|
|
5603
6063
|
}
|
|
5604
|
-
setupEvents(
|
|
6064
|
+
setupEvents() {
|
|
5605
6065
|
this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
|
|
5606
6066
|
this.selectionManager.renderSelectionRect(ctx);
|
|
5607
6067
|
if (!this.navigationOnly) {
|
|
@@ -5622,7 +6082,7 @@ var InteractionManager = class {
|
|
|
5622
6082
|
this.inputHandler.on("wheel", (event) => this.handleWheel(event));
|
|
5623
6083
|
this.inputHandler.on("pan", (event) => this.handlePan(event));
|
|
5624
6084
|
this.inputHandler.on("pinch", (event) => this.handlePinch(event));
|
|
5625
|
-
this.inputHandler.on("keydown", (event) => this.handleKeyDown(event
|
|
6085
|
+
this.inputHandler.on("keydown", (event) => this.handleKeyDown(event));
|
|
5626
6086
|
this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
|
|
5627
6087
|
if (!this.navigationOnly) {
|
|
5628
6088
|
this.dragManager.on("dragstart", (nodeIds) => {
|
|
@@ -5852,8 +6312,7 @@ var InteractionManager = class {
|
|
|
5852
6312
|
const component = node.getComponentAtPoint(point);
|
|
5853
6313
|
if (!component) return;
|
|
5854
6314
|
if (component.id !== void 0) this.renderer.emit("componentClick", node.id, component, point);
|
|
5855
|
-
|
|
5856
|
-
if (typeof asAny["onClick"] === "function") asAny["onClick"](component);
|
|
6315
|
+
component.onClick?.(component);
|
|
5857
6316
|
}
|
|
5858
6317
|
handleDoubleClick(event) {
|
|
5859
6318
|
if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
|
|
@@ -5919,13 +6378,13 @@ var InteractionManager = class {
|
|
|
5919
6378
|
if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
|
|
5920
6379
|
this.navigationManager.handlePinch(event);
|
|
5921
6380
|
}
|
|
5922
|
-
handleKeyDown(event
|
|
6381
|
+
handleKeyDown(event) {
|
|
5923
6382
|
const isCtrlOrMeta = event.ctrlKey || event.metaKey;
|
|
5924
|
-
const key =
|
|
6383
|
+
const key = getShortcutKey(event);
|
|
5925
6384
|
this.navigationManager.handleKeyDown(event);
|
|
5926
6385
|
if (this.handleViewportNavigationKey(event)) return;
|
|
5927
6386
|
if (this.navigationOnly) return;
|
|
5928
|
-
if (
|
|
6387
|
+
if (getHistoryShortcut(event) !== null) this.propertyChangeBatcher.flush();
|
|
5929
6388
|
if (this.historyManager.handleKeyDown(event)) return;
|
|
5930
6389
|
if (this.keymap.deleteKeys.includes(event.key)) {
|
|
5931
6390
|
event.preventDefault();
|
|
@@ -5934,12 +6393,12 @@ var InteractionManager = class {
|
|
|
5934
6393
|
}
|
|
5935
6394
|
if (isCtrlOrMeta && key === this.keymap.copyKey) {
|
|
5936
6395
|
event.preventDefault();
|
|
5937
|
-
this.copySelection();
|
|
6396
|
+
this.clipboardManager.copySelection();
|
|
5938
6397
|
return;
|
|
5939
6398
|
}
|
|
5940
6399
|
if (isCtrlOrMeta && key === this.keymap.pasteKey) {
|
|
5941
6400
|
event.preventDefault();
|
|
5942
|
-
this.pasteSelection(
|
|
6401
|
+
this.clipboardManager.pasteSelection();
|
|
5943
6402
|
return;
|
|
5944
6403
|
}
|
|
5945
6404
|
}
|
|
@@ -6044,49 +6503,6 @@ var InteractionManager = class {
|
|
|
6044
6503
|
edge.label.text = value;
|
|
6045
6504
|
});
|
|
6046
6505
|
}
|
|
6047
|
-
queuePropertyChange(kind, id, before, after) {
|
|
6048
|
-
const key = `${kind}:${id}`;
|
|
6049
|
-
const existing = this.pendingPropertyChanges.get(key);
|
|
6050
|
-
if (existing) {
|
|
6051
|
-
existing.after = after;
|
|
6052
|
-
window.clearTimeout(existing.timerId);
|
|
6053
|
-
existing.timerId = window.setTimeout(() => this.flushPendingPropertyChange(key), this.propertyChangeDebounceMs);
|
|
6054
|
-
return;
|
|
6055
|
-
}
|
|
6056
|
-
const timerId = window.setTimeout(() => this.flushPendingPropertyChange(key), this.propertyChangeDebounceMs);
|
|
6057
|
-
this.pendingPropertyChanges.set(key, {
|
|
6058
|
-
kind,
|
|
6059
|
-
id,
|
|
6060
|
-
before,
|
|
6061
|
-
after,
|
|
6062
|
-
timerId
|
|
6063
|
-
});
|
|
6064
|
-
}
|
|
6065
|
-
flushPendingPropertyChanges() {
|
|
6066
|
-
const keys = Array.from(this.pendingPropertyChanges.keys());
|
|
6067
|
-
for (const key of keys) this.flushPendingPropertyChange(key);
|
|
6068
|
-
}
|
|
6069
|
-
flushPendingPropertyChange(key) {
|
|
6070
|
-
const pending = this.pendingPropertyChanges.get(key);
|
|
6071
|
-
if (!pending) return;
|
|
6072
|
-
window.clearTimeout(pending.timerId);
|
|
6073
|
-
this.pendingPropertyChanges.delete(key);
|
|
6074
|
-
if (shallowEqual(pending.before, pending.after)) return;
|
|
6075
|
-
switch (pending.kind) {
|
|
6076
|
-
case "node":
|
|
6077
|
-
this.historyManager.execute(new ChangeNodePropertiesCommand((id) => this.renderer.getNode(id), pending.id, pending.before, pending.after));
|
|
6078
|
-
this.renderer.markStyleDirty();
|
|
6079
|
-
break;
|
|
6080
|
-
case "edge":
|
|
6081
|
-
this.historyManager.execute(new ChangeEdgePropertiesCommand((id) => this.renderer.getEdge(id), pending.id, pending.before, pending.after));
|
|
6082
|
-
this.renderer.markStyleDirty();
|
|
6083
|
-
break;
|
|
6084
|
-
case "group":
|
|
6085
|
-
this.historyManager.execute(new ChangeGroupPropertiesCommand((id) => this.renderer.getGroup(id), pending.id, pending.before, pending.after));
|
|
6086
|
-
this.renderer.markStyleDirty();
|
|
6087
|
-
break;
|
|
6088
|
-
}
|
|
6089
|
-
}
|
|
6090
6506
|
deleteSelection() {
|
|
6091
6507
|
const selectedIds = Array.from(this.selectionManager.selectedIds);
|
|
6092
6508
|
this.deleteByIds(selectedIds);
|
|
@@ -6117,10 +6533,14 @@ var InteractionManager = class {
|
|
|
6117
6533
|
nodeGroupIds.set(child.id, list);
|
|
6118
6534
|
}
|
|
6119
6535
|
}
|
|
6120
|
-
for (const edge of this.renderer.edges.values())
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6536
|
+
for (const edge of this.renderer.edges.values()) {
|
|
6537
|
+
const fromId = edge.from.nodeId;
|
|
6538
|
+
const toId = edge.to.nodeId;
|
|
6539
|
+
if (fromId && nodeIds.has(fromId) || toId && nodeIds.has(toId)) {
|
|
6540
|
+
if (!edgeIds.has(edge.id)) {
|
|
6541
|
+
edgeIds.add(edge.id);
|
|
6542
|
+
edges.push(edge);
|
|
6543
|
+
}
|
|
6124
6544
|
}
|
|
6125
6545
|
}
|
|
6126
6546
|
return new CompositeCommand([{
|
|
@@ -6142,112 +6562,16 @@ var InteractionManager = class {
|
|
|
6142
6562
|
}
|
|
6143
6563
|
}]);
|
|
6144
6564
|
}
|
|
6145
|
-
copySelection() {
|
|
6146
|
-
const selectedIds = new Set(this.selectionManager.selectedIds);
|
|
6147
|
-
if (selectedIds.size === 0) return;
|
|
6148
|
-
const nodes = [];
|
|
6149
|
-
const edges = [];
|
|
6150
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
6151
|
-
for (const id of selectedIds) {
|
|
6152
|
-
const node = this.renderer.getNode(id);
|
|
6153
|
-
if (node) {
|
|
6154
|
-
nodes.push({
|
|
6155
|
-
id: node.id,
|
|
6156
|
-
type: node.typeName,
|
|
6157
|
-
x: node.x,
|
|
6158
|
-
y: node.y,
|
|
6159
|
-
width: node.width,
|
|
6160
|
-
height: node.height,
|
|
6161
|
-
style: node.style,
|
|
6162
|
-
styleClass: node.styleClass,
|
|
6163
|
-
label: node.label?.text,
|
|
6164
|
-
labelStyleClass: node.label?.styleClass,
|
|
6165
|
-
ports: node.ports.map((port) => ({
|
|
6166
|
-
id: port.id,
|
|
6167
|
-
type: port.type,
|
|
6168
|
-
position: port.position,
|
|
6169
|
-
styleClass: port.styleClass
|
|
6170
|
-
})),
|
|
6171
|
-
data: Object.keys(node.data).length > 0 ? node.data : void 0
|
|
6172
|
-
});
|
|
6173
|
-
nodeIds.add(node.id);
|
|
6174
|
-
}
|
|
6175
|
-
}
|
|
6176
|
-
for (const edge of this.renderer.edges.values()) if (nodeIds.has(edge.from.nodeId) && nodeIds.has(edge.to.nodeId)) edges.push({
|
|
6177
|
-
id: edge.id,
|
|
6178
|
-
from: edge.from,
|
|
6179
|
-
to: edge.to,
|
|
6180
|
-
type: edge.type,
|
|
6181
|
-
controlPoints: edge.controlPoints,
|
|
6182
|
-
arrowType: edge.arrowType,
|
|
6183
|
-
style: edge.style,
|
|
6184
|
-
styleClass: edge.styleClass,
|
|
6185
|
-
label: edge.label?.text,
|
|
6186
|
-
labelStyleClass: edge.label?.styleClass,
|
|
6187
|
-
labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
|
|
6188
|
-
labelBackground: edge.labelBackground,
|
|
6189
|
-
labelLineGap: edge.labelLineGap ? true : void 0,
|
|
6190
|
-
data: Object.keys(edge.data).length > 0 ? edge.data : void 0
|
|
6191
|
-
});
|
|
6192
|
-
this.clipboard = {
|
|
6193
|
-
nodes,
|
|
6194
|
-
edges
|
|
6195
|
-
};
|
|
6196
|
-
}
|
|
6197
|
-
pasteSelection(options) {
|
|
6198
|
-
if (!this.clipboard || !options.nodeFactory || !options.edgeFactory) return;
|
|
6199
|
-
const offset = {
|
|
6200
|
-
x: 20,
|
|
6201
|
-
y: 20
|
|
6202
|
-
};
|
|
6203
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
6204
|
-
const newNodes = [];
|
|
6205
|
-
const newEdges = [];
|
|
6206
|
-
for (const nodeData of this.clipboard.nodes) {
|
|
6207
|
-
const newId = `${nodeData.id}_copy_${Date.now()}`;
|
|
6208
|
-
idMap.set(nodeData.id, newId);
|
|
6209
|
-
const copyData = {
|
|
6210
|
-
...nodeData,
|
|
6211
|
-
id: newId,
|
|
6212
|
-
x: nodeData.x + offset.x,
|
|
6213
|
-
y: nodeData.y + offset.y
|
|
6214
|
-
};
|
|
6215
|
-
newNodes.push(options.nodeFactory(copyData));
|
|
6216
|
-
}
|
|
6217
|
-
for (const edgeData of this.clipboard.edges) {
|
|
6218
|
-
const fromNodeId = idMap.get(edgeData.from.nodeId);
|
|
6219
|
-
const toNodeId = idMap.get(edgeData.to.nodeId);
|
|
6220
|
-
if (!fromNodeId || !toNodeId) continue;
|
|
6221
|
-
const newId = `${edgeData.id}_copy_${Date.now()}`;
|
|
6222
|
-
const copyEdge = {
|
|
6223
|
-
...edgeData,
|
|
6224
|
-
id: newId,
|
|
6225
|
-
from: {
|
|
6226
|
-
...edgeData.from,
|
|
6227
|
-
nodeId: fromNodeId
|
|
6228
|
-
},
|
|
6229
|
-
to: {
|
|
6230
|
-
...edgeData.to,
|
|
6231
|
-
nodeId: toNodeId
|
|
6232
|
-
}
|
|
6233
|
-
};
|
|
6234
|
-
newEdges.push(options.edgeFactory(copyEdge));
|
|
6235
|
-
}
|
|
6236
|
-
if (newNodes.length === 0 && newEdges.length === 0) return;
|
|
6237
|
-
this.historyManager.execute({
|
|
6238
|
-
execute: () => {
|
|
6239
|
-
for (const node of newNodes) this.renderer.addNode(node);
|
|
6240
|
-
for (const edge of newEdges) this.renderer.addEdge(edge);
|
|
6241
|
-
},
|
|
6242
|
-
undo: () => {
|
|
6243
|
-
for (const edge of newEdges) this.renderer.removeEdge(edge.id);
|
|
6244
|
-
for (const node of newNodes) this.renderer.removeNode(node.id);
|
|
6245
|
-
}
|
|
6246
|
-
});
|
|
6247
|
-
}
|
|
6248
6565
|
endpointsEqual(a, b) {
|
|
6249
|
-
if (a.
|
|
6566
|
+
if ((a.edgeId ?? null) !== (b.edgeId ?? null)) return false;
|
|
6567
|
+
if ((a.nodeId ?? null) !== (b.nodeId ?? null)) return false;
|
|
6250
6568
|
if ((a.portId ?? null) !== (b.portId ?? null)) return false;
|
|
6569
|
+
const aPath = a.pathParam;
|
|
6570
|
+
const bPath = b.pathParam;
|
|
6571
|
+
if (aPath !== void 0 || bPath !== void 0) {
|
|
6572
|
+
if (aPath === void 0 || bPath === void 0) return false;
|
|
6573
|
+
if (Math.abs(aPath - bPath) >= 1e-9) return false;
|
|
6574
|
+
}
|
|
6251
6575
|
const aParam = a.outlineParam;
|
|
6252
6576
|
const bParam = b.outlineParam;
|
|
6253
6577
|
if (aParam === void 0 && bParam === void 0) return true;
|
|
@@ -6629,40 +6953,459 @@ var ContextMenuManager = class extends EventEmitter {
|
|
|
6629
6953
|
iconEl.style.fontSize = "16px";
|
|
6630
6954
|
iconEl.textContent = icon.value;
|
|
6631
6955
|
}
|
|
6632
|
-
return iconEl;
|
|
6956
|
+
return iconEl;
|
|
6957
|
+
}
|
|
6958
|
+
isSvgString(value) {
|
|
6959
|
+
const trimmed = value.trim();
|
|
6960
|
+
return trimmed.startsWith("<svg") || trimmed.includes("</svg>");
|
|
6961
|
+
}
|
|
6962
|
+
adjustPosition(menu) {
|
|
6963
|
+
const rect = menu.getBoundingClientRect();
|
|
6964
|
+
const padding = 8;
|
|
6965
|
+
let left = rect.left;
|
|
6966
|
+
let top = rect.top;
|
|
6967
|
+
if (rect.right > window.innerWidth - padding) left = Math.max(padding, window.innerWidth - rect.width - padding);
|
|
6968
|
+
if (rect.bottom > window.innerHeight - padding) top = Math.max(padding, window.innerHeight - rect.height - padding);
|
|
6969
|
+
menu.style.left = `${left}px`;
|
|
6970
|
+
menu.style.top = `${top}px`;
|
|
6971
|
+
}
|
|
6972
|
+
};
|
|
6973
|
+
//#endregion
|
|
6974
|
+
//#region src/types.ts
|
|
6975
|
+
function isNodeEdgeEndpoint(endpoint) {
|
|
6976
|
+
return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
|
|
6977
|
+
}
|
|
6978
|
+
function isEdgeEdgeEndpoint(endpoint) {
|
|
6979
|
+
return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
|
|
6980
|
+
}
|
|
6981
|
+
//#endregion
|
|
6982
|
+
//#region src/core/EdgeEndpointUpdater.ts
|
|
6983
|
+
var EDGE_ATTACH_MAX_PASSES = 3;
|
|
6984
|
+
var EdgeEndpointUpdater = class {
|
|
6985
|
+
constructor(host) {
|
|
6986
|
+
this.host = host;
|
|
6987
|
+
}
|
|
6988
|
+
updateForDrag() {
|
|
6989
|
+
this.update(false);
|
|
6990
|
+
}
|
|
6991
|
+
updateAll() {
|
|
6992
|
+
this.update(true);
|
|
6993
|
+
}
|
|
6994
|
+
update(includeRoutingObstacles) {
|
|
6995
|
+
const obstacles = includeRoutingObstacles ? this.host.getNodeObstacles() : void 0;
|
|
6996
|
+
const nodes = this.host.getNodes();
|
|
6997
|
+
const edges = this.host.getEdges();
|
|
6998
|
+
const nodeNode = [];
|
|
6999
|
+
const edgeAttached = [];
|
|
7000
|
+
for (const edge of edges.values()) {
|
|
7001
|
+
if (!edge.autoUpdateEndpoints) continue;
|
|
7002
|
+
if (isEdgeEdgeEndpoint(edge.from) || isEdgeEdgeEndpoint(edge.to)) edgeAttached.push(edge);
|
|
7003
|
+
else nodeNode.push(edge);
|
|
7004
|
+
}
|
|
7005
|
+
for (const edge of nodeNode) this.updateNodeNodeEdge(edge, nodes, includeRoutingObstacles, obstacles);
|
|
7006
|
+
for (let pass = 0; pass < EDGE_ATTACH_MAX_PASSES; pass++) {
|
|
7007
|
+
let changed = false;
|
|
7008
|
+
for (const edge of edgeAttached) if (this.updateEdgeAttachedEdge(edge, nodes, edges, obstacles)) changed = true;
|
|
7009
|
+
if (!changed) break;
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
updateNodeNodeEdge(edge, nodes, includeRoutingObstacles, obstacles) {
|
|
7013
|
+
if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
|
|
7014
|
+
const fromNode = nodes.get(edge.from.nodeId);
|
|
7015
|
+
const toNode = nodes.get(edge.to.nodeId);
|
|
7016
|
+
if (!fromNode || !toNode) return;
|
|
7017
|
+
if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
|
|
7018
|
+
const from = this.resolveNodeEndpoint(fromNode, edge.from);
|
|
7019
|
+
const to = this.resolveNodeEndpoint(toNode, edge.to);
|
|
7020
|
+
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
|
|
7021
|
+
}
|
|
7022
|
+
/**
|
|
7023
|
+
* @returns true when the resolved geometry changed enough to warrant another pass
|
|
7024
|
+
*/
|
|
7025
|
+
updateEdgeAttachedEdge(edge, nodes, edges, obstacles) {
|
|
7026
|
+
const from = this.resolveAnyEndpoint(edge.from, nodes, edges);
|
|
7027
|
+
const to = this.resolveAnyEndpoint(edge.to, nodes, edges);
|
|
7028
|
+
if (!from || !to) return false;
|
|
7029
|
+
const before = edge.path;
|
|
7030
|
+
const beforeKey = before.length >= 2 ? `${before[0].x},${before[0].y}:${before[before.length - 1].x},${before[before.length - 1].y}` : "";
|
|
7031
|
+
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
|
|
7032
|
+
const after = edge.path;
|
|
7033
|
+
return beforeKey !== (after.length >= 2 ? `${after[0].x},${after[0].y}:${after[after.length - 1].x},${after[after.length - 1].y}` : "");
|
|
7034
|
+
}
|
|
7035
|
+
resolveAnyEndpoint(endpoint, nodes, edges) {
|
|
7036
|
+
if (isEdgeEdgeEndpoint(endpoint)) return this.resolveEdgeEndpoint(endpoint, edges);
|
|
7037
|
+
if (isNodeEdgeEndpoint(endpoint)) {
|
|
7038
|
+
const node = nodes.get(endpoint.nodeId);
|
|
7039
|
+
if (!node) return null;
|
|
7040
|
+
return this.resolveNodeEndpoint(node, endpoint);
|
|
7041
|
+
}
|
|
7042
|
+
return null;
|
|
7043
|
+
}
|
|
7044
|
+
resolveEdgeEndpoint(endpoint, edges) {
|
|
7045
|
+
const host = edges.get(endpoint.edgeId);
|
|
7046
|
+
if (!host) return null;
|
|
7047
|
+
const pathParam = endpoint.pathParam ?? .5;
|
|
7048
|
+
const at = host.getPointAt(pathParam);
|
|
7049
|
+
if (!at) return null;
|
|
7050
|
+
return {
|
|
7051
|
+
point: at.point,
|
|
7052
|
+
direction: directionFromAngle(at.angle)
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
lockAnchors(edge, fromNode, toNode) {
|
|
7056
|
+
if (isNodeEdgeEndpoint(edge.from) && edge.lockAnchors && edge.from.outlineParam === void 0 && !edge.from.portId) {
|
|
7057
|
+
const anchor = fromNode.getNearestAnchor(toNode.getCenter());
|
|
7058
|
+
if (anchor) edge.from = {
|
|
7059
|
+
...edge.from,
|
|
7060
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
if (isNodeEdgeEndpoint(edge.to) && edge.lockAnchors && edge.to.outlineParam === void 0 && !edge.to.portId) {
|
|
7064
|
+
const anchor = toNode.getNearestAnchor(fromNode.getCenter());
|
|
7065
|
+
if (anchor) edge.to = {
|
|
7066
|
+
...edge.to,
|
|
7067
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7068
|
+
};
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
resolveNodeEndpoint(node, endpoint) {
|
|
7072
|
+
if (endpoint.portId) {
|
|
7073
|
+
const anchorId = endpoint.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7074
|
+
const point = node.getAnchorPointById(anchorId);
|
|
7075
|
+
return point ? {
|
|
7076
|
+
point,
|
|
7077
|
+
direction: anchorId.split(":")[0]
|
|
7078
|
+
} : { point: node.getCenter() };
|
|
7079
|
+
}
|
|
7080
|
+
if (endpoint.outlineParam !== void 0) return {
|
|
7081
|
+
point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
|
|
7082
|
+
direction: getDirectionFromOutlineParam(endpoint.outlineParam)
|
|
7083
|
+
};
|
|
7084
|
+
return { point: node.getCenter() };
|
|
7085
|
+
}
|
|
7086
|
+
};
|
|
7087
|
+
//#endregion
|
|
7088
|
+
//#region src/core/ScrollbarController.ts
|
|
7089
|
+
var DEFAULT_SCROLLBAR_OPTIONS = {
|
|
7090
|
+
enabled: true,
|
|
7091
|
+
autoHide: false,
|
|
7092
|
+
autoHideDelay: 1200,
|
|
7093
|
+
fadeDuration: 220,
|
|
7094
|
+
thickness: 6,
|
|
7095
|
+
hoverThickness: 8,
|
|
7096
|
+
minThumbLength: 24,
|
|
7097
|
+
hitAreaPadding: 4,
|
|
7098
|
+
pageScrollRatio: .8,
|
|
7099
|
+
trackColor: "",
|
|
7100
|
+
thumbColor: "",
|
|
7101
|
+
thumbHoverColor: "",
|
|
7102
|
+
thumbActiveColor: ""
|
|
7103
|
+
};
|
|
7104
|
+
var ScrollbarController = class ScrollbarController {
|
|
7105
|
+
constructor(host, diagramOptions) {
|
|
7106
|
+
this.host = host;
|
|
7107
|
+
this.hoveredAxis = null;
|
|
7108
|
+
this.activeAxis = null;
|
|
7109
|
+
this.lastInteractionAt = performance.now();
|
|
7110
|
+
this.lastAlpha = -1;
|
|
7111
|
+
this.options = ScrollbarController.resolveOptions(diagramOptions);
|
|
7112
|
+
}
|
|
7113
|
+
updateAnimation(now) {
|
|
7114
|
+
if (!this.options.enabled || !this.options.autoHide) return false;
|
|
7115
|
+
const alpha = this.getAlpha(now);
|
|
7116
|
+
if (Math.abs(alpha - this.lastAlpha) <= .001) return false;
|
|
7117
|
+
this.lastAlpha = alpha;
|
|
7118
|
+
return true;
|
|
7119
|
+
}
|
|
7120
|
+
render(ctx, frameTime) {
|
|
7121
|
+
const metrics = this.getMetrics();
|
|
7122
|
+
if (!metrics) return;
|
|
7123
|
+
const alpha = this.getAlpha(frameTime);
|
|
7124
|
+
this.lastAlpha = alpha;
|
|
7125
|
+
if (alpha <= .001) return;
|
|
7126
|
+
const colors = this.resolveColors();
|
|
7127
|
+
ctx.save();
|
|
7128
|
+
const pixelRatio = this.host.getPixelRatio();
|
|
7129
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
7130
|
+
ctx.globalAlpha = alpha;
|
|
7131
|
+
if (metrics.horizontal) {
|
|
7132
|
+
const isHovered = this.hoveredAxis === "horizontal";
|
|
7133
|
+
const isActive = this.activeAxis === "horizontal";
|
|
7134
|
+
const thickness = isHovered || isActive ? this.options.hoverThickness : this.options.thickness;
|
|
7135
|
+
const trackY = metrics.horizontal.trackY + (metrics.horizontal.thickness - thickness) / 2;
|
|
7136
|
+
const radius = thickness / 2;
|
|
7137
|
+
ctx.fillStyle = colors.track;
|
|
7138
|
+
drawRoundedRectPath(ctx, metrics.horizontal.trackX, trackY, metrics.horizontal.trackLength, thickness, radius);
|
|
7139
|
+
ctx.fill();
|
|
7140
|
+
ctx.fillStyle = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7141
|
+
drawRoundedRectPath(ctx, metrics.horizontal.trackX + metrics.horizontal.thumbOffset, trackY, metrics.horizontal.thumbLength, thickness, radius);
|
|
7142
|
+
ctx.fill();
|
|
7143
|
+
}
|
|
7144
|
+
if (metrics.vertical) {
|
|
7145
|
+
const isHovered = this.hoveredAxis === "vertical";
|
|
7146
|
+
const isActive = this.activeAxis === "vertical";
|
|
7147
|
+
const thickness = isHovered || isActive ? this.options.hoverThickness : this.options.thickness;
|
|
7148
|
+
const trackX = metrics.vertical.trackX + (metrics.vertical.thickness - thickness) / 2;
|
|
7149
|
+
const radius = thickness / 2;
|
|
7150
|
+
ctx.fillStyle = colors.track;
|
|
7151
|
+
drawRoundedRectPath(ctx, trackX, metrics.vertical.trackY, thickness, metrics.vertical.trackLength, radius);
|
|
7152
|
+
ctx.fill();
|
|
7153
|
+
ctx.fillStyle = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7154
|
+
drawRoundedRectPath(ctx, trackX, metrics.vertical.trackY + metrics.vertical.thumbOffset, thickness, metrics.vertical.thumbLength, radius);
|
|
7155
|
+
ctx.fill();
|
|
7156
|
+
}
|
|
7157
|
+
ctx.restore();
|
|
7158
|
+
}
|
|
7159
|
+
notifyInteraction() {
|
|
7160
|
+
this.lastInteractionAt = performance.now();
|
|
7161
|
+
this.host.markDirty();
|
|
7162
|
+
}
|
|
7163
|
+
setActiveAxis(axis) {
|
|
7164
|
+
if (this.activeAxis === axis) return;
|
|
7165
|
+
this.activeAxis = axis;
|
|
7166
|
+
if (axis === null) this.host.markDirty();
|
|
7167
|
+
else this.notifyInteraction();
|
|
7168
|
+
}
|
|
7169
|
+
updateHover(screenX, screenY) {
|
|
7170
|
+
const axis = this.hitTestArea(screenX, screenY);
|
|
7171
|
+
if (this.hoveredAxis !== axis) {
|
|
7172
|
+
this.hoveredAxis = axis;
|
|
7173
|
+
this.host.markDirty();
|
|
7174
|
+
if (axis !== null) this.notifyInteraction();
|
|
7175
|
+
}
|
|
7176
|
+
return axis !== null;
|
|
7177
|
+
}
|
|
7178
|
+
clearHover() {
|
|
7179
|
+
if (this.hoveredAxis === null) return;
|
|
7180
|
+
this.hoveredAxis = null;
|
|
7181
|
+
this.host.markDirty();
|
|
7182
|
+
}
|
|
7183
|
+
hitTestThumb(screenX, screenY) {
|
|
7184
|
+
const metrics = this.getMetrics();
|
|
7185
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7186
|
+
if (!metrics || !point) return null;
|
|
7187
|
+
const padding = this.options.hitAreaPadding;
|
|
7188
|
+
if (metrics.horizontal) {
|
|
7189
|
+
const start = metrics.horizontal.trackX + metrics.horizontal.thumbOffset;
|
|
7190
|
+
if (point.x >= start - padding && point.x <= start + metrics.horizontal.thumbLength + padding && point.y >= metrics.horizontal.trackY - padding && point.y <= metrics.horizontal.trackY + metrics.horizontal.thickness + padding) return {
|
|
7191
|
+
axis: "horizontal",
|
|
7192
|
+
pointerOffset: point.x - start
|
|
7193
|
+
};
|
|
7194
|
+
}
|
|
7195
|
+
if (metrics.vertical) {
|
|
7196
|
+
const start = metrics.vertical.trackY + metrics.vertical.thumbOffset;
|
|
7197
|
+
if (point.x >= metrics.vertical.trackX - padding && point.x <= metrics.vertical.trackX + metrics.vertical.thickness + padding && point.y >= start - padding && point.y <= start + metrics.vertical.thumbLength + padding) return {
|
|
7198
|
+
axis: "vertical",
|
|
7199
|
+
pointerOffset: point.y - start
|
|
7200
|
+
};
|
|
7201
|
+
}
|
|
7202
|
+
return null;
|
|
7203
|
+
}
|
|
7204
|
+
dragThumb(axis, screenX, screenY, pointerOffset) {
|
|
7205
|
+
const metrics = this.getMetrics();
|
|
7206
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7207
|
+
if (!metrics || !point) return false;
|
|
7208
|
+
const track = axis === "horizontal" ? metrics.horizontal : metrics.vertical;
|
|
7209
|
+
if (!track) return false;
|
|
7210
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
7211
|
+
const trackStart = axis === "horizontal" ? track.trackX : track.trackY;
|
|
7212
|
+
const travel = Math.max(0, track.trackLength - track.thumbLength);
|
|
7213
|
+
const thumbOffset = Math.min(Math.max(coordinate - trackStart - pointerOffset, 0), travel);
|
|
7214
|
+
const scroll = travel > 0 && track.maxScroll > 0 ? thumbOffset / travel * track.maxScroll : 0;
|
|
7215
|
+
const viewportStart = (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y) + scroll;
|
|
7216
|
+
if (axis === "horizontal") this.host.setOffsetX(-viewportStart * this.host.getZoom());
|
|
7217
|
+
else this.host.setOffsetY(-viewportStart * this.host.getZoom());
|
|
7218
|
+
this.notifyInteraction();
|
|
7219
|
+
return true;
|
|
7220
|
+
}
|
|
7221
|
+
clickTrack(screenX, screenY) {
|
|
7222
|
+
const metrics = this.getMetrics();
|
|
7223
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7224
|
+
if (!metrics || !point) return false;
|
|
7225
|
+
if (metrics.horizontal && this.clickTrackAxis("horizontal", metrics, point)) return true;
|
|
7226
|
+
return metrics.vertical ? this.clickTrackAxis("vertical", metrics, point) : false;
|
|
7227
|
+
}
|
|
7228
|
+
scrollBy(screenDx, screenDy) {
|
|
7229
|
+
const state = this.getScrollState();
|
|
7230
|
+
if (!state) return false;
|
|
7231
|
+
const zoom = this.host.getZoom();
|
|
7232
|
+
const maxX = Math.max(0, state.contentBounds.width - state.viewportBounds.width);
|
|
7233
|
+
const maxY = Math.max(0, state.contentBounds.height - state.viewportBounds.height);
|
|
7234
|
+
const x = Math.min(Math.max(state.viewportBounds.x + screenDx / zoom, state.contentBounds.x), state.contentBounds.x + maxX);
|
|
7235
|
+
const y = Math.min(Math.max(state.viewportBounds.y + screenDy / zoom, state.contentBounds.y), state.contentBounds.y + maxY);
|
|
7236
|
+
this.host.setOffsetX(-x * zoom);
|
|
7237
|
+
this.host.setOffsetY(-y * zoom);
|
|
7238
|
+
this.notifyInteraction();
|
|
7239
|
+
return true;
|
|
7240
|
+
}
|
|
7241
|
+
scrollToStart() {
|
|
7242
|
+
const state = this.getScrollState();
|
|
7243
|
+
if (!state) return false;
|
|
7244
|
+
const zoom = this.host.getZoom();
|
|
7245
|
+
this.host.setOffsetX(-state.contentBounds.x * zoom);
|
|
7246
|
+
this.host.setOffsetY(-state.contentBounds.y * zoom);
|
|
7247
|
+
this.notifyInteraction();
|
|
7248
|
+
return true;
|
|
7249
|
+
}
|
|
7250
|
+
scrollToEnd() {
|
|
7251
|
+
const state = this.getScrollState();
|
|
7252
|
+
if (!state) return false;
|
|
7253
|
+
const maxX = Math.max(0, state.contentBounds.width - state.viewportBounds.width);
|
|
7254
|
+
const maxY = Math.max(0, state.contentBounds.height - state.viewportBounds.height);
|
|
7255
|
+
const zoom = this.host.getZoom();
|
|
7256
|
+
this.host.setOffsetX(-(state.contentBounds.x + maxX) * zoom);
|
|
7257
|
+
this.host.setOffsetY(-(state.contentBounds.y + maxY) * zoom);
|
|
7258
|
+
this.notifyInteraction();
|
|
7259
|
+
return true;
|
|
7260
|
+
}
|
|
7261
|
+
clickTrackAxis(axis, metrics, point) {
|
|
7262
|
+
const track = axis === "horizontal" ? metrics.horizontal : metrics.vertical;
|
|
7263
|
+
if (!track) return false;
|
|
7264
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
7265
|
+
const cross = axis === "horizontal" ? point.y : point.x;
|
|
7266
|
+
const start = axis === "horizontal" ? track.trackX : track.trackY;
|
|
7267
|
+
const crossStart = axis === "horizontal" ? track.trackY : track.trackX;
|
|
7268
|
+
if (coordinate < start || coordinate > start + track.trackLength || cross < crossStart || cross > crossStart + track.thickness) return false;
|
|
7269
|
+
const thumbStart = start + track.thumbOffset;
|
|
7270
|
+
const thumbEnd = thumbStart + track.thumbLength;
|
|
7271
|
+
if (coordinate >= thumbStart && coordinate <= thumbEnd) return false;
|
|
7272
|
+
const direction = coordinate < thumbStart ? -1 : 1;
|
|
7273
|
+
const viewportSize = axis === "horizontal" ? metrics.viewportBounds.width : metrics.viewportBounds.height;
|
|
7274
|
+
const current = (axis === "horizontal" ? metrics.viewportBounds.x : metrics.viewportBounds.y) - (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y);
|
|
7275
|
+
const next = Math.min(Math.max(current + viewportSize * this.options.pageScrollRatio * direction, 0), track.maxScroll);
|
|
7276
|
+
const viewportStart = (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y) + next;
|
|
7277
|
+
if (axis === "horizontal") this.host.setOffsetX(-viewportStart * this.host.getZoom());
|
|
7278
|
+
else this.host.setOffsetY(-viewportStart * this.host.getZoom());
|
|
7279
|
+
this.notifyInteraction();
|
|
7280
|
+
return true;
|
|
7281
|
+
}
|
|
7282
|
+
getMetrics() {
|
|
7283
|
+
if (!this.options.enabled) return null;
|
|
7284
|
+
const state = this.getScrollState();
|
|
7285
|
+
if (!state) return null;
|
|
7286
|
+
const { contentBounds, viewportBounds } = state;
|
|
7287
|
+
const contentWidth = Math.max(contentBounds.width, 1);
|
|
7288
|
+
const contentHeight = Math.max(contentBounds.height, 1);
|
|
7289
|
+
const showHorizontal = contentWidth > viewportBounds.width + .01;
|
|
7290
|
+
const showVertical = contentHeight > viewportBounds.height + .01;
|
|
7291
|
+
if (!showHorizontal && !showVertical) return null;
|
|
7292
|
+
const padding = 6;
|
|
7293
|
+
const spacing = 4;
|
|
7294
|
+
const width = this.host.getWidth();
|
|
7295
|
+
const height = this.host.getHeight();
|
|
7296
|
+
const availableWidth = width - padding * 2;
|
|
7297
|
+
const availableHeight = height - padding * 2;
|
|
7298
|
+
if (availableWidth <= 0 || availableHeight <= 0) return null;
|
|
7299
|
+
const horizontalLength = Math.max(0, availableWidth - (showVertical ? this.options.thickness + spacing : 0));
|
|
7300
|
+
const verticalLength = Math.max(0, availableHeight - (showHorizontal ? this.options.thickness + spacing : 0));
|
|
7301
|
+
return {
|
|
7302
|
+
contentBounds,
|
|
7303
|
+
viewportBounds,
|
|
7304
|
+
horizontal: showHorizontal ? this.createTrack(horizontalLength, viewportBounds.width, contentWidth, viewportBounds.x - contentBounds.x, padding, height - padding - this.options.thickness) : null,
|
|
7305
|
+
vertical: showVertical ? this.createTrack(verticalLength, viewportBounds.height, contentHeight, viewportBounds.y - contentBounds.y, width - padding - this.options.thickness, padding) : null
|
|
7306
|
+
};
|
|
7307
|
+
}
|
|
7308
|
+
createTrack(length, viewportSize, contentSize, scroll, trackX, trackY) {
|
|
7309
|
+
if (length <= 0) return null;
|
|
7310
|
+
const maxScroll = Math.max(0, contentSize - viewportSize);
|
|
7311
|
+
const thumbLength = Math.min(length, Math.max(this.options.minThumbLength, length * (viewportSize / contentSize)));
|
|
7312
|
+
const travel = Math.max(0, length - thumbLength);
|
|
7313
|
+
return {
|
|
7314
|
+
trackX,
|
|
7315
|
+
trackY,
|
|
7316
|
+
trackLength: length,
|
|
7317
|
+
thickness: this.options.thickness,
|
|
7318
|
+
thumbLength,
|
|
7319
|
+
thumbOffset: maxScroll > 0 ? Math.min(Math.max(scroll, 0), maxScroll) / maxScroll * travel : 0,
|
|
7320
|
+
maxScroll
|
|
7321
|
+
};
|
|
7322
|
+
}
|
|
7323
|
+
getScrollState() {
|
|
7324
|
+
const raw = this.host.getContentBounds();
|
|
7325
|
+
if (!raw) return null;
|
|
7326
|
+
const viewportBounds = this.getViewportBounds();
|
|
7327
|
+
const minX = Math.min(raw.x, viewportBounds.x);
|
|
7328
|
+
const minY = Math.min(raw.y, viewportBounds.y);
|
|
7329
|
+
const maxX = Math.max(raw.x + raw.width, viewportBounds.x + viewportBounds.width);
|
|
7330
|
+
const maxY = Math.max(raw.y + raw.height, viewportBounds.y + viewportBounds.height);
|
|
7331
|
+
return {
|
|
7332
|
+
contentBounds: {
|
|
7333
|
+
x: minX,
|
|
7334
|
+
y: minY,
|
|
7335
|
+
width: maxX - minX,
|
|
7336
|
+
height: maxY - minY
|
|
7337
|
+
},
|
|
7338
|
+
viewportBounds
|
|
7339
|
+
};
|
|
7340
|
+
}
|
|
7341
|
+
getViewportBounds() {
|
|
7342
|
+
const zoom = this.host.getZoom();
|
|
7343
|
+
return {
|
|
7344
|
+
x: -this.host.getOffsetX() / zoom,
|
|
7345
|
+
y: -this.host.getOffsetY() / zoom,
|
|
7346
|
+
width: this.host.getWidth() / zoom,
|
|
7347
|
+
height: this.host.getHeight() / zoom
|
|
7348
|
+
};
|
|
7349
|
+
}
|
|
7350
|
+
hitTestArea(screenX, screenY) {
|
|
7351
|
+
const metrics = this.getMetrics();
|
|
7352
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7353
|
+
if (!metrics || !point) return null;
|
|
7354
|
+
const padding = this.options.hitAreaPadding;
|
|
7355
|
+
for (const axis of ["horizontal", "vertical"]) {
|
|
7356
|
+
const track = metrics[axis];
|
|
7357
|
+
if (!track) continue;
|
|
7358
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
7359
|
+
const cross = axis === "horizontal" ? point.y : point.x;
|
|
7360
|
+
const start = axis === "horizontal" ? track.trackX : track.trackY;
|
|
7361
|
+
const crossStart = axis === "horizontal" ? track.trackY : track.trackX;
|
|
7362
|
+
if (coordinate >= start - padding && coordinate <= start + track.trackLength + padding && cross >= crossStart - padding && cross <= crossStart + track.thickness + padding) return axis;
|
|
7363
|
+
}
|
|
7364
|
+
return null;
|
|
6633
7365
|
}
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
7366
|
+
getAlpha(now) {
|
|
7367
|
+
if (!this.options.autoHide || this.activeAxis !== null || this.hoveredAxis !== null) return 1;
|
|
7368
|
+
const elapsed = now - this.lastInteractionAt;
|
|
7369
|
+
if (elapsed <= this.options.autoHideDelay) return 1;
|
|
7370
|
+
if (this.options.fadeDuration <= 0) return 0;
|
|
7371
|
+
return Math.max(0, 1 - (elapsed - this.options.autoHideDelay) / this.options.fadeDuration);
|
|
6637
7372
|
}
|
|
6638
|
-
|
|
6639
|
-
const
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
7373
|
+
resolveColors() {
|
|
7374
|
+
const fallback = this.host.isDarkTheme() ? {
|
|
7375
|
+
track: "rgba(148, 163, 184, 0.22)",
|
|
7376
|
+
thumb: "rgba(226, 232, 240, 0.68)",
|
|
7377
|
+
thumbHover: "rgba(226, 232, 240, 0.86)",
|
|
7378
|
+
thumbActive: "rgba(248, 250, 252, 0.96)"
|
|
7379
|
+
} : {
|
|
7380
|
+
track: "rgba(15, 23, 42, 0.16)",
|
|
7381
|
+
thumb: "rgba(51, 65, 85, 0.7)",
|
|
7382
|
+
thumbHover: "rgba(30, 41, 59, 0.82)",
|
|
7383
|
+
thumbActive: "rgba(15, 23, 42, 0.9)"
|
|
7384
|
+
};
|
|
7385
|
+
return {
|
|
7386
|
+
track: this.options.trackColor || fallback.track,
|
|
7387
|
+
thumb: this.options.thumbColor || fallback.thumb,
|
|
7388
|
+
thumbHover: this.options.thumbHoverColor || fallback.thumbHover,
|
|
7389
|
+
thumbActive: this.options.thumbActiveColor || fallback.thumbActive
|
|
7390
|
+
};
|
|
7391
|
+
}
|
|
7392
|
+
static resolveOptions(options) {
|
|
7393
|
+
const resolved = {
|
|
7394
|
+
...DEFAULT_SCROLLBAR_OPTIONS,
|
|
7395
|
+
enabled: options.scrollbarOverlay ?? DEFAULT_SCROLLBAR_OPTIONS.enabled
|
|
7396
|
+
};
|
|
7397
|
+
if (typeof options.scrollbar === "boolean") return {
|
|
7398
|
+
...resolved,
|
|
7399
|
+
enabled: options.scrollbar
|
|
7400
|
+
};
|
|
7401
|
+
return options.scrollbar ? {
|
|
7402
|
+
...resolved,
|
|
7403
|
+
...options.scrollbar
|
|
7404
|
+
} : resolved;
|
|
6647
7405
|
}
|
|
6648
7406
|
};
|
|
6649
7407
|
//#endregion
|
|
6650
7408
|
//#region src/core/DiagramRenderer.ts
|
|
6651
|
-
var DEFAULT_SCROLLBAR_OPTIONS = {
|
|
6652
|
-
enabled: true,
|
|
6653
|
-
autoHide: false,
|
|
6654
|
-
autoHideDelay: 1200,
|
|
6655
|
-
fadeDuration: 220,
|
|
6656
|
-
thickness: 6,
|
|
6657
|
-
hoverThickness: 8,
|
|
6658
|
-
minThumbLength: 24,
|
|
6659
|
-
hitAreaPadding: 4,
|
|
6660
|
-
pageScrollRatio: .8,
|
|
6661
|
-
trackColor: "",
|
|
6662
|
-
thumbColor: "",
|
|
6663
|
-
thumbHoverColor: "",
|
|
6664
|
-
thumbActiveColor: ""
|
|
6665
|
-
};
|
|
6666
7409
|
var DEFAULT_OPTIONS$1 = {
|
|
6667
7410
|
width: 800,
|
|
6668
7411
|
height: 600,
|
|
@@ -6699,15 +7442,12 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6699
7442
|
this.interactionManager = null;
|
|
6700
7443
|
this.contextMenuManager = null;
|
|
6701
7444
|
this.frameTime = 0;
|
|
6702
|
-
this.scrollbarHoveredAxis = null;
|
|
6703
|
-
this.scrollbarActiveAxis = null;
|
|
6704
|
-
this.scrollbarLastInteractionAt = 0;
|
|
6705
|
-
this.lastScrollbarAlpha = -1;
|
|
6706
7445
|
this._zoom = 1;
|
|
6707
7446
|
this._offsetX = 0;
|
|
6708
7447
|
this._offsetY = 0;
|
|
6709
7448
|
this._dirty = true;
|
|
6710
7449
|
this._styleDirty = true;
|
|
7450
|
+
this._contentDirty = true;
|
|
6711
7451
|
this._destroyed = false;
|
|
6712
7452
|
this._canvasRect = null;
|
|
6713
7453
|
this._nodeObstaclesCache = null;
|
|
@@ -6722,14 +7462,35 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6722
7462
|
...DEFAULT_OPTIONS$1,
|
|
6723
7463
|
...options
|
|
6724
7464
|
};
|
|
6725
|
-
this.scrollbar = this.resolveScrollbarOptions(options);
|
|
6726
7465
|
const ctx = this.canvas.getContext("2d");
|
|
6727
7466
|
if (ctx === null) throw new Error("Failed to get 2D rendering context");
|
|
6728
7467
|
this.ctx = ctx;
|
|
6729
7468
|
this.devicePixelRatio = this.options.retina ? window.devicePixelRatio || 1 : 1;
|
|
6730
7469
|
this._zoom = this.options.initialZoom;
|
|
6731
7470
|
this.animationManager = new AnimationManager(this.options.animations);
|
|
6732
|
-
this.
|
|
7471
|
+
this.scrollbarController = new ScrollbarController({
|
|
7472
|
+
getWidth: () => this.width,
|
|
7473
|
+
getHeight: () => this.height,
|
|
7474
|
+
getZoom: () => this.zoom,
|
|
7475
|
+
getOffsetX: () => this.offsetX,
|
|
7476
|
+
setOffsetX: (value) => {
|
|
7477
|
+
this.offsetX = value;
|
|
7478
|
+
},
|
|
7479
|
+
getOffsetY: () => this.offsetY,
|
|
7480
|
+
setOffsetY: (value) => {
|
|
7481
|
+
this.offsetY = value;
|
|
7482
|
+
},
|
|
7483
|
+
getPixelRatio: () => this.pixelRatio,
|
|
7484
|
+
markDirty: () => this.markDirty(),
|
|
7485
|
+
screenToCanvas: (x, y) => this.screenToCanvas(x, y),
|
|
7486
|
+
getContentBounds: () => this.getContentBounds(),
|
|
7487
|
+
isDarkTheme: () => this.styleManager?.theme.name === "dark"
|
|
7488
|
+
}, options);
|
|
7489
|
+
this.edgeEndpointUpdater = new EdgeEndpointUpdater({
|
|
7490
|
+
getNodes: () => this.nodes,
|
|
7491
|
+
getEdges: () => this.edges,
|
|
7492
|
+
getNodeObstacles: () => this.getNodeObstacles()
|
|
7493
|
+
});
|
|
6733
7494
|
this.setupCanvas();
|
|
6734
7495
|
this.startRenderLoop();
|
|
6735
7496
|
}
|
|
@@ -6743,7 +7504,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6743
7504
|
const clampedZoom = Math.max(this.options.minZoom, Math.min(this.options.maxZoom, value));
|
|
6744
7505
|
if (this._zoom !== clampedZoom) {
|
|
6745
7506
|
this._zoom = clampedZoom;
|
|
6746
|
-
this.
|
|
7507
|
+
this.scrollbarController.notifyInteraction();
|
|
6747
7508
|
this.markDirty();
|
|
6748
7509
|
this.emit("zoom", clampedZoom);
|
|
6749
7510
|
}
|
|
@@ -6757,7 +7518,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6757
7518
|
set offsetX(value) {
|
|
6758
7519
|
if (this._offsetX !== value) {
|
|
6759
7520
|
this._offsetX = value;
|
|
6760
|
-
this.
|
|
7521
|
+
this.scrollbarController.notifyInteraction();
|
|
6761
7522
|
this.markDirty();
|
|
6762
7523
|
this.emit("pan", this._offsetX, this._offsetY);
|
|
6763
7524
|
}
|
|
@@ -6771,7 +7532,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6771
7532
|
set offsetY(value) {
|
|
6772
7533
|
if (this._offsetY !== value) {
|
|
6773
7534
|
this._offsetY = value;
|
|
6774
|
-
this.
|
|
7535
|
+
this.scrollbarController.notifyInteraction();
|
|
6775
7536
|
this.markDirty();
|
|
6776
7537
|
this.emit("pan", this._offsetX, this._offsetY);
|
|
6777
7538
|
}
|
|
@@ -6991,12 +7752,11 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6991
7752
|
* Add a node to the diagram
|
|
6992
7753
|
*/
|
|
6993
7754
|
addNode(node) {
|
|
6994
|
-
node.setDirtyListener(() => this.
|
|
7755
|
+
node.setDirtyListener(() => this.markContentDirty());
|
|
6995
7756
|
node.setAttachToOutlineGetter(() => this._attachToOutline);
|
|
6996
7757
|
this._nodes.set(node.id, node);
|
|
6997
|
-
this._nodeObstaclesCache = null;
|
|
6998
7758
|
this.animationManager.registerEnter(node.id);
|
|
6999
|
-
this.
|
|
7759
|
+
this.markContentDirty();
|
|
7000
7760
|
this.emit("nodeAdd", node);
|
|
7001
7761
|
}
|
|
7002
7762
|
/**
|
|
@@ -7027,11 +7787,12 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7027
7787
|
*/
|
|
7028
7788
|
addEdge(edge) {
|
|
7029
7789
|
edge.setDirtyListener(() => this.markDirty());
|
|
7790
|
+
edge.setBindingListener(() => this.markContentDirty());
|
|
7030
7791
|
this._edges.set(edge.id, edge);
|
|
7031
|
-
this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
|
|
7032
|
-
this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
|
|
7792
|
+
if (edge.from.nodeId) this._updateEdgeIndex(edge.id, edge.from.nodeId, true);
|
|
7793
|
+
if (edge.to.nodeId) this._updateEdgeIndex(edge.id, edge.to.nodeId, true);
|
|
7033
7794
|
this.animationManager.registerEnter(edge.id);
|
|
7034
|
-
this.
|
|
7795
|
+
this.markContentDirty();
|
|
7035
7796
|
this.emit("edgeAdd", edge);
|
|
7036
7797
|
}
|
|
7037
7798
|
/**
|
|
@@ -7070,21 +7831,27 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7070
7831
|
if (edgesToRemove) for (const edgeId of Array.from(edgesToRemove)) this.removeEdgeImmediate(edgeId);
|
|
7071
7832
|
for (const group of this._groups.values()) group.removeChild(nodeId);
|
|
7072
7833
|
this._nodes.delete(nodeId);
|
|
7073
|
-
this._nodeObstaclesCache = null;
|
|
7074
7834
|
node.setDirtyListener(void 0);
|
|
7075
7835
|
node.setAttachToOutlineGetter(void 0);
|
|
7076
|
-
this.
|
|
7836
|
+
this.markContentDirty();
|
|
7077
7837
|
this.emit("nodeRemove", node);
|
|
7078
7838
|
return true;
|
|
7079
7839
|
}
|
|
7080
7840
|
removeEdgeImmediate(edgeId) {
|
|
7081
7841
|
const edge = this._edges.get(edgeId);
|
|
7082
7842
|
if (edge === void 0) return false;
|
|
7083
|
-
|
|
7084
|
-
|
|
7843
|
+
const dependents = [];
|
|
7844
|
+
for (const other of this._edges.values()) {
|
|
7845
|
+
if (other.id === edgeId) continue;
|
|
7846
|
+
if (other.from.edgeId === edgeId || other.to.edgeId === edgeId) dependents.push(other.id);
|
|
7847
|
+
}
|
|
7848
|
+
for (const dependentId of dependents) this.removeEdgeImmediate(dependentId);
|
|
7849
|
+
if (edge.from.nodeId) this._updateEdgeIndex(edgeId, edge.from.nodeId, false);
|
|
7850
|
+
if (edge.to.nodeId) this._updateEdgeIndex(edgeId, edge.to.nodeId, false);
|
|
7085
7851
|
this._edges.delete(edgeId);
|
|
7086
7852
|
edge.setDirtyListener(void 0);
|
|
7087
|
-
|
|
7853
|
+
edge.setBindingListener(void 0);
|
|
7854
|
+
this.markContentDirty();
|
|
7088
7855
|
this.emit("edgeRemove", edge);
|
|
7089
7856
|
return true;
|
|
7090
7857
|
}
|
|
@@ -7164,12 +7931,22 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7164
7931
|
for (const n of this._nodes.values()) n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
|
|
7165
7932
|
}
|
|
7166
7933
|
/**
|
|
7167
|
-
* Mark the diagram as needing re-render
|
|
7934
|
+
* Mark the diagram as needing re-render (viewport / cosmetics).
|
|
7935
|
+
* Does not resync edge endpoints — use {@link markContentDirty} after layout changes.
|
|
7168
7936
|
*/
|
|
7169
7937
|
markDirty() {
|
|
7170
7938
|
this._dirty = true;
|
|
7171
7939
|
}
|
|
7172
7940
|
/**
|
|
7941
|
+
* Mark that node/edge layout changed and edge endpoints must be recalculated.
|
|
7942
|
+
* Pan/zoom should call {@link markDirty} only so path routing is not redone every frame.
|
|
7943
|
+
*/
|
|
7944
|
+
markContentDirty() {
|
|
7945
|
+
this._contentDirty = true;
|
|
7946
|
+
this._nodeObstaclesCache = null;
|
|
7947
|
+
this._dirty = true;
|
|
7948
|
+
}
|
|
7949
|
+
/**
|
|
7173
7950
|
* Force an immediate render
|
|
7174
7951
|
*/
|
|
7175
7952
|
render() {
|
|
@@ -7235,13 +8012,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7235
8012
|
if (this._destroyed) return;
|
|
7236
8013
|
const now = performance.now();
|
|
7237
8014
|
if (this.animationManager.update(now) || this.animationManager.hasActive()) this._dirty = true;
|
|
7238
|
-
if (this.
|
|
7239
|
-
const alpha = this.getScrollbarAlpha(now);
|
|
7240
|
-
if (Math.abs(alpha - this.lastScrollbarAlpha) > .001) {
|
|
7241
|
-
this._dirty = true;
|
|
7242
|
-
this.lastScrollbarAlpha = alpha;
|
|
7243
|
-
}
|
|
7244
|
-
}
|
|
8015
|
+
if (this.scrollbarController.updateAnimation(now)) this._dirty = true;
|
|
7245
8016
|
this._canvasRect = null;
|
|
7246
8017
|
if (this._dirty) {
|
|
7247
8018
|
this.renderFrame(now);
|
|
@@ -7253,7 +8024,6 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7253
8024
|
}
|
|
7254
8025
|
renderFrame(now) {
|
|
7255
8026
|
this.frameTime = now;
|
|
7256
|
-
this._nodeObstaclesCache = null;
|
|
7257
8027
|
const ctx = this.ctx;
|
|
7258
8028
|
const { width, height, backgroundColor } = this.options;
|
|
7259
8029
|
ctx.setTransform(this.devicePixelRatio, 0, 0, this.devicePixelRatio, 0, 0);
|
|
@@ -7271,7 +8041,10 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7271
8041
|
this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
|
|
7272
8042
|
group.clearDirty();
|
|
7273
8043
|
}
|
|
7274
|
-
this.
|
|
8044
|
+
if (this._contentDirty) {
|
|
8045
|
+
this.edgeEndpointUpdater.updateAll();
|
|
8046
|
+
this._contentDirty = false;
|
|
8047
|
+
}
|
|
7275
8048
|
for (const node of this._nodes.values()) if (node.visible) {
|
|
7276
8049
|
this.renderElementWithAnimation(ctx, node, () => node.render(ctx));
|
|
7277
8050
|
node.clearDirty();
|
|
@@ -7283,7 +8056,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7283
8056
|
for (const edge of this._edges.values()) if (edge.visible) edge.renderHandles(ctx);
|
|
7284
8057
|
for (const overlayRenderer of this.overlayRenderers) overlayRenderer(ctx);
|
|
7285
8058
|
for (const topOverlay of this.topOverlayRenderers) topOverlay(ctx);
|
|
7286
|
-
this.
|
|
8059
|
+
this.scrollbarController.render(ctx, this.frameTime);
|
|
7287
8060
|
this.emit("render");
|
|
7288
8061
|
}
|
|
7289
8062
|
renderElementWithAnimation(ctx, element, renderFn) {
|
|
@@ -7315,93 +8088,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7315
8088
|
* Called during drag to keep control points in sync
|
|
7316
8089
|
*/
|
|
7317
8090
|
updateEdgeEndpointsForDrag() {
|
|
7318
|
-
|
|
7319
|
-
if (!edge.autoUpdateEndpoints) continue;
|
|
7320
|
-
const fromNode = this._nodes.get(edge.from.nodeId);
|
|
7321
|
-
const toNode = this._nodes.get(edge.to.nodeId);
|
|
7322
|
-
if (fromNode === void 0 || toNode === void 0) continue;
|
|
7323
|
-
let fromPoint;
|
|
7324
|
-
let toPoint;
|
|
7325
|
-
let fromDir;
|
|
7326
|
-
let toDir;
|
|
7327
|
-
if (edge.from.portId) {
|
|
7328
|
-
const anchorId = edge.from.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7329
|
-
const anchorPoint = fromNode.getAnchorPointById(anchorId);
|
|
7330
|
-
if (anchorPoint) {
|
|
7331
|
-
fromPoint = anchorPoint;
|
|
7332
|
-
fromDir = anchorId.split(":")[0];
|
|
7333
|
-
} else fromPoint = fromNode.getCenter();
|
|
7334
|
-
} else if (edge.from.outlineParam !== void 0) {
|
|
7335
|
-
fromPoint = fromNode.getConnectionPointAtOutlineParam(edge.from.outlineParam);
|
|
7336
|
-
fromDir = this.getDirectionFromOutlineParam(edge.from.outlineParam);
|
|
7337
|
-
} else fromPoint = fromNode.getCenter();
|
|
7338
|
-
if (edge.to.portId) {
|
|
7339
|
-
const anchorId = edge.to.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7340
|
-
const anchorPoint = toNode.getAnchorPointById(anchorId);
|
|
7341
|
-
if (anchorPoint) {
|
|
7342
|
-
toPoint = anchorPoint;
|
|
7343
|
-
toDir = anchorId.split(":")[0];
|
|
7344
|
-
} else toPoint = toNode.getCenter();
|
|
7345
|
-
} else if (edge.to.outlineParam !== void 0) {
|
|
7346
|
-
toPoint = toNode.getConnectionPointAtOutlineParam(edge.to.outlineParam);
|
|
7347
|
-
toDir = this.getDirectionFromOutlineParam(edge.to.outlineParam);
|
|
7348
|
-
} else toPoint = toNode.getCenter();
|
|
7349
|
-
edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir);
|
|
7350
|
-
}
|
|
7351
|
-
}
|
|
7352
|
-
updateEdgeEndpoints() {
|
|
7353
|
-
const obstacles = this.getNodeObstacles();
|
|
7354
|
-
for (const edge of this._edges.values()) {
|
|
7355
|
-
if (!edge.autoUpdateEndpoints) continue;
|
|
7356
|
-
const fromNode = this._nodes.get(edge.from.nodeId);
|
|
7357
|
-
const toNode = this._nodes.get(edge.to.nodeId);
|
|
7358
|
-
if (fromNode === void 0 || toNode === void 0) continue;
|
|
7359
|
-
if (edge.lockAnchors && edge.from.outlineParam === void 0) {
|
|
7360
|
-
if (!edge.from.portId) {
|
|
7361
|
-
const anchor = fromNode.getNearestAnchor(toNode.getCenter());
|
|
7362
|
-
if (anchor) edge.from = {
|
|
7363
|
-
...edge.from,
|
|
7364
|
-
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
|
-
}
|
|
7368
|
-
if (edge.lockAnchors && edge.to.outlineParam === void 0) {
|
|
7369
|
-
if (!edge.to.portId) {
|
|
7370
|
-
const anchor = toNode.getNearestAnchor(fromNode.getCenter());
|
|
7371
|
-
if (anchor) edge.to = {
|
|
7372
|
-
...edge.to,
|
|
7373
|
-
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7374
|
-
};
|
|
7375
|
-
}
|
|
7376
|
-
}
|
|
7377
|
-
let fromPoint;
|
|
7378
|
-
let toPoint;
|
|
7379
|
-
let fromDir;
|
|
7380
|
-
let toDir;
|
|
7381
|
-
if (edge.from.portId) {
|
|
7382
|
-
const anchorId = edge.from.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7383
|
-
const anchorPoint = fromNode.getAnchorPointById(anchorId);
|
|
7384
|
-
if (anchorPoint) {
|
|
7385
|
-
fromPoint = anchorPoint;
|
|
7386
|
-
fromDir = anchorId.split(":")[0];
|
|
7387
|
-
} else fromPoint = fromNode.getCenter();
|
|
7388
|
-
} else if (edge.from.outlineParam !== void 0) {
|
|
7389
|
-
fromPoint = fromNode.getConnectionPointAtOutlineParam(edge.from.outlineParam);
|
|
7390
|
-
fromDir = this.getDirectionFromOutlineParam(edge.from.outlineParam);
|
|
7391
|
-
} else fromPoint = fromNode.getCenter();
|
|
7392
|
-
if (edge.to.portId) {
|
|
7393
|
-
const anchorId = edge.to.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7394
|
-
const anchorPoint = toNode.getAnchorPointById(anchorId);
|
|
7395
|
-
if (anchorPoint) {
|
|
7396
|
-
toPoint = anchorPoint;
|
|
7397
|
-
toDir = anchorId.split(":")[0];
|
|
7398
|
-
} else toPoint = toNode.getCenter();
|
|
7399
|
-
} else if (edge.to.outlineParam !== void 0) {
|
|
7400
|
-
toPoint = toNode.getConnectionPointAtOutlineParam(edge.to.outlineParam);
|
|
7401
|
-
toDir = this.getDirectionFromOutlineParam(edge.to.outlineParam);
|
|
7402
|
-
} else toPoint = toNode.getCenter();
|
|
7403
|
-
edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir, { obstacles });
|
|
7404
|
-
}
|
|
8091
|
+
this.edgeEndpointUpdater.updateForDrag();
|
|
7405
8092
|
}
|
|
7406
8093
|
getNodeObstacles() {
|
|
7407
8094
|
if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
|
|
@@ -7414,56 +8101,6 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7414
8101
|
}));
|
|
7415
8102
|
return this._nodeObstaclesCache;
|
|
7416
8103
|
}
|
|
7417
|
-
getDirectionFromOutlineParam(param) {
|
|
7418
|
-
const p = (param % 1 + 1) % 1;
|
|
7419
|
-
if (p < .25) return "top";
|
|
7420
|
-
if (p < .5) return "right";
|
|
7421
|
-
if (p < .75) return "bottom";
|
|
7422
|
-
return "left";
|
|
7423
|
-
}
|
|
7424
|
-
renderScrollbars(ctx) {
|
|
7425
|
-
const metrics = this.getScrollbarMetrics();
|
|
7426
|
-
if (!metrics) return;
|
|
7427
|
-
const alpha = this.getScrollbarAlpha(this.frameTime);
|
|
7428
|
-
this.lastScrollbarAlpha = alpha;
|
|
7429
|
-
if (alpha <= .001) return;
|
|
7430
|
-
const colors = this.resolveScrollbarColors();
|
|
7431
|
-
const drawRoundedRect = (x, y, w, h, r) => {
|
|
7432
|
-
drawRoundedRectPath(ctx, x, y, w, h, r);
|
|
7433
|
-
};
|
|
7434
|
-
ctx.save();
|
|
7435
|
-
ctx.setTransform(this.devicePixelRatio, 0, 0, this.devicePixelRatio, 0, 0);
|
|
7436
|
-
ctx.globalAlpha = alpha;
|
|
7437
|
-
if (metrics.horizontal) {
|
|
7438
|
-
const isHovered = this.scrollbarHoveredAxis === "horizontal";
|
|
7439
|
-
const isActive = this.scrollbarActiveAxis === "horizontal";
|
|
7440
|
-
const thickness = isHovered || isActive ? this.scrollbar.hoverThickness : this.scrollbar.thickness;
|
|
7441
|
-
const trackY = metrics.horizontal.trackY + (metrics.horizontal.thickness - thickness) / 2;
|
|
7442
|
-
const radius = thickness / 2;
|
|
7443
|
-
const thumbColor = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7444
|
-
ctx.fillStyle = colors.track;
|
|
7445
|
-
drawRoundedRect(metrics.horizontal.trackX, trackY, metrics.horizontal.trackLength, thickness, radius);
|
|
7446
|
-
ctx.fill();
|
|
7447
|
-
ctx.fillStyle = thumbColor;
|
|
7448
|
-
drawRoundedRect(metrics.horizontal.trackX + metrics.horizontal.thumbOffset, trackY, metrics.horizontal.thumbLength, thickness, radius);
|
|
7449
|
-
ctx.fill();
|
|
7450
|
-
}
|
|
7451
|
-
if (metrics.vertical) {
|
|
7452
|
-
const isHovered = this.scrollbarHoveredAxis === "vertical";
|
|
7453
|
-
const isActive = this.scrollbarActiveAxis === "vertical";
|
|
7454
|
-
const thickness = isHovered || isActive ? this.scrollbar.hoverThickness : this.scrollbar.thickness;
|
|
7455
|
-
const trackX = metrics.vertical.trackX + (metrics.vertical.thickness - thickness) / 2;
|
|
7456
|
-
const radius = thickness / 2;
|
|
7457
|
-
const thumbColor = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7458
|
-
ctx.fillStyle = colors.track;
|
|
7459
|
-
drawRoundedRect(trackX, metrics.vertical.trackY, thickness, metrics.vertical.trackLength, radius);
|
|
7460
|
-
ctx.fill();
|
|
7461
|
-
ctx.fillStyle = thumbColor;
|
|
7462
|
-
drawRoundedRect(trackX, metrics.vertical.trackY + metrics.vertical.thumbOffset, thickness, metrics.vertical.thumbLength, radius);
|
|
7463
|
-
ctx.fill();
|
|
7464
|
-
}
|
|
7465
|
-
ctx.restore();
|
|
7466
|
-
}
|
|
7467
8104
|
getContentBounds() {
|
|
7468
8105
|
return getContentBounds({
|
|
7469
8106
|
nodes: this._nodes.values(),
|
|
@@ -7471,348 +8108,69 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7471
8108
|
groups: this._groups.values()
|
|
7472
8109
|
});
|
|
7473
8110
|
}
|
|
7474
|
-
getViewportBounds() {
|
|
7475
|
-
return {
|
|
7476
|
-
x: -this._offsetX / this._zoom,
|
|
7477
|
-
y: -this._offsetY / this._zoom,
|
|
7478
|
-
width: this.options.width / this._zoom,
|
|
7479
|
-
height: this.options.height / this._zoom
|
|
7480
|
-
};
|
|
7481
|
-
}
|
|
7482
8111
|
notifyScrollbarInteraction() {
|
|
7483
|
-
this.
|
|
7484
|
-
this.markDirty();
|
|
8112
|
+
this.scrollbarController.notifyInteraction();
|
|
7485
8113
|
}
|
|
7486
8114
|
setScrollbarActiveAxis(axis) {
|
|
7487
|
-
|
|
7488
|
-
this.scrollbarActiveAxis = axis;
|
|
7489
|
-
if (axis !== null) this.notifyScrollbarInteraction();
|
|
7490
|
-
else this.markDirty();
|
|
8115
|
+
this.scrollbarController.setActiveAxis(axis);
|
|
7491
8116
|
}
|
|
7492
8117
|
updateScrollbarHover(screenX, screenY) {
|
|
7493
|
-
|
|
7494
|
-
if (this.scrollbarHoveredAxis !== axis) {
|
|
7495
|
-
this.scrollbarHoveredAxis = axis;
|
|
7496
|
-
this.markDirty();
|
|
7497
|
-
if (axis !== null) this.notifyScrollbarInteraction();
|
|
7498
|
-
}
|
|
7499
|
-
return axis !== null;
|
|
8118
|
+
return this.scrollbarController.updateHover(screenX, screenY);
|
|
7500
8119
|
}
|
|
7501
8120
|
clearScrollbarHover() {
|
|
7502
|
-
|
|
7503
|
-
this.scrollbarHoveredAxis = null;
|
|
7504
|
-
this.markDirty();
|
|
7505
|
-
}
|
|
8121
|
+
this.scrollbarController.clearHover();
|
|
7506
8122
|
}
|
|
7507
8123
|
hitTestScrollbarThumb(screenX, screenY) {
|
|
7508
|
-
|
|
7509
|
-
if (!metrics) return null;
|
|
7510
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7511
|
-
if (!localPoint) return null;
|
|
7512
|
-
const { x, y } = localPoint;
|
|
7513
|
-
const hitPadding = this.scrollbar.hitAreaPadding;
|
|
7514
|
-
if (metrics.horizontal) {
|
|
7515
|
-
const thumbStartX = metrics.horizontal.trackX + metrics.horizontal.thumbOffset;
|
|
7516
|
-
if (x >= thumbStartX - hitPadding && x <= thumbStartX + metrics.horizontal.thumbLength + hitPadding && y >= metrics.horizontal.trackY - hitPadding && y <= metrics.horizontal.trackY + metrics.horizontal.thickness + hitPadding) return {
|
|
7517
|
-
axis: "horizontal",
|
|
7518
|
-
pointerOffset: x - thumbStartX
|
|
7519
|
-
};
|
|
7520
|
-
}
|
|
7521
|
-
if (metrics.vertical) {
|
|
7522
|
-
const thumbStartY = metrics.vertical.trackY + metrics.vertical.thumbOffset;
|
|
7523
|
-
if (x >= metrics.vertical.trackX - hitPadding && x <= metrics.vertical.trackX + metrics.vertical.thickness + hitPadding && y >= thumbStartY - hitPadding && y <= thumbStartY + metrics.vertical.thumbLength + hitPadding) return {
|
|
7524
|
-
axis: "vertical",
|
|
7525
|
-
pointerOffset: y - thumbStartY
|
|
7526
|
-
};
|
|
7527
|
-
}
|
|
7528
|
-
return null;
|
|
8124
|
+
return this.scrollbarController.hitTestThumb(screenX, screenY);
|
|
7529
8125
|
}
|
|
7530
8126
|
dragScrollbarThumb(axis, screenX, screenY, pointerOffset) {
|
|
7531
|
-
|
|
7532
|
-
if (!metrics) return false;
|
|
7533
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7534
|
-
if (!localPoint) return false;
|
|
7535
|
-
if (axis === "horizontal" && metrics.horizontal) {
|
|
7536
|
-
const travel = Math.max(0, metrics.horizontal.trackLength - metrics.horizontal.thumbLength);
|
|
7537
|
-
const desiredThumbOffset = localPoint.x - metrics.horizontal.trackX - pointerOffset;
|
|
7538
|
-
const clampedThumbOffset = Math.min(Math.max(desiredThumbOffset, 0), travel);
|
|
7539
|
-
const scrollX = travel > 0 && metrics.horizontal.maxScroll > 0 ? clampedThumbOffset / travel * metrics.horizontal.maxScroll : 0;
|
|
7540
|
-
this.offsetX = -(metrics.contentBounds.x + scrollX) * this._zoom;
|
|
7541
|
-
this.notifyScrollbarInteraction();
|
|
7542
|
-
return true;
|
|
7543
|
-
}
|
|
7544
|
-
if (axis === "vertical" && metrics.vertical) {
|
|
7545
|
-
const travel = Math.max(0, metrics.vertical.trackLength - metrics.vertical.thumbLength);
|
|
7546
|
-
const desiredThumbOffset = localPoint.y - metrics.vertical.trackY - pointerOffset;
|
|
7547
|
-
const clampedThumbOffset = Math.min(Math.max(desiredThumbOffset, 0), travel);
|
|
7548
|
-
const scrollY = travel > 0 && metrics.vertical.maxScroll > 0 ? clampedThumbOffset / travel * metrics.vertical.maxScroll : 0;
|
|
7549
|
-
this.offsetY = -(metrics.contentBounds.y + scrollY) * this._zoom;
|
|
7550
|
-
this.notifyScrollbarInteraction();
|
|
7551
|
-
return true;
|
|
7552
|
-
}
|
|
7553
|
-
return false;
|
|
8127
|
+
return this.scrollbarController.dragThumb(axis, screenX, screenY, pointerOffset);
|
|
7554
8128
|
}
|
|
7555
8129
|
clickScrollbarTrack(screenX, screenY) {
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
if (metrics.vertical) {
|
|
7577
|
-
const { trackX, trackY, trackLength, thumbOffset, thumbLength, maxScroll } = metrics.vertical;
|
|
7578
|
-
if (localPoint.x >= trackX && localPoint.x <= trackX + metrics.vertical.thickness && localPoint.y >= trackY && localPoint.y <= trackY + trackLength) {
|
|
7579
|
-
const thumbStart = trackY + thumbOffset;
|
|
7580
|
-
const thumbEnd = thumbStart + thumbLength;
|
|
7581
|
-
if (localPoint.y < thumbStart || localPoint.y > thumbEnd) {
|
|
7582
|
-
const direction = localPoint.y < thumbStart ? -1 : 1;
|
|
7583
|
-
const step = metrics.viewportBounds.height * this.scrollbar.pageScrollRatio * direction;
|
|
7584
|
-
const currentScrollY = Math.min(Math.max(metrics.viewportBounds.y - metrics.contentBounds.y, 0), maxScroll);
|
|
7585
|
-
const nextScrollY = Math.min(Math.max(currentScrollY + step, 0), maxScroll);
|
|
7586
|
-
this.offsetY = -(metrics.contentBounds.y + nextScrollY) * this.zoom;
|
|
7587
|
-
this.notifyScrollbarInteraction();
|
|
7588
|
-
return true;
|
|
7589
|
-
}
|
|
7590
|
-
}
|
|
7591
|
-
}
|
|
7592
|
-
return false;
|
|
7593
|
-
}
|
|
7594
|
-
scrollViewportBy(screenDx, screenDy) {
|
|
7595
|
-
const scrollState = this.getScrollState();
|
|
7596
|
-
if (!scrollState) return false;
|
|
7597
|
-
const worldDx = screenDx / this.zoom;
|
|
7598
|
-
const worldDy = screenDy / this.zoom;
|
|
7599
|
-
const currentViewportX = scrollState.viewportBounds.x;
|
|
7600
|
-
const currentViewportY = scrollState.viewportBounds.y;
|
|
7601
|
-
const maxScrollX = Math.max(0, scrollState.contentBounds.width - scrollState.viewportBounds.width);
|
|
7602
|
-
const maxScrollY = Math.max(0, scrollState.contentBounds.height - scrollState.viewportBounds.height);
|
|
7603
|
-
const nextViewportX = Math.min(Math.max(currentViewportX + worldDx, scrollState.contentBounds.x), scrollState.contentBounds.x + maxScrollX);
|
|
7604
|
-
const nextViewportY = Math.min(Math.max(currentViewportY + worldDy, scrollState.contentBounds.y), scrollState.contentBounds.y + maxScrollY);
|
|
7605
|
-
this.offsetX = -nextViewportX * this.zoom;
|
|
7606
|
-
this.offsetY = -nextViewportY * this.zoom;
|
|
7607
|
-
this.notifyScrollbarInteraction();
|
|
7608
|
-
return true;
|
|
7609
|
-
}
|
|
7610
|
-
scrollViewportToStart() {
|
|
7611
|
-
const scrollState = this.getScrollState();
|
|
7612
|
-
if (!scrollState) return false;
|
|
7613
|
-
this.offsetX = -scrollState.contentBounds.x * this.zoom;
|
|
7614
|
-
this.offsetY = -scrollState.contentBounds.y * this.zoom;
|
|
7615
|
-
this.notifyScrollbarInteraction();
|
|
7616
|
-
return true;
|
|
7617
|
-
}
|
|
7618
|
-
scrollViewportToEnd() {
|
|
7619
|
-
const scrollState = this.getScrollState();
|
|
7620
|
-
if (!scrollState) return false;
|
|
7621
|
-
const maxScrollX = Math.max(0, scrollState.contentBounds.width - scrollState.viewportBounds.width);
|
|
7622
|
-
const maxScrollY = Math.max(0, scrollState.contentBounds.height - scrollState.viewportBounds.height);
|
|
7623
|
-
const targetViewportX = scrollState.contentBounds.x + maxScrollX;
|
|
7624
|
-
const targetViewportY = scrollState.contentBounds.y + maxScrollY;
|
|
7625
|
-
this.offsetX = -targetViewportX * this.zoom;
|
|
7626
|
-
this.offsetY = -targetViewportY * this.zoom;
|
|
7627
|
-
this.notifyScrollbarInteraction();
|
|
7628
|
-
return true;
|
|
7629
|
-
}
|
|
7630
|
-
beginOverlayDrag(screenX, screenY) {
|
|
7631
|
-
for (const plugin of this.plugins) {
|
|
7632
|
-
const interactivePlugin = plugin;
|
|
7633
|
-
if (!interactivePlugin.beginOverlayDrag) continue;
|
|
7634
|
-
const payload = interactivePlugin.beginOverlayDrag(this, screenX, screenY);
|
|
7635
|
-
if (payload !== null && payload !== void 0) return {
|
|
7636
|
-
plugin: interactivePlugin,
|
|
7637
|
-
payload
|
|
7638
|
-
};
|
|
7639
|
-
}
|
|
7640
|
-
return null;
|
|
7641
|
-
}
|
|
7642
|
-
/**
|
|
7643
|
-
* Точка (client / screen) попадает в оверлей, который не должен «пробрасывать» hit-test в диаграмму.
|
|
7644
|
-
*/
|
|
7645
|
-
blocksDiagramPointerAtScreen(screenX, screenY) {
|
|
7646
|
-
for (const plugin of this.plugins) if (plugin.blocksDiagramPointerAtScreen?.(this, screenX, screenY)) return true;
|
|
7647
|
-
return false;
|
|
7648
|
-
}
|
|
7649
|
-
updateOverlayDrag(session, screenX, screenY) {
|
|
7650
|
-
return session.plugin.updateOverlayDrag?.(this, screenX, screenY, session.payload) ?? false;
|
|
7651
|
-
}
|
|
7652
|
-
endOverlayDrag(session) {
|
|
7653
|
-
session.plugin.endOverlayDrag?.(this, session.payload);
|
|
7654
|
-
}
|
|
7655
|
-
screenToCanvas(screenX, screenY) {
|
|
7656
|
-
const rect = this._canvasRect ?? this.canvas.getBoundingClientRect();
|
|
7657
|
-
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
7658
|
-
const scaleX = rect.width / this.options.width;
|
|
7659
|
-
const scaleY = rect.height / this.options.height;
|
|
7660
|
-
return {
|
|
7661
|
-
x: (screenX - rect.left) / scaleX,
|
|
7662
|
-
y: (screenY - rect.top) / scaleY
|
|
7663
|
-
};
|
|
7664
|
-
}
|
|
7665
|
-
getScrollbarMetrics() {
|
|
7666
|
-
if (!this.scrollbar.enabled) return null;
|
|
7667
|
-
const rawContentBounds = this.getContentBounds();
|
|
7668
|
-
if (!rawContentBounds) return null;
|
|
7669
|
-
const viewportBounds = this.getViewportBounds();
|
|
7670
|
-
const unionMinX = Math.min(rawContentBounds.x, viewportBounds.x);
|
|
7671
|
-
const unionMinY = Math.min(rawContentBounds.y, viewportBounds.y);
|
|
7672
|
-
const unionMaxX = Math.max(rawContentBounds.x + rawContentBounds.width, viewportBounds.x + viewportBounds.width);
|
|
7673
|
-
const unionMaxY = Math.max(rawContentBounds.y + rawContentBounds.height, viewportBounds.y + viewportBounds.height);
|
|
7674
|
-
const contentBounds = {
|
|
7675
|
-
x: unionMinX,
|
|
7676
|
-
y: unionMinY,
|
|
7677
|
-
width: unionMaxX - unionMinX,
|
|
7678
|
-
height: unionMaxY - unionMinY
|
|
7679
|
-
};
|
|
7680
|
-
const contentWidth = Math.max(contentBounds.width, 1);
|
|
7681
|
-
const contentHeight = Math.max(contentBounds.height, 1);
|
|
7682
|
-
const showHorizontal = contentWidth > viewportBounds.width + .01;
|
|
7683
|
-
const showVertical = contentHeight > viewportBounds.height + .01;
|
|
7684
|
-
if (!showHorizontal && !showVertical) return null;
|
|
7685
|
-
const { width, height } = this.options;
|
|
7686
|
-
const padding = 6;
|
|
7687
|
-
const thickness = this.scrollbar.thickness;
|
|
7688
|
-
const spacing = 4;
|
|
7689
|
-
const minThumbLength = this.scrollbar.minThumbLength;
|
|
7690
|
-
const availableWidth = width - padding * 2;
|
|
7691
|
-
const availableHeight = height - padding * 2;
|
|
7692
|
-
if (availableWidth <= 0 || availableHeight <= 0) return null;
|
|
7693
|
-
const horizontalTrackLength = Math.max(0, availableWidth - (showVertical ? thickness + spacing : 0));
|
|
7694
|
-
const verticalTrackLength = Math.max(0, availableHeight - (showHorizontal ? thickness + spacing : 0));
|
|
7695
|
-
const horizontalTrackX = padding;
|
|
7696
|
-
const horizontalTrackY = height - padding - thickness;
|
|
7697
|
-
const verticalTrackX = width - padding - thickness;
|
|
7698
|
-
const verticalTrackY = padding;
|
|
7699
|
-
let horizontal = null;
|
|
7700
|
-
if (showHorizontal && horizontalTrackLength > 0) {
|
|
7701
|
-
const maxScrollX = Math.max(0, contentWidth - viewportBounds.width);
|
|
7702
|
-
const scrollX = Math.min(Math.max(viewportBounds.x - contentBounds.x, 0), maxScrollX);
|
|
7703
|
-
const ratioX = viewportBounds.width / contentWidth;
|
|
7704
|
-
const thumbLength = Math.min(horizontalTrackLength, Math.max(minThumbLength, horizontalTrackLength * ratioX));
|
|
7705
|
-
const travel = Math.max(0, horizontalTrackLength - thumbLength);
|
|
7706
|
-
horizontal = {
|
|
7707
|
-
trackX: horizontalTrackX,
|
|
7708
|
-
trackY: horizontalTrackY,
|
|
7709
|
-
trackLength: horizontalTrackLength,
|
|
7710
|
-
thickness,
|
|
7711
|
-
thumbLength,
|
|
7712
|
-
thumbOffset: maxScrollX > 0 ? scrollX / maxScrollX * travel : 0,
|
|
7713
|
-
maxScroll: maxScrollX
|
|
7714
|
-
};
|
|
7715
|
-
}
|
|
7716
|
-
let vertical = null;
|
|
7717
|
-
if (showVertical && verticalTrackLength > 0) {
|
|
7718
|
-
const maxScrollY = Math.max(0, contentHeight - viewportBounds.height);
|
|
7719
|
-
const scrollY = Math.min(Math.max(viewportBounds.y - contentBounds.y, 0), maxScrollY);
|
|
7720
|
-
const ratioY = viewportBounds.height / contentHeight;
|
|
7721
|
-
const thumbLength = Math.min(verticalTrackLength, Math.max(minThumbLength, verticalTrackLength * ratioY));
|
|
7722
|
-
const travel = Math.max(0, verticalTrackLength - thumbLength);
|
|
7723
|
-
vertical = {
|
|
7724
|
-
trackX: verticalTrackX,
|
|
7725
|
-
trackY: verticalTrackY,
|
|
7726
|
-
trackLength: verticalTrackLength,
|
|
7727
|
-
thickness,
|
|
7728
|
-
thumbLength,
|
|
7729
|
-
thumbOffset: maxScrollY > 0 ? scrollY / maxScrollY * travel : 0,
|
|
7730
|
-
maxScroll: maxScrollY
|
|
7731
|
-
};
|
|
7732
|
-
}
|
|
7733
|
-
if (!horizontal && !vertical) return null;
|
|
7734
|
-
return {
|
|
7735
|
-
contentBounds,
|
|
7736
|
-
viewportBounds,
|
|
7737
|
-
thickness: this.scrollbar.thickness,
|
|
7738
|
-
horizontal,
|
|
7739
|
-
vertical
|
|
7740
|
-
};
|
|
7741
|
-
}
|
|
7742
|
-
resolveScrollbarOptions(options) {
|
|
7743
|
-
const fallbackEnabled = options.scrollbarOverlay ?? DEFAULT_SCROLLBAR_OPTIONS.enabled;
|
|
7744
|
-
const resolved = {
|
|
7745
|
-
...DEFAULT_SCROLLBAR_OPTIONS,
|
|
7746
|
-
enabled: fallbackEnabled
|
|
7747
|
-
};
|
|
7748
|
-
if (typeof options.scrollbar === "boolean") {
|
|
7749
|
-
resolved.enabled = options.scrollbar;
|
|
7750
|
-
return resolved;
|
|
7751
|
-
}
|
|
7752
|
-
if (options.scrollbar) return {
|
|
7753
|
-
...resolved,
|
|
7754
|
-
...options.scrollbar
|
|
7755
|
-
};
|
|
7756
|
-
return resolved;
|
|
7757
|
-
}
|
|
7758
|
-
resolveScrollbarColors() {
|
|
7759
|
-
const fallback = this.styleManager?.theme.name === "dark" ? {
|
|
7760
|
-
track: "rgba(148, 163, 184, 0.22)",
|
|
7761
|
-
thumb: "rgba(226, 232, 240, 0.68)",
|
|
7762
|
-
thumbHover: "rgba(226, 232, 240, 0.86)",
|
|
7763
|
-
thumbActive: "rgba(248, 250, 252, 0.96)"
|
|
7764
|
-
} : {
|
|
7765
|
-
track: "rgba(15, 23, 42, 0.16)",
|
|
7766
|
-
thumb: "rgba(51, 65, 85, 0.7)",
|
|
7767
|
-
thumbHover: "rgba(30, 41, 59, 0.82)",
|
|
7768
|
-
thumbActive: "rgba(15, 23, 42, 0.9)"
|
|
7769
|
-
};
|
|
7770
|
-
return {
|
|
7771
|
-
track: this.scrollbar.trackColor || fallback.track,
|
|
7772
|
-
thumb: this.scrollbar.thumbColor || fallback.thumb,
|
|
7773
|
-
thumbHover: this.scrollbar.thumbHoverColor || fallback.thumbHover,
|
|
7774
|
-
thumbActive: this.scrollbar.thumbActiveColor || fallback.thumbActive
|
|
7775
|
-
};
|
|
7776
|
-
}
|
|
7777
|
-
getScrollbarAlpha(now) {
|
|
7778
|
-
if (!this.scrollbar.autoHide) return 1;
|
|
7779
|
-
if (this.scrollbarActiveAxis !== null || this.scrollbarHoveredAxis !== null) return 1;
|
|
7780
|
-
const elapsed = now - this.scrollbarLastInteractionAt;
|
|
7781
|
-
if (elapsed <= this.scrollbar.autoHideDelay) return 1;
|
|
7782
|
-
if (this.scrollbar.fadeDuration <= 0) return 0;
|
|
7783
|
-
const fadeProgress = (elapsed - this.scrollbar.autoHideDelay) / this.scrollbar.fadeDuration;
|
|
7784
|
-
return Math.max(0, 1 - fadeProgress);
|
|
7785
|
-
}
|
|
7786
|
-
hitTestScrollbarArea(screenX, screenY) {
|
|
7787
|
-
const metrics = this.getScrollbarMetrics();
|
|
7788
|
-
if (!metrics) return null;
|
|
7789
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7790
|
-
if (!localPoint) return null;
|
|
7791
|
-
const hitPadding = this.scrollbar.hitAreaPadding;
|
|
7792
|
-
if (metrics.horizontal) {
|
|
7793
|
-
if (localPoint.x >= metrics.horizontal.trackX - hitPadding && localPoint.x <= metrics.horizontal.trackX + metrics.horizontal.trackLength + hitPadding && localPoint.y >= metrics.horizontal.trackY - hitPadding && localPoint.y <= metrics.horizontal.trackY + metrics.horizontal.thickness + hitPadding) return "horizontal";
|
|
7794
|
-
}
|
|
7795
|
-
if (metrics.vertical) {
|
|
7796
|
-
if (localPoint.x >= metrics.vertical.trackX - hitPadding && localPoint.x <= metrics.vertical.trackX + metrics.vertical.thickness + hitPadding && localPoint.y >= metrics.vertical.trackY - hitPadding && localPoint.y <= metrics.vertical.trackY + metrics.vertical.trackLength + hitPadding) return "vertical";
|
|
8130
|
+
return this.scrollbarController.clickTrack(screenX, screenY);
|
|
8131
|
+
}
|
|
8132
|
+
scrollViewportBy(screenDx, screenDy) {
|
|
8133
|
+
return this.scrollbarController.scrollBy(screenDx, screenDy);
|
|
8134
|
+
}
|
|
8135
|
+
scrollViewportToStart() {
|
|
8136
|
+
return this.scrollbarController.scrollToStart();
|
|
8137
|
+
}
|
|
8138
|
+
scrollViewportToEnd() {
|
|
8139
|
+
return this.scrollbarController.scrollToEnd();
|
|
8140
|
+
}
|
|
8141
|
+
beginOverlayDrag(screenX, screenY) {
|
|
8142
|
+
for (const plugin of this.plugins) {
|
|
8143
|
+
const interactivePlugin = plugin;
|
|
8144
|
+
if (!interactivePlugin.beginOverlayDrag) continue;
|
|
8145
|
+
const payload = interactivePlugin.beginOverlayDrag(this, screenX, screenY);
|
|
8146
|
+
if (payload !== null && payload !== void 0) return {
|
|
8147
|
+
plugin: interactivePlugin,
|
|
8148
|
+
payload
|
|
8149
|
+
};
|
|
7797
8150
|
}
|
|
7798
8151
|
return null;
|
|
7799
8152
|
}
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
const
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
8153
|
+
/**
|
|
8154
|
+
* Точка (client / screen) попадает в оверлей, который не должен «пробрасывать» hit-test в диаграмму.
|
|
8155
|
+
*/
|
|
8156
|
+
blocksDiagramPointerAtScreen(screenX, screenY) {
|
|
8157
|
+
for (const plugin of this.plugins) if (plugin.blocksDiagramPointerAtScreen?.(this, screenX, screenY)) return true;
|
|
8158
|
+
return false;
|
|
8159
|
+
}
|
|
8160
|
+
updateOverlayDrag(session, screenX, screenY) {
|
|
8161
|
+
return session.plugin.updateOverlayDrag?.(this, screenX, screenY, session.payload) ?? false;
|
|
8162
|
+
}
|
|
8163
|
+
endOverlayDrag(session) {
|
|
8164
|
+
session.plugin.endOverlayDrag?.(this, session.payload);
|
|
8165
|
+
}
|
|
8166
|
+
screenToCanvas(screenX, screenY) {
|
|
8167
|
+
const rect = this._canvasRect ?? this.canvas.getBoundingClientRect();
|
|
8168
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
8169
|
+
const scaleX = rect.width / this.options.width;
|
|
8170
|
+
const scaleY = rect.height / this.options.height;
|
|
7808
8171
|
return {
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
y: unionMinY,
|
|
7812
|
-
width: unionMaxX - unionMinX,
|
|
7813
|
-
height: unionMaxY - unionMinY
|
|
7814
|
-
},
|
|
7815
|
-
viewportBounds
|
|
8172
|
+
x: (screenX - rect.left) / scaleX,
|
|
8173
|
+
y: (screenY - rect.top) / scaleY
|
|
7816
8174
|
};
|
|
7817
8175
|
}
|
|
7818
8176
|
};
|
|
@@ -7930,8 +8288,10 @@ var SearchManager = class {
|
|
|
7930
8288
|
for (const edge of this.renderer.edges.values()) {
|
|
7931
8289
|
if (scope === "nodes") continue;
|
|
7932
8290
|
const matchesEdge = this.matchesEdge(edge, options);
|
|
7933
|
-
const
|
|
7934
|
-
const
|
|
8291
|
+
const fromId = edge.from.nodeId;
|
|
8292
|
+
const toId = edge.to.nodeId;
|
|
8293
|
+
const fromVisible = fromId ? this.renderer.getNode(fromId)?.visible ?? true : true;
|
|
8294
|
+
const toVisible = toId ? this.renderer.getNode(toId)?.visible ?? true : true;
|
|
7935
8295
|
edge.visible = matchesEdge && fromVisible && toVisible;
|
|
7936
8296
|
}
|
|
7937
8297
|
this.renderer.markDirty();
|
|
@@ -8032,6 +8392,7 @@ function generatePortId() {
|
|
|
8032
8392
|
}
|
|
8033
8393
|
/**
|
|
8034
8394
|
* Reset port ID counter (for testing)
|
|
8395
|
+
* @internal
|
|
8035
8396
|
*/
|
|
8036
8397
|
function resetPortIdCounter() {
|
|
8037
8398
|
portIdCounter = 1;
|
|
@@ -8159,6 +8520,61 @@ var Port = class {
|
|
|
8159
8520
|
}
|
|
8160
8521
|
};
|
|
8161
8522
|
//#endregion
|
|
8523
|
+
//#region src/utils/svgTint.ts
|
|
8524
|
+
/**
|
|
8525
|
+
* SVG tinting utilities — shared between NodeImage and CIcon.
|
|
8526
|
+
*/
|
|
8527
|
+
function styleSetColor(style, key, color) {
|
|
8528
|
+
if (new RegExp(`${key}\\s*:`).test(style)) return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
|
|
8529
|
+
return `${style}${style.trim().endsWith(";") || style.trim() === "" ? "" : ";"}${key}:${color};`;
|
|
8530
|
+
}
|
|
8531
|
+
/**
|
|
8532
|
+
* Tint all stroke/fill attributes and inline styles in an SVG string.
|
|
8533
|
+
*/
|
|
8534
|
+
function tintSvg(svgText, strokeColor, fillColor) {
|
|
8535
|
+
if (!strokeColor && !fillColor) return svgText;
|
|
8536
|
+
const root = new DOMParser().parseFromString(svgText, "image/svg+xml").documentElement;
|
|
8537
|
+
if (!root || root.nodeName.toLowerCase() === "parsererror") return svgText;
|
|
8538
|
+
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
8539
|
+
for (const el of all) {
|
|
8540
|
+
const stroke = el.getAttribute("stroke");
|
|
8541
|
+
const fill = el.getAttribute("fill");
|
|
8542
|
+
if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) el.setAttribute("stroke", strokeColor);
|
|
8543
|
+
if (fillColor && (fill === null || fill.toLowerCase() !== "none")) el.setAttribute("fill", fillColor);
|
|
8544
|
+
const style = el.getAttribute("style");
|
|
8545
|
+
if (style) {
|
|
8546
|
+
let next = style;
|
|
8547
|
+
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "stroke", strokeColor);
|
|
8548
|
+
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "fill", fillColor);
|
|
8549
|
+
if (next !== style) el.setAttribute("style", next);
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
return new XMLSerializer().serializeToString(root);
|
|
8553
|
+
}
|
|
8554
|
+
/**
|
|
8555
|
+
* Check if a string contains inline SVG markup.
|
|
8556
|
+
*/
|
|
8557
|
+
function isSvgMarkup(value) {
|
|
8558
|
+
const trimmed = value.trim().toLowerCase();
|
|
8559
|
+
return trimmed.startsWith("<svg") || trimmed.includes("<svg");
|
|
8560
|
+
}
|
|
8561
|
+
/**
|
|
8562
|
+
* Convert SVG markup to a data URL for use in HTMLImageElement.src.
|
|
8563
|
+
*/
|
|
8564
|
+
function svgToDataUrl(svg) {
|
|
8565
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ")}`;
|
|
8566
|
+
}
|
|
8567
|
+
/**
|
|
8568
|
+
* Check if a URL points to an SVG file.
|
|
8569
|
+
*/
|
|
8570
|
+
function isSvgUrl(url) {
|
|
8571
|
+
try {
|
|
8572
|
+
return new URL(url, "http://localhost").pathname.endsWith(".svg");
|
|
8573
|
+
} catch {
|
|
8574
|
+
return url.endsWith(".svg");
|
|
8575
|
+
}
|
|
8576
|
+
}
|
|
8577
|
+
//#endregion
|
|
8162
8578
|
//#region src/utils/LRUCache.ts
|
|
8163
8579
|
/**
|
|
8164
8580
|
* LRU (Least Recently Used) cache implementation
|
|
@@ -8225,66 +8641,174 @@ var LRUCache = class {
|
|
|
8225
8641
|
}
|
|
8226
8642
|
};
|
|
8227
8643
|
//#endregion
|
|
8228
|
-
//#region src/utils/
|
|
8644
|
+
//#region src/utils/svgAssetLoader.ts
|
|
8645
|
+
/** Shared LRU for fetched SVG text (NodeImage, CIcon, etc.). */
|
|
8646
|
+
var svgTextCache = new LRUCache(100);
|
|
8229
8647
|
/**
|
|
8230
|
-
* SVG
|
|
8648
|
+
* Fetch SVG text with a shared LRU cache.
|
|
8231
8649
|
*/
|
|
8232
|
-
function
|
|
8233
|
-
|
|
8234
|
-
|
|
8650
|
+
function fetchSvgText(url) {
|
|
8651
|
+
let promise = svgTextCache.get(url);
|
|
8652
|
+
if (!promise) {
|
|
8653
|
+
promise = fetch(url).then((r) => r.ok ? r.text() : "").catch(() => "");
|
|
8654
|
+
svgTextCache.set(url, promise);
|
|
8655
|
+
}
|
|
8656
|
+
return promise;
|
|
8235
8657
|
}
|
|
8236
8658
|
/**
|
|
8237
|
-
*
|
|
8659
|
+
* Synchronously read SVG from a URL (for SvgExporter). Uses cached promise result when already resolved.
|
|
8238
8660
|
*/
|
|
8239
|
-
function
|
|
8240
|
-
if (
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8661
|
+
function readSvgFromUrlSync(url) {
|
|
8662
|
+
if (typeof XMLHttpRequest === "undefined") return null;
|
|
8663
|
+
try {
|
|
8664
|
+
const xhr = new XMLHttpRequest();
|
|
8665
|
+
xhr.open("GET", url, false);
|
|
8666
|
+
xhr.send();
|
|
8667
|
+
if (xhr.status >= 200 && xhr.status < 300) return xhr.responseText || null;
|
|
8668
|
+
} catch {}
|
|
8669
|
+
return null;
|
|
8670
|
+
}
|
|
8671
|
+
/**
|
|
8672
|
+
* Resolve a source string to an href suitable for Image/SVG (async, with tint).
|
|
8673
|
+
*/
|
|
8674
|
+
async function resolveTintedSvgDataUrl(source, options = {}) {
|
|
8675
|
+
const { strokeColor, fillColor } = options;
|
|
8676
|
+
if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, strokeColor, fillColor));
|
|
8677
|
+
if (isSvgUrl(source) || source.toLowerCase().endsWith(".svg")) {
|
|
8678
|
+
const svgText = await fetchSvgText(source);
|
|
8679
|
+
if (!svgText) return null;
|
|
8680
|
+
return svgToDataUrl(tintSvg(svgText, strokeColor, fillColor));
|
|
8256
8681
|
}
|
|
8257
|
-
return
|
|
8682
|
+
return null;
|
|
8258
8683
|
}
|
|
8259
8684
|
/**
|
|
8260
|
-
*
|
|
8685
|
+
* Resolve a source to a tinted data URL synchronously (SvgExporter path).
|
|
8261
8686
|
*/
|
|
8262
|
-
function
|
|
8263
|
-
const
|
|
8264
|
-
|
|
8687
|
+
function resolveTintedSvgDataUrlSync(source, options = {}) {
|
|
8688
|
+
const { strokeColor, fillColor } = options;
|
|
8689
|
+
if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, strokeColor, fillColor));
|
|
8690
|
+
if (source.toLowerCase().endsWith(".svg")) {
|
|
8691
|
+
const svgText = readSvgFromUrlSync(source);
|
|
8692
|
+
if (!svgText) return null;
|
|
8693
|
+
return svgToDataUrl(tintSvg(svgText, strokeColor, fillColor));
|
|
8694
|
+
}
|
|
8695
|
+
return null;
|
|
8265
8696
|
}
|
|
8697
|
+
//#endregion
|
|
8698
|
+
//#region src/utils/iconLayout.ts
|
|
8266
8699
|
/**
|
|
8267
|
-
*
|
|
8700
|
+
* Icon zone size including inset padding around the image.
|
|
8268
8701
|
*/
|
|
8269
|
-
function
|
|
8270
|
-
return
|
|
8702
|
+
function getIconBoxSize(imageSize, inset) {
|
|
8703
|
+
return {
|
|
8704
|
+
width: imageSize.width + inset * 2,
|
|
8705
|
+
height: imageSize.height + inset * 2
|
|
8706
|
+
};
|
|
8271
8707
|
}
|
|
8272
8708
|
/**
|
|
8273
|
-
*
|
|
8709
|
+
* Bounds of the icon zone within a node, based on placement.
|
|
8274
8710
|
*/
|
|
8275
|
-
function
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8711
|
+
function getIconBounds(bounds, iconBoxSize, placement, inset) {
|
|
8712
|
+
switch (placement) {
|
|
8713
|
+
case "top": return {
|
|
8714
|
+
x: bounds.x,
|
|
8715
|
+
y: bounds.y,
|
|
8716
|
+
width: bounds.width,
|
|
8717
|
+
height: iconBoxSize.height
|
|
8718
|
+
};
|
|
8719
|
+
case "bottom": return {
|
|
8720
|
+
x: bounds.x,
|
|
8721
|
+
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8722
|
+
width: bounds.width,
|
|
8723
|
+
height: iconBoxSize.height
|
|
8724
|
+
};
|
|
8725
|
+
case "left": return {
|
|
8726
|
+
x: bounds.x,
|
|
8727
|
+
y: bounds.y,
|
|
8728
|
+
width: iconBoxSize.width,
|
|
8729
|
+
height: bounds.height
|
|
8730
|
+
};
|
|
8731
|
+
case "right": return {
|
|
8732
|
+
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8733
|
+
y: bounds.y,
|
|
8734
|
+
width: iconBoxSize.width,
|
|
8735
|
+
height: bounds.height
|
|
8736
|
+
};
|
|
8737
|
+
case "top-left": return {
|
|
8738
|
+
x: bounds.x + inset,
|
|
8739
|
+
y: bounds.y + inset,
|
|
8740
|
+
width: iconBoxSize.width,
|
|
8741
|
+
height: iconBoxSize.height
|
|
8742
|
+
};
|
|
8743
|
+
case "top-right": return {
|
|
8744
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
8745
|
+
y: bounds.y + inset,
|
|
8746
|
+
width: iconBoxSize.width,
|
|
8747
|
+
height: iconBoxSize.height
|
|
8748
|
+
};
|
|
8749
|
+
case "bottom-left": return {
|
|
8750
|
+
x: bounds.x + inset,
|
|
8751
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
8752
|
+
width: iconBoxSize.width,
|
|
8753
|
+
height: iconBoxSize.height
|
|
8754
|
+
};
|
|
8755
|
+
case "bottom-right": return {
|
|
8756
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
8757
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
8758
|
+
width: iconBoxSize.width,
|
|
8759
|
+
height: iconBoxSize.height
|
|
8760
|
+
};
|
|
8761
|
+
default: return bounds;
|
|
8280
8762
|
}
|
|
8281
8763
|
}
|
|
8764
|
+
/**
|
|
8765
|
+
* Compute the draw rectangle for an icon image inside its zone bounds.
|
|
8766
|
+
*/
|
|
8767
|
+
function computeIconDrawRect(bounds, opts, imageSize, inset) {
|
|
8768
|
+
const fit = opts.fit ?? "none";
|
|
8769
|
+
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
8770
|
+
const innerBounds = {
|
|
8771
|
+
x: bounds.x + inset,
|
|
8772
|
+
y: bounds.y + inset,
|
|
8773
|
+
width: Math.max(0, bounds.width - inset * 2),
|
|
8774
|
+
height: Math.max(0, bounds.height - inset * 2)
|
|
8775
|
+
};
|
|
8776
|
+
const availableWidth = Math.max(0, innerBounds.width);
|
|
8777
|
+
const availableHeight = Math.max(0, innerBounds.height);
|
|
8778
|
+
let drawWidth = opts.width ?? imageSize.width;
|
|
8779
|
+
let drawHeight = opts.height ?? imageSize.height;
|
|
8780
|
+
if (scaleWithBounds) {
|
|
8781
|
+
if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
|
|
8782
|
+
const scaleX = availableWidth / imageSize.width;
|
|
8783
|
+
const scaleY = availableHeight / imageSize.height;
|
|
8784
|
+
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
8785
|
+
drawWidth = imageSize.width * scale;
|
|
8786
|
+
drawHeight = imageSize.height * scale;
|
|
8787
|
+
} else if (fit === "stretch") {
|
|
8788
|
+
drawWidth = availableWidth;
|
|
8789
|
+
drawHeight = availableHeight;
|
|
8790
|
+
}
|
|
8791
|
+
}
|
|
8792
|
+
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
8793
|
+
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
8794
|
+
return {
|
|
8795
|
+
x: innerBounds.x + (innerBounds.width - drawWidth) / 2,
|
|
8796
|
+
y: innerBounds.y + (innerBounds.height - drawHeight) / 2,
|
|
8797
|
+
width: drawWidth,
|
|
8798
|
+
height: drawHeight
|
|
8799
|
+
};
|
|
8800
|
+
}
|
|
8801
|
+
/**
|
|
8802
|
+
* Convenience: compute draw rect from NodeImageOptions.
|
|
8803
|
+
*/
|
|
8804
|
+
function computeIconDrawRectFromOptions(bounds, opts, imageSize, inset) {
|
|
8805
|
+
return computeIconDrawRect(bounds, opts, imageSize, inset);
|
|
8806
|
+
}
|
|
8282
8807
|
//#endregion
|
|
8283
8808
|
//#region src/elements/NodeImage.ts
|
|
8284
8809
|
function isCornerPlacement(placement) {
|
|
8285
8810
|
return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
|
|
8286
8811
|
}
|
|
8287
|
-
var svgTextCache = new LRUCache(100);
|
|
8288
8812
|
/**
|
|
8289
8813
|
* Image helper for node rendering (supports images and inline SVG).
|
|
8290
8814
|
*/
|
|
@@ -8327,40 +8851,15 @@ var NodeImage = class {
|
|
|
8327
8851
|
render(ctx, bounds) {
|
|
8328
8852
|
if (!this._loaded) return;
|
|
8329
8853
|
const ins = this.inset;
|
|
8330
|
-
const fit = this._options.fit ?? "none";
|
|
8331
|
-
const scaleWithBounds = this._options.scaleWithBounds ?? false;
|
|
8332
8854
|
const opacity = this._options.opacity ?? 1;
|
|
8333
|
-
const
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
};
|
|
8339
|
-
const availableWidth = Math.max(0, innerBounds.width);
|
|
8340
|
-
const availableHeight = Math.max(0, innerBounds.height);
|
|
8341
|
-
let drawWidth = this._options.width ?? this._naturalWidth;
|
|
8342
|
-
let drawHeight = this._options.height ?? this._naturalHeight;
|
|
8343
|
-
if (scaleWithBounds) {
|
|
8344
|
-
if (fit === "contain" || fit === "cover") {
|
|
8345
|
-
const scaleX = availableWidth / this._naturalWidth;
|
|
8346
|
-
const scaleY = availableHeight / this._naturalHeight;
|
|
8347
|
-
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
8348
|
-
drawWidth = this._naturalWidth * scale;
|
|
8349
|
-
drawHeight = this._naturalHeight * scale;
|
|
8350
|
-
} else if (fit === "stretch") {
|
|
8351
|
-
drawWidth = availableWidth;
|
|
8352
|
-
drawHeight = availableHeight;
|
|
8353
|
-
}
|
|
8354
|
-
}
|
|
8355
|
-
const maxWidth = Math.max(0, availableWidth);
|
|
8356
|
-
const maxHeight = Math.max(0, availableHeight);
|
|
8357
|
-
drawWidth = Math.min(drawWidth, maxWidth);
|
|
8358
|
-
drawHeight = Math.min(drawHeight, maxHeight);
|
|
8359
|
-
const x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
|
|
8360
|
-
const y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
8855
|
+
const drawRect = computeIconDrawRect(bounds, this._options, {
|
|
8856
|
+
width: this._naturalWidth,
|
|
8857
|
+
height: this._naturalHeight
|
|
8858
|
+
}, ins);
|
|
8859
|
+
if (drawRect.width <= 0 || drawRect.height <= 0) return;
|
|
8361
8860
|
ctx.save();
|
|
8362
8861
|
ctx.globalAlpha = opacity;
|
|
8363
|
-
ctx.drawImage(this._image, x, y,
|
|
8862
|
+
ctx.drawImage(this._image, drawRect.x, drawRect.y, drawRect.width, drawRect.height);
|
|
8364
8863
|
ctx.restore();
|
|
8365
8864
|
}
|
|
8366
8865
|
getSize() {
|
|
@@ -8379,27 +8878,20 @@ var NodeImage = class {
|
|
|
8379
8878
|
if (source instanceof HTMLImageElement) return;
|
|
8380
8879
|
const version = ++this._sourceVersion;
|
|
8381
8880
|
if (isSvgMarkup(source)) {
|
|
8382
|
-
const
|
|
8881
|
+
const resolvedSource = resolveTintedSvgDataUrlSync(source, {
|
|
8882
|
+
strokeColor: this._options.strokeColor,
|
|
8883
|
+
fillColor: this._options.fillColor
|
|
8884
|
+
});
|
|
8383
8885
|
if (version !== this._sourceVersion) return;
|
|
8384
|
-
this._image.src =
|
|
8886
|
+
this._image.src = resolvedSource ?? source;
|
|
8385
8887
|
return;
|
|
8386
8888
|
}
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
svgTextCache.set(source, svgPromise);
|
|
8392
|
-
}
|
|
8393
|
-
const svgText = await svgPromise;
|
|
8394
|
-
if (version !== this._sourceVersion) return;
|
|
8395
|
-
if (svgText) {
|
|
8396
|
-
const tinted = tintSvg(svgText, this._options.strokeColor, this._options.fillColor);
|
|
8397
|
-
this._image.src = svgToDataUrl(tinted);
|
|
8398
|
-
return;
|
|
8399
|
-
}
|
|
8400
|
-
}
|
|
8889
|
+
const resolvedSource = await resolveTintedSvgDataUrl(source, {
|
|
8890
|
+
strokeColor: this._options.strokeColor,
|
|
8891
|
+
fillColor: this._options.fillColor
|
|
8892
|
+
});
|
|
8401
8893
|
if (version !== this._sourceVersion) return;
|
|
8402
|
-
this._image.src = source;
|
|
8894
|
+
this._image.src = resolvedSource ?? source;
|
|
8403
8895
|
}
|
|
8404
8896
|
attachHandlers() {
|
|
8405
8897
|
if (this._image.complete && this._image.naturalWidth > 0) {
|
|
@@ -9163,65 +9655,13 @@ var Node = class extends Element {
|
|
|
9163
9655
|
if (!this._icon) return;
|
|
9164
9656
|
const { width, height } = this._icon.getSize();
|
|
9165
9657
|
if (width <= 0 || height <= 0) return;
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
};
|
|
9658
|
+
return getIconBoxSize({
|
|
9659
|
+
width,
|
|
9660
|
+
height
|
|
9661
|
+
}, this._icon.inset);
|
|
9171
9662
|
}
|
|
9172
9663
|
getIconBounds(bounds, iconBoxSize, placement) {
|
|
9173
|
-
|
|
9174
|
-
switch (placement) {
|
|
9175
|
-
case "top": return {
|
|
9176
|
-
x: bounds.x,
|
|
9177
|
-
y: bounds.y,
|
|
9178
|
-
width: bounds.width,
|
|
9179
|
-
height: iconBoxSize.height
|
|
9180
|
-
};
|
|
9181
|
-
case "bottom": return {
|
|
9182
|
-
x: bounds.x,
|
|
9183
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
9184
|
-
width: bounds.width,
|
|
9185
|
-
height: iconBoxSize.height
|
|
9186
|
-
};
|
|
9187
|
-
case "left": return {
|
|
9188
|
-
x: bounds.x,
|
|
9189
|
-
y: bounds.y,
|
|
9190
|
-
width: iconBoxSize.width,
|
|
9191
|
-
height: bounds.height
|
|
9192
|
-
};
|
|
9193
|
-
case "right": return {
|
|
9194
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
9195
|
-
y: bounds.y,
|
|
9196
|
-
width: iconBoxSize.width,
|
|
9197
|
-
height: bounds.height
|
|
9198
|
-
};
|
|
9199
|
-
case "top-left": return {
|
|
9200
|
-
x: bounds.x + ins,
|
|
9201
|
-
y: bounds.y + ins,
|
|
9202
|
-
width: iconBoxSize.width,
|
|
9203
|
-
height: iconBoxSize.height
|
|
9204
|
-
};
|
|
9205
|
-
case "top-right": return {
|
|
9206
|
-
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9207
|
-
y: bounds.y + ins,
|
|
9208
|
-
width: iconBoxSize.width,
|
|
9209
|
-
height: iconBoxSize.height
|
|
9210
|
-
};
|
|
9211
|
-
case "bottom-left": return {
|
|
9212
|
-
x: bounds.x + ins,
|
|
9213
|
-
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9214
|
-
width: iconBoxSize.width,
|
|
9215
|
-
height: iconBoxSize.height
|
|
9216
|
-
};
|
|
9217
|
-
case "bottom-right": return {
|
|
9218
|
-
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9219
|
-
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9220
|
-
width: iconBoxSize.width,
|
|
9221
|
-
height: iconBoxSize.height
|
|
9222
|
-
};
|
|
9223
|
-
default: return bounds;
|
|
9224
|
-
}
|
|
9664
|
+
return getIconBounds(bounds, iconBoxSize, placement, this._icon?.inset ?? 0);
|
|
9225
9665
|
}
|
|
9226
9666
|
normalizeAnchorPoints(config) {
|
|
9227
9667
|
const normalize = (value) => {
|
|
@@ -10122,6 +10562,23 @@ var ShapeFactories = {
|
|
|
10122
10562
|
}
|
|
10123
10563
|
};
|
|
10124
10564
|
//#endregion
|
|
10565
|
+
//#region src/elements/composite/CompositeComponentBase.ts
|
|
10566
|
+
/**
|
|
10567
|
+
* Shared change notification and default bounds hit testing for composite components.
|
|
10568
|
+
*/
|
|
10569
|
+
var CompositeComponentBase = class {
|
|
10570
|
+
setOnChange(cb) {
|
|
10571
|
+
this._onChange = cb;
|
|
10572
|
+
}
|
|
10573
|
+
hitTest(point, bounds) {
|
|
10574
|
+
if (this.style.visible === false) return null;
|
|
10575
|
+
return point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height ? this : null;
|
|
10576
|
+
}
|
|
10577
|
+
markChanged() {
|
|
10578
|
+
this._onChange?.();
|
|
10579
|
+
}
|
|
10580
|
+
};
|
|
10581
|
+
//#endregion
|
|
10125
10582
|
//#region src/elements/composite/CText.ts
|
|
10126
10583
|
var DEFAULT_FONT_FAMILY = "sans-serif";
|
|
10127
10584
|
var DEFAULT_FONT_SIZE = 14;
|
|
@@ -10131,8 +10588,9 @@ var DEFAULT_COLOR$1 = "#000000";
|
|
|
10131
10588
|
* Lightweight text component for CompositeNode.
|
|
10132
10589
|
* Renders directly via canvas ctx without delegating to TextLabel.
|
|
10133
10590
|
*/
|
|
10134
|
-
var CText = class {
|
|
10591
|
+
var CText = class extends CompositeComponentBase {
|
|
10135
10592
|
constructor(options) {
|
|
10593
|
+
super();
|
|
10136
10594
|
this.type = "text";
|
|
10137
10595
|
this._cachedLines = null;
|
|
10138
10596
|
this.id = options.id;
|
|
@@ -10158,7 +10616,7 @@ var CText = class {
|
|
|
10158
10616
|
if (this._text !== value) {
|
|
10159
10617
|
this._text = value;
|
|
10160
10618
|
this._cachedLines = null;
|
|
10161
|
-
this.
|
|
10619
|
+
this.markChanged();
|
|
10162
10620
|
}
|
|
10163
10621
|
}
|
|
10164
10622
|
get fontFamily() {
|
|
@@ -10167,7 +10625,7 @@ var CText = class {
|
|
|
10167
10625
|
set fontFamily(value) {
|
|
10168
10626
|
if (this._fontFamily !== value) {
|
|
10169
10627
|
this._fontFamily = value;
|
|
10170
|
-
this.
|
|
10628
|
+
this.markChanged();
|
|
10171
10629
|
}
|
|
10172
10630
|
}
|
|
10173
10631
|
get fontWeight() {
|
|
@@ -10176,7 +10634,7 @@ var CText = class {
|
|
|
10176
10634
|
set fontWeight(value) {
|
|
10177
10635
|
if (this._fontWeight !== value) {
|
|
10178
10636
|
this._fontWeight = value;
|
|
10179
|
-
this.
|
|
10637
|
+
this.markChanged();
|
|
10180
10638
|
}
|
|
10181
10639
|
}
|
|
10182
10640
|
get fontStyle() {
|
|
@@ -10185,7 +10643,7 @@ var CText = class {
|
|
|
10185
10643
|
set fontStyle(value) {
|
|
10186
10644
|
if (this._fontStyle !== value) {
|
|
10187
10645
|
this._fontStyle = value;
|
|
10188
|
-
this.
|
|
10646
|
+
this.markChanged();
|
|
10189
10647
|
}
|
|
10190
10648
|
}
|
|
10191
10649
|
get fontSize() {
|
|
@@ -10194,7 +10652,7 @@ var CText = class {
|
|
|
10194
10652
|
set fontSize(value) {
|
|
10195
10653
|
if (this._fontSize !== value) {
|
|
10196
10654
|
this._fontSize = value;
|
|
10197
|
-
this.
|
|
10655
|
+
this.markChanged();
|
|
10198
10656
|
}
|
|
10199
10657
|
}
|
|
10200
10658
|
get color() {
|
|
@@ -10203,7 +10661,7 @@ var CText = class {
|
|
|
10203
10661
|
set color(value) {
|
|
10204
10662
|
if (this._color !== value) {
|
|
10205
10663
|
this._color = value;
|
|
10206
|
-
this.
|
|
10664
|
+
this.markChanged();
|
|
10207
10665
|
}
|
|
10208
10666
|
}
|
|
10209
10667
|
get align() {
|
|
@@ -10212,7 +10670,7 @@ var CText = class {
|
|
|
10212
10670
|
set align(value) {
|
|
10213
10671
|
if (this._align !== value) {
|
|
10214
10672
|
this._align = value;
|
|
10215
|
-
this.
|
|
10673
|
+
this.markChanged();
|
|
10216
10674
|
}
|
|
10217
10675
|
}
|
|
10218
10676
|
get verticalAlign() {
|
|
@@ -10221,7 +10679,7 @@ var CText = class {
|
|
|
10221
10679
|
set verticalAlign(value) {
|
|
10222
10680
|
if (this._verticalAlign !== value) {
|
|
10223
10681
|
this._verticalAlign = value;
|
|
10224
|
-
this.
|
|
10682
|
+
this.markChanged();
|
|
10225
10683
|
}
|
|
10226
10684
|
}
|
|
10227
10685
|
get maxLines() {
|
|
@@ -10230,7 +10688,7 @@ var CText = class {
|
|
|
10230
10688
|
set maxLines(value) {
|
|
10231
10689
|
if (this._maxLines !== value) {
|
|
10232
10690
|
this._maxLines = value;
|
|
10233
|
-
this.
|
|
10691
|
+
this.markChanged();
|
|
10234
10692
|
}
|
|
10235
10693
|
}
|
|
10236
10694
|
get lineHeight() {
|
|
@@ -10248,12 +10706,9 @@ var CText = class {
|
|
|
10248
10706
|
set rotation(value) {
|
|
10249
10707
|
if (this._rotation !== value) {
|
|
10250
10708
|
this._rotation = value;
|
|
10251
|
-
this.
|
|
10709
|
+
this.markChanged();
|
|
10252
10710
|
}
|
|
10253
10711
|
}
|
|
10254
|
-
setOnChange(cb) {
|
|
10255
|
-
this._onChange = cb;
|
|
10256
|
-
}
|
|
10257
10712
|
getFont() {
|
|
10258
10713
|
return `${this._fontStyle} ${this._fontWeight} ${this._fontSize}px ${this._fontFamily}`;
|
|
10259
10714
|
}
|
|
@@ -10265,34 +10720,16 @@ var CText = class {
|
|
|
10265
10720
|
*/
|
|
10266
10721
|
wrapText(ctx, maxWidth) {
|
|
10267
10722
|
ctx.font = this.getFont();
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
10273
|
-
if (ctx.measureText(testLine).width > maxWidth && currentLine) {
|
|
10274
|
-
lines.push(currentLine);
|
|
10275
|
-
currentLine = word;
|
|
10276
|
-
} else currentLine = testLine;
|
|
10277
|
-
}
|
|
10278
|
-
if (currentLine) lines.push(currentLine);
|
|
10279
|
-
if (this._maxLines !== void 0 && lines.length > this._maxLines) {
|
|
10280
|
-
const truncated = lines.slice(0, this._maxLines);
|
|
10281
|
-
const lastLine = truncated[truncated.length - 1];
|
|
10282
|
-
if (lastLine !== void 0) truncated[truncated.length - 1] = lastLine + "…";
|
|
10283
|
-
return truncated;
|
|
10284
|
-
}
|
|
10285
|
-
return lines;
|
|
10723
|
+
return wrapMeasuredText(ctx, this._text, maxWidth, {
|
|
10724
|
+
maxLines: this._maxLines,
|
|
10725
|
+
appendEllipsis: true
|
|
10726
|
+
});
|
|
10286
10727
|
}
|
|
10287
10728
|
measure(ctx) {
|
|
10288
10729
|
ctx.font = this.getFont();
|
|
10289
10730
|
const lineHeightPx = this.getLineHeightPx();
|
|
10290
10731
|
const rawLines = this._text.split("\n");
|
|
10291
|
-
|
|
10292
|
-
for (const line of rawLines) {
|
|
10293
|
-
const metrics = ctx.measureText(line);
|
|
10294
|
-
if (metrics.width > maxWidth) maxWidth = metrics.width;
|
|
10295
|
-
}
|
|
10732
|
+
const maxWidth = measureTextLines(ctx, rawLines);
|
|
10296
10733
|
let lineCount = rawLines.length;
|
|
10297
10734
|
if (this._maxLines !== void 0 && lineCount > this._maxLines) lineCount = this._maxLines;
|
|
10298
10735
|
const width = maxWidth;
|
|
@@ -10363,13 +10800,8 @@ var CText = class {
|
|
|
10363
10800
|
const offsetX = xBase - (textBounds.x + textBounds.width / 2);
|
|
10364
10801
|
const offsetY = yStart - (textBounds.y + textBounds.height / 2);
|
|
10365
10802
|
for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], offsetX, offsetY + i * lineHeightPx);
|
|
10366
|
-
} else for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
|
|
10367
|
-
ctx.restore();
|
|
10368
|
-
}
|
|
10369
|
-
hitTest(point, bounds) {
|
|
10370
|
-
if (this.style.visible === false) return null;
|
|
10371
|
-
if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) return this;
|
|
10372
|
-
return null;
|
|
10803
|
+
} else for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
|
|
10804
|
+
ctx.restore();
|
|
10373
10805
|
}
|
|
10374
10806
|
serialize() {
|
|
10375
10807
|
const data = {
|
|
@@ -10442,12 +10874,12 @@ function escapeXml(str) {
|
|
|
10442
10874
|
//#endregion
|
|
10443
10875
|
//#region src/elements/composite/CIcon.ts
|
|
10444
10876
|
var DEFAULT_ICON_SIZE = 24;
|
|
10445
|
-
var svgFetchCache = new LRUCache(100);
|
|
10446
10877
|
/**
|
|
10447
10878
|
* Lightweight SVG/image icon component for CompositeNode.
|
|
10448
10879
|
*/
|
|
10449
|
-
var CIcon = class {
|
|
10880
|
+
var CIcon = class extends CompositeComponentBase {
|
|
10450
10881
|
constructor(options) {
|
|
10882
|
+
super();
|
|
10451
10883
|
this.type = "icon";
|
|
10452
10884
|
this._loaded = false;
|
|
10453
10885
|
this.id = options.id;
|
|
@@ -10463,10 +10895,10 @@ var CIcon = class {
|
|
|
10463
10895
|
this._image = new Image();
|
|
10464
10896
|
this._image.onload = () => {
|
|
10465
10897
|
this._loaded = true;
|
|
10466
|
-
this.
|
|
10898
|
+
this.markChanged();
|
|
10467
10899
|
};
|
|
10468
10900
|
this._image.onerror = () => {
|
|
10469
|
-
this.
|
|
10901
|
+
this.markChanged();
|
|
10470
10902
|
};
|
|
10471
10903
|
this.loadSource(this._source);
|
|
10472
10904
|
}
|
|
@@ -10478,7 +10910,7 @@ var CIcon = class {
|
|
|
10478
10910
|
this._source = value;
|
|
10479
10911
|
this._loaded = false;
|
|
10480
10912
|
this.loadSource(value);
|
|
10481
|
-
this.
|
|
10913
|
+
this.markChanged();
|
|
10482
10914
|
}
|
|
10483
10915
|
}
|
|
10484
10916
|
get width() {
|
|
@@ -10487,7 +10919,7 @@ var CIcon = class {
|
|
|
10487
10919
|
set width(value) {
|
|
10488
10920
|
if (this._width !== value) {
|
|
10489
10921
|
this._width = value;
|
|
10490
|
-
this.
|
|
10922
|
+
this.markChanged();
|
|
10491
10923
|
}
|
|
10492
10924
|
}
|
|
10493
10925
|
get height() {
|
|
@@ -10496,7 +10928,7 @@ var CIcon = class {
|
|
|
10496
10928
|
set height(value) {
|
|
10497
10929
|
if (this._height !== value) {
|
|
10498
10930
|
this._height = value;
|
|
10499
|
-
this.
|
|
10931
|
+
this.markChanged();
|
|
10500
10932
|
}
|
|
10501
10933
|
}
|
|
10502
10934
|
get backgroundColor() {
|
|
@@ -10505,7 +10937,7 @@ var CIcon = class {
|
|
|
10505
10937
|
set backgroundColor(value) {
|
|
10506
10938
|
if (this._backgroundColor !== value) {
|
|
10507
10939
|
this._backgroundColor = value;
|
|
10508
|
-
this.
|
|
10940
|
+
this.markChanged();
|
|
10509
10941
|
}
|
|
10510
10942
|
}
|
|
10511
10943
|
get fillColor() {
|
|
@@ -10515,7 +10947,7 @@ var CIcon = class {
|
|
|
10515
10947
|
if (this._fillColor !== value) {
|
|
10516
10948
|
this._fillColor = value;
|
|
10517
10949
|
this.loadSource(this._source);
|
|
10518
|
-
this.
|
|
10950
|
+
this.markChanged();
|
|
10519
10951
|
}
|
|
10520
10952
|
}
|
|
10521
10953
|
get loaded() {
|
|
@@ -10527,31 +10959,12 @@ var CIcon = class {
|
|
|
10527
10959
|
set bindsNotationIcon(value) {
|
|
10528
10960
|
if (this._bindsNotationIcon !== value) {
|
|
10529
10961
|
this._bindsNotationIcon = value;
|
|
10530
|
-
this.
|
|
10962
|
+
this.markChanged();
|
|
10531
10963
|
}
|
|
10532
10964
|
}
|
|
10533
|
-
setOnChange(cb) {
|
|
10534
|
-
this._onChange = cb;
|
|
10535
|
-
}
|
|
10536
10965
|
async loadSource(source) {
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
this._image.src = svgToDataUrl(tinted);
|
|
10540
|
-
} else if (isSvgUrl(source) && this._fillColor) try {
|
|
10541
|
-
const tinted = tintSvg(await this.fetchSvg(source), void 0, this._fillColor);
|
|
10542
|
-
this._image.src = svgToDataUrl(tinted);
|
|
10543
|
-
} catch {
|
|
10544
|
-
this._image.src = source;
|
|
10545
|
-
}
|
|
10546
|
-
else this._image.src = source;
|
|
10547
|
-
}
|
|
10548
|
-
async fetchSvg(url) {
|
|
10549
|
-
let promise = svgFetchCache.get(url);
|
|
10550
|
-
if (!promise) {
|
|
10551
|
-
promise = fetch(url).then((r) => r.text());
|
|
10552
|
-
svgFetchCache.set(url, promise);
|
|
10553
|
-
}
|
|
10554
|
-
return promise;
|
|
10966
|
+
const resolvedSource = await resolveTintedSvgDataUrl(source, { fillColor: this._fillColor });
|
|
10967
|
+
this._image.src = resolvedSource ?? source;
|
|
10555
10968
|
}
|
|
10556
10969
|
measure(_ctx) {
|
|
10557
10970
|
return {
|
|
@@ -10578,11 +10991,6 @@ var CIcon = class {
|
|
|
10578
10991
|
}
|
|
10579
10992
|
ctx.restore();
|
|
10580
10993
|
}
|
|
10581
|
-
hitTest(point, bounds) {
|
|
10582
|
-
if (this.style.visible === false) return null;
|
|
10583
|
-
if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) return this;
|
|
10584
|
-
return null;
|
|
10585
|
-
}
|
|
10586
10994
|
serialize() {
|
|
10587
10995
|
const data = {
|
|
10588
10996
|
type: "icon",
|
|
@@ -10605,7 +11013,8 @@ var CIcon = class {
|
|
|
10605
11013
|
const drawHeight = Math.min(this._height, bounds.height);
|
|
10606
11014
|
const dx = bounds.x + (bounds.width - drawWidth) / 2;
|
|
10607
11015
|
const dy = bounds.y + (bounds.height - drawHeight) / 2;
|
|
10608
|
-
|
|
11016
|
+
const source = resolveTintedSvgDataUrlSync(this._source, { fillColor: this._fillColor }) ?? this._source;
|
|
11017
|
+
svg += `<image href="${escapeXmlAttr(source)}" x="${dx}" y="${dy}" width="${drawWidth}" height="${drawHeight}" />`;
|
|
10609
11018
|
return svg;
|
|
10610
11019
|
}
|
|
10611
11020
|
};
|
|
@@ -10622,8 +11031,9 @@ var DEFAULT_THICKNESS = 1;
|
|
|
10622
11031
|
* - In a column container → horizontal line
|
|
10623
11032
|
* - In a row container → vertical line
|
|
10624
11033
|
*/
|
|
10625
|
-
var CDivider = class {
|
|
11034
|
+
var CDivider = class extends CompositeComponentBase {
|
|
10626
11035
|
constructor(options = {}) {
|
|
11036
|
+
super();
|
|
10627
11037
|
this.type = "divider";
|
|
10628
11038
|
this.id = options.id;
|
|
10629
11039
|
this._color = options.color ?? DEFAULT_COLOR;
|
|
@@ -10636,7 +11046,7 @@ var CDivider = class {
|
|
|
10636
11046
|
set color(value) {
|
|
10637
11047
|
if (this._color !== value) {
|
|
10638
11048
|
this._color = value;
|
|
10639
|
-
this.
|
|
11049
|
+
this.markChanged();
|
|
10640
11050
|
}
|
|
10641
11051
|
}
|
|
10642
11052
|
get thickness() {
|
|
@@ -10645,12 +11055,9 @@ var CDivider = class {
|
|
|
10645
11055
|
set thickness(value) {
|
|
10646
11056
|
if (this._thickness !== value) {
|
|
10647
11057
|
this._thickness = value;
|
|
10648
|
-
this.
|
|
11058
|
+
this.markChanged();
|
|
10649
11059
|
}
|
|
10650
11060
|
}
|
|
10651
|
-
setOnChange(cb) {
|
|
10652
|
-
this._onChange = cb;
|
|
10653
|
-
}
|
|
10654
11061
|
measure(_ctx) {
|
|
10655
11062
|
return {
|
|
10656
11063
|
width: this._thickness,
|
|
@@ -10676,9 +11083,6 @@ var CDivider = class {
|
|
|
10676
11083
|
ctx.stroke();
|
|
10677
11084
|
ctx.restore();
|
|
10678
11085
|
}
|
|
10679
|
-
hitTest(_point, _bounds) {
|
|
10680
|
-
return null;
|
|
10681
|
-
}
|
|
10682
11086
|
serialize() {
|
|
10683
11087
|
const data = { type: "divider" };
|
|
10684
11088
|
if (this.id !== void 0) data.id = this.id;
|
|
@@ -10893,12 +11297,19 @@ function flexLayout(container, config, children) {
|
|
|
10893
11297
|
}
|
|
10894
11298
|
//#endregion
|
|
10895
11299
|
//#region src/elements/composite/CContainer.ts
|
|
11300
|
+
function isCContainer(component) {
|
|
11301
|
+
return component.type === "container";
|
|
11302
|
+
}
|
|
11303
|
+
function isCShape(component) {
|
|
11304
|
+
return component.type === "shape";
|
|
11305
|
+
}
|
|
10896
11306
|
/**
|
|
10897
11307
|
* Flex container component for CompositeNode.
|
|
10898
11308
|
* Lays out children using a flexbox-like algorithm.
|
|
10899
11309
|
*/
|
|
10900
|
-
var CContainer = class {
|
|
11310
|
+
var CContainer = class extends CompositeComponentBase {
|
|
10901
11311
|
constructor(options = {}) {
|
|
11312
|
+
super();
|
|
10902
11313
|
this.type = "container";
|
|
10903
11314
|
this._cachedBounds = null;
|
|
10904
11315
|
this._cachedContainerBounds = null;
|
|
@@ -10948,12 +11359,9 @@ var CContainer = class {
|
|
|
10948
11359
|
this._children.splice(index, 0, child);
|
|
10949
11360
|
this.handleChildChange();
|
|
10950
11361
|
}
|
|
10951
|
-
setOnChange(cb) {
|
|
10952
|
-
this._onChange = cb;
|
|
10953
|
-
}
|
|
10954
11362
|
handleChildChange() {
|
|
10955
11363
|
this._cachedBounds = null;
|
|
10956
|
-
this.
|
|
11364
|
+
this.markChanged();
|
|
10957
11365
|
}
|
|
10958
11366
|
getFlexConfig() {
|
|
10959
11367
|
return {
|
|
@@ -11047,11 +11455,11 @@ var CContainer = class {
|
|
|
11047
11455
|
findById(id) {
|
|
11048
11456
|
for (const child of this._children) {
|
|
11049
11457
|
if (child.id === id) return child;
|
|
11050
|
-
if (child
|
|
11458
|
+
if (isCContainer(child)) {
|
|
11051
11459
|
const found = child.findById(id);
|
|
11052
11460
|
if (found) return found;
|
|
11053
11461
|
}
|
|
11054
|
-
if (child
|
|
11462
|
+
if (isCShape(child)) {
|
|
11055
11463
|
const content = child.content;
|
|
11056
11464
|
if (content) {
|
|
11057
11465
|
const found = content.findById(id);
|
|
@@ -11107,8 +11515,9 @@ var CContainer = class {
|
|
|
11107
11515
|
* A bordered box component for CompositeNode.
|
|
11108
11516
|
* Can contain a CContainer for recursive composition (e.g. UML class sections).
|
|
11109
11517
|
*/
|
|
11110
|
-
var CShape = class {
|
|
11518
|
+
var CShape = class extends CompositeComponentBase {
|
|
11111
11519
|
constructor(options = {}) {
|
|
11520
|
+
super();
|
|
11112
11521
|
this.type = "shape";
|
|
11113
11522
|
this.id = options.id;
|
|
11114
11523
|
this._borderColor = options.borderColor;
|
|
@@ -11119,7 +11528,7 @@ var CShape = class {
|
|
|
11119
11528
|
this.onClick = options.onClick;
|
|
11120
11529
|
this._content = options.content;
|
|
11121
11530
|
this.style = options.style ?? {};
|
|
11122
|
-
if (this._content) this._content.setOnChange(() => this.
|
|
11531
|
+
if (this._content) this._content.setOnChange(() => this.markChanged());
|
|
11123
11532
|
}
|
|
11124
11533
|
get borderColor() {
|
|
11125
11534
|
return this._borderColor;
|
|
@@ -11127,7 +11536,7 @@ var CShape = class {
|
|
|
11127
11536
|
set borderColor(value) {
|
|
11128
11537
|
if (this._borderColor !== value) {
|
|
11129
11538
|
this._borderColor = value;
|
|
11130
|
-
this.
|
|
11539
|
+
this.markChanged();
|
|
11131
11540
|
}
|
|
11132
11541
|
}
|
|
11133
11542
|
get borderWidth() {
|
|
@@ -11136,7 +11545,7 @@ var CShape = class {
|
|
|
11136
11545
|
set borderWidth(value) {
|
|
11137
11546
|
if (this._borderWidth !== value) {
|
|
11138
11547
|
this._borderWidth = value;
|
|
11139
|
-
this.
|
|
11548
|
+
this.markChanged();
|
|
11140
11549
|
}
|
|
11141
11550
|
}
|
|
11142
11551
|
get backgroundColor() {
|
|
@@ -11145,7 +11554,7 @@ var CShape = class {
|
|
|
11145
11554
|
set backgroundColor(value) {
|
|
11146
11555
|
if (this._backgroundColor !== value) {
|
|
11147
11556
|
this._backgroundColor = value;
|
|
11148
|
-
this.
|
|
11557
|
+
this.markChanged();
|
|
11149
11558
|
}
|
|
11150
11559
|
}
|
|
11151
11560
|
get cornerRadius() {
|
|
@@ -11157,9 +11566,6 @@ var CShape = class {
|
|
|
11157
11566
|
get content() {
|
|
11158
11567
|
return this._content;
|
|
11159
11568
|
}
|
|
11160
|
-
setOnChange(cb) {
|
|
11161
|
-
this._onChange = cb;
|
|
11162
|
-
}
|
|
11163
11569
|
measure(ctx) {
|
|
11164
11570
|
const pad = normalizeSides(this._padding);
|
|
11165
11571
|
const bw = this._borderWidth;
|
|
@@ -11210,8 +11616,7 @@ var CShape = class {
|
|
|
11210
11616
|
ctx.restore();
|
|
11211
11617
|
}
|
|
11212
11618
|
hitTest(point, bounds) {
|
|
11213
|
-
if (
|
|
11214
|
-
if (point.x < bounds.x || point.x > bounds.x + bounds.width || point.y < bounds.y || point.y > bounds.y + bounds.height) return null;
|
|
11619
|
+
if (!super.hitTest(point, bounds)) return null;
|
|
11215
11620
|
if (this._content) {
|
|
11216
11621
|
const pad = normalizeSides(this._padding);
|
|
11217
11622
|
const bw = this._borderWidth;
|
|
@@ -12028,9 +12433,14 @@ var Serializer = class {
|
|
|
12028
12433
|
if (!Array.isArray(data.nodes)) throw new SerializerValidationError("Invalid or missing nodes array");
|
|
12029
12434
|
if (!Array.isArray(data.edges)) throw new SerializerValidationError("Invalid or missing edges array");
|
|
12030
12435
|
const nodeIds = new Set(data.nodes.map((n) => n.id));
|
|
12436
|
+
const edgeIds = new Set(data.edges.map((e) => e.id));
|
|
12031
12437
|
for (const edge of data.edges) {
|
|
12032
|
-
if (
|
|
12033
|
-
|
|
12438
|
+
if (edge.from.edgeId) {
|
|
12439
|
+
if (!edgeIds.has(edge.from.edgeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent host edge "${edge.from.edgeId}"`);
|
|
12440
|
+
} else if (!edge.from.nodeId || !nodeIds.has(edge.from.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent source node "${edge.from.nodeId}"`);
|
|
12441
|
+
if (edge.to.edgeId) {
|
|
12442
|
+
if (!edgeIds.has(edge.to.edgeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent host edge "${edge.to.edgeId}"`);
|
|
12443
|
+
} else if (!edge.to.nodeId || !nodeIds.has(edge.to.nodeId)) throw new SerializerValidationError(`Edge "${edge.id}" references non-existent target node "${edge.to.nodeId}"`);
|
|
12034
12444
|
}
|
|
12035
12445
|
}
|
|
12036
12446
|
/**
|
|
@@ -12299,174 +12709,6 @@ var ImageExporter = class {
|
|
|
12299
12709
|
}
|
|
12300
12710
|
};
|
|
12301
12711
|
//#endregion
|
|
12302
|
-
//#region src/utils/markers.ts
|
|
12303
|
-
/**
|
|
12304
|
-
* Calculate marker points based on edge path
|
|
12305
|
-
*/
|
|
12306
|
-
function calculateMarkerPoints(path, position, type) {
|
|
12307
|
-
if (path.length < 2) return null;
|
|
12308
|
-
if (type === "bezier" && path.length >= 4) {
|
|
12309
|
-
const epsilon = .001;
|
|
12310
|
-
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
12311
|
-
if (position === "end") {
|
|
12312
|
-
const endIndex = path.length - 1;
|
|
12313
|
-
const endPoint = path[endIndex];
|
|
12314
|
-
let from = path[endIndex - 1];
|
|
12315
|
-
if (isSame(from, endPoint)) {
|
|
12316
|
-
from = path[endIndex - 2];
|
|
12317
|
-
if (isSame(from, endPoint)) from = path[0];
|
|
12318
|
-
}
|
|
12319
|
-
return {
|
|
12320
|
-
from,
|
|
12321
|
-
to: endPoint
|
|
12322
|
-
};
|
|
12323
|
-
}
|
|
12324
|
-
const start = path[0];
|
|
12325
|
-
let next = path[1];
|
|
12326
|
-
if (isSame(next, start)) {
|
|
12327
|
-
next = path[2];
|
|
12328
|
-
if (isSame(next, start)) next = path[path.length - 1];
|
|
12329
|
-
}
|
|
12330
|
-
return {
|
|
12331
|
-
from: next,
|
|
12332
|
-
to: start
|
|
12333
|
-
};
|
|
12334
|
-
}
|
|
12335
|
-
if (position === "end") return {
|
|
12336
|
-
from: path[path.length - 2],
|
|
12337
|
-
to: path[path.length - 1]
|
|
12338
|
-
};
|
|
12339
|
-
return {
|
|
12340
|
-
from: path[1],
|
|
12341
|
-
to: path[0]
|
|
12342
|
-
};
|
|
12343
|
-
}
|
|
12344
|
-
/**
|
|
12345
|
-
* Calculate arrow marker vertex points
|
|
12346
|
-
*/
|
|
12347
|
-
function calculateArrowMarkerPoints(to, angle, size) {
|
|
12348
|
-
return {
|
|
12349
|
-
tip: to,
|
|
12350
|
-
left: {
|
|
12351
|
-
x: to.x - size * Math.cos(angle - ARROW_ANGLE),
|
|
12352
|
-
y: to.y - size * Math.sin(angle - ARROW_ANGLE)
|
|
12353
|
-
},
|
|
12354
|
-
right: {
|
|
12355
|
-
x: to.x - size * Math.cos(angle + ARROW_ANGLE),
|
|
12356
|
-
y: to.y - size * Math.sin(angle + ARROW_ANGLE)
|
|
12357
|
-
}
|
|
12358
|
-
};
|
|
12359
|
-
}
|
|
12360
|
-
/**
|
|
12361
|
-
* Calculate diamond marker vertex points
|
|
12362
|
-
*/
|
|
12363
|
-
function calculateDiamondMarkerPoints(to, angle, size) {
|
|
12364
|
-
const halfLength = size / 2;
|
|
12365
|
-
const halfWidth = size * .3;
|
|
12366
|
-
const cos = Math.cos(angle);
|
|
12367
|
-
const sin = Math.sin(angle);
|
|
12368
|
-
return [
|
|
12369
|
-
{
|
|
12370
|
-
x: to.x,
|
|
12371
|
-
y: to.y
|
|
12372
|
-
},
|
|
12373
|
-
{
|
|
12374
|
-
x: to.x - halfLength * cos + halfWidth * sin,
|
|
12375
|
-
y: to.y - halfLength * sin - halfWidth * cos
|
|
12376
|
-
},
|
|
12377
|
-
{
|
|
12378
|
-
x: to.x - size * cos,
|
|
12379
|
-
y: to.y - size * sin
|
|
12380
|
-
},
|
|
12381
|
-
{
|
|
12382
|
-
x: to.x - halfLength * cos - halfWidth * sin,
|
|
12383
|
-
y: to.y - halfLength * sin + halfWidth * cos
|
|
12384
|
-
}
|
|
12385
|
-
];
|
|
12386
|
-
}
|
|
12387
|
-
/**
|
|
12388
|
-
* Calculate circle marker center
|
|
12389
|
-
*/
|
|
12390
|
-
function calculateCircleMarkerCenter(to, angle, size) {
|
|
12391
|
-
return {
|
|
12392
|
-
x: to.x - size * Math.cos(angle),
|
|
12393
|
-
y: to.y - size * Math.sin(angle)
|
|
12394
|
-
};
|
|
12395
|
-
}
|
|
12396
|
-
/**
|
|
12397
|
-
* Generate SVG path for arrow marker
|
|
12398
|
-
*/
|
|
12399
|
-
function generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12400
|
-
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
12401
|
-
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} L ${right.x} ${right.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12402
|
-
}
|
|
12403
|
-
/**
|
|
12404
|
-
* Generate SVG path for open arrow marker
|
|
12405
|
-
*/
|
|
12406
|
-
function generateSvgOpenArrowMarker(to, angle, size, stroke) {
|
|
12407
|
-
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
12408
|
-
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} M ${to.x} ${to.y} L ${right.x} ${right.y}" fill="none" stroke="${stroke}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
12409
|
-
}
|
|
12410
|
-
/**
|
|
12411
|
-
* Generate SVG path for diamond marker
|
|
12412
|
-
*/
|
|
12413
|
-
function generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12414
|
-
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
12415
|
-
return `<path d="M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y} L ${points[2].x} ${points[2].y} L ${points[3].x} ${points[3].y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12416
|
-
}
|
|
12417
|
-
/**
|
|
12418
|
-
* Generate SVG circle marker
|
|
12419
|
-
*/
|
|
12420
|
-
function generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12421
|
-
const { x: cx, y: cy } = calculateCircleMarkerCenter(to, angle, size);
|
|
12422
|
-
return `<circle cx="${cx}" cy="${cy}" r="${size}" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12423
|
-
}
|
|
12424
|
-
/**
|
|
12425
|
-
* Square marker: front edge centered at `to`, depth 2×size along the edge toward `from`.
|
|
12426
|
-
*/
|
|
12427
|
-
function generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12428
|
-
const cos = Math.cos(angle);
|
|
12429
|
-
const sin = Math.sin(angle);
|
|
12430
|
-
const px = -sin;
|
|
12431
|
-
const py = cos;
|
|
12432
|
-
const p1 = {
|
|
12433
|
-
x: to.x + size * px,
|
|
12434
|
-
y: to.y + size * py
|
|
12435
|
-
};
|
|
12436
|
-
const p2 = {
|
|
12437
|
-
x: to.x - size * px,
|
|
12438
|
-
y: to.y - size * py
|
|
12439
|
-
};
|
|
12440
|
-
const bx = 2 * size * cos;
|
|
12441
|
-
const by = 2 * size * sin;
|
|
12442
|
-
const p3 = {
|
|
12443
|
-
x: p2.x - bx,
|
|
12444
|
-
y: p2.y - by
|
|
12445
|
-
};
|
|
12446
|
-
const p4 = {
|
|
12447
|
-
x: p1.x - bx,
|
|
12448
|
-
y: p1.y - by
|
|
12449
|
-
};
|
|
12450
|
-
return `<path d="M ${p1.x} ${p1.y} L ${p2.x} ${p2.y} L ${p3.x} ${p3.y} L ${p4.x} ${p4.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12451
|
-
}
|
|
12452
|
-
/**
|
|
12453
|
-
* Generate SVG path for any marker type
|
|
12454
|
-
*/
|
|
12455
|
-
function generateSvgMarker(marker, from, to, edgeStroke) {
|
|
12456
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
12457
|
-
const size = marker.size ?? 12;
|
|
12458
|
-
const stroke = marker.strokeColor ?? edgeStroke;
|
|
12459
|
-
const fill = marker.fillColor ?? stroke;
|
|
12460
|
-
const fillOpacity = marker.fillOpacity ?? 1;
|
|
12461
|
-
switch (marker.type) {
|
|
12462
|
-
case "open": return generateSvgOpenArrowMarker(to, angle, size, stroke);
|
|
12463
|
-
case "diamond": return generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12464
|
-
case "circle": return generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12465
|
-
case "square": return generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12466
|
-
default: return generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12467
|
-
}
|
|
12468
|
-
}
|
|
12469
|
-
//#endregion
|
|
12470
12712
|
//#region src/utils/SvgExporter.ts
|
|
12471
12713
|
/**
|
|
12472
12714
|
* Exports diagram to SVG
|
|
@@ -12969,9 +13211,7 @@ var SvgExporter = class {
|
|
|
12969
13211
|
const iconSize = icon.getSize();
|
|
12970
13212
|
if (iconSize.width <= 0 || iconSize.height <= 0) return "";
|
|
12971
13213
|
const iconInset = icon.inset;
|
|
12972
|
-
const
|
|
12973
|
-
const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center", iconInset);
|
|
12974
|
-
const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize, iconInset);
|
|
13214
|
+
const drawRect = computeIconDrawRectFromOptions(getIconBounds(nodeBounds, getIconBoxSize(iconSize, iconInset), opts.placement ?? "center", iconInset), opts, iconSize, iconInset);
|
|
12975
13215
|
if (drawRect.width <= 0 || drawRect.height <= 0) return "";
|
|
12976
13216
|
const href = this.resolveIconHref(opts);
|
|
12977
13217
|
if (!href) return "";
|
|
@@ -12979,150 +13219,14 @@ var SvgExporter = class {
|
|
|
12979
13219
|
const escapedHref = this.escapeAttribute(href);
|
|
12980
13220
|
return `<image href="${escapedHref}" xlink:href="${escapedHref}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
|
|
12981
13221
|
}
|
|
12982
|
-
getIconBoxSize(imageSize, inset) {
|
|
12983
|
-
return {
|
|
12984
|
-
width: imageSize.width + inset * 2,
|
|
12985
|
-
height: imageSize.height + inset * 2
|
|
12986
|
-
};
|
|
12987
|
-
}
|
|
12988
|
-
getIconBounds(bounds, iconBoxSize, placement, inset) {
|
|
12989
|
-
switch (placement) {
|
|
12990
|
-
case "top": return {
|
|
12991
|
-
x: bounds.x,
|
|
12992
|
-
y: bounds.y,
|
|
12993
|
-
width: bounds.width,
|
|
12994
|
-
height: iconBoxSize.height
|
|
12995
|
-
};
|
|
12996
|
-
case "bottom": return {
|
|
12997
|
-
x: bounds.x,
|
|
12998
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
12999
|
-
width: bounds.width,
|
|
13000
|
-
height: iconBoxSize.height
|
|
13001
|
-
};
|
|
13002
|
-
case "left": return {
|
|
13003
|
-
x: bounds.x,
|
|
13004
|
-
y: bounds.y,
|
|
13005
|
-
width: iconBoxSize.width,
|
|
13006
|
-
height: bounds.height
|
|
13007
|
-
};
|
|
13008
|
-
case "right": return {
|
|
13009
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
13010
|
-
y: bounds.y,
|
|
13011
|
-
width: iconBoxSize.width,
|
|
13012
|
-
height: bounds.height
|
|
13013
|
-
};
|
|
13014
|
-
case "top-left": return {
|
|
13015
|
-
x: bounds.x + inset,
|
|
13016
|
-
y: bounds.y + inset,
|
|
13017
|
-
width: iconBoxSize.width,
|
|
13018
|
-
height: iconBoxSize.height
|
|
13019
|
-
};
|
|
13020
|
-
case "top-right": return {
|
|
13021
|
-
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
13022
|
-
y: bounds.y + inset,
|
|
13023
|
-
width: iconBoxSize.width,
|
|
13024
|
-
height: iconBoxSize.height
|
|
13025
|
-
};
|
|
13026
|
-
case "bottom-left": return {
|
|
13027
|
-
x: bounds.x + inset,
|
|
13028
|
-
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
13029
|
-
width: iconBoxSize.width,
|
|
13030
|
-
height: iconBoxSize.height
|
|
13031
|
-
};
|
|
13032
|
-
case "bottom-right": return {
|
|
13033
|
-
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
13034
|
-
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
13035
|
-
width: iconBoxSize.width,
|
|
13036
|
-
height: iconBoxSize.height
|
|
13037
|
-
};
|
|
13038
|
-
default: return bounds;
|
|
13039
|
-
}
|
|
13040
|
-
}
|
|
13041
|
-
getIconDrawRect(bounds, opts, imageSize, inset) {
|
|
13042
|
-
const fit = opts.fit ?? "none";
|
|
13043
|
-
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
13044
|
-
const innerBounds = {
|
|
13045
|
-
x: bounds.x + inset,
|
|
13046
|
-
y: bounds.y + inset,
|
|
13047
|
-
width: Math.max(0, bounds.width - inset * 2),
|
|
13048
|
-
height: Math.max(0, bounds.height - inset * 2)
|
|
13049
|
-
};
|
|
13050
|
-
const availableWidth = Math.max(0, innerBounds.width);
|
|
13051
|
-
const availableHeight = Math.max(0, innerBounds.height);
|
|
13052
|
-
let drawWidth = opts.width ?? imageSize.width;
|
|
13053
|
-
let drawHeight = opts.height ?? imageSize.height;
|
|
13054
|
-
if (scaleWithBounds) {
|
|
13055
|
-
if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
|
|
13056
|
-
const scaleX = availableWidth / imageSize.width;
|
|
13057
|
-
const scaleY = availableHeight / imageSize.height;
|
|
13058
|
-
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
13059
|
-
drawWidth = imageSize.width * scale;
|
|
13060
|
-
drawHeight = imageSize.height * scale;
|
|
13061
|
-
} else if (fit === "stretch") {
|
|
13062
|
-
drawWidth = availableWidth;
|
|
13063
|
-
drawHeight = availableHeight;
|
|
13064
|
-
}
|
|
13065
|
-
}
|
|
13066
|
-
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
13067
|
-
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
13068
|
-
return {
|
|
13069
|
-
x: innerBounds.x + (innerBounds.width - drawWidth) / 2,
|
|
13070
|
-
y: innerBounds.y + (innerBounds.height - drawHeight) / 2,
|
|
13071
|
-
width: drawWidth,
|
|
13072
|
-
height: drawHeight
|
|
13073
|
-
};
|
|
13074
|
-
}
|
|
13075
13222
|
resolveIconHref(opts) {
|
|
13076
13223
|
const source = opts.source;
|
|
13077
13224
|
if (source instanceof HTMLImageElement) return source.src;
|
|
13078
13225
|
if (!source) return "";
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13082
|
-
|
|
13083
|
-
}
|
|
13084
|
-
return source;
|
|
13085
|
-
}
|
|
13086
|
-
readSvgFromUrlSync(url) {
|
|
13087
|
-
if (typeof XMLHttpRequest === "undefined") return null;
|
|
13088
|
-
try {
|
|
13089
|
-
const xhr = new XMLHttpRequest();
|
|
13090
|
-
xhr.open("GET", url, false);
|
|
13091
|
-
xhr.send();
|
|
13092
|
-
if (xhr.status >= 200 && xhr.status < 300) return xhr.responseText || null;
|
|
13093
|
-
} catch {}
|
|
13094
|
-
return null;
|
|
13095
|
-
}
|
|
13096
|
-
isSvgMarkup(value) {
|
|
13097
|
-
const trimmed = value.trim().toLowerCase();
|
|
13098
|
-
return trimmed.startsWith("<svg") || trimmed.includes("<svg");
|
|
13099
|
-
}
|
|
13100
|
-
styleSetColor(style, key, color) {
|
|
13101
|
-
if (new RegExp(`${key}\\s*:`).test(style)) return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
|
|
13102
|
-
return `${style}${style.trim().endsWith(";") || style.trim() === "" ? "" : ";"}${key}:${color};`;
|
|
13103
|
-
}
|
|
13104
|
-
tintSvg(svgText, strokeColor, fillColor) {
|
|
13105
|
-
if (!strokeColor && !fillColor) return svgText;
|
|
13106
|
-
const root = new DOMParser().parseFromString(svgText, "image/svg+xml").documentElement;
|
|
13107
|
-
if (!root || root.nodeName.toLowerCase() === "parsererror") return svgText;
|
|
13108
|
-
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
13109
|
-
for (const el of all) {
|
|
13110
|
-
const stroke = el.getAttribute("stroke");
|
|
13111
|
-
if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") el.setAttribute("stroke", strokeColor);
|
|
13112
|
-
const fill = el.getAttribute("fill");
|
|
13113
|
-
if (fillColor && fill !== null && fill.toLowerCase() !== "none") el.setAttribute("fill", fillColor);
|
|
13114
|
-
const style = el.getAttribute("style");
|
|
13115
|
-
if (style) {
|
|
13116
|
-
let next = style;
|
|
13117
|
-
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) next = this.styleSetColor(next, "stroke", strokeColor);
|
|
13118
|
-
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) next = this.styleSetColor(next, "fill", fillColor);
|
|
13119
|
-
if (next !== style) el.setAttribute("style", next);
|
|
13120
|
-
}
|
|
13121
|
-
}
|
|
13122
|
-
return new XMLSerializer().serializeToString(root);
|
|
13123
|
-
}
|
|
13124
|
-
svgToDataUrl(svg) {
|
|
13125
|
-
return `data:image/svg+xml;utf8,${encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ")}`;
|
|
13226
|
+
return resolveTintedSvgDataUrlSync(source, {
|
|
13227
|
+
strokeColor: opts.strokeColor,
|
|
13228
|
+
fillColor: opts.fillColor
|
|
13229
|
+
}) ?? source;
|
|
13126
13230
|
}
|
|
13127
13231
|
escapeAttribute(value) {
|
|
13128
13232
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -13179,8 +13283,11 @@ var AutoRouting = class {
|
|
|
13179
13283
|
const type = options.type ?? "polyline";
|
|
13180
13284
|
for (const edge of renderer.edges.values()) {
|
|
13181
13285
|
edge.type = type;
|
|
13182
|
-
const
|
|
13183
|
-
const
|
|
13286
|
+
const fromId = edge.from.nodeId;
|
|
13287
|
+
const toId = edge.to.nodeId;
|
|
13288
|
+
if (!fromId || !toId) continue;
|
|
13289
|
+
const fromNode = renderer.getNode(fromId);
|
|
13290
|
+
const toNode = renderer.getNode(toId);
|
|
13184
13291
|
if (!fromNode || !toNode) continue;
|
|
13185
13292
|
const fromPoint = edge.from.portId !== void 0 ? fromNode.getPortPosition(edge.from.portId) ?? fromNode.getCenter() : fromNode.getCenter();
|
|
13186
13293
|
const toPoint = edge.to.portId !== void 0 ? toNode.getPortPosition(edge.to.portId) ?? toNode.getCenter() : toNode.getCenter();
|
|
@@ -13419,11 +13526,10 @@ var GuidesOverlay = class extends BaseOverlay {
|
|
|
13419
13526
|
};
|
|
13420
13527
|
//#endregion
|
|
13421
13528
|
//#region src/core/overlays/MiniMap.ts
|
|
13422
|
-
var MiniMap = class {
|
|
13529
|
+
var MiniMap = class extends BaseOverlay {
|
|
13423
13530
|
constructor(options = {}) {
|
|
13424
|
-
|
|
13531
|
+
super(options.enabled ?? true);
|
|
13425
13532
|
this.options = {
|
|
13426
|
-
enabled: options.enabled ?? true,
|
|
13427
13533
|
width: options.width ?? 180,
|
|
13428
13534
|
height: options.height ?? 120,
|
|
13429
13535
|
padding: options.padding ?? 12,
|
|
@@ -13436,7 +13542,7 @@ var MiniMap = class {
|
|
|
13436
13542
|
}
|
|
13437
13543
|
install(renderer) {
|
|
13438
13544
|
this.removeOverlay = renderer.addTopOverlayRenderer((ctx) => {
|
|
13439
|
-
if (!this.
|
|
13545
|
+
if (!this.enabled) return;
|
|
13440
13546
|
const layout = this.computeLayout(renderer);
|
|
13441
13547
|
if (!layout) return;
|
|
13442
13548
|
const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY, viewRect } = layout;
|
|
@@ -13485,13 +13591,6 @@ var MiniMap = class {
|
|
|
13485
13591
|
ctx.restore();
|
|
13486
13592
|
});
|
|
13487
13593
|
}
|
|
13488
|
-
destroy() {
|
|
13489
|
-
this.removeOverlay?.();
|
|
13490
|
-
this.removeOverlay = null;
|
|
13491
|
-
}
|
|
13492
|
-
setEnabled(enabled) {
|
|
13493
|
-
this.options.enabled = enabled;
|
|
13494
|
-
}
|
|
13495
13594
|
beginOverlayDrag(renderer, screenX, screenY) {
|
|
13496
13595
|
const hit = this.hitTestMiniMap(renderer, screenX, screenY);
|
|
13497
13596
|
if (!hit) return null;
|
|
@@ -13508,7 +13607,7 @@ var MiniMap = class {
|
|
|
13508
13607
|
return this.hitTestMiniMap(renderer, screenX, screenY) !== null;
|
|
13509
13608
|
}
|
|
13510
13609
|
updateOverlayDrag(renderer, screenX, screenY, payload) {
|
|
13511
|
-
if (!this.
|
|
13610
|
+
if (!this.enabled) return false;
|
|
13512
13611
|
if (typeof payload !== "object" || payload === null || !("kind" in payload)) return false;
|
|
13513
13612
|
const dragPayload = payload;
|
|
13514
13613
|
if (dragPayload.kind === "block") return true;
|
|
@@ -13530,7 +13629,7 @@ var MiniMap = class {
|
|
|
13530
13629
|
return true;
|
|
13531
13630
|
}
|
|
13532
13631
|
hitTestMiniMap(renderer, screenX, screenY) {
|
|
13533
|
-
if (!this.
|
|
13632
|
+
if (!this.enabled) return null;
|
|
13534
13633
|
const layout = this.computeLayout(renderer);
|
|
13535
13634
|
if (!layout) return null;
|
|
13536
13635
|
const canvasPoint = renderer.screenToCanvas(screenX, screenY);
|
|
@@ -13663,6 +13762,6 @@ function applyEdgeStyle(ctx, style) {
|
|
|
13663
13762
|
return baseOpacity * strokeOpacity;
|
|
13664
13763
|
}
|
|
13665
13764
|
//#endregion
|
|
13666
|
-
export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, CContainer, CDivider, CIcon, CShape, CText, ChangeEditablePolylineControlPointsCommand, CircleNode, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, Port, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, icon, isCornerPlacement, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
|
|
13765
|
+
export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, directionFromAngle, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, getClosestPointOnPath, getPathPointAt, icon, isCornerPlacement, isEdgeEdgeEndpoint, isNodeEdgeEndpoint, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
|
|
13667
13766
|
|
|
13668
13767
|
//# sourceMappingURL=papirus.js.map
|