@statelyai/layout 0.2.0 → 0.4.0

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.
Files changed (132) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +221 -13
  3. package/dist/elkjs/index.cjs +3 -2
  4. package/dist/elkjs/index.d.cts +13 -2
  5. package/dist/elkjs/index.d.mts +13 -2
  6. package/dist/elkjs/index.mjs +3 -3
  7. package/dist/elkjs/main.cjs +2 -2
  8. package/dist/elkjs/main.d.cts +2 -2
  9. package/dist/elkjs/main.d.mts +2 -2
  10. package/dist/elkjs/main.mjs +2 -2
  11. package/dist/elkjs/worker-api.cjs +1 -1
  12. package/dist/elkjs/worker-api.d.cts +2 -2
  13. package/dist/elkjs/worker-api.d.mts +2 -2
  14. package/dist/elkjs/worker-api.mjs +1 -1
  15. package/dist/elkjs/worker.cjs +2 -2
  16. package/dist/elkjs/worker.mjs +2 -2
  17. package/dist/elkjs-UciYx7lD.mjs +2973 -0
  18. package/dist/elkjs-ggjsBuVP.cjs +3002 -0
  19. package/dist/index-B6cWNrMw.d.cts +80 -0
  20. package/dist/index-CJQLUKD9.d.mts +80 -0
  21. package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
  22. package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
  23. package/dist/index.cjs +39 -9
  24. package/dist/index.d.cts +4 -3
  25. package/dist/index.d.mts +4 -3
  26. package/dist/index.mjs +4 -3
  27. package/dist/layered/index.cjs +3 -1
  28. package/dist/layered/index.d.cts +3 -2
  29. package/dist/layered/index.d.mts +3 -2
  30. package/dist/layered/index.mjs +2 -2
  31. package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
  32. package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
  33. package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
  34. package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
  35. package/dist/routing/index.cjs +29 -0
  36. package/dist/routing/index.d.cts +3 -0
  37. package/dist/routing/index.d.mts +3 -0
  38. package/dist/routing/index.mjs +3 -0
  39. package/dist/routing-CR8flv8b.mjs +2780 -0
  40. package/dist/routing-QIdGGFBe.cjs +2996 -0
  41. package/dist/types-CT2akdK0.d.mts +133 -0
  42. package/dist/types-uxPlGRof.d.cts +133 -0
  43. package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
  44. package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
  45. package/package.json +14 -3
  46. package/src/authoring/coordinates.ts +38 -10
  47. package/src/authoring/partial.ts +7 -2
  48. package/src/authoring/routing.ts +4 -1
  49. package/src/constraints.ts +174 -6
  50. package/src/elkjs/index.ts +1397 -452
  51. package/src/elkjs/layout-quality.ts +219 -0
  52. package/src/elkjs/opposing-tracks.ts +161 -0
  53. package/src/elkjs/orthogonal-junctions.ts +174 -0
  54. package/src/elkjs/routes.ts +147 -0
  55. package/src/elkjs/statechart.ts +9 -3
  56. package/src/elkjs/types.ts +2 -0
  57. package/src/index.ts +8 -0
  58. package/src/internal/elkjs-compatibility.ts +4 -1
  59. package/src/internal/layered-trace.ts +48 -0
  60. package/src/internal/layout-engine.ts +2 -1
  61. package/src/java-random.ts +6 -0
  62. package/src/layered/bk-node-placement.ts +403 -92
  63. package/src/layered/center-labels.ts +398 -0
  64. package/src/layered/compaction-bounds.ts +26 -0
  65. package/src/layered/compaction-errors.ts +2 -0
  66. package/src/layered/compaction-scanline.ts +63 -0
  67. package/src/layered/compound-boundaries.ts +99 -0
  68. package/src/layered/compound.ts +445 -0
  69. package/src/layered/constraint-orientation.ts +141 -0
  70. package/src/layered/crossing-constraints.ts +243 -0
  71. package/src/layered/crossing-counter.ts +221 -0
  72. package/src/layered/crossing-graph.ts +401 -0
  73. package/src/layered/cycle-random.ts +22 -0
  74. package/src/layered/elk-port-lists.ts +243 -0
  75. package/src/layered/end-labels.ts +124 -0
  76. package/src/layered/external-port-dummy.ts +137 -0
  77. package/src/layered/feedback-chains.ts +114 -0
  78. package/src/layered/fixed-self-loop.ts +150 -0
  79. package/src/layered/greedy-switch-decider.ts +356 -0
  80. package/src/layered/grouped-compaction.ts +787 -0
  81. package/src/layered/gwt-sort.ts +53 -0
  82. package/src/layered/hierarchical-port-phases.ts +242 -0
  83. package/src/layered/hierarchical-port-restoration.ts +233 -0
  84. package/src/layered/hierarchical-port-routing.ts +179 -0
  85. package/src/layered/hierarchy-crossing.ts +121 -0
  86. package/src/layered/hierarchy-sweepiness.ts +83 -0
  87. package/src/layered/hints.ts +371 -0
  88. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  89. package/src/layered/index.ts +911 -261
  90. package/src/layered/initial-model-order.ts +404 -0
  91. package/src/layered/inverted-ports.ts +207 -0
  92. package/src/layered/label-dummy-switcher.ts +200 -0
  93. package/src/layered/long-edges.ts +135 -44
  94. package/src/layered/loop-envelopes.ts +184 -0
  95. package/src/layered/native-routing.ts +275 -0
  96. package/src/layered/network-simplex.ts +11 -15
  97. package/src/layered/node-margins.ts +59 -0
  98. package/src/layered/north-south-ports.ts +271 -0
  99. package/src/layered/opposing-tracks.ts +944 -0
  100. package/src/layered/orthogonal-cycle-order.ts +106 -0
  101. package/src/layered/orthogonal-hypersegments.ts +92 -0
  102. package/src/layered/orthogonal-junctions.ts +202 -0
  103. package/src/layered/orthogonal-segments.ts +243 -0
  104. package/src/layered/placement-orientation.ts +22 -0
  105. package/src/layered/port-distributor.ts +192 -0
  106. package/src/layered/port-edge-order.ts +20 -0
  107. package/src/layered/replace-routing.ts +45 -0
  108. package/src/layered/routing-coordinates.ts +32 -0
  109. package/src/layered/separate-exterior-labels.ts +24 -6
  110. package/src/layered/spacing.ts +34 -12
  111. package/src/layered/splitter-order.ts +106 -0
  112. package/src/layered/strategies.ts +2024 -883
  113. package/src/layered/types.ts +34 -0
  114. package/src/layered/weighted-compaction.ts +84 -0
  115. package/src/routing/adapters.ts +143 -0
  116. package/src/routing/algorithms.ts +1088 -0
  117. package/src/routing/coordination.ts +91 -0
  118. package/src/routing/engine.ts +473 -0
  119. package/src/routing/index.ts +69 -0
  120. package/src/routing/layout-cache.ts +65 -0
  121. package/src/routing/model.ts +218 -0
  122. package/src/routing/path.ts +324 -0
  123. package/src/routing/persistent.ts +117 -0
  124. package/src/routing/search.ts +358 -0
  125. package/src/routing/spatial.ts +78 -0
  126. package/src/routing/types.ts +146 -0
  127. package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
  128. package/dist/elkjs-CFlfkEBz.mjs +0 -1528
  129. /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
  130. /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
  131. /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
  132. /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
