@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.
- 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-BBfKN4Dw.mjs +3075 -0
- package/dist/elkjs-DNfXDPup.cjs +3104 -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-B2buJqMf.cjs → layered-5RxQhryb.cjs} +11184 -2932
- package/dist/{layered-DGFTeQSl.mjs → layered-DSesbctm.mjs} +12059 -3903
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-CLFySz3R.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-GpBIX6ng.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 +1476 -449
- package/src/elkjs/layout-quality.ts +267 -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 +233 -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 +79 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +875 -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 +1044 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +154 -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 +2124 -886
- 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,3075 @@
|
|
|
1
|
+
import { T as pathFromPoints, g as pathFromSplinePoints, j as freeze } from "./routing-CR8flv8b.mjs";
|
|
2
|
+
import { $ as attachExternalPortDummy, R as createLayerSweepSession, S as CROSSING_MODEL, Z as minimizeHierarchyCrossings, b as elkjs0111ResultPolicy, c as elkLayeredOptionDefinitions, et as createExternalPortDummy, h as separateOpposingTracks, nt as createOrthogonalHypersegments, t as createLayeredScopePipeline, tt as externalPortDummyOf, v as isPerpendicularPortRestored, y as hierarchicalPortSides } from "./layered-DSesbctm.mjs";
|
|
3
|
+
import { n as executeElkjs0111Layout } from "./layout-engine-GpBIX6ng.mjs";
|
|
4
|
+
import { createGraph } from "@statelyai/graph";
|
|
5
|
+
|
|
6
|
+
//#region src/layered/hierarchy-sweepiness.ts
|
|
7
|
+
/**
|
|
8
|
+
* Whether a child graph is swept on its own before its parent. Like ELK, a
|
|
9
|
+
* node starts a random path when no western port carries an edge and ends one
|
|
10
|
+
* when no eastern port does, on the canonical crossing graph's port sides.
|
|
11
|
+
*/
|
|
12
|
+
function useBottomUpHierarchySweep(input, graph, parent) {
|
|
13
|
+
const boundary = input.settings["crossingMinimization.hierarchicalSweepiness"] ?? .1;
|
|
14
|
+
if (boundary < -1 || parent.portOrderFixed || parent.inputPorts < 2 && parent.outputPorts < 2) return true;
|
|
15
|
+
if (parent.deterministic) return false;
|
|
16
|
+
const nodes = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
17
|
+
const owner = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const node of graph.layers.flat()) for (const port of node.ports) owner.set(port.id, node.id);
|
|
19
|
+
const degree = /* @__PURE__ */ new Map(), targets = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const { source, target } of graph.edges) {
|
|
21
|
+
degree.set(source, (degree.get(source) ?? 0) + 1);
|
|
22
|
+
degree.set(target, (degree.get(target) ?? 0) + 1);
|
|
23
|
+
const from = owner.get(source);
|
|
24
|
+
targets.set(from, [...targets.get(from) ?? [], owner.get(target)]);
|
|
25
|
+
}
|
|
26
|
+
const infos = new Map(graph.layers.flat().map((node) => [node.id, {
|
|
27
|
+
random: 0,
|
|
28
|
+
hierarchy: 0,
|
|
29
|
+
edges: 0
|
|
30
|
+
}]));
|
|
31
|
+
let randomPaths = 0, hierarchyPaths = 0;
|
|
32
|
+
const transfer = (source, target) => {
|
|
33
|
+
const a = infos.get(source), b = infos.get(target);
|
|
34
|
+
randomPaths += a.random;
|
|
35
|
+
hierarchyPaths += a.hierarchy;
|
|
36
|
+
b.random += a.random;
|
|
37
|
+
b.hierarchy += a.hierarchy;
|
|
38
|
+
b.edges += a.edges + 1;
|
|
39
|
+
};
|
|
40
|
+
const outputSide = {
|
|
41
|
+
right: "EAST",
|
|
42
|
+
left: "WEST",
|
|
43
|
+
down: "SOUTH",
|
|
44
|
+
up: "NORTH"
|
|
45
|
+
}[input.direction];
|
|
46
|
+
for (const layer of graph.layers) {
|
|
47
|
+
const northSouth = [];
|
|
48
|
+
for (const node of layer) {
|
|
49
|
+
if (node.type === "NORTH_SOUTH_PORT") {
|
|
50
|
+
northSouth.push(node.id);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const state = infos.get(node.id);
|
|
54
|
+
const connected = (side) => node.ports.some((port) => port.side === side && degree.has(port.id));
|
|
55
|
+
const external = externalPortDummyOf(nodes.get(node.id));
|
|
56
|
+
if (external) {
|
|
57
|
+
state.hierarchy = 1;
|
|
58
|
+
if (external.side === outputSide) hierarchyPaths += state.edges;
|
|
59
|
+
} else if (!connected("WEST")) state.random = 1;
|
|
60
|
+
else if (!connected("EAST")) randomPaths += state.edges;
|
|
61
|
+
for (const target of targets.get(node.id) ?? []) transfer(node.id, target);
|
|
62
|
+
for (const port of node.ports) if ((port.side === "NORTH" || port.side === "SOUTH") && port.dummy) transfer(node.id, port.dummy);
|
|
63
|
+
}
|
|
64
|
+
for (const id of northSouth) for (const target of targets.get(id) ?? []) transfer(id, target);
|
|
65
|
+
}
|
|
66
|
+
const all = randomPaths + hierarchyPaths;
|
|
67
|
+
return (all === 0 ? Number.POSITIVE_INFINITY : (randomPaths - hierarchyPaths) / all) >= boundary;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/layered/compound-boundaries.ts
|
|
72
|
+
/** Return both effects of ELK's transfer: parent port and corrected content-space dummy. */
|
|
73
|
+
function transferExternalPort(input) {
|
|
74
|
+
const { contentSize: size, padding: p, offset: o, borderOffset: b, portSize } = input;
|
|
75
|
+
const dummy = {
|
|
76
|
+
x: input.dummy.x,
|
|
77
|
+
y: input.dummy.y
|
|
78
|
+
};
|
|
79
|
+
const port = {
|
|
80
|
+
x: dummy.x + input.dummy.width / 2,
|
|
81
|
+
y: dummy.y + input.dummy.height / 2
|
|
82
|
+
};
|
|
83
|
+
switch (input.side) {
|
|
84
|
+
case "NORTH":
|
|
85
|
+
port.x += p.left + o.x - portSize.width / 2;
|
|
86
|
+
port.y = -portSize.height - b;
|
|
87
|
+
dummy.y = -(p.top + b + o.y);
|
|
88
|
+
break;
|
|
89
|
+
case "EAST":
|
|
90
|
+
port.x = size.width + p.left + p.right + b;
|
|
91
|
+
port.y += p.top + o.y - portSize.height / 2;
|
|
92
|
+
dummy.x = size.width + p.right + b - o.x;
|
|
93
|
+
break;
|
|
94
|
+
case "SOUTH":
|
|
95
|
+
port.x += p.left + o.x - portSize.width / 2;
|
|
96
|
+
port.y = size.height + p.top + p.bottom + b;
|
|
97
|
+
dummy.y = size.height + p.bottom + b - o.y;
|
|
98
|
+
break;
|
|
99
|
+
case "WEST":
|
|
100
|
+
port.x = -portSize.width - b;
|
|
101
|
+
port.y += p.top + o.y - portSize.height / 2;
|
|
102
|
+
dummy.x = -(p.left + b + o.x);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
port,
|
|
107
|
+
dummy
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Segments are directed source-to-target and translated to one reference graph. */
|
|
111
|
+
function joinCompoundRouteSegments(segments, unnecessaryBendpoints = false) {
|
|
112
|
+
const bends = [];
|
|
113
|
+
let start, end, last;
|
|
114
|
+
for (const segment of segments) {
|
|
115
|
+
if (segment.points.length < 2) throw new Error("Compound route segment needs two anchors");
|
|
116
|
+
const points = segment.points.map((p) => ({
|
|
117
|
+
x: p.x + segment.offset.x,
|
|
118
|
+
y: p.y + segment.offset.y
|
|
119
|
+
}));
|
|
120
|
+
const source = points[0], target = points.at(-1);
|
|
121
|
+
const inner = points.slice(1, -1);
|
|
122
|
+
if (!start) start = source;
|
|
123
|
+
if (last) {
|
|
124
|
+
const next = inner[0] ?? target;
|
|
125
|
+
const dx = Math.abs(last.x - next.x) > 1e-6;
|
|
126
|
+
const dy = Math.abs(last.y - next.y) > 1e-6;
|
|
127
|
+
if (unnecessaryBendpoints ? dx || dy : dx && dy) bends.push(source);
|
|
128
|
+
}
|
|
129
|
+
bends.push(...inner);
|
|
130
|
+
last = inner.at(-1) ?? source;
|
|
131
|
+
end = target;
|
|
132
|
+
}
|
|
133
|
+
if (!start || !end) return [];
|
|
134
|
+
const route = [
|
|
135
|
+
start,
|
|
136
|
+
...bends,
|
|
137
|
+
end
|
|
138
|
+
];
|
|
139
|
+
const straight = (a, b) => Math.abs(a.x - b.x) <= 1e-6 || Math.abs(a.y - b.y) <= 1e-6;
|
|
140
|
+
for (let index = 0; index + 1 < route.length; index++) {
|
|
141
|
+
const a = route[index], b = route[index + 1];
|
|
142
|
+
if (straight(a, b)) continue;
|
|
143
|
+
const next = route[index + 2], previous = route[index - 1];
|
|
144
|
+
const verticalFirst = next !== void 0 ? Math.abs(b.y - next.y) <= 1e-6 : previous !== void 0 && Math.abs(previous.x - a.x) <= 1e-6;
|
|
145
|
+
route.splice(index + 1, 0, verticalFirst ? {
|
|
146
|
+
x: a.x,
|
|
147
|
+
y: b.y
|
|
148
|
+
} : {
|
|
149
|
+
x: b.x,
|
|
150
|
+
y: a.y
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return route;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/elkjs/layout-quality.ts
|
|
158
|
+
const EPS = 1e-6;
|
|
159
|
+
/** Shared endpoint nodes get this clearance, so fan-in and fan-out are not penalized. */
|
|
160
|
+
const TERMINAL_MARGIN = 12;
|
|
161
|
+
const horizontal = (s) => Math.abs(s.a.y - s.b.y) < EPS;
|
|
162
|
+
const vertical = (s) => Math.abs(s.a.x - s.b.x) < EPS;
|
|
163
|
+
const span = (s, axis) => [Math.min(s.a[axis], s.b[axis]), Math.max(s.a[axis], s.b[axis])];
|
|
164
|
+
const strictlyInside = (value, low, high) => value > low + EPS && value < high - EPS;
|
|
165
|
+
const overlapLength = (a, b) => Math.max(0, Math.min(a[1], b[1]) - Math.max(a[0], b[0]));
|
|
166
|
+
/** Within `margin` of the rectangle, its border included. */
|
|
167
|
+
const inRect = (point, rect, margin) => point.x >= rect.x - margin - EPS && point.x <= rect.x + rect.width + margin + EPS && point.y >= rect.y - margin - EPS && point.y <= rect.y + rect.height + margin + EPS;
|
|
168
|
+
/**
|
|
169
|
+
* Measure an ELK JSON result in absolute coordinates. `defectsOnly` skips the
|
|
170
|
+
* pairwise route measures (crossings and overlap stay 0).
|
|
171
|
+
*/
|
|
172
|
+
function measureLayout(root, defectsOnly = false) {
|
|
173
|
+
const frames = new Map([[String(root.id), {
|
|
174
|
+
x: 0,
|
|
175
|
+
y: 0
|
|
176
|
+
}]]);
|
|
177
|
+
const rects = /* @__PURE__ */ new Map();
|
|
178
|
+
const owner = /* @__PURE__ */ new Map();
|
|
179
|
+
const leaves = [];
|
|
180
|
+
const bounds = [];
|
|
181
|
+
const place = (parent, offset) => {
|
|
182
|
+
for (const child of parent.children ?? []) {
|
|
183
|
+
const id = String(child.id);
|
|
184
|
+
const at = {
|
|
185
|
+
x: offset.x + (child.x ?? 0),
|
|
186
|
+
y: offset.y + (child.y ?? 0)
|
|
187
|
+
};
|
|
188
|
+
const rect = {
|
|
189
|
+
...at,
|
|
190
|
+
width: child.width ?? 0,
|
|
191
|
+
height: child.height ?? 0
|
|
192
|
+
};
|
|
193
|
+
frames.set(id, at);
|
|
194
|
+
rects.set(id, rect);
|
|
195
|
+
owner.set(id, id);
|
|
196
|
+
for (const port of child.ports ?? []) owner.set(String(port.id), id);
|
|
197
|
+
bounds.push(at, {
|
|
198
|
+
x: at.x + rect.width,
|
|
199
|
+
y: at.y + rect.height
|
|
200
|
+
});
|
|
201
|
+
if (child.children?.length) place(child, at);
|
|
202
|
+
else leaves.push(rect);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
place(root, {
|
|
206
|
+
x: 0,
|
|
207
|
+
y: 0
|
|
208
|
+
});
|
|
209
|
+
const routes = [];
|
|
210
|
+
let defects = 0, bends = 0, length = 0;
|
|
211
|
+
const collect = (parent) => {
|
|
212
|
+
for (const edge of parent.edges ?? []) {
|
|
213
|
+
const container = String(edge.container ?? parent.id);
|
|
214
|
+
const offset = frames.get(container) ?? {
|
|
215
|
+
x: 0,
|
|
216
|
+
y: 0
|
|
217
|
+
};
|
|
218
|
+
const ends = [edge.sources?.[0], edge.targets?.[0]].flatMap((end) => {
|
|
219
|
+
const node = owner.get(String(end));
|
|
220
|
+
return node === void 0 ? [] : [node];
|
|
221
|
+
});
|
|
222
|
+
const segments = [];
|
|
223
|
+
for (const section of edge.sections ?? []) {
|
|
224
|
+
const points = [
|
|
225
|
+
section.startPoint,
|
|
226
|
+
...section.bendPoints ?? [],
|
|
227
|
+
section.endPoint
|
|
228
|
+
].map((point) => ({
|
|
229
|
+
x: point.x + offset.x,
|
|
230
|
+
y: point.y + offset.y
|
|
231
|
+
}));
|
|
232
|
+
bounds.push(...points);
|
|
233
|
+
let direction;
|
|
234
|
+
for (let index = 0; index + 1 < points.length; index++) {
|
|
235
|
+
const segment = {
|
|
236
|
+
a: points[index],
|
|
237
|
+
b: points[index + 1]
|
|
238
|
+
};
|
|
239
|
+
const size = Math.hypot(segment.b.x - segment.a.x, segment.b.y - segment.a.y);
|
|
240
|
+
if (size < EPS) continue;
|
|
241
|
+
if (!horizontal(segment) && !vertical(segment)) defects++;
|
|
242
|
+
const next = {
|
|
243
|
+
x: Math.sign(segment.b.x - segment.a.x),
|
|
244
|
+
y: Math.sign(segment.b.y - segment.a.y)
|
|
245
|
+
};
|
|
246
|
+
if (direction && (direction.x !== next.x || direction.y !== next.y)) bends++;
|
|
247
|
+
direction = next;
|
|
248
|
+
length += size;
|
|
249
|
+
segments.push(segment);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const labels = (edge.labels ?? []).flatMap((label) => label.width && label.height ? [{
|
|
253
|
+
x: (label.x ?? 0) + offset.x,
|
|
254
|
+
y: (label.y ?? 0) + offset.y,
|
|
255
|
+
width: label.width,
|
|
256
|
+
height: label.height
|
|
257
|
+
}] : []);
|
|
258
|
+
for (const label of labels) bounds.push(label, {
|
|
259
|
+
x: label.x + label.width,
|
|
260
|
+
y: label.y + label.height
|
|
261
|
+
});
|
|
262
|
+
for (const segment of segments) for (const rect of leaves) if (horizontal(segment) && strictlyInside(segment.a.y, rect.y, rect.y + rect.height) && overlapLength(span(segment, "x"), [rect.x, rect.x + rect.width]) > EPS || vertical(segment) && strictlyInside(segment.a.x, rect.x, rect.x + rect.width) && overlapLength(span(segment, "y"), [rect.y, rect.y + rect.height]) > EPS) defects++;
|
|
263
|
+
for (const [index, first] of segments.entries()) for (const second of segments.slice(index + 1)) if (sharedLength(first, second) > EPS) defects++;
|
|
264
|
+
routes.push({
|
|
265
|
+
ends,
|
|
266
|
+
segments,
|
|
267
|
+
visible: labels.length && !defectsOnly ? segments.flatMap((s) => outsideAll(s, labels)) : segments,
|
|
268
|
+
labels
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
for (const child of parent.children ?? []) collect(child);
|
|
272
|
+
};
|
|
273
|
+
collect(root);
|
|
274
|
+
const allLabels = routes.flatMap((route) => route.labels);
|
|
275
|
+
const covers = (a, b) => a.x < b.x + b.width - EPS && a.x + a.width > b.x + EPS && a.y < b.y + b.height - EPS && a.y + a.height > b.y + EPS;
|
|
276
|
+
for (const [index, label] of allLabels.entries()) {
|
|
277
|
+
for (const rect of leaves) if (covers(label, rect)) defects++;
|
|
278
|
+
for (const other of allLabels.slice(index + 1)) if (covers(label, other)) defects++;
|
|
279
|
+
}
|
|
280
|
+
for (const route of routes) for (const other of routes) if (other !== route) {
|
|
281
|
+
for (const label of other.labels) if (route.segments.some((segment) => horizontal(segment) && strictlyInside(segment.a.y, label.y, label.y + label.height) && overlapLength(span(segment, "x"), [label.x, label.x + label.width]) > EPS || vertical(segment) && strictlyInside(segment.a.x, label.x, label.x + label.width) && overlapLength(span(segment, "y"), [label.y, label.y + label.height]) > EPS)) defects++;
|
|
282
|
+
}
|
|
283
|
+
let crossings = 0, overlap = 0;
|
|
284
|
+
if (!defectsOnly) for (const [index, first] of routes.entries()) for (const second of routes.slice(index + 1)) {
|
|
285
|
+
const shared = first.ends.filter((id) => second.ends.includes(id)).flatMap((id) => {
|
|
286
|
+
const rect = rects.get(id);
|
|
287
|
+
return rect ? [rect] : [];
|
|
288
|
+
});
|
|
289
|
+
const clear = (point) => shared.every((rect) => !inRect(point, rect, TERMINAL_MARGIN));
|
|
290
|
+
let points;
|
|
291
|
+
const cross$1 = (s, t) => {
|
|
292
|
+
const h = horizontal(s) && vertical(t) ? s : horizontal(t) && vertical(s) ? t : void 0;
|
|
293
|
+
if (!h) return;
|
|
294
|
+
const v = h === s ? t : s;
|
|
295
|
+
const point = {
|
|
296
|
+
x: v.a.x,
|
|
297
|
+
y: h.a.y
|
|
298
|
+
};
|
|
299
|
+
if (strictlyInside(point.x, ...span(h, "x")) && strictlyInside(point.y, ...span(v, "y")) && clear(point)) (points ??= /* @__PURE__ */ new Set()).add(`${Math.round(point.x / EPS)}:${Math.round(point.y / EPS)}`);
|
|
300
|
+
};
|
|
301
|
+
const unlabeled = first.visible === first.segments && second.visible === second.segments;
|
|
302
|
+
for (const s of first.segments) for (const t of second.segments) {
|
|
303
|
+
if (unlabeled) cross$1(s, t);
|
|
304
|
+
const shared$1 = sharedLength(s, t);
|
|
305
|
+
if (shared$1 > EPS && clear(s.a) && clear(s.b)) overlap += shared$1;
|
|
306
|
+
}
|
|
307
|
+
if (!unlabeled) for (const s of first.visible) for (const t of second.visible) cross$1(s, t);
|
|
308
|
+
crossings += points?.size ?? 0;
|
|
309
|
+
}
|
|
310
|
+
const xs = bounds.map((point) => point.x).filter(Number.isFinite), ys = bounds.map((point) => point.y).filter(Number.isFinite);
|
|
311
|
+
const area = xs.length ? (Math.max(...xs) - Math.min(...xs)) * (Math.max(...ys) - Math.min(...ys)) : 0;
|
|
312
|
+
return {
|
|
313
|
+
defects,
|
|
314
|
+
crossings,
|
|
315
|
+
overlap,
|
|
316
|
+
bends,
|
|
317
|
+
length,
|
|
318
|
+
area
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** The parts of an axis-parallel segment outside the open interiors of `rects`. */
|
|
322
|
+
function outsideAll(segment, rects) {
|
|
323
|
+
let parts = [segment];
|
|
324
|
+
for (const rect of rects) parts = parts.flatMap((part) => {
|
|
325
|
+
const axis = horizontal(part) ? "x" : vertical(part) ? "y" : void 0;
|
|
326
|
+
if (!axis) return [part];
|
|
327
|
+
const cross$1 = axis === "x" ? "y" : "x";
|
|
328
|
+
const [low, high] = axis === "x" ? [rect.x, rect.x + rect.width] : [rect.y, rect.y + rect.height];
|
|
329
|
+
const [crossLow, crossHigh] = cross$1 === "x" ? [rect.x, rect.x + rect.width] : [rect.y, rect.y + rect.height];
|
|
330
|
+
if (!strictlyInside(part.a[cross$1], crossLow, crossHigh)) return [part];
|
|
331
|
+
const [from, to] = span(part, axis);
|
|
332
|
+
if (overlapLength([from, to], [low, high]) <= EPS) return [part];
|
|
333
|
+
const at = (value) => ({
|
|
334
|
+
...part.a,
|
|
335
|
+
[axis]: value
|
|
336
|
+
});
|
|
337
|
+
return [[from, Math.max(from, low)], [Math.min(to, high), to]].filter(([a, b]) => b - a > EPS).map(([a, b]) => ({
|
|
338
|
+
a: at(a),
|
|
339
|
+
b: at(b)
|
|
340
|
+
}));
|
|
341
|
+
});
|
|
342
|
+
return parts;
|
|
343
|
+
}
|
|
344
|
+
function sharedLength(s, t) {
|
|
345
|
+
if (horizontal(s) && horizontal(t) && Math.abs(s.a.y - t.a.y) < EPS) return overlapLength(span(s, "x"), span(t, "x"));
|
|
346
|
+
if (vertical(s) && vertical(t) && Math.abs(s.a.x - t.a.x) < EPS) return overlapLength(span(s, "y"), span(t, "y"));
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
const TOLERANCE = .02;
|
|
350
|
+
/** True when `candidate` is better at the first measure where the two layouts differ. */
|
|
351
|
+
function isBetterLayout(candidate, current) {
|
|
352
|
+
for (const key of [
|
|
353
|
+
"defects",
|
|
354
|
+
"crossings",
|
|
355
|
+
"overlap",
|
|
356
|
+
"bends",
|
|
357
|
+
"length",
|
|
358
|
+
"area"
|
|
359
|
+
]) {
|
|
360
|
+
const limit = key === "overlap" || key === "length" || key === "area" ? Math.max(TOLERANCE * Math.abs(current[key]), EPS) : 0;
|
|
361
|
+
if (Math.abs(candidate[key] - current[key]) > limit) return candidate[key] < current[key];
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/elkjs/orthogonal-junctions.ts
|
|
368
|
+
/** Reconstruct native segment spans from final orthogonal tracks, then emit ELK's unique junctions. */
|
|
369
|
+
function applyOrthogonalJunctions(root, horizontal$1, increasing, mergeEdges, hypernodes, preservedEdges) {
|
|
370
|
+
const edges = root.edges ?? [], ports = [], byPort = /* @__PURE__ */ new Map();
|
|
371
|
+
const explicit = new Map((root.children ?? []).flatMap((n) => (n.ports ?? []).map((p) => [String(p.id), n])));
|
|
372
|
+
const owner = new Map((root.children ?? []).map((node) => [String(node.id), node]));
|
|
373
|
+
for (const [id, node] of explicit) owner.set(id, node);
|
|
374
|
+
const records = edges.flatMap((edge, index) => {
|
|
375
|
+
if (preservedEdges.has(String(edge.id))) return [];
|
|
376
|
+
const sourceOwner = owner.get(String(edge.sources?.[0] ?? edge.source));
|
|
377
|
+
if (sourceOwner !== void 0 && sourceOwner === owner.get(String(edge.targets?.[0] ?? edge.target))) return [];
|
|
378
|
+
const section = edge.sections?.[0];
|
|
379
|
+
if (!section || edge.sections?.length !== 1) return [];
|
|
380
|
+
let points = [
|
|
381
|
+
section.startPoint,
|
|
382
|
+
...section.bendPoints ?? [],
|
|
383
|
+
section.endPoint
|
|
384
|
+
];
|
|
385
|
+
if (points.some((p, i) => i > 0 && Math.abs(p.x - points[i - 1].x) > .001 && Math.abs(p.y - points[i - 1].y) > .001)) return [];
|
|
386
|
+
let source = String(edge.sources?.[0] ?? edge.source), target = String(edge.targets?.[0] ?? edge.target);
|
|
387
|
+
const flow$1 = (p) => horizontal$1 ? p.x : p.y;
|
|
388
|
+
if (flow$1(points[0]) > flow$1(points.at(-1)) === increasing) {
|
|
389
|
+
points = [...points].reverse();
|
|
390
|
+
[source, target] = [target, source];
|
|
391
|
+
}
|
|
392
|
+
const key = (ref, side) => explicit.has(ref) ? JSON.stringify(["port", ref]) : mergeEdges || hypernodes.has(ref) ? JSON.stringify([
|
|
393
|
+
"implicit",
|
|
394
|
+
ref,
|
|
395
|
+
side
|
|
396
|
+
]) : JSON.stringify([
|
|
397
|
+
"edge",
|
|
398
|
+
index,
|
|
399
|
+
side
|
|
400
|
+
]);
|
|
401
|
+
return [{
|
|
402
|
+
edge,
|
|
403
|
+
points,
|
|
404
|
+
source: key(source, "source"),
|
|
405
|
+
target: key(target, "target")
|
|
406
|
+
}];
|
|
407
|
+
});
|
|
408
|
+
const cross$1 = (p) => horizontal$1 ? p.y : p.x, flow = (p) => horizontal$1 ? p.x : p.y;
|
|
409
|
+
for (const side of ["source", "target"]) {
|
|
410
|
+
const ordered = [...records].sort((a, b) => cross$1(side === "source" ? a.points[0] : a.points.at(-1)) - cross$1(side === "source" ? b.points[0] : b.points.at(-1)));
|
|
411
|
+
for (const record of ordered) {
|
|
412
|
+
const id = record[side];
|
|
413
|
+
if (!byPort.has(id)) {
|
|
414
|
+
const port = {
|
|
415
|
+
id,
|
|
416
|
+
side,
|
|
417
|
+
position: cross$1(side === "source" ? record.points[0] : record.points.at(-1))
|
|
418
|
+
};
|
|
419
|
+
byPort.set(id, port);
|
|
420
|
+
ports.push(port);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const grouped = createOrthogonalHypersegments(ports, records.map((r) => ({
|
|
425
|
+
source: r.source,
|
|
426
|
+
target: r.target
|
|
427
|
+
})));
|
|
428
|
+
const created = /* @__PURE__ */ new Set();
|
|
429
|
+
for (const group of grouped.segments) {
|
|
430
|
+
const members = records.filter((r) => group.ports.includes(r.source));
|
|
431
|
+
if (members.length === 1) {
|
|
432
|
+
delete members[0].edge.junctionPoints;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
436
|
+
const track = (point) => {
|
|
437
|
+
const key = flow(point);
|
|
438
|
+
let t = tracks.get(key);
|
|
439
|
+
if (!t) {
|
|
440
|
+
t = {
|
|
441
|
+
start: Infinity,
|
|
442
|
+
end: -Infinity,
|
|
443
|
+
incoming: [],
|
|
444
|
+
outgoing: []
|
|
445
|
+
};
|
|
446
|
+
tracks.set(key, t);
|
|
447
|
+
}
|
|
448
|
+
t.start = Math.min(t.start, cross$1(point));
|
|
449
|
+
t.end = Math.max(t.end, cross$1(point));
|
|
450
|
+
return t;
|
|
451
|
+
};
|
|
452
|
+
for (const r of members) {
|
|
453
|
+
const bends = r.points.slice(1, -1);
|
|
454
|
+
if (!bends.length) continue;
|
|
455
|
+
for (const p of bends) track(p);
|
|
456
|
+
track(bends[0]).incoming.push(cross$1(bends[0]));
|
|
457
|
+
track(bends.at(-1)).outgoing.push(cross$1(bends.at(-1)));
|
|
458
|
+
for (let i = 1; i < bends.length; i++) if (Math.abs(flow(bends[i]) - flow(bends[i - 1])) > .001) {
|
|
459
|
+
track(bends[i - 1]).outgoing.push(cross$1(bends[i - 1]));
|
|
460
|
+
track(bends[i]).incoming.push(cross$1(bends[i]));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const r of members.filter((r$1) => r$1.points.length === 2)) {
|
|
464
|
+
const start = Math.min(flow(r.points[0]), flow(r.points[1]));
|
|
465
|
+
const end = Math.max(flow(r.points[0]), flow(r.points[1]));
|
|
466
|
+
for (const [position, t] of tracks) {
|
|
467
|
+
if (position < start || position > end) continue;
|
|
468
|
+
t.incoming.push(cross$1(r.points[0]));
|
|
469
|
+
t.outgoing.push(cross$1(r.points[1]));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const r of members) for (let index = 1; index < r.points.length; index++) {
|
|
473
|
+
const a = r.points[index - 1], b = r.points[index];
|
|
474
|
+
if (Math.abs(cross$1(a) - cross$1(b)) >= .001) continue;
|
|
475
|
+
const start = Math.min(flow(a), flow(b)), end = Math.max(flow(a), flow(b));
|
|
476
|
+
for (const [position, t] of tracks) {
|
|
477
|
+
if (position <= start || position >= end) continue;
|
|
478
|
+
t.incoming.push(cross$1(a));
|
|
479
|
+
t.outgoing.push(cross$1(b));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const t of tracks.values()) {
|
|
483
|
+
t.start = Math.min(t.start, ...t.incoming, ...t.outgoing);
|
|
484
|
+
t.end = Math.max(t.end, ...t.incoming, ...t.outgoing);
|
|
485
|
+
}
|
|
486
|
+
for (const port of group.ports) for (const r of members.filter((r$1) => r$1.source === port)) {
|
|
487
|
+
delete r.edge.junctionPoints;
|
|
488
|
+
for (const p of r.points.slice(1, -1)) {
|
|
489
|
+
const t = tracks.get(flow(p)), value = cross$1(p), key = `${p.x}:${p.y}`;
|
|
490
|
+
const boundary = t.incoming.length && t.outgoing.length && (Math.abs(value - Math.min(...t.incoming)) < .001 && Math.abs(value - Math.min(...t.outgoing)) < .001 || Math.abs(value - Math.max(...t.incoming)) < .001 && Math.abs(value - Math.max(...t.outgoing)) < .001);
|
|
491
|
+
if (!created.has(key) && (value > t.start && value < t.end || boundary)) {
|
|
492
|
+
(r.edge.junctionPoints ??= []).push({ ...p });
|
|
493
|
+
created.add(key);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/elkjs/opposing-tracks.ts
|
|
502
|
+
const option$1 = (options, suffix) => Object.entries(options ?? {}).find(([key]) => key === suffix || key.endsWith(`.${suffix}`))?.[1];
|
|
503
|
+
/**
|
|
504
|
+
* Separate opposite-direction track sharing in a finished ELK JSON layout,
|
|
505
|
+
* across nested containers, in world coordinates.
|
|
506
|
+
*/
|
|
507
|
+
function separateElkOpposingTracks(root, options) {
|
|
508
|
+
const orthogonal = (node, inherited) => {
|
|
509
|
+
const value = option$1(node.layoutOptions, "edgeRouting");
|
|
510
|
+
return value === void 0 ? inherited : String(value) === "ORTHOGONAL";
|
|
511
|
+
};
|
|
512
|
+
const rootOrthogonal = orthogonal(root, String(option$1(options, "edgeRouting") ?? "ORTHOGONAL") === "ORTHOGONAL");
|
|
513
|
+
const frames = new Map([[String(root.id), {
|
|
514
|
+
x: 0,
|
|
515
|
+
y: 0
|
|
516
|
+
}]]);
|
|
517
|
+
const routing = new Map([[String(root.id), rootOrthogonal]]);
|
|
518
|
+
const nodes = /* @__PURE__ */ new Map(), leaves = /* @__PURE__ */ new Set(), owner = /* @__PURE__ */ new Map(), ports = /* @__PURE__ */ new Map();
|
|
519
|
+
const place = (parent, offset) => {
|
|
520
|
+
for (const child of parent.children ?? []) {
|
|
521
|
+
const id = String(child.id);
|
|
522
|
+
const at = {
|
|
523
|
+
x: offset.x + (child.x ?? 0),
|
|
524
|
+
y: offset.y + (child.y ?? 0)
|
|
525
|
+
};
|
|
526
|
+
frames.set(id, at);
|
|
527
|
+
routing.set(id, orthogonal(child, routing.get(String(parent.id)) ?? rootOrthogonal));
|
|
528
|
+
nodes.set(id, {
|
|
529
|
+
...at,
|
|
530
|
+
width: child.width ?? 0,
|
|
531
|
+
height: child.height ?? 0
|
|
532
|
+
});
|
|
533
|
+
owner.set(id, id);
|
|
534
|
+
for (const port of child.ports ?? []) {
|
|
535
|
+
owner.set(String(port.id), id);
|
|
536
|
+
ports.set(String(port.id), {
|
|
537
|
+
node: id,
|
|
538
|
+
rect: {
|
|
539
|
+
x: at.x + (port.x ?? 0),
|
|
540
|
+
y: at.y + (port.y ?? 0),
|
|
541
|
+
width: port.width ?? 0,
|
|
542
|
+
height: port.height ?? 0
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (child.children?.length) place(child, at);
|
|
547
|
+
else leaves.add(id);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
place(root, {
|
|
551
|
+
x: 0,
|
|
552
|
+
y: 0
|
|
553
|
+
});
|
|
554
|
+
const records = [];
|
|
555
|
+
const collect = (parent) => {
|
|
556
|
+
for (const edge of parent.edges ?? []) {
|
|
557
|
+
if ((edge.sections?.length ?? 0) > 1) continue;
|
|
558
|
+
const container = String(edge.container ?? parent.id);
|
|
559
|
+
const offset = frames.get(container) ?? {
|
|
560
|
+
x: 0,
|
|
561
|
+
y: 0
|
|
562
|
+
};
|
|
563
|
+
const points = (edge.sections ?? []).flatMap((section) => [
|
|
564
|
+
section.startPoint,
|
|
565
|
+
...section.bendPoints ?? [],
|
|
566
|
+
section.endPoint
|
|
567
|
+
].map((p) => ({
|
|
568
|
+
x: p.x + offset.x,
|
|
569
|
+
y: p.y + offset.y
|
|
570
|
+
})));
|
|
571
|
+
const axisParallel = points.every((p, i) => i === 0 || Math.abs(p.x - points[i - 1].x) < 1e-6 || Math.abs(p.y - points[i - 1].y) < 1e-6);
|
|
572
|
+
const ends = [edge.sources?.[0] ?? edge.source, edge.targets?.[0] ?? edge.target].flatMap((end) => {
|
|
573
|
+
const id = owner.get(String(end));
|
|
574
|
+
return id === void 0 ? [] : [id];
|
|
575
|
+
});
|
|
576
|
+
const portOf = (end) => ports.has(String(end)) ? String(end) : void 0;
|
|
577
|
+
records.push({
|
|
578
|
+
edge,
|
|
579
|
+
offset,
|
|
580
|
+
route: {
|
|
581
|
+
id: String(edge.id),
|
|
582
|
+
ends,
|
|
583
|
+
points,
|
|
584
|
+
ports: [portOf(edge.sources?.[0] ?? edge.source), portOf(edge.targets?.[0] ?? edge.target)],
|
|
585
|
+
labels: (edge.labels ?? []).flatMap((label) => label.width && label.height ? [{
|
|
586
|
+
x: (label.x ?? 0) + offset.x,
|
|
587
|
+
y: (label.y ?? 0) + offset.y,
|
|
588
|
+
width: label.width,
|
|
589
|
+
height: label.height
|
|
590
|
+
}] : []),
|
|
591
|
+
movable: points.length > 0 && axisParallel && (routing.get(container) ?? rootOrthogonal)
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
for (const child of parent.children ?? []) collect(child);
|
|
596
|
+
};
|
|
597
|
+
collect(root);
|
|
598
|
+
if (!records.some((record) => record.route.movable)) return;
|
|
599
|
+
const spacing = Number(option$1(options, "spacing.edgeEdge") ?? 10);
|
|
600
|
+
const changed = separateOpposingTracks({
|
|
601
|
+
routes: records.map((record) => record.route),
|
|
602
|
+
nodes,
|
|
603
|
+
leaves,
|
|
604
|
+
ports,
|
|
605
|
+
spacing: Number.isFinite(spacing) && spacing > 0 ? spacing : 10
|
|
606
|
+
});
|
|
607
|
+
if (!changed.size) return;
|
|
608
|
+
const routes = new Map(records.map((record) => [record.route.id, changed.get(record.route.id) ?? record.route.points]));
|
|
609
|
+
const on = (point, points) => points.some((b, i) => {
|
|
610
|
+
const a = points[i - 1];
|
|
611
|
+
return a !== void 0 && point.x >= Math.min(a.x, b.x) - 1e-6 && point.x <= Math.max(a.x, b.x) + 1e-6 && point.y >= Math.min(a.y, b.y) - 1e-6 && point.y <= Math.max(a.y, b.y) + 1e-6;
|
|
612
|
+
});
|
|
613
|
+
for (const { edge, offset, route } of records) {
|
|
614
|
+
const points = changed.get(route.id);
|
|
615
|
+
if (points) {
|
|
616
|
+
const section = edge.sections[0];
|
|
617
|
+
const local = points.map((p) => ({
|
|
618
|
+
x: p.x - offset.x,
|
|
619
|
+
y: p.y - offset.y
|
|
620
|
+
}));
|
|
621
|
+
section.startPoint = local[0];
|
|
622
|
+
section.endPoint = local.at(-1);
|
|
623
|
+
section.bendPoints = local.slice(1, -1);
|
|
624
|
+
}
|
|
625
|
+
if (!edge.junctionPoints?.length) continue;
|
|
626
|
+
const own = routes.get(route.id);
|
|
627
|
+
edge.junctionPoints = edge.junctionPoints.filter((junction) => {
|
|
628
|
+
const point = {
|
|
629
|
+
x: junction.x + offset.x,
|
|
630
|
+
y: junction.y + offset.y
|
|
631
|
+
};
|
|
632
|
+
return on(point, own) && [...routes].some(([id, other]) => id !== route.id && on(point, other));
|
|
633
|
+
});
|
|
634
|
+
if (!edge.junctionPoints.length) delete edge.junctionPoints;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region src/elkjs/statechart.ts
|
|
640
|
+
function option(node, suffix) {
|
|
641
|
+
const settings = {
|
|
642
|
+
...node.properties,
|
|
643
|
+
...node.layoutOptions
|
|
644
|
+
};
|
|
645
|
+
return settings[suffix] ?? settings[`elk.${suffix}`] ?? settings[`org.eclipse.elk.${suffix}`];
|
|
646
|
+
}
|
|
647
|
+
const endpoint$1 = (edge, source) => source ? edge.sources?.[0] ?? edge.source : edge.targets?.[0] ?? edge.target;
|
|
648
|
+
/** Compile semantic hints into settings implemented by this package's ELK adapter. */
|
|
649
|
+
function compileStatechartLayout(input, scopes) {
|
|
650
|
+
const graph = structuredClone(input);
|
|
651
|
+
const paths = Object.create(null);
|
|
652
|
+
const commonExits = Object.create(null);
|
|
653
|
+
const containers = /* @__PURE__ */ new Map();
|
|
654
|
+
const edges = [];
|
|
655
|
+
const collect = (node) => {
|
|
656
|
+
if (node.id === void 0 || containers.has(String(node.id))) throw new Error("Statechart nodes require unique IDs");
|
|
657
|
+
containers.set(String(node.id), node);
|
|
658
|
+
edges.push(...node.edges ?? []);
|
|
659
|
+
node.children?.forEach(collect);
|
|
660
|
+
};
|
|
661
|
+
collect(graph);
|
|
662
|
+
for (const [id, scope] of Object.entries(scopes)) {
|
|
663
|
+
const container = containers.get(id);
|
|
664
|
+
if (!container) throw new Error(`Unknown statechart container: ${id}`);
|
|
665
|
+
const children = container.children ?? [];
|
|
666
|
+
const ids = new Set(children.map((node) => node.id));
|
|
667
|
+
const owner = /* @__PURE__ */ new Map();
|
|
668
|
+
const own = (node, childId) => {
|
|
669
|
+
owner.set(node.id, childId);
|
|
670
|
+
node.ports?.forEach((port) => owner.set(port.id, childId));
|
|
671
|
+
node.children?.forEach((descendant) => own(descendant, childId));
|
|
672
|
+
};
|
|
673
|
+
children.forEach((child) => own(child, child.id));
|
|
674
|
+
const localEdges = edges.filter((edge) => owner.has(endpoint$1(edge, true)) && owner.has(endpoint$1(edge, false)));
|
|
675
|
+
const successors = new Map(children.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
676
|
+
const predecessors = new Map(children.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
677
|
+
for (const edge of localEdges) {
|
|
678
|
+
const source = owner.get(endpoint$1(edge, true));
|
|
679
|
+
const target = owner.get(endpoint$1(edge, false));
|
|
680
|
+
if (source === target) continue;
|
|
681
|
+
successors.get(source).add(target);
|
|
682
|
+
predecessors.get(target).add(source);
|
|
683
|
+
}
|
|
684
|
+
const exits = children.filter((node) => successors.get(node.id).size === 0 && predecessors.get(node.id).size >= 2).map((node) => node.id);
|
|
685
|
+
commonExits[id] = exits;
|
|
686
|
+
const path = [...scope.preferredPath ?? (scope.initialNodeId === void 0 ? [] : [scope.initialNodeId])];
|
|
687
|
+
if (new Set(path).size !== path.length || path.some((nodeId) => !ids.has(nodeId))) throw new Error(`Path must contain distinct direct children of ${id}`);
|
|
688
|
+
if (scope.initialNodeId !== void 0 && (!ids.has(scope.initialNodeId) || path[0] !== scope.initialNodeId)) throw new Error(`Path must start at initial child of ${id}`);
|
|
689
|
+
if (scope.preferredPath) {
|
|
690
|
+
for (let i = 1; i < path.length; i++) if (!successors.get(path[i - 1])?.has(path[i])) throw new Error(`Disconnected preferred path in ${id}`);
|
|
691
|
+
} else while (path.length) {
|
|
692
|
+
const next = [...successors.get(path.at(-1))].filter((nodeId) => !path.includes(nodeId) && !exits.includes(nodeId));
|
|
693
|
+
if (next.length !== 1) break;
|
|
694
|
+
path.push(next[0]);
|
|
695
|
+
}
|
|
696
|
+
paths[id] = path;
|
|
697
|
+
container.children = [
|
|
698
|
+
...path,
|
|
699
|
+
...children.map((node) => node.id).filter((nodeId) => !path.includes(nodeId) && !exits.includes(nodeId)),
|
|
700
|
+
...exits.filter((nodeId) => !path.includes(nodeId))
|
|
701
|
+
].map((nodeId) => children.find((node) => node.id === nodeId));
|
|
702
|
+
container.layoutOptions = {
|
|
703
|
+
...path.length > 1 && option(container, "layered.cycleBreaking.strategy") === void 0 ? { "elk.layered.cycleBreaking.strategy": "MODEL_ORDER" } : {},
|
|
704
|
+
...path.length > 1 && option(container, "layered.feedbackEdges") === void 0 ? { "elk.layered.feedbackEdges": true } : {},
|
|
705
|
+
...container.layoutOptions,
|
|
706
|
+
...scope.direction ? { direction: scope.direction } : {}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
graph,
|
|
711
|
+
paths,
|
|
712
|
+
commonExits
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
716
|
+
const cross = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
717
|
+
/** Diagnostic objective, not a proof of geometric validity. Coordinates are container-relative. */
|
|
718
|
+
function scoreStatechartLayout(graph, paths = {}) {
|
|
719
|
+
const score = {
|
|
720
|
+
invalid: 0,
|
|
721
|
+
overlaps: 0,
|
|
722
|
+
pathOrder: 0,
|
|
723
|
+
crossings: 0,
|
|
724
|
+
bends: 0,
|
|
725
|
+
routeLength: 0
|
|
726
|
+
};
|
|
727
|
+
const segments = [];
|
|
728
|
+
const boxes = [];
|
|
729
|
+
const rect = (shape, x, y) => {
|
|
730
|
+
if ([
|
|
731
|
+
shape.x,
|
|
732
|
+
shape.y,
|
|
733
|
+
shape.width,
|
|
734
|
+
shape.height
|
|
735
|
+
].some((value) => value === void 0 || !Number.isFinite(value)) || (shape.width ?? 0) < 0 || (shape.height ?? 0) < 0) score.invalid++;
|
|
736
|
+
return {
|
|
737
|
+
x: x + (shape.x ?? 0),
|
|
738
|
+
y: y + (shape.y ?? 0),
|
|
739
|
+
width: shape.width ?? 0,
|
|
740
|
+
height: shape.height ?? 0
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
const visit = (node, x, y, ancestors, inheritedDirection = "RIGHT") => {
|
|
744
|
+
const direction = String(option(node, "direction") ?? inheritedDirection).toUpperCase();
|
|
745
|
+
const horizontal$1 = direction === "RIGHT" || direction === "LEFT";
|
|
746
|
+
const sign = direction === "LEFT" || direction === "UP" ? -1 : 1;
|
|
747
|
+
const path = paths[String(node.id)] ?? [];
|
|
748
|
+
for (let i = 1; i < path.length; i++) {
|
|
749
|
+
const a = node.children?.find((child) => child.id === path[i - 1]);
|
|
750
|
+
const b = node.children?.find((child) => child.id === path[i]);
|
|
751
|
+
if (!a || !b || sign * (horizontal$1 ? b.x - a.x : b.y - a.y) <= 0) score.pathOrder++;
|
|
752
|
+
}
|
|
753
|
+
for (const child of node.children ?? []) {
|
|
754
|
+
if (String(option(child, "noLayout")).toLowerCase() === "true") continue;
|
|
755
|
+
const box = rect(child, x, y);
|
|
756
|
+
if ((child.x ?? 0) < 0 || (child.y ?? 0) < 0 || (child.x ?? 0) + (child.width ?? 0) > (node.width ?? Infinity) + .001 || (child.y ?? 0) + (child.height ?? 0) > (node.height ?? Infinity) + .001) score.invalid++;
|
|
757
|
+
boxes.push({
|
|
758
|
+
rect: box,
|
|
759
|
+
ancestors,
|
|
760
|
+
node: child
|
|
761
|
+
});
|
|
762
|
+
visit(child, box.x, box.y, [...ancestors, child], direction);
|
|
763
|
+
}
|
|
764
|
+
for (const edge of node.edges ?? []) {
|
|
765
|
+
if (String(option(edge, "noLayout")).toLowerCase() === "true") continue;
|
|
766
|
+
if (!edge.sections?.length) score.invalid++;
|
|
767
|
+
for (const label of edge.labels ?? []) {
|
|
768
|
+
if (String(option(label, "noLayout")).toLowerCase() === "true") continue;
|
|
769
|
+
boxes.push({
|
|
770
|
+
rect: rect(label, x, y),
|
|
771
|
+
ancestors
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
for (const section of edge.sections ?? []) {
|
|
775
|
+
const points = [
|
|
776
|
+
section.startPoint,
|
|
777
|
+
...section.bendPoints ?? [],
|
|
778
|
+
section.endPoint
|
|
779
|
+
];
|
|
780
|
+
score.bends += section.bendPoints?.length ?? 0;
|
|
781
|
+
for (const point of points) if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) score.invalid++;
|
|
782
|
+
for (let i = 1; i < points.length; i++) {
|
|
783
|
+
const a = {
|
|
784
|
+
x: x + points[i - 1].x,
|
|
785
|
+
y: y + points[i - 1].y
|
|
786
|
+
};
|
|
787
|
+
const b = {
|
|
788
|
+
x: x + points[i].x,
|
|
789
|
+
y: y + points[i].y
|
|
790
|
+
};
|
|
791
|
+
score.routeLength += Math.hypot(b.x - a.x, b.y - a.y);
|
|
792
|
+
segments.push({
|
|
793
|
+
a,
|
|
794
|
+
b,
|
|
795
|
+
edge
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
visit(graph, 0, 0, [graph]);
|
|
802
|
+
for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
|
|
803
|
+
const a = boxes[i], b = boxes[j];
|
|
804
|
+
if (a.node && b.ancestors.includes(a.node) || b.node && a.ancestors.includes(b.node)) continue;
|
|
805
|
+
if (intersects(a.rect, b.rect)) score.overlaps++;
|
|
806
|
+
}
|
|
807
|
+
for (let i = 0; i < segments.length; i++) for (let j = i + 1; j < segments.length; j++) {
|
|
808
|
+
const a = segments[i], b = segments[j];
|
|
809
|
+
if (a.edge === b.edge) continue;
|
|
810
|
+
if (cross(a.a, a.b, b.a) * cross(a.a, a.b, b.b) < 0 && cross(b.a, b.b, a.a) * cross(b.a, b.b, a.b) < 0) score.crossings++;
|
|
811
|
+
}
|
|
812
|
+
if (!Number.isFinite(score.routeLength)) score.routeLength = Number.MAX_VALUE;
|
|
813
|
+
return score;
|
|
814
|
+
}
|
|
815
|
+
const objectives = [
|
|
816
|
+
"invalid",
|
|
817
|
+
"overlaps",
|
|
818
|
+
"pathOrder",
|
|
819
|
+
"crossings",
|
|
820
|
+
"bends",
|
|
821
|
+
"routeLength"
|
|
822
|
+
];
|
|
823
|
+
function better(a, b) {
|
|
824
|
+
for (const key of objectives) if (a[key] !== b[key]) return a[key] < b[key];
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
/** Each attempt starts from original input. No post-layout geometry edits. */
|
|
828
|
+
async function layoutStatechart(input, options) {
|
|
829
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
830
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 3) throw new RangeError("maxAttempts must be an integer from 1 to 3");
|
|
831
|
+
const plan = compileStatechartLayout(input, options.scopes);
|
|
832
|
+
const attempts = [];
|
|
833
|
+
let selected;
|
|
834
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
835
|
+
const candidate = structuredClone(attempt === 0 ? input : plan.graph);
|
|
836
|
+
const configure = (node) => {
|
|
837
|
+
const scope = Object.hasOwn(options.scopes, String(node.id)) ? options.scopes[String(node.id)] : void 0;
|
|
838
|
+
if (scope?.direction) node.layoutOptions = {
|
|
839
|
+
...node.layoutOptions,
|
|
840
|
+
direction: scope.direction
|
|
841
|
+
};
|
|
842
|
+
if (attempt === 2 && scope) node.layoutOptions = {
|
|
843
|
+
...node.layoutOptions,
|
|
844
|
+
"spacing.nodeNode": Math.max(40, Number(option(node, "spacing.nodeNode") ?? 20) * 1.5),
|
|
845
|
+
"layered.spacing.nodeNodeBetweenLayers": Math.max(60, Number(option(node, "layered.spacing.nodeNodeBetweenLayers") ?? 20) * 1.5)
|
|
846
|
+
};
|
|
847
|
+
if (attempt === 2 && scope) {
|
|
848
|
+
const groups = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const edge of node.edges ?? []) {
|
|
850
|
+
if (!edge.labels?.length) continue;
|
|
851
|
+
const key = JSON.stringify([endpoint$1(edge, true), endpoint$1(edge, false)]);
|
|
852
|
+
const group = groups.get(key) ?? [];
|
|
853
|
+
group.push(edge);
|
|
854
|
+
groups.set(key, group);
|
|
855
|
+
}
|
|
856
|
+
for (const group of groups.values()) for (let i = 1; i < Math.min(group.length, 3); i++) {
|
|
857
|
+
const edge = group[i];
|
|
858
|
+
if (option(edge, "edgeLabels.placement") === void 0 && edge.labels.every((label) => option(label, "edgeLabels.placement") === void 0)) {
|
|
859
|
+
const placement = i === 1 ? "HEAD" : "TAIL";
|
|
860
|
+
edge.layoutOptions = {
|
|
861
|
+
...edge.layoutOptions,
|
|
862
|
+
"elk.edgeLabels.placement": placement
|
|
863
|
+
};
|
|
864
|
+
edge.labels = edge.labels.map((label) => ({
|
|
865
|
+
...label,
|
|
866
|
+
layoutOptions: {
|
|
867
|
+
...label.layoutOptions,
|
|
868
|
+
"elk.edgeLabels.placement": placement
|
|
869
|
+
}
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
node.children?.forEach(configure);
|
|
875
|
+
};
|
|
876
|
+
configure(candidate);
|
|
877
|
+
const graph = await new ELK().layout(candidate);
|
|
878
|
+
const score = scoreStatechartLayout(graph, plan.paths);
|
|
879
|
+
attempts.push({
|
|
880
|
+
kind: attempt === 0 ? "baseline" : attempt === 1 ? "policy" : "spacing",
|
|
881
|
+
score
|
|
882
|
+
});
|
|
883
|
+
if (!selected || better(score, selected.score)) selected = {
|
|
884
|
+
graph,
|
|
885
|
+
score,
|
|
886
|
+
attempt
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
...selected,
|
|
891
|
+
attempts,
|
|
892
|
+
paths: plan.paths,
|
|
893
|
+
commonExits: plan.commonExits
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/elkjs/routes.ts
|
|
899
|
+
/** Normalize any ELK layout's section geometry, preserving ports and hyperedge junctions. */
|
|
900
|
+
function getElkRoutes(graph, options = {}) {
|
|
901
|
+
const shapes = /* @__PURE__ */ new Map(), frames = /* @__PURE__ */ new Map();
|
|
902
|
+
const edges = [];
|
|
903
|
+
function visit(node, offset, inherited) {
|
|
904
|
+
const position = {
|
|
905
|
+
x: offset.x + (node.x ?? 0),
|
|
906
|
+
y: offset.y + (node.y ?? 0)
|
|
907
|
+
};
|
|
908
|
+
const routing = node.layoutOptions?.["elk.edgeRouting"] ?? node.layoutOptions?.["org.eclipse.elk.edgeRouting"] ?? inherited;
|
|
909
|
+
frames.set(node.id, position);
|
|
910
|
+
shapes.set(node.id, {
|
|
911
|
+
ref: {
|
|
912
|
+
kind: "node",
|
|
913
|
+
nodeId: node.id
|
|
914
|
+
},
|
|
915
|
+
center: {
|
|
916
|
+
x: position.x + (node.width ?? 0) / 2,
|
|
917
|
+
y: position.y + (node.height ?? 0) / 2
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
for (const port of node.ports ?? []) shapes.set(port.id, {
|
|
921
|
+
ref: {
|
|
922
|
+
kind: "node",
|
|
923
|
+
nodeId: node.id,
|
|
924
|
+
port: port.id
|
|
925
|
+
},
|
|
926
|
+
center: {
|
|
927
|
+
x: position.x + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
928
|
+
y: position.y + (port.y ?? 0) + (port.height ?? 0) / 2
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
for (const edge of node.edges ?? []) edges.push({
|
|
932
|
+
edge,
|
|
933
|
+
offset: position,
|
|
934
|
+
routing: edge.layoutOptions?.["elk.edgeRouting"] ?? edge.layoutOptions?.["org.eclipse.elk.edgeRouting"] ?? routing
|
|
935
|
+
});
|
|
936
|
+
for (const child of node.children ?? []) visit(child, position, routing);
|
|
937
|
+
}
|
|
938
|
+
visit(graph, {
|
|
939
|
+
x: 0,
|
|
940
|
+
y: 0
|
|
941
|
+
}, options.routing ?? "POLYLINE");
|
|
942
|
+
const result = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const { edge, offset: inheritedOffset, routing } of edges) {
|
|
944
|
+
const offset = edge.container ? frames.get(edge.container) ?? inheritedOffset : inheritedOffset;
|
|
945
|
+
const point = (p) => ({
|
|
946
|
+
x: p.x + offset.x,
|
|
947
|
+
y: p.y + offset.y
|
|
948
|
+
});
|
|
949
|
+
const parents = /* @__PURE__ */ new Map();
|
|
950
|
+
const root = (key) => {
|
|
951
|
+
let current = key;
|
|
952
|
+
while (parents.has(current)) current = parents.get(current);
|
|
953
|
+
return current;
|
|
954
|
+
};
|
|
955
|
+
const join = (a, b) => {
|
|
956
|
+
const x = root(a), y = root(b);
|
|
957
|
+
if (x !== y) parents.set(x < y ? y : x, x < y ? x : y);
|
|
958
|
+
};
|
|
959
|
+
for (const section of edge.sections ?? []) {
|
|
960
|
+
for (const id of section.outgoingSections ?? []) join(`${section.id}:end`, `${id}:start`);
|
|
961
|
+
for (const id of section.incomingSections ?? []) join(`${id}:end`, `${section.id}:start`);
|
|
962
|
+
}
|
|
963
|
+
const sections = [];
|
|
964
|
+
let malformed = false;
|
|
965
|
+
for (const section of edge.sections ?? []) {
|
|
966
|
+
const points = [
|
|
967
|
+
section.startPoint,
|
|
968
|
+
...section.bendPoints ?? [],
|
|
969
|
+
section.endPoint
|
|
970
|
+
].map(point);
|
|
971
|
+
let path;
|
|
972
|
+
if (points.some((p) => !Number.isFinite(p.x) || !Number.isFinite(p.y))) {
|
|
973
|
+
malformed = true;
|
|
974
|
+
for (let i = 0; i < points.length; i++) if (!Number.isFinite(points[i].x) || !Number.isFinite(points[i].y)) points[i] = {
|
|
975
|
+
x: offset.x + i * 20,
|
|
976
|
+
y: offset.y
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
if (routing === "SPLINES") if ((points.length - 1) % 3 || points.length < 4) {
|
|
980
|
+
malformed = true;
|
|
981
|
+
path = pathFromPoints([points[0], points.at(-1)]);
|
|
982
|
+
} else path = pathFromSplinePoints(points);
|
|
983
|
+
else path = pathFromPoints(points);
|
|
984
|
+
const fromShape = section.incomingShape ?? (!section.incomingSections?.length && edge.sources.length === 1 ? edge.sources[0] : void 0);
|
|
985
|
+
const toShape = section.outgoingShape ?? (!section.outgoingSections?.length && edge.targets.length === 1 ? edge.targets[0] : void 0);
|
|
986
|
+
sections.push({
|
|
987
|
+
id: section.id,
|
|
988
|
+
from: fromShape ? shapes.get(fromShape)?.ref ?? {
|
|
989
|
+
kind: "node",
|
|
990
|
+
nodeId: fromShape
|
|
991
|
+
} : {
|
|
992
|
+
kind: "junction",
|
|
993
|
+
id: `${edge.id}:${root(`${section.id}:start`)}`
|
|
994
|
+
},
|
|
995
|
+
to: toShape ? shapes.get(toShape)?.ref ?? {
|
|
996
|
+
kind: "node",
|
|
997
|
+
nodeId: toShape
|
|
998
|
+
} : {
|
|
999
|
+
kind: "junction",
|
|
1000
|
+
id: `${edge.id}:${root(`${section.id}:end`)}`
|
|
1001
|
+
},
|
|
1002
|
+
path
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (!sections.length) {
|
|
1006
|
+
malformed = true;
|
|
1007
|
+
const source = shapes.get(edge.sources[0] ?? ""), origin = source?.center ?? {
|
|
1008
|
+
x: 0,
|
|
1009
|
+
y: 0
|
|
1010
|
+
};
|
|
1011
|
+
const targets = [...edge.targets, ...edge.sources.slice(1)];
|
|
1012
|
+
for (const [i, id] of (targets.length ? targets : [""]).entries()) {
|
|
1013
|
+
const target = shapes.get(id);
|
|
1014
|
+
sections.push({
|
|
1015
|
+
id: `${edge.id}:fallback:${i}`,
|
|
1016
|
+
from: source?.ref ?? {
|
|
1017
|
+
kind: "node",
|
|
1018
|
+
nodeId: edge.sources[0] ?? ""
|
|
1019
|
+
},
|
|
1020
|
+
to: target?.ref ?? {
|
|
1021
|
+
kind: "node",
|
|
1022
|
+
nodeId: id
|
|
1023
|
+
},
|
|
1024
|
+
path: pathFromPoints([origin, target?.center ?? {
|
|
1025
|
+
x: origin.x + 40,
|
|
1026
|
+
y: origin.y + 40
|
|
1027
|
+
}])
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
result.set(edge.id, freeze({
|
|
1032
|
+
edgeId: edge.id,
|
|
1033
|
+
sections,
|
|
1034
|
+
status: malformed ? "fallback" : "routed",
|
|
1035
|
+
diagnostics: malformed ? [{
|
|
1036
|
+
code: "MISSING_GEOMETRY",
|
|
1037
|
+
edgeId: edge.id,
|
|
1038
|
+
message: "ELK sections contain missing or invalid geometry; rendered fallback paths"
|
|
1039
|
+
}] : []
|
|
1040
|
+
}));
|
|
1041
|
+
}
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region src/elkjs/index.ts
|
|
1047
|
+
function isLayoutEdgeLabel(label) {
|
|
1048
|
+
return getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true && (Boolean(label.text) || label.width !== void 0 || label.height !== void 0);
|
|
1049
|
+
}
|
|
1050
|
+
/** The crossing-model setting a layout passes to crossing minimization. */
|
|
1051
|
+
const crossingModel = Symbol("crossingModel");
|
|
1052
|
+
var ELK = class {
|
|
1053
|
+
#options;
|
|
1054
|
+
#algorithmIds;
|
|
1055
|
+
constructor(options = {}) {
|
|
1056
|
+
this.#options = options;
|
|
1057
|
+
this.#algorithmIds = new Set([
|
|
1058
|
+
"box",
|
|
1059
|
+
"fixed",
|
|
1060
|
+
"random",
|
|
1061
|
+
"rectpacking",
|
|
1062
|
+
"sporeCompaction",
|
|
1063
|
+
"sporeOverlap",
|
|
1064
|
+
...(options.algorithms ?? ["layered"]).filter((id) => id === "layered")
|
|
1065
|
+
]);
|
|
1066
|
+
}
|
|
1067
|
+
async knownLayoutAlgorithms() {
|
|
1068
|
+
return [...this.#algorithmIds].map((id) => ({
|
|
1069
|
+
id: id === "layered" ? "org.eclipse.elk.layered" : id,
|
|
1070
|
+
name: {
|
|
1071
|
+
layered: "Layered",
|
|
1072
|
+
box: "Box",
|
|
1073
|
+
fixed: "Fixed",
|
|
1074
|
+
random: "Random",
|
|
1075
|
+
rectpacking: "Rectangle Packing",
|
|
1076
|
+
sporeCompaction: "SPOrE Compaction",
|
|
1077
|
+
sporeOverlap: "SPOrE Overlap Removal"
|
|
1078
|
+
}[id],
|
|
1079
|
+
category: id === "layered" ? "layered" : "other",
|
|
1080
|
+
knownOptions: id === "layered" ? elkLayeredOptionDefinitions.map((definition) => definition.elkId) : id === "box" ? [
|
|
1081
|
+
"padding",
|
|
1082
|
+
"spacing.nodeNode",
|
|
1083
|
+
"aspectRatio",
|
|
1084
|
+
"box.packingMode"
|
|
1085
|
+
] : id === "random" ? [
|
|
1086
|
+
"padding",
|
|
1087
|
+
"spacing.nodeNode",
|
|
1088
|
+
"aspectRatio",
|
|
1089
|
+
"randomSeed"
|
|
1090
|
+
] : ["position", "bendPoints"]
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1093
|
+
async knownLayoutOptions() {
|
|
1094
|
+
return [{
|
|
1095
|
+
id: "org.eclipse.elk.algorithm",
|
|
1096
|
+
name: "Layout Algorithm",
|
|
1097
|
+
type: "STRING"
|
|
1098
|
+
}, ...elkLayeredOptionDefinitions.map((definition) => ({
|
|
1099
|
+
id: definition.elkId,
|
|
1100
|
+
name: definition.name,
|
|
1101
|
+
type: definition.type,
|
|
1102
|
+
targets: [...definition.targets]
|
|
1103
|
+
}))];
|
|
1104
|
+
}
|
|
1105
|
+
async knownLayoutCategories() {
|
|
1106
|
+
return [{
|
|
1107
|
+
id: "layered",
|
|
1108
|
+
name: "Layered",
|
|
1109
|
+
knownLayouters: this.#algorithmIds.has("layered") ? ["layered"] : []
|
|
1110
|
+
}, {
|
|
1111
|
+
id: "other",
|
|
1112
|
+
name: "Other",
|
|
1113
|
+
knownLayouters: [
|
|
1114
|
+
"box",
|
|
1115
|
+
"fixed",
|
|
1116
|
+
"random"
|
|
1117
|
+
]
|
|
1118
|
+
}];
|
|
1119
|
+
}
|
|
1120
|
+
terminateWorker() {}
|
|
1121
|
+
async layout(graph, arguments_ = {}) {
|
|
1122
|
+
const options = {
|
|
1123
|
+
...this.#options.defaultLayoutOptions,
|
|
1124
|
+
...arguments_.layoutOptions
|
|
1125
|
+
};
|
|
1126
|
+
const seeds = crossingVariants(options, graph);
|
|
1127
|
+
const probe = mayShareTracks(graph, options) ? {
|
|
1128
|
+
mixedPorts: false,
|
|
1129
|
+
sharedTracks: false
|
|
1130
|
+
} : void 0;
|
|
1131
|
+
const pristine = seeds.length || probe ? structuredClone(graph) : void 0;
|
|
1132
|
+
let best = separated(await this.#layoutWithoutDefectiveCompaction(graph, probe ? {
|
|
1133
|
+
...arguments_,
|
|
1134
|
+
[crossingModel]: { probe }
|
|
1135
|
+
} : arguments_), options);
|
|
1136
|
+
if (!pristine) return best;
|
|
1137
|
+
const reseed = seeds.length > 0 && (thorough(options, graph) || measureLayout(best, true).defects > 0);
|
|
1138
|
+
const variants = [
|
|
1139
|
+
...probe?.mixedPorts ? [{
|
|
1140
|
+
...arguments_,
|
|
1141
|
+
[crossingModel]: { mixedPorts: false }
|
|
1142
|
+
}] : [],
|
|
1143
|
+
...probe?.sharedTracks ? [{
|
|
1144
|
+
...arguments_,
|
|
1145
|
+
[crossingModel]: {
|
|
1146
|
+
mixedPorts: false,
|
|
1147
|
+
separateSharedTracks: true
|
|
1148
|
+
}
|
|
1149
|
+
}] : [],
|
|
1150
|
+
...(reseed ? seeds : []).map((variant) => ({
|
|
1151
|
+
...arguments_,
|
|
1152
|
+
layoutOptions: {
|
|
1153
|
+
...arguments_.layoutOptions,
|
|
1154
|
+
...variant
|
|
1155
|
+
}
|
|
1156
|
+
}))
|
|
1157
|
+
];
|
|
1158
|
+
if (!variants.length) return best;
|
|
1159
|
+
let quality = measureLayout(best);
|
|
1160
|
+
for (const variant of variants) {
|
|
1161
|
+
const candidate = separated(await this.#layoutWithoutDefectiveCompaction(structuredClone(pristine), variant), options);
|
|
1162
|
+
const candidateQuality = measureLayout(candidate);
|
|
1163
|
+
if (isBetterLayout(candidateQuality, quality)) {
|
|
1164
|
+
best = candidate;
|
|
1165
|
+
quality = candidateQuality;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return best;
|
|
1169
|
+
}
|
|
1170
|
+
async #layoutWithoutDefectiveCompaction(graph, arguments_) {
|
|
1171
|
+
const pristine = usesPostCompaction({
|
|
1172
|
+
...this.#options.defaultLayoutOptions,
|
|
1173
|
+
...arguments_.layoutOptions
|
|
1174
|
+
}, graph) ? structuredClone(graph) : void 0;
|
|
1175
|
+
const result = await this.#layout(graph, arguments_);
|
|
1176
|
+
const defects = pristine ? measureLayout(result, true).defects : 0;
|
|
1177
|
+
if (!pristine || defects === 0) return result;
|
|
1178
|
+
const strategy = { "elk.layered.compaction.postCompaction.strategy": "NONE" };
|
|
1179
|
+
const uncompacted = await this.#layout(disablePostCompaction(pristine), {
|
|
1180
|
+
...arguments_,
|
|
1181
|
+
layoutOptions: {
|
|
1182
|
+
...arguments_.layoutOptions,
|
|
1183
|
+
...strategy
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
return measureLayout(uncompacted, true).defects < defects ? uncompacted : result;
|
|
1187
|
+
}
|
|
1188
|
+
async #layout(graph, arguments_ = {}, compoundLayout = false) {
|
|
1189
|
+
const prepared = await this.#prepareLayout(graph, arguments_, compoundLayout);
|
|
1190
|
+
const orders = coordinatePreparedScopes(prepared);
|
|
1191
|
+
return await prepared.finish(orders);
|
|
1192
|
+
}
|
|
1193
|
+
async #prepareLayout(graph, arguments_ = {}, compoundLayout = false) {
|
|
1194
|
+
const startedAt = performance.now();
|
|
1195
|
+
if (graph === void 0 || graph === null) throw new TypeError("Missing mandatory parameter: graph");
|
|
1196
|
+
if (typeof graph.id !== "string" && !(typeof graph.id === "number" && Number.isInteger(graph.id))) throw new TypeError("Graph id must be a string or integer");
|
|
1197
|
+
delete graph.logging;
|
|
1198
|
+
const layoutOptions = {
|
|
1199
|
+
...this.#options.defaultLayoutOptions,
|
|
1200
|
+
...arguments_.layoutOptions,
|
|
1201
|
+
...graph.properties,
|
|
1202
|
+
...graph.layoutOptions
|
|
1203
|
+
};
|
|
1204
|
+
getOption(layoutOptions, "debugMode");
|
|
1205
|
+
getOption(layoutOptions, "interactiveLayout");
|
|
1206
|
+
getOption(layoutOptions, "layered.generatePositionAndLayerIds");
|
|
1207
|
+
getOption(layoutOptions, "topdown.scaleFactor");
|
|
1208
|
+
getOption(layoutOptions, "contentAlignment");
|
|
1209
|
+
for (const child of graph.children ?? []) {
|
|
1210
|
+
const childOptions = child.layoutOptions ?? {};
|
|
1211
|
+
getOption(childOptions, "layered.layering.layerId");
|
|
1212
|
+
getOption(childOptions, "layered.crossingMinimization.positionId");
|
|
1213
|
+
getOption(childOptions, "layered.layering.layerChoiceConstraint");
|
|
1214
|
+
getOption(childOptions, "layered.crossingMinimization.positionChoiceConstraint");
|
|
1215
|
+
getOption(childOptions, "layered.crossingMinimization.inLayerPredOf");
|
|
1216
|
+
getOption(childOptions, "layered.crossingMinimization.inLayerSuccOf");
|
|
1217
|
+
getOption(childOptions, "topdown.scaleFactor");
|
|
1218
|
+
getOption(childOptions, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
1219
|
+
for (const port of child.ports ?? []) getOption(port.layoutOptions ?? {}, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
1220
|
+
}
|
|
1221
|
+
for (const edge of graph.edges ?? []) getOption(edge.layoutOptions ?? {}, "layered.considerModelOrder.groupModelOrder.componentGroupId");
|
|
1222
|
+
const requestedAlgorithm = String(getOption(layoutOptions, "algorithm") ?? "layered");
|
|
1223
|
+
const algorithm = requestedAlgorithm.replace(/^(?:org\.eclipse\.)?elk\./, "");
|
|
1224
|
+
if (algorithm !== "layered" && algorithm !== "box" && algorithm !== "fixed" && algorithm !== "random" && algorithm !== "rectpacking" && algorithm !== "sporeCompaction" && algorithm !== "sporeOverlap") throw new Error(`org.eclipse.elk.core.UnsupportedConfigurationException: Layout algorithm '${requestedAlgorithm}' not found`);
|
|
1225
|
+
const preparedChildren = /* @__PURE__ */ new Map();
|
|
1226
|
+
const finishChildren = [];
|
|
1227
|
+
const hasHierarchy = (graph.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
1228
|
+
const insideSelfLoopBaseHeightByNodeId = /* @__PURE__ */ new Map();
|
|
1229
|
+
const hierarchyRestorations = [];
|
|
1230
|
+
const boundaryRoutes = /* @__PURE__ */ new Map();
|
|
1231
|
+
const syntheticPortIds = /* @__PURE__ */ new Set();
|
|
1232
|
+
const authoredPortsByCompound = /* @__PURE__ */ new Map();
|
|
1233
|
+
const authoredOptionsByCompound = /* @__PURE__ */ new Map();
|
|
1234
|
+
const originalHierarchyEndpoints = new Map((graph.edges ?? []).map((edge) => [edge, {
|
|
1235
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
1236
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
1237
|
+
sources: edge.sources,
|
|
1238
|
+
targets: edge.targets,
|
|
1239
|
+
source: edge.source,
|
|
1240
|
+
target: edge.target
|
|
1241
|
+
}]));
|
|
1242
|
+
const hierarchyHandling = getOption(layoutOptions, "hierarchyHandling");
|
|
1243
|
+
const topdownLayout = getBooleanOption(layoutOptions, "topdownLayout") === true;
|
|
1244
|
+
const separateHierarchy = hasHierarchy && hierarchyHandling !== void 0 && hierarchyHandling !== "INCLUDE_CHILDREN";
|
|
1245
|
+
if (hasHierarchy && topdownLayout && hierarchyHandling === "INCLUDE_CHILDREN") throw new Error("org.eclipse.elk.core.UnsupportedConfigurationException: Topdown layout cannot be used together with hierarchy handling.");
|
|
1246
|
+
if (separateHierarchy) {
|
|
1247
|
+
const hasCrossHierarchyEdge = (container) => {
|
|
1248
|
+
const directEndpointIds = /* @__PURE__ */ new Set();
|
|
1249
|
+
for (const child of container.children ?? []) {
|
|
1250
|
+
directEndpointIds.add(String(child.id));
|
|
1251
|
+
for (const port of child.ports ?? []) directEndpointIds.add(String(port.id));
|
|
1252
|
+
}
|
|
1253
|
+
if ((container.edges ?? []).some((edge) => {
|
|
1254
|
+
const sourceId = String(edge.sources?.[0] ?? edge.source);
|
|
1255
|
+
const targetId = String(edge.targets?.[0] ?? edge.target);
|
|
1256
|
+
return !directEndpointIds.has(sourceId) || !directEndpointIds.has(targetId);
|
|
1257
|
+
})) return true;
|
|
1258
|
+
return (container.children ?? []).some((child) => hasCrossHierarchyEdge(child));
|
|
1259
|
+
};
|
|
1260
|
+
if (hasCrossHierarchyEdge(graph)) throw new Error("org.eclipse.elk.core.UnsupportedGraphException: Hierarchical edges require INCLUDE_CHILDREN");
|
|
1261
|
+
for (const child of graph.children ?? []) {
|
|
1262
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
1263
|
+
await this.#layout(child, {
|
|
1264
|
+
...arguments_,
|
|
1265
|
+
layoutOptions: {
|
|
1266
|
+
...arguments_.layoutOptions,
|
|
1267
|
+
...child.layoutOptions,
|
|
1268
|
+
hierarchyHandling
|
|
1269
|
+
},
|
|
1270
|
+
logging: false,
|
|
1271
|
+
measureExecutionTime: false
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (hasHierarchy && topdownLayout) {
|
|
1276
|
+
if (getOption(layoutOptions, "topdown.nodeType") === void 0) throw new Error(`${String(graph.id)} has not been assigned a top-down node type.`);
|
|
1277
|
+
for (const child of graph.children ?? []) {
|
|
1278
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
1279
|
+
const childOptions = {
|
|
1280
|
+
...layoutOptions,
|
|
1281
|
+
...child.layoutOptions
|
|
1282
|
+
};
|
|
1283
|
+
const childPadding = parsePadding(getOption(childOptions, "padding"), 12);
|
|
1284
|
+
const parallelNode = getOption(childOptions, "topdown.nodeType") === "PARALLEL_NODE";
|
|
1285
|
+
const width = getNumberOption(childOptions, "topdown.hierarchicalNodeWidth") ?? (parallelNode ? 150 : 0);
|
|
1286
|
+
const aspectRatio = getNumberOption(childOptions, "topdown.hierarchicalNodeAspectRatio") ?? (parallelNode ? 1.414 : 1 / Math.sqrt(2));
|
|
1287
|
+
child.width = Math.max(child.width ?? 0, width + childPadding.left + childPadding.right);
|
|
1288
|
+
child.height = Math.max(child.height ?? 0, width / aspectRatio + childPadding.top + childPadding.bottom);
|
|
1289
|
+
}
|
|
1290
|
+
} else if (hasHierarchy && !separateHierarchy) {
|
|
1291
|
+
const scopeSegmentOrder = segmentOrderByScope.get(graph) ?? elkSegmentOrder(graph, graph, originalHierarchyEndpoints, false);
|
|
1292
|
+
for (const child of graph.children ?? []) {
|
|
1293
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
1294
|
+
const descendantOwnerByEndpointId = /* @__PURE__ */ new Map();
|
|
1295
|
+
const collectDescendants = (node) => {
|
|
1296
|
+
for (const descendant of node.children ?? []) {
|
|
1297
|
+
descendantOwnerByEndpointId.set(String(descendant.id), descendant);
|
|
1298
|
+
for (const port of descendant.ports ?? []) descendantOwnerByEndpointId.set(String(port.id), descendant);
|
|
1299
|
+
collectDescendants(descendant);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
collectDescendants(child);
|
|
1303
|
+
const internalEdges = (graph.edges ?? []).filter((edge) => {
|
|
1304
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
1305
|
+
return descendantOwnerByEndpointId.has(sourceId) && descendantOwnerByEndpointId.has(targetId);
|
|
1306
|
+
});
|
|
1307
|
+
for (const edge of child.edges ?? []) if (!originalHierarchyEndpoints.has(edge)) originalHierarchyEndpoints.set(edge, {
|
|
1308
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
1309
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
1310
|
+
sources: edge.sources,
|
|
1311
|
+
targets: edge.targets,
|
|
1312
|
+
source: edge.source,
|
|
1313
|
+
target: edge.target
|
|
1314
|
+
});
|
|
1315
|
+
const crossingEdges = [...graph.edges ?? [], ...child.edges ?? []].filter((edge) => {
|
|
1316
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
1317
|
+
return descendantOwnerByEndpointId.has(sourceId) !== descendantOwnerByEndpointId.has(targetId);
|
|
1318
|
+
});
|
|
1319
|
+
const boundaryRank = new Map(elkBoundaryEdgeOrder(child, graph, originalHierarchyEndpoints).map((edge, index) => [edge, index]));
|
|
1320
|
+
crossingEdges.sort((a, b) => (boundaryRank.get(a) ?? Number.MAX_SAFE_INTEGER) - (boundaryRank.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
1321
|
+
const mergeHierarchyEdges = getBooleanOption(layoutOptions, "layered.mergeHierarchyEdges") !== false;
|
|
1322
|
+
const proxyByKind = /* @__PURE__ */ new Map();
|
|
1323
|
+
const implicitOwnPorts = /* @__PURE__ */ new Map();
|
|
1324
|
+
const ownPortFor = (edge) => {
|
|
1325
|
+
const e = originalHierarchyEndpoints.get(edge);
|
|
1326
|
+
const outside = descendantOwnerByEndpointId.has(e.sourceId) ? e.targetId : e.sourceId;
|
|
1327
|
+
if (e.sourceId === String(child.id) || e.targetId === String(child.id)) {
|
|
1328
|
+
let port = implicitOwnPorts.get(edge);
|
|
1329
|
+
if (!port) {
|
|
1330
|
+
const direction = getDirection(layoutOptions);
|
|
1331
|
+
const side = e.sourceId === String(child.id) ? {
|
|
1332
|
+
right: "EAST",
|
|
1333
|
+
left: "WEST",
|
|
1334
|
+
down: "SOUTH",
|
|
1335
|
+
up: "NORTH"
|
|
1336
|
+
}[direction] : {
|
|
1337
|
+
right: "WEST",
|
|
1338
|
+
left: "EAST",
|
|
1339
|
+
down: "NORTH",
|
|
1340
|
+
up: "SOUTH"
|
|
1341
|
+
}[direction];
|
|
1342
|
+
port = {
|
|
1343
|
+
id: `__native_hierarchy_own_${String(child.id)}_${implicitOwnPorts.size}`,
|
|
1344
|
+
width: 0,
|
|
1345
|
+
height: 0,
|
|
1346
|
+
layoutOptions: { "elk.port.side": side }
|
|
1347
|
+
};
|
|
1348
|
+
implicitOwnPorts.set(edge, port);
|
|
1349
|
+
syntheticPortIds.add(String(port.id));
|
|
1350
|
+
}
|
|
1351
|
+
return child.ports?.find((p) => p.id === port.id) ?? port;
|
|
1352
|
+
}
|
|
1353
|
+
return child.ports?.find((p) => String(p.id) === outside);
|
|
1354
|
+
};
|
|
1355
|
+
const proxyKey = (kind, edge) => {
|
|
1356
|
+
const own = ownPortFor(edge);
|
|
1357
|
+
if (own) return JSON.stringify(["authored", own.id]);
|
|
1358
|
+
const endpoints = originalHierarchyEndpoints.get(edge);
|
|
1359
|
+
const endpoint$2 = kind === "output" ? endpoints.sourceId : endpoints.targetId;
|
|
1360
|
+
const explicitPort = descendantOwnerByEndpointId.get(endpoint$2)?.ports?.some((port) => String(port.id) === endpoint$2);
|
|
1361
|
+
return JSON.stringify([kind, mergeHierarchyEdges && explicitPort ? endpoint$2 : String(edge.id)]);
|
|
1362
|
+
};
|
|
1363
|
+
const createBoundaryProxy = (key, kind, own, netFlow) => {
|
|
1364
|
+
let proxy = proxyByKind.get(key);
|
|
1365
|
+
if (!proxy) {
|
|
1366
|
+
const direction = getDirection({
|
|
1367
|
+
...layoutOptions,
|
|
1368
|
+
...child.layoutOptions
|
|
1369
|
+
});
|
|
1370
|
+
const authoredAnchor = own ? getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] : void 0;
|
|
1371
|
+
const origin = createExternalPortDummy({
|
|
1372
|
+
constraints: getOption(child.layoutOptions ?? {}, "portConstraints") ?? "FREE",
|
|
1373
|
+
side: own ? getOption(own.layoutOptions ?? {}, "port.side") ?? (kind === "output" ? "EAST" : "WEST") : kind === "output" ? "EAST" : "WEST",
|
|
1374
|
+
position: {
|
|
1375
|
+
x: own?.x ?? 0,
|
|
1376
|
+
y: own?.y ?? 0
|
|
1377
|
+
},
|
|
1378
|
+
anchor: authoredAnchor,
|
|
1379
|
+
index: own ? getNumberOption(own.layoutOptions ?? {}, "port.index") : void 0,
|
|
1380
|
+
direction: direction.toUpperCase(),
|
|
1381
|
+
netFlow,
|
|
1382
|
+
borderOffset: own ? getNumberOption(own.layoutOptions ?? {}, "port.borderOffset") ?? 0 : (getNumberOption(layoutOptions, "spacing.edgeEdge") ?? 10) / 2,
|
|
1383
|
+
size: {
|
|
1384
|
+
width: own?.width ?? 0,
|
|
1385
|
+
height: own?.height ?? 0
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
if (own) origin.parentPortId = String(own.id);
|
|
1389
|
+
const id = `__native_hierarchy_${String(child.id)}_${proxyByKind.size}`;
|
|
1390
|
+
proxy = attachExternalPortDummy({
|
|
1391
|
+
id,
|
|
1392
|
+
width: origin.width,
|
|
1393
|
+
height: origin.height,
|
|
1394
|
+
layoutOptions: {
|
|
1395
|
+
"elk.portConstraints": origin.constraints,
|
|
1396
|
+
"elk.layered.layering.layerConstraint": origin.side === {
|
|
1397
|
+
right: "EAST",
|
|
1398
|
+
left: "WEST",
|
|
1399
|
+
down: "SOUTH",
|
|
1400
|
+
up: "NORTH"
|
|
1401
|
+
}[direction] ? "LAST_SEPARATE" : origin.side === {
|
|
1402
|
+
right: "WEST",
|
|
1403
|
+
left: "EAST",
|
|
1404
|
+
down: "NORTH",
|
|
1405
|
+
up: "SOUTH"
|
|
1406
|
+
}[direction] ? "FIRST_SEPARATE" : void 0
|
|
1407
|
+
},
|
|
1408
|
+
ports: [{
|
|
1409
|
+
id: `${id}:boundary`,
|
|
1410
|
+
x: origin.port.x,
|
|
1411
|
+
y: origin.port.y,
|
|
1412
|
+
width: 0,
|
|
1413
|
+
height: 0,
|
|
1414
|
+
layoutOptions: { "elk.port.side": origin.port.side }
|
|
1415
|
+
}]
|
|
1416
|
+
}, origin);
|
|
1417
|
+
proxyByKind.set(key, proxy);
|
|
1418
|
+
}
|
|
1419
|
+
return proxy;
|
|
1420
|
+
};
|
|
1421
|
+
const ownedPortNetFlow = (id) => {
|
|
1422
|
+
let votes = 0;
|
|
1423
|
+
for (const edge of originalHierarchyEndpoints.values()) {
|
|
1424
|
+
if (edge.sourceId === id) votes += descendantOwnerByEndpointId.has(edge.targetId) ? -1 : 1;
|
|
1425
|
+
if (edge.targetId === id) votes += descendantOwnerByEndpointId.has(edge.sourceId) ? 1 : -1;
|
|
1426
|
+
}
|
|
1427
|
+
return votes;
|
|
1428
|
+
};
|
|
1429
|
+
const proxyFor = (kind, edge) => {
|
|
1430
|
+
const own = ownPortFor(edge);
|
|
1431
|
+
const flow = implicitOwnPorts.has(edge) ? kind === "output" ? 1 : -1 : own ? ownedPortNetFlow(String(own.id)) : kind === "output" ? 1 : -1;
|
|
1432
|
+
return createBoundaryProxy(proxyKey(kind, edge), kind, own, flow);
|
|
1433
|
+
};
|
|
1434
|
+
const temporaryEdges = [
|
|
1435
|
+
...crossingEdges.map((edge) => {
|
|
1436
|
+
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge);
|
|
1437
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
1438
|
+
const proxy = proxyFor(sourceInside ? "output" : "input", edge);
|
|
1439
|
+
const labels = (graph.edges ?? []).includes(edge) && !ownPortFor(edge) ? edge.labels?.filter((label) => (getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") !== "CENTER") : edge.labels;
|
|
1440
|
+
const segment = {
|
|
1441
|
+
...edge,
|
|
1442
|
+
labels,
|
|
1443
|
+
id: `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
1444
|
+
sources: [sourceInside ? sourceId : proxy.ports[0].id],
|
|
1445
|
+
targets: [sourceInside ? proxy.ports[0].id : targetId],
|
|
1446
|
+
source: void 0,
|
|
1447
|
+
target: void 0,
|
|
1448
|
+
sections: void 0
|
|
1449
|
+
};
|
|
1450
|
+
originalEdgeId.set(segment, originalEdgeId.get(edge) ?? String(edge.id));
|
|
1451
|
+
return segment;
|
|
1452
|
+
}),
|
|
1453
|
+
...(child.edges ?? []).filter((edge) => !crossingEdges.includes(edge)),
|
|
1454
|
+
...internalEdges.filter((edge) => !(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)))
|
|
1455
|
+
];
|
|
1456
|
+
const hasExternalPorts = proxyByKind.size > 0;
|
|
1457
|
+
const inactiveAuthoredPorts = [];
|
|
1458
|
+
if (hasExternalPorts) for (const port of child.ports ?? []) {
|
|
1459
|
+
const key = JSON.stringify(["authored", port.id]);
|
|
1460
|
+
if (proxyByKind.has(key)) continue;
|
|
1461
|
+
const flow = ownedPortNetFlow(String(port.id));
|
|
1462
|
+
inactiveAuthoredPorts.push({
|
|
1463
|
+
port,
|
|
1464
|
+
proxy: createBoundaryProxy(key, flow >= 0 ? "output" : "input", port, flow)
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
const outsideNodeEdges = !hasExternalPorts ? [] : (graph.edges ?? []).filter((edge) => {
|
|
1468
|
+
const e = originalHierarchyEndpoints.get(edge);
|
|
1469
|
+
return (e.sourceId === String(child.id) || e.targetId === String(child.id)) && !descendantOwnerByEndpointId.has(e.sourceId) && !descendantOwnerByEndpointId.has(e.targetId);
|
|
1470
|
+
});
|
|
1471
|
+
for (const edge of outsideNodeEdges) proxyFor(originalHierarchyEndpoints.get(edge).sourceId === String(child.id) ? "output" : "input", edge);
|
|
1472
|
+
const temporaryChild = {
|
|
1473
|
+
...child,
|
|
1474
|
+
layoutOptions: proxyByKind.size ? {
|
|
1475
|
+
...child.layoutOptions,
|
|
1476
|
+
"elk.portConstraints": "FIXED_SIDE"
|
|
1477
|
+
} : child.layoutOptions,
|
|
1478
|
+
children: [...child.children ?? [], ...proxyByKind.values()],
|
|
1479
|
+
edges: temporaryEdges
|
|
1480
|
+
};
|
|
1481
|
+
const childSegmentOrder = elkSegmentOrder(child, graph, originalHierarchyEndpoints, true);
|
|
1482
|
+
segmentOrderByScope.set(temporaryChild, childSegmentOrder);
|
|
1483
|
+
for (const edge of temporaryEdges) {
|
|
1484
|
+
const id = originalEdgeId.get(edge);
|
|
1485
|
+
const rank = id === void 0 ? void 0 : childSegmentOrder.get(id);
|
|
1486
|
+
if (rank !== void 0) hierarchySegmentRank.set(edge, rank);
|
|
1487
|
+
}
|
|
1488
|
+
const preparedChild = await this.#prepareLayout(temporaryChild, {
|
|
1489
|
+
...arguments_,
|
|
1490
|
+
layoutOptions: {
|
|
1491
|
+
...arguments_.layoutOptions,
|
|
1492
|
+
direction: getDirection(getOption(child.layoutOptions ?? {}, "direction") === void 0 ? layoutOptions : child.layoutOptions).toUpperCase(),
|
|
1493
|
+
hierarchyHandling: "INCLUDE_CHILDREN"
|
|
1494
|
+
},
|
|
1495
|
+
logging: false,
|
|
1496
|
+
measureExecutionTime: false
|
|
1497
|
+
}, true);
|
|
1498
|
+
preparedChildren.set(String(child.id), preparedChild);
|
|
1499
|
+
authoredPortsByCompound.set(child, child.ports);
|
|
1500
|
+
authoredOptionsByCompound.set(child, child.layoutOptions);
|
|
1501
|
+
const initialPorts = (child.ports ?? []).map((port) => ({
|
|
1502
|
+
...port,
|
|
1503
|
+
layoutOptions: port.layoutOptions ? { ...port.layoutOptions } : void 0
|
|
1504
|
+
}));
|
|
1505
|
+
for (const edge of crossingEdges) {
|
|
1506
|
+
const own = ownPortFor(edge);
|
|
1507
|
+
if (own) {
|
|
1508
|
+
const original$1 = originalHierarchyEndpoints.get(edge);
|
|
1509
|
+
const sourceInside$1 = descendantOwnerByEndpointId.has(original$1.sourceId);
|
|
1510
|
+
const origin$1 = externalPortDummyOf(proxyByKind.get(proxyKey(sourceInside$1 ? "output" : "input", edge)));
|
|
1511
|
+
let port = initialPorts.find((p) => p.id === own.id);
|
|
1512
|
+
if (!port) {
|
|
1513
|
+
port = {
|
|
1514
|
+
...own,
|
|
1515
|
+
layoutOptions: { ...own.layoutOptions }
|
|
1516
|
+
};
|
|
1517
|
+
initialPorts.push(port);
|
|
1518
|
+
}
|
|
1519
|
+
port.layoutOptions = {
|
|
1520
|
+
...port.layoutOptions,
|
|
1521
|
+
"elk.port.side": origin$1.side
|
|
1522
|
+
};
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
const original = originalHierarchyEndpoints.get(edge);
|
|
1526
|
+
const sourceInside = descendantOwnerByEndpointId.has(original.sourceId);
|
|
1527
|
+
const proxy = proxyByKind.get(proxyKey(sourceInside ? "output" : "input", edge));
|
|
1528
|
+
const origin = externalPortDummyOf(proxy);
|
|
1529
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
1530
|
+
syntheticPortIds.add(portId);
|
|
1531
|
+
if (!initialPorts.some((port) => port.id === portId)) initialPorts.push({
|
|
1532
|
+
id: portId,
|
|
1533
|
+
width: origin.externalSize.width,
|
|
1534
|
+
height: origin.externalSize.height,
|
|
1535
|
+
x: 0,
|
|
1536
|
+
y: 0,
|
|
1537
|
+
layoutOptions: {
|
|
1538
|
+
"elk.port.side": origin.side,
|
|
1539
|
+
"elk.port.borderOffset": origin.borderOffset
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) hierarchyRestorations.push({
|
|
1543
|
+
edge,
|
|
1544
|
+
...original
|
|
1545
|
+
});
|
|
1546
|
+
if (sourceInside) edge.sources = [portId];
|
|
1547
|
+
else edge.targets = [portId];
|
|
1548
|
+
const rank = scopeSegmentOrder.get(originalEdgeId.get(edge) ?? String(edge.id));
|
|
1549
|
+
if (rank !== void 0) hierarchySegmentRank.set(edge, rank);
|
|
1550
|
+
edge.source = void 0;
|
|
1551
|
+
edge.target = void 0;
|
|
1552
|
+
}
|
|
1553
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
1554
|
+
const actual = initialPorts.find((p) => p.id === port.id);
|
|
1555
|
+
actual.layoutOptions = {
|
|
1556
|
+
...actual.layoutOptions,
|
|
1557
|
+
"elk.port.side": externalPortDummyOf(proxy).side
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
for (const edge of outsideNodeEdges) {
|
|
1561
|
+
const original = originalHierarchyEndpoints.get(edge);
|
|
1562
|
+
const source = original.sourceId === String(child.id);
|
|
1563
|
+
const proxy = proxyFor(source ? "output" : "input", edge), own = ownPortFor(edge);
|
|
1564
|
+
const origin = externalPortDummyOf(proxy);
|
|
1565
|
+
if (!initialPorts.some((p) => p.id === own.id)) initialPorts.push({
|
|
1566
|
+
...own,
|
|
1567
|
+
layoutOptions: {
|
|
1568
|
+
...own.layoutOptions,
|
|
1569
|
+
"elk.port.side": origin.side
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
if (!hierarchyRestorations.some((r) => r.edge === edge)) hierarchyRestorations.push({
|
|
1573
|
+
edge,
|
|
1574
|
+
...original
|
|
1575
|
+
});
|
|
1576
|
+
if (source) edge.sources = [own.id];
|
|
1577
|
+
else edge.targets = [own.id];
|
|
1578
|
+
edge.source = void 0;
|
|
1579
|
+
edge.target = void 0;
|
|
1580
|
+
}
|
|
1581
|
+
child.ports = initialPorts;
|
|
1582
|
+
child.layoutOptions = {
|
|
1583
|
+
...child.layoutOptions,
|
|
1584
|
+
"elk.portConstraints": crossingEdges.length > 0 || outsideNodeEdges.length > 0 ? "FIXED_SIDE" : getOption(authoredOptionsByCompound.get(child) ?? {}, "portConstraints") ?? "FREE"
|
|
1585
|
+
};
|
|
1586
|
+
finishChildren.push(async () => {
|
|
1587
|
+
await preparedChild.finish(activeOrders);
|
|
1588
|
+
const childPadding = parsePadding(getOption({
|
|
1589
|
+
...layoutOptions,
|
|
1590
|
+
...child.layoutOptions
|
|
1591
|
+
}, "padding"), 12);
|
|
1592
|
+
for (const internalEdge of internalEdges) {
|
|
1593
|
+
const temporary = temporaryEdges.find((candidate) => String(candidate.id) === String(internalEdge.id));
|
|
1594
|
+
if (temporary?.sections) {
|
|
1595
|
+
internalEdge.sections = temporary.sections;
|
|
1596
|
+
internalEdge.container = temporary.container ?? child.id;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const childDirection = getDirection({
|
|
1600
|
+
...layoutOptions,
|
|
1601
|
+
...child.layoutOptions
|
|
1602
|
+
});
|
|
1603
|
+
const horizontalChild = childDirection === "right" || childDirection === "left";
|
|
1604
|
+
const restoredPerpendicular = [...proxyByKind.values()].some(isPerpendicularPortRestored);
|
|
1605
|
+
child.width = restoredPerpendicular ? temporaryChild.width : horizontalChild ? Math.max(0, ...(temporaryChild.children ?? []).map((node) => (node.x ?? 0) + (node.width ?? 0))) + childPadding.right : temporaryChild.width;
|
|
1606
|
+
child.height = restoredPerpendicular ? temporaryChild.height : horizontalChild ? temporaryChild.height : Math.max(0, ...(temporaryChild.children ?? []).map((node) => (node.y ?? 0) + (node.height ?? 0))) + childPadding.bottom;
|
|
1607
|
+
const relativeRect = (id) => {
|
|
1608
|
+
const owner = descendantOwnerByEndpointId.get(id);
|
|
1609
|
+
if (!owner) return void 0;
|
|
1610
|
+
const path = [];
|
|
1611
|
+
const visit = (parent) => {
|
|
1612
|
+
for (const candidate of parent.children ?? []) {
|
|
1613
|
+
path.push(candidate);
|
|
1614
|
+
if (candidate === owner || visit(candidate)) return true;
|
|
1615
|
+
path.pop();
|
|
1616
|
+
}
|
|
1617
|
+
return false;
|
|
1618
|
+
};
|
|
1619
|
+
if (!visit(child)) return void 0;
|
|
1620
|
+
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
1621
|
+
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
1622
|
+
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
1623
|
+
if (port) return {
|
|
1624
|
+
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
1625
|
+
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
1626
|
+
width: 0,
|
|
1627
|
+
height: 0
|
|
1628
|
+
};
|
|
1629
|
+
return {
|
|
1630
|
+
x: ownerX,
|
|
1631
|
+
y: ownerY,
|
|
1632
|
+
width: path.at(-1)?.width ?? 0,
|
|
1633
|
+
height: path.at(-1)?.height ?? 0
|
|
1634
|
+
};
|
|
1635
|
+
};
|
|
1636
|
+
const ports = (authoredPortsByCompound.get(child) ?? []).map((original) => child.ports?.find((port) => port.id === original.id) ?? original);
|
|
1637
|
+
for (const edge of crossingEdges) {
|
|
1638
|
+
const original = originalHierarchyEndpoints.get(edge);
|
|
1639
|
+
const { sourceId, targetId } = original;
|
|
1640
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
1641
|
+
if (!relativeRect(sourceInside ? sourceId : targetId)) continue;
|
|
1642
|
+
const internalEdge = temporaryEdges.find((candidate) => String(candidate.id) === `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`);
|
|
1643
|
+
const internalRoute = internalEdge?.sections;
|
|
1644
|
+
const kind = sourceInside ? "output" : "input";
|
|
1645
|
+
const proxy = proxyByKind.get(proxyKey(kind, edge));
|
|
1646
|
+
const origin = externalPortDummyOf(proxy);
|
|
1647
|
+
const endpoint$2 = sourceInside ? internalRoute?.at(-1)?.endPoint : internalRoute?.[0]?.startPoint;
|
|
1648
|
+
if (!endpoint$2) throw new Error(`Missing compound boundary route for ${String(edge.id)}`);
|
|
1649
|
+
const transferred = transferExternalPort({
|
|
1650
|
+
contentSize: {
|
|
1651
|
+
width: child.width - childPadding.left - childPadding.right,
|
|
1652
|
+
height: child.height - childPadding.top - childPadding.bottom
|
|
1653
|
+
},
|
|
1654
|
+
padding: childPadding,
|
|
1655
|
+
offset: {
|
|
1656
|
+
x: 0,
|
|
1657
|
+
y: 0
|
|
1658
|
+
},
|
|
1659
|
+
dummy: {
|
|
1660
|
+
x: endpoint$2.x - childPadding.left - (ownPortFor(edge) ? origin.port.x : 0),
|
|
1661
|
+
y: endpoint$2.y - childPadding.top - (ownPortFor(edge) ? origin.port.y : 0),
|
|
1662
|
+
width: origin.width,
|
|
1663
|
+
height: origin.height
|
|
1664
|
+
},
|
|
1665
|
+
side: origin.side,
|
|
1666
|
+
borderOffset: origin.borderOffset,
|
|
1667
|
+
portSize: origin.externalSize
|
|
1668
|
+
});
|
|
1669
|
+
const points = internalRoute.flatMap((section, index) => [
|
|
1670
|
+
...index === 0 ? [section.startPoint] : [],
|
|
1671
|
+
...section.bendPoints ?? [],
|
|
1672
|
+
section.endPoint
|
|
1673
|
+
]).map((point) => ({ ...point }));
|
|
1674
|
+
const boundary = sourceInside ? points.at(-1) : points[0];
|
|
1675
|
+
boundary.x = transferred.dummy.x + childPadding.left;
|
|
1676
|
+
boundary.y = transferred.dummy.y + childPadding.top;
|
|
1677
|
+
const own = ownPortFor(edge);
|
|
1678
|
+
if (own) {
|
|
1679
|
+
if (implicitOwnPorts.has(edge) && !ports.some((p) => p.id === own.id)) ports.push(own);
|
|
1680
|
+
own.x = transferred.port.x;
|
|
1681
|
+
own.y = transferred.port.y;
|
|
1682
|
+
const anchor = getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] ?? {
|
|
1683
|
+
x: origin.side === "EAST" ? own.width ?? 0 : origin.side === "WEST" ? 0 : (own.width ?? 0) / 2,
|
|
1684
|
+
y: origin.side === "SOUTH" ? own.height ?? 0 : origin.side === "NORTH" ? 0 : (own.height ?? 0) / 2
|
|
1685
|
+
};
|
|
1686
|
+
boundary.x = own.x + anchor.x;
|
|
1687
|
+
boundary.y = own.y + anchor.y;
|
|
1688
|
+
edge.sections = [{
|
|
1689
|
+
id: `${String(edge.id)}_s0`,
|
|
1690
|
+
startPoint: points[0],
|
|
1691
|
+
endPoint: points.at(-1),
|
|
1692
|
+
...points.length > 2 ? { bendPoints: points.slice(1, -1) } : {},
|
|
1693
|
+
incomingShape: sourceId,
|
|
1694
|
+
outgoingShape: targetId
|
|
1695
|
+
}];
|
|
1696
|
+
if (internalEdge?.junctionPoints?.length) edge.junctionPoints = internalEdge.junctionPoints.map((point) => ({ ...point }));
|
|
1697
|
+
else delete edge.junctionPoints;
|
|
1698
|
+
edge.container = child.id;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const routes = boundaryRoutes.get(edge) ?? {};
|
|
1702
|
+
routes[sourceInside ? "source" : "target"] = {
|
|
1703
|
+
owner: child,
|
|
1704
|
+
points,
|
|
1705
|
+
junctionPoints: internalEdge?.junctionPoints
|
|
1706
|
+
};
|
|
1707
|
+
boundaryRoutes.set(edge, routes);
|
|
1708
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
1709
|
+
syntheticPortIds.add(portId);
|
|
1710
|
+
if (!ports.some((port) => port.id === portId)) ports.push({
|
|
1711
|
+
id: portId,
|
|
1712
|
+
width: origin.externalSize.width,
|
|
1713
|
+
height: origin.externalSize.height,
|
|
1714
|
+
x: transferred.port.x,
|
|
1715
|
+
y: transferred.port.y,
|
|
1716
|
+
layoutOptions: {
|
|
1717
|
+
"elk.port.side": origin.side,
|
|
1718
|
+
"elk.port.borderOffset": origin.borderOffset
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) hierarchyRestorations.push({
|
|
1722
|
+
edge,
|
|
1723
|
+
...original
|
|
1724
|
+
});
|
|
1725
|
+
if (sourceInside) edge.sources = [portId];
|
|
1726
|
+
else edge.targets = [portId];
|
|
1727
|
+
edge.source = void 0;
|
|
1728
|
+
edge.target = void 0;
|
|
1729
|
+
}
|
|
1730
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
1731
|
+
const origin = externalPortDummyOf(proxy);
|
|
1732
|
+
const actual = ports.find((p) => p.id === port.id);
|
|
1733
|
+
const transferred = transferExternalPort({
|
|
1734
|
+
contentSize: {
|
|
1735
|
+
width: child.width - childPadding.left - childPadding.right,
|
|
1736
|
+
height: child.height - childPadding.top - childPadding.bottom
|
|
1737
|
+
},
|
|
1738
|
+
padding: childPadding,
|
|
1739
|
+
offset: {
|
|
1740
|
+
x: 0,
|
|
1741
|
+
y: 0
|
|
1742
|
+
},
|
|
1743
|
+
dummy: {
|
|
1744
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
1745
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
1746
|
+
width: origin.width,
|
|
1747
|
+
height: origin.height
|
|
1748
|
+
},
|
|
1749
|
+
side: origin.side,
|
|
1750
|
+
borderOffset: origin.borderOffset,
|
|
1751
|
+
portSize: origin.externalSize
|
|
1752
|
+
});
|
|
1753
|
+
actual.x = transferred.port.x;
|
|
1754
|
+
actual.y = transferred.port.y;
|
|
1755
|
+
}
|
|
1756
|
+
for (const edge of outsideNodeEdges) {
|
|
1757
|
+
const proxy = proxyFor(originalHierarchyEndpoints.get(edge).sourceId === String(child.id) ? "output" : "input", edge), origin = externalPortDummyOf(proxy), own = ownPortFor(edge);
|
|
1758
|
+
const transferred = transferExternalPort({
|
|
1759
|
+
contentSize: {
|
|
1760
|
+
width: child.width - childPadding.left - childPadding.right,
|
|
1761
|
+
height: child.height - childPadding.top - childPadding.bottom
|
|
1762
|
+
},
|
|
1763
|
+
padding: childPadding,
|
|
1764
|
+
offset: {
|
|
1765
|
+
x: 0,
|
|
1766
|
+
y: 0
|
|
1767
|
+
},
|
|
1768
|
+
dummy: {
|
|
1769
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
1770
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
1771
|
+
width: origin.width,
|
|
1772
|
+
height: origin.height
|
|
1773
|
+
},
|
|
1774
|
+
side: origin.side,
|
|
1775
|
+
borderOffset: origin.borderOffset,
|
|
1776
|
+
portSize: origin.externalSize
|
|
1777
|
+
});
|
|
1778
|
+
own.x = transferred.port.x;
|
|
1779
|
+
own.y = transferred.port.y;
|
|
1780
|
+
if (!ports.some((p) => p.id === own.id)) ports.push(own);
|
|
1781
|
+
}
|
|
1782
|
+
child.ports = ports;
|
|
1783
|
+
child.layoutOptions = {
|
|
1784
|
+
...child.layoutOptions,
|
|
1785
|
+
"elk.portConstraints": "FIXED_POS"
|
|
1786
|
+
};
|
|
1787
|
+
const insideLoopCount = (graph.edges ?? []).filter((edge) => isInsideSelfLoop(graph, edge, String(child.id))).length;
|
|
1788
|
+
if (insideLoopCount > 0) {
|
|
1789
|
+
const baseHeight = child.height ?? 0;
|
|
1790
|
+
insideSelfLoopBaseHeightByNodeId.set(String(child.id), baseHeight);
|
|
1791
|
+
child.height = baseHeight + insideLoopCount * 11;
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
applyNodeMicroLayout(graph, layoutOptions);
|
|
1797
|
+
const graph_ = toGraph(graph, layoutOptions);
|
|
1798
|
+
const hierarchyProxyIds = new Set((graph.children ?? []).filter((node) => String(node.id).startsWith("__native_hierarchy_")).map((node) => String(node.id)));
|
|
1799
|
+
const layerConstraintByNodeId = new Map((graph.children ?? []).map((node) => [String(node.id), String(getOption(node.layoutOptions ?? {}, "layered.layering.layerConstraint") ?? getOption(node.layoutOptions ?? {}, "layerConstraint") ?? "NONE")]));
|
|
1800
|
+
if (algorithm === "layered" && graph_.edges.some((edge) => {
|
|
1801
|
+
if (edge.sourceId === edge.targetId) return false;
|
|
1802
|
+
if (hierarchyProxyIds.has(edge.sourceId) || hierarchyProxyIds.has(edge.targetId)) return false;
|
|
1803
|
+
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
1804
|
+
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
1805
|
+
return (sourceConstraint === "FIRST" || sourceConstraint === "FIRST_SEPARATE") && (targetConstraint === "FIRST" || targetConstraint === "FIRST_SEPARATE");
|
|
1806
|
+
})) throw new Error("org.eclipse.elk.core.UnsupportedConfigurationException: Layer constraints conflict");
|
|
1807
|
+
const padding = parsePadding(getOption(layoutOptions, "padding"), algorithm === "layered" ? 12 : 0);
|
|
1808
|
+
const options = {
|
|
1809
|
+
direction: getDirection(layoutOptions),
|
|
1810
|
+
spacing: {
|
|
1811
|
+
node: getNumberOption(layoutOptions, "spacing.nodeNode") ?? getNumberOption(layoutOptions, "layered.spacing.baseValue"),
|
|
1812
|
+
layer: getNumberOption(layoutOptions, "layered.spacing.nodeNodeBetweenLayers") ?? getNumberOption(layoutOptions, "layered.spacing.baseValue") ?? 20
|
|
1813
|
+
},
|
|
1814
|
+
padding,
|
|
1815
|
+
constraints: { layer: () => void 0 },
|
|
1816
|
+
settings: {
|
|
1817
|
+
...getLayeredSettings(layoutOptions),
|
|
1818
|
+
...arguments_[crossingModel] ? { [CROSSING_MODEL]: arguments_[crossingModel] } : {},
|
|
1819
|
+
...compoundLayout || hasHierarchy && !separateHierarchy && !topdownLayout ? { separateConnectedComponents: false } : {},
|
|
1820
|
+
...hierarchyHandling === "INCLUDE_CHILDREN" ? { "crossingMinimization.greedySwitch.type": String(getOption(layoutOptions, "layered.crossingMinimization.greedySwitchHierarchical.type") ?? "TWO_SIDED") } : {}
|
|
1821
|
+
},
|
|
1822
|
+
nodeSettings: (node) => {
|
|
1823
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1824
|
+
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
1825
|
+
},
|
|
1826
|
+
edgeSettings: (edge) => {
|
|
1827
|
+
const elkEdge = findById(graph.edges, edge.id)?.item;
|
|
1828
|
+
return {
|
|
1829
|
+
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
1830
|
+
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {}),
|
|
1831
|
+
"edge.hierarchyRank": elkEdge && hierarchySegmentRank.get(elkEdge)
|
|
1832
|
+
};
|
|
1833
|
+
},
|
|
1834
|
+
portSettings: (port, node) => {
|
|
1835
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1836
|
+
const found = findById(child?.ports, port.name);
|
|
1837
|
+
const elkPort = found?.item;
|
|
1838
|
+
return {
|
|
1839
|
+
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
1840
|
+
"port.authoredIndex": found?.position,
|
|
1841
|
+
"port.labelWidth": Math.max(0, ...(elkPort?.labels ?? []).map((label) => label.width ?? 0)),
|
|
1842
|
+
"port.labelHeight": Math.max(0, ...(elkPort?.labels ?? []).map((label) => label.height ?? 0))
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
const pipeline = algorithm === "layered" ? createLayeredScopePipeline(graph_, options) : void 0;
|
|
1847
|
+
const initial = pipeline?.next();
|
|
1848
|
+
const phase = initial && !initial.done ? initial.value : void 0;
|
|
1849
|
+
let activeOrders;
|
|
1850
|
+
let finished;
|
|
1851
|
+
const prepared = {
|
|
1852
|
+
graph,
|
|
1853
|
+
native: graph_,
|
|
1854
|
+
phase,
|
|
1855
|
+
options,
|
|
1856
|
+
children: preparedChildren,
|
|
1857
|
+
finish: (orders) => {
|
|
1858
|
+
if (finished) return finished;
|
|
1859
|
+
activeOrders = orders;
|
|
1860
|
+
finished = finish();
|
|
1861
|
+
return finished;
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
const finishLayered = () => {
|
|
1865
|
+
if (!pipeline || !initial) throw new Error("Missing layered pipeline");
|
|
1866
|
+
if (initial.done) return initial.value;
|
|
1867
|
+
const completed = pipeline.next(activeOrders?.has(prepared) ? initial.value.finishOrder(activeOrders.get(prepared)) : initial.value.minimize());
|
|
1868
|
+
if (!completed.done) throw new Error("Layered scope yielded more than one crossing phase");
|
|
1869
|
+
return completed.value;
|
|
1870
|
+
};
|
|
1871
|
+
const finish = async () => {
|
|
1872
|
+
for (const complete of finishChildren) await complete();
|
|
1873
|
+
for (const nativeGraph of new Set([graph_, phase?.input.graph].filter((value) => value !== void 0))) for (const child of graph.children ?? []) {
|
|
1874
|
+
const native = nativeGraph.nodes.find((node) => node.id === String(child.id));
|
|
1875
|
+
if (!native || !preparedChildren.has(String(child.id))) continue;
|
|
1876
|
+
phase?.updateNodeSize(native.id, {
|
|
1877
|
+
width: child.width ?? 0,
|
|
1878
|
+
height: child.height ?? 0
|
|
1879
|
+
});
|
|
1880
|
+
Object.assign(native, {
|
|
1881
|
+
width: child.width,
|
|
1882
|
+
height: child.height
|
|
1883
|
+
});
|
|
1884
|
+
for (const port of native.ports ?? []) {
|
|
1885
|
+
const actual = child.ports?.find((candidate) => String(candidate.id) === port.name);
|
|
1886
|
+
if (actual) Object.assign(port, {
|
|
1887
|
+
x: actual.x,
|
|
1888
|
+
y: actual.y,
|
|
1889
|
+
width: actual.width,
|
|
1890
|
+
height: actual.height
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const laidOut = await (algorithm === "sporeCompaction" ? executeElkjs0111Layout({
|
|
1895
|
+
algorithm: "sporeCompaction",
|
|
1896
|
+
graph: graph_,
|
|
1897
|
+
options: {
|
|
1898
|
+
padding,
|
|
1899
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
1900
|
+
}
|
|
1901
|
+
}) : algorithm === "sporeOverlap" ? executeElkjs0111Layout({
|
|
1902
|
+
algorithm: "sporeOverlap",
|
|
1903
|
+
graph: graph_,
|
|
1904
|
+
options: {
|
|
1905
|
+
padding,
|
|
1906
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
1907
|
+
}
|
|
1908
|
+
}) : algorithm === "rectpacking" ? executeElkjs0111Layout({
|
|
1909
|
+
algorithm: "rectpacking",
|
|
1910
|
+
graph: graph_,
|
|
1911
|
+
options: {
|
|
1912
|
+
padding: getOption(layoutOptions, "padding") === void 0 ? 15 : padding,
|
|
1913
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode") ?? 15
|
|
1914
|
+
}
|
|
1915
|
+
}) : algorithm === "random" ? executeElkjs0111Layout({
|
|
1916
|
+
algorithm: "random",
|
|
1917
|
+
graph: graph_,
|
|
1918
|
+
options: {
|
|
1919
|
+
padding: getOption(layoutOptions, "padding") === void 0 ? 15 : padding,
|
|
1920
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
1921
|
+
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
1922
|
+
seed: getNumberOption(layoutOptions, "randomSeed")
|
|
1923
|
+
}
|
|
1924
|
+
}) : algorithm === "box" ? executeElkjs0111Layout({
|
|
1925
|
+
algorithm: "box",
|
|
1926
|
+
graph: graph_,
|
|
1927
|
+
options: {
|
|
1928
|
+
padding: getOption(layoutOptions, "padding") === void 0 ? 15 : padding,
|
|
1929
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
1930
|
+
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
1931
|
+
interactive: getBooleanOption(layoutOptions, "interactive"),
|
|
1932
|
+
expandNodes: getBooleanOption(layoutOptions, "expandNodes"),
|
|
1933
|
+
priority: (node) => {
|
|
1934
|
+
const child = graph.children?.find((candidate) => String(candidate.id) === node.id);
|
|
1935
|
+
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}) : algorithm === "fixed" ? executeElkjs0111Layout({
|
|
1939
|
+
algorithm: "fixed",
|
|
1940
|
+
graph: graph_,
|
|
1941
|
+
options: { direction: getDirection(layoutOptions) }
|
|
1942
|
+
}) : finishLayered());
|
|
1943
|
+
const resultPolicy = laidOut[elkjs0111ResultPolicy];
|
|
1944
|
+
applyLayout(graph, laidOut, padding, layoutOptions, resultPolicy);
|
|
1945
|
+
if (resultPolicy?.providerBounds) {
|
|
1946
|
+
graph.width = resultPolicy.providerBounds.width;
|
|
1947
|
+
graph.height = resultPolicy.providerBounds.height;
|
|
1948
|
+
}
|
|
1949
|
+
for (const restoration of hierarchyRestorations) {
|
|
1950
|
+
const splits = boundaryRoutes.get(restoration.edge);
|
|
1951
|
+
const sections = restoration.edge.sections;
|
|
1952
|
+
if (splits && sections?.length) {
|
|
1953
|
+
const parentPoints = sections.flatMap((section, index) => [
|
|
1954
|
+
...index === 0 ? [section.startPoint] : [],
|
|
1955
|
+
...section.bendPoints ?? [],
|
|
1956
|
+
section.endPoint
|
|
1957
|
+
]);
|
|
1958
|
+
const points = joinCompoundRouteSegments([
|
|
1959
|
+
...splits.source ? [{
|
|
1960
|
+
points: splits.source.points,
|
|
1961
|
+
offset: {
|
|
1962
|
+
x: splits.source.owner.x ?? 0,
|
|
1963
|
+
y: splits.source.owner.y ?? 0
|
|
1964
|
+
}
|
|
1965
|
+
}] : [],
|
|
1966
|
+
{
|
|
1967
|
+
points: parentPoints,
|
|
1968
|
+
offset: {
|
|
1969
|
+
x: 0,
|
|
1970
|
+
y: 0
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
...splits.target ? [{
|
|
1974
|
+
points: splits.target.points,
|
|
1975
|
+
offset: {
|
|
1976
|
+
x: splits.target.owner.x ?? 0,
|
|
1977
|
+
y: splits.target.owner.y ?? 0
|
|
1978
|
+
}
|
|
1979
|
+
}] : []
|
|
1980
|
+
], getBooleanOption(layoutOptions, "unnecessaryBendpoints") === true);
|
|
1981
|
+
const childJunctions = (split) => (split?.junctionPoints ?? []).map((point) => ({
|
|
1982
|
+
x: point.x + (split?.owner.x ?? 0),
|
|
1983
|
+
y: point.y + (split?.owner.y ?? 0)
|
|
1984
|
+
}));
|
|
1985
|
+
const junctions = [
|
|
1986
|
+
...childJunctions(splits.source),
|
|
1987
|
+
...restoration.edge.junctionPoints ?? [],
|
|
1988
|
+
...childJunctions(splits.target)
|
|
1989
|
+
];
|
|
1990
|
+
if (junctions.length) restoration.edge.junctionPoints = junctions;
|
|
1991
|
+
else delete restoration.edge.junctionPoints;
|
|
1992
|
+
restoration.edge.sections = [{
|
|
1993
|
+
...sections[0],
|
|
1994
|
+
startPoint: points[0],
|
|
1995
|
+
endPoint: points.at(-1),
|
|
1996
|
+
bendPoints: points.slice(1, -1)
|
|
1997
|
+
}];
|
|
1998
|
+
}
|
|
1999
|
+
restoration.edge.sources = restoration.sources;
|
|
2000
|
+
restoration.edge.targets = restoration.targets;
|
|
2001
|
+
restoration.edge.source = restoration.source;
|
|
2002
|
+
restoration.edge.target = restoration.target;
|
|
2003
|
+
for (const section of restoration.edge.sections ?? []) {
|
|
2004
|
+
if (section.incomingShape != null && syntheticPortIds.has(String(section.incomingShape))) section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
2005
|
+
if (section.outgoingShape != null && syntheticPortIds.has(String(section.outgoingShape))) section.outgoingShape = restoration.targets?.[0] ?? restoration.target;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
for (const [compound, ports] of authoredPortsByCompound) {
|
|
2009
|
+
for (const port of ports ?? []) {
|
|
2010
|
+
const actual = compound.ports?.find((p) => p.id === port.id);
|
|
2011
|
+
if (actual) Object.assign(port, {
|
|
2012
|
+
x: actual.x,
|
|
2013
|
+
y: actual.y,
|
|
2014
|
+
width: actual.width,
|
|
2015
|
+
height: actual.height
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
compound.ports = ports;
|
|
2019
|
+
}
|
|
2020
|
+
for (const [compound, options_] of authoredOptionsByCompound) compound.layoutOptions = options_;
|
|
2021
|
+
if (hasHierarchy && topdownLayout) for (const child of graph.children ?? []) {
|
|
2022
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
2023
|
+
const position = {
|
|
2024
|
+
x: child.x,
|
|
2025
|
+
y: child.y
|
|
2026
|
+
};
|
|
2027
|
+
await this.#layout(child, {
|
|
2028
|
+
...arguments_,
|
|
2029
|
+
logging: false,
|
|
2030
|
+
measureExecutionTime: false
|
|
2031
|
+
});
|
|
2032
|
+
child.x = position.x;
|
|
2033
|
+
child.y = position.y;
|
|
2034
|
+
}
|
|
2035
|
+
applyInsideSelfLoops(graph, insideSelfLoopBaseHeightByNodeId);
|
|
2036
|
+
if (arguments_.logging || arguments_.measureExecutionTime) graph.logging = {
|
|
2037
|
+
name: "Native TypeScript layout",
|
|
2038
|
+
children: [{ name: String(algorithm) }],
|
|
2039
|
+
...arguments_.measureExecutionTime ? { executionTime: (performance.now() - startedAt) / 1e3 } : {}
|
|
2040
|
+
};
|
|
2041
|
+
return graph;
|
|
2042
|
+
};
|
|
2043
|
+
return prepared;
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
/** Move opposite-direction track sharing apart on the finished layout. */
|
|
2047
|
+
function separated(graph, options) {
|
|
2048
|
+
separateElkOpposingTracks(graph, {
|
|
2049
|
+
...options,
|
|
2050
|
+
...graph.layoutOptions
|
|
2051
|
+
});
|
|
2052
|
+
return graph;
|
|
2053
|
+
}
|
|
2054
|
+
function coordinatePreparedScopes(root) {
|
|
2055
|
+
if (!root.phase || root.children.size === 0) return void 0;
|
|
2056
|
+
const prepared = [root];
|
|
2057
|
+
for (let i = 0; i < prepared.length; i++) prepared.push(...prepared[i].children.values());
|
|
2058
|
+
if (prepared.some((scope) => !scope.phase || !["LAYER_SWEEP", "MEDIAN_LAYER_SWEEP"].includes(scope.options?.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP"))) return void 0;
|
|
2059
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
2060
|
+
for (const scope of prepared) {
|
|
2061
|
+
const phase = scope.phase;
|
|
2062
|
+
scopes.set(scope, {
|
|
2063
|
+
session: createLayerSweepSession(phase.input, phase.orientation, phase.assignment, scope.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP" ? "median" : "mean", 7, { resetSeed: scope === root }),
|
|
2064
|
+
childrenByNodeId: /* @__PURE__ */ new Map(),
|
|
2065
|
+
useBottomUp: true,
|
|
2066
|
+
independentRandom: scope !== root
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
for (const parent of prepared) {
|
|
2070
|
+
const parentScope = scopes.get(parent);
|
|
2071
|
+
for (const [id, child] of parent.children) {
|
|
2072
|
+
const nativeParent = parent.phase.input.graph.nodes.find((node) => node.id === id);
|
|
2073
|
+
const parentPortOrderFixed = [
|
|
2074
|
+
"FIXED_ORDER",
|
|
2075
|
+
"FIXED_POS",
|
|
2076
|
+
"FIXED_RATIO"
|
|
2077
|
+
].includes(parent.phase.input.nodeSettings?.(nativeParent)?.portConstraints ?? "FREE");
|
|
2078
|
+
const ports = nativeParent.ports ?? [];
|
|
2079
|
+
const childPhase = child.phase;
|
|
2080
|
+
const childSession = scopes.get(child).session;
|
|
2081
|
+
const childNodeById = /* @__PURE__ */ new Map();
|
|
2082
|
+
for (const node of childPhase.input.graph.nodes) if (!childNodeById.has(node.id)) childNodeById.set(node.id, node);
|
|
2083
|
+
const parentEdgeById = /* @__PURE__ */ new Map();
|
|
2084
|
+
for (const edge of parent.phase.input.graph.edges) if (!parentEdgeById.has(edge.id)) parentEdgeById.set(edge.id, edge);
|
|
2085
|
+
const flowSides = hierarchicalPortSides(parent.phase.input);
|
|
2086
|
+
const inputPorts = ports.filter((port) => parent.phase.input.portSettings?.(port, nativeParent)?.["port.side"] === flowSides.before).length;
|
|
2087
|
+
const outputPorts = ports.filter((port) => parent.phase.input.portSettings?.(port, nativeParent)?.["port.side"] === flowSides.after).length;
|
|
2088
|
+
const childScope = {
|
|
2089
|
+
...scopes.get(child),
|
|
2090
|
+
useBottomUp: useBottomUpHierarchySweep(childPhase.input, childSession.crossingGraph(), {
|
|
2091
|
+
portOrderFixed: parentPortOrderFixed,
|
|
2092
|
+
inputPorts,
|
|
2093
|
+
outputPorts,
|
|
2094
|
+
deterministic: child.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP"
|
|
2095
|
+
}),
|
|
2096
|
+
alignBoundary: (forward) => {
|
|
2097
|
+
const childLayers = childSession.currentLayers();
|
|
2098
|
+
const layerIndex = forward ? 0 : childLayers.length - 1;
|
|
2099
|
+
const layer = childLayers[layerIndex];
|
|
2100
|
+
if (!layer.every((id$1) => externalPortDummyOf(childNodeById.get(id$1)))) return false;
|
|
2101
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
2102
|
+
const dummyByParentPort = new Map(childPhase.input.graph.nodes.filter((n) => externalPortDummyOf(n)).map((n) => [externalPortDummyOf(n).parentPortId ?? `${n.id}:parent`, n.id]));
|
|
2103
|
+
const ordered = physical.flatMap((portId) => {
|
|
2104
|
+
const [nodeId, portName] = JSON.parse(portId);
|
|
2105
|
+
const dummy = nodeId === id ? dummyByParentPort.get(portName) : void 0;
|
|
2106
|
+
return dummy && layer.includes(dummy) ? [dummy] : [];
|
|
2107
|
+
});
|
|
2108
|
+
if (forward) ordered.reverse();
|
|
2109
|
+
const unique = [...new Set(ordered)];
|
|
2110
|
+
if (unique.length !== layer.length) throw new Error(`Incomplete hierarchical port order for ${id}`);
|
|
2111
|
+
childSession.replaceLayer(layerIndex, unique);
|
|
2112
|
+
return true;
|
|
2113
|
+
},
|
|
2114
|
+
publishBoundary: (forward) => {
|
|
2115
|
+
const childLayers = childSession.currentLayers();
|
|
2116
|
+
const layer = childLayers[forward ? childLayers.length - 1 : 0];
|
|
2117
|
+
if (!layer.every((id$1) => externalPortDummyOf(childNodeById.get(id$1)))) return;
|
|
2118
|
+
const rank = new Map(layer.map((dummy, index) => [externalPortDummyOf(childNodeById.get(dummy)).parentPortId ?? `${dummy}:parent`, index]));
|
|
2119
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
2120
|
+
const boundarySlots = physical.flatMap((portId, index) => {
|
|
2121
|
+
const [nodeId, portName] = JSON.parse(portId);
|
|
2122
|
+
return nodeId === id && rank.has(portName) ? [index] : [];
|
|
2123
|
+
});
|
|
2124
|
+
const boundaryPorts = boundarySlots.map((index) => physical[index]).sort((a, b) => {
|
|
2125
|
+
const portA = JSON.parse(a)[1];
|
|
2126
|
+
const portB = JSON.parse(b)[1];
|
|
2127
|
+
const result = rank.get(portA) - rank.get(portB);
|
|
2128
|
+
return forward ? result : -result;
|
|
2129
|
+
});
|
|
2130
|
+
const updatedPhysical = [...physical];
|
|
2131
|
+
boundarySlots.forEach((slot, index) => {
|
|
2132
|
+
updatedPhysical[slot] = boundaryPorts[index];
|
|
2133
|
+
});
|
|
2134
|
+
const edgePortRank = (edgeId) => {
|
|
2135
|
+
const edge = parentEdgeById.get(edgeId);
|
|
2136
|
+
const portName = edge.sourceId === id ? edge.sourcePort : edge.targetPort;
|
|
2137
|
+
return updatedPhysical.indexOf(JSON.stringify([id, portName]));
|
|
2138
|
+
};
|
|
2139
|
+
const { incoming, outgoing } = parentScope.session.edgeOrders(id);
|
|
2140
|
+
const byPort = (current) => [...current].sort((a, b) => edgePortRank(a) - edgePortRank(b));
|
|
2141
|
+
parentScope.session.reorderNodePorts(id, updatedPhysical, byPort(incoming), byPort(outgoing));
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
childScope.publishBottomUp = () => {
|
|
2145
|
+
childScope.publishBoundary(true);
|
|
2146
|
+
childScope.publishBoundary(false);
|
|
2147
|
+
parentScope.session.lockPortOrder(id);
|
|
2148
|
+
};
|
|
2149
|
+
scopes.set(child, childScope);
|
|
2150
|
+
parentScope.childrenByNodeId.set(id, childScope);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const orders = minimizeHierarchyCrossings(scopes.get(root));
|
|
2154
|
+
return new Map(prepared.map((scope) => [scope, orders.get(scopes.get(scope))]));
|
|
2155
|
+
}
|
|
2156
|
+
function parsePadding(value, fallback = 0) {
|
|
2157
|
+
if (typeof value === "number") return {
|
|
2158
|
+
top: value,
|
|
2159
|
+
right: value,
|
|
2160
|
+
bottom: value,
|
|
2161
|
+
left: value
|
|
2162
|
+
};
|
|
2163
|
+
if (typeof value !== "string") return {
|
|
2164
|
+
top: fallback,
|
|
2165
|
+
right: fallback,
|
|
2166
|
+
bottom: fallback,
|
|
2167
|
+
left: fallback
|
|
2168
|
+
};
|
|
2169
|
+
const padding = {
|
|
2170
|
+
top: 0,
|
|
2171
|
+
right: 0,
|
|
2172
|
+
bottom: 0,
|
|
2173
|
+
left: 0
|
|
2174
|
+
};
|
|
2175
|
+
for (const match of value.matchAll(/(top|right|bottom|left)\s*=\s*(-?\d+(?:\.\d+)?)/g)) {
|
|
2176
|
+
const side = match[1];
|
|
2177
|
+
padding[side] = Number(match[2]);
|
|
2178
|
+
}
|
|
2179
|
+
return padding;
|
|
2180
|
+
}
|
|
2181
|
+
function parseVector(value) {
|
|
2182
|
+
if (typeof value === "object" && value !== null && "x" in value && "y" in value) return {
|
|
2183
|
+
x: Number(value.x),
|
|
2184
|
+
y: Number(value.y)
|
|
2185
|
+
};
|
|
2186
|
+
if (typeof value !== "string") return void 0;
|
|
2187
|
+
const match = value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/);
|
|
2188
|
+
return match ? {
|
|
2189
|
+
x: Number(match[1]),
|
|
2190
|
+
y: Number(match[2])
|
|
2191
|
+
} : void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function applyNodeMicroLayout(graph, globalOptions) {
|
|
2194
|
+
const labelPadding = parsePadding(getOption(globalOptions, "nodeLabels.padding"), 5);
|
|
2195
|
+
for (const node of graph.children ?? []) {
|
|
2196
|
+
const constraints = String(getOption(node.layoutOptions ?? {}, "nodeSize.constraints") ?? "");
|
|
2197
|
+
if (!constraints) continue;
|
|
2198
|
+
const configuredSizeOptions = getOption(node.layoutOptions ?? {}, "nodeSize.options");
|
|
2199
|
+
const sizeOptions = new Set(configuredSizeOptions === void 0 ? ["DEFAULT_MINIMUM_SIZE"] : String(configuredSizeOptions).split(/[\s,;]+/).filter(Boolean));
|
|
2200
|
+
const effectivelyFixedPortLabelSize = constraints.includes("PORT_LABELS") && !constraints.includes("NODE_LABELS") && !constraints.includes("MINIMUM_SIZE");
|
|
2201
|
+
const preserveComputedCompoundSize = (node.children?.length ?? 0) > 0;
|
|
2202
|
+
let width = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.width ?? 0 : 0;
|
|
2203
|
+
let height = effectivelyFixedPortLabelSize || preserveComputedCompoundSize ? node.height ?? 0 : 0;
|
|
2204
|
+
let insideHorizontalInset = 0;
|
|
2205
|
+
let insideVerticalInset = 0;
|
|
2206
|
+
const insideLabelCells = /* @__PURE__ */ new Map();
|
|
2207
|
+
if (constraints.includes("PORTS")) {
|
|
2208
|
+
const spacing = getNumberOption(globalOptions, "spacing.portPort") ?? 10;
|
|
2209
|
+
const sideCounts = {
|
|
2210
|
+
NORTH: 0,
|
|
2211
|
+
EAST: 0,
|
|
2212
|
+
SOUTH: 0,
|
|
2213
|
+
WEST: 0
|
|
2214
|
+
};
|
|
2215
|
+
for (const port of node.ports ?? []) {
|
|
2216
|
+
const side = String(getOption(port.layoutOptions ?? {}, "port.side") ?? "EAST");
|
|
2217
|
+
if (side in sideCounts) sideCounts[side]++;
|
|
2218
|
+
}
|
|
2219
|
+
const nodeId = String(node.id);
|
|
2220
|
+
for (const edge of graph.edges ?? []) {
|
|
2221
|
+
const source = String(edge.sources?.[0] ?? edge.source);
|
|
2222
|
+
const target = String(edge.targets?.[0] ?? edge.target);
|
|
2223
|
+
if (source === nodeId) sideCounts.EAST++;
|
|
2224
|
+
if (target === nodeId) sideCounts.WEST++;
|
|
2225
|
+
}
|
|
2226
|
+
width = Math.max(width, (Math.max(sideCounts.NORTH, sideCounts.SOUTH) + 1) * spacing);
|
|
2227
|
+
height = Math.max(height, (Math.max(sideCounts.EAST, sideCounts.WEST) + 1) * spacing);
|
|
2228
|
+
if (!preserveComputedCompoundSize && sideCounts.NORTH === 0 && sideCounts.SOUTH === 0) width = 0;
|
|
2229
|
+
if (!preserveComputedCompoundSize && sideCounts.EAST === 0 && sideCounts.WEST === 0) height = 0;
|
|
2230
|
+
}
|
|
2231
|
+
if (constraints.includes("NODE_LABELS")) {
|
|
2232
|
+
for (const label of node.labels ?? []) {
|
|
2233
|
+
if (!label.text) continue;
|
|
2234
|
+
const placement = String(getOption({
|
|
2235
|
+
...globalOptions,
|
|
2236
|
+
...node.layoutOptions,
|
|
2237
|
+
...label.layoutOptions
|
|
2238
|
+
}, "nodeLabels.placement") ?? "");
|
|
2239
|
+
if (!placement) continue;
|
|
2240
|
+
const labelWidth = label.width ?? 0;
|
|
2241
|
+
const labelHeight = label.height ?? 0;
|
|
2242
|
+
if (placement.includes("OUTSIDE")) {
|
|
2243
|
+
if (!sizeOptions.has("OUTSIDE_NODE_LABELS_OVERHANG")) width = Math.max(width, labelWidth);
|
|
2244
|
+
} else {
|
|
2245
|
+
width = Math.max(width, labelWidth + labelPadding.left + labelPadding.right);
|
|
2246
|
+
const verticalInset = labelHeight * (placement.includes("V_CENTER") ? 1 : 2) + labelPadding.top + labelPadding.bottom;
|
|
2247
|
+
height = Math.max(height, verticalInset);
|
|
2248
|
+
insideHorizontalInset = Math.max(insideHorizontalInset, labelPadding.left + labelPadding.right);
|
|
2249
|
+
insideVerticalInset = Math.max(insideVerticalInset, verticalInset);
|
|
2250
|
+
const key = `${placement.includes("V_CENTER") ? "center" : placement.includes("V_BOTTOM") ? "bottom" : "top"}:${placement.includes("H_CENTER") ? "center" : placement.includes("H_RIGHT") ? "right" : "left"}`;
|
|
2251
|
+
const cell = insideLabelCells.get(key) ?? {
|
|
2252
|
+
width: 0,
|
|
2253
|
+
height: 0
|
|
2254
|
+
};
|
|
2255
|
+
cell.width = Math.max(cell.width, labelWidth);
|
|
2256
|
+
cell.height += labelHeight;
|
|
2257
|
+
insideLabelCells.set(key, cell);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (insideLabelCells.size > 0) {
|
|
2261
|
+
const rows = [
|
|
2262
|
+
"top",
|
|
2263
|
+
"center",
|
|
2264
|
+
"bottom"
|
|
2265
|
+
];
|
|
2266
|
+
const columns = [
|
|
2267
|
+
"left",
|
|
2268
|
+
"center",
|
|
2269
|
+
"right"
|
|
2270
|
+
];
|
|
2271
|
+
const cellWidth = (row, column) => insideLabelCells.get(`${row}:${column}`)?.width ?? 0;
|
|
2272
|
+
const forceTabular = sizeOptions.has("FORCE_TABULAR_NODE_LABELS");
|
|
2273
|
+
const asymmetrical = sizeOptions.has("ASYMMETRICAL");
|
|
2274
|
+
const globalColumns = columns.map((column) => Math.max(...rows.map((row) => cellWidth(row, column))));
|
|
2275
|
+
const labelGridWidth = forceTabular ? globalColumns.reduce((sum, value) => sum + value, 0) : Math.max(...rows.map((row) => {
|
|
2276
|
+
const left = cellWidth(row, "left");
|
|
2277
|
+
const center = cellWidth(row, "center");
|
|
2278
|
+
const right = cellWidth(row, "right");
|
|
2279
|
+
return asymmetrical ? left + center + right : 2 * Math.max(left, right) + center;
|
|
2280
|
+
}));
|
|
2281
|
+
const labelGridHeight = rows.reduce((sum, row) => sum + Math.max(...columns.map((column) => insideLabelCells.get(`${row}:${column}`)?.height ?? 0)), 0);
|
|
2282
|
+
width = Math.max(width, labelGridWidth + labelPadding.left + labelPadding.right);
|
|
2283
|
+
height = Math.max(height, labelGridHeight + labelPadding.top + labelPadding.bottom);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
if (constraints.includes("MINIMUM_SIZE")) {
|
|
2287
|
+
const configuredMinimum = parseVector(getOption(node.layoutOptions ?? {}, "nodeSize.minimum"));
|
|
2288
|
+
const minimum = {
|
|
2289
|
+
x: configuredMinimum?.x && configuredMinimum.x > 0 ? configuredMinimum.x : sizeOptions.has("DEFAULT_MINIMUM_SIZE") ? 20 : 0,
|
|
2290
|
+
y: configuredMinimum?.y && configuredMinimum.y > 0 ? configuredMinimum.y : sizeOptions.has("DEFAULT_MINIMUM_SIZE") ? 20 : 0
|
|
2291
|
+
};
|
|
2292
|
+
if (sizeOptions.has("MINIMUM_SIZE_ACCOUNTS_FOR_PADDING")) {
|
|
2293
|
+
width = Math.max(width, minimum.x + insideHorizontalInset);
|
|
2294
|
+
height = Math.max(height, minimum.y + insideVerticalInset);
|
|
2295
|
+
} else {
|
|
2296
|
+
width = Math.max(width, minimum.x);
|
|
2297
|
+
height = Math.max(height, minimum.y);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
sizeOptions.has("COMPUTE_PADDING");
|
|
2301
|
+
node.width = width;
|
|
2302
|
+
node.height = height;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
function getOption(options, suffix) {
|
|
2306
|
+
const exactKeys = [
|
|
2307
|
+
suffix,
|
|
2308
|
+
`elk.${suffix}`,
|
|
2309
|
+
`org.eclipse.elk.${suffix}`
|
|
2310
|
+
];
|
|
2311
|
+
for (const key of exactKeys) if (options[key] !== void 0) return options[key];
|
|
2312
|
+
}
|
|
2313
|
+
const ergonomicallyMappedLayeredSettings = new Set([
|
|
2314
|
+
"direction",
|
|
2315
|
+
"padding",
|
|
2316
|
+
"spacing.node",
|
|
2317
|
+
"spacing.layer"
|
|
2318
|
+
]);
|
|
2319
|
+
function coerceLayeredOptionValue(value, type) {
|
|
2320
|
+
if (type === "BOOLEAN") {
|
|
2321
|
+
if (typeof value === "boolean") return value;
|
|
2322
|
+
if (typeof value === "string") return value.toLowerCase() === "true";
|
|
2323
|
+
}
|
|
2324
|
+
if (type === "DOUBLE" || type === "INT") {
|
|
2325
|
+
if (typeof value === "number") return value;
|
|
2326
|
+
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
|
2327
|
+
}
|
|
2328
|
+
return value;
|
|
2329
|
+
}
|
|
2330
|
+
function parseIndividualSpacing(value) {
|
|
2331
|
+
if (typeof value === "object" && value !== null) return value;
|
|
2332
|
+
if (typeof value !== "string") return value;
|
|
2333
|
+
const result = {};
|
|
2334
|
+
for (const entry of value.split(/;,;|;/)) {
|
|
2335
|
+
const match = entry.match(/^\s*(?:org\.eclipse\.elk\.)?(?:layered\.)?([^:]+)\s*:\s*(-?\d+(?:\.\d+)?)\s*$/);
|
|
2336
|
+
if (!match) continue;
|
|
2337
|
+
const sourceName = match[1];
|
|
2338
|
+
const definition = elkLayeredOptionDefinitions.find((candidate) => {
|
|
2339
|
+
const suffix = candidate.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
2340
|
+
return candidate.name === sourceName || suffix === sourceName;
|
|
2341
|
+
});
|
|
2342
|
+
if (definition) result[definition.name] = Number(match[2]);
|
|
2343
|
+
}
|
|
2344
|
+
return result;
|
|
2345
|
+
}
|
|
2346
|
+
function parseMargin(value) {
|
|
2347
|
+
if (typeof value === "object" && value !== null) return value;
|
|
2348
|
+
if (typeof value !== "string") return value;
|
|
2349
|
+
return parsePadding(value, 0);
|
|
2350
|
+
}
|
|
2351
|
+
const layeredOptionAliases = /* @__PURE__ */ new Map();
|
|
2352
|
+
for (const definition of elkLayeredOptionDefinitions) {
|
|
2353
|
+
const suffix = definition.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
2354
|
+
[
|
|
2355
|
+
definition.name,
|
|
2356
|
+
suffix,
|
|
2357
|
+
`elk.${suffix}`,
|
|
2358
|
+
definition.elkId
|
|
2359
|
+
].forEach((key, rank) => {
|
|
2360
|
+
const current = layeredOptionAliases.get(key);
|
|
2361
|
+
if (!current || rank < current.rank) layeredOptionAliases.set(key, {
|
|
2362
|
+
definition,
|
|
2363
|
+
rank
|
|
2364
|
+
});
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
const indexByIdCache = /* @__PURE__ */ new WeakMap();
|
|
2368
|
+
/** First element whose stringified id matches, like `find`, via a per-array index. */
|
|
2369
|
+
function findById(items, id) {
|
|
2370
|
+
if (!items) return void 0;
|
|
2371
|
+
let cached = indexByIdCache.get(items);
|
|
2372
|
+
if (!cached || cached.length !== items.length) {
|
|
2373
|
+
const index = /* @__PURE__ */ new Map();
|
|
2374
|
+
items.forEach((item, position$1) => {
|
|
2375
|
+
const key = String(item.id);
|
|
2376
|
+
if (!index.has(key)) index.set(key, position$1);
|
|
2377
|
+
});
|
|
2378
|
+
indexByIdCache.set(items, cached = {
|
|
2379
|
+
length: items.length,
|
|
2380
|
+
index
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
const position = cached.index.get(id);
|
|
2384
|
+
return position === void 0 ? void 0 : {
|
|
2385
|
+
item: items[position],
|
|
2386
|
+
position
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function getElementLayeredSettings(options) {
|
|
2390
|
+
const settings = {};
|
|
2391
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
2392
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2393
|
+
const alias = layeredOptionAliases.get(key);
|
|
2394
|
+
if (!alias || value === void 0) continue;
|
|
2395
|
+
const current = chosen.get(alias.definition);
|
|
2396
|
+
if (!current || alias.rank < current.rank) chosen.set(alias.definition, {
|
|
2397
|
+
rank: alias.rank,
|
|
2398
|
+
value
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
for (const [definition, { value }] of chosen) {
|
|
2402
|
+
const vectorMatch = definition.name === "port.anchor" && typeof value === "string" ? value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/) : void 0;
|
|
2403
|
+
settings[definition.name] = vectorMatch ? {
|
|
2404
|
+
x: Number(vectorMatch[1]),
|
|
2405
|
+
y: Number(vectorMatch[2])
|
|
2406
|
+
} : definition.name === "spacing.individual" ? parseIndividualSpacing(value) : definition.name === "spacing.portsSurrounding" ? parseMargin(value) : coerceLayeredOptionValue(value, definition.type);
|
|
2407
|
+
}
|
|
2408
|
+
return settings;
|
|
2409
|
+
}
|
|
2410
|
+
function getLayeredSettings(options) {
|
|
2411
|
+
const settings = getElementLayeredSettings(options);
|
|
2412
|
+
const baseValue = settings["spacing.baseValue"];
|
|
2413
|
+
if (baseValue !== void 0) for (const [name, defaultValue] of [
|
|
2414
|
+
["spacing.componentComponent", 20],
|
|
2415
|
+
["spacing.edgeEdge", 10],
|
|
2416
|
+
["spacing.edgeLabel", 2],
|
|
2417
|
+
["spacing.edgeNode", 10],
|
|
2418
|
+
["spacing.labelLabel", 0],
|
|
2419
|
+
["spacing.labelNode", 5],
|
|
2420
|
+
["spacing.labelPortHorizontal", 1],
|
|
2421
|
+
["spacing.labelPortVertical", 1],
|
|
2422
|
+
["spacing.nodeSelfLoop", 10],
|
|
2423
|
+
["spacing.portPort", 10],
|
|
2424
|
+
["spacing.edgeEdgeBetweenLayers", 10],
|
|
2425
|
+
["spacing.edgeNodeBetweenLayers", 10]
|
|
2426
|
+
]) settings[name] ??= baseValue * defaultValue / 20;
|
|
2427
|
+
for (const name of ergonomicallyMappedLayeredSettings) delete settings[name];
|
|
2428
|
+
return settings;
|
|
2429
|
+
}
|
|
2430
|
+
function getNumberOption(options, suffix) {
|
|
2431
|
+
const value = getOption(options, suffix);
|
|
2432
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2433
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2434
|
+
const parsed = Number(value);
|
|
2435
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function getBooleanOption(options, suffix) {
|
|
2439
|
+
const value = getOption(options, suffix);
|
|
2440
|
+
if (typeof value === "boolean") return value;
|
|
2441
|
+
if (typeof value === "string") {
|
|
2442
|
+
if (value.toLowerCase() === "true") return true;
|
|
2443
|
+
if (value.toLowerCase() === "false") return false;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function getDirection(options) {
|
|
2447
|
+
const direction = String(getOption(options, "direction") ?? "RIGHT").toLowerCase();
|
|
2448
|
+
return direction === "up" || direction === "left" || direction === "down" ? direction : "right";
|
|
2449
|
+
}
|
|
2450
|
+
function endpoint(value, portOwnerById) {
|
|
2451
|
+
const id = String(value);
|
|
2452
|
+
const ownerId = portOwnerById.get(id);
|
|
2453
|
+
return ownerId === void 0 ? { nodeId: id } : {
|
|
2454
|
+
nodeId: ownerId,
|
|
2455
|
+
port: id
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function isInsideSelfLoop(root, edge, expectedNodeId) {
|
|
2459
|
+
const source = String(edge.sources?.[0] ?? edge.source);
|
|
2460
|
+
if (source !== String(edge.targets?.[0] ?? edge.target) || expectedNodeId !== void 0 && source !== expectedNodeId) return false;
|
|
2461
|
+
const node = root.children?.find((child) => String(child.id) === source);
|
|
2462
|
+
return getBooleanOption(node?.layoutOptions ?? {}, "insideSelfLoops.activate") === true && getBooleanOption(edge.layoutOptions ?? {}, "insideSelfLoops.yo") === true;
|
|
2463
|
+
}
|
|
2464
|
+
function applyInsideSelfLoops(root, baseHeightByNodeId) {
|
|
2465
|
+
const indexByNodeId = /* @__PURE__ */ new Map();
|
|
2466
|
+
for (const edge of root.edges ?? []) {
|
|
2467
|
+
if (!isInsideSelfLoop(root, edge)) continue;
|
|
2468
|
+
const nodeId = String(edge.sources?.[0] ?? edge.source);
|
|
2469
|
+
const node = root.children?.find((child) => String(child.id) === nodeId);
|
|
2470
|
+
const baseHeight = baseHeightByNodeId.get(nodeId);
|
|
2471
|
+
if (!node || baseHeight === void 0) continue;
|
|
2472
|
+
const index = indexByNodeId.get(nodeId) ?? 0;
|
|
2473
|
+
indexByNodeId.set(nodeId, index + 1);
|
|
2474
|
+
const y = (node.y ?? 0) + baseHeight - 2 + index * 11;
|
|
2475
|
+
edge.sections = [{
|
|
2476
|
+
id: `${String(edge.id)}_s0`,
|
|
2477
|
+
startPoint: {
|
|
2478
|
+
x: node.x ?? 0,
|
|
2479
|
+
y
|
|
2480
|
+
},
|
|
2481
|
+
endPoint: {
|
|
2482
|
+
x: (node.x ?? 0) + (node.width ?? 0),
|
|
2483
|
+
y
|
|
2484
|
+
},
|
|
2485
|
+
incomingShape: edge.sources?.[0] ?? edge.source,
|
|
2486
|
+
outgoingShape: edge.targets?.[0] ?? edge.target
|
|
2487
|
+
}];
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
function toGraph(root, globalOptions = {}) {
|
|
2491
|
+
const children = (root.children ?? []).filter((child) => getBooleanOption(child.layoutOptions ?? {}, "noLayout") !== true);
|
|
2492
|
+
const nodeIds = new Set(children.map((child) => String(child.id)));
|
|
2493
|
+
const portOwnerById = /* @__PURE__ */ new Map();
|
|
2494
|
+
for (const child of children) for (const port of child.ports ?? []) if (port.id !== void 0) portOwnerById.set(String(port.id), String(child.id));
|
|
2495
|
+
const sourcePortIds = new Set((root.edges ?? []).flatMap((edge) => (edge.sources ?? (edge.source === void 0 ? [] : [edge.source])).map(String)));
|
|
2496
|
+
const targetPortIds = new Set((root.edges ?? []).flatMap((edge) => (edge.targets ?? (edge.target === void 0 ? [] : [edge.target])).map(String)));
|
|
2497
|
+
const sourcePortDegree = /* @__PURE__ */ new Map();
|
|
2498
|
+
const targetPortDegree = /* @__PURE__ */ new Map();
|
|
2499
|
+
const edgeModelOrderByPortId = /* @__PURE__ */ new Map();
|
|
2500
|
+
for (const [edgeIndex, edge] of (root.edges ?? []).entries()) {
|
|
2501
|
+
for (const source of edge.sources ?? (edge.source === void 0 ? [] : [edge.source])) {
|
|
2502
|
+
const id = String(source);
|
|
2503
|
+
sourcePortDegree.set(id, (sourcePortDegree.get(id) ?? 0) + 1);
|
|
2504
|
+
if (portOwnerById.has(id) && !edgeModelOrderByPortId.has(id)) edgeModelOrderByPortId.set(id, edgeIndex);
|
|
2505
|
+
}
|
|
2506
|
+
for (const target of edge.targets ?? (edge.target === void 0 ? [] : [edge.target])) {
|
|
2507
|
+
const id = String(target);
|
|
2508
|
+
targetPortDegree.set(id, (targetPortDegree.get(id) ?? 0) + 1);
|
|
2509
|
+
if (portOwnerById.has(id) && !edgeModelOrderByPortId.has(id)) edgeModelOrderByPortId.set(id, edgeIndex);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const orderedPorts = (child) => {
|
|
2513
|
+
const ports = [...child.ports ?? []];
|
|
2514
|
+
if (String(getOption(child.layoutOptions ?? {}, "portConstraints")) !== "FIXED_SIDE") return ports;
|
|
2515
|
+
const canonicalSides = getDirection(globalOptions) === "left" ? {
|
|
2516
|
+
NORTH: "NORTH",
|
|
2517
|
+
EAST: "WEST",
|
|
2518
|
+
SOUTH: "SOUTH",
|
|
2519
|
+
WEST: "EAST"
|
|
2520
|
+
} : getDirection(globalOptions) === "down" ? {
|
|
2521
|
+
NORTH: "WEST",
|
|
2522
|
+
EAST: "SOUTH",
|
|
2523
|
+
SOUTH: "EAST",
|
|
2524
|
+
WEST: "NORTH"
|
|
2525
|
+
} : getDirection(globalOptions) === "up" ? {
|
|
2526
|
+
NORTH: "EAST",
|
|
2527
|
+
EAST: "SOUTH",
|
|
2528
|
+
SOUTH: "WEST",
|
|
2529
|
+
WEST: "NORTH"
|
|
2530
|
+
} : {
|
|
2531
|
+
NORTH: "NORTH",
|
|
2532
|
+
EAST: "EAST",
|
|
2533
|
+
SOUTH: "SOUTH",
|
|
2534
|
+
WEST: "WEST"
|
|
2535
|
+
};
|
|
2536
|
+
const side = (port) => canonicalSides[String(getOption(port.layoutOptions ?? {}, "port.side"))] ?? "UNDEFINED";
|
|
2537
|
+
const sideOrder = [
|
|
2538
|
+
"NORTH",
|
|
2539
|
+
"EAST",
|
|
2540
|
+
"SOUTH",
|
|
2541
|
+
"WEST"
|
|
2542
|
+
];
|
|
2543
|
+
ports.sort((left, right) => {
|
|
2544
|
+
const leftSide = side(left);
|
|
2545
|
+
const rightSide = side(right);
|
|
2546
|
+
const sideDifference = sideOrder.indexOf(leftSide) - sideOrder.indexOf(rightSide);
|
|
2547
|
+
if (sideDifference !== 0) return sideDifference;
|
|
2548
|
+
if (String(getOption(globalOptions, "layered.portSortingStrategy")) === "PORT_DEGREE") {
|
|
2549
|
+
if (leftSide === "EAST") return (sourcePortDegree.get(String(right.id)) ?? 0) - (sourcePortDegree.get(String(left.id)) ?? 0);
|
|
2550
|
+
if (leftSide === "WEST") return (targetPortDegree.get(String(left.id)) ?? 0) - (targetPortDegree.get(String(right.id)) ?? 0);
|
|
2551
|
+
}
|
|
2552
|
+
const modelOrderStrategy = String(getOption(globalOptions, "layered.considerModelOrder.strategy") ?? "NONE");
|
|
2553
|
+
const usePortModelOrder = getBooleanOption(globalOptions, "layered.considerModelOrder.portModelOrder") === true;
|
|
2554
|
+
if (modelOrderStrategy !== "NONE" && !usePortModelOrder) {
|
|
2555
|
+
const leftOrder = edgeModelOrderByPortId.get(String(left.id));
|
|
2556
|
+
const rightOrder = edgeModelOrderByPortId.get(String(right.id));
|
|
2557
|
+
if (leftOrder === void 0 && rightOrder === void 0) return 0;
|
|
2558
|
+
if (leftOrder !== rightOrder) return (leftOrder ?? Infinity) - (rightOrder ?? Infinity);
|
|
2559
|
+
}
|
|
2560
|
+
return (leftSide === "WEST" ? -1 : 1) * ((child.ports?.indexOf(left) ?? 0) - (child.ports?.indexOf(right) ?? 0));
|
|
2561
|
+
});
|
|
2562
|
+
return ports;
|
|
2563
|
+
};
|
|
2564
|
+
const native = createGraph({
|
|
2565
|
+
id: String(root.id),
|
|
2566
|
+
nodes: children.map((child) => ({
|
|
2567
|
+
id: String(child.id),
|
|
2568
|
+
x: child.x,
|
|
2569
|
+
y: child.y,
|
|
2570
|
+
...parsePosition(getOption(child.layoutOptions ?? {}, "position")),
|
|
2571
|
+
width: child.width,
|
|
2572
|
+
height: child.height,
|
|
2573
|
+
label: child.labels?.[0]?.text,
|
|
2574
|
+
ports: orderedPorts(child).filter((port) => getBooleanOption(port.layoutOptions ?? {}, "noLayout") !== true).map((port) => ({
|
|
2575
|
+
name: String(port.id),
|
|
2576
|
+
direction: sourcePortIds.has(String(port.id)) ? targetPortIds.has(String(port.id)) ? "inout" : "out" : targetPortIds.has(String(port.id)) ? "in" : "inout",
|
|
2577
|
+
x: port.x,
|
|
2578
|
+
y: port.y,
|
|
2579
|
+
width: port.width,
|
|
2580
|
+
height: port.height
|
|
2581
|
+
}))
|
|
2582
|
+
})),
|
|
2583
|
+
edges: (root.edges ?? []).flatMap((edge) => {
|
|
2584
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true) return [];
|
|
2585
|
+
if (isInsideSelfLoop(root, edge)) return [];
|
|
2586
|
+
const source = endpoint(edge.sources?.[0] ?? edge.source, portOwnerById);
|
|
2587
|
+
const target = endpoint(edge.targets?.[0] ?? edge.target, portOwnerById);
|
|
2588
|
+
const labels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
2589
|
+
const labelLabelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
2590
|
+
const labelWidth = Math.max(0, ...labels.map((label) => label.width ?? 0));
|
|
2591
|
+
const labelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelLabelSpacing;
|
|
2592
|
+
return nodeIds.has(source.nodeId) && nodeIds.has(target.nodeId) ? [{
|
|
2593
|
+
id: String(edge.id),
|
|
2594
|
+
sourceId: source.nodeId,
|
|
2595
|
+
targetId: target.nodeId,
|
|
2596
|
+
sourcePort: source.port,
|
|
2597
|
+
targetPort: target.port,
|
|
2598
|
+
label: edge.labels?.[0]?.text,
|
|
2599
|
+
width: labelWidth,
|
|
2600
|
+
height: labelHeight,
|
|
2601
|
+
points: parsePoints(getOption(edge.layoutOptions ?? {}, "bendPoints"))
|
|
2602
|
+
}] : [];
|
|
2603
|
+
})
|
|
2604
|
+
});
|
|
2605
|
+
for (const node of native.nodes) {
|
|
2606
|
+
const child = children.find((candidate) => String(candidate.id) === node.id);
|
|
2607
|
+
const origin = child && externalPortDummyOf(child);
|
|
2608
|
+
if (origin) attachExternalPortDummy(node, origin);
|
|
2609
|
+
}
|
|
2610
|
+
return native;
|
|
2611
|
+
}
|
|
2612
|
+
function parsePoints(value) {
|
|
2613
|
+
if (typeof value !== "string") return void 0;
|
|
2614
|
+
const points = [...value.matchAll(/\{\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\}/g)].map((match) => ({
|
|
2615
|
+
x: Number(match[1]),
|
|
2616
|
+
y: Number(match[2])
|
|
2617
|
+
}));
|
|
2618
|
+
return points.length > 0 ? points : void 0;
|
|
2619
|
+
}
|
|
2620
|
+
function parsePosition(value) {
|
|
2621
|
+
if (typeof value !== "string") return void 0;
|
|
2622
|
+
const match = value.match(/\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/);
|
|
2623
|
+
return match ? {
|
|
2624
|
+
x: Number(match[1]),
|
|
2625
|
+
y: Number(match[2])
|
|
2626
|
+
} : void 0;
|
|
2627
|
+
}
|
|
2628
|
+
function toSection(edge, points) {
|
|
2629
|
+
const startPoint = points[0];
|
|
2630
|
+
const endPoint = points.at(-1);
|
|
2631
|
+
if (!startPoint || !endPoint) return void 0;
|
|
2632
|
+
return {
|
|
2633
|
+
id: `${String(edge.id)}_s0`,
|
|
2634
|
+
startPoint: { ...startPoint },
|
|
2635
|
+
endPoint: { ...endPoint },
|
|
2636
|
+
incomingShape: edge.sources?.[0] ?? edge.source,
|
|
2637
|
+
outgoingShape: edge.targets?.[0] ?? edge.target,
|
|
2638
|
+
...points.length > 2 ? { bendPoints: points.slice(1, -1).map((point) => ({ ...point })) } : {}
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
function applyLayout(root, graph, padding, layoutOptions, resultPolicy) {
|
|
2642
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2643
|
+
const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
|
|
2644
|
+
for (const child of root.children ?? []) {
|
|
2645
|
+
const node = nodeById.get(String(child.id));
|
|
2646
|
+
if (!node) continue;
|
|
2647
|
+
child.x = node.x;
|
|
2648
|
+
child.y = node.y;
|
|
2649
|
+
child.width = node.width;
|
|
2650
|
+
child.height = node.height;
|
|
2651
|
+
placeNodeLabels(child, layoutOptions);
|
|
2652
|
+
for (const port of child.ports ?? []) {
|
|
2653
|
+
if (getBooleanOption(port.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
2654
|
+
const laidOutPort = node.ports?.find((candidate) => candidate.name === String(port.id));
|
|
2655
|
+
if (!laidOutPort) continue;
|
|
2656
|
+
port.x = laidOutPort.x;
|
|
2657
|
+
port.y = laidOutPort.y;
|
|
2658
|
+
port.width = laidOutPort.width;
|
|
2659
|
+
port.height = laidOutPort.height;
|
|
2660
|
+
}
|
|
2661
|
+
placePortLabels(child, layoutOptions);
|
|
2662
|
+
}
|
|
2663
|
+
for (const edge of root.edges ?? []) {
|
|
2664
|
+
if (resultPolicy?.preserveEdgeSections) {
|
|
2665
|
+
for (const section$1 of edge.sections ?? []) {
|
|
2666
|
+
section$1.incomingShape ??= edge.sources?.[0] ?? edge.source;
|
|
2667
|
+
section$1.outgoingShape ??= edge.targets?.[0] ?? edge.target;
|
|
2668
|
+
}
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
const laidOutEdge = edgeById.get(String(edge.id));
|
|
2672
|
+
if (!laidOutEdge) {
|
|
2673
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true) {
|
|
2674
|
+
for (const section$2 of edge.sections ?? []) {
|
|
2675
|
+
section$2.incomingShape ??= edge.sources?.[0] ?? edge.source;
|
|
2676
|
+
section$2.outgoingShape ??= edge.targets?.[0] ?? edge.target;
|
|
2677
|
+
}
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const section$1 = getParentEdgeSection(root, edge);
|
|
2681
|
+
if (section$1) {
|
|
2682
|
+
edge.sections = [section$1];
|
|
2683
|
+
edge.container = root.id;
|
|
2684
|
+
}
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const nativeJunctions = resultPolicy?.junctionPointsByEdgeId?.get(String(edge.id));
|
|
2688
|
+
if (nativeJunctions) if (nativeJunctions.length) edge.junctionPoints = nativeJunctions.map((point) => ({ ...point }));
|
|
2689
|
+
else delete edge.junctionPoints;
|
|
2690
|
+
const section = toSection(edge, laidOutEdge.points ?? []);
|
|
2691
|
+
edge.sections = section ? [section] : [];
|
|
2692
|
+
if (section) edge.container = root.id;
|
|
2693
|
+
const target = root.children?.find((child) => String(child.id) === String(edge.targets?.[0] ?? edge.target));
|
|
2694
|
+
if (getBooleanOption(target?.layoutOptions ?? {}, "hypernode") === true && (section?.bendPoints?.length ?? 0) > 0) edge.junctionPoints = [section.bendPoints.at(-1)];
|
|
2695
|
+
let labelY = laidOutEdge.y ?? 0;
|
|
2696
|
+
const points = laidOutEdge.points ?? [];
|
|
2697
|
+
const firstPoint = points[0] ?? {
|
|
2698
|
+
x: laidOutEdge.x ?? 0,
|
|
2699
|
+
y: laidOutEdge.y ?? 0
|
|
2700
|
+
};
|
|
2701
|
+
const lastPoint = points.at(-1) ?? firstPoint;
|
|
2702
|
+
const midpointX = (laidOutEdge.x ?? 0) + (laidOutEdge.width ?? 0) / 2;
|
|
2703
|
+
const edgeLabelSpacing = getNumberOption(layoutOptions, "spacing.edgeLabel") ?? 2;
|
|
2704
|
+
const labelLabelSpacing = getNumberOption(layoutOptions, "spacing.labelLabel") ?? 0;
|
|
2705
|
+
const loneLabel = (edge.labels ?? []).filter(isLayoutEdgeLabel).length === 1;
|
|
2706
|
+
for (const label of edge.labels ?? []) {
|
|
2707
|
+
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) {
|
|
2708
|
+
label.x ??= 0;
|
|
2709
|
+
label.y ??= 0;
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
if (!isLayoutEdgeLabel(label)) {
|
|
2713
|
+
label.x ??= 0;
|
|
2714
|
+
label.y ??= 0;
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
const placement = String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER");
|
|
2718
|
+
const width = label.width ?? 0;
|
|
2719
|
+
label.x = loneLabel && placement !== "CENTER" && laidOutEdge.x !== void 0 ? laidOutEdge.x : placement === "TAIL" ? firstPoint.x + edgeLabelSpacing : placement === "HEAD" ? lastPoint.x - width - edgeLabelSpacing : midpointX - width / 2;
|
|
2720
|
+
label.y = labelY;
|
|
2721
|
+
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
if (!resultPolicy?.skipBoundsNormalization) normalizeElkGraphBounds(root, padding, layoutOptions, resultPolicy?.normalizationBounds, resultPolicy?.placementCrossBounds);
|
|
2725
|
+
if (!resultPolicy?.preserveEdgeSections && String(getOption(layoutOptions, "edgeRouting") ?? "ORTHOGONAL") === "ORTHOGONAL") {
|
|
2726
|
+
const direction = getDirection(layoutOptions);
|
|
2727
|
+
applyOrthogonalJunctions(root, direction === "right" || direction === "left", direction === "right" || direction === "down", getBooleanOption(layoutOptions, "layered.mergeEdges") === true, new Set((root.children ?? []).filter((n) => getBooleanOption(n.layoutOptions ?? {}, "hypernode") === true).map((n) => String(n.id))), new Set((root.edges ?? []).filter((e) => getBooleanOption(e.layoutOptions ?? {}, "noLayout") === true || resultPolicy?.junctionPointsByEdgeId?.has(String(e.id)) === true).map((e) => String(e.id))));
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function normalizeElkGraphBounds(root, padding, layoutOptions = {}, compactionBounds, placementCrossBounds) {
|
|
2731
|
+
const layoutEdges = (root.edges ?? []).filter((edge) => edge.container == null || String(edge.container) === String(root.id));
|
|
2732
|
+
const authoredWidth = root.width;
|
|
2733
|
+
const authoredHeight = root.height;
|
|
2734
|
+
const fixedGraphSize = getBooleanOption(layoutOptions, "nodeSize.fixedGraphSize") === true;
|
|
2735
|
+
const layoutChildren = (root.children ?? []).filter((node) => getBooleanOption(node.layoutOptions ?? {}, "noLayout") !== true);
|
|
2736
|
+
let minimumX = compactionBounds?.left ?? Number.POSITIVE_INFINITY;
|
|
2737
|
+
let minimumY = compactionBounds?.top ?? Number.POSITIVE_INFINITY;
|
|
2738
|
+
for (const node of layoutChildren) {
|
|
2739
|
+
minimumX = Math.min(minimumX, node.x ?? 0, ...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0)), ...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0))));
|
|
2740
|
+
minimumY = Math.min(minimumY, node.y ?? 0, ...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0)), ...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0)), ...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0))));
|
|
2741
|
+
}
|
|
2742
|
+
for (const edge of layoutEdges) {
|
|
2743
|
+
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
2744
|
+
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
2745
|
+
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
2746
|
+
if (getBooleanOption(edge.layoutOptions ?? {}, "noLayout") !== true) {
|
|
2747
|
+
const points = (edge.sections ?? []).flatMap((section) => [
|
|
2748
|
+
section.startPoint,
|
|
2749
|
+
...section.bendPoints ?? [],
|
|
2750
|
+
section.endPoint
|
|
2751
|
+
]);
|
|
2752
|
+
minimumX = Math.min(minimumX, ...points.map((point) => point.x));
|
|
2753
|
+
minimumY = Math.min(minimumY, ...points.map((point) => point.y));
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
const shiftX = Number.isFinite(minimumX) ? Math.max(0, padding.left - minimumX) : 0;
|
|
2757
|
+
const shiftY = Number.isFinite(minimumY) ? Math.max(0, padding.top - minimumY) : 0;
|
|
2758
|
+
if (shiftX !== 0 || shiftY !== 0) {
|
|
2759
|
+
for (const node of layoutChildren) {
|
|
2760
|
+
node.x = (node.x ?? 0) + shiftX;
|
|
2761
|
+
node.y = (node.y ?? 0) + shiftY;
|
|
2762
|
+
}
|
|
2763
|
+
for (const edge of layoutEdges) {
|
|
2764
|
+
for (const section of edge.sections ?? []) for (const point of [
|
|
2765
|
+
section.startPoint,
|
|
2766
|
+
...section.bendPoints ?? [],
|
|
2767
|
+
section.endPoint
|
|
2768
|
+
]) {
|
|
2769
|
+
point.x += shiftX;
|
|
2770
|
+
point.y += shiftY;
|
|
2771
|
+
}
|
|
2772
|
+
for (const point of edge.junctionPoints ?? []) {
|
|
2773
|
+
point.x += shiftX;
|
|
2774
|
+
point.y += shiftY;
|
|
2775
|
+
}
|
|
2776
|
+
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
2777
|
+
label.x = (label.x ?? 0) + shiftX;
|
|
2778
|
+
label.y = (label.y ?? 0) + shiftY;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const maximumNodeX = Math.max(0, ...layoutChildren.map((node) => (node.x ?? 0) + (node.width ?? 0)));
|
|
2783
|
+
const maximumNodeY = Math.max(0, ...layoutChildren.map((node) => (node.y ?? 0) + (node.height ?? 0)));
|
|
2784
|
+
const layoutEdgePoints = layoutEdges.flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
2785
|
+
section.startPoint,
|
|
2786
|
+
...section.bendPoints ?? [],
|
|
2787
|
+
section.endPoint
|
|
2788
|
+
]));
|
|
2789
|
+
const wrappingStrategy = String(getOption(layoutOptions, "layered.wrapping.strategy") ?? "OFF");
|
|
2790
|
+
const direction = getDirection(layoutOptions);
|
|
2791
|
+
const laidOutChildById = new Map((root.children ?? []).map((child) => [String(child.id), child]));
|
|
2792
|
+
const childByEndpointId = new Map(laidOutChildById);
|
|
2793
|
+
for (const child of root.children ?? []) for (const port of child.ports ?? []) childByEndpointId.set(String(port.id), child);
|
|
2794
|
+
const wrappedEdgeCount = layoutEdges.filter((edge) => {
|
|
2795
|
+
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
2796
|
+
const target = childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
|
|
2797
|
+
if (!source || !target) return false;
|
|
2798
|
+
return direction === "right" ? (source.x ?? 0) > (target.x ?? 0) : direction === "left" ? (source.x ?? 0) < (target.x ?? 0) : direction === "down" ? (source.y ?? 0) > (target.y ?? 0) : (source.y ?? 0) < (target.y ?? 0);
|
|
2799
|
+
}).length;
|
|
2800
|
+
const hasWrappedEdge = wrappedEdgeCount > 0;
|
|
2801
|
+
const retainPlacementBounds = wrappingStrategy !== "MULTI_EDGE" && !(wrappingStrategy === "SINGLE_EDGE" && hasWrappedEdge) && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") === "NONE" && getBooleanOption(layoutOptions, "layered.feedbackEdges") !== true && String(getOption(layoutOptions, "layered.layering.nodePromotion.strategy") ?? "NONE") !== "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
2802
|
+
const addBoundaryPixel = retainPlacementBounds && !(root.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
2803
|
+
const isSelfLoop = (edge) => {
|
|
2804
|
+
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
2805
|
+
return source !== void 0 && source === childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
|
|
2806
|
+
};
|
|
2807
|
+
const hasSelfLoops = layoutEdges.some(isSelfLoop);
|
|
2808
|
+
const edgeBoundsExtraX = addBoundaryPixel && placementCrossBounds?.axis !== "x" && !(hasSelfLoops && (direction === "right" || direction === "left")) && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9 ? 1 : 0;
|
|
2809
|
+
const edgeBoundsExtraY = addBoundaryPixel && placementCrossBounds?.axis !== "y" && !(hasSelfLoops && (direction === "down" || direction === "up")) && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
|
|
2810
|
+
const singleMultiEdgeCutBoundsExtraY = wrappingStrategy === "MULTI_EDGE" && wrappedEdgeCount === 1 && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9 ? 1 : 0;
|
|
2811
|
+
const postCompactionBoundsExtraX = !compactionBounds && !addBoundaryPixel && !hasWrappedEdge && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.x)) > maximumNodeX + 1e-9 ? .04 : 0;
|
|
2812
|
+
const postCompactionBoundsExtraY = !compactionBounds && !addBoundaryPixel && !hasWrappedEdge && String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !== "NONE" && layoutEdgePoints.length > 0 && Math.max(...layoutEdgePoints.map((point) => point.y)) > maximumNodeY + 1e-9 ? .04 : 0;
|
|
2813
|
+
const calculatedWidth = Math.max(compactionBounds ? compactionBounds.right + shiftX : 0, retainPlacementBounds && placementCrossBounds?.axis === "x" ? placementCrossBounds.maximum + shiftX : 0, ...layoutChildren.flatMap((node) => [
|
|
2814
|
+
(node.x ?? 0) + (node.width ?? 0),
|
|
2815
|
+
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
2816
|
+
...(node.ports ?? []).map((port) => (node.x ?? 0) + (port.x ?? 0) + (port.width ?? 0)),
|
|
2817
|
+
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.x ?? 0) + (port.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)))
|
|
2818
|
+
]), ...layoutEdges.flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.x ?? 0) + (label.width ?? 0))), ...layoutEdges.flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
2819
|
+
section.startPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
2820
|
+
...(section.bendPoints ?? []).map((point) => point.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX)),
|
|
2821
|
+
section.endPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX)
|
|
2822
|
+
]))) + padding.right + postCompactionBoundsExtraX + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up") ? 1 : 0);
|
|
2823
|
+
const calculatedHeight = Math.max(compactionBounds ? compactionBounds.bottom + shiftY : 0, retainPlacementBounds && placementCrossBounds?.axis === "y" ? placementCrossBounds.maximum + shiftY : 0, ...layoutChildren.flatMap((node) => [
|
|
2824
|
+
(node.y ?? 0) + (node.height ?? 0),
|
|
2825
|
+
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
2826
|
+
...(node.ports ?? []).map((port) => (node.y ?? 0) + (port.y ?? 0) + (port.height ?? 0)),
|
|
2827
|
+
...(node.ports ?? []).flatMap((port) => (port.labels ?? []).map((label) => (node.y ?? 0) + (port.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)))
|
|
2828
|
+
]), ...layoutEdges.flatMap((edge) => (edge.labels ?? []).filter(isLayoutEdgeLabel).map((label) => (label.y ?? 0) + (label.height ?? 0) + (String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") === "CENTER" && childByEndpointId.get(String(edge.sources?.[0] ?? edge.source)) !== childByEndpointId.get(String(edge.targets?.[0] ?? edge.target)) ? 1 : 0))), ...layoutEdges.flatMap((edge) => getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true ? [] : (edge.sections ?? []).flatMap((section) => [
|
|
2829
|
+
section.startPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
2830
|
+
...(section.bendPoints ?? []).map((point) => point.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY)),
|
|
2831
|
+
section.endPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY)
|
|
2832
|
+
]))) + padding.bottom + singleMultiEdgeCutBoundsExtraY + postCompactionBoundsExtraY + (getBooleanOption(layoutOptions, "layered.feedbackEdges") === true && (getDirection(layoutOptions) === "right" || getDirection(layoutOptions) === "left") ? 1 : 0);
|
|
2833
|
+
root.width = fixedGraphSize ? authoredWidth ?? 0 : calculatedWidth;
|
|
2834
|
+
root.height = fixedGraphSize ? authoredHeight ?? 0 : calculatedHeight;
|
|
2835
|
+
}
|
|
2836
|
+
function getParentEdgeSection(root, edge) {
|
|
2837
|
+
const sourceId = String(edge.sources?.[0] ?? edge.source);
|
|
2838
|
+
const targetId = String(edge.targets?.[0] ?? edge.target);
|
|
2839
|
+
const rootId = String(root.id);
|
|
2840
|
+
const source = root.children?.find((child) => String(child.id) === sourceId);
|
|
2841
|
+
const target = root.children?.find((child) => String(child.id) === targetId);
|
|
2842
|
+
if (source && targetId === rootId) {
|
|
2843
|
+
const startPoint = {
|
|
2844
|
+
x: (source.x ?? 0) + (source.width ?? 0) / 2,
|
|
2845
|
+
y: (source.y ?? 0) + (source.height ?? 0)
|
|
2846
|
+
};
|
|
2847
|
+
return {
|
|
2848
|
+
id: `${String(edge.id)}_s0`,
|
|
2849
|
+
startPoint,
|
|
2850
|
+
endPoint: {
|
|
2851
|
+
x: startPoint.x,
|
|
2852
|
+
y: 0
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
if (sourceId === rootId && target) {
|
|
2857
|
+
const endPoint = {
|
|
2858
|
+
x: (target.x ?? 0) + (target.width ?? 0) / 2,
|
|
2859
|
+
y: target.y ?? 0
|
|
2860
|
+
};
|
|
2861
|
+
return {
|
|
2862
|
+
id: `${String(edge.id)}_s0`,
|
|
2863
|
+
startPoint: {
|
|
2864
|
+
x: endPoint.x,
|
|
2865
|
+
y: 0
|
|
2866
|
+
},
|
|
2867
|
+
endPoint
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
function placeNodeLabels(node, globalOptions) {
|
|
2872
|
+
const labelPadding = parsePadding(getOption(globalOptions, "nodeLabels.padding"), 5);
|
|
2873
|
+
for (const label of node.labels ?? []) {
|
|
2874
|
+
if (!label.text) continue;
|
|
2875
|
+
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
2876
|
+
const placement = String(getOption({
|
|
2877
|
+
...globalOptions,
|
|
2878
|
+
...node.layoutOptions,
|
|
2879
|
+
...label.layoutOptions
|
|
2880
|
+
}, "nodeLabels.placement") ?? "");
|
|
2881
|
+
if (!placement) continue;
|
|
2882
|
+
const width = label.width ?? 0;
|
|
2883
|
+
const height = label.height ?? 0;
|
|
2884
|
+
const nodeWidth = node.width ?? 0;
|
|
2885
|
+
const nodeHeight = node.height ?? 0;
|
|
2886
|
+
label.x = placement.includes("H_CENTER") ? (nodeWidth - width + labelPadding.left - labelPadding.right) / 2 : placement.includes("H_RIGHT") ? nodeWidth - width - labelPadding.right : labelPadding.left;
|
|
2887
|
+
if (placement.includes("OUTSIDE") && placement.includes("V_TOP")) label.y = -height - Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
|
|
2888
|
+
else if (placement.includes("OUTSIDE") && placement.includes("V_BOTTOM")) label.y = nodeHeight + Number(getOption(globalOptions, "spacing.labelNode") ?? 5);
|
|
2889
|
+
else if (placement.includes("V_CENTER")) label.y = (nodeHeight - height + labelPadding.top - labelPadding.bottom) / 2;
|
|
2890
|
+
else if (placement.includes("V_BOTTOM")) label.y = nodeHeight - height - labelPadding.bottom;
|
|
2891
|
+
else label.y = labelPadding.top;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function placePortLabels(node, globalOptions) {
|
|
2895
|
+
const placement = String(getOption({
|
|
2896
|
+
...globalOptions,
|
|
2897
|
+
...node.layoutOptions
|
|
2898
|
+
}, "portLabels.placement") ?? "OUTSIDE");
|
|
2899
|
+
const inside = placement.includes("INSIDE");
|
|
2900
|
+
const alwaysOtherSide = placement.includes("ALWAYS_OTHER_SAME_SIDE");
|
|
2901
|
+
const spaceEfficient = placement.includes("SPACE_EFFICIENT") || String(getOption(node.layoutOptions ?? {}, "nodeSize.options") ?? "").includes("SPACE_EFFICIENT_PORT_LABELS");
|
|
2902
|
+
const horizontalSpacing = getNumberOption(globalOptions, "spacing.labelPortHorizontal") ?? 1;
|
|
2903
|
+
const verticalSpacing = getNumberOption(globalOptions, "spacing.labelPortVertical") ?? 1;
|
|
2904
|
+
const labelSpacing = getNumberOption(globalOptions, "spacing.labelLabel") ?? 0;
|
|
2905
|
+
const treatAsGroup = getBooleanOption(node.layoutOptions ?? {}, "portLabels.treatAsGroup") ?? false;
|
|
2906
|
+
const placeNextToPort = placement.includes("NEXT_TO_PORT_IF_POSSIBLE") || getBooleanOption(node.layoutOptions ?? {}, "portLabels.nextToPortIfPossible") === true;
|
|
2907
|
+
const nodeWidth = node.width ?? 0;
|
|
2908
|
+
for (const port of node.ports ?? []) {
|
|
2909
|
+
if (getBooleanOption(port.layoutOptions ?? {}, "noLayout") === true) continue;
|
|
2910
|
+
const portWidth = port.width ?? 0;
|
|
2911
|
+
const portHeight = port.height ?? 0;
|
|
2912
|
+
const side = (port.x ?? 0) < 0 ? "WEST" : (port.x ?? 0) >= nodeWidth ? "EAST" : (port.y ?? 0) < 0 ? "NORTH" : "SOUTH";
|
|
2913
|
+
const portsOnSide = (node.ports ?? []).filter((candidate) => {
|
|
2914
|
+
return ((candidate.x ?? 0) < 0 ? "WEST" : (candidate.x ?? 0) >= nodeWidth ? "EAST" : (candidate.y ?? 0) < 0 ? "NORTH" : "SOUTH") === side;
|
|
2915
|
+
});
|
|
2916
|
+
const labels = (port.labels ?? []).filter((label) => Boolean(label.text) && getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true);
|
|
2917
|
+
const totalLabelHeight = labels.reduce((sum, label) => sum + (label.height ?? 0), 0) + Math.max(0, labels.length - 1) * labelSpacing;
|
|
2918
|
+
let stackedY = labels.length > 1 ? inside || placeNextToPort ? treatAsGroup ? (portHeight - totalLabelHeight) / 2 : (portHeight - (labels[0]?.height ?? 0)) / 2 : portHeight + verticalSpacing : void 0;
|
|
2919
|
+
for (const label of labels) {
|
|
2920
|
+
if (!label.text) continue;
|
|
2921
|
+
const width = label.width ?? 0;
|
|
2922
|
+
const height = label.height ?? 0;
|
|
2923
|
+
if (side === "EAST") {
|
|
2924
|
+
label.x = inside ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
2925
|
+
label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
|
|
2926
|
+
} else if (side === "WEST") {
|
|
2927
|
+
label.x = inside ? portWidth + horizontalSpacing : -width - horizontalSpacing;
|
|
2928
|
+
label.y = stackedY ?? (inside || placeNextToPort ? (portHeight - height) / 2 : alwaysOtherSide ? -height - verticalSpacing : portHeight + verticalSpacing);
|
|
2929
|
+
} else if (side === "NORTH") {
|
|
2930
|
+
label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
2931
|
+
label.y = inside ? portHeight + verticalSpacing : -height - verticalSpacing;
|
|
2932
|
+
} else {
|
|
2933
|
+
label.x = inside || placeNextToPort ? (portWidth - width) / 2 : alwaysOtherSide ? -width - horizontalSpacing : portWidth + horizontalSpacing;
|
|
2934
|
+
label.y = inside ? -height - verticalSpacing : portHeight + verticalSpacing;
|
|
2935
|
+
}
|
|
2936
|
+
if (spaceEfficient && portsOnSide[0] === port && portsOnSide.length >= 2 && !placeNextToPort) if (side === "EAST" || side === "WEST") label.y = -height - verticalSpacing;
|
|
2937
|
+
else label.x = -width - horizontalSpacing;
|
|
2938
|
+
if (stackedY !== void 0) stackedY += height + labelSpacing;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
/** Edges crossing `group`, in the order ELK's CompoundGraphPreprocessor creates its boundary ports. */
|
|
2943
|
+
function elkBoundaryEdgeOrder(group, scope, originals) {
|
|
2944
|
+
const edges = [];
|
|
2945
|
+
const collect = (node) => {
|
|
2946
|
+
edges.push(...node.edges ?? []);
|
|
2947
|
+
for (const child of node.children ?? []) collect(child);
|
|
2948
|
+
};
|
|
2949
|
+
collect(scope);
|
|
2950
|
+
const endpoints = (edge) => originals.get(edge) ?? {
|
|
2951
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
2952
|
+
targetId: String(edge.targets?.[0] ?? edge.target)
|
|
2953
|
+
};
|
|
2954
|
+
const order = (node) => {
|
|
2955
|
+
const inside = /* @__PURE__ */ new Set();
|
|
2956
|
+
const mark = (parent) => {
|
|
2957
|
+
for (const child of parent.children ?? []) {
|
|
2958
|
+
inside.add(String(child.id));
|
|
2959
|
+
for (const port of child.ports ?? []) inside.add(String(port.id));
|
|
2960
|
+
mark(child);
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
mark(node);
|
|
2964
|
+
const leaves = (edge) => inside.has(endpoints(edge).sourceId) !== inside.has(endpoints(edge).targetId);
|
|
2965
|
+
const result = /* @__PURE__ */ new Set();
|
|
2966
|
+
for (const child of node.children ?? []) if (child.children?.length) {
|
|
2967
|
+
for (const edge of order(child)) if (leaves(edge)) result.add(edge);
|
|
2968
|
+
}
|
|
2969
|
+
for (const child of node.children ?? []) {
|
|
2970
|
+
for (const port of (child.ports ?? []).map((p) => String(p.id))) {
|
|
2971
|
+
for (const edge of edges) if (endpoints(edge).sourceId === port && leaves(edge)) result.add(edge);
|
|
2972
|
+
for (const edge of edges) if (endpoints(edge).targetId === port && leaves(edge)) result.add(edge);
|
|
2973
|
+
}
|
|
2974
|
+
const id = String(child.id);
|
|
2975
|
+
for (const edge of edges) {
|
|
2976
|
+
const { sourceId, targetId } = endpoints(edge);
|
|
2977
|
+
if ((sourceId === id || targetId === id) && leaves(edge)) result.add(edge);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return [...result];
|
|
2981
|
+
};
|
|
2982
|
+
return order(group);
|
|
2983
|
+
}
|
|
2984
|
+
/** Creation order of hierarchy segments in ELK's CompoundGraphPreprocessor. */
|
|
2985
|
+
const hierarchySegmentRank = /* @__PURE__ */ new WeakMap();
|
|
2986
|
+
const segmentOrderByScope = /* @__PURE__ */ new WeakMap();
|
|
2987
|
+
const originalEdgeId = /* @__PURE__ */ new WeakMap();
|
|
2988
|
+
/**
|
|
2989
|
+
* Segments inside `group`, in creation order: the inner pass connects child
|
|
2990
|
+
* groups' exported ports first, then the outer pass leaves `group` itself.
|
|
2991
|
+
*/
|
|
2992
|
+
function elkSegmentOrder(group, scope, originals, ownBoundary) {
|
|
2993
|
+
const order = /* @__PURE__ */ new Map();
|
|
2994
|
+
const add = (edge) => {
|
|
2995
|
+
const id = originalEdgeId.get(edge) ?? String(edge.id);
|
|
2996
|
+
if (!order.has(id)) order.set(id, order.size);
|
|
2997
|
+
};
|
|
2998
|
+
for (const child of group.children ?? []) if (child.children?.length) for (const edge of elkBoundaryEdgeOrder(child, scope, originals)) add(edge);
|
|
2999
|
+
if (ownBoundary) for (const edge of elkBoundaryEdgeOrder(group, scope, originals)) add(edge);
|
|
3000
|
+
return order;
|
|
3001
|
+
}
|
|
3002
|
+
const isPostCompactionKey = (key) => /(^|\.)compaction\.postCompaction\.strategy$/.test(key);
|
|
3003
|
+
function usesPostCompaction(options, node) {
|
|
3004
|
+
const active = (values) => Object.entries(values ?? {}).some(([key, value]) => isPostCompactionKey(key) && String(value) !== "NONE");
|
|
3005
|
+
const visit = (current) => active(current.layoutOptions) || active(current.properties) || (current.children ?? []).some(visit);
|
|
3006
|
+
return active(options) || visit(node);
|
|
3007
|
+
}
|
|
3008
|
+
function disablePostCompaction(node) {
|
|
3009
|
+
for (const values of [node.layoutOptions, node.properties]) for (const key of Object.keys(values ?? {})) if (isPostCompactionKey(key)) values[key] = "NONE";
|
|
3010
|
+
for (const child of node.children ?? []) disablePostCompaction(child);
|
|
3011
|
+
return node;
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Alternative random seeds worth trying for a compound layout, unless the
|
|
3015
|
+
* graph chooses its hierarchical greedy switch explicitly.
|
|
3016
|
+
*/
|
|
3017
|
+
function crossingVariants(options, graph) {
|
|
3018
|
+
const own = {
|
|
3019
|
+
...options,
|
|
3020
|
+
...graph.properties,
|
|
3021
|
+
...graph.layoutOptions
|
|
3022
|
+
};
|
|
3023
|
+
if (!(graph.children ?? []).some((child) => child.children?.length) || getOption(own, "hierarchyHandling") !== "INCLUDE_CHILDREN" || getOption(own, "layered.crossingMinimization.greedySwitchHierarchical.type") !== void 0) return [];
|
|
3024
|
+
return [{ "elk.randomSeed": "2" }, { "elk.randomSeed": "3" }];
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Whether two edges between different ends can share a port: an explicit port
|
|
3028
|
+
* carrying two or more of them, or implicit ports that merge because some
|
|
3029
|
+
* scope merges edges or a node is a hypernode.
|
|
3030
|
+
*/
|
|
3031
|
+
function mayShareTracks(graph, options) {
|
|
3032
|
+
const merging = (node, inherited = {}) => {
|
|
3033
|
+
const settings = getElementLayeredSettings({
|
|
3034
|
+
...inherited,
|
|
3035
|
+
...node.properties,
|
|
3036
|
+
...node.layoutOptions
|
|
3037
|
+
});
|
|
3038
|
+
return settings.mergeEdges === true || settings.hypernode === true;
|
|
3039
|
+
};
|
|
3040
|
+
const anyMerging = (node) => merging(node) || (node.children ?? []).some(anyMerging);
|
|
3041
|
+
if (merging(graph, options) || (graph.children ?? []).some(anyMerging)) return true;
|
|
3042
|
+
const ports = /* @__PURE__ */ new Set();
|
|
3043
|
+
const collectPorts = (node) => {
|
|
3044
|
+
for (const port of node.ports ?? []) ports.add(String(port.id));
|
|
3045
|
+
for (const child of node.children ?? []) collectPorts(child);
|
|
3046
|
+
};
|
|
3047
|
+
collectPorts(graph);
|
|
3048
|
+
if (!ports.size) return false;
|
|
3049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3050
|
+
const visit = (node) => {
|
|
3051
|
+
for (const edge of node.edges ?? []) {
|
|
3052
|
+
const source = String(edge.sources?.[0] ?? edge.source), target = String(edge.targets?.[0] ?? edge.target);
|
|
3053
|
+
if (source === target) continue;
|
|
3054
|
+
for (const end of [source, target]) {
|
|
3055
|
+
if (!ports.has(end)) continue;
|
|
3056
|
+
if (seen.has(end)) return true;
|
|
3057
|
+
seen.add(end);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return (node.children ?? []).some(visit);
|
|
3061
|
+
};
|
|
3062
|
+
return visit(graph);
|
|
3063
|
+
}
|
|
3064
|
+
/** Whether the graph asks for more than ELK's default layered thoroughness (7). */
|
|
3065
|
+
function thorough(options, graph) {
|
|
3066
|
+
const own = {
|
|
3067
|
+
...options,
|
|
3068
|
+
...graph.properties,
|
|
3069
|
+
...graph.layoutOptions
|
|
3070
|
+
};
|
|
3071
|
+
return Number(getOption(own, "layered.thoroughness") ?? 7) > 7;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
//#endregion
|
|
3075
|
+
export { scoreStatechartLayout as a, layoutStatechart as i, getElkRoutes as n, compileStatechartLayout as r, ELK as t };
|