@statelyai/layout 0.1.2 → 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-COIyXvwb.mjs → layered-Bz2nWmVp.mjs} +2298 -45
- package/dist/layered-Cf9Rn8l7.cjs +12061 -0
- package/dist/layout-engine-BNex9jxg.cjs +712 -0
- package/dist/{spore-BeKUA1Mb.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/layered/strategies.ts +68 -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
|
};
|
|
@@ -3645,7 +3645,57 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3645
3645
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
3646
3646
|
}
|
|
3647
3647
|
|
|
3648
|
+
const increasing = input.direction === "right" || input.direction === "down";
|
|
3649
|
+
const flowPortProtrusion = (edge: GraphEdge, endpoint: "source" | "target"): number => {
|
|
3650
|
+
const node = nodeById.get(endpoint === "source" ? edge.sourceId : edge.targetId);
|
|
3651
|
+
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
3652
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3653
|
+
if (!node || !port) return 0;
|
|
3654
|
+
const side = input.portSettings?.(port, node)?.["port.side"];
|
|
3655
|
+
const flowSide = horizontal
|
|
3656
|
+
? endpoint === "source"
|
|
3657
|
+
? input.direction === "right"
|
|
3658
|
+
? "EAST"
|
|
3659
|
+
: "WEST"
|
|
3660
|
+
: input.direction === "right"
|
|
3661
|
+
? "WEST"
|
|
3662
|
+
: "EAST"
|
|
3663
|
+
: endpoint === "source"
|
|
3664
|
+
? input.direction === "down"
|
|
3665
|
+
? "SOUTH"
|
|
3666
|
+
: "NORTH"
|
|
3667
|
+
: input.direction === "down"
|
|
3668
|
+
? "NORTH"
|
|
3669
|
+
: "SOUTH";
|
|
3670
|
+
return side === flowSide ? (horizontal ? (port.width ?? 0) : (port.height ?? 0)) : 0;
|
|
3671
|
+
};
|
|
3672
|
+
const adjacentNodeIds = new Map<string, Set<string>>();
|
|
3673
|
+
for (const edge of input.graph.edges) {
|
|
3674
|
+
const sourceNeighbors = adjacentNodeIds.get(edge.sourceId) ?? new Set<string>();
|
|
3675
|
+
sourceNeighbors.add(edge.targetId);
|
|
3676
|
+
adjacentNodeIds.set(edge.sourceId, sourceNeighbors);
|
|
3677
|
+
const targetNeighbors = adjacentNodeIds.get(edge.targetId) ?? new Set<string>();
|
|
3678
|
+
targetNeighbors.add(edge.sourceId);
|
|
3679
|
+
adjacentNodeIds.set(edge.targetId, targetNeighbors);
|
|
3680
|
+
}
|
|
3681
|
+
const feedbackComponentNodeIds = new Set<string>();
|
|
3682
|
+
for (const edge of input.graph.edges) {
|
|
3683
|
+
const sourceLayer = flowLayerByNodeId.get(edge.sourceId);
|
|
3684
|
+
const targetLayer = flowLayerByNodeId.get(edge.targetId);
|
|
3685
|
+
if (sourceLayer === undefined || targetLayer === undefined) continue;
|
|
3686
|
+
if (sourceLayer === targetLayer) continue;
|
|
3687
|
+
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
3688
|
+
if (forward) continue;
|
|
3689
|
+
const pending = [edge.sourceId, edge.targetId];
|
|
3690
|
+
while (pending.length > 0) {
|
|
3691
|
+
const nodeId = pending.pop()!;
|
|
3692
|
+
if (feedbackComponentNodeIds.has(nodeId)) continue;
|
|
3693
|
+
feedbackComponentNodeIds.add(nodeId);
|
|
3694
|
+
pending.push(...(adjacentNodeIds.get(nodeId) ?? []));
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3648
3697
|
const labelExtraByGap = flowLayers.slice(0, -1).map(() => 0);
|
|
3698
|
+
const compactLabelCorridorGaps = new Set<number>();
|
|
3649
3699
|
for (const edge of input.graph.edges) {
|
|
3650
3700
|
const labelFlowSize = horizontal ? (edge.width ?? 0) : (edge.height ?? 0);
|
|
3651
3701
|
if (labelFlowSize <= 0) continue;
|
|
@@ -3654,11 +3704,25 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
3654
3704
|
if (sourceLayer === undefined || targetLayer === undefined) continue;
|
|
3655
3705
|
if (Math.abs(sourceLayer - targetLayer) !== 1) continue;
|
|
3656
3706
|
const placement = input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER";
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3707
|
+
const edgeNodeSpacing = Number(input.settings["spacing.edgeNodeBetweenLayers"] ?? 10);
|
|
3708
|
+
// ELK models center labels as dummy layers. For an exterior port, using node-node
|
|
3709
|
+
// spacing beyond the port double-counts the corridor. Treat the label as edge
|
|
3710
|
+
// geometry while retaining normal layer spacing when it is the larger clearance.
|
|
3711
|
+
const requiredGap =
|
|
3712
|
+
labelFlowSize +
|
|
3713
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "source") + edgeNodeSpacing) +
|
|
3714
|
+
Math.max(input.spacing.layer, flowPortProtrusion(edge, "target") + edgeNodeSpacing);
|
|
3715
|
+
const gap = Math.min(sourceLayer, targetLayer);
|
|
3716
|
+
const availableGap = flowLayers[gap + 1]!.start - flowLayers[gap]!.end;
|
|
3717
|
+
const forward = increasing ? targetLayer > sourceLayer : targetLayer < sourceLayer;
|
|
3718
|
+
const compactCorridor =
|
|
3719
|
+
placement === "CENTER" && forward && !feedbackComponentNodeIds.has(edge.sourceId);
|
|
3720
|
+
if (compactCorridor) compactLabelCorridorGaps.add(gap);
|
|
3721
|
+
const extra = compactCorridor
|
|
3722
|
+
? Math.max(0, requiredGap - availableGap)
|
|
3723
|
+
: placement === "CENTER"
|
|
3659
3724
|
? labelFlowSize + input.spacing.layer
|
|
3660
3725
|
: labelFlowSize + Number(input.settings["spacing.edgeLabel"] ?? 2);
|
|
3661
|
-
const gap = Math.min(sourceLayer, targetLayer);
|
|
3662
3726
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
3663
3727
|
}
|
|
3664
3728
|
let labelShift = 0;
|
|
@@ -4049,6 +4113,7 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4049
4113
|
: Math.max(
|
|
4050
4114
|
preservesNodeFlexibilityGap ||
|
|
4051
4115
|
(labelExtraByGap[layerNo] ?? 0) > 0 ||
|
|
4116
|
+
compactLabelCorridorGaps.has(layerNo) ||
|
|
4052
4117
|
[...selfLoopsByNodeId].some(
|
|
4053
4118
|
([id, loops]) =>
|
|
4054
4119
|
(flowLayerByNodeId.get(id) === layerNo ||
|
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
|
|