@statelyai/layout 0.4.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.
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/{elkjs-UciYx7lD.mjs → elkjs-BBfKN4Dw.mjs} +165 -63
- package/dist/{elkjs-ggjsBuVP.cjs → elkjs-DNfXDPup.cjs} +165 -63
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-CBPg6_6_.cjs → layered-5RxQhryb.cjs} +245 -39
- package/dist/{layered-C7ERzZjR.mjs → layered-DSesbctm.mjs} +240 -34
- package/dist/{layout-engine-DAw06RhN.cjs → layout-engine-CLFySz3R.cjs} +1 -1
- package/dist/{layout-engine-DC3IYkIy.mjs → layout-engine-GpBIX6ng.mjs} +1 -1
- package/package.json +1 -1
- package/src/elkjs/index.ts +112 -28
- package/src/elkjs/layout-quality.ts +81 -33
- package/src/layered/crossing-counter.ts +17 -5
- package/src/layered/hierarchy-sweepiness.ts +37 -41
- package/src/layered/index.ts +0 -11
- package/src/layered/opposing-tracks.ts +108 -8
- package/src/layered/orthogonal-hypersegments.ts +69 -7
- package/src/layered/strategies.ts +115 -18
|
@@ -120,6 +120,14 @@ function crossing(s: Segment, t: Segment): boolean {
|
|
|
120
120
|
h.a.y < Math.max(v.a.y, v.b.y) - EPS
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
|
+
const onBorder = (p: TrackPoint, r: TrackRect) => {
|
|
124
|
+
const inX = p.x >= r.x - EPS && p.x <= r.x + r.width + EPS,
|
|
125
|
+
inY = p.y >= r.y - EPS && p.y <= r.y + r.height + EPS;
|
|
126
|
+
return (
|
|
127
|
+
(inY && (Math.abs(p.x - r.x) <= EPS || Math.abs(p.x - r.x - r.width) <= EPS)) ||
|
|
128
|
+
(inX && (Math.abs(p.y - r.y) <= EPS || Math.abs(p.y - r.y - r.height) <= EPS))
|
|
129
|
+
);
|
|
130
|
+
};
|
|
123
131
|
const segmentsOf = (points: readonly TrackPoint[]): Segment[] =>
|
|
124
132
|
points.slice(1).flatMap((b, i) => {
|
|
125
133
|
const s = { a: points[i]!, b };
|
|
@@ -159,8 +167,28 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
159
167
|
if (!rects) terminalCache.set(key, (rects = terminals(a.route, b.route)));
|
|
160
168
|
return rects;
|
|
161
169
|
};
|
|
170
|
+
const boxes = new WeakMap<Segment[], [number, number, number, number]>();
|
|
171
|
+
const boxOf = (segments: Segment[]) => {
|
|
172
|
+
let box = boxes.get(segments);
|
|
173
|
+
if (!box) {
|
|
174
|
+
box = [Infinity, Infinity, -Infinity, -Infinity];
|
|
175
|
+
for (const { a, b } of segments) {
|
|
176
|
+
box[0] = Math.min(box[0], a.x, b.x);
|
|
177
|
+
box[1] = Math.min(box[1], a.y, b.y);
|
|
178
|
+
box[2] = Math.max(box[2], a.x, b.x);
|
|
179
|
+
box[3] = Math.max(box[3], a.y, b.y);
|
|
180
|
+
}
|
|
181
|
+
boxes.set(segments, box);
|
|
182
|
+
}
|
|
183
|
+
return box;
|
|
184
|
+
};
|
|
185
|
+
const none = { opposing: 0, overlap: 0, crossings: 0 };
|
|
162
186
|
// The metric's pair measures: opposite-direction and same-direction shared length, crossings.
|
|
163
187
|
const pair = (mine: Segment[], self: State, other: State) => {
|
|
188
|
+
// Routes with disjoint bounds neither share a track nor cross.
|
|
189
|
+
const [ax0, ay0, ax1, ay1] = boxOf(mine),
|
|
190
|
+
[bx0, by0, bx1, by1] = boxOf(other.measured);
|
|
191
|
+
if (ax1 < bx0 - EPS || bx1 < ax0 - EPS || ay1 < by0 - EPS || by1 < ay0 - EPS) return none;
|
|
164
192
|
const rects = terminalsOf(self, other);
|
|
165
193
|
const as = rects.length ? clip(mine, rects, true) : mine,
|
|
166
194
|
bs = rects.length ? clip(other.measured, rects, true) : other.measured;
|
|
@@ -449,6 +477,41 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
449
477
|
0,
|
|
450
478
|
)
|
|
451
479
|
: along;
|
|
480
|
+
// A portless end may instead slide along its node side; its
|
|
481
|
+
// perpendicular neighbour stretches.
|
|
482
|
+
const after = { a: corner, b: beyond };
|
|
483
|
+
const against = (s: Segment) =>
|
|
484
|
+
states.reduce(
|
|
485
|
+
(sum, other) =>
|
|
486
|
+
other === state
|
|
487
|
+
? sum
|
|
488
|
+
: other.measured.reduce((n, t) => n + (opposite(s, t) ? shared(s, t) : 0), sum),
|
|
489
|
+
0,
|
|
490
|
+
);
|
|
491
|
+
if (
|
|
492
|
+
state.route.ends.length === 2 &&
|
|
493
|
+
state.route.ports?.[reversed ? 1 : 0] === undefined &&
|
|
494
|
+
rect &&
|
|
495
|
+
onBorder(port, rect) &&
|
|
496
|
+
(h ? vertical(after) : horizontal(after))
|
|
497
|
+
) {
|
|
498
|
+
const lo = h ? rect.y : rect.x,
|
|
499
|
+
hi = lo + (h ? rect.height : rect.width);
|
|
500
|
+
for (const delta of offsets) {
|
|
501
|
+
const moved = shift(port, delta);
|
|
502
|
+
if ((h ? moved.y : moved.x) < lo - EPS || (h ? moved.y : moved.x) > hi + EPS)
|
|
503
|
+
continue;
|
|
504
|
+
const next = [
|
|
505
|
+
{ a: moved, b: shift(corner, delta) },
|
|
506
|
+
{ a: shift(corner, delta), b: beyond },
|
|
507
|
+
];
|
|
508
|
+
// The stretched neighbour may not run against another route more than before.
|
|
509
|
+
if (!keeps(after, next[1]!) || against(next[1]!) > against(after) + EPS) continue;
|
|
510
|
+
const replaced = [moved, shift(corner, delta), ...path.slice(2)];
|
|
511
|
+
const points = reversed ? replaced.reverse() : replaced;
|
|
512
|
+
consider(points, [segment, after], next, next[0]!, 0, delta);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
452
515
|
for (const stub of new Set([along - clearance, spacing]))
|
|
453
516
|
for (const delta of offsets) {
|
|
454
517
|
if (stub < 1 || along - stub < spacing) continue;
|
|
@@ -491,17 +554,30 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
491
554
|
* half a spacing either side of the port's centre.
|
|
492
555
|
*/
|
|
493
556
|
function splitSharedPorts(tolerant: boolean) {
|
|
494
|
-
const ports = scene.ports;
|
|
495
|
-
if (!ports?.size) return;
|
|
496
557
|
type End = { state: State; end: 0 | 1 };
|
|
497
558
|
const groups = new Map<string, { port: TrackPort; normal: TrackPoint; ends: End[] }>();
|
|
559
|
+
// Implicit attachment points per node; points within EPS are one point.
|
|
560
|
+
const implicit = new Map<string, TrackPoint[]>();
|
|
498
561
|
for (const state of states)
|
|
499
562
|
for (const end of [0, 1] as const) {
|
|
500
|
-
|
|
501
|
-
const port = id === undefined ? undefined : ports.get(id);
|
|
502
|
-
if (!port || state.points.length < 2) continue;
|
|
563
|
+
if (state.points.length < 2) continue;
|
|
503
564
|
const path = end ? [...state.points].reverse() : state.points;
|
|
504
565
|
const [p, q] = path as [TrackPoint, TrackPoint];
|
|
566
|
+
let id = state.route.ports?.[end];
|
|
567
|
+
let port = id === undefined ? undefined : scene.ports?.get(id);
|
|
568
|
+
// Portless ends meeting at one point of a node border (merged edges)
|
|
569
|
+
// share an implicit port there.
|
|
570
|
+
const node = state.route.ends.length === 2 ? state.route.ends[end]! : undefined;
|
|
571
|
+
const rect = node === undefined ? undefined : nodes.get(node);
|
|
572
|
+
if (id === undefined && rect && onBorder(p, rect)) {
|
|
573
|
+
const points = implicit.get(node!) ?? [];
|
|
574
|
+
implicit.set(node!, points);
|
|
575
|
+
let at = points.find((o) => Math.abs(o.x - p.x) <= EPS && Math.abs(o.y - p.y) <= EPS);
|
|
576
|
+
if (!at) points.push((at = { x: p.x, y: p.y }));
|
|
577
|
+
id = `${node}\0${points.indexOf(at)}`;
|
|
578
|
+
port = { node: node!, rect: { ...at, width: 0, height: 0 } };
|
|
579
|
+
}
|
|
580
|
+
if (!port) continue;
|
|
505
581
|
const s = { a: p, b: q };
|
|
506
582
|
if (size(s) <= EPS || (!horizontal(s) && !vertical(s))) continue;
|
|
507
583
|
const normal = { x: Math.sign(q.x - p.x), y: Math.sign(q.y - p.y) };
|
|
@@ -510,7 +586,14 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
510
586
|
if (!group) groups.set(key, (group = { port, normal, ends: [] }));
|
|
511
587
|
group.ends.push({ state, end });
|
|
512
588
|
}
|
|
513
|
-
for (const
|
|
589
|
+
for (const group of groups.values()) {
|
|
590
|
+
const { port, normal } = group;
|
|
591
|
+
// An earlier split may have slid a straight route off an implicit port.
|
|
592
|
+
const ends = group.ends.filter(({ state, end }) => {
|
|
593
|
+
if (state.route.ports?.[end] !== undefined) return true;
|
|
594
|
+
const p = end ? state.points.at(-1)! : state.points[0]!;
|
|
595
|
+
return Math.abs(p.x - port.rect.x) <= EPS && Math.abs(p.y - port.rect.y) <= EPS;
|
|
596
|
+
});
|
|
514
597
|
const incoming = ends.filter((e) => e.end === 1),
|
|
515
598
|
outgoing = ends.filter((e) => e.end === 0);
|
|
516
599
|
if (!incoming.length || !outgoing.length) continue;
|
|
@@ -530,10 +613,27 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
530
613
|
sideHi = node[lateral] + (lateral === "x" ? node.width : node.height);
|
|
531
614
|
const wide = extent >= spacing - EPS;
|
|
532
615
|
const half = spacing / 2;
|
|
616
|
+
// A straight portless two-point route slides sideways whole, both ends
|
|
617
|
+
// staying on their node sides.
|
|
618
|
+
const slid = (state: State, points: TrackPoint[], end: 0 | 1, offset: number) => {
|
|
619
|
+
if (state.route.ports?.some((id) => id !== undefined)) return undefined;
|
|
620
|
+
if (Math.abs(points[0]![across] - points[1]![across]) <= EPS) return undefined;
|
|
621
|
+
const far = nodes.get(state.route.ends[1 - end] ?? "");
|
|
622
|
+
const moved = points.map((p) => ({ ...p, [lateral]: p[lateral] + offset }));
|
|
623
|
+
const within = (value: number, rect: TrackRect) => {
|
|
624
|
+
const lo = rect[lateral];
|
|
625
|
+
return (
|
|
626
|
+
value >= lo - EPS && value <= lo + (lateral === "x" ? rect.width : rect.height) + EPS
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
const value = moved[0]![lateral];
|
|
630
|
+
return within(value, node) && far && within(value, far) ? moved : undefined;
|
|
631
|
+
};
|
|
533
632
|
// Move one end of a route sideways by `offset`, keeping it orthogonal.
|
|
534
|
-
const shifted = (points: TrackPoint[], end: 0 | 1, offset: number) => {
|
|
633
|
+
const shifted = (state: State, points: TrackPoint[], end: 0 | 1, offset: number) => {
|
|
535
634
|
if (Math.abs(offset) <= EPS) return points;
|
|
536
635
|
const path = end ? [...points].reverse() : [...points];
|
|
636
|
+
if (path.length === 2) return slid(state, points, end, offset);
|
|
537
637
|
if (path.length < 3) return undefined;
|
|
538
638
|
const [p, q, r] = path as [TrackPoint, TrackPoint, TrackPoint];
|
|
539
639
|
const along = r[lateral] - q[lateral];
|
|
@@ -594,7 +694,7 @@ export function separateOpposingTracks(scene: TrackScene): Map<string, TrackPoin
|
|
|
594
694
|
const next = new Map<State, TrackPoint[]>();
|
|
595
695
|
let feasible = true;
|
|
596
696
|
for (const e of ends) {
|
|
597
|
-
const points = shifted(next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
|
|
697
|
+
const points = shifted(e.state, next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
|
|
598
698
|
if (!points) {
|
|
599
699
|
feasible = false;
|
|
600
700
|
break;
|
|
@@ -72,13 +72,34 @@ export function countOrthogonalHypersegmentCrossings(
|
|
|
72
72
|
ports: readonly OrthogonalPort[],
|
|
73
73
|
connections: readonly OrthogonalConnection[],
|
|
74
74
|
): number {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}));
|
|
82
103
|
let count = 0;
|
|
83
104
|
for (let i = 0; i < spans.length; i++)
|
|
84
105
|
for (let j = i + 1; j < spans.length; j++) {
|
|
@@ -88,5 +109,46 @@ export function countOrthogonalHypersegmentCrossings(
|
|
|
88
109
|
if (a.leftStart < b.leftEnd && b.leftStart < a.leftEnd) count++;
|
|
89
110
|
if (a.rightStart < b.rightEnd && b.rightStart < a.rightEnd) count++;
|
|
90
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
|
+
}
|
|
91
153
|
return count;
|
|
92
154
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./initial-model-order";
|
|
8
8
|
import { insertionSort } from "./gwt-sort";
|
|
9
9
|
import { CanonicalPortDistributor } from "./port-distributor";
|
|
10
|
-
import { countAllCrossings } from "./crossing-counter";
|
|
10
|
+
import { countAllCrossings, type CrossingGraph } from "./crossing-counter";
|
|
11
11
|
import {
|
|
12
12
|
createCrossingGraphBuilder,
|
|
13
13
|
crossingGraph,
|
|
@@ -124,6 +124,24 @@ function cycleModelOrder(input: LayeredPhaseInput): Map<string, number> {
|
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Internal setting: how crossing minimization models ports and tracks.
|
|
129
|
+
* `mixedPorts` (default true) counts a port carrying both directions as two
|
|
130
|
+
* ports and groups its neighbours by direction. `separateSharedTracks`
|
|
131
|
+
* (default false) counts a hyperedge joining two or more ports on each side
|
|
132
|
+
* of its gap edge by edge, since routing never lets edges run opposite ways
|
|
133
|
+
* along one track. A `probe` records whether either choice can change the
|
|
134
|
+
* order. Each helps on average but steers the sweeps into worse orders on
|
|
135
|
+
* some graphs, so the elkjs facade also lays such graphs out with the
|
|
136
|
+
* alternatives and keeps the best result.
|
|
137
|
+
*/
|
|
138
|
+
export const CROSSING_MODEL = "internal.crossingModel";
|
|
139
|
+
export interface CrossingModel {
|
|
140
|
+
mixedPorts?: boolean;
|
|
141
|
+
separateSharedTracks?: boolean;
|
|
142
|
+
probe?: { mixedPorts: boolean; sharedTracks: boolean };
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
export const breakCyclesWithDepthFirstSearch: CycleBreaker = (input) => {
|
|
128
146
|
const outgoing = new Map<string, GraphEdge[]>();
|
|
129
147
|
for (const node of input.graph.nodes) outgoing.set(node.id, []);
|
|
@@ -818,14 +836,7 @@ export function applyLayerConstraintOrder(input: LayeredPhaseInput, order: Layer
|
|
|
818
836
|
...order,
|
|
819
837
|
layers: order.layers.map((layer) => {
|
|
820
838
|
const unconstrained = layer.filter((id) => !constrained.has(id));
|
|
821
|
-
|
|
822
|
-
input.settings.mergeEdges === true
|
|
823
|
-
? [
|
|
824
|
-
...unconstrained.filter((id) => id.startsWith("__layout_dummy:")),
|
|
825
|
-
...unconstrained.filter((id) => !id.startsWith("__layout_dummy:")),
|
|
826
|
-
]
|
|
827
|
-
: unconstrained;
|
|
828
|
-
return [...mergedOrder, ...layer.filter((id) => constrained.has(id))];
|
|
839
|
+
return [...unconstrained, ...layer.filter((id) => constrained.has(id))];
|
|
829
840
|
}),
|
|
830
841
|
};
|
|
831
842
|
}
|
|
@@ -1897,6 +1908,8 @@ export interface LayerSweepSession {
|
|
|
1897
1908
|
currentLayers(): readonly (readonly string[])[];
|
|
1898
1909
|
/** Current canonical port ids of one node, as {@link snapshot} records them. */
|
|
1899
1910
|
physicalPortOrder(nodeId: string): string[];
|
|
1911
|
+
/** Canonical crossing graph in the current order; callers must not mutate it. */
|
|
1912
|
+
crossingGraph(): CrossingGraph;
|
|
1900
1913
|
/** Live incident edge orders of one node; callers must not mutate them. */
|
|
1901
1914
|
edgeOrders(nodeId: string): { incoming: readonly string[]; outgoing: readonly string[] };
|
|
1902
1915
|
/** Same as restoring a snapshot that differs only in one layer. */
|
|
@@ -1984,6 +1997,32 @@ export function createLayerSweepSession(
|
|
|
1984
1997
|
}
|
|
1985
1998
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
1986
1999
|
const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
|
|
2000
|
+
// ELK lists a compound's boundary ports in creation order: edges on them
|
|
2001
|
+
// take their slots in that order, in model order within a port. Authored
|
|
2002
|
+
// ports already arrive in ELK's sorted order and keep their edge order.
|
|
2003
|
+
const boundaryPortIndex = (nodeId: string, edgeId: string, incoming: boolean) => {
|
|
2004
|
+
const edge = edgeById.get(edgeId)!;
|
|
2005
|
+
const name =
|
|
2006
|
+
incoming !== orientation.reversedEdgeIds.has(edgeId) ? edge.targetPort : edge.sourcePort;
|
|
2007
|
+
if (!name?.startsWith("__native_hierarchy")) return -1;
|
|
2008
|
+
return nodeById.get(nodeId)!.ports?.findIndex((port) => port.name === name) ?? -1;
|
|
2009
|
+
};
|
|
2010
|
+
for (const [orders, incoming] of [
|
|
2011
|
+
[outputPortOrder, false],
|
|
2012
|
+
[inputPortOrder, true],
|
|
2013
|
+
] as const)
|
|
2014
|
+
for (const [nodeId, edgeIds] of orders) {
|
|
2015
|
+
if (edgeIds.length < 2) continue;
|
|
2016
|
+
const slots = edgeIds.flatMap((id, slot) =>
|
|
2017
|
+
boundaryPortIndex(nodeId, id, incoming) < 0 ? [] : [slot],
|
|
2018
|
+
);
|
|
2019
|
+
const sorted = slots
|
|
2020
|
+
.map((slot) => edgeIds[slot]!)
|
|
2021
|
+
.sort(
|
|
2022
|
+
(a, b) => boundaryPortIndex(nodeId, a, incoming) - boundaryPortIndex(nodeId, b, incoming),
|
|
2023
|
+
);
|
|
2024
|
+
slots.forEach((slot, index) => (edgeIds[slot] = sorted[index]!));
|
|
2025
|
+
}
|
|
1987
2026
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
1988
2027
|
const hierarchicalNodes = new Set<string>();
|
|
1989
2028
|
const fixedOrderNodes = new Set(
|
|
@@ -2037,6 +2076,10 @@ export function createLayerSweepSession(
|
|
|
2037
2076
|
}
|
|
2038
2077
|
}
|
|
2039
2078
|
const buildCrossingGraph = createCrossingGraphBuilder(input);
|
|
2079
|
+
const crossingModel: CrossingModel =
|
|
2080
|
+
(input.settings as { [CROSSING_MODEL]?: CrossingModel })[CROSSING_MODEL] ?? {};
|
|
2081
|
+
const mixedPorts = crossingModel.mixedPorts !== false,
|
|
2082
|
+
probe = crossingModel.probe;
|
|
2040
2083
|
const countCrossings = (candidateLayers: readonly (readonly string[])[]): number => {
|
|
2041
2084
|
if (exactPortSweep) {
|
|
2042
2085
|
const graph = buildCrossingGraph(
|
|
@@ -2045,7 +2088,26 @@ export function createLayerSweepSession(
|
|
|
2045
2088
|
inputPortOrder,
|
|
2046
2089
|
outputPortOrder,
|
|
2047
2090
|
);
|
|
2048
|
-
|
|
2091
|
+
// Edge orders rank a side's ports per direction; a fixed-side port with
|
|
2092
|
+
// a reversed edge mixes both, so count the sweep's physical port order.
|
|
2093
|
+
for (const layer of graph.layers)
|
|
2094
|
+
for (const node of layer) {
|
|
2095
|
+
const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : undefined;
|
|
2096
|
+
if (physical?.length !== node.ports.length) continue;
|
|
2097
|
+
const slot = new Map(physical.map((port, index) => [port.id, index]));
|
|
2098
|
+
if (node.ports.every((port) => slot.has(port.id)))
|
|
2099
|
+
node.ports.sort((a, b) => slot.get(a.id)! - slot.get(b.id)!);
|
|
2100
|
+
}
|
|
2101
|
+
const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
|
|
2102
|
+
const score = countAllCrossings(
|
|
2103
|
+
split,
|
|
2104
|
+
crossingModel.separateSharedTracks ? "separated" : "hyperedges",
|
|
2105
|
+
);
|
|
2106
|
+
if (probe) {
|
|
2107
|
+
if (split !== graph) probe.mixedPorts = true;
|
|
2108
|
+
if (score.sharedTracks) probe.sharedTracks = true;
|
|
2109
|
+
}
|
|
2110
|
+
let total = score.total;
|
|
2049
2111
|
const nodeInfluence = Number(
|
|
2050
2112
|
input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0,
|
|
2051
2113
|
);
|
|
@@ -2151,12 +2213,13 @@ export function createLayerSweepSession(
|
|
|
2151
2213
|
!forward,
|
|
2152
2214
|
nodeRelativePortRanks,
|
|
2153
2215
|
);
|
|
2154
|
-
|
|
2155
|
-
|
|
2216
|
+
// The fixed layer's edges toward the free layer, in edge order.
|
|
2217
|
+
const byEnd = forward ? canonicalEdgesBySource : canonicalEdgesByTarget;
|
|
2218
|
+
const indices = fixedLayer.flatMap((id) => byEnd.get(id) ?? []).sort((a, b) => a - b);
|
|
2219
|
+
for (const index of indices) {
|
|
2156
2220
|
const edge = canonicalEdges[index]!;
|
|
2157
2221
|
const [source, target] = canonicalOrientedEndpoints[index]!;
|
|
2158
|
-
if (!
|
|
2159
|
-
continue;
|
|
2222
|
+
if (!freeIds.has(forward ? target : source)) continue;
|
|
2160
2223
|
const endpoint = canonicalEndpoints.get(edge.id)!;
|
|
2161
2224
|
const rank =
|
|
2162
2225
|
canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target]!;
|
|
@@ -2288,6 +2351,18 @@ export function createLayerSweepSession(
|
|
|
2288
2351
|
const canonicalOrientedEndpoints = canonicalEdges.map((edge) =>
|
|
2289
2352
|
getOrientedEndpoints(edge, orientation),
|
|
2290
2353
|
);
|
|
2354
|
+
const canonicalEdgesBySource = new Map<string, number[]>(),
|
|
2355
|
+
canonicalEdgesByTarget = new Map<string, number[]>();
|
|
2356
|
+
canonicalOrientedEndpoints.forEach(([source, target], index) => {
|
|
2357
|
+
for (const [byEnd, id] of [
|
|
2358
|
+
[canonicalEdgesBySource, source],
|
|
2359
|
+
[canonicalEdgesByTarget, target],
|
|
2360
|
+
] as const) {
|
|
2361
|
+
const list = byEnd.get(id);
|
|
2362
|
+
if (list) list.push(index);
|
|
2363
|
+
else byEnd.set(id, [index]);
|
|
2364
|
+
}
|
|
2365
|
+
});
|
|
2291
2366
|
const canonicalEndpoints = new Map(
|
|
2292
2367
|
canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]!]),
|
|
2293
2368
|
);
|
|
@@ -2386,6 +2461,7 @@ export function createLayerSweepSession(
|
|
|
2386
2461
|
fixed: readonly string[],
|
|
2387
2462
|
beyond: readonly string[] = [],
|
|
2388
2463
|
) => {
|
|
2464
|
+
if (!mixedPorts) return;
|
|
2389
2465
|
const fixedIds = new Set(fixed);
|
|
2390
2466
|
const groups = new Map<string, Array<{ id: string; reversed: boolean }>>();
|
|
2391
2467
|
for (const id of free) {
|
|
@@ -2408,6 +2484,7 @@ export function createLayerSweepSession(
|
|
|
2408
2484
|
}
|
|
2409
2485
|
if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed)))
|
|
2410
2486
|
return;
|
|
2487
|
+
if (probe) probe.mixedPorts = true;
|
|
2411
2488
|
const index = new Map(free.map((id, i) => [id, i]));
|
|
2412
2489
|
// The direction whose nodes lead further toward the start of the layer
|
|
2413
2490
|
// beyond (from the previous sweep) goes first; forward edges by default.
|
|
@@ -2649,6 +2726,10 @@ export function createLayerSweepSession(
|
|
|
2649
2726
|
restore,
|
|
2650
2727
|
finish,
|
|
2651
2728
|
currentLayers: () => working,
|
|
2729
|
+
crossingGraph: () => ({
|
|
2730
|
+
layers: working.map((layer) => layer.map((id) => canonicalNodes.get(id)!)),
|
|
2731
|
+
edges: canonicalGraph.edges,
|
|
2732
|
+
}),
|
|
2652
2733
|
physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
|
|
2653
2734
|
edgeOrders: (id) => ({
|
|
2654
2735
|
incoming: inputPortOrder.get(id) ?? [],
|
|
@@ -3783,6 +3864,18 @@ function implicitEdgeEndpoints(
|
|
|
3783
3864
|
|
|
3784
3865
|
const result = new Map<string, { source: Point; target: Point }>();
|
|
3785
3866
|
const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
|
|
3867
|
+
// First positions in swept port orders, as `indexOf` finds them.
|
|
3868
|
+
const orderIndexes = new Map<readonly string[], Map<string, number>>();
|
|
3869
|
+
const orderIndex = (list: readonly string[], id: string) => {
|
|
3870
|
+
let positions = orderIndexes.get(list);
|
|
3871
|
+
if (!positions) {
|
|
3872
|
+
positions = new Map();
|
|
3873
|
+
for (const [index, item] of list.entries())
|
|
3874
|
+
if (!positions.has(item)) positions.set(item, index);
|
|
3875
|
+
orderIndexes.set(list, positions);
|
|
3876
|
+
}
|
|
3877
|
+
return positions.get(id) ?? -1;
|
|
3878
|
+
};
|
|
3786
3879
|
const placedPorts = new Map<GraphNode, { rect: EntityRect; ports: GraphPort[] | undefined }>();
|
|
3787
3880
|
const portSideCounts = new Map<GraphNode, Map<unknown, number>>();
|
|
3788
3881
|
const portSideCount = (node: GraphNode, side: unknown): number => {
|
|
@@ -3860,8 +3953,8 @@ function implicitEdgeEndpoints(
|
|
|
3860
3953
|
const selected = (
|
|
3861
3954
|
leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId
|
|
3862
3955
|
)?.get(nodeId);
|
|
3863
|
-
const a = selected
|
|
3864
|
-
b = selected
|
|
3956
|
+
const a = selected ? orderIndex(selected, left.edge.id) : -1,
|
|
3957
|
+
b = selected ? orderIndex(selected, right.edge.id) : -1;
|
|
3865
3958
|
if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
|
|
3866
3959
|
}
|
|
3867
3960
|
if (interactiveLeftSourceOrder) {
|
|
@@ -4931,9 +5024,13 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4931
5024
|
candidate.straight = Math.abs(segment.start - segment.end) < 1e-3;
|
|
4932
5025
|
candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
|
|
4933
5026
|
if (segment.partner !== undefined) {
|
|
5027
|
+
// The route leaves the graph source on the half holding that end.
|
|
4934
5028
|
const partner = result.segments[segment.partner]!;
|
|
4935
|
-
|
|
4936
|
-
|
|
5029
|
+
const [first, second] = physicalPort(candidate, "source").graphSource
|
|
5030
|
+
? [segment, partner]
|
|
5031
|
+
: [partner, segment];
|
|
5032
|
+
candidate.slot = increasing ? first.slot : maximumSlot - first.slot;
|
|
5033
|
+
candidate.secondSlot = increasing ? second.slot : maximumSlot - second.slot;
|
|
4937
5034
|
const sourcePort = portByCandidate.get(candidate)!.source;
|
|
4938
5035
|
const offset =
|
|
4939
5036
|
(increasing ? candidate.sourceCross : candidate.targetCross) -
|