@statelyai/layout 0.0.0 → 0.0.1

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.
@@ -1,439 +0,0 @@
1
- //#region src/errors.ts
2
- var LayoutError = class extends Error {
3
- constructor(message, code) {
4
- super(message);
5
- this.code = code;
6
- this.name = "LayoutError";
7
- }
8
- };
9
- var UnsupportedLayoutError = class extends LayoutError {
10
- constructor(message) {
11
- super(message, "UNSUPPORTED_LAYOUT");
12
- this.name = "UnsupportedLayoutError";
13
- }
14
- };
15
-
16
- //#endregion
17
- //#region src/layered/strategies.ts
18
- function getOrientedEndpoints(edge, orientation) {
19
- return orientation.reversedEdgeIds.has(edge.id) ? [edge.targetId, edge.sourceId] : [edge.sourceId, edge.targetId];
20
- }
21
- const breakCyclesWithDepthFirstSearch = (input) => {
22
- const outgoing = /* @__PURE__ */ new Map();
23
- for (const node of input.graph.nodes) outgoing.set(node.id, []);
24
- for (const edge of input.graph.edges) outgoing.get(edge.sourceId)?.push(edge);
25
- const state = /* @__PURE__ */ new Map();
26
- const reversedEdgeIds = /* @__PURE__ */ new Set();
27
- for (const node of input.graph.nodes) {
28
- if (state.get(node.id) !== void 0) continue;
29
- state.set(node.id, "active");
30
- const stack = [{
31
- nodeId: node.id,
32
- edgeIndex: 0
33
- }];
34
- while (stack.length > 0) {
35
- const frame = stack.at(-1);
36
- if (!frame) break;
37
- const edge = (outgoing.get(frame.nodeId) ?? [])[frame.edgeIndex];
38
- if (!edge) {
39
- state.set(frame.nodeId, "done");
40
- stack.pop();
41
- continue;
42
- }
43
- frame.edgeIndex++;
44
- if (edge.sourceId === edge.targetId) continue;
45
- const targetState = state.get(edge.targetId);
46
- if (targetState === "active") reversedEdgeIds.add(edge.id);
47
- else if (targetState === void 0) {
48
- state.set(edge.targetId, "active");
49
- stack.push({
50
- nodeId: edge.targetId,
51
- edgeIndex: 0
52
- });
53
- }
54
- }
55
- }
56
- return { reversedEdgeIds };
57
- };
58
- const assignLayersByLongestPath = (input, orientation) => {
59
- const indegree = /* @__PURE__ */ new Map();
60
- const successors = /* @__PURE__ */ new Map();
61
- const layerByNodeId = /* @__PURE__ */ new Map();
62
- for (const node of input.graph.nodes) {
63
- indegree.set(node.id, 0);
64
- successors.set(node.id, []);
65
- layerByNodeId.set(node.id, input.constrainedLayerByNodeId.get(node.id) ?? 0);
66
- }
67
- for (const edge of input.graph.edges) {
68
- const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
69
- if (sourceId === targetId) continue;
70
- const sourceConstraint = input.constrainedLayerByNodeId.get(sourceId);
71
- const targetConstraint = input.constrainedLayerByNodeId.get(targetId);
72
- if (sourceConstraint !== void 0 && targetConstraint !== void 0 && targetConstraint <= sourceConstraint) throw new LayoutError(`Layer constraints conflict on edge ${edge.id}`, "UNSATISFIABLE_CONSTRAINTS");
73
- successors.get(sourceId)?.push(targetId);
74
- indegree.set(targetId, (indegree.get(targetId) ?? 0) + 1);
75
- }
76
- const queue = input.graph.nodes.filter((node) => indegree.get(node.id) === 0).map((node) => node.id);
77
- for (let index = 0; index < queue.length; index++) {
78
- const sourceId = queue[index];
79
- if (sourceId === void 0) continue;
80
- const sourceLayer = layerByNodeId.get(sourceId) ?? 0;
81
- for (const targetId of successors.get(sourceId) ?? []) {
82
- const targetConstraint = input.constrainedLayerByNodeId.get(targetId);
83
- if (targetConstraint !== void 0 && targetConstraint < sourceLayer + 1) throw new LayoutError(`Layer constraint conflicts at node ${targetId}`, "UNSATISFIABLE_CONSTRAINTS");
84
- layerByNodeId.set(targetId, targetConstraint ?? Math.max(layerByNodeId.get(targetId) ?? 0, sourceLayer + 1));
85
- const nextIndegree = (indegree.get(targetId) ?? 1) - 1;
86
- indegree.set(targetId, nextIndegree);
87
- if (nextIndegree === 0) queue.push(targetId);
88
- }
89
- }
90
- return { layerByNodeId };
91
- };
92
- function sortLayerByBarycenter(layer, adjacentLayer, neighbors) {
93
- const adjacentIndex = new Map(adjacentLayer.map((nodeId, index) => [nodeId, index]));
94
- const originalIndex = new Map(layer.map((nodeId, index) => [nodeId, index]));
95
- layer.sort((a, b) => {
96
- const barycenter = (nodeId) => {
97
- const positions = (neighbors.get(nodeId) ?? []).map((id) => adjacentIndex.get(id)).filter((value) => value !== void 0);
98
- if (positions.length === 0) return originalIndex.get(nodeId) ?? 0;
99
- return positions.reduce((sum, value) => sum + value, 0) / positions.length;
100
- };
101
- return barycenter(a) - barycenter(b) || (originalIndex.get(a) ?? 0) - (originalIndex.get(b) ?? 0);
102
- });
103
- }
104
- function minimizeCrossingsWithBarycenter(sweeps = 4) {
105
- return (input, orientation, assignment) => {
106
- let maximumLayer = 0;
107
- for (const layer of assignment.layerByNodeId.values()) maximumLayer = Math.max(maximumLayer, layer);
108
- const layerCount = maximumLayer + 1;
109
- const layers = Array.from({ length: layerCount }, () => []);
110
- for (const node of input.graph.nodes) layers[assignment.layerByNodeId.get(node.id) ?? 0]?.push(node.id);
111
- const predecessors = /* @__PURE__ */ new Map();
112
- const successors = /* @__PURE__ */ new Map();
113
- for (const node of input.graph.nodes) {
114
- predecessors.set(node.id, []);
115
- successors.set(node.id, []);
116
- }
117
- for (const edge of input.graph.edges) {
118
- const [sourceId, targetId] = getOrientedEndpoints(edge, orientation);
119
- if (sourceId === targetId) continue;
120
- successors.get(sourceId)?.push(targetId);
121
- predecessors.get(targetId)?.push(sourceId);
122
- }
123
- for (let sweep = 0; sweep < sweeps; sweep++) {
124
- for (let layer = 1; layer < layers.length; layer++) {
125
- const current = layers[layer];
126
- const previous = layers[layer - 1];
127
- if (current && previous) sortLayerByBarycenter(current, previous, predecessors);
128
- }
129
- for (let layer = layers.length - 2; layer >= 0; layer--) {
130
- const current = layers[layer];
131
- const next = layers[layer + 1];
132
- if (current && next) sortLayerByBarycenter(current, next, successors);
133
- }
134
- }
135
- return { layers };
136
- };
137
- }
138
- function sumWithSpacing(ids, input, axis) {
139
- return ids.reduce((total, id, index) => total + (input.sizes.get(id)?.[axis] ?? 0) + (index === 0 ? 0 : input.spacing.node), 0);
140
- }
141
- const placeNodesInLayers = (input, order) => {
142
- const horizontal = input.direction === "left" || input.direction === "right";
143
- const layerFlowSizes = order.layers.map((layer) => Math.max(0, ...layer.map((id) => horizontal ? input.sizes.get(id)?.width ?? 0 : input.sizes.get(id)?.height ?? 0)));
144
- const layerCrossSizes = order.layers.map((layer) => sumWithSpacing(layer, input, horizontal ? "height" : "width"));
145
- let maxCrossSize = 0;
146
- for (const size of layerCrossSizes) maxCrossSize = Math.max(maxCrossSize, size);
147
- const rectByNodeId = /* @__PURE__ */ new Map();
148
- let flow = horizontal ? input.padding.left : input.padding.top;
149
- order.layers.forEach((layer, layerIndex) => {
150
- let cross = (horizontal ? input.padding.top : input.padding.left) + (maxCrossSize - (layerCrossSizes[layerIndex] ?? 0)) / 2;
151
- for (const id of layer) {
152
- const size = input.sizes.get(id) ?? {
153
- width: 0,
154
- height: 0
155
- };
156
- const rect = horizontal ? {
157
- x: flow,
158
- y: cross,
159
- ...size
160
- } : {
161
- x: cross,
162
- y: flow,
163
- ...size
164
- };
165
- rectByNodeId.set(id, rect);
166
- cross += (horizontal ? size.height : size.width) + input.spacing.node;
167
- }
168
- flow += (layerFlowSizes[layerIndex] ?? 0) + input.spacing.layer;
169
- });
170
- if (input.direction === "up" || input.direction === "left") {
171
- let contentEnd = 0;
172
- for (const rect of rectByNodeId.values()) contentEnd = Math.max(contentEnd, horizontal ? rect.x + rect.width : rect.y + rect.height);
173
- const leadingPadding = horizontal ? input.padding.left : input.padding.top;
174
- for (const [id, rect] of rectByNodeId) rectByNodeId.set(id, horizontal ? {
175
- ...rect,
176
- x: contentEnd - rect.x + leadingPadding - rect.width
177
- } : {
178
- ...rect,
179
- y: contentEnd - rect.y + leadingPadding - rect.height
180
- });
181
- }
182
- return { rectByNodeId };
183
- };
184
- function getPortPoint(node, portName, rect, fallback, direction) {
185
- if (portName === void 0) return fallback;
186
- const port = placePorts(node.ports, rect, direction)?.find((candidate) => candidate.name === portName);
187
- if (port?.x === void 0 || port.y === void 0) return fallback;
188
- return {
189
- x: rect.x + port.x + (port.width ?? 0) / 2,
190
- y: rect.y + port.y + (port.height ?? 0) / 2
191
- };
192
- }
193
- function removeDuplicatePoints(points) {
194
- return points.filter((point, index) => index === 0 || point.x !== points[index - 1]?.x || point.y !== points[index - 1]?.y);
195
- }
196
- const routeEdgesOrthogonally = (input, _orientation, placement) => {
197
- const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
198
- const pointsByEdgeId = /* @__PURE__ */ new Map();
199
- const horizontal = input.direction === "left" || input.direction === "right";
200
- const reverse = input.direction === "up" || input.direction === "left";
201
- for (const edge of input.graph.edges) {
202
- const source = nodeById.get(edge.sourceId);
203
- const target = nodeById.get(edge.targetId);
204
- const sourceRect = placement.rectByNodeId.get(edge.sourceId);
205
- const targetRect = placement.rectByNodeId.get(edge.targetId);
206
- if (!source || !target || !sourceRect || !targetRect) continue;
207
- if (source.id === target.id) {
208
- const x = sourceRect.x + sourceRect.width;
209
- const y = sourceRect.y + sourceRect.height / 2;
210
- pointsByEdgeId.set(edge.id, [
211
- {
212
- x,
213
- y
214
- },
215
- {
216
- x: x + 24,
217
- y
218
- },
219
- {
220
- x: x + 24,
221
- y: y - 24
222
- },
223
- {
224
- x,
225
- y: y - 24
226
- }
227
- ]);
228
- continue;
229
- }
230
- const sourceFallback = horizontal ? {
231
- x: sourceRect.x + (reverse ? 0 : sourceRect.width),
232
- y: sourceRect.y + sourceRect.height / 2
233
- } : {
234
- x: sourceRect.x + sourceRect.width / 2,
235
- y: sourceRect.y + (reverse ? 0 : sourceRect.height)
236
- };
237
- const targetFallback = horizontal ? {
238
- x: targetRect.x + (reverse ? targetRect.width : 0),
239
- y: targetRect.y + targetRect.height / 2
240
- } : {
241
- x: targetRect.x + targetRect.width / 2,
242
- y: targetRect.y + (reverse ? targetRect.height : 0)
243
- };
244
- const start = getPortPoint(source, edge.sourcePort, sourceRect, sourceFallback, input.direction);
245
- const end = getPortPoint(target, edge.targetPort, targetRect, targetFallback, input.direction);
246
- const middle = horizontal ? [{
247
- x: (start.x + end.x) / 2,
248
- y: start.y
249
- }, {
250
- x: (start.x + end.x) / 2,
251
- y: end.y
252
- }] : [{
253
- x: start.x,
254
- y: (start.y + end.y) / 2
255
- }, {
256
- x: end.x,
257
- y: (start.y + end.y) / 2
258
- }];
259
- pointsByEdgeId.set(edge.id, removeDuplicatePoints([
260
- start,
261
- ...middle,
262
- end
263
- ]));
264
- }
265
- return { pointsByEdgeId };
266
- };
267
- function placePorts(ports, rect, direction) {
268
- if (!ports) return void 0;
269
- const horizontal = direction === "left" || direction === "right";
270
- const reverse = direction === "up" || direction === "left";
271
- return ports.map((port, index) => {
272
- const size = {
273
- width: port.width ?? 8,
274
- height: port.height ?? 8
275
- };
276
- if (port.x !== void 0 && port.y !== void 0) return {
277
- ...port,
278
- ...size
279
- };
280
- const ratio = (index + 1) / (ports.length + 1);
281
- const outgoing = port.direction !== "in";
282
- const farSide = reverse ? !outgoing : outgoing;
283
- return {
284
- ...port,
285
- ...size,
286
- x: horizontal ? farSide ? rect.width - size.width / 2 : -size.width / 2 : ratio * rect.width - size.width / 2,
287
- y: horizontal ? ratio * rect.height - size.height / 2 : farSide ? rect.height - size.height / 2 : -size.height / 2
288
- };
289
- });
290
- }
291
- function getPolylineMidpoint(points) {
292
- if (points.length === 0) return {
293
- x: 0,
294
- y: 0
295
- };
296
- let total = 0;
297
- const lengths = [];
298
- for (let index = 1; index < points.length; index++) {
299
- const previous = points[index - 1];
300
- const point = points[index];
301
- if (!previous || !point) continue;
302
- const length = Math.hypot(point.x - previous.x, point.y - previous.y);
303
- lengths.push(length);
304
- total += length;
305
- }
306
- let remaining = total / 2;
307
- for (let index = 1; index < points.length; index++) {
308
- const previous = points[index - 1];
309
- const point = points[index];
310
- const length = lengths[index - 1] ?? 0;
311
- if (!previous || !point) continue;
312
- if (remaining <= length) {
313
- const ratio = length === 0 ? 0 : remaining / length;
314
- return {
315
- x: previous.x + (point.x - previous.x) * ratio,
316
- y: previous.y + (point.y - previous.y) * ratio
317
- };
318
- }
319
- remaining -= length;
320
- }
321
- return points.at(-1) ?? {
322
- x: 0,
323
- y: 0
324
- };
325
- }
326
-
327
- //#endregion
328
- //#region src/layered/index.ts
329
- const DEFAULT_NODE_SIZE = {
330
- width: 100,
331
- height: 50
332
- };
333
- function getNodeSize(node, options) {
334
- const measured = options.measure?.(node);
335
- if (measured) return measured;
336
- return {
337
- width: node.width !== void 0 && node.width > 0 ? node.width : DEFAULT_NODE_SIZE.width,
338
- height: node.height !== void 0 && node.height > 0 ? node.height : DEFAULT_NODE_SIZE.height
339
- };
340
- }
341
- function assertFlatGraph(graph) {
342
- const compoundNodeIds = graph.nodes.filter((node) => node.parentId != null).map((node) => node.id);
343
- if (compoundNodeIds.length > 0) throw new UnsupportedLayoutError(`The first layered milestone supports flat graphs only; nested nodes: ${compoundNodeIds.join(", ")}`);
344
- }
345
- function runLayeredPipeline(graph, options, context) {
346
- assertFlatGraph(graph);
347
- const direction = options.direction ?? graph.direction ?? "down";
348
- const padding = typeof options.padding === "number" ? {
349
- top: options.padding,
350
- right: options.padding,
351
- bottom: options.padding,
352
- left: options.padding
353
- } : {
354
- top: options.padding?.top ?? 0,
355
- right: options.padding?.right ?? 0,
356
- bottom: options.padding?.bottom ?? 0,
357
- left: options.padding?.left ?? 0
358
- };
359
- const input = {
360
- graph,
361
- sizes: new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)])),
362
- direction,
363
- spacing: {
364
- node: options.spacing?.node ?? 40,
365
- layer: options.spacing?.layer ?? 60
366
- },
367
- padding,
368
- constrainedLayerByNodeId: new Map(graph.nodes.flatMap((node) => {
369
- const layer = options.constraints?.layer?.(node);
370
- return layer === void 0 ? [] : [[node.id, layer]];
371
- }))
372
- };
373
- const measure = (id, run) => {
374
- context?.throwIfAborted();
375
- return context ? context.measurePhase(id, run) : run();
376
- };
377
- const orientation = measure("cycle-breaking", () => (options.strategies?.breakCycles ?? breakCyclesWithDepthFirstSearch)(input));
378
- const assignment = measure("layer-assignment", () => (options.strategies?.assignLayers ?? assignLayersByLongestPath)(input, orientation));
379
- const order = measure("crossing-minimization", () => (options.strategies?.minimizeCrossings ?? minimizeCrossingsWithBarycenter(options.crossingSweeps))(input, orientation, assignment));
380
- const placement = measure("node-placement", () => (options.strategies?.placeNodes ?? placeNodesInLayers)(input, order));
381
- const routes = measure("edge-routing", () => (options.strategies?.routeEdges ?? routeEdgesOrthogonally)(input, orientation, placement));
382
- const nodes = graph.nodes.map((node) => {
383
- const rect = placement.rectByNodeId.get(node.id);
384
- if (!rect) throw new Error(`Node placement missing for ${node.id}`);
385
- const ports = placePorts(node.ports, rect, direction);
386
- return {
387
- ...node,
388
- ...rect,
389
- ...ports === void 0 ? {} : { ports }
390
- };
391
- });
392
- const edges = graph.edges.map((edge) => {
393
- const points = [...routes.pointsByEdgeId.get(edge.id) ?? []];
394
- const midpoint = getPolylineMidpoint(points);
395
- const width = edge.width ?? 0;
396
- const height = edge.height ?? 0;
397
- return {
398
- ...edge,
399
- x: midpoint.x - width / 2,
400
- y: midpoint.y - height / 2,
401
- width,
402
- height,
403
- points,
404
- routing: "orthogonal"
405
- };
406
- });
407
- return {
408
- ...graph,
409
- direction,
410
- nodes,
411
- edges
412
- };
413
- }
414
- /**
415
- * Deterministic native layered layout for an `@statelyai/graph` graph.
416
- *
417
- * This initial vertical slice supports flat graphs, cycles, ports, self-loops,
418
- * four directions, custom phase strategies, and orthogonal routes.
419
- */
420
- function getLayeredLayout(graph, options = {}) {
421
- return runLayeredPipeline(graph, options);
422
- }
423
- const layeredAlgorithm = {
424
- id: "layered",
425
- capabilities: {
426
- full: true,
427
- incremental: false,
428
- partial: false,
429
- routeOnly: false,
430
- hierarchy: false,
431
- ports: true
432
- },
433
- layout(graph, options, context) {
434
- return runLayeredPipeline(graph, options ?? {}, context);
435
- }
436
- };
437
-
438
- //#endregion
439
- export { getPolylineMidpoint as a, placePorts as c, UnsupportedLayoutError as d, breakCyclesWithDepthFirstSearch as i, routeEdgesOrthogonally as l, layeredAlgorithm as n, minimizeCrossingsWithBarycenter as o, assignLayersByLongestPath as r, placeNodesInLayers as s, getLayeredLayout as t, LayoutError as u };