@nodius/layouting 0.1.0 → 0.1.3
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/LICENSE +201 -201
- package/README.md +1092 -122
- package/dist/algorithms/component-packing.d.ts +9 -0
- package/dist/algorithms/component-packing.d.ts.map +1 -0
- package/dist/algorithms/coordinate-assignment.d.ts +7 -0
- package/dist/algorithms/coordinate-assignment.d.ts.map +1 -0
- package/dist/algorithms/crossing-minimization.d.ts +11 -0
- package/dist/algorithms/crossing-minimization.d.ts.map +1 -0
- package/dist/algorithms/cycle-breaking.d.ts +8 -0
- package/dist/algorithms/cycle-breaking.d.ts.map +1 -0
- package/dist/algorithms/edge-routing.d.ts +17 -0
- package/dist/algorithms/edge-routing.d.ts.map +1 -0
- package/dist/algorithms/layer-assignment.d.ts +20 -0
- package/dist/algorithms/layer-assignment.d.ts.map +1 -0
- package/dist/algorithms/value-cluster.d.ts +15 -0
- package/dist/algorithms/value-cluster.d.ts.map +1 -0
- package/dist/algorithms/value-placement.d.ts +25 -0
- package/dist/algorithms/value-placement.d.ts.map +1 -0
- package/dist/debug.d.ts +20 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/graph.d.ts +50 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/incremental.d.ts +33 -0
- package/dist/incremental.d.ts.map +1 -0
- package/dist/index.d.ts +7 -176
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1159 -234
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1156 -233
- package/dist/index.mjs.map +1 -1
- package/dist/layout.d.ts +10 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/proposals.d.ts +44 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +5 -4
- package/dist/index.d.mts +0 -176
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
|
+
var QUALITY_PROFILES = {
|
|
3
|
+
draft: {
|
|
4
|
+
crossingMinimizationIterations: 6,
|
|
5
|
+
coordinateOptimizationIterations: 2,
|
|
6
|
+
skipTranspose: true
|
|
7
|
+
},
|
|
8
|
+
balanced: {
|
|
9
|
+
crossingMinimizationIterations: 24,
|
|
10
|
+
coordinateOptimizationIterations: 8,
|
|
11
|
+
skipTranspose: false
|
|
12
|
+
},
|
|
13
|
+
high: {
|
|
14
|
+
crossingMinimizationIterations: 48,
|
|
15
|
+
coordinateOptimizationIterations: 16,
|
|
16
|
+
skipTranspose: false
|
|
17
|
+
}
|
|
18
|
+
};
|
|
2
19
|
function resolveOptions(options) {
|
|
20
|
+
const quality = options?.quality ?? "balanced";
|
|
21
|
+
const profile = QUALITY_PROFILES[quality];
|
|
3
22
|
return {
|
|
4
23
|
direction: options?.direction ?? "TB",
|
|
24
|
+
quality,
|
|
5
25
|
nodeSpacing: options?.nodeSpacing ?? 40,
|
|
6
26
|
layerSpacing: options?.layerSpacing ?? 60,
|
|
7
|
-
crossingMinimizationIterations: options?.crossingMinimizationIterations ??
|
|
8
|
-
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ??
|
|
9
|
-
|
|
27
|
+
crossingMinimizationIterations: options?.crossingMinimizationIterations ?? profile.crossingMinimizationIterations,
|
|
28
|
+
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? profile.coordinateOptimizationIterations,
|
|
29
|
+
skipTranspose: options?.skipTranspose ?? profile.skipTranspose,
|
|
30
|
+
edgeMargin: options?.edgeMargin ?? 20,
|
|
31
|
+
controlWeight: options?.edgeWeights?.control ?? 1,
|
|
32
|
+
dataWeight: options?.edgeWeights?.data ?? 0.25,
|
|
33
|
+
packComponents: options?.packComponents ?? true,
|
|
34
|
+
compoundPadding: options?.compoundPadding ?? 24,
|
|
35
|
+
onProposal: options?.onProposal
|
|
10
36
|
};
|
|
11
37
|
}
|
|
12
38
|
|
|
@@ -69,7 +95,7 @@ var Graph = class {
|
|
|
69
95
|
return result;
|
|
70
96
|
}
|
|
71
97
|
};
|
|
72
|
-
function buildGraph(nodes, edges) {
|
|
98
|
+
function buildGraph(nodes, edges, ctx = { controlWeight: 1, dataWeight: 0.25 }) {
|
|
73
99
|
const graph = new Graph();
|
|
74
100
|
for (const node of nodes) {
|
|
75
101
|
graph.addNode({
|
|
@@ -81,10 +107,15 @@ function buildGraph(nodes, edges) {
|
|
|
81
107
|
layer: -1,
|
|
82
108
|
order: -1,
|
|
83
109
|
x: 0,
|
|
84
|
-
y: 0
|
|
110
|
+
y: 0,
|
|
111
|
+
parentId: node.parentId,
|
|
112
|
+
isCompound: false,
|
|
113
|
+
isValue: false
|
|
85
114
|
});
|
|
86
115
|
}
|
|
87
116
|
for (const edge of edges) {
|
|
117
|
+
const kind = edge.kind ?? "control";
|
|
118
|
+
const defaultWeight = kind === "control" ? ctx.controlWeight : ctx.dataWeight;
|
|
88
119
|
graph.addEdge({
|
|
89
120
|
id: edge.id,
|
|
90
121
|
from: edge.from,
|
|
@@ -92,9 +123,34 @@ function buildGraph(nodes, edges) {
|
|
|
92
123
|
fromHandle: edge.fromHandle,
|
|
93
124
|
toHandle: edge.toHandle,
|
|
94
125
|
reversed: false,
|
|
95
|
-
originalId: edge.id
|
|
126
|
+
originalId: edge.id,
|
|
127
|
+
kind,
|
|
128
|
+
weight: edge.weight ?? defaultWeight
|
|
96
129
|
});
|
|
97
130
|
}
|
|
131
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
132
|
+
const ins = graph.inEdges.get(nodeId);
|
|
133
|
+
const outs = graph.outEdges.get(nodeId);
|
|
134
|
+
let hasControl = false;
|
|
135
|
+
if (ins) {
|
|
136
|
+
for (const eid of ins) {
|
|
137
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
138
|
+
hasControl = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!hasControl && outs) {
|
|
144
|
+
for (const eid of outs) {
|
|
145
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
146
|
+
hasControl = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const incidentCount = (ins?.size ?? 0) + (outs?.size ?? 0);
|
|
152
|
+
node.isValue = !hasControl && incidentCount > 0;
|
|
153
|
+
}
|
|
98
154
|
return graph;
|
|
99
155
|
}
|
|
100
156
|
function getHandlePosition(node, handleId) {
|
|
@@ -182,28 +238,88 @@ function breakCycles(graph) {
|
|
|
182
238
|
function assignLayers(graph) {
|
|
183
239
|
const layers = /* @__PURE__ */ new Map();
|
|
184
240
|
const visiting = /* @__PURE__ */ new Set();
|
|
185
|
-
function
|
|
241
|
+
function controlLayer(nodeId) {
|
|
186
242
|
if (layers.has(nodeId)) return layers.get(nodeId);
|
|
187
243
|
if (visiting.has(nodeId)) return 0;
|
|
244
|
+
const node = graph.nodes.get(nodeId);
|
|
245
|
+
if (!node || node.isValue) return -1;
|
|
188
246
|
visiting.add(nodeId);
|
|
189
|
-
const
|
|
247
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
190
248
|
let maxPredLayer = -1;
|
|
191
|
-
|
|
192
|
-
|
|
249
|
+
if (inEdgeIds) {
|
|
250
|
+
for (const eid of inEdgeIds) {
|
|
251
|
+
const edge = graph.edges.get(eid);
|
|
252
|
+
if (!edge || edge.kind !== "control") continue;
|
|
253
|
+
const pred = graph.nodes.get(edge.from);
|
|
254
|
+
if (!pred || pred.isValue) continue;
|
|
255
|
+
maxPredLayer = Math.max(maxPredLayer, controlLayer(edge.from));
|
|
256
|
+
}
|
|
193
257
|
}
|
|
194
258
|
const layer = maxPredLayer + 1;
|
|
195
259
|
layers.set(nodeId, layer);
|
|
196
|
-
|
|
197
|
-
if (node) node.layer = layer;
|
|
260
|
+
node.layer = layer;
|
|
198
261
|
visiting.delete(nodeId);
|
|
199
262
|
return layer;
|
|
200
263
|
}
|
|
201
|
-
for (const nodeId of graph.nodes
|
|
202
|
-
|
|
264
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
265
|
+
if (!node.isValue) controlLayer(nodeId);
|
|
203
266
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
267
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
268
|
+
if (!node.isValue) continue;
|
|
269
|
+
const neighborLayers = [];
|
|
270
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
271
|
+
if (inEdgeIds) {
|
|
272
|
+
for (const eid of inEdgeIds) {
|
|
273
|
+
const edge = graph.edges.get(eid);
|
|
274
|
+
if (!edge) continue;
|
|
275
|
+
const nbr = graph.nodes.get(edge.from);
|
|
276
|
+
if (nbr && !nbr.isValue && layers.has(edge.from)) {
|
|
277
|
+
neighborLayers.push(layers.get(edge.from));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
282
|
+
if (outEdgeIds) {
|
|
283
|
+
for (const eid of outEdgeIds) {
|
|
284
|
+
const edge = graph.edges.get(eid);
|
|
285
|
+
if (!edge) continue;
|
|
286
|
+
const nbr = graph.nodes.get(edge.to);
|
|
287
|
+
if (nbr && !nbr.isValue && layers.has(edge.to)) {
|
|
288
|
+
neighborLayers.push(layers.get(edge.to));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let layer;
|
|
293
|
+
if (neighborLayers.length > 0) {
|
|
294
|
+
neighborLayers.sort((a, b) => a - b);
|
|
295
|
+
layer = neighborLayers[Math.floor(neighborLayers.length / 2)];
|
|
296
|
+
} else {
|
|
297
|
+
layer = 0;
|
|
298
|
+
}
|
|
299
|
+
layers.set(nodeId, layer);
|
|
300
|
+
node.layer = layer;
|
|
301
|
+
}
|
|
302
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
303
|
+
if (!layers.has(nodeId)) {
|
|
304
|
+
layers.set(nodeId, 0);
|
|
305
|
+
node.layer = 0;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
let minLayer = Infinity;
|
|
309
|
+
let maxLayer = -Infinity;
|
|
310
|
+
for (const l of layers.values()) {
|
|
311
|
+
if (l < minLayer) minLayer = l;
|
|
312
|
+
if (l > maxLayer) maxLayer = l;
|
|
313
|
+
}
|
|
314
|
+
if (!isFinite(minLayer)) return [];
|
|
315
|
+
if (minLayer !== 0) {
|
|
316
|
+
for (const [id, l] of layers) {
|
|
317
|
+
const adj = l - minLayer;
|
|
318
|
+
layers.set(id, adj);
|
|
319
|
+
const n = graph.nodes.get(id);
|
|
320
|
+
if (n) n.layer = adj;
|
|
321
|
+
}
|
|
322
|
+
maxLayer -= minLayer;
|
|
207
323
|
}
|
|
208
324
|
const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
|
|
209
325
|
for (const [nodeId, layer] of layers) {
|
|
@@ -215,6 +331,7 @@ function insertDummyNodes(graph, layers) {
|
|
|
215
331
|
let dummyCounter = 0;
|
|
216
332
|
const edgesToProcess = [...graph.edges.values()];
|
|
217
333
|
for (const edge of edgesToProcess) {
|
|
334
|
+
if (edge.kind !== "control") continue;
|
|
218
335
|
const fromNode = graph.nodes.get(edge.from);
|
|
219
336
|
const toNode = graph.nodes.get(edge.to);
|
|
220
337
|
if (!fromNode || !toNode) continue;
|
|
@@ -239,7 +356,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
239
356
|
layer: l,
|
|
240
357
|
order: -1,
|
|
241
358
|
x: 0,
|
|
242
|
-
y: 0
|
|
359
|
+
y: 0,
|
|
360
|
+
isCompound: false,
|
|
361
|
+
isValue: false
|
|
243
362
|
});
|
|
244
363
|
layers[l].push(dummyId);
|
|
245
364
|
graph.addEdge({
|
|
@@ -249,7 +368,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
249
368
|
fromHandle: prevHandleId,
|
|
250
369
|
toHandle: "in",
|
|
251
370
|
reversed: edge.reversed,
|
|
252
|
-
originalId: edge.originalId
|
|
371
|
+
originalId: edge.originalId,
|
|
372
|
+
kind: edge.kind,
|
|
373
|
+
weight: edge.weight
|
|
253
374
|
});
|
|
254
375
|
prevNodeId = dummyId;
|
|
255
376
|
prevHandleId = "out";
|
|
@@ -261,18 +382,20 @@ function insertDummyNodes(graph, layers) {
|
|
|
261
382
|
fromHandle: prevHandleId,
|
|
262
383
|
toHandle: edge.toHandle,
|
|
263
384
|
reversed: edge.reversed,
|
|
264
|
-
originalId: edge.originalId
|
|
385
|
+
originalId: edge.originalId,
|
|
386
|
+
kind: edge.kind,
|
|
387
|
+
weight: edge.weight
|
|
265
388
|
});
|
|
266
389
|
}
|
|
267
390
|
return layers;
|
|
268
391
|
}
|
|
269
392
|
|
|
270
393
|
// src/algorithms/crossing-minimization.ts
|
|
271
|
-
function minimizeCrossings(graph, layers, iterations) {
|
|
394
|
+
function minimizeCrossings(graph, layers, iterations, forceSkipTranspose = false) {
|
|
272
395
|
if (layers.length <= 1) return layers;
|
|
273
396
|
const totalNodes = layers.reduce((s, l) => s + l.length, 0);
|
|
274
397
|
const effectiveIter = totalNodes > 500 ? Math.min(iterations, 6) : totalNodes > 200 ? Math.min(iterations, 12) : iterations;
|
|
275
|
-
const skipTranspose = totalNodes > 800;
|
|
398
|
+
const skipTranspose = forceSkipTranspose || totalNodes > 800;
|
|
276
399
|
for (let l = 0; l < layers.length; l++) {
|
|
277
400
|
for (let i = 0; i < layers[l].length; i++) {
|
|
278
401
|
const node = graph.nodes.get(layers[l][i]);
|
|
@@ -319,45 +442,54 @@ function orderByBarycenter(graph, layers, layerIndex, direction) {
|
|
|
319
442
|
const layer = layers[layerIndex];
|
|
320
443
|
const adjLayerIndex = direction === "up" ? layerIndex - 1 : layerIndex + 1;
|
|
321
444
|
if (adjLayerIndex < 0 || adjLayerIndex >= layers.length) return;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
445
|
+
const n = layer.length;
|
|
446
|
+
const bary = new Float64Array(n);
|
|
447
|
+
if (direction === "up") {
|
|
448
|
+
for (let i = 0; i < n; i++) {
|
|
449
|
+
const nodeId = layer[i];
|
|
450
|
+
let sum = 0;
|
|
451
|
+
let count = 0;
|
|
329
452
|
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
330
453
|
if (inEdgeIds) {
|
|
331
454
|
for (const eid of inEdgeIds) {
|
|
332
455
|
const edge = graph.edges.get(eid);
|
|
333
|
-
if (edge
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
456
|
+
if (!edge) continue;
|
|
457
|
+
const neighbor = graph.nodes.get(edge.from);
|
|
458
|
+
if (neighbor && neighbor.layer === adjLayerIndex) {
|
|
459
|
+
sum += neighbor.order;
|
|
460
|
+
count++;
|
|
339
461
|
}
|
|
340
462
|
}
|
|
341
463
|
}
|
|
342
|
-
|
|
464
|
+
bary[i] = count > 0 ? sum / count : i;
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
for (let i = 0; i < n; i++) {
|
|
468
|
+
const nodeId = layer[i];
|
|
469
|
+
let sum = 0;
|
|
470
|
+
let count = 0;
|
|
343
471
|
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
344
472
|
if (outEdgeIds) {
|
|
345
473
|
for (const eid of outEdgeIds) {
|
|
346
474
|
const edge = graph.edges.get(eid);
|
|
347
|
-
if (edge
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
475
|
+
if (!edge) continue;
|
|
476
|
+
const neighbor = graph.nodes.get(edge.to);
|
|
477
|
+
if (neighbor && neighbor.layer === adjLayerIndex) {
|
|
478
|
+
sum += neighbor.order;
|
|
479
|
+
count++;
|
|
353
480
|
}
|
|
354
481
|
}
|
|
355
482
|
}
|
|
483
|
+
bary[i] = count > 0 ? sum / count : i;
|
|
356
484
|
}
|
|
357
|
-
barycenters.set(nodeId, count > 0 ? sum / count : i);
|
|
358
485
|
}
|
|
359
|
-
|
|
360
|
-
for (let i = 0; i <
|
|
486
|
+
const indices = new Array(n);
|
|
487
|
+
for (let i = 0; i < n; i++) indices[i] = i;
|
|
488
|
+
indices.sort((a, b) => bary[a] - bary[b]);
|
|
489
|
+
const reordered = new Array(n);
|
|
490
|
+
for (let i = 0; i < n; i++) reordered[i] = layer[indices[i]];
|
|
491
|
+
for (let i = 0; i < n; i++) {
|
|
492
|
+
layer[i] = reordered[i];
|
|
361
493
|
const node = graph.nodes.get(layer[i]);
|
|
362
494
|
if (node) node.order = i;
|
|
363
495
|
}
|
|
@@ -392,65 +524,65 @@ function transposeImprove(graph, layers, layerIndex) {
|
|
|
392
524
|
}
|
|
393
525
|
}
|
|
394
526
|
}
|
|
527
|
+
var _scratchA = [];
|
|
528
|
+
var _scratchB = [];
|
|
395
529
|
function countPairCrossings(graph, layers, layerIndex, nodeA, nodeB) {
|
|
396
530
|
let crossings = 0;
|
|
397
531
|
if (layerIndex > 0) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
532
|
+
const upperIdx = layerIndex - 1;
|
|
533
|
+
_scratchA.length = 0;
|
|
534
|
+
_scratchB.length = 0;
|
|
401
535
|
const inA = graph.inEdges.get(nodeA);
|
|
402
536
|
if (inA) {
|
|
403
537
|
for (const eid of inA) {
|
|
404
538
|
const edge = graph.edges.get(eid);
|
|
405
|
-
if (edge
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
539
|
+
if (!edge) continue;
|
|
540
|
+
const n = graph.nodes.get(edge.from);
|
|
541
|
+
if (n && n.layer === upperIdx) _scratchA.push(n.order);
|
|
409
542
|
}
|
|
410
543
|
}
|
|
411
544
|
const inB = graph.inEdges.get(nodeB);
|
|
412
545
|
if (inB) {
|
|
413
546
|
for (const eid of inB) {
|
|
414
547
|
const edge = graph.edges.get(eid);
|
|
415
|
-
if (edge
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
548
|
+
if (!edge) continue;
|
|
549
|
+
const n = graph.nodes.get(edge.from);
|
|
550
|
+
if (n && n.layer === upperIdx) _scratchB.push(n.order);
|
|
419
551
|
}
|
|
420
552
|
}
|
|
421
|
-
for (
|
|
422
|
-
|
|
423
|
-
|
|
553
|
+
for (let i = 0; i < _scratchA.length; i++) {
|
|
554
|
+
const pA = _scratchA[i];
|
|
555
|
+
for (let j = 0; j < _scratchB.length; j++) {
|
|
556
|
+
if (pA > _scratchB[j]) crossings++;
|
|
424
557
|
}
|
|
425
558
|
}
|
|
426
559
|
}
|
|
427
560
|
if (layerIndex < layers.length - 1) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
561
|
+
const lowerIdx = layerIndex + 1;
|
|
562
|
+
_scratchA.length = 0;
|
|
563
|
+
_scratchB.length = 0;
|
|
431
564
|
const outA = graph.outEdges.get(nodeA);
|
|
432
565
|
if (outA) {
|
|
433
566
|
for (const eid of outA) {
|
|
434
567
|
const edge = graph.edges.get(eid);
|
|
435
|
-
if (edge
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
568
|
+
if (!edge) continue;
|
|
569
|
+
const n = graph.nodes.get(edge.to);
|
|
570
|
+
if (n && n.layer === lowerIdx) _scratchA.push(n.order);
|
|
439
571
|
}
|
|
440
572
|
}
|
|
441
573
|
const outB = graph.outEdges.get(nodeB);
|
|
442
574
|
if (outB) {
|
|
443
575
|
for (const eid of outB) {
|
|
444
576
|
const edge = graph.edges.get(eid);
|
|
445
|
-
if (edge
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
577
|
+
if (!edge) continue;
|
|
578
|
+
const n = graph.nodes.get(edge.to);
|
|
579
|
+
if (n && n.layer === lowerIdx) _scratchB.push(n.order);
|
|
449
580
|
}
|
|
450
581
|
}
|
|
451
|
-
for (
|
|
452
|
-
|
|
453
|
-
|
|
582
|
+
for (let i = 0; i < _scratchA.length; i++) {
|
|
583
|
+
const sA = _scratchA[i];
|
|
584
|
+
for (let j = 0; j < _scratchB.length; j++) {
|
|
585
|
+
if (sA > _scratchB[j]) crossings++;
|
|
454
586
|
}
|
|
455
587
|
}
|
|
456
588
|
}
|
|
@@ -486,22 +618,36 @@ function countLayerCrossings(graph, upperLayer, lowerLayer) {
|
|
|
486
618
|
return mergeSortCount(lowerPositions);
|
|
487
619
|
}
|
|
488
620
|
function mergeSortCount(arr) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
let
|
|
494
|
-
let
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
621
|
+
const n = arr.length;
|
|
622
|
+
if (n <= 1) return 0;
|
|
623
|
+
let buf = new Array(n);
|
|
624
|
+
let src = arr;
|
|
625
|
+
let dst = buf;
|
|
626
|
+
let count = 0;
|
|
627
|
+
for (let width = 1; width < n; width <<= 1) {
|
|
628
|
+
for (let i = 0; i < n; i += width << 1) {
|
|
629
|
+
const left = i;
|
|
630
|
+
const mid = Math.min(i + width, n);
|
|
631
|
+
const right = Math.min(i + (width << 1), n);
|
|
632
|
+
let a = left, b = mid, k = left;
|
|
633
|
+
while (a < mid && b < right) {
|
|
634
|
+
if (src[a] <= src[b]) {
|
|
635
|
+
dst[k++] = src[a++];
|
|
636
|
+
} else {
|
|
637
|
+
count += mid - a;
|
|
638
|
+
dst[k++] = src[b++];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
while (a < mid) dst[k++] = src[a++];
|
|
642
|
+
while (b < right) dst[k++] = src[b++];
|
|
501
643
|
}
|
|
644
|
+
const tmp = src;
|
|
645
|
+
src = dst;
|
|
646
|
+
dst = tmp;
|
|
647
|
+
}
|
|
648
|
+
if (src !== arr) {
|
|
649
|
+
for (let i = 0; i < n; i++) arr[i] = src[i];
|
|
502
650
|
}
|
|
503
|
-
while (i < left.length) arr[k++] = left[i++];
|
|
504
|
-
while (j < right.length) arr[k++] = right[j++];
|
|
505
651
|
return count;
|
|
506
652
|
}
|
|
507
653
|
|
|
@@ -585,37 +731,69 @@ function optimizePositions(graph, layers, options, isHorizontal) {
|
|
|
585
731
|
}
|
|
586
732
|
centerAllLayers(graph, layers, isHorizontal);
|
|
587
733
|
}
|
|
734
|
+
var _neighborBuf = new Float64Array(64);
|
|
735
|
+
var _desiredBuf = new Float64Array(64);
|
|
736
|
+
function ensureBufSize(size) {
|
|
737
|
+
if (_neighborBuf.length < size) _neighborBuf = new Float64Array(size * 2);
|
|
738
|
+
if (_desiredBuf.length < size) _desiredBuf = new Float64Array(size * 2);
|
|
739
|
+
}
|
|
588
740
|
function optimizeLayer(graph, layer, options, isHorizontal) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
741
|
+
const n = layer.length;
|
|
742
|
+
if (n === 0) return;
|
|
743
|
+
let maxDegree = 0;
|
|
744
|
+
for (let i = 0; i < n; i++) {
|
|
745
|
+
const id = layer[i];
|
|
746
|
+
const d = (graph.inEdges.get(id)?.size ?? 0) + (graph.outEdges.get(id)?.size ?? 0);
|
|
747
|
+
if (d > maxDegree) maxDegree = d;
|
|
748
|
+
}
|
|
749
|
+
ensureBufSize(Math.max(n, maxDegree));
|
|
750
|
+
const desired = _desiredBuf;
|
|
751
|
+
for (let i = 0; i < n; i++) {
|
|
752
|
+
const nodeId = layer[i];
|
|
592
753
|
const node = graph.nodes.get(nodeId);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
754
|
+
let nbCount = 0;
|
|
755
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
756
|
+
if (inEdgeIds) {
|
|
757
|
+
for (const eid of inEdgeIds) {
|
|
758
|
+
const edge = graph.edges.get(eid);
|
|
759
|
+
if (!edge) continue;
|
|
760
|
+
const c = graph.nodes.get(edge.from);
|
|
761
|
+
if (!c) continue;
|
|
762
|
+
_neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
766
|
+
if (outEdgeIds) {
|
|
767
|
+
for (const eid of outEdgeIds) {
|
|
768
|
+
const edge = graph.edges.get(eid);
|
|
769
|
+
if (!edge) continue;
|
|
770
|
+
const c = graph.nodes.get(edge.to);
|
|
771
|
+
if (!c) continue;
|
|
772
|
+
_neighborBuf[nbCount++] = getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (nbCount === 0) {
|
|
776
|
+
desired[i] = getOrderPos(node, isHorizontal);
|
|
596
777
|
continue;
|
|
597
778
|
}
|
|
598
|
-
|
|
599
|
-
const c = graph.nodes.get(cId);
|
|
600
|
-
return getOrderPos(c, isHorizontal) + getOrderSize(c, isHorizontal) / 2;
|
|
601
|
-
}).sort((a, b) => a - b);
|
|
779
|
+
sortPrefix(_neighborBuf, nbCount);
|
|
602
780
|
const nodeSize = getOrderSize(node, isHorizontal);
|
|
603
|
-
const median =
|
|
604
|
-
desired
|
|
781
|
+
const median = (nbCount & 1) === 0 ? (_neighborBuf[(nbCount >> 1) - 1] + _neighborBuf[nbCount >> 1]) / 2 : _neighborBuf[nbCount >> 1];
|
|
782
|
+
desired[i] = median - nodeSize / 2;
|
|
605
783
|
}
|
|
606
|
-
for (let i = 0; i <
|
|
784
|
+
for (let i = 0; i < n; i++) {
|
|
607
785
|
const nodeId = layer[i];
|
|
608
786
|
const node = graph.nodes.get(nodeId);
|
|
609
|
-
let desiredPos = desired
|
|
787
|
+
let desiredPos = desired[i];
|
|
610
788
|
if (i > 0) {
|
|
611
789
|
const prevId = layer[i - 1];
|
|
612
790
|
const prev = graph.nodes.get(prevId);
|
|
613
791
|
const prevEnd = getOrderPos(prev, isHorizontal) + getOrderSize(prev, isHorizontal);
|
|
614
|
-
desiredPos
|
|
792
|
+
if (desiredPos < prevEnd + options.nodeSpacing) desiredPos = prevEnd + options.nodeSpacing;
|
|
615
793
|
}
|
|
616
794
|
setOrderPos(node, isHorizontal, desiredPos);
|
|
617
795
|
}
|
|
618
|
-
for (let i =
|
|
796
|
+
for (let i = n - 2; i >= 0; i--) {
|
|
619
797
|
const nodeId = layer[i];
|
|
620
798
|
const node = graph.nodes.get(nodeId);
|
|
621
799
|
const nextId = layer[i + 1];
|
|
@@ -627,6 +805,17 @@ function optimizeLayer(graph, layer, options, isHorizontal) {
|
|
|
627
805
|
}
|
|
628
806
|
}
|
|
629
807
|
}
|
|
808
|
+
function sortPrefix(arr, count) {
|
|
809
|
+
for (let i = 1; i < count; i++) {
|
|
810
|
+
const x = arr[i];
|
|
811
|
+
let j = i - 1;
|
|
812
|
+
while (j >= 0 && arr[j] > x) {
|
|
813
|
+
arr[j + 1] = arr[j];
|
|
814
|
+
j--;
|
|
815
|
+
}
|
|
816
|
+
arr[j + 1] = x;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
630
819
|
function centerAllLayers(graph, layers, isHorizontal) {
|
|
631
820
|
if (layers.length === 0) return;
|
|
632
821
|
let globalMin = Infinity;
|
|
@@ -682,6 +871,176 @@ function getOrderSize(node, isHorizontal) {
|
|
|
682
871
|
return isHorizontal ? node.height : node.width;
|
|
683
872
|
}
|
|
684
873
|
|
|
874
|
+
// src/algorithms/value-placement.ts
|
|
875
|
+
function placeValueSidecars(graph, layers, options) {
|
|
876
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
877
|
+
const attachments = [];
|
|
878
|
+
const orphanValues = [];
|
|
879
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
880
|
+
if (!node.isValue || node.isDummy) continue;
|
|
881
|
+
const consumer = findDominantConsumer(graph, nodeId);
|
|
882
|
+
if (consumer) {
|
|
883
|
+
attachments.push({ valueId: nodeId, consumerId: consumer });
|
|
884
|
+
} else {
|
|
885
|
+
orphanValues.push(nodeId);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const byConsumer = /* @__PURE__ */ new Map();
|
|
889
|
+
for (const { valueId, consumerId } of attachments) {
|
|
890
|
+
const arr = byConsumer.get(consumerId) ?? [];
|
|
891
|
+
arr.push(valueId);
|
|
892
|
+
byConsumer.set(consumerId, arr);
|
|
893
|
+
}
|
|
894
|
+
for (const [consumerId, vIds] of byConsumer) {
|
|
895
|
+
const consumer = graph.nodes.get(consumerId);
|
|
896
|
+
if (!consumer) continue;
|
|
897
|
+
const { beforeSide, afterSide } = splitValuesBySide(graph, consumerId, vIds, isHorizontal);
|
|
898
|
+
if (isHorizontal) {
|
|
899
|
+
const cxCenter = consumer.x + consumer.width / 2;
|
|
900
|
+
let topEdge = consumer.y;
|
|
901
|
+
for (const vid of beforeSide) {
|
|
902
|
+
const v = graph.nodes.get(vid);
|
|
903
|
+
if (!v) continue;
|
|
904
|
+
v.y = topEdge - options.nodeSpacing - v.height;
|
|
905
|
+
v.x = cxCenter - v.width / 2;
|
|
906
|
+
topEdge = v.y;
|
|
907
|
+
}
|
|
908
|
+
let bottomEdge = consumer.y + consumer.height;
|
|
909
|
+
for (const vid of afterSide) {
|
|
910
|
+
const v = graph.nodes.get(vid);
|
|
911
|
+
if (!v) continue;
|
|
912
|
+
v.y = bottomEdge + options.nodeSpacing;
|
|
913
|
+
v.x = cxCenter - v.width / 2;
|
|
914
|
+
bottomEdge = v.y + v.height;
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
const cyCenter = consumer.y + consumer.height / 2;
|
|
918
|
+
let leftEdge = consumer.x;
|
|
919
|
+
for (const vid of beforeSide) {
|
|
920
|
+
const v = graph.nodes.get(vid);
|
|
921
|
+
if (!v) continue;
|
|
922
|
+
v.x = leftEdge - options.nodeSpacing - v.width;
|
|
923
|
+
v.y = cyCenter - v.height / 2;
|
|
924
|
+
leftEdge = v.x;
|
|
925
|
+
}
|
|
926
|
+
let rightEdge = consumer.x + consumer.width;
|
|
927
|
+
for (const vid of afterSide) {
|
|
928
|
+
const v = graph.nodes.get(vid);
|
|
929
|
+
if (!v) continue;
|
|
930
|
+
v.x = rightEdge + options.nodeSpacing;
|
|
931
|
+
v.y = cyCenter - v.height / 2;
|
|
932
|
+
rightEdge = v.x + v.width;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
let orphanX = 0, orphanY = 0;
|
|
937
|
+
for (const orphanId of orphanValues) {
|
|
938
|
+
const v = graph.nodes.get(orphanId);
|
|
939
|
+
if (!v) continue;
|
|
940
|
+
v.x = orphanX;
|
|
941
|
+
v.y = orphanY;
|
|
942
|
+
if (isHorizontal) orphanX += v.width + options.nodeSpacing;
|
|
943
|
+
else orphanY += v.height + options.nodeSpacing;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function findDominantConsumer(graph, valueId) {
|
|
947
|
+
const counts = /* @__PURE__ */ new Map();
|
|
948
|
+
const outs = graph.outEdges.get(valueId);
|
|
949
|
+
if (outs) {
|
|
950
|
+
for (const eid of outs) {
|
|
951
|
+
const edge = graph.edges.get(eid);
|
|
952
|
+
if (!edge) continue;
|
|
953
|
+
const other = graph.nodes.get(edge.to);
|
|
954
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
955
|
+
counts.set(edge.to, (counts.get(edge.to) ?? 0) + 1);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const ins = graph.inEdges.get(valueId);
|
|
960
|
+
if (ins) {
|
|
961
|
+
for (const eid of ins) {
|
|
962
|
+
const edge = graph.edges.get(eid);
|
|
963
|
+
if (!edge) continue;
|
|
964
|
+
const other = graph.nodes.get(edge.from);
|
|
965
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
966
|
+
counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
let bestId = null;
|
|
971
|
+
let bestCount = -1;
|
|
972
|
+
for (const [id, count] of counts) {
|
|
973
|
+
if (count > bestCount || count === bestCount && bestId !== null && id < bestId) {
|
|
974
|
+
bestId = id;
|
|
975
|
+
bestCount = count;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return bestId;
|
|
979
|
+
}
|
|
980
|
+
function splitValuesBySide(graph, consumerId, valueIds, isHorizontal) {
|
|
981
|
+
const consumer = graph.nodes.get(consumerId);
|
|
982
|
+
if (!consumer) return { beforeSide: [...valueIds].sort(), afterSide: [] };
|
|
983
|
+
const slots = [];
|
|
984
|
+
for (const vId of valueIds) {
|
|
985
|
+
let side = "neutral";
|
|
986
|
+
let offset = 0.5;
|
|
987
|
+
const inspectHandle = (handleId) => {
|
|
988
|
+
if (!handleId) return;
|
|
989
|
+
const handle = consumer.handles.find((h) => h.id === handleId);
|
|
990
|
+
if (!handle) return;
|
|
991
|
+
offset = handle.offset ?? 0.5;
|
|
992
|
+
if (isHorizontal) {
|
|
993
|
+
if (handle.position === "top") side = "before";
|
|
994
|
+
else if (handle.position === "bottom") side = "after";
|
|
995
|
+
} else {
|
|
996
|
+
if (handle.position === "left") side = "before";
|
|
997
|
+
else if (handle.position === "right") side = "after";
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const outs = graph.outEdges.get(vId);
|
|
1001
|
+
if (outs) for (const eid of outs) {
|
|
1002
|
+
const e = graph.edges.get(eid);
|
|
1003
|
+
if (e && e.to === consumerId) {
|
|
1004
|
+
inspectHandle(e.toHandle);
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (side === "neutral") {
|
|
1009
|
+
const ins = graph.inEdges.get(vId);
|
|
1010
|
+
if (ins) for (const eid of ins) {
|
|
1011
|
+
const e = graph.edges.get(eid);
|
|
1012
|
+
if (e && e.from === consumerId) {
|
|
1013
|
+
inspectHandle(e.fromHandle);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
slots.push({ id: vId, side, offset });
|
|
1019
|
+
}
|
|
1020
|
+
const before = slots.filter((s) => s.side === "before");
|
|
1021
|
+
const after = slots.filter((s) => s.side === "after");
|
|
1022
|
+
const neutral = slots.filter((s) => s.side === "neutral");
|
|
1023
|
+
for (const n of neutral) {
|
|
1024
|
+
if (before.length <= after.length) before.push(n);
|
|
1025
|
+
else after.push(n);
|
|
1026
|
+
}
|
|
1027
|
+
const cmp = (a, b) => a.offset - b.offset || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1028
|
+
before.sort(cmp);
|
|
1029
|
+
after.sort(cmp);
|
|
1030
|
+
return {
|
|
1031
|
+
beforeSide: before.map((s) => s.id),
|
|
1032
|
+
afterSide: after.map((s) => s.id)
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function railLayers(layers, graph) {
|
|
1036
|
+
return layers.map(
|
|
1037
|
+
(layer) => layer.filter((id) => {
|
|
1038
|
+
const n = graph.nodes.get(id);
|
|
1039
|
+
return n && !n.isValue;
|
|
1040
|
+
})
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
685
1044
|
// src/algorithms/edge-routing.ts
|
|
686
1045
|
function routeEdges(graph, direction, edgeMargin) {
|
|
687
1046
|
const chains = collectEdgeChains(graph);
|
|
@@ -732,7 +1091,8 @@ function collectEdgeChains(graph) {
|
|
|
732
1091
|
fromHandle: edge.fromHandle,
|
|
733
1092
|
toHandle: currentEdge.toHandle,
|
|
734
1093
|
dummyNodes,
|
|
735
|
-
reversed: edge.reversed
|
|
1094
|
+
reversed: edge.reversed,
|
|
1095
|
+
kind: edge.kind
|
|
736
1096
|
});
|
|
737
1097
|
}
|
|
738
1098
|
return chains;
|
|
@@ -777,7 +1137,8 @@ function routeChain(graph, chain, direction, margin) {
|
|
|
777
1137
|
to: actualTo,
|
|
778
1138
|
fromHandle: actualFromHandle,
|
|
779
1139
|
toHandle: actualToHandle,
|
|
780
|
-
points
|
|
1140
|
+
points,
|
|
1141
|
+
kind: chain.kind
|
|
781
1142
|
};
|
|
782
1143
|
}
|
|
783
1144
|
function makeOrthogonal(points, direction) {
|
|
@@ -851,21 +1212,375 @@ function getDefaultTargetSide(direction) {
|
|
|
851
1212
|
}
|
|
852
1213
|
}
|
|
853
1214
|
|
|
1215
|
+
// src/algorithms/component-packing.ts
|
|
1216
|
+
function packComponents(nodes, edges, options) {
|
|
1217
|
+
if (nodes.length === 0) return;
|
|
1218
|
+
const idToNode = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const n of nodes) idToNode.set(n.id, n);
|
|
1220
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
1221
|
+
function topAncestor(id) {
|
|
1222
|
+
if (rootOf.has(id)) return rootOf.get(id);
|
|
1223
|
+
const n = idToNode.get(id);
|
|
1224
|
+
if (!n || !n.parentId || !idToNode.has(n.parentId)) {
|
|
1225
|
+
rootOf.set(id, id);
|
|
1226
|
+
return id;
|
|
1227
|
+
}
|
|
1228
|
+
const r = topAncestor(n.parentId);
|
|
1229
|
+
rootOf.set(id, r);
|
|
1230
|
+
return r;
|
|
1231
|
+
}
|
|
1232
|
+
for (const n of nodes) topAncestor(n.id);
|
|
1233
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1234
|
+
function find(x) {
|
|
1235
|
+
let cur = x;
|
|
1236
|
+
while (parent.get(cur) !== cur) {
|
|
1237
|
+
const p = parent.get(cur);
|
|
1238
|
+
parent.set(cur, parent.get(p));
|
|
1239
|
+
cur = parent.get(cur);
|
|
1240
|
+
}
|
|
1241
|
+
return cur;
|
|
1242
|
+
}
|
|
1243
|
+
function union(a, b) {
|
|
1244
|
+
const ra = find(a);
|
|
1245
|
+
const rb = find(b);
|
|
1246
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
1247
|
+
}
|
|
1248
|
+
for (const n of nodes) parent.set(topAncestor(n.id), topAncestor(n.id));
|
|
1249
|
+
for (const e of edges) {
|
|
1250
|
+
const ra = topAncestor(e.from);
|
|
1251
|
+
const rb = topAncestor(e.to);
|
|
1252
|
+
if (idToNode.has(ra) && idToNode.has(rb)) union(ra, rb);
|
|
1253
|
+
}
|
|
1254
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const n of nodes) {
|
|
1256
|
+
const root = topAncestor(n.id);
|
|
1257
|
+
const comp = find(root);
|
|
1258
|
+
const arr = groups.get(comp) ?? [];
|
|
1259
|
+
arr.push(n);
|
|
1260
|
+
groups.set(comp, arr);
|
|
1261
|
+
}
|
|
1262
|
+
if (groups.size <= 1) return;
|
|
1263
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
1264
|
+
const edgesByComp = /* @__PURE__ */ new Map();
|
|
1265
|
+
for (const e of edges) {
|
|
1266
|
+
const root = topAncestor(e.from);
|
|
1267
|
+
const comp = find(root);
|
|
1268
|
+
const arr = edgesByComp.get(comp) ?? [];
|
|
1269
|
+
arr.push(e);
|
|
1270
|
+
edgesByComp.set(comp, arr);
|
|
1271
|
+
}
|
|
1272
|
+
const boxes = [];
|
|
1273
|
+
for (const [compId, group] of groups) {
|
|
1274
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1275
|
+
for (const n of group) {
|
|
1276
|
+
if (n.x < minX) minX = n.x;
|
|
1277
|
+
if (n.y < minY) minY = n.y;
|
|
1278
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1279
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1280
|
+
}
|
|
1281
|
+
const compEdges = edgesByComp.get(compId) ?? [];
|
|
1282
|
+
for (const e of compEdges) {
|
|
1283
|
+
for (const p of e.points) {
|
|
1284
|
+
if (p.x < minX) minX = p.x;
|
|
1285
|
+
if (p.y < minY) minY = p.y;
|
|
1286
|
+
if (p.x > maxX) maxX = p.x;
|
|
1287
|
+
if (p.y > maxY) maxY = p.y;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
boxes.push({ id: compId, nodes: group, edges: compEdges, minX, minY, maxX, maxY });
|
|
1291
|
+
}
|
|
1292
|
+
boxes.sort((a, b) => {
|
|
1293
|
+
const sizeA = isHorizontal ? a.maxX - a.minX : a.maxY - a.minY;
|
|
1294
|
+
const sizeB = isHorizontal ? b.maxX - b.minX : b.maxY - b.minY;
|
|
1295
|
+
return sizeB - sizeA;
|
|
1296
|
+
});
|
|
1297
|
+
const gap = options.layerSpacing;
|
|
1298
|
+
let cursor = 0;
|
|
1299
|
+
for (const box of boxes) {
|
|
1300
|
+
if (isHorizontal) {
|
|
1301
|
+
const dy = cursor - box.minY;
|
|
1302
|
+
const dx = -box.minX;
|
|
1303
|
+
for (const n of box.nodes) {
|
|
1304
|
+
n.x += dx;
|
|
1305
|
+
n.y += dy;
|
|
1306
|
+
for (const h of n.handles) {
|
|
1307
|
+
h.x += dx;
|
|
1308
|
+
h.y += dy;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
for (const e of box.edges) {
|
|
1312
|
+
for (const p of e.points) {
|
|
1313
|
+
p.x += dx;
|
|
1314
|
+
p.y += dy;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
cursor += box.maxY - box.minY + gap;
|
|
1318
|
+
} else {
|
|
1319
|
+
const dx = cursor - box.minX;
|
|
1320
|
+
const dy = -box.minY;
|
|
1321
|
+
for (const n of box.nodes) {
|
|
1322
|
+
n.x += dx;
|
|
1323
|
+
n.y += dy;
|
|
1324
|
+
for (const h of n.handles) {
|
|
1325
|
+
h.x += dx;
|
|
1326
|
+
h.y += dy;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
for (const e of box.edges) {
|
|
1330
|
+
for (const p of e.points) {
|
|
1331
|
+
p.x += dx;
|
|
1332
|
+
p.y += dy;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
cursor += box.maxX - box.minX + gap;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/proposals.ts
|
|
1341
|
+
function applyRotationProposals(input, options) {
|
|
1342
|
+
if (!options.onProposal) return input;
|
|
1343
|
+
const nodeIndex = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1344
|
+
const newNodes = input.nodes.map((node) => {
|
|
1345
|
+
const role = classifyNode(node, input.edges);
|
|
1346
|
+
const expected = expectedSidesFor(node, role, input.edges, nodeIndex, options.direction);
|
|
1347
|
+
const proposal = computeProposal(node, expected, options.direction, role);
|
|
1348
|
+
if (!proposal) return node;
|
|
1349
|
+
const accepted = options.onProposal(proposal);
|
|
1350
|
+
return accepted ?? node;
|
|
1351
|
+
});
|
|
1352
|
+
return { nodes: newNodes, edges: input.edges };
|
|
1353
|
+
}
|
|
1354
|
+
function classifyNode(node, edges) {
|
|
1355
|
+
const incident = edges.filter((e) => e.from === node.id || e.to === node.id);
|
|
1356
|
+
if (incident.length === 0) return { kind: "isolated" };
|
|
1357
|
+
const allData = incident.every((e) => (e.kind ?? "control") === "data");
|
|
1358
|
+
if (!allData) return { kind: "rail" };
|
|
1359
|
+
for (const e of incident) {
|
|
1360
|
+
const consumerId = e.from === node.id ? e.to : e.from;
|
|
1361
|
+
const consumerHandleId = e.from === node.id ? e.toHandle : e.fromHandle;
|
|
1362
|
+
const sideHint = sideHintFromHandle(
|
|
1363
|
+
consumerId,
|
|
1364
|
+
consumerHandleId,
|
|
1365
|
+
edges,
|
|
1366
|
+
/* recursing */
|
|
1367
|
+
false
|
|
1368
|
+
);
|
|
1369
|
+
if (sideHint) return { kind: "value", consumerSide: sideHint };
|
|
1370
|
+
}
|
|
1371
|
+
return { kind: "value", consumerSide: null };
|
|
1372
|
+
}
|
|
1373
|
+
function sideHintFromHandle(_consumerId, _handleId, _edges, _recursing) {
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
function expectedSidesFor(node, role, edges, index, direction) {
|
|
1377
|
+
if (role.kind === "isolated" || role.kind === "rail") {
|
|
1378
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1379
|
+
}
|
|
1380
|
+
const consumerHandleSide = findConsumerHandleSide(node, edges, index);
|
|
1381
|
+
if (!consumerHandleSide) {
|
|
1382
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1383
|
+
}
|
|
1384
|
+
const isVerticalFlow = direction === "TB" || direction === "BT";
|
|
1385
|
+
let outputSide;
|
|
1386
|
+
if (isVerticalFlow) {
|
|
1387
|
+
if (consumerHandleSide === "left") outputSide = "right";
|
|
1388
|
+
else if (consumerHandleSide === "right") outputSide = "left";
|
|
1389
|
+
else outputSide = "right";
|
|
1390
|
+
} else {
|
|
1391
|
+
if (consumerHandleSide === "top") outputSide = "bottom";
|
|
1392
|
+
else if (consumerHandleSide === "bottom") outputSide = "top";
|
|
1393
|
+
else outputSide = "bottom";
|
|
1394
|
+
}
|
|
1395
|
+
return { input: outputSide, output: outputSide };
|
|
1396
|
+
}
|
|
1397
|
+
function findConsumerHandleSide(value, edges, index) {
|
|
1398
|
+
for (const e of edges) {
|
|
1399
|
+
const isFromValue = e.from === value.id;
|
|
1400
|
+
const isToValue = e.to === value.id;
|
|
1401
|
+
if (!isFromValue && !isToValue) continue;
|
|
1402
|
+
const consumerId = isFromValue ? e.to : e.from;
|
|
1403
|
+
const consumer = index.get(consumerId);
|
|
1404
|
+
if (!consumer) continue;
|
|
1405
|
+
const consumerHandleId = isFromValue ? e.toHandle : e.fromHandle;
|
|
1406
|
+
const consumerHandle = consumer.handles.find((h) => h.id === consumerHandleId);
|
|
1407
|
+
if (consumerHandle) return consumerHandle.position;
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
function computeProposal(node, expected, direction, role) {
|
|
1412
|
+
if (node.handles.length === 0) return null;
|
|
1413
|
+
const currentScore = score(node.handles, expected);
|
|
1414
|
+
if (currentScore.matchRatio >= 1) return null;
|
|
1415
|
+
const candidates = [];
|
|
1416
|
+
for (const rot of [90, -90, 180]) {
|
|
1417
|
+
const rotated = rotateHandles(node.handles, rot);
|
|
1418
|
+
candidates.push({ rotation: rot, score: score(rotated, expected) });
|
|
1419
|
+
}
|
|
1420
|
+
candidates.sort((a, b) => b.score.matchRatio - a.score.matchRatio);
|
|
1421
|
+
const best = candidates[0];
|
|
1422
|
+
if (best.score.matchRatio <= currentScore.matchRatio) return null;
|
|
1423
|
+
return buildProposal(node, best.rotation, currentScore, best.score, direction, expected, role);
|
|
1424
|
+
}
|
|
1425
|
+
function buildProposal(node, rotation, current, proposedScore, direction, expected, role) {
|
|
1426
|
+
const proposed = { ...node, handles: rotateHandles(node.handles, rotation) };
|
|
1427
|
+
const roleDesc = role.kind === "value" ? `value node should face ${expected.output} (its sidecar lands opposite the consumer's handle)` : `direction ${direction} expects inputs on ${expected.input} and outputs on ${expected.output}`;
|
|
1428
|
+
return {
|
|
1429
|
+
type: "rotate",
|
|
1430
|
+
nodeId: node.id,
|
|
1431
|
+
current: node,
|
|
1432
|
+
proposed,
|
|
1433
|
+
rotation,
|
|
1434
|
+
reason: `${roleDesc}; ${current.matched}/${current.total} handles match, ${proposedScore.matched}/${proposedScore.total} after rotation`
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function inputSideFor(direction) {
|
|
1438
|
+
switch (direction) {
|
|
1439
|
+
case "TB":
|
|
1440
|
+
return "top";
|
|
1441
|
+
case "BT":
|
|
1442
|
+
return "bottom";
|
|
1443
|
+
case "LR":
|
|
1444
|
+
return "left";
|
|
1445
|
+
case "RL":
|
|
1446
|
+
return "right";
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function outputSideFor(direction) {
|
|
1450
|
+
switch (direction) {
|
|
1451
|
+
case "TB":
|
|
1452
|
+
return "bottom";
|
|
1453
|
+
case "BT":
|
|
1454
|
+
return "top";
|
|
1455
|
+
case "LR":
|
|
1456
|
+
return "right";
|
|
1457
|
+
case "RL":
|
|
1458
|
+
return "left";
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function score(handles, expected) {
|
|
1462
|
+
let matched = 0;
|
|
1463
|
+
let total = 0;
|
|
1464
|
+
for (const h of handles) {
|
|
1465
|
+
total++;
|
|
1466
|
+
if (h.type === "input" && h.position === expected.input) matched++;
|
|
1467
|
+
else if (h.type === "output" && h.position === expected.output) matched++;
|
|
1468
|
+
}
|
|
1469
|
+
return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
|
|
1470
|
+
}
|
|
1471
|
+
function applyRelocateProposals(input, preview, options) {
|
|
1472
|
+
if (!options.onProposal) return input;
|
|
1473
|
+
const previewIndex = new Map(preview.nodes.map((n) => [n.id, n]));
|
|
1474
|
+
const inputIndex = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1475
|
+
const newNodes = input.nodes.map((node) => {
|
|
1476
|
+
const proposal = computeRelocateProposal(node, input.edges, inputIndex, previewIndex);
|
|
1477
|
+
if (!proposal) return node;
|
|
1478
|
+
const accepted = options.onProposal(proposal);
|
|
1479
|
+
return accepted ?? node;
|
|
1480
|
+
});
|
|
1481
|
+
return { nodes: newNodes, edges: input.edges };
|
|
1482
|
+
}
|
|
1483
|
+
function computeRelocateProposal(node, edges, inputIndex, previewIndex) {
|
|
1484
|
+
const selfPreview = previewIndex.get(node.id);
|
|
1485
|
+
if (!selfPreview) return null;
|
|
1486
|
+
if (node.handles.length === 0) return null;
|
|
1487
|
+
const usedHandleIds = /* @__PURE__ */ new Set();
|
|
1488
|
+
for (const e of edges) {
|
|
1489
|
+
if (e.from === node.id) usedHandleIds.add(e.fromHandle);
|
|
1490
|
+
if (e.to === node.id) usedHandleIds.add(e.toHandle);
|
|
1491
|
+
}
|
|
1492
|
+
if (usedHandleIds.size === 0) return null;
|
|
1493
|
+
const votesByHandle = /* @__PURE__ */ new Map();
|
|
1494
|
+
const selfCenter = {
|
|
1495
|
+
x: selfPreview.x + selfPreview.width / 2,
|
|
1496
|
+
y: selfPreview.y + selfPreview.height / 2
|
|
1497
|
+
};
|
|
1498
|
+
for (const e of edges) {
|
|
1499
|
+
const isFrom = e.from === node.id;
|
|
1500
|
+
const isTo = e.to === node.id;
|
|
1501
|
+
if (!isFrom && !isTo) continue;
|
|
1502
|
+
const handleId = isFrom ? e.fromHandle : e.toHandle;
|
|
1503
|
+
const neighborId = isFrom ? e.to : e.from;
|
|
1504
|
+
const neighborPreview = previewIndex.get(neighborId);
|
|
1505
|
+
if (!neighborPreview) continue;
|
|
1506
|
+
const neighborCenter = {
|
|
1507
|
+
x: neighborPreview.x + neighborPreview.width / 2,
|
|
1508
|
+
y: neighborPreview.y + neighborPreview.height / 2
|
|
1509
|
+
};
|
|
1510
|
+
const dx = neighborCenter.x - selfCenter.x;
|
|
1511
|
+
const dy = neighborCenter.y - selfCenter.y;
|
|
1512
|
+
const side = Math.abs(dx) >= Math.abs(dy) ? dx >= 0 ? "right" : "left" : dy >= 0 ? "bottom" : "top";
|
|
1513
|
+
const dist = Math.hypot(dx, dy);
|
|
1514
|
+
const weight = dist > 0 ? 1 / dist : 1;
|
|
1515
|
+
const arr = votesByHandle.get(handleId) ?? [];
|
|
1516
|
+
arr.push({ side, weight });
|
|
1517
|
+
votesByHandle.set(handleId, arr);
|
|
1518
|
+
}
|
|
1519
|
+
const changes = {};
|
|
1520
|
+
const newHandles = node.handles.map((h) => {
|
|
1521
|
+
const votes = votesByHandle.get(h.id);
|
|
1522
|
+
if (!votes || votes.length === 0) return h;
|
|
1523
|
+
const tallies = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1524
|
+
for (const v of votes) tallies[v.side] += v.weight;
|
|
1525
|
+
const best = Object.entries(tallies).reduce((acc, cur) => cur[1] > acc[1] ? cur : acc, ["top", -Infinity])[0];
|
|
1526
|
+
if (best !== h.position) {
|
|
1527
|
+
changes[h.id] = { from: h.position, to: best };
|
|
1528
|
+
return { ...h, position: best };
|
|
1529
|
+
}
|
|
1530
|
+
return h;
|
|
1531
|
+
});
|
|
1532
|
+
if (Object.keys(changes).length === 0) return null;
|
|
1533
|
+
const proposed = { ...node, handles: newHandles };
|
|
1534
|
+
const summary = Object.entries(changes).map(([id, c]) => `${id}: ${c.from}\u2192${c.to}`).join(", ");
|
|
1535
|
+
return {
|
|
1536
|
+
type: "relocate-handles",
|
|
1537
|
+
nodeId: node.id,
|
|
1538
|
+
current: node,
|
|
1539
|
+
proposed,
|
|
1540
|
+
changes,
|
|
1541
|
+
reason: `${Object.keys(changes).length} handle(s) point away from their neighbor (${summary})`
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function rotateHandles(handles, rot) {
|
|
1545
|
+
const rotateSide = (s) => {
|
|
1546
|
+
if (rot === 180) {
|
|
1547
|
+
return s === "top" ? "bottom" : s === "bottom" ? "top" : s === "left" ? "right" : "left";
|
|
1548
|
+
}
|
|
1549
|
+
if (rot === 90) {
|
|
1550
|
+
return s === "top" ? "right" : s === "right" ? "bottom" : s === "bottom" ? "left" : "top";
|
|
1551
|
+
}
|
|
1552
|
+
return s === "top" ? "left" : s === "left" ? "bottom" : s === "bottom" ? "right" : "top";
|
|
1553
|
+
};
|
|
1554
|
+
return handles.map((h) => ({ ...h, position: rotateSide(h.position) }));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
854
1557
|
// src/layout.ts
|
|
1558
|
+
var COMPOUND_HEADER = 28;
|
|
855
1559
|
function layout(input, options) {
|
|
856
1560
|
const resolved = resolveOptions(options);
|
|
857
|
-
|
|
858
|
-
|
|
1561
|
+
if (input.nodes.length === 0) return { nodes: [], edges: [] };
|
|
1562
|
+
let adjusted = applyRotationProposals(input, resolved);
|
|
1563
|
+
if (resolved.onProposal) {
|
|
1564
|
+
const previewOptions = { ...resolved, onProposal: void 0 };
|
|
1565
|
+
const preview = layoutCompound(adjusted, previewOptions);
|
|
1566
|
+
adjusted = applyRelocateProposals(adjusted, preview, resolved);
|
|
1567
|
+
}
|
|
1568
|
+
return layoutCompound(adjusted, resolved);
|
|
859
1569
|
}
|
|
860
1570
|
function computeLayout(graph, options) {
|
|
861
|
-
if (graph.nodes.size === 0) {
|
|
862
|
-
return { nodes: [], edges: [] };
|
|
863
|
-
}
|
|
1571
|
+
if (graph.nodes.size === 0) return { nodes: [], edges: [] };
|
|
864
1572
|
breakCycles(graph);
|
|
865
1573
|
let layers = assignLayers(graph);
|
|
866
1574
|
layers = insertDummyNodes(graph, layers);
|
|
867
|
-
|
|
868
|
-
|
|
1575
|
+
let rail = railLayers(layers, graph);
|
|
1576
|
+
rail = minimizeCrossings(
|
|
1577
|
+
graph,
|
|
1578
|
+
rail,
|
|
1579
|
+
options.crossingMinimizationIterations,
|
|
1580
|
+
options.skipTranspose
|
|
1581
|
+
);
|
|
1582
|
+
assignCoordinates(graph, rail, options);
|
|
1583
|
+
placeValueSidecars(graph, layers, options);
|
|
869
1584
|
const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
|
|
870
1585
|
return buildResult(graph, routedEdges);
|
|
871
1586
|
}
|
|
@@ -876,13 +1591,7 @@ function buildResult(graph, routedEdges) {
|
|
|
876
1591
|
if (node.isDummy) continue;
|
|
877
1592
|
const handles = node.handles.map((h) => {
|
|
878
1593
|
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
|
-
};
|
|
1594
|
+
return { id: h.id, type: h.type, position: h.position, x: pos.x, y: pos.y };
|
|
886
1595
|
});
|
|
887
1596
|
nodes.push({
|
|
888
1597
|
id: node.id,
|
|
@@ -890,7 +1599,8 @@ function buildResult(graph, routedEdges) {
|
|
|
890
1599
|
y: node.y,
|
|
891
1600
|
width: node.width,
|
|
892
1601
|
height: node.height,
|
|
893
|
-
handles
|
|
1602
|
+
handles,
|
|
1603
|
+
parentId: node.parentId
|
|
894
1604
|
});
|
|
895
1605
|
}
|
|
896
1606
|
for (const route of routedEdges) {
|
|
@@ -900,11 +1610,146 @@ function buildResult(graph, routedEdges) {
|
|
|
900
1610
|
to: route.to,
|
|
901
1611
|
fromHandle: route.fromHandle,
|
|
902
1612
|
toHandle: route.toHandle,
|
|
903
|
-
points: route.points
|
|
1613
|
+
points: route.points,
|
|
1614
|
+
kind: route.kind
|
|
904
1615
|
});
|
|
905
1616
|
}
|
|
906
1617
|
return { nodes, edges };
|
|
907
1618
|
}
|
|
1619
|
+
function layoutCompound(input, options) {
|
|
1620
|
+
const nodeMap = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1621
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1622
|
+
const rootNodes = [];
|
|
1623
|
+
for (const n of input.nodes) {
|
|
1624
|
+
if (n.parentId && nodeMap.has(n.parentId)) {
|
|
1625
|
+
const arr = childrenByParent.get(n.parentId) ?? [];
|
|
1626
|
+
arr.push(n);
|
|
1627
|
+
childrenByParent.set(n.parentId, arr);
|
|
1628
|
+
} else {
|
|
1629
|
+
rootNodes.push(n);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
const compoundIds = new Set(childrenByParent.keys());
|
|
1633
|
+
const depthCache = /* @__PURE__ */ new Map();
|
|
1634
|
+
function depthOf(id) {
|
|
1635
|
+
if (depthCache.has(id)) return depthCache.get(id);
|
|
1636
|
+
const n = nodeMap.get(id);
|
|
1637
|
+
const d = n?.parentId && nodeMap.has(n.parentId) ? depthOf(n.parentId) + 1 : 0;
|
|
1638
|
+
depthCache.set(id, d);
|
|
1639
|
+
return d;
|
|
1640
|
+
}
|
|
1641
|
+
for (const n of input.nodes) depthOf(n.id);
|
|
1642
|
+
const sortedCompounds = [...compoundIds].sort((a, b) => depthOf(b) - depthOf(a));
|
|
1643
|
+
const subLayouts = /* @__PURE__ */ new Map();
|
|
1644
|
+
const compoundSize = /* @__PURE__ */ new Map();
|
|
1645
|
+
for (const compoundId of sortedCompounds) {
|
|
1646
|
+
const children = childrenByParent.get(compoundId) ?? [];
|
|
1647
|
+
if (children.length === 0) continue;
|
|
1648
|
+
const childIdSet = new Set(children.map((c) => c.id));
|
|
1649
|
+
const subEdges = input.edges.filter((e) => childIdSet.has(e.from) && childIdSet.has(e.to));
|
|
1650
|
+
const sizedChildren = children.map((c) => {
|
|
1651
|
+
const sz = compoundSize.get(c.id);
|
|
1652
|
+
return sz ? { ...c, width: sz.width, height: sz.height } : c;
|
|
1653
|
+
});
|
|
1654
|
+
const subOptions = { ...options, packComponents: false };
|
|
1655
|
+
const subInput = { nodes: sizedChildren.map(stripParent), edges: subEdges };
|
|
1656
|
+
const subResult = layoutFlat(subInput, subOptions);
|
|
1657
|
+
subLayouts.set(compoundId, subResult);
|
|
1658
|
+
const bbox = computeBoundingBox(subResult.nodes);
|
|
1659
|
+
const innerW = bbox.width + options.compoundPadding * 2;
|
|
1660
|
+
const innerH = bbox.height + options.compoundPadding * 2 + COMPOUND_HEADER;
|
|
1661
|
+
const original = nodeMap.get(compoundId);
|
|
1662
|
+
compoundSize.set(compoundId, {
|
|
1663
|
+
width: Math.max(innerW, original.width),
|
|
1664
|
+
height: Math.max(innerH, original.height)
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
const sizedRootNodes = rootNodes.map((n) => {
|
|
1668
|
+
const sz = compoundSize.get(n.id);
|
|
1669
|
+
return sz ? { ...n, width: sz.width, height: sz.height } : n;
|
|
1670
|
+
});
|
|
1671
|
+
const rootIdSet = new Set(rootNodes.map((n) => n.id));
|
|
1672
|
+
const rootEdges = input.edges.filter((e) => {
|
|
1673
|
+
return rootIdSet.has(e.from) && rootIdSet.has(e.to);
|
|
1674
|
+
});
|
|
1675
|
+
const rootResult = layoutFlat({ nodes: sizedRootNodes.map(stripParent), edges: rootEdges }, options);
|
|
1676
|
+
const finalNodes = [];
|
|
1677
|
+
const finalEdges = [];
|
|
1678
|
+
for (const n of rootResult.nodes) {
|
|
1679
|
+
const wasCompound = compoundIds.has(n.id);
|
|
1680
|
+
finalNodes.push({
|
|
1681
|
+
...n,
|
|
1682
|
+
parentId: nodeMap.get(n.id)?.parentId
|
|
1683
|
+
});
|
|
1684
|
+
if (wasCompound) {
|
|
1685
|
+
placeCompoundChildren(n, (compoundId) => subLayouts.get(compoundId), finalNodes, finalEdges, options, nodeMap);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
finalEdges.push(...rootResult.edges);
|
|
1689
|
+
if (options.packComponents) {
|
|
1690
|
+
packComponents(finalNodes, finalEdges, options);
|
|
1691
|
+
}
|
|
1692
|
+
return { nodes: finalNodes, edges: finalEdges };
|
|
1693
|
+
}
|
|
1694
|
+
function placeCompoundChildren(parent, getSub, finalNodes, finalEdges, options, nodeMap) {
|
|
1695
|
+
const sub = getSub(parent.id);
|
|
1696
|
+
if (!sub) return;
|
|
1697
|
+
const bbox = computeBoundingBox(sub.nodes);
|
|
1698
|
+
const dx = parent.x + options.compoundPadding - bbox.minX;
|
|
1699
|
+
const dy = parent.y + options.compoundPadding + COMPOUND_HEADER - bbox.minY;
|
|
1700
|
+
const availableW = parent.width - options.compoundPadding * 2;
|
|
1701
|
+
const slackX = (availableW - bbox.width) / 2;
|
|
1702
|
+
const availableH = parent.height - options.compoundPadding * 2 - COMPOUND_HEADER;
|
|
1703
|
+
const slackY = (availableH - bbox.height) / 2;
|
|
1704
|
+
const cx = dx + Math.max(0, slackX);
|
|
1705
|
+
const cy = dy + Math.max(0, slackY);
|
|
1706
|
+
for (const child of sub.nodes) {
|
|
1707
|
+
const placed = {
|
|
1708
|
+
...child,
|
|
1709
|
+
x: child.x + cx,
|
|
1710
|
+
y: child.y + cy,
|
|
1711
|
+
handles: child.handles.map((h) => ({ ...h, x: h.x + cx, y: h.y + cy })),
|
|
1712
|
+
parentId: nodeMap.get(child.id)?.parentId ?? parent.id
|
|
1713
|
+
};
|
|
1714
|
+
finalNodes.push(placed);
|
|
1715
|
+
if (getSub(child.id)) {
|
|
1716
|
+
placeCompoundChildren(placed, getSub, finalNodes, finalEdges, options, nodeMap);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
for (const edge of sub.edges) {
|
|
1720
|
+
finalEdges.push({
|
|
1721
|
+
...edge,
|
|
1722
|
+
points: edge.points.map((p) => ({ x: p.x + cx, y: p.y + cy }))
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function stripParent(n) {
|
|
1727
|
+
const { parentId: _ignored, ...rest } = n;
|
|
1728
|
+
return rest;
|
|
1729
|
+
}
|
|
1730
|
+
function layoutFlat(input, options) {
|
|
1731
|
+
const graph = buildGraph(input.nodes, input.edges, {
|
|
1732
|
+
controlWeight: options.controlWeight,
|
|
1733
|
+
dataWeight: options.dataWeight
|
|
1734
|
+
});
|
|
1735
|
+
return computeLayout(graph, options);
|
|
1736
|
+
}
|
|
1737
|
+
function computeBoundingBox(nodes) {
|
|
1738
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1739
|
+
for (const n of nodes) {
|
|
1740
|
+
if (n.x < minX) minX = n.x;
|
|
1741
|
+
if (n.y < minY) minY = n.y;
|
|
1742
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1743
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1744
|
+
}
|
|
1745
|
+
if (!isFinite(minX)) {
|
|
1746
|
+
minX = 0;
|
|
1747
|
+
minY = 0;
|
|
1748
|
+
maxX = 0;
|
|
1749
|
+
maxY = 0;
|
|
1750
|
+
}
|
|
1751
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
1752
|
+
}
|
|
908
1753
|
|
|
909
1754
|
// src/incremental.ts
|
|
910
1755
|
var IncrementalLayout = class {
|
|
@@ -913,82 +1758,40 @@ var IncrementalLayout = class {
|
|
|
913
1758
|
this.inputEdges = /* @__PURE__ */ new Map();
|
|
914
1759
|
this.lastResult = null;
|
|
915
1760
|
this.nodePositions = /* @__PURE__ */ new Map();
|
|
916
|
-
this.options =
|
|
1761
|
+
this.options = options;
|
|
1762
|
+
this.resolvedOptions = resolveOptions(options);
|
|
917
1763
|
}
|
|
918
|
-
/**
|
|
919
|
-
* Set the full graph and compute a complete layout.
|
|
920
|
-
*/
|
|
921
1764
|
setGraph(input) {
|
|
922
1765
|
this.inputNodes.clear();
|
|
923
1766
|
this.inputEdges.clear();
|
|
924
|
-
for (const node of input.nodes)
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
for (const edge of input.edges) {
|
|
928
|
-
this.inputEdges.set(edge.id, edge);
|
|
929
|
-
}
|
|
1767
|
+
for (const node of input.nodes) this.inputNodes.set(node.id, node);
|
|
1768
|
+
for (const edge of input.edges) this.inputEdges.set(edge.id, edge);
|
|
930
1769
|
return this.recompute();
|
|
931
1770
|
}
|
|
932
|
-
/**
|
|
933
|
-
* Add nodes and edges incrementally.
|
|
934
|
-
* Attempts to minimize layout changes for existing nodes.
|
|
935
|
-
*/
|
|
936
1771
|
addNodes(nodes, edges) {
|
|
937
|
-
for (const node of nodes)
|
|
938
|
-
|
|
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
|
-
);
|
|
1772
|
+
for (const node of nodes) this.inputNodes.set(node.id, node);
|
|
1773
|
+
if (edges) for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1774
|
+
return this.recomputeWithStability(new Set(nodes.map((n) => n.id)));
|
|
949
1775
|
}
|
|
950
|
-
/**
|
|
951
|
-
* Remove nodes (and their connected edges) from the layout.
|
|
952
|
-
*/
|
|
953
1776
|
removeNodes(nodeIds) {
|
|
954
1777
|
const removedSet = new Set(nodeIds);
|
|
955
|
-
for (const id of nodeIds)
|
|
956
|
-
this.inputNodes.delete(id);
|
|
957
|
-
}
|
|
1778
|
+
for (const id of nodeIds) this.inputNodes.delete(id);
|
|
958
1779
|
for (const [edgeId, edge] of this.inputEdges) {
|
|
959
1780
|
if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
|
|
960
1781
|
this.inputEdges.delete(edgeId);
|
|
961
1782
|
}
|
|
962
1783
|
}
|
|
963
|
-
for (const id of nodeIds)
|
|
964
|
-
this.nodePositions.delete(id);
|
|
965
|
-
}
|
|
1784
|
+
for (const id of nodeIds) this.nodePositions.delete(id);
|
|
966
1785
|
return this.recompute();
|
|
967
1786
|
}
|
|
968
|
-
/**
|
|
969
|
-
* Add edges between existing nodes.
|
|
970
|
-
*/
|
|
971
1787
|
addEdges(edges) {
|
|
972
|
-
for (const edge of edges)
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
return this.recomputeIncremental(
|
|
976
|
-
/* @__PURE__ */ new Set(),
|
|
977
|
-
new Set(edges.map((e) => e.id))
|
|
978
|
-
);
|
|
1788
|
+
for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1789
|
+
return this.recomputeWithStability(/* @__PURE__ */ new Set());
|
|
979
1790
|
}
|
|
980
|
-
/**
|
|
981
|
-
* Remove edges from the layout.
|
|
982
|
-
*/
|
|
983
1791
|
removeEdges(edgeIds) {
|
|
984
|
-
for (const id of edgeIds)
|
|
985
|
-
this.inputEdges.delete(id);
|
|
986
|
-
}
|
|
1792
|
+
for (const id of edgeIds) this.inputEdges.delete(id);
|
|
987
1793
|
return this.recompute();
|
|
988
1794
|
}
|
|
989
|
-
/**
|
|
990
|
-
* Get the current layout result.
|
|
991
|
-
*/
|
|
992
1795
|
getResult() {
|
|
993
1796
|
return this.lastResult;
|
|
994
1797
|
}
|
|
@@ -997,42 +1800,48 @@ var IncrementalLayout = class {
|
|
|
997
1800
|
nodes: [...this.inputNodes.values()],
|
|
998
1801
|
edges: [...this.inputEdges.values()]
|
|
999
1802
|
};
|
|
1000
|
-
|
|
1001
|
-
this.lastResult = computeLayout(graph, this.options);
|
|
1803
|
+
this.lastResult = layout(input, this.options);
|
|
1002
1804
|
this.cachePositions();
|
|
1003
1805
|
return this.lastResult;
|
|
1004
1806
|
}
|
|
1005
|
-
|
|
1807
|
+
recomputeWithStability(newNodeIds) {
|
|
1006
1808
|
const input = {
|
|
1007
1809
|
nodes: [...this.inputNodes.values()],
|
|
1008
1810
|
edges: [...this.inputEdges.values()]
|
|
1009
1811
|
};
|
|
1010
|
-
const
|
|
1011
|
-
|
|
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);
|
|
1812
|
+
const fresh = layout(input, this.options);
|
|
1813
|
+
this.lastResult = this.applyStability(fresh, newNodeIds);
|
|
1019
1814
|
this.cachePositions();
|
|
1020
1815
|
return this.lastResult;
|
|
1021
1816
|
}
|
|
1022
1817
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1818
|
+
* Blend the freshly computed positions with the previous ones, so existing
|
|
1819
|
+
* nodes don't jump too far when a small change happens.
|
|
1025
1820
|
*/
|
|
1026
|
-
applyStability(
|
|
1027
|
-
if (this.nodePositions.size === 0) return;
|
|
1821
|
+
applyStability(fresh, newNodeIds) {
|
|
1822
|
+
if (this.nodePositions.size === 0) return fresh;
|
|
1028
1823
|
const STABILITY_WEIGHT = 0.3;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1824
|
+
const blended = {
|
|
1825
|
+
nodes: fresh.nodes.map((n) => {
|
|
1826
|
+
if (newNodeIds.has(n.id)) return n;
|
|
1827
|
+
const old = this.nodePositions.get(n.id);
|
|
1828
|
+
if (!old) return n;
|
|
1829
|
+
const dx = old.x - n.x;
|
|
1830
|
+
const dy = old.y - n.y;
|
|
1831
|
+
const x = n.x + dx * STABILITY_WEIGHT;
|
|
1832
|
+
const y = n.y + dy * STABILITY_WEIGHT;
|
|
1833
|
+
const ddx = x - n.x;
|
|
1834
|
+
const ddy = y - n.y;
|
|
1835
|
+
return {
|
|
1836
|
+
...n,
|
|
1837
|
+
x,
|
|
1838
|
+
y,
|
|
1839
|
+
handles: n.handles.map((h) => ({ ...h, x: h.x + ddx, y: h.y + ddy }))
|
|
1840
|
+
};
|
|
1841
|
+
}),
|
|
1842
|
+
edges: fresh.edges
|
|
1843
|
+
};
|
|
1844
|
+
return blended;
|
|
1036
1845
|
}
|
|
1037
1846
|
cachePositions() {
|
|
1038
1847
|
if (!this.lastResult) return;
|
|
@@ -1041,46 +1850,160 @@ var IncrementalLayout = class {
|
|
|
1041
1850
|
this.nodePositions.set(node.id, { x: node.x, y: node.y });
|
|
1042
1851
|
}
|
|
1043
1852
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
// src/debug.ts
|
|
1856
|
+
function printLayout(result, options = {}) {
|
|
1857
|
+
const lines = [];
|
|
1858
|
+
const { nodes, edges } = result;
|
|
1859
|
+
if (nodes.length === 0) return "(empty layout)";
|
|
1860
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1861
|
+
for (const n of nodes) {
|
|
1862
|
+
if (n.x < minX) minX = n.x;
|
|
1863
|
+
if (n.y < minY) minY = n.y;
|
|
1864
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1865
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1866
|
+
}
|
|
1867
|
+
lines.push(`=== layout ${nodes.length} nodes, ${edges.length} edges ===`);
|
|
1868
|
+
lines.push(`bbox: x=[${minX.toFixed(0)}..${maxX.toFixed(0)}] y=[${minY.toFixed(0)}..${maxY.toFixed(0)}] (${(maxX - minX).toFixed(0)} x ${(maxY - minY).toFixed(0)})`);
|
|
1869
|
+
const byY = [...nodes].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
1870
|
+
const bands = [];
|
|
1871
|
+
for (const n of byY) {
|
|
1872
|
+
const placed = bands.find((b) => Math.abs(b[0].y - n.y) < 1);
|
|
1873
|
+
if (placed) placed.push(n);
|
|
1874
|
+
else bands.push([n]);
|
|
1875
|
+
}
|
|
1876
|
+
lines.push("");
|
|
1877
|
+
lines.push("--- Y bands ---");
|
|
1878
|
+
for (const band of bands) {
|
|
1879
|
+
const sorted = [...band].sort((a, b) => a.x - b.x);
|
|
1880
|
+
const summary = sorted.map((n) => {
|
|
1881
|
+
const p = n.parentId ? `[${n.parentId}/]` : "";
|
|
1882
|
+
return `${p}${n.id}@(${n.x.toFixed(0)},${n.y.toFixed(0)} ${n.width}x${n.height})`;
|
|
1883
|
+
}).join(" ");
|
|
1884
|
+
lines.push(` y=${band[0].y.toFixed(0).padStart(4)} : ${summary}`);
|
|
1885
|
+
}
|
|
1886
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1887
|
+
for (const n of nodes) {
|
|
1888
|
+
const key = n.parentId;
|
|
1889
|
+
const arr = byParent.get(key) ?? [];
|
|
1890
|
+
arr.push(n);
|
|
1891
|
+
byParent.set(key, arr);
|
|
1892
|
+
}
|
|
1893
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1894
|
+
for (const k of byParent.keys()) {
|
|
1895
|
+
if (k && nodes.find((n) => n.id === k)) compoundIds.add(k);
|
|
1896
|
+
}
|
|
1897
|
+
if (compoundIds.size > 0) {
|
|
1898
|
+
let printSubtree2 = function(id, depth) {
|
|
1899
|
+
const children = byParent.get(id) ?? [];
|
|
1900
|
+
for (const c of children) {
|
|
1901
|
+
const prefix = " ".repeat(depth);
|
|
1902
|
+
const tag = compoundIds.has(c.id) ? " (compound)" : "";
|
|
1903
|
+
lines.push(`${prefix}- ${c.id}${tag} bbox=(${c.x.toFixed(0)},${c.y.toFixed(0)})..(${(c.x + c.width).toFixed(0)},${(c.y + c.height).toFixed(0)})`);
|
|
1904
|
+
if (compoundIds.has(c.id)) printSubtree2(c.id, depth + 1);
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
var printSubtree = printSubtree2;
|
|
1908
|
+
lines.push("");
|
|
1909
|
+
lines.push("--- hierarchy ---");
|
|
1910
|
+
printSubtree2(void 0, 0);
|
|
1911
|
+
}
|
|
1912
|
+
lines.push("");
|
|
1913
|
+
lines.push("--- edges ---");
|
|
1914
|
+
for (const e of edges) {
|
|
1915
|
+
const head = e.points[0];
|
|
1916
|
+
const tail = e.points[e.points.length - 1];
|
|
1917
|
+
lines.push(` [${e.kind}] ${e.from}.${e.fromHandle} \u2192 ${e.to}.${e.toHandle} (${head.x.toFixed(0)},${head.y.toFixed(0)}) \u2192 (${tail.x.toFixed(0)},${tail.y.toFixed(0)}) via ${e.points.length} pts`);
|
|
1918
|
+
}
|
|
1919
|
+
const overlaps = findOverlaps(nodes);
|
|
1920
|
+
if (overlaps.length > 0) {
|
|
1921
|
+
lines.push("");
|
|
1922
|
+
lines.push("--- OVERLAPS (problem!) ---");
|
|
1923
|
+
for (const o of overlaps) {
|
|
1924
|
+
lines.push(` ${o.a} overlaps ${o.b}`);
|
|
1067
1925
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1926
|
+
}
|
|
1927
|
+
if (options.grid !== false) {
|
|
1928
|
+
lines.push("");
|
|
1929
|
+
lines.push("--- ASCII grid ---");
|
|
1930
|
+
lines.push(asciiGrid(nodes, edges, options));
|
|
1931
|
+
}
|
|
1932
|
+
return lines.join("\n");
|
|
1933
|
+
}
|
|
1934
|
+
function findOverlaps(nodes) {
|
|
1935
|
+
const out = [];
|
|
1936
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1937
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1938
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1939
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1940
|
+
const a = nodes[i];
|
|
1941
|
+
const b = nodes[j];
|
|
1942
|
+
if (a.id === b.parentId || b.id === a.parentId) continue;
|
|
1943
|
+
const overlapX = a.x < b.x + b.width && b.x < a.x + a.width;
|
|
1944
|
+
const overlapY = a.y < b.y + b.height && b.y < a.y + a.height;
|
|
1945
|
+
if (overlapX && overlapY) {
|
|
1946
|
+
out.push({ a: a.id, b: b.id });
|
|
1947
|
+
}
|
|
1077
1948
|
}
|
|
1078
|
-
return { nodes, edges };
|
|
1079
1949
|
}
|
|
1080
|
-
|
|
1950
|
+
return out;
|
|
1951
|
+
}
|
|
1952
|
+
function asciiGrid(nodes, edges, options) {
|
|
1953
|
+
if (nodes.length === 0) return "";
|
|
1954
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1955
|
+
for (const n of nodes) {
|
|
1956
|
+
if (n.x < minX) minX = n.x;
|
|
1957
|
+
if (n.y < minY) minY = n.y;
|
|
1958
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1959
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1960
|
+
}
|
|
1961
|
+
for (const e of edges) for (const p of e.points) {
|
|
1962
|
+
if (p.x < minX) minX = p.x;
|
|
1963
|
+
if (p.y < minY) minY = p.y;
|
|
1964
|
+
if (p.x > maxX) maxX = p.x;
|
|
1965
|
+
if (p.y > maxY) maxY = p.y;
|
|
1966
|
+
}
|
|
1967
|
+
const targetW = options.gridWidth ?? 80;
|
|
1968
|
+
const layoutW = Math.max(1, maxX - minX);
|
|
1969
|
+
const layoutH = Math.max(1, maxY - minY);
|
|
1970
|
+
const scaleX = options.gridScale ?? targetW / layoutW;
|
|
1971
|
+
const scaleY = scaleX * 0.5;
|
|
1972
|
+
const w = Math.max(1, Math.ceil(layoutW * scaleX) + 1);
|
|
1973
|
+
const h = Math.max(1, Math.ceil(layoutH * scaleY) + 1);
|
|
1974
|
+
if (h > 80) return "(grid suppressed: too tall \u2014 pass {grid:false} to skip)";
|
|
1975
|
+
const grid = Array.from({ length: h }, () => Array.from({ length: w }, () => " "));
|
|
1976
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1977
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1978
|
+
for (const n of nodes) {
|
|
1979
|
+
const x0 = Math.round((n.x - minX) * scaleX);
|
|
1980
|
+
const y0 = Math.round((n.y - minY) * scaleY);
|
|
1981
|
+
const x1 = Math.max(x0, Math.round((n.x + n.width - minX) * scaleX) - 1);
|
|
1982
|
+
const y1 = Math.max(y0, Math.round((n.y + n.height - minY) * scaleY) - 1);
|
|
1983
|
+
const isCompound = compoundIds.has(n.id);
|
|
1984
|
+
const ch = isCompound ? "." : "#";
|
|
1985
|
+
for (let y = y0; y <= y1 && y < h; y++) {
|
|
1986
|
+
for (let x = x0; x <= x1 && x < w; x++) {
|
|
1987
|
+
if (y < 0 || x < 0) continue;
|
|
1988
|
+
if (y === y0 || y === y1 || x === x0 || x === x1) grid[y][x] = ch;
|
|
1989
|
+
else if (isCompound) {
|
|
1990
|
+
} else grid[y][x] = ch;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const label = n.id.slice(0, Math.min(n.id.length, Math.max(2, x1 - x0 - 1)));
|
|
1994
|
+
const lx = Math.max(0, x0 + 1);
|
|
1995
|
+
const ly = Math.max(0, y0);
|
|
1996
|
+
for (let i = 0; i < label.length && lx + i < w; i++) {
|
|
1997
|
+
if (ly < h) grid[ly][lx + i] = label[i];
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return grid.map((row) => row.join("")).join("\n");
|
|
2001
|
+
}
|
|
1081
2002
|
export {
|
|
1082
2003
|
IncrementalLayout,
|
|
1083
2004
|
countAllCrossings,
|
|
1084
|
-
layout
|
|
2005
|
+
layout,
|
|
2006
|
+
printLayout,
|
|
2007
|
+
rotateHandles
|
|
1085
2008
|
};
|
|
1086
2009
|
//# sourceMappingURL=index.mjs.map
|