@statelyai/layout 0.1.3 → 0.3.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 +153 -6
- package/dist/elkjs/index.cjs +9 -0
- package/dist/elkjs/index.d.cts +155 -0
- package/dist/elkjs/index.d.mts +51 -39
- package/dist/elkjs/index.mjs +3 -1484
- 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-DaHMISnQ.mjs +1677 -0
- package/dist/elkjs-dMghiiWW.cjs +1706 -0
- package/dist/index-BEA_pzeT.d.cts +72 -0
- package/dist/{index-8xYkohbz.d.mts → index-D9dhxrRl.d.cts} +111 -5
- package/dist/index-DNTLRsDl.d.mts +72 -0
- package/dist/index-DNixtE3h.d.mts +1318 -0
- package/dist/index.cjs +215 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +25 -25
- package/dist/layered/index.cjs +38 -0
- package/dist/layered/index.d.cts +3 -0
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +1 -1
- package/dist/layered-B2buJqMf.cjs +12009 -0
- package/dist/{layered-CoeH9LRO.mjs → layered-DGFTeQSl.mjs} +2323 -149
- package/dist/{spore-BtTSZbb6.mjs → layout-engine-Cs5tatA6.mjs} +159 -5
- package/dist/layout-engine-Dn1PiFwP.cjs +712 -0
- package/dist/public-types-9NCPoqbp.d.mts +109 -0
- package/dist/public-types-BcRZoJE3.d.cts +109 -0
- package/dist/routing/index.cjs +28 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-A2hrK9MP.mjs +2396 -0
- package/dist/routing-ujEP7Qh6.cjs +2606 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/worker-client-BNjwT1AJ.mjs +92 -0
- package/dist/worker-client-BwGGRD5y.d.mts +19 -0
- package/dist/worker-client-D4oEvhAl.cjs +98 -0
- package/dist/worker-client-D4zNyPEI.d.cts +19 -0
- package/package.json +45 -10
- package/src/authoring/coordinates.ts +114 -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 +104 -39
- package/src/elkjs/main.ts +19 -0
- package/src/elkjs/public-types.ts +125 -0
- package/src/elkjs/routes.ts +147 -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 +19 -0
- package/src/internal/elkjs-compatibility.ts +17 -0
- package/src/internal/layout-engine.ts +126 -0
- package/src/layered/compound.ts +417 -0
- package/src/layered/index.ts +58 -105
- package/src/layered/types.ts +21 -0
- package/src/layout.ts +35 -28
- package/src/packing.ts +22 -0
- package/src/random.ts +75 -3
- package/src/routing/adapters.ts +111 -0
- package/src/routing/algorithms.ts +676 -0
- package/src/routing/coordination.ts +86 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +68 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +276 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- package/src/types.ts +17 -2
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
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
/** Compatibility for single-section native consumers. Refuses to erase gaps or topology. */
|
|
92
|
+
export function routeToGraphPatch(
|
|
93
|
+
route: Route,
|
|
94
|
+
options: { readonly tolerance?: number; readonly offset?: RoutePoint } = {},
|
|
95
|
+
): GraphPatch {
|
|
96
|
+
if (
|
|
97
|
+
route.sections.length !== 1 ||
|
|
98
|
+
route.sections[0]!.sharedId ||
|
|
99
|
+
route.sections[0]!.from.kind !== "node" ||
|
|
100
|
+
route.sections[0]!.to.kind !== "node"
|
|
101
|
+
)
|
|
102
|
+
throw new Error(
|
|
103
|
+
"This route requires section-aware rendering; a single GraphEdge.points array would lose topology",
|
|
104
|
+
);
|
|
105
|
+
const offset = options.offset ?? { x: 0, y: 0 };
|
|
106
|
+
const points: Point[] = flattenPath(route.sections[0]!.path, options).map((p) => ({
|
|
107
|
+
x: p.x - offset.x,
|
|
108
|
+
y: p.y - offset.y,
|
|
109
|
+
}));
|
|
110
|
+
return { op: "updateEdge", id: route.edgeId, data: { points, routing: "polyline" } };
|
|
111
|
+
}
|