@statelyai/layout 0.3.0 → 0.4.0

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 (110) hide show
  1. package/NOTICE.md +4 -2
  2. package/README.md +150 -11
  3. package/dist/elkjs/index.cjs +2 -2
  4. package/dist/elkjs/index.d.cts +2 -0
  5. package/dist/elkjs/index.d.mts +2 -0
  6. package/dist/elkjs/index.mjs +2 -2
  7. package/dist/elkjs/main.cjs +1 -1
  8. package/dist/elkjs/main.mjs +1 -1
  9. package/dist/elkjs/worker.cjs +2 -2
  10. package/dist/elkjs/worker.mjs +2 -2
  11. package/dist/elkjs-UciYx7lD.mjs +2973 -0
  12. package/dist/elkjs-ggjsBuVP.cjs +3002 -0
  13. package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
  14. package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
  15. package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
  16. package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
  17. package/dist/index.cjs +6 -3
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.mts +3 -3
  20. package/dist/index.mjs +4 -4
  21. package/dist/layered/index.cjs +3 -1
  22. package/dist/layered/index.d.cts +2 -2
  23. package/dist/layered/index.d.mts +2 -2
  24. package/dist/layered/index.mjs +2 -2
  25. package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
  26. package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
  27. package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
  28. package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
  29. package/dist/routing/index.cjs +2 -1
  30. package/dist/routing/index.d.cts +2 -2
  31. package/dist/routing/index.d.mts +2 -2
  32. package/dist/routing/index.mjs +2 -2
  33. package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
  34. package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
  35. package/package.json +8 -2
  36. package/src/authoring/coordinates.ts +28 -8
  37. package/src/authoring/partial.ts +7 -2
  38. package/src/authoring/routing.ts +4 -1
  39. package/src/constraints.ts +174 -6
  40. package/src/elkjs/index.ts +1392 -449
  41. package/src/elkjs/layout-quality.ts +219 -0
  42. package/src/elkjs/opposing-tracks.ts +161 -0
  43. package/src/elkjs/orthogonal-junctions.ts +174 -0
  44. package/src/elkjs/statechart.ts +9 -3
  45. package/src/elkjs/types.ts +2 -0
  46. package/src/index.ts +2 -0
  47. package/src/internal/elkjs-compatibility.ts +4 -1
  48. package/src/internal/layered-trace.ts +48 -0
  49. package/src/internal/layout-engine.ts +2 -1
  50. package/src/java-random.ts +6 -0
  51. package/src/layered/bk-node-placement.ts +403 -92
  52. package/src/layered/center-labels.ts +398 -0
  53. package/src/layered/compaction-bounds.ts +26 -0
  54. package/src/layered/compaction-errors.ts +2 -0
  55. package/src/layered/compaction-scanline.ts +63 -0
  56. package/src/layered/compound-boundaries.ts +99 -0
  57. package/src/layered/compound.ts +35 -7
  58. package/src/layered/constraint-orientation.ts +141 -0
  59. package/src/layered/crossing-constraints.ts +243 -0
  60. package/src/layered/crossing-counter.ts +221 -0
  61. package/src/layered/crossing-graph.ts +401 -0
  62. package/src/layered/cycle-random.ts +22 -0
  63. package/src/layered/elk-port-lists.ts +243 -0
  64. package/src/layered/end-labels.ts +124 -0
  65. package/src/layered/external-port-dummy.ts +137 -0
  66. package/src/layered/feedback-chains.ts +114 -0
  67. package/src/layered/fixed-self-loop.ts +150 -0
  68. package/src/layered/greedy-switch-decider.ts +356 -0
  69. package/src/layered/grouped-compaction.ts +787 -0
  70. package/src/layered/gwt-sort.ts +53 -0
  71. package/src/layered/hierarchical-port-phases.ts +242 -0
  72. package/src/layered/hierarchical-port-restoration.ts +233 -0
  73. package/src/layered/hierarchical-port-routing.ts +179 -0
  74. package/src/layered/hierarchy-crossing.ts +121 -0
  75. package/src/layered/hierarchy-sweepiness.ts +83 -0
  76. package/src/layered/hints.ts +371 -0
  77. package/src/layered/hyperedge-dummy-merger.ts +195 -0
  78. package/src/layered/index.ts +886 -161
  79. package/src/layered/initial-model-order.ts +404 -0
  80. package/src/layered/inverted-ports.ts +207 -0
  81. package/src/layered/label-dummy-switcher.ts +200 -0
  82. package/src/layered/long-edges.ts +135 -44
  83. package/src/layered/loop-envelopes.ts +184 -0
  84. package/src/layered/native-routing.ts +275 -0
  85. package/src/layered/network-simplex.ts +11 -15
  86. package/src/layered/node-margins.ts +59 -0
  87. package/src/layered/north-south-ports.ts +271 -0
  88. package/src/layered/opposing-tracks.ts +944 -0
  89. package/src/layered/orthogonal-cycle-order.ts +106 -0
  90. package/src/layered/orthogonal-hypersegments.ts +92 -0
  91. package/src/layered/orthogonal-junctions.ts +202 -0
  92. package/src/layered/orthogonal-segments.ts +243 -0
  93. package/src/layered/placement-orientation.ts +22 -0
  94. package/src/layered/port-distributor.ts +192 -0
  95. package/src/layered/port-edge-order.ts +20 -0
  96. package/src/layered/replace-routing.ts +45 -0
  97. package/src/layered/routing-coordinates.ts +32 -0
  98. package/src/layered/separate-exterior-labels.ts +24 -6
  99. package/src/layered/spacing.ts +34 -12
  100. package/src/layered/splitter-order.ts +106 -0
  101. package/src/layered/strategies.ts +2024 -883
  102. package/src/layered/types.ts +13 -0
  103. package/src/layered/weighted-compaction.ts +84 -0
  104. package/src/routing/adapters.ts +32 -0
  105. package/src/routing/algorithms.ts +433 -21
  106. package/src/routing/coordination.ts +8 -3
  107. package/src/routing/index.ts +1 -0
  108. package/src/routing/search.ts +93 -11
  109. package/dist/elkjs-DaHMISnQ.mjs +0 -1677
  110. package/dist/elkjs-dMghiiWW.cjs +0 -1706
