@statelyai/layout 0.0.4 → 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.
@@ -1,5 +1,5 @@
1
- import { o as elkLayeredOptionDefinitions, t as getLayeredLayout } from "../layered-Bp8MkxWK.mjs";
2
- import { a as getRandomLayout, f as getFixedLayout, n as getSporeOverlapRemovalLayout, s as getRectanglePackingLayout, t as getSporeCompactionLayout, u as getBoxLayout } from "../spore-ePWz2LtF.mjs";
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
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-Bp8MkxWK.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-ePWz2LtF.mjs";
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
@@ -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-Bp8MkxWK.mjs";
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) {
@@ -9289,6 +9360,19 @@ function runLayeredPipeline(graph, options, context) {
9289
9360
  routing: edgeRouting === "POLYLINE" ? "polyline" : edgeRouting === "SPLINES" ? "splines" : "orthogonal"
9290
9361
  };
9291
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
+ });
9292
9376
  return {
9293
9377
  ...graph,
9294
9378
  direction,
@@ -1,4 +1,4 @@
1
- import { F as placePorts, O as getPolylineMidpoint, z as JavaRandom } from "./layered-Bp8MkxWK.mjs";
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.4",
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",
@@ -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,
@@ -2582,6 +2583,22 @@ function runLayeredPipeline<N, E, G, P>(
2582
2583
  };
2583
2584
  });
2584
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
+
2585
2602
  return {
2586
2603
  ...graph,
2587
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
+ }