@ngroznykh/papirus 0.8.0 → 0.8.2
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.map +1 -1
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/EdgeEndpointUpdater.d.ts +19 -0
- package/dist/core/EdgeEndpointUpdater.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/paths/PolylinePathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/routeOrthogonalAround.d.ts +36 -0
- package/dist/elements/paths/routeOrthogonalAround.d.ts.map +1 -1
- package/dist/papirus.js +873 -102
- 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;
|
|
@@ -1996,12 +2012,13 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1996
2012
|
let role = "other";
|
|
1997
2013
|
if (node.id === sourceNodeId) role = "source";
|
|
1998
2014
|
else if (node.id === targetNodeId) role = "target";
|
|
2015
|
+
const pad = 4;
|
|
1999
2016
|
obstacles.push({
|
|
2000
2017
|
id: node.id,
|
|
2001
|
-
x: node.x -
|
|
2002
|
-
y: node.y -
|
|
2003
|
-
width: node.width +
|
|
2004
|
-
height: node.height +
|
|
2018
|
+
x: node.x - pad,
|
|
2019
|
+
y: node.y - pad,
|
|
2020
|
+
width: node.width + pad * 2,
|
|
2021
|
+
height: node.height + pad * 2,
|
|
2005
2022
|
role
|
|
2006
2023
|
});
|
|
2007
2024
|
}
|
|
@@ -2016,7 +2033,10 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2016
2033
|
}
|
|
2017
2034
|
getTargetDirForReconnect(edge, endpoint) {
|
|
2018
2035
|
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
2019
|
-
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
|
+
}
|
|
2020
2040
|
return (ep.portId?.replace(ANCHOR_PORT_PREFIX, ""))?.split(":")[0];
|
|
2021
2041
|
}
|
|
2022
2042
|
/**
|
|
@@ -2331,8 +2351,9 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2331
2351
|
ctx.lineTo(end.x, end.y);
|
|
2332
2352
|
ctx.stroke();
|
|
2333
2353
|
} else {
|
|
2334
|
-
const
|
|
2335
|
-
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];
|
|
2336
2357
|
this.drawBezierPreview(ctx, start, end, fromDir, toDir);
|
|
2337
2358
|
}
|
|
2338
2359
|
ctx.setLineDash([]);
|
|
@@ -3803,6 +3824,9 @@ var StraightPathStrategy = class {
|
|
|
3803
3824
|
var DEFAULT_MARGIN = 12;
|
|
3804
3825
|
var DEFAULT_EXIT = 20;
|
|
3805
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;
|
|
3806
3830
|
function expandRect(r, m) {
|
|
3807
3831
|
return {
|
|
3808
3832
|
...r,
|
|
@@ -3812,6 +3836,133 @@ function expandRect(r, m) {
|
|
|
3812
3836
|
height: r.height + 2 * m
|
|
3813
3837
|
};
|
|
3814
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
|
+
}
|
|
3815
3966
|
function moveByDir$1(p, dir, d) {
|
|
3816
3967
|
switch (dir) {
|
|
3817
3968
|
case "top": return {
|
|
@@ -3857,11 +4008,41 @@ function segmentHitsObstacles(a, b, obstacles) {
|
|
|
3857
4008
|
function manhattan$1(a, b) {
|
|
3858
4009
|
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
3859
4010
|
}
|
|
3860
|
-
/**
|
|
3861
|
-
|
|
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 blocker (and at least exitDistance). */
|
|
4042
|
+
function computeStartExit(from, fromDir, blocker, margin, exitDistance) {
|
|
3862
4043
|
let p = moveByDir$1(from, fromDir, exitDistance);
|
|
3863
|
-
if (!
|
|
3864
|
-
const expanded = expandRect(
|
|
4044
|
+
if (!blocker) return p;
|
|
4045
|
+
const expanded = expandRect(blocker, margin);
|
|
3865
4046
|
let guard = 0;
|
|
3866
4047
|
while (pointInOpenInterior(p, expanded) && guard++ < 64) p = moveByDir$1(p, fromDir, exitDistance);
|
|
3867
4048
|
if (pointInOpenInterior(p, expanded)) {
|
|
@@ -3895,8 +4076,190 @@ function computeStartExit(from, fromDir, parent, margin, exitDistance) {
|
|
|
3895
4076
|
}
|
|
3896
4077
|
return p;
|
|
3897
4078
|
}
|
|
3898
|
-
function
|
|
3899
|
-
return
|
|
4079
|
+
function pointInRectClosed(p, r) {
|
|
4080
|
+
return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Push a same-side exit/entry far enough that the orthogonal span between
|
|
4084
|
+
* `spanA` and `spanB` clears every `others` obstacle (siblings in a stack).
|
|
4085
|
+
*/
|
|
4086
|
+
function clearCorridorOfOthers(exit, dir, others, spanA, spanB, margin) {
|
|
4087
|
+
if (others.length === 0) return exit;
|
|
4088
|
+
let p = { ...exit };
|
|
4089
|
+
const y1 = Math.min(spanA.y, spanB.y);
|
|
4090
|
+
const y2 = Math.max(spanA.y, spanB.y);
|
|
4091
|
+
const x1 = Math.min(spanA.x, spanB.x);
|
|
4092
|
+
const x2 = Math.max(spanA.x, spanB.x);
|
|
4093
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
4094
|
+
let blocked;
|
|
4095
|
+
for (const raw of others) {
|
|
4096
|
+
const o = expandRect(raw, margin);
|
|
4097
|
+
if (dir === "left" || dir === "right") {
|
|
4098
|
+
if (p.x > o.x && p.x < o.x + o.width && y2 > o.y && y1 < o.y + o.height) {
|
|
4099
|
+
blocked = o;
|
|
4100
|
+
break;
|
|
4101
|
+
}
|
|
4102
|
+
} else if (p.y > o.y && p.y < o.y + o.height && x2 > o.x && x1 < o.x + o.width) {
|
|
4103
|
+
blocked = o;
|
|
4104
|
+
break;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
if (!blocked) return p;
|
|
4108
|
+
switch (dir) {
|
|
4109
|
+
case "left":
|
|
4110
|
+
p = {
|
|
4111
|
+
x: blocked.x - 1,
|
|
4112
|
+
y: p.y
|
|
4113
|
+
};
|
|
4114
|
+
break;
|
|
4115
|
+
case "right":
|
|
4116
|
+
p = {
|
|
4117
|
+
x: blocked.x + blocked.width + 1,
|
|
4118
|
+
y: p.y
|
|
4119
|
+
};
|
|
4120
|
+
break;
|
|
4121
|
+
case "top":
|
|
4122
|
+
p = {
|
|
4123
|
+
x: p.x,
|
|
4124
|
+
y: blocked.y - 1
|
|
4125
|
+
};
|
|
4126
|
+
break;
|
|
4127
|
+
case "bottom":
|
|
4128
|
+
p = {
|
|
4129
|
+
x: p.x,
|
|
4130
|
+
y: blocked.y + blocked.height + 1
|
|
4131
|
+
};
|
|
4132
|
+
break;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
return p;
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* When same-side exits still share an axis that crosses a blocker (top→top over
|
|
4139
|
+
* a stacked target), slide the *start* exit to the nearer free side. Keep
|
|
4140
|
+
* endEntry on the approach ray at `to` so the final stub stays ⊥ to the side.
|
|
4141
|
+
*/
|
|
4142
|
+
function offsetSameSideAroundBlockers(startExit, endEntry, dir, blockers, margin, from, to) {
|
|
4143
|
+
if (blockers.length === 0) return {
|
|
4144
|
+
startExit,
|
|
4145
|
+
endEntry
|
|
4146
|
+
};
|
|
4147
|
+
let s = { ...startExit };
|
|
4148
|
+
const e = { ...endEntry };
|
|
4149
|
+
const y1 = Math.min(from.y, to.y, s.y, e.y);
|
|
4150
|
+
const y2 = Math.max(from.y, to.y, s.y, e.y);
|
|
4151
|
+
const x1 = Math.min(from.x, to.x, s.x, e.x);
|
|
4152
|
+
const x2 = Math.max(from.x, to.x, s.x, e.x);
|
|
4153
|
+
for (const raw of blockers) {
|
|
4154
|
+
const o = expandRect(raw, margin);
|
|
4155
|
+
if (dir === "top" || dir === "bottom") {
|
|
4156
|
+
if (!(s.x > o.x && s.x < o.x + o.width && y2 > o.y && y1 < o.y + o.height)) continue;
|
|
4157
|
+
const leftX = o.x - 1;
|
|
4158
|
+
const rightX = o.x + o.width + 1;
|
|
4159
|
+
s = {
|
|
4160
|
+
...s,
|
|
4161
|
+
x: s.x <= o.x + o.width / 2 ? leftX : rightX
|
|
4162
|
+
};
|
|
4163
|
+
} else {
|
|
4164
|
+
if (!(s.y > o.y && s.y < o.y + o.height && x2 > o.x && x1 < o.x + o.width)) continue;
|
|
4165
|
+
const topY = o.y - 1;
|
|
4166
|
+
const bottomY = o.y + o.height + 1;
|
|
4167
|
+
s = {
|
|
4168
|
+
...s,
|
|
4169
|
+
y: s.y <= o.y + o.height / 2 ? topY : bottomY
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return {
|
|
4174
|
+
startExit: s,
|
|
4175
|
+
endEntry: e
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
/** True when collapsing same-side exits onto one rail would not pierce blockers. */
|
|
4179
|
+
function canAlignSameSideExits(dir, startExit, endEntry, from, to, blockers, margin) {
|
|
4180
|
+
let alignedStart = { ...startExit };
|
|
4181
|
+
let alignedEnd = { ...endEntry };
|
|
4182
|
+
if (dir === "left") {
|
|
4183
|
+
const x = Math.min(startExit.x, endEntry.x);
|
|
4184
|
+
alignedStart = {
|
|
4185
|
+
...alignedStart,
|
|
4186
|
+
x
|
|
4187
|
+
};
|
|
4188
|
+
alignedEnd = {
|
|
4189
|
+
...alignedEnd,
|
|
4190
|
+
x
|
|
4191
|
+
};
|
|
4192
|
+
} else if (dir === "right") {
|
|
4193
|
+
const x = Math.max(startExit.x, endEntry.x);
|
|
4194
|
+
alignedStart = {
|
|
4195
|
+
...alignedStart,
|
|
4196
|
+
x
|
|
4197
|
+
};
|
|
4198
|
+
alignedEnd = {
|
|
4199
|
+
...alignedEnd,
|
|
4200
|
+
x
|
|
4201
|
+
};
|
|
4202
|
+
} else if (dir === "top") {
|
|
4203
|
+
const y = Math.min(startExit.y, endEntry.y);
|
|
4204
|
+
alignedStart = {
|
|
4205
|
+
...alignedStart,
|
|
4206
|
+
y
|
|
4207
|
+
};
|
|
4208
|
+
alignedEnd = {
|
|
4209
|
+
...alignedEnd,
|
|
4210
|
+
y
|
|
4211
|
+
};
|
|
4212
|
+
} else {
|
|
4213
|
+
const y = Math.max(startExit.y, endEntry.y);
|
|
4214
|
+
alignedStart = {
|
|
4215
|
+
...alignedStart,
|
|
4216
|
+
y
|
|
4217
|
+
};
|
|
4218
|
+
alignedEnd = {
|
|
4219
|
+
...alignedEnd,
|
|
4220
|
+
y
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
for (const raw of blockers) {
|
|
4224
|
+
const o = expandRect(raw, margin);
|
|
4225
|
+
if (segmentIntersectsOpenInterior(from, alignedStart, o)) return false;
|
|
4226
|
+
if (segmentIntersectsOpenInterior(alignedEnd, to, o)) return false;
|
|
4227
|
+
if (segmentIntersectsOpenInterior(alignedStart, alignedEnd, o)) return false;
|
|
4228
|
+
}
|
|
4229
|
+
return true;
|
|
4230
|
+
}
|
|
4231
|
+
function computeEndEntry(to, toDir, exitDistance, rawTarget, rawNeighbor) {
|
|
4232
|
+
const stub = moveByDir$1(to, toDir, exitDistance);
|
|
4233
|
+
if (!rawTarget || !rawNeighbor) return stub;
|
|
4234
|
+
const mid = facingGapMid(rawTarget, rawNeighbor);
|
|
4235
|
+
if (!mid) return stub;
|
|
4236
|
+
switch (toDir) {
|
|
4237
|
+
case "bottom":
|
|
4238
|
+
if (mid.y > to.y && rawNeighbor.y >= rawTarget.y + rawTarget.height) return {
|
|
4239
|
+
x: to.x,
|
|
4240
|
+
y: mid.y
|
|
4241
|
+
};
|
|
4242
|
+
break;
|
|
4243
|
+
case "top":
|
|
4244
|
+
if (mid.y < to.y && rawNeighbor.y + rawNeighbor.height <= rawTarget.y) return {
|
|
4245
|
+
x: to.x,
|
|
4246
|
+
y: mid.y
|
|
4247
|
+
};
|
|
4248
|
+
break;
|
|
4249
|
+
case "right":
|
|
4250
|
+
if (mid.x > to.x && rawNeighbor.x >= rawTarget.x + rawTarget.width) return {
|
|
4251
|
+
x: mid.x,
|
|
4252
|
+
y: to.y
|
|
4253
|
+
};
|
|
4254
|
+
break;
|
|
4255
|
+
case "left":
|
|
4256
|
+
if (mid.x < to.x && rawNeighbor.x + rawNeighbor.width <= rawTarget.x) return {
|
|
4257
|
+
x: mid.x,
|
|
4258
|
+
y: to.y
|
|
4259
|
+
};
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
return stub;
|
|
3900
4263
|
}
|
|
3901
4264
|
/**
|
|
3902
4265
|
* When nodes face each other closer than 2×exitDistance, fixed stubs cross
|
|
@@ -3972,8 +4335,12 @@ function simplifyPath$1(path) {
|
|
|
3972
4335
|
const a = out[i];
|
|
3973
4336
|
const b = out[i + 1];
|
|
3974
4337
|
const c = out[i + 2];
|
|
3975
|
-
const
|
|
3976
|
-
|
|
4338
|
+
const colinearV = Math.abs(a.x - b.x) < .001 && Math.abs(b.x - c.x) < .001;
|
|
4339
|
+
const colinearH = Math.abs(a.y - b.y) < .001 && Math.abs(b.y - c.y) < .001;
|
|
4340
|
+
const colinear = colinearV || colinearH;
|
|
4341
|
+
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);
|
|
4342
|
+
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);
|
|
4343
|
+
if (uturn || spur || colinear && i > 0 && i + 2 < out.length - 1) out.splice(i + 1, 1);
|
|
3977
4344
|
else i++;
|
|
3978
4345
|
}
|
|
3979
4346
|
return out;
|
|
@@ -4009,13 +4376,11 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
|
|
|
4009
4376
|
ySet.add(o.y - 1);
|
|
4010
4377
|
ySet.add(o.y + o.height + 1);
|
|
4011
4378
|
}
|
|
4012
|
-
if (parent && target
|
|
4013
|
-
const ep = expandRect(parent, margin);
|
|
4014
|
-
const et = expandRect(target, margin);
|
|
4379
|
+
if (parent && target) {
|
|
4015
4380
|
const addGapY = (gapY) => {
|
|
4016
4381
|
ySet.add(gapY);
|
|
4017
|
-
const left = Math.min(
|
|
4018
|
-
const right = Math.max(
|
|
4382
|
+
const left = Math.min(parent.x, target.x, startExit.x, endEntry.x);
|
|
4383
|
+
const right = Math.max(parent.x + parent.width, target.x + target.width, startExit.x, endEntry.x);
|
|
4019
4384
|
xSet.add(left);
|
|
4020
4385
|
xSet.add(right);
|
|
4021
4386
|
xSet.add(startExit.x);
|
|
@@ -4023,17 +4388,24 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
|
|
|
4023
4388
|
};
|
|
4024
4389
|
const addGapX = (gapX) => {
|
|
4025
4390
|
xSet.add(gapX);
|
|
4026
|
-
const top = Math.min(
|
|
4027
|
-
const bottom = Math.max(
|
|
4391
|
+
const top = Math.min(parent.y, target.y, startExit.y, endEntry.y);
|
|
4392
|
+
const bottom = Math.max(parent.y + parent.height, target.y + target.height, startExit.y, endEntry.y);
|
|
4028
4393
|
ySet.add(top);
|
|
4029
4394
|
ySet.add(bottom);
|
|
4030
4395
|
ySet.add(startExit.y);
|
|
4031
4396
|
ySet.add(endEntry.y);
|
|
4032
4397
|
};
|
|
4033
|
-
|
|
4034
|
-
if (
|
|
4035
|
-
|
|
4036
|
-
if (
|
|
4398
|
+
const mid = facingGapMid(parent, target);
|
|
4399
|
+
if (mid) if (parent.y + parent.height < target.y || target.y + target.height < parent.y) addGapY(mid.y);
|
|
4400
|
+
else addGapX(mid.x);
|
|
4401
|
+
else if (margin != null) {
|
|
4402
|
+
const ep = expandRect(parent, margin);
|
|
4403
|
+
const et = expandRect(target, margin);
|
|
4404
|
+
if (et.y + et.height < ep.y) addGapY((et.y + et.height + ep.y) / 2);
|
|
4405
|
+
if (ep.y + ep.height < et.y) addGapY((ep.y + ep.height + et.y) / 2);
|
|
4406
|
+
if (et.x + et.width < ep.x) addGapX((et.x + et.width + ep.x) / 2);
|
|
4407
|
+
if (ep.x + ep.width < et.x) addGapX((ep.x + ep.width + et.x) / 2);
|
|
4408
|
+
}
|
|
4037
4409
|
}
|
|
4038
4410
|
return {
|
|
4039
4411
|
xs: [...xSet].sort((a, b) => a - b),
|
|
@@ -4089,7 +4461,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
|
|
|
4089
4461
|
const a = nodes[list[i - 1]];
|
|
4090
4462
|
const b = nodes[list[i]];
|
|
4091
4463
|
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4092
|
-
const length = manhattan$1(a, b);
|
|
4464
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4093
4465
|
pushEdge(list[i - 1], list[i], "v", length);
|
|
4094
4466
|
pushEdge(list[i], list[i - 1], "v", length);
|
|
4095
4467
|
}
|
|
@@ -4101,7 +4473,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
|
|
|
4101
4473
|
const a = nodes[list[i - 1]];
|
|
4102
4474
|
const b = nodes[list[i]];
|
|
4103
4475
|
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4104
|
-
const length = manhattan$1(a, b);
|
|
4476
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4105
4477
|
pushEdge(list[i - 1], list[i], "h", length);
|
|
4106
4478
|
pushEdge(list[i], list[i - 1], "h", length);
|
|
4107
4479
|
}
|
|
@@ -4169,18 +4541,20 @@ function pathIsValid(path, obstacles) {
|
|
|
4169
4541
|
for (let i = 1; i < path.length; i++) if (segmentHitsObstacles(path[i - 1], path[i], obstacles)) return false;
|
|
4170
4542
|
return true;
|
|
4171
4543
|
}
|
|
4544
|
+
/** Always orthogonal: never emit a diagonal between startExit and endEntry. */
|
|
4545
|
+
function orthogonalElbow(startExit, endEntry) {
|
|
4546
|
+
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) return [startExit, endEntry];
|
|
4547
|
+
return [
|
|
4548
|
+
startExit,
|
|
4549
|
+
{
|
|
4550
|
+
x: startExit.x,
|
|
4551
|
+
y: endEntry.y
|
|
4552
|
+
},
|
|
4553
|
+
endEntry
|
|
4554
|
+
];
|
|
4555
|
+
}
|
|
4172
4556
|
function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
4173
|
-
if (obstacles.length === 0)
|
|
4174
|
-
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) return [startExit, endEntry];
|
|
4175
|
-
return [
|
|
4176
|
-
startExit,
|
|
4177
|
-
{
|
|
4178
|
-
x: startExit.x,
|
|
4179
|
-
y: endEntry.y
|
|
4180
|
-
},
|
|
4181
|
-
endEntry
|
|
4182
|
-
];
|
|
4183
|
-
}
|
|
4557
|
+
if (obstacles.length === 0) return orthogonalElbow(startExit, endEntry);
|
|
4184
4558
|
const u = unionBBox(obstacles);
|
|
4185
4559
|
const pad = 1;
|
|
4186
4560
|
const top = u.y - pad;
|
|
@@ -4235,6 +4609,22 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
|
4235
4609
|
y: endEntry.y
|
|
4236
4610
|
},
|
|
4237
4611
|
endEntry
|
|
4612
|
+
],
|
|
4613
|
+
[
|
|
4614
|
+
startExit,
|
|
4615
|
+
{
|
|
4616
|
+
x: startExit.x,
|
|
4617
|
+
y: endEntry.y
|
|
4618
|
+
},
|
|
4619
|
+
endEntry
|
|
4620
|
+
],
|
|
4621
|
+
[
|
|
4622
|
+
startExit,
|
|
4623
|
+
{
|
|
4624
|
+
x: endEntry.x,
|
|
4625
|
+
y: startExit.y
|
|
4626
|
+
},
|
|
4627
|
+
endEntry
|
|
4238
4628
|
]
|
|
4239
4629
|
];
|
|
4240
4630
|
let best = null;
|
|
@@ -4251,14 +4641,7 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
|
4251
4641
|
best = simplified;
|
|
4252
4642
|
}
|
|
4253
4643
|
}
|
|
4254
|
-
return best ??
|
|
4255
|
-
startExit,
|
|
4256
|
-
{
|
|
4257
|
-
x: startExit.x,
|
|
4258
|
-
y: endEntry.y
|
|
4259
|
-
},
|
|
4260
|
-
endEntry
|
|
4261
|
-
];
|
|
4644
|
+
return best ?? orthogonalElbow(startExit, endEntry);
|
|
4262
4645
|
}
|
|
4263
4646
|
function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
|
|
4264
4647
|
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) {
|
|
@@ -4275,39 +4658,231 @@ function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
|
|
|
4275
4658
|
function routeOrthogonalAround(input) {
|
|
4276
4659
|
const margin = input.margin ?? DEFAULT_MARGIN;
|
|
4277
4660
|
const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
|
|
4278
|
-
const
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4661
|
+
const others = input.others ?? [];
|
|
4662
|
+
const internalParent = !!input.parent && pointInRectClosed(input.from, input.parent) && pointInRectClosed(input.to, input.parent);
|
|
4663
|
+
const exitBlocker = internalParent ? input.source : input.parent ?? input.source;
|
|
4664
|
+
let rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
|
|
4665
|
+
const gapNeighbor = internalParent ? input.source : input.parent ?? input.source;
|
|
4666
|
+
const peeledTargetNeighbor = input.target && gapNeighbor ? peelForFacingGap(input.target, gapNeighbor) : null;
|
|
4667
|
+
let rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance, peeledTargetNeighbor?.a, peeledTargetNeighbor?.b);
|
|
4668
|
+
if (input.fromDir === input.toDir) {
|
|
4669
|
+
const sameSideBlockers = [...others];
|
|
4670
|
+
if (input.target) sameSideBlockers.push(input.target);
|
|
4671
|
+
if (input.source) sameSideBlockers.push(input.source);
|
|
4672
|
+
if (sameSideBlockers.length > 0) {
|
|
4673
|
+
rawStartExit = clearCorridorOfOthers(rawStartExit, input.fromDir, sameSideBlockers, input.from, input.to, margin);
|
|
4674
|
+
rawEndEntry = clearCorridorOfOthers(rawEndEntry, input.toDir, sameSideBlockers, input.from, input.to, margin);
|
|
4675
|
+
({startExit: rawStartExit, endEntry: rawEndEntry} = offsetSameSideAroundBlockers(rawStartExit, rawEndEntry, input.fromDir, sameSideBlockers, margin, input.from, input.to));
|
|
4676
|
+
}
|
|
4677
|
+
if (canAlignSameSideExits(input.fromDir, rawStartExit, rawEndEntry, input.from, input.to, sameSideBlockers.length > 0 ? sameSideBlockers : [input.target, input.source].filter((r) => !!r), margin)) {
|
|
4678
|
+
if (input.fromDir === "left") {
|
|
4679
|
+
const x = Math.min(rawStartExit.x, rawEndEntry.x);
|
|
4680
|
+
rawStartExit = {
|
|
4681
|
+
...rawStartExit,
|
|
4682
|
+
x
|
|
4683
|
+
};
|
|
4684
|
+
rawEndEntry = {
|
|
4685
|
+
...rawEndEntry,
|
|
4686
|
+
x
|
|
4687
|
+
};
|
|
4688
|
+
} else if (input.fromDir === "right") {
|
|
4689
|
+
const x = Math.max(rawStartExit.x, rawEndEntry.x);
|
|
4690
|
+
rawStartExit = {
|
|
4691
|
+
...rawStartExit,
|
|
4692
|
+
x
|
|
4693
|
+
};
|
|
4694
|
+
rawEndEntry = {
|
|
4695
|
+
...rawEndEntry,
|
|
4696
|
+
x
|
|
4697
|
+
};
|
|
4698
|
+
} else if (input.fromDir === "top") {
|
|
4699
|
+
const y = Math.min(rawStartExit.y, rawEndEntry.y);
|
|
4700
|
+
rawStartExit = {
|
|
4701
|
+
...rawStartExit,
|
|
4702
|
+
y
|
|
4703
|
+
};
|
|
4704
|
+
rawEndEntry = {
|
|
4705
|
+
...rawEndEntry,
|
|
4706
|
+
y
|
|
4707
|
+
};
|
|
4708
|
+
} else if (input.fromDir === "bottom") {
|
|
4709
|
+
const y = Math.max(rawStartExit.y, rawEndEntry.y);
|
|
4710
|
+
rawStartExit = {
|
|
4711
|
+
...rawStartExit,
|
|
4712
|
+
y
|
|
4713
|
+
};
|
|
4714
|
+
rawEndEntry = {
|
|
4715
|
+
...rawEndEntry,
|
|
4716
|
+
y
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4281
4721
|
const { startExit, endEntry } = clampFacingExitEntry(input.from, input.to, input.fromDir, input.toDir, rawStartExit, rawEndEntry);
|
|
4722
|
+
let sourceObs = input.source ? expandRect(input.source, margin) : void 0;
|
|
4723
|
+
let parentObs = input.parent && !internalParent ? expandRect(input.parent, margin) : void 0;
|
|
4724
|
+
let targetObs = input.target ? expandRect(input.target, margin) : void 0;
|
|
4725
|
+
const openCorridor = (expA, expB, inputA, inputB) => {
|
|
4726
|
+
const peeled = peelForFacingGap(inputA, inputB);
|
|
4727
|
+
if (peeled) return preserveFacingGap(expA, expB, peeled.a, peeled.b);
|
|
4728
|
+
return preserveFacingGap(expA, expB, inputA, inputB);
|
|
4729
|
+
};
|
|
4730
|
+
if (parentObs && targetObs && input.parent && input.target) [parentObs, targetObs] = openCorridor(parentObs, targetObs, input.parent, input.target);
|
|
4731
|
+
else if (sourceObs && targetObs && input.source && input.target) [sourceObs, targetObs] = openCorridor(sourceObs, targetObs, input.source, input.target);
|
|
4732
|
+
if (sourceObs && parentObs && input.source && input.parent) [sourceObs, parentObs] = openCorridor(sourceObs, parentObs, input.source, input.parent);
|
|
4282
4733
|
const obstacles = [];
|
|
4283
|
-
if (
|
|
4284
|
-
if (
|
|
4285
|
-
if (
|
|
4286
|
-
const
|
|
4287
|
-
|
|
4734
|
+
if (sourceObs) obstacles.push(sourceObs);
|
|
4735
|
+
if (parentObs) obstacles.push(parentObs);
|
|
4736
|
+
if (targetObs) obstacles.push(targetObs);
|
|
4737
|
+
for (const other of others) obstacles.push(expandRect(other, margin));
|
|
4738
|
+
const peeledLattice = !internalParent && input.parent && input.target ? peelForFacingGap(input.parent, input.target) : input.source && input.target ? peelForFacingGap(input.source, input.target) : null;
|
|
4739
|
+
const mid = routeMidPath(startExit, endEntry, obstacles, peeledLattice?.a ?? (internalParent ? input.source : input.parent ?? input.source), peeledLattice?.b ?? input.target, margin).slice(1, -1);
|
|
4740
|
+
const lastMid = mid[mid.length - 1];
|
|
4741
|
+
const alreadyApproaching = !!lastMid && isOnApproachOutside(lastMid, input.to, input.toDir, 1);
|
|
4742
|
+
return simplifyPath$1(ensureOrthogonalPath([
|
|
4288
4743
|
input.from,
|
|
4289
4744
|
startExit,
|
|
4290
4745
|
...mid,
|
|
4291
|
-
endEntry,
|
|
4746
|
+
...alreadyApproaching ? [] : [endEntry],
|
|
4292
4747
|
input.to
|
|
4293
|
-
]
|
|
4748
|
+
], {
|
|
4749
|
+
exitDir: input.fromDir,
|
|
4750
|
+
enterDir: input.toDir
|
|
4751
|
+
}));
|
|
4752
|
+
}
|
|
4753
|
+
/** Elbow that leaves `prev` along `dir` first (source exit). */
|
|
4754
|
+
function elbowExitFirst(prev, next, dir) {
|
|
4755
|
+
switch (dir) {
|
|
4756
|
+
case "left":
|
|
4757
|
+
case "right": return {
|
|
4758
|
+
x: next.x,
|
|
4759
|
+
y: prev.y
|
|
4760
|
+
};
|
|
4761
|
+
case "top":
|
|
4762
|
+
case "bottom": return {
|
|
4763
|
+
x: prev.x,
|
|
4764
|
+
y: next.y
|
|
4765
|
+
};
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
/** Elbow that arrives at `next` along `dir` last (target entry). */
|
|
4769
|
+
function elbowEnterLast(prev, next, dir) {
|
|
4770
|
+
switch (dir) {
|
|
4771
|
+
case "left":
|
|
4772
|
+
case "right": return {
|
|
4773
|
+
x: prev.x,
|
|
4774
|
+
y: next.y
|
|
4775
|
+
};
|
|
4776
|
+
case "top":
|
|
4777
|
+
case "bottom": return {
|
|
4778
|
+
x: next.x,
|
|
4779
|
+
y: prev.y
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
/** Insert elbows so no consecutive points form a diagonal (safety net). */
|
|
4784
|
+
function ensureOrthogonalPath(path, options) {
|
|
4785
|
+
if (path.length < 2) return path;
|
|
4786
|
+
const out = [path[0]];
|
|
4787
|
+
for (let i = 1; i < path.length; i++) {
|
|
4788
|
+
const prev = out[out.length - 1];
|
|
4789
|
+
const next = path[i];
|
|
4790
|
+
if (!(Math.abs(prev.x - next.x) < .001 || Math.abs(prev.y - next.y) < .001)) {
|
|
4791
|
+
const isFirst = out.length === 1;
|
|
4792
|
+
const isLast = i === path.length - 1;
|
|
4793
|
+
let elbow;
|
|
4794
|
+
if (isFirst && options?.exitDir) elbow = elbowExitFirst(prev, next, options.exitDir);
|
|
4795
|
+
else if (isLast && options?.enterDir) elbow = elbowEnterLast(prev, next, options.enterDir);
|
|
4796
|
+
else elbow = {
|
|
4797
|
+
x: prev.x,
|
|
4798
|
+
y: next.y
|
|
4799
|
+
};
|
|
4800
|
+
out.push(elbow);
|
|
4801
|
+
}
|
|
4802
|
+
out.push(next);
|
|
4803
|
+
}
|
|
4804
|
+
return out;
|
|
4805
|
+
}
|
|
4806
|
+
function isOnApproachOutside(p, to, toDir, minClearance) {
|
|
4807
|
+
switch (toDir) {
|
|
4808
|
+
case "top": return Math.abs(p.x - to.x) < .001 && p.y <= to.y - minClearance;
|
|
4809
|
+
case "bottom": return Math.abs(p.x - to.x) < .001 && p.y >= to.y + minClearance;
|
|
4810
|
+
case "left": return Math.abs(p.y - to.y) < .001 && p.x <= to.x - minClearance;
|
|
4811
|
+
case "right": return Math.abs(p.y - to.y) < .001 && p.x >= to.x + minClearance;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
//#endregion
|
|
4815
|
+
//#region src/utils/routeDebug.ts
|
|
4816
|
+
function isRouteDebugEnabled() {
|
|
4817
|
+
if (typeof globalThis === "undefined") return false;
|
|
4818
|
+
const g = globalThis;
|
|
4819
|
+
if (g.__PAPIRUS_ROUTE_DEBUG__ === true || g.__PAPIRUS_ROUTE_DEBUG__ === 1) return true;
|
|
4820
|
+
try {
|
|
4821
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("papirusRouteDebug") === "1") return true;
|
|
4822
|
+
} catch {}
|
|
4823
|
+
return false;
|
|
4824
|
+
}
|
|
4825
|
+
function routeDebug(tag, payload) {
|
|
4826
|
+
if (!isRouteDebugEnabled()) return;
|
|
4827
|
+
console.log(`[papirus:route] ${tag}`, payload);
|
|
4294
4828
|
}
|
|
4295
4829
|
//#endregion
|
|
4296
4830
|
//#region src/elements/paths/PolylinePathStrategy.ts
|
|
4297
4831
|
function isSide(v) {
|
|
4298
4832
|
return v === "top" || v === "right" || v === "bottom" || v === "left";
|
|
4299
4833
|
}
|
|
4834
|
+
function pointInObstacle(p, o) {
|
|
4835
|
+
return p.x >= o.x && p.x <= o.x + o.width && p.y >= o.y && p.y <= o.y + o.height;
|
|
4836
|
+
}
|
|
4837
|
+
function pointOnObstacleOutline(p, o, tol = 1) {
|
|
4838
|
+
const onVertical = (Math.abs(p.x - o.x) < tol || Math.abs(p.x - (o.x + o.width)) < tol) && p.y >= o.y - tol && p.y <= o.y + o.height + tol;
|
|
4839
|
+
const onHorizontal = (Math.abs(p.y - o.y) < tol || Math.abs(p.y - (o.y + o.height)) < tol) && p.x >= o.x - tol && p.x <= o.x + o.width + tol;
|
|
4840
|
+
return onVertical || onHorizontal;
|
|
4841
|
+
}
|
|
4842
|
+
/** Prefer the tightest rect that contains `p` (source/composite vs ancestor). */
|
|
4843
|
+
function smallestContaining(p, obstacles, exclude) {
|
|
4844
|
+
let best;
|
|
4845
|
+
let bestArea = Infinity;
|
|
4846
|
+
for (const o of obstacles) {
|
|
4847
|
+
if (exclude.has(o) || !pointInObstacle(p, o)) continue;
|
|
4848
|
+
const area = o.width * o.height;
|
|
4849
|
+
if (area < bestArea) {
|
|
4850
|
+
best = o;
|
|
4851
|
+
bestArea = area;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
return best;
|
|
4855
|
+
}
|
|
4300
4856
|
function resolveRoutingRoles(from, to, obstacles) {
|
|
4301
|
-
const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) =>
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4857
|
+
const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) => pointOnObstacleOutline(to, o));
|
|
4858
|
+
const excludeForSource = /* @__PURE__ */ new Set();
|
|
4859
|
+
if (target) excludeForSource.add(target);
|
|
4860
|
+
const source = obstacles.find((o) => o.role === "source" && o !== target) ?? smallestContaining(from, obstacles, excludeForSource);
|
|
4861
|
+
const excludeForParent = /* @__PURE__ */ new Set();
|
|
4862
|
+
if (target) excludeForParent.add(target);
|
|
4863
|
+
if (source) excludeForParent.add(source);
|
|
4864
|
+
const parent = smallestContaining(from, obstacles, excludeForParent);
|
|
4865
|
+
const spanPad = 40;
|
|
4866
|
+
const span = {
|
|
4867
|
+
x: Math.min(from.x, to.x) - spanPad,
|
|
4868
|
+
y: Math.min(from.y, to.y) - spanPad,
|
|
4869
|
+
width: Math.abs(to.x - from.x) + spanPad * 2,
|
|
4870
|
+
height: Math.abs(to.y - from.y) + spanPad * 2
|
|
4871
|
+
};
|
|
4872
|
+
const rectsOverlap = (a, b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
4307
4873
|
return {
|
|
4308
|
-
parent
|
|
4874
|
+
parent,
|
|
4309
4875
|
source,
|
|
4310
|
-
target
|
|
4876
|
+
target,
|
|
4877
|
+
others: obstacles.filter((o) => {
|
|
4878
|
+
if (o === parent || o === source || o === target) return false;
|
|
4879
|
+
if (pointInObstacle(from, o) && pointInObstacle(to, o)) return false;
|
|
4880
|
+
if (parent) return pointInObstacle({
|
|
4881
|
+
x: o.x + o.width / 2,
|
|
4882
|
+
y: o.y + o.height / 2
|
|
4883
|
+
}, parent);
|
|
4884
|
+
return rectsOverlap(o, span);
|
|
4885
|
+
})
|
|
4311
4886
|
};
|
|
4312
4887
|
}
|
|
4313
4888
|
var MIN_SEGMENT_LENGTH = 20;
|
|
@@ -4417,10 +4992,11 @@ function inferApproachDir(from, to) {
|
|
|
4417
4992
|
}
|
|
4418
4993
|
/** Which side of `target` the attachment point `to` lies on (border hit). */
|
|
4419
4994
|
function sideOfPointOnRect(to, target) {
|
|
4420
|
-
const
|
|
4421
|
-
const
|
|
4422
|
-
const
|
|
4423
|
-
const
|
|
4995
|
+
const tol = 6;
|
|
4996
|
+
const onLeft = Math.abs(to.x - target.x) <= tol;
|
|
4997
|
+
const onRight = Math.abs(to.x - (target.x + target.width)) <= tol;
|
|
4998
|
+
const onTop = Math.abs(to.y - target.y) <= tol;
|
|
4999
|
+
const onBottom = Math.abs(to.y - (target.y + target.height)) <= tol;
|
|
4424
5000
|
if (onTop && !onLeft && !onRight) return "top";
|
|
4425
5001
|
if (onBottom && !onLeft && !onRight) return "bottom";
|
|
4426
5002
|
if (onLeft && !onTop && !onBottom) return "left";
|
|
@@ -4591,12 +5167,11 @@ var PolylinePathStrategy = class {
|
|
|
4591
5167
|
calculatePath(from, to, fromDir, toDir, _options) {
|
|
4592
5168
|
const controlPoints = _options?.controlPoints;
|
|
4593
5169
|
if (_options?.editablePolyline) {
|
|
4594
|
-
|
|
5170
|
+
const path = controlPoints && controlPoints.length > 0 ? [
|
|
4595
5171
|
from,
|
|
4596
5172
|
...controlPoints,
|
|
4597
5173
|
to
|
|
4598
|
-
]
|
|
4599
|
-
return [
|
|
5174
|
+
] : [
|
|
4600
5175
|
from,
|
|
4601
5176
|
{
|
|
4602
5177
|
x: (from.x + to.x) / 2,
|
|
@@ -4604,10 +5179,32 @@ var PolylinePathStrategy = class {
|
|
|
4604
5179
|
},
|
|
4605
5180
|
to
|
|
4606
5181
|
];
|
|
5182
|
+
routeDebug("polyline.via", {
|
|
5183
|
+
via: "editable",
|
|
5184
|
+
path
|
|
5185
|
+
});
|
|
5186
|
+
return path;
|
|
4607
5187
|
}
|
|
4608
5188
|
const dist = distance(from, to);
|
|
4609
5189
|
const selfLoop = _options?.selfLoop ?? false;
|
|
4610
5190
|
const obstacles = _options?.obstacles ?? [];
|
|
5191
|
+
const finish = (via, path) => {
|
|
5192
|
+
const roles = resolveRoutingRoles(from, to, obstacles);
|
|
5193
|
+
routeDebug("polyline.via", {
|
|
5194
|
+
via,
|
|
5195
|
+
fromDir,
|
|
5196
|
+
toDir,
|
|
5197
|
+
approachDir: roles.target ? resolveApproachDir(from, to, toDir, roles.target) : toDir,
|
|
5198
|
+
roles: {
|
|
5199
|
+
parent: roles.parent?.id,
|
|
5200
|
+
source: roles.source?.id,
|
|
5201
|
+
target: roles.target?.id
|
|
5202
|
+
},
|
|
5203
|
+
obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`),
|
|
5204
|
+
path: path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → ")
|
|
5205
|
+
});
|
|
5206
|
+
return path;
|
|
5207
|
+
};
|
|
4611
5208
|
if (selfLoop && dist < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) switch (fromDir ?? toDir) {
|
|
4612
5209
|
case "bottom": return [
|
|
4613
5210
|
from,
|
|
@@ -4736,9 +5333,9 @@ var PolylinePathStrategy = class {
|
|
|
4736
5333
|
];
|
|
4737
5334
|
}
|
|
4738
5335
|
if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
|
|
4739
|
-
const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
|
|
5336
|
+
const { parent, source, target, others } = resolveRoutingRoles(from, to, obstacles);
|
|
4740
5337
|
const approachDir = resolveApproachDir(from, to, toDir, target);
|
|
4741
|
-
if ((parent || source || target) && isSide(approachDir)) return routeOrthogonalAround({
|
|
5338
|
+
if ((parent || source || target) && isSide(approachDir)) return finish("routeOrthogonalAround", routeOrthogonalAround({
|
|
4742
5339
|
from,
|
|
4743
5340
|
to,
|
|
4744
5341
|
fromDir,
|
|
@@ -4746,17 +5343,31 @@ var PolylinePathStrategy = class {
|
|
|
4746
5343
|
parent,
|
|
4747
5344
|
source,
|
|
4748
5345
|
target,
|
|
5346
|
+
others,
|
|
4749
5347
|
margin: 4,
|
|
4750
5348
|
exitDistance: 20
|
|
5349
|
+
}));
|
|
5350
|
+
routeDebug("polyline.via", {
|
|
5351
|
+
via: "roles-missed-around",
|
|
5352
|
+
fromDir,
|
|
5353
|
+
toDir,
|
|
5354
|
+
approachDir,
|
|
5355
|
+
roles: {
|
|
5356
|
+
parent: parent?.id,
|
|
5357
|
+
source: source?.id,
|
|
5358
|
+
target: target?.id,
|
|
5359
|
+
others: others.map((o) => o.id).filter(Boolean)
|
|
5360
|
+
},
|
|
5361
|
+
obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`)
|
|
4751
5362
|
});
|
|
4752
5363
|
}
|
|
4753
5364
|
if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
|
|
4754
5365
|
const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
|
|
4755
|
-
if (routed) return routed;
|
|
5366
|
+
if (routed) return finish("buildRoutedPolyline", routed);
|
|
4756
5367
|
}
|
|
4757
|
-
if (fromDir || toDir) return this.calculateDirectedPath(from, to, fromDir, toDir);
|
|
5368
|
+
if (fromDir || toDir) return finish("calculateDirectedPath", this.calculateDirectedPath(from, to, fromDir, toDir));
|
|
4758
5369
|
const midX = (from.x + to.x) / 2;
|
|
4759
|
-
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return [
|
|
5370
|
+
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return finish("manhattan-h", [
|
|
4760
5371
|
from,
|
|
4761
5372
|
{
|
|
4762
5373
|
x: midX,
|
|
@@ -4767,9 +5378,9 @@ var PolylinePathStrategy = class {
|
|
|
4767
5378
|
y: to.y
|
|
4768
5379
|
},
|
|
4769
5380
|
to
|
|
4770
|
-
];
|
|
5381
|
+
]);
|
|
4771
5382
|
const midY = (from.y + to.y) / 2;
|
|
4772
|
-
return [
|
|
5383
|
+
return finish("manhattan-v", [
|
|
4773
5384
|
from,
|
|
4774
5385
|
{
|
|
4775
5386
|
x: from.x,
|
|
@@ -4780,7 +5391,7 @@ var PolylinePathStrategy = class {
|
|
|
4780
5391
|
y: midY
|
|
4781
5392
|
},
|
|
4782
5393
|
to
|
|
4783
|
-
];
|
|
5394
|
+
]);
|
|
4784
5395
|
}
|
|
4785
5396
|
calculateDirectedPath(from, to, fromDir, toDir) {
|
|
4786
5397
|
const points = [from];
|
|
@@ -5482,6 +6093,14 @@ function getEdgeLabelRotation(options) {
|
|
|
5482
6093
|
return angle;
|
|
5483
6094
|
}
|
|
5484
6095
|
//#endregion
|
|
6096
|
+
//#region src/types.ts
|
|
6097
|
+
function isNodeEdgeEndpoint(endpoint) {
|
|
6098
|
+
return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
|
|
6099
|
+
}
|
|
6100
|
+
function isEdgeEdgeEndpoint(endpoint) {
|
|
6101
|
+
return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
|
|
6102
|
+
}
|
|
6103
|
+
//#endregion
|
|
5485
6104
|
//#region src/elements/Edge.ts
|
|
5486
6105
|
var DEFAULT_EDGE_STYLE = {
|
|
5487
6106
|
strokeColor: "#666666",
|
|
@@ -5851,6 +6470,11 @@ var Edge = class extends Element {
|
|
|
5851
6470
|
const routingOptionsChanged = !routingOptionsEqual(this._pathOptions, options);
|
|
5852
6471
|
if (geometryUnchanged && !routingOptionsChanged) {
|
|
5853
6472
|
this._pathOptions = options;
|
|
6473
|
+
routeDebug("edge.updateEndpoints:skip", {
|
|
6474
|
+
id: this.id,
|
|
6475
|
+
type: this._type,
|
|
6476
|
+
reason: "geometry+routing unchanged"
|
|
6477
|
+
});
|
|
5854
6478
|
return;
|
|
5855
6479
|
}
|
|
5856
6480
|
this._fromPoint = fromPoint;
|
|
@@ -5870,6 +6494,51 @@ var Edge = class extends Element {
|
|
|
5870
6494
|
editablePolyline: this.isEditablePolyline(),
|
|
5871
6495
|
selfLoop: this._from.nodeId === this._to.nodeId
|
|
5872
6496
|
});
|
|
6497
|
+
if (this._type === "polyline" || this._type === "editable-polyline" || this._type === "bezier") {
|
|
6498
|
+
const obstacles = this._pathOptions?.obstacles ?? [];
|
|
6499
|
+
const segs = [];
|
|
6500
|
+
for (let i = 1; i < this._path.length; i++) {
|
|
6501
|
+
const a = this._path[i - 1];
|
|
6502
|
+
const b = this._path[i];
|
|
6503
|
+
const dx = b.x - a.x;
|
|
6504
|
+
const dy = b.y - a.y;
|
|
6505
|
+
const axis = Math.abs(dy) < .01 ? "H" : Math.abs(dx) < .01 ? "V" : `DIAG(${dx.toFixed(0)},${dy.toFixed(0)})`;
|
|
6506
|
+
segs.push({
|
|
6507
|
+
axis,
|
|
6508
|
+
len: Math.round(Math.abs(dx) + Math.abs(dy)),
|
|
6509
|
+
a: `${Math.round(a.x)},${Math.round(a.y)}`,
|
|
6510
|
+
b: `${Math.round(b.x)},${Math.round(b.y)}`
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6513
|
+
routeDebug("edge.recalculatePath", {
|
|
6514
|
+
id: this.id,
|
|
6515
|
+
type: this._type,
|
|
6516
|
+
from: {
|
|
6517
|
+
nodeId: this._from.nodeId,
|
|
6518
|
+
portId: isNodeEdgeEndpoint(this._from) ? this._from.portId : void 0,
|
|
6519
|
+
outlineParam: isNodeEdgeEndpoint(this._from) ? this._from.outlineParam : void 0,
|
|
6520
|
+
point: `${Math.round(this._fromPoint.x)},${Math.round(this._fromPoint.y)}`,
|
|
6521
|
+
dir: this._fromDir
|
|
6522
|
+
},
|
|
6523
|
+
to: {
|
|
6524
|
+
nodeId: this._to.nodeId,
|
|
6525
|
+
portId: isNodeEdgeEndpoint(this._to) ? this._to.portId : void 0,
|
|
6526
|
+
outlineParam: isNodeEdgeEndpoint(this._to) ? this._to.outlineParam : void 0,
|
|
6527
|
+
point: `${Math.round(this._toPoint.x)},${Math.round(this._toPoint.y)}`,
|
|
6528
|
+
dir: this._toDir
|
|
6529
|
+
},
|
|
6530
|
+
lockAnchors: this.lockAnchors,
|
|
6531
|
+
obstacleCount: obstacles.length,
|
|
6532
|
+
obstacles: obstacles.map((o) => ({
|
|
6533
|
+
id: o.id,
|
|
6534
|
+
role: o.role,
|
|
6535
|
+
box: `${Math.round(o.x)},${Math.round(o.y)} ${Math.round(o.width)}x${Math.round(o.height)}`
|
|
6536
|
+
})),
|
|
6537
|
+
path: this._path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → "),
|
|
6538
|
+
segments: segs,
|
|
6539
|
+
controlPoints: this._controlPoints?.length ?? 0
|
|
6540
|
+
});
|
|
6541
|
+
}
|
|
5873
6542
|
this.updateBounds();
|
|
5874
6543
|
this.markDirty();
|
|
5875
6544
|
}
|
|
@@ -7597,14 +8266,6 @@ var ContextMenuManager = class extends EventEmitter {
|
|
|
7597
8266
|
}
|
|
7598
8267
|
};
|
|
7599
8268
|
//#endregion
|
|
7600
|
-
//#region src/types.ts
|
|
7601
|
-
function isNodeEdgeEndpoint(endpoint) {
|
|
7602
|
-
return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
|
|
7603
|
-
}
|
|
7604
|
-
function isEdgeEdgeEndpoint(endpoint) {
|
|
7605
|
-
return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
|
|
7606
|
-
}
|
|
7607
|
-
//#endregion
|
|
7608
8269
|
//#region src/core/EdgeEndpointUpdater.ts
|
|
7609
8270
|
var EDGE_ATTACH_MAX_PASSES = 3;
|
|
7610
8271
|
var EdgeEndpointUpdater = class {
|
|
@@ -7612,7 +8273,7 @@ var EdgeEndpointUpdater = class {
|
|
|
7612
8273
|
this.host = host;
|
|
7613
8274
|
}
|
|
7614
8275
|
updateForDrag() {
|
|
7615
|
-
this.update(
|
|
8276
|
+
this.update(true);
|
|
7616
8277
|
}
|
|
7617
8278
|
updateAll() {
|
|
7618
8279
|
this.update(true);
|
|
@@ -7640,7 +8301,10 @@ var EdgeEndpointUpdater = class {
|
|
|
7640
8301
|
const fromNode = nodes.get(edge.from.nodeId);
|
|
7641
8302
|
const toNode = nodes.get(edge.to.nodeId);
|
|
7642
8303
|
if (!fromNode || !toNode) return;
|
|
7643
|
-
if (includeRoutingObstacles)
|
|
8304
|
+
if (includeRoutingObstacles) {
|
|
8305
|
+
this.preferFacingSidePorts(edge, fromNode, toNode);
|
|
8306
|
+
this.lockAnchors(edge, fromNode, toNode);
|
|
8307
|
+
}
|
|
7644
8308
|
const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
|
|
7645
8309
|
const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
|
|
7646
8310
|
const fromId = edge.from.nodeId;
|
|
@@ -7656,6 +8320,27 @@ var EdgeEndpointUpdater = class {
|
|
|
7656
8320
|
};
|
|
7657
8321
|
return obstacle;
|
|
7658
8322
|
});
|
|
8323
|
+
routeDebug("updater.nodeNode", {
|
|
8324
|
+
edgeId: edge.id,
|
|
8325
|
+
type: edge.type,
|
|
8326
|
+
includeRoutingObstacles,
|
|
8327
|
+
lockAnchors: edge.lockAnchors,
|
|
8328
|
+
from: {
|
|
8329
|
+
nodeId: fromId,
|
|
8330
|
+
portId: edge.from.portId,
|
|
8331
|
+
outlineParam: edge.from.outlineParam,
|
|
8332
|
+
point: `${Math.round(from.point.x)},${Math.round(from.point.y)}`,
|
|
8333
|
+
dir: from.direction
|
|
8334
|
+
},
|
|
8335
|
+
to: {
|
|
8336
|
+
nodeId: toId,
|
|
8337
|
+
portId: edge.to.portId,
|
|
8338
|
+
outlineParam: edge.to.outlineParam,
|
|
8339
|
+
point: `${Math.round(to.point.x)},${Math.round(to.point.y)}`,
|
|
8340
|
+
dir: to.direction
|
|
8341
|
+
},
|
|
8342
|
+
obstacleCount: routingObstacles?.length ?? 0
|
|
8343
|
+
});
|
|
7659
8344
|
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, routingObstacles ? { obstacles: routingObstacles } : void 0);
|
|
7660
8345
|
}
|
|
7661
8346
|
/**
|
|
@@ -7706,6 +8391,88 @@ var EdgeEndpointUpdater = class {
|
|
|
7706
8391
|
}
|
|
7707
8392
|
}
|
|
7708
8393
|
/**
|
|
8394
|
+
* When nodes are clearly stacked, retarget unset / OEF-lateral ports toward
|
|
8395
|
+
* facing sides. Intentional same-side wraps (bottom→bottom, top→top, left→left)
|
|
8396
|
+
* are kept — orthogonal routing clears them around the stack.
|
|
8397
|
+
*
|
|
8398
|
+
* Uses bounding-box gaps (not center dx/dy): a large horizontal offset must not
|
|
8399
|
+
* flip bottom→left while the source is still below the target — that reintroduces
|
|
8400
|
+
* the left-edge contour jog when the user drags the node leftward.
|
|
8401
|
+
*
|
|
8402
|
+
* - Works for portId and outlineParam (OEF / attach-to-outline).
|
|
8403
|
+
* - Does NOT move an endpoint that is already on a chosen side — otherwise
|
|
8404
|
+
* dragging a locked/outline anchor snaps back on every updateAll.
|
|
8405
|
+
* - Keeps intentional source left/right exits for nest wrap around a parent.
|
|
8406
|
+
*/
|
|
8407
|
+
preferFacingSidePorts(edge, fromNode, toNode) {
|
|
8408
|
+
if (!edge.lockAnchors) return;
|
|
8409
|
+
if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
|
|
8410
|
+
const fromB = fromNode.getBounds();
|
|
8411
|
+
const toB = toNode.getBounds();
|
|
8412
|
+
const fromBelow = fromB.y - (toB.y + toB.height);
|
|
8413
|
+
const fromAbove = toB.y - (fromB.y + fromB.height);
|
|
8414
|
+
const fromRight = fromB.x - (toB.x + toB.width);
|
|
8415
|
+
const fromLeft = toB.x - (fromB.x + fromB.width);
|
|
8416
|
+
const fc = fromNode.getCenter();
|
|
8417
|
+
const tc = toNode.getCenter();
|
|
8418
|
+
const curFrom = this.endpointSide(edge.from, fromNode);
|
|
8419
|
+
const curTo = this.endpointSide(edge.to, toNode);
|
|
8420
|
+
if (fromBelow > 0 || fromAbove > 0) {
|
|
8421
|
+
const fromSide = fromBelow > 0 ? "top" : "bottom";
|
|
8422
|
+
const toSide = fromBelow > 0 ? "bottom" : "top";
|
|
8423
|
+
const toOutlineLateral = (curTo === "left" || curTo === "right") && isNodeEdgeEndpoint(edge.to) && edge.to.outlineParam !== void 0;
|
|
8424
|
+
const toPortLateral = (curTo === "left" || curTo === "right") && !toOutlineLateral;
|
|
8425
|
+
if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
|
|
8426
|
+
if (!curTo || toPortLateral) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
|
|
8427
|
+
return;
|
|
8428
|
+
}
|
|
8429
|
+
if (fromLeft > 0 || fromRight > 0) {
|
|
8430
|
+
const fromSide = fromRight > 0 ? "left" : "right";
|
|
8431
|
+
const toSide = fromRight > 0 ? "right" : "left";
|
|
8432
|
+
const toOutlineVertical = (curTo === "top" || curTo === "bottom") && isNodeEdgeEndpoint(edge.to) && edge.to.outlineParam !== void 0;
|
|
8433
|
+
const toPortVertical = (curTo === "top" || curTo === "bottom") && !toOutlineVertical;
|
|
8434
|
+
if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
|
|
8435
|
+
if (!curTo || toPortVertical) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
portSide(portId) {
|
|
8439
|
+
if (!portId?.startsWith("anchor:")) return void 0;
|
|
8440
|
+
const side = portId.slice(ANCHOR_PORT_PREFIX.length).split(":")[0];
|
|
8441
|
+
if (side === "top" || side === "right" || side === "bottom" || side === "left") return side;
|
|
8442
|
+
}
|
|
8443
|
+
endpointSide(endpoint, node) {
|
|
8444
|
+
if (!isNodeEdgeEndpoint(endpoint)) return void 0;
|
|
8445
|
+
const fromPort = this.portSide(endpoint.portId);
|
|
8446
|
+
if (fromPort) return fromPort;
|
|
8447
|
+
if (endpoint.outlineParam === void 0) return void 0;
|
|
8448
|
+
return getDirectionFromOutlineParam(endpoint.outlineParam, node.getBounds());
|
|
8449
|
+
}
|
|
8450
|
+
nearestAnchorOnSide(node, side, toward) {
|
|
8451
|
+
const anchors = node.getAnchors().filter((a) => a.id.startsWith(`${side}:`));
|
|
8452
|
+
if (anchors.length === 0) return null;
|
|
8453
|
+
let best = anchors[0];
|
|
8454
|
+
let minDist = Infinity;
|
|
8455
|
+
for (const anchor of anchors) {
|
|
8456
|
+
const dist = (anchor.point.x - toward.x) ** 2 + (anchor.point.y - toward.y) ** 2;
|
|
8457
|
+
if (dist < minDist) {
|
|
8458
|
+
minDist = dist;
|
|
8459
|
+
best = anchor;
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
return best;
|
|
8463
|
+
}
|
|
8464
|
+
setEndpointSidePort(edge, end, node, side, toward) {
|
|
8465
|
+
const anchor = this.nearestAnchorOnSide(node, side, toward);
|
|
8466
|
+
if (!anchor) return;
|
|
8467
|
+
const next = {
|
|
8468
|
+
...end === "from" ? edge.from : edge.to,
|
|
8469
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`,
|
|
8470
|
+
outlineParam: void 0
|
|
8471
|
+
};
|
|
8472
|
+
if (end === "from") edge.from = next;
|
|
8473
|
+
else edge.to = next;
|
|
8474
|
+
}
|
|
8475
|
+
/**
|
|
7709
8476
|
* Resolve a node endpoint:
|
|
7710
8477
|
* - lockAnchors + portId → fixed side port (wins over leftover outlineParam)
|
|
7711
8478
|
* - outlineParam → fixed point on outline (attach-to-outline)
|
|
@@ -7720,10 +8487,13 @@ var EdgeEndpointUpdater = class {
|
|
|
7720
8487
|
direction: anchorId.split(":")[0]
|
|
7721
8488
|
};
|
|
7722
8489
|
}
|
|
7723
|
-
if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0)
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
8490
|
+
if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0) {
|
|
8491
|
+
const bounds = node.getBounds();
|
|
8492
|
+
return {
|
|
8493
|
+
point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
|
|
8494
|
+
direction: getDirectionFromOutlineParam(endpoint.outlineParam, bounds)
|
|
8495
|
+
};
|
|
8496
|
+
}
|
|
7727
8497
|
if (toward) return this.resolveFloatingEndpoint(node, toward);
|
|
7728
8498
|
return { point: node.getCenter() };
|
|
7729
8499
|
}
|
|
@@ -8744,12 +9514,13 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
8744
9514
|
}
|
|
8745
9515
|
getNodeObstacles() {
|
|
8746
9516
|
if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
|
|
9517
|
+
const pad = 4;
|
|
8747
9518
|
this._nodeObstaclesCache = Array.from(this._nodes.values()).map((node) => ({
|
|
8748
9519
|
id: node.id,
|
|
8749
|
-
x: node.x -
|
|
8750
|
-
y: node.y -
|
|
8751
|
-
width: node.width +
|
|
8752
|
-
height: node.height +
|
|
9520
|
+
x: node.x - pad,
|
|
9521
|
+
y: node.y - pad,
|
|
9522
|
+
width: node.width + pad * 2,
|
|
9523
|
+
height: node.height + pad * 2,
|
|
8753
9524
|
role: "other"
|
|
8754
9525
|
}));
|
|
8755
9526
|
return this._nodeObstaclesCache;
|