@statelyai/layout 0.1.1 → 0.1.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/README.md +1 -1
- package/dist/elkjs/index.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-CZs9YWVx.mjs → layered-COIyXvwb.mjs} +67 -10
- package/dist/{spore-9N0W9dZn.mjs → spore-BeKUA1Mb.mjs} +1 -1
- package/package.json +1 -1
- package/src/layered/separate-exterior-labels.ts +122 -21
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Native algorithms never consume ELK JSON directly.
|
|
|
73
73
|
|
|
74
74
|
<!-- fixed-port placement and inline center-label guarantees from src/layered/index.ts and src/layered/strategies.ts -->
|
|
75
75
|
|
|
76
|
-
Fixed port sides constrain routes without collapsing fan-out targets onto each other. Port preferences cannot reintroduce cycles before layering. Inline center labels use reserved inter-rank space;
|
|
76
|
+
Fixed port sides constrain routes without collapsing fan-out targets onto each other. Port preferences cannot reintroduce cycles before layering. Inline center labels use reserved inter-rank space; orthogonally routed labels use distinct cross-axis lanes that avoid other labels and states. Dedicated label layers and route-track compaction retain their placement strategies. Inline self-loop labels reserve clearance on their assigned sides from both their owner and neighboring nodes. Hierarchy decomposition preserves native self-loop routes for ancestor-to-descendant edges. FIRST/FIRST_SEPARATE nodes may have self-loops.
|
|
77
77
|
|
|
78
78
|
Advanced layered settings use shorter names such as
|
|
79
79
|
`layering.strategy`, `spacing.edgeNode`, and `nodePlacement.strategy`.
|
package/dist/elkjs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-
|
|
2
|
-
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-
|
|
1
|
+
import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-COIyXvwb.mjs";
|
|
2
|
+
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-BeKUA1Mb.mjs";
|
|
3
3
|
import { createGraph } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/elkjs/statechart.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-
|
|
2
|
-
import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-
|
|
1
|
+
import { A as minimizeCrossingsWithBarycenter, B as LayoutError, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, V as UnsupportedLayoutError, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "./layered-COIyXvwb.mjs";
|
|
2
|
+
import { a as getRandomLayout, c as rectanglePackingAlgorithm, d as fixedAlgorithm, f as getFixedLayout, i as sporeOverlapRemovalAlgorithm, l as boxAlgorithm, n as getSporeOverlapRemovalLayout, o as randomAlgorithm, r as sporeCompactionAlgorithm, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "./spore-BeKUA1Mb.mjs";
|
|
3
3
|
import { getGraphIssues } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/layout.ts
|
package/dist/layered/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-
|
|
1
|
+
import { A as minimizeCrossingsWithBarycenter, C as breakCyclesGreedilyByModelOrder, D as breakCyclesWithModelOrderDepthFirstSearch, E as breakCyclesWithModelOrderBreadthFirstSearch, I as routeEdgesOrthogonally, L as routeEdgesWithPolylines, M as minimizeCrossingsWithModelOrder, N as placeNodesInLayers, P as placeNodesInteractively, R as routeEdgesWithSplines, S as breakCyclesGreedily, T as breakCyclesWithDepthFirstSearch, _ as assignLayersInteractively, a as elkLayeredEnumValues, b as breakCyclesByStronglyConnectedConnectivity, c as placeNodesWithLinearSegments, d as assignLayersWithMinWidth, f as assignLayersWithNetworkSimplex, g as assignLayersByLongestPathToSink, h as assignLayersByLongestPath, i as toElkLayeredOptions, j as minimizeCrossingsWithMedian, k as minimizeCrossingsInteractively, l as placeNodesWithBrandesKoepf, m as assignLayersByDepthFirstModelOrder, n as layeredAlgorithm, o as elkLayeredOptionDefinitions, p as assignLayersByBreadthFirstModelOrder, r as fromElkLayeredOptionId, s as placeNodesWithNetworkSimplex, t as getLayeredLayout, u as assignLayersWithStretchWidth, v as assignLayersWithCoffmanGraham, w as breakCyclesInteractively, x as breakCyclesByStronglyConnectedNodeType, y as breakCyclesByModelOrder } from "../layered-COIyXvwb.mjs";
|
|
2
2
|
|
|
3
3
|
export { assignLayersByBreadthFirstModelOrder, assignLayersByDepthFirstModelOrder, assignLayersByLongestPath, assignLayersByLongestPathToSink, assignLayersInteractively, assignLayersWithCoffmanGraham, assignLayersWithMinWidth, assignLayersWithNetworkSimplex, assignLayersWithStretchWidth, breakCyclesByModelOrder, breakCyclesByStronglyConnectedConnectivity, breakCyclesByStronglyConnectedNodeType, breakCyclesGreedily, breakCyclesGreedilyByModelOrder, breakCyclesInteractively, breakCyclesWithDepthFirstSearch, breakCyclesWithModelOrderBreadthFirstSearch, breakCyclesWithModelOrderDepthFirstSearch, elkLayeredEnumValues, elkLayeredOptionDefinitions, fromElkLayeredOptionId, getLayeredLayout, layeredAlgorithm, minimizeCrossingsInteractively, minimizeCrossingsWithBarycenter, minimizeCrossingsWithMedian, minimizeCrossingsWithModelOrder, placeNodesInLayers, placeNodesInteractively, placeNodesWithBrandesKoepf, placeNodesWithLinearSegments, placeNodesWithNetworkSimplex, routeEdgesOrthogonally, routeEdgesWithPolylines, routeEdgesWithSplines, toElkLayeredOptions };
|
|
@@ -3781,7 +3781,7 @@ function getPolylineMidpoint(points) {
|
|
|
3781
3781
|
|
|
3782
3782
|
//#endregion
|
|
3783
3783
|
//#region src/layered/separate-exterior-labels.ts
|
|
3784
|
-
/**
|
|
3784
|
+
/** Assigns collision-free cross-axis lanes to inline labels and their route tracks. */
|
|
3785
3785
|
function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings }) {
|
|
3786
3786
|
const horizontalFlow = direction === "left" || direction === "right";
|
|
3787
3787
|
const nodeCrossStart = Math.min(...nodeRects.map((rect) => horizontalFlow ? rect.y : rect.x));
|
|
@@ -3808,25 +3808,80 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3808
3808
|
const trackCross = horizontalFlow ? point.y : point.x;
|
|
3809
3809
|
return isFlowSegment && (trackCross < nodeCrossStart || trackCross > nodeCrossEnd) ? [{
|
|
3810
3810
|
cross: trackCross,
|
|
3811
|
-
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y)
|
|
3811
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
3812
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
3813
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
3814
|
+
startIndex: index
|
|
3812
3815
|
}] : [];
|
|
3813
3816
|
}).sort((left, right) => right.length - left.length)[0];
|
|
3814
|
-
|
|
3817
|
+
const routeTrack = exteriorTrack ?? edge.points.flatMap((point, index) => {
|
|
3818
|
+
const next = edge.points[index + 1];
|
|
3819
|
+
if (!next) return [];
|
|
3820
|
+
if (!(horizontalFlow ? point.y === next.y && point.x !== next.x : point.x === next.x && point.y !== next.y)) return [];
|
|
3821
|
+
return [{
|
|
3822
|
+
cross: horizontalFlow ? point.y : point.x,
|
|
3823
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
3824
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
3825
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
3826
|
+
startIndex: index
|
|
3827
|
+
}];
|
|
3828
|
+
}).sort((left, right) => Math.abs(left.cross - (crossStart(labelRect) + crossEnd(labelRect)) / 2) - Math.abs(right.cross - (crossStart(labelRect) + crossEnd(labelRect)) / 2) || right.length - left.length)[0];
|
|
3829
|
+
if (!routeTrack) return [];
|
|
3815
3830
|
return [{
|
|
3816
3831
|
edge,
|
|
3817
|
-
exteriorTrack,
|
|
3832
|
+
exteriorTrack: routeTrack,
|
|
3833
|
+
preserveAnchors: exteriorTrack === void 0,
|
|
3818
3834
|
labelRect,
|
|
3819
|
-
lowSide:
|
|
3835
|
+
lowSide: (crossStart(labelRect) + crossEnd(labelRect)) / 2 < (nodeCrossStart + nodeCrossEnd) / 2
|
|
3820
3836
|
}];
|
|
3821
3837
|
});
|
|
3822
3838
|
movableLabels.sort((left, right) => {
|
|
3823
3839
|
if (left.lowSide !== right.lowSide) return left.lowSide ? -1 : 1;
|
|
3824
3840
|
return (left.lowSide ? crossStart(left.labelRect) - crossStart(right.labelRect) : crossStart(right.labelRect) - crossStart(left.labelRect)) || flowStart(left.labelRect) - flowStart(right.labelRect) || (String(left.edge.id) < String(right.edge.id) ? -1 : String(left.edge.id) > String(right.edge.id) ? 1 : 0);
|
|
3825
3841
|
});
|
|
3826
|
-
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
3842
|
+
for (const { edge, exteriorTrack, labelRect, lowSide, preserveAnchors } of movableLabels) {
|
|
3827
3843
|
const obstacles = [...nodeRects, ...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) => edgeId === String(edge.id) ? [] : [rect])];
|
|
3828
3844
|
let totalDelta = 0;
|
|
3829
|
-
|
|
3845
|
+
if (preserveAnchors) {
|
|
3846
|
+
const labelCrossStart = crossStart(labelRect);
|
|
3847
|
+
const labelCrossSize = crossEnd(labelRect) - labelCrossStart;
|
|
3848
|
+
const trackOffset = exteriorTrack.cross - labelCrossStart;
|
|
3849
|
+
const candidates = new Set([labelCrossStart]);
|
|
3850
|
+
for (const obstacle of obstacles) {
|
|
3851
|
+
candidates.add(crossStart(obstacle) - spacing - labelCrossSize);
|
|
3852
|
+
candidates.add(crossEnd(obstacle) + spacing);
|
|
3853
|
+
candidates.add(crossStart(obstacle) - spacing - trackOffset);
|
|
3854
|
+
candidates.add(crossEnd(obstacle) + spacing - trackOffset);
|
|
3855
|
+
}
|
|
3856
|
+
const orderedCandidates = [...candidates].sort((left, right) => {
|
|
3857
|
+
const leftPreferred = lowSide ? left <= labelCrossStart : left >= labelCrossStart;
|
|
3858
|
+
const rightPreferred = lowSide ? right <= labelCrossStart : right >= labelCrossStart;
|
|
3859
|
+
return Number(rightPreferred) - Number(leftPreferred) || Math.abs(left - labelCrossStart) - Math.abs(right - labelCrossStart) || left - right;
|
|
3860
|
+
});
|
|
3861
|
+
const lineIntersects = (rect, candidateTrackCross, connectorFlow) => {
|
|
3862
|
+
const movedCrossStart = Math.min(exteriorTrack.cross, candidateTrackCross);
|
|
3863
|
+
const movedCrossEnd = Math.max(exteriorTrack.cross, candidateTrackCross);
|
|
3864
|
+
const trackIntersects = exteriorTrack.flowStart < flowEnd(rect) && exteriorTrack.flowEnd > flowStart(rect) && candidateTrackCross > crossStart(rect) && candidateTrackCross < crossEnd(rect);
|
|
3865
|
+
const connectorIntersects = connectorFlow > flowStart(rect) && connectorFlow < flowEnd(rect) && movedCrossStart < crossEnd(rect) && movedCrossEnd > crossStart(rect);
|
|
3866
|
+
return trackIntersects || connectorIntersects;
|
|
3867
|
+
};
|
|
3868
|
+
const candidate = orderedCandidates.find((candidateLabelCross) => {
|
|
3869
|
+
const delta = candidateLabelCross - labelCrossStart;
|
|
3870
|
+
const candidateLabel = horizontalFlow ? {
|
|
3871
|
+
...labelRect,
|
|
3872
|
+
y: labelRect.y + delta
|
|
3873
|
+
} : {
|
|
3874
|
+
...labelRect,
|
|
3875
|
+
x: labelRect.x + delta
|
|
3876
|
+
};
|
|
3877
|
+
const candidateTrackCross = exteriorTrack.cross + delta;
|
|
3878
|
+
return obstacles.every((obstacle) => !overlaps(candidateLabel, obstacle)) && nodeRects.every((nodeRect) => !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowStart) && !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowEnd));
|
|
3879
|
+
});
|
|
3880
|
+
if (candidate === void 0) continue;
|
|
3881
|
+
totalDelta = candidate - labelCrossStart;
|
|
3882
|
+
if (horizontalFlow) labelRect.y += totalDelta;
|
|
3883
|
+
else labelRect.x += totalDelta;
|
|
3884
|
+
} else while (true) {
|
|
3830
3885
|
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
3831
3886
|
if (blockers.length === 0) break;
|
|
3832
3887
|
const delta = (lowSide ? Math.min(...blockers.map(crossStart)) - spacing - (horizontalFlow ? edge.height : edge.width) : Math.max(...blockers.map(crossEnd)) + spacing) - crossStart(labelRect);
|
|
@@ -3837,15 +3892,17 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3837
3892
|
if (totalDelta === 0) continue;
|
|
3838
3893
|
if (horizontalFlow) edge.y += totalDelta;
|
|
3839
3894
|
else edge.x += totalDelta;
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3895
|
+
if (!exteriorTrack) continue;
|
|
3896
|
+
edge.points = edge.points.flatMap((point, index) => {
|
|
3897
|
+
if (index !== exteriorTrack.startIndex && index !== exteriorTrack.startIndex + 1) return [point];
|
|
3898
|
+
const shifted = horizontalFlow ? {
|
|
3843
3899
|
...point,
|
|
3844
3900
|
y: point.y + totalDelta
|
|
3845
3901
|
} : {
|
|
3846
3902
|
...point,
|
|
3847
3903
|
x: point.x + totalDelta
|
|
3848
3904
|
};
|
|
3905
|
+
return preserveAnchors && index === exteriorTrack.startIndex && index === 0 ? [point, shifted] : preserveAnchors && index === exteriorTrack.startIndex + 1 && index === edge.points.length - 1 ? [shifted, point] : [shifted];
|
|
3849
3906
|
});
|
|
3850
3907
|
}
|
|
3851
3908
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-
|
|
1
|
+
import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-COIyXvwb.mjs";
|
|
2
2
|
import { getNodeSize } from "@statelyai/graph/layout";
|
|
3
3
|
|
|
4
4
|
//#region src/fixed.ts
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ interface SeparateExteriorLabelsInput<E extends ExteriorLabelEdge> {
|
|
|
22
22
|
settings: (edge: E) => ExteriorLabelSettings;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/** Assigns collision-free cross-axis lanes to inline labels and their route tracks. */
|
|
26
26
|
export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
27
27
|
edges,
|
|
28
28
|
nodeRects,
|
|
@@ -72,18 +72,50 @@ export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
|
72
72
|
{
|
|
73
73
|
cross: trackCross,
|
|
74
74
|
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
75
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
76
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
77
|
+
startIndex: index,
|
|
75
78
|
},
|
|
76
79
|
]
|
|
77
80
|
: [];
|
|
78
81
|
})
|
|
79
82
|
.sort((left, right) => right.length - left.length)[0];
|
|
80
|
-
|
|
83
|
+
const routeTrack =
|
|
84
|
+
exteriorTrack ??
|
|
85
|
+
edge.points
|
|
86
|
+
.flatMap((point, index) => {
|
|
87
|
+
const next = edge.points[index + 1];
|
|
88
|
+
if (!next) return [];
|
|
89
|
+
const isFlowSegment = horizontalFlow
|
|
90
|
+
? point.y === next.y && point.x !== next.x
|
|
91
|
+
: point.x === next.x && point.y !== next.y;
|
|
92
|
+
if (!isFlowSegment) return [];
|
|
93
|
+
const cross = horizontalFlow ? point.y : point.x;
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
cross,
|
|
97
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
98
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
99
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
100
|
+
startIndex: index,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
})
|
|
104
|
+
.sort(
|
|
105
|
+
(left, right) =>
|
|
106
|
+
Math.abs(left.cross - (crossStart(labelRect) + crossEnd(labelRect)) / 2) -
|
|
107
|
+
Math.abs(right.cross - (crossStart(labelRect) + crossEnd(labelRect)) / 2) ||
|
|
108
|
+
right.length - left.length,
|
|
109
|
+
)[0];
|
|
110
|
+
if (!routeTrack) return [];
|
|
81
111
|
return [
|
|
82
112
|
{
|
|
83
113
|
edge,
|
|
84
|
-
exteriorTrack,
|
|
114
|
+
exteriorTrack: routeTrack,
|
|
115
|
+
preserveAnchors: exteriorTrack === undefined,
|
|
85
116
|
labelRect,
|
|
86
|
-
lowSide:
|
|
117
|
+
lowSide:
|
|
118
|
+
(crossStart(labelRect) + crossEnd(labelRect)) / 2 < (nodeCrossStart + nodeCrossEnd) / 2,
|
|
87
119
|
},
|
|
88
120
|
];
|
|
89
121
|
});
|
|
@@ -104,7 +136,7 @@ export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
|
104
136
|
);
|
|
105
137
|
});
|
|
106
138
|
|
|
107
|
-
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
139
|
+
for (const { edge, exteriorTrack, labelRect, lowSide, preserveAnchors } of movableLabels) {
|
|
108
140
|
const obstacles = [
|
|
109
141
|
...nodeRects,
|
|
110
142
|
...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) =>
|
|
@@ -112,28 +144,97 @@ export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
|
112
144
|
),
|
|
113
145
|
];
|
|
114
146
|
let totalDelta = 0;
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
if (preserveAnchors) {
|
|
148
|
+
const labelCrossStart = crossStart(labelRect);
|
|
149
|
+
const labelCrossSize = crossEnd(labelRect) - labelCrossStart;
|
|
150
|
+
const trackOffset = exteriorTrack.cross - labelCrossStart;
|
|
151
|
+
const candidates = new Set<number>([labelCrossStart]);
|
|
152
|
+
for (const obstacle of obstacles) {
|
|
153
|
+
candidates.add(crossStart(obstacle) - spacing - labelCrossSize);
|
|
154
|
+
candidates.add(crossEnd(obstacle) + spacing);
|
|
155
|
+
candidates.add(crossStart(obstacle) - spacing - trackOffset);
|
|
156
|
+
candidates.add(crossEnd(obstacle) + spacing - trackOffset);
|
|
157
|
+
}
|
|
158
|
+
const orderedCandidates = [...candidates].sort((left, right) => {
|
|
159
|
+
const leftPreferred = lowSide ? left <= labelCrossStart : left >= labelCrossStart;
|
|
160
|
+
const rightPreferred = lowSide ? right <= labelCrossStart : right >= labelCrossStart;
|
|
161
|
+
return (
|
|
162
|
+
Number(rightPreferred) - Number(leftPreferred) ||
|
|
163
|
+
Math.abs(left - labelCrossStart) - Math.abs(right - labelCrossStart) ||
|
|
164
|
+
left - right
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
const lineIntersects = (
|
|
168
|
+
rect: EntityRect,
|
|
169
|
+
candidateTrackCross: number,
|
|
170
|
+
connectorFlow: number,
|
|
171
|
+
): boolean => {
|
|
172
|
+
const movedCrossStart = Math.min(exteriorTrack.cross, candidateTrackCross);
|
|
173
|
+
const movedCrossEnd = Math.max(exteriorTrack.cross, candidateTrackCross);
|
|
174
|
+
const trackIntersects =
|
|
175
|
+
exteriorTrack.flowStart < flowEnd(rect) &&
|
|
176
|
+
exteriorTrack.flowEnd > flowStart(rect) &&
|
|
177
|
+
candidateTrackCross > crossStart(rect) &&
|
|
178
|
+
candidateTrackCross < crossEnd(rect);
|
|
179
|
+
const connectorIntersects =
|
|
180
|
+
connectorFlow > flowStart(rect) &&
|
|
181
|
+
connectorFlow < flowEnd(rect) &&
|
|
182
|
+
movedCrossStart < crossEnd(rect) &&
|
|
183
|
+
movedCrossEnd > crossStart(rect);
|
|
184
|
+
return trackIntersects || connectorIntersects;
|
|
185
|
+
};
|
|
186
|
+
const candidate = orderedCandidates.find((candidateLabelCross) => {
|
|
187
|
+
const delta = candidateLabelCross - labelCrossStart;
|
|
188
|
+
const candidateLabel = horizontalFlow
|
|
189
|
+
? { ...labelRect, y: labelRect.y + delta }
|
|
190
|
+
: { ...labelRect, x: labelRect.x + delta };
|
|
191
|
+
const candidateTrackCross = exteriorTrack.cross + delta;
|
|
192
|
+
return (
|
|
193
|
+
obstacles.every((obstacle) => !overlaps(candidateLabel, obstacle)) &&
|
|
194
|
+
nodeRects.every(
|
|
195
|
+
(nodeRect) =>
|
|
196
|
+
!lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowStart) &&
|
|
197
|
+
!lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowEnd),
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
if (candidate === undefined) continue;
|
|
202
|
+
totalDelta = candidate - labelCrossStart;
|
|
203
|
+
if (horizontalFlow) labelRect.y += totalDelta;
|
|
204
|
+
else labelRect.x += totalDelta;
|
|
205
|
+
} else {
|
|
206
|
+
while (true) {
|
|
207
|
+
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
208
|
+
if (blockers.length === 0) break;
|
|
209
|
+
const desiredCross = lowSide
|
|
210
|
+
? Math.min(...blockers.map(crossStart)) -
|
|
211
|
+
spacing -
|
|
212
|
+
(horizontalFlow ? edge.height : edge.width)
|
|
213
|
+
: Math.max(...blockers.map(crossEnd)) + spacing;
|
|
214
|
+
const delta = desiredCross - crossStart(labelRect);
|
|
215
|
+
totalDelta += delta;
|
|
216
|
+
if (horizontalFlow) labelRect.y += delta;
|
|
217
|
+
else labelRect.x += delta;
|
|
218
|
+
}
|
|
127
219
|
}
|
|
128
220
|
if (totalDelta === 0) continue;
|
|
129
221
|
if (horizontalFlow) edge.y += totalDelta;
|
|
130
222
|
else edge.x += totalDelta;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
223
|
+
if (!exteriorTrack) continue;
|
|
224
|
+
edge.points = edge.points.flatMap((point, index) => {
|
|
225
|
+
if (index !== exteriorTrack.startIndex && index !== exteriorTrack.startIndex + 1)
|
|
226
|
+
return [point];
|
|
227
|
+
const shifted = horizontalFlow
|
|
135
228
|
? { ...point, y: point.y + totalDelta }
|
|
136
229
|
: { ...point, x: point.x + totalDelta };
|
|
230
|
+
// Keep authored endpoint anchors attached when the chosen track ends there.
|
|
231
|
+
return preserveAnchors && index === exteriorTrack.startIndex && index === 0
|
|
232
|
+
? [point, shifted]
|
|
233
|
+
: preserveAnchors &&
|
|
234
|
+
index === exteriorTrack.startIndex + 1 &&
|
|
235
|
+
index === edge.points.length - 1
|
|
236
|
+
? [shifted, point]
|
|
237
|
+
: [shifted];
|
|
137
238
|
});
|
|
138
239
|
}
|
|
139
240
|
}
|