@nodius/layouting 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +280 -126
- 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 +7 -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 +904 -149
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +901 -148
- 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 +31 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/types.d.ts +155 -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,7 +22,9 @@ 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
|
|
|
@@ -34,7 +36,12 @@ function resolveOptions(options) {
|
|
|
34
36
|
layerSpacing: options?.layerSpacing ?? 60,
|
|
35
37
|
crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
|
|
36
38
|
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
|
|
37
|
-
edgeMargin: options?.edgeMargin ?? 20
|
|
39
|
+
edgeMargin: options?.edgeMargin ?? 20,
|
|
40
|
+
controlWeight: options?.edgeWeights?.control ?? 1,
|
|
41
|
+
dataWeight: options?.edgeWeights?.data ?? 0.25,
|
|
42
|
+
packComponents: options?.packComponents ?? true,
|
|
43
|
+
compoundPadding: options?.compoundPadding ?? 24,
|
|
44
|
+
onProposal: options?.onProposal
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -97,7 +104,7 @@ var Graph = class {
|
|
|
97
104
|
return result;
|
|
98
105
|
}
|
|
99
106
|
};
|
|
100
|
-
function buildGraph(nodes, edges) {
|
|
107
|
+
function buildGraph(nodes, edges, ctx = { controlWeight: 1, dataWeight: 0.25 }) {
|
|
101
108
|
const graph = new Graph();
|
|
102
109
|
for (const node of nodes) {
|
|
103
110
|
graph.addNode({
|
|
@@ -109,10 +116,15 @@ function buildGraph(nodes, edges) {
|
|
|
109
116
|
layer: -1,
|
|
110
117
|
order: -1,
|
|
111
118
|
x: 0,
|
|
112
|
-
y: 0
|
|
119
|
+
y: 0,
|
|
120
|
+
parentId: node.parentId,
|
|
121
|
+
isCompound: false,
|
|
122
|
+
isValue: false
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
125
|
for (const edge of edges) {
|
|
126
|
+
const kind = edge.kind ?? "control";
|
|
127
|
+
const defaultWeight = kind === "control" ? ctx.controlWeight : ctx.dataWeight;
|
|
116
128
|
graph.addEdge({
|
|
117
129
|
id: edge.id,
|
|
118
130
|
from: edge.from,
|
|
@@ -120,9 +132,34 @@ function buildGraph(nodes, edges) {
|
|
|
120
132
|
fromHandle: edge.fromHandle,
|
|
121
133
|
toHandle: edge.toHandle,
|
|
122
134
|
reversed: false,
|
|
123
|
-
originalId: edge.id
|
|
135
|
+
originalId: edge.id,
|
|
136
|
+
kind,
|
|
137
|
+
weight: edge.weight ?? defaultWeight
|
|
124
138
|
});
|
|
125
139
|
}
|
|
140
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
141
|
+
const ins = graph.inEdges.get(nodeId);
|
|
142
|
+
const outs = graph.outEdges.get(nodeId);
|
|
143
|
+
let hasControl = false;
|
|
144
|
+
if (ins) {
|
|
145
|
+
for (const eid of ins) {
|
|
146
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
147
|
+
hasControl = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!hasControl && outs) {
|
|
153
|
+
for (const eid of outs) {
|
|
154
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
155
|
+
hasControl = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const incidentCount = (ins?.size ?? 0) + (outs?.size ?? 0);
|
|
161
|
+
node.isValue = !hasControl && incidentCount > 0;
|
|
162
|
+
}
|
|
126
163
|
return graph;
|
|
127
164
|
}
|
|
128
165
|
function getHandlePosition(node, handleId) {
|
|
@@ -210,28 +247,88 @@ function breakCycles(graph) {
|
|
|
210
247
|
function assignLayers(graph) {
|
|
211
248
|
const layers = /* @__PURE__ */ new Map();
|
|
212
249
|
const visiting = /* @__PURE__ */ new Set();
|
|
213
|
-
function
|
|
250
|
+
function controlLayer(nodeId) {
|
|
214
251
|
if (layers.has(nodeId)) return layers.get(nodeId);
|
|
215
252
|
if (visiting.has(nodeId)) return 0;
|
|
253
|
+
const node = graph.nodes.get(nodeId);
|
|
254
|
+
if (!node || node.isValue) return -1;
|
|
216
255
|
visiting.add(nodeId);
|
|
217
|
-
const
|
|
256
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
218
257
|
let maxPredLayer = -1;
|
|
219
|
-
|
|
220
|
-
|
|
258
|
+
if (inEdgeIds) {
|
|
259
|
+
for (const eid of inEdgeIds) {
|
|
260
|
+
const edge = graph.edges.get(eid);
|
|
261
|
+
if (!edge || edge.kind !== "control") continue;
|
|
262
|
+
const pred = graph.nodes.get(edge.from);
|
|
263
|
+
if (!pred || pred.isValue) continue;
|
|
264
|
+
maxPredLayer = Math.max(maxPredLayer, controlLayer(edge.from));
|
|
265
|
+
}
|
|
221
266
|
}
|
|
222
267
|
const layer = maxPredLayer + 1;
|
|
223
268
|
layers.set(nodeId, layer);
|
|
224
|
-
|
|
225
|
-
if (node) node.layer = layer;
|
|
269
|
+
node.layer = layer;
|
|
226
270
|
visiting.delete(nodeId);
|
|
227
271
|
return layer;
|
|
228
272
|
}
|
|
229
|
-
for (const nodeId of graph.nodes
|
|
230
|
-
|
|
273
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
274
|
+
if (!node.isValue) controlLayer(nodeId);
|
|
275
|
+
}
|
|
276
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
277
|
+
if (!node.isValue) continue;
|
|
278
|
+
const neighborLayers = [];
|
|
279
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
280
|
+
if (inEdgeIds) {
|
|
281
|
+
for (const eid of inEdgeIds) {
|
|
282
|
+
const edge = graph.edges.get(eid);
|
|
283
|
+
if (!edge) continue;
|
|
284
|
+
const nbr = graph.nodes.get(edge.from);
|
|
285
|
+
if (nbr && !nbr.isValue && layers.has(edge.from)) {
|
|
286
|
+
neighborLayers.push(layers.get(edge.from));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
291
|
+
if (outEdgeIds) {
|
|
292
|
+
for (const eid of outEdgeIds) {
|
|
293
|
+
const edge = graph.edges.get(eid);
|
|
294
|
+
if (!edge) continue;
|
|
295
|
+
const nbr = graph.nodes.get(edge.to);
|
|
296
|
+
if (nbr && !nbr.isValue && layers.has(edge.to)) {
|
|
297
|
+
neighborLayers.push(layers.get(edge.to));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let layer;
|
|
302
|
+
if (neighborLayers.length > 0) {
|
|
303
|
+
neighborLayers.sort((a, b) => a - b);
|
|
304
|
+
layer = neighborLayers[Math.floor(neighborLayers.length / 2)];
|
|
305
|
+
} else {
|
|
306
|
+
layer = 0;
|
|
307
|
+
}
|
|
308
|
+
layers.set(nodeId, layer);
|
|
309
|
+
node.layer = layer;
|
|
231
310
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
311
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
312
|
+
if (!layers.has(nodeId)) {
|
|
313
|
+
layers.set(nodeId, 0);
|
|
314
|
+
node.layer = 0;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let minLayer = Infinity;
|
|
318
|
+
let maxLayer = -Infinity;
|
|
319
|
+
for (const l of layers.values()) {
|
|
320
|
+
if (l < minLayer) minLayer = l;
|
|
321
|
+
if (l > maxLayer) maxLayer = l;
|
|
322
|
+
}
|
|
323
|
+
if (!isFinite(minLayer)) return [];
|
|
324
|
+
if (minLayer !== 0) {
|
|
325
|
+
for (const [id, l] of layers) {
|
|
326
|
+
const adj = l - minLayer;
|
|
327
|
+
layers.set(id, adj);
|
|
328
|
+
const n = graph.nodes.get(id);
|
|
329
|
+
if (n) n.layer = adj;
|
|
330
|
+
}
|
|
331
|
+
maxLayer -= minLayer;
|
|
235
332
|
}
|
|
236
333
|
const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
|
|
237
334
|
for (const [nodeId, layer] of layers) {
|
|
@@ -243,6 +340,7 @@ function insertDummyNodes(graph, layers) {
|
|
|
243
340
|
let dummyCounter = 0;
|
|
244
341
|
const edgesToProcess = [...graph.edges.values()];
|
|
245
342
|
for (const edge of edgesToProcess) {
|
|
343
|
+
if (edge.kind !== "control") continue;
|
|
246
344
|
const fromNode = graph.nodes.get(edge.from);
|
|
247
345
|
const toNode = graph.nodes.get(edge.to);
|
|
248
346
|
if (!fromNode || !toNode) continue;
|
|
@@ -267,7 +365,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
267
365
|
layer: l,
|
|
268
366
|
order: -1,
|
|
269
367
|
x: 0,
|
|
270
|
-
y: 0
|
|
368
|
+
y: 0,
|
|
369
|
+
isCompound: false,
|
|
370
|
+
isValue: false
|
|
271
371
|
});
|
|
272
372
|
layers[l].push(dummyId);
|
|
273
373
|
graph.addEdge({
|
|
@@ -277,7 +377,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
277
377
|
fromHandle: prevHandleId,
|
|
278
378
|
toHandle: "in",
|
|
279
379
|
reversed: edge.reversed,
|
|
280
|
-
originalId: edge.originalId
|
|
380
|
+
originalId: edge.originalId,
|
|
381
|
+
kind: edge.kind,
|
|
382
|
+
weight: edge.weight
|
|
281
383
|
});
|
|
282
384
|
prevNodeId = dummyId;
|
|
283
385
|
prevHandleId = "out";
|
|
@@ -289,7 +391,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
289
391
|
fromHandle: prevHandleId,
|
|
290
392
|
toHandle: edge.toHandle,
|
|
291
393
|
reversed: edge.reversed,
|
|
292
|
-
originalId: edge.originalId
|
|
394
|
+
originalId: edge.originalId,
|
|
395
|
+
kind: edge.kind,
|
|
396
|
+
weight: edge.weight
|
|
293
397
|
});
|
|
294
398
|
}
|
|
295
399
|
return layers;
|
|
@@ -710,6 +814,176 @@ function getOrderSize(node, isHorizontal) {
|
|
|
710
814
|
return isHorizontal ? node.height : node.width;
|
|
711
815
|
}
|
|
712
816
|
|
|
817
|
+
// src/algorithms/value-placement.ts
|
|
818
|
+
function placeValueSidecars(graph, layers, options) {
|
|
819
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
820
|
+
const attachments = [];
|
|
821
|
+
const orphanValues = [];
|
|
822
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
823
|
+
if (!node.isValue || node.isDummy) continue;
|
|
824
|
+
const consumer = findDominantConsumer(graph, nodeId);
|
|
825
|
+
if (consumer) {
|
|
826
|
+
attachments.push({ valueId: nodeId, consumerId: consumer });
|
|
827
|
+
} else {
|
|
828
|
+
orphanValues.push(nodeId);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const byConsumer = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const { valueId, consumerId } of attachments) {
|
|
833
|
+
const arr = byConsumer.get(consumerId) ?? [];
|
|
834
|
+
arr.push(valueId);
|
|
835
|
+
byConsumer.set(consumerId, arr);
|
|
836
|
+
}
|
|
837
|
+
for (const [consumerId, vIds] of byConsumer) {
|
|
838
|
+
const consumer = graph.nodes.get(consumerId);
|
|
839
|
+
if (!consumer) continue;
|
|
840
|
+
const { beforeSide, afterSide } = splitValuesBySide(graph, consumerId, vIds, isHorizontal);
|
|
841
|
+
if (isHorizontal) {
|
|
842
|
+
const cxCenter = consumer.x + consumer.width / 2;
|
|
843
|
+
let topEdge = consumer.y;
|
|
844
|
+
for (const vid of beforeSide) {
|
|
845
|
+
const v = graph.nodes.get(vid);
|
|
846
|
+
if (!v) continue;
|
|
847
|
+
v.y = topEdge - options.nodeSpacing - v.height;
|
|
848
|
+
v.x = cxCenter - v.width / 2;
|
|
849
|
+
topEdge = v.y;
|
|
850
|
+
}
|
|
851
|
+
let bottomEdge = consumer.y + consumer.height;
|
|
852
|
+
for (const vid of afterSide) {
|
|
853
|
+
const v = graph.nodes.get(vid);
|
|
854
|
+
if (!v) continue;
|
|
855
|
+
v.y = bottomEdge + options.nodeSpacing;
|
|
856
|
+
v.x = cxCenter - v.width / 2;
|
|
857
|
+
bottomEdge = v.y + v.height;
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
const cyCenter = consumer.y + consumer.height / 2;
|
|
861
|
+
let leftEdge = consumer.x;
|
|
862
|
+
for (const vid of beforeSide) {
|
|
863
|
+
const v = graph.nodes.get(vid);
|
|
864
|
+
if (!v) continue;
|
|
865
|
+
v.x = leftEdge - options.nodeSpacing - v.width;
|
|
866
|
+
v.y = cyCenter - v.height / 2;
|
|
867
|
+
leftEdge = v.x;
|
|
868
|
+
}
|
|
869
|
+
let rightEdge = consumer.x + consumer.width;
|
|
870
|
+
for (const vid of afterSide) {
|
|
871
|
+
const v = graph.nodes.get(vid);
|
|
872
|
+
if (!v) continue;
|
|
873
|
+
v.x = rightEdge + options.nodeSpacing;
|
|
874
|
+
v.y = cyCenter - v.height / 2;
|
|
875
|
+
rightEdge = v.x + v.width;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
let orphanX = 0, orphanY = 0;
|
|
880
|
+
for (const orphanId of orphanValues) {
|
|
881
|
+
const v = graph.nodes.get(orphanId);
|
|
882
|
+
if (!v) continue;
|
|
883
|
+
v.x = orphanX;
|
|
884
|
+
v.y = orphanY;
|
|
885
|
+
if (isHorizontal) orphanX += v.width + options.nodeSpacing;
|
|
886
|
+
else orphanY += v.height + options.nodeSpacing;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function findDominantConsumer(graph, valueId) {
|
|
890
|
+
const counts = /* @__PURE__ */ new Map();
|
|
891
|
+
const outs = graph.outEdges.get(valueId);
|
|
892
|
+
if (outs) {
|
|
893
|
+
for (const eid of outs) {
|
|
894
|
+
const edge = graph.edges.get(eid);
|
|
895
|
+
if (!edge) continue;
|
|
896
|
+
const other = graph.nodes.get(edge.to);
|
|
897
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
898
|
+
counts.set(edge.to, (counts.get(edge.to) ?? 0) + 1);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const ins = graph.inEdges.get(valueId);
|
|
903
|
+
if (ins) {
|
|
904
|
+
for (const eid of ins) {
|
|
905
|
+
const edge = graph.edges.get(eid);
|
|
906
|
+
if (!edge) continue;
|
|
907
|
+
const other = graph.nodes.get(edge.from);
|
|
908
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
909
|
+
counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
let bestId = null;
|
|
914
|
+
let bestCount = -1;
|
|
915
|
+
for (const [id, count] of counts) {
|
|
916
|
+
if (count > bestCount || count === bestCount && bestId !== null && id < bestId) {
|
|
917
|
+
bestId = id;
|
|
918
|
+
bestCount = count;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return bestId;
|
|
922
|
+
}
|
|
923
|
+
function splitValuesBySide(graph, consumerId, valueIds, isHorizontal) {
|
|
924
|
+
const consumer = graph.nodes.get(consumerId);
|
|
925
|
+
if (!consumer) return { beforeSide: [...valueIds].sort(), afterSide: [] };
|
|
926
|
+
const slots = [];
|
|
927
|
+
for (const vId of valueIds) {
|
|
928
|
+
let side = "neutral";
|
|
929
|
+
let offset = 0.5;
|
|
930
|
+
const inspectHandle = (handleId) => {
|
|
931
|
+
if (!handleId) return;
|
|
932
|
+
const handle = consumer.handles.find((h) => h.id === handleId);
|
|
933
|
+
if (!handle) return;
|
|
934
|
+
offset = handle.offset ?? 0.5;
|
|
935
|
+
if (isHorizontal) {
|
|
936
|
+
if (handle.position === "top") side = "before";
|
|
937
|
+
else if (handle.position === "bottom") side = "after";
|
|
938
|
+
} else {
|
|
939
|
+
if (handle.position === "left") side = "before";
|
|
940
|
+
else if (handle.position === "right") side = "after";
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const outs = graph.outEdges.get(vId);
|
|
944
|
+
if (outs) for (const eid of outs) {
|
|
945
|
+
const e = graph.edges.get(eid);
|
|
946
|
+
if (e && e.to === consumerId) {
|
|
947
|
+
inspectHandle(e.toHandle);
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (side === "neutral") {
|
|
952
|
+
const ins = graph.inEdges.get(vId);
|
|
953
|
+
if (ins) for (const eid of ins) {
|
|
954
|
+
const e = graph.edges.get(eid);
|
|
955
|
+
if (e && e.from === consumerId) {
|
|
956
|
+
inspectHandle(e.fromHandle);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
slots.push({ id: vId, side, offset });
|
|
962
|
+
}
|
|
963
|
+
const before = slots.filter((s) => s.side === "before");
|
|
964
|
+
const after = slots.filter((s) => s.side === "after");
|
|
965
|
+
const neutral = slots.filter((s) => s.side === "neutral");
|
|
966
|
+
for (const n of neutral) {
|
|
967
|
+
if (before.length <= after.length) before.push(n);
|
|
968
|
+
else after.push(n);
|
|
969
|
+
}
|
|
970
|
+
const cmp = (a, b) => a.offset - b.offset || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
971
|
+
before.sort(cmp);
|
|
972
|
+
after.sort(cmp);
|
|
973
|
+
return {
|
|
974
|
+
beforeSide: before.map((s) => s.id),
|
|
975
|
+
afterSide: after.map((s) => s.id)
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function railLayers(layers, graph) {
|
|
979
|
+
return layers.map(
|
|
980
|
+
(layer) => layer.filter((id) => {
|
|
981
|
+
const n = graph.nodes.get(id);
|
|
982
|
+
return n && !n.isValue;
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
713
987
|
// src/algorithms/edge-routing.ts
|
|
714
988
|
function routeEdges(graph, direction, edgeMargin) {
|
|
715
989
|
const chains = collectEdgeChains(graph);
|
|
@@ -760,7 +1034,8 @@ function collectEdgeChains(graph) {
|
|
|
760
1034
|
fromHandle: edge.fromHandle,
|
|
761
1035
|
toHandle: currentEdge.toHandle,
|
|
762
1036
|
dummyNodes,
|
|
763
|
-
reversed: edge.reversed
|
|
1037
|
+
reversed: edge.reversed,
|
|
1038
|
+
kind: edge.kind
|
|
764
1039
|
});
|
|
765
1040
|
}
|
|
766
1041
|
return chains;
|
|
@@ -805,7 +1080,8 @@ function routeChain(graph, chain, direction, margin) {
|
|
|
805
1080
|
to: actualTo,
|
|
806
1081
|
fromHandle: actualFromHandle,
|
|
807
1082
|
toHandle: actualToHandle,
|
|
808
|
-
points
|
|
1083
|
+
points,
|
|
1084
|
+
kind: chain.kind
|
|
809
1085
|
};
|
|
810
1086
|
}
|
|
811
1087
|
function makeOrthogonal(points, direction) {
|
|
@@ -879,21 +1155,292 @@ function getDefaultTargetSide(direction) {
|
|
|
879
1155
|
}
|
|
880
1156
|
}
|
|
881
1157
|
|
|
1158
|
+
// src/algorithms/component-packing.ts
|
|
1159
|
+
function packComponents(nodes, edges, options) {
|
|
1160
|
+
if (nodes.length === 0) return;
|
|
1161
|
+
const idToNode = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const n of nodes) idToNode.set(n.id, n);
|
|
1163
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
1164
|
+
function topAncestor(id) {
|
|
1165
|
+
if (rootOf.has(id)) return rootOf.get(id);
|
|
1166
|
+
const n = idToNode.get(id);
|
|
1167
|
+
if (!n || !n.parentId || !idToNode.has(n.parentId)) {
|
|
1168
|
+
rootOf.set(id, id);
|
|
1169
|
+
return id;
|
|
1170
|
+
}
|
|
1171
|
+
const r = topAncestor(n.parentId);
|
|
1172
|
+
rootOf.set(id, r);
|
|
1173
|
+
return r;
|
|
1174
|
+
}
|
|
1175
|
+
for (const n of nodes) topAncestor(n.id);
|
|
1176
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1177
|
+
function find(x) {
|
|
1178
|
+
let cur = x;
|
|
1179
|
+
while (parent.get(cur) !== cur) {
|
|
1180
|
+
const p = parent.get(cur);
|
|
1181
|
+
parent.set(cur, parent.get(p));
|
|
1182
|
+
cur = parent.get(cur);
|
|
1183
|
+
}
|
|
1184
|
+
return cur;
|
|
1185
|
+
}
|
|
1186
|
+
function union(a, b) {
|
|
1187
|
+
const ra = find(a);
|
|
1188
|
+
const rb = find(b);
|
|
1189
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
1190
|
+
}
|
|
1191
|
+
for (const n of nodes) parent.set(topAncestor(n.id), topAncestor(n.id));
|
|
1192
|
+
for (const e of edges) {
|
|
1193
|
+
const ra = topAncestor(e.from);
|
|
1194
|
+
const rb = topAncestor(e.to);
|
|
1195
|
+
if (idToNode.has(ra) && idToNode.has(rb)) union(ra, rb);
|
|
1196
|
+
}
|
|
1197
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1198
|
+
for (const n of nodes) {
|
|
1199
|
+
const root = topAncestor(n.id);
|
|
1200
|
+
const comp = find(root);
|
|
1201
|
+
const arr = groups.get(comp) ?? [];
|
|
1202
|
+
arr.push(n);
|
|
1203
|
+
groups.set(comp, arr);
|
|
1204
|
+
}
|
|
1205
|
+
if (groups.size <= 1) return;
|
|
1206
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
1207
|
+
const edgesByComp = /* @__PURE__ */ new Map();
|
|
1208
|
+
for (const e of edges) {
|
|
1209
|
+
const root = topAncestor(e.from);
|
|
1210
|
+
const comp = find(root);
|
|
1211
|
+
const arr = edgesByComp.get(comp) ?? [];
|
|
1212
|
+
arr.push(e);
|
|
1213
|
+
edgesByComp.set(comp, arr);
|
|
1214
|
+
}
|
|
1215
|
+
const boxes = [];
|
|
1216
|
+
for (const [compId, group] of groups) {
|
|
1217
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1218
|
+
for (const n of group) {
|
|
1219
|
+
if (n.x < minX) minX = n.x;
|
|
1220
|
+
if (n.y < minY) minY = n.y;
|
|
1221
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1222
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1223
|
+
}
|
|
1224
|
+
const compEdges = edgesByComp.get(compId) ?? [];
|
|
1225
|
+
for (const e of compEdges) {
|
|
1226
|
+
for (const p of e.points) {
|
|
1227
|
+
if (p.x < minX) minX = p.x;
|
|
1228
|
+
if (p.y < minY) minY = p.y;
|
|
1229
|
+
if (p.x > maxX) maxX = p.x;
|
|
1230
|
+
if (p.y > maxY) maxY = p.y;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
boxes.push({ id: compId, nodes: group, edges: compEdges, minX, minY, maxX, maxY });
|
|
1234
|
+
}
|
|
1235
|
+
boxes.sort((a, b) => {
|
|
1236
|
+
const sizeA = isHorizontal ? a.maxX - a.minX : a.maxY - a.minY;
|
|
1237
|
+
const sizeB = isHorizontal ? b.maxX - b.minX : b.maxY - b.minY;
|
|
1238
|
+
return sizeB - sizeA;
|
|
1239
|
+
});
|
|
1240
|
+
const gap = options.layerSpacing;
|
|
1241
|
+
let cursor = 0;
|
|
1242
|
+
for (const box of boxes) {
|
|
1243
|
+
if (isHorizontal) {
|
|
1244
|
+
const dy = cursor - box.minY;
|
|
1245
|
+
const dx = -box.minX;
|
|
1246
|
+
for (const n of box.nodes) {
|
|
1247
|
+
n.x += dx;
|
|
1248
|
+
n.y += dy;
|
|
1249
|
+
for (const h of n.handles) {
|
|
1250
|
+
h.x += dx;
|
|
1251
|
+
h.y += dy;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
for (const e of box.edges) {
|
|
1255
|
+
for (const p of e.points) {
|
|
1256
|
+
p.x += dx;
|
|
1257
|
+
p.y += dy;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
cursor += box.maxY - box.minY + gap;
|
|
1261
|
+
} else {
|
|
1262
|
+
const dx = cursor - box.minX;
|
|
1263
|
+
const dy = -box.minY;
|
|
1264
|
+
for (const n of box.nodes) {
|
|
1265
|
+
n.x += dx;
|
|
1266
|
+
n.y += dy;
|
|
1267
|
+
for (const h of n.handles) {
|
|
1268
|
+
h.x += dx;
|
|
1269
|
+
h.y += dy;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
for (const e of box.edges) {
|
|
1273
|
+
for (const p of e.points) {
|
|
1274
|
+
p.x += dx;
|
|
1275
|
+
p.y += dy;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
cursor += box.maxX - box.minX + gap;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/proposals.ts
|
|
1284
|
+
function applyRotationProposals(input, options) {
|
|
1285
|
+
if (!options.onProposal) return input;
|
|
1286
|
+
const nodeIndex = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1287
|
+
const newNodes = input.nodes.map((node) => {
|
|
1288
|
+
const role = classifyNode(node, input.edges);
|
|
1289
|
+
const expected = expectedSidesFor(node, role, input.edges, nodeIndex, options.direction);
|
|
1290
|
+
const proposal = computeProposal(node, expected, options.direction, role);
|
|
1291
|
+
if (!proposal) return node;
|
|
1292
|
+
const accepted = options.onProposal(proposal);
|
|
1293
|
+
return accepted ?? node;
|
|
1294
|
+
});
|
|
1295
|
+
return { nodes: newNodes, edges: input.edges };
|
|
1296
|
+
}
|
|
1297
|
+
function classifyNode(node, edges) {
|
|
1298
|
+
const incident = edges.filter((e) => e.from === node.id || e.to === node.id);
|
|
1299
|
+
if (incident.length === 0) return { kind: "isolated" };
|
|
1300
|
+
const allData = incident.every((e) => (e.kind ?? "control") === "data");
|
|
1301
|
+
if (!allData) return { kind: "rail" };
|
|
1302
|
+
for (const e of incident) {
|
|
1303
|
+
const consumerId = e.from === node.id ? e.to : e.from;
|
|
1304
|
+
const consumerHandleId = e.from === node.id ? e.toHandle : e.fromHandle;
|
|
1305
|
+
const sideHint = sideHintFromHandle(
|
|
1306
|
+
consumerId,
|
|
1307
|
+
consumerHandleId,
|
|
1308
|
+
edges,
|
|
1309
|
+
/* recursing */
|
|
1310
|
+
false
|
|
1311
|
+
);
|
|
1312
|
+
if (sideHint) return { kind: "value", consumerSide: sideHint };
|
|
1313
|
+
}
|
|
1314
|
+
return { kind: "value", consumerSide: null };
|
|
1315
|
+
}
|
|
1316
|
+
function sideHintFromHandle(_consumerId, _handleId, _edges, _recursing) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
function expectedSidesFor(node, role, edges, index, direction) {
|
|
1320
|
+
if (role.kind === "isolated" || role.kind === "rail") {
|
|
1321
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1322
|
+
}
|
|
1323
|
+
const consumerHandleSide = findConsumerHandleSide(node, edges, index);
|
|
1324
|
+
if (!consumerHandleSide) {
|
|
1325
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1326
|
+
}
|
|
1327
|
+
const isVerticalFlow = direction === "TB" || direction === "BT";
|
|
1328
|
+
let outputSide;
|
|
1329
|
+
if (isVerticalFlow) {
|
|
1330
|
+
if (consumerHandleSide === "left") outputSide = "right";
|
|
1331
|
+
else if (consumerHandleSide === "right") outputSide = "left";
|
|
1332
|
+
else outputSide = "right";
|
|
1333
|
+
} else {
|
|
1334
|
+
if (consumerHandleSide === "top") outputSide = "bottom";
|
|
1335
|
+
else if (consumerHandleSide === "bottom") outputSide = "top";
|
|
1336
|
+
else outputSide = "bottom";
|
|
1337
|
+
}
|
|
1338
|
+
return { input: outputSide, output: outputSide };
|
|
1339
|
+
}
|
|
1340
|
+
function findConsumerHandleSide(value, edges, index) {
|
|
1341
|
+
for (const e of edges) {
|
|
1342
|
+
const isFromValue = e.from === value.id;
|
|
1343
|
+
const isToValue = e.to === value.id;
|
|
1344
|
+
if (!isFromValue && !isToValue) continue;
|
|
1345
|
+
const consumerId = isFromValue ? e.to : e.from;
|
|
1346
|
+
const consumer = index.get(consumerId);
|
|
1347
|
+
if (!consumer) continue;
|
|
1348
|
+
const consumerHandleId = isFromValue ? e.toHandle : e.fromHandle;
|
|
1349
|
+
const consumerHandle = consumer.handles.find((h) => h.id === consumerHandleId);
|
|
1350
|
+
if (consumerHandle) return consumerHandle.position;
|
|
1351
|
+
}
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
function computeProposal(node, expected, direction, role) {
|
|
1355
|
+
if (node.handles.length === 0) return null;
|
|
1356
|
+
const currentScore = score(node.handles, expected);
|
|
1357
|
+
if (currentScore.matchRatio >= 1) return null;
|
|
1358
|
+
const candidates = [];
|
|
1359
|
+
for (const rot of [90, -90, 180]) {
|
|
1360
|
+
const rotated = rotateHandles(node.handles, rot);
|
|
1361
|
+
candidates.push({ rotation: rot, score: score(rotated, expected) });
|
|
1362
|
+
}
|
|
1363
|
+
candidates.sort((a, b) => b.score.matchRatio - a.score.matchRatio);
|
|
1364
|
+
const best = candidates[0];
|
|
1365
|
+
if (best.score.matchRatio <= currentScore.matchRatio) return null;
|
|
1366
|
+
return buildProposal(node, best.rotation, currentScore, best.score, direction, expected, role);
|
|
1367
|
+
}
|
|
1368
|
+
function buildProposal(node, rotation, current, proposedScore, direction, expected, role) {
|
|
1369
|
+
const proposed = { ...node, handles: rotateHandles(node.handles, rotation) };
|
|
1370
|
+
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}`;
|
|
1371
|
+
return {
|
|
1372
|
+
type: "rotate",
|
|
1373
|
+
nodeId: node.id,
|
|
1374
|
+
current: node,
|
|
1375
|
+
proposed,
|
|
1376
|
+
rotation,
|
|
1377
|
+
reason: `${roleDesc}; ${current.matched}/${current.total} handles match, ${proposedScore.matched}/${proposedScore.total} after rotation`
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function inputSideFor(direction) {
|
|
1381
|
+
switch (direction) {
|
|
1382
|
+
case "TB":
|
|
1383
|
+
return "top";
|
|
1384
|
+
case "BT":
|
|
1385
|
+
return "bottom";
|
|
1386
|
+
case "LR":
|
|
1387
|
+
return "left";
|
|
1388
|
+
case "RL":
|
|
1389
|
+
return "right";
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function outputSideFor(direction) {
|
|
1393
|
+
switch (direction) {
|
|
1394
|
+
case "TB":
|
|
1395
|
+
return "bottom";
|
|
1396
|
+
case "BT":
|
|
1397
|
+
return "top";
|
|
1398
|
+
case "LR":
|
|
1399
|
+
return "right";
|
|
1400
|
+
case "RL":
|
|
1401
|
+
return "left";
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function score(handles, expected) {
|
|
1405
|
+
let matched = 0;
|
|
1406
|
+
let total = 0;
|
|
1407
|
+
for (const h of handles) {
|
|
1408
|
+
total++;
|
|
1409
|
+
if (h.type === "input" && h.position === expected.input) matched++;
|
|
1410
|
+
else if (h.type === "output" && h.position === expected.output) matched++;
|
|
1411
|
+
}
|
|
1412
|
+
return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
|
|
1413
|
+
}
|
|
1414
|
+
function rotateHandles(handles, rot) {
|
|
1415
|
+
const rotateSide = (s) => {
|
|
1416
|
+
if (rot === 180) {
|
|
1417
|
+
return s === "top" ? "bottom" : s === "bottom" ? "top" : s === "left" ? "right" : "left";
|
|
1418
|
+
}
|
|
1419
|
+
if (rot === 90) {
|
|
1420
|
+
return s === "top" ? "right" : s === "right" ? "bottom" : s === "bottom" ? "left" : "top";
|
|
1421
|
+
}
|
|
1422
|
+
return s === "top" ? "left" : s === "left" ? "bottom" : s === "bottom" ? "right" : "top";
|
|
1423
|
+
};
|
|
1424
|
+
return handles.map((h) => ({ ...h, position: rotateSide(h.position) }));
|
|
1425
|
+
}
|
|
1426
|
+
|
|
882
1427
|
// src/layout.ts
|
|
1428
|
+
var COMPOUND_HEADER = 28;
|
|
883
1429
|
function layout(input, options) {
|
|
884
1430
|
const resolved = resolveOptions(options);
|
|
885
|
-
|
|
886
|
-
|
|
1431
|
+
if (input.nodes.length === 0) return { nodes: [], edges: [] };
|
|
1432
|
+
const adjusted = applyRotationProposals(input, resolved);
|
|
1433
|
+
return layoutCompound(adjusted, resolved);
|
|
887
1434
|
}
|
|
888
1435
|
function computeLayout(graph, options) {
|
|
889
|
-
if (graph.nodes.size === 0) {
|
|
890
|
-
return { nodes: [], edges: [] };
|
|
891
|
-
}
|
|
1436
|
+
if (graph.nodes.size === 0) return { nodes: [], edges: [] };
|
|
892
1437
|
breakCycles(graph);
|
|
893
1438
|
let layers = assignLayers(graph);
|
|
894
1439
|
layers = insertDummyNodes(graph, layers);
|
|
895
|
-
|
|
896
|
-
|
|
1440
|
+
let rail = railLayers(layers, graph);
|
|
1441
|
+
rail = minimizeCrossings(graph, rail, options.crossingMinimizationIterations);
|
|
1442
|
+
assignCoordinates(graph, rail, options);
|
|
1443
|
+
placeValueSidecars(graph, layers, options);
|
|
897
1444
|
const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
|
|
898
1445
|
return buildResult(graph, routedEdges);
|
|
899
1446
|
}
|
|
@@ -904,13 +1451,7 @@ function buildResult(graph, routedEdges) {
|
|
|
904
1451
|
if (node.isDummy) continue;
|
|
905
1452
|
const handles = node.handles.map((h) => {
|
|
906
1453
|
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
|
-
};
|
|
1454
|
+
return { id: h.id, type: h.type, position: h.position, x: pos.x, y: pos.y };
|
|
914
1455
|
});
|
|
915
1456
|
nodes.push({
|
|
916
1457
|
id: node.id,
|
|
@@ -918,7 +1459,8 @@ function buildResult(graph, routedEdges) {
|
|
|
918
1459
|
y: node.y,
|
|
919
1460
|
width: node.width,
|
|
920
1461
|
height: node.height,
|
|
921
|
-
handles
|
|
1462
|
+
handles,
|
|
1463
|
+
parentId: node.parentId
|
|
922
1464
|
});
|
|
923
1465
|
}
|
|
924
1466
|
for (const route of routedEdges) {
|
|
@@ -928,11 +1470,146 @@ function buildResult(graph, routedEdges) {
|
|
|
928
1470
|
to: route.to,
|
|
929
1471
|
fromHandle: route.fromHandle,
|
|
930
1472
|
toHandle: route.toHandle,
|
|
931
|
-
points: route.points
|
|
1473
|
+
points: route.points,
|
|
1474
|
+
kind: route.kind
|
|
932
1475
|
});
|
|
933
1476
|
}
|
|
934
1477
|
return { nodes, edges };
|
|
935
1478
|
}
|
|
1479
|
+
function layoutCompound(input, options) {
|
|
1480
|
+
const nodeMap = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1481
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1482
|
+
const rootNodes = [];
|
|
1483
|
+
for (const n of input.nodes) {
|
|
1484
|
+
if (n.parentId && nodeMap.has(n.parentId)) {
|
|
1485
|
+
const arr = childrenByParent.get(n.parentId) ?? [];
|
|
1486
|
+
arr.push(n);
|
|
1487
|
+
childrenByParent.set(n.parentId, arr);
|
|
1488
|
+
} else {
|
|
1489
|
+
rootNodes.push(n);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const compoundIds = new Set(childrenByParent.keys());
|
|
1493
|
+
const depthCache = /* @__PURE__ */ new Map();
|
|
1494
|
+
function depthOf(id) {
|
|
1495
|
+
if (depthCache.has(id)) return depthCache.get(id);
|
|
1496
|
+
const n = nodeMap.get(id);
|
|
1497
|
+
const d = n?.parentId && nodeMap.has(n.parentId) ? depthOf(n.parentId) + 1 : 0;
|
|
1498
|
+
depthCache.set(id, d);
|
|
1499
|
+
return d;
|
|
1500
|
+
}
|
|
1501
|
+
for (const n of input.nodes) depthOf(n.id);
|
|
1502
|
+
const sortedCompounds = [...compoundIds].sort((a, b) => depthOf(b) - depthOf(a));
|
|
1503
|
+
const subLayouts = /* @__PURE__ */ new Map();
|
|
1504
|
+
const compoundSize = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const compoundId of sortedCompounds) {
|
|
1506
|
+
const children = childrenByParent.get(compoundId) ?? [];
|
|
1507
|
+
if (children.length === 0) continue;
|
|
1508
|
+
const childIdSet = new Set(children.map((c) => c.id));
|
|
1509
|
+
const subEdges = input.edges.filter((e) => childIdSet.has(e.from) && childIdSet.has(e.to));
|
|
1510
|
+
const sizedChildren = children.map((c) => {
|
|
1511
|
+
const sz = compoundSize.get(c.id);
|
|
1512
|
+
return sz ? { ...c, width: sz.width, height: sz.height } : c;
|
|
1513
|
+
});
|
|
1514
|
+
const subOptions = { ...options, packComponents: false };
|
|
1515
|
+
const subInput = { nodes: sizedChildren.map(stripParent), edges: subEdges };
|
|
1516
|
+
const subResult = layoutFlat(subInput, subOptions);
|
|
1517
|
+
subLayouts.set(compoundId, subResult);
|
|
1518
|
+
const bbox = computeBoundingBox(subResult.nodes);
|
|
1519
|
+
const innerW = bbox.width + options.compoundPadding * 2;
|
|
1520
|
+
const innerH = bbox.height + options.compoundPadding * 2 + COMPOUND_HEADER;
|
|
1521
|
+
const original = nodeMap.get(compoundId);
|
|
1522
|
+
compoundSize.set(compoundId, {
|
|
1523
|
+
width: Math.max(innerW, original.width),
|
|
1524
|
+
height: Math.max(innerH, original.height)
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
const sizedRootNodes = rootNodes.map((n) => {
|
|
1528
|
+
const sz = compoundSize.get(n.id);
|
|
1529
|
+
return sz ? { ...n, width: sz.width, height: sz.height } : n;
|
|
1530
|
+
});
|
|
1531
|
+
const rootIdSet = new Set(rootNodes.map((n) => n.id));
|
|
1532
|
+
const rootEdges = input.edges.filter((e) => {
|
|
1533
|
+
return rootIdSet.has(e.from) && rootIdSet.has(e.to);
|
|
1534
|
+
});
|
|
1535
|
+
const rootResult = layoutFlat({ nodes: sizedRootNodes.map(stripParent), edges: rootEdges }, options);
|
|
1536
|
+
const finalNodes = [];
|
|
1537
|
+
const finalEdges = [];
|
|
1538
|
+
for (const n of rootResult.nodes) {
|
|
1539
|
+
const wasCompound = compoundIds.has(n.id);
|
|
1540
|
+
finalNodes.push({
|
|
1541
|
+
...n,
|
|
1542
|
+
parentId: nodeMap.get(n.id)?.parentId
|
|
1543
|
+
});
|
|
1544
|
+
if (wasCompound) {
|
|
1545
|
+
placeCompoundChildren(n, (compoundId) => subLayouts.get(compoundId), finalNodes, finalEdges, options, nodeMap);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
finalEdges.push(...rootResult.edges);
|
|
1549
|
+
if (options.packComponents) {
|
|
1550
|
+
packComponents(finalNodes, finalEdges, options);
|
|
1551
|
+
}
|
|
1552
|
+
return { nodes: finalNodes, edges: finalEdges };
|
|
1553
|
+
}
|
|
1554
|
+
function placeCompoundChildren(parent, getSub, finalNodes, finalEdges, options, nodeMap) {
|
|
1555
|
+
const sub = getSub(parent.id);
|
|
1556
|
+
if (!sub) return;
|
|
1557
|
+
const bbox = computeBoundingBox(sub.nodes);
|
|
1558
|
+
const dx = parent.x + options.compoundPadding - bbox.minX;
|
|
1559
|
+
const dy = parent.y + options.compoundPadding + COMPOUND_HEADER - bbox.minY;
|
|
1560
|
+
const availableW = parent.width - options.compoundPadding * 2;
|
|
1561
|
+
const slackX = (availableW - bbox.width) / 2;
|
|
1562
|
+
const availableH = parent.height - options.compoundPadding * 2 - COMPOUND_HEADER;
|
|
1563
|
+
const slackY = (availableH - bbox.height) / 2;
|
|
1564
|
+
const cx = dx + Math.max(0, slackX);
|
|
1565
|
+
const cy = dy + Math.max(0, slackY);
|
|
1566
|
+
for (const child of sub.nodes) {
|
|
1567
|
+
const placed = {
|
|
1568
|
+
...child,
|
|
1569
|
+
x: child.x + cx,
|
|
1570
|
+
y: child.y + cy,
|
|
1571
|
+
handles: child.handles.map((h) => ({ ...h, x: h.x + cx, y: h.y + cy })),
|
|
1572
|
+
parentId: nodeMap.get(child.id)?.parentId ?? parent.id
|
|
1573
|
+
};
|
|
1574
|
+
finalNodes.push(placed);
|
|
1575
|
+
if (getSub(child.id)) {
|
|
1576
|
+
placeCompoundChildren(placed, getSub, finalNodes, finalEdges, options, nodeMap);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
for (const edge of sub.edges) {
|
|
1580
|
+
finalEdges.push({
|
|
1581
|
+
...edge,
|
|
1582
|
+
points: edge.points.map((p) => ({ x: p.x + cx, y: p.y + cy }))
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function stripParent(n) {
|
|
1587
|
+
const { parentId: _ignored, ...rest } = n;
|
|
1588
|
+
return rest;
|
|
1589
|
+
}
|
|
1590
|
+
function layoutFlat(input, options) {
|
|
1591
|
+
const graph = buildGraph(input.nodes, input.edges, {
|
|
1592
|
+
controlWeight: options.controlWeight,
|
|
1593
|
+
dataWeight: options.dataWeight
|
|
1594
|
+
});
|
|
1595
|
+
return computeLayout(graph, options);
|
|
1596
|
+
}
|
|
1597
|
+
function computeBoundingBox(nodes) {
|
|
1598
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1599
|
+
for (const n of nodes) {
|
|
1600
|
+
if (n.x < minX) minX = n.x;
|
|
1601
|
+
if (n.y < minY) minY = n.y;
|
|
1602
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1603
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1604
|
+
}
|
|
1605
|
+
if (!isFinite(minX)) {
|
|
1606
|
+
minX = 0;
|
|
1607
|
+
minY = 0;
|
|
1608
|
+
maxX = 0;
|
|
1609
|
+
maxY = 0;
|
|
1610
|
+
}
|
|
1611
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
1612
|
+
}
|
|
936
1613
|
|
|
937
1614
|
// src/incremental.ts
|
|
938
1615
|
var IncrementalLayout = class {
|
|
@@ -941,82 +1618,40 @@ var IncrementalLayout = class {
|
|
|
941
1618
|
this.inputEdges = /* @__PURE__ */ new Map();
|
|
942
1619
|
this.lastResult = null;
|
|
943
1620
|
this.nodePositions = /* @__PURE__ */ new Map();
|
|
944
|
-
this.options =
|
|
1621
|
+
this.options = options;
|
|
1622
|
+
this.resolvedOptions = resolveOptions(options);
|
|
945
1623
|
}
|
|
946
|
-
/**
|
|
947
|
-
* Set the full graph and compute a complete layout.
|
|
948
|
-
*/
|
|
949
1624
|
setGraph(input) {
|
|
950
1625
|
this.inputNodes.clear();
|
|
951
1626
|
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
|
-
}
|
|
1627
|
+
for (const node of input.nodes) this.inputNodes.set(node.id, node);
|
|
1628
|
+
for (const edge of input.edges) this.inputEdges.set(edge.id, edge);
|
|
958
1629
|
return this.recompute();
|
|
959
1630
|
}
|
|
960
|
-
/**
|
|
961
|
-
* Add nodes and edges incrementally.
|
|
962
|
-
* Attempts to minimize layout changes for existing nodes.
|
|
963
|
-
*/
|
|
964
1631
|
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
|
-
);
|
|
1632
|
+
for (const node of nodes) this.inputNodes.set(node.id, node);
|
|
1633
|
+
if (edges) for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1634
|
+
return this.recomputeWithStability(new Set(nodes.map((n) => n.id)));
|
|
977
1635
|
}
|
|
978
|
-
/**
|
|
979
|
-
* Remove nodes (and their connected edges) from the layout.
|
|
980
|
-
*/
|
|
981
1636
|
removeNodes(nodeIds) {
|
|
982
1637
|
const removedSet = new Set(nodeIds);
|
|
983
|
-
for (const id of nodeIds)
|
|
984
|
-
this.inputNodes.delete(id);
|
|
985
|
-
}
|
|
1638
|
+
for (const id of nodeIds) this.inputNodes.delete(id);
|
|
986
1639
|
for (const [edgeId, edge] of this.inputEdges) {
|
|
987
1640
|
if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
|
|
988
1641
|
this.inputEdges.delete(edgeId);
|
|
989
1642
|
}
|
|
990
1643
|
}
|
|
991
|
-
for (const id of nodeIds)
|
|
992
|
-
this.nodePositions.delete(id);
|
|
993
|
-
}
|
|
1644
|
+
for (const id of nodeIds) this.nodePositions.delete(id);
|
|
994
1645
|
return this.recompute();
|
|
995
1646
|
}
|
|
996
|
-
/**
|
|
997
|
-
* Add edges between existing nodes.
|
|
998
|
-
*/
|
|
999
1647
|
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
|
-
);
|
|
1648
|
+
for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1649
|
+
return this.recomputeWithStability(/* @__PURE__ */ new Set());
|
|
1007
1650
|
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Remove edges from the layout.
|
|
1010
|
-
*/
|
|
1011
1651
|
removeEdges(edgeIds) {
|
|
1012
|
-
for (const id of edgeIds)
|
|
1013
|
-
this.inputEdges.delete(id);
|
|
1014
|
-
}
|
|
1652
|
+
for (const id of edgeIds) this.inputEdges.delete(id);
|
|
1015
1653
|
return this.recompute();
|
|
1016
1654
|
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Get the current layout result.
|
|
1019
|
-
*/
|
|
1020
1655
|
getResult() {
|
|
1021
1656
|
return this.lastResult;
|
|
1022
1657
|
}
|
|
@@ -1025,42 +1660,48 @@ var IncrementalLayout = class {
|
|
|
1025
1660
|
nodes: [...this.inputNodes.values()],
|
|
1026
1661
|
edges: [...this.inputEdges.values()]
|
|
1027
1662
|
};
|
|
1028
|
-
|
|
1029
|
-
this.lastResult = computeLayout(graph, this.options);
|
|
1663
|
+
this.lastResult = layout(input, this.options);
|
|
1030
1664
|
this.cachePositions();
|
|
1031
1665
|
return this.lastResult;
|
|
1032
1666
|
}
|
|
1033
|
-
|
|
1667
|
+
recomputeWithStability(newNodeIds) {
|
|
1034
1668
|
const input = {
|
|
1035
1669
|
nodes: [...this.inputNodes.values()],
|
|
1036
1670
|
edges: [...this.inputEdges.values()]
|
|
1037
1671
|
};
|
|
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);
|
|
1672
|
+
const fresh = layout(input, this.options);
|
|
1673
|
+
this.lastResult = this.applyStability(fresh, newNodeIds);
|
|
1047
1674
|
this.cachePositions();
|
|
1048
1675
|
return this.lastResult;
|
|
1049
1676
|
}
|
|
1050
1677
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1678
|
+
* Blend the freshly computed positions with the previous ones, so existing
|
|
1679
|
+
* nodes don't jump too far when a small change happens.
|
|
1053
1680
|
*/
|
|
1054
|
-
applyStability(
|
|
1055
|
-
if (this.nodePositions.size === 0) return;
|
|
1681
|
+
applyStability(fresh, newNodeIds) {
|
|
1682
|
+
if (this.nodePositions.size === 0) return fresh;
|
|
1056
1683
|
const STABILITY_WEIGHT = 0.3;
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1684
|
+
const blended = {
|
|
1685
|
+
nodes: fresh.nodes.map((n) => {
|
|
1686
|
+
if (newNodeIds.has(n.id)) return n;
|
|
1687
|
+
const old = this.nodePositions.get(n.id);
|
|
1688
|
+
if (!old) return n;
|
|
1689
|
+
const dx = old.x - n.x;
|
|
1690
|
+
const dy = old.y - n.y;
|
|
1691
|
+
const x = n.x + dx * STABILITY_WEIGHT;
|
|
1692
|
+
const y = n.y + dy * STABILITY_WEIGHT;
|
|
1693
|
+
const ddx = x - n.x;
|
|
1694
|
+
const ddy = y - n.y;
|
|
1695
|
+
return {
|
|
1696
|
+
...n,
|
|
1697
|
+
x,
|
|
1698
|
+
y,
|
|
1699
|
+
handles: n.handles.map((h) => ({ ...h, x: h.x + ddx, y: h.y + ddy }))
|
|
1700
|
+
};
|
|
1701
|
+
}),
|
|
1702
|
+
edges: fresh.edges
|
|
1703
|
+
};
|
|
1704
|
+
return blended;
|
|
1064
1705
|
}
|
|
1065
1706
|
cachePositions() {
|
|
1066
1707
|
if (!this.lastResult) return;
|
|
@@ -1069,47 +1710,161 @@ var IncrementalLayout = class {
|
|
|
1069
1710
|
this.nodePositions.set(node.id, { x: node.x, y: node.y });
|
|
1070
1711
|
}
|
|
1071
1712
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
// src/debug.ts
|
|
1716
|
+
function printLayout(result, options = {}) {
|
|
1717
|
+
const lines = [];
|
|
1718
|
+
const { nodes, edges } = result;
|
|
1719
|
+
if (nodes.length === 0) return "(empty layout)";
|
|
1720
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1721
|
+
for (const n of nodes) {
|
|
1722
|
+
if (n.x < minX) minX = n.x;
|
|
1723
|
+
if (n.y < minY) minY = n.y;
|
|
1724
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1725
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1726
|
+
}
|
|
1727
|
+
lines.push(`=== layout ${nodes.length} nodes, ${edges.length} edges ===`);
|
|
1728
|
+
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)})`);
|
|
1729
|
+
const byY = [...nodes].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
1730
|
+
const bands = [];
|
|
1731
|
+
for (const n of byY) {
|
|
1732
|
+
const placed = bands.find((b) => Math.abs(b[0].y - n.y) < 1);
|
|
1733
|
+
if (placed) placed.push(n);
|
|
1734
|
+
else bands.push([n]);
|
|
1735
|
+
}
|
|
1736
|
+
lines.push("");
|
|
1737
|
+
lines.push("--- Y bands ---");
|
|
1738
|
+
for (const band of bands) {
|
|
1739
|
+
const sorted = [...band].sort((a, b) => a.x - b.x);
|
|
1740
|
+
const summary = sorted.map((n) => {
|
|
1741
|
+
const p = n.parentId ? `[${n.parentId}/]` : "";
|
|
1742
|
+
return `${p}${n.id}@(${n.x.toFixed(0)},${n.y.toFixed(0)} ${n.width}x${n.height})`;
|
|
1743
|
+
}).join(" ");
|
|
1744
|
+
lines.push(` y=${band[0].y.toFixed(0).padStart(4)} : ${summary}`);
|
|
1745
|
+
}
|
|
1746
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const n of nodes) {
|
|
1748
|
+
const key = n.parentId;
|
|
1749
|
+
const arr = byParent.get(key) ?? [];
|
|
1750
|
+
arr.push(n);
|
|
1751
|
+
byParent.set(key, arr);
|
|
1752
|
+
}
|
|
1753
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1754
|
+
for (const k of byParent.keys()) {
|
|
1755
|
+
if (k && nodes.find((n) => n.id === k)) compoundIds.add(k);
|
|
1756
|
+
}
|
|
1757
|
+
if (compoundIds.size > 0) {
|
|
1758
|
+
let printSubtree2 = function(id, depth) {
|
|
1759
|
+
const children = byParent.get(id) ?? [];
|
|
1760
|
+
for (const c of children) {
|
|
1761
|
+
const prefix = " ".repeat(depth);
|
|
1762
|
+
const tag = compoundIds.has(c.id) ? " (compound)" : "";
|
|
1763
|
+
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)})`);
|
|
1764
|
+
if (compoundIds.has(c.id)) printSubtree2(c.id, depth + 1);
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
var printSubtree = printSubtree2;
|
|
1768
|
+
lines.push("");
|
|
1769
|
+
lines.push("--- hierarchy ---");
|
|
1770
|
+
printSubtree2(void 0, 0);
|
|
1771
|
+
}
|
|
1772
|
+
lines.push("");
|
|
1773
|
+
lines.push("--- edges ---");
|
|
1774
|
+
for (const e of edges) {
|
|
1775
|
+
const head = e.points[0];
|
|
1776
|
+
const tail = e.points[e.points.length - 1];
|
|
1777
|
+
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`);
|
|
1778
|
+
}
|
|
1779
|
+
const overlaps = findOverlaps(nodes);
|
|
1780
|
+
if (overlaps.length > 0) {
|
|
1781
|
+
lines.push("");
|
|
1782
|
+
lines.push("--- OVERLAPS (problem!) ---");
|
|
1783
|
+
for (const o of overlaps) {
|
|
1784
|
+
lines.push(` ${o.a} overlaps ${o.b}`);
|
|
1095
1785
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1786
|
+
}
|
|
1787
|
+
if (options.grid !== false) {
|
|
1788
|
+
lines.push("");
|
|
1789
|
+
lines.push("--- ASCII grid ---");
|
|
1790
|
+
lines.push(asciiGrid(nodes, edges, options));
|
|
1791
|
+
}
|
|
1792
|
+
return lines.join("\n");
|
|
1793
|
+
}
|
|
1794
|
+
function findOverlaps(nodes) {
|
|
1795
|
+
const out = [];
|
|
1796
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1797
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1798
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1799
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1800
|
+
const a = nodes[i];
|
|
1801
|
+
const b = nodes[j];
|
|
1802
|
+
if (a.id === b.parentId || b.id === a.parentId) continue;
|
|
1803
|
+
const overlapX = a.x < b.x + b.width && b.x < a.x + a.width;
|
|
1804
|
+
const overlapY = a.y < b.y + b.height && b.y < a.y + a.height;
|
|
1805
|
+
if (overlapX && overlapY) {
|
|
1806
|
+
out.push({ a: a.id, b: b.id });
|
|
1807
|
+
}
|
|
1105
1808
|
}
|
|
1106
|
-
return { nodes, edges };
|
|
1107
1809
|
}
|
|
1108
|
-
|
|
1810
|
+
return out;
|
|
1811
|
+
}
|
|
1812
|
+
function asciiGrid(nodes, edges, options) {
|
|
1813
|
+
if (nodes.length === 0) return "";
|
|
1814
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1815
|
+
for (const n of nodes) {
|
|
1816
|
+
if (n.x < minX) minX = n.x;
|
|
1817
|
+
if (n.y < minY) minY = n.y;
|
|
1818
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1819
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1820
|
+
}
|
|
1821
|
+
for (const e of edges) for (const p of e.points) {
|
|
1822
|
+
if (p.x < minX) minX = p.x;
|
|
1823
|
+
if (p.y < minY) minY = p.y;
|
|
1824
|
+
if (p.x > maxX) maxX = p.x;
|
|
1825
|
+
if (p.y > maxY) maxY = p.y;
|
|
1826
|
+
}
|
|
1827
|
+
const targetW = options.gridWidth ?? 80;
|
|
1828
|
+
const layoutW = Math.max(1, maxX - minX);
|
|
1829
|
+
const layoutH = Math.max(1, maxY - minY);
|
|
1830
|
+
const scaleX = options.gridScale ?? targetW / layoutW;
|
|
1831
|
+
const scaleY = scaleX * 0.5;
|
|
1832
|
+
const w = Math.max(1, Math.ceil(layoutW * scaleX) + 1);
|
|
1833
|
+
const h = Math.max(1, Math.ceil(layoutH * scaleY) + 1);
|
|
1834
|
+
if (h > 80) return "(grid suppressed: too tall \u2014 pass {grid:false} to skip)";
|
|
1835
|
+
const grid = Array.from({ length: h }, () => Array.from({ length: w }, () => " "));
|
|
1836
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1837
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1838
|
+
for (const n of nodes) {
|
|
1839
|
+
const x0 = Math.round((n.x - minX) * scaleX);
|
|
1840
|
+
const y0 = Math.round((n.y - minY) * scaleY);
|
|
1841
|
+
const x1 = Math.max(x0, Math.round((n.x + n.width - minX) * scaleX) - 1);
|
|
1842
|
+
const y1 = Math.max(y0, Math.round((n.y + n.height - minY) * scaleY) - 1);
|
|
1843
|
+
const isCompound = compoundIds.has(n.id);
|
|
1844
|
+
const ch = isCompound ? "." : "#";
|
|
1845
|
+
for (let y = y0; y <= y1 && y < h; y++) {
|
|
1846
|
+
for (let x = x0; x <= x1 && x < w; x++) {
|
|
1847
|
+
if (y < 0 || x < 0) continue;
|
|
1848
|
+
if (y === y0 || y === y1 || x === x0 || x === x1) grid[y][x] = ch;
|
|
1849
|
+
else if (isCompound) {
|
|
1850
|
+
} else grid[y][x] = ch;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
const label = n.id.slice(0, Math.min(n.id.length, Math.max(2, x1 - x0 - 1)));
|
|
1854
|
+
const lx = Math.max(0, x0 + 1);
|
|
1855
|
+
const ly = Math.max(0, y0);
|
|
1856
|
+
for (let i = 0; i < label.length && lx + i < w; i++) {
|
|
1857
|
+
if (ly < h) grid[ly][lx + i] = label[i];
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return grid.map((row) => row.join("")).join("\n");
|
|
1861
|
+
}
|
|
1109
1862
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1110
1863
|
0 && (module.exports = {
|
|
1111
1864
|
IncrementalLayout,
|
|
1112
1865
|
countAllCrossings,
|
|
1113
|
-
layout
|
|
1866
|
+
layout,
|
|
1867
|
+
printLayout,
|
|
1868
|
+
rotateHandles
|
|
1114
1869
|
});
|
|
1115
1870
|
//# sourceMappingURL=index.js.map
|