@statelyai/layout 0.0.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.
@@ -0,0 +1,487 @@
1
+ import { a as getPolylineMidpoint, c as placePorts } from "./layered-ByNCZQgJ.mjs";
2
+ import { getNodeSize } from "@statelyai/graph/layout";
3
+
4
+ //#region src/fixed.ts
5
+ function defaultRoute(source, target) {
6
+ return [{
7
+ x: source.x + source.width / 2,
8
+ y: source.y + source.height / 2
9
+ }, {
10
+ x: target.x + target.width / 2,
11
+ y: target.y + target.height / 2
12
+ }];
13
+ }
14
+ /** Preserve authored positions and routes while completing visual geometry. */
15
+ function getFixedLayout(graph, options = {}) {
16
+ const direction = options.direction ?? graph.direction ?? "down";
17
+ const nodes = graph.nodes.map((node) => {
18
+ const size = getNodeSize(node, options);
19
+ const rect = {
20
+ x: node.x ?? 0,
21
+ y: node.y ?? 0,
22
+ ...size
23
+ };
24
+ const ports = placePorts(node.ports, rect, direction);
25
+ return {
26
+ ...node,
27
+ ...rect,
28
+ ...ports === void 0 ? {} : { ports }
29
+ };
30
+ });
31
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
32
+ const edges = graph.edges.map((edge) => {
33
+ const source = nodeById.get(edge.sourceId);
34
+ const target = nodeById.get(edge.targetId);
35
+ const points = edge.points ? [...edge.points] : source && target ? defaultRoute(source, target) : [];
36
+ const midpoint = getPolylineMidpoint(points);
37
+ const width = edge.width ?? 0;
38
+ const height = edge.height ?? 0;
39
+ return {
40
+ ...edge,
41
+ x: edge.x ?? midpoint.x - width / 2,
42
+ y: edge.y ?? midpoint.y - height / 2,
43
+ width,
44
+ height,
45
+ points,
46
+ routing: edge.routing ?? "polyline"
47
+ };
48
+ });
49
+ return {
50
+ ...graph,
51
+ direction,
52
+ nodes,
53
+ edges
54
+ };
55
+ }
56
+ const fixedAlgorithm = {
57
+ id: "fixed",
58
+ capabilities: {
59
+ full: true,
60
+ incremental: false,
61
+ partial: false,
62
+ routeOnly: false,
63
+ hierarchy: true,
64
+ ports: true
65
+ },
66
+ layout(graph, options) {
67
+ return getFixedLayout(graph, options ?? {});
68
+ }
69
+ };
70
+
71
+ //#endregion
72
+ //#region src/box.ts
73
+ function getPadding$3(value) {
74
+ if (typeof value === "number") return {
75
+ top: value,
76
+ right: value,
77
+ bottom: value,
78
+ left: value
79
+ };
80
+ return {
81
+ top: value?.top ?? 15,
82
+ right: value?.right ?? 15,
83
+ bottom: value?.bottom ?? 15,
84
+ left: value?.left ?? 15
85
+ };
86
+ }
87
+ function standardDeviation(values, mean) {
88
+ if (values.length < 2) return 0;
89
+ const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0);
90
+ return Math.sqrt(variance / (values.length - 1));
91
+ }
92
+ /** ELK Box SIMPLE packing translated onto `@statelyai/graph`. */
93
+ function getBoxLayout(graph, options = {}) {
94
+ const spacing = Math.fround(options.spacing ?? 15);
95
+ const padding = getPadding$3(options.padding);
96
+ const aspectRatio = (options.aspectRatio ?? 1.3) > 0 ? options.aspectRatio ?? 1.3 : 1.3;
97
+ const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
98
+ const sortedNodes = [...graph.nodes].sort((first, second) => {
99
+ const priorityDifference = (options.priority?.(second) ?? 0) - (options.priority?.(first) ?? 0);
100
+ if (priorityDifference !== 0) return priorityDifference;
101
+ if (options.interactive) {
102
+ const yDifference = (first.y ?? 0) - (second.y ?? 0);
103
+ if (yDifference !== 0) return yDifference;
104
+ const xDifference = (first.x ?? 0) - (second.x ?? 0);
105
+ if (xDifference !== 0) return xDifference;
106
+ }
107
+ const firstSize = sizes.get(first.id) ?? {
108
+ width: 0,
109
+ height: 0
110
+ };
111
+ const secondSize = sizes.get(second.id) ?? {
112
+ width: 0,
113
+ height: 0
114
+ };
115
+ return firstSize.width * firstSize.height - secondSize.width * secondSize.height;
116
+ });
117
+ const areas = sortedNodes.map((node) => {
118
+ const size = sizes.get(node.id) ?? {
119
+ width: 0,
120
+ height: 0
121
+ };
122
+ return size.width * size.height;
123
+ });
124
+ let totalArea = areas.reduce((sum, area) => sum + area, 0);
125
+ const mean = areas.length === 0 ? 0 : totalArea / areas.length;
126
+ totalArea += areas.length * standardDeviation(areas, mean);
127
+ totalArea += Math.sqrt(totalArea) * (padding.bottom + padding.top);
128
+ totalArea += Math.sqrt(totalArea) * padding.right;
129
+ const widestNode = Math.max(0, ...sortedNodes.map((node) => sizes.get(node.id)?.width ?? 0));
130
+ const maximumRowWidth = Math.max(widestNode, Math.sqrt(totalArea * aspectRatio)) + padding.left;
131
+ let x = padding.left;
132
+ let y = padding.top;
133
+ let rowHeight = 0;
134
+ let rowStart = 0;
135
+ const rows = [];
136
+ const nodes = [];
137
+ for (const node of sortedNodes) {
138
+ const size = sizes.get(node.id) ?? {
139
+ width: 0,
140
+ height: 0
141
+ };
142
+ if (x + size.width > maximumRowWidth) {
143
+ rows.push({
144
+ start: rowStart,
145
+ end: nodes.length,
146
+ height: rowHeight
147
+ });
148
+ rowStart = nodes.length;
149
+ x = padding.left;
150
+ y += rowHeight + spacing;
151
+ rowHeight = 0;
152
+ }
153
+ nodes.push({
154
+ ...node,
155
+ x,
156
+ y,
157
+ ...size
158
+ });
159
+ x += size.width + spacing;
160
+ rowHeight = Math.max(rowHeight, size.height);
161
+ }
162
+ rows.push({
163
+ start: rowStart,
164
+ end: nodes.length,
165
+ height: rowHeight
166
+ });
167
+ if (options.expandNodes) {
168
+ const broadestRow = Math.max(padding.left + padding.right, ...nodes.map((node) => node.x + node.width + padding.right));
169
+ for (const row of rows) for (let index = row.start; index < row.end; index++) {
170
+ const node = nodes[index];
171
+ if (!node) continue;
172
+ node.height = row.height;
173
+ if (index === row.end - 1) node.width = broadestRow - node.x - padding.right;
174
+ }
175
+ }
176
+ const placedById = new Map(nodes.map((node) => [node.id, node]));
177
+ return getFixedLayout({
178
+ ...graph,
179
+ nodes: graph.nodes.map((node) => placedById.get(node.id) ?? node)
180
+ }, { direction: options.direction ?? graph.direction });
181
+ }
182
+ const boxAlgorithm = {
183
+ id: "box",
184
+ capabilities: {
185
+ full: true,
186
+ incremental: false,
187
+ partial: false,
188
+ routeOnly: false,
189
+ hierarchy: false,
190
+ ports: true
191
+ },
192
+ layout(graph, options) {
193
+ return getBoxLayout(graph, options ?? {});
194
+ }
195
+ };
196
+
197
+ //#endregion
198
+ //#region src/packing.ts
199
+ function getPadding$2(value) {
200
+ if (typeof value === "number") return {
201
+ top: value,
202
+ right: value,
203
+ bottom: value,
204
+ left: value
205
+ };
206
+ return {
207
+ top: value?.top ?? 0,
208
+ right: value?.right ?? 0,
209
+ bottom: value?.bottom ?? 0,
210
+ left: value?.left ?? 0
211
+ };
212
+ }
213
+ /** Deterministic shelf-based rectangle packing for `@statelyai/graph`. */
214
+ function getRectanglePackingLayout(graph, options = {}) {
215
+ const spacing = options.spacing ?? 20;
216
+ const padding = getPadding$2(options.padding);
217
+ const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
218
+ const totalArea = [...sizes.values()].reduce((area, size) => area + (size.width + spacing) * (size.height + spacing), 0);
219
+ const targetWidth = options.targetWidth ?? Math.max(1, Math.sqrt(totalArea) * 1.5);
220
+ let x = padding.left;
221
+ let y = padding.top;
222
+ let rowHeight = 0;
223
+ const nodes = [];
224
+ for (const node of graph.nodes) {
225
+ const size = sizes.get(node.id) ?? {
226
+ width: 0,
227
+ height: 0
228
+ };
229
+ if (x > padding.left && x + size.width > padding.left + targetWidth) {
230
+ x = padding.left;
231
+ y += rowHeight + spacing;
232
+ rowHeight = 0;
233
+ }
234
+ nodes.push({
235
+ ...node,
236
+ x,
237
+ y,
238
+ ...size
239
+ });
240
+ x += size.width + spacing;
241
+ rowHeight = Math.max(rowHeight, size.height);
242
+ }
243
+ return getFixedLayout({
244
+ ...graph,
245
+ nodes
246
+ }, { direction: options.direction ?? graph.direction });
247
+ }
248
+ const rectanglePackingAlgorithm = {
249
+ id: "rectpacking",
250
+ capabilities: {
251
+ full: true,
252
+ incremental: false,
253
+ partial: false,
254
+ routeOnly: false,
255
+ hierarchy: false,
256
+ ports: true
257
+ },
258
+ layout(graph, options) {
259
+ return getRectanglePackingLayout(graph, options ?? {});
260
+ }
261
+ };
262
+
263
+ //#endregion
264
+ //#region src/random.ts
265
+ var JavaRandom = class JavaRandom {
266
+ static #multiplier = 25214903917n;
267
+ static #addend = 11n;
268
+ static #mask = (1n << 48n) - 1n;
269
+ #seed;
270
+ constructor(seed) {
271
+ this.#seed = (BigInt(seed) ^ JavaRandom.#multiplier) & JavaRandom.#mask;
272
+ }
273
+ #next(bits) {
274
+ this.#seed = this.#seed * JavaRandom.#multiplier + JavaRandom.#addend & JavaRandom.#mask;
275
+ return Number(this.#seed >> BigInt(48 - bits));
276
+ }
277
+ nextDouble() {
278
+ return (this.#next(26) * 2 ** 27 + this.#next(27)) / 2 ** 53;
279
+ }
280
+ nextFloat() {
281
+ return this.#next(24) / 2 ** 24;
282
+ }
283
+ nextInt(bound) {
284
+ if (bound <= 0) throw new RangeError("bound must be positive");
285
+ if ((bound & -bound) === bound) return Math.floor(bound * this.#next(31) / 2 ** 31);
286
+ let bits;
287
+ let value;
288
+ do {
289
+ bits = this.#next(31);
290
+ value = bits % bound;
291
+ } while (bits - value + (bound - 1) >= 2 ** 31);
292
+ return value;
293
+ }
294
+ };
295
+ function getPadding$1(value) {
296
+ if (typeof value === "number") return {
297
+ top: value,
298
+ right: value,
299
+ bottom: value,
300
+ left: value
301
+ };
302
+ return {
303
+ top: value?.top ?? 15,
304
+ right: value?.right ?? 15,
305
+ bottom: value?.bottom ?? 15,
306
+ left: value?.left ?? 15
307
+ };
308
+ }
309
+ function borderPoint(source, target) {
310
+ const sourceCenter = {
311
+ x: source.x + source.width / 2,
312
+ y: source.y + source.height / 2
313
+ };
314
+ const targetCenter = {
315
+ x: target.x + target.width / 2,
316
+ y: target.y + target.height / 2
317
+ };
318
+ const dx = targetCenter.x - sourceCenter.x;
319
+ const dy = targetCenter.y - sourceCenter.y;
320
+ if (dx === 0 && dy === 0) return {
321
+ x: source.x + source.width,
322
+ y: sourceCenter.y
323
+ };
324
+ const scale = Math.min(dx === 0 ? Infinity : source.width / 2 / Math.abs(dx), dy === 0 ? Infinity : source.height / 2 / Math.abs(dy));
325
+ return {
326
+ x: sourceCenter.x + dx * scale,
327
+ y: sourceCenter.y + dy * scale
328
+ };
329
+ }
330
+ /** Seeded random distribution using Java's 48-bit `Random` sequence. */
331
+ function getRandomLayout(graph, options = {}) {
332
+ if (graph.nodes.length === 0) return getFixedLayout(graph, options);
333
+ const random = new JavaRandom(options.seed && options.seed !== 0 ? options.seed : Date.now());
334
+ const aspectRatio = Math.fround(options.aspectRatio ?? 1.6);
335
+ const spacing = Math.fround(options.spacing ?? 15);
336
+ const padding = getPadding$1(options.padding);
337
+ const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
338
+ const nodeArea = [...sizes.values()].reduce((sum, size) => sum + size.width * size.height, 0);
339
+ const maximumWidth = Math.max(...[...sizes.values()].map((size) => size.width));
340
+ const maximumHeight = Math.max(...[...sizes.values()].map((size) => size.height));
341
+ const edgeFactor = 1 + graph.edges.length;
342
+ const drawArea = nodeArea + 2 * spacing * spacing * edgeFactor * graph.nodes.length;
343
+ const areaRoot = Math.sqrt(drawArea);
344
+ const drawWidth = Math.max(areaRoot * aspectRatio, maximumWidth);
345
+ const drawHeight = Math.max(areaRoot / aspectRatio, maximumHeight);
346
+ const nodes = graph.nodes.map((node) => {
347
+ const size = sizes.get(node.id) ?? {
348
+ width: 0,
349
+ height: 0
350
+ };
351
+ return {
352
+ ...node,
353
+ x: padding.left + random.nextDouble() * (drawWidth - size.width),
354
+ y: padding.left + random.nextDouble() * (drawHeight - size.height),
355
+ ...size
356
+ };
357
+ });
358
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
359
+ const totalWidth = drawWidth + padding.left + padding.right;
360
+ const totalHeight = drawHeight + padding.top + padding.bottom;
361
+ const edges = graph.edges.map((edge) => {
362
+ const source = nodeById.get(edge.sourceId);
363
+ const target = nodeById.get(edge.targetId);
364
+ if (!source || !target) return edge;
365
+ const start = borderPoint(source, target);
366
+ const end = borderPoint(target, source);
367
+ const bendCount = random.nextInt(5) + (source === target ? 1 : 0);
368
+ const maximumDeviation = Math.hypot(end.x - start.x, end.y - start.y) * .2;
369
+ const points = [start];
370
+ for (let index = 1; index <= bendCount; index++) {
371
+ const progress = index / (bendCount + 1);
372
+ points.push({
373
+ x: Math.min(totalWidth - 1, Math.max(1, start.x + (end.x - start.x) * progress + random.nextFloat() * maximumDeviation - maximumDeviation / 2)),
374
+ y: Math.min(totalHeight - 1, Math.max(1, start.y + (end.y - start.y) * progress + random.nextFloat() * maximumDeviation - maximumDeviation / 2))
375
+ });
376
+ }
377
+ points.push(end);
378
+ return {
379
+ ...edge,
380
+ points,
381
+ routing: "polyline"
382
+ };
383
+ });
384
+ return getFixedLayout({
385
+ ...graph,
386
+ nodes,
387
+ edges
388
+ }, { direction: options.direction ?? graph.direction });
389
+ }
390
+ const randomAlgorithm = {
391
+ id: "random",
392
+ capabilities: {
393
+ full: true,
394
+ incremental: false,
395
+ partial: false,
396
+ routeOnly: false,
397
+ hierarchy: false,
398
+ ports: false
399
+ },
400
+ layout(graph, options) {
401
+ return getRandomLayout(graph, options ?? {});
402
+ }
403
+ };
404
+
405
+ //#endregion
406
+ //#region src/spore.ts
407
+ function getPadding(value) {
408
+ if (typeof value === "number") return {
409
+ top: value,
410
+ right: value,
411
+ bottom: value,
412
+ left: value
413
+ };
414
+ return {
415
+ top: value?.top ?? 0,
416
+ right: value?.right ?? 0,
417
+ bottom: value?.bottom ?? 0,
418
+ left: value?.left ?? 0
419
+ };
420
+ }
421
+ function getSporeLayout(graph, options, compact) {
422
+ const spacing = options.spacing ?? 20;
423
+ const padding = getPadding(options.padding);
424
+ const nodes = [];
425
+ graph.nodes.forEach((node, index) => {
426
+ const size = getNodeSize(node, options);
427
+ const previous = graph.nodes[index - 1];
428
+ const placedPrevious = nodes[index - 1];
429
+ if (!previous || !placedPrevious) {
430
+ nodes.push({
431
+ ...node,
432
+ x: padding.left,
433
+ y: padding.top,
434
+ ...size
435
+ });
436
+ return;
437
+ }
438
+ const deltaX = (node.x ?? 0) - (previous.x ?? 0);
439
+ const deltaY = (node.y ?? 0) - (previous.y ?? 0);
440
+ const requiredX = placedPrevious.width + spacing;
441
+ const requiredY = placedPrevious.height + spacing;
442
+ const distance = (delta, required) => {
443
+ if (delta === 0) return 0;
444
+ const magnitude = compact ? required : Math.max(Math.abs(delta), required);
445
+ return Math.sign(delta) * magnitude;
446
+ };
447
+ nodes.push({
448
+ ...node,
449
+ x: placedPrevious.x + distance(deltaX, requiredX),
450
+ y: placedPrevious.y + distance(deltaY, requiredY),
451
+ ...size
452
+ });
453
+ });
454
+ return getFixedLayout({
455
+ ...graph,
456
+ nodes
457
+ }, { direction: options.direction ?? graph.direction });
458
+ }
459
+ /** Compact an existing layout while preserving its relative directions. */
460
+ function getSporeCompactionLayout(graph, options = {}) {
461
+ return getSporeLayout(graph, options, true);
462
+ }
463
+ /** Remove overlap while preserving existing distances that already fit. */
464
+ function getSporeOverlapRemovalLayout(graph, options = {}) {
465
+ return getSporeLayout(graph, options, false);
466
+ }
467
+ function algorithm(id, compact) {
468
+ return {
469
+ id,
470
+ capabilities: {
471
+ full: true,
472
+ incremental: false,
473
+ partial: false,
474
+ routeOnly: false,
475
+ hierarchy: false,
476
+ ports: true
477
+ },
478
+ layout(graph, options) {
479
+ return getSporeLayout(graph, options ?? {}, compact);
480
+ }
481
+ };
482
+ }
483
+ const sporeCompactionAlgorithm = algorithm("sporeCompaction", true);
484
+ const sporeOverlapRemovalAlgorithm = algorithm("sporeOverlap", false);
485
+
486
+ //#endregion
487
+ export { getRandomLayout as a, rectanglePackingAlgorithm as c, fixedAlgorithm as d, getFixedLayout as f, sporeOverlapRemovalAlgorithm as i, boxAlgorithm as l, getSporeOverlapRemovalLayout as n, randomAlgorithm as o, sporeCompactionAlgorithm as r, getRectanglePackingLayout as s, getSporeCompactionLayout as t, getBoxLayout as u };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@statelyai/layout",
3
+ "version": "0.0.0",
4
+ "description": "Extensible, graph-native layout algorithms for @statelyai/graph",
5
+ "license": "EPL-2.0 OR GPL-3.0-or-later",
6
+ "files": [
7
+ "dist",
8
+ "LICENSE.md",
9
+ "NOTICE.md"
10
+ ],
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "main": "./dist/index.mjs",
14
+ "module": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts",
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./elkjs": "./dist/elkjs/index.mjs",
19
+ "./layered": "./dist/layered/index.mjs",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "bench": "vitest bench --run",
25
+ "demo": "vite --config demo/vite.config.ts",
26
+ "demo:build": "vite build --config demo/vite.config.ts",
27
+ "demo:check": "tsx scripts/generate-demo-corpus.ts --check",
28
+ "demo:generate": "tsx scripts/generate-demo-corpus.ts",
29
+ "demo:typecheck": "tsc --project demo/tsconfig.json",
30
+ "dev": "tsdown --watch",
31
+ "format": "oxfmt .",
32
+ "format:check": "oxfmt --check .",
33
+ "lint": "oxlint",
34
+ "lint:fix": "oxlint --fix",
35
+ "test": "vitest",
36
+ "typecheck": "tsc --noEmit",
37
+ "typecheck:repo": "tsc --project tsconfig.repo.json",
38
+ "verify": "oxfmt --check . && oxlint && tsc --noEmit && tsc --project tsconfig.repo.json && tsc --project demo/tsconfig.json && tsx scripts/generate-demo-corpus.ts --check && vitest --run && tsdown && vite build --config demo/vite.config.ts && publint --pack npm && tsx scripts/smoke-package.ts",
39
+ "validate:package": "publint --pack npm && tsx scripts/smoke-package.ts"
40
+ },
41
+ "dependencies": {
42
+ "@statelyai/graph": "^2.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@statelyai/sdk": "^0.20.1",
46
+ "@types/node": "^25.0.3",
47
+ "elkjs": "^0.11.1",
48
+ "oxfmt": "0.62.0",
49
+ "oxlint": "1.77.0",
50
+ "publint": "^0.3.15",
51
+ "tsdown": "^0.18.1",
52
+ "tsx": "^4.22.4",
53
+ "typescript": "^5.9.3",
54
+ "vite": "^8.2.1",
55
+ "vitest": "^4.0.18"
56
+ },
57
+ "peerDependencies": {
58
+ "@statelyai/graph": "^2.1.0"
59
+ },
60
+ "packageManager": "pnpm@10.28.2"
61
+ }