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