@statelyai/layout 0.1.3 → 0.2.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 (60) hide show
  1. package/NOTICE.md +43 -0
  2. package/README.md +82 -4
  3. package/dist/elkjs/index.cjs +8 -0
  4. package/dist/elkjs/index.d.cts +146 -0
  5. package/dist/elkjs/index.d.mts +42 -39
  6. package/dist/elkjs/index.mjs +2 -1483
  7. package/dist/elkjs/main.cjs +17 -0
  8. package/dist/elkjs/main.d.cts +10 -0
  9. package/dist/elkjs/main.d.mts +11 -0
  10. package/dist/elkjs/main.mjs +17 -0
  11. package/dist/elkjs/worker-api.cjs +3 -0
  12. package/dist/elkjs/worker-api.d.cts +3 -0
  13. package/dist/elkjs/worker-api.d.mts +3 -0
  14. package/dist/elkjs/worker-api.mjs +3 -0
  15. package/dist/elkjs/worker.cjs +86 -0
  16. package/dist/elkjs/worker.d.cts +17 -0
  17. package/dist/elkjs/worker.d.mts +17 -0
  18. package/dist/elkjs/worker.mjs +86 -0
  19. package/dist/elkjs-BVnDwnBQ.cjs +1551 -0
  20. package/dist/elkjs-CFlfkEBz.mjs +1528 -0
  21. package/dist/{index-8xYkohbz.d.mts → index-BSH2bloA.d.mts} +77 -3
  22. package/dist/index-CG712jzV.d.cts +1286 -0
  23. package/dist/index.cjs +188 -0
  24. package/dist/index.d.cts +77 -0
  25. package/dist/index.d.mts +2 -2
  26. package/dist/index.mjs +24 -25
  27. package/dist/layered/index.cjs +38 -0
  28. package/dist/layered/index.d.cts +2 -0
  29. package/dist/layered/index.d.mts +1 -1
  30. package/dist/layered/index.mjs +1 -1
  31. package/dist/{layered-CoeH9LRO.mjs → layered-Bz2nWmVp.mjs} +2261 -47
  32. package/dist/layered-Cf9Rn8l7.cjs +12061 -0
  33. package/dist/layout-engine-BNex9jxg.cjs +712 -0
  34. package/dist/{spore-BtTSZbb6.mjs → layout-engine-WbC0YjjT.mjs} +159 -5
  35. package/dist/public-types-B8rAALJq.d.mts +109 -0
  36. package/dist/public-types-CVB0hXGk.d.cts +109 -0
  37. package/dist/worker-client-D3HMK33r.cjs +98 -0
  38. package/dist/worker-client-DAT_z2OR.d.mts +19 -0
  39. package/dist/worker-client-DVhklF6A.d.cts +19 -0
  40. package/dist/worker-client-DkwS4sHx.mjs +92 -0
  41. package/package.json +39 -9
  42. package/src/authoring/coordinates.ts +106 -0
  43. package/src/authoring/partial.ts +584 -0
  44. package/src/authoring/routing.ts +211 -0
  45. package/src/box.ts +22 -0
  46. package/src/constraints.ts +222 -0
  47. package/src/elkjs/index.ts +100 -37
  48. package/src/elkjs/main.ts +19 -0
  49. package/src/elkjs/public-types.ts +125 -0
  50. package/src/elkjs/worker-api.ts +29 -0
  51. package/src/elkjs/worker-client.ts +138 -0
  52. package/src/elkjs/worker.ts +117 -0
  53. package/src/index.ts +13 -0
  54. package/src/internal/elkjs-compatibility.ts +17 -0
  55. package/src/internal/layout-engine.ts +126 -0
  56. package/src/layered/index.ts +31 -3
  57. package/src/layout.ts +35 -28
  58. package/src/packing.ts +22 -0
  59. package/src/random.ts +75 -3
  60. package/src/types.ts +17 -2
