@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.
- package/NOTICE.md +43 -0
- package/README.md +82 -4
- package/dist/elkjs/index.cjs +8 -0
- package/dist/elkjs/index.d.cts +146 -0
- package/dist/elkjs/index.d.mts +42 -39
- package/dist/elkjs/index.mjs +2 -1483
- package/dist/elkjs/main.cjs +17 -0
- package/dist/elkjs/main.d.cts +10 -0
- package/dist/elkjs/main.d.mts +11 -0
- package/dist/elkjs/main.mjs +17 -0
- package/dist/elkjs/worker-api.cjs +3 -0
- package/dist/elkjs/worker-api.d.cts +3 -0
- package/dist/elkjs/worker-api.d.mts +3 -0
- package/dist/elkjs/worker-api.mjs +3 -0
- package/dist/elkjs/worker.cjs +86 -0
- package/dist/elkjs/worker.d.cts +17 -0
- package/dist/elkjs/worker.d.mts +17 -0
- package/dist/elkjs/worker.mjs +86 -0
- package/dist/elkjs-BVnDwnBQ.cjs +1551 -0
- package/dist/elkjs-CFlfkEBz.mjs +1528 -0
- package/dist/{index-8xYkohbz.d.mts → index-BSH2bloA.d.mts} +77 -3
- package/dist/index-CG712jzV.d.cts +1286 -0
- package/dist/index.cjs +188 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +24 -25
- package/dist/layered/index.cjs +38 -0
- package/dist/layered/index.d.cts +2 -0
- package/dist/layered/index.d.mts +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-CoeH9LRO.mjs → layered-Bz2nWmVp.mjs} +2261 -47
- package/dist/layered-Cf9Rn8l7.cjs +12061 -0
- package/dist/layout-engine-BNex9jxg.cjs +712 -0
- package/dist/{spore-BtTSZbb6.mjs → layout-engine-WbC0YjjT.mjs} +159 -5
- package/dist/public-types-B8rAALJq.d.mts +109 -0
- package/dist/public-types-CVB0hXGk.d.cts +109 -0
- package/dist/worker-client-D3HMK33r.cjs +98 -0
- package/dist/worker-client-DAT_z2OR.d.mts +19 -0
- package/dist/worker-client-DVhklF6A.d.cts +19 -0
- package/dist/worker-client-DkwS4sHx.mjs +92 -0
- package/package.json +39 -9
- package/src/authoring/coordinates.ts +106 -0
- package/src/authoring/partial.ts +584 -0
- package/src/authoring/routing.ts +211 -0
- package/src/box.ts +22 -0
- package/src/constraints.ts +222 -0
- package/src/elkjs/index.ts +100 -37
- package/src/elkjs/main.ts +19 -0
- package/src/elkjs/public-types.ts +125 -0
- package/src/elkjs/worker-api.ts +29 -0
- package/src/elkjs/worker-client.ts +138 -0
- package/src/elkjs/worker.ts +117 -0
- package/src/index.ts +13 -0
- package/src/internal/elkjs-compatibility.ts +17 -0
- package/src/internal/layout-engine.ts +126 -0
- package/src/layered/index.ts +31 -3
- package/src/layout.ts +35 -28
- package/src/packing.ts +22 -0
- package/src/random.ts +75 -3
- 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
|
+
}
|
package/src/layered/index.ts
CHANGED
|
@@ -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:
|
|
2683
|
-
routeOnly:
|
|
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
|
-
|
|
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 {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
[
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
106
|
-
...(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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|