@@ -1,5 +1,7 @@
1
+ import type { NativeRoutingStrategy, RoutingSettings } from "../routing/types";
1
2
  import type { EntityRect, Graph, GraphEdge, GraphNode, GraphPort, Point } from "@statelyai/graph";
2
3
  import type { LayoutConstraints } from "@statelyai/graph/layout";
4
+ import type { LayoutHint } from "./hints";
3
5
  import type { LayoutDirection } from "../types";
4
6
  import type { ElkLayeredOptionValueByName, LayeredAdvancedOptions } from "./elk-options";
5
7
 
@@ -21,6 +23,8 @@ export interface LayoutPadding {
21
23
  }
22
24
 
23
25
  export interface LayeredPhaseInput {
26
+ /** Authored edge order retained through label and long-edge expansion. */
27
+ modelOrderByEdgeId?: ReadonlyMap<string, number>;
24
28
  graph: Graph<unknown, unknown, unknown, unknown>;
25
29
  sizes: ReadonlyMap<string, NodeSize>;
26
30
  direction: LayoutDirection;
@@ -38,12 +42,15 @@ export interface AcyclicOrientation {
38
42
  }
39
43
 
40
44
  export interface LayerAssignment {
45
+ layerCount?: number;
41
46
  layerByNodeId: ReadonlyMap<string, number>;
42
47
  /** Layer-internal seed order produced by layerers whose insertion order is observable. */
43
48
  seedOrder?: readonly string[];
44
49
  }
45
50
 
46
51
  export interface LayerOrder {
52
+ /** Canonical clockwise physical port identities, independent of edge reversal. */
53
+ physicalPortOrderByNodeId?: ReadonlyMap<string, readonly string[]>;
47
54
  layers: readonly (readonly string[])[];
48
55
  /** Internal ELK sweep state retained for exact port-aware placement. */
49
56
  inputPortOrderByNodeId?: ReadonlyMap<string, readonly string[]>;
@@ -57,6 +64,8 @@ export interface NodePlacement {
57
64
 
58
65
  export interface EdgeRoutes {
59
66
  pointsByEdgeId: ReadonlyMap<string, readonly Point[]>;
67
+ /** Branch points generated on physical orthogonal hypersegments before restoration. */
68
+ junctionPointsByEdgeId?: ReadonlyMap<string, readonly Point[]>;
60
69
  /** ELK spline segment NUB controls retained until long-edge joining. */
61
70
  splineNubControlsByEdgeId?: ReadonlyMap<string, readonly Point[]>;
62
71
  /** Reversed fixed-side routes that must stay outside the node envelope during compaction. */
@@ -92,11 +101,36 @@ export interface LayeredStrategies {
92
101
  routeEdges?: EdgeRouter;
93
102
  }
94
103
 
104
+ /** Geometry reserved while laying out a compound, before its ancestors. */
105
+ export interface CompoundLayoutOptions {
106
+ padding?: number | Partial<LayoutPadding>;
107
+ direction?: LayoutDirection;
108
+ header?: NodeSize & { side?: "top" | "right" | "bottom" | "left" };
109
+ minContentSize?: Partial<NodeSize>;
110
+ }
111
+ export interface CompoundLayoutGeometry {
112
+ /** Parent-relative outer rectangle; header/content are relative to this rectangle. */
113
+ bounds: EntityRect;
114
+ header?: EntityRect;
115
+ content: EntityRect;
116
+ }
117
+ export interface CompoundEdgeAttachment {
118
+ /** Content attachments leave toward the interior; outer attachments leave outward. */
119
+ source?: "content" | "outer";
120
+ target?: "content" | "outer";
121
+ }
122
+
95
123
  export interface LayeredLayoutOptions {
124
+ /** Replace initial routes after initial placement and routing, without moving geometry. */
125
+ routing?: { strategy: Pick<NativeRoutingStrategy, "route">; settings?: RoutingSettings };
96
126
  direction?: LayoutDirection;
127
+ compound?: (node: GraphNode) => CompoundLayoutOptions | undefined;
128
+ edgeAttachment?: (edge: GraphEdge) => CompoundEdgeAttachment | undefined;
97
129
  spacing?: Partial<LayeredSpacing>;
98
130
  padding?: number | Partial<LayoutPadding>;
99
131
  constraints?: LayoutConstraints;
132
+ /** Reader-facing preferences applied during layering, ordering and placement. */
133
+ hints?: readonly LayoutHint[];
100
134
  measure?: (node: GraphNode) => NodeSize;
101
135
  crossingSweeps?: number;
102
136
  strategies?: LayeredStrategies;
@@ -0,0 +1,84 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2017 Kiel University and others.
3
+ *
4
+ * Adapted from ELK v0.11.0 NetworkSimplexCompaction.java.
5
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
6
+ * SPDX-License-Identifier: EPL-2.0
7
+ *******************************************************************************/
8
+ import { runNetworkSimplex, type SimplexEdge, type SimplexNode } from "./network-simplex";
9
+ import { InfeasibleCompactionError } from "./compaction-errors";
10
+
11
+ export interface CompactionConstraint {
12
+ source: string;
13
+ target: string;
14
+ delta: number;
15
+ weight: number;
16
+ }
17
+
18
+ /** Minimize weighted separation subject to integral lower-bound constraints. */
19
+ export function solveWeightedCompaction(
20
+ groupIds: readonly string[],
21
+ constraints: readonly CompactionConstraint[],
22
+ ): Map<string, number> {
23
+ const nodes = groupIds.map((id, order): SimplexNode => ({
24
+ id,
25
+ order,
26
+ layer: 0,
27
+ incoming: [],
28
+ outgoing: [],
29
+ treeNode: false,
30
+ }));
31
+ if (nodes.length === 0) return new Map();
32
+ const byId = new Map(nodes.map((node) => [node.id, node]));
33
+ const edges: SimplexEdge[] = [];
34
+ const add = (source: SimplexNode, target: SimplexNode, delta: number, weight: number): void => {
35
+ const edge: SimplexEdge = {
36
+ id: `constraint:${edges.length}`,
37
+ order: edges.length,
38
+ source,
39
+ target,
40
+ delta: Math.max(0, Math.ceil(delta)),
41
+ weight,
42
+ treeEdge: false,
43
+ };
44
+ edges.push(edge);
45
+ source.outgoing.push(edge);
46
+ target.incoming.push(edge);
47
+ };
48
+ for (const constraint of constraints) {
49
+ const source = byId.get(constraint.source);
50
+ const target = byId.get(constraint.target);
51
+ if (!source || !target) throw new Error("Unknown compaction group");
52
+ if (source !== target) add(source, target, constraint.delta, constraint.weight);
53
+ }
54
+ // Fail explicitly on invalid constraint cycles rather than hanging tight-tree growth.
55
+ const remaining = new Map(nodes.map((node) => [node, node.incoming.length]));
56
+ const queue = nodes.filter((node) => node.incoming.length === 0);
57
+ let visited = 0;
58
+ for (let index = 0; index < queue.length; index++) {
59
+ const node = queue[index]!;
60
+ visited++;
61
+ for (const edge of node.outgoing) {
62
+ const count = remaining.get(edge.target)! - 1;
63
+ remaining.set(edge.target, count);
64
+ if (count === 0) queue.push(edge.target);
65
+ }
66
+ }
67
+ if (visited !== nodes.length)
68
+ throw new InfeasibleCompactionError("Cyclic compaction constraints");
69
+ const sources = nodes.filter((node) => node.incoming.length === 0);
70
+ if (sources.length > 1) {
71
+ const source: SimplexNode = {
72
+ id: "",
73
+ order: nodes.length,
74
+ layer: 0,
75
+ incoming: [],
76
+ outgoing: [],
77
+ treeNode: false,
78
+ };
79
+ nodes.push(source);
80
+ for (const target of sources) add(source, target, 1, 0);
81
+ }
82
+ runNetworkSimplex(nodes, edges, Number.MAX_SAFE_INTEGER, undefined, false);
83
+ return new Map(groupIds.map((id) => [id, byId.get(id)!.layer]));
84
+ }
@@ -0,0 +1,143 @@
1
+ import type { GraphPatch, Point, VisualGraph } from "@statelyai/graph";
2
+ import { hasCurrentRouteGeometry } from "./layout-cache";
3
+ import { worldGeometry } from "../authoring/coordinates";
4
+ import { freeze } from "./model";
5
+ import { flattenPath, pathFromPoints } from "./path";
6
+ import { PersistentMap } from "./persistent";
7
+ import type { Route, RoutePath, RoutePoint, RouteSegment } from "./types";
8
+
9
+ /** ELK/native spline output: start, then repeating control1/control2/end triples. */
10
+ export function pathFromSplinePoints(points: readonly RoutePoint[]): RoutePath {
11
+ if (points.length < 4 || (points.length - 1) % 3 !== 0)
12
+ throw new RangeError("Cubic spline points require a start and control1/control2/end triples");
13
+ const segments: RouteSegment[] = [];
14
+ for (let i = 1; i < points.length; i += 3)
15
+ segments.push({
16
+ kind: "cubic",
17
+ control1: { ...points[i]! },
18
+ control2: { ...points[i + 1]! },
19
+ to: { ...points[i + 2]! },
20
+ });
21
+ return { start: { ...points[0]! }, segments };
22
+ }
23
+ /** Normalize routes from any native layout algorithm, without running layout again. */
24
+ export function getLayoutRoutes(graph: VisualGraph): ReadonlyMap<string, Route> {
25
+ const compound = graph as VisualGraph & { compoundRoutes?: ReadonlyMap<string, Route> };
26
+ if (compound.compoundRoutes?.size && hasCurrentRouteGeometry(graph, compound.compoundRoutes))
27
+ return compound.compoundRoutes;
28
+ const world = worldGeometry(graph),
29
+ nodes = new Map(world.nodes.map((n) => [n.id, n]));
30
+ let result = new PersistentMap<Route>();
31
+ for (const edge of world.edges) {
32
+ const points = edge.points;
33
+ let path: RoutePath | undefined;
34
+ if (points?.length && points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
35
+ if (edge.routing === "splines") {
36
+ if (points.length >= 4 && (points.length - 1) % 3 === 0)
37
+ path = pathFromSplinePoints(points);
38
+ } else path = pathFromPoints(points);
39
+ }
40
+ const routed = path !== undefined;
41
+ if (!path) {
42
+ const a = nodes.get(edge.sourceId),
43
+ b = nodes.get(edge.targetId);
44
+ path = pathFromPoints([
45
+ { x: a ? a.x + a.width : 0, y: a ? a.y + a.height / 2 : 0 },
46
+ { x: b?.x ?? 40, y: b ? b.y + b.height / 2 : 40 },
47
+ ]);
48
+ }
49
+ result = result.set(
50
+ edge.id,
51
+ freeze({
52
+ edgeId: edge.id,
53
+ status: routed ? "routed" : "fallback",
54
+ diagnostics: routed
55
+ ? []
56
+ : [
57
+ {
58
+ code: "MISSING_GEOMETRY",
59
+ edgeId: edge.id,
60
+ message: "Layout has no valid route; rendered a fallback connection",
61
+ },
62
+ ],
63
+ sections: [
64
+ {
65
+ id: `${edge.id}:0`,
66
+ from: {
67
+ kind: "node",
68
+ nodeId: edge.sourceId,
69
+ ...(edge.sourcePort === undefined ? {} : { port: edge.sourcePort }),
70
+ },
71
+ to: {
72
+ kind: "node",
73
+ nodeId: edge.targetId,
74
+ ...(edge.targetPort === undefined ? {} : { port: edge.targetPort }),
75
+ },
76
+ path,
77
+ },
78
+ ],
79
+ }),
80
+ );
81
+ }
82
+ return result;
83
+ }
84
+ /** One polyline per section preserves label gaps and branching for simple renderers. */
85
+ export function routeToPolylines(
86
+ route: Route,
87
+ options?: { readonly tolerance?: number },
88
+ ): readonly (readonly RoutePoint[])[] {
89
+ return route.sections.map((s) => flattenPath(s.path, options));
90
+ }
91
+ /**
92
+ * One point list for consumers of `GraphEdge.points`. Sections are joined
93
+ * through their gaps (an edge label, for example); between orthogonal
94
+ * sections the join is orthogonal too, so it never adds a diagonal.
95
+ */
96
+ export function routeToPoints(
97
+ route: Route,
98
+ options?: { readonly tolerance?: number },
99
+ ): RoutePoint[] {
100
+ const points: RoutePoint[] = [];
101
+ const axis = (a: RoutePoint, b: RoutePoint) =>
102
+ Math.abs(a.y - b.y) < 1e-9 ? "horizontal" : Math.abs(a.x - b.x) < 1e-9 ? "vertical" : undefined;
103
+ for (const polyline of routeToPolylines(route, options)) {
104
+ const a = points.at(-1),
105
+ b = polyline[0];
106
+ if (a && b && axis(a, b) === undefined) {
107
+ const before = points.length > 1 ? axis(points.at(-2)!, a) : undefined;
108
+ const after = polyline.length > 1 ? axis(b, polyline[1]!) : undefined;
109
+ if (before && after) {
110
+ if (before === "horizontal" && after === "horizontal") {
111
+ const x = (a.x + b.x) / 2;
112
+ points.push({ x, y: a.y }, { x, y: b.y });
113
+ } else if (before === "vertical" && after === "vertical") {
114
+ const y = (a.y + b.y) / 2;
115
+ points.push({ x: a.x, y }, { x: b.x, y });
116
+ } else points.push(before === "horizontal" ? { x: b.x, y: a.y } : { x: a.x, y: b.y });
117
+ }
118
+ }
119
+ points.push(...polyline.map((point) => ({ ...point })));
120
+ }
121
+ return points;
122
+ }
123
+ /** Compatibility for single-section native consumers. Refuses to erase gaps or topology. */
124
+ export function routeToGraphPatch(
125
+ route: Route,
126
+ options: { readonly tolerance?: number; readonly offset?: RoutePoint } = {},
127
+ ): GraphPatch {
128
+ if (
129
+ route.sections.length !== 1 ||
130
+ route.sections[0]!.sharedId ||
131
+ route.sections[0]!.from.kind !== "node" ||
132
+ route.sections[0]!.to.kind !== "node"
133
+ )
134
+ throw new Error(
135
+ "This route requires section-aware rendering; a single GraphEdge.points array would lose topology",
136
+ );
137
+ const offset = options.offset ?? { x: 0, y: 0 };
138
+ const points: Point[] = flattenPath(route.sections[0]!.path, options).map((p) => ({
139
+ x: p.x - offset.x,
140
+ y: p.y - offset.y,
141
+ }));
142
+ return { op: "updateEdge", id: route.edgeId, data: { points, routing: "polyline" } };
143
+ }