package/src/index.ts CHANGED
@@ -90,3 +90,16 @@ export type {
90
90
  NodePlacer,
91
91
  NodeSize,
92
92
  } from "./layered";
93
+
94
+ export { c } from "./constraints";
95
+ export type {
96
+ LayoutConstraint,
97
+ GeometryReference,
98
+ GeometryAttribute,
99
+ ConstraintStrength,
100
+ AlignConstraint,
101
+ DistributeConstraint,
102
+ PinConstraint,
103
+ LinearConstraint,
104
+ WaypointConstraint,
105
+ } from "./constraints";
@@ -0,0 +1,17 @@
1
+ import type { VisualGraph } from "@statelyai/graph";
2
+
3
+ export interface Elkjs0111ResultPolicy {
4
+ providerBounds?: { width: number; height: number };
5
+ preserveEdgeSections?: boolean;
6
+ skipBoundsNormalization?: boolean;
7
+ }
8
+
9
+ export const elkjs0111ResultPolicy = Symbol("elkjs0111ResultPolicy");
10
+
11
+ export function setElkjs0111ResultPolicy<N, E, G, P>(
12
+ graph: VisualGraph<N, E, G, P>,
13
+ policy: Elkjs0111ResultPolicy,
14
+ ): VisualGraph<N, E, G, P> {
15
+ Object.defineProperty(graph, elkjs0111ResultPolicy, { value: policy });
16
+ return graph;
17
+ }
@@ -0,0 +1,126 @@
1
+ import type { Graph, VisualGraph } from "@statelyai/graph";
2
+ import { boxAlgorithm, elkjs0111BoxAlgorithm, type BoxLayoutOptions } from "../box";
3
+ import { fixedAlgorithm, type FixedLayoutOptions } from "../fixed";
4
+ import { layeredAlgorithm, type LayeredLayoutOptions } from "../layered";
5
+ import {
6
+ elkjs0111RectanglePackingAlgorithm,
7
+ rectanglePackingAlgorithm,
8
+ type RectanglePackingLayoutOptions,
9
+ } from "../packing";
10
+ import { elkjs0111RandomAlgorithm, randomAlgorithm, type RandomLayoutOptions } from "../random";
11
+ import {
12
+ sporeCompactionAlgorithm,
13
+ sporeOverlapRemovalAlgorithm,
14
+ type SporeLayoutOptions,
15
+ } from "../spore";
16
+ import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
17
+
18
+ export interface BuiltInLayoutOptionsById {
19
+ box: BoxLayoutOptions;
20
+ fixed: FixedLayoutOptions;
21
+ layered: LayeredLayoutOptions;
22
+ random: RandomLayoutOptions;
23
+ rectpacking: RectanglePackingLayoutOptions;
24
+ sporeCompaction: SporeLayoutOptions;
25
+ sporeOverlap: SporeLayoutOptions;
26
+ }
27
+
28
+ export type BuiltInLayoutAlgorithmId = keyof BuiltInLayoutOptionsById;
29
+
30
+ export const builtInLayoutAlgorithms = {
31
+ box: boxAlgorithm,
32
+ fixed: fixedAlgorithm,
33
+ layered: layeredAlgorithm,
34
+ random: randomAlgorithm,
35
+ rectpacking: rectanglePackingAlgorithm,
36
+ sporeCompaction: sporeCompactionAlgorithm,
37
+ sporeOverlap: sporeOverlapRemovalAlgorithm,
38
+ } satisfies {
39
+ [Id in BuiltInLayoutAlgorithmId]: LayoutAlgorithm<BuiltInLayoutOptionsById[Id]>;
40
+ };
41
+
42
+ const elkjs0111LayoutAlgorithms = {
43
+ ...builtInLayoutAlgorithms,
44
+ box: elkjs0111BoxAlgorithm,
45
+ random: elkjs0111RandomAlgorithm,
46
+ rectpacking: elkjs0111RectanglePackingAlgorithm,
47
+ } satisfies {
48
+ [Id in BuiltInLayoutAlgorithmId]: LayoutAlgorithm<BuiltInLayoutOptionsById[Id]>;
49
+ };
50
+
51
+ export function getBuiltInLayoutAlgorithm(id: string): LayoutAlgorithm<unknown> | undefined {
52
+ if (!Object.hasOwn(builtInLayoutAlgorithms, id)) return undefined;
53
+ return builtInLayoutAlgorithms[id as BuiltInLayoutAlgorithmId] as
54
+ | LayoutAlgorithm<unknown>
55
+ | undefined;
56
+ }
57
+
58
+ interface ExecuteLayoutAlgorithmRequest<N, E, G, P, O> {
59
+ algorithm: LayoutAlgorithm<O>;
60
+ graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>;
61
+ options: O;
62
+ context: LayoutExecutionContext;
63
+ }
64
+
65
+ /** Execute one algorithm through the shared engine seam. */
66
+ export function executeLayoutAlgorithm<N, E, G, P, O>({
67
+ algorithm,
68
+ graph,
69
+ options,
70
+ context,
71
+ }: ExecuteLayoutAlgorithmRequest<N, E, G, P, O>):
72
+ | VisualGraph<N, E, G, P>
73
+ | Promise<VisualGraph<N, E, G, P>> {
74
+ return algorithm.layout(graph, options, context);
75
+ }
76
+
77
+ function createDirectExecutionContext(): LayoutExecutionContext {
78
+ return {
79
+ scope: { mode: "full" },
80
+ diagnostics: [],
81
+ measurePhase(_id, run) {
82
+ return run();
83
+ },
84
+ throwIfAborted() {},
85
+ };
86
+ }
87
+
88
+ interface ExecuteBuiltInLayoutRequest<N, E, G, P, Id extends BuiltInLayoutAlgorithmId> {
89
+ algorithm: Id;
90
+ graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>;
91
+ options: BuiltInLayoutOptionsById[Id];
92
+ }
93
+
94
+ /** Execute a built-in algorithm through the shared engine seam. */
95
+ export function executeBuiltInLayout<N, E, G, P, Id extends BuiltInLayoutAlgorithmId>({
96
+ algorithm,
97
+ graph,
98
+ options,
99
+ }: ExecuteBuiltInLayoutRequest<N, E, G, P, Id>):
100
+ | VisualGraph<N, E, G, P>
101
+ | Promise<VisualGraph<N, E, G, P>> {
102
+ return executeLayoutAlgorithm({
103
+ algorithm: builtInLayoutAlgorithms[algorithm] as LayoutAlgorithm<BuiltInLayoutOptionsById[Id]>,
104
+ graph,
105
+ options,
106
+ context: createDirectExecutionContext(),
107
+ });
108
+ }
109
+
110
+ /** Execute through the pinned elkjs 0.11.1 compatibility policy. */
111
+ export function executeElkjs0111Layout<N, E, G, P, Id extends BuiltInLayoutAlgorithmId>({
112
+ algorithm,
113
+ graph,
114
+ options,
115
+ }: ExecuteBuiltInLayoutRequest<N, E, G, P, Id>):
116
+ | VisualGraph<N, E, G, P>
117
+ | Promise<VisualGraph<N, E, G, P>> {
118
+ return executeLayoutAlgorithm({
119
+ algorithm: elkjs0111LayoutAlgorithms[algorithm] as LayoutAlgorithm<
120
+ BuiltInLayoutOptionsById[Id]
121
+ >,
122
+ graph,
123
+ options,
124
+ context: createDirectExecutionContext(),
125
+ });
126
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Graph, GraphEdge, GraphNode, Point, VisualGraph, VisualNode } from "@statelyai/graph";
2
+ import { labelReferences, runPartialLayout } from "../authoring/partial";
2
3
  import { UnsupportedLayoutError } from "../errors";
