@ngroznykh/papirus 0.8.0 → 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.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/PolylinePathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/routeOrthogonalAround.d.ts +25 -0
- package/dist/elements/paths/routeOrthogonalAround.d.ts.map +1 -1
- package/dist/papirus.js +552 -83
- 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,6 +4008,36 @@ 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
|
}
|
|
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
|
+
}
|
|
3860
4041
|
/** Push along dir until outside expanded parent (and at least exitDistance). */
|
|
3861
4042
|
function computeStartExit(from, fromDir, parent, margin, exitDistance) {
|
|
3862
4043
|
let p = moveByDir$1(from, fromDir, exitDistance);
|
|
@@ -3895,8 +4076,38 @@ function computeStartExit(from, fromDir, parent, margin, exitDistance) {
|
|
|
3895
4076
|
}
|
|
3896
4077
|
return p;
|
|
3897
4078
|
}
|
|
3898
|
-
function computeEndEntry(to, toDir, exitDistance) {
|
|
3899
|
-
|
|
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;
|
|
3900
4111
|
}
|
|
3901
4112
|
/**
|
|
3902
4113
|
* When nodes face each other closer than 2×exitDistance, fixed stubs cross
|
|
@@ -3972,8 +4183,12 @@ function simplifyPath$1(path) {
|
|
|
3972
4183
|
const a = out[i];
|
|
3973
4184
|
const b = out[i + 1];
|
|
3974
4185
|
const c = out[i + 2];
|
|
3975
|
-
const
|
|
3976
|
-
|
|
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);
|
|
3977
4192
|
else i++;
|
|
3978
4193
|
}
|
|
3979
4194
|
return out;
|
|
@@ -4009,13 +4224,11 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
|
|
|
4009
4224
|
ySet.add(o.y - 1);
|
|
4010
4225
|
ySet.add(o.y + o.height + 1);
|
|
4011
4226
|
}
|
|
4012
|
-
if (parent && target
|
|
4013
|
-
const ep = expandRect(parent, margin);
|
|
4014
|
-
const et = expandRect(target, margin);
|
|
4227
|
+
if (parent && target) {
|
|
4015
4228
|
const addGapY = (gapY) => {
|
|
4016
4229
|
ySet.add(gapY);
|
|
4017
|
-
const left = Math.min(
|
|
4018
|
-
const right = Math.max(
|
|
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);
|
|
4019
4232
|
xSet.add(left);
|
|
4020
4233
|
xSet.add(right);
|
|
4021
4234
|
xSet.add(startExit.x);
|
|
@@ -4023,17 +4236,24 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
|
|
|
4023
4236
|
};
|
|
4024
4237
|
const addGapX = (gapX) => {
|
|
4025
4238
|
xSet.add(gapX);
|
|
4026
|
-
const top = Math.min(
|
|
4027
|
-
const bottom = Math.max(
|
|
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);
|
|
4028
4241
|
ySet.add(top);
|
|
4029
4242
|
ySet.add(bottom);
|
|
4030
4243
|
ySet.add(startExit.y);
|
|
4031
4244
|
ySet.add(endEntry.y);
|
|
4032
4245
|
};
|
|
4033
|
-
|
|
4034
|
-
if (
|
|
4035
|
-
|
|
4036
|
-
if (
|
|
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
|
+
}
|
|
4037
4257
|
}
|
|
4038
4258
|
return {
|
|
4039
4259
|
xs: [...xSet].sort((a, b) => a - b),
|
|
@@ -4089,7 +4309,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
|
|
|
4089
4309
|
const a = nodes[list[i - 1]];
|
|
4090
4310
|
const b = nodes[list[i]];
|
|
4091
4311
|
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4092
|
-
const length = manhattan$1(a, b);
|
|
4312
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4093
4313
|
pushEdge(list[i - 1], list[i], "v", length);
|
|
4094
4314
|
pushEdge(list[i], list[i - 1], "v", length);
|
|
4095
4315
|
}
|
|
@@ -4101,7 +4321,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
|
|
|
4101
4321
|
const a = nodes[list[i - 1]];
|
|
4102
4322
|
const b = nodes[list[i]];
|
|
4103
4323
|
if (!segmentHitsObstacles(a, b, obstacles)) {
|
|
4104
|
-
const length = manhattan$1(a, b);
|
|
4324
|
+
const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
|
|
4105
4325
|
pushEdge(list[i - 1], list[i], "h", length);
|
|
4106
4326
|
pushEdge(list[i], list[i - 1], "h", length);
|
|
4107
4327
|
}
|
|
@@ -4169,18 +4389,20 @@ function pathIsValid(path, obstacles) {
|
|
|
4169
4389
|
for (let i = 1; i < path.length; i++) if (segmentHitsObstacles(path[i - 1], path[i], obstacles)) return false;
|
|
4170
4390
|
return true;
|
|
4171
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
|
+
}
|
|
4172
4404
|
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
|
-
}
|
|
4405
|
+
if (obstacles.length === 0) return orthogonalElbow(startExit, endEntry);
|
|
4184
4406
|
const u = unionBBox(obstacles);
|
|
4185
4407
|
const pad = 1;
|
|
4186
4408
|
const top = u.y - pad;
|
|
@@ -4235,6 +4457,22 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
|
4235
4457
|
y: endEntry.y
|
|
4236
4458
|
},
|
|
4237
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
|
|
4238
4476
|
]
|
|
4239
4477
|
];
|
|
4240
4478
|
let best = null;
|
|
@@ -4251,14 +4489,7 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
|
|
|
4251
4489
|
best = simplified;
|
|
4252
4490
|
}
|
|
4253
4491
|
}
|
|
4254
|
-
return best ??
|
|
4255
|
-
startExit,
|
|
4256
|
-
{
|
|
4257
|
-
x: startExit.x,
|
|
4258
|
-
y: endEntry.y
|
|
4259
|
-
},
|
|
4260
|
-
endEntry
|
|
4261
|
-
];
|
|
4492
|
+
return best ?? orthogonalElbow(startExit, endEntry);
|
|
4262
4493
|
}
|
|
4263
4494
|
function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
|
|
4264
4495
|
if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) {
|
|
@@ -4277,20 +4508,74 @@ function routeOrthogonalAround(input) {
|
|
|
4277
4508
|
const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
|
|
4278
4509
|
const exitBlocker = input.parent ?? input.source;
|
|
4279
4510
|
const rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
|
|
4280
|
-
const
|
|
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);
|
|
4281
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);
|
|
4282
4526
|
const obstacles = [];
|
|
4283
|
-
if (
|
|
4284
|
-
if (
|
|
4285
|
-
if (
|
|
4286
|
-
const
|
|
4287
|
-
|
|
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([
|
|
4288
4535
|
input.from,
|
|
4289
4536
|
startExit,
|
|
4290
4537
|
...mid,
|
|
4291
|
-
endEntry,
|
|
4538
|
+
...alreadyApproaching ? [] : [endEntry],
|
|
4292
4539
|
input.to
|
|
4293
|
-
]);
|
|
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);
|
|
4294
4579
|
}
|
|
4295
4580
|
//#endregion
|
|
4296
4581
|
//#region src/elements/paths/PolylinePathStrategy.ts
|
|
@@ -4591,12 +4876,11 @@ var PolylinePathStrategy = class {
|
|
|
4591
4876
|
calculatePath(from, to, fromDir, toDir, _options) {
|
|
4592
4877
|
const controlPoints = _options?.controlPoints;
|
|
4593
4878
|
if (_options?.editablePolyline) {
|
|
4594
|
-
|
|
4879
|
+
const path = controlPoints && controlPoints.length > 0 ? [
|
|
4595
4880
|
from,
|
|
4596
4881
|
...controlPoints,
|
|
4597
4882
|
to
|
|
4598
|
-
]
|
|
4599
|
-
return [
|
|
4883
|
+
] : [
|
|
4600
4884
|
from,
|
|
4601
4885
|
{
|
|
4602
4886
|
x: (from.x + to.x) / 2,
|
|
@@ -4604,10 +4888,32 @@ var PolylinePathStrategy = class {
|
|
|
4604
4888
|
},
|
|
4605
4889
|
to
|
|
4606
4890
|
];
|
|
4891
|
+
routeDebug("polyline.via", {
|
|
4892
|
+
via: "editable",
|
|
4893
|
+
path
|
|
4894
|
+
});
|
|
4895
|
+
return path;
|
|
4607
4896
|
}
|
|
4608
4897
|
const dist = distance(from, to);
|
|
4609
4898
|
const selfLoop = _options?.selfLoop ?? false;
|
|
4610
4899
|
const obstacles = _options?.obstacles ?? [];
|
|
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
|
+
};
|
|
4611
4917
|
if (selfLoop && dist < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) switch (fromDir ?? toDir) {
|
|
4612
4918
|
case "bottom": return [
|
|
4613
4919
|
from,
|
|
@@ -4738,7 +5044,7 @@ var PolylinePathStrategy = class {
|
|
|
4738
5044
|
if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
|
|
4739
5045
|
const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
|
|
4740
5046
|
const approachDir = resolveApproachDir(from, to, toDir, target);
|
|
4741
|
-
if ((parent || source || target) && isSide(approachDir)) return routeOrthogonalAround({
|
|
5047
|
+
if ((parent || source || target) && isSide(approachDir)) return finish("routeOrthogonalAround", routeOrthogonalAround({
|
|
4742
5048
|
from,
|
|
4743
5049
|
to,
|
|
4744
5050
|
fromDir,
|
|
@@ -4748,15 +5054,27 @@ var PolylinePathStrategy = class {
|
|
|
4748
5054
|
target,
|
|
4749
5055
|
margin: 4,
|
|
4750
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 ?? "?"}`)
|
|
4751
5069
|
});
|
|
4752
5070
|
}
|
|
4753
5071
|
if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
|
|
4754
5072
|
const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
|
|
4755
|
-
if (routed) return routed;
|
|
5073
|
+
if (routed) return finish("buildRoutedPolyline", routed);
|
|
4756
5074
|
}
|
|
4757
|
-
if (fromDir || toDir) return this.calculateDirectedPath(from, to, fromDir, toDir);
|
|
5075
|
+
if (fromDir || toDir) return finish("calculateDirectedPath", this.calculateDirectedPath(from, to, fromDir, toDir));
|
|
4758
5076
|
const midX = (from.x + to.x) / 2;
|
|
4759
|
-
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", [
|
|
4760
5078
|
from,
|
|
4761
5079
|
{
|
|
4762
5080
|
x: midX,
|
|
@@ -4767,9 +5085,9 @@ var PolylinePathStrategy = class {
|
|
|
4767
5085
|
y: to.y
|
|
4768
5086
|
},
|
|
4769
5087
|
to
|
|
4770
|
-
];
|
|
5088
|
+
]);
|
|
4771
5089
|
const midY = (from.y + to.y) / 2;
|
|
4772
|
-
return [
|
|
5090
|
+
return finish("manhattan-v", [
|
|
4773
5091
|
from,
|
|
4774
5092
|
{
|
|
4775
5093
|
x: from.x,
|
|
@@ -4780,7 +5098,7 @@ var PolylinePathStrategy = class {
|
|
|
4780
5098
|
y: midY
|
|
4781
5099
|
},
|
|
4782
5100
|
to
|
|
4783
|
-
];
|
|
5101
|
+
]);
|
|
4784
5102
|
}
|
|
4785
5103
|
calculateDirectedPath(from, to, fromDir, toDir) {
|
|
4786
5104
|
const points = [from];
|
|
@@ -5482,6 +5800,14 @@ function getEdgeLabelRotation(options) {
|
|
|
5482
5800
|
return angle;
|
|
5483
5801
|
}
|
|
5484
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
|
|
5485
5811
|
//#region src/elements/Edge.ts
|
|
5486
5812
|
var DEFAULT_EDGE_STYLE = {
|
|
5487
5813
|
strokeColor: "#666666",
|
|
@@ -5851,6 +6177,11 @@ var Edge = class extends Element {
|
|
|
5851
6177
|
const routingOptionsChanged = !routingOptionsEqual(this._pathOptions, options);
|
|
5852
6178
|
if (geometryUnchanged && !routingOptionsChanged) {
|
|
5853
6179
|
this._pathOptions = options;
|
|
6180
|
+
routeDebug("edge.updateEndpoints:skip", {
|
|
6181
|
+
id: this.id,
|
|
6182
|
+
type: this._type,
|
|
6183
|
+
reason: "geometry+routing unchanged"
|
|
6184
|
+
});
|
|
5854
6185
|
return;
|
|
5855
6186
|
}
|
|
5856
6187
|
this._fromPoint = fromPoint;
|
|
@@ -5870,6 +6201,51 @@ var Edge = class extends Element {
|
|
|
5870
6201
|
editablePolyline: this.isEditablePolyline(),
|
|
5871
6202
|
selfLoop: this._from.nodeId === this._to.nodeId
|
|
5872
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
|
+
}
|
|
5873
6249
|
this.updateBounds();
|
|
5874
6250
|
this.markDirty();
|
|
5875
6251
|
}
|
|
@@ -7597,14 +7973,6 @@ var ContextMenuManager = class extends EventEmitter {
|
|
|
7597
7973
|
}
|
|
7598
7974
|
};
|
|
7599
7975
|
//#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
7976
|
//#region src/core/EdgeEndpointUpdater.ts
|
|
7609
7977
|
var EDGE_ATTACH_MAX_PASSES = 3;
|
|
7610
7978
|
var EdgeEndpointUpdater = class {
|
|
@@ -7612,7 +7980,7 @@ var EdgeEndpointUpdater = class {
|
|
|
7612
7980
|
this.host = host;
|
|
7613
7981
|
}
|
|
7614
7982
|
updateForDrag() {
|
|
7615
|
-
this.update(
|
|
7983
|
+
this.update(true);
|
|
7616
7984
|
}
|
|
7617
7985
|
updateAll() {
|
|
7618
7986
|
this.update(true);
|
|
@@ -7640,7 +8008,10 @@ var EdgeEndpointUpdater = class {
|
|
|
7640
8008
|
const fromNode = nodes.get(edge.from.nodeId);
|
|
7641
8009
|
const toNode = nodes.get(edge.to.nodeId);
|
|
7642
8010
|
if (!fromNode || !toNode) return;
|
|
7643
|
-
if (includeRoutingObstacles)
|
|
8011
|
+
if (includeRoutingObstacles) {
|
|
8012
|
+
this.preferFacingSidePorts(edge, fromNode, toNode);
|
|
8013
|
+
this.lockAnchors(edge, fromNode, toNode);
|
|
8014
|
+
}
|
|
7644
8015
|
const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
|
|
7645
8016
|
const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
|
|
7646
8017
|
const fromId = edge.from.nodeId;
|
|
@@ -7656,6 +8027,27 @@ var EdgeEndpointUpdater = class {
|
|
|
7656
8027
|
};
|
|
7657
8028
|
return obstacle;
|
|
7658
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
|
+
});
|
|
7659
8051
|
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, routingObstacles ? { obstacles: routingObstacles } : void 0);
|
|
7660
8052
|
}
|
|
7661
8053
|
/**
|
|
@@ -7706,6 +8098,79 @@ var EdgeEndpointUpdater = class {
|
|
|
7706
8098
|
}
|
|
7707
8099
|
}
|
|
7708
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
|
+
/**
|
|
7709
8174
|
* Resolve a node endpoint:
|
|
7710
8175
|
* - lockAnchors + portId → fixed side port (wins over leftover outlineParam)
|
|
7711
8176
|
* - outlineParam → fixed point on outline (attach-to-outline)
|
|
@@ -7720,10 +8185,13 @@ var EdgeEndpointUpdater = class {
|
|
|
7720
8185
|
direction: anchorId.split(":")[0]
|
|
7721
8186
|
};
|
|
7722
8187
|
}
|
|
7723
|
-
if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0)
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
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
|
+
}
|
|
7727
8195
|
if (toward) return this.resolveFloatingEndpoint(node, toward);
|
|
7728
8196
|
return { point: node.getCenter() };
|
|
7729
8197
|
}
|
|
@@ -8744,12 +9212,13 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
8744
9212
|
}
|
|
8745
9213
|
getNodeObstacles() {
|
|
8746
9214
|
if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
|
|
9215
|
+
const pad = 4;
|
|
8747
9216
|
this._nodeObstaclesCache = Array.from(this._nodes.values()).map((node) => ({
|
|
8748
9217
|
id: node.id,
|
|
8749
|
-
x: node.x -
|
|
8750
|
-
y: node.y -
|
|
8751
|
-
width: node.width +
|
|
8752
|
-
height: node.height +
|
|
9218
|
+
x: node.x - pad,
|
|
9219
|
+
y: node.y - pad,
|
|
9220
|
+
width: node.width + pad * 2,
|
|
9221
|
+
height: node.height + pad * 2,
|
|
8753
9222
|
role: "other"
|
|
8754
9223
|
}));
|
|
8755
9224
|
return this._nodeObstaclesCache;
|