@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.
@@ -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 { assertNorthSouthPortSides, GreedySwitchDecider } from "./greedy-switch-decider";
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
- return new Map([...(orders ?? [])].map(([id, edges]) => [id, [...edges]]));
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
- const slot = new Map(physical.map((port, index) => [port.id, index]));
2098
- if (node.ports.every((port) => slot.has(port.id)))
2099
- node.ports.sort((a, b) => slot.get(a.id)! - slot.get(b.id)!);
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
- // Port sorting reorders ports, not the edges on one port; those keep
2301
- // ELK's edge list order. Expanded segments keep their listed edge id.
2302
- const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
2303
- const rank = (edgeId: string) => listed?.get(listedEdgeId(edgeId));
2304
- group.sort(
2305
- (a, b) =>
2306
- (rank(a) !== undefined && rank(b) !== undefined ? rank(a)! - rank(b)! : 0) ||
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: new Map(
2720
- [...canonicalNodes].map(([id, node]) => [id, node.ports.map((port) => port.id)]),
2721
- ),
2722
- layers: working.map((layer) => [...layer]),
2723
- inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
2724
- outputPortOrderByNodeId: clonePortOrders(outputPortOrder),
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.find((candidate) => candidate.id === id);
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
- const groups = new Map<string, Array<{ edge: GraphEdge; endpoint: "source" | "target" }>>();
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
- const sourceKey = `${edge.sourceId}:${sourceSide}`;
3856
- const targetKey = `${edge.targetId}:${targetSide}`;
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 = input.portSettings?.(port, node)?.["port.side"];
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 [key, entries] of groups) {
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 ? input.nodeSettings?.(node) : undefined;
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 ? input.portSettings?.(explicitPort, node)?.["port.side"] : undefined;
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 [id, rect] of placement.rectByNodeId) {
4572
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
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 = [...flowLayerByNodeId].filter(([, index]) => index === layer);
5144
+ const ids = flowLayerIds[layer]!;
5067
5145
  return ids.length &&
5068
- ids.every(([id]) => {
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 [id, rect] of placement.rectByNodeId) {
5082
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
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 [id, rect] of placement.rectByNodeId) {
5443
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
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.find((candidate) => candidate.id === id);
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;