@ngroznykh/papirus 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ConnectionManager.d.ts +27 -1
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts +5 -0
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +2 -0
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/overlays/MiniMap.d.ts +2 -0
- package/dist/core/overlays/MiniMap.d.ts.map +1 -1
- package/dist/elements/Node.d.ts +18 -0
- package/dist/elements/Node.d.ts.map +1 -1
- package/dist/elements/nodes/CircleNode.d.ts +5 -0
- package/dist/elements/nodes/CircleNode.d.ts.map +1 -1
- package/dist/elements/nodes/DiamondNode.d.ts +5 -0
- package/dist/elements/nodes/DiamondNode.d.ts.map +1 -1
- package/dist/papirus.js +446 -51
- package/dist/papirus.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -435,6 +435,7 @@ const ANCHOR_POINT_HITBOX_RADIUS = 10;
|
|
|
435
435
|
const NODE_HITBOX_PADDING = 10;
|
|
436
436
|
const ANCHOR_PORT_PREFIX = "anchor:";
|
|
437
437
|
const BEZIER_MAX_OFFSET = 100;
|
|
438
|
+
const OUTLINE_SNAP_SCREEN_TOLERANCE = 40;
|
|
438
439
|
const SELECTION_RECT_MIN_SIZE = 1;
|
|
439
440
|
const DEFAULT_SELECTION_COLOR = "#3b82f6";
|
|
440
441
|
const DEFAULT_HOVER_COLOR = "#6366f1";
|
|
@@ -1571,16 +1572,29 @@ class ConnectionManager extends EventEmitter {
|
|
|
1571
1572
|
this.hoverDisabled = false;
|
|
1572
1573
|
this.reconnectPoint = null;
|
|
1573
1574
|
this.previewTargetAnchorId = null;
|
|
1575
|
+
this.sourceOutlineParam = null;
|
|
1576
|
+
this.previewTargetOutlineParam = null;
|
|
1577
|
+
this.previewTargetNodeId = null;
|
|
1574
1578
|
this.isReconnecting = false;
|
|
1575
1579
|
this.reconnectingEdge = null;
|
|
1576
1580
|
this.reconnectingEndpoint = null;
|
|
1577
1581
|
this.originalEdgeEndpoint = null;
|
|
1582
|
+
this.reconnectingOutlineParam = null;
|
|
1583
|
+
this.reconnectingTargetNodeId = null;
|
|
1578
1584
|
this.activeControlPointDrag = null;
|
|
1579
1585
|
this.renderer = options.renderer;
|
|
1580
1586
|
this.createEdge = options.createEdge;
|
|
1581
1587
|
this.addEdge = options.addEdge ?? ((edge) => this.renderer.addEdge(edge));
|
|
1582
1588
|
this.snapToGrid = options.snapToGrid ?? false;
|
|
1583
1589
|
this.gridSize = options.gridSize ?? 20;
|
|
1590
|
+
this.attachToOutline = options.attachToOutline ?? false;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Enable/disable attach-to-outline mode (edges attach anywhere on shape contour)
|
|
1594
|
+
*/
|
|
1595
|
+
setAttachToOutline(enabled) {
|
|
1596
|
+
this.attachToOutline = enabled;
|
|
1597
|
+
this.renderer.attachToOutline = enabled;
|
|
1584
1598
|
}
|
|
1585
1599
|
/**
|
|
1586
1600
|
* Enable/disable snap to grid for editable polyline control points.
|
|
@@ -1646,9 +1660,17 @@ class ConnectionManager extends EventEmitter {
|
|
|
1646
1660
|
this.sourceNode = node;
|
|
1647
1661
|
this.isConnecting = true;
|
|
1648
1662
|
const target = { x: event.worldX, y: event.worldY };
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1663
|
+
if (this.attachToOutline) {
|
|
1664
|
+
const { point, param } = node.getClosestPointOnOutline(target);
|
|
1665
|
+
this.sourceOutlineParam = param;
|
|
1666
|
+
this.sourceAnchorId = null;
|
|
1667
|
+
this.sourcePoint = point;
|
|
1668
|
+
} else {
|
|
1669
|
+
const anchor = node.getNearestAnchor(target);
|
|
1670
|
+
this.sourceOutlineParam = null;
|
|
1671
|
+
this.sourceAnchorId = anchor?.id ?? null;
|
|
1672
|
+
this.sourcePoint = anchor?.point ?? node.getConnectionPoint(target);
|
|
1673
|
+
}
|
|
1652
1674
|
this.previewEndpoint = target;
|
|
1653
1675
|
this.emit("connectionStart", node);
|
|
1654
1676
|
return true;
|
|
@@ -1668,7 +1690,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
1668
1690
|
return false;
|
|
1669
1691
|
}
|
|
1670
1692
|
/**
|
|
1671
|
-
* Try to start connection from a hovered anchor point
|
|
1693
|
+
* Try to start connection from a hovered anchor point.
|
|
1694
|
+
* When attachToOutline + Shift: start from anywhere on node outline.
|
|
1695
|
+
* Otherwise: requires clicking on an anchor so that node drag still works.
|
|
1672
1696
|
*/
|
|
1673
1697
|
tryStartConnectionAtPoint(event) {
|
|
1674
1698
|
const point = { x: event.worldX, y: event.worldY };
|
|
@@ -1676,6 +1700,18 @@ class ConnectionManager extends EventEmitter {
|
|
|
1676
1700
|
if (!node) {
|
|
1677
1701
|
return false;
|
|
1678
1702
|
}
|
|
1703
|
+
if (this.attachToOutline && event.shiftKey) {
|
|
1704
|
+
const { point: outlinePoint, param } = node.getClosestPointOnOutline(point);
|
|
1705
|
+
this.sourceNode = node;
|
|
1706
|
+
this.isConnecting = true;
|
|
1707
|
+
this.sourceAnchorId = null;
|
|
1708
|
+
this.sourcePoint = outlinePoint;
|
|
1709
|
+
this.sourceOutlineParam = param;
|
|
1710
|
+
this.previewEndpoint = point;
|
|
1711
|
+
this.emit("connectionStart", node);
|
|
1712
|
+
this.renderer.markDirty();
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1679
1715
|
const hover = this.getAnchorAtPoint(node, point);
|
|
1680
1716
|
if (!hover) {
|
|
1681
1717
|
return false;
|
|
@@ -1685,6 +1721,12 @@ class ConnectionManager extends EventEmitter {
|
|
|
1685
1721
|
this.sourceAnchorId = hover.id;
|
|
1686
1722
|
this.sourcePoint = hover.point;
|
|
1687
1723
|
this.previewEndpoint = point;
|
|
1724
|
+
if (this.attachToOutline) {
|
|
1725
|
+
const { param } = node.getClosestPointOnOutline(point);
|
|
1726
|
+
this.sourceOutlineParam = param;
|
|
1727
|
+
} else {
|
|
1728
|
+
this.sourceOutlineParam = null;
|
|
1729
|
+
}
|
|
1688
1730
|
this.emit("connectionStart", node);
|
|
1689
1731
|
this.renderer.markDirty();
|
|
1690
1732
|
return true;
|
|
@@ -1735,32 +1777,122 @@ class ConnectionManager extends EventEmitter {
|
|
|
1735
1777
|
const toNode = this.renderer.getNode(edge.to.nodeId);
|
|
1736
1778
|
let snappedPoint = point;
|
|
1737
1779
|
let snappedDir;
|
|
1738
|
-
|
|
1739
|
-
if (
|
|
1780
|
+
let targetNode = this.getNodeAtPoint(point, true);
|
|
1781
|
+
if (this.attachToOutline) {
|
|
1782
|
+
const tolerance = OUTLINE_SNAP_SCREEN_TOLERANCE / Math.max(this.renderer.zoom, 1e-4);
|
|
1783
|
+
const toleranceSq = tolerance * tolerance;
|
|
1784
|
+
let bestDistSq = Infinity;
|
|
1785
|
+
let bestResult = null;
|
|
1786
|
+
for (const node of this.renderer.nodes.values()) {
|
|
1787
|
+
if (!node.visible) continue;
|
|
1788
|
+
const { point: outlinePoint, param } = node.getClosestPointOnOutline(point);
|
|
1789
|
+
const dx = point.x - outlinePoint.x;
|
|
1790
|
+
const dy = point.y - outlinePoint.y;
|
|
1791
|
+
const distSq = dx * dx + dy * dy;
|
|
1792
|
+
if (distSq <= toleranceSq && distSq < bestDistSq) {
|
|
1793
|
+
bestDistSq = distSq;
|
|
1794
|
+
bestResult = { node, point: outlinePoint, param };
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (bestResult) {
|
|
1798
|
+
targetNode = bestResult.node;
|
|
1799
|
+
snappedPoint = bestResult.point;
|
|
1800
|
+
this.reconnectingOutlineParam = bestResult.param;
|
|
1801
|
+
this.reconnectingTargetNodeId = bestResult.node.id;
|
|
1802
|
+
snappedDir = this.getDirectionFromOutlineParam(bestResult.param, bestResult.node);
|
|
1803
|
+
const otherNode = this.reconnectingEndpoint === "start" ? toNode : fromNode;
|
|
1804
|
+
if (otherNode) {
|
|
1805
|
+
const alignRef = edge.isEditablePolyline() ? this.getNearestBendPointForAxisAlignment(edge, this.reconnectingEndpoint) : this.reconnectingEndpoint === "start" ? this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint) : this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1806
|
+
snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, alignRef);
|
|
1807
|
+
const reprojected = targetNode.getClosestPointOnOutline(snappedPoint);
|
|
1808
|
+
snappedPoint = reprojected.point;
|
|
1809
|
+
this.reconnectingOutlineParam = reprojected.param;
|
|
1810
|
+
snappedDir = this.getDirectionFromOutlineParam(reprojected.param, targetNode);
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
this.reconnectingOutlineParam = null;
|
|
1814
|
+
this.reconnectingTargetNodeId = null;
|
|
1815
|
+
}
|
|
1816
|
+
} else if (targetNode) {
|
|
1740
1817
|
const nearestAnchor = targetNode.getNearestAnchor(point);
|
|
1741
1818
|
if (nearestAnchor) {
|
|
1742
1819
|
snappedPoint = nearestAnchor.point;
|
|
1743
1820
|
snappedDir = nearestAnchor.id.split(":")[0];
|
|
1744
1821
|
}
|
|
1822
|
+
this.reconnectingOutlineParam = null;
|
|
1823
|
+
this.reconnectingTargetNodeId = null;
|
|
1824
|
+
} else {
|
|
1825
|
+
this.reconnectingOutlineParam = null;
|
|
1826
|
+
this.reconnectingTargetNodeId = null;
|
|
1745
1827
|
}
|
|
1746
1828
|
this.reconnectPoint = snappedPoint;
|
|
1747
1829
|
if (this.reconnectingEndpoint === "start") {
|
|
1748
1830
|
if (toNode) {
|
|
1749
|
-
const
|
|
1750
|
-
const
|
|
1751
|
-
const toDir = toAnchorId?.split(":")[0];
|
|
1831
|
+
const target = this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint);
|
|
1832
|
+
const toDir = this.getTargetDirForReconnect(edge, toNode, "to");
|
|
1752
1833
|
edge.updateEndpoints(snappedPoint, target, snappedDir, toDir);
|
|
1753
1834
|
}
|
|
1754
1835
|
} else {
|
|
1755
1836
|
if (fromNode) {
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
1758
|
-
const fromDir = fromAnchorId?.split(":")[0];
|
|
1837
|
+
const start = this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1838
|
+
const fromDir = this.getTargetDirForReconnect(edge, fromNode, "from");
|
|
1759
1839
|
edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir);
|
|
1760
1840
|
}
|
|
1761
1841
|
}
|
|
1762
1842
|
this.renderer.markDirty();
|
|
1763
1843
|
}
|
|
1844
|
+
getTargetPointForReconnect(edge, node, endpoint, fallbackPoint) {
|
|
1845
|
+
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1846
|
+
if (this.attachToOutline && ep.outlineParam !== void 0) {
|
|
1847
|
+
return node.getConnectionPointAtOutlineParam(ep.outlineParam);
|
|
1848
|
+
}
|
|
1849
|
+
const anchorId = ep.portId?.replace(ANCHOR_PORT_PREFIX, "");
|
|
1850
|
+
if (anchorId) {
|
|
1851
|
+
return node.getAnchorPointById(anchorId) ?? node.getConnectionPoint(fallbackPoint);
|
|
1852
|
+
}
|
|
1853
|
+
return node.getConnectionPoint(fallbackPoint);
|
|
1854
|
+
}
|
|
1855
|
+
getTargetDirForReconnect(edge, node, endpoint) {
|
|
1856
|
+
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1857
|
+
if (this.attachToOutline && ep.outlineParam !== void 0) {
|
|
1858
|
+
return this.getDirectionFromOutlineParam(ep.outlineParam, node);
|
|
1859
|
+
}
|
|
1860
|
+
const anchorId = ep.portId?.replace(ANCHOR_PORT_PREFIX, "");
|
|
1861
|
+
return anchorId?.split(":")[0];
|
|
1862
|
+
}
|
|
1863
|
+
getDirectionFromOutlineParam(param, _node) {
|
|
1864
|
+
const p = (param % 1 + 1) % 1;
|
|
1865
|
+
if (p < 0.25) return "top";
|
|
1866
|
+
if (p < 0.5) return "right";
|
|
1867
|
+
if (p < 0.75) return "bottom";
|
|
1868
|
+
return "left";
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* For editable polyline: return the nearest bend point for axis alignment.
|
|
1872
|
+
* When reconnecting 'end', use last control point (or start). When reconnecting 'start', use first control point (or end).
|
|
1873
|
+
*/
|
|
1874
|
+
getNearestBendPointForAxisAlignment(edge, endpoint) {
|
|
1875
|
+
const cps = edge.isEditablePolyline() ? edge.controlPoints ?? edge.getEditableControlPoints() : [];
|
|
1876
|
+
if (endpoint === "end") {
|
|
1877
|
+
return cps.length > 0 ? cps[cps.length - 1] : edge.startPoint;
|
|
1878
|
+
}
|
|
1879
|
+
return cps.length > 0 ? cps[0] : edge.endPoint;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* When attachToOutline: snap moving point to horizontal/vertical alignment with fixed point.
|
|
1883
|
+
*/
|
|
1884
|
+
applyAxisAlignmentToPoint(moving, fixed) {
|
|
1885
|
+
const tolerance = EDGE_AXIS_MAGNET_SCREEN_TOLERANCE / Math.max(this.renderer.zoom, 1e-4);
|
|
1886
|
+
let x = moving.x;
|
|
1887
|
+
let y = moving.y;
|
|
1888
|
+
if (Math.abs(moving.x - fixed.x) <= tolerance) {
|
|
1889
|
+
x = fixed.x;
|
|
1890
|
+
}
|
|
1891
|
+
if (Math.abs(moving.y - fixed.y) <= tolerance) {
|
|
1892
|
+
y = fixed.y;
|
|
1893
|
+
}
|
|
1894
|
+
return { x, y };
|
|
1895
|
+
}
|
|
1764
1896
|
/**
|
|
1765
1897
|
* Handle mouse move during connection
|
|
1766
1898
|
*/
|
|
@@ -1792,25 +1924,60 @@ class ConnectionManager extends EventEmitter {
|
|
|
1792
1924
|
const cursorPoint = { x: event.worldX, y: event.worldY };
|
|
1793
1925
|
let snappedPoint = cursorPoint;
|
|
1794
1926
|
this.previewTargetAnchorId = null;
|
|
1795
|
-
|
|
1796
|
-
|
|
1927
|
+
this.previewTargetOutlineParam = null;
|
|
1928
|
+
this.previewTargetNodeId = null;
|
|
1929
|
+
let targetNode = this.getNodeAtPoint(cursorPoint, false);
|
|
1930
|
+
if (this.attachToOutline) {
|
|
1931
|
+
const tolerance = OUTLINE_SNAP_SCREEN_TOLERANCE / Math.max(this.renderer.zoom, 1e-4);
|
|
1932
|
+
const toleranceSq = tolerance * tolerance;
|
|
1933
|
+
let bestDistSq = Infinity;
|
|
1934
|
+
let bestResult = null;
|
|
1935
|
+
for (const node of this.renderer.nodes.values()) {
|
|
1936
|
+
if (!node.visible || !this.isCompatibleTarget(node)) continue;
|
|
1937
|
+
const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, node.id);
|
|
1938
|
+
if (forbidden) continue;
|
|
1939
|
+
const { point: outlinePoint, param } = node.getClosestPointOnOutline(cursorPoint);
|
|
1940
|
+
const dx = cursorPoint.x - outlinePoint.x;
|
|
1941
|
+
const dy = cursorPoint.y - outlinePoint.y;
|
|
1942
|
+
const distSq = dx * dx + dy * dy;
|
|
1943
|
+
if (distSq <= toleranceSq && distSq < bestDistSq) {
|
|
1944
|
+
bestDistSq = distSq;
|
|
1945
|
+
bestResult = { node, point: outlinePoint, param };
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (bestResult) {
|
|
1949
|
+
targetNode = bestResult.node;
|
|
1950
|
+
snappedPoint = bestResult.point;
|
|
1951
|
+
this.previewTargetOutlineParam = bestResult.param;
|
|
1952
|
+
this.previewTargetNodeId = bestResult.node.id;
|
|
1953
|
+
if (this.sourcePoint) {
|
|
1954
|
+
snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, this.sourcePoint);
|
|
1955
|
+
const reprojected = bestResult.node.getClosestPointOnOutline(snappedPoint);
|
|
1956
|
+
snappedPoint = reprojected.point;
|
|
1957
|
+
this.previewTargetOutlineParam = reprojected.param;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
} else if (targetNode && this.isCompatibleTarget(targetNode)) {
|
|
1797
1961
|
const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
|
|
1798
|
-
if (forbidden) {
|
|
1799
|
-
this.setCursor("not-allowed");
|
|
1800
|
-
} else {
|
|
1801
|
-
this.setCursor("crosshair");
|
|
1962
|
+
if (!forbidden) {
|
|
1802
1963
|
const nearestAnchor = targetNode.getNearestAnchor(cursorPoint);
|
|
1803
1964
|
if (nearestAnchor) {
|
|
1804
1965
|
snappedPoint = nearestAnchor.point;
|
|
1805
1966
|
this.previewTargetAnchorId = nearestAnchor.id;
|
|
1806
1967
|
}
|
|
1807
1968
|
}
|
|
1969
|
+
}
|
|
1970
|
+
if (targetNode) {
|
|
1971
|
+
const forbidden = this._connectionValidator && this.sourceNode && !this._connectionValidator(this.sourceNode.id, targetNode.id);
|
|
1972
|
+
this.setCursor(forbidden ? "not-allowed" : "crosshair");
|
|
1808
1973
|
} else {
|
|
1809
1974
|
this.setCursor("crosshair");
|
|
1810
1975
|
}
|
|
1811
1976
|
this.previewEndpoint = snappedPoint;
|
|
1812
|
-
if (this.sourceNode && !this.sourceAnchorId) {
|
|
1977
|
+
if (this.sourceNode && !this.sourceAnchorId && !this.sourceOutlineParam) {
|
|
1813
1978
|
this.sourcePoint = this.sourceNode.getConnectionPoint(this.previewEndpoint);
|
|
1979
|
+
} else if (this.sourceNode && this.sourceOutlineParam !== null) {
|
|
1980
|
+
this.sourcePoint = this.sourceNode.getConnectionPointAtOutlineParam(this.sourceOutlineParam);
|
|
1814
1981
|
}
|
|
1815
1982
|
this.emit(
|
|
1816
1983
|
"connectionMove",
|
|
@@ -1866,19 +2033,29 @@ class ConnectionManager extends EventEmitter {
|
|
|
1866
2033
|
if (this.isReconnecting && this.reconnectingEdge) {
|
|
1867
2034
|
const point2 = { x: event.worldX, y: event.worldY };
|
|
1868
2035
|
let reconnected = false;
|
|
1869
|
-
|
|
2036
|
+
let node = this.getNodeAtPoint(point2, true);
|
|
2037
|
+
if (!node && this.attachToOutline && this.reconnectingTargetNodeId) {
|
|
2038
|
+
node = this.renderer.getNode(this.reconnectingTargetNodeId) ?? null;
|
|
2039
|
+
}
|
|
1870
2040
|
if (node) {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
this.reconnectingEdge.from = {
|
|
2041
|
+
if (this.attachToOutline && this.reconnectingOutlineParam !== null) {
|
|
2042
|
+
const endpoint = {
|
|
1874
2043
|
nodeId: node.id,
|
|
1875
|
-
|
|
2044
|
+
outlineParam: this.reconnectingOutlineParam
|
|
1876
2045
|
};
|
|
2046
|
+
if (this.reconnectingEndpoint === "start") {
|
|
2047
|
+
this.reconnectingEdge.from = endpoint;
|
|
2048
|
+
} else {
|
|
2049
|
+
this.reconnectingEdge.to = endpoint;
|
|
2050
|
+
}
|
|
1877
2051
|
} else {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2052
|
+
const anchor = node.getNearestAnchor(point2);
|
|
2053
|
+
const portId = this.reconnectingEdge.lockAnchors && anchor ? `${ANCHOR_PORT_PREFIX}${anchor.id}` : void 0;
|
|
2054
|
+
if (this.reconnectingEndpoint === "start") {
|
|
2055
|
+
this.reconnectingEdge.from = { nodeId: node.id, portId };
|
|
2056
|
+
} else {
|
|
2057
|
+
this.reconnectingEdge.to = { nodeId: node.id, portId };
|
|
2058
|
+
}
|
|
1882
2059
|
}
|
|
1883
2060
|
reconnected = true;
|
|
1884
2061
|
this.emit("edgeReconnect", this.reconnectingEdge, this.reconnectingEndpoint);
|
|
@@ -1900,20 +2077,36 @@ class ConnectionManager extends EventEmitter {
|
|
|
1900
2077
|
}
|
|
1901
2078
|
const point = { x: event.worldX, y: event.worldY };
|
|
1902
2079
|
let createdEdge = null;
|
|
1903
|
-
|
|
2080
|
+
let targetNode = this.getNodeAtPoint(point, false);
|
|
2081
|
+
if (!targetNode && this.attachToOutline && this.previewTargetNodeId) {
|
|
2082
|
+
targetNode = this.renderer.getNode(this.previewTargetNodeId) ?? null;
|
|
2083
|
+
}
|
|
1904
2084
|
if (targetNode) {
|
|
1905
2085
|
const allowed = !this._connectionValidator || this._connectionValidator(this.sourceNode.id, targetNode.id);
|
|
1906
2086
|
if (allowed) {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2087
|
+
let from;
|
|
2088
|
+
let to;
|
|
2089
|
+
if (this.attachToOutline) {
|
|
2090
|
+
const targetOutline = targetNode.getClosestPointOnOutline(point);
|
|
2091
|
+
from = {
|
|
2092
|
+
nodeId: this.sourceNode.id,
|
|
2093
|
+
outlineParam: this.sourceOutlineParam ?? void 0
|
|
2094
|
+
};
|
|
2095
|
+
to = {
|
|
2096
|
+
nodeId: targetNode.id,
|
|
2097
|
+
outlineParam: targetOutline.param
|
|
2098
|
+
};
|
|
2099
|
+
} else {
|
|
2100
|
+
const nearestTargetAnchor = targetNode.getNearestAnchor(point);
|
|
2101
|
+
from = {
|
|
2102
|
+
nodeId: this.sourceNode.id,
|
|
2103
|
+
portId: this.sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${this.sourceAnchorId}` : void 0
|
|
2104
|
+
};
|
|
2105
|
+
to = {
|
|
2106
|
+
nodeId: targetNode.id,
|
|
2107
|
+
portId: nearestTargetAnchor ? `${ANCHOR_PORT_PREFIX}${nearestTargetAnchor.id}` : void 0
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
1917
2110
|
createdEdge = this.createEdge(from, to);
|
|
1918
2111
|
this.addEdge(createdEdge);
|
|
1919
2112
|
this.emit("connect", createdEdge);
|
|
@@ -1940,7 +2133,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
1940
2133
|
*/
|
|
1941
2134
|
renderPreview(ctx) {
|
|
1942
2135
|
if (this.isReconnecting && this.reconnectPoint) {
|
|
1943
|
-
|
|
2136
|
+
if (!this.attachToOutline) {
|
|
2137
|
+
this.renderAnchorHighlights(ctx, this.reconnectPoint, true);
|
|
2138
|
+
}
|
|
1944
2139
|
return;
|
|
1945
2140
|
}
|
|
1946
2141
|
if (!this.isConnecting || this.previewEndpoint === null) {
|
|
@@ -1948,14 +2143,16 @@ class ConnectionManager extends EventEmitter {
|
|
|
1948
2143
|
}
|
|
1949
2144
|
const start = this.sourcePoint;
|
|
1950
2145
|
const end = this.previewEndpoint;
|
|
1951
|
-
const fromDir = this.sourceAnchorId?.split(":")[0];
|
|
1952
|
-
const toDir = this.previewTargetAnchorId?.split(":")[0];
|
|
2146
|
+
const fromDir = this.sourceOutlineParam !== null ? this.getDirectionFromOutlineParam(this.sourceOutlineParam, this.sourceNode) : this.sourceAnchorId?.split(":")[0];
|
|
2147
|
+
const toDir = this.previewTargetOutlineParam !== null ? this.getDirectionFromOutlineParam(this.previewTargetOutlineParam, this.sourceNode) : this.previewTargetAnchorId?.split(":")[0];
|
|
1953
2148
|
ctx.strokeStyle = "#3b82f6";
|
|
1954
2149
|
ctx.lineWidth = 2;
|
|
1955
2150
|
ctx.setLineDash([6, 4]);
|
|
1956
2151
|
this.drawBezierPreview(ctx, start, end, fromDir, toDir);
|
|
1957
2152
|
ctx.setLineDash([]);
|
|
1958
|
-
this.
|
|
2153
|
+
if (!this.attachToOutline) {
|
|
2154
|
+
this.renderAnchorHighlights(ctx, end, false);
|
|
2155
|
+
}
|
|
1959
2156
|
}
|
|
1960
2157
|
/**
|
|
1961
2158
|
* Get control point offset based on direction
|
|
@@ -2035,6 +2232,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
2035
2232
|
this.sourcePoint = null;
|
|
2036
2233
|
this.previewEndpoint = null;
|
|
2037
2234
|
this.sourceAnchorId = null;
|
|
2235
|
+
this.sourceOutlineParam = null;
|
|
2236
|
+
this.previewTargetOutlineParam = null;
|
|
2237
|
+
this.previewTargetNodeId = null;
|
|
2038
2238
|
this.setCursor("");
|
|
2039
2239
|
}
|
|
2040
2240
|
resetReconnection() {
|
|
@@ -2045,6 +2245,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
2045
2245
|
this.reconnectingEdge = null;
|
|
2046
2246
|
this.reconnectingEndpoint = null;
|
|
2047
2247
|
this.originalEdgeEndpoint = null;
|
|
2248
|
+
this.reconnectingOutlineParam = null;
|
|
2249
|
+
this.reconnectingTargetNodeId = null;
|
|
2048
2250
|
this.reconnectPoint = null;
|
|
2049
2251
|
this.setCursor("");
|
|
2050
2252
|
}
|
|
@@ -2056,6 +2258,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
2056
2258
|
return;
|
|
2057
2259
|
}
|
|
2058
2260
|
this.renderEditablePolylineControls(ctx);
|
|
2261
|
+
if (this.attachToOutline) {
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2059
2264
|
if (!this.hoverNodeId) {
|
|
2060
2265
|
return;
|
|
2061
2266
|
}
|
|
@@ -4644,11 +4849,14 @@ class InteractionManager {
|
|
|
4644
4849
|
this.navigationManager = new NavigationManager({ renderer: this.renderer });
|
|
4645
4850
|
this.historyManager = new HistoryManager();
|
|
4646
4851
|
this.keymap = { ...DEFAULT_KEYMAP, ...options.keymap };
|
|
4852
|
+
const attachToOutline = options.attachToOutline ?? false;
|
|
4853
|
+
this.renderer.attachToOutline = attachToOutline;
|
|
4647
4854
|
this.connectionManager = new ConnectionManager({
|
|
4648
4855
|
renderer: this.renderer,
|
|
4649
4856
|
createEdge: options.createEdge ?? ((from, to) => new Edge({ from, to, type: "bezier", arrowType: "single" })),
|
|
4650
4857
|
snapToGrid: options.snapToGrid ?? this.renderer.snapToGrid,
|
|
4651
4858
|
gridSize: options.gridSize ?? 20,
|
|
4859
|
+
attachToOutline,
|
|
4652
4860
|
addEdge: (edge) => {
|
|
4653
4861
|
this.historyManager.execute({
|
|
4654
4862
|
execute: () => this.renderer.addEdge(edge),
|
|
@@ -5474,7 +5682,13 @@ class InteractionManager {
|
|
|
5474
5682
|
});
|
|
5475
5683
|
}
|
|
5476
5684
|
endpointsEqual(a, b) {
|
|
5477
|
-
|
|
5685
|
+
if (a.nodeId !== b.nodeId) return false;
|
|
5686
|
+
if ((a.portId ?? null) !== (b.portId ?? null)) return false;
|
|
5687
|
+
const aParam = a.outlineParam;
|
|
5688
|
+
const bParam = b.outlineParam;
|
|
5689
|
+
if (aParam === void 0 && bParam === void 0) return true;
|
|
5690
|
+
if (aParam === void 0 || bParam === void 0) return false;
|
|
5691
|
+
return Math.abs(aParam % 1 - bParam % 1) < 1e-9;
|
|
5478
5692
|
}
|
|
5479
5693
|
getSelectionBounds() {
|
|
5480
5694
|
const selected = Array.from(this.selectionManager.selectedIds);
|
|
@@ -5957,6 +6171,7 @@ class DiagramRenderer extends EventEmitter {
|
|
|
5957
6171
|
this._nodes = /* @__PURE__ */ new Map();
|
|
5958
6172
|
this._edges = /* @__PURE__ */ new Map();
|
|
5959
6173
|
this._groups = /* @__PURE__ */ new Map();
|
|
6174
|
+
this._attachToOutline = false;
|
|
5960
6175
|
this.canvas = this.resolveCanvas(canvas);
|
|
5961
6176
|
this.options = { ...DEFAULT_OPTIONS$1, ...options };
|
|
5962
6177
|
this.scrollbar = this.resolveScrollbarOptions(options);
|
|
@@ -5972,6 +6187,16 @@ class DiagramRenderer extends EventEmitter {
|
|
|
5972
6187
|
this.setupCanvas();
|
|
5973
6188
|
this.startRenderLoop();
|
|
5974
6189
|
}
|
|
6190
|
+
/** When true, ports and anchor points are hidden (attach anywhere on outline) */
|
|
6191
|
+
get attachToOutline() {
|
|
6192
|
+
return this._attachToOutline;
|
|
6193
|
+
}
|
|
6194
|
+
set attachToOutline(value) {
|
|
6195
|
+
if (this._attachToOutline !== value) {
|
|
6196
|
+
this._attachToOutline = value;
|
|
6197
|
+
this.markDirty();
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
5975
6200
|
/**
|
|
5976
6201
|
* Current zoom level
|
|
5977
6202
|
*/
|
|
@@ -6208,6 +6433,7 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6208
6433
|
*/
|
|
6209
6434
|
addNode(node) {
|
|
6210
6435
|
node.setDirtyListener(() => this.markDirty());
|
|
6436
|
+
node.setAttachToOutlineGetter(() => this._attachToOutline);
|
|
6211
6437
|
this._nodes.set(node.id, node);
|
|
6212
6438
|
this.animationManager.registerEnter(node.id);
|
|
6213
6439
|
this.markDirty();
|
|
@@ -6291,6 +6517,7 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6291
6517
|
}
|
|
6292
6518
|
this._nodes.delete(nodeId);
|
|
6293
6519
|
node.setDirtyListener(void 0);
|
|
6520
|
+
node.setAttachToOutlineGetter(void 0);
|
|
6294
6521
|
this.markDirty();
|
|
6295
6522
|
this.emit("nodeRemove", node);
|
|
6296
6523
|
return true;
|
|
@@ -6542,13 +6769,15 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6542
6769
|
if (fromNode === void 0 || toNode === void 0) {
|
|
6543
6770
|
continue;
|
|
6544
6771
|
}
|
|
6545
|
-
if (edge.lockAnchors) {
|
|
6772
|
+
if (edge.lockAnchors && edge.from.outlineParam === void 0) {
|
|
6546
6773
|
if (!edge.from.portId) {
|
|
6547
6774
|
const anchor = fromNode.getNearestAnchor(toNode.getCenter());
|
|
6548
6775
|
if (anchor) {
|
|
6549
6776
|
edge.from = { ...edge.from, portId: `${ANCHOR_PORT_PREFIX}${anchor.id}` };
|
|
6550
6777
|
}
|
|
6551
6778
|
}
|
|
6779
|
+
}
|
|
6780
|
+
if (edge.lockAnchors && edge.to.outlineParam === void 0) {
|
|
6552
6781
|
if (!edge.to.portId) {
|
|
6553
6782
|
const anchor = toNode.getNearestAnchor(fromNode.getCenter());
|
|
6554
6783
|
if (anchor) {
|
|
@@ -6560,7 +6789,10 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6560
6789
|
let toPoint = toNode.getConnectionPoint(fromNode.getCenter());
|
|
6561
6790
|
let fromDir;
|
|
6562
6791
|
let toDir;
|
|
6563
|
-
if (edge.
|
|
6792
|
+
if (edge.from.outlineParam !== void 0) {
|
|
6793
|
+
fromPoint = fromNode.getConnectionPointAtOutlineParam(edge.from.outlineParam);
|
|
6794
|
+
fromDir = this.getDirectionFromOutlineParam(edge.from.outlineParam);
|
|
6795
|
+
} else if (edge.lockAnchors && edge.from.portId?.startsWith(ANCHOR_PORT_PREFIX)) {
|
|
6564
6796
|
const anchorId = edge.from.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
6565
6797
|
const anchorPoint = fromNode.getAnchorPointById(anchorId);
|
|
6566
6798
|
if (anchorPoint) {
|
|
@@ -6568,7 +6800,10 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6568
6800
|
fromDir = anchorId.split(":")[0];
|
|
6569
6801
|
}
|
|
6570
6802
|
}
|
|
6571
|
-
if (edge.
|
|
6803
|
+
if (edge.to.outlineParam !== void 0) {
|
|
6804
|
+
toPoint = toNode.getConnectionPointAtOutlineParam(edge.to.outlineParam);
|
|
6805
|
+
toDir = this.getDirectionFromOutlineParam(edge.to.outlineParam);
|
|
6806
|
+
} else if (edge.lockAnchors && edge.to.portId?.startsWith(ANCHOR_PORT_PREFIX)) {
|
|
6572
6807
|
const anchorId = edge.to.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
6573
6808
|
const anchorPoint = toNode.getAnchorPointById(anchorId);
|
|
6574
6809
|
if (anchorPoint) {
|
|
@@ -6586,6 +6821,13 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6586
6821
|
edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir, { obstacles });
|
|
6587
6822
|
}
|
|
6588
6823
|
}
|
|
6824
|
+
getDirectionFromOutlineParam(param) {
|
|
6825
|
+
const p = (param % 1 + 1) % 1;
|
|
6826
|
+
if (p < 0.25) return "top";
|
|
6827
|
+
if (p < 0.5) return "right";
|
|
6828
|
+
if (p < 0.75) return "bottom";
|
|
6829
|
+
return "left";
|
|
6830
|
+
}
|
|
6589
6831
|
renderScrollbars(ctx) {
|
|
6590
6832
|
const metrics = this.getScrollbarMetrics();
|
|
6591
6833
|
if (!metrics) {
|
|
@@ -7891,10 +8133,19 @@ class Node extends Element {
|
|
|
7891
8133
|
get defaultSize() {
|
|
7892
8134
|
return { ...this._defaultSize };
|
|
7893
8135
|
}
|
|
8136
|
+
/**
|
|
8137
|
+
* Set getter for attachToOutline (called by DiagramRenderer when node is added)
|
|
8138
|
+
*/
|
|
8139
|
+
setAttachToOutlineGetter(getter) {
|
|
8140
|
+
this._attachToOutlineGetter = getter;
|
|
8141
|
+
}
|
|
7894
8142
|
/**
|
|
7895
8143
|
* Render ports
|
|
7896
8144
|
*/
|
|
7897
8145
|
renderPorts(ctx) {
|
|
8146
|
+
if (this._attachToOutlineGetter?.()) {
|
|
8147
|
+
return;
|
|
8148
|
+
}
|
|
7898
8149
|
const shouldShow = this._showPortsAlways || this._ports.some((port) => port.hovered) || this._state === "hover" || this._state === "selected" || this._state === "dragging";
|
|
7899
8150
|
if (!shouldShow) {
|
|
7900
8151
|
return;
|
|
@@ -8177,6 +8428,52 @@ class Node extends Element {
|
|
|
8177
8428
|
height: this._height
|
|
8178
8429
|
};
|
|
8179
8430
|
}
|
|
8431
|
+
/**
|
|
8432
|
+
* Get point on outline at normalized param (0-1 along perimeter).
|
|
8433
|
+
* Base implementation uses rectangular outline. Override for non-rectangular shapes.
|
|
8434
|
+
*/
|
|
8435
|
+
getConnectionPointAtOutlineParam(param) {
|
|
8436
|
+
const bounds = this.getBounds();
|
|
8437
|
+
const { x, y, width: w, height: h } = bounds;
|
|
8438
|
+
const perimeter = 2 * (w + h);
|
|
8439
|
+
let s = (param % 1 + 1) % 1 * perimeter;
|
|
8440
|
+
if (s < w) return { x: x + s, y };
|
|
8441
|
+
s -= w;
|
|
8442
|
+
if (s < h) return { x: x + w, y: y + s };
|
|
8443
|
+
s -= h;
|
|
8444
|
+
if (s < w) return { x: x + w - s, y: y + h };
|
|
8445
|
+
s -= w;
|
|
8446
|
+
return { x, y: y + h - s };
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Get closest point on outline and its param (0-1).
|
|
8450
|
+
* Base implementation uses rectangular outline. Override for non-rectangular shapes.
|
|
8451
|
+
*/
|
|
8452
|
+
getClosestPointOnOutline(target) {
|
|
8453
|
+
const bounds = this.getBounds();
|
|
8454
|
+
const { x, y, width: w, height: h } = bounds;
|
|
8455
|
+
const P = 2 * (w + h);
|
|
8456
|
+
const projectSegment = (ax, ay, bx, by, segLen, segStartParam) => {
|
|
8457
|
+
const dx = bx - ax;
|
|
8458
|
+
const dy = by - ay;
|
|
8459
|
+
const lenSq = dx * dx + dy * dy;
|
|
8460
|
+
let t = lenSq > 0 ? ((target.x - ax) * dx + (target.y - ay) * dy) / lenSq : 0;
|
|
8461
|
+
t = Math.max(0, Math.min(1, t));
|
|
8462
|
+
const px = ax + t * dx;
|
|
8463
|
+
const py = ay + t * dy;
|
|
8464
|
+
const distSq = (target.x - px) ** 2 + (target.y - py) ** 2;
|
|
8465
|
+
const param = segStartParam + t * segLen / P;
|
|
8466
|
+
return { point: { x: px, y: py }, param, distSq };
|
|
8467
|
+
};
|
|
8468
|
+
let best = projectSegment(x, y, x + w, y, w, 0);
|
|
8469
|
+
let r = projectSegment(x + w, y, x + w, y + h, h, w / P);
|
|
8470
|
+
if (r.distSq < best.distSq) best = r;
|
|
8471
|
+
r = projectSegment(x + w, y + h, x, y + h, w, (w + h) / P);
|
|
8472
|
+
if (r.distSq < best.distSq) best = r;
|
|
8473
|
+
r = projectSegment(x, y + h, x, y, h, (2 * w + h) / P);
|
|
8474
|
+
if (r.distSq < best.distSq) best = r;
|
|
8475
|
+
return { point: best.point, param: best.param };
|
|
8476
|
+
}
|
|
8180
8477
|
/**
|
|
8181
8478
|
* Get intersection point on the shape outline toward the target.
|
|
8182
8479
|
* Override for non-rectangular shapes.
|
|
@@ -8724,6 +9021,30 @@ class CircleNode extends Node {
|
|
|
8724
9021
|
const dy = point.y - center.y;
|
|
8725
9022
|
return dx * dx / (rx * rx) + dy * dy / (ry * ry) <= 1;
|
|
8726
9023
|
}
|
|
9024
|
+
getConnectionPointAtOutlineParam(param) {
|
|
9025
|
+
const center = this.getCenter();
|
|
9026
|
+
const rx = this._width / 2;
|
|
9027
|
+
const ry = this._height / 2;
|
|
9028
|
+
const t = (param % 1 + 1) % 1 * Math.PI * 2;
|
|
9029
|
+
const angle2 = Math.PI * 1.5 - t;
|
|
9030
|
+
return {
|
|
9031
|
+
x: center.x + rx * Math.cos(angle2),
|
|
9032
|
+
y: center.y + ry * Math.sin(angle2)
|
|
9033
|
+
};
|
|
9034
|
+
}
|
|
9035
|
+
getClosestPointOnOutline(target) {
|
|
9036
|
+
const point = this.getOutlinePointToward(target);
|
|
9037
|
+
const center = this.getCenter();
|
|
9038
|
+
const rx = this._width / 2;
|
|
9039
|
+
const ry = this._height / 2;
|
|
9040
|
+
const t = Math.atan2(
|
|
9041
|
+
(point.y - center.y) / ry,
|
|
9042
|
+
(point.x - center.x) / rx
|
|
9043
|
+
);
|
|
9044
|
+
const param = (Math.PI * 1.5 - t) / (Math.PI * 2);
|
|
9045
|
+
const normalizedParam = (param % 1 + 1) % 1;
|
|
9046
|
+
return { point, param: normalizedParam };
|
|
9047
|
+
}
|
|
8727
9048
|
getOutlinePointToward(target) {
|
|
8728
9049
|
const center = this.getCenter();
|
|
8729
9050
|
const rx = this._width / 2;
|
|
@@ -8785,6 +9106,66 @@ class DiamondNode extends Node {
|
|
|
8785
9106
|
const dy = Math.abs(point.y - center.y);
|
|
8786
9107
|
return dx / hw + dy / hh <= 1;
|
|
8787
9108
|
}
|
|
9109
|
+
getConnectionPointAtOutlineParam(param) {
|
|
9110
|
+
const center = this.getCenter();
|
|
9111
|
+
const hw = this._width / 2;
|
|
9112
|
+
const hh = this._height / 2;
|
|
9113
|
+
const vertices = [
|
|
9114
|
+
{ x: center.x, y: center.y - hh },
|
|
9115
|
+
{ x: center.x + hw, y: center.y },
|
|
9116
|
+
{ x: center.x, y: center.y + hh },
|
|
9117
|
+
{ x: center.x - hw, y: center.y }
|
|
9118
|
+
];
|
|
9119
|
+
const segLen = Math.sqrt(hw * hw + hh * hh);
|
|
9120
|
+
const perimeter = 4 * segLen;
|
|
9121
|
+
let s = (param % 1 + 1) % 1 * perimeter;
|
|
9122
|
+
for (let i = 0; i < 4; i++) {
|
|
9123
|
+
if (s < segLen) {
|
|
9124
|
+
const a = vertices[i];
|
|
9125
|
+
const b = vertices[(i + 1) % 4];
|
|
9126
|
+
const t = s / segLen;
|
|
9127
|
+
return {
|
|
9128
|
+
x: a.x + t * (b.x - a.x),
|
|
9129
|
+
y: a.y + t * (b.y - a.y)
|
|
9130
|
+
};
|
|
9131
|
+
}
|
|
9132
|
+
s -= segLen;
|
|
9133
|
+
}
|
|
9134
|
+
return vertices[0];
|
|
9135
|
+
}
|
|
9136
|
+
getClosestPointOnOutline(target) {
|
|
9137
|
+
const center = this.getCenter();
|
|
9138
|
+
const hw = this._width / 2;
|
|
9139
|
+
const hh = this._height / 2;
|
|
9140
|
+
const vertices = [
|
|
9141
|
+
{ x: center.x, y: center.y - hh },
|
|
9142
|
+
{ x: center.x + hw, y: center.y },
|
|
9143
|
+
{ x: center.x, y: center.y + hh },
|
|
9144
|
+
{ x: center.x - hw, y: center.y }
|
|
9145
|
+
];
|
|
9146
|
+
const segLen = Math.sqrt(hw * hw + hh * hh);
|
|
9147
|
+
const P = 4 * segLen;
|
|
9148
|
+
const projectSegment = (ax, ay, bx, by, segStartParam) => {
|
|
9149
|
+
const dx = bx - ax;
|
|
9150
|
+
const dy = by - ay;
|
|
9151
|
+
const lenSq = dx * dx + dy * dy;
|
|
9152
|
+
let t = lenSq > 0 ? ((target.x - ax) * dx + (target.y - ay) * dy) / lenSq : 0;
|
|
9153
|
+
t = Math.max(0, Math.min(1, t));
|
|
9154
|
+
const px = ax + t * dx;
|
|
9155
|
+
const py = ay + t * dy;
|
|
9156
|
+
const distSq = (target.x - px) ** 2 + (target.y - py) ** 2;
|
|
9157
|
+
const param = segStartParam + t * segLen / P;
|
|
9158
|
+
return { point: { x: px, y: py }, param, distSq };
|
|
9159
|
+
};
|
|
9160
|
+
let best = projectSegment(vertices[0].x, vertices[0].y, vertices[1].x, vertices[1].y, 0);
|
|
9161
|
+
for (let i = 1; i < 4; i++) {
|
|
9162
|
+
const a = vertices[i];
|
|
9163
|
+
const b = vertices[(i + 1) % 4];
|
|
9164
|
+
const r = projectSegment(a.x, a.y, b.x, b.y, i * segLen / P);
|
|
9165
|
+
if (r.distSq < best.distSq) best = r;
|
|
9166
|
+
}
|
|
9167
|
+
return { point: best.point, param: best.param };
|
|
9168
|
+
}
|
|
8788
9169
|
getOutlinePointToward(target) {
|
|
8789
9170
|
const center = this.getCenter();
|
|
8790
9171
|
const dx = target.x - center.x;
|
|
@@ -10556,6 +10937,7 @@ class MiniMap {
|
|
|
10556
10937
|
width: options.width ?? 180,
|
|
10557
10938
|
height: options.height ?? 120,
|
|
10558
10939
|
padding: options.padding ?? 12,
|
|
10940
|
+
contentMargin: options.contentMargin ?? 0,
|
|
10559
10941
|
backgroundColor: options.backgroundColor ?? "rgba(24, 24, 27, 0.5)",
|
|
10560
10942
|
borderColor: options.borderColor ?? "#52525b",
|
|
10561
10943
|
viewportColor: options.viewportColor ?? "#22c55e",
|
|
@@ -10694,14 +11076,27 @@ class MiniMap {
|
|
|
10694
11076
|
if (!rawBounds) {
|
|
10695
11077
|
return null;
|
|
10696
11078
|
}
|
|
11079
|
+
const margin = this.options.contentMargin;
|
|
11080
|
+
const expandedContentBounds = {
|
|
11081
|
+
x: rawBounds.x - margin,
|
|
11082
|
+
y: rawBounds.y - margin,
|
|
11083
|
+
width: rawBounds.width + 2 * margin,
|
|
11084
|
+
height: rawBounds.height + 2 * margin
|
|
11085
|
+
};
|
|
10697
11086
|
const { width, height, padding, anchor } = this.options;
|
|
10698
11087
|
const x = anchor === "bottom-left" || anchor === "top-left" ? padding : renderer.width - width - padding;
|
|
10699
11088
|
const y = anchor === "top-left" || anchor === "top-right" ? padding : renderer.height - height - padding;
|
|
10700
11089
|
const viewportBounds = this.getViewportBounds(renderer);
|
|
10701
|
-
const unionMinX = Math.min(
|
|
10702
|
-
const unionMinY = Math.min(
|
|
10703
|
-
const unionMaxX = Math.max(
|
|
10704
|
-
|
|
11090
|
+
const unionMinX = Math.min(expandedContentBounds.x, viewportBounds.x);
|
|
11091
|
+
const unionMinY = Math.min(expandedContentBounds.y, viewportBounds.y);
|
|
11092
|
+
const unionMaxX = Math.max(
|
|
11093
|
+
expandedContentBounds.x + expandedContentBounds.width,
|
|
11094
|
+
viewportBounds.x + viewportBounds.width
|
|
11095
|
+
);
|
|
11096
|
+
const unionMaxY = Math.max(
|
|
11097
|
+
expandedContentBounds.y + expandedContentBounds.height,
|
|
11098
|
+
viewportBounds.y + viewportBounds.height
|
|
11099
|
+
);
|
|
10705
11100
|
const bounds = {
|
|
10706
11101
|
x: unionMinX,
|
|
10707
11102
|
y: unionMinY,
|