@statelyai/layout 0.0.3 → 0.0.5
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 +5 -2
- package/dist/elkjs/index.mjs +11 -8
- package/dist/index.mjs +2 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-f_3mmGHr.mjs → layered-BYhTymAk.mjs} +127 -6
- package/dist/{spore-BfTduMDc.mjs → spore-B504wOFR.mjs} +1 -1
- package/package.json +2 -1
- package/src/elkjs/index.ts +14 -20
- package/src/layered/index.ts +97 -16
- package/src/layered/separate-exterior-labels.ts +139 -0
package/README.md
CHANGED
|
@@ -9,12 +9,15 @@ return `VisualGraph`; positions remain node fields and routes remain
|
|
|
9
9
|
|
|
10
10
|
## Status
|
|
11
11
|
|
|
12
|
+
<!-- layered compatibility coverage from test/oracle*.test.ts -->
|
|
13
|
+
|
|
12
14
|
The native layered implementation covers the complete 152-option elkjs 0.11.1
|
|
13
15
|
layered inventory with one simplified typed name per ELK option. Flat and
|
|
14
16
|
compound graphs, cross-hierarchy edges, ports, labels, self-loops, wrapping,
|
|
15
17
|
four directions, constraints, and replaceable phases are differential-tested
|
|
16
|
-
against elkjs
|
|
17
|
-
|
|
18
|
+
against elkjs, including a curated complex state-machine corpus in both primary
|
|
19
|
+
layout orientations. Partial, incremental, and route-only layout remain
|
|
20
|
+
explicit unimplemented capabilities.
|
|
18
21
|
|
|
19
22
|
## Install
|
|
20
23
|
|
package/dist/elkjs/index.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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-BYhTymAk.mjs";
|
|
2
|
+
import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-B504wOFR.mjs";
|
|
3
3
|
import { createGraph } from "@statelyai/graph";
|
|
4
4
|
|
|
5
5
|
//#region src/elkjs/index.ts
|
|
6
|
+
function isLayoutEdgeLabel(label) {
|
|
7
|
+
return getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true && (Boolean(label.text) || label.width !== void 0 || label.height !== void 0);
|
|
8
|
+
}
|
|
6
9
|
var ELK = class {
|
|
7
10
|
#options;
|
|
8
11
|
#algorithmIds;
|
|
@@ -897,7 +900,7 @@ function toGraph(root, globalOptions = {}) {
|
|
|
897
900
|
if (isInsideSelfLoop(root, edge)) return [];
|
|
898
901
|
const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
|
|
899
902
|
const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
|
|
900
|
-
const labels = (edge.labels ?? []).filter(
|
|
903
|
+
const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
901
904
|
const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
902
905
|
const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
|
|
903
906
|
const labelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelLabelSpacing;
|
|
@@ -1000,7 +1003,7 @@ function applyLayout(root, graph, padding, layoutOptions) {
|
|
|
1000
1003
|
label.y ??= 0;
|
|
1001
1004
|
continue;
|
|
1002
1005
|
}
|
|
1003
|
-
if (!label
|
|
1006
|
+
if (!isLayoutEdgeLabel(label)) {
|
|
1004
1007
|
label.x ??= 0;
|
|
1005
1008
|
label.y ??= 0;
|
|
1006
1009
|
continue;
|
|
@@ -1026,7 +1029,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
|
|
|
1026
1029
|
minimumY = Math.min(minimumY, node.y ?? 0, ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0)), ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0))));
|
|
1027
1030
|
}
|
|
1028
1031
|
for (const edge of root.edges ?? []) {
|
|
1029
|
-
const laidOutLabels = (edge.labels ?? []).filter(
|
|
1032
|
+
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1030
1033
|
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1031
1034
|
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
1032
1035
|
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
|
|
@@ -1055,7 +1058,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
|
|
|
1055
1058
|
point.x += shiftX;
|
|
1056
1059
|
point.y += shiftY;
|
|
1057
1060
|
}
|
|
1058
|
-
for (const label of (edge.labels ?? []).filter(
|
|
1061
|
+
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
1059
1062
|
label.x = (label.x ?? 0) + shiftX;
|
|
1060
1063
|
label.y = (label.y ?? 0) + shiftY;
|
|
1061
1064
|
}
|
|
@@ -1091,7 +1094,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
|
|
|
1091
1094
|
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
1092
1095
|
...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
|
|
1093
1096
|
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
|
|
1094
|
-
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(
|
|
1097
|
+
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.x ?? 0) + (label.width ?? 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
1095
1098
|
section.startPoint.x,
|
|
1096
1099
|
...(section.bendPoints ?? []).map((point) => point.x),
|
|
1097
1100
|
section.endPoint.x
|
|
@@ -1101,7 +1104,7 @@ function normalizeElkGraphBounds(root, padding, layoutOptions = {}) {
|
|
|
1101
1104
|
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
1102
1105
|
...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
|
|
1103
1106
|
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
|
|
1104
|
-
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(
|
|
1107
|
+
]), ...(root.edges ?? []).flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.y ?? 0) + (label.height ?? 0) + (String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") === "CENTER" ? 1 : 0))), ...(root.edges ?? []).flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
1105
1108
|
section.startPoint.y,
|
|
1106
1109
|
...(section.bendPoints ?? []).map((point) => point.y),
|
|
1107
1110
|
section.endPoint.y
|
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-BYhTymAk.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-B504wOFR.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-BYhTymAk.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 };
|
|
@@ -3699,6 +3699,77 @@ function getPolylineMidpoint(points) {
|
|
|
3699
3699
|
};
|
|
3700
3700
|
}
|
|
3701
3701
|
|
|
3702
|
+
//#endregion
|
|
3703
|
+
//#region src/layered/separate-exterior-labels.ts
|
|
3704
|
+
/** Separates movable inline labels and their exterior orthogonal route tracks. */
|
|
3705
|
+
function separateExteriorLabels({ edges, nodeRects, direction, spacing, settings }) {
|
|
3706
|
+
const horizontalFlow = direction === "left" || direction === "right";
|
|
3707
|
+
const nodeCrossStart = Math.min(...nodeRects.map((rect) => horizontalFlow ? rect.y : rect.x));
|
|
3708
|
+
const nodeCrossEnd = Math.max(...nodeRects.map((rect) => horizontalFlow ? rect.y + rect.height : rect.x + rect.width));
|
|
3709
|
+
const flowStart = (rect) => horizontalFlow ? rect.x : rect.y;
|
|
3710
|
+
const flowEnd = (rect) => horizontalFlow ? rect.x + rect.width : rect.y + rect.height;
|
|
3711
|
+
const crossStart = (rect) => horizontalFlow ? rect.y : rect.x;
|
|
3712
|
+
const crossEnd = (rect) => horizontalFlow ? rect.y + rect.height : rect.x + rect.width;
|
|
3713
|
+
const overlaps = (left, right) => flowStart(left) < flowEnd(right) && flowEnd(left) > flowStart(right) && crossStart(left) < crossEnd(right) && crossEnd(left) > crossStart(right);
|
|
3714
|
+
const labelRectByEdgeId = new Map(edges.filter((edge) => edge.width > 0 && edge.height > 0).map((edge) => [String(edge.id), {
|
|
3715
|
+
x: edge.x,
|
|
3716
|
+
y: edge.y,
|
|
3717
|
+
width: edge.width,
|
|
3718
|
+
height: edge.height
|
|
3719
|
+
}]));
|
|
3720
|
+
const movableLabels = edges.flatMap((edge) => {
|
|
3721
|
+
const labelRect = labelRectByEdgeId.get(String(edge.id));
|
|
3722
|
+
const labelSettings = settings(edge);
|
|
3723
|
+
if (!labelRect || !labelSettings.inline || labelSettings.placement !== "CENTER") return [];
|
|
3724
|
+
const exteriorTrack = edge.points.flatMap((point, index) => {
|
|
3725
|
+
const next = edge.points[index + 1];
|
|
3726
|
+
if (!next) return [];
|
|
3727
|
+
const isFlowSegment = horizontalFlow ? point.y === next.y && point.x !== next.x : point.x === next.x && point.y !== next.y;
|
|
3728
|
+
const trackCross = horizontalFlow ? point.y : point.x;
|
|
3729
|
+
return isFlowSegment && (trackCross < nodeCrossStart || trackCross > nodeCrossEnd) ? [{
|
|
3730
|
+
cross: trackCross,
|
|
3731
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y)
|
|
3732
|
+
}] : [];
|
|
3733
|
+
}).sort((left, right) => right.length - left.length)[0];
|
|
3734
|
+
if (!exteriorTrack) return [];
|
|
3735
|
+
return [{
|
|
3736
|
+
edge,
|
|
3737
|
+
exteriorTrack,
|
|
3738
|
+
labelRect,
|
|
3739
|
+
lowSide: exteriorTrack.cross < (nodeCrossStart + nodeCrossEnd) / 2
|
|
3740
|
+
}];
|
|
3741
|
+
});
|
|
3742
|
+
movableLabels.sort((left, right) => {
|
|
3743
|
+
if (left.lowSide !== right.lowSide) return left.lowSide ? -1 : 1;
|
|
3744
|
+
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);
|
|
3745
|
+
});
|
|
3746
|
+
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
3747
|
+
const obstacles = [...nodeRects, ...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) => edgeId === String(edge.id) ? [] : [rect])];
|
|
3748
|
+
let totalDelta = 0;
|
|
3749
|
+
while (true) {
|
|
3750
|
+
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
3751
|
+
if (blockers.length === 0) break;
|
|
3752
|
+
const delta = (lowSide ? Math.min(...blockers.map(crossStart)) - spacing - (horizontalFlow ? edge.height : edge.width) : Math.max(...blockers.map(crossEnd)) + spacing) - crossStart(labelRect);
|
|
3753
|
+
totalDelta += delta;
|
|
3754
|
+
if (horizontalFlow) labelRect.y += delta;
|
|
3755
|
+
else labelRect.x += delta;
|
|
3756
|
+
}
|
|
3757
|
+
if (totalDelta === 0) continue;
|
|
3758
|
+
if (horizontalFlow) edge.y += totalDelta;
|
|
3759
|
+
else edge.x += totalDelta;
|
|
3760
|
+
edge.points = edge.points.map((point) => {
|
|
3761
|
+
if ((horizontalFlow ? point.y : point.x) !== exteriorTrack.cross) return point;
|
|
3762
|
+
return horizontalFlow ? {
|
|
3763
|
+
...point,
|
|
3764
|
+
y: point.y + totalDelta
|
|
3765
|
+
} : {
|
|
3766
|
+
...point,
|
|
3767
|
+
x: point.x + totalDelta
|
|
3768
|
+
};
|
|
3769
|
+
});
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3702
3773
|
//#endregion
|
|
3703
3774
|
//#region src/layered/network-simplex.ts
|
|
3704
3775
|
function connectedEdges(node) {
|
|
@@ -8745,6 +8816,7 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8745
8816
|
let expandedRoutes = measure("edge-routing", () => edgeRouter(expanded.input, expanded.orientation, placement));
|
|
8746
8817
|
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
8747
8818
|
const antiparallelLabelPositions = /* @__PURE__ */ new Map();
|
|
8819
|
+
const outerAntiparallelLabelIds = /* @__PURE__ */ new Set();
|
|
8748
8820
|
const parallelLabelPositions = /* @__PURE__ */ new Map();
|
|
8749
8821
|
const postCompactionNodeCrossDeltas = /* @__PURE__ */ new Map();
|
|
8750
8822
|
const horizontalAntiparallelFlow = direction === "left" || direction === "right";
|
|
@@ -8834,12 +8906,13 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8834
8906
|
const secondRect = placement.rectByNodeId.get(pair[0].targetId);
|
|
8835
8907
|
if (!firstNode || !secondNode || !firstRect || !secondRect) continue;
|
|
8836
8908
|
const nonSelfNeighbors = (nodeId) => new Set(graph.edges.flatMap((edge) => edge.sourceId === edge.targetId ? [] : edge.sourceId === nodeId ? [edge.targetId] : edge.targetId === nodeId ? [edge.sourceId] : []));
|
|
8837
|
-
|
|
8909
|
+
const isolatedAntiparallelPair = nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
|
|
8838
8910
|
const hasFlexiblePorts = (node) => {
|
|
8839
8911
|
const constraints = options.nodeSettings?.(node)?.portConstraints;
|
|
8840
8912
|
return constraints === void 0 || constraints === "UNDEFINED" || constraints === "FREE";
|
|
8841
8913
|
};
|
|
8842
|
-
|
|
8914
|
+
const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
|
|
8915
|
+
if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
|
|
8843
8916
|
const firstBeforeSecond = horizontal ? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2 : firstRect.y + firstRect.height / 2 < secondRect.y + secondRect.height / 2;
|
|
8844
8917
|
const beforeRect = firstBeforeSecond ? firstRect : secondRect;
|
|
8845
8918
|
const afterRect = firstBeforeSecond ? secondRect : firstRect;
|
|
@@ -8850,13 +8923,20 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8850
8923
|
for (const edge of pair) {
|
|
8851
8924
|
const width = edge.width ?? 0;
|
|
8852
8925
|
const height = edge.height ?? 0;
|
|
8853
|
-
antiparallelLabelPositions.set(edge.id, horizontal ? {
|
|
8926
|
+
antiparallelLabelPositions.set(edge.id, isolatedAntiparallelPair ? horizontal ? {
|
|
8854
8927
|
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
8855
8928
|
y: cross
|
|
8856
8929
|
} : {
|
|
8857
8930
|
x: cross,
|
|
8858
8931
|
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
|
|
8932
|
+
} : horizontal ? {
|
|
8933
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
8934
|
+
y: edge === forward ? (firstRect.y + firstRect.height / 2 + secondRect.y + secondRect.height / 2) / 2 - height / 2 : Math.max(firstRect.y + firstRect.height, secondRect.y + secondRect.height) + edgeSpacing
|
|
8935
|
+
} : {
|
|
8936
|
+
x: edge === forward ? (firstRect.x + firstRect.width / 2 + secondRect.x + secondRect.width / 2) / 2 - width / 2 : Math.max(firstRect.x + firstRect.width, secondRect.x + secondRect.width) + edgeSpacing,
|
|
8937
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2
|
|
8859
8938
|
});
|
|
8939
|
+
if (!isolatedAntiparallelPair && edge !== forward) outerAntiparallelLabelIds.add(edge.id);
|
|
8860
8940
|
cross += (horizontal ? height : width) + edgeSpacing;
|
|
8861
8941
|
}
|
|
8862
8942
|
const forwardLabel = antiparallelLabelPositions.get(forward.id);
|
|
@@ -8881,8 +8961,10 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
8881
8961
|
x: nextCross
|
|
8882
8962
|
});
|
|
8883
8963
|
};
|
|
8884
|
-
|
|
8885
|
-
|
|
8964
|
+
if (isolatedAntiparallelPair) {
|
|
8965
|
+
alignPort(nodesById.get(forward.sourceId), forward.sourcePort);
|
|
8966
|
+
alignPort(nodesById.get(forward.targetId), forward.targetPort);
|
|
8967
|
+
}
|
|
8886
8968
|
}
|
|
8887
8969
|
}
|
|
8888
8970
|
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
@@ -9239,7 +9321,33 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9239
9321
|
};
|
|
9240
9322
|
const flexibleFeedbackLabel = labelPlacement === "CENTER" && inlineLabel && expanded.orientation.reversedEdgeIds.has(edge.id) && hasFlexiblePorts(sourceNode) && hasFlexiblePorts(targetNode) && sourceRect !== void 0 && targetRect !== void 0;
|
|
9241
9323
|
const [beforeFlowRect, afterFlowRect] = sourceRect && targetRect ? horizontal ? sourceRect.x <= targetRect.x ? [sourceRect, targetRect] : [targetRect, sourceRect] : sourceRect.y <= targetRect.y ? [sourceRect, targetRect] : [targetRect, sourceRect] : [void 0, void 0];
|
|
9242
|
-
const
|
|
9324
|
+
const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
|
|
9325
|
+
const explicitLabelPosition = (() => {
|
|
9326
|
+
if (!outerAntiparallelLabelIds.has(edge.id)) return void 0;
|
|
9327
|
+
const edgeNodeSpacing$1 = Number(options.settings?.["spacing.edgeNode"] ?? 10);
|
|
9328
|
+
if (horizontal) {
|
|
9329
|
+
const x$1 = (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2;
|
|
9330
|
+
const blockers$1 = [
|
|
9331
|
+
sourceRect,
|
|
9332
|
+
targetRect,
|
|
9333
|
+
...[...placement.rectByNodeId.values()].filter((rect) => rect.x < x$1 + width && rect.x + rect.width > x$1)
|
|
9334
|
+
];
|
|
9335
|
+
return {
|
|
9336
|
+
x: x$1,
|
|
9337
|
+
y: Math.max(...blockers$1.map((rect) => rect.y + rect.height)) + edgeNodeSpacing$1
|
|
9338
|
+
};
|
|
9339
|
+
}
|
|
9340
|
+
const y$1 = (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2;
|
|
9341
|
+
const blockers = [
|
|
9342
|
+
sourceRect,
|
|
9343
|
+
targetRect,
|
|
9344
|
+
...[...placement.rectByNodeId.values()].filter((rect) => rect.y < y$1 + height && rect.y + rect.height > y$1)
|
|
9345
|
+
];
|
|
9346
|
+
return {
|
|
9347
|
+
x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing$1,
|
|
9348
|
+
y: y$1
|
|
9349
|
+
};
|
|
9350
|
+
})() ?? antiparallelLabelPosition ?? parallelLabelPositions.get(edge.id);
|
|
9243
9351
|
const x = explicitLabelPosition ? explicitLabelPosition.x : flexibleFeedbackLabel ? horizontal ? (beforeFlowRect.x + beforeFlowRect.width + afterFlowRect.x - width) / 2 : targetRect.x + (targetRect.width - width) / 2 : labelPlacement === "CENTER" && horizontal && labelDummyRect ? labelDummyRect.x : routeX;
|
|
9244
9352
|
const y = explicitLabelPosition ? explicitLabelPosition.y : flexibleFeedbackLabel ? horizontal ? targetRect.y + (targetRect.height - height) / 2 : (beforeFlowRect.y + beforeFlowRect.height + afterFlowRect.y - height) / 2 : labelPlacement === "CENTER" && !horizontal && labelDummyRect ? labelDummyRect.y : routeY;
|
|
9245
9353
|
return {
|
|
@@ -9252,6 +9360,19 @@ function runLayeredPipeline(graph, options, context) {
|
|
|
9252
9360
|
routing: edgeRouting === "POLYLINE" ? "polyline" : edgeRouting === "SPLINES" ? "splines" : "orthogonal"
|
|
9253
9361
|
};
|
|
9254
9362
|
});
|
|
9363
|
+
if (edgeRouting === "ORTHOGONAL") separateExteriorLabels({
|
|
9364
|
+
edges,
|
|
9365
|
+
nodeRects: feedbackNodeRects,
|
|
9366
|
+
direction,
|
|
9367
|
+
spacing: Number(options.settings?.["spacing.edgeEdge"] ?? 10),
|
|
9368
|
+
settings: (edge) => {
|
|
9369
|
+
const edgeSettings = options.edgeSettings?.(edge);
|
|
9370
|
+
return {
|
|
9371
|
+
inline: edgeSettings?.["edgeLabels.inline"] === true,
|
|
9372
|
+
placement: edgeSettings?.["edgeLabels.placement"] ?? "CENTER"
|
|
9373
|
+
};
|
|
9374
|
+
}
|
|
9375
|
+
});
|
|
9255
9376
|
return {
|
|
9256
9377
|
...graph,
|
|
9257
9378
|
direction,
|
|
@@ -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-BYhTymAk.mjs";
|
|
2
2
|
import { getNodeSize } from "@statelyai/graph/layout";
|
|
3
3
|
|
|
4
4
|
//#region src/fixed.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/layout",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Extensible, graph-native layout algorithms for @statelyai/graph",
|
|
5
5
|
"license": "EPL-2.0 OR GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@changesets/cli": "3.0.1",
|
|
48
48
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
49
49
|
"@codemirror/lint": "^6.9.7",
|
|
50
|
+
"@statelyai/layout-pre-exterior-label-separation": "npm:@statelyai/layout@0.0.4",
|
|
50
51
|
"@types/node": "^25.0.3",
|
|
51
52
|
"@types/react": "^19.2.18",
|
|
52
53
|
"@types/react-dom": "^19.2.4",
|
package/src/elkjs/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ElkConstructorArguments,
|
|
15
15
|
ElkLayoutAlgorithmDescription,
|
|
16
16
|
ElkEdge,
|
|
17
|
+
ElkLabel,
|
|
17
18
|
ElkLayoutArguments,
|
|
18
19
|
ElkLayoutCategoryDescription,
|
|
19
20
|
ElkLayoutOptionDescription,
|
|
@@ -25,6 +26,13 @@ import type {
|
|
|
25
26
|
LaidOutElkNode,
|
|
26
27
|
} from "./types";
|
|
27
28
|
|
|
29
|
+
function isLayoutEdgeLabel(label: ElkLabel): boolean {
|
|
30
|
+
return (
|
|
31
|
+
getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true &&
|
|
32
|
+
(Boolean(label.text) || label.width !== undefined || label.height !== undefined)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export type {
|
|
29
37
|
ElkConstructorArguments,
|
|
30
38
|
ElkCommonDescription,
|
|
@@ -1264,10 +1272,7 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1264
1272
|
if (isInsideSelfLoop(root, edge)) return [];
|
|
1265
1273
|
const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
|
|
1266
1274
|
const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
|
|
1267
|
-
const labels = (edge.labels ?? []).filter(
|
|
1268
|
-
(label) =>
|
|
1269
|
-
Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
|
|
1270
|
-
);
|
|
1275
|
+
const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1271
1276
|
const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
1272
1277
|
const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
|
|
1273
1278
|
const labelHeight =
|
|
@@ -1387,7 +1392,7 @@ function applyLayout(
|
|
|
1387
1392
|
label.y ??= 0;
|
|
1388
1393
|
continue;
|
|
1389
1394
|
}
|
|
1390
|
-
if (!label
|
|
1395
|
+
if (!isLayoutEdgeLabel(label)) {
|
|
1391
1396
|
label.x ??= 0;
|
|
1392
1397
|
label.y ??= 0;
|
|
1393
1398
|
continue;
|
|
@@ -1443,10 +1448,7 @@ function normalizeElkGraphBounds(
|
|
|
1443
1448
|
);
|
|
1444
1449
|
}
|
|
1445
1450
|
for (const edge of root.edges ?? []) {
|
|
1446
|
-
const laidOutLabels = (edge.labels ?? []).filter(
|
|
1447
|
-
(label) =>
|
|
1448
|
-
Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
|
|
1449
|
-
);
|
|
1451
|
+
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1450
1452
|
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1451
1453
|
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
1452
1454
|
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
|
|
@@ -1473,7 +1475,7 @@ function normalizeElkGraphBounds(
|
|
|
1473
1475
|
point.y += shiftY;
|
|
1474
1476
|
}
|
|
1475
1477
|
}
|
|
1476
|
-
for (const label of (edge.labels ?? []).filter(
|
|
1478
|
+
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
1477
1479
|
label.x = (label.x ?? 0) + shiftX;
|
|
1478
1480
|
label.y = (label.y ?? 0) + shiftY;
|
|
1479
1481
|
}
|
|
@@ -1578,11 +1580,7 @@ function normalizeElkGraphBounds(
|
|
|
1578
1580
|
]),
|
|
1579
1581
|
...(root.edges ?? []).flatMap((edge) =>
|
|
1580
1582
|
(edge.labels ?? [])
|
|
1581
|
-
.filter(
|
|
1582
|
-
(label) =>
|
|
1583
|
-
Boolean(label.text) &&
|
|
1584
|
-
getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
|
|
1585
|
-
)
|
|
1583
|
+
.filter(isLayoutEdgeLabel)
|
|
1586
1584
|
.map((label) => (label.x ?? 0) + (label.width ?? 0)),
|
|
1587
1585
|
),
|
|
1588
1586
|
...(root.edges ?? []).flatMap((edge) =>
|
|
@@ -1617,11 +1615,7 @@ function normalizeElkGraphBounds(
|
|
|
1617
1615
|
]),
|
|
1618
1616
|
...(root.edges ?? []).flatMap((edge) =>
|
|
1619
1617
|
(edge.labels ?? [])
|
|
1620
|
-
.filter(
|
|
1621
|
-
(label) =>
|
|
1622
|
-
Boolean(label.text) &&
|
|
1623
|
-
getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true,
|
|
1624
|
-
)
|
|
1618
|
+
.filter(isLayoutEdgeLabel)
|
|
1625
1619
|
.map(
|
|
1626
1620
|
(label) =>
|
|
1627
1621
|
(label.y ?? 0) +
|
package/src/layered/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Graph, GraphEdge, GraphNode, Point, VisualGraph, VisualNode } from "@statelyai/graph";
|
|
2
2
|
import { UnsupportedLayoutError } from "../errors";
|
|
3
3
|
import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
|
|
4
|
+
import { separateExteriorLabels } from "./separate-exterior-labels";
|
|
4
5
|
import {
|
|
5
6
|
assignLayersByLongestPath,
|
|
6
7
|
assignLayersByLongestPathToSink,
|
|
@@ -1760,6 +1761,7 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1760
1761
|
);
|
|
1761
1762
|
measure("post-compaction", () => applyPostCompaction(expanded.input, placement, expandedRoutes));
|
|
1762
1763
|
const antiparallelLabelPositions = new Map<string, Point>();
|
|
1764
|
+
const outerAntiparallelLabelIds = new Set<string>();
|
|
1763
1765
|
const parallelLabelPositions = new Map<string, Point>();
|
|
1764
1766
|
const postCompactionNodeCrossDeltas = new Map<string, number>();
|
|
1765
1767
|
const horizontalAntiparallelFlow = direction === "left" || direction === "right";
|
|
@@ -1898,14 +1900,14 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1898
1900
|
: [],
|
|
1899
1901
|
),
|
|
1900
1902
|
);
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
}
|
|
1903
|
+
const isolatedAntiparallelPair =
|
|
1904
|
+
nonSelfNeighbors(firstNode.id).size === 1 && nonSelfNeighbors(secondNode.id).size === 1;
|
|
1904
1905
|
const hasFlexiblePorts = (node: GraphNode): boolean => {
|
|
1905
1906
|
const constraints = options.nodeSettings?.(node)?.portConstraints;
|
|
1906
1907
|
return constraints === undefined || constraints === "UNDEFINED" || constraints === "FREE";
|
|
1907
1908
|
};
|
|
1908
|
-
|
|
1909
|
+
const hasFlexiblePairPorts = hasFlexiblePorts(firstNode) && hasFlexiblePorts(secondNode);
|
|
1910
|
+
if (isolatedAntiparallelPair && !hasFlexiblePairPorts) continue;
|
|
1909
1911
|
|
|
1910
1912
|
const firstBeforeSecond = horizontal
|
|
1911
1913
|
? firstRect.x + firstRect.width / 2 < secondRect.x + secondRect.width / 2
|
|
@@ -1929,16 +1931,44 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1929
1931
|
const height = edge.height ?? 0;
|
|
1930
1932
|
antiparallelLabelPositions.set(
|
|
1931
1933
|
edge.id,
|
|
1932
|
-
|
|
1933
|
-
?
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1934
|
+
isolatedAntiparallelPair
|
|
1935
|
+
? horizontal
|
|
1936
|
+
? {
|
|
1937
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
1938
|
+
y: cross,
|
|
1939
|
+
}
|
|
1940
|
+
: {
|
|
1941
|
+
x: cross,
|
|
1942
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
|
|
1943
|
+
}
|
|
1944
|
+
: horizontal
|
|
1945
|
+
? {
|
|
1946
|
+
x: (beforeRect.x + beforeRect.width + afterRect.x - width) / 2,
|
|
1947
|
+
y:
|
|
1948
|
+
edge === forward
|
|
1949
|
+
? (firstRect.y +
|
|
1950
|
+
firstRect.height / 2 +
|
|
1951
|
+
secondRect.y +
|
|
1952
|
+
secondRect.height / 2) /
|
|
1953
|
+
2 -
|
|
1954
|
+
height / 2
|
|
1955
|
+
: Math.max(firstRect.y + firstRect.height, secondRect.y + secondRect.height) +
|
|
1956
|
+
edgeSpacing,
|
|
1957
|
+
}
|
|
1958
|
+
: {
|
|
1959
|
+
x:
|
|
1960
|
+
edge === forward
|
|
1961
|
+
? (firstRect.x + firstRect.width / 2 + secondRect.x + secondRect.width / 2) /
|
|
1962
|
+
2 -
|
|
1963
|
+
width / 2
|
|
1964
|
+
: Math.max(firstRect.x + firstRect.width, secondRect.x + secondRect.width) +
|
|
1965
|
+
edgeSpacing,
|
|
1966
|
+
y: (beforeRect.y + beforeRect.height + afterRect.y - height) / 2,
|
|
1967
|
+
},
|
|
1941
1968
|
);
|
|
1969
|
+
if (!isolatedAntiparallelPair && edge !== forward) {
|
|
1970
|
+
outerAntiparallelLabelIds.add(edge.id);
|
|
1971
|
+
}
|
|
1942
1972
|
cross += (horizontal ? height : width) + edgeSpacing;
|
|
1943
1973
|
}
|
|
1944
1974
|
|
|
@@ -1974,8 +2004,10 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
1974
2004
|
horizontal ? { ...rect, y: nextCross } : { ...rect, x: nextCross },
|
|
1975
2005
|
);
|
|
1976
2006
|
};
|
|
1977
|
-
|
|
1978
|
-
|
|
2007
|
+
if (isolatedAntiparallelPair) {
|
|
2008
|
+
alignPort(nodesById.get(forward.sourceId)!, forward.sourcePort);
|
|
2009
|
+
alignPort(nodesById.get(forward.targetId)!, forward.targetPort);
|
|
2010
|
+
}
|
|
1979
2011
|
}
|
|
1980
2012
|
}
|
|
1981
2013
|
if (postCompactionNodeCrossDeltas.size > 0) {
|
|
@@ -2482,8 +2514,41 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2482
2514
|
? [sourceRect, targetRect]
|
|
2483
2515
|
: [targetRect, sourceRect]
|
|
2484
2516
|
: [undefined, undefined];
|
|
2517
|
+
const antiparallelLabelPosition = antiparallelLabelPositions.get(edge.id);
|
|
2518
|
+
const outerAntiparallelLabelPosition = (() => {
|
|
2519
|
+
if (!outerAntiparallelLabelIds.has(edge.id)) return undefined;
|
|
2520
|
+
const edgeNodeSpacing = Number(options.settings?.["spacing.edgeNode"] ?? 10);
|
|
2521
|
+
if (horizontal) {
|
|
2522
|
+
const x = (beforeFlowRect!.x + beforeFlowRect!.width + afterFlowRect!.x - width) / 2;
|
|
2523
|
+
const blockers = [
|
|
2524
|
+
sourceRect!,
|
|
2525
|
+
targetRect!,
|
|
2526
|
+
...[...placement.rectByNodeId.values()].filter(
|
|
2527
|
+
(rect) => rect.x < x + width && rect.x + rect.width > x,
|
|
2528
|
+
),
|
|
2529
|
+
];
|
|
2530
|
+
return {
|
|
2531
|
+
x,
|
|
2532
|
+
y: Math.max(...blockers.map((rect) => rect.y + rect.height)) + edgeNodeSpacing,
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
const y = (beforeFlowRect!.y + beforeFlowRect!.height + afterFlowRect!.y - height) / 2;
|
|
2536
|
+
const blockers = [
|
|
2537
|
+
sourceRect!,
|
|
2538
|
+
targetRect!,
|
|
2539
|
+
...[...placement.rectByNodeId.values()].filter(
|
|
2540
|
+
(rect) => rect.y < y + height && rect.y + rect.height > y,
|
|
2541
|
+
),
|
|
2542
|
+
];
|
|
2543
|
+
return {
|
|
2544
|
+
x: Math.max(...blockers.map((rect) => rect.x + rect.width)) + edgeNodeSpacing,
|
|
2545
|
+
y,
|
|
2546
|
+
};
|
|
2547
|
+
})();
|
|
2485
2548
|
const explicitLabelPosition =
|
|
2486
|
-
|
|
2549
|
+
outerAntiparallelLabelPosition ??
|
|
2550
|
+
antiparallelLabelPosition ??
|
|
2551
|
+
parallelLabelPositions.get(edge.id);
|
|
2487
2552
|
const x = explicitLabelPosition
|
|
2488
2553
|
? explicitLabelPosition.x
|
|
2489
2554
|
: flexibleFeedbackLabel
|
|
@@ -2518,6 +2583,22 @@ function runLayeredPipeline<N, E, G, P>(
|
|
|
2518
2583
|
};
|
|
2519
2584
|
});
|
|
2520
2585
|
|
|
2586
|
+
if (edgeRouting === "ORTHOGONAL") {
|
|
2587
|
+
separateExteriorLabels({
|
|
2588
|
+
edges,
|
|
2589
|
+
nodeRects: feedbackNodeRects,
|
|
2590
|
+
direction,
|
|
2591
|
+
spacing: Number(options.settings?.["spacing.edgeEdge"] ?? 10),
|
|
2592
|
+
settings: (edge) => {
|
|
2593
|
+
const edgeSettings = options.edgeSettings?.(edge);
|
|
2594
|
+
return {
|
|
2595
|
+
inline: edgeSettings?.["edgeLabels.inline"] === true,
|
|
2596
|
+
placement: edgeSettings?.["edgeLabels.placement"] ?? "CENTER",
|
|
2597
|
+
};
|
|
2598
|
+
},
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2521
2602
|
return {
|
|
2522
2603
|
...graph,
|
|
2523
2604
|
direction,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { EntityRect, Point } from "@statelyai/graph";
|
|
2
|
+
|
|
3
|
+
export interface ExteriorLabelEdge {
|
|
4
|
+
id: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
points: Point[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ExteriorLabelSettings {
|
|
13
|
+
inline: boolean;
|
|
14
|
+
placement: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SeparateExteriorLabelsInput<E extends ExteriorLabelEdge> {
|
|
18
|
+
edges: E[];
|
|
19
|
+
nodeRects: readonly EntityRect[];
|
|
20
|
+
direction: "up" | "down" | "left" | "right";
|
|
21
|
+
spacing: number;
|
|
22
|
+
settings: (edge: E) => ExteriorLabelSettings;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Separates movable inline labels and their exterior orthogonal route tracks. */
|
|
26
|
+
export function separateExteriorLabels<E extends ExteriorLabelEdge>({
|
|
27
|
+
edges,
|
|
28
|
+
nodeRects,
|
|
29
|
+
direction,
|
|
30
|
+
spacing,
|
|
31
|
+
settings,
|
|
32
|
+
}: SeparateExteriorLabelsInput<E>): void {
|
|
33
|
+
const horizontalFlow = direction === "left" || direction === "right";
|
|
34
|
+
const nodeCrossStart = Math.min(...nodeRects.map((rect) => (horizontalFlow ? rect.y : rect.x)));
|
|
35
|
+
const nodeCrossEnd = Math.max(
|
|
36
|
+
...nodeRects.map((rect) => (horizontalFlow ? rect.y + rect.height : rect.x + rect.width)),
|
|
37
|
+
);
|
|
38
|
+
const flowStart = (rect: EntityRect): number => (horizontalFlow ? rect.x : rect.y);
|
|
39
|
+
const flowEnd = (rect: EntityRect): number =>
|
|
40
|
+
horizontalFlow ? rect.x + rect.width : rect.y + rect.height;
|
|
41
|
+
const crossStart = (rect: EntityRect): number => (horizontalFlow ? rect.y : rect.x);
|
|
42
|
+
const crossEnd = (rect: EntityRect): number =>
|
|
43
|
+
horizontalFlow ? rect.y + rect.height : rect.x + rect.width;
|
|
44
|
+
const overlaps = (left: EntityRect, right: EntityRect): boolean =>
|
|
45
|
+
flowStart(left) < flowEnd(right) &&
|
|
46
|
+
flowEnd(left) > flowStart(right) &&
|
|
47
|
+
crossStart(left) < crossEnd(right) &&
|
|
48
|
+
crossEnd(left) > crossStart(right);
|
|
49
|
+
type MutableRect = { x: number; y: number; width: number; height: number };
|
|
50
|
+
const labelRectByEdgeId = new Map<string, MutableRect>(
|
|
51
|
+
edges
|
|
52
|
+
.filter((edge) => edge.width > 0 && edge.height > 0)
|
|
53
|
+
.map((edge) => [
|
|
54
|
+
String(edge.id),
|
|
55
|
+
{ x: edge.x, y: edge.y, width: edge.width, height: edge.height },
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
const movableLabels = edges.flatMap((edge) => {
|
|
59
|
+
const labelRect = labelRectByEdgeId.get(String(edge.id));
|
|
60
|
+
const labelSettings = settings(edge);
|
|
61
|
+
if (!labelRect || !labelSettings.inline || labelSettings.placement !== "CENTER") return [];
|
|
62
|
+
const exteriorTrack = edge.points
|
|
63
|
+
.flatMap((point, index) => {
|
|
64
|
+
const next = edge.points[index + 1];
|
|
65
|
+
if (!next) return [];
|
|
66
|
+
const isFlowSegment = horizontalFlow
|
|
67
|
+
? point.y === next.y && point.x !== next.x
|
|
68
|
+
: point.x === next.x && point.y !== next.y;
|
|
69
|
+
const trackCross = horizontalFlow ? point.y : point.x;
|
|
70
|
+
return isFlowSegment && (trackCross < nodeCrossStart || trackCross > nodeCrossEnd)
|
|
71
|
+
? [
|
|
72
|
+
{
|
|
73
|
+
cross: trackCross,
|
|
74
|
+
length: horizontalFlow ? Math.abs(next.x - point.x) : Math.abs(next.y - point.y),
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
: [];
|
|
78
|
+
})
|
|
79
|
+
.sort((left, right) => right.length - left.length)[0];
|
|
80
|
+
if (!exteriorTrack) return [];
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
edge,
|
|
84
|
+
exteriorTrack,
|
|
85
|
+
labelRect,
|
|
86
|
+
lowSide: exteriorTrack.cross < (nodeCrossStart + nodeCrossEnd) / 2,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
movableLabels.sort((left, right) => {
|
|
92
|
+
if (left.lowSide !== right.lowSide) return left.lowSide ? -1 : 1;
|
|
93
|
+
const crossDifference = left.lowSide
|
|
94
|
+
? crossStart(left.labelRect) - crossStart(right.labelRect)
|
|
95
|
+
: crossStart(right.labelRect) - crossStart(left.labelRect);
|
|
96
|
+
return (
|
|
97
|
+
crossDifference ||
|
|
98
|
+
flowStart(left.labelRect) - flowStart(right.labelRect) ||
|
|
99
|
+
(String(left.edge.id) < String(right.edge.id)
|
|
100
|
+
? -1
|
|
101
|
+
: String(left.edge.id) > String(right.edge.id)
|
|
102
|
+
? 1
|
|
103
|
+
: 0)
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
for (const { edge, exteriorTrack, labelRect, lowSide } of movableLabels) {
|
|
108
|
+
const obstacles = [
|
|
109
|
+
...nodeRects,
|
|
110
|
+
...[...labelRectByEdgeId.entries()].flatMap(([edgeId, rect]) =>
|
|
111
|
+
edgeId === String(edge.id) ? [] : [rect],
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
let totalDelta = 0;
|
|
115
|
+
while (true) {
|
|
116
|
+
const blockers = obstacles.filter((rect) => overlaps(labelRect, rect));
|
|
117
|
+
if (blockers.length === 0) break;
|
|
118
|
+
const desiredCross = lowSide
|
|
119
|
+
? Math.min(...blockers.map(crossStart)) -
|
|
120
|
+
spacing -
|
|
121
|
+
(horizontalFlow ? edge.height : edge.width)
|
|
122
|
+
: Math.max(...blockers.map(crossEnd)) + spacing;
|
|
123
|
+
const delta = desiredCross - crossStart(labelRect);
|
|
124
|
+
totalDelta += delta;
|
|
125
|
+
if (horizontalFlow) labelRect.y += delta;
|
|
126
|
+
else labelRect.x += delta;
|
|
127
|
+
}
|
|
128
|
+
if (totalDelta === 0) continue;
|
|
129
|
+
if (horizontalFlow) edge.y += totalDelta;
|
|
130
|
+
else edge.x += totalDelta;
|
|
131
|
+
edge.points = edge.points.map((point) => {
|
|
132
|
+
const pointCross = horizontalFlow ? point.y : point.x;
|
|
133
|
+
if (pointCross !== exteriorTrack.cross) return point;
|
|
134
|
+
return horizontalFlow
|
|
135
|
+
? { ...point, y: point.y + totalDelta }
|
|
136
|
+
: { ...point, x: point.x + totalDelta };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|