@@ -0,0 +1,106 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2010, 2020 Kiel University and others.
3
+ * Adapted from ELK v0.11.0 HyperEdgeCycleDetector.
4
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
5
+ * SPDX-License-Identifier: EPL-2.0
6
+ *******************************************************************************/
7
+ import type { JavaRandom } from "../java-random";
8
+ export interface OrthogonalDependency {
9
+ source: number;
10
+ target: number;
11
+ weight: number;
12
+ critical: boolean;
13
+ }
14
+ /** ELK's weighted segment ordering; returned dependencies point backwards in that order. */
15
+ export function detectOrthogonalCycles(
16
+ count: number,
17
+ dependencies: readonly OrthogonalDependency[],
18
+ criticalOnly: boolean,
19
+ random: JavaRandom,
20
+ ): { marks: number[]; backwards: OrthogonalDependency[] } {
21
+ const considered = dependencies.filter((d) => !criticalOnly || d.critical);
22
+ const incoming = Array.from({ length: count }, () => [] as OrthogonalDependency[]);
23
+ const outgoing = Array.from({ length: count }, () => [] as OrthogonalDependency[]);
24
+ for (const dependency of considered) {
25
+ incoming[dependency.target]!.push(dependency);
26
+ outgoing[dependency.source]!.push(dependency);
27
+ }
28
+ const inWeight = incoming.map((edges) => edges.reduce((sum, e) => sum + e.weight, 0));
29
+ const outWeight = outgoing.map((edges) => edges.reduce((sum, e) => sum + e.weight, 0));
30
+ const criticalIn = incoming.map((edges) =>
31
+ edges.reduce((sum, e) => sum + (e.critical ? e.weight : 0), 0),
32
+ );
33
+ const criticalOut = outgoing.map((edges) =>
34
+ edges.reduce((sum, e) => sum + (e.critical ? e.weight : 0), 0),
35
+ );
36
+ const marks = Array.from({ length: count }, (_, i) => -i - 1);
37
+ const remaining = new Set(marks.map((_, i) => i));
38
+ const sinks: number[] = [],
39
+ sources: number[] = [];
40
+ for (let i = 0; i < count; i++)
41
+ if (outWeight[i] === 0) sinks.push(i);
42
+ else if (inWeight[i] === 0) sources.push(i);
43
+ let nextSink = count - 1,
44
+ nextSource = count + 1;
45
+ const update = (node: number) => {
46
+ for (const edge of outgoing[node]!) {
47
+ const target = edge.target;
48
+ if (marks[target]! < 0 && edge.weight > 0) {
49
+ inWeight[target]! -= edge.weight;
50
+ if (edge.critical) criticalIn[target]! -= edge.weight;
51
+ if (inWeight[target]! <= 0 && outWeight[target]! > 0) sources.push(target);
52
+ }
53
+ }
54
+ for (const edge of incoming[node]!) {
55
+ const source = edge.source;
56
+ if (marks[source]! < 0 && edge.weight > 0) {
57
+ outWeight[source]! -= edge.weight;
58
+ if (edge.critical) criticalOut[source]! -= edge.weight;
59
+ if (outWeight[source]! <= 0 && inWeight[source]! > 0) sinks.push(source);
60
+ }
61
+ }
62
+ };
63
+ while (remaining.size) {
64
+ while (sinks.length) {
65
+ const node = sinks.shift()!;
66
+ remaining.delete(node);
67
+ marks[node] = nextSink--;
68
+ update(node);
69
+ }
70
+ while (sources.length) {
71
+ const node = sources.shift()!;
72
+ remaining.delete(node);
73
+ marks[node] = nextSource++;
74
+ update(node);
75
+ }
76
+ let maximum = -2147483648;
77
+ let candidates: number[] = [];
78
+ // TreeSet compares initial negative marks, hence reverse segment creation order.
79
+ for (const node of [...remaining].sort((a, b) => marks[a]! - marks[b]!)) {
80
+ if (!criticalOnly && criticalOut[node]! > 0 && criticalIn[node]! <= 0) {
81
+ candidates = [node];
82
+ break;
83
+ }
84
+ const outflow = outWeight[node]! - inWeight[node]!;
85
+ if (outflow >= maximum) {
86
+ if (outflow > maximum) {
87
+ candidates = [];
88
+ maximum = outflow;
89
+ }
90
+ candidates.push(node);
91
+ }
92
+ }
93
+ if (candidates.length) {
94
+ const node = candidates[random.nextInt(candidates.length)]!;
95
+ remaining.delete(node);
96
+ marks[node] = nextSource++;
97
+ update(node);
98
+ }
99
+ }
100
+ for (let i = 0; i < count; i++) if (marks[i]! < count) marks[i]! += count + 1;
101
+ // Upstream iterates each source's outgoing list, not the global edge list.
102
+ const backwards = outgoing.flatMap((edges) =>
103
+ edges.filter((e) => marks[e.source]! > marks[e.target]!),
104
+ );
105
+ return { marks, backwards };
106
+ }
@@ -0,0 +1,92 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2010, 2016, 2020 Kiel University and others.
3
+ * Adapted from ELK v0.11.0 OrthogonalRoutingGenerator, HyperEdgeSegment
4
+ * and HyperedgeCrossingsCounter.
5
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
6
+ * SPDX-License-Identifier: EPL-2.0
7
+ *******************************************************************************/
8
+ export interface OrthogonalPort {
9
+ id: string;
10
+ side: "source" | "target";
11
+ position: number;
12
+ }
13
+ export interface OrthogonalConnection {
14
+ source: string;
15
+ target: string;
16
+ }
17
+ /** ELK visits output ports on each boundary side, then recursively follows all connected edges. */
18
+ export function createOrthogonalHypersegments(
19
+ ports: readonly OrthogonalPort[],
20
+ connections: readonly OrthogonalConnection[],
21
+ /** Position of a connection in its port's incoming or outgoing edge list, when known. */
22
+ listRank?: (connection: number, list: "incoming" | "outgoing") => number | undefined,
23
+ ) {
24
+ const byId = new Map(ports.map((p) => [p.id, p]));
25
+ const incoming = new Map(ports.map((p) => [p.id, [] as number[]]));
26
+ const outgoing = new Map(ports.map((p) => [p.id, [] as number[]]));
27
+ for (const [index, edge] of connections.entries()) {
28
+ incoming.get(edge.target)!.push(index);
29
+ outgoing.get(edge.source)!.push(index);
30
+ }
31
+ // ELK follows each port's edges in list order; fall back to connection order.
32
+ if (listRank)
33
+ for (const [lists, list] of [
34
+ [incoming, "incoming"],
35
+ [outgoing, "outgoing"],
36
+ ] as const)
37
+ for (const members of lists.values())
38
+ members.sort((a, b) => {
39
+ const ra = listRank(a, list),
40
+ rb = listRank(b, list);
41
+ return ra !== undefined && rb !== undefined ? ra - rb || a - b : a - b;
42
+ });
43
+ const segmentByPort = new Map<string, number>();
44
+ const segments: Array<{ ports: string[]; incoming: number[]; outgoing: number[] }> = [];
45
+ const visit = (id: string, index: number) => {
46
+ if (segmentByPort.has(id)) return;
47
+ segmentByPort.set(id, index);
48
+ const port = byId.get(id)!,
49
+ segment = segments[index]!;
50
+ segment.ports.push(id);
51
+ (port.side === "source" ? segment.incoming : segment.outgoing).push(port.position);
52
+ for (const connection of incoming.get(id)!) visit(connections[connection]!.source, index);
53
+ for (const connection of outgoing.get(id)!) visit(connections[connection]!.target, index);
54
+ };
55
+ for (const side of ["source", "target"] as const)
56
+ for (const port of ports) {
57
+ if (port.side !== side || outgoing.get(port.id)!.length === 0 || segmentByPort.has(port.id))
58
+ continue;
59
+ const index = segments.length;
60
+ segments.push({ ports: [], incoming: [], outgoing: [] });
61
+ visit(port.id, index);
62
+ }
63
+ for (const segment of segments) {
64
+ segment.incoming = [...new Set(segment.incoming)].sort((a, b) => a - b);
65
+ segment.outgoing = [...new Set(segment.outgoing)].sort((a, b) => a - b);
66
+ }
67
+ return { segments, segmentByPort };
68
+ }
69
+
70
+ /** ELK's hyperedge crossing estimate; positions are distinct ordinal port ranks on each side. */
71
+ export function countOrthogonalHypersegmentCrossings(
72
+ ports: readonly OrthogonalPort[],
73
+ connections: readonly OrthogonalConnection[],
74
+ ): number {
75
+ const { segments } = createOrthogonalHypersegments(ports, connections);
76
+ const spans = segments.map((s) => ({
77
+ leftStart: Math.min(...s.incoming),
78
+ leftEnd: Math.max(...s.incoming),
79
+ rightStart: Math.min(...s.outgoing),
80
+ rightEnd: Math.max(...s.outgoing),
81
+ }));
82
+ let count = 0;
83
+ for (let i = 0; i < spans.length; i++)
84
+ for (let j = i + 1; j < spans.length; j++) {
85
+ const a = spans[i]!,
86
+ b = spans[j]!;
87
+ if ((a.leftStart - b.leftStart) * (a.rightStart - b.rightStart) < 0) count++;
88
+ if (a.leftStart < b.leftEnd && b.leftStart < a.leftEnd) count++;
89
+ if (a.rightStart < b.rightEnd && b.rightStart < a.rightEnd) count++;
90
+ }
91
+ return count;
92
+ }
@@ -0,0 +1,202 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2010, 2020 Kiel University and others.
3
+ * Native adaptation of ELK orthogonal routing junction generation.
4
+ * SPDX-License-Identifier: EPL-2.0
5
+ *******************************************************************************/
6
+ import type { GraphEdge, GraphNode, GraphPort, Point } from "@statelyai/graph";
7
+ import type { AcyclicOrientation, LayeredPhaseInput } from "./types";
8
+ import type { OrthogonalSegment } from "./orthogonal-segments";
9
+ import { createOrthogonalHypersegments, type OrthogonalPort } from "./orthogonal-hypersegments";
10
+ import { isMergedHyperedgeDummy } from "./hyperedge-dummy-merger";
11
+ export type OrthogonalJunctionGroup = Array<{
12
+ edgeId: string;
13
+ reversed: boolean;
14
+ segment: OrthogonalSegment;
15
+ partner?: OrthogonalSegment;
16
+ sourceValue: number;
17
+ targetValue: number;
18
+ }>;
19
+ /** Emit branches from routing-port spans, before endpoint restoration and compaction. */
20
+ export function orthogonalJunctionPoints(
21
+ input: LayeredPhaseInput,
22
+ orientation: AcyclicOrientation,
23
+ raw: ReadonlyMap<string, { source: Point; target: Point }>,
24
+ flowLayerByNodeId: ReadonlyMap<string, number>,
25
+ junctionGroups: OrthogonalJunctionGroup[],
26
+ pointsByEdgeId: ReadonlyMap<string, readonly Point[]>,
27
+ orthogonalTrackByEdgeId: ReadonlyMap<string, number>,
28
+ orthogonalDetourByEdgeId: ReadonlyMap<
29
+ string,
30
+ { firstTrack: number; secondTrack: number; crossover: number }
31
+ >,
32
+ portDirection: (node: GraphNode, port: GraphPort) => GraphPort["direction"],
33
+ ): ReadonlyMap<string, readonly Point[]> {
34
+ const horizontal = input.direction === "left" || input.direction === "right";
35
+ const increasing = input.direction === "right" || input.direction === "down";
36
+ const nodeById = new Map(input.graph.nodes.map((n) => [n.id, n]));
37
+ const junctionPointsByEdgeId = new Map<string, readonly Point[]>();
38
+
39
+ type BoundaryEdge = {
40
+ edge: GraphEdge;
41
+ source: string;
42
+ target: string;
43
+ sourceValue: number;
44
+ targetValue: number;
45
+ inLayer: boolean;
46
+ };
47
+ const boundaries = new Map<
48
+ number,
49
+ { ports: Map<string, OrthogonalPort>; edges: BoundaryEdge[] }
50
+ >();
51
+ const face = (edge: GraphEdge, source: boolean) => {
52
+ const id = source ? edge.sourceId : edge.targetId;
53
+ const node = nodeById.get(id)!;
54
+ const name = source ? edge.sourcePort : edge.targetPort;
55
+ const port = node.ports?.find((p) => p.name === name);
56
+ if (node.id.startsWith("__layout_dummy:") && (name === "input" || name === "output"))
57
+ return horizontal
58
+ ? (name === "output") === increasing
59
+ ? "EAST"
60
+ : "WEST"
61
+ : (name === "output") === increasing
62
+ ? "SOUTH"
63
+ : "NORTH";
64
+ const side = port && input.portSettings?.(port, node)?.["port.side"];
65
+ if (side && side !== "UNDEFINED") return side;
66
+ const physicalSource = source !== orientation.reversedEdgeIds.has(edge.id);
67
+ const outgoing = port
68
+ ? (portDirection(node, port) ?? port.direction) === "out"
69
+ : physicalSource;
70
+ return horizontal
71
+ ? outgoing === increasing
72
+ ? "EAST"
73
+ : "WEST"
74
+ : outgoing === increasing
75
+ ? "SOUTH"
76
+ : "NORTH";
77
+ };
78
+ for (const edge of input.graph.edges) {
79
+ const from = flowLayerByNodeId.get(edge.sourceId),
80
+ to = flowLayerByNodeId.get(edge.targetId);
81
+ const endpoints = raw.get(edge.id);
82
+ if (from === undefined || to === undefined || !endpoints || edge.sourceId === edge.targetId)
83
+ continue;
84
+ const firstFace = face(edge, true),
85
+ lastFace = face(edge, false);
86
+ const positiveFace = horizontal ? "EAST" : "SOUTH",
87
+ negativeFace = horizontal ? "WEST" : "NORTH";
88
+ if (
89
+ ![positiveFace, negativeFace].includes(String(firstFace)) ||
90
+ ![positiveFace, negativeFace].includes(String(lastFace))
91
+ )
92
+ continue;
93
+ const firstBoundary = from - (firstFace === negativeFace ? 1 : 0),
94
+ lastBoundary = to - (lastFace === negativeFace ? 1 : 0);
95
+ if (firstBoundary !== lastBoundary) continue;
96
+ let boundary = boundaries.get(firstBoundary);
97
+ if (!boundary) boundaries.set(firstBoundary, (boundary = { ports: new Map(), edges: [] }));
98
+ const key = (source: boolean) => {
99
+ const id = source ? edge.sourceId : edge.targetId,
100
+ name = source ? edge.sourcePort : edge.targetPort;
101
+ if (isMergedHyperedgeDummy(input, id))
102
+ return JSON.stringify([id, source ? firstFace : lastFace]);
103
+ return name !== undefined ? JSON.stringify([id, name]) : JSON.stringify([edge.id, source]);
104
+ };
105
+ for (const source of [true, false]) {
106
+ const id = key(source),
107
+ point = source ? endpoints.source : endpoints.target;
108
+ if (!boundary.ports.has(id))
109
+ boundary.ports.set(id, {
110
+ id,
111
+ side:
112
+ ((source ? firstFace : lastFace) === positiveFace) === increasing ? "source" : "target",
113
+ position: horizontal ? point.y : point.x,
114
+ });
115
+ }
116
+ const reversed = orientation.reversedEdgeIds.has(edge.id);
117
+ const source = key(!reversed),
118
+ target = key(reversed);
119
+ boundary.edges.push({
120
+ edge,
121
+ source,
122
+ target,
123
+ sourceValue: boundary.ports.get(source)!.position,
124
+ targetValue: boundary.ports.get(target)!.position,
125
+ inLayer: from === to,
126
+ });
127
+ }
128
+ const routedEdges = new Set(
129
+ junctionGroups.flatMap((group) => group.map((record) => record.edgeId)),
130
+ );
131
+ const inLayerGroups: typeof junctionGroups = [];
132
+ for (const [, boundary] of [...boundaries].sort(([a], [b]) => (increasing ? a - b : b - a))) {
133
+ if (!boundary.edges.some((e) => e.inLayer)) continue;
134
+ const ports = [...boundary.ports.values()].sort((a, b) => a.position - b.position);
135
+ const grouped = createOrthogonalHypersegments(ports, boundary.edges);
136
+ const records: (typeof junctionGroups)[number] = [];
137
+ for (const group of grouped.segments)
138
+ for (const port of group.ports) {
139
+ const members = boundary.edges.filter(
140
+ (e) => e.source === port && e.inLayer && !routedEdges.has(e.edge.id),
141
+ );
142
+ for (const record of members) {
143
+ const values = [...group.incoming, ...group.outgoing];
144
+ records.push({
145
+ edgeId: record.edge.id,
146
+ reversed: orientation.reversedEdgeIds.has(record.edge.id),
147
+ sourceValue: record.sourceValue,
148
+ targetValue: record.targetValue,
149
+ segment: { ...group, start: Math.min(...values), end: Math.max(...values), slot: 0 },
150
+ });
151
+ }
152
+ }
153
+ inLayerGroups.push(records);
154
+ }
155
+ junctionGroups = [...inLayerGroups, ...junctionGroups];
156
+ const createdJunctions = new Set<string>();
157
+ for (const group of junctionGroups) {
158
+ for (const { edgeId, reversed, segment, partner, sourceValue, targetValue } of group) {
159
+ const physical = [...(pointsByEdgeId.get(edgeId) ?? [])];
160
+ if (reversed) physical.reverse();
161
+ const bends = Math.abs(segment.start - segment.end) < 1e-3 ? [] : physical.slice(1, -1);
162
+ const junctions: Point[] = [];
163
+ for (const [index, point] of bends.entries()) {
164
+ const track = orthogonalTrackByEdgeId.get(edgeId);
165
+ const detour = orthogonalDetourByEdgeId.get(edgeId);
166
+ const flow = horizontal ? point.x : point.y;
167
+ if (track !== undefined && Math.abs(flow - track) > 1e-9) continue;
168
+ if (
169
+ detour &&
170
+ Math.abs(flow - detour.firstTrack) > 1e-9 &&
171
+ Math.abs(flow - detour.secondTrack) > 1e-9
172
+ )
173
+ continue;
174
+ const current = partner && index >= 2 ? partner : segment;
175
+ const value =
176
+ index === 0
177
+ ? sourceValue
178
+ : index === bends.length - 1
179
+ ? targetValue
180
+ : segment.outgoing[0]!;
181
+ const boundary =
182
+ current.incoming.length > 0 &&
183
+ current.outgoing.length > 0 &&
184
+ ((Math.abs(value - current.incoming[0]!) < 1e-3 &&
185
+ Math.abs(value - current.outgoing[0]!) < 1e-3) ||
186
+ (Math.abs(value - current.incoming.at(-1)!) < 1e-3 &&
187
+ Math.abs(value - current.outgoing.at(-1)!) < 1e-3));
188
+ const key = `${point.x}:${point.y}`;
189
+ if (
190
+ !createdJunctions.has(key) &&
191
+ ((value > current.start && value < current.end) || boundary)
192
+ ) {
193
+ junctions.push(point);
194
+ createdJunctions.add(key);
195
+ }
196
+ }
197
+ junctionPointsByEdgeId.set(edgeId, junctions);
198
+ }
199
+ }
200
+
201
+ return junctionPointsByEdgeId;
202
+ }
@@ -0,0 +1,243 @@
1
+ /*******************************************************************************
2
+ * Copyright (c) 2010, 2020 Kiel University and others.
3
+ * Adapted from ELK v0.11.0 orthogonal segment splitting and routing generation.
4
+ * Source commit: 54123e884b1ae743b453260f713b20c9bf5787f2
5
+ * SPDX-License-Identifier: EPL-2.0
6
+ *******************************************************************************/
7
+ import type { JavaRandom } from "../java-random";
8
+ import { detectOrthogonalCycles, type OrthogonalDependency } from "./orthogonal-cycle-order";
9
+ export interface OrthogonalSegmentInput {
10
+ incoming: readonly number[];
11
+ outgoing: readonly number[];
12
+ }
13
+ export interface OrthogonalSegment {
14
+ incoming: number[];
15
+ outgoing: number[];
16
+ start: number;
17
+ end: number;
18
+ slot: number;
19
+ partner?: number;
20
+ splitBy?: number;
21
+ }
22
+ const extent = (s: OrthogonalSegmentInput) => {
23
+ const values = [...s.incoming, ...s.outgoing];
24
+ return {
25
+ start: values.length ? Math.min(...values) : NaN,
26
+ end: values.length ? Math.max(...values) : NaN,
27
+ };
28
+ };
29
+ const crossings = (positions: readonly number[], start: number, end: number) =>
30
+ positions.filter((p) => p >= start && p <= end).length;
31
+ const orderingCrossings = (a: OrthogonalSegment, b: OrthogonalSegment) =>
32
+ crossings(a.outgoing, b.start, b.end) + crossings(b.incoming, a.start, a.end);
33
+ /** Full ELK segment dependency, critical split, cycle removal and rank phases. */
34
+ export function routeOrthogonalSegments(
35
+ input: readonly OrthogonalSegmentInput[],
36
+ conflictThreshold: number,
37
+ criticalThreshold: number,
38
+ random: JavaRandom,
39
+ ) {
40
+ const segments: OrthogonalSegment[] = input.map((s) => ({
41
+ incoming: [...s.incoming],
42
+ outgoing: [...s.outgoing],
43
+ ...extent(s),
44
+ slot: 0,
45
+ }));
46
+ let dependencies: OrthogonalDependency[] = [];
47
+ const add = (source: number, target: number, critical: boolean, weight: number) =>
48
+ dependencies.push({ source, target, critical, weight });
49
+ const conflicts = (a: readonly number[], b: readonly number[]) => {
50
+ if (!a.length || !b.length) return 0;
51
+ let i = 0,
52
+ j = 0,
53
+ count = 0;
54
+ while (true) {
55
+ const x = a[i]!,
56
+ y = b[j]!;
57
+ if (x > y - criticalThreshold && x < y + criticalThreshold) return -1;
58
+ if (x > y - conflictThreshold && x < y + conflictThreshold) count++;
59
+ if (x <= y && i < a.length - 1) i++;
60
+ else if (y <= x && j < b.length - 1) j++;
61
+ else break;
62
+ }
63
+ return count;
64
+ };
65
+ const create = (a: number, b: number) => {
66
+ const left = segments[a]!,
67
+ right = segments[b]!;
68
+ if (Math.abs(left.start - left.end) < 1e-3 || Math.abs(right.start - right.end) < 1e-3)
69
+ return 0;
70
+ const c1 = conflicts(left.outgoing, right.incoming),
71
+ c2 = conflicts(right.outgoing, left.incoming);
72
+ if (c1 === -1 || c2 === -1) {
73
+ if (c1 === -1) add(b, a, true, 1);
74
+ if (c2 === -1) add(a, b, true, 1);
75
+ return Number(c1 === -1) + Number(c2 === -1);
76
+ }
77
+ const v1 = c1 + 16 * orderingCrossings(left, right),
78
+ v2 = c2 + 16 * orderingCrossings(right, left);
79
+ if (v1 < v2) add(a, b, false, v2 - v1);
80
+ else if (v1 > v2) add(b, a, false, v1 - v2);
81
+ else if (v1 > 0) {
82
+ add(a, b, false, 0);
83
+ add(b, a, false, 0);
84
+ }
85
+ return 0;
86
+ };
87
+ let criticalCount = 0;
88
+ for (let a = 0; a < segments.length - 1; a++)
89
+ for (let b = a + 1; b < segments.length; b++) criticalCount += create(a, b);
90
+ if (criticalCount >= 2) {
91
+ const cycle = detectOrthogonalCycles(segments.length, dependencies, true, random);
92
+ const selected = new Set<number>();
93
+ for (const dependency of cycle.backwards) {
94
+ let { source, target } = dependency;
95
+ if (selected.has(source) || selected.has(target)) continue;
96
+ const a = segments[source]!,
97
+ b = segments[target]!;
98
+ if (a.incoming.length + a.outgoing.length > 2 && b.incoming.length + b.outgoing.length <= 2)
99
+ [source, target] = [target, source];
100
+ selected.add(source);
101
+ segments[source]!.splitBy = target;
102
+ }
103
+ const coords = segments.flatMap((s) => [...s.incoming, ...s.outgoing]).sort((a, b) => a - b);
104
+ const areas: Array<{ start: number; end: number }> = [];
105
+ for (let i = 1; i < coords.length; i++)
106
+ if (coords[i]! - coords[i - 1]! >= 2 * criticalThreshold)
107
+ areas.push({
108
+ start: coords[i - 1]! + criticalThreshold,
109
+ end: coords[i]! - criticalThreshold,
110
+ });
111
+ const ordered = [...selected].sort(
112
+ (a, b) => segments[a]!.end - segments[a]!.start - (segments[b]!.end - segments[b]!.start),
113
+ );
114
+ for (const index of ordered) {
115
+ const segment = segments[index]!,
116
+ causing = segment.splitBy!;
117
+ let position = (segment.start + segment.end) / 2;
118
+ const possible = areas.flatMap((area, i) =>
119
+ area.start <= segment.end && area.end >= segment.start ? [i] : [],
120
+ );
121
+ if (possible.length) {
122
+ const rating = (areaIndex: number) => {
123
+ const area = areas[areaIndex]!,
124
+ center = (area.start + area.end) / 2;
125
+ // ELK rates using the provisional halves' extents before adding the bridge.
126
+ const left = {
127
+ ...segment,
128
+ outgoing: [center],
129
+ ...extent({ incoming: segment.incoming, outgoing: [] }),
130
+ };
131
+ const right = {
132
+ ...segment,
133
+ incoming: [center],
134
+ ...extent({ incoming: [], outgoing: segment.outgoing }),
135
+ };
136
+ let depCount = 2,
137
+ cross = 0;
138
+ const neighbors = [
139
+ ...dependencies.filter((d) => d.target === index).map((d) => d.source),
140
+ ...dependencies.filter((d) => d.source === index).map((d) => d.target),
141
+ ];
142
+ for (const other of neighbors)
143
+ for (const half of [left, right]) {
144
+ const c1 = orderingCrossings(half, segments[other]!),
145
+ c2 = orderingCrossings(segments[other]!, half);
146
+ if (c1 === c2) {
147
+ if (c1 > 0) {
148
+ depCount += 2;
149
+ cross += c1;
150
+ }
151
+ } else {
152
+ depCount++;
153
+ cross += Math.min(c1, c2);
154
+ }
155
+ }
156
+ cross +=
157
+ orderingCrossings(left, segments[causing]!) +
158
+ orderingCrossings(segments[causing]!, right);
159
+ return { cross, depCount, size: area.end - area.start };
160
+ };
161
+ let best = possible[0]!,
162
+ score = rating(best);
163
+ for (const candidate of possible.slice(1)) {
164
+ const curr = rating(candidate);
165
+ if (
166
+ curr.cross < score.cross ||
167
+ (curr.cross === score.cross &&
168
+ (curr.depCount < score.depCount ||
169
+ (curr.depCount === score.depCount && curr.size > score.size)))
170
+ ) {
171
+ best = candidate;
172
+ score = curr;
173
+ }
174
+ }
175
+ const area = areas[best]!;
176
+ position = (area.start + area.end) / 2;
177
+ areas.splice(best, 1);
178
+ if ((area.end - area.start) / 2 >= criticalThreshold) {
179
+ const replacement = [];
180
+ if (area.start <= position - criticalThreshold)
181
+ replacement.push({ start: area.start, end: position - criticalThreshold });
182
+ if (position + criticalThreshold <= area.end)
183
+ replacement.push({ start: position + criticalThreshold, end: area.end });
184
+ areas.splice(best, 0, ...replacement);
185
+ }
186
+ }
187
+ const partner = segments.length;
188
+ const outgoing = segment.outgoing;
189
+ segment.outgoing = [position];
190
+ segment.partner = partner;
191
+ Object.assign(segment, extent(segment));
192
+ segments.push({
193
+ incoming: [position],
194
+ outgoing,
195
+ ...extent({ incoming: [position], outgoing }),
196
+ slot: 0,
197
+ partner: index,
198
+ });
199
+ dependencies = dependencies.filter((d) => d.source !== index && d.target !== index);
200
+ add(index, causing, true, 1);
201
+ add(causing, partner, true, 1);
202
+ for (let other = 0; other < segments.length; other++)
203
+ if (other !== index && other !== partner && other !== causing) {
204
+ create(other, index);
205
+ create(other, partner);
206
+ }
207
+ }
208
+ }
209
+ const regular = detectOrthogonalCycles(segments.length, dependencies, false, random);
210
+ // Reversal appends to both adjacency lists, just like ELK's setters.
211
+ for (const backwards of regular.backwards) {
212
+ dependencies.splice(dependencies.indexOf(backwards), 1);
213
+ if (backwards.weight > 0)
214
+ dependencies.push({ ...backwards, source: backwards.target, target: backwards.source });
215
+ }
216
+ const inCount = segments.map((_, i) => dependencies.filter((d) => d.target === i).length),
217
+ outCount = segments.map((_, i) => dependencies.filter((d) => d.source === i).length);
218
+ const sources = inCount.flatMap((n, i) => (n === 0 ? [i] : [])),
219
+ targets = outCount.flatMap((n, i) =>
220
+ n === 0 && segments[i]!.incoming.length === 0 ? [i] : [],
221
+ );
222
+ let maxRank = -1;
223
+ while (sources.length) {
224
+ const index = sources.shift()!;
225
+ for (const d of dependencies.filter((d) => d.source === index)) {
226
+ segments[d.target]!.slot = Math.max(segments[d.target]!.slot, segments[index]!.slot + 1);
227
+ maxRank = Math.max(maxRank, segments[d.target]!.slot);
228
+ if (--inCount[d.target]! === 0) sources.push(d.target);
229
+ }
230
+ }
231
+ if (maxRank > -1) {
232
+ for (const index of targets) segments[index]!.slot = maxRank;
233
+ while (targets.length) {
234
+ const index = targets.shift()!;
235
+ for (const d of dependencies.filter((d) => d.target === index)) {
236
+ if (segments[d.source]!.incoming.length) continue;
237
+ segments[d.source]!.slot = Math.min(segments[d.source]!.slot, segments[index]!.slot - 1);
238
+ if (--outCount[d.source]! === 0) targets.push(d.source);
239
+ }
240
+ }
241
+ }
242
+ return { segments, dependencies };
243
+ }
@@ -0,0 +1,22 @@
1
+ import type { AcyclicOrientation, LayeredPhaseInput, LayerOrder } from "./types";
2
+
3
+ const orientationByInput = new WeakMap<LayeredPhaseInput, AcyclicOrientation>();
4
+
5
+ /** Retain the active acyclic topology without altering authored endpoints or public output. */
6
+ export function setPlacementOrientation(
7
+ input: LayeredPhaseInput,
8
+ orientation: AcyclicOrientation,
9
+ ): void {
10
+ orientationByInput.set(input, orientation);
11
+ }
12
+ export function getPlacementOrientation(input: LayeredPhaseInput): AcyclicOrientation | undefined {
13
+ return orientationByInput.get(input);
14
+ }
15
+
16
+ const orderByInput = new WeakMap<LayeredPhaseInput, LayerOrder>();
17
+ export function setPlacementOrder(input: LayeredPhaseInput, order: LayerOrder): void {
18
+ orderByInput.set(input, order);
19
+ }
20
+ export function getPlacementOrder(input: LayeredPhaseInput): LayerOrder | undefined {
21
+ return orderByInput.get(input);
22
+ }