@nodius/layouting 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,1086 @@
1
+ // src/types.ts
2
+ function resolveOptions(options) {
3
+ return {
4
+ direction: options?.direction ?? "TB",
5
+ nodeSpacing: options?.nodeSpacing ?? 40,
6
+ layerSpacing: options?.layerSpacing ?? 60,
7
+ crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
8
+ coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
9
+ edgeMargin: options?.edgeMargin ?? 20
10
+ };
11
+ }
12
+
13
+ // src/graph.ts
14
+ var Graph = class {
15
+ constructor() {
16
+ this.nodes = /* @__PURE__ */ new Map();
17
+ this.edges = /* @__PURE__ */ new Map();
18
+ this.outEdges = /* @__PURE__ */ new Map();
19
+ this.inEdges = /* @__PURE__ */ new Map();
20
+ }
21
+ addNode(node) {
22
+ this.nodes.set(node.id, node);
23
+ if (!this.outEdges.has(node.id)) this.outEdges.set(node.id, /* @__PURE__ */ new Set());
24
+ if (!this.inEdges.has(node.id)) this.inEdges.set(node.id, /* @__PURE__ */ new Set());
25
+ }
26
+ addEdge(edge) {
27
+ this.edges.set(edge.id, edge);
28
+ if (!this.outEdges.has(edge.from)) this.outEdges.set(edge.from, /* @__PURE__ */ new Set());
29
+ if (!this.inEdges.has(edge.to)) this.inEdges.set(edge.to, /* @__PURE__ */ new Set());
30
+ this.outEdges.get(edge.from).add(edge.id);
31
+ this.inEdges.get(edge.to).add(edge.id);
32
+ }
33
+ removeEdge(edgeId) {
34
+ const edge = this.edges.get(edgeId);
35
+ if (!edge) return;
36
+ this.outEdges.get(edge.from)?.delete(edgeId);
37
+ this.inEdges.get(edge.to)?.delete(edgeId);
38
+ this.edges.delete(edgeId);
39
+ }
40
+ removeNode(nodeId) {
41
+ const outEdgeIds = [...this.outEdges.get(nodeId) || []];
42
+ const inEdgeIds = [...this.inEdges.get(nodeId) || []];
43
+ for (const eid of outEdgeIds) this.removeEdge(eid);
44
+ for (const eid of inEdgeIds) this.removeEdge(eid);
45
+ this.nodes.delete(nodeId);
46
+ this.outEdges.delete(nodeId);
47
+ this.inEdges.delete(nodeId);
48
+ }
49
+ predecessors(nodeId) {
50
+ const result = [];
51
+ const inEdgeIds = this.inEdges.get(nodeId);
52
+ if (inEdgeIds) {
53
+ for (const eid of inEdgeIds) {
54
+ const edge = this.edges.get(eid);
55
+ if (edge) result.push(edge.from);
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+ successors(nodeId) {
61
+ const result = [];
62
+ const outEdgeIds = this.outEdges.get(nodeId);
63
+ if (outEdgeIds) {
64
+ for (const eid of outEdgeIds) {
65
+ const edge = this.edges.get(eid);
66
+ if (edge) result.push(edge.to);
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ };
72
+ function buildGraph(nodes, edges) {
73
+ const graph = new Graph();
74
+ for (const node of nodes) {
75
+ graph.addNode({
76
+ id: node.id,
77
+ width: node.width,
78
+ height: node.height,
79
+ handles: node.handles.map((h) => ({ ...h, offset: h.offset ?? 0.5 })),
80
+ isDummy: false,
81
+ layer: -1,
82
+ order: -1,
83
+ x: 0,
84
+ y: 0
85
+ });
86
+ }
87
+ for (const edge of edges) {
88
+ graph.addEdge({
89
+ id: edge.id,
90
+ from: edge.from,
91
+ to: edge.to,
92
+ fromHandle: edge.fromHandle,
93
+ toHandle: edge.toHandle,
94
+ reversed: false,
95
+ originalId: edge.id
96
+ });
97
+ }
98
+ return graph;
99
+ }
100
+ function getHandlePosition(node, handleId) {
101
+ const handle = node.handles.find((h) => h.id === handleId);
102
+ if (!handle) {
103
+ return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
104
+ }
105
+ const offset = handle.offset ?? 0.5;
106
+ switch (handle.position) {
107
+ case "top":
108
+ return { x: node.x + offset * node.width, y: node.y };
109
+ case "bottom":
110
+ return { x: node.x + offset * node.width, y: node.y + node.height };
111
+ case "left":
112
+ return { x: node.x, y: node.y + offset * node.height };
113
+ case "right":
114
+ return { x: node.x + node.width, y: node.y + offset * node.height };
115
+ }
116
+ }
117
+ function getHandleDirection(side) {
118
+ switch (side) {
119
+ case "top":
120
+ return { x: 0, y: -1 };
121
+ case "bottom":
122
+ return { x: 0, y: 1 };
123
+ case "left":
124
+ return { x: -1, y: 0 };
125
+ case "right":
126
+ return { x: 1, y: 0 };
127
+ }
128
+ }
129
+
130
+ // src/algorithms/cycle-breaking.ts
131
+ function breakCycles(graph) {
132
+ const WHITE = 0, GRAY = 1, BLACK = 2;
133
+ const state = /* @__PURE__ */ new Map();
134
+ const reversed = /* @__PURE__ */ new Set();
135
+ for (const nodeId of graph.nodes.keys()) {
136
+ state.set(nodeId, WHITE);
137
+ }
138
+ const nodeIds = [...graph.nodes.keys()].sort((a, b) => {
139
+ const aDiff = (graph.outEdges.get(a)?.size || 0) - (graph.inEdges.get(a)?.size || 0);
140
+ const bDiff = (graph.outEdges.get(b)?.size || 0) - (graph.inEdges.get(b)?.size || 0);
141
+ return bDiff - aDiff;
142
+ });
143
+ function dfs(nodeId) {
144
+ state.set(nodeId, GRAY);
145
+ const outEdgeIds = graph.outEdges.get(nodeId);
146
+ if (outEdgeIds) {
147
+ for (const edgeId of outEdgeIds) {
148
+ const edge = graph.edges.get(edgeId);
149
+ if (!edge) continue;
150
+ const targetState = state.get(edge.to);
151
+ if (targetState === GRAY) {
152
+ reversed.add(edgeId);
153
+ } else if (targetState === WHITE) {
154
+ dfs(edge.to);
155
+ }
156
+ }
157
+ }
158
+ state.set(nodeId, BLACK);
159
+ }
160
+ for (const nodeId of nodeIds) {
161
+ if (state.get(nodeId) === WHITE) {
162
+ dfs(nodeId);
163
+ }
164
+ }
165
+ for (const edgeId of reversed) {
166
+ const edge = graph.edges.get(edgeId);
167
+ if (!edge) continue;
168
+ graph.removeEdge(edgeId);
169
+ graph.addEdge({
170
+ ...edge,
171
+ from: edge.to,
172
+ to: edge.from,
173
+ fromHandle: edge.toHandle,
174
+ toHandle: edge.fromHandle,
175
+ reversed: !edge.reversed
176
+ });
177
+ }
178
+ return reversed;
179
+ }
180
+
181
+ // src/algorithms/layer-assignment.ts
182
+ function assignLayers(graph) {
183
+ const layers = /* @__PURE__ */ new Map();
184
+ const visiting = /* @__PURE__ */ new Set();
185
+ function computeLayer(nodeId) {
186
+ if (layers.has(nodeId)) return layers.get(nodeId);
187
+ if (visiting.has(nodeId)) return 0;
188
+ visiting.add(nodeId);
189
+ const preds = graph.predecessors(nodeId);
190
+ let maxPredLayer = -1;
191
+ for (const pred of preds) {
192
+ maxPredLayer = Math.max(maxPredLayer, computeLayer(pred));
193
+ }
194
+ const layer = maxPredLayer + 1;
195
+ layers.set(nodeId, layer);
196
+ const node = graph.nodes.get(nodeId);
197
+ if (node) node.layer = layer;
198
+ visiting.delete(nodeId);
199
+ return layer;
200
+ }
201
+ for (const nodeId of graph.nodes.keys()) {
202
+ computeLayer(nodeId);
203
+ }
204
+ let maxLayer = 0;
205
+ for (const layer of layers.values()) {
206
+ maxLayer = Math.max(maxLayer, layer);
207
+ }
208
+ const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
209
+ for (const [nodeId, layer] of layers) {
210
+ layersArray[layer].push(nodeId);
211
+ }
212
+ return layersArray;
213
+ }
214
+ function insertDummyNodes(graph, layers) {
215
+ let dummyCounter = 0;
216
+ const edgesToProcess = [...graph.edges.values()];
217
+ for (const edge of edgesToProcess) {
218
+ const fromNode = graph.nodes.get(edge.from);
219
+ const toNode = graph.nodes.get(edge.to);
220
+ if (!fromNode || !toNode) continue;
221
+ const fromLayer = fromNode.layer;
222
+ const toLayer = toNode.layer;
223
+ const span = toLayer - fromLayer;
224
+ if (span <= 1) continue;
225
+ graph.removeEdge(edge.id);
226
+ let prevNodeId = edge.from;
227
+ let prevHandleId = edge.fromHandle;
228
+ for (let l = fromLayer + 1; l < toLayer; l++) {
229
+ const dummyId = `__dummy_${dummyCounter++}`;
230
+ graph.addNode({
231
+ id: dummyId,
232
+ width: 0,
233
+ height: 0,
234
+ handles: [
235
+ { id: "in", type: "input", position: "top", offset: 0.5 },
236
+ { id: "out", type: "output", position: "bottom", offset: 0.5 }
237
+ ],
238
+ isDummy: true,
239
+ layer: l,
240
+ order: -1,
241
+ x: 0,
242
+ y: 0
243
+ });
244
+ layers[l].push(dummyId);
245
+ graph.addEdge({
246
+ id: `__dedge_${dummyCounter}_${l}_in`,
247
+ from: prevNodeId,
248
+ to: dummyId,
249
+ fromHandle: prevHandleId,
250
+ toHandle: "in",
251
+ reversed: edge.reversed,
252
+ originalId: edge.originalId
253
+ });
254
+ prevNodeId = dummyId;
255
+ prevHandleId = "out";
256
+ }
257
+ graph.addEdge({
258
+ id: `__dedge_${dummyCounter}_final`,
259
+ from: prevNodeId,
260
+ to: edge.to,
261
+ fromHandle: prevHandleId,
262
+ toHandle: edge.toHandle,
263
+ reversed: edge.reversed,
264
+ originalId: edge.originalId
265
+ });
266
+ }
267
+ return layers;
268
+ }
269
+
270
+ // src/algorithms/crossing-minimization.ts
271
+ function minimizeCrossings(graph, layers, iterations) {
272
+ if (layers.length <= 1) return layers;
273
+ const totalNodes = layers.reduce((s, l) => s + l.length, 0);
274
+ const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
275
+ const skipTranspose = totalNodes > 800;
276
+ for (let l = 0; l < layers.length; l++) {
277
+ for (let i = 0; i < layers[l].length; i++) {
278
+ const node = graph.nodes.get(layers[l][i]);
279
+ if (node) node.order = i;
280
+ }
281
+ }
282
+ let bestLayers = layers.map((l) => [...l]);
283
+ let bestCrossings = countAllCrossings(graph, layers);
284
+ let noImprovementCount = 0;
285
+ for (let iter = 0; iter < effectiveIter; iter++) {
286
+ for (let l = 1; l < layers.length; l++) {
287
+ orderByBarycenter(graph, layers, l, "up");
288
+ }
289
+ for (let l = layers.length - 2; l >= 0; l--) {
290
+ orderByBarycenter(graph, layers, l, "down");
291
+ }
292
+ if (!skipTranspose) {
293
+ for (let l = 0; l < layers.length; l++) {
294
+ if (layers[l].length <= 100) {
295
+ transposeImprove(graph, layers, l);
296
+ }
297
+ }
298
+ }
299
+ const crossings = countAllCrossings(graph, layers);
300
+ if (crossings < bestCrossings) {
301
+ bestCrossings = crossings;
302
+ bestLayers = layers.map((l) => [...l]);
303
+ noImprovementCount = 0;
304
+ } else {
305
+ noImprovementCount++;
306
+ }
307
+ if (crossings === 0 || noImprovementCount >= 3) break;
308
+ }
309
+ for (let l = 0; l < layers.length; l++) {
310
+ layers[l] = bestLayers[l];
311
+ for (let i = 0; i < layers[l].length; i++) {
312
+ const node = graph.nodes.get(layers[l][i]);
313
+ if (node) node.order = i;
314
+ }
315
+ }
316
+ return layers;
317
+ }
318
+ function orderByBarycenter(graph, layers, layerIndex, direction) {
319
+ const layer = layers[layerIndex];
320
+ const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
321
+ if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
322
+ const adjLayerSet = new Set(layers[adjLayerIndex]);
323
+ const barycenters = /* @__PURE__ */ new Map();
324
+ for (let i = 0; i < layer.length; i++) {
325
+ const nodeId = layer[i];
326
+ let sum = 0;
327
+ let count = 0;
328
+ if (direction === "up") {
329
+ const inEdgeIds = graph.inEdges.get(nodeId);
330
+ if (inEdgeIds) {
331
+ for (const eid of inEdgeIds) {
332
+ const edge = graph.edges.get(eid);
333
+ if (edge && adjLayerSet.has(edge.from)) {
334
+ const neighbor = graph.nodes.get(edge.from);
335
+ if (neighbor) {
336
+ sum += neighbor.order;
337
+ count++;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ } else {
343
+ const outEdgeIds = graph.outEdges.get(nodeId);
344
+ if (outEdgeIds) {
345
+ for (const eid of outEdgeIds) {
346
+ const edge = graph.edges.get(eid);
347
+ if (edge && adjLayerSet.has(edge.to)) {
348
+ const neighbor = graph.nodes.get(edge.to);
349
+ if (neighbor) {
350
+ sum += neighbor.order;
351
+ count++;
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ barycenters.set(nodeId, count > 0 ? sum / count : i);
358
+ }
359
+ layer.sort((a, b) => barycenters.get(a) - barycenters.get(b));
360
+ for (let i = 0; i < layer.length; i++) {
361
+ const node = graph.nodes.get(layer[i]);
362
+ if (node) node.order = i;
363
+ }
364
+ }
365
+ function transposeImprove(graph, layers, layerIndex) {
366
+ const layer = layers[layerIndex];
367
+ if (layer.length <= 1) return;
368
+ let improved = true;
369
+ let passes = 0;
370
+ while (improved && passes < 2) {
371
+ improved = false;
372
+ passes++;
373
+ for (let i = 0; i < layer.length - 1; i++) {
374
+ const nodeA = layer[i];
375
+ const nodeB = layer[i + 1];
376
+ const crossingsBefore = countPairCrossings(graph, layers, layerIndex, nodeA, nodeB);
377
+ layer[i] = nodeB;
378
+ layer[i + 1] = nodeA;
379
+ const nA = graph.nodes.get(nodeA);
380
+ const nB = graph.nodes.get(nodeB);
381
+ if (nA) nA.order = i + 1;
382
+ if (nB) nB.order = i;
383
+ const crossingsAfter = countPairCrossings(graph, layers, layerIndex, nodeB, nodeA);
384
+ if (crossingsAfter < crossingsBefore) {
385
+ improved = true;
386
+ } else {
387
+ layer[i] = nodeA;
388
+ layer[i + 1] = nodeB;
389
+ if (nA) nA.order = i;
390
+ if (nB) nB.order = i + 1;
391
+ }
392
+ }
393
+ }
394
+ }
395
+ function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
396
+ let crossings = 0;
397
+ if (layerIndex > 0) {
398
+ const upperLayer = new Set(layers[layerIndex - 1]);
399
+ const predsA = [];
400
+ const predsB = [];
401
+ const inA = graph.inEdges.get(nodeA);
402
+ if (inA) {
403
+ for (const eid of inA) {
404
+ const edge = graph.edges.get(eid);
405
+ if (edge && upperLayer.has(edge.from)) {
406
+ const n = graph.nodes.get(edge.from);
407
+ if (n) predsA.push(n.order);
408
+ }
409
+ }
410
+ }
411
+ const inB = graph.inEdges.get(nodeB);
412
+ if (inB) {
413
+ for (const eid of inB) {
414
+ const edge = graph.edges.get(eid);
415
+ if (edge && upperLayer.has(edge.from)) {
416
+ const n = graph.nodes.get(edge.from);
417
+ if (n) predsB.push(n.order);
418
+ }
419
+ }
420
+ }
421
+ for (const pA of predsA) {
422
+ for (const pB of predsB) {
423
+ if (pA > pB) crossings++;
424
+ }
425
+ }
426
+ }
427
+ if (layerIndex < layers.length - 1) {
428
+ const lowerLayer = new Set(layers[layerIndex + 1]);
429
+ const succsA = [];
430
+ const succsB = [];
431
+ const outA = graph.outEdges.get(nodeA);
432
+ if (outA) {
433
+ for (const eid of outA) {
434
+ const edge = graph.edges.get(eid);
435
+ if (edge && lowerLayer.has(edge.to)) {
436
+ const n = graph.nodes.get(edge.to);
437
+ if (n) succsA.push(n.order);
438
+ }
439
+ }
440
+ }
441
+ const outB = graph.outEdges.get(nodeB);
442
+ if (outB) {
443
+ for (const eid of outB) {
444
+ const edge = graph.edges.get(eid);
445
+ if (edge && lowerLayer.has(edge.to)) {
446
+ const n = graph.nodes.get(edge.to);
447
+ if (n) succsB.push(n.order);
448
+ }
449
+ }
450
+ }
451
+ for (const sA of succsA) {
452
+ for (const sB of succsB) {
453
+ if (sA > sB) crossings++;
454
+ }
455
+ }
456
+ }
457
+ return crossings;
458
+ }
459
+ function countAllCrossings(graph, layers) {
460
+ let total = 0;
461
+ for (let l = 0; l < layers.length - 1; l++) {
462
+ total += countLayerCrossings(graph, layers[l], layers[l + 1]);
463
+ }
464
+ return total;
465
+ }
466
+ function countLayerCrossings(graph, upperLayer, lowerLayer) {
467
+ const lowerPos = /* @__PURE__ */ new Map();
468
+ lowerLayer.forEach((id, pos) => lowerPos.set(id, pos));
469
+ const edgePairs = [];
470
+ for (let uPos = 0; uPos < upperLayer.length; uPos++) {
471
+ const nodeId = upperLayer[uPos];
472
+ const outEdgeIds = graph.outEdges.get(nodeId);
473
+ if (!outEdgeIds) continue;
474
+ for (const eid of outEdgeIds) {
475
+ const edge = graph.edges.get(eid);
476
+ if (!edge) continue;
477
+ const lp = lowerPos.get(edge.to);
478
+ if (lp !== void 0) {
479
+ edgePairs.push([uPos, lp]);
480
+ }
481
+ }
482
+ }
483
+ if (edgePairs.length <= 1) return 0;
484
+ edgePairs.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
485
+ const lowerPositions = edgePairs.map((e) => e[1]);
486
+ return mergeSortCount(lowerPositions);
487
+ }
488
+ function mergeSortCount(arr) {
489
+ if (arr.length <= 1) return 0;
490
+ const mid = arr.length >> 1;
491
+ const left = arr.slice(0, mid);
492
+ const right = arr.slice(mid);
493
+ let count = mergeSortCount(left) + mergeSortCount(right);
494
+ let i = 0, j = 0, k = 0;
495
+ while (i < left.length && j < right.length) {
496
+ if (left[i] <= right[j]) {
497
+ arr[k++] = left[i++];
498
+ } else {
499
+ count += left.length - i;
500
+ arr[k++] = right[j++];
501
+ }
502
+ }
503
+ while (i < left.length) arr[k++] = left[i++];
504
+ while (j < right.length) arr[k++] = right[j++];
505
+ return count;
506
+ }
507
+
508
+ // src/algorithms/coordinate-assignment.ts
509
+ function assignCoordinates(graph, layers, options) {
510
+ const isHorizontal = options.direction === "LR" || options.direction === "RL";
511
+ const isReversed = options.direction === "BT" || options.direction === "RL";
512
+ assignRankPositions(graph, layers, options, isHorizontal, isReversed);
513
+ assignOrderPositions(graph, layers, options, isHorizontal);
514
+ optimizePositions(graph, layers, options, isHorizontal);
515
+ }
516
+ function assignRankPositions(graph, layers, options, isHorizontal, isReversed) {
517
+ const layerSizes = [];
518
+ for (const layer of layers) {
519
+ let maxSize = 0;
520
+ for (const nodeId of layer) {
521
+ const node = graph.nodes.get(nodeId);
522
+ const size = isHorizontal ? node.width : node.height;
523
+ maxSize = Math.max(maxSize, size);
524
+ }
525
+ layerSizes.push(maxSize);
526
+ }
527
+ const layerPositions = [];
528
+ let pos = 0;
529
+ for (let l = 0; l < layers.length; l++) {
530
+ layerPositions.push(pos);
531
+ pos += layerSizes[l] + options.layerSpacing;
532
+ }
533
+ if (isReversed) {
534
+ const totalSize = pos - options.layerSpacing;
535
+ for (let l = 0; l < layerPositions.length; l++) {
536
+ layerPositions[l] = totalSize - layerPositions[l] - layerSizes[l];
537
+ }
538
+ }
539
+ for (let l = 0; l < layers.length; l++) {
540
+ for (const nodeId of layers[l]) {
541
+ const node = graph.nodes.get(nodeId);
542
+ const nodeSize = isHorizontal ? node.width : node.height;
543
+ const offset = (layerSizes[l] - nodeSize) / 2;
544
+ if (isHorizontal) {
545
+ node.x = layerPositions[l] + offset;
546
+ } else {
547
+ node.y = layerPositions[l] + offset;
548
+ }
549
+ }
550
+ }
551
+ }
552
+ function assignOrderPositions(graph, layers, options, isHorizontal) {
553
+ for (const layer of layers) {
554
+ let pos = 0;
555
+ for (const nodeId of layer) {
556
+ const node = graph.nodes.get(nodeId);
557
+ const size = isHorizontal ? node.height : node.width;
558
+ if (isHorizontal) {
559
+ node.y = pos;
560
+ } else {
561
+ node.x = pos;
562
+ }
563
+ pos += size + options.nodeSpacing;
564
+ }
565
+ const totalSize = pos - options.nodeSpacing;
566
+ const centerOffset = -totalSize / 2;
567
+ for (const nodeId of layer) {
568
+ const node = graph.nodes.get(nodeId);
569
+ if (isHorizontal) {
570
+ node.y += centerOffset;
571
+ } else {
572
+ node.x += centerOffset;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ function optimizePositions(graph, layers, options, isHorizontal) {
578
+ for (let iter = 0; iter < options.coordinateOptimizationIterations; iter++) {
579
+ for (let l = 0; l < layers.length; l++) {
580
+ optimizeLayer(graph, layers[l], options, isHorizontal);
581
+ }
582
+ for (let l = layers.length - 1; l >= 0; l--) {
583
+ optimizeLayer(graph, layers[l], options, isHorizontal);
584
+ }
585
+ }
586
+ centerAllLayers(graph, layers, isHorizontal);
587
+ }
588
+ function optimizeLayer(graph, layer, options, isHorizontal) {
589
+ if (layer.length === 0) return;
590
+ const desired = /* @__PURE__ */ new Map();
591
+ for (const nodeId of layer) {
592
+ const node = graph.nodes.get(nodeId);
593
+ const connected = [...graph.predecessors(nodeId), ...graph.successors(nodeId)];
594
+ if (connected.length === 0) {
595
+ desired.set(nodeId, getOrderPos(node, isHorizontal));
596
+ continue;
597
+ }
598
+ const positions = connected.map((cId) => {
599
+ const c = graph.nodes.get(cId);
600
+ return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
601
+ }).sort((a, b) => a - b);
602
+ const nodeSize = getOrderSize(node, isHorizontal);
603
+ const median = positions.length % 2 === 0 ? (positions[positions.length / 2 - 1] + positions[positions.length / 2]) / 2 : positions[Math.floor(positions.length / 2)];
604
+ desired.set(nodeId, median - nodeSize / 2);
605
+ }
606
+ for (let i = 0; i < layer.length; i++) {
607
+ const nodeId = layer[i];
608
+ const node = graph.nodes.get(nodeId);
609
+ let desiredPos = desired.get(nodeId);
610
+ if (i > 0) {
611
+ const prevId = layer[i - 1];
612
+ const prev = graph.nodes.get(prevId);
613
+ const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
614
+ desiredPos = Math.max(desiredPos, prevEnd + options.nodeSpacing);
615
+ }
616
+ setOrderPos(node, isHorizontal, desiredPos);
617
+ }
618
+ for (let i = layer.length - 2; i >= 0; i--) {
619
+ const nodeId = layer[i];
620
+ const node = graph.nodes.get(nodeId);
621
+ const nextId = layer[i + 1];
622
+ const next = graph.nodes.get(nextId);
623
+ const nodeEnd = getOrderPos(node, isHorizontal) + getOrderSize(node, isHorizontal);
624
+ const nextStart = getOrderPos(next, isHorizontal);
625
+ if (nodeEnd + options.nodeSpacing > nextStart) {
626
+ setOrderPos(node, isHorizontal, nextStart - options.nodeSpacing - getOrderSize(node, isHorizontal));
627
+ }
628
+ }
629
+ }
630
+ function centerAllLayers(graph, layers, isHorizontal) {
631
+ if (layers.length === 0) return;
632
+ let globalMin = Infinity;
633
+ let globalMax = -Infinity;
634
+ for (const layer of layers) {
635
+ for (const nodeId of layer) {
636
+ const node = graph.nodes.get(nodeId);
637
+ const pos = getOrderPos(node, isHorizontal);
638
+ const size = getOrderSize(node, isHorizontal);
639
+ globalMin = Math.min(globalMin, pos);
640
+ globalMax = Math.max(globalMax, pos + size);
641
+ }
642
+ }
643
+ const offset = -globalMin;
644
+ for (const layer of layers) {
645
+ for (const nodeId of layer) {
646
+ const node = graph.nodes.get(nodeId);
647
+ setOrderPos(node, isHorizontal, getOrderPos(node, isHorizontal) + offset);
648
+ }
649
+ }
650
+ let rankMin = Infinity;
651
+ for (const layer of layers) {
652
+ for (const nodeId of layer) {
653
+ const node = graph.nodes.get(nodeId);
654
+ const pos = isHorizontal ? node.x : node.y;
655
+ rankMin = Math.min(rankMin, pos);
656
+ }
657
+ }
658
+ if (rankMin !== 0) {
659
+ for (const layer of layers) {
660
+ for (const nodeId of layer) {
661
+ const node = graph.nodes.get(nodeId);
662
+ if (isHorizontal) {
663
+ node.x -= rankMin;
664
+ } else {
665
+ node.y -= rankMin;
666
+ }
667
+ }
668
+ }
669
+ }
670
+ }
671
+ function getOrderPos(node, isHorizontal) {
672
+ return isHorizontal ? node.y : node.x;
673
+ }
674
+ function setOrderPos(node, isHorizontal, value) {
675
+ if (isHorizontal) {
676
+ node.y = value;
677
+ } else {
678
+ node.x = value;
679
+ }
680
+ }
681
+ function getOrderSize(node, isHorizontal) {
682
+ return isHorizontal ? node.height : node.width;
683
+ }
684
+
685
+ // src/algorithms/edge-routing.ts
686
+ function routeEdges(graph, direction, edgeMargin) {
687
+ const chains = collectEdgeChains(graph);
688
+ const routes = [];
689
+ for (const chain of chains) {
690
+ routes.push(routeChain(graph, chain, direction, edgeMargin));
691
+ }
692
+ return routes;
693
+ }
694
+ function collectEdgeChains(graph) {
695
+ const chains = [];
696
+ const visited = /* @__PURE__ */ new Set();
697
+ for (const [edgeId, edge] of graph.edges) {
698
+ if (visited.has(edgeId)) continue;
699
+ const fromNode = graph.nodes.get(edge.from);
700
+ if (!fromNode || fromNode.isDummy) continue;
701
+ const dummyNodes = [];
702
+ let currentEdge = edge;
703
+ visited.add(edgeId);
704
+ while (true) {
705
+ const toNode = graph.nodes.get(currentEdge.to);
706
+ if (!toNode || !toNode.isDummy) break;
707
+ dummyNodes.push(currentEdge.to);
708
+ const outEdgeIds = graph.outEdges.get(currentEdge.to);
709
+ if (!outEdgeIds || outEdgeIds.size === 0) break;
710
+ let nextEdge = null;
711
+ for (const eid of outEdgeIds) {
712
+ const e = graph.edges.get(eid);
713
+ if (e && e.originalId === edge.originalId) {
714
+ nextEdge = e;
715
+ visited.add(eid);
716
+ break;
717
+ }
718
+ }
719
+ if (!nextEdge) {
720
+ const eid = outEdgeIds.values().next().value;
721
+ if (!eid) break;
722
+ nextEdge = graph.edges.get(eid);
723
+ if (!nextEdge) break;
724
+ visited.add(eid);
725
+ }
726
+ currentEdge = nextEdge;
727
+ }
728
+ chains.push({
729
+ originalId: edge.originalId,
730
+ from: edge.from,
731
+ to: currentEdge.to,
732
+ fromHandle: edge.fromHandle,
733
+ toHandle: currentEdge.toHandle,
734
+ dummyNodes,
735
+ reversed: edge.reversed
736
+ });
737
+ }
738
+ return chains;
739
+ }
740
+ function routeChain(graph, chain, direction, margin) {
741
+ const fromNode = graph.nodes.get(chain.from);
742
+ const toNode = graph.nodes.get(chain.to);
743
+ const actualFrom = chain.reversed ? chain.to : chain.from;
744
+ const actualTo = chain.reversed ? chain.from : chain.to;
745
+ const actualFromHandle = chain.reversed ? chain.toHandle : chain.fromHandle;
746
+ const actualToHandle = chain.reversed ? chain.fromHandle : chain.toHandle;
747
+ const sourceNode = chain.reversed ? toNode : fromNode;
748
+ const targetNode = chain.reversed ? fromNode : toNode;
749
+ const sourcePos = getHandlePosition(sourceNode, actualFromHandle);
750
+ const targetPos = getHandlePosition(targetNode, actualToHandle);
751
+ const sourceHandle = sourceNode.handles.find((h) => h.id === actualFromHandle);
752
+ const targetHandle = targetNode.handles.find((h) => h.id === actualToHandle);
753
+ const sourceSide = sourceHandle?.position || getDefaultSourceSide(direction);
754
+ const targetSide = targetHandle?.position || getDefaultTargetSide(direction);
755
+ const rawPoints = [];
756
+ rawPoints.push(sourcePos);
757
+ const exitDir = getHandleDirection(sourceSide);
758
+ rawPoints.push({
759
+ x: sourcePos.x + exitDir.x * margin,
760
+ y: sourcePos.y + exitDir.y * margin
761
+ });
762
+ const dummies = chain.reversed ? [...chain.dummyNodes].reverse() : chain.dummyNodes;
763
+ for (const dummyId of dummies) {
764
+ const dummy = graph.nodes.get(dummyId);
765
+ rawPoints.push({ x: dummy.x, y: dummy.y });
766
+ }
767
+ const entryDir = getHandleDirection(targetSide);
768
+ rawPoints.push({
769
+ x: targetPos.x + entryDir.x * margin,
770
+ y: targetPos.y + entryDir.y * margin
771
+ });
772
+ rawPoints.push(targetPos);
773
+ const points = makeOrthogonal(rawPoints, direction);
774
+ return {
775
+ id: chain.originalId,
776
+ from: actualFrom,
777
+ to: actualTo,
778
+ fromHandle: actualFromHandle,
779
+ toHandle: actualToHandle,
780
+ points
781
+ };
782
+ }
783
+ function makeOrthogonal(points, direction) {
784
+ if (points.length < 2) return points;
785
+ const result = [points[0]];
786
+ const isVerticalFlow = direction === "TB" || direction === "BT";
787
+ for (let i = 1; i < points.length; i++) {
788
+ const prev = result[result.length - 1];
789
+ const curr = points[i];
790
+ const dx = Math.abs(prev.x - curr.x);
791
+ const dy = Math.abs(prev.y - curr.y);
792
+ if (dx < 0.01 || dy < 0.01) {
793
+ result.push(curr);
794
+ continue;
795
+ }
796
+ if (isVerticalFlow) {
797
+ const midY = (prev.y + curr.y) / 2;
798
+ result.push({ x: prev.x, y: midY });
799
+ result.push({ x: curr.x, y: midY });
800
+ } else {
801
+ const midX = (prev.x + curr.x) / 2;
802
+ result.push({ x: midX, y: prev.y });
803
+ result.push({ x: midX, y: curr.y });
804
+ }
805
+ result.push(curr);
806
+ }
807
+ return cleanupPoints(result);
808
+ }
809
+ function cleanupPoints(points) {
810
+ if (points.length <= 2) return points;
811
+ const result = [points[0]];
812
+ for (let i = 1; i < points.length - 1; i++) {
813
+ const prev = result[result.length - 1];
814
+ const curr = points[i];
815
+ const next = points[i + 1];
816
+ const sameX = Math.abs(prev.x - curr.x) < 0.01 && Math.abs(curr.x - next.x) < 0.01;
817
+ const sameY = Math.abs(prev.y - curr.y) < 0.01 && Math.abs(curr.y - next.y) < 0.01;
818
+ if (!sameX && !sameY) {
819
+ result.push(curr);
820
+ } else if (sameX || sameY) {
821
+ continue;
822
+ } else {
823
+ result.push(curr);
824
+ }
825
+ }
826
+ result.push(points[points.length - 1]);
827
+ return result;
828
+ }
829
+ function getDefaultSourceSide(direction) {
830
+ switch (direction) {
831
+ case "TB":
832
+ return "bottom";
833
+ case "BT":
834
+ return "top";
835
+ case "LR":
836
+ return "right";
837
+ case "RL":
838
+ return "left";
839
+ }
840
+ }
841
+ function getDefaultTargetSide(direction) {
842
+ switch (direction) {
843
+ case "TB":
844
+ return "top";
845
+ case "BT":
846
+ return "bottom";
847
+ case "LR":
848
+ return "left";
849
+ case "RL":
850
+ return "right";
851
+ }
852
+ }
853
+
854
+ // src/layout.ts
855
+ function layout(input, options) {
856
+ const resolved = resolveOptions(options);
857
+ const graph = buildGraph(input.nodes, input.edges);
858
+ return computeLayout(graph, resolved);
859
+ }
860
+ function computeLayout(graph, options) {
861
+ if (graph.nodes.size === 0) {
862
+ return { nodes: [], edges: [] };
863
+ }
864
+ breakCycles(graph);
865
+ let layers = assignLayers(graph);
866
+ layers = insertDummyNodes(graph, layers);
867
+ layers = minimizeCrossings(graph, layers, options.crossingMinimizationIterations);
868
+ assignCoordinates(graph, layers, options);
869
+ const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
870
+ return buildResult(graph, routedEdges);
871
+ }
872
+ function buildResult(graph, routedEdges) {
873
+ const nodes = [];
874
+ const edges = [];
875
+ for (const [, node] of graph.nodes) {
876
+ if (node.isDummy) continue;
877
+ const handles = node.handles.map((h) => {
878
+ const pos = getHandlePosition(node, h.id);
879
+ return {
880
+ id: h.id,
881
+ type: h.type,
882
+ position: h.position,
883
+ x: pos.x,
884
+ y: pos.y
885
+ };
886
+ });
887
+ nodes.push({
888
+ id: node.id,
889
+ x: node.x,
890
+ y: node.y,
891
+ width: node.width,
892
+ height: node.height,
893
+ handles
894
+ });
895
+ }
896
+ for (const route of routedEdges) {
897
+ edges.push({
898
+ id: route.id,
899
+ from: route.from,
900
+ to: route.to,
901
+ fromHandle: route.fromHandle,
902
+ toHandle: route.toHandle,
903
+ points: route.points
904
+ });
905
+ }
906
+ return { nodes, edges };
907
+ }
908
+
909
+ // src/incremental.ts
910
+ var IncrementalLayout = class {
911
+ constructor(options) {
912
+ this.inputNodes = /* @__PURE__ */ new Map();
913
+ this.inputEdges = /* @__PURE__ */ new Map();
914
+ this.lastResult = null;
915
+ this.nodePositions = /* @__PURE__ */ new Map();
916
+ this.options = resolveOptions(options);
917
+ }
918
+ /**
919
+ * Set the full graph and compute a complete layout.
920
+ */
921
+ setGraph(input) {
922
+ this.inputNodes.clear();
923
+ this.inputEdges.clear();
924
+ for (const node of input.nodes) {
925
+ this.inputNodes.set(node.id, node);
926
+ }
927
+ for (const edge of input.edges) {
928
+ this.inputEdges.set(edge.id, edge);
929
+ }
930
+ return this.recompute();
931
+ }
932
+ /**
933
+ * Add nodes and edges incrementally.
934
+ * Attempts to minimize layout changes for existing nodes.
935
+ */
936
+ addNodes(nodes, edges) {
937
+ for (const node of nodes) {
938
+ this.inputNodes.set(node.id, node);
939
+ }
940
+ if (edges) {
941
+ for (const edge of edges) {
942
+ this.inputEdges.set(edge.id, edge);
943
+ }
944
+ }
945
+ return this.recomputeIncremental(
946
+ new Set(nodes.map((n) => n.id)),
947
+ new Set(edges?.map((e) => e.id) || [])
948
+ );
949
+ }
950
+ /**
951
+ * Remove nodes (and their connected edges) from the layout.
952
+ */
953
+ removeNodes(nodeIds) {
954
+ const removedSet = new Set(nodeIds);
955
+ for (const id of nodeIds) {
956
+ this.inputNodes.delete(id);
957
+ }
958
+ for (const [edgeId, edge] of this.inputEdges) {
959
+ if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
960
+ this.inputEdges.delete(edgeId);
961
+ }
962
+ }
963
+ for (const id of nodeIds) {
964
+ this.nodePositions.delete(id);
965
+ }
966
+ return this.recompute();
967
+ }
968
+ /**
969
+ * Add edges between existing nodes.
970
+ */
971
+ addEdges(edges) {
972
+ for (const edge of edges) {
973
+ this.inputEdges.set(edge.id, edge);
974
+ }
975
+ return this.recomputeIncremental(
976
+ /* @__PURE__ */ new Set(),
977
+ new Set(edges.map((e) => e.id))
978
+ );
979
+ }
980
+ /**
981
+ * Remove edges from the layout.
982
+ */
983
+ removeEdges(edgeIds) {
984
+ for (const id of edgeIds) {
985
+ this.inputEdges.delete(id);
986
+ }
987
+ return this.recompute();
988
+ }
989
+ /**
990
+ * Get the current layout result.
991
+ */
992
+ getResult() {
993
+ return this.lastResult;
994
+ }
995
+ recompute() {
996
+ const input = {
997
+ nodes: [...this.inputNodes.values()],
998
+ edges: [...this.inputEdges.values()]
999
+ };
1000
+ const graph = buildGraph(input.nodes, input.edges);
1001
+ this.lastResult = computeLayout(graph, this.options);
1002
+ this.cachePositions();
1003
+ return this.lastResult;
1004
+ }
1005
+ recomputeIncremental(newNodeIds, newEdgeIds) {
1006
+ const input = {
1007
+ nodes: [...this.inputNodes.values()],
1008
+ edges: [...this.inputEdges.values()]
1009
+ };
1010
+ const graph = buildGraph(input.nodes, input.edges);
1011
+ breakCycles(graph);
1012
+ let layers = assignLayers(graph);
1013
+ layers = insertDummyNodes(graph, layers);
1014
+ layers = minimizeCrossings(graph, layers, this.options.crossingMinimizationIterations);
1015
+ assignCoordinates(graph, layers, this.options);
1016
+ this.applyStability(graph, newNodeIds);
1017
+ const routedEdges = routeEdges(graph, this.options.direction, this.options.edgeMargin);
1018
+ this.lastResult = this.buildResult(graph, routedEdges);
1019
+ this.cachePositions();
1020
+ return this.lastResult;
1021
+ }
1022
+ /**
1023
+ * Apply stability: blend new positions with old positions for existing nodes.
1024
+ * This reduces visual disruption when adding new nodes.
1025
+ */
1026
+ applyStability(graph, newNodeIds) {
1027
+ if (this.nodePositions.size === 0) return;
1028
+ const STABILITY_WEIGHT = 0.3;
1029
+ for (const [nodeId, node] of graph.nodes) {
1030
+ if (node.isDummy || newNodeIds.has(nodeId)) continue;
1031
+ const oldPos = this.nodePositions.get(nodeId);
1032
+ if (!oldPos) continue;
1033
+ node.x = node.x * (1 - STABILITY_WEIGHT) + oldPos.x * STABILITY_WEIGHT;
1034
+ node.y = node.y * (1 - STABILITY_WEIGHT) + oldPos.y * STABILITY_WEIGHT;
1035
+ }
1036
+ }
1037
+ cachePositions() {
1038
+ if (!this.lastResult) return;
1039
+ this.nodePositions.clear();
1040
+ for (const node of this.lastResult.nodes) {
1041
+ this.nodePositions.set(node.id, { x: node.x, y: node.y });
1042
+ }
1043
+ }
1044
+ buildResult(graph, routedEdges) {
1045
+ const nodes = [];
1046
+ const edges = [];
1047
+ for (const [, node] of graph.nodes) {
1048
+ if (node.isDummy) continue;
1049
+ const handles = node.handles.map((h) => {
1050
+ const pos = getHandlePosition(node, h.id);
1051
+ return {
1052
+ id: h.id,
1053
+ type: h.type,
1054
+ position: h.position,
1055
+ x: pos.x,
1056
+ y: pos.y
1057
+ };
1058
+ });
1059
+ nodes.push({
1060
+ id: node.id,
1061
+ x: node.x,
1062
+ y: node.y,
1063
+ width: node.width,
1064
+ height: node.height,
1065
+ handles
1066
+ });
1067
+ }
1068
+ for (const route of routedEdges) {
1069
+ edges.push({
1070
+ id: route.id,
1071
+ from: route.from,
1072
+ to: route.to,
1073
+ fromHandle: route.fromHandle,
1074
+ toHandle: route.toHandle,
1075
+ points: route.points
1076
+ });
1077
+ }
1078
+ return { nodes, edges };
1079
+ }
1080
+ };
1081
+ export {
1082
+ IncrementalLayout,
1083
+ countAllCrossings,
1084
+ layout
1085
+ };
1086
+ //# sourceMappingURL=index.mjs.map