@statelyai/layout 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +4 -2
- package/README.md +150 -11
- package/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.d.cts +2 -0
- package/dist/elkjs/index.d.mts +2 -0
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/elkjs-UciYx7lD.mjs +2973 -0
- package/dist/elkjs-ggjsBuVP.cjs +3002 -0
- package/dist/{index-BEA_pzeT.d.cts → index-B6cWNrMw.d.cts} +9 -1
- package/dist/{index-DNTLRsDl.d.mts → index-CJQLUKD9.d.mts} +9 -1
- package/dist/{index-DNixtE3h.d.mts → index-DT6lPMfS.d.cts} +85 -5
- package/dist/{index-D9dhxrRl.d.cts → index-F1U-fCTu.d.mts} +85 -5
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +2 -2
- package/dist/layered/index.d.mts +2 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-DGFTeQSl.mjs → layered-C7ERzZjR.mjs} +9605 -1655
- package/dist/{layered-B2buJqMf.cjs → layered-CBPg6_6_.cjs} +12481 -4435
- package/dist/{layout-engine-Dn1PiFwP.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-Cs5tatA6.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +2 -1
- package/dist/routing/index.d.cts +2 -2
- package/dist/routing/index.d.mts +2 -2
- package/dist/routing/index.mjs +2 -2
- package/dist/{routing-A2hrK9MP.mjs → routing-CR8flv8b.mjs} +977 -593
- package/dist/{routing-ujEP7Qh6.cjs → routing-QIdGGFBe.cjs} +986 -596
- package/package.json +8 -2
- package/src/authoring/coordinates.ts +28 -8
- package/src/authoring/partial.ts +7 -2
- package/src/authoring/routing.ts +4 -1
- package/src/constraints.ts +174 -6
- package/src/elkjs/index.ts +1392 -449
- package/src/elkjs/layout-quality.ts +219 -0
- package/src/elkjs/opposing-tracks.ts +161 -0
- package/src/elkjs/orthogonal-junctions.ts +174 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +2 -0
- package/src/internal/elkjs-compatibility.ts +4 -1
- package/src/internal/layered-trace.ts +48 -0
- package/src/internal/layout-engine.ts +2 -1
- package/src/java-random.ts +6 -0
- package/src/layered/bk-node-placement.ts +403 -92
- package/src/layered/center-labels.ts +398 -0
- package/src/layered/compaction-bounds.ts +26 -0
- package/src/layered/compaction-errors.ts +2 -0
- package/src/layered/compaction-scanline.ts +63 -0
- package/src/layered/compound-boundaries.ts +99 -0
- package/src/layered/compound.ts +35 -7
- package/src/layered/constraint-orientation.ts +141 -0
- package/src/layered/crossing-constraints.ts +243 -0
- package/src/layered/crossing-counter.ts +221 -0
- package/src/layered/crossing-graph.ts +401 -0
- package/src/layered/cycle-random.ts +22 -0
- package/src/layered/elk-port-lists.ts +243 -0
- package/src/layered/end-labels.ts +124 -0
- package/src/layered/external-port-dummy.ts +137 -0
- package/src/layered/feedback-chains.ts +114 -0
- package/src/layered/fixed-self-loop.ts +150 -0
- package/src/layered/greedy-switch-decider.ts +356 -0
- package/src/layered/grouped-compaction.ts +787 -0
- package/src/layered/gwt-sort.ts +53 -0
- package/src/layered/hierarchical-port-phases.ts +242 -0
- package/src/layered/hierarchical-port-restoration.ts +233 -0
- package/src/layered/hierarchical-port-routing.ts +179 -0
- package/src/layered/hierarchy-crossing.ts +121 -0
- package/src/layered/hierarchy-sweepiness.ts +83 -0
- package/src/layered/hints.ts +371 -0
- package/src/layered/hyperedge-dummy-merger.ts +195 -0
- package/src/layered/index.ts +886 -161
- package/src/layered/initial-model-order.ts +404 -0
- package/src/layered/inverted-ports.ts +207 -0
- package/src/layered/label-dummy-switcher.ts +200 -0
- package/src/layered/long-edges.ts +135 -44
- package/src/layered/loop-envelopes.ts +184 -0
- package/src/layered/native-routing.ts +275 -0
- package/src/layered/network-simplex.ts +11 -15
- package/src/layered/node-margins.ts +59 -0
- package/src/layered/north-south-ports.ts +271 -0
- package/src/layered/opposing-tracks.ts +944 -0
- package/src/layered/orthogonal-cycle-order.ts +106 -0
- package/src/layered/orthogonal-hypersegments.ts +92 -0
- package/src/layered/orthogonal-junctions.ts +202 -0
- package/src/layered/orthogonal-segments.ts +243 -0
- package/src/layered/placement-orientation.ts +22 -0
- package/src/layered/port-distributor.ts +192 -0
- package/src/layered/port-edge-order.ts +20 -0
- package/src/layered/replace-routing.ts +45 -0
- package/src/layered/routing-coordinates.ts +32 -0
- package/src/layered/separate-exterior-labels.ts +24 -6
- package/src/layered/spacing.ts +34 -12
- package/src/layered/splitter-order.ts +106 -0
- package/src/layered/strategies.ts +2024 -883
- package/src/layered/types.ts +13 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +32 -0
- package/src/routing/algorithms.ts +433 -21
- package/src/routing/coordination.ts +8 -3
- package/src/routing/index.ts +1 -0
- package/src/routing/search.ts +93 -11
- package/dist/elkjs-DaHMISnQ.mjs +0 -1677
- package/dist/elkjs-dMghiiWW.cjs +0 -1706
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 } 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,15 @@ export type {
|
|
|
57
83
|
} from "./public-types";
|
|
58
84
|
export type { ElkId, ElkLogging } from "./types";
|
|
59
85
|
|
|
86
|
+
interface PreparedElkScope {
|
|
87
|
+
graph: ElkNode;
|
|
88
|
+
native: Graph;
|
|
89
|
+
phase?: LayeredCrossingPhase;
|
|
90
|
+
options?: LayeredLayoutOptions;
|
|
91
|
+
children: ReadonlyMap<string, PreparedElkScope>;
|
|
92
|
+
finish(orders?: ReadonlyMap<PreparedElkScope, LayerOrder>): Promise<ElkNode>;
|
|
93
|
+
}
|
|
94
|
+
|
|
60
95
|
export default class ELK {
|
|
61
96
|
readonly #options: ElkConstructorArguments;
|
|
62
97
|
readonly #algorithmIds: ReadonlySet<string>;
|
|
@@ -128,17 +163,74 @@ export default class ELK {
|
|
|
128
163
|
arguments_?: PublicElkLayoutArguments,
|
|
129
164
|
): Promise<PublicElkNode & PublicLaidOutElkNode<T>>;
|
|
130
165
|
layout<T extends ElkNode>(graph: T, arguments_?: ElkLayoutArguments): Promise<LaidOutElkNode<T>>;
|
|
131
|
-
layout<T extends ElkNode>(
|
|
166
|
+
async layout<T extends ElkNode>(
|
|
132
167
|
graph: T,
|
|
133
168
|
arguments_: ElkLayoutArguments = {},
|
|
134
169
|
): Promise<LaidOutElkNode<T>> {
|
|
135
|
-
|
|
170
|
+
const options = { ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions };
|
|
171
|
+
const variants = crossingVariants(options, graph);
|
|
172
|
+
const pristine = variants.length ? structuredClone(graph) : undefined;
|
|
173
|
+
let best = separated(await this.#layoutWithoutDefectiveCompaction(graph, arguments_), options);
|
|
174
|
+
if (!pristine) return best;
|
|
175
|
+
// A compound layout with defects tries other random seeds and keeps the
|
|
176
|
+
// best measured result. Above ELK's default thoroughness, every compound
|
|
177
|
+
// layout does, trading time for fewer crossings and bends.
|
|
178
|
+
let quality = measureLayout(best);
|
|
179
|
+
if (quality.defects === 0 && !thorough(options, graph)) return best;
|
|
180
|
+
for (const variant of variants) {
|
|
181
|
+
const candidate = separated(
|
|
182
|
+
await this.#layoutWithoutDefectiveCompaction(structuredClone(pristine), {
|
|
183
|
+
...arguments_,
|
|
184
|
+
layoutOptions: { ...arguments_.layoutOptions, ...variant },
|
|
185
|
+
}),
|
|
186
|
+
options,
|
|
187
|
+
);
|
|
188
|
+
const candidateQuality = measureLayout(candidate);
|
|
189
|
+
if (isBetterLayout(candidateQuality, quality)) {
|
|
190
|
+
best = candidate;
|
|
191
|
+
quality = candidateQuality;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return best;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async #layoutWithoutDefectiveCompaction<T extends ElkNode>(
|
|
198
|
+
graph: T,
|
|
199
|
+
arguments_: ElkLayoutArguments,
|
|
200
|
+
): Promise<LaidOutElkNode<T>> {
|
|
201
|
+
const compacting = usesPostCompaction(
|
|
202
|
+
{ ...this.#options.defaultLayoutOptions, ...arguments_.layoutOptions },
|
|
203
|
+
graph,
|
|
204
|
+
);
|
|
205
|
+
const pristine = compacting ? structuredClone(graph) : undefined;
|
|
206
|
+
const result = await this.#layout(graph, arguments_);
|
|
207
|
+
const defects = pristine ? measureLayout(result).defects : 0;
|
|
208
|
+
if (!pristine || defects === 0) return result;
|
|
209
|
+
// Post-compaction is an optimization; it must not route edges through nodes
|
|
210
|
+
// or fold a route back onto itself.
|
|
211
|
+
const strategy = { "elk.layered.compaction.postCompaction.strategy": "NONE" };
|
|
212
|
+
const uncompacted = await this.#layout(disablePostCompaction(pristine), {
|
|
213
|
+
...arguments_,
|
|
214
|
+
layoutOptions: { ...arguments_.layoutOptions, ...strategy },
|
|
215
|
+
});
|
|
216
|
+
return measureLayout(uncompacted).defects < defects ? uncompacted : result;
|
|
136
217
|
}
|
|
137
218
|
|
|
138
219
|
async #layout<T extends ElkNode>(
|
|
139
220
|
graph: T,
|
|
140
221
|
arguments_: ElkLayoutArguments = {},
|
|
222
|
+
compoundLayout = false,
|
|
141
223
|
): Promise<LaidOutElkNode<T>> {
|
|
224
|
+
const prepared = await this.#prepareLayout(graph, arguments_, compoundLayout);
|
|
225
|
+
const orders = coordinatePreparedScopes(prepared);
|
|
226
|
+
return (await prepared.finish(orders)) as LaidOutElkNode<T>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async #prepareLayout<T extends ElkNode>(
|
|
230
|
+
graph: T,
|
|
231
|
+
arguments_: ElkLayoutArguments = {},
|
|
232
|
+
compoundLayout = false,
|
|
233
|
+
): Promise<PreparedElkScope> {
|
|
142
234
|
const startedAt = performance.now();
|
|
143
235
|
if (graph === undefined || graph === null) {
|
|
144
236
|
throw new TypeError("Missing mandatory parameter: graph");
|
|
@@ -201,6 +293,8 @@ export default class ELK {
|
|
|
201
293
|
`org.eclipse.elk.core.UnsupportedConfigurationException: Layout algorithm '${requestedAlgorithm}' not found`,
|
|
202
294
|
);
|
|
203
295
|
}
|
|
296
|
+
const preparedChildren = new Map<string, PreparedElkScope>();
|
|
297
|
+
const finishChildren: Array<() => Promise<void>> = [];
|
|
204
298
|
const hasHierarchy = (graph.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
205
299
|
const insideSelfLoopBaseHeightByNodeId = new Map<string, number>();
|
|
206
300
|
const hierarchyRestorations: Array<{
|
|
@@ -210,10 +304,16 @@ export default class ELK {
|
|
|
210
304
|
source?: ElkId;
|
|
211
305
|
target?: ElkId;
|
|
212
306
|
}> = [];
|
|
307
|
+
const boundaryRoutes = new Map<
|
|
308
|
+
ElkEdge,
|
|
309
|
+
{
|
|
310
|
+
source?: { owner: ElkNode; points: ElkPoint[]; junctionPoints?: ElkPoint[] };
|
|
311
|
+
target?: { owner: ElkNode; points: ElkPoint[]; junctionPoints?: ElkPoint[] };
|
|
312
|
+
}
|
|
313
|
+
>();
|
|
213
314
|
const syntheticPortIds = new Set<string>();
|
|
214
315
|
const authoredPortsByCompound = new Map<ElkNode, ElkPort[] | undefined>();
|
|
215
316
|
const authoredOptionsByCompound = new Map<ElkNode, Record<string, unknown> | undefined>();
|
|
216
|
-
const hierarchyBoundaryCountByEdge = new Map<ElkEdge, number>();
|
|
217
317
|
const originalHierarchyEndpoints = new Map(
|
|
218
318
|
(graph.edges ?? []).map((edge) => [
|
|
219
319
|
edge,
|
|
@@ -227,7 +327,6 @@ export default class ELK {
|
|
|
227
327
|
},
|
|
228
328
|
]),
|
|
229
329
|
);
|
|
230
|
-
let hasHierarchyCrossingEdges = false;
|
|
231
330
|
const hierarchyHandling = getOption(layoutOptions, "hierarchyHandling");
|
|
232
331
|
const topdownLayout = getBooleanOption(layoutOptions, "topdownLayout") === true;
|
|
233
332
|
const separateHierarchy =
|
|
@@ -296,17 +395,15 @@ export default class ELK {
|
|
|
296
395
|
);
|
|
297
396
|
}
|
|
298
397
|
} else if (hasHierarchy && !separateHierarchy) {
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
398
|
+
// Prepare boundary identities before coordinating parent and child crossing sweeps.
|
|
399
|
+
const scopeSegmentOrder =
|
|
400
|
+
segmentOrderByScope.get(graph) ??
|
|
401
|
+
elkSegmentOrder(graph, graph, originalHierarchyEndpoints, false);
|
|
303
402
|
for (const child of graph.children ?? []) {
|
|
304
403
|
if ((child.children?.length ?? 0) === 0) continue;
|
|
305
|
-
const descendantById = new Map<string, ElkNode>();
|
|
306
404
|
const descendantOwnerByEndpointId = new Map<string, ElkNode>();
|
|
307
405
|
const collectDescendants = (node: ElkNode): void => {
|
|
308
406
|
for (const descendant of node.children ?? []) {
|
|
309
|
-
descendantById.set(String(descendant.id), descendant);
|
|
310
407
|
descendantOwnerByEndpointId.set(String(descendant.id), descendant);
|
|
311
408
|
for (const port of descendant.ports ?? []) {
|
|
312
409
|
descendantOwnerByEndpointId.set(String(port.id), descendant);
|
|
@@ -321,198 +418,611 @@ export default class ELK {
|
|
|
321
418
|
descendantOwnerByEndpointId.has(sourceId) && descendantOwnerByEndpointId.has(targetId)
|
|
322
419
|
);
|
|
323
420
|
});
|
|
324
|
-
const
|
|
421
|
+
for (const edge of child.edges ?? [])
|
|
422
|
+
if (!originalHierarchyEndpoints.has(edge))
|
|
423
|
+
originalHierarchyEndpoints.set(edge, {
|
|
424
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
425
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
426
|
+
sources: edge.sources,
|
|
427
|
+
targets: edge.targets,
|
|
428
|
+
source: edge.source,
|
|
429
|
+
target: edge.target,
|
|
430
|
+
});
|
|
431
|
+
const crossingEdges = [...(graph.edges ?? []), ...(child.edges ?? [])].filter((edge) => {
|
|
325
432
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
326
433
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
327
434
|
const targetInside = descendantOwnerByEndpointId.has(targetId);
|
|
328
435
|
return sourceInside !== targetInside;
|
|
329
436
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
437
|
+
// CompoundGraphPreprocessor creates boundary ports bottom-up: ports
|
|
438
|
+
// exported by nested groups first, then direct children's ports in
|
|
439
|
+
// port order, outgoing edges before incoming edges.
|
|
440
|
+
const boundaryRank = new Map(
|
|
441
|
+
elkBoundaryEdgeOrder(child, graph, originalHierarchyEndpoints).map((edge, index) => [
|
|
442
|
+
edge,
|
|
443
|
+
index,
|
|
444
|
+
]),
|
|
445
|
+
);
|
|
446
|
+
crossingEdges.sort(
|
|
447
|
+
(a, b) =>
|
|
448
|
+
(boundaryRank.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
449
|
+
(boundaryRank.get(b) ?? Number.MAX_SAFE_INTEGER),
|
|
450
|
+
);
|
|
334
451
|
const mergeHierarchyEdges =
|
|
335
452
|
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;
|
|
453
|
+
// ELK merges only edges incident to the same descendant port.
|
|
340
454
|
const proxyByKind = new Map<string, ElkNode>();
|
|
341
|
-
const
|
|
342
|
-
|
|
455
|
+
const implicitOwnPorts = new Map<ElkEdge, ElkPort>();
|
|
456
|
+
const ownPortFor = (edge: ElkEdge): ElkPort | undefined => {
|
|
457
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
458
|
+
const outside = descendantOwnerByEndpointId.has(e.sourceId) ? e.targetId : e.sourceId;
|
|
459
|
+
if (e.sourceId === String(child.id) || e.targetId === String(child.id)) {
|
|
460
|
+
let port = implicitOwnPorts.get(edge);
|
|
461
|
+
if (!port) {
|
|
462
|
+
const direction = getDirection(layoutOptions);
|
|
463
|
+
const sourceInside = e.sourceId === String(child.id);
|
|
464
|
+
const side = sourceInside
|
|
465
|
+
? ({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[direction]
|
|
466
|
+
: ({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[direction];
|
|
467
|
+
port = {
|
|
468
|
+
id: `__native_hierarchy_own_${String(child.id)}_${implicitOwnPorts.size}`,
|
|
469
|
+
width: 0,
|
|
470
|
+
height: 0,
|
|
471
|
+
layoutOptions: { "elk.port.side": side },
|
|
472
|
+
};
|
|
473
|
+
implicitOwnPorts.set(edge, port);
|
|
474
|
+
syntheticPortIds.add(String(port.id));
|
|
475
|
+
}
|
|
476
|
+
return child.ports?.find((p) => p.id === port!.id) ?? port;
|
|
477
|
+
}
|
|
478
|
+
return child.ports?.find((p) => String(p.id) === outside);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const proxyKey = (kind: "input" | "output", edge: ElkEdge): string => {
|
|
482
|
+
const own = ownPortFor(edge);
|
|
483
|
+
if (own) return JSON.stringify(["authored", own.id]);
|
|
484
|
+
const endpoints = originalHierarchyEndpoints.get(edge)!;
|
|
485
|
+
const endpoint = kind === "output" ? endpoints.sourceId : endpoints.targetId;
|
|
486
|
+
const owner = descendantOwnerByEndpointId.get(endpoint);
|
|
487
|
+
const explicitPort = owner?.ports?.some((port) => String(port.id) === endpoint);
|
|
488
|
+
return JSON.stringify([
|
|
489
|
+
kind,
|
|
490
|
+
mergeHierarchyEdges && explicitPort ? endpoint : String(edge.id),
|
|
491
|
+
]);
|
|
492
|
+
};
|
|
493
|
+
const createBoundaryProxy = (
|
|
494
|
+
key: string,
|
|
495
|
+
kind: "input" | "output",
|
|
496
|
+
own: ElkPort | undefined,
|
|
497
|
+
netFlow: number,
|
|
498
|
+
): ElkNode => {
|
|
343
499
|
let proxy = proxyByKind.get(key);
|
|
344
500
|
if (!proxy) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
501
|
+
const direction = getDirection({ ...layoutOptions, ...child.layoutOptions });
|
|
502
|
+
const authoredAnchor = own
|
|
503
|
+
? (getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] as
|
|
504
|
+
| ElkPoint
|
|
505
|
+
| undefined)
|
|
506
|
+
: undefined;
|
|
507
|
+
const origin = createExternalPortDummy({
|
|
508
|
+
constraints: (getOption(child.layoutOptions ?? {}, "portConstraints") ??
|
|
509
|
+
"FREE") as ExternalPortConstraints,
|
|
510
|
+
side: own
|
|
511
|
+
? ((getOption(own.layoutOptions ?? {}, "port.side") ??
|
|
512
|
+
(kind === "output" ? "EAST" : "WEST")) as ExternalPortSide)
|
|
513
|
+
: kind === "output"
|
|
514
|
+
? "EAST"
|
|
515
|
+
: "WEST",
|
|
516
|
+
position: { x: own?.x ?? 0, y: own?.y ?? 0 },
|
|
517
|
+
anchor: authoredAnchor,
|
|
518
|
+
index: own ? getNumberOption(own.layoutOptions ?? {}, "port.index") : undefined,
|
|
519
|
+
direction: direction.toUpperCase() as "RIGHT" | "LEFT" | "DOWN" | "UP",
|
|
520
|
+
netFlow,
|
|
521
|
+
borderOffset: own
|
|
522
|
+
? (getNumberOption(own.layoutOptions ?? {}, "port.borderOffset") ?? 0)
|
|
523
|
+
: (getNumberOption(layoutOptions, "spacing.edgeEdge") ?? 10) / 2,
|
|
524
|
+
size: { width: own?.width ?? 0, height: own?.height ?? 0 },
|
|
525
|
+
});
|
|
526
|
+
if (own) origin.parentPortId = String(own.id);
|
|
527
|
+
const id = `__native_hierarchy_${String(child.id)}_${proxyByKind.size}`;
|
|
528
|
+
proxy = attachExternalPortDummy(
|
|
529
|
+
{
|
|
530
|
+
id,
|
|
531
|
+
width: origin.width,
|
|
532
|
+
height: origin.height,
|
|
533
|
+
layoutOptions: {
|
|
534
|
+
"elk.portConstraints": origin.constraints,
|
|
535
|
+
"elk.layered.layering.layerConstraint":
|
|
536
|
+
origin.side ===
|
|
537
|
+
({ right: "EAST", left: "WEST", down: "SOUTH", up: "NORTH" } as const)[
|
|
538
|
+
direction
|
|
539
|
+
]
|
|
540
|
+
? "LAST_SEPARATE"
|
|
541
|
+
: origin.side ===
|
|
542
|
+
({ right: "WEST", left: "EAST", down: "NORTH", up: "SOUTH" } as const)[
|
|
543
|
+
direction
|
|
544
|
+
]
|
|
545
|
+
? "FIRST_SEPARATE"
|
|
546
|
+
: undefined,
|
|
547
|
+
},
|
|
548
|
+
ports: [
|
|
549
|
+
{
|
|
550
|
+
id: `${id}:boundary`,
|
|
551
|
+
x: origin.port.x,
|
|
552
|
+
y: origin.port.y,
|
|
553
|
+
width: 0,
|
|
554
|
+
height: 0,
|
|
555
|
+
layoutOptions: { "elk.port.side": origin.port.side },
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
},
|
|
559
|
+
origin,
|
|
560
|
+
);
|
|
350
561
|
proxyByKind.set(key, proxy);
|
|
351
562
|
}
|
|
352
563
|
return proxy;
|
|
353
564
|
};
|
|
565
|
+
const ownedPortNetFlow = (id: string): number => {
|
|
566
|
+
let votes = 0;
|
|
567
|
+
for (const edge of originalHierarchyEndpoints.values()) {
|
|
568
|
+
if (edge.sourceId === id)
|
|
569
|
+
votes += descendantOwnerByEndpointId.has(edge.targetId) ? -1 : 1;
|
|
570
|
+
if (edge.targetId === id)
|
|
571
|
+
votes += descendantOwnerByEndpointId.has(edge.sourceId) ? 1 : -1;
|
|
572
|
+
}
|
|
573
|
+
return votes;
|
|
574
|
+
};
|
|
575
|
+
const proxyFor = (kind: "input" | "output", edge: ElkEdge): ElkNode => {
|
|
576
|
+
const own = ownPortFor(edge);
|
|
577
|
+
const flow = implicitOwnPorts.has(edge)
|
|
578
|
+
? kind === "output"
|
|
579
|
+
? 1
|
|
580
|
+
: -1
|
|
581
|
+
: own
|
|
582
|
+
? ownedPortNetFlow(String(own.id))
|
|
583
|
+
: kind === "output"
|
|
584
|
+
? 1
|
|
585
|
+
: -1;
|
|
586
|
+
return createBoundaryProxy(proxyKey(kind, edge), kind, own, flow);
|
|
587
|
+
};
|
|
588
|
+
// ELK imports ancestor edges before descendant-local edges. Preserve
|
|
589
|
+
// that implicit port creation order through child phase preparation.
|
|
354
590
|
const temporaryEdges: ElkEdge[] = [
|
|
355
|
-
...(child.edges ?? []),
|
|
356
|
-
...internalEdges.filter(
|
|
357
|
-
(edge) =>
|
|
358
|
-
!(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)),
|
|
359
|
-
),
|
|
360
591
|
...crossingEdges.map((edge) => {
|
|
361
592
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
362
593
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
363
|
-
const proxy = proxyFor(sourceInside ? "output" : "input");
|
|
364
|
-
|
|
594
|
+
const proxy = proxyFor(sourceInside ? "output" : "input", edge);
|
|
595
|
+
const segment: ElkEdge = {
|
|
365
596
|
...edge,
|
|
366
597
|
id: `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
367
|
-
sources: [sourceInside ? sourceId : proxy.id!],
|
|
368
|
-
targets: [sourceInside ? proxy.id! : targetId],
|
|
598
|
+
sources: [sourceInside ? sourceId : proxy.ports![0]!.id!],
|
|
599
|
+
targets: [sourceInside ? proxy.ports![0]!.id! : targetId],
|
|
369
600
|
source: undefined,
|
|
370
601
|
target: undefined,
|
|
371
602
|
sections: undefined,
|
|
372
603
|
};
|
|
604
|
+
originalEdgeId.set(segment, originalEdgeId.get(edge) ?? String(edge.id));
|
|
605
|
+
return segment;
|
|
373
606
|
}),
|
|
607
|
+
...(child.edges ?? []).filter((edge) => !crossingEdges.includes(edge)),
|
|
608
|
+
...internalEdges.filter(
|
|
609
|
+
(edge) =>
|
|
610
|
+
!(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)),
|
|
611
|
+
),
|
|
374
612
|
];
|
|
613
|
+
const hasExternalPorts = proxyByKind.size > 0;
|
|
614
|
+
const inactiveAuthoredPorts: Array<{ port: ElkPort; proxy: ElkNode }> = [];
|
|
615
|
+
if (hasExternalPorts) {
|
|
616
|
+
for (const port of child.ports ?? []) {
|
|
617
|
+
const key = JSON.stringify(["authored", port.id]);
|
|
618
|
+
if (proxyByKind.has(key)) continue;
|
|
619
|
+
const flow = ownedPortNetFlow(String(port.id));
|
|
620
|
+
inactiveAuthoredPorts.push({
|
|
621
|
+
port,
|
|
622
|
+
proxy: createBoundaryProxy(key, flow >= 0 ? "output" : "input", port, flow),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const outsideNodeEdges = !hasExternalPorts
|
|
627
|
+
? []
|
|
628
|
+
: (graph.edges ?? []).filter((edge) => {
|
|
629
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
630
|
+
return (
|
|
631
|
+
(e.sourceId === String(child.id) || e.targetId === String(child.id)) &&
|
|
632
|
+
!descendantOwnerByEndpointId.has(e.sourceId) &&
|
|
633
|
+
!descendantOwnerByEndpointId.has(e.targetId)
|
|
634
|
+
);
|
|
635
|
+
});
|
|
636
|
+
for (const edge of outsideNodeEdges) {
|
|
637
|
+
const e = originalHierarchyEndpoints.get(edge)!;
|
|
638
|
+
proxyFor(e.sourceId === String(child.id) ? "output" : "input", edge);
|
|
639
|
+
}
|
|
375
640
|
const temporaryChild: ElkNode = {
|
|
376
641
|
...child,
|
|
642
|
+
layoutOptions: proxyByKind.size
|
|
643
|
+
? { ...child.layoutOptions, "elk.portConstraints": "FIXED_SIDE" }
|
|
644
|
+
: child.layoutOptions,
|
|
377
645
|
children: [...(child.children ?? []), ...proxyByKind.values()],
|
|
378
646
|
edges: temporaryEdges,
|
|
379
647
|
};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
648
|
+
const childSegmentOrder = elkSegmentOrder(child, graph, originalHierarchyEndpoints, true);
|
|
649
|
+
segmentOrderByScope.set(temporaryChild, childSegmentOrder);
|
|
650
|
+
// Boundary segments copied into the child scope keep their original identity.
|
|
651
|
+
for (const edge of temporaryEdges) {
|
|
652
|
+
const id = originalEdgeId.get(edge);
|
|
653
|
+
const rank = id === undefined ? undefined : childSegmentOrder.get(id);
|
|
654
|
+
if (rank !== undefined) hierarchySegmentRank.set(edge, rank);
|
|
655
|
+
}
|
|
656
|
+
const preparedChild = await this.#prepareLayout(
|
|
657
|
+
temporaryChild,
|
|
658
|
+
{
|
|
659
|
+
...arguments_,
|
|
660
|
+
layoutOptions: {
|
|
661
|
+
...arguments_.layoutOptions,
|
|
662
|
+
// The compound's own direction wins over the inherited one.
|
|
663
|
+
direction: getDirection(
|
|
664
|
+
getOption(child.layoutOptions ?? {}, "direction") === undefined
|
|
665
|
+
? layoutOptions
|
|
666
|
+
: child.layoutOptions!,
|
|
667
|
+
).toUpperCase(),
|
|
668
|
+
hierarchyHandling: "INCLUDE_CHILDREN",
|
|
669
|
+
},
|
|
670
|
+
logging: false,
|
|
671
|
+
measureExecutionTime: false,
|
|
385
672
|
},
|
|
386
|
-
|
|
387
|
-
measureExecutionTime: false,
|
|
388
|
-
});
|
|
389
|
-
const childPadding = parsePadding(
|
|
390
|
-
getOption({ ...layoutOptions, ...child.layoutOptions }, "padding"),
|
|
391
|
-
12,
|
|
673
|
+
true,
|
|
392
674
|
);
|
|
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
|
-
};
|
|
675
|
+
preparedChildren.set(String(child.id), preparedChild);
|
|
465
676
|
authoredPortsByCompound.set(child, child.ports);
|
|
466
677
|
authoredOptionsByCompound.set(child, child.layoutOptions);
|
|
467
|
-
const
|
|
678
|
+
const initialPorts = (child.ports ?? []).map((port) => ({
|
|
679
|
+
...port,
|
|
680
|
+
layoutOptions: port.layoutOptions ? { ...port.layoutOptions } : undefined,
|
|
681
|
+
}));
|
|
468
682
|
for (const edge of crossingEdges) {
|
|
683
|
+
const own = ownPortFor(edge);
|
|
684
|
+
if (own) {
|
|
685
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
686
|
+
const sourceInside = descendantOwnerByEndpointId.has(original.sourceId);
|
|
687
|
+
const origin = externalPortDummyOf(
|
|
688
|
+
proxyByKind.get(proxyKey(sourceInside ? "output" : "input", edge))!,
|
|
689
|
+
)!;
|
|
690
|
+
let port = initialPorts.find((p) => p.id === own.id);
|
|
691
|
+
if (!port) {
|
|
692
|
+
port = { ...own, layoutOptions: { ...own.layoutOptions } };
|
|
693
|
+
initialPorts.push(port);
|
|
694
|
+
}
|
|
695
|
+
port.layoutOptions = { ...port.layoutOptions, "elk.port.side": origin.side };
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
469
698
|
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)}`;
|
|
699
|
+
const sourceInside = descendantOwnerByEndpointId.has(original.sourceId);
|
|
700
|
+
const proxy = proxyByKind.get(proxyKey(sourceInside ? "output" : "input", edge))!;
|
|
701
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
702
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
476
703
|
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)) {
|
|
704
|
+
if (!initialPorts.some((port) => port.id === portId))
|
|
705
|
+
initialPorts.push({
|
|
706
|
+
id: portId,
|
|
707
|
+
width: origin.externalSize.width,
|
|
708
|
+
height: origin.externalSize.height,
|
|
709
|
+
x: 0,
|
|
710
|
+
y: 0,
|
|
711
|
+
layoutOptions: {
|
|
712
|
+
"elk.port.side": origin.side,
|
|
713
|
+
"elk.port.borderOffset": origin.borderOffset,
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge))
|
|
495
717
|
hierarchyRestorations.push({ edge, ...original });
|
|
496
|
-
}
|
|
497
718
|
if (sourceInside) edge.sources = [portId];
|
|
498
719
|
else edge.targets = [portId];
|
|
720
|
+
const rank = scopeSegmentOrder.get(originalEdgeId.get(edge) ?? String(edge.id));
|
|
721
|
+
if (rank !== undefined) hierarchySegmentRank.set(edge, rank);
|
|
499
722
|
edge.source = undefined;
|
|
500
723
|
edge.target = undefined;
|
|
501
724
|
}
|
|
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;
|
|
725
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
726
|
+
const actual = initialPorts.find((p) => p.id === port.id)!;
|
|
727
|
+
actual.layoutOptions = {
|
|
728
|
+
...actual.layoutOptions,
|
|
729
|
+
"elk.port.side": externalPortDummyOf(proxy)!.side,
|
|
730
|
+
};
|
|
511
731
|
}
|
|
732
|
+
for (const edge of outsideNodeEdges) {
|
|
733
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
734
|
+
const source = original.sourceId === String(child.id);
|
|
735
|
+
const proxy = proxyFor(source ? "output" : "input", edge),
|
|
736
|
+
own = ownPortFor(edge)!;
|
|
737
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
738
|
+
if (!initialPorts.some((p) => p.id === own.id))
|
|
739
|
+
initialPorts.push({
|
|
740
|
+
...own,
|
|
741
|
+
layoutOptions: { ...own.layoutOptions, "elk.port.side": origin.side },
|
|
742
|
+
});
|
|
743
|
+
if (!hierarchyRestorations.some((r) => r.edge === edge))
|
|
744
|
+
hierarchyRestorations.push({ edge, ...original });
|
|
745
|
+
if (source) edge.sources = [own.id!];
|
|
746
|
+
else edge.targets = [own.id!];
|
|
747
|
+
edge.source = undefined;
|
|
748
|
+
edge.target = undefined;
|
|
749
|
+
}
|
|
750
|
+
child.ports = initialPorts;
|
|
751
|
+
child.layoutOptions = {
|
|
752
|
+
...child.layoutOptions,
|
|
753
|
+
"elk.portConstraints":
|
|
754
|
+
// ELK fixes physical boundary sides after introducing hierarchy dummies.
|
|
755
|
+
// Authored constraints are restored when finishing the public graph.
|
|
756
|
+
crossingEdges.length > 0 || outsideNodeEdges.length > 0
|
|
757
|
+
? "FIXED_SIDE"
|
|
758
|
+
: (getOption(authoredOptionsByCompound.get(child) ?? {}, "portConstraints") ??
|
|
759
|
+
"FREE"),
|
|
760
|
+
};
|
|
761
|
+
finishChildren.push(async () => {
|
|
762
|
+
await preparedChild.finish(activeOrders);
|
|
763
|
+
const childPadding = parsePadding(
|
|
764
|
+
getOption({ ...layoutOptions, ...child.layoutOptions }, "padding"),
|
|
765
|
+
12,
|
|
766
|
+
);
|
|
767
|
+
for (const internalEdge of internalEdges) {
|
|
768
|
+
const temporary = temporaryEdges.find(
|
|
769
|
+
(candidate) => String(candidate.id) === String(internalEdge.id),
|
|
770
|
+
);
|
|
771
|
+
if (temporary?.sections) {
|
|
772
|
+
internalEdge.sections = temporary.sections;
|
|
773
|
+
// A nested scope may own the route (an edge from a compound's
|
|
774
|
+
// own port to its child); keep that coordinate frame.
|
|
775
|
+
internalEdge.container = (temporary as { container?: string }).container ?? child.id;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const childDirection = getDirection({ ...layoutOptions, ...child.layoutOptions });
|
|
779
|
+
const horizontalChild = childDirection === "right" || childDirection === "left";
|
|
780
|
+
// External dummy positions retain any routing corridor at the boundary.
|
|
781
|
+
const restoredPerpendicular = [...proxyByKind.values()].some(isPerpendicularPortRestored);
|
|
782
|
+
child.width = restoredPerpendicular
|
|
783
|
+
? temporaryChild.width
|
|
784
|
+
: horizontalChild
|
|
785
|
+
? Math.max(
|
|
786
|
+
0,
|
|
787
|
+
...(temporaryChild.children ?? []).map(
|
|
788
|
+
(node) => (node.x ?? 0) + (node.width ?? 0),
|
|
789
|
+
),
|
|
790
|
+
) + childPadding.right
|
|
791
|
+
: temporaryChild.width;
|
|
792
|
+
child.height = restoredPerpendicular
|
|
793
|
+
? temporaryChild.height
|
|
794
|
+
: horizontalChild
|
|
795
|
+
? temporaryChild.height
|
|
796
|
+
: Math.max(
|
|
797
|
+
0,
|
|
798
|
+
...(temporaryChild.children ?? []).map(
|
|
799
|
+
(node) => (node.y ?? 0) + (node.height ?? 0),
|
|
800
|
+
),
|
|
801
|
+
) + childPadding.bottom;
|
|
802
|
+
|
|
803
|
+
const relativeRect = (id: string): ElkShape | undefined => {
|
|
804
|
+
const owner = descendantOwnerByEndpointId.get(id);
|
|
805
|
+
if (!owner) return undefined;
|
|
806
|
+
const path: ElkNode[] = [];
|
|
807
|
+
const visit = (parent: ElkNode): boolean => {
|
|
808
|
+
for (const candidate of parent.children ?? []) {
|
|
809
|
+
path.push(candidate);
|
|
810
|
+
if (candidate === owner || visit(candidate)) return true;
|
|
811
|
+
path.pop();
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
};
|
|
815
|
+
if (!visit(child)) return undefined;
|
|
816
|
+
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
817
|
+
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
818
|
+
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
819
|
+
if (port) {
|
|
820
|
+
return {
|
|
821
|
+
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
822
|
+
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
823
|
+
width: 0,
|
|
824
|
+
height: 0,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
x: ownerX,
|
|
829
|
+
y: ownerY,
|
|
830
|
+
width: path.at(-1)?.width ?? 0,
|
|
831
|
+
height: path.at(-1)?.height ?? 0,
|
|
832
|
+
};
|
|
833
|
+
};
|
|
834
|
+
const ports = (authoredPortsByCompound.get(child) ?? []).map(
|
|
835
|
+
(original) => child.ports?.find((port) => port.id === original.id) ?? original,
|
|
836
|
+
);
|
|
837
|
+
for (const edge of crossingEdges) {
|
|
838
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
839
|
+
const { sourceId, targetId } = original;
|
|
840
|
+
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
841
|
+
const descendantId = sourceInside ? sourceId : targetId;
|
|
842
|
+
const rect = relativeRect(descendantId);
|
|
843
|
+
if (!rect) continue;
|
|
844
|
+
const internalEdge = temporaryEdges.find(
|
|
845
|
+
(candidate) =>
|
|
846
|
+
String(candidate.id) ===
|
|
847
|
+
`__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
848
|
+
);
|
|
849
|
+
const internalRoute = internalEdge?.sections;
|
|
850
|
+
const kind = sourceInside ? "output" : "input";
|
|
851
|
+
const proxy = proxyByKind.get(proxyKey(kind, edge))!;
|
|
852
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
853
|
+
const endpoint = sourceInside
|
|
854
|
+
? internalRoute?.at(-1)?.endPoint
|
|
855
|
+
: internalRoute?.[0]?.startPoint;
|
|
856
|
+
if (!endpoint)
|
|
857
|
+
throw new Error(`Missing compound boundary route for ${String(edge.id)}`);
|
|
858
|
+
const transferred = transferExternalPort({
|
|
859
|
+
contentSize: {
|
|
860
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
861
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
862
|
+
},
|
|
863
|
+
padding: childPadding,
|
|
864
|
+
offset: { x: 0, y: 0 },
|
|
865
|
+
dummy: {
|
|
866
|
+
x: endpoint.x - childPadding.left - (ownPortFor(edge) ? origin.port.x : 0),
|
|
867
|
+
y: endpoint.y - childPadding.top - (ownPortFor(edge) ? origin.port.y : 0),
|
|
868
|
+
width: origin.width,
|
|
869
|
+
height: origin.height,
|
|
870
|
+
},
|
|
871
|
+
side: origin.side,
|
|
872
|
+
borderOffset: origin.borderOffset,
|
|
873
|
+
portSize: origin.externalSize,
|
|
874
|
+
});
|
|
875
|
+
const points = internalRoute!
|
|
876
|
+
.flatMap((section, index) => [
|
|
877
|
+
...(index === 0 ? [section.startPoint] : []),
|
|
878
|
+
...(section.bendPoints ?? []),
|
|
879
|
+
section.endPoint,
|
|
880
|
+
])
|
|
881
|
+
.map((point) => ({ ...point }));
|
|
882
|
+
const boundary = sourceInside ? points.at(-1)! : points[0]!;
|
|
883
|
+
boundary.x = transferred.dummy.x + childPadding.left;
|
|
884
|
+
boundary.y = transferred.dummy.y + childPadding.top;
|
|
885
|
+
const own = ownPortFor(edge);
|
|
886
|
+
if (own) {
|
|
887
|
+
if (implicitOwnPorts.has(edge) && !ports.some((p) => p.id === own.id))
|
|
888
|
+
ports.push(own);
|
|
889
|
+
own.x = transferred.port.x;
|
|
890
|
+
own.y = transferred.port.y;
|
|
891
|
+
const anchor = (getElementLayeredSettings(own.layoutOptions ?? {})["port.anchor"] as
|
|
892
|
+
| ElkPoint
|
|
893
|
+
| undefined) ?? {
|
|
894
|
+
x:
|
|
895
|
+
origin.side === "EAST"
|
|
896
|
+
? (own.width ?? 0)
|
|
897
|
+
: origin.side === "WEST"
|
|
898
|
+
? 0
|
|
899
|
+
: (own.width ?? 0) / 2,
|
|
900
|
+
y:
|
|
901
|
+
origin.side === "SOUTH"
|
|
902
|
+
? (own.height ?? 0)
|
|
903
|
+
: origin.side === "NORTH"
|
|
904
|
+
? 0
|
|
905
|
+
: (own.height ?? 0) / 2,
|
|
906
|
+
};
|
|
907
|
+
boundary.x = own.x + anchor.x;
|
|
908
|
+
boundary.y = own.y + anchor.y;
|
|
909
|
+
edge.sections = [
|
|
910
|
+
{
|
|
911
|
+
id: `${String(edge.id)}_s0`,
|
|
912
|
+
startPoint: points[0]!,
|
|
913
|
+
endPoint: points.at(-1)!,
|
|
914
|
+
...(points.length > 2 ? { bendPoints: points.slice(1, -1) } : {}),
|
|
915
|
+
incomingShape: sourceId,
|
|
916
|
+
outgoingShape: targetId,
|
|
917
|
+
},
|
|
918
|
+
];
|
|
919
|
+
if (internalEdge?.junctionPoints?.length)
|
|
920
|
+
edge.junctionPoints = internalEdge.junctionPoints.map((point) => ({ ...point }));
|
|
921
|
+
else delete edge.junctionPoints;
|
|
922
|
+
edge.container = child.id;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const routes = boundaryRoutes.get(edge) ?? {};
|
|
926
|
+
routes[sourceInside ? "source" : "target"] = {
|
|
927
|
+
owner: child,
|
|
928
|
+
points,
|
|
929
|
+
junctionPoints: internalEdge?.junctionPoints,
|
|
930
|
+
};
|
|
931
|
+
boundaryRoutes.set(edge, routes);
|
|
932
|
+
const portId = `${String(proxy.id)}:parent`;
|
|
933
|
+
syntheticPortIds.add(portId);
|
|
934
|
+
if (!ports.some((port) => port.id === portId))
|
|
935
|
+
ports.push({
|
|
936
|
+
id: portId,
|
|
937
|
+
width: origin.externalSize.width,
|
|
938
|
+
height: origin.externalSize.height,
|
|
939
|
+
x: transferred.port.x,
|
|
940
|
+
y: transferred.port.y,
|
|
941
|
+
layoutOptions: {
|
|
942
|
+
"elk.port.side": origin.side,
|
|
943
|
+
"elk.port.borderOffset": origin.borderOffset,
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
if (!hierarchyRestorations.some((restoration) => restoration.edge === edge)) {
|
|
947
|
+
hierarchyRestorations.push({ edge, ...original });
|
|
948
|
+
}
|
|
949
|
+
if (sourceInside) edge.sources = [portId];
|
|
950
|
+
else edge.targets = [portId];
|
|
951
|
+
edge.source = undefined;
|
|
952
|
+
edge.target = undefined;
|
|
953
|
+
}
|
|
954
|
+
for (const { port, proxy } of inactiveAuthoredPorts) {
|
|
955
|
+
const origin = externalPortDummyOf(proxy)!;
|
|
956
|
+
const actual = ports.find((p) => p.id === port.id)!;
|
|
957
|
+
const transferred = transferExternalPort({
|
|
958
|
+
contentSize: {
|
|
959
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
960
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
961
|
+
},
|
|
962
|
+
padding: childPadding,
|
|
963
|
+
offset: { x: 0, y: 0 },
|
|
964
|
+
dummy: {
|
|
965
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
966
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
967
|
+
width: origin.width,
|
|
968
|
+
height: origin.height,
|
|
969
|
+
},
|
|
970
|
+
side: origin.side,
|
|
971
|
+
borderOffset: origin.borderOffset,
|
|
972
|
+
portSize: origin.externalSize,
|
|
973
|
+
});
|
|
974
|
+
actual.x = transferred.port.x;
|
|
975
|
+
actual.y = transferred.port.y;
|
|
976
|
+
}
|
|
977
|
+
for (const edge of outsideNodeEdges) {
|
|
978
|
+
const original = originalHierarchyEndpoints.get(edge)!;
|
|
979
|
+
const proxy = proxyFor(
|
|
980
|
+
original.sourceId === String(child.id) ? "output" : "input",
|
|
981
|
+
edge,
|
|
982
|
+
),
|
|
983
|
+
origin = externalPortDummyOf(proxy)!,
|
|
984
|
+
own = ownPortFor(edge)!;
|
|
985
|
+
const transferred = transferExternalPort({
|
|
986
|
+
contentSize: {
|
|
987
|
+
width: child.width! - childPadding.left - childPadding.right,
|
|
988
|
+
height: child.height! - childPadding.top - childPadding.bottom,
|
|
989
|
+
},
|
|
990
|
+
padding: childPadding,
|
|
991
|
+
offset: { x: 0, y: 0 },
|
|
992
|
+
dummy: {
|
|
993
|
+
x: (proxy.x ?? 0) - childPadding.left,
|
|
994
|
+
y: (proxy.y ?? 0) - childPadding.top,
|
|
995
|
+
width: origin.width,
|
|
996
|
+
height: origin.height,
|
|
997
|
+
},
|
|
998
|
+
side: origin.side,
|
|
999
|
+
borderOffset: origin.borderOffset,
|
|
1000
|
+
portSize: origin.externalSize,
|
|
1001
|
+
});
|
|
1002
|
+
own.x = transferred.port.x;
|
|
1003
|
+
own.y = transferred.port.y;
|
|
1004
|
+
if (!ports.some((p) => p.id === own.id)) ports.push(own);
|
|
1005
|
+
}
|
|
1006
|
+
child.ports = ports;
|
|
1007
|
+
child.layoutOptions = { ...child.layoutOptions, "elk.portConstraints": "FIXED_POS" };
|
|
1008
|
+
const insideLoopCount = (graph.edges ?? []).filter((edge) =>
|
|
1009
|
+
isInsideSelfLoop(graph, edge, String(child.id)),
|
|
1010
|
+
).length;
|
|
1011
|
+
if (insideLoopCount > 0) {
|
|
1012
|
+
const baseHeight = child.height ?? 0;
|
|
1013
|
+
insideSelfLoopBaseHeightByNodeId.set(String(child.id), baseHeight);
|
|
1014
|
+
child.height = baseHeight + insideLoopCount * 11;
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
512
1017
|
}
|
|
513
1018
|
}
|
|
514
1019
|
applyNodeMicroLayout(graph, layoutOptions);
|
|
515
1020
|
const graph_ = toGraph(graph, layoutOptions);
|
|
1021
|
+
const hierarchyProxyIds = new Set(
|
|
1022
|
+
(graph.children ?? [])
|
|
1023
|
+
.filter((node) => String(node.id).startsWith("__native_hierarchy_"))
|
|
1024
|
+
.map((node) => String(node.id)),
|
|
1025
|
+
);
|
|
516
1026
|
const layerConstraintByNodeId = new Map(
|
|
517
1027
|
(graph.children ?? []).map((node) => [
|
|
518
1028
|
String(node.id),
|
|
@@ -527,6 +1037,10 @@ export default class ELK {
|
|
|
527
1037
|
algorithm === "layered" &&
|
|
528
1038
|
graph_.edges.some((edge) => {
|
|
529
1039
|
if (edge.sourceId === edge.targetId) return false;
|
|
1040
|
+
// A boundary proxy for an edge from the enclosing compound sits in its own
|
|
1041
|
+
// separate layer ahead of the scope, so it never conflicts with a first node.
|
|
1042
|
+
if (hierarchyProxyIds.has(edge.sourceId) || hierarchyProxyIds.has(edge.targetId))
|
|
1043
|
+
return false;
|
|
530
1044
|
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
531
1045
|
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
532
1046
|
return (
|
|
@@ -543,307 +1057,471 @@ export default class ELK {
|
|
|
543
1057
|
getOption(layoutOptions, "padding"),
|
|
544
1058
|
algorithm === "layered" ? 12 : 0,
|
|
545
1059
|
);
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1060
|
+
const options: LayeredLayoutOptions = {
|
|
1061
|
+
direction: getDirection(layoutOptions),
|
|
1062
|
+
spacing: {
|
|
1063
|
+
node:
|
|
1064
|
+
getNumberOption(layoutOptions, "spacing.nodeNode") ??
|
|
1065
|
+
getNumberOption(layoutOptions, "layered.spacing.baseValue"),
|
|
1066
|
+
layer:
|
|
1067
|
+
getNumberOption(layoutOptions, "layered.spacing.nodeNodeBetweenLayers") ??
|
|
1068
|
+
getNumberOption(layoutOptions, "layered.spacing.baseValue") ??
|
|
1069
|
+
20,
|
|
1070
|
+
},
|
|
1071
|
+
padding,
|
|
1072
|
+
constraints: {
|
|
1073
|
+
layer: () => undefined,
|
|
1074
|
+
},
|
|
1075
|
+
settings: {
|
|
1076
|
+
...getLayeredSettings(layoutOptions),
|
|
1077
|
+
...(compoundLayout || (hasHierarchy && !separateHierarchy && !topdownLayout)
|
|
1078
|
+
? { separateConnectedComponents: false }
|
|
1079
|
+
: {}),
|
|
1080
|
+
...(hierarchyHandling === "INCLUDE_CHILDREN"
|
|
1081
|
+
? {
|
|
1082
|
+
"crossingMinimization.greedySwitch.type": String(
|
|
1083
|
+
getOption(
|
|
1084
|
+
layoutOptions,
|
|
1085
|
+
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
1086
|
+
) ?? "TWO_SIDED",
|
|
1087
|
+
) as LayeredAdvancedOptions["crossingMinimization.greedySwitch.type"],
|
|
1088
|
+
}
|
|
1089
|
+
: {}),
|
|
1090
|
+
},
|
|
1091
|
+
nodeSettings: (node) => {
|
|
1092
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1093
|
+
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
1094
|
+
},
|
|
1095
|
+
edgeSettings: (edge) => {
|
|
1096
|
+
const elkEdge = findById(graph.edges, edge.id)?.item;
|
|
1097
|
+
return {
|
|
1098
|
+
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
1099
|
+
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {}),
|
|
1100
|
+
// ELK appends hierarchy segments to port edge lists in creation order.
|
|
1101
|
+
"edge.hierarchyRank": elkEdge && hierarchySegmentRank.get(elkEdge),
|
|
1102
|
+
};
|
|
1103
|
+
},
|
|
1104
|
+
portSettings: (port, node) => {
|
|
1105
|
+
const child = findById(graph.children, node.id)?.item;
|
|
1106
|
+
const found = findById(child?.ports, port.name);
|
|
1107
|
+
const elkPort = found?.item;
|
|
1108
|
+
return {
|
|
1109
|
+
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
1110
|
+
// ELK imports ports in authored order and sorts sides after dummy insertion.
|
|
1111
|
+
"port.authoredIndex": found?.position,
|
|
1112
|
+
"port.labelWidth": Math.max(
|
|
1113
|
+
0,
|
|
1114
|
+
...(elkPort?.labels ?? []).map((label) => label.width ?? 0),
|
|
1115
|
+
),
|
|
1116
|
+
"port.labelHeight": Math.max(
|
|
1117
|
+
0,
|
|
1118
|
+
...(elkPort?.labels ?? []).map((label) => label.height ?? 0),
|
|
1119
|
+
),
|
|
1120
|
+
} as ElkLayeredOptionValueByName;
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
const pipeline =
|
|
1124
|
+
algorithm === "layered" ? createLayeredScopePipeline(graph_, options) : undefined;
|
|
1125
|
+
const initial = pipeline?.next();
|
|
1126
|
+
const phase = initial && !initial.done ? initial.value : undefined;
|
|
1127
|
+
let activeOrders: ReadonlyMap<PreparedElkScope, LayerOrder> | undefined;
|
|
1128
|
+
let finished: Promise<ElkNode> | undefined;
|
|
1129
|
+
const prepared: PreparedElkScope = {
|
|
1130
|
+
graph,
|
|
1131
|
+
native: graph_,
|
|
1132
|
+
phase,
|
|
1133
|
+
options,
|
|
1134
|
+
children: preparedChildren,
|
|
1135
|
+
finish: (orders) => {
|
|
1136
|
+
if (finished) return finished;
|
|
1137
|
+
activeOrders = orders;
|
|
1138
|
+
finished = finish();
|
|
1139
|
+
return finished;
|
|
1140
|
+
},
|
|
1141
|
+
};
|
|
1142
|
+
const finishLayered = (): VisualGraph => {
|
|
1143
|
+
if (!pipeline || !initial) throw new Error("Missing layered pipeline");
|
|
1144
|
+
if (initial.done) return initial.value;
|
|
1145
|
+
const completed = pipeline.next(
|
|
1146
|
+
activeOrders?.has(prepared)
|
|
1147
|
+
? initial.value.finishOrder(activeOrders.get(prepared)!)
|
|
1148
|
+
: initial.value.minimize(),
|
|
1149
|
+
);
|
|
1150
|
+
if (!completed.done) throw new Error("Layered scope yielded more than one crossing phase");
|
|
1151
|
+
return completed.value;
|
|
1152
|
+
};
|
|
1153
|
+
const finish = async (): Promise<ElkNode> => {
|
|
1154
|
+
for (const complete of finishChildren) await complete();
|
|
1155
|
+
for (const nativeGraph of new Set(
|
|
1156
|
+
[graph_, phase?.input.graph].filter((value) => value !== undefined),
|
|
1157
|
+
)) {
|
|
1158
|
+
for (const child of graph.children ?? []) {
|
|
1159
|
+
const native = nativeGraph.nodes.find((node) => node.id === String(child.id));
|
|
1160
|
+
if (!native || !preparedChildren.has(String(child.id))) continue;
|
|
1161
|
+
phase?.updateNodeSize(native.id, { width: child.width ?? 0, height: child.height ?? 0 });
|
|
1162
|
+
Object.assign(native, { width: child.width, height: child.height });
|
|
1163
|
+
for (const port of native.ports ?? []) {
|
|
1164
|
+
const actual = child.ports?.find((candidate) => String(candidate.id) === port.name);
|
|
1165
|
+
if (actual)
|
|
1166
|
+
Object.assign(port, {
|
|
1167
|
+
x: actual.x,
|
|
1168
|
+
y: actual.y,
|
|
1169
|
+
width: actual.width,
|
|
1170
|
+
height: actual.height,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const laidOut = await (algorithm === "sporeCompaction"
|
|
556
1176
|
? executeElkjs0111Layout({
|
|
557
|
-
algorithm: "
|
|
1177
|
+
algorithm: "sporeCompaction",
|
|
558
1178
|
graph: graph_,
|
|
559
1179
|
options: {
|
|
560
1180
|
padding,
|
|
561
1181
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
562
1182
|
},
|
|
563
1183
|
})
|
|
564
|
-
: algorithm === "
|
|
1184
|
+
: algorithm === "sporeOverlap"
|
|
565
1185
|
? executeElkjs0111Layout({
|
|
566
|
-
algorithm: "
|
|
1186
|
+
algorithm: "sporeOverlap",
|
|
567
1187
|
graph: graph_,
|
|
568
1188
|
options: {
|
|
569
|
-
padding
|
|
570
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
1189
|
+
padding,
|
|
1190
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
571
1191
|
},
|
|
572
1192
|
})
|
|
573
|
-
: algorithm === "
|
|
1193
|
+
: algorithm === "rectpacking"
|
|
574
1194
|
? executeElkjs0111Layout({
|
|
575
|
-
algorithm: "
|
|
1195
|
+
algorithm: "rectpacking",
|
|
576
1196
|
graph: graph_,
|
|
577
1197
|
options: {
|
|
578
1198
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
579
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
580
|
-
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
581
|
-
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
1199
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode") ?? 15,
|
|
582
1200
|
},
|
|
583
1201
|
})
|
|
584
|
-
: algorithm === "
|
|
1202
|
+
: algorithm === "random"
|
|
585
1203
|
? executeElkjs0111Layout({
|
|
586
|
-
algorithm: "
|
|
1204
|
+
algorithm: "random",
|
|
587
1205
|
graph: graph_,
|
|
588
1206
|
options: {
|
|
589
1207
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
590
1208
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
591
1209
|
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
|
-
},
|
|
1210
|
+
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
600
1211
|
},
|
|
601
1212
|
})
|
|
602
|
-
: algorithm === "
|
|
1213
|
+
: algorithm === "box"
|
|
603
1214
|
? executeElkjs0111Layout({
|
|
604
|
-
algorithm: "
|
|
605
|
-
graph: graph_,
|
|
606
|
-
options: { direction: getDirection(layoutOptions) },
|
|
607
|
-
})
|
|
608
|
-
: executeElkjs0111Layout({
|
|
609
|
-
algorithm: "layered",
|
|
1215
|
+
algorithm: "box",
|
|
610
1216
|
graph: graph_,
|
|
611
1217
|
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) => {
|
|
650
|
-
const child = graph.children?.find(
|
|
651
|
-
(candidate) => String(candidate.id) === node.id,
|
|
652
|
-
);
|
|
653
|
-
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
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) => {
|
|
1218
|
+
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
1219
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
1220
|
+
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
1221
|
+
interactive: getBooleanOption(layoutOptions, "interactive"),
|
|
1222
|
+
expandNodes: getBooleanOption(layoutOptions, "expandNodes"),
|
|
1223
|
+
priority: (node) => {
|
|
665
1224
|
const child = graph.children?.find(
|
|
666
1225
|
(candidate) => String(candidate.id) === node.id,
|
|
667
1226
|
);
|
|
668
|
-
|
|
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;
|
|
1227
|
+
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
682
1228
|
},
|
|
683
1229
|
},
|
|
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
|
-
}
|
|
1230
|
+
})
|
|
1231
|
+
: algorithm === "fixed"
|
|
1232
|
+
? executeElkjs0111Layout({
|
|
1233
|
+
algorithm: "fixed",
|
|
1234
|
+
graph: graph_,
|
|
1235
|
+
options: { direction: getDirection(layoutOptions) },
|
|
1236
|
+
})
|
|
1237
|
+
: finishLayered());
|
|
1238
|
+
const resultPolicy = (
|
|
1239
|
+
laidOut as typeof laidOut & {
|
|
1240
|
+
[elkjs0111ResultPolicy]?: Elkjs0111ResultPolicy;
|
|
722
1241
|
}
|
|
1242
|
+
)[elkjs0111ResultPolicy];
|
|
1243
|
+
applyLayout(graph, laidOut, padding, layoutOptions, resultPolicy);
|
|
1244
|
+
if (resultPolicy?.providerBounds) {
|
|
1245
|
+
graph.width = resultPolicy.providerBounds.width;
|
|
1246
|
+
graph.height = resultPolicy.providerBounds.height;
|
|
723
1247
|
}
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1248
|
+
for (const restoration of hierarchyRestorations) {
|
|
1249
|
+
const splits = boundaryRoutes.get(restoration.edge);
|
|
1250
|
+
const sections = restoration.edge.sections;
|
|
1251
|
+
if (splits && sections?.length) {
|
|
1252
|
+
const parentPoints = sections.flatMap((section, index) => [
|
|
1253
|
+
...(index === 0 ? [section.startPoint] : []),
|
|
1254
|
+
...(section.bendPoints ?? []),
|
|
1255
|
+
section.endPoint,
|
|
1256
|
+
]);
|
|
1257
|
+
const segments = [
|
|
1258
|
+
...(splits.source
|
|
1259
|
+
? [
|
|
1260
|
+
{
|
|
1261
|
+
points: splits.source.points,
|
|
1262
|
+
offset: { x: splits.source.owner.x ?? 0, y: splits.source.owner.y ?? 0 },
|
|
1263
|
+
},
|
|
1264
|
+
]
|
|
1265
|
+
: []),
|
|
1266
|
+
{ points: parentPoints, offset: { x: 0, y: 0 } },
|
|
1267
|
+
...(splits.target
|
|
1268
|
+
? [
|
|
1269
|
+
{
|
|
1270
|
+
points: splits.target.points,
|
|
1271
|
+
offset: { x: splits.target.owner.x ?? 0, y: splits.target.owner.y ?? 0 },
|
|
1272
|
+
},
|
|
1273
|
+
]
|
|
1274
|
+
: []),
|
|
1275
|
+
];
|
|
1276
|
+
const points = joinCompoundRouteSegments(
|
|
1277
|
+
segments,
|
|
1278
|
+
getBooleanOption(layoutOptions, "unnecessaryBendpoints") === true,
|
|
746
1279
|
);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1280
|
+
const childJunctions = (split: typeof splits.source) =>
|
|
1281
|
+
(split?.junctionPoints ?? []).map((point) => ({
|
|
1282
|
+
x: point.x + (split?.owner.x ?? 0),
|
|
1283
|
+
y: point.y + (split?.owner.y ?? 0),
|
|
1284
|
+
}));
|
|
1285
|
+
const junctions = [
|
|
1286
|
+
...childJunctions(splits.source),
|
|
1287
|
+
...(restoration.edge.junctionPoints ?? []),
|
|
1288
|
+
...childJunctions(splits.target),
|
|
1289
|
+
];
|
|
1290
|
+
if (junctions.length) restoration.edge.junctionPoints = junctions;
|
|
1291
|
+
else delete restoration.edge.junctionPoints;
|
|
1292
|
+
restoration.edge.sections = [
|
|
1293
|
+
{
|
|
1294
|
+
...sections[0]!,
|
|
1295
|
+
startPoint: points[0]!,
|
|
1296
|
+
endPoint: points.at(-1)!,
|
|
1297
|
+
bendPoints: points.slice(1, -1),
|
|
1298
|
+
},
|
|
1299
|
+
];
|
|
1300
|
+
}
|
|
1301
|
+
restoration.edge.sources = restoration.sources;
|
|
1302
|
+
restoration.edge.targets = restoration.targets;
|
|
1303
|
+
restoration.edge.source = restoration.source;
|
|
1304
|
+
restoration.edge.target = restoration.target;
|
|
1305
|
+
for (const section of restoration.edge.sections ?? []) {
|
|
1306
|
+
if (
|
|
1307
|
+
section.incomingShape != null &&
|
|
1308
|
+
syntheticPortIds.has(String(section.incomingShape))
|
|
1309
|
+
) {
|
|
1310
|
+
section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
1311
|
+
}
|
|
1312
|
+
if (
|
|
1313
|
+
section.outgoingShape != null &&
|
|
1314
|
+
syntheticPortIds.has(String(section.outgoingShape))
|
|
1315
|
+
) {
|
|
1316
|
+
section.outgoingShape = restoration.targets?.[0] ?? restoration.target;
|
|
763
1317
|
}
|
|
764
|
-
crossEnd = compactedCross + (horizontal ? node.height : node.width);
|
|
765
1318
|
}
|
|
766
1319
|
}
|
|
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;
|
|
1320
|
+
for (const [compound, ports] of authoredPortsByCompound) {
|
|
1321
|
+
for (const port of ports ?? []) {
|
|
1322
|
+
const actual = compound.ports?.find((p) => p.id === port.id);
|
|
1323
|
+
if (actual)
|
|
1324
|
+
Object.assign(port, {
|
|
1325
|
+
x: actual.x,
|
|
1326
|
+
y: actual.y,
|
|
1327
|
+
width: actual.width,
|
|
1328
|
+
height: actual.height,
|
|
1329
|
+
});
|
|
781
1330
|
}
|
|
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
|
-
);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const resultPolicy = (
|
|
797
|
-
laidOut as typeof laidOut & {
|
|
798
|
-
[elkjs0111ResultPolicy]?: Elkjs0111ResultPolicy;
|
|
1331
|
+
compound.ports = ports;
|
|
799
1332
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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;
|
|
1333
|
+
for (const [compound, options_] of authoredOptionsByCompound)
|
|
1334
|
+
compound.layoutOptions = options_;
|
|
1335
|
+
if (hasHierarchy && topdownLayout) {
|
|
1336
|
+
for (const child of graph.children ?? []) {
|
|
1337
|
+
if ((child.children?.length ?? 0) === 0) continue;
|
|
1338
|
+
const position = { x: child.x, y: child.y };
|
|
1339
|
+
await this.#layout(child, {
|
|
1340
|
+
...arguments_,
|
|
1341
|
+
logging: false,
|
|
1342
|
+
measureExecutionTime: false,
|
|
1343
|
+
});
|
|
1344
|
+
child.x = position.x;
|
|
1345
|
+
child.y = position.y;
|
|
817
1346
|
}
|
|
818
1347
|
}
|
|
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;
|
|
1348
|
+
applyInsideSelfLoops(graph, insideSelfLoopBaseHeightByNodeId);
|
|
1349
|
+
if (arguments_.logging || arguments_.measureExecutionTime) {
|
|
1350
|
+
graph.logging = {
|
|
1351
|
+
name: "Native TypeScript layout",
|
|
1352
|
+
children: [{ name: String(algorithm) }],
|
|
1353
|
+
...(arguments_.measureExecutionTime
|
|
1354
|
+
? { executionTime: (performance.now() - startedAt) / 1_000 }
|
|
1355
|
+
: {}),
|
|
1356
|
+
};
|
|
833
1357
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1358
|
+
return graph;
|
|
1359
|
+
};
|
|
1360
|
+
return prepared;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Move opposite-direction track sharing apart on the finished layout. */
|
|
1365
|
+
function separated<T extends ElkNode>(graph: T, options: Readonly<Record<string, unknown>>): T {
|
|
1366
|
+
separateElkOpposingTracks(graph, { ...options, ...graph.layoutOptions });
|
|
1367
|
+
return graph;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function coordinatePreparedScopes(
|
|
1371
|
+
root: PreparedElkScope,
|
|
1372
|
+
): ReadonlyMap<PreparedElkScope, LayerOrder> | undefined {
|
|
1373
|
+
if (!root.phase || root.children.size === 0) return undefined;
|
|
1374
|
+
const prepared = [root];
|
|
1375
|
+
for (let i = 0; i < prepared.length; i++) prepared.push(...prepared[i]!.children.values());
|
|
1376
|
+
if (
|
|
1377
|
+
prepared.some(
|
|
1378
|
+
(scope) =>
|
|
1379
|
+
!scope.phase ||
|
|
1380
|
+
!["LAYER_SWEEP", "MEDIAN_LAYER_SWEEP"].includes(
|
|
1381
|
+
scope.options?.settings?.["crossingMinimization.strategy"] ?? "LAYER_SWEEP",
|
|
1382
|
+
),
|
|
1383
|
+
)
|
|
1384
|
+
)
|
|
1385
|
+
return undefined;
|
|
1386
|
+
const scopes = new Map<PreparedElkScope, HierarchyCrossingScope>();
|
|
1387
|
+
for (const scope of prepared) {
|
|
1388
|
+
const phase = scope.phase!;
|
|
1389
|
+
scopes.set(scope, {
|
|
1390
|
+
session: createLayerSweepSession(
|
|
1391
|
+
phase.input,
|
|
1392
|
+
phase.orientation,
|
|
1393
|
+
phase.assignment,
|
|
1394
|
+
scope.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP"
|
|
1395
|
+
? "median"
|
|
1396
|
+
: "mean",
|
|
1397
|
+
7,
|
|
1398
|
+
{ resetSeed: scope === root },
|
|
1399
|
+
),
|
|
1400
|
+
childrenByNodeId: new Map(),
|
|
1401
|
+
useBottomUp: true,
|
|
1402
|
+
independentRandom: scope !== root,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
for (const parent of prepared) {
|
|
1406
|
+
const parentScope = scopes.get(parent)!;
|
|
1407
|
+
for (const [id, child] of parent.children) {
|
|
1408
|
+
const nativeParent = parent.phase!.input.graph.nodes.find((node) => node.id === id)!;
|
|
1409
|
+
const parentPortOrderFixed = ["FIXED_ORDER", "FIXED_POS", "FIXED_RATIO"].includes(
|
|
1410
|
+
parent.phase!.input.nodeSettings?.(nativeParent)?.portConstraints ?? "FREE",
|
|
1411
|
+
);
|
|
1412
|
+
const ports = nativeParent.ports ?? [];
|
|
1413
|
+
const childPhase = child.phase!;
|
|
1414
|
+
const childSession = scopes.get(child)!.session;
|
|
1415
|
+
const childNodeById = new Map<string, (typeof childPhase.input.graph.nodes)[number]>();
|
|
1416
|
+
for (const node of childPhase.input.graph.nodes)
|
|
1417
|
+
if (!childNodeById.has(node.id)) childNodeById.set(node.id, node);
|
|
1418
|
+
const parentEdgeById = new Map<string, (typeof childPhase.input.graph.edges)[number]>();
|
|
1419
|
+
for (const edge of parent.phase!.input.graph.edges)
|
|
1420
|
+
if (!parentEdgeById.has(edge.id)) parentEdgeById.set(edge.id, edge);
|
|
1421
|
+
const flowSides = hierarchicalPortSides(parent.phase!.input);
|
|
1422
|
+
const inputPorts = ports.filter(
|
|
1423
|
+
(port) =>
|
|
1424
|
+
parent.phase!.input.portSettings?.(port, nativeParent)?.["port.side"] ===
|
|
1425
|
+
flowSides.before,
|
|
1426
|
+
).length;
|
|
1427
|
+
const outputPorts = ports.filter(
|
|
1428
|
+
(port) =>
|
|
1429
|
+
parent.phase!.input.portSettings?.(port, nativeParent)?.["port.side"] === flowSides.after,
|
|
1430
|
+
).length;
|
|
1431
|
+
const childScope: HierarchyCrossingScope = {
|
|
1432
|
+
...scopes.get(child)!,
|
|
1433
|
+
useBottomUp: useBottomUpHierarchySweep(
|
|
1434
|
+
childPhase.input,
|
|
1435
|
+
childPhase.orientation,
|
|
1436
|
+
childSession.snapshot().layers,
|
|
1437
|
+
{
|
|
1438
|
+
portOrderFixed: parentPortOrderFixed,
|
|
1439
|
+
inputPorts,
|
|
1440
|
+
outputPorts,
|
|
1441
|
+
deterministic:
|
|
1442
|
+
child.options?.settings?.["crossingMinimization.strategy"] === "MEDIAN_LAYER_SWEEP",
|
|
1443
|
+
},
|
|
1444
|
+
),
|
|
1445
|
+
alignBoundary: (forward) => {
|
|
1446
|
+
const childLayers = childSession.currentLayers();
|
|
1447
|
+
const layerIndex = forward ? 0 : childLayers.length - 1;
|
|
1448
|
+
const layer = childLayers[layerIndex]!;
|
|
1449
|
+
if (!layer.every((id) => externalPortDummyOf(childNodeById.get(id)!))) return false;
|
|
1450
|
+
// Parent port positions are physical; feedback reversal changes
|
|
1451
|
+
// edge direction without changing the boundary port's identity.
|
|
1452
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
1453
|
+
const dummyByParentPort = new Map(
|
|
1454
|
+
childPhase.input.graph.nodes
|
|
1455
|
+
.filter((n) => externalPortDummyOf(n))
|
|
1456
|
+
.map((n) => [externalPortDummyOf(n)!.parentPortId ?? `${n.id}:parent`, n.id]),
|
|
1457
|
+
);
|
|
1458
|
+
const ordered = physical.flatMap((portId) => {
|
|
1459
|
+
const [nodeId, portName] = JSON.parse(portId) as [string, string];
|
|
1460
|
+
const dummy = nodeId === id ? dummyByParentPort.get(portName) : undefined;
|
|
1461
|
+
return dummy && layer.includes(dummy) ? [dummy] : [];
|
|
1462
|
+
});
|
|
1463
|
+
if (forward) ordered.reverse();
|
|
1464
|
+
const unique = [...new Set(ordered)];
|
|
1465
|
+
if (unique.length !== layer.length)
|
|
1466
|
+
throw new Error(`Incomplete hierarchical port order for ${id}`);
|
|
1467
|
+
childSession.replaceLayer(layerIndex, unique);
|
|
1468
|
+
return true;
|
|
1469
|
+
},
|
|
1470
|
+
publishBoundary: (forward) => {
|
|
1471
|
+
const childLayers = childSession.currentLayers();
|
|
1472
|
+
const layer = childLayers[forward ? childLayers.length - 1 : 0]!;
|
|
1473
|
+
if (!layer.every((id) => externalPortDummyOf(childNodeById.get(id)!))) return;
|
|
1474
|
+
const rank = new Map(
|
|
1475
|
+
layer.map((dummy, index) => [
|
|
1476
|
+
externalPortDummyOf(childNodeById.get(dummy)!)!.parentPortId ?? `${dummy}:parent`,
|
|
1477
|
+
index,
|
|
1478
|
+
]),
|
|
1479
|
+
);
|
|
1480
|
+
const physical = parentScope.session.physicalPortOrder(id);
|
|
1481
|
+
const boundarySlots = physical.flatMap((portId, index) => {
|
|
1482
|
+
const [nodeId, portName] = JSON.parse(portId) as [string, string];
|
|
1483
|
+
return nodeId === id && rank.has(portName) ? [index] : [];
|
|
1484
|
+
});
|
|
1485
|
+
const boundaryPorts = boundarySlots
|
|
1486
|
+
.map((index) => physical[index]!)
|
|
1487
|
+
.sort((a, b) => {
|
|
1488
|
+
const portA = (JSON.parse(a) as [string, string])[1];
|
|
1489
|
+
const portB = (JSON.parse(b) as [string, string])[1];
|
|
1490
|
+
const result = rank.get(portA)! - rank.get(portB)!;
|
|
1491
|
+
return forward ? result : -result;
|
|
1492
|
+
});
|
|
1493
|
+
const updatedPhysical = [...physical];
|
|
1494
|
+
boundarySlots.forEach((slot, index) => {
|
|
1495
|
+
updatedPhysical[slot] = boundaryPorts[index]!;
|
|
1496
|
+
});
|
|
1497
|
+
const edgePortRank = (edgeId: string): number => {
|
|
1498
|
+
const edge = parentEdgeById.get(edgeId)!;
|
|
1499
|
+
const portName = edge.sourceId === id ? edge.sourcePort : edge.targetPort;
|
|
1500
|
+
return updatedPhysical.indexOf(JSON.stringify([id, portName]));
|
|
1501
|
+
};
|
|
1502
|
+
const { incoming, outgoing } = parentScope.session.edgeOrders(id);
|
|
1503
|
+
const byPort = (current: readonly string[]) =>
|
|
1504
|
+
[...current].sort((a, b) => edgePortRank(a) - edgePortRank(b));
|
|
1505
|
+
parentScope.session.reorderNodePorts(
|
|
1506
|
+
id,
|
|
1507
|
+
updatedPhysical,
|
|
1508
|
+
byPort(incoming),
|
|
1509
|
+
byPort(outgoing),
|
|
1510
|
+
);
|
|
1511
|
+
},
|
|
843
1512
|
};
|
|
1513
|
+
childScope.publishBottomUp = () => {
|
|
1514
|
+
childScope.publishBoundary!(true);
|
|
1515
|
+
childScope.publishBoundary!(false);
|
|
1516
|
+
parentScope.session.lockPortOrder(id);
|
|
1517
|
+
};
|
|
1518
|
+
scopes.set(child, childScope);
|
|
1519
|
+
(parentScope.childrenByNodeId as Map<string, HierarchyCrossingScope>).set(id, childScope);
|
|
844
1520
|
}
|
|
845
|
-
return graph as LaidOutElkNode<T>;
|
|
846
1521
|
}
|
|
1522
|
+
const orders = minimizeHierarchyCrossings(scopes.get(root)!);
|
|
1523
|
+
|
|
1524
|
+
return new Map(prepared.map((scope) => [scope, orders.get(scopes.get(scope)!)!]));
|
|
847
1525
|
}
|
|
848
1526
|
|
|
849
1527
|
function parsePadding(value: unknown, fallback = 0) {
|
|
@@ -1083,16 +1761,56 @@ function parseMargin(value: unknown): unknown {
|
|
|
1083
1761
|
return parsePadding(value, 0);
|
|
1084
1762
|
}
|
|
1085
1763
|
|
|
1764
|
+
// Every accepted spelling of a layered option, with its precedence (lower wins).
|
|
1765
|
+
const layeredOptionAliases = new Map<
|
|
1766
|
+
string,
|
|
1767
|
+
{ definition: (typeof elkLayeredOptionDefinitions)[number]; rank: number }
|
|
1768
|
+
>();
|
|
1769
|
+
for (const definition of elkLayeredOptionDefinitions) {
|
|
1770
|
+
const suffix = definition.elkId.replace(/^org\.eclipse\.elk\./, "");
|
|
1771
|
+
[definition.name, suffix, `elk.${suffix}`, definition.elkId].forEach((key, rank) => {
|
|
1772
|
+
const current = layeredOptionAliases.get(key);
|
|
1773
|
+
if (!current || rank < current.rank) layeredOptionAliases.set(key, { definition, rank });
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const indexByIdCache = new WeakMap<
|
|
1778
|
+
readonly { id?: ElkId }[],
|
|
1779
|
+
{ length: number; index: Map<string, number> }
|
|
1780
|
+
>();
|
|
1781
|
+
|
|
1782
|
+
/** First element whose stringified id matches, like `find`, via a per-array index. */
|
|
1783
|
+
function findById<T extends { id?: ElkId }>(items: readonly T[] | undefined, id: string) {
|
|
1784
|
+
if (!items) return undefined;
|
|
1785
|
+
let cached = indexByIdCache.get(items);
|
|
1786
|
+
if (!cached || cached.length !== items.length) {
|
|
1787
|
+
const index = new Map<string, number>();
|
|
1788
|
+
items.forEach((item, position) => {
|
|
1789
|
+
const key = String(item.id);
|
|
1790
|
+
if (!index.has(key)) index.set(key, position);
|
|
1791
|
+
});
|
|
1792
|
+
indexByIdCache.set(items, (cached = { length: items.length, index }));
|
|
1793
|
+
}
|
|
1794
|
+
const position = cached.index.get(id);
|
|
1795
|
+
return position === undefined ? undefined : { item: items[position]!, position };
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1086
1798
|
function getElementLayeredSettings(
|
|
1087
1799
|
options: Readonly<Record<string, unknown>>,
|
|
1088
1800
|
): ElkLayeredOptionValueByName {
|
|
1089
1801
|
const settings: ElkLayeredOptionValueByName = {};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1802
|
+
const chosen = new Map<
|
|
1803
|
+
(typeof elkLayeredOptionDefinitions)[number],
|
|
1804
|
+
{ rank: number; value: unknown }
|
|
1805
|
+
>();
|
|
1806
|
+
for (const [key, value] of Object.entries(options)) {
|
|
1807
|
+
const alias = layeredOptionAliases.get(key);
|
|
1808
|
+
if (!alias || value === undefined) continue;
|
|
1809
|
+
const current = chosen.get(alias.definition);
|
|
1810
|
+
if (!current || alias.rank < current.rank)
|
|
1811
|
+
chosen.set(alias.definition, { rank: alias.rank, value });
|
|
1812
|
+
}
|
|
1813
|
+
for (const [definition, { value }] of chosen) {
|
|
1096
1814
|
const vectorMatch =
|
|
1097
1815
|
definition.name === "port.anchor" && typeof value === "string"
|
|
1098
1816
|
? value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/)
|
|
@@ -1260,7 +1978,16 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1260
1978
|
if (String(getOption(child.layoutOptions ?? {}, "portConstraints")) !== "FIXED_SIDE") {
|
|
1261
1979
|
return ports;
|
|
1262
1980
|
}
|
|
1263
|
-
const
|
|
1981
|
+
const canonicalSides: Record<string, string> =
|
|
1982
|
+
getDirection(globalOptions) === "left"
|
|
1983
|
+
? { NORTH: "NORTH", EAST: "WEST", SOUTH: "SOUTH", WEST: "EAST" }
|
|
1984
|
+
: getDirection(globalOptions) === "down"
|
|
1985
|
+
? { NORTH: "WEST", EAST: "SOUTH", SOUTH: "EAST", WEST: "NORTH" }
|
|
1986
|
+
: getDirection(globalOptions) === "up"
|
|
1987
|
+
? { NORTH: "EAST", EAST: "SOUTH", SOUTH: "WEST", WEST: "NORTH" }
|
|
1988
|
+
: { NORTH: "NORTH", EAST: "EAST", SOUTH: "SOUTH", WEST: "WEST" };
|
|
1989
|
+
const side = (port: ElkPort) =>
|
|
1990
|
+
canonicalSides[String(getOption(port.layoutOptions ?? {}, "port.side"))] ?? "UNDEFINED";
|
|
1264
1991
|
const sideOrder = ["NORTH", "EAST", "SOUTH", "WEST"];
|
|
1265
1992
|
ports.sort((left, right) => {
|
|
1266
1993
|
const leftSide = side(left);
|
|
@@ -1287,10 +2014,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1287
2014
|
const usePortModelOrder =
|
|
1288
2015
|
getBooleanOption(globalOptions, "layered.considerModelOrder.portModelOrder") === true;
|
|
1289
2016
|
if (modelOrderStrategy !== "NONE" && !usePortModelOrder) {
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
2017
|
+
const leftOrder = edgeModelOrderByPortId.get(String(left.id));
|
|
2018
|
+
const rightOrder = edgeModelOrderByPortId.get(String(right.id));
|
|
2019
|
+
// Ports without edges order last and keep their order among themselves,
|
|
2020
|
+
// unreversed even on the WEST side, as ELK does.
|
|
2021
|
+
if (leftOrder === undefined && rightOrder === undefined) return 0;
|
|
2022
|
+
if (leftOrder !== rightOrder) return (leftOrder ?? Infinity) - (rightOrder ?? Infinity);
|
|
1294
2023
|
}
|
|
1295
2024
|
const direction = leftSide === "WEST" ? -1 : 1;
|
|
1296
2025
|
return direction * ((child.ports?.indexOf(left) ?? 0) - (child.ports?.indexOf(right) ?? 0));
|
|
@@ -1298,7 +2027,7 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1298
2027
|
return ports;
|
|
1299
2028
|
};
|
|
1300
2029
|
|
|
1301
|
-
|
|
2030
|
+
const native = createGraph({
|
|
1302
2031
|
id: String(root.id),
|
|
1303
2032
|
nodes: children.map((child) => ({
|
|
1304
2033
|
id: String(child.id),
|
|
@@ -1353,6 +2082,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1353
2082
|
: [];
|
|
1354
2083
|
}),
|
|
1355
2084
|
});
|
|
2085
|
+
for (const node of native.nodes) {
|
|
2086
|
+
const child = children.find((candidate) => String(candidate.id) === node.id);
|
|
2087
|
+
const origin = child && externalPortDummyOf(child);
|
|
2088
|
+
if (origin) attachExternalPortDummy(node, origin);
|
|
2089
|
+
}
|
|
2090
|
+
return native;
|
|
1356
2091
|
}
|
|
1357
2092
|
|
|
1358
2093
|
function parsePoints(value: unknown): ElkPoint[] | undefined {
|
|
@@ -1431,11 +2166,21 @@ function applyLayout(
|
|
|
1431
2166
|
continue;
|
|
1432
2167
|
}
|
|
1433
2168
|
const section = getParentEdgeSection(root, edge);
|
|
1434
|
-
if (section)
|
|
2169
|
+
if (section) {
|
|
2170
|
+
edge.sections = [section];
|
|
2171
|
+
edge.container = root.id;
|
|
2172
|
+
}
|
|
1435
2173
|
continue;
|
|
1436
2174
|
}
|
|
2175
|
+
const nativeJunctions = resultPolicy?.junctionPointsByEdgeId?.get(String(edge.id));
|
|
2176
|
+
if (nativeJunctions) {
|
|
2177
|
+
if (nativeJunctions.length)
|
|
2178
|
+
edge.junctionPoints = nativeJunctions.map((point) => ({ ...point }));
|
|
2179
|
+
else delete edge.junctionPoints;
|
|
2180
|
+
}
|
|
1437
2181
|
const section = toSection(edge, laidOutEdge.points ?? []);
|
|
1438
2182
|
edge.sections = section ? [section] : [];
|
|
2183
|
+
if (section) edge.container = root.id;
|
|
1439
2184
|
const target = root.children?.find(
|
|
1440
2185
|
(child) => String(child.id) === String(edge.targets?.[0] ?? edge.target),
|
|
1441
2186
|
);
|
|
@@ -1452,6 +2197,8 @@ function applyLayout(
|
|
|
1452
2197
|
const midpointX = (laidOutEdge.x ?? 0) + (laidOutEdge.width ?? 0) / 2;
|
|
1453
2198
|
const edgeLabelSpacing = getNumberOption(layoutOptions, "spacing.edgeLabel") ?? 2;
|
|
1454
2199
|
const labelLabelSpacing = getNumberOption(layoutOptions, "spacing.labelLabel") ?? 0;
|
|
2200
|
+
// A lone label keeps the native position, which already avoids collisions.
|
|
2201
|
+
const loneLabel = (edge.labels ?? []).filter(isLayoutEdgeLabel).length === 1;
|
|
1455
2202
|
for (const label of edge.labels ?? []) {
|
|
1456
2203
|
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) {
|
|
1457
2204
|
label.x ??= 0;
|
|
@@ -1468,17 +2215,51 @@ function applyLayout(
|
|
|
1468
2215
|
);
|
|
1469
2216
|
const width = label.width ?? 0;
|
|
1470
2217
|
label.x =
|
|
1471
|
-
placement
|
|
1472
|
-
?
|
|
1473
|
-
: placement === "
|
|
1474
|
-
?
|
|
1475
|
-
:
|
|
2218
|
+
loneLabel && placement !== "CENTER" && laidOutEdge.x !== undefined
|
|
2219
|
+
? laidOutEdge.x
|
|
2220
|
+
: placement === "TAIL"
|
|
2221
|
+
? firstPoint.x + edgeLabelSpacing
|
|
2222
|
+
: placement === "HEAD"
|
|
2223
|
+
? lastPoint.x - width - edgeLabelSpacing
|
|
2224
|
+
: midpointX - width / 2;
|
|
1476
2225
|
label.y = labelY;
|
|
1477
2226
|
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
1478
2227
|
}
|
|
1479
2228
|
}
|
|
1480
2229
|
if (!resultPolicy?.skipBoundsNormalization) {
|
|
1481
|
-
normalizeElkGraphBounds(
|
|
2230
|
+
normalizeElkGraphBounds(
|
|
2231
|
+
root,
|
|
2232
|
+
padding,
|
|
2233
|
+
layoutOptions,
|
|
2234
|
+
resultPolicy?.normalizationBounds,
|
|
2235
|
+
resultPolicy?.placementCrossBounds,
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
if (
|
|
2239
|
+
!resultPolicy?.preserveEdgeSections &&
|
|
2240
|
+
String(getOption(layoutOptions, "edgeRouting") ?? "ORTHOGONAL") === "ORTHOGONAL"
|
|
2241
|
+
) {
|
|
2242
|
+
const direction = getDirection(layoutOptions);
|
|
2243
|
+
applyOrthogonalJunctions(
|
|
2244
|
+
root,
|
|
2245
|
+
direction === "right" || direction === "left",
|
|
2246
|
+
direction === "right" || direction === "down",
|
|
2247
|
+
getBooleanOption(layoutOptions, "layered.mergeEdges") === true,
|
|
2248
|
+
new Set(
|
|
2249
|
+
(root.children ?? [])
|
|
2250
|
+
.filter((n) => getBooleanOption(n.layoutOptions ?? {}, "hypernode") === true)
|
|
2251
|
+
.map((n) => String(n.id)),
|
|
2252
|
+
),
|
|
2253
|
+
new Set(
|
|
2254
|
+
(root.edges ?? [])
|
|
2255
|
+
.filter(
|
|
2256
|
+
(e) =>
|
|
2257
|
+
getBooleanOption(e.layoutOptions ?? {}, "noLayout") === true ||
|
|
2258
|
+
resultPolicy?.junctionPointsByEdgeId?.has(String(e.id)) === true,
|
|
2259
|
+
)
|
|
2260
|
+
.map((e) => String(e.id)),
|
|
2261
|
+
),
|
|
2262
|
+
);
|
|
1482
2263
|
}
|
|
1483
2264
|
}
|
|
1484
2265
|
|
|
@@ -1486,15 +2267,21 @@ function normalizeElkGraphBounds(
|
|
|
1486
2267
|
root: ElkNode,
|
|
1487
2268
|
padding: { top: number; right: number; bottom: number; left: number },
|
|
1488
2269
|
layoutOptions: Readonly<Record<string, unknown>> = {},
|
|
2270
|
+
compactionBounds?: Elkjs0111ResultPolicy["normalizationBounds"],
|
|
2271
|
+
placementCrossBounds?: Elkjs0111ResultPolicy["placementCrossBounds"],
|
|
1489
2272
|
): void {
|
|
2273
|
+
// Edge coordinates belong to their layout container, not their JSON storage owner.
|
|
2274
|
+
const layoutEdges = (root.edges ?? []).filter(
|
|
2275
|
+
(edge) => edge.container == null || String(edge.container) === String(root.id),
|
|
2276
|
+
);
|
|
1490
2277
|
const authoredWidth = root.width;
|
|
1491
2278
|
const authoredHeight = root.height;
|
|
1492
2279
|
const fixedGraphSize = getBooleanOption(layoutOptions, "nodeSize.fixedGraphSize") === true;
|
|
1493
2280
|
const layoutChildren = (root.children ?? []).filter(
|
|
1494
2281
|
(node) => getBooleanOption(node.layoutOptions ?? {}, "noLayout") !== true,
|
|
1495
2282
|
);
|
|
1496
|
-
let minimumX = Number.POSITIVE_INFINITY;
|
|
1497
|
-
let minimumY = Number.POSITIVE_INFINITY;
|
|
2283
|
+
let minimumX = compactionBounds?.left ?? Number.POSITIVE_INFINITY;
|
|
2284
|
+
let minimumY = compactionBounds?.top ?? Number.POSITIVE_INFINITY;
|
|
1498
2285
|
for (const node of layoutChildren) {
|
|
1499
2286
|
minimumX = Math.min(
|
|
1500
2287
|
minimumX,
|
|
@@ -1515,7 +2302,7 @@ function normalizeElkGraphBounds(
|
|
|
1515
2302
|
),
|
|
1516
2303
|
);
|
|
1517
2304
|
}
|
|
1518
|
-
for (const edge of
|
|
2305
|
+
for (const edge of layoutEdges) {
|
|
1519
2306
|
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1520
2307
|
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1521
2308
|
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
@@ -1536,13 +2323,17 @@ function normalizeElkGraphBounds(
|
|
|
1536
2323
|
node.x = (node.x ?? 0) + shiftX;
|
|
1537
2324
|
node.y = (node.y ?? 0) + shiftY;
|
|
1538
2325
|
}
|
|
1539
|
-
for (const edge of
|
|
2326
|
+
for (const edge of layoutEdges) {
|
|
1540
2327
|
for (const section of edge.sections ?? []) {
|
|
1541
2328
|
for (const point of [section.startPoint, ...(section.bendPoints ?? []), section.endPoint]) {
|
|
1542
2329
|
point.x += shiftX;
|
|
1543
2330
|
point.y += shiftY;
|
|
1544
2331
|
}
|
|
1545
2332
|
}
|
|
2333
|
+
for (const point of edge.junctionPoints ?? []) {
|
|
2334
|
+
point.x += shiftX;
|
|
2335
|
+
point.y += shiftY;
|
|
2336
|
+
}
|
|
1546
2337
|
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
1547
2338
|
label.x = (label.x ?? 0) + shiftX;
|
|
1548
2339
|
label.y = (label.y ?? 0) + shiftY;
|
|
@@ -1557,7 +2348,7 @@ function normalizeElkGraphBounds(
|
|
|
1557
2348
|
0,
|
|
1558
2349
|
...layoutChildren.map((node) => (node.y ?? 0) + (node.height ?? 0)),
|
|
1559
2350
|
);
|
|
1560
|
-
const layoutEdgePoints =
|
|
2351
|
+
const layoutEdgePoints = layoutEdges.flatMap((edge) =>
|
|
1561
2352
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1562
2353
|
? []
|
|
1563
2354
|
: (edge.sections ?? []).flatMap((section) => [
|
|
@@ -1573,7 +2364,7 @@ function normalizeElkGraphBounds(
|
|
|
1573
2364
|
for (const child of root.children ?? []) {
|
|
1574
2365
|
for (const port of child.ports ?? []) childByEndpointId.set(String(port.id), child);
|
|
1575
2366
|
}
|
|
1576
|
-
const wrappedEdgeCount =
|
|
2367
|
+
const wrappedEdgeCount = layoutEdges.filter((edge) => {
|
|
1577
2368
|
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
1578
2369
|
const target = childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
|
|
1579
2370
|
if (!source || !target) return false;
|
|
@@ -1586,24 +2377,44 @@ function normalizeElkGraphBounds(
|
|
|
1586
2377
|
: (source.y ?? 0) < (target.y ?? 0);
|
|
1587
2378
|
}).length;
|
|
1588
2379
|
const hasWrappedEdge = wrappedEdgeCount > 0;
|
|
1589
|
-
const
|
|
2380
|
+
const retainPlacementBounds =
|
|
1590
2381
|
wrappingStrategy !== "MULTI_EDGE" &&
|
|
1591
2382
|
!(wrappingStrategy === "SINGLE_EDGE" && hasWrappedEdge) &&
|
|
1592
2383
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") ===
|
|
1593
2384
|
"NONE" &&
|
|
1594
2385
|
getBooleanOption(layoutOptions, "layered.feedbackEdges") !== true &&
|
|
1595
2386
|
String(getOption(layoutOptions, "layered.layering.nodePromotion.strategy") ?? "NONE") !==
|
|
1596
|
-
"MODEL_ORDER_LEFT_TO_RIGHT"
|
|
1597
|
-
|
|
2387
|
+
"MODEL_ORDER_LEFT_TO_RIGHT";
|
|
2388
|
+
// Actual helper extents survive nested scopes. The legacy route-pixel
|
|
2389
|
+
// allowance still excludes compound scopes.
|
|
2390
|
+
const addBoundaryPixel =
|
|
2391
|
+
retainPlacementBounds &&
|
|
1598
2392
|
!(root.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
2393
|
+
// Prefer the retained placement extent on the cross axis. Other placers
|
|
2394
|
+
// retain the legacy route-bound allowance until they expose phase bounds.
|
|
2395
|
+
// Loop envelopes own their bounds. Ordinary long-edge dummies still have
|
|
2396
|
+
// a one-pixel cross-axis extent when another edge in the graph is a loop.
|
|
2397
|
+
// Apply that allowance to route points, before unioning labels and margins.
|
|
2398
|
+
const isSelfLoop = (edge: ElkEdge) => {
|
|
2399
|
+
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
2400
|
+
return (
|
|
2401
|
+
source !== undefined &&
|
|
2402
|
+
source === childByEndpointId.get(String(edge.targets?.[0] ?? edge.target))
|
|
2403
|
+
);
|
|
2404
|
+
};
|
|
2405
|
+
const hasSelfLoops = layoutEdges.some(isSelfLoop);
|
|
1599
2406
|
const edgeBoundsExtraX =
|
|
1600
2407
|
addBoundaryPixel &&
|
|
2408
|
+
placementCrossBounds?.axis !== "x" &&
|
|
2409
|
+
!(hasSelfLoops && (direction === "right" || direction === "left")) &&
|
|
1601
2410
|
layoutEdgePoints.length > 0 &&
|
|
1602
2411
|
Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9
|
|
1603
2412
|
? 1
|
|
1604
2413
|
: 0;
|
|
1605
2414
|
const edgeBoundsExtraY =
|
|
1606
2415
|
addBoundaryPixel &&
|
|
2416
|
+
placementCrossBounds?.axis !== "y" &&
|
|
2417
|
+
!(hasSelfLoops && (direction === "down" || direction === "up")) &&
|
|
1607
2418
|
layoutEdgePoints.length > 0 &&
|
|
1608
2419
|
Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9
|
|
1609
2420
|
? 1
|
|
@@ -1616,6 +2427,7 @@ function normalizeElkGraphBounds(
|
|
|
1616
2427
|
? 1
|
|
1617
2428
|
: 0;
|
|
1618
2429
|
const postCompactionBoundsExtraX =
|
|
2430
|
+
!compactionBounds &&
|
|
1619
2431
|
!addBoundaryPixel &&
|
|
1620
2432
|
!hasWrappedEdge &&
|
|
1621
2433
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1625,6 +2437,7 @@ function normalizeElkGraphBounds(
|
|
|
1625
2437
|
? 0.04
|
|
1626
2438
|
: 0;
|
|
1627
2439
|
const postCompactionBoundsExtraY =
|
|
2440
|
+
!compactionBounds &&
|
|
1628
2441
|
!addBoundaryPixel &&
|
|
1629
2442
|
!hasWrappedEdge &&
|
|
1630
2443
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1635,7 +2448,10 @@ function normalizeElkGraphBounds(
|
|
|
1635
2448
|
: 0;
|
|
1636
2449
|
const calculatedWidth =
|
|
1637
2450
|
Math.max(
|
|
1638
|
-
0,
|
|
2451
|
+
compactionBounds ? compactionBounds.right + shiftX : 0,
|
|
2452
|
+
retainPlacementBounds && placementCrossBounds?.axis === "x"
|
|
2453
|
+
? placementCrossBounds.maximum + shiftX
|
|
2454
|
+
: 0,
|
|
1639
2455
|
...layoutChildren.flatMap((node) => [
|
|
1640
2456
|
(node.x ?? 0) + (node.width ?? 0),
|
|
1641
2457
|
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
@@ -1646,23 +2462,24 @@ function normalizeElkGraphBounds(
|
|
|
1646
2462
|
),
|
|
1647
2463
|
),
|
|
1648
2464
|
]),
|
|
1649
|
-
...
|
|
2465
|
+
...layoutEdges.flatMap((edge) =>
|
|
1650
2466
|
(edge.labels ?? [])
|
|
1651
2467
|
.filter(isLayoutEdgeLabel)
|
|
1652
2468
|
.map((label) => (label.x ?? 0) + (label.width ?? 0)),
|
|
1653
2469
|
),
|
|
1654
|
-
...
|
|
2470
|
+
...layoutEdges.flatMap((edge) =>
|
|
1655
2471
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1656
2472
|
? []
|
|
1657
2473
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1658
|
-
section.startPoint.x,
|
|
1659
|
-
...(section.bendPoints ?? []).map(
|
|
1660
|
-
|
|
2474
|
+
section.startPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
2475
|
+
...(section.bendPoints ?? []).map(
|
|
2476
|
+
(point) => point.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
2477
|
+
),
|
|
2478
|
+
section.endPoint.x + (isSelfLoop(edge) ? 0 : edgeBoundsExtraX),
|
|
1661
2479
|
]),
|
|
1662
2480
|
),
|
|
1663
2481
|
) +
|
|
1664
2482
|
padding.right +
|
|
1665
|
-
edgeBoundsExtraX +
|
|
1666
2483
|
postCompactionBoundsExtraX +
|
|
1667
2484
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
1668
2485
|
(getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up")
|
|
@@ -1670,7 +2487,10 @@ function normalizeElkGraphBounds(
|
|
|
1670
2487
|
: 0);
|
|
1671
2488
|
const calculatedHeight =
|
|
1672
2489
|
Math.max(
|
|
1673
|
-
0,
|
|
2490
|
+
compactionBounds ? compactionBounds.bottom + shiftY : 0,
|
|
2491
|
+
retainPlacementBounds && placementCrossBounds?.axis === "y"
|
|
2492
|
+
? placementCrossBounds.maximum + shiftY
|
|
2493
|
+
: 0,
|
|
1674
2494
|
...layoutChildren.flatMap((node) => [
|
|
1675
2495
|
(node.y ?? 0) + (node.height ?? 0),
|
|
1676
2496
|
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
@@ -1681,7 +2501,7 @@ function normalizeElkGraphBounds(
|
|
|
1681
2501
|
),
|
|
1682
2502
|
),
|
|
1683
2503
|
]),
|
|
1684
|
-
...
|
|
2504
|
+
...layoutEdges.flatMap((edge) =>
|
|
1685
2505
|
(edge.labels ?? [])
|
|
1686
2506
|
.filter(isLayoutEdgeLabel)
|
|
1687
2507
|
.map(
|
|
@@ -1689,23 +2509,26 @@ function normalizeElkGraphBounds(
|
|
|
1689
2509
|
(label.y ?? 0) +
|
|
1690
2510
|
(label.height ?? 0) +
|
|
1691
2511
|
(String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") ===
|
|
1692
|
-
|
|
2512
|
+
"CENTER" &&
|
|
2513
|
+
childByEndpointId.get(String(edge.sources?.[0] ?? edge.source)) !==
|
|
2514
|
+
childByEndpointId.get(String(edge.targets?.[0] ?? edge.target))
|
|
1693
2515
|
? 1
|
|
1694
2516
|
: 0),
|
|
1695
2517
|
),
|
|
1696
2518
|
),
|
|
1697
|
-
...
|
|
2519
|
+
...layoutEdges.flatMap((edge) =>
|
|
1698
2520
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1699
2521
|
? []
|
|
1700
2522
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1701
|
-
section.startPoint.y,
|
|
1702
|
-
...(section.bendPoints ?? []).map(
|
|
1703
|
-
|
|
2523
|
+
section.startPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
2524
|
+
...(section.bendPoints ?? []).map(
|
|
2525
|
+
(point) => point.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
2526
|
+
),
|
|
2527
|
+
section.endPoint.y + (isSelfLoop(edge) ? 0 : edgeBoundsExtraY),
|
|
1704
2528
|
]),
|
|
1705
2529
|
),
|
|
1706
2530
|
) +
|
|
1707
2531
|
padding.bottom +
|
|
1708
|
-
edgeBoundsExtraY +
|
|
1709
2532
|
singleMultiEdgeCutBoundsExtraY +
|
|
1710
2533
|
postCompactionBoundsExtraY +
|
|
1711
2534
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
@@ -1903,3 +2726,123 @@ export type {
|
|
|
1903
2726
|
|
|
1904
2727
|
export { getElkRoutes } from "./routes";
|
|
1905
2728
|
export type { ElkRouteOptions } from "./routes";
|
|
2729
|
+
|
|
2730
|
+
/** Edges crossing `group`, in the order ELK's CompoundGraphPreprocessor creates its boundary ports. */
|
|
2731
|
+
function elkBoundaryEdgeOrder(
|
|
2732
|
+
group: ElkNode,
|
|
2733
|
+
scope: ElkNode,
|
|
2734
|
+
originals: ReadonlyMap<ElkEdge, { sourceId: string; targetId: string }>,
|
|
2735
|
+
): ElkEdge[] {
|
|
2736
|
+
const edges: ElkEdge[] = [];
|
|
2737
|
+
const collect = (node: ElkNode) => {
|
|
2738
|
+
edges.push(...(node.edges ?? []));
|
|
2739
|
+
for (const child of node.children ?? []) collect(child);
|
|
2740
|
+
};
|
|
2741
|
+
collect(scope);
|
|
2742
|
+
const endpoints = (edge: ElkEdge) =>
|
|
2743
|
+
originals.get(edge) ?? {
|
|
2744
|
+
sourceId: String(edge.sources?.[0] ?? edge.source),
|
|
2745
|
+
targetId: String(edge.targets?.[0] ?? edge.target),
|
|
2746
|
+
};
|
|
2747
|
+
const order = (node: ElkNode): ElkEdge[] => {
|
|
2748
|
+
const inside = new Set<string>();
|
|
2749
|
+
const mark = (parent: ElkNode) => {
|
|
2750
|
+
for (const child of parent.children ?? []) {
|
|
2751
|
+
inside.add(String(child.id));
|
|
2752
|
+
for (const port of child.ports ?? []) inside.add(String(port.id));
|
|
2753
|
+
mark(child);
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
mark(node);
|
|
2757
|
+
const leaves = (edge: ElkEdge) =>
|
|
2758
|
+
inside.has(endpoints(edge).sourceId) !== inside.has(endpoints(edge).targetId);
|
|
2759
|
+
const result = new Set<ElkEdge>();
|
|
2760
|
+
for (const child of node.children ?? [])
|
|
2761
|
+
if (child.children?.length)
|
|
2762
|
+
for (const edge of order(child)) if (leaves(edge)) result.add(edge);
|
|
2763
|
+
for (const child of node.children ?? []) {
|
|
2764
|
+
for (const port of [...(child.ports ?? []).map((p) => String(p.id)), String(child.id)]) {
|
|
2765
|
+
for (const edge of edges)
|
|
2766
|
+
if (endpoints(edge).sourceId === port && leaves(edge)) result.add(edge);
|
|
2767
|
+
for (const edge of edges)
|
|
2768
|
+
if (endpoints(edge).targetId === port && leaves(edge)) result.add(edge);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return [...result];
|
|
2772
|
+
};
|
|
2773
|
+
return order(group);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
/** Creation order of hierarchy segments in ELK's CompoundGraphPreprocessor. */
|
|
2777
|
+
const hierarchySegmentRank = new WeakMap<ElkEdge, number>();
|
|
2778
|
+
const segmentOrderByScope = new WeakMap<ElkNode, ReadonlyMap<string, number>>();
|
|
2779
|
+
const originalEdgeId = new WeakMap<ElkEdge, string>();
|
|
2780
|
+
|
|
2781
|
+
/**
|
|
2782
|
+
* Segments inside `group`, in creation order: the inner pass connects child
|
|
2783
|
+
* groups' exported ports first, then the outer pass leaves `group` itself.
|
|
2784
|
+
*/
|
|
2785
|
+
function elkSegmentOrder(
|
|
2786
|
+
group: ElkNode,
|
|
2787
|
+
scope: ElkNode,
|
|
2788
|
+
originals: ReadonlyMap<ElkEdge, { sourceId: string; targetId: string }>,
|
|
2789
|
+
ownBoundary: boolean,
|
|
2790
|
+
): Map<string, number> {
|
|
2791
|
+
const order = new Map<string, number>();
|
|
2792
|
+
const add = (edge: ElkEdge) => {
|
|
2793
|
+
const id = originalEdgeId.get(edge) ?? String(edge.id);
|
|
2794
|
+
if (!order.has(id)) order.set(id, order.size);
|
|
2795
|
+
};
|
|
2796
|
+
for (const child of group.children ?? [])
|
|
2797
|
+
if (child.children?.length)
|
|
2798
|
+
for (const edge of elkBoundaryEdgeOrder(child, scope, originals)) add(edge);
|
|
2799
|
+
if (ownBoundary) for (const edge of elkBoundaryEdgeOrder(group, scope, originals)) add(edge);
|
|
2800
|
+
return order;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
const isPostCompactionKey = (key: string) =>
|
|
2804
|
+
/(^|\.)compaction\.postCompaction\.strategy$/.test(key);
|
|
2805
|
+
function usesPostCompaction(options: Readonly<Record<string, unknown>>, node: ElkNode): boolean {
|
|
2806
|
+
const active = (values: Readonly<Record<string, unknown>> | undefined) =>
|
|
2807
|
+
Object.entries(values ?? {}).some(
|
|
2808
|
+
([key, value]) => isPostCompactionKey(key) && String(value) !== "NONE",
|
|
2809
|
+
);
|
|
2810
|
+
const visit = (current: ElkNode): boolean =>
|
|
2811
|
+
active(current.layoutOptions) ||
|
|
2812
|
+
active(current.properties as Record<string, unknown> | undefined) ||
|
|
2813
|
+
(current.children ?? []).some(visit);
|
|
2814
|
+
return active(options) || visit(node);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
function disablePostCompaction<T extends ElkNode>(node: T): T {
|
|
2818
|
+
for (const values of [node.layoutOptions, node.properties as Record<string, unknown> | undefined])
|
|
2819
|
+
for (const key of Object.keys(values ?? {}))
|
|
2820
|
+
if (isPostCompactionKey(key)) (values as Record<string, unknown>)[key] = "NONE";
|
|
2821
|
+
for (const child of node.children ?? []) disablePostCompaction(child);
|
|
2822
|
+
return node;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Alternative random seeds worth trying for a compound layout, unless the
|
|
2827
|
+
* graph chooses its hierarchical greedy switch explicitly.
|
|
2828
|
+
*/
|
|
2829
|
+
function crossingVariants(
|
|
2830
|
+
options: Readonly<Record<string, unknown>>,
|
|
2831
|
+
graph: ElkNode,
|
|
2832
|
+
): Array<Record<string, string>> {
|
|
2833
|
+
const own = { ...options, ...graph.properties, ...graph.layoutOptions };
|
|
2834
|
+
const compound = (graph.children ?? []).some((child) => child.children?.length);
|
|
2835
|
+
if (
|
|
2836
|
+
!compound ||
|
|
2837
|
+
getOption(own, "hierarchyHandling") !== "INCLUDE_CHILDREN" ||
|
|
2838
|
+
getOption(own, "layered.crossingMinimization.greedySwitchHierarchical.type") !== undefined
|
|
2839
|
+
)
|
|
2840
|
+
return [];
|
|
2841
|
+
return [{ "elk.randomSeed": "2" }, { "elk.randomSeed": "3" }];
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
/** Whether the graph asks for more than ELK's default layered thoroughness (7). */
|
|
2845
|
+
function thorough(options: Readonly<Record<string, unknown>>, graph: ElkNode): boolean {
|
|
2846
|
+
const own = { ...options, ...graph.properties, ...graph.layoutOptions };
|
|
2847
|
+
return Number(getOption(own, "layered.thoroughness") ?? 7) > 7;
|
|
2848
|
+
}
|