@statelyai/layout 0.2.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 +221 -13
- package/dist/elkjs/index.cjs +3 -2
- package/dist/elkjs/index.d.cts +13 -2
- package/dist/elkjs/index.d.mts +13 -2
- package/dist/elkjs/index.mjs +3 -3
- package/dist/elkjs/main.cjs +2 -2
- package/dist/elkjs/main.d.cts +2 -2
- package/dist/elkjs/main.d.mts +2 -2
- package/dist/elkjs/main.mjs +2 -2
- package/dist/elkjs/worker-api.cjs +1 -1
- package/dist/elkjs/worker-api.d.cts +2 -2
- package/dist/elkjs/worker-api.d.mts +2 -2
- package/dist/elkjs/worker-api.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-B6cWNrMw.d.cts +80 -0
- package/dist/index-CJQLUKD9.d.mts +80 -0
- package/dist/{index-BSH2bloA.d.mts → index-DT6lPMfS.d.cts} +117 -5
- package/dist/{index-CG712jzV.d.cts → index-F1U-fCTu.d.mts} +117 -5
- package/dist/index.cjs +39 -9
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +4 -3
- package/dist/layered/index.cjs +3 -1
- package/dist/layered/index.d.cts +3 -2
- package/dist/layered/index.d.mts +3 -2
- package/dist/layered/index.mjs +2 -2
- package/dist/{layered-Bz2nWmVp.mjs → layered-C7ERzZjR.mjs} +12592 -4682
- package/dist/{layered-Cf9Rn8l7.cjs → layered-CBPg6_6_.cjs} +12711 -4717
- package/dist/{layout-engine-BNex9jxg.cjs → layout-engine-DAw06RhN.cjs} +5 -18
- package/dist/{layout-engine-WbC0YjjT.mjs → layout-engine-DC3IYkIy.mjs} +3 -10
- package/dist/routing/index.cjs +29 -0
- package/dist/routing/index.d.cts +3 -0
- package/dist/routing/index.d.mts +3 -0
- package/dist/routing/index.mjs +3 -0
- package/dist/routing-CR8flv8b.mjs +2780 -0
- package/dist/routing-QIdGGFBe.cjs +2996 -0
- package/dist/types-CT2akdK0.d.mts +133 -0
- package/dist/types-uxPlGRof.d.cts +133 -0
- package/dist/{worker-client-DAT_z2OR.d.mts → worker-client-BwGGRD5y.d.mts} +1 -1
- package/dist/{worker-client-DVhklF6A.d.cts → worker-client-D4zNyPEI.d.cts} +1 -1
- package/package.json +14 -3
- package/src/authoring/coordinates.ts +38 -10
- 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 +1397 -452
- 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/routes.ts +147 -0
- package/src/elkjs/statechart.ts +9 -3
- package/src/elkjs/types.ts +2 -0
- package/src/index.ts +8 -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 +445 -0
- 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 +911 -261
- 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 +34 -0
- package/src/layered/weighted-compaction.ts +84 -0
- package/src/routing/adapters.ts +143 -0
- package/src/routing/algorithms.ts +1088 -0
- package/src/routing/coordination.ts +91 -0
- package/src/routing/engine.ts +473 -0
- package/src/routing/index.ts +69 -0
- package/src/routing/layout-cache.ts +65 -0
- package/src/routing/model.ts +218 -0
- package/src/routing/path.ts +324 -0
- package/src/routing/persistent.ts +117 -0
- package/src/routing/search.ts +358 -0
- package/src/routing/spatial.ts +78 -0
- package/src/routing/types.ts +146 -0
- package/dist/elkjs-BVnDwnBQ.cjs +0 -1551
- package/dist/elkjs-CFlfkEBz.mjs +0 -1528
- /package/dist/{public-types-B8rAALJq.d.mts → public-types-9NCPoqbp.d.mts} +0 -0
- /package/dist/{public-types-CVB0hXGk.d.cts → public-types-BcRZoJE3.d.cts} +0 -0
- /package/dist/{worker-client-DkwS4sHx.mjs → worker-client-BNjwT1AJ.mjs} +0 -0
- /package/dist/{worker-client-D3HMK33r.cjs → worker-client-D4oEvhAl.cjs} +0 -0
package/src/elkjs/index.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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";
|
|
26
|
+
import { createGraph, type Graph, type VisualGraph } from "@statelyai/graph";
|
|
3
27
|
import {
|
|
4
28
|
elkLayeredOptionDefinitions,
|
|
5
29
|
type ElkLayeredOptionValueByName,
|
|
@@ -29,6 +53,7 @@ import type {
|
|
|
29
53
|
} from "./public-types";
|
|
30
54
|
|
|
31
55
|
function isLayoutEdgeLabel(label: ElkLabel): boolean {
|
|
56
|
+
// Unlike ELK, a dimensioned label without text still reserves space: apps often render the text themselves.
|
|
32
57
|
return (
|
|
33
58
|
getBooleanOption(label.layoutOptions ?? {}, "noLayout") !== true &&
|
|
34
59
|
(Boolean(label.text) || label.width !== undefined || label.height !== undefined)
|
|
@@ -58,6 +83,15 @@ export type {
|
|
|
58
83
|
} from "./public-types";
|
|
59
84
|
export type { ElkId, ElkLogging } from "./types";
|
|
60
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
|
+
|
|
61
95
|
export default class ELK {
|
|
62
96
|
readonly #options: ElkConstructorArguments;
|
|
63
97
|
readonly #algorithmIds: ReadonlySet<string>;
|
|
@@ -129,17 +163,74 @@ export default class ELK {
|
|
|
129
163
|
arguments_?: PublicElkLayoutArguments,
|
|
130
164
|
): Promise<PublicElkNode & PublicLaidOutElkNode<T>>;
|
|
131
165
|
layout<T extends ElkNode>(graph: T, arguments_?: ElkLayoutArguments): Promise<LaidOutElkNode<T>>;
|
|
132
|
-
layout<T extends ElkNode>(
|
|
166
|
+
async layout<T extends ElkNode>(
|
|
133
167
|
graph: T,
|
|
134
168
|
arguments_: ElkLayoutArguments = {},
|
|
135
169
|
): Promise<LaidOutElkNode<T>> {
|
|
136
|
-
|
|
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;
|
|
137
217
|
}
|
|
138
218
|
|
|
139
219
|
async #layout<T extends ElkNode>(
|
|
140
220
|
graph: T,
|
|
141
221
|
arguments_: ElkLayoutArguments = {},
|
|
222
|
+
compoundLayout = false,
|
|
142
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> {
|
|
143
234
|
const startedAt = performance.now();
|
|
144
235
|
if (graph === undefined || graph === null) {
|
|
145
236
|
throw new TypeError("Missing mandatory parameter: graph");
|
|
@@ -202,6 +293,8 @@ export default class ELK {
|
|
|
202
293
|
`org.eclipse.elk.core.UnsupportedConfigurationException: Layout algorithm '${requestedAlgorithm}' not found`,
|
|
203
294
|
);
|
|
204
295
|
}
|
|
296
|
+
const preparedChildren = new Map<string, PreparedElkScope>();
|
|
297
|
+
const finishChildren: Array<() => Promise<void>> = [];
|
|
205
298
|
const hasHierarchy = (graph.children ?? []).some((child) => (child.children?.length ?? 0) > 0);
|
|
206
299
|
const insideSelfLoopBaseHeightByNodeId = new Map<string, number>();
|
|
207
300
|
const hierarchyRestorations: Array<{
|
|
@@ -211,10 +304,16 @@ export default class ELK {
|
|
|
211
304
|
source?: ElkId;
|
|
212
305
|
target?: ElkId;
|
|
213
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
|
+
>();
|
|
214
314
|
const syntheticPortIds = new Set<string>();
|
|
215
315
|
const authoredPortsByCompound = new Map<ElkNode, ElkPort[] | undefined>();
|
|
216
316
|
const authoredOptionsByCompound = new Map<ElkNode, Record<string, unknown> | undefined>();
|
|
217
|
-
const hierarchyBoundaryCountByEdge = new Map<ElkEdge, number>();
|
|
218
317
|
const originalHierarchyEndpoints = new Map(
|
|
219
318
|
(graph.edges ?? []).map((edge) => [
|
|
220
319
|
edge,
|
|
@@ -228,7 +327,6 @@ export default class ELK {
|
|
|
228
327
|
},
|
|
229
328
|
]),
|
|
230
329
|
);
|
|
231
|
-
let hasHierarchyCrossingEdges = false;
|
|
232
330
|
const hierarchyHandling = getOption(layoutOptions, "hierarchyHandling");
|
|
233
331
|
const topdownLayout = getBooleanOption(layoutOptions, "topdownLayout") === true;
|
|
234
332
|
const separateHierarchy =
|
|
@@ -297,17 +395,15 @@ export default class ELK {
|
|
|
297
395
|
);
|
|
298
396
|
}
|
|
299
397
|
} else if (hasHierarchy && !separateHierarchy) {
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
398
|
+
// Prepare boundary identities before coordinating parent and child crossing sweeps.
|
|
399
|
+
const scopeSegmentOrder =
|
|
400
|
+
segmentOrderByScope.get(graph) ??
|
|
401
|
+
elkSegmentOrder(graph, graph, originalHierarchyEndpoints, false);
|
|
304
402
|
for (const child of graph.children ?? []) {
|
|
305
403
|
if ((child.children?.length ?? 0) === 0) continue;
|
|
306
|
-
const descendantById = new Map<string, ElkNode>();
|
|
307
404
|
const descendantOwnerByEndpointId = new Map<string, ElkNode>();
|
|
308
405
|
const collectDescendants = (node: ElkNode): void => {
|
|
309
406
|
for (const descendant of node.children ?? []) {
|
|
310
|
-
descendantById.set(String(descendant.id), descendant);
|
|
311
407
|
descendantOwnerByEndpointId.set(String(descendant.id), descendant);
|
|
312
408
|
for (const port of descendant.ports ?? []) {
|
|
313
409
|
descendantOwnerByEndpointId.set(String(port.id), descendant);
|
|
@@ -322,198 +418,611 @@ export default class ELK {
|
|
|
322
418
|
descendantOwnerByEndpointId.has(sourceId) && descendantOwnerByEndpointId.has(targetId)
|
|
323
419
|
);
|
|
324
420
|
});
|
|
325
|
-
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) => {
|
|
326
432
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
327
433
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
328
434
|
const targetInside = descendantOwnerByEndpointId.has(targetId);
|
|
329
435
|
return sourceInside !== targetInside;
|
|
330
436
|
});
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
);
|
|
335
451
|
const mergeHierarchyEdges =
|
|
336
452
|
getBooleanOption(layoutOptions, "layered.mergeHierarchyEdges") !== false;
|
|
337
|
-
//
|
|
338
|
-
// are removed before elkjs serialization and do not alter the exported
|
|
339
|
-
// geometry for a shared hierarchy boundary.
|
|
340
|
-
void mergeHierarchyEdges;
|
|
453
|
+
// ELK merges only edges incident to the same descendant port.
|
|
341
454
|
const proxyByKind = new Map<string, ElkNode>();
|
|
342
|
-
const
|
|
343
|
-
|
|
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 => {
|
|
344
499
|
let proxy = proxyByKind.get(key);
|
|
345
500
|
if (!proxy) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
);
|
|
351
561
|
proxyByKind.set(key, proxy);
|
|
352
562
|
}
|
|
353
563
|
return proxy;
|
|
354
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.
|
|
355
590
|
const temporaryEdges: ElkEdge[] = [
|
|
356
|
-
...(child.edges ?? []),
|
|
357
|
-
...internalEdges.filter(
|
|
358
|
-
(edge) =>
|
|
359
|
-
!(child.edges ?? []).some((candidate) => String(candidate.id) === String(edge.id)),
|
|
360
|
-
),
|
|
361
591
|
...crossingEdges.map((edge) => {
|
|
362
592
|
const { sourceId, targetId } = originalHierarchyEndpoints.get(edge)!;
|
|
363
593
|
const sourceInside = descendantOwnerByEndpointId.has(sourceId);
|
|
364
|
-
const proxy = proxyFor(sourceInside ? "output" : "input");
|
|
365
|
-
|
|
594
|
+
const proxy = proxyFor(sourceInside ? "output" : "input", edge);
|
|
595
|
+
const segment: ElkEdge = {
|
|
366
596
|
...edge,
|
|
367
597
|
id: `__native_hierarchy_edge_${String(child.id)}_${String(edge.id)}`,
|
|
368
|
-
sources: [sourceInside ? sourceId : proxy.id!],
|
|
369
|
-
targets: [sourceInside ? proxy.id! : targetId],
|
|
598
|
+
sources: [sourceInside ? sourceId : proxy.ports![0]!.id!],
|
|
599
|
+
targets: [sourceInside ? proxy.ports![0]!.id! : targetId],
|
|
370
600
|
source: undefined,
|
|
371
601
|
target: undefined,
|
|
372
602
|
sections: undefined,
|
|
373
603
|
};
|
|
604
|
+
originalEdgeId.set(segment, originalEdgeId.get(edge) ?? String(edge.id));
|
|
605
|
+
return segment;
|
|
374
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
|
+
),
|
|
375
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
|
+
}
|
|
376
640
|
const temporaryChild: ElkNode = {
|
|
377
641
|
...child,
|
|
642
|
+
layoutOptions: proxyByKind.size
|
|
643
|
+
? { ...child.layoutOptions, "elk.portConstraints": "FIXED_SIDE" }
|
|
644
|
+
: child.layoutOptions,
|
|
378
645
|
children: [...(child.children ?? []), ...proxyByKind.values()],
|
|
379
646
|
edges: temporaryEdges,
|
|
380
647
|
};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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,
|
|
386
672
|
},
|
|
387
|
-
|
|
388
|
-
measureExecutionTime: false,
|
|
389
|
-
});
|
|
390
|
-
const childPadding = parsePadding(
|
|
391
|
-
getOption({ ...layoutOptions, ...child.layoutOptions }, "padding"),
|
|
392
|
-
12,
|
|
673
|
+
true,
|
|
393
674
|
);
|
|
394
|
-
|
|
395
|
-
const direction = getDirection(layoutOptions);
|
|
396
|
-
const horizontal = direction === "right" || direction === "left";
|
|
397
|
-
const increasing = direction === "right" || direction === "down";
|
|
398
|
-
if (increasing) {
|
|
399
|
-
const minimumFlow = Math.min(
|
|
400
|
-
...(child.children ?? []).map((node) => (horizontal ? (node.x ?? 0) : (node.y ?? 0))),
|
|
401
|
-
);
|
|
402
|
-
const desiredFlow = horizontal ? childPadding.left : childPadding.top;
|
|
403
|
-
const delta = desiredFlow - minimumFlow;
|
|
404
|
-
for (const node of child.children ?? []) {
|
|
405
|
-
if (horizontal) node.x = (node.x ?? 0) + delta;
|
|
406
|
-
else node.y = (node.y ?? 0) + delta;
|
|
407
|
-
}
|
|
408
|
-
for (const edge of temporaryEdges) {
|
|
409
|
-
for (const section of edge.sections ?? []) {
|
|
410
|
-
for (const point of [
|
|
411
|
-
section.startPoint,
|
|
412
|
-
...(section.bendPoints ?? []),
|
|
413
|
-
section.endPoint,
|
|
414
|
-
]) {
|
|
415
|
-
if (horizontal) point.x += delta;
|
|
416
|
-
else point.y += delta;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
for (const internalEdge of internalEdges) {
|
|
423
|
-
const temporary = temporaryEdges.find(
|
|
424
|
-
(candidate) => String(candidate.id) === String(internalEdge.id),
|
|
425
|
-
);
|
|
426
|
-
if (temporary?.sections) internalEdge.sections = temporary.sections;
|
|
427
|
-
}
|
|
428
|
-
child.width =
|
|
429
|
-
Math.max(0, ...(child.children ?? []).map((node) => (node.x ?? 0) + (node.width ?? 0))) +
|
|
430
|
-
childPadding.right;
|
|
431
|
-
child.height =
|
|
432
|
-
Math.max(0, ...(child.children ?? []).map((node) => (node.y ?? 0) + (node.height ?? 0))) +
|
|
433
|
-
childPadding.bottom;
|
|
434
|
-
|
|
435
|
-
const relativeRect = (id: string): ElkShape | undefined => {
|
|
436
|
-
const owner = descendantOwnerByEndpointId.get(id);
|
|
437
|
-
if (!owner) return undefined;
|
|
438
|
-
const path: ElkNode[] = [];
|
|
439
|
-
const visit = (parent: ElkNode): boolean => {
|
|
440
|
-
for (const candidate of parent.children ?? []) {
|
|
441
|
-
path.push(candidate);
|
|
442
|
-
if (candidate === owner || visit(candidate)) return true;
|
|
443
|
-
path.pop();
|
|
444
|
-
}
|
|
445
|
-
return false;
|
|
446
|
-
};
|
|
447
|
-
if (!visit(child)) return undefined;
|
|
448
|
-
const port = owner.ports?.find((candidate) => String(candidate.id) === id);
|
|
449
|
-
const ownerX = path.reduce((sum, node) => sum + (node.x ?? 0), 0);
|
|
450
|
-
const ownerY = path.reduce((sum, node) => sum + (node.y ?? 0), 0);
|
|
451
|
-
if (port) {
|
|
452
|
-
return {
|
|
453
|
-
x: ownerX + (port.x ?? 0) + (port.width ?? 0) / 2,
|
|
454
|
-
y: ownerY + (port.y ?? 0) + (port.height ?? 0) / 2,
|
|
455
|
-
width: 0,
|
|
456
|
-
height: 0,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
x: ownerX,
|
|
461
|
-
y: ownerY,
|
|
462
|
-
width: path.at(-1)?.width ?? 0,
|
|
463
|
-
height: path.at(-1)?.height ?? 0,
|
|
464
|
-
};
|
|
465
|
-
};
|
|
675
|
+
preparedChildren.set(String(child.id), preparedChild);
|
|
466
676
|
authoredPortsByCompound.set(child, child.ports);
|
|
467
677
|
authoredOptionsByCompound.set(child, child.layoutOptions);
|
|
468
|
-
const
|
|
678
|
+
const initialPorts = (child.ports ?? []).map((port) => ({
|
|
679
|
+
...port,
|
|
680
|
+
layoutOptions: port.layoutOptions ? { ...port.layoutOptions } : undefined,
|
|
681
|
+
}));
|
|
469
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
|
+
}
|
|
470
698
|
const original = originalHierarchyEndpoints.get(edge)!;
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
if (!rect) continue;
|
|
476
|
-
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`;
|
|
477
703
|
syntheticPortIds.add(portId);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
direction === "down" || direction === "up"
|
|
492
|
-
? (rect.y ?? 0) + (useFarSide ? (rect.height ?? 0) : 0)
|
|
493
|
-
: (rect.y ?? 0) + (rect.height ?? 0) / 2,
|
|
494
|
-
});
|
|
495
|
-
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))
|
|
496
717
|
hierarchyRestorations.push({ edge, ...original });
|
|
497
|
-
}
|
|
498
718
|
if (sourceInside) edge.sources = [portId];
|
|
499
719
|
else edge.targets = [portId];
|
|
720
|
+
const rank = scopeSegmentOrder.get(originalEdgeId.get(edge) ?? String(edge.id));
|
|
721
|
+
if (rank !== undefined) hierarchySegmentRank.set(edge, rank);
|
|
500
722
|
edge.source = undefined;
|
|
501
723
|
edge.target = undefined;
|
|
502
724
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
};
|
|
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;
|
|
512
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
|
+
});
|
|
513
1017
|
}
|
|
514
1018
|
}
|
|
515
1019
|
applyNodeMicroLayout(graph, layoutOptions);
|
|
516
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
|
+
);
|
|
517
1026
|
const layerConstraintByNodeId = new Map(
|
|
518
1027
|
(graph.children ?? []).map((node) => [
|
|
519
1028
|
String(node.id),
|
|
@@ -528,6 +1037,10 @@ export default class ELK {
|
|
|
528
1037
|
algorithm === "layered" &&
|
|
529
1038
|
graph_.edges.some((edge) => {
|
|
530
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;
|
|
531
1044
|
const sourceConstraint = layerConstraintByNodeId.get(edge.sourceId);
|
|
532
1045
|
const targetConstraint = layerConstraintByNodeId.get(edge.targetId);
|
|
533
1046
|
return (
|
|
@@ -544,307 +1057,471 @@ export default class ELK {
|
|
|
544
1057
|
getOption(layoutOptions, "padding"),
|
|
545
1058
|
algorithm === "layered" ? 12 : 0,
|
|
546
1059
|
);
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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"
|
|
557
1176
|
? executeElkjs0111Layout({
|
|
558
|
-
algorithm: "
|
|
1177
|
+
algorithm: "sporeCompaction",
|
|
559
1178
|
graph: graph_,
|
|
560
1179
|
options: {
|
|
561
1180
|
padding,
|
|
562
1181
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
563
1182
|
},
|
|
564
1183
|
})
|
|
565
|
-
: algorithm === "
|
|
1184
|
+
: algorithm === "sporeOverlap"
|
|
566
1185
|
? executeElkjs0111Layout({
|
|
567
|
-
algorithm: "
|
|
1186
|
+
algorithm: "sporeOverlap",
|
|
568
1187
|
graph: graph_,
|
|
569
1188
|
options: {
|
|
570
|
-
padding
|
|
571
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode")
|
|
1189
|
+
padding,
|
|
1190
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
572
1191
|
},
|
|
573
1192
|
})
|
|
574
|
-
: algorithm === "
|
|
1193
|
+
: algorithm === "rectpacking"
|
|
575
1194
|
? executeElkjs0111Layout({
|
|
576
|
-
algorithm: "
|
|
1195
|
+
algorithm: "rectpacking",
|
|
577
1196
|
graph: graph_,
|
|
578
1197
|
options: {
|
|
579
1198
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
580
|
-
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
581
|
-
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
582
|
-
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
1199
|
+
spacing: getNumberOption(layoutOptions, "spacing.nodeNode") ?? 15,
|
|
583
1200
|
},
|
|
584
1201
|
})
|
|
585
|
-
: algorithm === "
|
|
1202
|
+
: algorithm === "random"
|
|
586
1203
|
? executeElkjs0111Layout({
|
|
587
|
-
algorithm: "
|
|
1204
|
+
algorithm: "random",
|
|
588
1205
|
graph: graph_,
|
|
589
1206
|
options: {
|
|
590
1207
|
padding: getOption(layoutOptions, "padding") === undefined ? 15 : padding,
|
|
591
1208
|
spacing: getNumberOption(layoutOptions, "spacing.nodeNode"),
|
|
592
1209
|
aspectRatio: getNumberOption(layoutOptions, "aspectRatio"),
|
|
593
|
-
|
|
594
|
-
expandNodes: getBooleanOption(layoutOptions, "expandNodes"),
|
|
595
|
-
priority: (node) => {
|
|
596
|
-
const child = graph.children?.find(
|
|
597
|
-
(candidate) => String(candidate.id) === node.id,
|
|
598
|
-
);
|
|
599
|
-
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
600
|
-
},
|
|
1210
|
+
seed: getNumberOption(layoutOptions, "randomSeed"),
|
|
601
1211
|
},
|
|
602
1212
|
})
|
|
603
|
-
: algorithm === "
|
|
1213
|
+
: algorithm === "box"
|
|
604
1214
|
? executeElkjs0111Layout({
|
|
605
|
-
algorithm: "
|
|
606
|
-
graph: graph_,
|
|
607
|
-
options: { direction: getDirection(layoutOptions) },
|
|
608
|
-
})
|
|
609
|
-
: executeElkjs0111Layout({
|
|
610
|
-
algorithm: "layered",
|
|
1215
|
+
algorithm: "box",
|
|
611
1216
|
graph: graph_,
|
|
612
1217
|
options: {
|
|
613
|
-
|
|
614
|
-
spacing:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
(getNumberOption(
|
|
620
|
-
layoutOptions,
|
|
621
|
-
"layered.spacing.nodeNodeBetweenLayers",
|
|
622
|
-
) ??
|
|
623
|
-
getNumberOption(layoutOptions, "layered.spacing.baseValue") ??
|
|
624
|
-
20) +
|
|
625
|
-
(hasHierarchyCrossingEdges
|
|
626
|
-
? 5 * Math.max(...hierarchyBoundaryCountByEdge.values())
|
|
627
|
-
: 0),
|
|
628
|
-
},
|
|
629
|
-
padding,
|
|
630
|
-
constraints: {
|
|
631
|
-
layer: () => undefined,
|
|
632
|
-
},
|
|
633
|
-
settings: {
|
|
634
|
-
...getLayeredSettings(layoutOptions),
|
|
635
|
-
...(hierarchyHandling === "INCLUDE_CHILDREN" &&
|
|
636
|
-
getOption(
|
|
637
|
-
layoutOptions,
|
|
638
|
-
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
639
|
-
) !== undefined
|
|
640
|
-
? {
|
|
641
|
-
"crossingMinimization.greedySwitch.type": String(
|
|
642
|
-
getOption(
|
|
643
|
-
layoutOptions,
|
|
644
|
-
"layered.crossingMinimization.greedySwitchHierarchical.type",
|
|
645
|
-
),
|
|
646
|
-
) as LayeredAdvancedOptions["crossingMinimization.greedySwitch.type"],
|
|
647
|
-
}
|
|
648
|
-
: {}),
|
|
649
|
-
},
|
|
650
|
-
nodeSettings: (node) => {
|
|
651
|
-
const child = graph.children?.find(
|
|
652
|
-
(candidate) => String(candidate.id) === node.id,
|
|
653
|
-
);
|
|
654
|
-
return getElementLayeredSettings(child?.layoutOptions ?? {});
|
|
655
|
-
},
|
|
656
|
-
edgeSettings: (edge) => {
|
|
657
|
-
const elkEdge = graph.edges?.find(
|
|
658
|
-
(candidate) => String(candidate.id) === edge.id,
|
|
659
|
-
);
|
|
660
|
-
return {
|
|
661
|
-
...getElementLayeredSettings(elkEdge?.layoutOptions ?? {}),
|
|
662
|
-
...getElementLayeredSettings(elkEdge?.labels?.[0]?.layoutOptions ?? {}),
|
|
663
|
-
};
|
|
664
|
-
},
|
|
665
|
-
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) => {
|
|
666
1224
|
const child = graph.children?.find(
|
|
667
1225
|
(candidate) => String(candidate.id) === node.id,
|
|
668
1226
|
);
|
|
669
|
-
|
|
670
|
-
(candidate) => String(candidate.id) === port.name,
|
|
671
|
-
);
|
|
672
|
-
return {
|
|
673
|
-
...getElementLayeredSettings(elkPort?.layoutOptions ?? {}),
|
|
674
|
-
"port.labelWidth": Math.max(
|
|
675
|
-
0,
|
|
676
|
-
...(elkPort?.labels ?? []).map((label) => label.width ?? 0),
|
|
677
|
-
),
|
|
678
|
-
"port.labelHeight": Math.max(
|
|
679
|
-
0,
|
|
680
|
-
...(elkPort?.labels ?? []).map((label) => label.height ?? 0),
|
|
681
|
-
),
|
|
682
|
-
} as ElkLayeredOptionValueByName;
|
|
1227
|
+
return getNumberOption(child?.layoutOptions ?? {}, "priority");
|
|
683
1228
|
},
|
|
684
1229
|
},
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const originalTargetId = String(restoration.targets?.[0] ?? restoration.target);
|
|
697
|
-
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
698
|
-
const outsideId = sourceInside ? originalTargetId : originalSourceId;
|
|
699
|
-
const delta = sourceInside
|
|
700
|
-
? cross(route[0]!) - cross(route.at(-1)!)
|
|
701
|
-
: cross(route.at(-1)!) - cross(route[0]!);
|
|
702
|
-
const candidates = shiftsByOutsideId.get(outsideId) ?? [];
|
|
703
|
-
candidates.push(delta);
|
|
704
|
-
shiftsByOutsideId.set(outsideId, candidates);
|
|
705
|
-
}
|
|
706
|
-
for (const [outsideId, candidates] of shiftsByOutsideId) {
|
|
707
|
-
const delta = [...candidates].sort((left, right) => Math.abs(left) - Math.abs(right))[0]!;
|
|
708
|
-
const node = laidOut.nodes.find((candidate) => candidate.id === outsideId);
|
|
709
|
-
if (!node || Math.abs(delta) < 1e-9 || Math.abs(delta) > nodeSpacing) continue;
|
|
710
|
-
if (horizontal) node.y = (node.y ?? 0) + delta;
|
|
711
|
-
else node.x = (node.x ?? 0) + delta;
|
|
712
|
-
for (const edge of laidOut.edges) {
|
|
713
|
-
const points = edge.points;
|
|
714
|
-
if (!points || points.length === 0) continue;
|
|
715
|
-
if (edge.sourceId === outsideId) {
|
|
716
|
-
if (horizontal) points[0]!.y += delta;
|
|
717
|
-
else points[0]!.x += delta;
|
|
718
|
-
}
|
|
719
|
-
if (edge.targetId === outsideId) {
|
|
720
|
-
if (horizontal) points.at(-1)!.y += delta;
|
|
721
|
-
else points.at(-1)!.x += delta;
|
|
722
|
-
}
|
|
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;
|
|
723
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;
|
|
724
1247
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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,
|
|
747
1279
|
);
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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;
|
|
764
1317
|
}
|
|
765
|
-
crossEnd = compactedCross + (horizontal ? node.height : node.width);
|
|
766
1318
|
}
|
|
767
1319
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (nativeEdge.sourceId === nativeEdge.targetId) continue;
|
|
779
|
-
if (Math.abs(cross(start) - cross(end)) < 1e-9) {
|
|
780
|
-
route.splice(1, route.length - 2);
|
|
781
|
-
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
|
+
});
|
|
782
1330
|
}
|
|
783
|
-
|
|
784
|
-
const sourceInside = !laidOut.nodes.some((node) => node.id === originalSourceId);
|
|
785
|
-
const flowForward = direction === "right" || direction === "down";
|
|
786
|
-
const track = sourceInside
|
|
787
|
-
? (horizontal ? end.x : end.y) - (flowForward ? edgeNodeSpacing : -edgeNodeSpacing)
|
|
788
|
-
: (horizontal ? start.x : start.y) + (flowForward ? edgeNodeSpacing : -edgeNodeSpacing);
|
|
789
|
-
route.splice(
|
|
790
|
-
1,
|
|
791
|
-
route.length - 2,
|
|
792
|
-
horizontal ? { x: track, y: start.y } : { x: start.x, y: track },
|
|
793
|
-
horizontal ? { x: track, y: end.y } : { x: end.x, y: track },
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
const resultPolicy = (
|
|
798
|
-
laidOut as typeof laidOut & {
|
|
799
|
-
[elkjs0111ResultPolicy]?: Elkjs0111ResultPolicy;
|
|
1331
|
+
compound.ports = ports;
|
|
800
1332
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
section.incomingShape = restoration.sources?.[0] ?? restoration.source;
|
|
815
|
-
}
|
|
816
|
-
if (section.outgoingShape != null && syntheticPortIds.has(String(section.outgoingShape))) {
|
|
817
|
-
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;
|
|
818
1346
|
}
|
|
819
1347
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
logging: false,
|
|
830
|
-
measureExecutionTime: false,
|
|
831
|
-
});
|
|
832
|
-
child.x = position.x;
|
|
833
|
-
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
|
+
};
|
|
834
1357
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
+
},
|
|
1512
|
+
};
|
|
1513
|
+
childScope.publishBottomUp = () => {
|
|
1514
|
+
childScope.publishBoundary!(true);
|
|
1515
|
+
childScope.publishBoundary!(false);
|
|
1516
|
+
parentScope.session.lockPortOrder(id);
|
|
844
1517
|
};
|
|
1518
|
+
scopes.set(child, childScope);
|
|
1519
|
+
(parentScope.childrenByNodeId as Map<string, HierarchyCrossingScope>).set(id, childScope);
|
|
845
1520
|
}
|
|
846
|
-
return graph as LaidOutElkNode<T>;
|
|
847
1521
|
}
|
|
1522
|
+
const orders = minimizeHierarchyCrossings(scopes.get(root)!);
|
|
1523
|
+
|
|
1524
|
+
return new Map(prepared.map((scope) => [scope, orders.get(scopes.get(scope)!)!]));
|
|
848
1525
|
}
|
|
849
1526
|
|
|
850
1527
|
function parsePadding(value: unknown, fallback = 0) {
|
|
@@ -1084,16 +1761,56 @@ function parseMargin(value: unknown): unknown {
|
|
|
1084
1761
|
return parsePadding(value, 0);
|
|
1085
1762
|
}
|
|
1086
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
|
+
|
|
1087
1798
|
function getElementLayeredSettings(
|
|
1088
1799
|
options: Readonly<Record<string, unknown>>,
|
|
1089
1800
|
): ElkLayeredOptionValueByName {
|
|
1090
1801
|
const settings: ElkLayeredOptionValueByName = {};
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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) {
|
|
1097
1814
|
const vectorMatch =
|
|
1098
1815
|
definition.name === "port.anchor" && typeof value === "string"
|
|
1099
1816
|
? value.match(/^\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)\s*$/)
|
|
@@ -1261,7 +1978,16 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1261
1978
|
if (String(getOption(child.layoutOptions ?? {}, "portConstraints")) !== "FIXED_SIDE") {
|
|
1262
1979
|
return ports;
|
|
1263
1980
|
}
|
|
1264
|
-
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";
|
|
1265
1991
|
const sideOrder = ["NORTH", "EAST", "SOUTH", "WEST"];
|
|
1266
1992
|
ports.sort((left, right) => {
|
|
1267
1993
|
const leftSide = side(left);
|
|
@@ -1288,10 +2014,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1288
2014
|
const usePortModelOrder =
|
|
1289
2015
|
getBooleanOption(globalOptions, "layered.considerModelOrder.portModelOrder") === true;
|
|
1290
2016
|
if (modelOrderStrategy !== "NONE" && !usePortModelOrder) {
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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);
|
|
1295
2023
|
}
|
|
1296
2024
|
const direction = leftSide === "WEST" ? -1 : 1;
|
|
1297
2025
|
return direction * ((child.ports?.indexOf(left) ?? 0) - (child.ports?.indexOf(right) ?? 0));
|
|
@@ -1299,7 +2027,7 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1299
2027
|
return ports;
|
|
1300
2028
|
};
|
|
1301
2029
|
|
|
1302
|
-
|
|
2030
|
+
const native = createGraph({
|
|
1303
2031
|
id: String(root.id),
|
|
1304
2032
|
nodes: children.map((child) => ({
|
|
1305
2033
|
id: String(child.id),
|
|
@@ -1354,6 +2082,12 @@ function toGraph(root: ElkNode, globalOptions: Readonly<Record<string, unknown>>
|
|
|
1354
2082
|
: [];
|
|
1355
2083
|
}),
|
|
1356
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;
|
|
1357
2091
|
}
|
|
1358
2092
|
|
|
1359
2093
|
function parsePoints(value: unknown): ElkPoint[] | undefined {
|
|
@@ -1388,7 +2122,7 @@ function toSection(edge: ElkEdge, points: readonly ElkPoint[]) {
|
|
|
1388
2122
|
|
|
1389
2123
|
function applyLayout(
|
|
1390
2124
|
root: ElkNode,
|
|
1391
|
-
graph:
|
|
2125
|
+
graph: VisualGraph,
|
|
1392
2126
|
padding: { top: number; right: number; bottom: number; left: number },
|
|
1393
2127
|
layoutOptions: Readonly<Record<string, unknown>>,
|
|
1394
2128
|
resultPolicy?: Elkjs0111ResultPolicy,
|
|
@@ -1432,11 +2166,21 @@ function applyLayout(
|
|
|
1432
2166
|
continue;
|
|
1433
2167
|
}
|
|
1434
2168
|
const section = getParentEdgeSection(root, edge);
|
|
1435
|
-
if (section)
|
|
2169
|
+
if (section) {
|
|
2170
|
+
edge.sections = [section];
|
|
2171
|
+
edge.container = root.id;
|
|
2172
|
+
}
|
|
1436
2173
|
continue;
|
|
1437
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
|
+
}
|
|
1438
2181
|
const section = toSection(edge, laidOutEdge.points ?? []);
|
|
1439
2182
|
edge.sections = section ? [section] : [];
|
|
2183
|
+
if (section) edge.container = root.id;
|
|
1440
2184
|
const target = root.children?.find(
|
|
1441
2185
|
(child) => String(child.id) === String(edge.targets?.[0] ?? edge.target),
|
|
1442
2186
|
);
|
|
@@ -1453,6 +2197,8 @@ function applyLayout(
|
|
|
1453
2197
|
const midpointX = (laidOutEdge.x ?? 0) + (laidOutEdge.width ?? 0) / 2;
|
|
1454
2198
|
const edgeLabelSpacing = getNumberOption(layoutOptions, "spacing.edgeLabel") ?? 2;
|
|
1455
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;
|
|
1456
2202
|
for (const label of edge.labels ?? []) {
|
|
1457
2203
|
if (getBooleanOption(label.layoutOptions ?? {}, "noLayout") === true) {
|
|
1458
2204
|
label.x ??= 0;
|
|
@@ -1469,17 +2215,51 @@ function applyLayout(
|
|
|
1469
2215
|
);
|
|
1470
2216
|
const width = label.width ?? 0;
|
|
1471
2217
|
label.x =
|
|
1472
|
-
placement
|
|
1473
|
-
?
|
|
1474
|
-
: placement === "
|
|
1475
|
-
?
|
|
1476
|
-
:
|
|
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;
|
|
1477
2225
|
label.y = labelY;
|
|
1478
2226
|
labelY += (label.height ?? 0) + labelLabelSpacing;
|
|
1479
2227
|
}
|
|
1480
2228
|
}
|
|
1481
2229
|
if (!resultPolicy?.skipBoundsNormalization) {
|
|
1482
|
-
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
|
+
);
|
|
1483
2263
|
}
|
|
1484
2264
|
}
|
|
1485
2265
|
|
|
@@ -1487,15 +2267,21 @@ function normalizeElkGraphBounds(
|
|
|
1487
2267
|
root: ElkNode,
|
|
1488
2268
|
padding: { top: number; right: number; bottom: number; left: number },
|
|
1489
2269
|
layoutOptions: Readonly<Record<string, unknown>> = {},
|
|
2270
|
+
compactionBounds?: Elkjs0111ResultPolicy["normalizationBounds"],
|
|
2271
|
+
placementCrossBounds?: Elkjs0111ResultPolicy["placementCrossBounds"],
|
|
1490
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
|
+
);
|
|
1491
2277
|
const authoredWidth = root.width;
|
|
1492
2278
|
const authoredHeight = root.height;
|
|
1493
2279
|
const fixedGraphSize = getBooleanOption(layoutOptions, "nodeSize.fixedGraphSize") === true;
|
|
1494
2280
|
const layoutChildren = (root.children ?? []).filter(
|
|
1495
2281
|
(node) => getBooleanOption(node.layoutOptions ?? {}, "noLayout") !== true,
|
|
1496
2282
|
);
|
|
1497
|
-
let minimumX = Number.POSITIVE_INFINITY;
|
|
1498
|
-
let minimumY = Number.POSITIVE_INFINITY;
|
|
2283
|
+
let minimumX = compactionBounds?.left ?? Number.POSITIVE_INFINITY;
|
|
2284
|
+
let minimumY = compactionBounds?.top ?? Number.POSITIVE_INFINITY;
|
|
1499
2285
|
for (const node of layoutChildren) {
|
|
1500
2286
|
minimumX = Math.min(
|
|
1501
2287
|
minimumX,
|
|
@@ -1516,7 +2302,7 @@ function normalizeElkGraphBounds(
|
|
|
1516
2302
|
),
|
|
1517
2303
|
);
|
|
1518
2304
|
}
|
|
1519
|
-
for (const edge of
|
|
2305
|
+
for (const edge of layoutEdges) {
|
|
1520
2306
|
const laidOutLabels = (edge.labels ?? []).filter(isLayoutEdgeLabel);
|
|
1521
2307
|
minimumX = Math.min(minimumX, ...laidOutLabels.map((label) => label.x ?? 0));
|
|
1522
2308
|
minimumY = Math.min(minimumY, ...laidOutLabels.map((label) => label.y ?? 0));
|
|
@@ -1537,13 +2323,17 @@ function normalizeElkGraphBounds(
|
|
|
1537
2323
|
node.x = (node.x ?? 0) + shiftX;
|
|
1538
2324
|
node.y = (node.y ?? 0) + shiftY;
|
|
1539
2325
|
}
|
|
1540
|
-
for (const edge of
|
|
2326
|
+
for (const edge of layoutEdges) {
|
|
1541
2327
|
for (const section of edge.sections ?? []) {
|
|
1542
2328
|
for (const point of [section.startPoint, ...(section.bendPoints ?? []), section.endPoint]) {
|
|
1543
2329
|
point.x += shiftX;
|
|
1544
2330
|
point.y += shiftY;
|
|
1545
2331
|
}
|
|
1546
2332
|
}
|
|
2333
|
+
for (const point of edge.junctionPoints ?? []) {
|
|
2334
|
+
point.x += shiftX;
|
|
2335
|
+
point.y += shiftY;
|
|
2336
|
+
}
|
|
1547
2337
|
for (const label of (edge.labels ?? []).filter(isLayoutEdgeLabel)) {
|
|
1548
2338
|
label.x = (label.x ?? 0) + shiftX;
|
|
1549
2339
|
label.y = (label.y ?? 0) + shiftY;
|
|
@@ -1558,7 +2348,7 @@ function normalizeElkGraphBounds(
|
|
|
1558
2348
|
0,
|
|
1559
2349
|
...layoutChildren.map((node) => (node.y ?? 0) + (node.height ?? 0)),
|
|
1560
2350
|
);
|
|
1561
|
-
const layoutEdgePoints =
|
|
2351
|
+
const layoutEdgePoints = layoutEdges.flatMap((edge) =>
|
|
1562
2352
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1563
2353
|
? []
|
|
1564
2354
|
: (edge.sections ?? []).flatMap((section) => [
|
|
@@ -1574,7 +2364,7 @@ function normalizeElkGraphBounds(
|
|
|
1574
2364
|
for (const child of root.children ?? []) {
|
|
1575
2365
|
for (const port of child.ports ?? []) childByEndpointId.set(String(port.id), child);
|
|
1576
2366
|
}
|
|
1577
|
-
const wrappedEdgeCount =
|
|
2367
|
+
const wrappedEdgeCount = layoutEdges.filter((edge) => {
|
|
1578
2368
|
const source = childByEndpointId.get(String(edge.sources?.[0] ?? edge.source));
|
|
1579
2369
|
const target = childByEndpointId.get(String(edge.targets?.[0] ?? edge.target));
|
|
1580
2370
|
if (!source || !target) return false;
|
|
@@ -1587,24 +2377,44 @@ function normalizeElkGraphBounds(
|
|
|
1587
2377
|
: (source.y ?? 0) < (target.y ?? 0);
|
|
1588
2378
|
}).length;
|
|
1589
2379
|
const hasWrappedEdge = wrappedEdgeCount > 0;
|
|
1590
|
-
const
|
|
2380
|
+
const retainPlacementBounds =
|
|
1591
2381
|
wrappingStrategy !== "MULTI_EDGE" &&
|
|
1592
2382
|
!(wrappingStrategy === "SINGLE_EDGE" && hasWrappedEdge) &&
|
|
1593
2383
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") ===
|
|
1594
2384
|
"NONE" &&
|
|
1595
2385
|
getBooleanOption(layoutOptions, "layered.feedbackEdges") !== true &&
|
|
1596
2386
|
String(getOption(layoutOptions, "layered.layering.nodePromotion.strategy") ?? "NONE") !==
|
|
1597
|
-
"MODEL_ORDER_LEFT_TO_RIGHT"
|
|
1598
|
-
|
|
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 &&
|
|
1599
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);
|
|
1600
2406
|
const edgeBoundsExtraX =
|
|
1601
2407
|
addBoundaryPixel &&
|
|
2408
|
+
placementCrossBounds?.axis !== "x" &&
|
|
2409
|
+
!(hasSelfLoops && (direction === "right" || direction === "left")) &&
|
|
1602
2410
|
layoutEdgePoints.length > 0 &&
|
|
1603
2411
|
Math.max(...layoutEdgePoints.map((point) => point.x)) >= maximumNodeX - 1e-9
|
|
1604
2412
|
? 1
|
|
1605
2413
|
: 0;
|
|
1606
2414
|
const edgeBoundsExtraY =
|
|
1607
2415
|
addBoundaryPixel &&
|
|
2416
|
+
placementCrossBounds?.axis !== "y" &&
|
|
2417
|
+
!(hasSelfLoops && (direction === "down" || direction === "up")) &&
|
|
1608
2418
|
layoutEdgePoints.length > 0 &&
|
|
1609
2419
|
Math.max(...layoutEdgePoints.map((point) => point.y)) >= maximumNodeY - 1e-9
|
|
1610
2420
|
? 1
|
|
@@ -1617,6 +2427,7 @@ function normalizeElkGraphBounds(
|
|
|
1617
2427
|
? 1
|
|
1618
2428
|
: 0;
|
|
1619
2429
|
const postCompactionBoundsExtraX =
|
|
2430
|
+
!compactionBounds &&
|
|
1620
2431
|
!addBoundaryPixel &&
|
|
1621
2432
|
!hasWrappedEdge &&
|
|
1622
2433
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1626,6 +2437,7 @@ function normalizeElkGraphBounds(
|
|
|
1626
2437
|
? 0.04
|
|
1627
2438
|
: 0;
|
|
1628
2439
|
const postCompactionBoundsExtraY =
|
|
2440
|
+
!compactionBounds &&
|
|
1629
2441
|
!addBoundaryPixel &&
|
|
1630
2442
|
!hasWrappedEdge &&
|
|
1631
2443
|
String(getOption(layoutOptions, "layered.compaction.postCompaction.strategy") ?? "NONE") !==
|
|
@@ -1636,7 +2448,10 @@ function normalizeElkGraphBounds(
|
|
|
1636
2448
|
: 0;
|
|
1637
2449
|
const calculatedWidth =
|
|
1638
2450
|
Math.max(
|
|
1639
|
-
0,
|
|
2451
|
+
compactionBounds ? compactionBounds.right + shiftX : 0,
|
|
2452
|
+
retainPlacementBounds && placementCrossBounds?.axis === "x"
|
|
2453
|
+
? placementCrossBounds.maximum + shiftX
|
|
2454
|
+
: 0,
|
|
1640
2455
|
...layoutChildren.flatMap((node) => [
|
|
1641
2456
|
(node.x ?? 0) + (node.width ?? 0),
|
|
1642
2457
|
...(node.labels ?? []).map((label) => (node.x ?? 0) + (label.x ?? 0) + (label.width ?? 0)),
|
|
@@ -1647,23 +2462,24 @@ function normalizeElkGraphBounds(
|
|
|
1647
2462
|
),
|
|
1648
2463
|
),
|
|
1649
2464
|
]),
|
|
1650
|
-
...
|
|
2465
|
+
...layoutEdges.flatMap((edge) =>
|
|
1651
2466
|
(edge.labels ?? [])
|
|
1652
2467
|
.filter(isLayoutEdgeLabel)
|
|
1653
2468
|
.map((label) => (label.x ?? 0) + (label.width ?? 0)),
|
|
1654
2469
|
),
|
|
1655
|
-
...
|
|
2470
|
+
...layoutEdges.flatMap((edge) =>
|
|
1656
2471
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1657
2472
|
? []
|
|
1658
2473
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1659
|
-
section.startPoint.x,
|
|
1660
|
-
...(section.bendPoints ?? []).map(
|
|
1661
|
-
|
|
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),
|
|
1662
2479
|
]),
|
|
1663
2480
|
),
|
|
1664
2481
|
) +
|
|
1665
2482
|
padding.right +
|
|
1666
|
-
edgeBoundsExtraX +
|
|
1667
2483
|
postCompactionBoundsExtraX +
|
|
1668
2484
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
1669
2485
|
(getDirection(layoutOptions) === "down" || getDirection(layoutOptions) === "up")
|
|
@@ -1671,7 +2487,10 @@ function normalizeElkGraphBounds(
|
|
|
1671
2487
|
: 0);
|
|
1672
2488
|
const calculatedHeight =
|
|
1673
2489
|
Math.max(
|
|
1674
|
-
0,
|
|
2490
|
+
compactionBounds ? compactionBounds.bottom + shiftY : 0,
|
|
2491
|
+
retainPlacementBounds && placementCrossBounds?.axis === "y"
|
|
2492
|
+
? placementCrossBounds.maximum + shiftY
|
|
2493
|
+
: 0,
|
|
1675
2494
|
...layoutChildren.flatMap((node) => [
|
|
1676
2495
|
(node.y ?? 0) + (node.height ?? 0),
|
|
1677
2496
|
...(node.labels ?? []).map((label) => (node.y ?? 0) + (label.y ?? 0) + (label.height ?? 0)),
|
|
@@ -1682,7 +2501,7 @@ function normalizeElkGraphBounds(
|
|
|
1682
2501
|
),
|
|
1683
2502
|
),
|
|
1684
2503
|
]),
|
|
1685
|
-
...
|
|
2504
|
+
...layoutEdges.flatMap((edge) =>
|
|
1686
2505
|
(edge.labels ?? [])
|
|
1687
2506
|
.filter(isLayoutEdgeLabel)
|
|
1688
2507
|
.map(
|
|
@@ -1690,23 +2509,26 @@ function normalizeElkGraphBounds(
|
|
|
1690
2509
|
(label.y ?? 0) +
|
|
1691
2510
|
(label.height ?? 0) +
|
|
1692
2511
|
(String(getOption(label.layoutOptions ?? {}, "edgeLabels.placement") ?? "CENTER") ===
|
|
1693
|
-
|
|
2512
|
+
"CENTER" &&
|
|
2513
|
+
childByEndpointId.get(String(edge.sources?.[0] ?? edge.source)) !==
|
|
2514
|
+
childByEndpointId.get(String(edge.targets?.[0] ?? edge.target))
|
|
1694
2515
|
? 1
|
|
1695
2516
|
: 0),
|
|
1696
2517
|
),
|
|
1697
2518
|
),
|
|
1698
|
-
...
|
|
2519
|
+
...layoutEdges.flatMap((edge) =>
|
|
1699
2520
|
getBooleanOption(edge.layoutOptions ?? {}, "noLayout") === true
|
|
1700
2521
|
? []
|
|
1701
2522
|
: (edge.sections ?? []).flatMap((section) => [
|
|
1702
|
-
section.startPoint.y,
|
|
1703
|
-
...(section.bendPoints ?? []).map(
|
|
1704
|
-
|
|
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),
|
|
1705
2528
|
]),
|
|
1706
2529
|
),
|
|
1707
2530
|
) +
|
|
1708
2531
|
padding.bottom +
|
|
1709
|
-
edgeBoundsExtraY +
|
|
1710
2532
|
singleMultiEdgeCutBoundsExtraY +
|
|
1711
2533
|
postCompactionBoundsExtraY +
|
|
1712
2534
|
(getBooleanOption(layoutOptions, "layered.feedbackEdges") === true &&
|
|
@@ -1901,3 +2723,126 @@ export type {
|
|
|
1901
2723
|
StatechartLayoutScore,
|
|
1902
2724
|
StatechartLayoutPlan,
|
|
1903
2725
|
} from "./statechart";
|
|
2726
|
+
|
|
2727
|
+
export { getElkRoutes } from "./routes";
|
|
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
|
+
}
|