@statelyai/layout 0.1.0 → 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 +6 -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/elkjs/statechart.ts +10 -10
- 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`.
|
|
@@ -161,6 +161,11 @@ pnpm release
|
|
|
161
161
|
generated layered-option and demo-corpus freshness, tests,
|
|
162
162
|
declarations/runtime builds, the demo bundle, and the packed package surface.
|
|
163
163
|
|
|
164
|
+
<!-- exported source compatibility derived from tsconfig.json -->
|
|
165
|
+
|
|
166
|
+
The source project checks indexed reads with `noUncheckedIndexedAccess` so
|
|
167
|
+
published TypeScript source supports consumers using that option.
|
|
168
|
+
|
|
164
169
|
## Releases
|
|
165
170
|
|
|
166
171
|
<!-- release process derived from package.json#scripts and .changeset/config.json -->
|
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
package/src/elkjs/statechart.ts
CHANGED
|
@@ -98,16 +98,16 @@ export function compileStatechartLayout(
|
|
|
98
98
|
}
|
|
99
99
|
if (scope.preferredPath) {
|
|
100
100
|
for (let i = 1; i < path.length; i++) {
|
|
101
|
-
if (!successors.get(path[i - 1])?.has(path[i]))
|
|
101
|
+
if (!successors.get(path[i - 1]!)?.has(path[i]!))
|
|
102
102
|
throw new Error(`Disconnected preferred path in ${id}`);
|
|
103
103
|
}
|
|
104
104
|
} else {
|
|
105
105
|
while (path.length) {
|
|
106
|
-
const next = [...successors.get(path.at(-1))!].filter(
|
|
106
|
+
const next = [...successors.get(path.at(-1)!)!].filter(
|
|
107
107
|
(nodeId) => !path.includes(nodeId) && !exits.includes(nodeId),
|
|
108
108
|
);
|
|
109
109
|
if (next.length !== 1) break;
|
|
110
|
-
path.push(next[0]);
|
|
110
|
+
path.push(next[0]!);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
paths[id] = path;
|
|
@@ -214,8 +214,8 @@ export function scoreStatechartLayout(
|
|
|
214
214
|
for (const point of points)
|
|
215
215
|
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) score.invalid++;
|
|
216
216
|
for (let i = 1; i < points.length; i++) {
|
|
217
|
-
const a = { x: x + points[i - 1]
|
|
218
|
-
const b = { x: x + points[i]
|
|
217
|
+
const a = { x: x + points[i - 1]!.x, y: y + points[i - 1]!.y };
|
|
218
|
+
const b = { x: x + points[i]!.x, y: y + points[i]!.y };
|
|
219
219
|
score.routeLength += Math.hypot(b.x - a.x, b.y - a.y);
|
|
220
220
|
segments.push({ a, b, edge });
|
|
221
221
|
}
|
|
@@ -225,16 +225,16 @@ export function scoreStatechartLayout(
|
|
|
225
225
|
visit(graph, 0, 0, [graph]);
|
|
226
226
|
for (let i = 0; i < boxes.length; i++)
|
|
227
227
|
for (let j = i + 1; j < boxes.length; j++) {
|
|
228
|
-
const a = boxes[i]
|
|
229
|
-
b = boxes[j]
|
|
228
|
+
const a = boxes[i]!,
|
|
229
|
+
b = boxes[j]!;
|
|
230
230
|
if ((a.node && b.ancestors.includes(a.node)) || (b.node && a.ancestors.includes(b.node)))
|
|
231
231
|
continue;
|
|
232
232
|
if (intersects(a.rect, b.rect)) score.overlaps++;
|
|
233
233
|
}
|
|
234
234
|
for (let i = 0; i < segments.length; i++)
|
|
235
235
|
for (let j = i + 1; j < segments.length; j++) {
|
|
236
|
-
const a = segments[i]
|
|
237
|
-
b = segments[j]
|
|
236
|
+
const a = segments[i]!,
|
|
237
|
+
b = segments[j]!;
|
|
238
238
|
if (a.edge === b.edge) continue;
|
|
239
239
|
if (
|
|
240
240
|
cross(a.a, a.b, b.a) * cross(a.a, a.b, b.b) < 0 &&
|
|
@@ -303,7 +303,7 @@ export async function layoutStatechart<T extends ElkNode>(
|
|
|
303
303
|
}
|
|
304
304
|
for (const group of groups.values())
|
|
305
305
|
for (let i = 1; i < Math.min(group.length, 3); i++) {
|
|
306
|
-
const edge = group[i]
|
|
306
|
+
const edge = group[i]!;
|
|
307
307
|
if (
|
|
308
308
|
option(edge, "edgeLabels.placement") === undefined &&
|
|
309
309
|
edge.labels!.every((label) => option(label, "edgeLabels.placement") === undefined)
|
|
@@ -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
|
}
|