3
4
  import type { LayoutAlgorithm, LayoutExecutionContext } from "../types";
4
5
  import { separateExteriorLabels } from "./separate-exterior-labels";
@@ -2679,12 +2680,39 @@ export const layeredAlgorithm: LayoutAlgorithm<LayeredLayoutOptions> = {
2679
2680
  capabilities: {
2680
2681
  full: true,
2681
2682
  incremental: false,
2682
- partial: false,
2683
- routeOnly: false,
2683
+ partial: true,
2684
+ routeOnly: true,
2685
+ constraints: true,
2684
2686
  hierarchy: true,
2685
2687
  ports: true,
2686
2688
  },
2687
2689
  layout(graph, options, context) {
2688
- return runLayeredPipeline(graph, options ?? {}, context);
2690
+ if (context.scope.mode === "partial" || context.scope.mode === "route-only") {
2691
+ return context.measurePhase("partial-layout", () =>
2692
+ runPartialLayout(graph, options ?? {}, context, getLayeredLayout),
2693
+ );
2694
+ }
2695
+ if (context.constraints?.length && graph.nodes.some((node) => node.parentId != null))
2696
+ throw new UnsupportedLayoutError(
2697
+ "Full layout with geometry constraints does not support containers; use partial leaf constraints or full layout without geometry constraints",
2698
+ );
2699
+ const result = runLayeredPipeline(graph, options ?? {}, context);
2700
+ if (!context.constraints?.length) return result;
2701
+ return context.measurePhase("constraints", () =>
2702
+ runPartialLayout(
2703
+ result,
2704
+ options ?? {},
2705
+ {
2706
+ ...context,
2707
+ scope: {
2708
+ mode: "partial",
2709
+ nodeIds: result.nodes.map((n) => n.id),
2710
+ edgeIds: [...labelReferences(context.constraints ?? [])],
2711
+ },
2712
+ },
2713
+ getLayeredLayout,
2714
+ "constraints",
2715
+ ),
2716
+ );
2689
2717
  },
2690
2718
  };
