@statelyai/layout 0.0.0 → 0.0.2

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.
Files changed (46) hide show
  1. package/NOTICE.md +9 -1
  2. package/README.md +50 -25
  3. package/dist/elkjs/index.d.mts +3 -0
  4. package/dist/elkjs/index.mjs +904 -97
  5. package/dist/index-8xYkohbz.d.mts +1212 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +3 -3
  8. package/dist/layered/index.d.mts +2 -2
  9. package/dist/layered/index.mjs +2 -2
  10. package/dist/layered-QwJn2gb2.mjs +8978 -0
  11. package/dist/{spore-cihb_Aht.mjs → spore-D15xIQKj.mjs} +1 -31
  12. package/package.json +59 -26
  13. package/src/box.ts +135 -0
  14. package/src/elkjs/index.ts +1825 -0
  15. package/src/elkjs/types.ts +103 -0
  16. package/src/errors.ts +16 -0
  17. package/src/fixed.ts +80 -0
  18. package/src/index.ts +92 -0
  19. package/src/java-random.ts +46 -0
  20. package/src/layered/bk-node-placement.ts +715 -0
  21. package/src/layered/elk-enum-values.ts +171 -0
  22. package/src/layered/elk-options.generated.ts +315 -0
  23. package/src/layered/elk-options.ts +98 -0
  24. package/src/layered/flexible-ports.ts +11 -0
  25. package/src/layered/high-degree.ts +127 -0
  26. package/src/layered/index.ts +2138 -0
  27. package/src/layered/layer-unzipping.ts +217 -0
  28. package/src/layered/linear-segments-node-placement.ts +447 -0
  29. package/src/layered/long-edges.ts +405 -0
  30. package/src/layered/min-width.ts +159 -0
  31. package/src/layered/multi-edge-wrapping.ts +460 -0
  32. package/src/layered/network-simplex-node-placement.ts +500 -0
  33. package/src/layered/network-simplex.ts +346 -0
  34. package/src/layered/node-promotion.ts +197 -0
  35. package/src/layered/spacing.ts +37 -0
  36. package/src/layered/spline-bezier.ts +102 -0
  37. package/src/layered/strategies.ts +5343 -0
  38. package/src/layered/stretch-width.ts +136 -0
  39. package/src/layered/types.ts +111 -0
  40. package/src/layout.ts +202 -0
  41. package/src/packing.ts +74 -0
  42. package/src/random.ts +142 -0
  43. package/src/spore.ts +103 -0
  44. package/src/types.ts +84 -0
  45. package/dist/index-v0P1Ake8.d.mts +0 -149
  46. package/dist/layered-ByNCZQgJ.mjs +0 -439
