@statelyai/layout 0.1.1 → 0.1.3
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-CoeH9LRO.mjs} +108 -12
- package/dist/{spore-9N0W9dZn.mjs → spore-BtTSZbb6.mjs} +1 -1
- package/package.json +1 -1
- package/src/layered/separate-exterior-labels.ts +122 -21
- package/src/layered/strategies.ts +68 -3
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-CoeH9LRO.mjs";
|
|
2
|
+
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-BtTSZbb6.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-CoeH9LRO.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-BtTSZbb6.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-CoeH9LRO.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 };
|
|
@@ -2494,7 +2494,40 @@ function routeEdges(style) {
|
|
|
2494
2494
|
}
|
|
2495
2495
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
2496
2496
|
}
|
|
2497
|
+
const increasing = input.direction === "right" || input.direction === "down";
|
|
2498
|
+
const flowPortProtrusion = (edge, endpoint) => {
|
|
2499
|
+
const node = nodeById.get(endpoint === "source" ? edge.sourceId : edge.targetId);
|
|
2500
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
2501
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
2502
|
+
if (!node || !port) return 0;
|
|
2503
|
+
return input.portSettings?.(port, node)?.["port.side"] === (horizontal ? endpoint === "source" ? input.direction === "right" ? "EAST" : "WEST" : input.direction === "right" ? "WEST" : "EAST" : endpoint === "source" ? input.direction === "down" ? "SOUTH" : "NORTH" : input.direction === "down" ? "NORTH" : "SOUTH") ? horizontal ? port.width ?? 0 : port.height ?? 0 : 0;
|
|
2504
|
+
};
|
|
2505
|
+
const adjacentNodeIds = /* @__PURE__ */ new Map();
|
|
2506
|
+
for (const edge of input.graph.edges) {
|
|
2507
|
+
const sourceNeighbors = adjacentNodeIds.get(edge.sourceId) ?? /* @__PURE__ */ new Set();
|
|
2508
|
+
sourceNeighbors.add(edge.targetId);
|
|
2509
|
+
adjacentNodeIds.set(edge.sourceId, sourceNeighbors);
|
|
2510
|
+
const targetNeighbors = adjacentNodeIds.get(edge.targetId) ?? /* @__PURE__ */ new Set();
|
|
2511
|
+
targetNeighbors.add(edge.sourceId);
|
|
2512
|
+
adjacentNodeIds.set(edge.targetId, targetNeighbors);
|
|
2513
|
+
}
|
|
2514
|
+
const feedbackComponentNodeIds = /* @__PURE__ */ new Set();
|
|
2515
|
+
for (const edge of input.graph.edges) {
|
|
2516
|
+
const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
|
|
2517
|
+
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
2518
|
+
if (sourceLayer === void 0 || targetLayer === void 0) continue;
|
|
2519
|
+
if (sourceLayer === targetLayer) continue;
|
|
2520
|
+
if (increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer) continue;
|
|
2521
|
+
const pending = [edge.sourceId, edge.targetId];
|
|
2522
|
+
while (pending.length > 0) {
|
|
2523
|
+
const nodeId = pending.pop();
|
|
2524
|
+
if (feedbackComponentNodeIds.has(nodeId)) continue;
|
|
2525
|
+
feedbackComponentNodeIds.add(nodeId);
|
|
2526
|
+
pending.push(...adjacentNodeIds.get(nodeId) ?? []);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2497
2529
|
const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
|
|
2530
|
+
const compactLabelCorridorGaps = /* @__PURE__ */ new Set();
|
|
2498
2531
|
for (const edge of input.graph.edges) {
|
|
2499
2532
|
const labelFlowSize = horizontal ? edge.width ?? 0 : edge.height ?? 0;
|
|
2500
2533
|
if (labelFlowSize <= 0) continue;
|
|
@@ -2502,8 +2535,14 @@ function routeEdges(style) {
|
|
|
2502
2535
|
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
2503
2536
|
if (sourceLayer === void 0 || targetLayer === void 0) continue;
|
|
2504
2537
|
if (Math.abs(sourceLayer - targetLayer) !== 1) continue;
|
|
2505
|
-
const
|
|
2538
|
+
const placement$1 = input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER";
|
|
2539
|
+
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
2540
|
+
const requiredGap = labelFlowSize + Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) + Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing);
|
|
2506
2541
|
const gap = Math.min(sourceLayer, targetLayer);
|
|
2542
|
+
const availableGap = flowLayers[gap + 1].start - flowLayers[gap].end;
|
|
2543
|
+
const compactCorridor = placement$1 === "CENTER" && (increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer) && !feedbackComponentNodeIds.has(edge.sourceId);
|
|
2544
|
+
if (compactCorridor) compactLabelCorridorGaps.add(gap);
|
|
2545
|
+
const extra = compactCorridor ? Math.max(0, requiredGap - availableGap) : placement$1 === "CENTER" ? labelFlowSize + input.spacing.layer : labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
2507
2546
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
2508
2547
|
}
|
|
2509
2548
|
let labelShift = 0;
|
|
@@ -2749,7 +2788,7 @@ function routeEdges(style) {
|
|
|
2749
2788
|
const slots = slotsByGap[layerNo] ?? 0;
|
|
2750
2789
|
const routesNearTarget = candidatesByGap[layerNo]?.some((candidate) => hasZeroFixedSideTarget(candidate.edge) && candidatesByGap[layerNo].filter(({ edge }) => edge.targetId === candidate.edge.targetId).length > candidatesByGap[layerNo].filter(({ edge }) => edge.sourceId === candidate.edge.sourceId).length);
|
|
2751
2790
|
const preservedGap = (existingGapByLayer[layerNo] ?? input.spacing.layer) - (routesNearTarget ? edgeEdgeSpacing : 0);
|
|
2752
|
-
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 || [...selfLoopsByNodeId].some(([id, loops]) => (flowLayerByNodeId.get(id) === layerNo || flowLayerByNodeId.get(id) === layerNo + 1) && loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2791
|
+
const gapSpacing = slots === 0 ? existingGapByLayer[layerNo] ?? input.spacing.layer : Math.max(preservesNodeFlexibilityGap || (labelExtraByGap[layerNo] ?? 0) > 0 || compactLabelCorridorGaps.has(layerNo) || [...selfLoopsByNodeId].some(([id, loops]) => (flowLayerByNodeId.get(id) === layerNo || flowLayerByNodeId.get(id) === layerNo + 1) && loops.some((edge) => input.edgeSettings?.(edge)?.["edgeLabels.inline"] === true)) ? preservedGap : input.spacing.layer, 2 * edgeNodeSpacing + Math.max(0, slots - 1) * edgeEdgeSpacing);
|
|
2753
2792
|
nextStart = bounds.end + gapSpacing;
|
|
2754
2793
|
}
|
|
2755
2794
|
implicitEndpoints = implicitEdgeEndpoints(input, placement, orientation);
|
|
@@ -3781,7 +3820,7 @@ function getPolylineMidpoint(points) {
|
|
|
3781
3820
|
|
|
3782
3821
|
//#endregion
|
|
3783
3822
|
//#region src/layered/separate-exterior-labels.ts
|
|
3784
|
-
/**
|
|
3823
|
+
/** Assigns collision-free cross-axis lanes to inline labels and their route tracks. */
|
|
3785
3824
|
function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings }) {
|
|
3786
3825
|
const horizontalFlow = direction === "left" || direction === "right";
|
|
3787
3826
|
const nodeCrossStart = Math.min(...nodeRects.map((rect) => horizontalFlow ? rect.y : rect.x));
|
|
@@ -3808,25 +3847,80 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3808
3847
|
const trackCross = horizontalFlow ? point.y : point.x;
|
|
3809
3848
|
return isFlowSegment && (trackCross < nodeCrossStart || trackCross > nodeCrossEnd) ? [{
|
|
3810
3849
|
cross: trackCross,
|
|
3811
|
-
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y)
|
|
3850
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
3851
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
3852
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
3853
|
+
startIndex: index
|
|
3812
3854
|
}] : [];
|
|
3813
3855
|
}).sort((left, right) => right.length - left.length)[0];
|
|
3814
|
-
|
|
3856
|
+
const routeTrack = exteriorTrack ?? edge.points.flatMap((point, index) => {
|
|
3857
|
+
const next = edge.points[index + 1];
|
|
3858
|
+
if (!next) return [];
|
|
3859
|
+
if (!(horizontalFlow ? point.y === next.y && point.x !== next.x : point.x === next.x && point.y !== next.y)) return [];
|
|
3860
|
+
return [{
|
|
3861
|
+
cross: horizontalFlow ? point.y : point.x,
|
|
3862
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
3863
|
+
flowStart: horizontalFlow ? Math.min(point.x, next.x) : Math.min(point.y, next.y),
|
|
3864
|
+
flowEnd: horizontalFlow ? Math.max(point.x, next.x) : Math.max(point.y, next.y),
|
|
3865
|
+
startIndex: index
|
|
3866
|
+
}];
|
|
3867
|
+
}).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];
|
|
3868
|
+
if (!routeTrack) return [];
|
|
3815
3869
|
return [{
|
|
3816
3870
|
edge,
|
|
3817
|
-
exteriorTrack,
|
|
3871
|
+
exteriorTrack: routeTrack,
|
|
3872
|
+
preserveAnchors: exteriorTrack === void 0,
|
|
3818
3873
|
labelRect,
|
|
3819
|
-
lowSide:
|
|
3874
|
+
lowSide: (crossStart(labelRect) + crossEnd(labelRect)) / 2 < (nodeCrossStart + nodeCrossEnd) / 2
|
|
3820
3875
|
}];
|
|
3821
3876
|
});
|
|
3822
3877
|
movableLabels.sort((left, right) => {
|
|
3823
3878
|
if (left.lowSide !== right.lowSide) return left.lowSide ? -1 : 1;
|
|
3824
3879
|
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
3880
|
});
|
|
3826
|
-
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
3881
|
+
for (const { edge, exteriorTrack, labelRect, lowSide, preserveAnchors } of movableLabels) {
|
|
3827
3882
|
const obstacles = [...nodeRects, ...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) => edgeId === String(edge.id) ? [] : [rect])];
|
|
3828
3883
|
let totalDelta = 0;
|
|
3829
|
-
|
|
3884
|
+
if (preserveAnchors) {
|
|
3885
|
+
const labelCrossStart = crossStart(labelRect);
|
|
3886
|
+
const labelCrossSize = crossEnd(labelRect) - labelCrossStart;
|
|
3887
|
+
const trackOffset = exteriorTrack.cross - labelCrossStart;
|
|
3888
|
+
const candidates = new Set([labelCrossStart]);
|
|
3889
|
+
for (const obstacle of obstacles) {
|
|
3890
|
+
candidates.add(crossStart(obstacle) - spacing - labelCrossSize);
|
|
3891
|
+
candidates.add(crossEnd(obstacle) + spacing);
|
|
3892
|
+
candidates.add(crossStart(obstacle) - spacing - trackOffset);
|
|
3893
|
+
candidates.add(crossEnd(obstacle) + spacing - trackOffset);
|
|
3894
|
+
}
|
|
3895
|
+
const orderedCandidates = [...candidates].sort((left, right) => {
|
|
3896
|
+
const leftPreferred = lowSide ? left <= labelCrossStart : left >= labelCrossStart;
|
|
3897
|
+
const rightPreferred = lowSide ? right <= labelCrossStart : right >= labelCrossStart;
|
|
3898
|
+
return Number(rightPreferred) - Number(leftPreferred) || Math.abs(left - labelCrossStart) - Math.abs(right - labelCrossStart) || left - right;
|
|
3899
|
+
});
|
|
3900
|
+
const lineIntersects = (rect, candidateTrackCross, connectorFlow) => {
|
|
3901
|
+
const movedCrossStart = Math.min(exteriorTrack.cross, candidateTrackCross);
|
|
3902
|
+
const movedCrossEnd = Math.max(exteriorTrack.cross, candidateTrackCross);
|
|
3903
|
+
const trackIntersects = exteriorTrack.flowStart < flowEnd(rect) && exteriorTrack.flowEnd > flowStart(rect) && candidateTrackCross > crossStart(rect) && candidateTrackCross < crossEnd(rect);
|
|
3904
|
+
const connectorIntersects = connectorFlow > flowStart(rect) && connectorFlow < flowEnd(rect) && movedCrossStart < crossEnd(rect) && movedCrossEnd > crossStart(rect);
|
|
3905
|
+
return trackIntersects || connectorIntersects;
|
|
3906
|
+
};
|
|
3907
|
+
const candidate = orderedCandidates.find((candidateLabelCross) => {
|
|
3908
|
+
const delta = candidateLabelCross - labelCrossStart;
|
|
3909
|
+
const candidateLabel = horizontalFlow ? {
|
|
3910
|
+
...labelRect,
|
|
3911
|
+
y: labelRect.y + delta
|
|
3912
|
+
} : {
|
|
3913
|
+
...labelRect,
|
|
3914
|
+
x: labelRect.x + delta
|
|
3915
|
+
};
|
|
3916
|
+
const candidateTrackCross = exteriorTrack.cross + delta;
|
|
3917
|
+
return obstacles.every((obstacle) => !overlaps(candidateLabel, obstacle)) && nodeRects.every((nodeRect) => !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowStart) && !lineIntersects(nodeRect, candidateTrackCross, exteriorTrack.flowEnd));
|
|
3918
|
+
});
|
|
3919
|
+
if (candidate === void 0) continue;
|
|
3920
|
+
totalDelta = candidate - labelCrossStart;
|
|
3921
|
+
if (horizontalFlow) labelRect.y += totalDelta;
|
|
3922
|
+
else labelRect.x += totalDelta;
|
|
3923
|
+
} else while (true) {
|
|
3830
3924
|
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
3831
3925
|
if (blockers.length === 0) break;
|
|
3832
3926
|
const delta = (lowSide ? Math.min(...blockers.map(crossStart)) - spacing - (horizontalFlow ? edge.height : edge.width) : Math.max(...blockers.map(crossEnd)) + spacing) - crossStart(labelRect);
|
|
@@ -3837,15 +3931,17 @@ function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings
|
|
|
3837
3931
|
if (totalDelta === 0) continue;
|
|
3838
3932
|
if (horizontalFlow) edge.y += totalDelta;
|
|
3839
3933
|
else edge.x += totalDelta;
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3934
|
+
if (!exteriorTrack) continue;
|
|
3935
|
+
edge.points = edge.points.flatMap((point, index) => {
|
|
3936
|
+
if (index !== exteriorTrack.startIndex && index !== exteriorTrack.startIndex + 1) return [point];
|
|
3937
|
+
const shifted = horizontalFlow ? {
|
|
3843
3938
|
...point,
|
|
3844
3939
|
y: point.y + totalDelta
|
|
3845
3940
|
} : {
|
|
3846
3941
|
...point,
|
|
3847
3942
|
x: point.x + totalDelta
|
|
3848
3943
|
};
|
|
3944
|
+
return preserveAnchors && index === exteriorTrack.startIndex && index === 0 ? [point, shifted] : preserveAnchors && index === exteriorTrack.startIndex + 1 && index === edge.points.length - 1 ? [shifted, point] : [shifted];
|
|
3849
3945
|
});
|
|
3850
3946
|
}
|
|
3851
3947
|
}
|
|
@@ -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-CoeH9LRO.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
|
}
|
|
@@ -3645,7 +3645,57 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3645
3645
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
3646
3646
|
}
|
|
3647
3647
|
|
|
3648
|
+
const increasing = input.direction === "right" || input.direction === "down";
|
|
3649
|
+
const flowPortProtrusion = (edge: GraphEdge, endpoint: "source" | "target"): number => {
|
|
3650
|
+
const node = nodeById.get(endpoint === "source" ? edge.sourceId : edge.targetId);
|
|
3651
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
3652
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3653
|
+
if (!node || !port) return 0;
|
|
3654
|
+
const side = input.portSettings?.(port, node)?.["port.side"];
|
|
3655
|
+
const flowSide = horizontal
|
|
3656
|
+
? endpoint === "source"
|
|
3657
|
+
? input.direction === "right"
|
|
3658
|
+
? "EAST"
|
|
3659
|
+
: "WEST"
|
|
3660
|
+
: input.direction === "right"
|
|
3661
|
+
? "WEST"
|
|
3662
|
+
: "EAST"
|
|
3663
|
+
: endpoint === "source"
|
|
3664
|
+
? input.direction === "down"
|
|
3665
|
+
? "SOUTH"
|
|
3666
|
+
: "NORTH"
|
|
3667
|
+
: input.direction === "down"
|
|
3668
|
+
? "NORTH"
|
|
3669
|
+
: "SOUTH";
|
|
3670
|
+
return side === flowSide ? (horizontal ? (port.width ?? 0) : (port.height ?? 0)) : 0;
|
|
3671
|
+
};
|
|
3672
|
+
const adjacentNodeIds = new Map<string, Set<string>>();
|
|
3673
|
+
for (const edge of input.graph.edges) {
|
|
3674
|
+
const sourceNeighbors = adjacentNodeIds.get(edge.sourceId) ?? new Set<string>();
|
|
3675
|
+
sourceNeighbors.add(edge.targetId);
|
|
3676
|
+
adjacentNodeIds.set(edge.sourceId, sourceNeighbors);
|
|
3677
|
+
const targetNeighbors = adjacentNodeIds.get(edge.targetId) ?? new Set<string>();
|
|
3678
|
+
targetNeighbors.add(edge.sourceId);
|
|
3679
|
+
adjacentNodeIds.set(edge.targetId, targetNeighbors);
|
|
3680
|
+
}
|
|
3681
|
+
const feedbackComponentNodeIds = new Set<string>();
|
|
3682
|
+
for (const edge of input.graph.edges) {
|
|
3683
|
+
const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
|
|
3684
|
+
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
3685
|
+
if (sourceLayer === undefined || targetLayer === undefined) continue;
|
|
3686
|
+
if (sourceLayer === targetLayer) continue;
|
|
3687
|
+
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
3688
|
+
if (forward) continue;
|
|
3689
|
+
const pending = [edge.sourceId, edge.targetId];
|
|
3690
|
+
while (pending.length > 0) {
|
|
3691
|
+
const nodeId = pending.pop()!;
|
|
3692
|
+
if (feedbackComponentNodeIds.has(nodeId)) continue;
|
|
3693
|
+
feedbackComponentNodeIds.add(nodeId);
|
|
3694
|
+
pending.push(...(adjacentNodeIds.get(nodeId) ?? []));
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3648
3697
|
const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
|
|
3698
|
+
const compactLabelCorridorGaps = new Set<number>();
|
|
3649
3699
|
for (const edge of input.graph.edges) {
|
|
3650
3700
|
const labelFlowSize = horizontal ? (edge.width ?? 0) : (edge.height ?? 0);
|
|
3651
3701
|
if (labelFlowSize <= 0) continue;
|
|
@@ -3654,11 +3704,25 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3654
3704
|
if (sourceLayer === undefined || targetLayer === undefined) continue;
|
|
3655
3705
|
if (Math.abs(sourceLayer - targetLayer) !== 1) continue;
|
|
3656
3706
|
const placement = input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER";
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3707
|
+
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
3708
|
+
// ELK models center labels as dummy layers. For an exterior port, using node-node
|
|
3709
|
+
// spacing beyond the port double-counts the corridor. Treat the label as edge
|
|
3710
|
+
// geometry while retaining normal layer spacing when it is the larger clearance.
|
|
3711
|
+
const requiredGap =
|
|
3712
|
+
labelFlowSize +
|
|
3713
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) +
|
|
3714
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing);
|
|
3715
|
+
const gap = Math.min(sourceLayer, targetLayer);
|
|
3716
|
+
const availableGap = flowLayers[gap + 1]!.start - flowLayers[gap]!.end;
|
|
3717
|
+
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
3718
|
+
const compactCorridor =
|
|
3719
|
+
placement === "CENTER" && forward && !feedbackComponentNodeIds.has(edge.sourceId);
|
|
3720
|
+
if (compactCorridor) compactLabelCorridorGaps.add(gap);
|
|
3721
|
+
const extra = compactCorridor
|
|
3722
|
+
? Math.max(0, requiredGap - availableGap)
|
|
3723
|
+
: placement === "CENTER"
|
|
3659
3724
|
? labelFlowSize + input.spacing.layer
|
|
3660
3725
|
: labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
3661
|
-
const gap = Math.min(sourceLayer, targetLayer);
|
|
3662
3726
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
3663
3727
|
}
|
|
3664
3728
|
let labelShift = 0;
|
|
@@ -4049,6 +4113,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4049
4113
|
: Math.max(
|
|
4050
4114
|
preservesNodeFlexibilityGap ||
|
|
4051
4115
|
(labelExtraByGap[layerNo] ?? 0) > 0 ||
|
|
4116
|
+
compactLabelCorridorGaps.has(layerNo) ||
|
|
4052
4117
|
[...selfLoopsByNodeId].some(
|
|
4053
4118
|
([id, loops]) =>
|
|
4054
4119
|
(flowLayerByNodeId.get(id) === layerNo ||
|