package/src/layout.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { getGraphIssues, type Graph, type GraphPatch, type VisualGraph } from "@statelyai/graph";
2
2
  import { LayoutError, UnsupportedLayoutError } from "./errors";
3
- import { boxAlgorithm } from "./box";
4
- import { fixedAlgorithm } from "./fixed";
5
- import { layeredAlgorithm } from "./layered";
6
- import { rectanglePackingAlgorithm } from "./packing";
7
- import { randomAlgorithm } from "./random";
8
- import { sporeCompactionAlgorithm, sporeOverlapRemovalAlgorithm } from "./spore";
3
+ import {
4
+ builtInLayoutAlgorithms,
5
+ executeLayoutAlgorithm,
6
+ getBuiltInLayoutAlgorithm,
7
+ } from "./internal/layout-engine";
9
8
  import type {
10
9
  LayoutAlgorithm,
11
10
  LayoutDiagnostic,
@@ -16,15 +15,12 @@ import type {
16
15
  LayoutScope,
17
16
  } from "./types";
18
17
 
19
- const algorithms = new Map<string, LayoutAlgorithm<never>>([
20
- ["box", boxAlgorithm as LayoutAlgorithm<never>],
21
- ["layered", layeredAlgorithm as LayoutAlgorithm<never>],
22
- ["fixed", fixedAlgorithm as LayoutAlgorithm<never>],
23
- ["rectpacking", rectanglePackingAlgorithm as LayoutAlgorithm<never>],
24
- ["random", randomAlgorithm as LayoutAlgorithm<never>],
25
- ["sporeCompaction", sporeCompactionAlgorithm as LayoutAlgorithm<never>],
26
- ["sporeOverlap", sporeOverlapRemovalAlgorithm as LayoutAlgorithm<never>],
27
- ]);
18
+ const algorithms = new Map<string, LayoutAlgorithm<never>>(
19
+ Object.entries(builtInLayoutAlgorithms).map(([id, algorithm]) => [
20
+ id,
21
+ algorithm as LayoutAlgorithm<never>,
22
+ ]),
23
+ );
28
24
 
29
25
  function supportsScope(algorithm: LayoutAlgorithm<unknown>, scope: LayoutScope): boolean {
30
26
  switch (scope.mode) {
@@ -73,11 +69,11 @@ function getLayoutPatches<N, E, G, P>(
73
69
  op: "updateNode",
74
70
  id: node.id,
75
71
  data: {
76
- x: next.x,
77
- y: next.y,
78
- width: next.width,
79
- height: next.height,
80
- ...(next.ports === undefined ? {} : { ports: next.ports }),
72
+ ...(node.x === next.x ? {} : { x: next.x ?? null }),
73
+ ...(node.y === next.y ? {} : { y: next.y ?? null }),
74
+ ...(node.width === next.width ? {} : { width: next.width ?? null }),
75
+ ...(node.height === next.height ? {} : { height: next.height ?? null }),
76
+ ...(node.ports === next.ports ? {} : { ports: next.ports ?? null }),
81
77
  },
82
78
  description: "Apply layout geometry",
83
79
  });
@@ -98,12 +94,12 @@ function getLayoutPatches<N, E, G, P>(
98
94
  op: "updateEdge",
99
95
  id: edge.id,
100
96
  data: {
101
- x: next.x,
102
- y: next.y,
103
- width: next.width,
104
- height: next.height,
105
- ...(next.routing === undefined ? {} : { routing: next.routing }),
106
- ...(next.points === undefined ? {} : { points: next.points }),
97
+ ...(edge.x === next.x ? {} : { x: next.x ?? null }),
98
+ ...(edge.y === next.y ? {} : { y: next.y ?? null }),
99
+ ...(edge.width === next.width ? {} : { width: next.width ?? null }),
100
+ ...(edge.height === next.height ? {} : { height: next.height ?? null }),
101
+ ...(edge.routing === next.routing ? {} : { routing: next.routing ?? null }),
102
+ ...(samePoints(edge.points, next.points) ? {} : { points: next.points ?? null }),
107
103
  },
108
104
  description: "Apply layout geometry",
109
105
  });
@@ -123,7 +119,9 @@ export function registerLayoutAlgorithm<O>(algorithm: LayoutAlgorithm<O>): () =>
123
119
  }
