@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.
- package/NOTICE.md +4 -2
- package/README.md +150 -11
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +2 -0
- package/dist/elkjs/index.d.mts +2 -0
- 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 +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
- package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
- package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
- package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
- package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +2 -1
- package/dist/routing/index.d.cts +2 -2
- package/dist/routing/index.d.mts +2 -2
- package/dist/routing/index.mjs +2 -2
- package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
- package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
- package/package.json +8 -2
- package/src/authoring/coordinates.ts +28 -8
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1392 -449
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +2 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +35 -7
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +886 -161
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +13 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +32 -0
- package/src/routing/algorithms.ts +433 -21
- package/src/routing/coordination.ts +8 -3
- package/src/routing/index.ts +1 -0
- package/src/routing/search.ts +93 -11
- package/dist/elkjs-DaHMISnQ.mjs +0 -1677
- package/dist/elkjs-dMghiiWW.cjs +0 -1706
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Copyright (c) 2010, 2020 Kiel University and others.
|
|
3
|
+
* Adapted from ELK HierarchicalPortOrthogonalEdgeRouter in elkjs 0.11.1.
|
|
4
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
5
|
+
*******************************************************************************/
|
|
6
|
+
import type { EntityRect, Point } from "@statelyai/graph";
|
|
7
|
+
import type { JavaRandom } from "../java-random";
|
|
8
|
+
import type { ExternalPortConstraints, ExternalPortDummy } from "./external-port-dummy";
|
|
9
|
+
import { createOrthogonalHypersegments } from "./orthogonal-hypersegments";
|
|
10
|
+
import { routeOrthogonalSegments } from "./orthogonal-segments";
|
|
11
|
+
import type { LayoutPadding, NodeSize } from "./types";
|
|
12
|
+
|
|
13
|
+
export interface CanonicalHierarchicalPortRouting {
|
|
14
|
+
constraints: ExternalPortConstraints;
|
|
15
|
+
size: NodeSize;
|
|
16
|
+
offset: Point;
|
|
17
|
+
padding: LayoutPadding;
|
|
18
|
+
spacing: { node: number; edge: number; port: number };
|
|
19
|
+
originals: readonly {
|
|
20
|
+
id: string;
|
|
21
|
+
origin: ExternalPortDummy;
|
|
22
|
+
margin?: { left: number; right: number };
|
|
23
|
+
}[];
|
|
24
|
+
helpers: readonly {
|
|
25
|
+
id: string;
|
|
26
|
+
originalId: string;
|
|
27
|
+
rect: EntityRect;
|
|
28
|
+
incoming: readonly string[];
|
|
29
|
+
outgoing: readonly string[];
|
|
30
|
+
}[];
|
|
31
|
+
bends: ReadonlyMap<string, readonly Point[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Operates in ELK's canonical LTR content coordinates, before graph export. */
|
|
35
|
+
export function routeHierarchicalPorts(data: CanonicalHierarchicalPortRouting, random: JavaRandom) {
|
|
36
|
+
const size = { ...data.size },
|
|
37
|
+
offset = { ...data.offset };
|
|
38
|
+
const originals = new Map(data.originals.map((original) => [original.id, original]));
|
|
39
|
+
const rects = new Map<string, EntityRect>();
|
|
40
|
+
const bends = new Map(
|
|
41
|
+
[...data.bends].map(([id, points]) => [id, points.map((point) => ({ ...point }))]),
|
|
42
|
+
);
|
|
43
|
+
for (const original of data.originals) {
|
|
44
|
+
const { origin, id } = original;
|
|
45
|
+
const connected = data.helpers.filter((helper) => helper.originalId === id);
|
|
46
|
+
let x = connected.length
|
|
47
|
+
? connected.reduce((sum, helper) => sum + helper.rect.x, 0) / connected.length -
|
|
48
|
+
origin.anchor.x
|
|
49
|
+
: 0;
|
|
50
|
+
if (data.constraints === "FIXED_POS" || data.constraints === "FIXED_RATIO") {
|
|
51
|
+
x =
|
|
52
|
+
(origin.ratioOrPosition ?? 0) *
|
|
53
|
+
(data.constraints === "FIXED_RATIO"
|
|
54
|
+
? size.width + data.padding.left + data.padding.right
|
|
55
|
+
: 1) -
|
|
56
|
+
origin.anchor.x -
|
|
57
|
+
data.padding.left -
|
|
58
|
+
offset.x;
|
|
59
|
+
if (data.constraints === "FIXED_POS") size.width = Math.max(size.width, x + origin.width / 2);
|
|
60
|
+
}
|
|
61
|
+
rects.set(id, {
|
|
62
|
+
x,
|
|
63
|
+
y:
|
|
64
|
+
origin.side === "NORTH"
|
|
65
|
+
? -data.padding.top - offset.y
|
|
66
|
+
: size.height + data.padding.top + data.padding.bottom - offset.y,
|
|
67
|
+
width: origin.width,
|
|
68
|
+
height: origin.height,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
for (const side of ["NORTH", "SOUTH"] as const) {
|
|
72
|
+
const ordered = data.originals.filter((original) => original.origin.side === side);
|
|
73
|
+
if (
|
|
74
|
+
data.constraints === "FIXED_SIDE" ||
|
|
75
|
+
data.constraints === "FREE" ||
|
|
76
|
+
data.constraints === "UNDEFINED"
|
|
77
|
+
)
|
|
78
|
+
ordered.sort((a, b) => rects.get(a.id)!.x - rects.get(b.id)!.x);
|
|
79
|
+
else if (data.constraints === "FIXED_ORDER")
|
|
80
|
+
ordered.sort((a, b) => (a.origin.ratioOrPosition ?? 0) - (b.origin.ratioOrPosition ?? 0));
|
|
81
|
+
else continue;
|
|
82
|
+
let next: number | undefined;
|
|
83
|
+
for (const original of ordered) {
|
|
84
|
+
const rect = rects.get(original.id)!;
|
|
85
|
+
if (next !== undefined) rect.x = Math.max(rect.x, next + (original.margin?.left ?? 0));
|
|
86
|
+
if (next !== undefined) size.width = Math.max(size.width, rect.x + rect.width);
|
|
87
|
+
next = rect.x + rect.width + (original.margin?.right ?? 0) + data.spacing.port;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const connectorBends = new Map<string, Point[]>();
|
|
91
|
+
const minimumDifference = (values: readonly number[]) => {
|
|
92
|
+
const sorted = [...new Set(values)].sort((a, b) => a - b);
|
|
93
|
+
let min = Number.MAX_VALUE;
|
|
94
|
+
for (let i = 1; i < sorted.length; i++) min = Math.min(min, sorted[i]! - sorted[i - 1]!);
|
|
95
|
+
return min;
|
|
96
|
+
};
|
|
97
|
+
for (const side of ["NORTH", "SOUTH"] as const) {
|
|
98
|
+
const selectedOriginals = data.originals.filter((original) => original.origin.side === side);
|
|
99
|
+
const selectedHelpers = selectedOriginals.flatMap((original) =>
|
|
100
|
+
data.helpers.filter((helper) => helper.originalId === original.id),
|
|
101
|
+
);
|
|
102
|
+
if (!selectedHelpers.length) continue;
|
|
103
|
+
const ports = [
|
|
104
|
+
...selectedHelpers.map((helper) => ({
|
|
105
|
+
id: helper.id,
|
|
106
|
+
side: "source" as const,
|
|
107
|
+
position: helper.rect.x,
|
|
108
|
+
})),
|
|
109
|
+
...selectedOriginals.map((original) => ({
|
|
110
|
+
id: original.id,
|
|
111
|
+
side: "target" as const,
|
|
112
|
+
position: rects.get(original.id)!.x + original.origin.port.x,
|
|
113
|
+
})),
|
|
114
|
+
];
|
|
115
|
+
const grouped = createOrthogonalHypersegments(
|
|
116
|
+
ports,
|
|
117
|
+
selectedHelpers.map((helper) => ({ source: helper.id, target: helper.originalId })),
|
|
118
|
+
);
|
|
119
|
+
const critical =
|
|
120
|
+
0.2 *
|
|
121
|
+
Math.min(
|
|
122
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.incoming)),
|
|
123
|
+
minimumDifference(grouped.segments.flatMap((segment) => segment.outgoing)),
|
|
124
|
+
);
|
|
125
|
+
const routed = routeOrthogonalSegments(
|
|
126
|
+
grouped.segments,
|
|
127
|
+
0.5 * data.spacing.edge,
|
|
128
|
+
critical,
|
|
129
|
+
random,
|
|
130
|
+
);
|
|
131
|
+
const start =
|
|
132
|
+
side === "NORTH" ? -data.spacing.node - offset.y : size.height + data.spacing.node - offset.y;
|
|
133
|
+
const sign = side === "NORTH" ? -1 : 1;
|
|
134
|
+
let slots = 0;
|
|
135
|
+
for (const segment of routed.segments)
|
|
136
|
+
if (Math.abs(segment.end - segment.start) >= 1e-3) slots = Math.max(slots, segment.slot + 1);
|
|
137
|
+
for (const helper of selectedHelpers) {
|
|
138
|
+
const segment = routed.segments[grouped.segmentByPort.get(helper.id)!]!;
|
|
139
|
+
const target = originals.get(helper.originalId)!,
|
|
140
|
+
targetX = rects.get(target.id)!.x + target.origin.port.x;
|
|
141
|
+
const points: Point[] = [];
|
|
142
|
+
if (Math.abs(helper.rect.x - targetX) > 1e-3) {
|
|
143
|
+
let track = start + sign * segment.slot * data.spacing.edge;
|
|
144
|
+
points.push({ x: helper.rect.x, y: track });
|
|
145
|
+
if (segment.partner !== undefined) {
|
|
146
|
+
const partner = routed.segments[segment.partner]!;
|
|
147
|
+
const split = partner.incoming[0]!;
|
|
148
|
+
points.push({ x: split, y: track });
|
|
149
|
+
track = start + sign * partner.slot * data.spacing.edge;
|
|
150
|
+
points.push({ x: split, y: track });
|
|
151
|
+
}
|
|
152
|
+
points.push({ x: targetX, y: track });
|
|
153
|
+
}
|
|
154
|
+
connectorBends.set(helper.id, points);
|
|
155
|
+
}
|
|
156
|
+
if (slots > 0) {
|
|
157
|
+
const reserve = data.spacing.node + (slots - 1) * data.spacing.edge;
|
|
158
|
+
size.height += reserve;
|
|
159
|
+
if (side === "NORTH") offset.y += reserve;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const helper of data.helpers) {
|
|
163
|
+
const connector = connectorBends.get(helper.id) ?? [];
|
|
164
|
+
const joint = { x: helper.rect.x, y: helper.rect.y };
|
|
165
|
+
for (const id of helper.incoming)
|
|
166
|
+
bends.set(id, [...(bends.get(id) ?? []), joint, ...connector]);
|
|
167
|
+
for (const id of helper.outgoing)
|
|
168
|
+
bends.set(id, [...connector.toReversed(), joint, ...(bends.get(id) ?? [])]);
|
|
169
|
+
}
|
|
170
|
+
// The real coordinate fixer runs after reserving connector tracks.
|
|
171
|
+
for (const original of data.originals) {
|
|
172
|
+
const rect = rects.get(original.id)!;
|
|
173
|
+
rect.y =
|
|
174
|
+
original.origin.side === "NORTH"
|
|
175
|
+
? -data.padding.top - offset.y
|
|
176
|
+
: size.height + data.padding.bottom - offset.y;
|
|
177
|
+
}
|
|
178
|
+
return { size, offset, rects, bends };
|
|
179
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { traceLayeredPhase } from "../internal/layered-trace";
|
|
2
|
+
import type { LayerSweepSession } from "./strategies";
|
|
3
|
+
import type { LayerOrder } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Internal prepared scope; boundary callbacks synchronize parent ports and child dummies. */
|
|
6
|
+
export interface HierarchyCrossingScope {
|
|
7
|
+
readonly session: LayerSweepSession;
|
|
8
|
+
readonly childrenByNodeId: ReadonlyMap<string, HierarchyCrossingScope>;
|
|
9
|
+
readonly useBottomUp: boolean;
|
|
10
|
+
/** ELK child graph heuristics retain their own graph random stream. */
|
|
11
|
+
readonly independentRandom?: boolean;
|
|
12
|
+
/** Returns true when an external first layer was ordered from parent ports. */
|
|
13
|
+
alignBoundary?(forward: boolean): boolean;
|
|
14
|
+
publishBoundary?(forward: boolean): void;
|
|
15
|
+
publishBottomUp?(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ELK's counter-based hierarchy traversal: enter children between parent layers,
|
|
20
|
+
* count the coupled subtree, and retain all of its node/port orders together.
|
|
21
|
+
* Scope assembly, sweepiness selection and boundary-port mapping belong to the
|
|
22
|
+
* caller; this coordinator does not infer them from finalized geometry.
|
|
23
|
+
*/
|
|
24
|
+
export function minimizeHierarchyCrossings(
|
|
25
|
+
root: HierarchyCrossingScope,
|
|
26
|
+
): ReadonlyMap<HierarchyCrossingScope, LayerOrder> {
|
|
27
|
+
if (!root.useBottomUp) throw new Error("Root crossing scope must be bottom-up");
|
|
28
|
+
const scopes = [root],
|
|
29
|
+
seen = new Set([root]);
|
|
30
|
+
for (let index = 0; index < scopes.length; index++) {
|
|
31
|
+
const scope = scopes[index]!;
|
|
32
|
+
const nodeIds = new Set(scope.session.snapshot().layers.flat());
|
|
33
|
+
for (const [id, child] of scope.childrenByNodeId) {
|
|
34
|
+
if (!nodeIds.has(id)) throw new Error(`Missing parent crossing node ${id}`);
|
|
35
|
+
scope.session.markHierarchicalNode(id);
|
|
36
|
+
if (seen.has(child)) throw new Error("Crossing scopes must form a tree");
|
|
37
|
+
seen.add(child);
|
|
38
|
+
scopes.push(child);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const initialRandom = root.session.random.clone();
|
|
42
|
+
const coupled = (scope: HierarchyCrossingScope): HierarchyCrossingScope[] => [
|
|
43
|
+
scope,
|
|
44
|
+
...[...scope.childrenByNodeId.values()].flatMap((child) =>
|
|
45
|
+
child.useBottomUp ? [] : coupled(child),
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
const sweep = (scope: HierarchyCrossingScope, forward: boolean, first: boolean): void => {
|
|
49
|
+
scope.session.sweep(forward, first, (_layer, ids) => {
|
|
50
|
+
for (const id of ids) {
|
|
51
|
+
const child = scope.childrenByNodeId.get(id);
|
|
52
|
+
if (!child || child.useBottomUp) continue;
|
|
53
|
+
if (!child.alignBoundary?.(forward)) child.session.shuffleFirstLayer(forward);
|
|
54
|
+
sweep(child, forward, first);
|
|
55
|
+
child.publishBoundary?.(forward);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
// ELK prepends every bottom-up scope during breadth-first initialization.
|
|
60
|
+
for (const scope of [...scopes].reverse().filter((scope) => scope.useBottomUp)) {
|
|
61
|
+
const stream = initialRandom.clone();
|
|
62
|
+
for (const scope of scopes) if (!scope.independentRandom) scope.session.useRandom(stream);
|
|
63
|
+
const subtree = coupled(scope);
|
|
64
|
+
const snapshot = () => new Map(subtree.map((scope) => [scope, scope.session.snapshot()]));
|
|
65
|
+
const count = () => subtree.reduce((total, scope) => total + scope.session.countCrossings(), 0);
|
|
66
|
+
const rollback = subtree.some((scope) => scope.session.restoreRejectedSweep);
|
|
67
|
+
let bestCount = Number.POSITIVE_INFINITY,
|
|
68
|
+
best = snapshot();
|
|
69
|
+
for (let attempt = 0; attempt < scope.session.attempts; attempt++) {
|
|
70
|
+
let forward = stream.nextBoolean();
|
|
71
|
+
if (scope.session.usesInitialModelOrder && attempt === 0) {
|
|
72
|
+
if (attempt === 0 && count() === 0) {
|
|
73
|
+
bestCount = 0;
|
|
74
|
+
best = snapshot();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// ELK 0.11.1 gives FIRST_TRY and SECOND_TRY the same property id.
|
|
78
|
+
// Clearing SECOND_TRY therefore clears FIRST_TRY after this attempt.
|
|
79
|
+
forward = true;
|
|
80
|
+
} else scope.session.shuffleFirstLayer(forward);
|
|
81
|
+
sweep(scope, forward, true);
|
|
82
|
+
scope.session.finishInitialOrderAttempt?.();
|
|
83
|
+
let crossings = count(),
|
|
84
|
+
selected = snapshot();
|
|
85
|
+
const trace = (first: boolean, value: number) =>
|
|
86
|
+
traceLayeredPhase(() => ({
|
|
87
|
+
kind: "sweep",
|
|
88
|
+
scope: scope.session.scope,
|
|
89
|
+
attempt,
|
|
90
|
+
forward,
|
|
91
|
+
firstSweep: first,
|
|
92
|
+
crossings: value,
|
|
93
|
+
layers: scope.session.snapshot().layers,
|
|
94
|
+
}));
|
|
95
|
+
trace(true, crossings);
|
|
96
|
+
while (crossings > 0) {
|
|
97
|
+
forward = !forward;
|
|
98
|
+
const previous = rollback ? snapshot() : undefined;
|
|
99
|
+
sweep(scope, forward, false);
|
|
100
|
+
const next = count();
|
|
101
|
+
trace(false, next);
|
|
102
|
+
if (next >= crossings) {
|
|
103
|
+
// Preserve the legacy wrapped/unzipped standalone rollback policy.
|
|
104
|
+
for (const [scope, order] of previous ?? [])
|
|
105
|
+
if (scope.session.restoreRejectedSweep) scope.session.restore(order);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
crossings = next;
|
|
109
|
+
selected = snapshot();
|
|
110
|
+
}
|
|
111
|
+
if (crossings < bestCount) {
|
|
112
|
+
bestCount = crossings;
|
|
113
|
+
best = selected;
|
|
114
|
+
if (crossings === 0) break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [scope, order] of best) scope.session.restore(order);
|
|
118
|
+
scope.publishBottomUp?.();
|
|
119
|
+
}
|
|
120
|
+
return new Map(scopes.map((scope) => [scope, scope.session.finish(scope.session.snapshot())]));
|
|
121
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Native adaptation of ELK 0.11.1 LayerSweepTypeDecider. SPDX-License-Identifier: EPL-2.0 */
|
|
2
|
+
import { externalPortDummyOf } from "./external-port-dummy";
|
|
3
|
+
import { getCrossingUnits } from "./crossing-constraints";
|
|
4
|
+
import type { AcyclicOrientation, LayeredPhaseInput } from "./types";
|
|
5
|
+
|
|
6
|
+
export function useBottomUpHierarchySweep(
|
|
7
|
+
input: LayeredPhaseInput,
|
|
8
|
+
orientation: AcyclicOrientation,
|
|
9
|
+
layers: readonly (readonly string[])[],
|
|
10
|
+
parent: {
|
|
11
|
+
portOrderFixed: boolean;
|
|
12
|
+
inputPorts: number;
|
|
13
|
+
outputPorts: number;
|
|
14
|
+
deterministic: boolean;
|
|
15
|
+
},
|
|
16
|
+
): boolean {
|
|
17
|
+
const boundary = input.settings["crossingMinimization.hierarchicalSweepiness"] ?? 0.1;
|
|
18
|
+
if (boundary < -1 || parent.portOrderFixed || (parent.inputPorts < 2 && parent.outputPorts < 2))
|
|
19
|
+
return true;
|
|
20
|
+
if (parent.deterministic) return false;
|
|
21
|
+
const nodes = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
22
|
+
const infos = new Map(
|
|
23
|
+
input.graph.nodes.map((node) => [node.id, { random: 0, hierarchy: 0, edges: 0 }]),
|
|
24
|
+
);
|
|
25
|
+
const outgoing = new Map<string, string[]>(),
|
|
26
|
+
incoming = new Map<string, string[]>();
|
|
27
|
+
for (const edge of input.graph.edges) {
|
|
28
|
+
const reversed = orientation.reversedEdgeIds.has(edge.id);
|
|
29
|
+
const source = reversed ? edge.targetId : edge.sourceId,
|
|
30
|
+
target = reversed ? edge.sourceId : edge.targetId;
|
|
31
|
+
outgoing.set(source, [...(outgoing.get(source) ?? []), target]);
|
|
32
|
+
incoming.set(target, [...(incoming.get(target) ?? []), source]);
|
|
33
|
+
}
|
|
34
|
+
const units = getCrossingUnits(input);
|
|
35
|
+
let randomPaths = 0,
|
|
36
|
+
hierarchyPaths = 0;
|
|
37
|
+
const transfer = (source: string, target: string) => {
|
|
38
|
+
const a = infos.get(source)!,
|
|
39
|
+
b = infos.get(target)!;
|
|
40
|
+
b.random += a.random;
|
|
41
|
+
b.hierarchy += a.hierarchy;
|
|
42
|
+
b.edges += a.edges + 1;
|
|
43
|
+
};
|
|
44
|
+
const outputSide = ({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[
|
|
45
|
+
input.direction
|
|
46
|
+
];
|
|
47
|
+
const passEdges = (id: string) => {
|
|
48
|
+
const state = infos.get(id)!;
|
|
49
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
50
|
+
randomPaths += state.random;
|
|
51
|
+
hierarchyPaths += state.hierarchy;
|
|
52
|
+
transfer(id, target);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (const layer of layers) {
|
|
56
|
+
const northSouth: string[] = [];
|
|
57
|
+
for (const id of layer) {
|
|
58
|
+
if (id.startsWith("__layout_dummy:north-south:")) {
|
|
59
|
+
northSouth.push(id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const node = nodes.get(id)!,
|
|
63
|
+
state = infos.get(id)!;
|
|
64
|
+
const external = externalPortDummyOf(node);
|
|
65
|
+
if (external) {
|
|
66
|
+
state.hierarchy = 1;
|
|
67
|
+
if (external.side === outputSide) hierarchyPaths += state.edges;
|
|
68
|
+
} else {
|
|
69
|
+
if (!incoming.get(id)?.length) state.random = 1;
|
|
70
|
+
else if (!outgoing.get(id)?.length) randomPaths += state.edges;
|
|
71
|
+
}
|
|
72
|
+
passEdges(id);
|
|
73
|
+
for (const dummy of units?.associates.get(id) ?? []) {
|
|
74
|
+
randomPaths += state.random;
|
|
75
|
+
hierarchyPaths += state.hierarchy;
|
|
76
|
+
transfer(id, dummy);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const id of northSouth) passEdges(id);
|
|
80
|
+
}
|
|
81
|
+
const all = randomPaths + hierarchyPaths;
|
|
82
|
+
return (all === 0 ? Number.POSITIVE_INFINITY : (randomPaths - hierarchyPaths) / all) >= boundary;
|
|
83
|
+
}
|