@ngroznykh/papirus 0.7.3 → 0.8.1
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/dist/core/ConnectionManager.d.ts +5 -0
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/EdgeEndpointUpdater.d.ts +17 -0
- package/dist/core/EdgeEndpointUpdater.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/paths/PathStrategy.d.ts +2 -0
- package/dist/elements/paths/PathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/PolylinePathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/index.d.ts +2 -0
- package/dist/elements/paths/index.d.ts.map +1 -1
- package/dist/elements/paths/routeOrthogonalAround.d.ts +60 -0
- package/dist/elements/paths/routeOrthogonalAround.d.ts.map +1 -0
- package/dist/papirus.js +1171 -62
- package/dist/papirus.js.map +1 -1
- package/dist/utils/direction.d.ts +11 -2
- package/dist/utils/direction.d.ts.map +1 -1
- package/dist/utils/routeDebug.d.ts +14 -0
- package/dist/utils/routeDebug.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -1672,10 +1672,26 @@ var NavigationManager = class extends EventEmitter {
|
|
|
1672
1672
|
//#endregion
|
|
1673
1673
|
//#region src/utils/direction.ts
|
|
1674
1674
|
/**
|
|
1675
|
-
* Map outline param [0,1) to cardinal direction
|
|
1675
|
+
* Map outline param [0,1) to cardinal direction.
|
|
1676
|
+
*
|
|
1677
|
+
* When `bounds` are provided, uses the same perimeter walk as
|
|
1678
|
+
* `Node.getConnectionPointAtOutlineParam` (top → right → bottom → left).
|
|
1679
|
+
* Equal-quarter mapping is only a fallback for square-ish shapes / legacy callers —
|
|
1680
|
+
* on wide/short nodes it wrongly labels the left half of the bottom edge as `left`.
|
|
1676
1681
|
*/
|
|
1677
|
-
function getDirectionFromOutlineParam(param) {
|
|
1682
|
+
function getDirectionFromOutlineParam(param, bounds) {
|
|
1678
1683
|
const p = (param % 1 + 1) % 1;
|
|
1684
|
+
const w = bounds?.width ?? 0;
|
|
1685
|
+
const h = bounds?.height ?? 0;
|
|
1686
|
+
if (w > 0 && h > 0) {
|
|
1687
|
+
let s = p * (2 * (w + h));
|
|
1688
|
+
if (s < w) return "top";
|
|
1689
|
+
s -= w;
|
|
1690
|
+
if (s < h) return "right";
|
|
1691
|
+
s -= h;
|
|
1692
|
+
if (s < w) return "bottom";
|
|
1693
|
+
return "left";
|
|
1694
|
+
}
|
|
1679
1695
|
if (p < .25) return "top";
|
|
1680
1696
|
if (p < .5) return "right";
|
|
1681
1697
|
if (p < .75) return "bottom";
|
|
@@ -1946,14 +1962,14 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1946
1962
|
snappedPoint = bestResult.point;
|
|
1947
1963
|
this.reconnectingOutlineParam = bestResult.param;
|
|
1948
1964
|
this.reconnectingTargetNodeId = bestResult.node.id;
|
|
1949
|
-
snappedDir = getDirectionFromOutlineParam(bestResult.param);
|
|
1965
|
+
snappedDir = getDirectionFromOutlineParam(bestResult.param, bestResult.node.getBounds());
|
|
1950
1966
|
if (this.reconnectingEndpoint === "start" ? toNode : fromNode) {
|
|
1951
1967
|
const alignRef = edge.isEditablePolyline() ? this.getNearestBendPointForAxisAlignment(edge, this.reconnectingEndpoint) : this.reconnectingEndpoint === "start" ? this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint) : this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1952
1968
|
snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, alignRef);
|
|
1953
1969
|
const reprojected = targetNode.getClosestPointOnOutline(snappedPoint);
|
|
1954
1970
|
snappedPoint = reprojected.point;
|
|
1955
1971
|
this.reconnectingOutlineParam = reprojected.param;
|
|
1956
|
-
snappedDir = getDirectionFromOutlineParam(reprojected.param);
|
|
1972
|
+
snappedDir = getDirectionFromOutlineParam(reprojected.param, targetNode.getBounds());
|
|
1957
1973
|
}
|
|
1958
1974
|
} else {
|
|
1959
1975
|
this.reconnectingOutlineParam = null;
|
|
@@ -1976,15 +1992,38 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1976
1992
|
if (toNode) {
|
|
1977
1993
|
const target = this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint);
|
|
1978
1994
|
const toDir = this.getTargetDirForReconnect(edge, "to");
|
|
1979
|
-
|
|
1995
|
+
const sourceId = targetNode?.id ?? edge.from.nodeId;
|
|
1996
|
+
edge.updateEndpoints(snappedPoint, target, snappedDir, toDir, this.buildReconnectRoutingOptions(sourceId, toNode.id));
|
|
1980
1997
|
}
|
|
1981
1998
|
} else if (fromNode) {
|
|
1982
1999
|
const start = this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1983
2000
|
const fromDir = this.getTargetDirForReconnect(edge, "from");
|
|
1984
|
-
edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir);
|
|
2001
|
+
edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir, this.buildReconnectRoutingOptions(fromNode.id, targetNode?.id));
|
|
1985
2002
|
}
|
|
1986
2003
|
this.renderer.markDirty();
|
|
1987
2004
|
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Match DiagramRenderer.getNodeObstacles padding and EdgeEndpointUpdater roles
|
|
2007
|
+
* so reconnect preview uses the same polyline routing as updateAll.
|
|
2008
|
+
*/
|
|
2009
|
+
buildReconnectRoutingOptions(sourceNodeId, targetNodeId) {
|
|
2010
|
+
const obstacles = [];
|
|
2011
|
+
for (const node of this.renderer.nodes.values()) {
|
|
2012
|
+
let role = "other";
|
|
2013
|
+
if (node.id === sourceNodeId) role = "source";
|
|
2014
|
+
else if (node.id === targetNodeId) role = "target";
|
|
2015
|
+
const pad = 4;
|
|
2016
|
+
obstacles.push({
|
|
2017
|
+
id: node.id,
|
|
2018
|
+
x: node.x - pad,
|
|
2019
|
+
y: node.y - pad,
|
|
2020
|
+
width: node.width + pad * 2,
|
|
2021
|
+
height: node.height + pad * 2,
|
|
2022
|
+
role
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
return { obstacles };
|
|
2026
|
+
}
|
|
1988
2027
|
getTargetPointForReconnect(edge, node, endpoint, fallbackPoint) {
|
|
1989
2028
|
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1990
2029
|
if (this.attachToOutline && ep.outlineParam !== void 0) return node.getConnectionPointAtOutlineParam(ep.outlineParam);
|
|
@@ -1994,7 +2033,10 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1994
2033
|
}
|
|
1995
2034
|
getTargetDirForReconnect(edge, endpoint) {
|
|
1996
2035
|
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1997
|
-
if (this.attachToOutline && ep.outlineParam !== void 0)
|
|
2036
|
+
if (this.attachToOutline && ep.outlineParam !== void 0) {
|
|
2037
|
+
const node = ep.nodeId ? this.renderer.getNode(ep.nodeId) : void 0;
|
|
2038
|
+
return getDirectionFromOutlineParam(ep.outlineParam, node?.getBounds());
|
|
2039
|
+
}
|
|
1998
2040
|
return (ep.portId?.replace(ANCHOR_PORT_PREFIX, ""))?.split(":")[0];
|
|
1999
2041
|
}
|
|
2000
2042
|
/**
|
|
@@ -2309,8 +2351,9 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2309
2351
|
ctx.lineTo(end.x, end.y);
|
|
2310
2352
|
ctx.stroke();
|
|
2311
2353
|
} else {
|
|
2312
|
-
const
|
|
2313
|
-
const
|
|
2354
|
+
const previewTarget = this.previewTargetNodeId != null ? this.renderer.getNode(this.previewTargetNodeId) : void 0;
|
|
2355
|
+
const fromDir = this.sourceOutlineParam !== null ? getDirectionFromOutlineParam(this.sourceOutlineParam, this.sourceNode?.getBounds()) : this.sourceAnchorId?.split(":")[0];
|
|
2356
|
+
const toDir = this.previewTargetOutlineParam !== null ? getDirectionFromOutlineParam(this.previewTargetOutlineParam, previewTarget?.getBounds()) : this.previewTargetAnchorId?.split(":")[0];
|
|
2314
2357
|
this.drawBezierPreview(ctx, start, end, fromDir, toDir);
|
|
2315
2358
|
}
|
|
2316
2359
|
ctx.setLineDash([]);
|
|
@@ -3777,7 +3820,781 @@ var StraightPathStrategy = class {
|
|
|
3777
3820
|
}
|
|
3778
3821
|
};
|
|
3779
3822
|
//#endregion
|
|
3823
|
+
//#region src/elements/paths/routeOrthogonalAround.ts
|
|
3824
|
+
var DEFAULT_MARGIN = 12;
|
|
3825
|
+
var DEFAULT_EXIT = 20;
|
|
3826
|
+
var BEND_PENALTY = .001;
|
|
3827
|
+
/** Prefer mid-gap over hugging obstacle borders (same Manhattan, fewer bends). */
|
|
3828
|
+
var CONTOUR_BAND = 18;
|
|
3829
|
+
var CONTOUR_PENALTY = 140;
|
|
3830
|
+
function expandRect(r, m) {
|
|
3831
|
+
return {
|
|
3832
|
+
...r,
|
|
3833
|
+
x: r.x - m,
|
|
3834
|
+
y: r.y - m,
|
|
3835
|
+
width: r.width + 2 * m,
|
|
3836
|
+
height: r.height + 2 * m
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Diagram padding + router margin can seal a real visual gap between stacked nodes
|
|
3841
|
+
* (e.g. ArchiMate BP header above a Product ~12px apart, 8px pad each side).
|
|
3842
|
+
* Shrink facing sides of the expanded rects so a mid-gap corridor stays open.
|
|
3843
|
+
*/
|
|
3844
|
+
function preserveFacingGap(expandedA, expandedB, rawA, rawB, minCorridor = 2) {
|
|
3845
|
+
let a = { ...expandedA };
|
|
3846
|
+
let b = { ...expandedB };
|
|
3847
|
+
if (rawB.y - (rawA.y + rawA.height) >= minCorridor) {
|
|
3848
|
+
const mid = (rawA.y + rawA.height + rawB.y) / 2;
|
|
3849
|
+
const half = minCorridor / 2;
|
|
3850
|
+
const aBottom = mid - half;
|
|
3851
|
+
const bTop = mid + half;
|
|
3852
|
+
if (a.y + a.height > aBottom) a = {
|
|
3853
|
+
...a,
|
|
3854
|
+
height: Math.max(0, aBottom - a.y)
|
|
3855
|
+
};
|
|
3856
|
+
if (b.y < bTop) b = {
|
|
3857
|
+
...b,
|
|
3858
|
+
height: Math.max(0, b.y + b.height - bTop),
|
|
3859
|
+
y: bTop
|
|
3860
|
+
};
|
|
3861
|
+
return [a, b];
|
|
3862
|
+
}
|
|
3863
|
+
if (rawA.y - (rawB.y + rawB.height) >= minCorridor) {
|
|
3864
|
+
const mid = (rawB.y + rawB.height + rawA.y) / 2;
|
|
3865
|
+
const half = minCorridor / 2;
|
|
3866
|
+
const bBottom = mid - half;
|
|
3867
|
+
const aTop = mid + half;
|
|
3868
|
+
if (b.y + b.height > bBottom) b = {
|
|
3869
|
+
...b,
|
|
3870
|
+
height: Math.max(0, bBottom - b.y)
|
|
3871
|
+
};
|
|
3872
|
+
if (a.y < aTop) a = {
|
|
3873
|
+
...a,
|
|
3874
|
+
height: Math.max(0, a.y + a.height - aTop),
|
|
3875
|
+
y: aTop
|
|
3876
|
+
};
|
|
3877
|
+
return [a, b];
|
|
3878
|
+
}
|
|
3879
|
+
if (rawB.x - (rawA.x + rawA.width) >= minCorridor) {
|
|
3880
|
+
const mid = (rawA.x + rawA.width + rawB.x) / 2;
|
|
3881
|
+
const half = minCorridor / 2;
|
|
3882
|
+
const aRight = mid - half;
|
|
3883
|
+
const bLeft = mid + half;
|
|
3884
|
+
if (a.x + a.width > aRight) a = {
|
|
3885
|
+
...a,
|
|
3886
|
+
width: Math.max(0, aRight - a.x)
|
|
3887
|
+
};
|
|
3888
|
+
if (b.x < bLeft) b = {
|
|
3889
|
+
...b,
|
|
3890
|
+
width: Math.max(0, b.x + b.width - bLeft),
|
|
3891
|
+
x: bLeft
|
|
3892
|
+
};
|
|
3893
|
+
return [a, b];
|
|
3894
|
+
}
|
|
3895
|
+
if (rawA.x - (rawB.x + rawB.width) >= minCorridor) {
|
|
3896
|
+
const mid = (rawB.x + rawB.width + rawA.x) / 2;
|
|
3897
|
+
const half = minCorridor / 2;
|
|
3898
|
+
const bRight = mid - half;
|
|
3899
|
+
const aLeft = mid + half;
|
|
3900
|
+
if (b.x + b.width > bRight) b = {
|
|
3901
|
+
...b,
|
|
3902
|
+
width: Math.max(0, bRight - b.x)
|
|
3903
|
+
};
|
|
3904
|
+
if (a.x < aLeft) a = {
|
|
3905
|
+
...a,
|
|
3906
|
+
width: Math.max(0, a.x + a.width - aLeft),
|
|
3907
|
+
x: aLeft
|
|
3908
|
+
};
|
|
3909
|
+
return [a, b];
|
|
3910
|
+
}
|
|
3911
|
+
return [a, b];
|
|
3912
|
+
}
|
|
3913
|
+
function insetRect(r, m) {
|
|
3914
|
+
if (m <= 0) return r;
|
|
3915
|
+
const width = r.width - 2 * m;
|
|
3916
|
+
const height = r.height - 2 * m;
|
|
3917
|
+
if (width <= 1 || height <= 1) return null;
|
|
3918
|
+
return {
|
|
3919
|
+
...r,
|
|
3920
|
+
x: r.x + m,
|
|
3921
|
+
y: r.y + m,
|
|
3922
|
+
width,
|
|
3923
|
+
height
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
/** Midline of a positive gap between two raw rects, if any. */
|
|
3927
|
+
function facingGapMid(rawA, rawB) {
|
|
3928
|
+
if (rawA.y + rawA.height < rawB.y) return {
|
|
3929
|
+
x: (rawA.x + rawA.width / 2 + rawB.x + rawB.width / 2) / 2,
|
|
3930
|
+
y: (rawA.y + rawA.height + rawB.y) / 2
|
|
3931
|
+
};
|
|
3932
|
+
if (rawB.y + rawB.height < rawA.y) return {
|
|
3933
|
+
x: (rawA.x + rawA.width / 2 + rawB.x + rawB.width / 2) / 2,
|
|
3934
|
+
y: (rawB.y + rawB.height + rawA.y) / 2
|
|
3935
|
+
};
|
|
3936
|
+
if (rawA.x + rawA.width < rawB.x) return {
|
|
3937
|
+
x: (rawA.x + rawA.width + rawB.x) / 2,
|
|
3938
|
+
y: (rawA.y + rawA.height / 2 + rawB.y + rawB.height / 2) / 2
|
|
3939
|
+
};
|
|
3940
|
+
if (rawB.x + rawB.width < rawA.x) return {
|
|
3941
|
+
x: (rawB.x + rawB.width + rawA.x) / 2,
|
|
3942
|
+
y: (rawA.y + rawA.height / 2 + rawB.y + rawB.height / 2) / 2
|
|
3943
|
+
};
|
|
3944
|
+
return null;
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* Obstacles arrive pre-padded (diagram pad 4–8). Peel common pad amounts so a
|
|
3948
|
+
* real visual gap between stacked nodes is visible to corridor / endEntry logic.
|
|
3949
|
+
*/
|
|
3950
|
+
function peelForFacingGap(a, b) {
|
|
3951
|
+
for (const peel of [
|
|
3952
|
+
0,
|
|
3953
|
+
4,
|
|
3954
|
+
8
|
|
3955
|
+
]) {
|
|
3956
|
+
const aa = insetRect(a, peel);
|
|
3957
|
+
const bb = insetRect(b, peel);
|
|
3958
|
+
if (!aa || !bb) continue;
|
|
3959
|
+
if (facingGapMid(aa, bb)) return {
|
|
3960
|
+
a: aa,
|
|
3961
|
+
b: bb
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
return null;
|
|
3965
|
+
}
|
|
3966
|
+
function moveByDir$1(p, dir, d) {
|
|
3967
|
+
switch (dir) {
|
|
3968
|
+
case "top": return {
|
|
3969
|
+
x: p.x,
|
|
3970
|
+
y: p.y - d
|
|
3971
|
+
};
|
|
3972
|
+
case "bottom": return {
|
|
3973
|
+
x: p.x,
|
|
3974
|
+
y: p.y + d
|
|
3975
|
+
};
|
|
3976
|
+
case "left": return {
|
|
3977
|
+
x: p.x - d,
|
|
3978
|
+
y: p.y
|
|
3979
|
+
};
|
|
3980
|
+
case "right": return {
|
|
3981
|
+
x: p.x + d,
|
|
3982
|
+
y: p.y
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
function pointInOpenInterior(p, r) {
|
|
3987
|
+
return p.x > r.x && p.x < r.x + r.width && p.y > r.y && p.y < r.y + r.height;
|
|
3988
|
+
}
|
|
3989
|
+
/** Axis-aligned segment vs open interior of a rect (boundary grazing allowed). */
|
|
3990
|
+
function segmentIntersectsOpenInterior(a, b, r) {
|
|
3991
|
+
if (Math.abs(a.x - b.x) < .001) {
|
|
3992
|
+
const x = a.x;
|
|
3993
|
+
const y1 = Math.min(a.y, b.y);
|
|
3994
|
+
const y2 = Math.max(a.y, b.y);
|
|
3995
|
+
return x > r.x && x < r.x + r.width && y2 > r.y && y1 < r.y + r.height;
|
|
3996
|
+
}
|
|
3997
|
+
if (Math.abs(a.y - b.y) < .001) {
|
|
3998
|
+
const y = a.y;
|
|
3999
|
+
const x1 = Math.min(a.x, b.x);
|
|
4000
|
+
const x2 = Math.max(a.x, b.x);
|
|
4001
|
+
return y > r.y && y < r.y + r.height && x2 > r.x && x1 < r.x + r.width;
|
|
4002
|
+
}
|
|
4003
|
+
return true;
|
|
4004
|
+
}
|
|
4005
|
+
function segmentHitsObstacles(a, b, obstacles) {
|
|
4006
|
+
return obstacles.some((o) => segmentIntersectsOpenInterior(a, b, o));
|
|
4007
|
+
}
|
|
4008
|
+
function manhattan$1(a, b) {
|
|
4009
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Extra cost when a segment runs parallel to an obstacle edge inside CONTOUR_BAND.
|
|
4013
|
+
* Scaled by segment length so a long “lick” along a wide BP costs far more than a short stub.
|
|
4014
|
+
*/
|
|
4015
|
+
function contourProximityPenalty(a, b, obstacles) {
|
|
4016
|
+
if (obstacles.length === 0) return 0;
|
|
4017
|
+
const segLen = manhattan$1(a, b);
|
|
4018
|
+
if (segLen < 1) return 0;
|
|
4019
|
+
let minDist = Infinity;
|
|
4020
|
+
if (Math.abs(a.y - b.y) < .001) {
|
|
4021
|
+
const y = a.y;
|
|
4022
|
+
const x1 = Math.min(a.x, b.x);
|
|
4023
|
+
const x2 = Math.max(a.x, b.x);
|
|
4024
|
+
for (const o of obstacles) {
|
|
4025
|
+
if (x2 < o.x - 1 || x1 > o.x + o.width + 1) continue;
|
|
4026
|
+
minDist = Math.min(minDist, Math.abs(y - o.y), Math.abs(y - (o.y + o.height)));
|
|
4027
|
+
}
|
|
4028
|
+
} else if (Math.abs(a.x - b.x) < .001) {
|
|
4029
|
+
const x = a.x;
|
|
4030
|
+
const y1 = Math.min(a.y, b.y);
|
|
4031
|
+
const y2 = Math.max(a.y, b.y);
|
|
4032
|
+
for (const o of obstacles) {
|
|
4033
|
+
if (y2 < o.y - 1 || y1 > o.y + o.height + 1) continue;
|
|
4034
|
+
minDist = Math.min(minDist, Math.abs(x - o.x), Math.abs(x - (o.x + o.width)));
|
|
4035
|
+
}
|
|
4036
|
+
} else return 0;
|
|
4037
|
+
if (!Number.isFinite(minDist) || minDist >= CONTOUR_BAND) return 0;
|
|
4038
|
+
const proximity = 1 - minDist / CONTOUR_BAND;
|
|
4039
|
+
return segLen * (CONTOUR_PENALTY / 10) * proximity;
|
|
4040
|
+
}
|
|
4041
|
+
/** Push along dir until outside expanded parent (and at least exitDistance). */
|
|
4042
|
+
function computeStartExit(from, fromDir, parent, margin, exitDistance) {
|
|
4043
|
+
let p = moveByDir$1(from, fromDir, exitDistance);
|
|
4044
|
+
if (!parent) return p;
|
|
4045
|
+
const expanded = expandRect(parent, margin);
|
|
4046
|
+
let guard = 0;
|
|
4047
|
+
while (pointInOpenInterior(p, expanded) && guard++ < 64) p = moveByDir$1(p, fromDir, exitDistance);
|
|
4048
|
+
if (pointInOpenInterior(p, expanded)) {
|
|
4049
|
+
const e = expanded;
|
|
4050
|
+
switch (fromDir) {
|
|
4051
|
+
case "left":
|
|
4052
|
+
p = {
|
|
4053
|
+
x: e.x - 1,
|
|
4054
|
+
y: from.y
|
|
4055
|
+
};
|
|
4056
|
+
break;
|
|
4057
|
+
case "right":
|
|
4058
|
+
p = {
|
|
4059
|
+
x: e.x + e.width + 1,
|
|
4060
|
+
y: from.y
|
|
4061
|
+
};
|
|
4062
|
+
break;
|
|
4063
|
+
case "top":
|
|
4064
|
+
p = {
|
|
4065
|
+
x: from.x,
|
|
4066
|
+
y: e.y - 1
|
|
4067
|
+
};
|
|
4068
|
+
break;
|
|
4069
|
+
case "bottom":
|
|
4070
|
+
p = {
|
|
4071
|
+
x: from.x,
|
|
4072
|
+
y: e.y + e.height + 1
|
|
4073
|
+
};
|
|
4074
|
+
break;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
return p;
|
|
4078
|
+
}
|
|
4079
|
+
function computeEndEntry(to, toDir, exitDistance, rawTarget, rawNeighbor) {
|
|
4080
|
+
const stub = moveByDir$1(to, toDir, exitDistance);
|
|
4081
|
+
if (!rawTarget || !rawNeighbor) return stub;
|
|
4082
|
+
const mid = facingGapMid(rawTarget, rawNeighbor);
|
|
4083
|
+
if (!mid) return stub;
|
|
4084
|
+
switch (toDir) {
|
|
4085
|
+
case "bottom":
|
|
4086
|
+
if (mid.y > to.y && rawNeighbor.y >= rawTarget.y + rawTarget.height) return {
|
|
4087
|
+
x: to.x,
|
|
4088
|
+
y: mid.y
|
|
4089
|
+
};
|
|
4090
|
+
break;
|
|
4091
|
+
case "top":
|
|
4092
|
+
if (mid.y < to.y && rawNeighbor.y + rawNeighbor.height <= rawTarget.y) return {
|
|
4093
|
+
x: to.x,
|
|
4094
|
+
y: mid.y
|
|
4095
|
+
};
|
|
4096
|
+
break;
|
|
4097
|
+
case "right":
|
|
4098
|
+
if (mid.x > to.x && rawNeighbor.x >= rawTarget.x + rawTarget.width) return {
|
|
4099
|
+
x: mid.x,
|
|
4100
|
+
y: to.y
|
|
4101
|
+
};
|
|
4102
|
+
break;
|
|
4103
|
+
case "left":
|
|
4104
|
+
if (mid.x < to.x && rawNeighbor.x + rawNeighbor.width <= rawTarget.x) return {
|
|
4105
|
+
x: mid.x,
|
|
4106
|
+
y: to.y
|
|
4107
|
+
};
|
|
4108
|
+
break;
|
|
4109
|
+
}
|
|
4110
|
+
return stub;
|
|
4111
|
+
}
|
|
4112
|
+
/**
|
|
4113
|
+
* When nodes face each other closer than 2×exitDistance, fixed stubs cross
|
|
4114
|
+
* (startExit past endEntry). That draws a U-turn "tail" at the first bend.
|
|
4115
|
+
* Meet both stubs on the mid-gap instead.
|
|
4116
|
+
*/
|
|
4117
|
+
function clampFacingExitEntry(from, to, fromDir, toDir, startExit, endEntry) {
|
|
4118
|
+
if (fromDir === "bottom" && toDir === "top" && to.y > from.y && startExit.y > endEntry.y) {
|
|
4119
|
+
const midY = (from.y + to.y) / 2;
|
|
4120
|
+
return {
|
|
4121
|
+
startExit: {
|
|
4122
|
+
x: startExit.x,
|
|
4123
|
+
y: midY
|
|
4124
|
+
},
|
|
4125
|
+
endEntry: {
|
|
4126
|
+
x: endEntry.x,
|
|
4127
|
+
y: midY
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
if (fromDir === "top" && toDir === "bottom" && to.y < from.y && startExit.y < endEntry.y) {
|
|
4132
|
+
const midY = (from.y + to.y) / 2;
|
|
4133
|
+
return {
|
|
4134
|
+
startExit: {
|
|
4135
|
+
x: startExit.x,
|
|
4136
|
+
y: midY
|
|
4137
|
+
},
|
|
4138
|
+
endEntry: {
|
|
4139
|
+
x: endEntry.x,
|
|
4140
|
+
y: midY
|
|
4141
|
+
}
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
if (fromDir === "right" && toDir === "left" && to.x > from.x && startExit.x > endEntry.x) {
|
|
4145
|
+
const midX = (from.x + to.x) / 2;
|
|
4146
|
+
return {
|
|
4147
|
+
startExit: {
|
|
4148
|
+
x: midX,
|
|
4149
|
+
y: startExit.y
|
|
4150
|
+
},
|
|
4151
|
+
endEntry: {
|
|
4152
|
+
x: midX,
|
|
4153
|
+
y: endEntry.y
|
|
4154
|
+
}
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
if (fromDir === "left" && toDir === "right" && to.x < from.x && startExit.x < endEntry.x) {
|
|
4158
|
+
const midX = (from.x + to.x) / 2;
|
|
4159
|
+
return {
|
|
4160
|
+
startExit: {
|
|
4161
|
+
x: midX,
|
|
4162
|
+
y: startExit.y
|
|
4163
|
+
},
|
|
4164
|
+
endEntry: {
|
|
4165
|
+
x: midX,
|
|
4166
|
+
y: endEntry.y
|
|
4167
|
+
}
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
return {
|
|
4171
|
+
startExit,
|
|
4172
|
+
endEntry
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
function simplifyPath$1(path) {
|
|
4176
|
+
const out = [];
|
|
4177
|
+
for (const p of path) {
|
|
4178
|
+
const prev = out[out.length - 1];
|
|
4179
|
+
if (!prev || Math.abs(prev.x - p.x) > .001 || Math.abs(prev.y - p.y) > .001) out.push(p);
|
|
4180
|
+
}
|
|
4181
|
+
let i = 0;
|
|
4182
|
+
while (i + 2 < out.length) {
|
|
4183
|
+
const a = out[i];
|
|
4184
|
+
const b = out[i + 1];
|
|
4185
|
+
const c = out[i + 2];
|
|
4186
|
+
const colinearV = Math.abs(a.x - b.x) < .001 && Math.abs(b.x - c.x) < .001;
|
|
4187
|
+
const colinearH = Math.abs(a.y - b.y) < .001 && Math.abs(b.y - c.y) < .001;
|
|
4188
|
+
const colinear = colinearV || colinearH;
|
|
4189
|
+
const uturn = colinearV && (a.y < c.y && c.y < b.y || b.y < c.y && c.y < a.y) || colinearH && (a.x < c.x && c.x < b.x || b.x < c.x && c.x < a.x);
|
|
4190
|
+
const spur = colinearV && (b.y < a.y && a.y < c.y || c.y < a.y && a.y < b.y) || colinearH && (b.x < a.x && a.x < c.x || c.x < a.x && a.x < b.x);
|
|
4191
|
+
if (uturn || spur || colinear && i > 0 && i + 2 < out.length - 1) out.splice(i + 1, 1);
|
|
4192
|
+
else i++;
|
|
4193
|
+
}
|
|
4194
|
+
return out;
|
|
4195
|
+
}
|
|
4196
|
+
function unionBBox(obstacles) {
|
|
4197
|
+
let x1 = Infinity;
|
|
4198
|
+
let y1 = Infinity;
|
|
4199
|
+
let x2 = -Infinity;
|
|
4200
|
+
let y2 = -Infinity;
|
|
4201
|
+
for (const o of obstacles) {
|
|
4202
|
+
x1 = Math.min(x1, o.x);
|
|
4203
|
+
y1 = Math.min(y1, o.y);
|
|
4204
|
+
x2 = Math.max(x2, o.x + o.width);
|
|
4205
|
+
y2 = Math.max(y2, o.y + o.height);
|
|
4206
|
+
}
|
|
4207
|
+
return {
|
|
4208
|
+
x: x1,
|
|
4209
|
+
y: y1,
|
|
4210
|
+
width: x2 - x1,
|
|
4211
|
+
height: y2 - y1
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, margin) {
|
|
4215
|
+
const xSet = new Set([startExit.x, endEntry.x]);
|
|
4216
|
+
const ySet = new Set([startExit.y, endEntry.y]);
|
|
4217
|
+
for (const o of obstacles) {
|
|
4218
|
+
xSet.add(o.x);
|
|
4219
|
+
xSet.add(o.x + o.width);
|
|
4220
|
+
ySet.add(o.y);
|
|
4221
|
+
ySet.add(o.y + o.height);
|
|
4222
|
+
xSet.add(o.x - 1);
|
|
4223
|
+
xSet.add(o.x + o.width + 1);
|
|
4224
|
+
ySet.add(o.y - 1);
|
|
4225
|
+
ySet.add(o.y + o.height + 1);
|
|
4226
|
+
}
|
|
4227
|
+
if (parent && target) {
|
|
4228
|
+
const addGapY = (gapY) => {
|
|
4229
|
+
ySet.add(gapY);
|
|
4230
|
+
const left = Math.min(parent.x, target.x, startExit.x, endEntry.x);
|
|
4231
|
+
const right = Math.max(parent.x + parent.width, target.x + target.width, startExit.x, endEntry.x);
|
|
4232
|
+
xSet.add(left);
|
|
4233
|
+
xSet.add(right);
|
|
4234
|
+
xSet.add(startExit.x);
|
|
4235
|
+
xSet.add(endEntry.x);
|
|
4236
|
+
};
|
|
4237
|
+
const addGapX = (gapX) => {
|
|
4238
|
+
xSet.add(gapX);
|
|
4239
|
+
const top = Math.min(parent.y, target.y, startExit.y, endEntry.y);
|
|
4240
|
+
const bottom = Math.max(parent.y + parent.height, target.y + target.height, startExit.y, endEntry.y);
|
|
4241
|
+
ySet.add(top);
|
|
4242
|
+
ySet.add(bottom);
|
|
4243
|
+
ySet.add(startExit.y);
|
|
4244
|
+
ySet.add(endEntry.y);
|
|
4245
|
+
};
|
|
4246
|
+
const mid = facingGapMid(parent, target);
|
|
4247
|
+
if (mid) if (parent.y + parent.height < target.y || target.y + target.height < parent.y) addGapY(mid.y);
|
|
4248
|
+
else addGapX(mid.x);
|
|
4249
|
+
else if (margin != null) {
|
|
4250
|
+
const ep = expandRect(parent, margin);
|
|
4251
|
+
const et = expandRect(target, margin);
|
|
4252
|
+
if (et.y + et.height < ep.y) addGapY((et.y + et.height + ep.y) / 2);
|
|
4253
|
+
if (ep.y + ep.height < et.y) addGapY((ep.y + ep.height + et.y) / 2);
|
|
4254
|
+
if (et.x + et.width < ep.x) addGapX((et.x + et.width + ep.x) / 2);
|
|
4255
|
+
if (ep.x + ep.width < et.x) addGapX((ep.x + ep.width + et.x) / 2);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return {
|
|
4259
|
+
xs: [...xSet].sort((a, b) => a - b),
|
|
4260
|
+
ys: [...ySet].sort((a, b) => a - b)
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin) {
|
|
4264
|
+
const { xs, ys } = collectLatticeCoords(startExit, endEntry, obstacles, parent, target, margin);
|
|
4265
|
+
const nodes = [];
|
|
4266
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
4267
|
+
const nodeKey = (x, y) => `${x}|${y}`;
|
|
4268
|
+
const addNode = (x, y) => {
|
|
4269
|
+
const key = nodeKey(x, y);
|
|
4270
|
+
if (nodeIndex.has(key)) return;
|
|
4271
|
+
const point = {
|
|
4272
|
+
x,
|
|
4273
|
+
y
|
|
4274
|
+
};
|
|
4275
|
+
if (obstacles.some((o) => pointInOpenInterior(point, o))) return;
|
|
4276
|
+
nodeIndex.set(key, nodes.length);
|
|
4277
|
+
nodes.push(point);
|
|
4278
|
+
};
|
|
4279
|
+
for (const x of xs) for (const y of ys) addNode(x, y);
|
|
4280
|
+
addNode(startExit.x, startExit.y);
|
|
4281
|
+
addNode(endEntry.x, endEntry.y);
|
|
4282
|
+
const startIdx = nodeIndex.get(nodeKey(startExit.x, startExit.y));
|
|
4283
|
+
const goalIdx = nodeIndex.get(nodeKey(endEntry.x, endEntry.y));
|
|
4284
|
+
if (startIdx == null || goalIdx == null) return null;
|
|
4285
|
+
const edges = /* @__PURE__ */ new Map();
|
|
4286
|
+
const pushEdge = (a, b, dir, length) => {
|
|
4287
|
+
const list = edges.get(a) ?? [];
|
|
4288
|
+
list.push({
|
|
4289
|
+
to: b,
|
|
4290
|
+
dir,
|
|
4291
|
+
length
|
|
4292
|
+
});
|
|
4293
|
+
edges.set(a, list);
|
|
4294
|
+
};
|
|
4295
|
+
const byX = /* @__PURE__ */ new Map();
|
|
4296
|
+
const byY = /* @__PURE__ */ new Map();
|
|
4297
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
4298
|
+
const node = nodes[i];
|
|
4299
|
+
const xsList = byX.get(node.x) ?? [];
|
|
4300
|
+
xsList.push(i);
|
|
4301
|
+
byX.set(node.x, xsList);
|
|
4302
|
+
const ysList = byY.get(node.y) ?? [];
|
|
4303
|
+
ysList.push(i);
|
|
4304
|
+
byY.set(node.y, ysList);
|
|
4305
|
+
}
|
|
4306
|
+
for (const list of byX.values()) {
|
|
4307
|
+
list.sort((a, b) => nodes[a].y - nodes[b].y);
|
|
4308
|
+
for (let i = 1; i < list.length; i++) {
|
|
4309
|
+
const a = nodes[list[i - 1]];
|
|
4310
|
+
const b = nodes[list[i]];
|
|
4311
|
+
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4312
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4313
|
+
pushEdge(list[i - 1], list[i], "v", length);
|
|
4314
|
+
pushEdge(list[i], list[i - 1], "v", length);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
for (const list of byY.values()) {
|
|
4319
|
+
list.sort((a, b) => nodes[a].x - nodes[b].x);
|
|
4320
|
+
for (let i = 1; i < list.length; i++) {
|
|
4321
|
+
const a = nodes[list[i - 1]];
|
|
4322
|
+
const b = nodes[list[i]];
|
|
4323
|
+
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4324
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4325
|
+
pushEdge(list[i - 1], list[i], "h", length);
|
|
4326
|
+
pushEdge(list[i], list[i - 1], "h", length);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
return {
|
|
4331
|
+
nodes,
|
|
4332
|
+
edges,
|
|
4333
|
+
startIdx,
|
|
4334
|
+
goalIdx
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
function searchMidPath(nodes, edges, startIdx, goalIdx) {
|
|
4338
|
+
const makeStateKey = (node, dir) => `${node}:${dir}`;
|
|
4339
|
+
const open = [];
|
|
4340
|
+
const best = /* @__PURE__ */ new Map();
|
|
4341
|
+
const prev = /* @__PURE__ */ new Map();
|
|
4342
|
+
const startKey = makeStateKey(startIdx, "s");
|
|
4343
|
+
best.set(startKey, 0);
|
|
4344
|
+
open.push({
|
|
4345
|
+
node: startIdx,
|
|
4346
|
+
dir: "s",
|
|
4347
|
+
g: 0,
|
|
4348
|
+
f: manhattan$1(nodes[startIdx], nodes[goalIdx]),
|
|
4349
|
+
key: startKey
|
|
4350
|
+
});
|
|
4351
|
+
let goalKey = null;
|
|
4352
|
+
while (open.length > 0) {
|
|
4353
|
+
let bestIdx = 0;
|
|
4354
|
+
for (let i = 1; i < open.length; i++) if (open[i].f < open[bestIdx].f) bestIdx = i;
|
|
4355
|
+
const current = open.splice(bestIdx, 1)[0];
|
|
4356
|
+
if (current.node === goalIdx) {
|
|
4357
|
+
goalKey = current.key;
|
|
4358
|
+
break;
|
|
4359
|
+
}
|
|
4360
|
+
for (const edge of edges.get(current.node) ?? []) {
|
|
4361
|
+
const nextDir = edge.dir;
|
|
4362
|
+
let stepCost = edge.length;
|
|
4363
|
+
if (current.dir !== "s" && current.dir !== nextDir) stepCost += BEND_PENALTY;
|
|
4364
|
+
const nextG = current.g + stepCost;
|
|
4365
|
+
const nextKey = makeStateKey(edge.to, nextDir);
|
|
4366
|
+
if (nextG >= (best.get(nextKey) ?? Infinity)) continue;
|
|
4367
|
+
best.set(nextKey, nextG);
|
|
4368
|
+
prev.set(nextKey, current.key);
|
|
4369
|
+
open.push({
|
|
4370
|
+
node: edge.to,
|
|
4371
|
+
dir: nextDir,
|
|
4372
|
+
g: nextG,
|
|
4373
|
+
f: nextG + manhattan$1(nodes[edge.to], nodes[goalIdx]),
|
|
4374
|
+
key: nextKey
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
if (!goalKey) return null;
|
|
4379
|
+
const path = [];
|
|
4380
|
+
let key = goalKey;
|
|
4381
|
+
while (key) {
|
|
4382
|
+
const nodeIdx = Number(key.split(":")[0]);
|
|
4383
|
+
path.unshift(nodes[nodeIdx]);
|
|
4384
|
+
key = prev.get(key);
|
|
4385
|
+
}
|
|
4386
|
+
return path;
|
|
4387
|
+
}
|
|
4388
|
+
function pathIsValid(path, obstacles) {
|
|
4389
|
+
for (let i = 1; i < path.length; i++) if (segmentHitsObstacles(path[i - 1], path[i], obstacles)) return false;
|
|
4390
|
+
return true;
|
|
4391
|
+
}
|
|
4392
|
+
/** Always orthogonal: never emit a diagonal between startExit and endEntry. */
|
|
4393
|
+
function orthogonalElbow(startExit, endEntry) {
|
|
4394
|
+
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) return [startExit, endEntry];
|
|
4395
|
+
return [
|
|
4396
|
+
startExit,
|
|
4397
|
+
{
|
|
4398
|
+
x: startExit.x,
|
|
4399
|
+
y: endEntry.y
|
|
4400
|
+
},
|
|
4401
|
+
endEntry
|
|
4402
|
+
];
|
|
4403
|
+
}
|
|
4404
|
+
function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
4405
|
+
if (obstacles.length === 0) return orthogonalElbow(startExit, endEntry);
|
|
4406
|
+
const u = unionBBox(obstacles);
|
|
4407
|
+
const pad = 1;
|
|
4408
|
+
const top = u.y - pad;
|
|
4409
|
+
const bottom = u.y + u.height + pad;
|
|
4410
|
+
const left = u.x - pad;
|
|
4411
|
+
const right = u.x + u.width + pad;
|
|
4412
|
+
const candidates = [
|
|
4413
|
+
[
|
|
4414
|
+
startExit,
|
|
4415
|
+
{
|
|
4416
|
+
x: startExit.x,
|
|
4417
|
+
y: top
|
|
4418
|
+
},
|
|
4419
|
+
{
|
|
4420
|
+
x: endEntry.x,
|
|
4421
|
+
y: top
|
|
4422
|
+
},
|
|
4423
|
+
endEntry
|
|
4424
|
+
],
|
|
4425
|
+
[
|
|
4426
|
+
startExit,
|
|
4427
|
+
{
|
|
4428
|
+
x: startExit.x,
|
|
4429
|
+
y: bottom
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
x: endEntry.x,
|
|
4433
|
+
y: bottom
|
|
4434
|
+
},
|
|
4435
|
+
endEntry
|
|
4436
|
+
],
|
|
4437
|
+
[
|
|
4438
|
+
startExit,
|
|
4439
|
+
{
|
|
4440
|
+
x: left,
|
|
4441
|
+
y: startExit.y
|
|
4442
|
+
},
|
|
4443
|
+
{
|
|
4444
|
+
x: left,
|
|
4445
|
+
y: endEntry.y
|
|
4446
|
+
},
|
|
4447
|
+
endEntry
|
|
4448
|
+
],
|
|
4449
|
+
[
|
|
4450
|
+
startExit,
|
|
4451
|
+
{
|
|
4452
|
+
x: right,
|
|
4453
|
+
y: startExit.y
|
|
4454
|
+
},
|
|
4455
|
+
{
|
|
4456
|
+
x: right,
|
|
4457
|
+
y: endEntry.y
|
|
4458
|
+
},
|
|
4459
|
+
endEntry
|
|
4460
|
+
],
|
|
4461
|
+
[
|
|
4462
|
+
startExit,
|
|
4463
|
+
{
|
|
4464
|
+
x: startExit.x,
|
|
4465
|
+
y: endEntry.y
|
|
4466
|
+
},
|
|
4467
|
+
endEntry
|
|
4468
|
+
],
|
|
4469
|
+
[
|
|
4470
|
+
startExit,
|
|
4471
|
+
{
|
|
4472
|
+
x: endEntry.x,
|
|
4473
|
+
y: startExit.y
|
|
4474
|
+
},
|
|
4475
|
+
endEntry
|
|
4476
|
+
]
|
|
4477
|
+
];
|
|
4478
|
+
let best = null;
|
|
4479
|
+
let bestLen = Infinity;
|
|
4480
|
+
for (const candidate of candidates) {
|
|
4481
|
+
const simplified = simplifyPath$1(candidate);
|
|
4482
|
+
if (!pathIsValid(simplified, obstacles)) continue;
|
|
4483
|
+
const len = simplified.reduce((sum, p, i) => {
|
|
4484
|
+
if (i === 0) return 0;
|
|
4485
|
+
return sum + manhattan$1(simplified[i - 1], p);
|
|
4486
|
+
}, 0);
|
|
4487
|
+
if (len < bestLen) {
|
|
4488
|
+
bestLen = len;
|
|
4489
|
+
best = simplified;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
return best ?? orthogonalElbow(startExit, endEntry);
|
|
4493
|
+
}
|
|
4494
|
+
function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
|
|
4495
|
+
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) {
|
|
4496
|
+
const direct = [startExit, endEntry];
|
|
4497
|
+
if (pathIsValid(direct, obstacles)) return direct;
|
|
4498
|
+
}
|
|
4499
|
+
const graph = buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin);
|
|
4500
|
+
if (graph) {
|
|
4501
|
+
const mid = searchMidPath(graph.nodes, graph.edges, graph.startIdx, graph.goalIdx);
|
|
4502
|
+
if (mid && pathIsValid(mid, obstacles)) return mid;
|
|
4503
|
+
}
|
|
4504
|
+
return fallbackOuterRoute(startExit, endEntry, obstacles);
|
|
4505
|
+
}
|
|
4506
|
+
function routeOrthogonalAround(input) {
|
|
4507
|
+
const margin = input.margin ?? DEFAULT_MARGIN;
|
|
4508
|
+
const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
|
|
4509
|
+
const exitBlocker = input.parent ?? input.source;
|
|
4510
|
+
const rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
|
|
4511
|
+
const gapNeighbor = input.parent ?? input.source;
|
|
4512
|
+
const peeledTargetNeighbor = input.target && gapNeighbor ? peelForFacingGap(input.target, gapNeighbor) : null;
|
|
4513
|
+
const rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance, peeledTargetNeighbor?.a, peeledTargetNeighbor?.b);
|
|
4514
|
+
const { startExit, endEntry } = clampFacingExitEntry(input.from, input.to, input.fromDir, input.toDir, rawStartExit, rawEndEntry);
|
|
4515
|
+
let sourceObs = input.source ? expandRect(input.source, margin) : void 0;
|
|
4516
|
+
let parentObs = input.parent ? expandRect(input.parent, margin) : void 0;
|
|
4517
|
+
let targetObs = input.target ? expandRect(input.target, margin) : void 0;
|
|
4518
|
+
const openCorridor = (expA, expB, inputA, inputB) => {
|
|
4519
|
+
const peeled = peelForFacingGap(inputA, inputB);
|
|
4520
|
+
if (peeled) return preserveFacingGap(expA, expB, peeled.a, peeled.b);
|
|
4521
|
+
return preserveFacingGap(expA, expB, inputA, inputB);
|
|
4522
|
+
};
|
|
4523
|
+
if (parentObs && targetObs && input.parent && input.target) [parentObs, targetObs] = openCorridor(parentObs, targetObs, input.parent, input.target);
|
|
4524
|
+
else if (sourceObs && targetObs && input.source && input.target) [sourceObs, targetObs] = openCorridor(sourceObs, targetObs, input.source, input.target);
|
|
4525
|
+
if (sourceObs && parentObs && input.source && input.parent) [sourceObs, parentObs] = openCorridor(sourceObs, parentObs, input.source, input.parent);
|
|
4526
|
+
const obstacles = [];
|
|
4527
|
+
if (sourceObs) obstacles.push(sourceObs);
|
|
4528
|
+
if (parentObs) obstacles.push(parentObs);
|
|
4529
|
+
if (targetObs) obstacles.push(targetObs);
|
|
4530
|
+
const peeledLattice = input.parent && input.target ? peelForFacingGap(input.parent, input.target) : input.source && input.target ? peelForFacingGap(input.source, input.target) : null;
|
|
4531
|
+
const mid = routeMidPath(startExit, endEntry, obstacles, peeledLattice?.a ?? input.parent ?? input.source, peeledLattice?.b ?? input.target, margin).slice(1, -1);
|
|
4532
|
+
const lastMid = mid[mid.length - 1];
|
|
4533
|
+
const alreadyApproaching = !!lastMid && isOnApproachOutside(lastMid, input.to, input.toDir, 1);
|
|
4534
|
+
return simplifyPath$1(ensureOrthogonalPath([
|
|
4535
|
+
input.from,
|
|
4536
|
+
startExit,
|
|
4537
|
+
...mid,
|
|
4538
|
+
...alreadyApproaching ? [] : [endEntry],
|
|
4539
|
+
input.to
|
|
4540
|
+
]));
|
|
4541
|
+
}
|
|
4542
|
+
/** Insert elbows so no consecutive points form a diagonal (safety net). */
|
|
4543
|
+
function ensureOrthogonalPath(path) {
|
|
4544
|
+
if (path.length < 2) return path;
|
|
4545
|
+
const out = [path[0]];
|
|
4546
|
+
for (let i = 1; i < path.length; i++) {
|
|
4547
|
+
const prev = out[out.length - 1];
|
|
4548
|
+
const next = path[i];
|
|
4549
|
+
if (!(Math.abs(prev.x - next.x) < .001 || Math.abs(prev.y - next.y) < .001)) out.push({
|
|
4550
|
+
x: prev.x,
|
|
4551
|
+
y: next.y
|
|
4552
|
+
});
|
|
4553
|
+
out.push(next);
|
|
4554
|
+
}
|
|
4555
|
+
return out;
|
|
4556
|
+
}
|
|
4557
|
+
function isOnApproachOutside(p, to, toDir, minClearance) {
|
|
4558
|
+
switch (toDir) {
|
|
4559
|
+
case "top": return Math.abs(p.x - to.x) < .001 && p.y <= to.y - minClearance;
|
|
4560
|
+
case "bottom": return Math.abs(p.x - to.x) < .001 && p.y >= to.y + minClearance;
|
|
4561
|
+
case "left": return Math.abs(p.y - to.y) < .001 && p.x <= to.x - minClearance;
|
|
4562
|
+
case "right": return Math.abs(p.y - to.y) < .001 && p.x >= to.x + minClearance;
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
//#endregion
|
|
4566
|
+
//#region src/utils/routeDebug.ts
|
|
4567
|
+
function isRouteDebugEnabled() {
|
|
4568
|
+
if (typeof globalThis === "undefined") return false;
|
|
4569
|
+
const g = globalThis;
|
|
4570
|
+
if (g.__PAPIRUS_ROUTE_DEBUG__ === true || g.__PAPIRUS_ROUTE_DEBUG__ === 1) return true;
|
|
4571
|
+
try {
|
|
4572
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("papirusRouteDebug") === "1") return true;
|
|
4573
|
+
} catch {}
|
|
4574
|
+
return false;
|
|
4575
|
+
}
|
|
4576
|
+
function routeDebug(tag, payload) {
|
|
4577
|
+
if (!isRouteDebugEnabled()) return;
|
|
4578
|
+
console.log(`[papirus:route] ${tag}`, payload);
|
|
4579
|
+
}
|
|
4580
|
+
//#endregion
|
|
3780
4581
|
//#region src/elements/paths/PolylinePathStrategy.ts
|
|
4582
|
+
function isSide(v) {
|
|
4583
|
+
return v === "top" || v === "right" || v === "bottom" || v === "left";
|
|
4584
|
+
}
|
|
4585
|
+
function resolveRoutingRoles(from, to, obstacles) {
|
|
4586
|
+
const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) => {
|
|
4587
|
+
const onVertical = (Math.abs(to.x - o.x) < 1 || Math.abs(to.x - (o.x + o.width)) < 1) && to.y >= o.y - 1 && to.y <= o.y + o.height + 1;
|
|
4588
|
+
const onHorizontal = (Math.abs(to.y - o.y) < 1 || Math.abs(to.y - (o.y + o.height)) < 1) && to.x >= o.x - 1 && to.x <= o.x + o.width + 1;
|
|
4589
|
+
return onVertical || onHorizontal;
|
|
4590
|
+
});
|
|
4591
|
+
const source = obstacles.find((o) => o.role === "source" && o !== target);
|
|
4592
|
+
return {
|
|
4593
|
+
parent: obstacles.find((o) => o !== target && o !== source && o.role !== "source" && from.x >= o.x && from.x <= o.x + o.width && from.y >= o.y && from.y <= o.y + o.height),
|
|
4594
|
+
source,
|
|
4595
|
+
target
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
3781
4598
|
var MIN_SEGMENT_LENGTH = 20;
|
|
3782
4599
|
var SELF_LOOP_MIN_DISTANCE$1 = 1;
|
|
3783
4600
|
var SELF_LOOP_OFFSET$1 = 42;
|
|
@@ -3855,27 +4672,66 @@ function moveByDir(point, dir, distance) {
|
|
|
3855
4672
|
default: return point;
|
|
3856
4673
|
}
|
|
3857
4674
|
}
|
|
3858
|
-
|
|
4675
|
+
/** Push `point` along `dir` until it clears every obstacle (plus margin). */
|
|
4676
|
+
function moveOutsideObstacles(point, dir, obstacles, minDistance) {
|
|
4677
|
+
if (!dir || obstacles.length === 0) return moveByDir(point, dir, minDistance);
|
|
4678
|
+
const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
|
|
4679
|
+
let dist = minDistance;
|
|
4680
|
+
let candidate = moveByDir(point, dir, dist);
|
|
4681
|
+
const step = 16;
|
|
4682
|
+
const maxDist = 4e3;
|
|
4683
|
+
while (dist < maxDist && expanded.some((obstacle) => pointInsideObstacle(candidate, obstacle))) {
|
|
4684
|
+
dist += step;
|
|
4685
|
+
candidate = moveByDir(point, dir, dist);
|
|
4686
|
+
}
|
|
4687
|
+
return candidate;
|
|
4688
|
+
}
|
|
4689
|
+
function inferExitDir(from, to) {
|
|
3859
4690
|
const dx = to.x - from.x;
|
|
3860
4691
|
const dy = to.y - from.y;
|
|
3861
4692
|
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
|
|
3862
4693
|
return dy >= 0 ? "bottom" : "top";
|
|
3863
4694
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
4695
|
+
/** Outward side at `to` we should approach from (not opposite of fromDir). */
|
|
4696
|
+
function inferApproachDir(from, to) {
|
|
4697
|
+
const dx = from.x - to.x;
|
|
4698
|
+
const dy = from.y - to.y;
|
|
4699
|
+
if (Math.abs(dy) > 1) return dy >= 0 ? "bottom" : "top";
|
|
4700
|
+
if (Math.abs(dx) > 1) return dx >= 0 ? "right" : "left";
|
|
4701
|
+
return dy >= 0 ? "bottom" : "top";
|
|
4702
|
+
}
|
|
4703
|
+
/** Which side of `target` the attachment point `to` lies on (border hit). */
|
|
4704
|
+
function sideOfPointOnRect(to, target) {
|
|
4705
|
+
const onLeft = Math.abs(to.x - target.x) <= 1;
|
|
4706
|
+
const onRight = Math.abs(to.x - (target.x + target.width)) <= 1;
|
|
4707
|
+
const onTop = Math.abs(to.y - target.y) <= 1;
|
|
4708
|
+
const onBottom = Math.abs(to.y - (target.y + target.height)) <= 1;
|
|
4709
|
+
if (onTop && !onLeft && !onRight) return "top";
|
|
4710
|
+
if (onBottom && !onLeft && !onRight) return "bottom";
|
|
4711
|
+
if (onLeft && !onTop && !onBottom) return "left";
|
|
4712
|
+
if (onRight && !onTop && !onBottom) return "right";
|
|
4713
|
+
}
|
|
4714
|
+
/**
|
|
4715
|
+
* Resolve approach side at `to`.
|
|
4716
|
+
* Prefer the side of the target rect where `to` actually sits — never override a
|
|
4717
|
+
* left/right attachment with top/bottom from source geometry (that puts endEntry
|
|
4718
|
+
* inside the target and draws the last bend through the node).
|
|
4719
|
+
*/
|
|
4720
|
+
function resolveApproachDir(from, to, toDir, target) {
|
|
4721
|
+
if (target) {
|
|
4722
|
+
const side = sideOfPointOnRect(to, target);
|
|
4723
|
+
if (side) return side;
|
|
3871
4724
|
}
|
|
4725
|
+
if (toDir && isSide(toDir)) return toDir;
|
|
4726
|
+
return inferApproachDir(from, to);
|
|
3872
4727
|
}
|
|
3873
4728
|
function buildRoutedPolyline(from, to, fromDir, toDir, obstacles) {
|
|
3874
4729
|
if (obstacles.length === 0) return null;
|
|
3875
|
-
const effectiveFromDir = fromDir ??
|
|
3876
|
-
const
|
|
3877
|
-
const
|
|
3878
|
-
const
|
|
4730
|
+
const effectiveFromDir = fromDir ?? inferExitDir(from, to);
|
|
4731
|
+
const { target: routedTarget } = resolveRoutingRoles(from, to, obstacles);
|
|
4732
|
+
const effectiveToDir = resolveApproachDir(from, to, toDir, routedTarget);
|
|
4733
|
+
const startExit = moveOutsideObstacles(from, effectiveFromDir, obstacles, ROUTE_EXIT_DISTANCE);
|
|
4734
|
+
const endEntry = moveOutsideObstacles(to, effectiveToDir, obstacles, ROUTE_EXIT_DISTANCE);
|
|
3879
4735
|
const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
|
|
3880
4736
|
const xs = new Set([startExit.x, endEntry.x]);
|
|
3881
4737
|
const ys = new Set([startExit.y, endEntry.y]);
|
|
@@ -4020,12 +4876,11 @@ var PolylinePathStrategy = class {
|
|
|
4020
4876
|
calculatePath(from, to, fromDir, toDir, _options) {
|
|
4021
4877
|
const controlPoints = _options?.controlPoints;
|
|
4022
4878
|
if (_options?.editablePolyline) {
|
|
4023
|
-
|
|
4879
|
+
const path = controlPoints && controlPoints.length > 0 ? [
|
|
4024
4880
|
from,
|
|
4025
4881
|
...controlPoints,
|
|
4026
4882
|
to
|
|
4027
|
-
]
|
|
4028
|
-
return [
|
|
4883
|
+
] : [
|
|
4029
4884
|
from,
|
|
4030
4885
|
{
|
|
4031
4886
|
x: (from.x + to.x) / 2,
|
|
@@ -4033,14 +4888,32 @@ var PolylinePathStrategy = class {
|
|
|
4033
4888
|
},
|
|
4034
4889
|
to
|
|
4035
4890
|
];
|
|
4891
|
+
routeDebug("polyline.via", {
|
|
4892
|
+
via: "editable",
|
|
4893
|
+
path
|
|
4894
|
+
});
|
|
4895
|
+
return path;
|
|
4036
4896
|
}
|
|
4037
4897
|
const dist = distance(from, to);
|
|
4038
4898
|
const selfLoop = _options?.selfLoop ?? false;
|
|
4039
4899
|
const obstacles = _options?.obstacles ?? [];
|
|
4040
|
-
|
|
4041
|
-
const
|
|
4042
|
-
|
|
4043
|
-
|
|
4900
|
+
const finish = (via, path) => {
|
|
4901
|
+
const roles = resolveRoutingRoles(from, to, obstacles);
|
|
4902
|
+
routeDebug("polyline.via", {
|
|
4903
|
+
via,
|
|
4904
|
+
fromDir,
|
|
4905
|
+
toDir,
|
|
4906
|
+
approachDir: roles.target ? resolveApproachDir(from, to, toDir, roles.target) : toDir,
|
|
4907
|
+
roles: {
|
|
4908
|
+
parent: roles.parent?.id,
|
|
4909
|
+
source: roles.source?.id,
|
|
4910
|
+
target: roles.target?.id
|
|
4911
|
+
},
|
|
4912
|
+
obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`),
|
|
4913
|
+
path: path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → ")
|
|
4914
|
+
});
|
|
4915
|
+
return path;
|
|
4916
|
+
};
|
|
4044
4917
|
if (selfLoop && dist < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) switch (fromDir ?? toDir) {
|
|
4045
4918
|
case "bottom": return [
|
|
4046
4919
|
from,
|
|
@@ -4168,9 +5041,40 @@ var PolylinePathStrategy = class {
|
|
|
4168
5041
|
to
|
|
4169
5042
|
];
|
|
4170
5043
|
}
|
|
4171
|
-
if (
|
|
5044
|
+
if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
|
|
5045
|
+
const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
|
|
5046
|
+
const approachDir = resolveApproachDir(from, to, toDir, target);
|
|
5047
|
+
if ((parent || source || target) && isSide(approachDir)) return finish("routeOrthogonalAround", routeOrthogonalAround({
|
|
5048
|
+
from,
|
|
5049
|
+
to,
|
|
5050
|
+
fromDir,
|
|
5051
|
+
toDir: approachDir,
|
|
5052
|
+
parent,
|
|
5053
|
+
source,
|
|
5054
|
+
target,
|
|
5055
|
+
margin: 4,
|
|
5056
|
+
exitDistance: 20
|
|
5057
|
+
}));
|
|
5058
|
+
routeDebug("polyline.via", {
|
|
5059
|
+
via: "roles-missed-around",
|
|
5060
|
+
fromDir,
|
|
5061
|
+
toDir,
|
|
5062
|
+
approachDir,
|
|
5063
|
+
roles: {
|
|
5064
|
+
parent: parent?.id,
|
|
5065
|
+
source: source?.id,
|
|
5066
|
+
target: target?.id
|
|
5067
|
+
},
|
|
5068
|
+
obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`)
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
|
|
5072
|
+
const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
|
|
5073
|
+
if (routed) return finish("buildRoutedPolyline", routed);
|
|
5074
|
+
}
|
|
5075
|
+
if (fromDir || toDir) return finish("calculateDirectedPath", this.calculateDirectedPath(from, to, fromDir, toDir));
|
|
4172
5076
|
const midX = (from.x + to.x) / 2;
|
|
4173
|
-
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return [
|
|
5077
|
+
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return finish("manhattan-h", [
|
|
4174
5078
|
from,
|
|
4175
5079
|
{
|
|
4176
5080
|
x: midX,
|
|
@@ -4181,9 +5085,9 @@ var PolylinePathStrategy = class {
|
|
|
4181
5085
|
y: to.y
|
|
4182
5086
|
},
|
|
4183
5087
|
to
|
|
4184
|
-
];
|
|
5088
|
+
]);
|
|
4185
5089
|
const midY = (from.y + to.y) / 2;
|
|
4186
|
-
return [
|
|
5090
|
+
return finish("manhattan-v", [
|
|
4187
5091
|
from,
|
|
4188
5092
|
{
|
|
4189
5093
|
x: from.x,
|
|
@@ -4194,7 +5098,7 @@ var PolylinePathStrategy = class {
|
|
|
4194
5098
|
y: midY
|
|
4195
5099
|
},
|
|
4196
5100
|
to
|
|
4197
|
-
];
|
|
5101
|
+
]);
|
|
4198
5102
|
}
|
|
4199
5103
|
calculateDirectedPath(from, to, fromDir, toDir) {
|
|
4200
5104
|
const points = [from];
|
|
@@ -4248,15 +5152,39 @@ var PolylinePathStrategy = class {
|
|
|
4248
5152
|
y: to.y
|
|
4249
5153
|
});
|
|
4250
5154
|
}
|
|
4251
|
-
} else if (fromVertical && toHorizontal)
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
5155
|
+
} else if (fromVertical && toHorizontal) {
|
|
5156
|
+
const exitY = fromDir === "top" ? from.y - MIN_SEGMENT_LENGTH : from.y + MIN_SEGMENT_LENGTH;
|
|
5157
|
+
const entryX = toDir === "left" ? to.x - MIN_SEGMENT_LENGTH : to.x + MIN_SEGMENT_LENGTH;
|
|
5158
|
+
const jogX = (from.x + to.x) / 2;
|
|
5159
|
+
const approachX = Math.abs(jogX - to.x) >= MIN_SEGMENT_LENGTH ? jogX : entryX;
|
|
5160
|
+
points.push({
|
|
5161
|
+
x: from.x,
|
|
5162
|
+
y: exitY
|
|
5163
|
+
});
|
|
5164
|
+
points.push({
|
|
5165
|
+
x: approachX,
|
|
5166
|
+
y: exitY
|
|
5167
|
+
});
|
|
5168
|
+
points.push({
|
|
5169
|
+
x: approachX,
|
|
5170
|
+
y: to.y
|
|
5171
|
+
});
|
|
5172
|
+
} else if (fromHorizontal && toVertical) {
|
|
5173
|
+
const exitX = fromDir === "left" ? from.x - MIN_SEGMENT_LENGTH : from.x + MIN_SEGMENT_LENGTH;
|
|
5174
|
+
const jogY = (from.y + to.y) / 2;
|
|
5175
|
+
points.push({
|
|
5176
|
+
x: exitX,
|
|
5177
|
+
y: from.y
|
|
5178
|
+
});
|
|
5179
|
+
points.push({
|
|
5180
|
+
x: exitX,
|
|
5181
|
+
y: jogY
|
|
5182
|
+
});
|
|
5183
|
+
points.push({
|
|
5184
|
+
x: to.x,
|
|
5185
|
+
y: jogY
|
|
5186
|
+
});
|
|
5187
|
+
} else {
|
|
4260
5188
|
const midX = (from.x + to.x) / 2;
|
|
4261
5189
|
points.push({
|
|
4262
5190
|
x: midX,
|
|
@@ -4872,12 +5800,34 @@ function getEdgeLabelRotation(options) {
|
|
|
4872
5800
|
return angle;
|
|
4873
5801
|
}
|
|
4874
5802
|
//#endregion
|
|
5803
|
+
//#region src/types.ts
|
|
5804
|
+
function isNodeEdgeEndpoint(endpoint) {
|
|
5805
|
+
return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
|
|
5806
|
+
}
|
|
5807
|
+
function isEdgeEdgeEndpoint(endpoint) {
|
|
5808
|
+
return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
|
|
5809
|
+
}
|
|
5810
|
+
//#endregion
|
|
4875
5811
|
//#region src/elements/Edge.ts
|
|
4876
5812
|
var DEFAULT_EDGE_STYLE = {
|
|
4877
5813
|
strokeColor: "#666666",
|
|
4878
5814
|
strokeWidth: 2,
|
|
4879
5815
|
opacity: 1
|
|
4880
5816
|
};
|
|
5817
|
+
/** Compare path options that affect polyline routing (not control-point identity). */
|
|
5818
|
+
function routingOptionsEqual(a, b) {
|
|
5819
|
+
const aObs = a?.obstacles;
|
|
5820
|
+
const bObs = b?.obstacles;
|
|
5821
|
+
if (aObs === bObs) return true;
|
|
5822
|
+
if (!aObs || !bObs) return !aObs && !bObs;
|
|
5823
|
+
if (aObs.length !== bObs.length) return false;
|
|
5824
|
+
for (let i = 0; i < aObs.length; i++) {
|
|
5825
|
+
const left = aObs[i];
|
|
5826
|
+
const right = bObs[i];
|
|
5827
|
+
if (left.id !== right.id || left.role !== right.role || left.x !== right.x || left.y !== right.y || left.width !== right.width || left.height !== right.height) return false;
|
|
5828
|
+
}
|
|
5829
|
+
return true;
|
|
5830
|
+
}
|
|
4881
5831
|
/**
|
|
4882
5832
|
* Connection edge between nodes
|
|
4883
5833
|
*/
|
|
@@ -5223,8 +6173,15 @@ var Edge = class extends Element {
|
|
|
5223
6173
|
* @param toDir Direction at end point (top, right, bottom, left)
|
|
5224
6174
|
*/
|
|
5225
6175
|
updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
|
|
5226
|
-
|
|
6176
|
+
const geometryUnchanged = 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;
|
|
6177
|
+
const routingOptionsChanged = !routingOptionsEqual(this._pathOptions, options);
|
|
6178
|
+
if (geometryUnchanged && !routingOptionsChanged) {
|
|
5227
6179
|
this._pathOptions = options;
|
|
6180
|
+
routeDebug("edge.updateEndpoints:skip", {
|
|
6181
|
+
id: this.id,
|
|
6182
|
+
type: this._type,
|
|
6183
|
+
reason: "geometry+routing unchanged"
|
|
6184
|
+
});
|
|
5228
6185
|
return;
|
|
5229
6186
|
}
|
|
5230
6187
|
this._fromPoint = fromPoint;
|
|
@@ -5244,6 +6201,51 @@ var Edge = class extends Element {
|
|
|
5244
6201
|
editablePolyline: this.isEditablePolyline(),
|
|
5245
6202
|
selfLoop: this._from.nodeId === this._to.nodeId
|
|
5246
6203
|
});
|
|
6204
|
+
if (this._type === "polyline" || this._type === "editable-polyline" || this._type === "bezier") {
|
|
6205
|
+
const obstacles = this._pathOptions?.obstacles ?? [];
|
|
6206
|
+
const segs = [];
|
|
6207
|
+
for (let i = 1; i < this._path.length; i++) {
|
|
6208
|
+
const a = this._path[i - 1];
|
|
6209
|
+
const b = this._path[i];
|
|
6210
|
+
const dx = b.x - a.x;
|
|
6211
|
+
const dy = b.y - a.y;
|
|
6212
|
+
const axis = Math.abs(dy) < .01 ? "H" : Math.abs(dx) < .01 ? "V" : `DIAG(${dx.toFixed(0)},${dy.toFixed(0)})`;
|
|
6213
|
+
segs.push({
|
|
6214
|
+
axis,
|
|
6215
|
+
len: Math.round(Math.abs(dx) + Math.abs(dy)),
|
|
6216
|
+
a: `${Math.round(a.x)},${Math.round(a.y)}`,
|
|
6217
|
+
b: `${Math.round(b.x)},${Math.round(b.y)}`
|
|
6218
|
+
});
|
|
6219
|
+
}
|
|
6220
|
+
routeDebug("edge.recalculatePath", {
|
|
6221
|
+
id: this.id,
|
|
6222
|
+
type: this._type,
|
|
6223
|
+
from: {
|
|
6224
|
+
nodeId: this._from.nodeId,
|
|
6225
|
+
portId: isNodeEdgeEndpoint(this._from) ? this._from.portId : void 0,
|
|
6226
|
+
outlineParam: isNodeEdgeEndpoint(this._from) ? this._from.outlineParam : void 0,
|
|
6227
|
+
point: `${Math.round(this._fromPoint.x)},${Math.round(this._fromPoint.y)}`,
|
|
6228
|
+
dir: this._fromDir
|
|
6229
|
+
},
|
|
6230
|
+
to: {
|
|
6231
|
+
nodeId: this._to.nodeId,
|
|
6232
|
+
portId: isNodeEdgeEndpoint(this._to) ? this._to.portId : void 0,
|
|
6233
|
+
outlineParam: isNodeEdgeEndpoint(this._to) ? this._to.outlineParam : void 0,
|
|
6234
|
+
point: `${Math.round(this._toPoint.x)},${Math.round(this._toPoint.y)}`,
|
|
6235
|
+
dir: this._toDir
|
|
6236
|
+
},
|
|
6237
|
+
lockAnchors: this.lockAnchors,
|
|
6238
|
+
obstacleCount: obstacles.length,
|
|
6239
|
+
obstacles: obstacles.map((o) => ({
|
|
6240
|
+
id: o.id,
|
|
6241
|
+
role: o.role,
|
|
6242
|
+
box: `${Math.round(o.x)},${Math.round(o.y)} ${Math.round(o.width)}x${Math.round(o.height)}`
|
|
6243
|
+
})),
|
|
6244
|
+
path: this._path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → "),
|
|
6245
|
+
segments: segs,
|
|
6246
|
+
controlPoints: this._controlPoints?.length ?? 0
|
|
6247
|
+
});
|
|
6248
|
+
}
|
|
5247
6249
|
this.updateBounds();
|
|
5248
6250
|
this.markDirty();
|
|
5249
6251
|
}
|
|
@@ -6971,14 +7973,6 @@ var ContextMenuManager = class extends EventEmitter {
|
|
|
6971
7973
|
}
|
|
6972
7974
|
};
|
|
6973
7975
|
//#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
7976
|
//#region src/core/EdgeEndpointUpdater.ts
|
|
6983
7977
|
var EDGE_ATTACH_MAX_PASSES = 3;
|
|
6984
7978
|
var EdgeEndpointUpdater = class {
|
|
@@ -6986,7 +7980,7 @@ var EdgeEndpointUpdater = class {
|
|
|
6986
7980
|
this.host = host;
|
|
6987
7981
|
}
|
|
6988
7982
|
updateForDrag() {
|
|
6989
|
-
this.update(
|
|
7983
|
+
this.update(true);
|
|
6990
7984
|
}
|
|
6991
7985
|
updateAll() {
|
|
6992
7986
|
this.update(true);
|
|
@@ -7014,10 +8008,47 @@ var EdgeEndpointUpdater = class {
|
|
|
7014
8008
|
const fromNode = nodes.get(edge.from.nodeId);
|
|
7015
8009
|
const toNode = nodes.get(edge.to.nodeId);
|
|
7016
8010
|
if (!fromNode || !toNode) return;
|
|
7017
|
-
if (includeRoutingObstacles)
|
|
8011
|
+
if (includeRoutingObstacles) {
|
|
8012
|
+
this.preferFacingSidePorts(edge, fromNode, toNode);
|
|
8013
|
+
this.lockAnchors(edge, fromNode, toNode);
|
|
8014
|
+
}
|
|
7018
8015
|
const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
|
|
7019
8016
|
const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
|
|
7020
|
-
edge.
|
|
8017
|
+
const fromId = edge.from.nodeId;
|
|
8018
|
+
const toId = edge.to.nodeId;
|
|
8019
|
+
const routingObstacles = obstacles?.map((obstacle) => {
|
|
8020
|
+
if (obstacle.id === fromId) return {
|
|
8021
|
+
...obstacle,
|
|
8022
|
+
role: "source"
|
|
8023
|
+
};
|
|
8024
|
+
if (obstacle.id === toId) return {
|
|
8025
|
+
...obstacle,
|
|
8026
|
+
role: "target"
|
|
8027
|
+
};
|
|
8028
|
+
return obstacle;
|
|
8029
|
+
});
|
|
8030
|
+
routeDebug("updater.nodeNode", {
|
|
8031
|
+
edgeId: edge.id,
|
|
8032
|
+
type: edge.type,
|
|
8033
|
+
includeRoutingObstacles,
|
|
8034
|
+
lockAnchors: edge.lockAnchors,
|
|
8035
|
+
from: {
|
|
8036
|
+
nodeId: fromId,
|
|
8037
|
+
portId: edge.from.portId,
|
|
8038
|
+
outlineParam: edge.from.outlineParam,
|
|
8039
|
+
point: `${Math.round(from.point.x)},${Math.round(from.point.y)}`,
|
|
8040
|
+
dir: from.direction
|
|
8041
|
+
},
|
|
8042
|
+
to: {
|
|
8043
|
+
nodeId: toId,
|
|
8044
|
+
portId: edge.to.portId,
|
|
8045
|
+
outlineParam: edge.to.outlineParam,
|
|
8046
|
+
point: `${Math.round(to.point.x)},${Math.round(to.point.y)}`,
|
|
8047
|
+
dir: to.direction
|
|
8048
|
+
},
|
|
8049
|
+
obstacleCount: routingObstacles?.length ?? 0
|
|
8050
|
+
});
|
|
8051
|
+
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, routingObstacles ? { obstacles: routingObstacles } : void 0);
|
|
7021
8052
|
}
|
|
7022
8053
|
/**
|
|
7023
8054
|
* @returns true when the resolved geometry changed enough to warrant another pass
|
|
@@ -7067,6 +8098,79 @@ var EdgeEndpointUpdater = class {
|
|
|
7067
8098
|
}
|
|
7068
8099
|
}
|
|
7069
8100
|
/**
|
|
8101
|
+
* When nodes are clearly stacked, retarget side ports that would force the
|
|
8102
|
+
* polyline to approach along the long edge (contour crawl).
|
|
8103
|
+
*
|
|
8104
|
+
* Uses bounding-box gaps (not center dx/dy): a large horizontal offset must not
|
|
8105
|
+
* flip bottom→left while the source is still below the target — that reintroduces
|
|
8106
|
+
* the left-edge contour jog when the user drags the node leftward.
|
|
8107
|
+
*
|
|
8108
|
+
* - Always set facing ports when unset (so lockAnchors does not pick left/right).
|
|
8109
|
+
* - Rewrite target left/right → top/bottom when there is a clear vertical gap.
|
|
8110
|
+
* - Do not rewrite an intentional source side exit (e.g. left exit from a nested
|
|
8111
|
+
* component around its parent).
|
|
8112
|
+
*/
|
|
8113
|
+
preferFacingSidePorts(edge, fromNode, toNode) {
|
|
8114
|
+
if (!edge.lockAnchors) return;
|
|
8115
|
+
if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
|
|
8116
|
+
if (edge.from.outlineParam !== void 0 || edge.to.outlineParam !== void 0) return;
|
|
8117
|
+
const fromB = fromNode.getBounds();
|
|
8118
|
+
const toB = toNode.getBounds();
|
|
8119
|
+
const fromBelow = fromB.y - (toB.y + toB.height);
|
|
8120
|
+
const fromAbove = toB.y - (fromB.y + fromB.height);
|
|
8121
|
+
const fromRight = fromB.x - (toB.x + toB.width);
|
|
8122
|
+
const fromLeft = toB.x - (fromB.x + fromB.width);
|
|
8123
|
+
const fc = fromNode.getCenter();
|
|
8124
|
+
const tc = toNode.getCenter();
|
|
8125
|
+
if (fromBelow > 0 || fromAbove > 0) {
|
|
8126
|
+
const fromSide = fromBelow > 0 ? "top" : "bottom";
|
|
8127
|
+
const toSide = fromBelow > 0 ? "bottom" : "top";
|
|
8128
|
+
const curFrom = this.portSide(edge.from.portId);
|
|
8129
|
+
const curTo = this.portSide(edge.to.portId);
|
|
8130
|
+
if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
|
|
8131
|
+
if (!curTo || curTo === "left" || curTo === "right" || curTo === toSide) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
|
|
8132
|
+
return;
|
|
8133
|
+
}
|
|
8134
|
+
if (fromLeft > 0 || fromRight > 0) {
|
|
8135
|
+
const fromSide = fromRight > 0 ? "left" : "right";
|
|
8136
|
+
const toSide = fromRight > 0 ? "right" : "left";
|
|
8137
|
+
const curFrom = this.portSide(edge.from.portId);
|
|
8138
|
+
const curTo = this.portSide(edge.to.portId);
|
|
8139
|
+
if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
|
|
8140
|
+
if (!curTo || curTo === "top" || curTo === "bottom") this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
8143
|
+
portSide(portId) {
|
|
8144
|
+
if (!portId?.startsWith("anchor:")) return void 0;
|
|
8145
|
+
const side = portId.slice(ANCHOR_PORT_PREFIX.length).split(":")[0];
|
|
8146
|
+
if (side === "top" || side === "right" || side === "bottom" || side === "left") return side;
|
|
8147
|
+
}
|
|
8148
|
+
nearestAnchorOnSide(node, side, toward) {
|
|
8149
|
+
const anchors = node.getAnchors().filter((a) => a.id.startsWith(`${side}:`));
|
|
8150
|
+
if (anchors.length === 0) return null;
|
|
8151
|
+
let best = anchors[0];
|
|
8152
|
+
let minDist = Infinity;
|
|
8153
|
+
for (const anchor of anchors) {
|
|
8154
|
+
const dist = (anchor.point.x - toward.x) ** 2 + (anchor.point.y - toward.y) ** 2;
|
|
8155
|
+
if (dist < minDist) {
|
|
8156
|
+
minDist = dist;
|
|
8157
|
+
best = anchor;
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
return best;
|
|
8161
|
+
}
|
|
8162
|
+
setEndpointSidePort(edge, end, node, side, toward) {
|
|
8163
|
+
const anchor = this.nearestAnchorOnSide(node, side, toward);
|
|
8164
|
+
if (!anchor) return;
|
|
8165
|
+
const next = {
|
|
8166
|
+
...end === "from" ? edge.from : edge.to,
|
|
8167
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`,
|
|
8168
|
+
outlineParam: void 0
|
|
8169
|
+
};
|
|
8170
|
+
if (end === "from") edge.from = next;
|
|
8171
|
+
else edge.to = next;
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
7070
8174
|
* Resolve a node endpoint:
|
|
7071
8175
|
* - lockAnchors + portId → fixed side port (wins over leftover outlineParam)
|
|
7072
8176
|
* - outlineParam → fixed point on outline (attach-to-outline)
|
|
@@ -7081,10 +8185,13 @@ var EdgeEndpointUpdater = class {
|
|
|
7081
8185
|
direction: anchorId.split(":")[0]
|
|
7082
8186
|
};
|
|
7083
8187
|
}
|
|
7084
|
-
if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0)
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
8188
|
+
if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0) {
|
|
8189
|
+
const bounds = node.getBounds();
|
|
8190
|
+
return {
|
|
8191
|
+
point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
|
|
8192
|
+
direction: getDirectionFromOutlineParam(endpoint.outlineParam, bounds)
|
|
8193
|
+
};
|
|
8194
|
+
}
|
|
7088
8195
|
if (toward) return this.resolveFloatingEndpoint(node, toward);
|
|
7089
8196
|
return { point: node.getCenter() };
|
|
7090
8197
|
}
|
|
@@ -8105,11 +9212,13 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
8105
9212
|
}
|
|
8106
9213
|
getNodeObstacles() {
|
|
8107
9214
|
if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
|
|
9215
|
+
const pad = 4;
|
|
8108
9216
|
this._nodeObstaclesCache = Array.from(this._nodes.values()).map((node) => ({
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
9217
|
+
id: node.id,
|
|
9218
|
+
x: node.x - pad,
|
|
9219
|
+
y: node.y - pad,
|
|
9220
|
+
width: node.width + pad * 2,
|
|
9221
|
+
height: node.height + pad * 2,
|
|
8113
9222
|
role: "other"
|
|
8114
9223
|
}));
|
|
8115
9224
|
return this._nodeObstaclesCache;
|