124
120
 
125
121
  export function getLayoutAlgorithm(id: string): LayoutAlgorithm<unknown> | undefined {
126
- return algorithms.get(id) as LayoutAlgorithm<unknown> | undefined;
122
+ return (
123
+ (algorithms.get(id) as LayoutAlgorithm<unknown> | undefined) ?? getBuiltInLayoutAlgorithm(id)
124
+ );
127
125
  }
128
126
 
129
127
  /**
@@ -152,6 +150,9 @@ export async function getLayout<N, E, G, P, O = unknown>(
152
150
  throw new UnsupportedLayoutError(`${algorithm.id} does not support ${scope.mode} layout yet`);
153
151
  }
154
152
 
153
+ if (request.constraints?.length && !algorithm.capabilities.constraints) {
154
+ throw new UnsupportedLayoutError(`${algorithm.id} does not support geometry constraints`);
155
+ }
155
156
  const issues = getGraphIssues(request.graph as Graph);
156
157
  if (issues.length > 0) {
157
158
  throw new LayoutError(issues.map((issue) => issue.message).join("; "), "INVALID_GRAPH");
@@ -159,6 +160,7 @@ export async function getLayout<N, E, G, P, O = unknown>(
159
160
 
160
161
  const context: LayoutExecutionContext = {
161
162
  scope,
163
+ constraints: request.constraints,
162
164
  diagnostics,
163
165
  ...(request.signal === undefined ? {} : { signal: request.signal }),
164
166
  measurePhase(id, run) {
@@ -185,7 +187,12 @@ export async function getLayout<N, E, G, P, O = unknown>(
185
187
  },
186
188
  };
187
189
  context.throwIfAborted();
188
- const graph = await algorithm.layout(request.graph, request.options as O, context);
190
+ const graph = await executeLayoutAlgorithm({
191
+ algorithm: algorithm as LayoutAlgorithm<O>,
192
+ graph: request.graph,
193
+ options: request.options as O,
194
+ context,
195
+ });
189
196
  context.throwIfAborted();
190
197
 
191
198
  return {
package/src/packing.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Graph, VisualGraph, VisualNode } from "@statelyai/graph";
2
2
  import { getNodeSize, type LayoutOptions } from "@statelyai/graph/layout";
3
3
  import { getFixedLayout } from "./fixed";
4
+ import { setElkjs0111ResultPolicy } from "./internal/elkjs-compatibility";
4
5
  import type { LayoutPadding } from "./layered";
5
6
  import type { LayoutAlgorithm } from "./types";
6
7
 
@@ -72,3 +73,24 @@ export const rectanglePackingAlgorithm: LayoutAlgorithm<RectanglePackingLayoutOp
72
73
  return getRectanglePackingLayout(graph, options ?? {});
73
74
  },
74
75
  };
76
+
77
+ /** Pinned output policy used only by the elkjs 0.11.1 compatibility adapter. */
78
+ export const elkjs0111RectanglePackingAlgorithm: LayoutAlgorithm<RectanglePackingLayoutOptions> = {
79
+ ...rectanglePackingAlgorithm,
80
+ layout(graph, options = {}) {
81
+ const result = getRectanglePackingLayout(graph, options);
82
+ const padding = getPadding(options.padding);
83
+ return setElkjs0111ResultPolicy(result, {
84
+ providerBounds:
85
+ result.nodes.length === 0
86
+ ? { width: 0, height: 0 }
87
+ : {
88
+ width: Math.max(...result.nodes.map((node) => node.x + node.width)) + padding.right,
89
+ height:
90
+ Math.max(...result.nodes.map((node) => node.y + node.height)) + padding.bottom,
91
+ },
92
+ preserveEdgeSections: true,
93
+ skipBoundsNormalization: true,
94
+ });
95
+ },
96
+ };
package/src/random.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { Graph, Point, VisualGraph, VisualNode } from "@statelyai/graph";
9
9
  import { getNodeSize, type LayoutOptions } from "@statelyai/graph/layout";