@@ -0,0 +1,405 @@
1
+ import type { GraphEdge, GraphNode, Point } from "@statelyai/graph";
2
+ import type {
3
+ AcyclicOrientation,
4
+ EdgeRoutes,
5
+ LayerAssignment,
6
+ LayeredPhaseInput,
7
+ NodeSize,
8
+ } from "./types";
9
+ import { uniformCubicSplineToBezier } from "./spline-bezier";
10
+
11
+ export interface LongEdgeExpansion {
12
+ input: LayeredPhaseInput;
13
+ orientation: AcyclicOrientation;
14
+ assignment: LayerAssignment;
15
+ segmentIdsByEdgeId: ReadonlyMap<string, readonly string[]>;
16
+ labelDummyIdByEdgeId: ReadonlyMap<string, string>;
17
+ }
18
+
19
+ function uniqueDummyId(usedIds: Set<string>, edgeId: string, layer: number): string {
20
+ const base = `__layout_dummy:${edgeId}:${layer}`;
21
+ let id = base;
22
+ let suffix = 1;
23
+ while (usedIds.has(id)) id = `${base}:${suffix++}`;
24
+ usedIds.add(id);
25
+ return id;
26
+ }
27
+
28
+ /** ELK LongEdgeSplitter equivalent for flat normal-node graphs. */
29
+ export function splitLongEdges(
30
+ input: LayeredPhaseInput,
31
+ orientation: AcyclicOrientation,
32
+ assignment: LayerAssignment,
33
+ ): LongEdgeExpansion {
34
+ const nodes = [...input.graph.nodes] as GraphNode[];
35
+ const edges: GraphEdge[] = [];
36
+ const sizes = new Map<string, NodeSize>(input.sizes);
37
+ const layerByNodeId = new Map(assignment.layerByNodeId);
38
+ const reversedEdgeIds = new Set<string>();
39
+ const segmentIdsByEdgeId = new Map<string, readonly string[]>();
40
+ const labelDummyIdByEdgeId = new Map<string, string>();
41
+ const originalEdgeBySegmentId = new Map<string, GraphEdge>();
42
+ const usedNodeIds = new Set(nodes.map((node) => node.id));
43
+ const originalNodeIds = new Set(usedNodeIds);
44
+ const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
45
+ const forwardSourceSide =
46
+ input.direction === "right"
47
+ ? "EAST"
48
+ : input.direction === "left"
49
+ ? "WEST"
50
+ : input.direction === "down"
51
+ ? "SOUTH"
52
+ : "NORTH";
53
+ const forwardTargetSide =
54
+ input.direction === "right"
55
+ ? "WEST"
56
+ : input.direction === "left"
57
+ ? "EAST"
58
+ : input.direction === "down"
59
+ ? "NORTH"
60
+ : "SOUTH";
61
+
62
+ for (const edge of input.graph.edges) {
63
+ const sourceLayer = layerByNodeId.get(edge.sourceId) ?? 0;
64
+ const targetLayer = layerByNodeId.get(edge.targetId) ?? 0;
65
+ const span = Math.abs(targetLayer - sourceLayer);
66
+ const source = nodeById.get(edge.sourceId);
67
+ const target = nodeById.get(edge.targetId);
68
+ const sourcePort = source?.ports?.find((port) => port.name === edge.sourcePort);
69
+ const targetPort = target?.ports?.find((port) => port.name === edge.targetPort);
70
+ const sourceSide =
71
+ source && sourcePort ? input.portSettings?.(sourcePort, source)?.["port.side"] : undefined;
72
+ const targetSide =
73
+ target && targetPort ? input.portSettings?.(targetPort, target)?.["port.side"] : undefined;
74
+ const fixedSideFeedback =
75
+ sourceLayer > targetLayer &&
76
+ ((source !== undefined &&
77
+ input.nodeSettings?.(source)?.portConstraints === "FIXED_SIDE" &&
78
+ sourceSide === forwardSourceSide) ||
79
+ (target !== undefined &&
80
+ input.nodeSettings?.(target)?.portConstraints === "FIXED_SIDE" &&
81
+ targetSide === forwardTargetSide));
82
+ if (
83
+ span <= 1 ||
84
+ edge.sourceId === edge.targetId ||
85
+ (input.settings.feedbackEdges === true && orientation.reversedEdgeIds.has(edge.id)) ||
86
+ fixedSideFeedback
87
+ ) {
88
+ edges.push(edge);
89
+ originalEdgeBySegmentId.set(edge.id, edge);
90
+ if (orientation.reversedEdgeIds.has(edge.id)) reversedEdgeIds.add(edge.id);
91
+ segmentIdsByEdgeId.set(edge.id, [edge.id]);
92
+ continue;
93
+ }
94
+
95
+ const step = targetLayer > sourceLayer ? 1 : -1;
96
+ const chain = [edge.sourceId];
97
+ const dummyIds: string[] = [];
98
+ const dummyLayers: number[] = [];
99
+ for (let layer = sourceLayer + step; layer !== targetLayer; layer += step) {
100
+ const id = uniqueDummyId(usedNodeIds, edge.id, layer);
101
+ nodes.push({ type: "node", id, data: undefined, width: 0, height: 0 });
102
+ sizes.set(id, { width: 0, height: 0 });
103
+ layerByNodeId.set(id, layer);
104
+ chain.push(id);
105
+ dummyIds.push(id);
106
+ dummyLayers.push(layer);
107
+ }
108
+ chain.push(edge.targetId);
109
+
110
+ if ((edge.width ?? 0) > 0 || (edge.height ?? 0) > 0) {
111
+ const strategy = String(
112
+ input.edgeSettings?.(edge)?.["edgeLabels.centerLabelPlacementStrategy"] ??
113
+ input.settings["edgeLabels.centerLabelPlacementStrategy"] ??
114
+ "MEDIAN_LAYER",
115
+ );
116
+ const horizontal = input.direction === "left" || input.direction === "right";
117
+ const layerWidth = (layer: number): number =>
118
+ Math.max(
119
+ 0,
120
+ ...input.graph.nodes
121
+ .filter((node) => assignment.layerByNodeId.get(node.id) === layer)
122
+ .map((node) => {
123
+ const size = input.sizes.get(node.id);
124
+ return horizontal ? (size?.width ?? 0) : (size?.height ?? 0);
125
+ }),
126
+ );
127
+ let labelIndex = Math.floor((dummyIds.length - 1) / 2);
128
+ if (strategy === "TAIL_LAYER") labelIndex = 0;
129
+ else if (strategy === "HEAD_LAYER") labelIndex = dummyIds.length - 1;
130
+ else if (strategy === "WIDEST_LAYER" || strategy === "SPACE_EFFICIENT_LAYER") {
131
+ labelIndex = 0;
132
+ for (let index = 1; index < dummyLayers.length; index++) {
133
+ if (layerWidth(dummyLayers[index]!) > layerWidth(dummyLayers[labelIndex]!)) {
134
+ labelIndex = index;
135
+ }
136
+ }
137
+ } else if (strategy === "CENTER_LAYER") {
138
+ const spacing = input.spacing.layer;
139
+ const accumulated = dummyLayers.map((_, index) =>
140
+ dummyLayers
141
+ .slice(0, index + 1)
142
+ .reduce((sum, layer) => sum + layerWidth(layer) + spacing, -spacing),
143
+ );
144
+ const half = (accumulated.at(-1) ?? 0) / 2;
145
+ labelIndex = Math.max(
146
+ 0,
147
+ accumulated.findIndex((value) => value >= half),
148
+ );
149
+ }
150
+ const labelId = dummyIds[labelIndex]!;
151
+ sizes.set(labelId, { width: edge.width ?? 0, height: edge.height ?? 0 });
152
+ labelDummyIdByEdgeId.set(edge.id, labelId);
153
+ }
154
+
155
+ const segmentIds: string[] = [];
156
+ for (let index = 0; index < chain.length - 1; index++) {
157
+ const id = `${edge.id}::segment:${index}`;
158
+ const segment: GraphEdge = {
159
+ ...edge,
160
+ id,
161
+ sourceId: chain[index] as string,
162
+ targetId: chain[index + 1] as string,
163
+ sourcePort: index === 0 ? edge.sourcePort : undefined,
164
+ targetPort: index === chain.length - 2 ? edge.targetPort : undefined,
165
+ points: undefined,
166
+ width: 0,
167
+ height: 0,
168
+ };
169
+ edges.push(segment);
170
+ segmentIds.push(id);
171
+ originalEdgeBySegmentId.set(id, edge);
172
+ if (orientation.reversedEdgeIds.has(edge.id)) reversedEdgeIds.add(id);
173
+ }
174
+ segmentIdsByEdgeId.set(edge.id, segmentIds);
175
+ }
176
+
177
+ // ELK creates long-edge dummies while walking layers. Thus dummies for an
178
+ // edge whose source is in the next layer can precede later parts of an edge
179
+ // that started in an earlier layer. Preserve that order for crossing ties.
180
+ const maximumLayer = Math.max(0, ...layerByNodeId.values());
181
+ const nodesByLayer = Array.from({ length: maximumLayer + 1 }, () => [] as string[]);
182
+ for (const node of input.graph.nodes) {
183
+ nodesByLayer[layerByNodeId.get(node.id) ?? 0]?.push(node.id);
184
+ }
185
+ const outgoingBySource = new Map<string, GraphEdge[]>();
186
+ for (const edge of edges) {
187
+ const outgoing = outgoingBySource.get(edge.sourceId) ?? [];
188
+ outgoing.push(edge);
189
+ outgoingBySource.set(edge.sourceId, outgoing);
190
+ }
191
+ const appendNextLayerDummies = (layer: number, step: 1 | -1): void => {
192
+ for (const sourceId of nodesByLayer[layer] ?? []) {
193
+ for (const edge of outgoingBySource.get(sourceId) ?? []) {
194
+ const targetLayer = layerByNodeId.get(edge.targetId);
195
+ if (
196
+ targetLayer !== layer + step ||
197
+ originalNodeIds.has(edge.targetId) ||
198
+ nodesByLayer[targetLayer]?.includes(edge.targetId)
199
+ ) {
200
+ continue;
201
+ }
202
+ nodesByLayer[targetLayer]?.push(edge.targetId);
203
+ }
204
+ }
205
+ };
206
+ for (let layer = 0; layer < maximumLayer; layer++) appendNextLayerDummies(layer, 1);
207
+ for (let layer = maximumLayer; layer > 0; layer--) appendNextLayerDummies(layer, -1);
208
+ const dummyOrder = new Map<string, number>();
209
+ for (const layer of nodesByLayer) {
210
+ layer.forEach((id, index) => {
211
+ if (!originalNodeIds.has(id)) dummyOrder.set(id, index);
212
+ });
213
+ }
214
+ const orderedNodes = [
215
+ ...input.graph.nodes,
216
+ ...nodes
217
+ .filter((node) => !originalNodeIds.has(node.id))
218
+ .sort(
219
+ (left, right) =>
220
+ (layerByNodeId.get(left.id) ?? 0) - (layerByNodeId.get(right.id) ?? 0) ||
221
+ (dummyOrder.get(left.id) ?? 0) - (dummyOrder.get(right.id) ?? 0),
222
+ ),
223
+ ];
224
+ const graph = { ...input.graph, nodes: orderedNodes, edges } as LayeredPhaseInput["graph"];
225
+ return {
226
+ input: {
227
+ ...input,
228
+ graph,
229
+ sizes,
230
+ edgeSettings: (edge) => {
231
+ const original = originalEdgeBySegmentId.get(edge.id) ?? edge;
232
+ return input.edgeSettings?.(original);
233
+ },
234
+ nodeSettings: (node) => {
235
+ const original = input.nodeSettings?.(node);
236
+ const edgeId = [...labelDummyIdByEdgeId].find(([, id]) => id === node.id)?.[0];
237
+ if (!edgeId) return original;
238
+ const edge = input.graph.edges.find((candidate) => candidate.id === edgeId);
239
+ const strategy = String(
240
+ (edge && input.edgeSettings?.(edge)?.["edgeLabels.centerLabelPlacementStrategy"]) ??
241
+ input.settings["edgeLabels.centerLabelPlacementStrategy"] ??
242
+ "MEDIAN_LAYER",
243
+ );
244
+ return {
245
+ ...original,
246
+ ...(strategy === "TAIL_LAYER"
247
+ ? {
248
+ alignment:
249
+ input.direction === "left" || input.direction === "up" ? "RIGHT" : "LEFT",
250
+ }
251
+ : strategy === "HEAD_LAYER"
252
+ ? {
253
+ alignment:
254
+ input.direction === "left" || input.direction === "up" ? "LEFT" : "RIGHT",
255
+ }
256
+ : {}),
257
+ };
258
+ },
259
+ },
260
+ orientation: { reversedEdgeIds },
261
+ assignment: { ...assignment, layerByNodeId },
262
+ segmentIdsByEdgeId,
263
+ labelDummyIdByEdgeId,
264
+ };
265
+ }
266
+
267
+ function appendPoints(
268
+ target: Point[],
269
+ points: readonly Point[],
270
+ preserveInternalDuplicates: boolean,
271
+ ): void {
272
+ for (const [index, point] of points.entries()) {
273
+ const previous = target.at(-1);
274
+ if (
275
+ previous?.x === point.x &&
276
+ previous.y === point.y &&
277
+ (!preserveInternalDuplicates || index === 0)
278
+ )
279
+ continue;
280
+ target.push(point);
281
+ }
282
+ }
283
+
284
+ /** ELK LongEdgeJoiner equivalent for public edge routes. */
285
+ export function joinLongEdgeRoutes(
286
+ routes: EdgeRoutes,
287
+ segmentIdsByEdgeId: ReadonlyMap<string, readonly string[]>,
288
+ preserveInternalDuplicates = false,
289
+ convertLongSplines = false,
290
+ longSplineEdgeNodeSpacing = 10,
291
+ ): EdgeRoutes {
292
+ const simplify = (points: readonly Point[]): Point[] => {
293
+ const result: Point[] = [];
294
+ const equal = (left: number, right: number) => Math.abs(left - right) < 1e-9;
295
+ for (const point of points) {
296
+ const previous = result.at(-1);
297
+ if (previous && equal(previous.x, point.x) && equal(previous.y, point.y)) continue;
298
+ result.push(point);
299
+ while (result.length >= 3) {
300
+ const first = result.at(-3)!;
301
+ const middle = result.at(-2)!;
302
+ const last = result.at(-1)!;
303
+ if (
304
+ (equal(first.x, middle.x) && equal(middle.x, last.x)) ||
305
+ (equal(first.y, middle.y) && equal(middle.y, last.y))
306
+ ) {
307
+ result.splice(-2, 1);
308
+ } else break;
309
+ }
310
+ }
311
+ return result;
312
+ };
313
+ const pointsByEdgeId = new Map<string, readonly Point[]>();
314
+ const outsideFeedbackEdgeIds = new Set<string>();
315
+ for (const [edgeId, segmentIds] of segmentIdsByEdgeId) {
316
+ if (segmentIds.some((segmentId) => routes.outsideFeedbackEdgeIds?.has(segmentId))) {
317
+ outsideFeedbackEdgeIds.add(edgeId);
318
+ }
319
+ if (convertLongSplines && segmentIds.length > 1) {
320
+ const segments = segmentIds
321
+ .map((segmentId) => routes.pointsByEdgeId.get(segmentId) ?? [])
322
+ .filter((points) => points.length >= 2);
323
+ if (segments.length > 1) {
324
+ const source = segments[0]![0]!;
325
+ const target = segments.at(-1)!.at(-1)!;
326
+ const horizontal = Math.abs(target.x - source.x) >= Math.abs(target.y - source.y);
327
+ const flow = (point: Point) => (horizontal ? point.x : point.y);
328
+ const cross = (point: Point) => (horizontal ? point.y : point.x);
329
+ const point = (flowValue: number, crossValue: number): Point =>
330
+ horizontal ? { x: flowValue, y: crossValue } : { x: crossValue, y: flowValue };
331
+ const controlsBySegment = segments.map((segment, index): Point[] => {
332
+ const retained = routes.splineNubControlsByEdgeId?.get(segmentIds[index]!);
333
+ if (retained) return [...retained];
334
+ const start = segment[0]!;
335
+ const end = segment.at(-1)!;
336
+ const center = segment.at(-2) ?? start;
337
+ if (index === 0) {
338
+ const centerFlow = flow(center);
339
+ return [point(centerFlow, cross(end)), point(2 * centerFlow - flow(start), cross(end))];
340
+ }
341
+ if (index === segments.length - 1) {
342
+ const centerFlow = flow(center);
343
+ return [
344
+ point(2 * centerFlow - flow(end), cross(start)),
345
+ point(centerFlow, cross(start)),
346
+ ];
347
+ }
348
+ if (Math.abs(cross(start) - cross(end)) >= 1e-6) {
349
+ const centerFlow = flow(center);
350
+ const sign = Math.sign(flow(end) - flow(start)) || 1;
351
+ return [
352
+ point(centerFlow - sign * longSplineEdgeNodeSpacing, cross(start)),
353
+ point(centerFlow, cross(start)),
354
+ point(centerFlow, cross(end)),
355
+ point(centerFlow + sign * longSplineEdgeNodeSpacing, cross(end)),
356
+ ];
357
+ }
358
+ return [point((flow(start) + flow(end)) / 2, (cross(start) + cross(end)) / 2)];
359
+ });
360
+ const nubControls: Point[] = [{ ...source }];
361
+ let lastControl: Point | undefined;
362
+ let addMidpoint = false;
363
+ for (const controls of controlsBySegment) {
364
+ if (controls.length === 0) continue;
365
+ if (addMidpoint && lastControl) {
366
+ nubControls.push({
367
+ x: (lastControl.x + controls[0]!.x) / 2,
368
+ y: (lastControl.y + controls[0]!.y) / 2,
369
+ });
370
+ addMidpoint = false;
371
+ } else {
372
+ addMidpoint = true;
373
+ }
374
+ nubControls.push(...controls);
375
+ lastControl = controls.at(-1);
376
+ }
377
+ nubControls.push({ ...target });
378
+ pointsByEdgeId.set(edgeId, uniformCubicSplineToBezier(nubControls));
379
+ continue;
380
+ }
381
+ }
382
+ const points: Point[] = [];
383
+ if (!preserveInternalDuplicates && segmentIds.length > 1) {
384
+ const segments = segmentIds.map((segmentId) => routes.pointsByEdgeId.get(segmentId) ?? []);
385
+ const firstPoint = segments[0]?.[0];
386
+ if (firstPoint) points.push(firstPoint);
387
+ for (const segment of segments) points.push(...segment.slice(1, -1));
388
+ const lastPoint = segments.at(-1)?.at(-1);
389
+ if (lastPoint) points.push(lastPoint);
390
+ } else {
391
+ for (const segmentId of segmentIds) {
392
+ appendPoints(
393
+ points,
394
+ routes.pointsByEdgeId.get(segmentId) ?? [],
395
+ preserveInternalDuplicates || segmentIds.length === 1,
396
+ );
397
+ }
398
+ }
399
+ pointsByEdgeId.set(
400
+ edgeId,
401
+ preserveInternalDuplicates || segmentIds.length === 1 ? points : simplify(points),
402
+ );
403
+ }
404
+ return { pointsByEdgeId, outsideFeedbackEdgeIds };
405
+ }
@@ -0,0 +1,159 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2016, 2020 Kiel University and others.
3
+ *
4
+ * Translated from ELK v0.11.0 MinWidthLayerer.java.
5
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
6
+ * SPDX-License-Identifier: EPL-2.0
7
+ *******************************************************************************/
8
+ import type { GraphEdge } from "@statelyai/graph";
9
+ import type { AcyclicOrientation, LayerAssigner, LayeredPhaseInput } from "./types";
10
+
11
+ interface MinWidthNode {
12
+ id: string;
13
+ index: number;
14
+ modelOrder: number;
15
+ normalizedSize: number;
16
+ incoming: MinWidthEdge[];
17
+ outgoing: MinWidthEdge[];
18
+ }
19
+
20
+ interface MinWidthEdge {
21
+ edge: GraphEdge;
22
+ source: MinWidthNode;
23
+ target: MinWidthNode;
24
+ }
25
+
26
+ function createGraph(input: LayeredPhaseInput, orientation: AcyclicOrientation) {
27
+ const horizontal = input.direction === "left" || input.direction === "right";
28
+ const crossSize = (id: string) => {
29
+ const size = input.sizes.get(id);
30
+ return horizontal ? (size?.height ?? 0) : (size?.width ?? 0);
31
+ };
32
+ const minimumSize = Math.max(1, Math.min(...input.graph.nodes.map((node) => crossSize(node.id))));
33
+ const nodes = input.graph.nodes.map((node, index): MinWidthNode => ({
34
+ id: node.id,
35
+ index,
36
+ modelOrder: index,
37
+ normalizedSize: crossSize(node.id) / minimumSize,
38
+ incoming: [],
39
+ outgoing: [],
40
+ }));
41
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
42
+ for (const graphEdge of input.graph.edges) {
43
+ if (graphEdge.sourceId === graphEdge.targetId) continue;
44
+ const reversed = orientation.reversedEdgeIds.has(graphEdge.id);
45
+ const source = nodeById.get(reversed ? graphEdge.targetId : graphEdge.sourceId);
46
+ const target = nodeById.get(reversed ? graphEdge.sourceId : graphEdge.targetId);
47
+ if (!source || !target) continue;
48
+ const edge = { edge: graphEdge, source, target };
49
+ source.outgoing.push(edge);
50
+ target.incoming.push(edge);
51
+ }
52
+ return { nodes, minimumSize };
53
+ }
54
+
55
+ function computeLayering(
56
+ nodes: readonly MinWidthNode[],
57
+ upperBoundOnWidth: number,
58
+ compensator: number,
59
+ averageSize: number,
60
+ dummySize: number,
61
+ ): { maximumWidth: number; layers: MinWidthNode[][] } {
62
+ const layers: MinWidthNode[][] = [];
63
+ const unplaced = new Set(nodes);
64
+ const alreadyPlacedInCurrentLayer = new Set<MinWidthNode>();
65
+ const alreadyPlacedInOtherLayers = new Set<MinWidthNode>();
66
+ let currentLayer: MinWidthNode[] = [];
67
+ let widthCurrent = 0;
68
+ let widthUp = 0;
69
+ let maximumWidth = 0;
70
+ let realWidth = 0;
71
+ let currentSpanningEdges = 0;
72
+ let goingOutFromThisLayer = 0;
73
+ const sizeAwareBound = upperBoundOnWidth * averageSize;
74
+
75
+ while (unplaced.size > 0) {
76
+ const currentNode = [...unplaced].find((node) =>
77
+ node.outgoing.every((edge) => alreadyPlacedInOtherLayers.has(edge.target)),
78
+ );
79
+ if (currentNode) {
80
+ unplaced.delete(currentNode);
81
+ currentLayer.push(currentNode);
82
+ alreadyPlacedInCurrentLayer.add(currentNode);
83
+ const outdegree = currentNode.outgoing.length;
84
+ widthCurrent += currentNode.normalizedSize - outdegree * dummySize;
85
+ widthUp += currentNode.incoming.length * dummySize;
86
+ goingOutFromThisLayer += outdegree * dummySize;
87
+ realWidth += currentNode.normalizedSize;
88
+ }
89
+
90
+ if (
91
+ !currentNode ||
92
+ unplaced.size === 0 ||
93
+ (widthCurrent >= sizeAwareBound &&
94
+ currentNode.normalizedSize > currentNode.outgoing.length * dummySize) ||
95
+ widthUp >= compensator * sizeAwareBound
96
+ ) {
97
+ layers.push(currentLayer);
98
+ currentLayer = [];
99
+ for (const node of alreadyPlacedInCurrentLayer) alreadyPlacedInOtherLayers.add(node);
100
+ alreadyPlacedInCurrentLayer.clear();
101
+ currentSpanningEdges -= goingOutFromThisLayer;
102
+ maximumWidth = Math.max(maximumWidth, currentSpanningEdges * dummySize + realWidth);
103
+ currentSpanningEdges += widthUp;
104
+ widthCurrent = widthUp;
105
+ widthUp = 0;
106
+ goingOutFromThisLayer = 0;
107
+ realWidth = 0;
108
+ }
109
+ }
110
+ return { maximumWidth, layers };
111
+ }
112
+
113
+ export const assignLayersWithMinWidth: LayerAssigner = (input, orientation) => {
114
+ if (input.graph.nodes.length === 0) return { layerByNodeId: new Map() };
115
+ const { nodes, minimumSize } = createGraph(input, orientation);
116
+ const orderedNodes = [...nodes].sort(
117
+ (left, right) =>
118
+ right.outgoing.length - left.outgoing.length || left.modelOrder - right.modelOrder,
119
+ );
120
+ const averageSize = nodes.reduce((total, node) => total + node.normalizedSize, 0) / nodes.length;
121
+ const dummySize = (input.settings["spacing.edgeEdge"] ?? 10) / minimumSize;
122
+ const configuredUpperBound = input.settings["layering.minWidth.upperBoundOnWidth"] ?? 4;
123
+ const configuredCompensator =
124
+ input.settings["layering.minWidth.upperLayerEstimationScalingFactor"] ?? 2;
125
+ const upperBounds = configuredUpperBound < 0 ? [1, 2, 3, 4] : [configuredUpperBound];
126
+ const compensators = configuredCompensator < 0 ? [1, 2] : [configuredCompensator];
127
+
128
+ let winner: { maximumWidth: number; layers: MinWidthNode[][] } | undefined;
129
+ for (const upperBound of upperBounds) {
130
+ for (const compensator of compensators) {
131
+ const candidate = computeLayering(
132
+ orderedNodes,
133
+ upperBound,
134
+ compensator,
135
+ averageSize,
136
+ dummySize,
137
+ );
138
+ if (
139
+ !winner ||
140
+ candidate.maximumWidth < winner.maximumWidth ||
141
+ (candidate.maximumWidth === winner.maximumWidth &&
142
+ candidate.layers.length < winner.layers.length)
143
+ ) {
144
+ winner = candidate;
145
+ }
146
+ }
147
+ }
148
+ if (!winner) throw new Error("MinWidth produced no layering");
149
+
150
+ const layerByNodeId = new Map<string, number>();
151
+ const layerCount = winner.layers.length;
152
+ for (const [bottomUpLayer, layer] of winner.layers.entries()) {
153
+ for (const node of layer) layerByNodeId.set(node.id, layerCount - bottomUpLayer - 1);
154
+ }
155
+ return {
156
+ layerByNodeId,
157
+ seedOrder: [...winner.layers].reverse().flatMap((layer) => layer.map((node) => node.id)),
158
+ };
159
+ };