@statelyai/layout 0.3.0 → 0.4.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.
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-BBfKN4Dw.mjs +3075 -0
  12. package/dist/elkjs-DNfXDPup.cjs +3104 -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-B2buJqMf.cjs → layered-5RxQhryb.cjs} +11184 -2932
  26. package/dist/{layered-DGFTeQSl.mjs → layered-DSesbctm.mjs} +12059 -3903
  27. package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-CLFySz3R.cjs} +5 -18
  28. package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-GpBIX6ng.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 +1476 -449
  41. package/src/elkjs/layout-quality.ts +267 -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 +233 -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 +79 -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 +875 -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 +1044 -0
  89. package/src/layered/orthogonal-cycle-order.ts +106 -0
  90. package/src/layered/orthogonal-hypersegments.ts +154 -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 +2124 -886
  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,154 @@
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
+ return scoreOrthogonalHypersegments(ports, connections).count;
76
+ }
77
+
78
+ /**
79
+ * {@link countOrthogonalHypersegmentCrossings}, optionally separating shared
80
+ * tracks. ELK routes a hyperedge on one shared track. When it joins two or
81
+ * more ports on each side, its edges would run opposite ways along that
82
+ * track, which routing never allows; `separateShared` counts such a
83
+ * hyperedge's edges one by one instead. `shared` reports whether one exists.
84
+ */
85
+ export function scoreOrthogonalHypersegments(
86
+ ports: readonly OrthogonalPort[],
87
+ connections: readonly OrthogonalConnection[],
88
+ separateShared = false,
89
+ ): { count: number; shared: boolean } {
90
+ const { segments: merged, segmentByPort } = createOrthogonalHypersegments(ports, connections);
91
+ const sharedSegments = new Set(
92
+ merged.flatMap((s, i) => (s.incoming.length > 1 && s.outgoing.length > 1 ? [i] : [])),
93
+ );
94
+ const separating = separateShared && sharedSegments.size > 0;
95
+ const spans = merged
96
+ .filter((_, i) => !separating || !sharedSegments.has(i))
97
+ .map((s) => ({
98
+ leftStart: Math.min(...s.incoming),
99
+ leftEnd: Math.max(...s.incoming),
100
+ rightStart: Math.min(...s.outgoing),
101
+ rightEnd: Math.max(...s.outgoing),
102
+ }));
103
+ let count = 0;
104
+ for (let i = 0; i < spans.length; i++)
105
+ for (let j = i + 1; j < spans.length; j++) {
106
+ const a = spans[i]!,
107
+ b = spans[j]!;
108
+ if ((a.leftStart - b.leftStart) * (a.rightStart - b.rightStart) < 0) count++;
109
+ if (a.leftStart < b.leftEnd && b.leftStart < a.leftEnd) count++;
110
+ if (a.rightStart < b.rightEnd && b.rightStart < a.rightEnd) count++;
111
+ }
112
+ if (separating) {
113
+ // A separated edge is a one-point span on each side.
114
+ const position = new Map(ports.map((p) => [p.id, p.position]));
115
+ const edges = connections
116
+ .filter((c) => sharedSegments.has(segmentByPort.get(c.source)!))
117
+ .map((c) => [position.get(c.source)!, position.get(c.target)!] as const);
118
+ for (const [left, right] of edges)
119
+ for (const b of spans) {
120
+ if ((left - b.leftStart) * (right - b.rightStart) < 0) count++;
121
+ if (left < b.leftEnd && b.leftStart < left) count++;
122
+ if (right < b.rightEnd && b.rightStart < right) count++;
123
+ }
124
+ count += strictInversions(edges);
125
+ }
126
+ return { count, shared: sharedSegments.size > 0 };
127
+ }
128
+
129
+ /** Pairs whose ends are strictly inverted: two separated edges that cross, in O(n log n). */
130
+ function strictInversions(pairs: ReadonlyArray<readonly [number, number]>): number {
131
+ const ranks = [...new Set(pairs.map(([, right]) => right))].sort((a, b) => a - b);
132
+ const rankOf = new Map(ranks.map((value, index) => [value, index + 1]));
133
+ const tree = new Array<number>(ranks.length + 1).fill(0);
134
+ const atMost = (rank: number) => {
135
+ let sum = 0;
136
+ for (let i = rank; i > 0; i -= i & -i) sum += tree[i]!;
137
+ return sum;
138
+ };
139
+ const sorted = [...pairs].sort((a, b) => a[0] - b[0]);
140
+ let inserted = 0,
141
+ count = 0;
142
+ for (let start = 0; start < sorted.length;) {
143
+ let end = start;
144
+ while (end < sorted.length && sorted[end]![0] === sorted[start]![0]) end++;
145
+ // Edges with an equal left end never cross; compare against earlier ones only.
146
+ for (let i = start; i < end; i++) count += inserted - atMost(rankOf.get(sorted[i]![1])!);
147
+ for (let i = start; i < end; i++) {
148
+ for (let r = rankOf.get(sorted[i]![1])!; r <= ranks.length; r += r & -r) tree[r]!++;
149
+ inserted++;
150
+ }
151
+ start = end;
152
+ }
153
+ return count;
154
+ }
@@ -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
+ }