10
10
  import { getFixedLayout } from "./fixed";
11
+ import { setElkjs0111ResultPolicy } from "./internal/elkjs-compatibility";
11
12
  import { JavaRandom } from "./java-random";
12
13
  import type { LayoutPadding } from "./layered";
13
14
  import type { LayoutAlgorithm } from "./types";
@@ -19,6 +20,21 @@ export interface RandomLayoutOptions extends Pick<LayoutOptions, "direction" | "
19
20
  seed?: number;
20
21
  }
21
22
 
23
+ interface RandomLayoutBehavior {
24
+ elkjs0111EdgeTargetBug: boolean;
25
+ preserveProviderBounds: boolean;
26
+ }
27
+
28
+ const nativeBehavior: RandomLayoutBehavior = {
29
+ elkjs0111EdgeTargetBug: false,
30
+ preserveProviderBounds: false,
31
+ };
32
+
33
+ const elkjs0111Behavior: RandomLayoutBehavior = {
34
+ elkjs0111EdgeTargetBug: true,
35
+ preserveProviderBounds: true,
36
+ };
37
+
22
38
  function getPadding(value: RandomLayoutOptions["padding"]): LayoutPadding {
23
39
  if (typeof value === "number") {
24
40
  return { top: value, right: value, bottom: value, left: value };
@@ -54,6 +70,14 @@ function borderPoint(source: VisualNode, target: VisualNode): Point {
54
70
  export function getRandomLayout<N, E, G, P>(
55
71
  graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
56
72
  options: RandomLayoutOptions = {},
73
+ ): VisualGraph<N, E, G, P> {
74
+ return getRandomLayoutWithBehavior(graph, options, nativeBehavior);
75
+ }
76
+
77
+ function getRandomLayoutWithBehavior<N, E, G, P>(
78
+ graph: Graph<N, E, G, P> | VisualGraph<N, E, G, P>,
79
+ options: RandomLayoutOptions,
80
+ behavior: RandomLayoutBehavior,
57
81
  ): VisualGraph<N, E, G, P> {
58
82
  if (graph.nodes.length === 0) return getFixedLayout(graph, options);
59
83
  const random = new JavaRandom(options.seed && options.seed !== 0 ? options.seed : Date.now());
@@ -82,10 +106,36 @@ export function getRandomLayout<N, E, G, P>(
82
106
  const nodeById = new Map(nodes.map((node) => [node.id, node]));
83
107
  const totalWidth = drawWidth + padding.left + padding.right;
84
108
  const totalHeight = drawHeight + padding.top + padding.bottom;
85
- const edges = graph.edges.map((edge) => {
109
+ const layoutEdge = (edge: (typeof graph.edges)[number]) => {
86
110
  const source = nodeById.get(edge.sourceId);
87
111
  const target = nodeById.get(edge.targetId);
88
112
  if (!source || !target) return edge;
113
+ if (behavior.elkjs0111EdgeTargetBug) {
114
+ const start = { x: source.x + source.width, y: source.y + source.height / 2 };
115
+ const end = { x: source.x + source.width / 2, y: source.y + source.height };
116
+ const bendCount = random.nextInt(5) + 1;
117
+ const xDifference = end.x - start.x;
118
+ const yDifference = end.y - start.y;
119
+ const maximumDeviation = Math.hypot(xDifference, yDifference) * Math.fround(0.2);
120
+ const xIncrement = xDifference / (bendCount + 1);
121
+ const yIncrement = yDifference / (bendCount + 1);
122
+ let x = start.x;
123
+ let y = start.y;
124
+ const points: Point[] = [start];
125
+ for (let index = 0; index < bendCount; index++) {
126
+ x += xIncrement;
127
+ y += yIncrement;
128
+ let randomX = x + random.nextFloat() * maximumDeviation - maximumDeviation / 2;
129
+ if (randomX < 0) randomX = 1;
130
+ else if (randomX > totalWidth) randomX = totalWidth - 1;
131
+ let randomY = y + random.nextFloat() * maximumDeviation - maximumDeviation / 2;
132
+ if (randomY < 0) randomY = 1;
133
+ else if (randomY > totalHeight) randomY = totalHeight - 1;
134
+ points.push({ x: randomX, y: randomY });
135
+ }
136
+ points.push(end);
137
+ return { ...edge, points, routing: "polyline" as const };
138
+ }
89
139
  const start = borderPoint(source, target);
90
140
  const end = borderPoint(target, source);
91
141
  const bendCount = random.nextInt(5) + (source === target ? 1 : 0);
@@ -119,11 +169,25 @@ export function getRandomLayout<N, E, G, P>(
119
169
  }
120
170
  points.push(end);
121
171
  return { ...edge, points, routing: "polyline" as const };
122
- });
123
- return getFixedLayout(
172
+ };
173
+ const edges = behavior.elkjs0111EdgeTargetBug
174
+ ? graph.nodes.flatMap((node) =>
175
+ graph.edges.filter((edge) => edge.sourceId === node.id).map(layoutEdge),
176
+ )
177
+ : graph.edges.map(layoutEdge);
178
+ const result = getFixedLayout(
124
179
  { ...graph, nodes, edges },
125
180
  { direction: options.direction ?? graph.direction },
126
181
  );
182
+ if (behavior.preserveProviderBounds) {
183
+ setElkjs0111ResultPolicy(result, {
184
+ providerBounds: {
185
+ width: drawWidth + 2 * (padding.left + padding.right),
186
+ height: drawHeight + 2 * (padding.top + padding.bottom),
187
+ },
188
+ });
189
+ }
190
+ return result;
127
191
  }
128
192
 
129
193
  export const randomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
@@ -140,3 +204,11 @@ export const randomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
140
204
  return getRandomLayout(graph, options ?? {});
141
205
  },
142
206
  };
207
+
208
+ /** Pinned behavior used only by the elkjs 0.11.1 compatibility adapter. */
209
+ export const elkjs0111RandomAlgorithm: LayoutAlgorithm<RandomLayoutOptions> = {
210
+ ...randomAlgorithm,
211
+ layout(graph, options) {
212
+ return getRandomLayoutWithBehavior(graph, options ?? {}, elkjs0111Behavior);
213
+ },
214
+ };
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Graph, GraphPatch, VisualGraph } from "@statelyai/graph";
2
+ import type { LayoutConstraint } from "./constraints";
2
3
 
3
4
  export type AnyGraph = Graph<unknown, unknown, unknown, unknown>;
4
5
 
@@ -12,8 +13,16 @@ export type LayoutScope =
12
13
  }
13
14
  | {
14
15
  mode: "partial";
15
- previous: VisualGraph;
16
- nodeIds: readonly string[];
16
+ /** Missing current geometry falls back to these matching entity IDs. */
17
+ previous?: VisualGraph;
18
+ /** Position permission only. Omitted or empty selects no nodes. */
19
+ nodeIds?: readonly string[];
20
+ /** Edge selection never grants permission to move endpoints. */
21
+ edgeIds?: readonly string[];
22
+ /** Defaults to affected; automatic changes obey edgeGeometry. */
23
+ routing?: "selected" | "affected";
24
+ /** Defaults to both; dimensions remain fixed. */
25
+ edgeGeometry?: "routes" | "labels" | "both";
17
26
  }
18
27
  | {
19
28
  mode: "route-only";
@@ -27,6 +36,8 @@ export interface LayoutDiagnostic {
27
36
  message: string;
28
37
  entityIds?: readonly string[];
29
38
  phase?: string;
39
+ constraintIds?: readonly string[];
40
+ geometry?: "node" | "routes" | "labels";
30
41
  }
31
42
 
32
43
  export interface LayoutPhaseMetrics {
@@ -42,6 +53,8 @@ export interface LayoutMetrics {
42
53
  }
43
54
 
44
55
  export interface LayoutCapabilities {
56
+ /** Common geometry constraints; absent means unsupported. */
57
+ constraints?: boolean;
45
58
  full: boolean;
46
59
  incremental: boolean;
47
60
  partial: boolean;
@@ -62,6 +75,7 @@ export interface LayoutAlgorithm<Options = unknown> {
62
75
 
63
76
  export interface LayoutExecutionContext {
64
77
  readonly scope: LayoutScope;
78
+ readonly constraints?: readonly LayoutConstraint[];
65
79
  readonly signal?: AbortSignal;
66
80
  readonly diagnostics: LayoutDiagnostic[];
67
81
  measurePhase<T>(id: string, run: () => T): T;
@@ -73,6 +87,7 @@ export interface LayoutRequest<N = unknown, E = unknown, G = unknown, P = unknow
73
87
  algorithm?: string | LayoutAlgorithm<O>;
74
88
  options?: O;
75
89
  scope?: LayoutScope;
90
+ constraints?: readonly LayoutConstraint[];
76
91
  signal?: AbortSignal;
77
92
  }
78
93