@statelyai/layout 0.4.1 → 0.4.2
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/dist/elkjs/index.cjs +2 -2
- package/dist/elkjs/index.mjs +2 -2
- package/dist/elkjs/main.cjs +1 -1
- package/dist/elkjs/main.mjs +1 -1
- package/dist/elkjs/worker.cjs +2 -2
- package/dist/elkjs/worker.mjs +2 -2
- package/dist/{elkjs-BBfKN4Dw.mjs → elkjs-CSi7oB5J.mjs} +18 -8
- package/dist/{elkjs-DNfXDPup.cjs → elkjs-CnBvdyBu.cjs} +18 -8
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/layered/index.cjs +1 -1
- package/dist/layered/index.mjs +1 -1
- package/dist/{layered-5RxQhryb.cjs → layered-BJu8A21s.cjs} +231 -121
- package/dist/{layered-DSesbctm.mjs → layered-BsAdm_u8.mjs} +231 -121
- package/dist/{layout-engine-CLFySz3R.cjs → layout-engine-BFP1TZbF.cjs} +1 -1
- package/dist/{layout-engine-GpBIX6ng.mjs → layout-engine-DnlmGxHL.mjs} +1 -1
- package/package.json +1 -1
- package/src/elkjs/index.ts +23 -5
- package/src/layered/bk-node-placement.ts +10 -7
- package/src/layered/crossing-counter.ts +10 -4
- package/src/layered/crossing-graph.ts +2 -2
- package/src/layered/greedy-switch-decider.ts +44 -17
- package/src/layered/index.ts +15 -10
- package/src/layered/loop-envelopes.ts +10 -4
- package/src/layered/node-lookup.ts +29 -0
- package/src/layered/port-distributor.ts +26 -14
- package/src/layered/port-edge-order.ts +2 -1
- package/src/layered/spacing.ts +4 -3
- package/src/layered/strategies.ts +176 -96
|
@@ -60,7 +60,13 @@ import type { ElkLayeredOptionValueByName } from "./elk-options";
|
|
|
60
60
|
import { conservativeSpline } from "./spline-bezier";
|
|
61
61
|
import { getFlexiblePortPosition } from "./flexible-ports";
|
|
62
62
|
import { traceLayeredPhase } from "../internal/layered-trace";
|
|
63
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
assertNorthSouthPortSides,
|
|
65
|
+
GreedySwitchDecider,
|
|
66
|
+
greedySwitchTopology,
|
|
67
|
+
type GreedySwitchTopology,
|
|
68
|
+
} from "./greedy-switch-decider";
|
|
69
|
+
import { findNode } from "./node-lookup";
|
|
64
70
|
|
|
65
71
|
const modelSweepInputs = new WeakSet<LayeredPhaseInput>();
|
|
66
72
|
|
|
@@ -1033,6 +1039,8 @@ export function applyGreedySwitch(
|
|
|
1033
1039
|
(layerIndex + 1 < layers.length ? countBoundary(layerIndex) : 0)
|
|
1034
1040
|
);
|
|
1035
1041
|
};
|
|
1042
|
+
// Switches stay within layers, so port connectivity and layers are fixed.
|
|
1043
|
+
let greedyTopology: GreedySwitchTopology | undefined;
|
|
1036
1044
|
const sweep = (forward: boolean): boolean => {
|
|
1037
1045
|
let changed = false;
|
|
1038
1046
|
const indices = forward
|
|
@@ -1049,6 +1057,7 @@ export function applyGreedySwitch(
|
|
|
1049
1057
|
greedyGraph,
|
|
1050
1058
|
layerIndex,
|
|
1051
1059
|
type === "ONE_SIDED" ? (forward ? "WEST" : "EAST") : undefined,
|
|
1060
|
+
(greedyTopology ??= greedySwitchTopology(greedyGraph)),
|
|
1052
1061
|
);
|
|
1053
1062
|
do {
|
|
1054
1063
|
improved = false;
|
|
@@ -1924,7 +1933,9 @@ export interface LayerSweepSession {
|
|
|
1924
1933
|
}
|
|
1925
1934
|
|
|
1926
1935
|
function clonePortOrders(orders?: ReadonlyMap<string, readonly string[]>): Map<string, string[]> {
|
|
1927
|
-
|
|
1936
|
+
const clone = new Map<string, string[]>();
|
|
1937
|
+
if (orders) for (const [id, edges] of orders) clone.set(id, edges.slice());
|
|
1938
|
+
return clone;
|
|
1928
1939
|
}
|
|
1929
1940
|
|
|
1930
1941
|
function minimizeCrossingsWithLayerSweep(
|
|
@@ -2080,6 +2091,8 @@ export function createLayerSweepSession(
|
|
|
2080
2091
|
(input.settings as { [CROSSING_MODEL]?: CrossingModel })[CROSSING_MODEL] ?? {};
|
|
2081
2092
|
const mixedPorts = crossingModel.mixedPorts !== false,
|
|
2082
2093
|
probe = crossingModel.probe;
|
|
2094
|
+
// Reused per count: physical port positions of one node.
|
|
2095
|
+
const physicalSlot = new Map<string, number>();
|
|
2083
2096
|
const countCrossings = (candidateLayers: readonly (readonly string[])[]): number => {
|
|
2084
2097
|
if (exactPortSweep) {
|
|
2085
2098
|
const graph = buildCrossingGraph(
|
|
@@ -2094,9 +2107,10 @@ export function createLayerSweepSession(
|
|
|
2094
2107
|
for (const node of layer) {
|
|
2095
2108
|
const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : undefined;
|
|
2096
2109
|
if (physical?.length !== node.ports.length) continue;
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2110
|
+
physicalSlot.clear();
|
|
2111
|
+
for (const [index, port] of physical.entries()) physicalSlot.set(port.id, index);
|
|
2112
|
+
if (node.ports.every((port) => physicalSlot.has(port.id)))
|
|
2113
|
+
node.ports.sort((a, b) => physicalSlot.get(a.id)! - physicalSlot.get(b.id)!);
|
|
2100
2114
|
}
|
|
2101
2115
|
const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
|
|
2102
2116
|
const score = countAllCrossings(
|
|
@@ -2286,6 +2300,20 @@ export function createLayerSweepSession(
|
|
|
2286
2300
|
// and same-layer recursion must not be separated into independent sums.
|
|
2287
2301
|
for (const id of freeLayer) {
|
|
2288
2302
|
const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
|
|
2303
|
+
const visit = (edgeId: string) => {
|
|
2304
|
+
const edge = edgeById.get(edgeId)!;
|
|
2305
|
+
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2306
|
+
const neighbor = forward ? source : target;
|
|
2307
|
+
const rank = edgeRanks.get(edgeId);
|
|
2308
|
+
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2309
|
+
visits.get(id)!.push(neighbor);
|
|
2310
|
+
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
2311
|
+
};
|
|
2312
|
+
// One edge forms one group; nothing to sort.
|
|
2313
|
+
if (order.length < 2) {
|
|
2314
|
+
for (const edgeId of order) visit(edgeId);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2289
2317
|
const groups = new Map<string, string[]>();
|
|
2290
2318
|
for (const edgeId of order) {
|
|
2291
2319
|
const edge = edgeById.get(edgeId)!;
|
|
@@ -2296,25 +2324,18 @@ export function createLayerSweepSession(
|
|
|
2296
2324
|
if (group) group.push(edgeId);
|
|
2297
2325
|
else groups.set(key, [edgeId]);
|
|
2298
2326
|
}
|
|
2327
|
+
// Port sorting reorders ports, not the edges on one port; those keep
|
|
2328
|
+
// ELK's edge list order. Expanded segments keep their listed edge id.
|
|
2329
|
+
const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
|
|
2330
|
+
const rank = (edgeId: string) => listed?.get(listedEdgeId(edgeId));
|
|
2299
2331
|
for (const group of groups.values()) {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2308
|
-
);
|
|
2309
|
-
for (const edgeId of group) {
|
|
2310
|
-
const edge = edgeById.get(edgeId)!;
|
|
2311
|
-
const [source, target] = getOrientedEndpoints(edge, orientation);
|
|
2312
|
-
const neighbor = forward ? source : target;
|
|
2313
|
-
const rank = edgeRanks.get(edgeId);
|
|
2314
|
-
if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id))
|
|
2315
|
-
visits.get(id)!.push(neighbor);
|
|
2316
|
-
else if (rank !== undefined) visits.get(id)!.push(rank);
|
|
2317
|
-
}
|
|
2332
|
+
if (group.length > 1)
|
|
2333
|
+
group.sort(
|
|
2334
|
+
(a, b) =>
|
|
2335
|
+
(rank(a) !== undefined && rank(b) !== undefined ? rank(a)! - rank(b)! : 0) ||
|
|
2336
|
+
edgeModelOrder.get(a)! - edgeModelOrder.get(b)!,
|
|
2337
|
+
);
|
|
2338
|
+
for (const edgeId of group) visit(edgeId);
|
|
2318
2339
|
}
|
|
2319
2340
|
}
|
|
2320
2341
|
return { ranks, visits };
|
|
@@ -2391,6 +2412,13 @@ export function createLayerSweepSession(
|
|
|
2391
2412
|
});
|
|
2392
2413
|
for (const index of touched)
|
|
2393
2414
|
for (const node of canonicalGraph.layers[index]!) {
|
|
2415
|
+
// One port leaves every edge order tied, and one edge has no order.
|
|
2416
|
+
if (
|
|
2417
|
+
node.ports.length < 2 ||
|
|
2418
|
+
((outputPortOrder.get(node.id)?.length ?? 0) < 2 &&
|
|
2419
|
+
(inputPortOrder.get(node.id)?.length ?? 0) < 2)
|
|
2420
|
+
)
|
|
2421
|
+
continue;
|
|
2394
2422
|
const ordinal = new Map(node.ports.map((port, index) => [port.id, index]));
|
|
2395
2423
|
for (const [orders, source] of [
|
|
2396
2424
|
[outputPortOrder, true],
|
|
@@ -2715,14 +2743,20 @@ export function createLayerSweepSession(
|
|
|
2715
2743
|
shuffleFirstLayer,
|
|
2716
2744
|
sweep,
|
|
2717
2745
|
countCrossings: () => countCrossings(working),
|
|
2718
|
-
snapshot: () =>
|
|
2719
|
-
physicalPortOrderByNodeId
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2746
|
+
snapshot: () => {
|
|
2747
|
+
const physicalPortOrderByNodeId = new Map<string, string[]>();
|
|
2748
|
+
for (const [id, node] of canonicalNodes)
|
|
2749
|
+
physicalPortOrderByNodeId.set(
|
|
2750
|
+
id,
|
|
2751
|
+
node.ports.map((port) => port.id),
|
|
2752
|
+
);
|
|
2753
|
+
return {
|
|
2754
|
+
physicalPortOrderByNodeId,
|
|
2755
|
+
layers: working.map((layer) => [...layer]),
|
|
2756
|
+
inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
|
|
2757
|
+
outputPortOrderByNodeId: clonePortOrders(outputPortOrder),
|
|
2758
|
+
};
|
|
2759
|
+
},
|
|
2726
2760
|
restore,
|
|
2727
2761
|
finish,
|
|
2728
2762
|
currentLayers: () => working,
|
|
@@ -3422,7 +3456,7 @@ function nodeFlowOffset(
|
|
|
3422
3456
|
layerByNodeId: ReadonlyMap<string, number>,
|
|
3423
3457
|
margins?: { leading: number; trailing: number; maxLeading: number; maxTrailing: number },
|
|
3424
3458
|
): number {
|
|
3425
|
-
const node = input.graph.nodes
|
|
3459
|
+
const node = findNode(input.graph.nodes, id);
|
|
3426
3460
|
const alignment = node
|
|
3427
3461
|
? (input.nodeSettings?.(node)?.alignment ?? input.settings.alignment)
|
|
3428
3462
|
: input.settings.alignment;
|
|
@@ -3788,7 +3822,85 @@ function implicitEdgeEndpoints(
|
|
|
3788
3822
|
): ReadonlyMap<string, { source: Point; target: Point }> {
|
|
3789
3823
|
const horizontal = input.direction === "left" || input.direction === "right";
|
|
3790
3824
|
const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
|
|
3791
|
-
|
|
3825
|
+
// Settings stay fixed for the call; resolve each node's and port's once.
|
|
3826
|
+
const nodeSettingsByNode = new Map<
|
|
3827
|
+
GraphNode,
|
|
3828
|
+
ReturnType<NonNullable<typeof input.nodeSettings>>
|
|
3829
|
+
>();
|
|
3830
|
+
const nodeSettingsOf = (node: GraphNode) => {
|
|
3831
|
+
if (!nodeSettingsByNode.has(node)) nodeSettingsByNode.set(node, input.nodeSettings?.(node));
|
|
3832
|
+
return nodeSettingsByNode.get(node);
|
|
3833
|
+
};
|
|
3834
|
+
// Port settings depend on the owning node too, so cache them per node.
|
|
3835
|
+
const portSettingsByNode = new Map<
|
|
3836
|
+
GraphNode,
|
|
3837
|
+
Map<GraphPort, ReturnType<NonNullable<typeof input.portSettings>>>
|
|
3838
|
+
>();
|
|
3839
|
+
const portSettingsOf = (port: GraphPort, node: GraphNode) => {
|
|
3840
|
+
let byPort = portSettingsByNode.get(node);
|
|
3841
|
+
if (!byPort) portSettingsByNode.set(node, (byPort = new Map()));
|
|
3842
|
+
if (!byPort.has(port)) byPort.set(port, input.portSettings?.(port, node));
|
|
3843
|
+
return byPort.get(port);
|
|
3844
|
+
};
|
|
3845
|
+
// Endpoint groups by side and node, in first-entry order.
|
|
3846
|
+
type EndpointGroup = {
|
|
3847
|
+
nodeId: string;
|
|
3848
|
+
side: string;
|
|
3849
|
+
entries: Array<{ edge: GraphEdge; endpoint: "source" | "target" }>;
|
|
3850
|
+
};
|
|
3851
|
+
const groups: EndpointGroup[] = [];
|
|
3852
|
+
const groupsBySide = new Map<string, Map<string, EndpointGroup>>();
|
|
3853
|
+
const modelOrderPromotion =
|
|
3854
|
+
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3855
|
+
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
3856
|
+
const endpointSide = (nodeId: string, portName: string | undefined): string | undefined => {
|
|
3857
|
+
const node = nodeById.get(nodeId);
|
|
3858
|
+
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3859
|
+
const constraints = node ? nodeSettingsOf(node)?.portConstraints : undefined;
|
|
3860
|
+
if (
|
|
3861
|
+
!node ||
|
|
3862
|
+
!port ||
|
|
3863
|
+
(constraints !== "FIXED_SIDE" &&
|
|
3864
|
+
constraints !== "FIXED_ORDER" &&
|
|
3865
|
+
constraints !== "FIXED_RATIO" &&
|
|
3866
|
+
constraints !== "FIXED_POS")
|
|
3867
|
+
) {
|
|
3868
|
+
return undefined;
|
|
3869
|
+
}
|
|
3870
|
+
const configured = portSettingsOf(port, node)?.["port.side"];
|
|
3871
|
+
return configured === "WEST" || configured === "NORTH"
|
|
3872
|
+
? "before"
|
|
3873
|
+
: configured === "EAST" || configured === "SOUTH"
|
|
3874
|
+
? "after"
|
|
3875
|
+
: undefined;
|
|
3876
|
+
};
|
|
3877
|
+
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3878
|
+
const node = nodeById.get(nodeId);
|
|
3879
|
+
const constraints = node && nodeSettingsOf(node)?.portConstraints;
|
|
3880
|
+
if (
|
|
3881
|
+
portName !== undefined ||
|
|
3882
|
+
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3883
|
+
)
|
|
3884
|
+
return undefined;
|
|
3885
|
+
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3886
|
+
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3887
|
+
return source !== directionReversed ? "after" : "before";
|
|
3888
|
+
};
|
|
3889
|
+
const addEntry = (
|
|
3890
|
+
nodeId: string,
|
|
3891
|
+
side: string,
|
|
3892
|
+
entry: { edge: GraphEdge; endpoint: "source" | "target" },
|
|
3893
|
+
) => {
|
|
3894
|
+
let byNode = groupsBySide.get(side);
|
|
3895
|
+
if (!byNode) groupsBySide.set(side, (byNode = new Map()));
|
|
3896
|
+
const group = byNode.get(nodeId);
|
|
3897
|
+
if (group) group.entries.push(entry);
|
|
3898
|
+
else {
|
|
3899
|
+
const created = { nodeId, side, entries: [entry] };
|
|
3900
|
+
byNode.set(nodeId, created);
|
|
3901
|
+
groups.push(created);
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3792
3904
|
for (const edge of input.graph.edges) {
|
|
3793
3905
|
if (hasMovableLoopPorts(input, edge)) continue;
|
|
3794
3906
|
const sourceRect = placement.rectByNodeId.get(edge.sourceId);
|
|
@@ -3796,8 +3908,6 @@ function implicitEdgeEndpoints(
|
|
|
3796
3908
|
if (!sourceRect || !targetRect) continue;
|
|
3797
3909
|
const sourceFlow = horizontal ? sourceRect.x : sourceRect.y;
|
|
3798
3910
|
const targetFlow = horizontal ? targetRect.x : targetRect.y;
|
|
3799
|
-
const modelOrderPromotion =
|
|
3800
|
-
input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
|
|
3801
3911
|
const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
|
|
3802
3912
|
const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
|
|
3803
3913
|
// A zero-gap boundary can coincide with the node's flow origin.
|
|
@@ -3810,40 +3920,6 @@ function implicitEdgeEndpoints(
|
|
|
3810
3920
|
: sourceFlow <= targetFlow);
|
|
3811
3921
|
const feedback =
|
|
3812
3922
|
input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
|
|
3813
|
-
const directionReversed = input.direction === "left" || input.direction === "up";
|
|
3814
|
-
const endpointSide = (nodeId: string, portName: string | undefined): string | undefined => {
|
|
3815
|
-
const node = nodeById.get(nodeId);
|
|
3816
|
-
const port = node?.ports?.find((candidate) => candidate.name === portName);
|
|
3817
|
-
const constraints = node ? input.nodeSettings?.(node)?.portConstraints : undefined;
|
|
3818
|
-
if (
|
|
3819
|
-
!node ||
|
|
3820
|
-
!port ||
|
|
3821
|
-
(constraints !== "FIXED_SIDE" &&
|
|
3822
|
-
constraints !== "FIXED_ORDER" &&
|
|
3823
|
-
constraints !== "FIXED_RATIO" &&
|
|
3824
|
-
constraints !== "FIXED_POS")
|
|
3825
|
-
) {
|
|
3826
|
-
return undefined;
|
|
3827
|
-
}
|
|
3828
|
-
const configured = input.portSettings?.(port, node)?.["port.side"];
|
|
3829
|
-
return configured === "WEST" || configured === "NORTH"
|
|
3830
|
-
? "before"
|
|
3831
|
-
: configured === "EAST" || configured === "SOUTH"
|
|
3832
|
-
? "after"
|
|
3833
|
-
: undefined;
|
|
3834
|
-
};
|
|
3835
|
-
const fixedImplicitSide = (nodeId: string, portName: string | undefined, source: boolean) => {
|
|
3836
|
-
const node = nodeById.get(nodeId);
|
|
3837
|
-
const constraints = node && input.nodeSettings?.(node)?.portConstraints;
|
|
3838
|
-
if (
|
|
3839
|
-
portName !== undefined ||
|
|
3840
|
-
!["FIXED_SIDE", "FIXED_ORDER", "FIXED_RATIO", "FIXED_POS"].includes(String(constraints))
|
|
3841
|
-
)
|
|
3842
|
-
return undefined;
|
|
3843
|
-
// ELK assigns implicit port sides before cycle breaking. Fixed sides
|
|
3844
|
-
// survive edge reversal; a feedback edge cannot move its endpoint.
|
|
3845
|
-
return source !== directionReversed ? "after" : "before";
|
|
3846
|
-
};
|
|
3847
3923
|
const sourceSide =
|
|
3848
3924
|
endpointSide(edge.sourceId, edge.sourcePort) ??
|
|
3849
3925
|
fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ??
|
|
@@ -3852,14 +3928,8 @@ function implicitEdgeEndpoints(
|
|
|
3852
3928
|
endpointSide(edge.targetId, edge.targetPort) ??
|
|
3853
3929
|
fixedImplicitSide(edge.targetId, edge.targetPort, false) ??
|
|
3854
3930
|
(feedback ? (directionReversed ? "after" : "before") : forward ? "before" : "after");
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
const sourceGroup = groups.get(sourceKey) ?? [];
|
|
3858
|
-
sourceGroup.push({ edge, endpoint: "source" });
|
|
3859
|
-
groups.set(sourceKey, sourceGroup);
|
|
3860
|
-
const targetGroup = groups.get(targetKey) ?? [];
|
|
3861
|
-
targetGroup.push({ edge, endpoint: "target" });
|
|
3862
|
-
groups.set(targetKey, targetGroup);
|
|
3931
|
+
addEntry(edge.sourceId, sourceSide, { edge, endpoint: "source" });
|
|
3932
|
+
addEntry(edge.targetId, targetSide, { edge, endpoint: "target" });
|
|
3863
3933
|
}
|
|
3864
3934
|
|
|
3865
3935
|
const result = new Map<string, { source: Point; target: Point }>();
|
|
@@ -3883,22 +3953,19 @@ function implicitEdgeEndpoints(
|
|
|
3883
3953
|
if (!counts) {
|
|
3884
3954
|
counts = new Map();
|
|
3885
3955
|
for (const port of node.ports ?? []) {
|
|
3886
|
-
const portSide =
|
|
3956
|
+
const portSide = portSettingsOf(port, node)?.["port.side"];
|
|
3887
3957
|
counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
|
|
3888
3958
|
}
|
|
3889
3959
|
portSideCounts.set(node, counts);
|
|
3890
3960
|
}
|
|
3891
3961
|
return counts.get(side) ?? 0;
|
|
3892
3962
|
};
|
|
3893
|
-
for (const
|
|
3894
|
-
const split = key.lastIndexOf(":");
|
|
3895
|
-
const nodeId = key.slice(0, split);
|
|
3896
|
-
const side = key.slice(split + 1);
|
|
3963
|
+
for (const { nodeId, side, entries } of groups) {
|
|
3897
3964
|
const rect = placement.rectByNodeId.get(nodeId);
|
|
3898
3965
|
if (!rect) continue;
|
|
3899
3966
|
const mergeEdges = input.settings.mergeEdges === true;
|
|
3900
3967
|
const node = nodeById.get(nodeId);
|
|
3901
|
-
const nodeSettings = node ?
|
|
3968
|
+
const nodeSettings = node ? nodeSettingsOf(node) : undefined;
|
|
3902
3969
|
const hypernode = nodeSettings?.hypernode;
|
|
3903
3970
|
const unzipping = (input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING";
|
|
3904
3971
|
const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
|
|
@@ -4099,7 +4166,7 @@ function implicitEdgeEndpoints(
|
|
|
4099
4166
|
const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
|
|
4100
4167
|
const explicitPort = node?.ports?.find((port) => port.name === portName);
|
|
4101
4168
|
const physicalSide =
|
|
4102
|
-
node && explicitPort ?
|
|
4169
|
+
node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.side"] : undefined;
|
|
4103
4170
|
const singleFixedSidePort =
|
|
4104
4171
|
constraints === "FIXED_SIDE" &&
|
|
4105
4172
|
physicalSide !== undefined &&
|
|
@@ -4108,9 +4175,7 @@ function implicitEdgeEndpoints(
|
|
|
4108
4175
|
// Movable port ordering comes from the layer sweep above. Fixed coordinates
|
|
4109
4176
|
// and authored anchors must replace the synthesized node-origin fallback.
|
|
4110
4177
|
const authoredAnchor =
|
|
4111
|
-
node && explicitPort
|
|
4112
|
-
? input.portSettings?.(explicitPort, node)?.["port.anchor"]
|
|
4113
|
-
: undefined;
|
|
4178
|
+
node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.anchor"] : undefined;
|
|
4114
4179
|
if (
|
|
4115
4180
|
portName !== undefined &&
|
|
4116
4181
|
node &&
|
|
@@ -4439,6 +4504,16 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4439
4504
|
flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
|
|
4440
4505
|
}
|
|
4441
4506
|
const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
|
|
4507
|
+
// Placed node ids per flow layer, in placement order. Layer loops only
|
|
4508
|
+
// move these nodes, so a grouping stays valid while its loop runs.
|
|
4509
|
+
const placedIdsByFlowLayer = () => {
|
|
4510
|
+
const ids = flowLayers.map(() => [] as string[]);
|
|
4511
|
+
for (const id of placement.rectByNodeId.keys()) {
|
|
4512
|
+
const layer = flowLayerByNodeId.get(id);
|
|
4513
|
+
if (layer !== undefined) ids[layer]!.push(id);
|
|
4514
|
+
}
|
|
4515
|
+
return ids;
|
|
4516
|
+
};
|
|
4442
4517
|
const labelFlowLayers = new Set(
|
|
4443
4518
|
[...flowLayerByNodeId]
|
|
4444
4519
|
.filter(([id]) => id.startsWith("__layout_dummy:label:"))
|
|
@@ -4566,10 +4641,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
4566
4641
|
labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
|
|
4567
4642
|
}
|
|
4568
4643
|
let labelShift = 0;
|
|
4644
|
+
const labelShiftIds = placedIdsByFlowLayer();
|
|
4569
4645
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
4570
4646
|
if (labelShift !== 0) {
|
|
4571
|
-
for (const
|
|
4572
|
-
|
|
4647
|
+
for (const id of labelShiftIds[layerNo]!) {
|
|
4648
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
4573
4649
|
mutableRects.set(
|
|
4574
4650
|
id,
|
|
4575
4651
|
horizontal ? { ...rect, x: rect.x + labelShift } : { ...rect, y: rect.y + labelShift },
|
|
@@ -5061,11 +5137,13 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5061
5137
|
leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
|
|
5062
5138
|
let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
|
|
5063
5139
|
// ELK omits ordinary node spacing beside an external-port-only layer.
|
|
5140
|
+
const flowLayerIds = flowLayers.map(() => [] as string[]);
|
|
5141
|
+
for (const [id, layer] of flowLayerByNodeId) flowLayerIds[layer]!.push(id);
|
|
5064
5142
|
const externalFlowLayers = new Set(
|
|
5065
5143
|
flowLayers.flatMap((_, layer) => {
|
|
5066
|
-
const ids = [
|
|
5144
|
+
const ids = flowLayerIds[layer]!;
|
|
5067
5145
|
return ids.length &&
|
|
5068
|
-
ids.every((
|
|
5146
|
+
ids.every((id) => {
|
|
5069
5147
|
const node = nodeById.get(id);
|
|
5070
5148
|
const side = node && externalPortDummyOf(node)?.side;
|
|
5071
5149
|
return horizontal
|
|
@@ -5076,10 +5154,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5076
5154
|
: [];
|
|
5077
5155
|
}),
|
|
5078
5156
|
);
|
|
5157
|
+
const shiftedIds = placedIdsByFlowLayer();
|
|
5079
5158
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
5080
5159
|
const shift = nextStart - bounds.start;
|
|
5081
|
-
for (const
|
|
5082
|
-
|
|
5160
|
+
for (const id of shiftedIds[layerNo]!) {
|
|
5161
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
5083
5162
|
mutableRects.set(
|
|
5084
5163
|
id,
|
|
5085
5164
|
horizontal ? { ...rect, x: rect.x + shift } : { ...rect, y: rect.y + shift },
|
|
@@ -5437,10 +5516,11 @@ function routeEdges(style: "ORTHOGONAL" | "POLYLINE" | "SPLINES"): EdgeRouter {
|
|
|
5437
5516
|
}
|
|
5438
5517
|
}
|
|
5439
5518
|
let nextStart = flowLayers[0]?.start ?? 0;
|
|
5519
|
+
const shiftedIds = placedIdsByFlowLayer();
|
|
5440
5520
|
for (const [layerNo, bounds] of flowLayers.entries()) {
|
|
5441
5521
|
const shift = nextStart - bounds.start;
|
|
5442
|
-
for (const
|
|
5443
|
-
|
|
5522
|
+
for (const id of shiftedIds[layerNo]!) {
|
|
5523
|
+
const rect = placement.rectByNodeId.get(id)!;
|
|
5444
5524
|
mutableRects.set(
|
|
5445
5525
|
id,
|
|
5446
5526
|
horizontal ? { ...rect, x: rect.x + shift } : { ...rect, y: rect.y + shift },
|
|
@@ -6774,7 +6854,7 @@ export function normalizePlacementForPortExtents(
|
|
|
6774
6854
|
[nextLayer, false],
|
|
6775
6855
|
] as const) {
|
|
6776
6856
|
for (const id of ids) {
|
|
6777
|
-
const node = input.graph.nodes
|
|
6857
|
+
const node = findNode(input.graph.nodes, id);
|
|
6778
6858
|
const rect = placement.rectByNodeId.get(id);
|
|
6779
6859
|
if (!node || !rect) continue;
|
|
6780
6860
|
const bodyStart = horizontal ? rect.x : rect.y;
|