@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.mjs
CHANGED
|
@@ -6,7 +6,12 @@ function resolveOptions(options) {
|
|
|
6
6
|
layerSpacing: options?.layerSpacing ?? 60,
|
|
7
7
|
crossingMinimizationIterations: options?.crossingMinimizationIterations ?? 24,
|
|
8
8
|
coordinateOptimizationIterations: options?.coordinateOptimizationIterations ?? 8,
|
|
9
|
-
edgeMargin: options?.edgeMargin ?? 20
|
|
9
|
+
edgeMargin: options?.edgeMargin ?? 20,
|
|
10
|
+
controlWeight: options?.edgeWeights?.control ?? 1,
|
|
11
|
+
dataWeight: options?.edgeWeights?.data ?? 0.25,
|
|
12
|
+
packComponents: options?.packComponents ?? true,
|
|
13
|
+
compoundPadding: options?.compoundPadding ?? 24,
|
|
14
|
+
onProposal: options?.onProposal
|
|
10
15
|
};
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -69,7 +74,7 @@ var Graph = class {
|
|
|
69
74
|
return result;
|
|
70
75
|
}
|
|
71
76
|
};
|
|
72
|
-
function buildGraph(nodes, edges) {
|
|
77
|
+
function buildGraph(nodes, edges, ctx = { controlWeight: 1, dataWeight: 0.25 }) {
|
|
73
78
|
const graph = new Graph();
|
|
74
79
|
for (const node of nodes) {
|
|
75
80
|
graph.addNode({
|
|
@@ -81,10 +86,15 @@ function buildGraph(nodes, edges) {
|
|
|
81
86
|
layer: -1,
|
|
82
87
|
order: -1,
|
|
83
88
|
x: 0,
|
|
84
|
-
y: 0
|
|
89
|
+
y: 0,
|
|
90
|
+
parentId: node.parentId,
|
|
91
|
+
isCompound: false,
|
|
92
|
+
isValue: false
|
|
85
93
|
});
|
|
86
94
|
}
|
|
87
95
|
for (const edge of edges) {
|
|
96
|
+
const kind = edge.kind ?? "control";
|
|
97
|
+
const defaultWeight = kind === "control" ? ctx.controlWeight : ctx.dataWeight;
|
|
88
98
|
graph.addEdge({
|
|
89
99
|
id: edge.id,
|
|
90
100
|
from: edge.from,
|
|
@@ -92,9 +102,34 @@ function buildGraph(nodes, edges) {
|
|
|
92
102
|
fromHandle: edge.fromHandle,
|
|
93
103
|
toHandle: edge.toHandle,
|
|
94
104
|
reversed: false,
|
|
95
|
-
originalId: edge.id
|
|
105
|
+
originalId: edge.id,
|
|
106
|
+
kind,
|
|
107
|
+
weight: edge.weight ?? defaultWeight
|
|
96
108
|
});
|
|
97
109
|
}
|
|
110
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
111
|
+
const ins = graph.inEdges.get(nodeId);
|
|
112
|
+
const outs = graph.outEdges.get(nodeId);
|
|
113
|
+
let hasControl = false;
|
|
114
|
+
if (ins) {
|
|
115
|
+
for (const eid of ins) {
|
|
116
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
117
|
+
hasControl = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!hasControl && outs) {
|
|
123
|
+
for (const eid of outs) {
|
|
124
|
+
if (graph.edges.get(eid)?.kind === "control") {
|
|
125
|
+
hasControl = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const incidentCount = (ins?.size ?? 0) + (outs?.size ?? 0);
|
|
131
|
+
node.isValue = !hasControl && incidentCount > 0;
|
|
132
|
+
}
|
|
98
133
|
return graph;
|
|
99
134
|
}
|
|
100
135
|
function getHandlePosition(node, handleId) {
|
|
@@ -182,28 +217,88 @@ function breakCycles(graph) {
|
|
|
182
217
|
function assignLayers(graph) {
|
|
183
218
|
const layers = /* @__PURE__ */ new Map();
|
|
184
219
|
const visiting = /* @__PURE__ */ new Set();
|
|
185
|
-
function
|
|
220
|
+
function controlLayer(nodeId) {
|
|
186
221
|
if (layers.has(nodeId)) return layers.get(nodeId);
|
|
187
222
|
if (visiting.has(nodeId)) return 0;
|
|
223
|
+
const node = graph.nodes.get(nodeId);
|
|
224
|
+
if (!node || node.isValue) return -1;
|
|
188
225
|
visiting.add(nodeId);
|
|
189
|
-
const
|
|
226
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
190
227
|
let maxPredLayer = -1;
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
if (inEdgeIds) {
|
|
229
|
+
for (const eid of inEdgeIds) {
|
|
230
|
+
const edge = graph.edges.get(eid);
|
|
231
|
+
if (!edge || edge.kind !== "control") continue;
|
|
232
|
+
const pred = graph.nodes.get(edge.from);
|
|
233
|
+
if (!pred || pred.isValue) continue;
|
|
234
|
+
maxPredLayer = Math.max(maxPredLayer, controlLayer(edge.from));
|
|
235
|
+
}
|
|
193
236
|
}
|
|
194
237
|
const layer = maxPredLayer + 1;
|
|
195
238
|
layers.set(nodeId, layer);
|
|
196
|
-
|
|
197
|
-
if (node) node.layer = layer;
|
|
239
|
+
node.layer = layer;
|
|
198
240
|
visiting.delete(nodeId);
|
|
199
241
|
return layer;
|
|
200
242
|
}
|
|
201
|
-
for (const nodeId of graph.nodes
|
|
202
|
-
|
|
243
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
244
|
+
if (!node.isValue) controlLayer(nodeId);
|
|
245
|
+
}
|
|
246
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
247
|
+
if (!node.isValue) continue;
|
|
248
|
+
const neighborLayers = [];
|
|
249
|
+
const inEdgeIds = graph.inEdges.get(nodeId);
|
|
250
|
+
if (inEdgeIds) {
|
|
251
|
+
for (const eid of inEdgeIds) {
|
|
252
|
+
const edge = graph.edges.get(eid);
|
|
253
|
+
if (!edge) continue;
|
|
254
|
+
const nbr = graph.nodes.get(edge.from);
|
|
255
|
+
if (nbr && !nbr.isValue && layers.has(edge.from)) {
|
|
256
|
+
neighborLayers.push(layers.get(edge.from));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const outEdgeIds = graph.outEdges.get(nodeId);
|
|
261
|
+
if (outEdgeIds) {
|
|
262
|
+
for (const eid of outEdgeIds) {
|
|
263
|
+
const edge = graph.edges.get(eid);
|
|
264
|
+
if (!edge) continue;
|
|
265
|
+
const nbr = graph.nodes.get(edge.to);
|
|
266
|
+
if (nbr && !nbr.isValue && layers.has(edge.to)) {
|
|
267
|
+
neighborLayers.push(layers.get(edge.to));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let layer;
|
|
272
|
+
if (neighborLayers.length > 0) {
|
|
273
|
+
neighborLayers.sort((a, b) => a - b);
|
|
274
|
+
layer = neighborLayers[Math.floor(neighborLayers.length / 2)];
|
|
275
|
+
} else {
|
|
276
|
+
layer = 0;
|
|
277
|
+
}
|
|
278
|
+
layers.set(nodeId, layer);
|
|
279
|
+
node.layer = layer;
|
|
203
280
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
281
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
282
|
+
if (!layers.has(nodeId)) {
|
|
283
|
+
layers.set(nodeId, 0);
|
|
284
|
+
node.layer = 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let minLayer = Infinity;
|
|
288
|
+
let maxLayer = -Infinity;
|
|
289
|
+
for (const l of layers.values()) {
|
|
290
|
+
if (l < minLayer) minLayer = l;
|
|
291
|
+
if (l > maxLayer) maxLayer = l;
|
|
292
|
+
}
|
|
293
|
+
if (!isFinite(minLayer)) return [];
|
|
294
|
+
if (minLayer !== 0) {
|
|
295
|
+
for (const [id, l] of layers) {
|
|
296
|
+
const adj = l - minLayer;
|
|
297
|
+
layers.set(id, adj);
|
|
298
|
+
const n = graph.nodes.get(id);
|
|
299
|
+
if (n) n.layer = adj;
|
|
300
|
+
}
|
|
301
|
+
maxLayer -= minLayer;
|
|
207
302
|
}
|
|
208
303
|
const layersArray = Array.from({ length: maxLayer + 1 }, () => []);
|
|
209
304
|
for (const [nodeId, layer] of layers) {
|
|
@@ -215,6 +310,7 @@ function insertDummyNodes(graph, layers) {
|
|
|
215
310
|
let dummyCounter = 0;
|
|
216
311
|
const edgesToProcess = [...graph.edges.values()];
|
|
217
312
|
for (const edge of edgesToProcess) {
|
|
313
|
+
if (edge.kind !== "control") continue;
|
|
218
314
|
const fromNode = graph.nodes.get(edge.from);
|
|
219
315
|
const toNode = graph.nodes.get(edge.to);
|
|
220
316
|
if (!fromNode || !toNode) continue;
|
|
@@ -239,7 +335,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
239
335
|
layer: l,
|
|
240
336
|
order: -1,
|
|
241
337
|
x: 0,
|
|
242
|
-
y: 0
|
|
338
|
+
y: 0,
|
|
339
|
+
isCompound: false,
|
|
340
|
+
isValue: false
|
|
243
341
|
});
|
|
244
342
|
layers[l].push(dummyId);
|
|
245
343
|
graph.addEdge({
|
|
@@ -249,7 +347,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
249
347
|
fromHandle: prevHandleId,
|
|
250
348
|
toHandle: "in",
|
|
251
349
|
reversed: edge.reversed,
|
|
252
|
-
originalId: edge.originalId
|
|
350
|
+
originalId: edge.originalId,
|
|
351
|
+
kind: edge.kind,
|
|
352
|
+
weight: edge.weight
|
|
253
353
|
});
|
|
254
354
|
prevNodeId = dummyId;
|
|
255
355
|
prevHandleId = "out";
|
|
@@ -261,7 +361,9 @@ function insertDummyNodes(graph, layers) {
|
|
|
261
361
|
fromHandle: prevHandleId,
|
|
262
362
|
toHandle: edge.toHandle,
|
|
263
363
|
reversed: edge.reversed,
|
|
264
|
-
originalId: edge.originalId
|
|
364
|
+
originalId: edge.originalId,
|
|
365
|
+
kind: edge.kind,
|
|
366
|
+
weight: edge.weight
|
|
265
367
|
});
|
|
266
368
|
}
|
|
267
369
|
return layers;
|
|
@@ -682,6 +784,176 @@ function getOrderSize(node, isHorizontal) {
|
|
|
682
784
|
return isHorizontal ? node.height : node.width;
|
|
683
785
|
}
|
|
684
786
|
|
|
787
|
+
// src/algorithms/value-placement.ts
|
|
788
|
+
function placeValueSidecars(graph, layers, options) {
|
|
789
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
790
|
+
const attachments = [];
|
|
791
|
+
const orphanValues = [];
|
|
792
|
+
for (const [nodeId, node] of graph.nodes) {
|
|
793
|
+
if (!node.isValue || node.isDummy) continue;
|
|
794
|
+
const consumer = findDominantConsumer(graph, nodeId);
|
|
795
|
+
if (consumer) {
|
|
796
|
+
attachments.push({ valueId: nodeId, consumerId: consumer });
|
|
797
|
+
} else {
|
|
798
|
+
orphanValues.push(nodeId);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const byConsumer = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const { valueId, consumerId } of attachments) {
|
|
803
|
+
const arr = byConsumer.get(consumerId) ?? [];
|
|
804
|
+
arr.push(valueId);
|
|
805
|
+
byConsumer.set(consumerId, arr);
|
|
806
|
+
}
|
|
807
|
+
for (const [consumerId, vIds] of byConsumer) {
|
|
808
|
+
const consumer = graph.nodes.get(consumerId);
|
|
809
|
+
if (!consumer) continue;
|
|
810
|
+
const { beforeSide, afterSide } = splitValuesBySide(graph, consumerId, vIds, isHorizontal);
|
|
811
|
+
if (isHorizontal) {
|
|
812
|
+
const cxCenter = consumer.x + consumer.width / 2;
|
|
813
|
+
let topEdge = consumer.y;
|
|
814
|
+
for (const vid of beforeSide) {
|
|
815
|
+
const v = graph.nodes.get(vid);
|
|
816
|
+
if (!v) continue;
|
|
817
|
+
v.y = topEdge - options.nodeSpacing - v.height;
|
|
818
|
+
v.x = cxCenter - v.width / 2;
|
|
819
|
+
topEdge = v.y;
|
|
820
|
+
}
|
|
821
|
+
let bottomEdge = consumer.y + consumer.height;
|
|
822
|
+
for (const vid of afterSide) {
|
|
823
|
+
const v = graph.nodes.get(vid);
|
|
824
|
+
if (!v) continue;
|
|
825
|
+
v.y = bottomEdge + options.nodeSpacing;
|
|
826
|
+
v.x = cxCenter - v.width / 2;
|
|
827
|
+
bottomEdge = v.y + v.height;
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
const cyCenter = consumer.y + consumer.height / 2;
|
|
831
|
+
let leftEdge = consumer.x;
|
|
832
|
+
for (const vid of beforeSide) {
|
|
833
|
+
const v = graph.nodes.get(vid);
|
|
834
|
+
if (!v) continue;
|
|
835
|
+
v.x = leftEdge - options.nodeSpacing - v.width;
|
|
836
|
+
v.y = cyCenter - v.height / 2;
|
|
837
|
+
leftEdge = v.x;
|
|
838
|
+
}
|
|
839
|
+
let rightEdge = consumer.x + consumer.width;
|
|
840
|
+
for (const vid of afterSide) {
|
|
841
|
+
const v = graph.nodes.get(vid);
|
|
842
|
+
if (!v) continue;
|
|
843
|
+
v.x = rightEdge + options.nodeSpacing;
|
|
844
|
+
v.y = cyCenter - v.height / 2;
|
|
845
|
+
rightEdge = v.x + v.width;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
let orphanX = 0, orphanY = 0;
|
|
850
|
+
for (const orphanId of orphanValues) {
|
|
851
|
+
const v = graph.nodes.get(orphanId);
|
|
852
|
+
if (!v) continue;
|
|
853
|
+
v.x = orphanX;
|
|
854
|
+
v.y = orphanY;
|
|
855
|
+
if (isHorizontal) orphanX += v.width + options.nodeSpacing;
|
|
856
|
+
else orphanY += v.height + options.nodeSpacing;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function findDominantConsumer(graph, valueId) {
|
|
860
|
+
const counts = /* @__PURE__ */ new Map();
|
|
861
|
+
const outs = graph.outEdges.get(valueId);
|
|
862
|
+
if (outs) {
|
|
863
|
+
for (const eid of outs) {
|
|
864
|
+
const edge = graph.edges.get(eid);
|
|
865
|
+
if (!edge) continue;
|
|
866
|
+
const other = graph.nodes.get(edge.to);
|
|
867
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
868
|
+
counts.set(edge.to, (counts.get(edge.to) ?? 0) + 1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const ins = graph.inEdges.get(valueId);
|
|
873
|
+
if (ins) {
|
|
874
|
+
for (const eid of ins) {
|
|
875
|
+
const edge = graph.edges.get(eid);
|
|
876
|
+
if (!edge) continue;
|
|
877
|
+
const other = graph.nodes.get(edge.from);
|
|
878
|
+
if (other && !other.isValue && !other.isDummy) {
|
|
879
|
+
counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
let bestId = null;
|
|
884
|
+
let bestCount = -1;
|
|
885
|
+
for (const [id, count] of counts) {
|
|
886
|
+
if (count > bestCount || count === bestCount && bestId !== null && id < bestId) {
|
|
887
|
+
bestId = id;
|
|
888
|
+
bestCount = count;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return bestId;
|
|
892
|
+
}
|
|
893
|
+
function splitValuesBySide(graph, consumerId, valueIds, isHorizontal) {
|
|
894
|
+
const consumer = graph.nodes.get(consumerId);
|
|
895
|
+
if (!consumer) return { beforeSide: [...valueIds].sort(), afterSide: [] };
|
|
896
|
+
const slots = [];
|
|
897
|
+
for (const vId of valueIds) {
|
|
898
|
+
let side = "neutral";
|
|
899
|
+
let offset = 0.5;
|
|
900
|
+
const inspectHandle = (handleId) => {
|
|
901
|
+
if (!handleId) return;
|
|
902
|
+
const handle = consumer.handles.find((h) => h.id === handleId);
|
|
903
|
+
if (!handle) return;
|
|
904
|
+
offset = handle.offset ?? 0.5;
|
|
905
|
+
if (isHorizontal) {
|
|
906
|
+
if (handle.position === "top") side = "before";
|
|
907
|
+
else if (handle.position === "bottom") side = "after";
|
|
908
|
+
} else {
|
|
909
|
+
if (handle.position === "left") side = "before";
|
|
910
|
+
else if (handle.position === "right") side = "after";
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
const outs = graph.outEdges.get(vId);
|
|
914
|
+
if (outs) for (const eid of outs) {
|
|
915
|
+
const e = graph.edges.get(eid);
|
|
916
|
+
if (e && e.to === consumerId) {
|
|
917
|
+
inspectHandle(e.toHandle);
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (side === "neutral") {
|
|
922
|
+
const ins = graph.inEdges.get(vId);
|
|
923
|
+
if (ins) for (const eid of ins) {
|
|
924
|
+
const e = graph.edges.get(eid);
|
|
925
|
+
if (e && e.from === consumerId) {
|
|
926
|
+
inspectHandle(e.fromHandle);
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
slots.push({ id: vId, side, offset });
|
|
932
|
+
}
|
|
933
|
+
const before = slots.filter((s) => s.side === "before");
|
|
934
|
+
const after = slots.filter((s) => s.side === "after");
|
|
935
|
+
const neutral = slots.filter((s) => s.side === "neutral");
|
|
936
|
+
for (const n of neutral) {
|
|
937
|
+
if (before.length <= after.length) before.push(n);
|
|
938
|
+
else after.push(n);
|
|
939
|
+
}
|
|
940
|
+
const cmp = (a, b) => a.offset - b.offset || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
941
|
+
before.sort(cmp);
|
|
942
|
+
after.sort(cmp);
|
|
943
|
+
return {
|
|
944
|
+
beforeSide: before.map((s) => s.id),
|
|
945
|
+
afterSide: after.map((s) => s.id)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function railLayers(layers, graph) {
|
|
949
|
+
return layers.map(
|
|
950
|
+
(layer) => layer.filter((id) => {
|
|
951
|
+
const n = graph.nodes.get(id);
|
|
952
|
+
return n && !n.isValue;
|
|
953
|
+
})
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
685
957
|
// src/algorithms/edge-routing.ts
|
|
686
958
|
function routeEdges(graph, direction, edgeMargin) {
|
|
687
959
|
const chains = collectEdgeChains(graph);
|
|
@@ -732,7 +1004,8 @@ function collectEdgeChains(graph) {
|
|
|
732
1004
|
fromHandle: edge.fromHandle,
|
|
733
1005
|
toHandle: currentEdge.toHandle,
|
|
734
1006
|
dummyNodes,
|
|
735
|
-
reversed: edge.reversed
|
|
1007
|
+
reversed: edge.reversed,
|
|
1008
|
+
kind: edge.kind
|
|
736
1009
|
});
|
|
737
1010
|
}
|
|
738
1011
|
return chains;
|
|
@@ -777,7 +1050,8 @@ function routeChain(graph, chain, direction, margin) {
|
|
|
777
1050
|
to: actualTo,
|
|
778
1051
|
fromHandle: actualFromHandle,
|
|
779
1052
|
toHandle: actualToHandle,
|
|
780
|
-
points
|
|
1053
|
+
points,
|
|
1054
|
+
kind: chain.kind
|
|
781
1055
|
};
|
|
782
1056
|
}
|
|
783
1057
|
function makeOrthogonal(points, direction) {
|
|
@@ -851,21 +1125,292 @@ function getDefaultTargetSide(direction) {
|
|
|
851
1125
|
}
|
|
852
1126
|
}
|
|
853
1127
|
|
|
1128
|
+
// src/algorithms/component-packing.ts
|
|
1129
|
+
function packComponents(nodes, edges, options) {
|
|
1130
|
+
if (nodes.length === 0) return;
|
|
1131
|
+
const idToNode = /* @__PURE__ */ new Map();
|
|
1132
|
+
for (const n of nodes) idToNode.set(n.id, n);
|
|
1133
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
1134
|
+
function topAncestor(id) {
|
|
1135
|
+
if (rootOf.has(id)) return rootOf.get(id);
|
|
1136
|
+
const n = idToNode.get(id);
|
|
1137
|
+
if (!n || !n.parentId || !idToNode.has(n.parentId)) {
|
|
1138
|
+
rootOf.set(id, id);
|
|
1139
|
+
return id;
|
|
1140
|
+
}
|
|
1141
|
+
const r = topAncestor(n.parentId);
|
|
1142
|
+
rootOf.set(id, r);
|
|
1143
|
+
return r;
|
|
1144
|
+
}
|
|
1145
|
+
for (const n of nodes) topAncestor(n.id);
|
|
1146
|
+
const parent = /* @__PURE__ */ new Map();
|
|
1147
|
+
function find(x) {
|
|
1148
|
+
let cur = x;
|
|
1149
|
+
while (parent.get(cur) !== cur) {
|
|
1150
|
+
const p = parent.get(cur);
|
|
1151
|
+
parent.set(cur, parent.get(p));
|
|
1152
|
+
cur = parent.get(cur);
|
|
1153
|
+
}
|
|
1154
|
+
return cur;
|
|
1155
|
+
}
|
|
1156
|
+
function union(a, b) {
|
|
1157
|
+
const ra = find(a);
|
|
1158
|
+
const rb = find(b);
|
|
1159
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
1160
|
+
}
|
|
1161
|
+
for (const n of nodes) parent.set(topAncestor(n.id), topAncestor(n.id));
|
|
1162
|
+
for (const e of edges) {
|
|
1163
|
+
const ra = topAncestor(e.from);
|
|
1164
|
+
const rb = topAncestor(e.to);
|
|
1165
|
+
if (idToNode.has(ra) && idToNode.has(rb)) union(ra, rb);
|
|
1166
|
+
}
|
|
1167
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1168
|
+
for (const n of nodes) {
|
|
1169
|
+
const root = topAncestor(n.id);
|
|
1170
|
+
const comp = find(root);
|
|
1171
|
+
const arr = groups.get(comp) ?? [];
|
|
1172
|
+
arr.push(n);
|
|
1173
|
+
groups.set(comp, arr);
|
|
1174
|
+
}
|
|
1175
|
+
if (groups.size <= 1) return;
|
|
1176
|
+
const isHorizontal = options.direction === "LR" || options.direction === "RL";
|
|
1177
|
+
const edgesByComp = /* @__PURE__ */ new Map();
|
|
1178
|
+
for (const e of edges) {
|
|
1179
|
+
const root = topAncestor(e.from);
|
|
1180
|
+
const comp = find(root);
|
|
1181
|
+
const arr = edgesByComp.get(comp) ?? [];
|
|
1182
|
+
arr.push(e);
|
|
1183
|
+
edgesByComp.set(comp, arr);
|
|
1184
|
+
}
|
|
1185
|
+
const boxes = [];
|
|
1186
|
+
for (const [compId, group] of groups) {
|
|
1187
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1188
|
+
for (const n of group) {
|
|
1189
|
+
if (n.x < minX) minX = n.x;
|
|
1190
|
+
if (n.y < minY) minY = n.y;
|
|
1191
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1192
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1193
|
+
}
|
|
1194
|
+
const compEdges = edgesByComp.get(compId) ?? [];
|
|
1195
|
+
for (const e of compEdges) {
|
|
1196
|
+
for (const p of e.points) {
|
|
1197
|
+
if (p.x < minX) minX = p.x;
|
|
1198
|
+
if (p.y < minY) minY = p.y;
|
|
1199
|
+
if (p.x > maxX) maxX = p.x;
|
|
1200
|
+
if (p.y > maxY) maxY = p.y;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
boxes.push({ id: compId, nodes: group, edges: compEdges, minX, minY, maxX, maxY });
|
|
1204
|
+
}
|
|
1205
|
+
boxes.sort((a, b) => {
|
|
1206
|
+
const sizeA = isHorizontal ? a.maxX - a.minX : a.maxY - a.minY;
|
|
1207
|
+
const sizeB = isHorizontal ? b.maxX - b.minX : b.maxY - b.minY;
|
|
1208
|
+
return sizeB - sizeA;
|
|
1209
|
+
});
|
|
1210
|
+
const gap = options.layerSpacing;
|
|
1211
|
+
let cursor = 0;
|
|
1212
|
+
for (const box of boxes) {
|
|
1213
|
+
if (isHorizontal) {
|
|
1214
|
+
const dy = cursor - box.minY;
|
|
1215
|
+
const dx = -box.minX;
|
|
1216
|
+
for (const n of box.nodes) {
|
|
1217
|
+
n.x += dx;
|
|
1218
|
+
n.y += dy;
|
|
1219
|
+
for (const h of n.handles) {
|
|
1220
|
+
h.x += dx;
|
|
1221
|
+
h.y += dy;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (const e of box.edges) {
|
|
1225
|
+
for (const p of e.points) {
|
|
1226
|
+
p.x += dx;
|
|
1227
|
+
p.y += dy;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
cursor += box.maxY - box.minY + gap;
|
|
1231
|
+
} else {
|
|
1232
|
+
const dx = cursor - box.minX;
|
|
1233
|
+
const dy = -box.minY;
|
|
1234
|
+
for (const n of box.nodes) {
|
|
1235
|
+
n.x += dx;
|
|
1236
|
+
n.y += dy;
|
|
1237
|
+
for (const h of n.handles) {
|
|
1238
|
+
h.x += dx;
|
|
1239
|
+
h.y += dy;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const e of box.edges) {
|
|
1243
|
+
for (const p of e.points) {
|
|
1244
|
+
p.x += dx;
|
|
1245
|
+
p.y += dy;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
cursor += box.maxX - box.minX + gap;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/proposals.ts
|
|
1254
|
+
function applyRotationProposals(input, options) {
|
|
1255
|
+
if (!options.onProposal) return input;
|
|
1256
|
+
const nodeIndex = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1257
|
+
const newNodes = input.nodes.map((node) => {
|
|
1258
|
+
const role = classifyNode(node, input.edges);
|
|
1259
|
+
const expected = expectedSidesFor(node, role, input.edges, nodeIndex, options.direction);
|
|
1260
|
+
const proposal = computeProposal(node, expected, options.direction, role);
|
|
1261
|
+
if (!proposal) return node;
|
|
1262
|
+
const accepted = options.onProposal(proposal);
|
|
1263
|
+
return accepted ?? node;
|
|
1264
|
+
});
|
|
1265
|
+
return { nodes: newNodes, edges: input.edges };
|
|
1266
|
+
}
|
|
1267
|
+
function classifyNode(node, edges) {
|
|
1268
|
+
const incident = edges.filter((e) => e.from === node.id || e.to === node.id);
|
|
1269
|
+
if (incident.length === 0) return { kind: "isolated" };
|
|
1270
|
+
const allData = incident.every((e) => (e.kind ?? "control") === "data");
|
|
1271
|
+
if (!allData) return { kind: "rail" };
|
|
1272
|
+
for (const e of incident) {
|
|
1273
|
+
const consumerId = e.from === node.id ? e.to : e.from;
|
|
1274
|
+
const consumerHandleId = e.from === node.id ? e.toHandle : e.fromHandle;
|
|
1275
|
+
const sideHint = sideHintFromHandle(
|
|
1276
|
+
consumerId,
|
|
1277
|
+
consumerHandleId,
|
|
1278
|
+
edges,
|
|
1279
|
+
/* recursing */
|
|
1280
|
+
false
|
|
1281
|
+
);
|
|
1282
|
+
if (sideHint) return { kind: "value", consumerSide: sideHint };
|
|
1283
|
+
}
|
|
1284
|
+
return { kind: "value", consumerSide: null };
|
|
1285
|
+
}
|
|
1286
|
+
function sideHintFromHandle(_consumerId, _handleId, _edges, _recursing) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
function expectedSidesFor(node, role, edges, index, direction) {
|
|
1290
|
+
if (role.kind === "isolated" || role.kind === "rail") {
|
|
1291
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1292
|
+
}
|
|
1293
|
+
const consumerHandleSide = findConsumerHandleSide(node, edges, index);
|
|
1294
|
+
if (!consumerHandleSide) {
|
|
1295
|
+
return { input: inputSideFor(direction), output: outputSideFor(direction) };
|
|
1296
|
+
}
|
|
1297
|
+
const isVerticalFlow = direction === "TB" || direction === "BT";
|
|
1298
|
+
let outputSide;
|
|
1299
|
+
if (isVerticalFlow) {
|
|
1300
|
+
if (consumerHandleSide === "left") outputSide = "right";
|
|
1301
|
+
else if (consumerHandleSide === "right") outputSide = "left";
|
|
1302
|
+
else outputSide = "right";
|
|
1303
|
+
} else {
|
|
1304
|
+
if (consumerHandleSide === "top") outputSide = "bottom";
|
|
1305
|
+
else if (consumerHandleSide === "bottom") outputSide = "top";
|
|
1306
|
+
else outputSide = "bottom";
|
|
1307
|
+
}
|
|
1308
|
+
return { input: outputSide, output: outputSide };
|
|
1309
|
+
}
|
|
1310
|
+
function findConsumerHandleSide(value, edges, index) {
|
|
1311
|
+
for (const e of edges) {
|
|
1312
|
+
const isFromValue = e.from === value.id;
|
|
1313
|
+
const isToValue = e.to === value.id;
|
|
1314
|
+
if (!isFromValue && !isToValue) continue;
|
|
1315
|
+
const consumerId = isFromValue ? e.to : e.from;
|
|
1316
|
+
const consumer = index.get(consumerId);
|
|
1317
|
+
if (!consumer) continue;
|
|
1318
|
+
const consumerHandleId = isFromValue ? e.toHandle : e.fromHandle;
|
|
1319
|
+
const consumerHandle = consumer.handles.find((h) => h.id === consumerHandleId);
|
|
1320
|
+
if (consumerHandle) return consumerHandle.position;
|
|
1321
|
+
}
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
function computeProposal(node, expected, direction, role) {
|
|
1325
|
+
if (node.handles.length === 0) return null;
|
|
1326
|
+
const currentScore = score(node.handles, expected);
|
|
1327
|
+
if (currentScore.matchRatio >= 1) return null;
|
|
1328
|
+
const candidates = [];
|
|
1329
|
+
for (const rot of [90, -90, 180]) {
|
|
1330
|
+
const rotated = rotateHandles(node.handles, rot);
|
|
1331
|
+
candidates.push({ rotation: rot, score: score(rotated, expected) });
|
|
1332
|
+
}
|
|
1333
|
+
candidates.sort((a, b) => b.score.matchRatio - a.score.matchRatio);
|
|
1334
|
+
const best = candidates[0];
|
|
1335
|
+
if (best.score.matchRatio <= currentScore.matchRatio) return null;
|
|
1336
|
+
return buildProposal(node, best.rotation, currentScore, best.score, direction, expected, role);
|
|
1337
|
+
}
|
|
1338
|
+
function buildProposal(node, rotation, current, proposedScore, direction, expected, role) {
|
|
1339
|
+
const proposed = { ...node, handles: rotateHandles(node.handles, rotation) };
|
|
1340
|
+
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}`;
|
|
1341
|
+
return {
|
|
1342
|
+
type: "rotate",
|
|
1343
|
+
nodeId: node.id,
|
|
1344
|
+
current: node,
|
|
1345
|
+
proposed,
|
|
1346
|
+
rotation,
|
|
1347
|
+
reason: `${roleDesc}; ${current.matched}/${current.total} handles match, ${proposedScore.matched}/${proposedScore.total} after rotation`
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function inputSideFor(direction) {
|
|
1351
|
+
switch (direction) {
|
|
1352
|
+
case "TB":
|
|
1353
|
+
return "top";
|
|
1354
|
+
case "BT":
|
|
1355
|
+
return "bottom";
|
|
1356
|
+
case "LR":
|
|
1357
|
+
return "left";
|
|
1358
|
+
case "RL":
|
|
1359
|
+
return "right";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function outputSideFor(direction) {
|
|
1363
|
+
switch (direction) {
|
|
1364
|
+
case "TB":
|
|
1365
|
+
return "bottom";
|
|
1366
|
+
case "BT":
|
|
1367
|
+
return "top";
|
|
1368
|
+
case "LR":
|
|
1369
|
+
return "right";
|
|
1370
|
+
case "RL":
|
|
1371
|
+
return "left";
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function score(handles, expected) {
|
|
1375
|
+
let matched = 0;
|
|
1376
|
+
let total = 0;
|
|
1377
|
+
for (const h of handles) {
|
|
1378
|
+
total++;
|
|
1379
|
+
if (h.type === "input" && h.position === expected.input) matched++;
|
|
1380
|
+
else if (h.type === "output" && h.position === expected.output) matched++;
|
|
1381
|
+
}
|
|
1382
|
+
return { matched, total, matchRatio: total === 0 ? 1 : matched / total };
|
|
1383
|
+
}
|
|
1384
|
+
function rotateHandles(handles, rot) {
|
|
1385
|
+
const rotateSide = (s) => {
|
|
1386
|
+
if (rot === 180) {
|
|
1387
|
+
return s === "top" ? "bottom" : s === "bottom" ? "top" : s === "left" ? "right" : "left";
|
|
1388
|
+
}
|
|
1389
|
+
if (rot === 90) {
|
|
1390
|
+
return s === "top" ? "right" : s === "right" ? "bottom" : s === "bottom" ? "left" : "top";
|
|
1391
|
+
}
|
|
1392
|
+
return s === "top" ? "left" : s === "left" ? "bottom" : s === "bottom" ? "right" : "top";
|
|
1393
|
+
};
|
|
1394
|
+
return handles.map((h) => ({ ...h, position: rotateSide(h.position) }));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
854
1397
|
// src/layout.ts
|
|
1398
|
+
var COMPOUND_HEADER = 28;
|
|
855
1399
|
function layout(input, options) {
|
|
856
1400
|
const resolved = resolveOptions(options);
|
|
857
|
-
|
|
858
|
-
|
|
1401
|
+
if (input.nodes.length === 0) return { nodes: [], edges: [] };
|
|
1402
|
+
const adjusted = applyRotationProposals(input, resolved);
|
|
1403
|
+
return layoutCompound(adjusted, resolved);
|
|
859
1404
|
}
|
|
860
1405
|
function computeLayout(graph, options) {
|
|
861
|
-
if (graph.nodes.size === 0) {
|
|
862
|
-
return { nodes: [], edges: [] };
|
|
863
|
-
}
|
|
1406
|
+
if (graph.nodes.size === 0) return { nodes: [], edges: [] };
|
|
864
1407
|
breakCycles(graph);
|
|
865
1408
|
let layers = assignLayers(graph);
|
|
866
1409
|
layers = insertDummyNodes(graph, layers);
|
|
867
|
-
|
|
868
|
-
|
|
1410
|
+
let rail = railLayers(layers, graph);
|
|
1411
|
+
rail = minimizeCrossings(graph, rail, options.crossingMinimizationIterations);
|
|
1412
|
+
assignCoordinates(graph, rail, options);
|
|
1413
|
+
placeValueSidecars(graph, layers, options);
|
|
869
1414
|
const routedEdges = routeEdges(graph, options.direction, options.edgeMargin);
|
|
870
1415
|
return buildResult(graph, routedEdges);
|
|
871
1416
|
}
|
|
@@ -876,13 +1421,7 @@ function buildResult(graph, routedEdges) {
|
|
|
876
1421
|
if (node.isDummy) continue;
|
|
877
1422
|
const handles = node.handles.map((h) => {
|
|
878
1423
|
const pos = getHandlePosition(node, h.id);
|
|
879
|
-
return {
|
|
880
|
-
id: h.id,
|
|
881
|
-
type: h.type,
|
|
882
|
-
position: h.position,
|
|
883
|
-
x: pos.x,
|
|
884
|
-
y: pos.y
|
|
885
|
-
};
|
|
1424
|
+
return { id: h.id, type: h.type, position: h.position, x: pos.x, y: pos.y };
|
|
886
1425
|
});
|
|
887
1426
|
nodes.push({
|
|
888
1427
|
id: node.id,
|
|
@@ -890,7 +1429,8 @@ function buildResult(graph, routedEdges) {
|
|
|
890
1429
|
y: node.y,
|
|
891
1430
|
width: node.width,
|
|
892
1431
|
height: node.height,
|
|
893
|
-
handles
|
|
1432
|
+
handles,
|
|
1433
|
+
parentId: node.parentId
|
|
894
1434
|
});
|
|
895
1435
|
}
|
|
896
1436
|
for (const route of routedEdges) {
|
|
@@ -900,11 +1440,146 @@ function buildResult(graph, routedEdges) {
|
|
|
900
1440
|
to: route.to,
|
|
901
1441
|
fromHandle: route.fromHandle,
|
|
902
1442
|
toHandle: route.toHandle,
|
|
903
|
-
points: route.points
|
|
1443
|
+
points: route.points,
|
|
1444
|
+
kind: route.kind
|
|
904
1445
|
});
|
|
905
1446
|
}
|
|
906
1447
|
return { nodes, edges };
|
|
907
1448
|
}
|
|
1449
|
+
function layoutCompound(input, options) {
|
|
1450
|
+
const nodeMap = new Map(input.nodes.map((n) => [n.id, n]));
|
|
1451
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1452
|
+
const rootNodes = [];
|
|
1453
|
+
for (const n of input.nodes) {
|
|
1454
|
+
if (n.parentId && nodeMap.has(n.parentId)) {
|
|
1455
|
+
const arr = childrenByParent.get(n.parentId) ?? [];
|
|
1456
|
+
arr.push(n);
|
|
1457
|
+
childrenByParent.set(n.parentId, arr);
|
|
1458
|
+
} else {
|
|
1459
|
+
rootNodes.push(n);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const compoundIds = new Set(childrenByParent.keys());
|
|
1463
|
+
const depthCache = /* @__PURE__ */ new Map();
|
|
1464
|
+
function depthOf(id) {
|
|
1465
|
+
if (depthCache.has(id)) return depthCache.get(id);
|
|
1466
|
+
const n = nodeMap.get(id);
|
|
1467
|
+
const d = n?.parentId && nodeMap.has(n.parentId) ? depthOf(n.parentId) + 1 : 0;
|
|
1468
|
+
depthCache.set(id, d);
|
|
1469
|
+
return d;
|
|
1470
|
+
}
|
|
1471
|
+
for (const n of input.nodes) depthOf(n.id);
|
|
1472
|
+
const sortedCompounds = [...compoundIds].sort((a, b) => depthOf(b) - depthOf(a));
|
|
1473
|
+
const subLayouts = /* @__PURE__ */ new Map();
|
|
1474
|
+
const compoundSize = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const compoundId of sortedCompounds) {
|
|
1476
|
+
const children = childrenByParent.get(compoundId) ?? [];
|
|
1477
|
+
if (children.length === 0) continue;
|
|
1478
|
+
const childIdSet = new Set(children.map((c) => c.id));
|
|
1479
|
+
const subEdges = input.edges.filter((e) => childIdSet.has(e.from) && childIdSet.has(e.to));
|
|
1480
|
+
const sizedChildren = children.map((c) => {
|
|
1481
|
+
const sz = compoundSize.get(c.id);
|
|
1482
|
+
return sz ? { ...c, width: sz.width, height: sz.height } : c;
|
|
1483
|
+
});
|
|
1484
|
+
const subOptions = { ...options, packComponents: false };
|
|
1485
|
+
const subInput = { nodes: sizedChildren.map(stripParent), edges: subEdges };
|
|
1486
|
+
const subResult = layoutFlat(subInput, subOptions);
|
|
1487
|
+
subLayouts.set(compoundId, subResult);
|
|
1488
|
+
const bbox = computeBoundingBox(subResult.nodes);
|
|
1489
|
+
const innerW = bbox.width + options.compoundPadding * 2;
|
|
1490
|
+
const innerH = bbox.height + options.compoundPadding * 2 + COMPOUND_HEADER;
|
|
1491
|
+
const original = nodeMap.get(compoundId);
|
|
1492
|
+
compoundSize.set(compoundId, {
|
|
1493
|
+
width: Math.max(innerW, original.width),
|
|
1494
|
+
height: Math.max(innerH, original.height)
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
const sizedRootNodes = rootNodes.map((n) => {
|
|
1498
|
+
const sz = compoundSize.get(n.id);
|
|
1499
|
+
return sz ? { ...n, width: sz.width, height: sz.height } : n;
|
|
1500
|
+
});
|
|
1501
|
+
const rootIdSet = new Set(rootNodes.map((n) => n.id));
|
|
1502
|
+
const rootEdges = input.edges.filter((e) => {
|
|
1503
|
+
return rootIdSet.has(e.from) && rootIdSet.has(e.to);
|
|
1504
|
+
});
|
|
1505
|
+
const rootResult = layoutFlat({ nodes: sizedRootNodes.map(stripParent), edges: rootEdges }, options);
|
|
1506
|
+
const finalNodes = [];
|
|
1507
|
+
const finalEdges = [];
|
|
1508
|
+
for (const n of rootResult.nodes) {
|
|
1509
|
+
const wasCompound = compoundIds.has(n.id);
|
|
1510
|
+
finalNodes.push({
|
|
1511
|
+
...n,
|
|
1512
|
+
parentId: nodeMap.get(n.id)?.parentId
|
|
1513
|
+
});
|
|
1514
|
+
if (wasCompound) {
|
|
1515
|
+
placeCompoundChildren(n, (compoundId) => subLayouts.get(compoundId), finalNodes, finalEdges, options, nodeMap);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
finalEdges.push(...rootResult.edges);
|
|
1519
|
+
if (options.packComponents) {
|
|
1520
|
+
packComponents(finalNodes, finalEdges, options);
|
|
1521
|
+
}
|
|
1522
|
+
return { nodes: finalNodes, edges: finalEdges };
|
|
1523
|
+
}
|
|
1524
|
+
function placeCompoundChildren(parent, getSub, finalNodes, finalEdges, options, nodeMap) {
|
|
1525
|
+
const sub = getSub(parent.id);
|
|
1526
|
+
if (!sub) return;
|
|
1527
|
+
const bbox = computeBoundingBox(sub.nodes);
|
|
1528
|
+
const dx = parent.x + options.compoundPadding - bbox.minX;
|
|
1529
|
+
const dy = parent.y + options.compoundPadding + COMPOUND_HEADER - bbox.minY;
|
|
1530
|
+
const availableW = parent.width - options.compoundPadding * 2;
|
|
1531
|
+
const slackX = (availableW - bbox.width) / 2;
|
|
1532
|
+
const availableH = parent.height - options.compoundPadding * 2 - COMPOUND_HEADER;
|
|
1533
|
+
const slackY = (availableH - bbox.height) / 2;
|
|
1534
|
+
const cx = dx + Math.max(0, slackX);
|
|
1535
|
+
const cy = dy + Math.max(0, slackY);
|
|
1536
|
+
for (const child of sub.nodes) {
|
|
1537
|
+
const placed = {
|
|
1538
|
+
...child,
|
|
1539
|
+
x: child.x + cx,
|
|
1540
|
+
y: child.y + cy,
|
|
1541
|
+
handles: child.handles.map((h) => ({ ...h, x: h.x + cx, y: h.y + cy })),
|
|
1542
|
+
parentId: nodeMap.get(child.id)?.parentId ?? parent.id
|
|
1543
|
+
};
|
|
1544
|
+
finalNodes.push(placed);
|
|
1545
|
+
if (getSub(child.id)) {
|
|
1546
|
+
placeCompoundChildren(placed, getSub, finalNodes, finalEdges, options, nodeMap);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
for (const edge of sub.edges) {
|
|
1550
|
+
finalEdges.push({
|
|
1551
|
+
...edge,
|
|
1552
|
+
points: edge.points.map((p) => ({ x: p.x + cx, y: p.y + cy }))
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function stripParent(n) {
|
|
1557
|
+
const { parentId: _ignored, ...rest } = n;
|
|
1558
|
+
return rest;
|
|
1559
|
+
}
|
|
1560
|
+
function layoutFlat(input, options) {
|
|
1561
|
+
const graph = buildGraph(input.nodes, input.edges, {
|
|
1562
|
+
controlWeight: options.controlWeight,
|
|
1563
|
+
dataWeight: options.dataWeight
|
|
1564
|
+
});
|
|
1565
|
+
return computeLayout(graph, options);
|
|
1566
|
+
}
|
|
1567
|
+
function computeBoundingBox(nodes) {
|
|
1568
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1569
|
+
for (const n of nodes) {
|
|
1570
|
+
if (n.x < minX) minX = n.x;
|
|
1571
|
+
if (n.y < minY) minY = n.y;
|
|
1572
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1573
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1574
|
+
}
|
|
1575
|
+
if (!isFinite(minX)) {
|
|
1576
|
+
minX = 0;
|
|
1577
|
+
minY = 0;
|
|
1578
|
+
maxX = 0;
|
|
1579
|
+
maxY = 0;
|
|
1580
|
+
}
|
|
1581
|
+
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
|
|
1582
|
+
}
|
|
908
1583
|
|
|
909
1584
|
// src/incremental.ts
|
|
910
1585
|
var IncrementalLayout = class {
|
|
@@ -913,82 +1588,40 @@ var IncrementalLayout = class {
|
|
|
913
1588
|
this.inputEdges = /* @__PURE__ */ new Map();
|
|
914
1589
|
this.lastResult = null;
|
|
915
1590
|
this.nodePositions = /* @__PURE__ */ new Map();
|
|
916
|
-
this.options =
|
|
1591
|
+
this.options = options;
|
|
1592
|
+
this.resolvedOptions = resolveOptions(options);
|
|
917
1593
|
}
|
|
918
|
-
/**
|
|
919
|
-
* Set the full graph and compute a complete layout.
|
|
920
|
-
*/
|
|
921
1594
|
setGraph(input) {
|
|
922
1595
|
this.inputNodes.clear();
|
|
923
1596
|
this.inputEdges.clear();
|
|
924
|
-
for (const node of input.nodes)
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
for (const edge of input.edges) {
|
|
928
|
-
this.inputEdges.set(edge.id, edge);
|
|
929
|
-
}
|
|
1597
|
+
for (const node of input.nodes) this.inputNodes.set(node.id, node);
|
|
1598
|
+
for (const edge of input.edges) this.inputEdges.set(edge.id, edge);
|
|
930
1599
|
return this.recompute();
|
|
931
1600
|
}
|
|
932
|
-
/**
|
|
933
|
-
* Add nodes and edges incrementally.
|
|
934
|
-
* Attempts to minimize layout changes for existing nodes.
|
|
935
|
-
*/
|
|
936
1601
|
addNodes(nodes, edges) {
|
|
937
|
-
for (const node of nodes)
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (edges) {
|
|
941
|
-
for (const edge of edges) {
|
|
942
|
-
this.inputEdges.set(edge.id, edge);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return this.recomputeIncremental(
|
|
946
|
-
new Set(nodes.map((n) => n.id)),
|
|
947
|
-
new Set(edges?.map((e) => e.id) || [])
|
|
948
|
-
);
|
|
1602
|
+
for (const node of nodes) this.inputNodes.set(node.id, node);
|
|
1603
|
+
if (edges) for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1604
|
+
return this.recomputeWithStability(new Set(nodes.map((n) => n.id)));
|
|
949
1605
|
}
|
|
950
|
-
/**
|
|
951
|
-
* Remove nodes (and their connected edges) from the layout.
|
|
952
|
-
*/
|
|
953
1606
|
removeNodes(nodeIds) {
|
|
954
1607
|
const removedSet = new Set(nodeIds);
|
|
955
|
-
for (const id of nodeIds)
|
|
956
|
-
this.inputNodes.delete(id);
|
|
957
|
-
}
|
|
1608
|
+
for (const id of nodeIds) this.inputNodes.delete(id);
|
|
958
1609
|
for (const [edgeId, edge] of this.inputEdges) {
|
|
959
1610
|
if (removedSet.has(edge.from) || removedSet.has(edge.to)) {
|
|
960
1611
|
this.inputEdges.delete(edgeId);
|
|
961
1612
|
}
|
|
962
1613
|
}
|
|
963
|
-
for (const id of nodeIds)
|
|
964
|
-
this.nodePositions.delete(id);
|
|
965
|
-
}
|
|
1614
|
+
for (const id of nodeIds) this.nodePositions.delete(id);
|
|
966
1615
|
return this.recompute();
|
|
967
1616
|
}
|
|
968
|
-
/**
|
|
969
|
-
* Add edges between existing nodes.
|
|
970
|
-
*/
|
|
971
1617
|
addEdges(edges) {
|
|
972
|
-
for (const edge of edges)
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
return this.recomputeIncremental(
|
|
976
|
-
/* @__PURE__ */ new Set(),
|
|
977
|
-
new Set(edges.map((e) => e.id))
|
|
978
|
-
);
|
|
1618
|
+
for (const edge of edges) this.inputEdges.set(edge.id, edge);
|
|
1619
|
+
return this.recomputeWithStability(/* @__PURE__ */ new Set());
|
|
979
1620
|
}
|
|
980
|
-
/**
|
|
981
|
-
* Remove edges from the layout.
|
|
982
|
-
*/
|
|
983
1621
|
removeEdges(edgeIds) {
|
|
984
|
-
for (const id of edgeIds)
|
|
985
|
-
this.inputEdges.delete(id);
|
|
986
|
-
}
|
|
1622
|
+
for (const id of edgeIds) this.inputEdges.delete(id);
|
|
987
1623
|
return this.recompute();
|
|
988
1624
|
}
|
|
989
|
-
/**
|
|
990
|
-
* Get the current layout result.
|
|
991
|
-
*/
|
|
992
1625
|
getResult() {
|
|
993
1626
|
return this.lastResult;
|
|
994
1627
|
}
|
|
@@ -997,42 +1630,48 @@ var IncrementalLayout = class {
|
|
|
997
1630
|
nodes: [...this.inputNodes.values()],
|
|
998
1631
|
edges: [...this.inputEdges.values()]
|
|
999
1632
|
};
|
|
1000
|
-
|
|
1001
|
-
this.lastResult = computeLayout(graph, this.options);
|
|
1633
|
+
this.lastResult = layout(input, this.options);
|
|
1002
1634
|
this.cachePositions();
|
|
1003
1635
|
return this.lastResult;
|
|
1004
1636
|
}
|
|
1005
|
-
|
|
1637
|
+
recomputeWithStability(newNodeIds) {
|
|
1006
1638
|
const input = {
|
|
1007
1639
|
nodes: [...this.inputNodes.values()],
|
|
1008
1640
|
edges: [...this.inputEdges.values()]
|
|
1009
1641
|
};
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
let layers = assignLayers(graph);
|
|
1013
|
-
layers = insertDummyNodes(graph, layers);
|
|
1014
|
-
layers = minimizeCrossings(graph, layers, this.options.crossingMinimizationIterations);
|
|
1015
|
-
assignCoordinates(graph, layers, this.options);
|
|
1016
|
-
this.applyStability(graph, newNodeIds);
|
|
1017
|
-
const routedEdges = routeEdges(graph, this.options.direction, this.options.edgeMargin);
|
|
1018
|
-
this.lastResult = this.buildResult(graph, routedEdges);
|
|
1642
|
+
const fresh = layout(input, this.options);
|
|
1643
|
+
this.lastResult = this.applyStability(fresh, newNodeIds);
|
|
1019
1644
|
this.cachePositions();
|
|
1020
1645
|
return this.lastResult;
|
|
1021
1646
|
}
|
|
1022
1647
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1648
|
+
* Blend the freshly computed positions with the previous ones, so existing
|
|
1649
|
+
* nodes don't jump too far when a small change happens.
|
|
1025
1650
|
*/
|
|
1026
|
-
applyStability(
|
|
1027
|
-
if (this.nodePositions.size === 0) return;
|
|
1651
|
+
applyStability(fresh, newNodeIds) {
|
|
1652
|
+
if (this.nodePositions.size === 0) return fresh;
|
|
1028
1653
|
const STABILITY_WEIGHT = 0.3;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1654
|
+
const blended = {
|
|
1655
|
+
nodes: fresh.nodes.map((n) => {
|
|
1656
|
+
if (newNodeIds.has(n.id)) return n;
|
|
1657
|
+
const old = this.nodePositions.get(n.id);
|
|
1658
|
+
if (!old) return n;
|
|
1659
|
+
const dx = old.x - n.x;
|
|
1660
|
+
const dy = old.y - n.y;
|
|
1661
|
+
const x = n.x + dx * STABILITY_WEIGHT;
|
|
1662
|
+
const y = n.y + dy * STABILITY_WEIGHT;
|
|
1663
|
+
const ddx = x - n.x;
|
|
1664
|
+
const ddy = y - n.y;
|
|
1665
|
+
return {
|
|
1666
|
+
...n,
|
|
1667
|
+
x,
|
|
1668
|
+
y,
|
|
1669
|
+
handles: n.handles.map((h) => ({ ...h, x: h.x + ddx, y: h.y + ddy }))
|
|
1670
|
+
};
|
|
1671
|
+
}),
|
|
1672
|
+
edges: fresh.edges
|
|
1673
|
+
};
|
|
1674
|
+
return blended;
|
|
1036
1675
|
}
|
|
1037
1676
|
cachePositions() {
|
|
1038
1677
|
if (!this.lastResult) return;
|
|
@@ -1041,46 +1680,160 @@ var IncrementalLayout = class {
|
|
|
1041
1680
|
this.nodePositions.set(node.id, { x: node.x, y: node.y });
|
|
1042
1681
|
}
|
|
1043
1682
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
// src/debug.ts
|
|
1686
|
+
function printLayout(result, options = {}) {
|
|
1687
|
+
const lines = [];
|
|
1688
|
+
const { nodes, edges } = result;
|
|
1689
|
+
if (nodes.length === 0) return "(empty layout)";
|
|
1690
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1691
|
+
for (const n of nodes) {
|
|
1692
|
+
if (n.x < minX) minX = n.x;
|
|
1693
|
+
if (n.y < minY) minY = n.y;
|
|
1694
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1695
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1696
|
+
}
|
|
1697
|
+
lines.push(`=== layout ${nodes.length} nodes, ${edges.length} edges ===`);
|
|
1698
|
+
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)})`);
|
|
1699
|
+
const byY = [...nodes].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
1700
|
+
const bands = [];
|
|
1701
|
+
for (const n of byY) {
|
|
1702
|
+
const placed = bands.find((b) => Math.abs(b[0].y - n.y) < 1);
|
|
1703
|
+
if (placed) placed.push(n);
|
|
1704
|
+
else bands.push([n]);
|
|
1705
|
+
}
|
|
1706
|
+
lines.push("");
|
|
1707
|
+
lines.push("--- Y bands ---");
|
|
1708
|
+
for (const band of bands) {
|
|
1709
|
+
const sorted = [...band].sort((a, b) => a.x - b.x);
|
|
1710
|
+
const summary = sorted.map((n) => {
|
|
1711
|
+
const p = n.parentId ? `[${n.parentId}/]` : "";
|
|
1712
|
+
return `${p}${n.id}@(${n.x.toFixed(0)},${n.y.toFixed(0)} ${n.width}x${n.height})`;
|
|
1713
|
+
}).join(" ");
|
|
1714
|
+
lines.push(` y=${band[0].y.toFixed(0).padStart(4)} : ${summary}`);
|
|
1715
|
+
}
|
|
1716
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1717
|
+
for (const n of nodes) {
|
|
1718
|
+
const key = n.parentId;
|
|
1719
|
+
const arr = byParent.get(key) ?? [];
|
|
1720
|
+
arr.push(n);
|
|
1721
|
+
byParent.set(key, arr);
|
|
1722
|
+
}
|
|
1723
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1724
|
+
for (const k of byParent.keys()) {
|
|
1725
|
+
if (k && nodes.find((n) => n.id === k)) compoundIds.add(k);
|
|
1726
|
+
}
|
|
1727
|
+
if (compoundIds.size > 0) {
|
|
1728
|
+
let printSubtree2 = function(id, depth) {
|
|
1729
|
+
const children = byParent.get(id) ?? [];
|
|
1730
|
+
for (const c of children) {
|
|
1731
|
+
const prefix = " ".repeat(depth);
|
|
1732
|
+
const tag = compoundIds.has(c.id) ? " (compound)" : "";
|
|
1733
|
+
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)})`);
|
|
1734
|
+
if (compoundIds.has(c.id)) printSubtree2(c.id, depth + 1);
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
var printSubtree = printSubtree2;
|
|
1738
|
+
lines.push("");
|
|
1739
|
+
lines.push("--- hierarchy ---");
|
|
1740
|
+
printSubtree2(void 0, 0);
|
|
1741
|
+
}
|
|
1742
|
+
lines.push("");
|
|
1743
|
+
lines.push("--- edges ---");
|
|
1744
|
+
for (const e of edges) {
|
|
1745
|
+
const head = e.points[0];
|
|
1746
|
+
const tail = e.points[e.points.length - 1];
|
|
1747
|
+
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`);
|
|
1748
|
+
}
|
|
1749
|
+
const overlaps = findOverlaps(nodes);
|
|
1750
|
+
if (overlaps.length > 0) {
|
|
1751
|
+
lines.push("");
|
|
1752
|
+
lines.push("--- OVERLAPS (problem!) ---");
|
|
1753
|
+
for (const o of overlaps) {
|
|
1754
|
+
lines.push(` ${o.a} overlaps ${o.b}`);
|
|
1067
1755
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1756
|
+
}
|
|
1757
|
+
if (options.grid !== false) {
|
|
1758
|
+
lines.push("");
|
|
1759
|
+
lines.push("--- ASCII grid ---");
|
|
1760
|
+
lines.push(asciiGrid(nodes, edges, options));
|
|
1761
|
+
}
|
|
1762
|
+
return lines.join("\n");
|
|
1763
|
+
}
|
|
1764
|
+
function findOverlaps(nodes) {
|
|
1765
|
+
const out = [];
|
|
1766
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1767
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1768
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1769
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1770
|
+
const a = nodes[i];
|
|
1771
|
+
const b = nodes[j];
|
|
1772
|
+
if (a.id === b.parentId || b.id === a.parentId) continue;
|
|
1773
|
+
const overlapX = a.x < b.x + b.width && b.x < a.x + a.width;
|
|
1774
|
+
const overlapY = a.y < b.y + b.height && b.y < a.y + a.height;
|
|
1775
|
+
if (overlapX && overlapY) {
|
|
1776
|
+
out.push({ a: a.id, b: b.id });
|
|
1777
|
+
}
|
|
1077
1778
|
}
|
|
1078
|
-
return { nodes, edges };
|
|
1079
1779
|
}
|
|
1080
|
-
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1782
|
+
function asciiGrid(nodes, edges, options) {
|
|
1783
|
+
if (nodes.length === 0) return "";
|
|
1784
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1785
|
+
for (const n of nodes) {
|
|
1786
|
+
if (n.x < minX) minX = n.x;
|
|
1787
|
+
if (n.y < minY) minY = n.y;
|
|
1788
|
+
if (n.x + n.width > maxX) maxX = n.x + n.width;
|
|
1789
|
+
if (n.y + n.height > maxY) maxY = n.y + n.height;
|
|
1790
|
+
}
|
|
1791
|
+
for (const e of edges) for (const p of e.points) {
|
|
1792
|
+
if (p.x < minX) minX = p.x;
|
|
1793
|
+
if (p.y < minY) minY = p.y;
|
|
1794
|
+
if (p.x > maxX) maxX = p.x;
|
|
1795
|
+
if (p.y > maxY) maxY = p.y;
|
|
1796
|
+
}
|
|
1797
|
+
const targetW = options.gridWidth ?? 80;
|
|
1798
|
+
const layoutW = Math.max(1, maxX - minX);
|
|
1799
|
+
const layoutH = Math.max(1, maxY - minY);
|
|
1800
|
+
const scaleX = options.gridScale ?? targetW / layoutW;
|
|
1801
|
+
const scaleY = scaleX * 0.5;
|
|
1802
|
+
const w = Math.max(1, Math.ceil(layoutW * scaleX) + 1);
|
|
1803
|
+
const h = Math.max(1, Math.ceil(layoutH * scaleY) + 1);
|
|
1804
|
+
if (h > 80) return "(grid suppressed: too tall \u2014 pass {grid:false} to skip)";
|
|
1805
|
+
const grid = Array.from({ length: h }, () => Array.from({ length: w }, () => " "));
|
|
1806
|
+
const compoundIds = /* @__PURE__ */ new Set();
|
|
1807
|
+
for (const n of nodes) if (n.parentId) compoundIds.add(n.parentId);
|
|
1808
|
+
for (const n of nodes) {
|
|
1809
|
+
const x0 = Math.round((n.x - minX) * scaleX);
|
|
1810
|
+
const y0 = Math.round((n.y - minY) * scaleY);
|
|
1811
|
+
const x1 = Math.max(x0, Math.round((n.x + n.width - minX) * scaleX) - 1);
|
|
1812
|
+
const y1 = Math.max(y0, Math.round((n.y + n.height - minY) * scaleY) - 1);
|
|
1813
|
+
const isCompound = compoundIds.has(n.id);
|
|
1814
|
+
const ch = isCompound ? "." : "#";
|
|
1815
|
+
for (let y = y0; y <= y1 && y < h; y++) {
|
|
1816
|
+
for (let x = x0; x <= x1 && x < w; x++) {
|
|
1817
|
+
if (y < 0 || x < 0) continue;
|
|
1818
|
+
if (y === y0 || y === y1 || x === x0 || x === x1) grid[y][x] = ch;
|
|
1819
|
+
else if (isCompound) {
|
|
1820
|
+
} else grid[y][x] = ch;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const label = n.id.slice(0, Math.min(n.id.length, Math.max(2, x1 - x0 - 1)));
|
|
1824
|
+
const lx = Math.max(0, x0 + 1);
|
|
1825
|
+
const ly = Math.max(0, y0);
|
|
1826
|
+
for (let i = 0; i < label.length && lx + i < w; i++) {
|
|
1827
|
+
if (ly < h) grid[ly][lx + i] = label[i];
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return grid.map((row) => row.join("")).join("\n");
|
|
1831
|
+
}
|
|
1081
1832
|
export {
|
|
1082
1833
|
IncrementalLayout,
|
|
1083
1834
|
countAllCrossings,
|
|
1084
|
-
layout
|
|
1835
|
+
layout,
|
|
1836
|
+
printLayout,
|
|
1837
|
+
rotateHandles
|
|
1085
1838
|
};
|
|
1086
1839
|
//# sourceMappingURL=index.mjs.map
|