@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
package/src/elkjs/index.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
import { hierarchicalPortSides } from "../layered/hierarchical-port-phases";
|
|
2
|
+
import { isPerpendicularPortRestored } from "../layered/hierarchical-port-restoration";
|
|
3
|
+
import { useBottomUpHierarchySweep } from "../layered/hierarchy-sweepiness";
|
|
4
|
+
import {
|
|
5
|
+
createLayeredScopePipeline,
|
|
6
|
+
type LayeredCrossingPhase,
|
|
7
|
+
type LayeredLayoutOptions,
|
|
8
|
+
type LayerOrder,
|
|
9
|
+
} from "../layered";
|
|
10
|
+
import { createLayerSweepSession, CROSSING_MODEL, type CrossingModel } from "../layered/strategies";
|
|
11
|
+
import {
|
|
12
|
+
minimizeHierarchyCrossings,
|
|
13
|
+
type HierarchyCrossingScope,
|
|
14
|
+
} from "../layered/hierarchy-crossing";
|
|
15
|
+
import { transferExternalPort, joinCompoundRouteSegments } from "../layered/compound-boundaries";
|
|
16
|
+
import {
|
|
17
|
+
attachExternalPortDummy,
|
|
18
|
+
createExternalPortDummy,
|
|
19
|
+
externalPortDummyOf,
|
|
20
|
+
type ExternalPortConstraints,
|
|
21
|
+
type ExternalPortSide,
|
|
22
|
+
} from "../layered/external-port-dummy";
|
|
23
|
+
import { isBetterLayout, measureLayout } from "./layout-quality";
|
|
24
|
+
import { applyOrthogonalJunctions } from "./orthogonal-junctions";
|
|
25
|
+
import { separateElkOpposingTracks } from "./opposing-tracks";
|
|
1
26
|
import { createGraph, type Graph, type VisualGraph } from "@statelyai/graph";
|
|
2
27
|
import {
|
|
3
28
|
elkLayeredOptionDefinitions,
|
|
@@ -28,6 +53,7 @@ import type {
|
|
|
28
53
|
} from "./public-types";
|
|
29
54
|
|
|
30
55
|
function isLayoutEdgeLabel(label: ElkLabel): boolean {
|
|
56
|
+
// Unlike ELK, a dimensioned label without text still reserves space: apps often render the text themselves.
|
|
31
57
|
return (
|
|
32
58
|
getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true &&
|
|
33
59
|
(Boolean(label.text) || label.width !== undefined || label.height !== undefined)
|
|
@@ -57,6 +83,19 @@ export type {
|
|
|
57
83
|
} from "./public-types";
|
|
58
84
|
export type { ElkId, ElkLogging } from "./types";
|
|
59
85
|
|
|
86
|
+
/** The crossing-model setting a layout passes to crossing minimization. */
|
|
87
|
+
const crossingModel = Symbol("crossingModel");
|
|
88
|
+
type ModelArguments = ElkLayoutArguments & { [crossingModel]?: CrossingModel };
|
|
89
|
+
|
|
90
|
+
interface PreparedElkScope {
|
|
91
|
+
graph: ElkNode;
|
|
92
|
+
native: Graph;
|
|
93
|
+
phase?: LayeredCrossingPhase;
|
|
94
|
+
options?: LayeredLayoutOptions;
|
|
95
|
+
children: ReadonlyMap<string, PreparedElkScope>;
|
|
96
|
+
finish(orders?: ReadonlyMap<PreparedElkScope, LayerOrder>): Promise<ElkNode>;
|
|
97
|
+
}
|
|
98
|
+
|
|
60
99
|
export default class ELK {
|
|
61
100
|
readonly #options: ElkConstructorArguments;
|
|
62
101
|
readonly #algorithmIds: ReadonlySet<string>;
|
|
@@ -128,17 +167,98 @@ export default class ELK {
|
|
|
128
167
|
arguments_?: PublicElkLayoutArguments,
|
|
129
168
|
): Promise<PublicElkNode & PublicLaidOutElkNode<T>>;
|
|
130
169
|
layout<T extends ElkNode>(graph: T, arguments_?: ElkLayoutArguments): Promise<LaidOutElkNode<T>>;
|
|
131
|
-
layout<T extends ElkNode>(
|
|
170
|
+
async layout<T extends ElkNode>(
|
|
132
171
|
graph: T,
|
|
133
172
|
arguments_: ElkLayoutArguments = {},
|
|
134
173
|
): Promise<LaidOutElkNode<T>> {
|
|
135
|
-
|
|
174
|
+
const options = { ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions };
|
|
175
|
+
const seeds = crossingVariants(options, graph);
|
|
176
|
+
const probe = mayShareTracks(graph, options)
|
|
177
|
+
? { mixedPorts: false, sharedTracks: false }
|
|
178
|
+
: undefined;
|
|
179
|
+
const pristine = seeds.length || probe ? structuredClone(graph) : undefined;
|
|
180
|
+
let best = separated(
|
|
181
|
+
await this.#layoutWithoutDefectiveCompaction(
|
|
182
|
+
graph,
|
|
183
|
+
probe ? { ...arguments_, [crossingModel]: { probe } } : arguments_,
|
|
184
|
+
),
|
|
185
|
+
options,
|
|
186
|
+
);
|
|
187
|
+
if (!pristine) return best;
|
|
188
|
+
// Crossing minimization models a port carrying both directions as two
|
|
189
|
+
// ports with grouped neighbours, and counts hyperedges as ELK routes
|
|
190
|
+
// them. Each choice steers the sweeps into worse orders on some graphs,
|
|
191
|
+
// so a graph where the mixed-port model applied is also laid out without
|
|
192
|
+
// it, and one with a hyperedge that routing must split is also laid out
|
|
193
|
+
// counting that hyperedge edge by edge. The best result wins.
|
|
194
|
+
// A compound layout with defects tries other random seeds too. Above
|
|
195
|
+
// ELK's default thoroughness, every compound layout does, trading time
|
|
196
|
+
// for fewer crossings and bends.
|
|
197
|
+
const reseed =
|
|
198
|
+
seeds.length > 0 && (thorough(options, graph) || measureLayout(best, true).defects > 0);
|
|
199
|
+
const variants: ModelArguments[] = [
|
|
200
|
+
...(probe?.mixedPorts ? [{ ...arguments_, [crossingModel]: { mixedPorts: false } }] : []),
|
|
201
|
+
...(probe?.sharedTracks
|
|
202
|
+
? [{ ...arguments_, [crossingModel]: { mixedPorts: false, separateSharedTracks: true } }]
|
|
203
|
+
: []),
|
|
204
|
+
...(reseed ? seeds : []).map((variant) => ({
|
|
205
|
+
...arguments_,
|
|
206
|
+
layoutOptions: { ...arguments_.layoutOptions, ...variant },
|
|
207
|
+
})),
|
|
208
|
+
];
|
|
209
|
+
if (!variants.length) return best;
|
|
210
|
+
let quality = measureLayout(best);
|
|
211
|
+
for (const variant of variants) {
|
|
212
|
+
const candidate = separated(
|
|
213
|
+
await this.#layoutWithoutDefectiveCompaction(structuredClone(pristine), variant),
|
|
214
|
+
options,
|
|
215
|
+
);
|
|
216
|
+
const candidateQuality = measureLayout(candidate);
|
|
217
|
+
if (isBetterLayout(candidateQuality, quality)) {
|
|
218
|
+
best = candidate;
|
|
219
|
+
quality = candidateQuality;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return best;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async #layoutWithoutDefectiveCompaction<T extends ElkNode>(
|
|
226
|
+
graph: T,
|
|
227
|
+
arguments_: ModelArguments,
|
|
228
|
+
): Promise<LaidOutElkNode<T>> {
|
|
229
|
+
const compacting = usesPostCompaction(
|
|
230
|
+
{ ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions },
|
|
231
|
+
graph,
|
|
232
|
+
);
|
|
233
|
+
const pristine = compacting ? structuredClone(graph) : undefined;
|
|
234
|
+
const result = await this.#layout(graph, arguments_);
|
|
235
|
+
const defects = pristine ? measureLayout(result, true).defects : 0;
|
|
236
|
+
if (!pristine || defects === 0) return result;
|
|
237
|
+
// Post-compaction is an optimization; it must not route edges through nodes
|
|
238
|
+
// or fold a route back onto itself.
|
|
239
|
+
const strategy = { "elk.layered.compaction.postCompaction.strategy": "NONE" };
|
|
240
|
+
const uncompacted = await this.#layout(disablePostCompaction(pristine), {
|
|
241
|
+
...arguments_,
|
|
242
|
+
layoutOptions: { ...arguments_.layoutOptions, ...strategy },
|
|
243
|
+
});
|
|
244
|
+
return measureLayout(uncompacted, true).defects < defects ? uncompacted : result;
|
|
136
245
|
}
|
|
137
246
|
|
|
138
247
|
async #layout<T extends ElkNode>(
|
|
139
248
|
graph: T,
|
|
140
249
|
arguments_: ElkLayoutArguments = {},
|
|
250
|
+
compoundLayout = false,
|
|
141
251
|
): Promise<LaidOutElkNode<T>> {
|
|
252
|
+
const prepared = await this.#prepareLayout(graph, arguments_, compoundLayout);
|
|
253
|
+
const orders = coordinatePreparedScopes(prepared);
|
|
254
|
+
return (await prepared.finish(orders)) as LaidOutElkNode<T>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async #prepareLayout<T extends ElkNode>(
|
|
258
|
+
graph: T,
|
|
259
|
+
arguments_: ElkLayoutArguments = {},
|
|
260
|
+
compoundLayout = false,
|
|
261
|
+
): Promise<PreparedElkScope> {
|
|
142
262
|
const startedAt = performance.now();
|
|
143
263
|
if (graph === undefined || graph === null) {
|
|
144
264
|
throw new TypeError("Missing mandatory parameter: graph");
|
|
@@ -201,6 +321,8 @@ export default class ELK {
|
|
|
201
321
|
`org.eclipse.elk.core.UnsupportedConfigurationException: Layout algorithm '${requestedAlgorithm}' not found`,
|
|
202
322
|
);
|
|
203
323
|
}
|
|
324
|
+
const preparedChildren = new Map<string, PreparedElkScope>();
|
|
325
|
+
const finishChildren: Array<() => Promise<void>> = [];
|
|
204
326
|
const hasHierarchy = (graph.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
205
327
|
const insideSelfLoopBaseHeightByNodeId = new Map<string, number>();
|
|
206
328
|
const hierarchyRestorations: Array<{
|
|
@@ -210,10 +332,16 @@ export default class ELK {
|
|
|
210
332
|
source?: ElkId;
|
|
211
333
|
target?: ElkId;
|
|
212
334
|
}> = [];
|
|
335
|
+
const boundaryRoutes = new Map<
|
|
336
|
+
ElkEdge,
|
|
337
|
+
{
|
|
338
|
+
source?: { owner: ElkNode; points: ElkPoint[]; junctionPoints?: ElkPoint[] };
|
|
339
|
+
target?: { owner: ElkNode; points: ElkPoint[]; junctionPoints?: ElkPoint[] };
|
|
340
|
+
}
|
|
341
|
+
>();
|
|
213
342
|
const syntheticPortIds = new Set<string>();
|
|
214
343
|
const authoredPortsByCompound = new Map<ElkNode, ElkPort[] | undefined>();
|
|
215
344
|
const authoredOptionsByCompound = new Map<ElkNode, Record<string, unknown> | undefined>();
|
|
216
|
-
const hierarchyBoundaryCountByEdge = new Map<ElkEdge, number>();
|
|
217
345
|
const originalHierarchyEndpoints = new Map(
|
|
218
346
|
(graph.edges ?? []).map((edge) => [
|
|
219
347
|
edge,
|
|
@@ -227,7 +355,6 @@ export default class ELK {
|
|
|
227
355
|
},
|
|
228
356
|
]),
|
|
229
357
|
);
|
|
230
|
-
let hasHierarchyCrossingEdges = false;
|
|
231
358
|
const hierarchyHandling = getOption(layoutOptions, "hierarchyHandling");
|
|
232
359
|
const topdownLayout = getBooleanOption(layoutOptions, "topdownLayout") === true;
|
|
233
360
|
const separateHierarchy =
|
|
@@ -296,17 +423,15 @@ export default class ELK {
|
|
|
296
423
|
);
|
|
297
424
|
}
|
|
298
425
|
} else if (hasHierarchy && !separateHierarchy) {
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
426
|
+
// Prepare boundary identities before coordinating parent and child crossing sweeps.
|
|
427
|
+
const scopeSegmentOrder =
|
|
428
|
+
segmentOrderByScope.get(graph) ??
|
|
429
|
+
elkSegmentOrder(graph, graph, originalHierarchyEndpoints, false);
|
|
303
430
|
for (const child of graph.children ?? []) {
|
|
304
431
|
if ((child.children?.length ?? 0) === 0) continue;
|
|
305
|
-
const descendantById = new Map<string, ElkNode>();
|
|
306
432
|
const descendantOwnerByEndpointId = new Map<string, ElkNode>();
|
|
307
433
|
const collectDescendants = (node: ElkNode): void => {
|
|
308
434
|
for (const descendant of node.children ?? []) {
|
|
309
|
-
descendantById.set(String(descendant.id), descendant);
|
|
310
435
|
descendantOwnerByEndpointId.set(String(descendant.id), descendant);
|
|
311
436
|
for (const port of descendant.ports ?? []) {
|
|
312
437
|
descendantOwnerByEndpointId.set(String(port.id), descendant);
|
|
@@ -321,198 +446,622 @@ export default class ELK {
|
|
|
321
446
|
descendantOwnerByEndpointId.has(sourceId) && descendantOwnerByEndpointId.has(targetId)
|
|
322
447
|
);
|
|
323
448
|
});
|
|
324
|
-
const
|
|
449
|
+
for (const edge of child.edges ?? [])
|
|
450
|
+
if (!originalHierarchyEndpoints.has(edge))
|
|
451
|
+
originalHierarchyEndpoints.set(edge, {
|
|
452
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
453
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
454
|
+
sources: edge.sources,
|
|
455
|
+
targets: edge.targets,
|
|
456
|
+
source: edge.source,
|
|
457
|
+
target: edge.target,
|
|
458
|
+
});
|
|
459
|
+
const crossingEdges = [...(graph.edges ?? []), ...(child.edges ?? [])].filter((edge) => {
|
|
325
460
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
326
461
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
327
462
|
const targetInside = descendantOwnerByEndpointId.has(targetId);
|
|
328
463
|
return sourceInside !== targetInside;
|
|
329
464
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
465
|
+
// CompoundGraphPreprocessor creates boundary ports bottom-up: ports
|
|
466
|
+
// exported by nested groups first, then direct children's ports in
|
|
467
|
+
// port order, outgoing edges before incoming edges.
|
|
468
|
+
const boundaryRank = new Map(
|
|
469
|
+
elkBoundaryEdgeOrder(child, graph, originalHierarchyEndpoints).map((edge, index) => [
|
|
470
|
+
edge,
|
|
471
|
+
index,
|
|
472
|
+
]),
|
|
473
|
+
);
|
|
474
|
+
crossingEdges.sort(
|
|
475
|
+
(a, b) =>
|
|
476
|
+
(boundaryRank.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
477
|
+
(boundaryRank.get(b) ?? Number.MAX_SAFE_INTEGER),
|
|
478
|
+
);
|
|
334
479
|
const mergeHierarchyEdges =
|
|
335
480
|
getBooleanOption(layoutOptions, "layered.mergeHierarchyEdges") !== false;
|
|
336
|
-
//
|
|
337
|
-
// are removed before elkjs serialization and do not alter the exported
|
|
338
|
-
// geometry for a shared hierarchy boundary.
|
|
339
|
-
void mergeHierarchyEdges;
|
|
481
|
+
// ELK merges only edges incident to the same descendant port.
|
|
340
482
|
const proxyByKind = new Map<string, ElkNode>();
|
|
341
|
-
const
|
|
342
|
-
|
|
483
|
+
const implicitOwnPorts = new Map<ElkEdge, ElkPort>();
|
|
484
|
+
const ownPortFor = (edge: ElkEdge): ElkPort | undefined => {
|
|
485
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
486
|
+
const outside = descendantOwnerByEndpointId.has(e.sourceId) ? e.targetId : e.sourceId;
|
|
487
|
+
if (e.sourceId === String(child.id) || e.targetId === String(child.id)) {
|
|
488
|
+
let port = implicitOwnPorts.get(edge);
|
|
489
|
+
if (!port) {
|
|
490
|
+
const direction = getDirection(layoutOptions);
|
|
491
|
+
const sourceInside = e.sourceId === String(child.id);
|
|
492
|
+
const side = sourceInside
|
|
493
|
+
? ({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[direction]
|
|
494
|
+
: ({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[direction];
|
|
495
|
+
port = {
|
|
496
|
+
id: `__native_hierarchy_own_${String(child.id)}_${implicitOwnPorts.size}`,
|
|
497
|
+
width: 0,
|
|
498
|
+
height: 0,
|
|
499
|
+
layoutOptions: { "elk.port.side": side },
|
|
500
|
+
};
|
|
501
|
+
implicitOwnPorts.set(edge, port);
|
|
502
|
+
syntheticPortIds.add(String(port.id));
|
|
503
|
+
}
|
|
504
|
+
return child.ports?.find((p) => p.id === port!.id) ?? port;
|
|
505
|
+
}
|
|
506
|
+
return child.ports?.find((p) => String(p.id) === outside);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const proxyKey = (kind: "input" | "output", edge: ElkEdge): string => {
|
|
510
|
+
const own = ownPortFor(edge);
|
|
511
|
+
if (own) return JSON.stringify(["authored", own.id]);
|
|
512
|
+
const endpoints = originalHierarchyEndpoints.get(edge)!;
|
|
513
|
+
const endpoint = kind === "output" ? endpoints.sourceId : endpoints.targetId;
|
|
514
|
+
const owner = descendantOwnerByEndpointId.get(endpoint);
|
|
515
|
+
const explicitPort = owner?.ports?.some((port) => String(port.id) === endpoint);
|
|
516
|
+
return JSON.stringify([
|
|
517
|
+
kind,
|
|
518
|
+
mergeHierarchyEdges && explicitPort ? endpoint : String(edge.id),
|
|
519
|
+
]);
|
|
520
|
+
};
|
|
521
|
+
const createBoundaryProxy = (
|
|
522
|
+
key: string,
|
|
523
|
+
kind: "input" | "output",
|
|
524
|
+
own: ElkPort | undefined,
|
|
525
|
+
netFlow: number,
|
|
526
|
+
): ElkNode => {
|
|
343
527
|
let proxy = proxyByKind.get(key);
|
|
344
528
|
if (!proxy) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
529
|
+
const direction = getDirection({ ...layoutOptions, ...child.layoutOptions });
|
|
530
|
+
const authoredAnchor = own
|
|
531
|
+
? (getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] as
|
|
532
|
+
| ElkPoint
|
|
533
|
+
| undefined)
|
|
534
|
+
: undefined;
|
|
535
|
+
const origin = createExternalPortDummy({
|
|
536
|
+
constraints: (getOption(child.layoutOptions ?? {}, "portConstraints") ??
|
|
537
|
+
"FREE") as ExternalPortConstraints,
|
|
538
|
+
side: own
|
|
539
|
+
? ((getOption(own.layoutOptions ?? {}, "port.side") ??
|
|
540
|
+
(kind === "output" ? "EAST" : "WEST")) as ExternalPortSide)
|
|
541
|
+
: kind === "output"
|
|
542
|
+
? "EAST"
|
|
543
|
+
: "WEST",
|
|
544
|
+
position: { x: own?.x ?? 0, y: own?.y ?? 0 },
|
|
545
|
+
anchor: authoredAnchor,
|
|
546
|
+
index: own ? getNumberOption(own.layoutOptions ?? {}, "port.index") : undefined,
|
|
547
|
+
direction: direction.toUpperCase() as "RIGHT" | "LEFT" | "DOWN" | "UP",
|
|
548
|
+
netFlow,
|
|
549
|
+
borderOffset: own
|
|
550
|
+
? (getNumberOption(own.layoutOptions ?? {}, "port.borderOffset") ?? 0)
|
|
551
|
+
: (getNumberOption(layoutOptions, "spacing.edgeEdge") ?? 10) / 2,
|
|
552
|
+
size: { width: own?.width ?? 0, height: own?.height ?? 0 },
|
|
553
|
+
});
|
|
554
|
+
if (own) origin.parentPortId = String(own.id);
|
|
555
|
+
const id = `__native_hierarchy_${String(child.id)}_${proxyByKind.size}`;
|
|
556
|
+
proxy = attachExternalPortDummy(
|
|
557
|
+
{
|
|
558
|
+
id,
|
|
559
|
+
width: origin.width,
|
|
560
|
+
height: origin.height,
|
|
561
|
+
layoutOptions: {
|
|
562
|
+
"elk.portConstraints": origin.constraints,
|
|
563
|
+
"elk.layered.layering.layerConstraint":
|
|
564
|
+
origin.side ===
|
|
565
|
+
({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[
|
|
566
|
+
direction
|
|
567
|
+
]
|
|
568
|
+
? "LAST_SEPARATE"
|
|
569
|
+
: origin.side ===
|
|
570
|
+
({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[
|
|
571
|
+
direction
|
|
572
|
+
]
|
|
573
|
+
? "FIRST_SEPARATE"
|
|
574
|
+
: undefined,
|
|
575
|
+
},
|
|
576
|
+
ports: [
|
|
577
|
+
{
|
|
578
|
+
id: `${id}:boundary`,
|
|
579
|
+
x: origin.port.x,
|
|
580
|
+
y: origin.port.y,
|
|
581
|
+
width: 0,
|
|
582
|
+
height: 0,
|
|
583
|
+
layoutOptions: { "elk.port.side": origin.port.side },
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
origin,
|
|
588
|
+
);
|
|
350
589
|
proxyByKind.set(key, proxy);
|
|
351
590
|
}
|
|
352
591
|
return proxy;
|
|
353
592
|
};
|
|
593
|
+
const ownedPortNetFlow = (id: string): number => {
|
|
594
|
+
let votes = 0;
|
|
595
|
+
for (const edge of originalHierarchyEndpoints.values()) {
|
|
596
|
+
if (edge.sourceId === id)
|
|
597
|
+
votes += descendantOwnerByEndpointId.has(edge.targetId) ? -1 : 1;
|
|
598
|
+
if (edge.targetId === id)
|
|
599
|
+
votes += descendantOwnerByEndpointId.has(edge.sourceId) ? 1 : -1;
|
|
600
|
+
}
|
|
601
|
+
return votes;
|
|
602
|
+
};
|
|
603
|
+
const proxyFor = (kind: "input" | "output", edge: ElkEdge): ElkNode => {
|
|
604
|
+
const own = ownPortFor(edge);
|
|
605
|
+
const flow = implicitOwnPorts.has(edge)
|
|
606
|
+
? kind === "output"
|
|
607
|
+
? 1
|
|
608
|
+
: -1
|
|
609
|
+
: own
|
|
610
|
+
? ownedPortNetFlow(String(own.id))
|
|
611
|
+
: kind === "output"
|
|
612
|
+
? 1
|
|
613
|
+
: -1;
|
|
614
|
+
return createBoundaryProxy(proxyKey(kind, edge), kind, own, flow);
|
|
615
|
+
};
|
|
616
|
+
// ELK imports ancestor edges before descendant-local edges. Preserve
|
|
617
|
+
// that implicit port creation order through child phase preparation.
|
|
354
618
|
const temporaryEdges: ElkEdge[] = [
|
|
355
|
-
...(child.edges ?? []),
|
|
356
|
-
...internalEdges.filter(
|
|
357
|
-
(edge) =>
|
|
358
|
-
!(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)),
|
|
359
|
-
),
|
|
360
619
|
...crossingEdges.map((edge) => {
|
|
361
620
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
362
621
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
363
|
-
const proxy = proxyFor(sourceInside ? "output" : "input");
|
|
364
|
-
|
|
622
|
+
const proxy = proxyFor(sourceInside ? "output" : "input", edge);
|
|
623
|
+
// ELK keeps center labels on the shallowest segment: the parent's,
|
|
624
|
+
// unless the edge enters through the child's own port.
|
|
625
|
+
const labels =
|
|
626
|
+
(graph.edges ?? []).includes(edge) && !ownPortFor(edge)
|
|
627
|
+
? edge.labels?.filter(
|
|
628
|
+
(label) =>
|
|
629
|
+
(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") !==
|
|
630
|
+
"CENTER",
|
|
631
|
+
)
|
|
632
|
+
: edge.labels;
|
|
633
|
+
const segment: ElkEdge = {
|
|
365
634
|
...edge,
|
|
635
|
+
labels,
|
|
366
636
|
id: `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
367
|
-
sources: [sourceInside ? sourceId : proxy.id!],
|
|
368
|
-
targets: [sourceInside ? proxy.id! : targetId],
|
|
637
|
+
sources: [sourceInside ? sourceId : proxy.ports![0]!.id!],
|
|
638
|
+
targets: [sourceInside ? proxy.ports![0]!.id! : targetId],
|
|
369
639
|
source: undefined,
|
|
370
640
|
target: undefined,
|
|
371
641
|
sections: undefined,
|
|
372
642
|
};
|
|
643
|
+
originalEdgeId.set(segment, originalEdgeId.get(edge) ?? String(edge.id));
|
|
644
|
+
return segment;
|
|
373
645
|
}),
|
|
646
|
+
...(child.edges ?? []).filter((edge) => !crossingEdges.includes(edge)),
|
|
647
|
+
...internalEdges.filter(
|
|
648
|
+
(edge) =>
|
|
649
|
+
!(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)),
|
|
650
|
+
),
|
|
374
651
|
];
|
|
652
|
+
const hasExternalPorts = proxyByKind.size > 0;
|
|
653
|
+
const inactiveAuthoredPorts: Array<{ port: ElkPort; proxy: ElkNode }> = [];
|
|
654
|
+
if (hasExternalPorts) {
|
|
655
|
+
for (const port of child.ports ?? []) {
|
|
656
|
+
const key = JSON.stringify(["authored", port.id]);
|
|
657
|
+
if (proxyByKind.has(key)) continue;
|
|
658
|
+
const flow = ownedPortNetFlow(String(port.id));
|
|
659
|
+
inactiveAuthoredPorts.push({
|
|
660
|
+
port,
|
|
661
|
+
proxy: createBoundaryProxy(key, flow >= 0 ? "output" : "input", port, flow),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const outsideNodeEdges = !hasExternalPorts
|
|
666
|
+
? []
|
|
667
|
+
: (graph.edges ?? []).filter((edge) => {
|
|
668
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
669
|
+
return (
|
|
670
|
+
(e.sourceId === String(child.id) || e.targetId === String(child.id)) &&
|
|
671
|
+
!descendantOwnerByEndpointId.has(e.sourceId) &&
|
|
672
|
+
!descendantOwnerByEndpointId.has(e.targetId)
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
for (const edge of outsideNodeEdges) {
|
|
676
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
677
|
+
proxyFor(e.sourceId === String(child.id) ? "output" : "input", edge);
|
|
678
|
+
}
|
|
375
679
|
const temporaryChild: ElkNode = {
|
|
376
680
|
...child,
|
|
681
|
+
layoutOptions: proxyByKind.size
|
|
682
|
+
? { ...child.layoutOptions, "elk.portConstraints": "FIXED_SIDE" }
|
|
683
|
+
: child.layoutOptions,
|
|
377
684
|
children: [...(child.children ?? []), ...proxyByKind.values()],
|
|
378
685
|
edges: temporaryEdges,
|
|
379
686
|
};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
687
|
+
const childSegmentOrder = elkSegmentOrder(child, graph, originalHierarchyEndpoints, true);
|
|
688
|
+
segmentOrderByScope.set(temporaryChild, childSegmentOrder);
|
|
689
|
+
// Boundary segments copied into the child scope keep their original identity.
|
|
690
|
+
for (const edge of temporaryEdges) {
|
|
691
|
+
const id = originalEdgeId.get(edge);
|
|
692
|
+
const rank = id === undefined ? undefined : childSegmentOrder.get(id);
|
|
693
|
+
if (rank !== undefined) hierarchySegmentRank.set(edge, rank);
|
|
694
|
+
}
|
|
695
|
+
const preparedChild = await this.#prepareLayout(
|
|
696
|
+
temporaryChild,
|
|
697
|
+
{
|
|
698
|
+
...arguments_,
|
|
699
|
+
layoutOptions: {
|
|
700
|
+
...arguments_.layoutOptions,
|
|
701
|
+
// The compound's own direction wins over the inherited one.
|
|
702
|
+
direction: getDirection(
|
|
703
|
+
getOption(child.layoutOptions ?? {}, "direction") === undefined
|
|
704
|
+
? layoutOptions
|
|
705
|
+
: child.layoutOptions!,
|
|
706
|
+
).toUpperCase(),
|
|
707
|
+
hierarchyHandling: "INCLUDE_CHILDREN",
|
|
708
|
+
},
|
|
709
|
+
logging: false,
|
|
710
|
+
measureExecutionTime: false,
|
|
385
711
|
},
|
|
386
|
-
|
|
387
|
-
measureExecutionTime: false,
|
|
388
|
-
});
|
|
389
|
-
const childPadding = parsePadding(
|
|
390
|
-
getOption({ ...layoutOptions, ...child.layoutOptions }, "padding"),
|
|
391
|
-
12,
|
|
712
|
+
true,
|
|
392
713
|
);
|
|
393
|
-
|
|
394
|
-
const direction = getDirection(layoutOptions);
|
|
395
|
-
const horizontal = direction === "right" || direction === "left";
|
|
396
|
-
const increasing = direction === "right" || direction === "down";
|
|
397
|
-
if (increasing) {
|
|
398
|
-
const minimumFlow = Math.min(
|
|
399
|
-
...(child.children ?? []).map((node) => (horizontal ? (node.x ?? 0) : (node.y ?? 0))),
|
|
400
|
-
);
|
|
401
|
-
const desiredFlow = horizontal ? childPadding.left : childPadding.top;
|
|
402
|
-
const delta = desiredFlow - minimumFlow;
|
|
403
|
-
for (const node of child.children ?? []) {
|
|
404
|
-
if (horizontal) node.x = (node.x ?? 0) + delta;
|
|
405
|
-
else node.y = (node.y ?? 0) + delta;
|
|
406
|
-
}
|
|
407
|
-
for (const edge of temporaryEdges) {
|
|
408
|
-
for (const section of edge.sections ?? []) {
|
|
409
|
-
for (const point of [
|
|
410
|
-
section.startPoint,
|
|
411
|
-
...(section.bendPoints ?? []),
|
|
412
|
-
section.endPoint,
|
|
413
|
-
]) {
|
|
414
|
-
if (horizontal) point.x += delta;
|
|
415
|
-
else point.y += delta;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
for (const internalEdge of internalEdges) {
|
|
422
|
-
const temporary = temporaryEdges.find(
|
|
423
|
-
(candidate) => String(candidate.id) === String(internalEdge.id),
|
|
424
|
-
);
|
|
425
|
-
if (temporary?.sections) internalEdge.sections = temporary.sections;
|
|
426
|
-
}
|
|
427
|
-
child.width =
|
|
428
|
-
Math.max(0, ...(child.children ?? []).map((node) => (node.x ?? 0) + (node.width ?? 0))) +
|
|
429
|
-
childPadding.right;
|
|
430
|
-
child.height =
|
|
431
|
-
Math.max(0, ...(child.children ?? []).map((node) => (node.y ?? 0) + (node.height ?? 0))) +
|
|
432
|
-
childPadding.bottom;
|
|
433
|
-
|
|
434
|
-
const relativeRect = (id: string): ElkShape | undefined => {
|
|
435
|
-
const owner = descendantOwnerByEndpointId.get(id);
|
|
436
|
-
if (!owner) return undefined;
|
|
437
|
-
const path: ElkNode[] = [];
|
|
438
|
-
const visit = (parent: ElkNode): boolean => {
|
|
439
|
-
for (const candidate of parent.children ?? []) {
|
|
440
|
-
path.push(candidate);
|
|
441
|
-
if (candidate === owner || visit(candidate)) return true;
|
|
442
|
-
path.pop();
|
|
443
|
-
}
|
|
444
|
-
return false;
|
|
445
|
-
};
|
|
446
|
-
if (!visit(child)) return undefined;
|
|
447
|
-
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
448
|
-
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
449
|
-
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
450
|
-
if (port) {
|
|
451
|
-
return {
|
|
452
|
-
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
453
|
-
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
454
|
-
width: 0,
|
|
455
|
-
height: 0,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
x: ownerX,
|
|
460
|
-
y: ownerY,
|
|
461
|
-
width: path.at(-1)?.width ?? 0,
|
|
462
|
-
height: path.at(-1)?.height ?? 0,
|
|
463
|
-
};
|
|
464
|
-
};
|
|
714
|
+
preparedChildren.set(String(child.id), preparedChild);
|
|
465
715
|
authoredPortsByCompound.set(child, child.ports);
|
|
466
716
|
authoredOptionsByCompound.set(child, child.layoutOptions);
|
|
467
|
-
const
|
|
717
|
+
const initialPorts = (child.ports ?? []).map((port) => ({
|
|
718
|
+
...port,
|
|
719
|
+
layoutOptions: port.layoutOptions ? { ...port.layoutOptions } : undefined,
|
|
720
|
+
}));
|
|
468
721
|
for (const edge of crossingEdges) {
|
|
722
|
+
const own = ownPortFor(edge);
|
|
723
|
+
if (own) {
|
|
724
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
725
|
+
const sourceInside = descendantOwnerByEndpointId.has(original.sourceId);
|
|
726
|
+
const origin = externalPortDummyOf(
|
|
727
|
+
proxyByKind.get(proxyKey(sourceInside ? "output" : "input", edge))!,
|
|
728
|
+
)!;
|
|
729
|
+
let port = initialPorts.find((p) => p.id === own.id);
|
|
730
|
+
if (!port) {
|
|
731
|
+
port = { ...own, layoutOptions: { ...own.layoutOptions } };
|
|
732
|
+
initialPorts.push(port);
|
|
733
|
+
}
|
|
734
|
+
port.layoutOptions = { ...port.layoutOptions, "elk.port.side": origin.side };
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
469
737
|
const original = originalHierarchyEndpoints.get(edge)!;
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
if (!rect) continue;
|
|
475
|
-
const portId = `__native_hierarchy_port_${String(child.id)}_${String(edge.id)}`;
|
|
738
|
+
const sourceInside = descendantOwnerByEndpointId.has(original.sourceId);
|
|
739
|
+
const proxy = proxyByKind.get(proxyKey(sourceInside ? "output" : "input", edge))!;
|
|
740
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
741
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
476
742
|
syntheticPortIds.add(portId);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
direction === "down" || direction === "up"
|
|
491
|
-
? (rect.y ?? 0) + (useFarSide ? (rect.height ?? 0) : 0)
|
|
492
|
-
: (rect.y ?? 0) + (rect.height ?? 0) / 2,
|
|
493
|
-
});
|
|
494
|
-
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) {
|
|
743
|
+
if (!initialPorts.some((port) => port.id === portId))
|
|
744
|
+
initialPorts.push({
|
|
745
|
+
id: portId,
|
|
746
|
+
width: origin.externalSize.width,
|
|
747
|
+
height: origin.externalSize.height,
|
|
748
|
+
x: 0,
|
|
749
|
+
y: 0,
|
|
750
|
+
layoutOptions: {
|
|
751
|
+
"elk.port.side": origin.side,
|
|
752
|
+
"elk.port.borderOffset": origin.borderOffset,
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge))
|
|
495
756
|
hierarchyRestorations.push({ edge, ...original });
|
|
496
|
-
}
|
|
497
757
|
if (sourceInside) edge.sources = [portId];
|
|
498
758
|
else edge.targets = [portId];
|
|
759
|
+
const rank = scopeSegmentOrder.get(originalEdgeId.get(edge) ?? String(edge.id));
|
|
760
|
+
if (rank !== undefined) hierarchySegmentRank.set(edge, rank);
|
|
499
761
|
edge.source = undefined;
|
|
500
762
|
edge.target = undefined;
|
|
501
763
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const baseHeight = child.height ?? 0;
|
|
509
|
-
insideSelfLoopBaseHeightByNodeId.set(String(child.id), baseHeight);
|
|
510
|
-
child.height = baseHeight + insideLoopCount * 11;
|
|
764
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
765
|
+
const actual = initialPorts.find((p) => p.id === port.id)!;
|
|
766
|
+
actual.layoutOptions = {
|
|
767
|
+
...actual.layoutOptions,
|
|
768
|
+
"elk.port.side": externalPortDummyOf(proxy)!.side,
|
|
769
|
+
};
|
|
511
770
|
}
|
|
771
|
+
for (const edge of outsideNodeEdges) {
|
|
772
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
773
|
+
const source = original.sourceId === String(child.id);
|
|
774
|
+
const proxy = proxyFor(source ? "output" : "input", edge),
|
|
775
|
+
own = ownPortFor(edge)!;
|
|
776
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
777
|
+
if (!initialPorts.some((p) => p.id === own.id))
|
|
778
|
+
initialPorts.push({
|
|
779
|
+
...own,
|
|
780
|
+
layoutOptions: { ...own.layoutOptions, "elk.port.side": origin.side },
|
|
781
|
+
});
|
|
782
|
+
if (!hierarchyRestorations.some((r) => r.edge === edge))
|
|
783
|
+
hierarchyRestorations.push({ edge, ...original });
|
|
784
|
+
if (source) edge.sources = [own.id!];
|
|
785
|
+
else edge.targets = [own.id!];
|
|
786
|
+
edge.source = undefined;
|
|
787
|
+
edge.target = undefined;
|
|
788
|
+
}
|
|
789
|
+
child.ports = initialPorts;
|
|
790
|
+
child.layoutOptions = {
|
|
791
|
+
...child.layoutOptions,
|
|
792
|
+
"elk.portConstraints":
|
|
793
|
+
// ELK fixes physical boundary sides after introducing hierarchy dummies.
|
|
794
|
+
// Authored constraints are restored when finishing the public graph.
|
|
795
|
+
crossingEdges.length > 0 || outsideNodeEdges.length > 0
|
|
796
|
+
? "FIXED_SIDE"
|
|
797
|
+
: (getOption(authoredOptionsByCompound.get(child) ?? {}, "portConstraints") ??
|
|
798
|
+
"FREE"),
|
|
799
|
+
};
|
|
800
|
+
finishChildren.push(async () => {
|
|
801
|
+
await preparedChild.finish(activeOrders);
|
|
802
|
+
const childPadding = parsePadding(
|
|
803
|
+
getOption({ ...layoutOptions, ...child.layoutOptions }, "padding"),
|
|
804
|
+
12,
|
|
805
|
+
);
|
|
806
|
+
for (const internalEdge of internalEdges) {
|
|
807
|
+
const temporary = temporaryEdges.find(
|
|
808
|
+
(candidate) => String(candidate.id) === String(internalEdge.id),
|
|
809
|
+
);
|
|
810
|
+
if (temporary?.sections) {
|
|
811
|
+
internalEdge.sections = temporary.sections;
|
|
812
|
+
// A nested scope may own the route (an edge from a compound's
|
|
813
|
+
// own port to its child); keep that coordinate frame.
|
|
814
|
+
internalEdge.container = (temporary as { container?: string }).container ?? child.id;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const childDirection = getDirection({ ...layoutOptions, ...child.layoutOptions });
|
|
818
|
+
const horizontalChild = childDirection === "right" || childDirection === "left";
|
|
819
|
+
// External dummy positions retain any routing corridor at the boundary.
|
|
820
|
+
const restoredPerpendicular = [...proxyByKind.values()].some(isPerpendicularPortRestored);
|
|
821
|
+
child.width = restoredPerpendicular
|
|
822
|
+
? temporaryChild.width
|
|
823
|
+
: horizontalChild
|
|
824
|
+
? Math.max(
|
|
825
|
+
0,
|
|
826
|
+
...(temporaryChild.children ?? []).map(
|
|
827
|
+
(node) => (node.x ?? 0) + (node.width ?? 0),
|
|
828
|
+
),
|
|
829
|
+
) + childPadding.right
|
|
830
|
+
: temporaryChild.width;
|
|
831
|
+
child.height = restoredPerpendicular
|
|
832
|
+
? temporaryChild.height
|
|
833
|
+
: horizontalChild
|
|
834
|
+
? temporaryChild.height
|
|
835
|
+
: Math.max(
|
|
836
|
+
0,
|
|
837
|
+
...(temporaryChild.children ?? []).map(
|
|
838
|
+
(node) => (node.y ?? 0) + (node.height ?? 0),
|
|
839
|
+
),
|
|
840
|
+
) + childPadding.bottom;
|
|
841
|
+
|
|
842
|
+
const relativeRect = (id: string): ElkShape | undefined => {
|
|
843
|
+
const owner = descendantOwnerByEndpointId.get(id);
|
|
844
|
+
if (!owner) return undefined;
|
|
845
|
+
const path: ElkNode[] = [];
|
|
846
|
+
const visit = (parent: ElkNode): boolean => {
|
|
847
|
+
for (const candidate of parent.children ?? []) {
|
|
848
|
+
path.push(candidate);
|
|
849
|
+
if (candidate === owner || visit(candidate)) return true;
|
|
850
|
+
path.pop();
|
|
851
|
+
}
|
|
852
|
+
return false;
|
|
853
|
+
};
|
|
854
|
+
if (!visit(child)) return undefined;
|
|
855
|
+
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
856
|
+
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
857
|
+
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
858
|
+
if (port) {
|
|
859
|
+
return {
|
|
860
|
+
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
861
|
+
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
862
|
+
width: 0,
|
|
863
|
+
height: 0,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
x: ownerX,
|
|
868
|
+
y: ownerY,
|
|
869
|
+
width: path.at(-1)?.width ?? 0,
|
|
870
|
+
height: path.at(-1)?.height ?? 0,
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
const ports = (authoredPortsByCompound.get(child) ?? []).map(
|
|
874
|
+
(original) => child.ports?.find((port) => port.id === original.id) ?? original,
|
|
875
|
+
);
|
|
876
|
+
for (const edge of crossingEdges) {
|
|
877
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
878
|
+
const { sourceId, targetId } = original;
|
|
879
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
880
|
+
const descendantId = sourceInside ? sourceId : targetId;
|
|
881
|
+
const rect = relativeRect(descendantId);
|
|
882
|
+
if (!rect) continue;
|
|
883
|
+
const internalEdge = temporaryEdges.find(
|
|
884
|
+
(candidate) =>
|
|
885
|
+
String(candidate.id) ===
|
|
886
|
+
`__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
887
|
+
);
|
|
888
|
+
const internalRoute = internalEdge?.sections;
|
|
889
|
+
const kind = sourceInside ? "output" : "input";
|
|
890
|
+
const proxy = proxyByKind.get(proxyKey(kind, edge))!;
|
|
891
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
892
|
+
const endpoint = sourceInside
|
|
893
|
+
? internalRoute?.at(-1)?.endPoint
|
|
894
|
+
: internalRoute?.[0]?.startPoint;
|
|
895
|
+
if (!endpoint)
|
|
896
|
+
throw new Error(`Missing compound boundary route for ${String(edge.id)}`);
|
|
897
|
+
const transferred = transferExternalPort({
|
|
898
|
+
contentSize: {
|
|
899
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
900
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
901
|
+
},
|
|
902
|
+
padding: childPadding,
|
|
903
|
+
offset: { x: 0, y: 0 },
|
|
904
|
+
dummy: {
|
|
905
|
+
x: endpoint.x - childPadding.left - (ownPortFor(edge) ? origin.port.x : 0),
|
|
906
|
+
y: endpoint.y - childPadding.top - (ownPortFor(edge) ? origin.port.y : 0),
|
|
907
|
+
width: origin.width,
|
|
908
|
+
height: origin.height,
|
|
909
|
+
},
|
|
910
|
+
side: origin.side,
|
|
911
|
+
borderOffset: origin.borderOffset,
|
|
912
|
+
portSize: origin.externalSize,
|
|
913
|
+
});
|
|
914
|
+
const points = internalRoute!
|
|
915
|
+
.flatMap((section, index) => [
|
|
916
|
+
...(index === 0 ? [section.startPoint] : []),
|
|
917
|
+
...(section.bendPoints ?? []),
|
|
918
|
+
section.endPoint,
|
|
919
|
+
])
|
|
920
|
+
.map((point) => ({ ...point }));
|
|
921
|
+
const boundary = sourceInside ? points.at(-1)! : points[0]!;
|
|
922
|
+
boundary.x = transferred.dummy.x + childPadding.left;
|
|
923
|
+
boundary.y = transferred.dummy.y + childPadding.top;
|
|
924
|
+
const own = ownPortFor(edge);
|
|
925
|
+
if (own) {
|
|
926
|
+
if (implicitOwnPorts.has(edge) && !ports.some((p) => p.id === own.id))
|
|
927
|
+
ports.push(own);
|
|
928
|
+
own.x = transferred.port.x;
|
|
929
|
+
own.y = transferred.port.y;
|
|
930
|
+
const anchor = (getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] as
|
|
931
|
+
| ElkPoint
|
|
932
|
+
| undefined) ?? {
|
|
933
|
+
x:
|
|
934
|
+
origin.side === "EAST"
|
|
935
|
+
? (own.width ?? 0)
|
|
936
|
+
: origin.side === "WEST"
|
|
937
|
+
? 0
|
|
938
|
+
: (own.width ?? 0) / 2,
|
|
939
|
+
y:
|
|
940
|
+
origin.side === "SOUTH"
|
|
941
|
+
? (own.height ?? 0)
|
|
942
|
+
: origin.side === "NORTH"
|
|
943
|
+
? 0
|
|
944
|
+
: (own.height ?? 0) / 2,
|
|
945
|
+
};
|
|
946
|
+
boundary.x = own.x + anchor.x;
|
|
947
|
+
boundary.y = own.y + anchor.y;
|
|
948
|
+
edge.sections = [
|
|
949
|
+
{
|
|
950
|
+
id: `${String(edge.id)}_s0`,
|
|
951
|
+
startPoint: points[0]!,
|
|
952
|
+
endPoint: points.at(-1)!,
|
|
953
|
+
...(points.length > 2 ? { bendPoints: points.slice(1, -1) } : {}),
|
|
954
|
+
incomingShape: sourceId,
|
|
955
|
+
outgoingShape: targetId,
|
|
956
|
+
},
|
|
957
|
+
];
|
|
958
|
+
if (internalEdge?.junctionPoints?.length)
|
|
959
|
+
edge.junctionPoints = internalEdge.junctionPoints.map((point) => ({ ...point }));
|
|
960
|
+
else delete edge.junctionPoints;
|
|
961
|
+
edge.container = child.id;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const routes = boundaryRoutes.get(edge) ?? {};
|
|
965
|
+
routes[sourceInside ? "source" : "target"] = {
|
|
966
|
+
owner: child,
|
|
967
|
+
points,
|
|
968
|
+
junctionPoints: internalEdge?.junctionPoints,
|
|
969
|
+
};
|
|
970
|
+
boundaryRoutes.set(edge, routes);
|
|
971
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
972
|
+
syntheticPortIds.add(portId);
|
|
973
|
+
if (!ports.some((port) => port.id === portId))
|
|
974
|
+
ports.push({
|
|
975
|
+
id: portId,
|
|
976
|
+
width: origin.externalSize.width,
|
|
977
|
+
height: origin.externalSize.height,
|
|
978
|
+
x: transferred.port.x,
|
|
979
|
+
y: transferred.port.y,
|
|
980
|
+
layoutOptions: {
|
|
981
|
+
"elk.port.side": origin.side,
|
|
982
|
+
"elk.port.borderOffset": origin.borderOffset,
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) {
|
|
986
|
+
hierarchyRestorations.push({ edge, ...original });
|
|
987
|
+
}
|
|
988
|
+
if (sourceInside) edge.sources = [portId];
|
|
989
|
+
else edge.targets = [portId];
|
|
990
|
+
edge.source = undefined;
|
|
991
|
+
edge.target = undefined;
|
|
992
|
+
}
|
|
993
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
994
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
995
|
+
const actual = ports.find((p) => p.id === port.id)!;
|
|
996
|
+
const transferred = transferExternalPort({
|
|
997
|
+
contentSize: {
|
|
998
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
999
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
1000
|
+
},
|
|
1001
|
+
padding: childPadding,
|
|
1002
|
+
offset: { x: 0, y: 0 },
|
|
1003
|
+
dummy: {
|
|
1004
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
1005
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
1006
|
+
width: origin.width,
|
|
1007
|
+
height: origin.height,
|
|
1008
|
+
},
|
|
1009
|
+
side: origin.side,
|
|
1010
|
+
borderOffset: origin.borderOffset,
|
|
1011
|
+
portSize: origin.externalSize,
|
|
1012
|
+
});
|
|
1013
|
+
actual.x = transferred.port.x;
|
|
1014
|
+
actual.y = transferred.port.y;
|
|
1015
|
+
}
|
|
1016
|
+
for (const edge of outsideNodeEdges) {
|
|
1017
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
1018
|
+
const proxy = proxyFor(
|
|
1019
|
+
original.sourceId === String(child.id) ? "output" : "input",
|
|
1020
|
+
edge,
|
|
1021
|
+
),
|
|
1022
|
+
origin = externalPortDummyOf(proxy)!,
|
|
1023
|
+
own = ownPortFor(edge)!;
|
|
1024
|
+
const transferred = transferExternalPort({
|
|
1025
|
+
contentSize: {
|
|
1026
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
1027
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
1028
|
+
},
|
|
1029
|
+
padding: childPadding,
|
|
1030
|
+
offset: { x: 0, y: 0 },
|
|
1031
|
+
dummy: {
|
|
1032
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
1033
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
1034
|
+
width: origin.width,
|
|
1035
|
+
height: origin.height,
|
|
1036
|
+
},
|
|
1037
|
+
side: origin.side,
|
|
1038
|
+
borderOffset: origin.borderOffset,
|
|
1039
|
+
portSize: origin.externalSize,
|
|
1040
|
+
});
|
|
1041
|
+
own.x = transferred.port.x;
|
|
1042
|
+
own.y = transferred.port.y;
|
|
1043
|
+
if (!ports.some((p) => p.id === own.id)) ports.push(own);
|
|
1044
|
+
}
|
|
1045
|
+
child.ports = ports;
|
|
1046
|
+
child.layoutOptions = { ...child.layoutOptions, "elk.portConstraints": "FIXED_POS" };
|
|
1047
|
+
const insideLoopCount = (graph.edges ?? []).filter((edge) =>
|
|
1048
|
+
isInsideSelfLoop(graph, edge, String(child.id)),
|
|
1049
|
+
).length;
|
|
1050
|
+
if (insideLoopCount > 0) {
|
|
1051
|
+
const baseHeight = child.height ?? 0;
|
|
1052
|
+
insideSelfLoopBaseHeightByNodeId.set(String(child.id), baseHeight);
|
|
1053
|
+
child.height = baseHeight + insideLoopCount * 11;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
512
1056
|
}
|
|
513
1057
|
}
|
|
514
1058
|
applyNodeMicroLayout(graph, layoutOptions);
|
|
515
1059
|
const graph_ = toGraph(graph, layoutOptions);
|
|
1060
|
+
const hierarchyProxyIds = new Set(
|
|
1061
|
+
(graph.children ?? [])
|
|
1062
|
+
.filter((node) => String(node.id).startsWith("__native_hierarchy_"))
|
|
1063
|
+
.map((node) => String(node.id)),
|
|
1064
|
+
);
|
|
516
1065
|
const layerConstraintByNodeId = new Map(
|
|
517
1066
|
(graph.children ?? []).map((node) => [
|
|
518
1067
|
String(node.id),
|
|
@@ -527,6 +1076,10 @@ export default class ELK {
|
|
|
527
1076
|
algorithm === "layered" &&
|
|
528
1077
|
graph_.edges.some((edge) => {
|
|
529
1078
|
if (edge.sourceId === edge.targetId) return false;
|
|
1079
|
+
// A boundary proxy for an edge from the enclosing compound sits in its own
|
|
1080
|
+
// separate layer ahead of the scope, so it never conflicts with a first node.
|
|
1081
|
+
if (hierarchyProxyIds.has(edge.sourceId) || hierarchyProxyIds.has(edge.targetId))
|
|
1082
|
+
return false;
|
|
530
1083
|
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
531
1084
|
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
532
1085
|
return (
|
|
@@ -543,307 +1096,469 @@ export default class ELK {
|
|
|
543
1096
|
getOption(layoutOptions, "padding"),
|
|
544
1097
|
algorithm === "layered" ? 12 : 0,
|
|
545
1098
|
);
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1099
|
+
const options: LayeredLayoutOptions = {
|
|
1100
|
+
direction: getDirection(layoutOptions),
|
|
1101
|
+
spacing: {
|
|
1102
|
+
node:
|
|
1103
|
+
getNumberOption(layoutOptions, "spacing.nodeNode") ??
|
|
1104
|
+
getNumberOption(layoutOptions, "layered.spacing.baseValue"),
|
|
1105
|
+
layer:
|
|
1106
|
+
getNumberOption(layoutOptions, "layered.spacing.nodeNodeBetweenLayers") ??
|
|
1107
|
+
getNumberOption(layoutOptions, "layered.spacing.baseValue") ??
|
|
1108
|
+
20,
|
|
1109
|
+
},
|
|
1110
|
+
padding,
|
|
1111
|
+
constraints: {
|
|
1112
|
+
layer: () => undefined,
|
|
1113
|
+
},
|
|
1114
|
+
settings: {
|
|
1115
|
+
...getLayeredSettings(layoutOptions),
|
|
1116
|
+
...((arguments_ as ModelArguments)[crossingModel]
|
|
1117
|
+
? { [CROSSING_MODEL]: (arguments_ as ModelArguments)[crossingModel] }
|
|
1118
|
+
: {}),
|
|
1119
|
+
...(compoundLayout || (hasHierarchy && !separateHierarchy && !topdownLayout)
|
|
1120
|
+
? { separateConnectedComponents: false }
|
|
1121
|
+
: {}),
|
|
1122
|
+
...(hierarchyHandling === "INCLUDE_CHILDREN"
|
|
1123
|
+
? {
|
|
1124
|
+
"crossingMinimization.greedySwitch.type": String(
|
|
1125
|
+
getOption(
|
|
1126
|
+
layoutOptions,
|
|
1127
|
+
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
1128
|
+
) ?? "TWO_SIDED",
|
|
1129
|
+
) as LayeredAdvancedOptions["crossingMinimization.greedySwitch.type"],
|
|
1130
|
+
}
|
|
1131
|
+
: {}),
|
|
1132
|
+
},
|
|
1133
|
+
nodeSettings: (node) => {
|
|
1134
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1135
|
+
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
1136
|
+
},
|
|
1137
|
+
edgeSettings: (edge) => {
|
|
1138
|
+
const elkEdge = findById(graph.edges, edge.id)?.item;
|
|
1139
|
+
return {
|
|
1140
|
+
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
1141
|
+
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {}),
|
|
1142
|
+
// ELK appends hierarchy segments to port edge lists in creation order.
|
|
1143
|
+
"edge.hierarchyRank": elkEdge && hierarchySegmentRank.get(elkEdge),
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
1146
|
+
portSettings: (port, node) => {
|
|
1147
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1148
|
+
const found = findById(child?.ports, port.name);
|
|
1149
|
+
const elkPort = found?.item;
|
|
1150
|
+
return {
|
|
1151
|
+
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
1152
|
+
// ELK imports ports in authored order and sorts sides after dummy insertion.
|
|
1153
|
+
"port.authoredIndex": found?.position,
|
|
1154
|
+
"port.labelWidth": Math.max(
|
|
1155
|
+
0,
|
|
1156
|
+
...(elkPort?.labels ?? []).map((label) => label.width ?? 0),
|
|
1157
|
+
),
|
|
1158
|
+
"port.labelHeight": Math.max(
|
|
1159
|
+
0,
|
|
1160
|
+
...(elkPort?.labels ?? []).map((label) => label.height ?? 0),
|
|
1161
|
+
),
|
|
1162
|
+
} as ElkLayeredOptionValueByName;
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
const pipeline =
|
|
1166
|
+
algorithm === "layered" ? createLayeredScopePipeline(graph_, options) : undefined;
|
|
1167
|
+
const initial = pipeline?.next();
|
|
1168
|
+
const phase = initial && !initial.done ? initial.value : undefined;
|
|
1169
|
+
let activeOrders: ReadonlyMap<PreparedElkScope, LayerOrder> | undefined;
|
|
1170
|
+
let finished: Promise<ElkNode> | undefined;
|
|
1171
|
+
const prepared: PreparedElkScope = {
|
|
1172
|
+
graph,
|
|
1173
|
+
native: graph_,
|
|
1174
|
+
phase,
|
|
1175
|
+
options,
|
|
1176
|
+
children: preparedChildren,
|
|
1177
|
+
finish: (orders) => {
|
|
1178
|
+
if (finished) return finished;
|
|
1179
|
+
activeOrders = orders;
|
|
1180
|
+
finished = finish();
|
|
1181
|
+
return finished;
|
|
1182
|
+
},
|
|
1183
|
+
};
|
|
1184
|
+
const finishLayered = (): VisualGraph => {
|
|
1185
|
+
if (!pipeline || !initial) throw new Error("Missing layered pipeline");
|
|
1186
|
+
if (initial.done) return initial.value;
|
|
1187
|
+
const completed = pipeline.next(
|
|
1188
|
+
activeOrders?.has(prepared)
|
|
1189
|
+
? initial.value.finishOrder(activeOrders.get(prepared)!)
|
|
1190
|
+
: initial.value.minimize(),
|
|
1191
|
+
);
|
|
1192
|
+
if (!completed.done) throw new Error("Layered scope yielded more than one crossing phase");
|
|
1193
|
+
return completed.value;
|
|
1194
|
+
};
|
|
1195
|
+
const finish = async (): Promise<ElkNode> => {
|
|
1196
|
+
for (const complete of finishChildren) await complete();
|
|
1197
|
+
for (const nativeGraph of new Set(
|
|
1198
|
+
[graph_, phase?.input.graph].filter((value) => value !== undefined),
|
|
1199
|
+
)) {
|
|
1200
|
+
for (const child of graph.children ?? []) {
|
|
1201
|
+
const native = nativeGraph.nodes.find((node) => node.id === String(child.id));
|
|
1202
|
+
if (!native || !preparedChildren.has(String(child.id))) continue;
|
|
1203
|
+
phase?.updateNodeSize(native.id, { width: child.width ?? 0, height: child.height ?? 0 });
|
|
1204
|
+
Object.assign(native, { width: child.width, height: child.height });
|
|
1205
|
+
for (const port of native.ports ?? []) {
|
|
1206
|
+
const actual = child.ports?.find((candidate) => String(candidate.id) === port.name);
|
|
1207
|
+
if (actual)
|
|
1208
|
+
Object.assign(port, {
|
|
1209
|
+
x: actual.x,
|
|
1210
|
+
y: actual.y,
|
|
1211
|
+
width: actual.width,
|
|
1212
|
+
height: actual.height,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const laidOut = await (algorithm === "sporeCompaction"
|
|
556
1218
|
? executeElkjs0111Layout({
|
|
557
|
-
algorithm: "
|
|
1219
|
+
algorithm: "sporeCompaction",
|
|
558
1220
|
graph: graph_,
|
|
559
1221
|
options: {
|
|
560
1222
|
padding,
|
|
561
1223
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
562
1224
|
},
|
|
563
1225
|
})
|
|
564
|
-
: algorithm === "
|
|
1226
|
+
: algorithm === "sporeOverlap"
|
|
565
1227
|
? executeElkjs0111Layout({
|
|
566
|
-
algorithm: "
|
|
1228
|
+
algorithm: "sporeOverlap",
|
|
567
1229
|
graph: graph_,
|
|
568
1230
|
options: {
|
|
569
|
-
padding
|
|
570
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
1231
|
+
padding,
|
|
1232
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
571
1233
|
},
|
|
572
1234
|
})
|
|
573
|
-
: algorithm === "
|
|
1235
|
+
: algorithm === "rectpacking"
|
|
574
1236
|
? executeElkjs0111Layout({
|
|
575
|
-
algorithm: "
|
|
1237
|
+
algorithm: "rectpacking",
|
|
576
1238
|
graph: graph_,
|
|
577
1239
|
options: {
|
|
578
1240
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
579
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
580
|
-
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
581
|
-
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
1241
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode") ?? 15,
|
|
582
1242
|
},
|
|
583
1243
|
})
|
|
584
|
-
: algorithm === "
|
|
1244
|
+
: algorithm === "random"
|
|
585
1245
|
? executeElkjs0111Layout({
|
|
586
|
-
algorithm: "
|
|
1246
|
+
algorithm: "random",
|
|
587
1247
|
graph: graph_,
|
|
588
1248
|
options: {
|
|
589
1249
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
590
1250
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
591
1251
|
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
592
|
-
|
|
593
|
-
expandNodes: getBooleanOption(layoutOptions, "expandNodes"),
|
|
594
|
-
priority: (node) => {
|
|
595
|
-
const child = graph.children?.find(
|
|
596
|
-
(candidate) => String(candidate.id) === node.id,
|
|
597
|
-
);
|
|
598
|
-
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
599
|
-
},
|
|
1252
|
+
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
600
1253
|
},
|
|
601
1254
|
})
|
|
602
|
-
: algorithm === "
|
|
1255
|
+
: algorithm === "box"
|
|
603
1256
|
? executeElkjs0111Layout({
|
|
604
|
-
algorithm: "
|
|
605
|
-
graph: graph_,
|
|
606
|
-
options: { direction: getDirection(layoutOptions) },
|
|
607
|
-
})
|
|
608
|
-
: executeElkjs0111Layout({
|
|
609
|
-
algorithm: "layered",
|
|
1257
|
+
algorithm: "box",
|
|
610
1258
|
graph: graph_,
|
|
611
1259
|
options: {
|
|
612
|
-
|
|
613
|
-
spacing:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
(getNumberOption(
|
|
619
|
-
layoutOptions,
|
|
620
|
-
"layered.spacing.nodeNodeBetweenLayers",
|
|
621
|
-
) ??
|
|
622
|
-
getNumberOption(layoutOptions, "layered.spacing.baseValue") ??
|
|
623
|
-
20) +
|
|
624
|
-
(hasHierarchyCrossingEdges
|
|
625
|
-
? 5 * Math.max(...hierarchyBoundaryCountByEdge.values())
|
|
626
|
-
: 0),
|
|
627
|
-
},
|
|
628
|
-
padding,
|
|
629
|
-
constraints: {
|
|
630
|
-
layer: () => undefined,
|
|
631
|
-
},
|
|
632
|
-
settings: {
|
|
633
|
-
...getLayeredSettings(layoutOptions),
|
|
634
|
-
...(hierarchyHandling === "INCLUDE_CHILDREN" &&
|
|
635
|
-
getOption(
|
|
636
|
-
layoutOptions,
|
|
637
|
-
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
638
|
-
) !== undefined
|
|
639
|
-
? {
|
|
640
|
-
"crossingMinimization.greedySwitch.type": String(
|
|
641
|
-
getOption(
|
|
642
|
-
layoutOptions,
|
|
643
|
-
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
644
|
-
),
|
|
645
|
-
) as LayeredAdvancedOptions["crossingMinimization.greedySwitch.type"],
|
|
646
|
-
}
|
|
647
|
-
: {}),
|
|
648
|
-
},
|
|
649
|
-
nodeSettings: (node) => {
|
|
1260
|
+
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
1261
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
1262
|
+
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
1263
|
+
interactive: getBooleanOption(layoutOptions, "interactive"),
|
|
1264
|
+
expandNodes: getBooleanOption(layoutOptions, "expandNodes"),
|
|
1265
|
+
priority: (node) => {
|
|
650
1266
|
const child = graph.children?.find(
|
|
651
1267
|
(candidate) => String(candidate.id) === node.id,
|
|
652
1268
|
);
|
|
653
|
-
return
|
|
654
|
-
},
|
|
655
|
-
edgeSettings: (edge) => {
|
|
656
|
-
const elkEdge = graph.edges?.find(
|
|
657
|
-
(candidate) => String(candidate.id) === edge.id,
|
|
658
|
-
);
|
|
659
|
-
return {
|
|
660
|
-
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
661
|
-
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {}),
|
|
662
|
-
};
|
|
663
|
-
},
|
|
664
|
-
portSettings: (port, node) => {
|
|
665
|
-
const child = graph.children?.find(
|
|
666
|
-
(candidate) => String(candidate.id) === node.id,
|
|
667
|
-
);
|
|
668
|
-
const elkPort = child?.ports?.find(
|
|
669
|
-
(candidate) => String(candidate.id) === port.name,
|
|
670
|
-
);
|
|
671
|
-
return {
|
|
672
|
-
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
673
|
-
"port.labelWidth": Math.max(
|
|
674
|
-
0,
|
|
675
|
-
...(elkPort?.labels ?? []).map((label) => label.width ?? 0),
|
|
676
|
-
),
|
|
677
|
-
"port.labelHeight": Math.max(
|
|
678
|
-
0,
|
|
679
|
-
...(elkPort?.labels ?? []).map((label) => label.height ?? 0),
|
|
680
|
-
),
|
|
681
|
-
} as ElkLayeredOptionValueByName;
|
|
1269
|
+
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
682
1270
|
},
|
|
683
1271
|
},
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const originalTargetId = String(restoration.targets?.[0] ?? restoration.target);
|
|
696
|
-
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
697
|
-
const outsideId = sourceInside ? originalTargetId : originalSourceId;
|
|
698
|
-
const delta = sourceInside
|
|
699
|
-
? cross(route[0]!) - cross(route.at(-1)!)
|
|
700
|
-
: cross(route.at(-1)!) - cross(route[0]!);
|
|
701
|
-
const candidates = shiftsByOutsideId.get(outsideId) ?? [];
|
|
702
|
-
candidates.push(delta);
|
|
703
|
-
shiftsByOutsideId.set(outsideId, candidates);
|
|
704
|
-
}
|
|
705
|
-
for (const [outsideId, candidates] of shiftsByOutsideId) {
|
|
706
|
-
const delta = [...candidates].sort((left, right) => Math.abs(left) - Math.abs(right))[0]!;
|
|
707
|
-
const node = laidOut.nodes.find((candidate) => candidate.id === outsideId);
|
|
708
|
-
if (!node || Math.abs(delta) < 1e-9 || Math.abs(delta) > nodeSpacing) continue;
|
|
709
|
-
if (horizontal) node.y = (node.y ?? 0) + delta;
|
|
710
|
-
else node.x = (node.x ?? 0) + delta;
|
|
711
|
-
for (const edge of laidOut.edges) {
|
|
712
|
-
const points = edge.points;
|
|
713
|
-
if (!points || points.length === 0) continue;
|
|
714
|
-
if (edge.sourceId === outsideId) {
|
|
715
|
-
if (horizontal) points[0]!.y += delta;
|
|
716
|
-
else points[0]!.x += delta;
|
|
717
|
-
}
|
|
718
|
-
if (edge.targetId === outsideId) {
|
|
719
|
-
if (horizontal) points.at(-1)!.y += delta;
|
|
720
|
-
else points.at(-1)!.x += delta;
|
|
721
|
-
}
|
|
1272
|
+
})
|
|
1273
|
+
: algorithm === "fixed"
|
|
1274
|
+
? executeElkjs0111Layout({
|
|
1275
|
+
algorithm: "fixed",
|
|
1276
|
+
graph: graph_,
|
|
1277
|
+
options: { direction: getDirection(layoutOptions) },
|
|
1278
|
+
})
|
|
1279
|
+
: finishLayered());
|
|
1280
|
+
const resultPolicy = (
|
|
1281
|
+
laidOut as typeof laidOut & {
|
|
1282
|
+
[elkjs0111ResultPolicy]?: Elkjs0111ResultPolicy;
|
|
722
1283
|
}
|
|
1284
|
+
)[elkjs0111ResultPolicy];
|
|
1285
|
+
applyLayout(graph, laidOut, padding, layoutOptions, resultPolicy);
|
|
1286
|
+
if (resultPolicy?.providerBounds) {
|
|
1287
|
+
graph.width = resultPolicy.providerBounds.width;
|
|
1288
|
+
graph.height = resultPolicy.providerBounds.height;
|
|
723
1289
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1290
|
+
for (const restoration of hierarchyRestorations) {
|
|
1291
|
+
const splits = boundaryRoutes.get(restoration.edge);
|
|
1292
|
+
const sections = restoration.edge.sections;
|
|
1293
|
+
if (splits && sections?.length) {
|
|
1294
|
+
const parentPoints = sections.flatMap((section, index) => [
|
|
1295
|
+
...(index === 0 ? [section.startPoint] : []),
|
|
1296
|
+
...(section.bendPoints ?? []),
|
|
1297
|
+
section.endPoint,
|
|
1298
|
+
]);
|
|
1299
|
+
const segments = [
|
|
1300
|
+
...(splits.source
|
|
1301
|
+
? [
|
|
1302
|
+
{
|
|
1303
|
+
points: splits.source.points,
|
|
1304
|
+
offset: { x: splits.source.owner.x ?? 0, y: splits.source.owner.y ?? 0 },
|
|
1305
|
+
},
|
|
1306
|
+
]
|
|
1307
|
+
: []),
|
|
1308
|
+
{ points: parentPoints, offset: { x: 0, y: 0 } },
|
|
1309
|
+
...(splits.target
|
|
1310
|
+
? [
|
|
1311
|
+
{
|
|
1312
|
+
points: splits.target.points,
|
|
1313
|
+
offset: { x: splits.target.owner.x ?? 0, y: splits.target.owner.y ?? 0 },
|
|
1314
|
+
},
|
|
1315
|
+
]
|
|
1316
|
+
: []),
|
|
1317
|
+
];
|
|
1318
|
+
const points = joinCompoundRouteSegments(
|
|
1319
|
+
segments,
|
|
1320
|
+
getBooleanOption(layoutOptions, "unnecessaryBendpoints") === true,
|
|
746
1321
|
);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1322
|
+
const childJunctions = (split: typeof splits.source) =>
|
|
1323
|
+
(split?.junctionPoints ?? []).map((point) => ({
|
|
1324
|
+
x: point.x + (split?.owner.x ?? 0),
|
|
1325
|
+
y: point.y + (split?.owner.y ?? 0),
|
|
1326
|
+
}));
|
|
1327
|
+
const junctions = [
|
|
1328
|
+
...childJunctions(splits.source),
|
|
1329
|
+
...(restoration.edge.junctionPoints ?? []),
|
|
1330
|
+
...childJunctions(splits.target),
|
|
1331
|
+
];
|
|
1332
|
+
if (junctions.length) restoration.edge.junctionPoints = junctions;
|
|
1333
|
+
else delete restoration.edge.junctionPoints;
|
|
1334
|
+
restoration.edge.sections = [
|
|
1335
|
+
{
|
|
1336
|
+
...sections[0]!,
|
|
1337
|
+
startPoint: points[0]!,
|
|
1338
|
+
endPoint: points.at(-1)!,
|
|
1339
|
+
bendPoints: points.slice(1, -1),
|
|
1340
|
+
},
|
|
1341
|
+
];
|
|
1342
|
+
}
|
|
1343
|
+
restoration.edge.sources = restoration.sources;
|
|
1344
|
+
restoration.edge.targets = restoration.targets;
|
|
1345
|
+
restoration.edge.source = restoration.source;
|
|
1346
|
+
restoration.edge.target = restoration.target;
|
|
1347
|
+
for (const section of restoration.edge.sections ?? []) {
|
|
1348
|
+
if (
|
|
1349
|
+
section.incomingShape != null &&
|
|
1350
|
+
syntheticPortIds.has(String(section.incomingShape))
|
|
1351
|
+
) {
|
|
1352
|
+
section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
1353
|
+
}
|
|
1354
|
+
if (
|
|
1355
|
+
section.outgoingShape != null &&
|
|
1356
|
+
syntheticPortIds.has(String(section.outgoingShape))
|
|
1357
|
+
) {
|
|
1358
|
+
section.outgoingShape = restoration.targets?.[0] ?? restoration.target;
|
|
763
1359
|
}
|
|
764
|
-
crossEnd = compactedCross + (horizontal ? node.height : node.width);
|
|
765
1360
|
}
|
|
766
1361
|
}
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (nativeEdge.sourceId === nativeEdge.targetId) continue;
|
|
778
|
-
if (Math.abs(cross(start) - cross(end)) < 1e-9) {
|
|
779
|
-
route.splice(1, route.length - 2);
|
|
780
|
-
continue;
|
|
1362
|
+
for (const [compound, ports] of authoredPortsByCompound) {
|
|
1363
|
+
for (const port of ports ?? []) {
|
|
1364
|
+
const actual = compound.ports?.find((p) => p.id === port.id);
|
|
1365
|
+
if (actual)
|
|
1366
|
+
Object.assign(port, {
|
|
1367
|
+
x: actual.x,
|
|
1368
|
+
y: actual.y,
|
|
1369
|
+
width: actual.width,
|
|
1370
|
+
height: actual.height,
|
|
1371
|
+
});
|
|
781
1372
|
}
|
|
782
|
-
|
|
783
|
-
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
784
|
-
const flowForward = direction === "right" || direction === "down";
|
|
785
|
-
const track = sourceInside
|
|
786
|
-
? (horizontal ? end.x : end.y) - (flowForward ? edgeNodeSpacing : -edgeNodeSpacing)
|
|
787
|
-
: (horizontal ? start.x : start.y) + (flowForward ? edgeNodeSpacing : -edgeNodeSpacing);
|
|
788
|
-
route.splice(
|
|
789
|
-
1,
|
|
790
|
-
route.length - 2,
|
|
791
|
-
horizontal ? { x: track, y: start.y } : { x: start.x, y: track },
|
|
792
|
-
horizontal ? { x: track, y: end.y } : { x: end.x, y: track },
|
|
793
|
-
);
|
|
1373
|
+
compound.ports = ports;
|
|
794
1374
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
[
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
restoration.edge.targets = restoration.targets;
|
|
809
|
-
restoration.edge.source = restoration.source;
|
|
810
|
-
restoration.edge.target = restoration.target;
|
|
811
|
-
for (const section of restoration.edge.sections ?? []) {
|
|
812
|
-
if (section.incomingShape != null && syntheticPortIds.has(String(section.incomingShape))) {
|
|
813
|
-
section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
814
|
-
}
|
|
815
|
-
if (section.outgoingShape != null && syntheticPortIds.has(String(section.outgoingShape))) {
|
|
816
|
-
section.outgoingShape = restoration.targets?.[0] ?? restoration.target;
|
|
1375
|
+
for (const [compound, options_] of authoredOptionsByCompound)
|
|
1376
|
+
compound.layoutOptions = options_;
|
|
1377
|
+
if (hasHierarchy && topdownLayout) {
|
|
1378
|
+
for (const child of graph.children ?? []) {
|
|
1379
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
1380
|
+
const position = { x: child.x, y: child.y };
|
|
1381
|
+
await this.#layout(child, {
|
|
1382
|
+
...arguments_,
|
|
1383
|
+
logging: false,
|
|
1384
|
+
measureExecutionTime: false,
|
|
1385
|
+
});
|
|
1386
|
+
child.x = position.x;
|
|
1387
|
+
child.y = position.y;
|
|
817
1388
|
}
|
|
818
1389
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
logging: false,
|
|
829
|
-
measureExecutionTime: false,
|
|
830
|
-
});
|
|
831
|
-
child.x = position.x;
|
|
832
|
-
child.y = position.y;
|
|
1390
|
+
applyInsideSelfLoops(graph, insideSelfLoopBaseHeightByNodeId);
|
|
1391
|
+
if (arguments_.logging || arguments_.measureExecutionTime) {
|
|
1392
|
+
graph.logging = {
|
|
1393
|
+
name: "Native TypeScript layout",
|
|
1394
|
+
children: [{ name: String(algorithm) }],
|
|
1395
|
+
...(arguments_.measureExecutionTime
|
|
1396
|
+
? { executionTime: (performance.now() - startedAt) / 1_000 }
|
|
1397
|
+
: {}),
|
|
1398
|
+
};
|
|
833
1399
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1400
|
+
return graph;
|
|
1401
|
+
};
|
|
1402
|
+
return prepared;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** Move opposite-direction track sharing apart on the finished layout. */
|
|
1407
|
+
function separated<T extends ElkNode>(graph: T, options: Readonly<Record<string, unknown>>): T {
|
|
1408
|
+
separateElkOpposingTracks(graph, { ...options, ...graph.layoutOptions });
|
|
1409
|
+
return graph;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function coordinatePreparedScopes(
|
|
1413
|
+
root: PreparedElkScope,
|
|
1414
|
+
): ReadonlyMap<PreparedElkScope, LayerOrder> | undefined {
|
|
1415
|
+
if (!root.phase || root.children.size === 0) return undefined;
|
|
1416
|
+
const prepared = [root];
|
|
1417
|
+
for (let i = 0; i < prepared.length; i++) prepared.push(...prepared[i]!.children.values());
|
|
1418
|
+
if (
|
|
1419
|
+
prepared.some(
|
|
1420
|
+
(scope) =>
|
|
1421
|
+
!scope.phase ||
|
|
1422
|
+
!["LAYER_SWEEP", "MEDIAN_LAYER_SWEEP"].includes(
|
|
1423
|
+
scope.options?.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP",
|
|
1424
|
+
),
|
|
1425
|
+
)
|
|
1426
|
+
)
|
|
1427
|
+
return undefined;
|
|
1428
|
+
const scopes = new Map<PreparedElkScope, HierarchyCrossingScope>();
|
|
1429
|
+
for (const scope of prepared) {
|
|
1430
|
+
const phase = scope.phase!;
|
|
1431
|
+
scopes.set(scope, {
|
|
1432
|
+
session: createLayerSweepSession(
|
|
1433
|
+
phase.input,
|
|
1434
|
+
phase.orientation,
|
|
1435
|
+
phase.assignment,
|
|
1436
|
+
scope.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP"
|
|
1437
|
+
? "median"
|
|
1438
|
+
: "mean",
|
|
1439
|
+
7,
|
|
1440
|
+
{ resetSeed: scope === root },
|
|
1441
|
+
),
|
|
1442
|
+
childrenByNodeId: new Map(),
|
|
1443
|
+
useBottomUp: true,
|
|
1444
|
+
independentRandom: scope !== root,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
for (const parent of prepared) {
|
|
1448
|
+
const parentScope = scopes.get(parent)!;
|
|
1449
|
+
for (const [id, child] of parent.children) {
|
|
1450
|
+
const nativeParent = parent.phase!.input.graph.nodes.find((node) => node.id === id)!;
|
|
1451
|
+
const parentPortOrderFixed = ["FIXED_ORDER", "FIXED_POS", "FIXED_RATIO"].includes(
|
|
1452
|
+
parent.phase!.input.nodeSettings?.(nativeParent)?.portConstraints ?? "FREE",
|
|
1453
|
+
);
|
|
1454
|
+
const ports = nativeParent.ports ?? [];
|
|
1455
|
+
const childPhase = child.phase!;
|
|
1456
|
+
const childSession = scopes.get(child)!.session;
|
|
1457
|
+
const childNodeById = new Map<string, (typeof childPhase.input.graph.nodes)[number]>();
|
|
1458
|
+
for (const node of childPhase.input.graph.nodes)
|
|
1459
|
+
if (!childNodeById.has(node.id)) childNodeById.set(node.id, node);
|
|
1460
|
+
const parentEdgeById = new Map<string, (typeof childPhase.input.graph.edges)[number]>();
|
|
1461
|
+
for (const edge of parent.phase!.input.graph.edges)
|
|
1462
|
+
if (!parentEdgeById.has(edge.id)) parentEdgeById.set(edge.id, edge);
|
|
1463
|
+
const flowSides = hierarchicalPortSides(parent.phase!.input);
|
|
1464
|
+
const inputPorts = ports.filter(
|
|
1465
|
+
(port) =>
|
|
1466
|
+
parent.phase!.input.portSettings?.(port, nativeParent)?.["port.side"] ===
|
|
1467
|
+
flowSides.before,
|
|
1468
|
+
).length;
|
|
1469
|
+
const outputPorts = ports.filter(
|
|
1470
|
+
(port) =>
|
|
1471
|
+
parent.phase!.input.portSettings?.(port, nativeParent)?.["port.side"] === flowSides.after,
|
|
1472
|
+
).length;
|
|
1473
|
+
const childScope: HierarchyCrossingScope = {
|
|
1474
|
+
...scopes.get(child)!,
|
|
1475
|
+
useBottomUp: useBottomUpHierarchySweep(childPhase.input, childSession.crossingGraph(), {
|
|
1476
|
+
portOrderFixed: parentPortOrderFixed,
|
|
1477
|
+
inputPorts,
|
|
1478
|
+
outputPorts,
|
|
1479
|
+
deterministic:
|
|
1480
|
+
child.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP",
|
|
1481
|
+
}),
|
|
1482
|
+
alignBoundary: (forward) => {
|
|
1483
|
+
const childLayers = childSession.currentLayers();
|
|
1484
|
+
const layerIndex = forward ? 0 : childLayers.length - 1;
|
|
1485
|
+
const layer = childLayers[layerIndex]!;
|
|
1486
|
+
if (!layer.every((id) => externalPortDummyOf(childNodeById.get(id)!))) return false;
|
|
1487
|
+
// Parent port positions are physical; feedback reversal changes
|
|
1488
|
+
// edge direction without changing the boundary port's identity.
|
|
1489
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
1490
|
+
const dummyByParentPort = new Map(
|
|
1491
|
+
childPhase.input.graph.nodes
|
|
1492
|
+
.filter((n) => externalPortDummyOf(n))
|
|
1493
|
+
.map((n) => [externalPortDummyOf(n)!.parentPortId ?? `${n.id}:parent`, n.id]),
|
|
1494
|
+
);
|
|
1495
|
+
const ordered = physical.flatMap((portId) => {
|
|
1496
|
+
const [nodeId, portName] = JSON.parse(portId) as [string, string];
|
|
1497
|
+
const dummy = nodeId === id ? dummyByParentPort.get(portName) : undefined;
|
|
1498
|
+
return dummy && layer.includes(dummy) ? [dummy] : [];
|
|
1499
|
+
});
|
|
1500
|
+
if (forward) ordered.reverse();
|
|
1501
|
+
const unique = [...new Set(ordered)];
|
|
1502
|
+
if (unique.length !== layer.length)
|
|
1503
|
+
throw new Error(`Incomplete hierarchical port order for ${id}`);
|
|
1504
|
+
childSession.replaceLayer(layerIndex, unique);
|
|
1505
|
+
return true;
|
|
1506
|
+
},
|
|
1507
|
+
publishBoundary: (forward) => {
|
|
1508
|
+
const childLayers = childSession.currentLayers();
|
|
1509
|
+
const layer = childLayers[forward ? childLayers.length - 1 : 0]!;
|
|
1510
|
+
if (!layer.every((id) => externalPortDummyOf(childNodeById.get(id)!))) return;
|
|
1511
|
+
const rank = new Map(
|
|
1512
|
+
layer.map((dummy, index) => [
|
|
1513
|
+
externalPortDummyOf(childNodeById.get(dummy)!)!.parentPortId ?? `${dummy}:parent`,
|
|
1514
|
+
index,
|
|
1515
|
+
]),
|
|
1516
|
+
);
|
|
1517
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
1518
|
+
const boundarySlots = physical.flatMap((portId, index) => {
|
|
1519
|
+
const [nodeId, portName] = JSON.parse(portId) as [string, string];
|
|
1520
|
+
return nodeId === id && rank.has(portName) ? [index] : [];
|
|
1521
|
+
});
|
|
1522
|
+
const boundaryPorts = boundarySlots
|
|
1523
|
+
.map((index) => physical[index]!)
|
|
1524
|
+
.sort((a, b) => {
|
|
1525
|
+
const portA = (JSON.parse(a) as [string, string])[1];
|
|
1526
|
+
const portB = (JSON.parse(b) as [string, string])[1];
|
|
1527
|
+
const result = rank.get(portA)! - rank.get(portB)!;
|
|
1528
|
+
return forward ? result : -result;
|
|
1529
|
+
});
|
|
1530
|
+
const updatedPhysical = [...physical];
|
|
1531
|
+
boundarySlots.forEach((slot, index) => {
|
|
1532
|
+
updatedPhysical[slot] = boundaryPorts[index]!;
|
|
1533
|
+
});
|
|
1534
|
+
const edgePortRank = (edgeId: string): number => {
|
|
1535
|
+
const edge = parentEdgeById.get(edgeId)!;
|
|
1536
|
+
const portName = edge.sourceId === id ? edge.sourcePort : edge.targetPort;
|
|
1537
|
+
return updatedPhysical.indexOf(JSON.stringify([id, portName]));
|
|
1538
|
+
};
|
|
1539
|
+
const { incoming, outgoing } = parentScope.session.edgeOrders(id);
|
|
1540
|
+
const byPort = (current: readonly string[]) =>
|
|
1541
|
+
[...current].sort((a, b) => edgePortRank(a) - edgePortRank(b));
|
|
1542
|
+
parentScope.session.reorderNodePorts(
|
|
1543
|
+
id,
|
|
1544
|
+
updatedPhysical,
|
|
1545
|
+
byPort(incoming),
|
|
1546
|
+
byPort(outgoing),
|
|
1547
|
+
);
|
|
1548
|
+
},
|
|
843
1549
|
};
|
|
1550
|
+
childScope.publishBottomUp = () => {
|
|
1551
|
+
childScope.publishBoundary!(true);
|
|
1552
|
+
childScope.publishBoundary!(false);
|
|
1553
|
+
parentScope.session.lockPortOrder(id);
|
|
1554
|
+
};
|
|
1555
|
+
scopes.set(child, childScope);
|
|
1556
|
+
(parentScope.childrenByNodeId as Map<string, HierarchyCrossingScope>).set(id, childScope);
|
|
844
1557
|
}
|
|
845
|
-
return graph as LaidOutElkNode<T>;
|
|
846
1558
|
}
|
|
1559
|
+
const orders = minimizeHierarchyCrossings(scopes.get(root)!);
|
|
1560
|
+
|
|
1561
|
+
return new Map(prepared.map((scope) => [scope, orders.get(scopes.get(scope)!)!]));
|
|
847
1562
|
}
|
|
848
1563
|
|
|
849
1564
|
function parsePadding(value: unknown, fallback = 0) {
|
|
@@ -1083,16 +1798,56 @@ function parseMargin(value: unknown): unknown {
|
|
|
1083
1798
|
return parsePadding(value, 0);
|
|
1084
1799
|
}
|
|
1085
1800
|
|
|
1801
|
+
// Every accepted spelling of a layered option, with its precedence (lower wins).
|
|
1802
|
+
const layeredOptionAliases = new Map<
|
|
1803
|
+
string,
|
|
1804
|
+
{ definition: (typeof elkLayeredOptionDefinitions)[number]; rank: number }
|
|
1805
|
+
>();
|
|
1806
|
+
for (const definition of elkLayeredOptionDefinitions) {
|
|
1807
|
+
const suffix = definition.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
1808
|
+
[definition.name, suffix, `elk.${suffix}`, definition.elkId].forEach((key, rank) => {
|
|
1809
|
+
const current = layeredOptionAliases.get(key);
|
|
1810
|
+
if (!current || rank < current.rank) layeredOptionAliases.set(key, { definition, rank });
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const indexByIdCache = new WeakMap<
|
|
1815
|
+
readonly { id?: ElkId }[],
|
|
1816
|
+
{ length: number; index: Map<string, number> }
|
|
1817
|
+
>();
|
|
1818
|
+
|
|
1819
|
+
/** First element whose stringified id matches, like `find`, via a per-array index. */
|
|
1820
|
+
function findById<T extends { id?: ElkId }>(items: readonly T[] | undefined, id: string) {
|
|
1821
|
+
if (!items) return undefined;
|
|
1822
|
+
let cached = indexByIdCache.get(items);
|
|
1823
|
+
if (!cached || cached.length !== items.length) {
|
|
1824
|
+
const index = new Map<string, number>();
|
|
1825
|
+
items.forEach((item, position) => {
|
|
1826
|
+
const key = String(item.id);
|
|
1827
|
+
if (!index.has(key)) index.set(key, position);
|
|
1828
|
+
});
|
|
1829
|
+
indexByIdCache.set(items, (cached = { length: items.length, index }));
|
|
1830
|
+
}
|
|
1831
|
+
const position = cached.index.get(id);
|
|
1832
|
+
return position === undefined ? undefined : { item: items[position]!, position };
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1086
1835
|
function getElementLayeredSettings(
|
|
1087
1836
|
options: Readonly<Record<string, unknown>>,
|
|
1088
1837
|
): ElkLayeredOptionValueByName {
|
|
1089
1838
|
const settings: ElkLayeredOptionValueByName = {};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1839
|
+
const chosen = new Map<
|
|
1840
|
+
(typeof elkLayeredOptionDefinitions)[number],
|
|
1841
|
+
{ rank: number; value: unknown }
|
|
1842
|
+
>();
|
|
1843
|
+
for (const [key, value] of Object.entries(options)) {
|
|
1844
|
+
const alias = layeredOptionAliases.get(key);
|
|
1845
|
+
if (!alias || value === undefined) continue;
|
|
1846
|
+
const current = chosen.get(alias.definition);
|
|
1847
|
+
if (!current || alias.rank < current.rank)
|
|
1848
|
+
chosen.set(alias.definition, { rank: alias.rank, value });
|
|
1849
|
+
}
|
|
1850
|
+
for (const [definition, { value }] of chosen) {
|
|
1096
1851
|
const vectorMatch =
|
|
1097
1852
|
definition.name === "port.anchor" && typeof value === "string"
|
|
1098
1853
|
? value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/)
|
|
@@ -1260,7 +2015,16 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1260
2015
|
if (String(getOption(child.layoutOptions ?? {}, "portConstraints")) !== "FIXED_SIDE") {
|
|
1261
2016
|
return ports;
|
|
1262
2017
|
}
|
|
1263
|
-
const
|
|
2018
|
+
const canonicalSides: Record<string, string> =
|
|
2019
|
+
getDirection(globalOptions) === "left"
|
|
2020
|
+
? { NORTH: "NORTH", EAST: "WEST", SOUTH: "SOUTH", WEST: "EAST" }
|
|
2021
|
+
: getDirection(globalOptions) === "down"
|
|
2022
|
+
? { NORTH: "WEST", EAST: "SOUTH", SOUTH: "EAST", WEST: "NORTH" }
|
|
2023
|
+
: getDirection(globalOptions) === "up"
|
|
2024
|
+
? { NORTH: "EAST", EAST: "SOUTH", SOUTH: "WEST", WEST: "NORTH" }
|
|
2025
|
+
: { NORTH: "NORTH", EAST: "EAST", SOUTH: "SOUTH", WEST: "WEST" };
|
|
2026
|
+
const side = (port: ElkPort) =>
|
|
2027
|
+
canonicalSides[String(getOption(port.layoutOptions ?? {}, "port.side"))] ?? "UNDEFINED";
|
|
1264
2028
|
const sideOrder = ["NORTH", "EAST", "SOUTH", "WEST"];
|
|
1265
2029
|
ports.sort((left, right) => {
|
|
1266
2030
|
const leftSide = side(left);
|
|
@@ -1287,10 +2051,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1287
2051
|
const usePortModelOrder =
|
|
1288
2052
|
getBooleanOption(globalOptions, "layered.considerModelOrder.portModelOrder") === true;
|
|
1289
2053
|
if (modelOrderStrategy !== "NONE" && !usePortModelOrder) {
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
2054
|
+
const leftOrder = edgeModelOrderByPortId.get(String(left.id));
|
|
2055
|
+
const rightOrder = edgeModelOrderByPortId.get(String(right.id));
|
|
2056
|
+
// Ports without edges order last and keep their order among themselves,
|
|
2057
|
+
// unreversed even on the WEST side, as ELK does.
|
|
2058
|
+
if (leftOrder === undefined && rightOrder === undefined) return 0;
|
|
2059
|
+
if (leftOrder !== rightOrder) return (leftOrder ?? Infinity) - (rightOrder ?? Infinity);
|
|
1294
2060
|
}
|
|
1295
2061
|
const direction = leftSide === "WEST" ? -1 : 1;
|
|
1296
2062
|
return direction * ((child.ports?.indexOf(left) ?? 0) - (child.ports?.indexOf(right) ?? 0));
|
|
@@ -1298,7 +2064,7 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1298
2064
|
return ports;
|
|
1299
2065
|
};
|
|
1300
2066
|
|
|
1301
|
-
|
|
2067
|
+
const native = createGraph({
|
|
1302
2068
|
id: String(root.id),
|
|
1303
2069
|
nodes: children.map((child) => ({
|
|
1304
2070
|
id: String(child.id),
|
|
@@ -1353,6 +2119,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1353
2119
|
: [];
|
|
1354
2120
|
}),
|
|
1355
2121
|
});
|
|
2122
|
+
for (const node of native.nodes) {
|
|
2123
|
+
const child = children.find((candidate) => String(candidate.id) === node.id);
|
|
2124
|
+
const origin = child && externalPortDummyOf(child);
|
|
2125
|
+
if (origin) attachExternalPortDummy(node, origin);
|
|
2126
|
+
}
|
|
2127
|
+
return native;
|
|
1356
2128
|
}
|
|
1357
2129
|
|
|
1358
2130
|
function parsePoints(value: unknown): ElkPoint[] | undefined {
|
|
@@ -1431,11 +2203,21 @@ function applyLayout(
|
|
|
1431
2203
|
continue;
|
|
1432
2204
|
}
|
|
1433
2205
|
const section = getParentEdgeSection(root, edge);
|
|
1434
|
-
if (section)
|
|
2206
|
+
if (section) {
|
|
2207
|
+
edge.sections = [section];
|
|
2208
|
+
edge.container = root.id;
|
|
2209
|
+
}
|
|
1435
2210
|
continue;
|
|
1436
2211
|
}
|
|
2212
|
+
const nativeJunctions = resultPolicy?.junctionPointsByEdgeId?.get(String(edge.id));
|
|
2213
|
+
if (nativeJunctions) {
|
|
2214
|
+
if (nativeJunctions.length)
|
|
2215
|
+
edge.junctionPoints = nativeJunctions.map((point) => ({ ...point }));
|
|
2216
|
+
else delete edge.junctionPoints;
|
|
2217
|
+
}
|
|
1437
2218
|
const section = toSection(edge, laidOutEdge.points ?? []);
|
|
1438
2219
|
edge.sections = section ? [section] : [];
|
|
2220
|
+
if (section) edge.container = root.id;
|
|
1439
2221
|
const target = root.children?.find(
|
|
1440
2222
|
(child) => String(child.id) === String(edge.targets?.[0] ?? edge.target),
|
|
1441
2223
|
);
|
|
@@ -1452,6 +2234,8 @@ function applyLayout(
|
|
|
1452
2234
|
const midpointX = (laidOutEdge.x ?? 0) + (laidOutEdge.width ?? 0) / 2;
|
|
1453
2235
|
const edgeLabelSpacing = getNumberOption(layoutOptions, "spacing.edgeLabel") ?? 2;
|
|
1454
2236
|
const labelLabelSpacing = getNumberOption(layoutOptions, "spacing.labelLabel") ?? 0;
|
|
2237
|
+
// A lone label keeps the native position, which already avoids collisions.
|
|
2238
|
+
const loneLabel = (edge.labels ?? []).filter(isLayoutEdgeLabel).length === 1;
|
|
1455
2239
|
for (const label of edge.labels ?? []) {
|
|
1456
2240
|
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) {
|
|
1457
2241
|
label.x ??= 0;
|
|
@@ -1468,17 +2252,51 @@ function applyLayout(
|
|
|
1468
2252
|
);
|
|
1469
2253
|
const width = label.width ?? 0;
|
|
1470
2254
|
label.x =
|
|
1471
|
-
placement
|
|
1472
|
-
?
|
|
1473
|
-
: placement === "
|
|
1474
|
-
?
|
|
1475
|
-
:
|
|
2255
|
+
loneLabel && placement !== "CENTER" && laidOutEdge.x !== undefined
|
|
2256
|
+
? laidOutEdge.x
|
|
2257
|
+
: placement === "TAIL"
|
|
2258
|
+
? firstPoint.x + edgeLabelSpacing
|
|
2259
|
+
: placement === "HEAD"
|
|
2260
|
+
? lastPoint.x - width - edgeLabelSpacing
|
|
2261
|
+
: midpointX - width / 2;
|
|
1476
2262
|
label.y = labelY;
|
|
1477
2263
|
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
1478
2264
|
}
|
|
1479
2265
|
}
|
|
1480
2266
|
if (!resultPolicy?.skipBoundsNormalization) {
|
|
1481
|
-
normalizeElkGraphBounds(
|
|
2267
|
+
normalizeElkGraphBounds(
|
|
2268
|
+
root,
|
|
2269
|
+
padding,
|
|
2270
|
+
layoutOptions,
|
|
2271
|
+
resultPolicy?.normalizationBounds,
|
|
2272
|
+
resultPolicy?.placementCrossBounds,
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
if (
|
|
2276
|
+
!resultPolicy?.preserveEdgeSections &&
|
|
2277
|
+
String(getOption(layoutOptions, "edgeRouting") ?? "ORTHOGONAL") === "ORTHOGONAL"
|
|
2278
|
+
) {
|
|
2279
|
+
const direction = getDirection(layoutOptions);
|
|
2280
|
+
applyOrthogonalJunctions(
|
|
2281
|
+
root,
|
|
2282
|
+
direction === "right" || direction === "left",
|
|
2283
|
+
direction === "right" || direction === "down",
|
|
2284
|
+
getBooleanOption(layoutOptions, "layered.mergeEdges") === true,
|
|
2285
|
+
new Set(
|
|
2286
|
+
(root.children ?? [])
|
|
2287
|
+
.filter((n) => getBooleanOption(n.layoutOptions ?? {}, "hypernode") === true)
|
|
2288
|
+
.map((n) => String(n.id)),
|
|
2289
|
+
),
|
|
2290
|
+
new Set(
|
|
2291
|
+
(root.edges ?? [])
|
|
2292
|
+
.filter(
|
|
2293
|
+
(e) =>
|
|
2294
|
+
getBooleanOption(e.layoutOptions ?? {}, "noLayout") === true ||
|
|
2295
|
+
resultPolicy?.junctionPointsByEdgeId?.has(String(e.id)) === true,
|
|
2296
|
+
)
|
|
2297
|
+
.map((e) => String(e.id)),
|
|
2298
|
+
),
|
|
2299
|
+
);
|
|
1482
2300
|
}
|
|
1483
2301
|
}
|
|
1484
2302
|
|
|
@@ -1486,15 +2304,21 @@ function normalizeElkGraphBounds(
|
|
|
1486
2304
|
root: ElkNode,
|
|
1487
2305
|
padding: { top: number; right: number; bottom: number; left: number },
|
|
1488
2306
|
layoutOptions: Readonly<Record<string, unknown>> = {},
|
|
2307
|
+
compactionBounds?: Elkjs0111ResultPolicy["normalizationBounds"],
|
|
2308
|
+
placementCrossBounds?: Elkjs0111ResultPolicy["placementCrossBounds"],
|
|
1489
2309
|
): void {
|
|
2310
|
+
// Edge coordinates belong to their layout container, not their JSON storage owner.
|
|
2311
|
+
const layoutEdges = (root.edges ?? []).filter(
|
|
2312
|
+
(edge) => edge.container == null || String(edge.container) === String(root.id),
|
|
2313
|
+
);
|
|
1490
2314
|
const authoredWidth = root.width;
|
|
1491
2315
|
const authoredHeight = root.height;
|
|
1492
2316
|
const fixedGraphSize = getBooleanOption(layoutOptions, "nodeSize.fixedGraphSize") === true;
|
|
1493
2317
|
const layoutChildren = (root.children ?? []).filter(
|
|
1494
2318
|
(node) => getBooleanOption(node.layoutOptions ?? {}, "noLayout") !== true,
|
|
1495
2319
|
);
|
|
1496
|
-
let minimumX = Number.POSITIVE_INFINITY;
|
|
1497
|
-
let minimumY = Number.POSITIVE_INFINITY;
|
|
2320
|
+
let minimumX = compactionBounds?.left ?? Number.POSITIVE_INFINITY;
|
|
2321
|
+
let minimumY = compactionBounds?.top ?? Number.POSITIVE_INFINITY;
|
|
1498
2322
|
for (const node of layoutChildren) {
|
|
1499
2323
|
minimumX = Math.min(
|
|
1500
2324
|
minimumX,
|
|
@@ -1515,7 +2339,7 @@ function normalizeElkGraphBounds(
|
|
|
1515
2339
|
),
|
|
1516
2340
|
);
|
|
1517
2341
|
}
|
|
1518
|
-
for (const edge of
|
|
2342
|
+
for (const edge of layoutEdges) {
|
|
1519
2343
|
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1520
2344
|
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1521
2345
|
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
@@ -1536,13 +2360,17 @@ function normalizeElkGraphBounds(
|
|
|
1536
2360
|
node.x = (node.x ?? 0) + shiftX;
|
|
1537
2361
|
node.y = (node.y ?? 0) + shiftY;
|
|
1538
2362
|
}
|
|
1539
|
-
for (const edge of
|
|
2363
|
+
for (const edge of layoutEdges) {
|
|
1540
2364
|
for (const section of edge.sections ?? []) {
|
|
1541
2365
|
for (const point of [section.startPoint, ...(section.bendPoints ?? []), section.endPoint]) {
|
|
1542
2366
|
point.x += shiftX;
|
|
1543
2367
|
point.y += shiftY;
|
|
1544
2368
|
}
|
|
1545
2369
|
}
|
|
2370
|
+
for (const point of edge.junctionPoints ?? []) {
|
|
2371
|
+
point.x += shiftX;
|
|
2372
|
+
point.y += shiftY;
|
|
2373
|
+
}
|
|
1546
2374
|
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
1547
2375
|
label.x = (label.x ?? 0) + shiftX;
|
|
1548
2376
|
label.y = (label.y ?? 0) + shiftY;
|
|
@@ -1557,7 +2385,7 @@ function normalizeElkGraphBounds(
|
|
|
1557
2385
|
0,
|
|
1558
2386
|
...layoutChildren.map((node) => (node.y ?? 0) + (node.height ?? 0)),
|
|
1559
2387
|
);
|
|
1560
|
-
const layoutEdgePoints =
|
|
2388
|
+
const layoutEdgePoints = layoutEdges.flatMap((edge) =>
|
|
1561
2389
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1562
2390
|
? []
|
|
1563
2391
|
: (edge.sections ?? []).flatMap((section) => [
|
|
@@ -1573,7 +2401,7 @@ function normalizeElkGraphBounds(
|
|
|
1573
2401
|
for (const child of root.children ?? []) {
|
|
1574
2402
|
for (const port of child.ports ?? []) childByEndpointId.set(String(port.id), child);
|
|
1575
2403
|
}
|
|
1576
|
-
const wrappedEdgeCount =
|
|
2404
|
+
const wrappedEdgeCount = layoutEdges.filter((edge) => {
|
|
1577
2405
|
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
1578
2406
|
const target = childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
|
|
1579
2407
|
if (!source || !target) return false;
|
|
@@ -1586,24 +2414,44 @@ function normalizeElkGraphBounds(
|
|
|
1586
2414
|
: (source.y ?? 0) < (target.y ?? 0);
|
|
1587
2415
|
}).length;
|
|
1588
2416
|
const hasWrappedEdge = wrappedEdgeCount > 0;
|
|
1589
|
-
const
|
|
2417
|
+
const retainPlacementBounds =
|
|
1590
2418
|
wrappingStrategy !== "MULTI_EDGE" &&
|
|
1591
2419
|
!(wrappingStrategy === "SINGLE_EDGE" && hasWrappedEdge) &&
|
|
1592
2420
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") ===
|
|
1593
2421
|
"NONE" &&
|
|
1594
2422
|
getBooleanOption(layoutOptions, "layered.feedbackEdges") !== true &&
|
|
1595
2423
|
String(getOption(layoutOptions, "layered.layering.nodePromotion.strategy") ?? "NONE") !==
|
|
1596
|
-
"MODEL_ORDER_LEFT_TO_RIGHT"
|
|
1597
|
-
|
|
2424
|
+
"MODEL_ORDER_LEFT_TO_RIGHT";
|
|
2425
|
+
// Actual helper extents survive nested scopes. The legacy route-pixel
|
|
2426
|
+
// allowance still excludes compound scopes.
|
|
2427
|
+
const addBoundaryPixel =
|
|
2428
|
+
retainPlacementBounds &&
|
|
1598
2429
|
!(root.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
2430
|
+
// Prefer the retained placement extent on the cross axis. Other placers
|
|
2431
|
+
// retain the legacy route-bound allowance until they expose phase bounds.
|
|
2432
|
+
// Loop envelopes own their bounds. Ordinary long-edge dummies still have
|
|
2433
|
+
// a one-pixel cross-axis extent when another edge in the graph is a loop.
|
|
2434
|
+
// Apply that allowance to route points, before unioning labels and margins.
|
|
2435
|
+
const isSelfLoop = (edge: ElkEdge) => {
|
|
2436
|
+
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
2437
|
+
return (
|
|
2438
|
+
source !== undefined &&
|
|
2439
|
+
source === childByEndpointId.get(String(edge.targets?.[0] ?? edge.target))
|
|
2440
|
+
);
|
|
2441
|
+
};
|
|
2442
|
+
const hasSelfLoops = layoutEdges.some(isSelfLoop);
|
|
1599
2443
|
const edgeBoundsExtraX =
|
|
1600
2444
|
addBoundaryPixel &&
|
|
2445
|
+
placementCrossBounds?.axis !== "x" &&
|
|
2446
|
+
!(hasSelfLoops && (direction === "right" || direction === "left")) &&
|
|
1601
2447
|
layoutEdgePoints.length > 0 &&
|
|
1602
2448
|
Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9
|
|
1603
2449
|
? 1
|
|
1604
2450
|
: 0;
|
|
1605
2451
|
const edgeBoundsExtraY =
|
|
1606
2452
|
addBoundaryPixel &&
|
|
2453
|
+
placementCrossBounds?.axis !== "y" &&
|
|
2454
|
+
!(hasSelfLoops && (direction === "down" || direction === "up")) &&
|
|
1607
2455
|
layoutEdgePoints.length > 0 &&
|
|
1608
2456
|
Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9
|
|
1609
2457
|
? 1
|
|
@@ -1616,6 +2464,7 @@ function normalizeElkGraphBounds(
|
|
|
1616
2464
|
? 1
|
|
1617
2465
|
: 0;
|
|
1618
2466
|
const postCompactionBoundsExtraX =
|
|
2467
|
+
!compactionBounds &&
|
|
1619
2468
|
!addBoundaryPixel &&
|
|
1620
2469
|
!hasWrappedEdge &&
|
|
1621
2470
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1625,6 +2474,7 @@ function normalizeElkGraphBounds(
|
|
|
1625
2474
|
? 0.04
|
|
1626
2475
|
: 0;
|
|
1627
2476
|
const postCompactionBoundsExtraY =
|
|
2477
|
+
!compactionBounds &&
|
|
1628
2478
|
!addBoundaryPixel &&
|
|
1629
2479
|
!hasWrappedEdge &&
|
|
1630
2480
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1635,7 +2485,10 @@ function normalizeElkGraphBounds(
|
|
|
1635
2485
|
: 0;
|
|
1636
2486
|
const calculatedWidth =
|
|
1637
2487
|
Math.max(
|
|
1638
|
-
0,
|
|
2488
|
+
compactionBounds ? compactionBounds.right + shiftX : 0,
|
|
2489
|
+
retainPlacementBounds && placementCrossBounds?.axis === "x"
|
|
2490
|
+
? placementCrossBounds.maximum + shiftX
|
|
2491
|
+
: 0,
|
|
1639
2492
|
...layoutChildren.flatMap((node) => [
|
|
1640
2493
|
(node.x ?? 0) + (node.width ?? 0),
|
|
1641
2494
|
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
@@ -1646,23 +2499,24 @@ function normalizeElkGraphBounds(
|
|
|
1646
2499
|
),
|
|
1647
2500
|
),
|
|
1648
2501
|
]),
|
|
1649
|
-
...
|
|
2502
|
+
...layoutEdges.flatMap((edge) =>
|
|
1650
2503
|
(edge.labels ?? [])
|
|
1651
2504
|
.filter(isLayoutEdgeLabel)
|
|
1652
2505
|
.map((label) => (label.x ?? 0) + (label.width ?? 0)),
|
|
1653
2506
|
),
|
|
1654
|
-
...
|
|
2507
|
+
...layoutEdges.flatMap((edge) =>
|
|
1655
2508
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1656
2509
|
? []
|
|
1657
2510
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1658
|
-
section.startPoint.x,
|
|
1659
|
-
...(section.bendPoints ?? []).map(
|
|
1660
|
-
|
|
2511
|
+
section.startPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
2512
|
+
...(section.bendPoints ?? []).map(
|
|
2513
|
+
(point) => point.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
2514
|
+
),
|
|
2515
|
+
section.endPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
1661
2516
|
]),
|
|
1662
2517
|
),
|
|
1663
2518
|
) +
|
|
1664
2519
|
padding.right +
|
|
1665
|
-
edgeBoundsExtraX +
|
|
1666
2520
|
postCompactionBoundsExtraX +
|
|
1667
2521
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
1668
2522
|
(getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up")
|
|
@@ -1670,7 +2524,10 @@ function normalizeElkGraphBounds(
|
|
|
1670
2524
|
: 0);
|
|
1671
2525
|
const calculatedHeight =
|
|
1672
2526
|
Math.max(
|
|
1673
|
-
0,
|
|
2527
|
+
compactionBounds ? compactionBounds.bottom + shiftY : 0,
|
|
2528
|
+
retainPlacementBounds && placementCrossBounds?.axis === "y"
|
|
2529
|
+
? placementCrossBounds.maximum + shiftY
|
|
2530
|
+
: 0,
|
|
1674
2531
|
...layoutChildren.flatMap((node) => [
|
|
1675
2532
|
(node.y ?? 0) + (node.height ?? 0),
|
|
1676
2533
|
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
@@ -1681,7 +2538,7 @@ function normalizeElkGraphBounds(
|
|
|
1681
2538
|
),
|
|
1682
2539
|
),
|
|
1683
2540
|
]),
|
|
1684
|
-
...
|
|
2541
|
+
...layoutEdges.flatMap((edge) =>
|
|
1685
2542
|
(edge.labels ?? [])
|
|
1686
2543
|
.filter(isLayoutEdgeLabel)
|
|
1687
2544
|
.map(
|
|
@@ -1689,23 +2546,26 @@ function normalizeElkGraphBounds(
|
|
|
1689
2546
|
(label.y ?? 0) +
|
|
1690
2547
|
(label.height ?? 0) +
|
|
1691
2548
|
(String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") ===
|
|
1692
|
-
|
|
2549
|
+
"CENTER" &&
|
|
2550
|
+
childByEndpointId.get(String(edge.sources?.[0] ?? edge.source)) !==
|
|
2551
|
+
childByEndpointId.get(String(edge.targets?.[0] ?? edge.target))
|
|
1693
2552
|
? 1
|
|
1694
2553
|
: 0),
|
|
1695
2554
|
),
|
|
1696
2555
|
),
|
|
1697
|
-
...
|
|
2556
|
+
...layoutEdges.flatMap((edge) =>
|
|
1698
2557
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1699
2558
|
? []
|
|
1700
2559
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1701
|
-
section.startPoint.y,
|
|
1702
|
-
...(section.bendPoints ?? []).map(
|
|
1703
|
-
|
|
2560
|
+
section.startPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
2561
|
+
...(section.bendPoints ?? []).map(
|
|
2562
|
+
(point) => point.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
2563
|
+
),
|
|
2564
|
+
section.endPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
1704
2565
|
]),
|
|
1705
2566
|
),
|
|
1706
2567
|
) +
|
|
1707
2568
|
padding.bottom +
|
|
1708
|
-
edgeBoundsExtraY +
|
|
1709
2569
|
singleMultiEdgeCutBoundsExtraY +
|
|
1710
2570
|
postCompactionBoundsExtraY +
|
|
1711
2571
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
@@ -1903,3 +2763,170 @@ export type {
|
|
|
1903
2763
|
|
|
1904
2764
|
export { getElkRoutes } from "./routes";
|
|
1905
2765
|
export type { ElkRouteOptions } from "./routes";
|
|
2766
|
+
|
|
2767
|
+
/** Edges crossing `group`, in the order ELK's CompoundGraphPreprocessor creates its boundary ports. */
|
|
2768
|
+
function elkBoundaryEdgeOrder(
|
|
2769
|
+
group: ElkNode,
|
|
2770
|
+
scope: ElkNode,
|
|
2771
|
+
originals: ReadonlyMap<ElkEdge, { sourceId: string; targetId: string }>,
|
|
2772
|
+
): ElkEdge[] {
|
|
2773
|
+
const edges: ElkEdge[] = [];
|
|
2774
|
+
const collect = (node: ElkNode) => {
|
|
2775
|
+
edges.push(...(node.edges ?? []));
|
|
2776
|
+
for (const child of node.children ?? []) collect(child);
|
|
2777
|
+
};
|
|
2778
|
+
collect(scope);
|
|
2779
|
+
const endpoints = (edge: ElkEdge) =>
|
|
2780
|
+
originals.get(edge) ?? {
|
|
2781
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
2782
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
2783
|
+
};
|
|
2784
|
+
const order = (node: ElkNode): ElkEdge[] => {
|
|
2785
|
+
const inside = new Set<string>();
|
|
2786
|
+
const mark = (parent: ElkNode) => {
|
|
2787
|
+
for (const child of parent.children ?? []) {
|
|
2788
|
+
inside.add(String(child.id));
|
|
2789
|
+
for (const port of child.ports ?? []) inside.add(String(port.id));
|
|
2790
|
+
mark(child);
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
mark(node);
|
|
2794
|
+
const leaves = (edge: ElkEdge) =>
|
|
2795
|
+
inside.has(endpoints(edge).sourceId) !== inside.has(endpoints(edge).targetId);
|
|
2796
|
+
const result = new Set<ElkEdge>();
|
|
2797
|
+
for (const child of node.children ?? [])
|
|
2798
|
+
if (child.children?.length)
|
|
2799
|
+
for (const edge of order(child)) if (leaves(edge)) result.add(edge);
|
|
2800
|
+
for (const child of node.children ?? []) {
|
|
2801
|
+
for (const port of (child.ports ?? []).map((p) => String(p.id))) {
|
|
2802
|
+
for (const edge of edges)
|
|
2803
|
+
if (endpoints(edge).sourceId === port && leaves(edge)) result.add(edge);
|
|
2804
|
+
for (const edge of edges)
|
|
2805
|
+
if (endpoints(edge).targetId === port && leaves(edge)) result.add(edge);
|
|
2806
|
+
}
|
|
2807
|
+
// Each edge on the node itself gets its own implicit port, in import order.
|
|
2808
|
+
const id = String(child.id);
|
|
2809
|
+
for (const edge of edges) {
|
|
2810
|
+
const { sourceId, targetId } = endpoints(edge);
|
|
2811
|
+
if ((sourceId === id || targetId === id) && leaves(edge)) result.add(edge);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return [...result];
|
|
2815
|
+
};
|
|
2816
|
+
return order(group);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/** Creation order of hierarchy segments in ELK's CompoundGraphPreprocessor. */
|
|
2820
|
+
const hierarchySegmentRank = new WeakMap<ElkEdge, number>();
|
|
2821
|
+
const segmentOrderByScope = new WeakMap<ElkNode, ReadonlyMap<string, number>>();
|
|
2822
|
+
const originalEdgeId = new WeakMap<ElkEdge, string>();
|
|
2823
|
+
|
|
2824
|
+
/**
|
|
2825
|
+
* Segments inside `group`, in creation order: the inner pass connects child
|
|
2826
|
+
* groups' exported ports first, then the outer pass leaves `group` itself.
|
|
2827
|
+
*/
|
|
2828
|
+
function elkSegmentOrder(
|
|
2829
|
+
group: ElkNode,
|
|
2830
|
+
scope: ElkNode,
|
|
2831
|
+
originals: ReadonlyMap<ElkEdge, { sourceId: string; targetId: string }>,
|
|
2832
|
+
ownBoundary: boolean,
|
|
2833
|
+
): Map<string, number> {
|
|
2834
|
+
const order = new Map<string, number>();
|
|
2835
|
+
const add = (edge: ElkEdge) => {
|
|
2836
|
+
const id = originalEdgeId.get(edge) ?? String(edge.id);
|
|
2837
|
+
if (!order.has(id)) order.set(id, order.size);
|
|
2838
|
+
};
|
|
2839
|
+
for (const child of group.children ?? [])
|
|
2840
|
+
if (child.children?.length)
|
|
2841
|
+
for (const edge of elkBoundaryEdgeOrder(child, scope, originals)) add(edge);
|
|
2842
|
+
if (ownBoundary) for (const edge of elkBoundaryEdgeOrder(group, scope, originals)) add(edge);
|
|
2843
|
+
return order;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
const isPostCompactionKey = (key: string) =>
|
|
2847
|
+
/(^|\.)compaction\.postCompaction\.strategy$/.test(key);
|
|
2848
|
+
function usesPostCompaction(options: Readonly<Record<string, unknown>>, node: ElkNode): boolean {
|
|
2849
|
+
const active = (values: Readonly<Record<string, unknown>> | undefined) =>
|
|
2850
|
+
Object.entries(values ?? {}).some(
|
|
2851
|
+
([key, value]) => isPostCompactionKey(key) && String(value) !== "NONE",
|
|
2852
|
+
);
|
|
2853
|
+
const visit = (current: ElkNode): boolean =>
|
|
2854
|
+
active(current.layoutOptions) ||
|
|
2855
|
+
active(current.properties as Record<string, unknown> | undefined) ||
|
|
2856
|
+
(current.children ?? []).some(visit);
|
|
2857
|
+
return active(options) || visit(node);
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
function disablePostCompaction<T extends ElkNode>(node: T): T {
|
|
2861
|
+
for (const values of [node.layoutOptions, node.properties as Record<string, unknown> | undefined])
|
|
2862
|
+
for (const key of Object.keys(values ?? {}))
|
|
2863
|
+
if (isPostCompactionKey(key)) (values as Record<string, unknown>)[key] = "NONE";
|
|
2864
|
+
for (const child of node.children ?? []) disablePostCompaction(child);
|
|
2865
|
+
return node;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
/**
|
|
2869
|
+
* Alternative random seeds worth trying for a compound layout, unless the
|
|
2870
|
+
* graph chooses its hierarchical greedy switch explicitly.
|
|
2871
|
+
*/
|
|
2872
|
+
function crossingVariants(
|
|
2873
|
+
options: Readonly<Record<string, unknown>>,
|
|
2874
|
+
graph: ElkNode,
|
|
2875
|
+
): Array<Record<string, string>> {
|
|
2876
|
+
const own = { ...options, ...graph.properties, ...graph.layoutOptions };
|
|
2877
|
+
const compound = (graph.children ?? []).some((child) => child.children?.length);
|
|
2878
|
+
if (
|
|
2879
|
+
!compound ||
|
|
2880
|
+
getOption(own, "hierarchyHandling") !== "INCLUDE_CHILDREN" ||
|
|
2881
|
+
getOption(own, "layered.crossingMinimization.greedySwitchHierarchical.type") !== undefined
|
|
2882
|
+
)
|
|
2883
|
+
return [];
|
|
2884
|
+
return [{ "elk.randomSeed": "2" }, { "elk.randomSeed": "3" }];
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
/**
|
|
2888
|
+
* Whether two edges between different ends can share a port: an explicit port
|
|
2889
|
+
* carrying two or more of them, or implicit ports that merge because some
|
|
2890
|
+
* scope merges edges or a node is a hypernode.
|
|
2891
|
+
*/
|
|
2892
|
+
function mayShareTracks(graph: ElkNode, options: Readonly<Record<string, unknown>>): boolean {
|
|
2893
|
+
const merging = (node: ElkNode, inherited: Readonly<Record<string, unknown>> = {}) => {
|
|
2894
|
+
const settings = getElementLayeredSettings({
|
|
2895
|
+
...inherited,
|
|
2896
|
+
...node.properties,
|
|
2897
|
+
...node.layoutOptions,
|
|
2898
|
+
});
|
|
2899
|
+
return settings.mergeEdges === true || settings.hypernode === true;
|
|
2900
|
+
};
|
|
2901
|
+
const anyMerging = (node: ElkNode): boolean =>
|
|
2902
|
+
merging(node) || (node.children ?? []).some(anyMerging);
|
|
2903
|
+
if (merging(graph, options) || (graph.children ?? []).some(anyMerging)) return true;
|
|
2904
|
+
const ports = new Set<string>();
|
|
2905
|
+
const collectPorts = (node: ElkNode) => {
|
|
2906
|
+
for (const port of node.ports ?? []) ports.add(String(port.id));
|
|
2907
|
+
for (const child of node.children ?? []) collectPorts(child);
|
|
2908
|
+
};
|
|
2909
|
+
collectPorts(graph);
|
|
2910
|
+
if (!ports.size) return false;
|
|
2911
|
+
const seen = new Set<string>();
|
|
2912
|
+
const visit = (node: ElkNode): boolean => {
|
|
2913
|
+
for (const edge of node.edges ?? []) {
|
|
2914
|
+
const source = String(edge.sources?.[0] ?? edge.source),
|
|
2915
|
+
target = String(edge.targets?.[0] ?? edge.target);
|
|
2916
|
+
if (source === target) continue;
|
|
2917
|
+
for (const end of [source, target]) {
|
|
2918
|
+
if (!ports.has(end)) continue;
|
|
2919
|
+
if (seen.has(end)) return true;
|
|
2920
|
+
seen.add(end);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return (node.children ?? []).some(visit);
|
|
2924
|
+
};
|
|
2925
|
+
return visit(graph);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/** Whether the graph asks for more than ELK's default layered thoroughness (7). */
|
|
2929
|
+
function thorough(options: Readonly<Record<string, unknown>>, graph: ElkNode): boolean {
|
|
2930
|
+
const own = { ...options, ...graph.properties, ...graph.layoutOptions };
|
|
2931
|
+
return Number(getOption(own, "layered.thoroughness") ?? 7) > 7;
|
|
2932
|
+
}
|