@statelyai/layout 0.4.1 → 0.4.3

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.
@@ -311,6 +311,12 @@ const restoreInitialModelOrder = (input, graph) => {
311
311
  //#endregion
312
312
  //#region src/layered/port-distributor.ts
313
313
  /** Stateful clockwise port distribution across successive layer sweeps. */
314
+ const sideRank = {
315
+ NORTH: 0,
316
+ EAST: 1,
317
+ SOUTH: 2,
318
+ WEST: 3
319
+ };
314
320
  var CanonicalPortDistributor = class {
315
321
  state;
316
322
  topologyGraph;
@@ -359,25 +365,33 @@ var CanonicalPortDistributor = class {
359
365
  calculatePortRanks(graph, layer, input, nodeRelative) {
360
366
  this.prepareTopology(graph);
361
367
  let consumed = 0;
368
+ const ends = input ? this.incoming : this.outgoing;
369
+ const ranked = (port) => ends.get(port.id).length > 0;
362
370
  for (const node of layer) {
363
- const ports = node.ports.filter((port) => (input ? this.incoming : this.outgoing).get(port.id).length > 0);
364
- const increment = nodeRelative ? 1 / (ports.length + 1) : 1;
371
+ let count = 0, northCount = 0;
372
+ for (const port of node.ports) if (ranked(port)) {
373
+ count++;
374
+ if (port.side === "NORTH") northCount++;
375
+ }
376
+ const increment = nodeRelative ? 1 / (count + 1) : 1;
365
377
  if (input) {
366
- let north = consumed + ports.filter((port) => port.side === "NORTH").length * increment;
367
- let rest = nodeRelative ? consumed + 1 - increment : consumed + ports.length;
368
- for (const port of ports) {
378
+ let north = consumed + northCount * increment;
379
+ let rest = nodeRelative ? consumed + 1 - increment : consumed + count;
380
+ for (const port of node.ports) {
381
+ if (!ranked(port)) continue;
369
382
  this.state.ranks[port.id] = port.side === "NORTH" ? north : rest;
370
383
  if (port.side === "NORTH") north -= increment;
371
384
  else rest -= increment;
372
385
  }
373
386
  } else {
374
387
  let position = consumed + increment;
375
- for (const port of ports) {
388
+ for (const port of node.ports) {
389
+ if (!ranked(port)) continue;
376
390
  this.state.ranks[port.id] = position;
377
391
  position += increment;
378
392
  }
379
393
  }
380
- consumed += nodeRelative ? 1 : ports.length;
394
+ consumed += nodeRelative ? 1 : count;
381
395
  }
382
396
  }
383
397
  distribute(graph, index, forward, options) {
@@ -386,7 +400,7 @@ var CanonicalPortDistributor = class {
386
400
  const degree = (port) => incoming.get(port).length + outgoing.get(port).length;
387
401
  const ranks = (layer, input) => this.calculatePortRanks(graph, layer, input, options.nodeRelative);
388
402
  const distributeNode = (node, side$1) => {
389
- if (options.fixedOrder.has(node.id)) return;
403
+ if (options.fixedOrder.has(node.id) || !node.ports.length) return;
390
404
  const layerIndex = node.ports.length ? layerByPort.get(node.ports[0].id) : index;
391
405
  const size$1 = graph.layers[layerIndex].length;
392
406
  for (const face of [
@@ -396,7 +410,8 @@ var CanonicalPortDistributor = class {
396
410
  ]) {
397
411
  let minimum = 0, maximum = 0;
398
412
  const inLayer = [];
399
- for (const port of node.ports.filter((port$1) => port$1.side === face)) {
413
+ for (const port of node.ports) {
414
+ if (port.side !== face) continue;
400
415
  let sum = 0;
401
416
  const cross = port.side === "NORTH" || port.side === "SOUTH";
402
417
  if (cross) {
@@ -414,7 +429,8 @@ var CanonicalPortDistributor = class {
414
429
  else if (output && !input) sum += position + 1;
415
430
  else if (input && output) sum += port.side === "NORTH" ? 0 : large / 2;
416
431
  } else {
417
- if ([...outgoing.get(port.id), ...incoming.get(port.id)].some((peer) => layerByPort.get(peer) === layerIndex)) {
432
+ const sameLayer = (peer) => layerByPort.get(peer) === layerIndex;
433
+ if (outgoing.get(port.id).some(sameLayer) || incoming.get(port.id).some(sameLayer)) {
418
434
  inLayer.push(port);
419
435
  continue;
420
436
  }
@@ -435,12 +451,7 @@ var CanonicalPortDistributor = class {
435
451
  this.state.barycenters[port.id] = port.side === "EAST" ? above ? minimum - value : maximum + (size$1 + 1 - value) : above ? maximum + value : minimum - (size$1 + 1 - value);
436
452
  }
437
453
  }
438
- const sideRank = {
439
- NORTH: 0,
440
- EAST: 1,
441
- SOUTH: 2,
442
- WEST: 3
443
- };
454
+ if (node.ports.length < 2) return;
444
455
  node.ports.sort((a, b) => {
445
456
  const side$2 = sideRank[a.side] - sideRank[b.side];
446
457
  if (side$2) return side$2;
@@ -600,7 +611,7 @@ var IndexTree = class {
600
611
  };
601
612
  /** Score a canonical candidate without changing its node or port orders. */
602
613
  function countAllCrossings(graph, boundaryMode = "hyperedges") {
603
- const nodes = new Map(graph.layers.flat().map((node) => [node.id, node]));
614
+ let nodes;
604
615
  const layerByPort = /* @__PURE__ */ new Map();
605
616
  const incoming = /* @__PURE__ */ new Map(), outgoing = /* @__PURE__ */ new Map();
606
617
  for (const [layer, members] of graph.layers.entries()) for (const node of members) for (const port of node.ports) {
@@ -612,7 +623,6 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
612
623
  outgoing.get(edge.source).push(edge.target);
613
624
  incoming.get(edge.target).push(edge.source);
614
625
  }
615
- const connected = (id) => [...incoming.get(id), ...outgoing.get(id)];
616
626
  const degree = (id) => incoming.get(id).length + outgoing.get(id).length;
617
627
  const face = (layer, side, topDown = true) => (topDown ? layer : [...layer].reverse()).flatMap((node) => {
618
628
  const ports = node.ports.filter((port) => port.side === side);
@@ -625,7 +635,9 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
625
635
  for (const [position, port] of ports.entries()) {
626
636
  tree.remove(position);
627
637
  const pending = [];
628
- for (const peer of connected(port.id)) {
638
+ const before = incoming.get(port.id), after = outgoing.get(port.id);
639
+ for (let index = 0; index < before.length + after.length; index++) {
640
+ const peer = index < before.length ? before[index] : after[index - before.length];
629
641
  if (inLayerOnly && layerByPort.get(peer) !== layerByPort.get(port.id)) {
630
642
  crossings$1 += tree.size;
631
643
  continue;
@@ -669,7 +681,7 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
669
681
  for (const [position, port] of ports.entries()) {
670
682
  tree.remove(position);
671
683
  const node = ownerByPort.get(port.id);
672
- const targets = node.type === "NORMAL" ? nodes.get(port.dummy).ports.map((peer) => ({
684
+ const targets = node.type === "NORMAL" ? (nodes ??= new Map(graph.layers.flat().map((node$1) => [node$1.id, node$1]))).get(port.dummy).ports.map((peer) => ({
673
685
  id: peer.id,
674
686
  degree: degree(peer.id)
675
687
  })) : node.type === "LONG_EDGE" ? node.ports.filter((peer) => peer.id !== port.id).slice(0, 1).map((peer) => ({
@@ -1102,7 +1114,7 @@ function buildCrossingGraph(plan, layers, incoming, outgoing) {
1102
1114
  for (const member of ports.get(operation.northSouthDummy)?.values() ?? []) member.port.origin = operation.id;
1103
1115
  }
1104
1116
  }
1105
- const sideRank = {
1117
+ const sideRank$1 = {
1106
1118
  NORTH: 0,
1107
1119
  EAST: 1,
1108
1120
  SOUTH: 2,
@@ -1115,7 +1127,7 @@ function buildCrossingGraph(plan, layers, incoming, outgoing) {
1115
1127
  id,
1116
1128
  type: kind?.type ?? kindOf(id, plan.units),
1117
1129
  unit: kind ? kind.unit : plan.units?.units.get(id),
1118
- ports: [...ports.get(id)?.values() ?? []].sort((a, b) => sideRank[a.port.side] - sideRank[b.port.side] || a.rank - b.rank).map((member) => member.port)
1130
+ ports: [...ports.get(id)?.values() ?? []].sort((a, b) => sideRank$1[a.port.side] - sideRank$1[b.port.side] || a.rank - b.rank).map((member) => member.port)
1119
1131
  };
1120
1132
  })),
1121
1133
  edges: plan.edges.map((edge) => ({
@@ -1135,7 +1147,8 @@ function splitMixedCrossingPorts(graph, reversed) {
1135
1147
  const directions = /* @__PURE__ */ new Map();
1136
1148
  graph.edges.forEach((edge, index) => {
1137
1149
  const bit = reversed[index] ? 2 : 1;
1138
- for (const port of [edge.source, edge.target]) directions.set(port, (directions.get(port) ?? 0) | bit);
1150
+ directions.set(edge.source, (directions.get(edge.source) ?? 0) | bit);
1151
+ directions.set(edge.target, (directions.get(edge.target) ?? 0) | bit);
1139
1152
  });
1140
1153
  const mixed = /* @__PURE__ */ new Set();
1141
1154
  for (const layer of graph.layers) for (const node of layer) for (const port of node.ports) if (directions.get(port.id) === 3 && (port.side === "EAST" || port.side === "WEST")) mixed.add(port.id);
@@ -1575,20 +1588,46 @@ function selfLoopTracks(loops) {
1575
1588
  }));
1576
1589
  }
1577
1590
 
1591
+ //#endregion
1592
+ //#region src/layered/node-lookup.ts
1593
+ const indexByNodes = /* @__PURE__ */ new WeakMap();
1594
+ function indexNodes(nodes) {
1595
+ const positions = /* @__PURE__ */ new Map();
1596
+ for (const [index, node] of nodes.entries()) if (!positions.has(node.id)) positions.set(node.id, index);
1597
+ indexByNodes.set(nodes, {
1598
+ length: nodes.length,
1599
+ positions
1600
+ });
1601
+ return positions;
1602
+ }
1603
+ /** Same as `nodes.find((node) => node.id === id)`, indexed per list. */
1604
+ function findNode(nodes, id) {
1605
+ const cached = indexByNodes.get(nodes);
1606
+ let position = (cached && cached.length === nodes.length ? cached.positions : indexNodes(nodes)).get(id);
1607
+ if (position !== void 0 && nodes[position]?.id !== id) position = indexNodes(nodes).get(id);
1608
+ return position !== void 0 && nodes[position]?.id === id ? nodes[position] : nodes.find((node) => node.id === id);
1609
+ }
1610
+
1578
1611
  //#endregion
1579
1612
  //#region src/layered/loop-envelopes.ts
1580
1613
  /** Unconstrained implicit loop ports are restored on loop faces after the sweep. */
1581
1614
  function hasMovableLoopPorts(input, edge) {
1582
1615
  if (edge.sourceId !== edge.targetId || edge.sourcePort !== void 0 || edge.targetPort !== void 0) return false;
1583
- const node = input.graph.nodes.find((candidate) => candidate.id === edge.sourceId);
1616
+ const node = findNode(input.graph.nodes, edge.sourceId);
1584
1617
  const constraints = node && input.nodeSettings?.(node)?.portConstraints;
1585
1618
  return constraints === void 0 || constraints === "FREE" || constraints === "UNDEFINED";
1586
1619
  }
1587
1620
  function loopEnvelopes(input) {
1588
1621
  const envelopes = /* @__PURE__ */ new Map();
1589
1622
  if ((input.settings.edgeRouting ?? "ORTHOGONAL") !== "ORTHOGONAL") return envelopes;
1623
+ const loopsByNodeId = /* @__PURE__ */ new Map();
1624
+ for (const edge of input.graph.edges) if (edge.sourceId === edge.targetId) {
1625
+ const loops = loopsByNodeId.get(edge.sourceId);
1626
+ if (loops) loops.push(edge);
1627
+ else loopsByNodeId.set(edge.sourceId, [edge]);
1628
+ }
1590
1629
  for (const node of input.graph.nodes) {
1591
- const allLoops = input.graph.edges.filter((edge) => edge.sourceId === node.id && edge.targetId === node.id);
1630
+ const allLoops = loopsByNodeId.get(node.id)?.slice() ?? [];
1592
1631
  const constraints = input.nodeSettings?.(node)?.portConstraints;
1593
1632
  if (allLoops.length && constraints && constraints !== "FREE" && constraints !== "UNDEFINED" && allLoops.every((edge) => edge.sourcePort !== void 0 && edge.targetPort !== void 0 && input.edgeSettings?.(edge)?.["edgeLabels.inline"] !== true && ((edge.width ?? 0) === 0 && (edge.height ?? 0) === 0 || (input.edgeSettings?.(edge)?.["edgeLabels.placement"] ?? "CENTER") === "CENTER"))) {
1594
1633
  const size$1 = input.sizes.get(node.id) ?? {
@@ -1701,7 +1740,7 @@ function preparedLoopEnvelopes(input) {
1701
1740
  //#region src/layered/port-edge-order.ts
1702
1741
  /** ELK traverses connected edges through the node's port list, preserving each port's edge order. */
1703
1742
  function connectedEdgesInPortOrder(input, nodeId) {
1704
- const node = input.graph.nodes.find((candidate) => candidate.id === nodeId);
1743
+ const node = findNode(input.graph.nodes, nodeId);
1705
1744
  const portRank = new Map(node?.ports?.map((port, index) => [port.name, index]));
1706
1745
  const explicitCount = portRank.size;
1707
1746
  return input.graph.edges.map((edge, index) => ({
@@ -2590,8 +2629,8 @@ function nodeNodeSpacing(input, firstId, secondId) {
2590
2629
  const secondBreakingPoint = secondId.startsWith("__layout_breaking:");
2591
2630
  const firstLabel = firstId.startsWith("__layout_dummy:label:");
2592
2631
  const secondLabel = secondId.startsWith("__layout_dummy:label:");
2593
- const firstNode = input.graph.nodes.find((node) => node.id === firstId);
2594
- const secondNode = input.graph.nodes.find((node) => node.id === secondId);
2632
+ const firstNode = findNode(input.graph.nodes, firstId);
2633
+ const secondNode = findNode(input.graph.nodes, secondId);
2595
2634
  const firstExternal = firstNode !== void 0 && externalPortDummyOf(firstNode) !== void 0;
2596
2635
  const secondExternal = secondNode !== void 0 && externalPortDummyOf(secondNode) !== void 0;
2597
2636
  const firstDummy = firstId.startsWith("__layout_dummy:") || firstBreakingPoint || firstExternal;
@@ -2599,7 +2638,7 @@ function nodeNodeSpacing(input, firstId, secondId) {
2599
2638
  const spacingName = firstExternal && secondExternal ? "spacing.portPort" : firstExternal && secondLabel || secondExternal && firstLabel ? "spacing.labelPortVertical" : firstLabel && secondLabel ? "spacing.edgeEdge" : firstLabel && !secondDummy || secondLabel && !firstDummy ? "spacing.node" : firstLabel && secondDummy || secondLabel && firstDummy ? "spacing.edgeNode" : firstDummy && secondDummy && firstBreakingPoint === secondBreakingPoint ? "spacing.edgeEdge" : firstDummy || secondDummy ? "spacing.edgeNode" : "spacing.node";
2600
2639
  let spacing = spacingName === "spacing.portPort" ? Number(input.settings["spacing.portPort"] ?? 10) : spacingName === "spacing.labelPortVertical" ? Number(input.settings["spacing.labelPortVertical"] ?? 1) : spacingName === "spacing.edgeEdge" ? Number(input.settings["spacing.edgeEdge"] ?? 10) : spacingName === "spacing.edgeNode" ? Number(input.settings["spacing.edgeNode"] ?? 10) : input.spacing.node;
2601
2640
  for (const id of [firstId, secondId]) {
2602
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
2641
+ const node = findNode(input.graph.nodes, id);
2603
2642
  if (!node) continue;
2604
2643
  const individual = input.nodeSettings?.(node)?.["spacing.individual"];
2605
2644
  if (individual && typeof individual === "object") {
@@ -3410,16 +3449,35 @@ function sidePorts(node, side) {
3410
3449
  const ports = node.ports.filter((port) => port.side === side);
3411
3450
  return side === "WEST" || side === "SOUTH" ? ports.reverse() : ports;
3412
3451
  }
3452
+ function greedySwitchTopology(graph) {
3453
+ const topology = {
3454
+ incoming: /* @__PURE__ */ new Map(),
3455
+ outgoing: /* @__PURE__ */ new Map(),
3456
+ layerByPort: /* @__PURE__ */ new Map(),
3457
+ ownerByPort: /* @__PURE__ */ new Map()
3458
+ };
3459
+ for (const [layer, nodes] of graph.layers.entries()) for (const node of nodes) for (const port of node.ports) {
3460
+ topology.layerByPort.set(port.id, layer);
3461
+ topology.ownerByPort.set(port.id, node);
3462
+ topology.incoming.set(port.id, []);
3463
+ topology.outgoing.set(port.id, []);
3464
+ }
3465
+ for (const edge of graph.edges) {
3466
+ topology.outgoing.get(edge.source)?.push(edge.target);
3467
+ topology.incoming.get(edge.target)?.push(edge.source);
3468
+ }
3469
+ return topology;
3470
+ }
3413
3471
  /**
3414
3472
  * Decides greedy switches in one free layer from local two-node crossing
3415
3473
  * counts, exactly as ELK does; these estimates can differ from a global recount.
3416
3474
  */
3417
3475
  var GreedySwitchDecider = class {
3418
3476
  #free;
3419
- #incoming = /* @__PURE__ */ new Map();
3420
- #outgoing = /* @__PURE__ */ new Map();
3421
- #layerByPort = /* @__PURE__ */ new Map();
3422
- #ownerByPort = /* @__PURE__ */ new Map();
3477
+ #incoming;
3478
+ #outgoing;
3479
+ #layerByPort;
3480
+ #ownerByPort;
3423
3481
  #neighbourPositions = /* @__PURE__ */ new Map();
3424
3482
  #adjacencies = /* @__PURE__ */ new Map();
3425
3483
  #matrix = /* @__PURE__ */ new Map();
@@ -3427,19 +3485,13 @@ var GreedySwitchDecider = class {
3427
3485
  #cardinality = /* @__PURE__ */ new Map();
3428
3486
  #inLayerSize = /* @__PURE__ */ new Map();
3429
3487
  #oneSided;
3430
- constructor(graph, freeLayerIndex, oneSided) {
3488
+ constructor(graph, freeLayerIndex, oneSided, topology = greedySwitchTopology(graph)) {
3431
3489
  this.#free = graph.layers[freeLayerIndex] ?? [];
3432
3490
  this.#oneSided = oneSided;
3433
- for (const [layer, nodes] of graph.layers.entries()) for (const node of nodes) for (const port of node.ports) {
3434
- this.#layerByPort.set(port.id, layer);
3435
- this.#ownerByPort.set(port.id, node);
3436
- this.#incoming.set(port.id, []);
3437
- this.#outgoing.set(port.id, []);
3438
- }
3439
- for (const edge of graph.edges) {
3440
- this.#outgoing.get(edge.source)?.push(edge.target);
3441
- this.#incoming.get(edge.target)?.push(edge.source);
3442
- }
3491
+ this.#incoming = topology.incoming;
3492
+ this.#outgoing = topology.outgoing;
3493
+ this.#layerByPort = topology.layerByPort;
3494
+ this.#ownerByPort = topology.ownerByPort;
3443
3495
  for (const [layerIndex, side] of [[freeLayerIndex - 1, "EAST"], [freeLayerIndex + 1, "WEST"]]) {
3444
3496
  let position = 0;
3445
3497
  for (const node of graph.layers[layerIndex] ?? []) for (const port of sidePorts(node, side)) this.#neighbourPositions.set(port.id, position++);
@@ -4311,6 +4363,7 @@ function applyGreedySwitch(input, orientation, order) {
4311
4363
  if (oneSidedBoundary !== void 0) return countBoundary(oneSidedBoundary);
4312
4364
  return (layerIndex > 0 ? countBoundary(layerIndex - 1) : 0) + (layerIndex + 1 < layers.length ? countBoundary(layerIndex) : 0);
4313
4365
  };
4366
+ let greedyTopology;
4314
4367
  const sweep = (forward$1) => {
4315
4368
  let changed = false;
4316
4369
  const indices = forward$1 ? Array.from({ length: layers.length }, (_, index) => index) : Array.from({ length: layers.length }, (_, index) => layers.length - index - 1);
@@ -4320,7 +4373,7 @@ function applyGreedySwitch(input, orientation, order) {
4320
4373
  if (greedyGraph) {
4321
4374
  const units = getCrossingUnits(input);
4322
4375
  greedyGraph.layers = layers.map((ids) => ids.map((id) => greedyNodes.get(id)));
4323
- const decider = new GreedySwitchDecider(greedyGraph, layerIndex, type === "ONE_SIDED" ? forward$1 ? "WEST" : "EAST" : void 0);
4376
+ const decider = new GreedySwitchDecider(greedyGraph, layerIndex, type === "ONE_SIDED" ? forward$1 ? "WEST" : "EAST" : void 0, greedyTopology ??= greedySwitchTopology(greedyGraph));
4324
4377
  do {
4325
4378
  improved$1 = false;
4326
4379
  for (let index = 0; index + 1 < layer.length; index++) {
@@ -4913,7 +4966,9 @@ function sortLayerByAdjacentPosition(layer, ranksByNodeId, statistic, random, pr
4913
4966
  if (units && !forcedCompare) layer.splice(0, layer.length, ...resolveCrossingConstraints(layer, adjacentPosition, units));
4914
4967
  }
4915
4968
  function clonePortOrders(orders) {
4916
- return new Map([...orders ?? []].map(([id, edges]) => [id, [...edges]]));
4969
+ const clone = /* @__PURE__ */ new Map();
4970
+ if (orders) for (const [id, edges] of orders) clone.set(id, edges.slice());
4971
+ return clone;
4917
4972
  }
4918
4973
  function minimizeCrossingsWithLayerSweep(statistic, sweeps = 7) {
4919
4974
  return (input, orientation, assignment) => createLayerSweepSession(input, orientation, assignment, statistic, sweeps).minimize();
@@ -5006,14 +5061,16 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5006
5061
  const buildCrossingGraph$1 = createCrossingGraphBuilder(input);
5007
5062
  const crossingModel = input.settings[CROSSING_MODEL] ?? {};
5008
5063
  const mixedPorts = crossingModel.mixedPorts !== false, probe = crossingModel.probe;
5064
+ const physicalSlot = /* @__PURE__ */ new Map();
5009
5065
  const countCrossings = (candidateLayers) => {
5010
5066
  if (exactPortSweep) {
5011
5067
  const graph = buildCrossingGraph$1(orientation, candidateLayers, inputPortOrder, outputPortOrder);
5012
5068
  for (const layer of graph.layers) for (const node of layer) {
5013
5069
  const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : void 0;
5014
5070
  if (physical?.length !== node.ports.length) continue;
5015
- const slot = new Map(physical.map((port, index) => [port.id, index]));
5016
- if (node.ports.every((port) => slot.has(port.id))) node.ports.sort((a, b) => slot.get(a.id) - slot.get(b.id));
5071
+ physicalSlot.clear();
5072
+ for (const [index, port] of physical.entries()) physicalSlot.set(port.id, index);
5073
+ if (node.ports.every((port) => physicalSlot.has(port.id))) node.ports.sort((a, b) => physicalSlot.get(a.id) - physicalSlot.get(b.id));
5017
5074
  }
5018
5075
  const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
5019
5076
  const score = countAllCrossings(split, crossingModel.separateSharedTracks ? "separated" : "hyperedges");
@@ -5115,6 +5172,17 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5115
5172
  }
5116
5173
  for (const id of freeLayer) {
5117
5174
  const order = (forward ? inputPortOrder : outputPortOrder).get(id) ?? [];
5175
+ const visit = (edgeId) => {
5176
+ const [source, target] = getOrientedEndpoints(edgeById.get(edgeId), orientation);
5177
+ const neighbor = forward ? source : target;
5178
+ const rank$1 = edgeRanks.get(edgeId);
5179
+ if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id)) visits.get(id).push(neighbor);
5180
+ else if (rank$1 !== void 0) visits.get(id).push(rank$1);
5181
+ };
5182
+ if (order.length < 2) {
5183
+ for (const edgeId of order) visit(edgeId);
5184
+ continue;
5185
+ }
5118
5186
  const groups = /* @__PURE__ */ new Map();
5119
5187
  for (const edgeId of order) {
5120
5188
  const edge = edgeById.get(edgeId);
@@ -5123,17 +5191,11 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5123
5191
  if (group) group.push(edgeId);
5124
5192
  else groups.set(key, [edgeId]);
5125
5193
  }
5194
+ const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
5195
+ const rank = (edgeId) => listed?.get(listedEdgeId(edgeId));
5126
5196
  for (const group of groups.values()) {
5127
- const listed = units?.edgeListRanks?.[forward ? "incoming" : "outgoing"].get(id);
5128
- const rank = (edgeId) => listed?.get(listedEdgeId(edgeId));
5129
- group.sort((a, b) => (rank(a) !== void 0 && rank(b) !== void 0 ? rank(a) - rank(b) : 0) || edgeModelOrder.get(a) - edgeModelOrder.get(b));
5130
- for (const edgeId of group) {
5131
- const [source, target] = getOrientedEndpoints(edgeById.get(edgeId), orientation);
5132
- const neighbor = forward ? source : target;
5133
- const rank$1 = edgeRanks.get(edgeId);
5134
- if (assignment.layerByNodeId.get(neighbor) === assignment.layerByNodeId.get(id)) visits.get(id).push(neighbor);
5135
- else if (rank$1 !== void 0) visits.get(id).push(rank$1);
5136
- }
5197
+ if (group.length > 1) group.sort((a, b) => (rank(a) !== void 0 && rank(b) !== void 0 ? rank(a) - rank(b) : 0) || edgeModelOrder.get(a) - edgeModelOrder.get(b));
5198
+ for (const edgeId of group) visit(edgeId);
5137
5199
  }
5138
5200
  }
5139
5201
  return {
@@ -5181,6 +5243,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5181
5243
  hierarchical: hierarchicalNodes
5182
5244
  });
5183
5245
  for (const index of touched) for (const node of canonicalGraph.layers[index]) {
5246
+ if (node.ports.length < 2 || (outputPortOrder.get(node.id)?.length ?? 0) < 2 && (inputPortOrder.get(node.id)?.length ?? 0) < 2) continue;
5184
5247
  const ordinal = new Map(node.ports.map((port, index$1) => [port.id, index$1]));
5185
5248
  for (const [orders, source] of [[outputPortOrder, true], [inputPortOrder, false]]) orders.get(node.id)?.sort((left, right) => ordinal.get(canonicalEndpoints.get(left)[source ? "source" : "target"]) - ordinal.get(canonicalEndpoints.get(right)[source ? "source" : "target"]));
5186
5249
  }
@@ -5398,12 +5461,16 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5398
5461
  shuffleFirstLayer,
5399
5462
  sweep,
5400
5463
  countCrossings: () => countCrossings(working),
5401
- snapshot: () => ({
5402
- physicalPortOrderByNodeId: new Map([...canonicalNodes].map(([id, node]) => [id, node.ports.map((port) => port.id)])),
5403
- layers: working.map((layer) => [...layer]),
5404
- inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
5405
- outputPortOrderByNodeId: clonePortOrders(outputPortOrder)
5406
- }),
5464
+ snapshot: () => {
5465
+ const physicalPortOrderByNodeId = /* @__PURE__ */ new Map();
5466
+ for (const [id, node] of canonicalNodes) physicalPortOrderByNodeId.set(id, node.ports.map((port) => port.id));
5467
+ return {
5468
+ physicalPortOrderByNodeId,
5469
+ layers: working.map((layer) => [...layer]),
5470
+ inputPortOrderByNodeId: clonePortOrders(inputPortOrder),
5471
+ outputPortOrderByNodeId: clonePortOrders(outputPortOrder)
5472
+ };
5473
+ },
5407
5474
  restore,
5408
5475
  finish,
5409
5476
  currentLayers: () => working,
@@ -5813,7 +5880,7 @@ function sumWithSpacing(ids, input, axis) {
5813
5880
  }, 0);
5814
5881
  }
5815
5882
  function nodeFlowOffset(input, id, layerFlowSize, nodeFlowSize, layerByNodeId, margins) {
5816
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
5883
+ const node = findNode(input.graph.nodes, id);
5817
5884
  const alignment = node ? input.nodeSettings?.(node)?.alignment ?? input.settings.alignment : input.settings.alignment;
5818
5885
  let ratio;
5819
5886
  if (alignment === "LEFT" || alignment === "TOP") ratio = 0;
@@ -6062,7 +6129,56 @@ function simplifyRoute$1(points) {
6062
6129
  function implicitEdgeEndpoints(input, placement, orientation) {
6063
6130
  const horizontal$1 = input.direction === "left" || input.direction === "right";
6064
6131
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
6065
- const groups = /* @__PURE__ */ new Map();
6132
+ const nodeSettingsByNode = /* @__PURE__ */ new Map();
6133
+ const nodeSettingsOf = (node) => {
6134
+ if (!nodeSettingsByNode.has(node)) nodeSettingsByNode.set(node, input.nodeSettings?.(node));
6135
+ return nodeSettingsByNode.get(node);
6136
+ };
6137
+ const portSettingsByNode = /* @__PURE__ */ new Map();
6138
+ const portSettingsOf = (port, node) => {
6139
+ let byPort = portSettingsByNode.get(node);
6140
+ if (!byPort) portSettingsByNode.set(node, byPort = /* @__PURE__ */ new Map());
6141
+ if (!byPort.has(port)) byPort.set(port, input.portSettings?.(port, node));
6142
+ return byPort.get(port);
6143
+ };
6144
+ const groups = [];
6145
+ const groupsBySide = /* @__PURE__ */ new Map();
6146
+ const modelOrderPromotion = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
6147
+ const directionReversed = input.direction === "left" || input.direction === "up";
6148
+ const endpointSide = (nodeId, portName) => {
6149
+ const node = nodeById.get(nodeId);
6150
+ const port = node?.ports?.find((candidate) => candidate.name === portName);
6151
+ const constraints = node ? nodeSettingsOf(node)?.portConstraints : void 0;
6152
+ if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
6153
+ const configured = portSettingsOf(port, node)?.["port.side"];
6154
+ return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
6155
+ };
6156
+ const fixedImplicitSide = (nodeId, portName, source) => {
6157
+ const node = nodeById.get(nodeId);
6158
+ const constraints = node && nodeSettingsOf(node)?.portConstraints;
6159
+ if (portName !== void 0 || ![
6160
+ "FIXED_SIDE",
6161
+ "FIXED_ORDER",
6162
+ "FIXED_RATIO",
6163
+ "FIXED_POS"
6164
+ ].includes(String(constraints))) return void 0;
6165
+ return source !== directionReversed ? "after" : "before";
6166
+ };
6167
+ const addEntry = (nodeId, side, entry) => {
6168
+ let byNode = groupsBySide.get(side);
6169
+ if (!byNode) groupsBySide.set(side, byNode = /* @__PURE__ */ new Map());
6170
+ const group = byNode.get(nodeId);
6171
+ if (group) group.entries.push(entry);
6172
+ else {
6173
+ const created = {
6174
+ nodeId,
6175
+ side,
6176
+ entries: [entry]
6177
+ };
6178
+ byNode.set(nodeId, created);
6179
+ groups.push(created);
6180
+ }
6181
+ };
6066
6182
  for (const edge of input.graph.edges) {
6067
6183
  if (hasMovableLoopPorts(input, edge)) continue;
6068
6184
  const sourceRect = placement.rectByNodeId.get(edge.sourceId);
@@ -6070,47 +6186,20 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6070
6186
  if (!sourceRect || !targetRect) continue;
6071
6187
  const sourceFlow = horizontal$1 ? sourceRect.x : sourceRect.y;
6072
6188
  const targetFlow = horizontal$1 ? targetRect.x : targetRect.y;
6073
- const modelOrderPromotion = input.settings["layering.nodePromotion.strategy"] === "MODEL_ORDER_LEFT_TO_RIGHT";
6074
6189
  const sourceBoundary = externalPortDummyOf(nodeById.get(edge.sourceId) ?? {});
6075
6190
  const targetBoundary = externalPortDummyOf(nodeById.get(edge.targetId) ?? {});
6076
6191
  const forward = modelOrderPromotion || (sourceFlow === targetFlow && (sourceBoundary || targetBoundary) ? sourceBoundary ? sourceBoundary.port.side === "EAST" || sourceBoundary.port.side === "SOUTH" : targetBoundary.port.side === "WEST" || targetBoundary.port.side === "NORTH" : sourceFlow <= targetFlow);
6077
6192
  const feedback = input.settings.feedbackEdges === true && orientation?.reversedEdgeIds.has(edge.id) === true;
6078
- const directionReversed = input.direction === "left" || input.direction === "up";
6079
- const endpointSide = (nodeId, portName) => {
6080
- const node = nodeById.get(nodeId);
6081
- const port = node?.ports?.find((candidate) => candidate.name === portName);
6082
- const constraints = node ? input.nodeSettings?.(node)?.portConstraints : void 0;
6083
- if (!node || !port || constraints !== "FIXED_SIDE" && constraints !== "FIXED_ORDER" && constraints !== "FIXED_RATIO" && constraints !== "FIXED_POS") return;
6084
- const configured = input.portSettings?.(port, node)?.["port.side"];
6085
- return configured === "WEST" || configured === "NORTH" ? "before" : configured === "EAST" || configured === "SOUTH" ? "after" : void 0;
6086
- };
6087
- const fixedImplicitSide = (nodeId, portName, source) => {
6088
- const node = nodeById.get(nodeId);
6089
- const constraints = node && input.nodeSettings?.(node)?.portConstraints;
6090
- if (portName !== void 0 || ![
6091
- "FIXED_SIDE",
6092
- "FIXED_ORDER",
6093
- "FIXED_RATIO",
6094
- "FIXED_POS"
6095
- ].includes(String(constraints))) return void 0;
6096
- return source !== directionReversed ? "after" : "before";
6097
- };
6098
6193
  const sourceSide = endpointSide(edge.sourceId, edge.sourcePort) ?? fixedImplicitSide(edge.sourceId, edge.sourcePort, true) ?? (feedback ? directionReversed ? "before" : "after" : forward ? "after" : "before");
6099
6194
  const targetSide = endpointSide(edge.targetId, edge.targetPort) ?? fixedImplicitSide(edge.targetId, edge.targetPort, false) ?? (feedback ? directionReversed ? "after" : "before" : forward ? "before" : "after");
6100
- const sourceKey = `${edge.sourceId}:${sourceSide}`;
6101
- const targetKey = `${edge.targetId}:${targetSide}`;
6102
- const sourceGroup = groups.get(sourceKey) ?? [];
6103
- sourceGroup.push({
6195
+ addEntry(edge.sourceId, sourceSide, {
6104
6196
  edge,
6105
6197
  endpoint: "source"
6106
6198
  });
6107
- groups.set(sourceKey, sourceGroup);
6108
- const targetGroup = groups.get(targetKey) ?? [];
6109
- targetGroup.push({
6199
+ addEntry(edge.targetId, targetSide, {
6110
6200
  edge,
6111
6201
  endpoint: "target"
6112
6202
  });
6113
- groups.set(targetKey, targetGroup);
6114
6203
  }
6115
6204
  const result = /* @__PURE__ */ new Map();
6116
6205
  const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
@@ -6131,22 +6220,19 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6131
6220
  if (!counts) {
6132
6221
  counts = /* @__PURE__ */ new Map();
6133
6222
  for (const port of node.ports ?? []) {
6134
- const portSide = input.portSettings?.(port, node)?.["port.side"];
6223
+ const portSide = portSettingsOf(port, node)?.["port.side"];
6135
6224
  counts.set(portSide, (counts.get(portSide) ?? 0) + 1);
6136
6225
  }
6137
6226
  portSideCounts.set(node, counts);
6138
6227
  }
6139
6228
  return counts.get(side) ?? 0;
6140
6229
  };
6141
- for (const [key, entries] of groups) {
6142
- const split = key.lastIndexOf(":");
6143
- const nodeId = key.slice(0, split);
6144
- const side = key.slice(split + 1);
6230
+ for (const { nodeId, side, entries } of groups) {
6145
6231
  const rect = placement.rectByNodeId.get(nodeId);
6146
6232
  if (!rect) continue;
6147
6233
  const mergeEdges = input.settings.mergeEdges === true;
6148
6234
  const node = nodeById.get(nodeId);
6149
- const nodeSettings = node ? input.nodeSettings?.(node) : void 0;
6235
+ const nodeSettings = node ? nodeSettingsOf(node) : void 0;
6150
6236
  const hypernode = nodeSettings?.hypernode;
6151
6237
  const unzipping = (input.settings["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING";
6152
6238
  const crossingStrategy = input.settings["crossingMinimization.strategy"] ?? "LAYER_SWEEP";
@@ -6238,9 +6324,9 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6238
6324
  };
6239
6325
  const portName = endpoint === "source" ? edge.sourcePort : edge.targetPort;
6240
6326
  const explicitPort = node?.ports?.find((port) => port.name === portName);
6241
- const physicalSide = node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.side"] : void 0;
6327
+ const physicalSide = node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.side"] : void 0;
6242
6328
  const singleFixedSidePort = constraints === "FIXED_SIDE" && physicalSide !== void 0 && node?.ports !== void 0 && portSideCount(node, physicalSide) === 1;
6243
- const authoredAnchor = node && explicitPort ? input.portSettings?.(explicitPort, node)?.["port.anchor"] : void 0;
6329
+ const authoredAnchor = node && explicitPort ? portSettingsOf(explicitPort, node)?.["port.anchor"] : void 0;
6244
6330
  if (portName !== void 0 && node && (constraints === "FIXED_POS" || constraints === "FIXED_RATIO" || constraints === "FIXED_ORDER" || singleFixedSidePort || authoredAnchor !== void 0)) point = getPortPoint(node, portName, rect, point, input.direction, input, placedPorts);
6245
6331
  const pair = result.get(edge.id) ?? {
6246
6332
  source: point,
@@ -6451,6 +6537,14 @@ function routeEdges(style) {
6451
6537
  flowLayerByNodeId.set(interval.id, flowLayers.length - 1);
6452
6538
  }
6453
6539
  const bodyFlowLayers = flowLayers.map((bounds) => ({ ...bounds }));
6540
+ const placedIdsByFlowLayer = () => {
6541
+ const ids = flowLayers.map(() => []);
6542
+ for (const id of placement.rectByNodeId.keys()) {
6543
+ const layer = flowLayerByNodeId.get(id);
6544
+ if (layer !== void 0) ids[layer].push(id);
6545
+ }
6546
+ return ids;
6547
+ };
6454
6548
  const labelFlowLayers = new Set([...flowLayerByNodeId].filter(([id]) => id.startsWith("__layout_dummy:label:")).map(([, layer]) => layer));
6455
6549
  for (const [id, rect] of placement.rectByNodeId) {
6456
6550
  const node = nodeById.get(id);
@@ -6527,10 +6621,11 @@ function routeEdges(style) {
6527
6621
  labelExtraByGap[gap] = Math.max(labelExtraByGap[gap] ?? 0, extra);
6528
6622
  }
6529
6623
  let labelShift = 0;
6624
+ const labelShiftIds = placedIdsByFlowLayer();
6530
6625
  for (const [layerNo, bounds] of flowLayers.entries()) {
6531
6626
  if (labelShift !== 0) {
6532
- for (const [id, rect] of placement.rectByNodeId) {
6533
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
6627
+ for (const id of labelShiftIds[layerNo]) {
6628
+ const rect = placement.rectByNodeId.get(id);
6534
6629
  mutableRects.set(id, horizontal$1 ? {
6535
6630
  ...rect,
6536
6631
  x: rect.x + labelShift
@@ -6878,18 +6973,21 @@ function routeEdges(style) {
6878
6973
  const leadingSlots = slotsByGap[0] ?? 0;
6879
6974
  const leadingWidth = leadingSlots > 0 ? edgeNodeSpacing + (leadingSlots - 1) * edgeEdgeSpacing : 0;
6880
6975
  let nextStart = (flowLayers[0]?.start ?? 0) + leadingWidth;
6976
+ const flowLayerIds = flowLayers.map(() => []);
6977
+ for (const [id, layer] of flowLayerByNodeId) flowLayerIds[layer].push(id);
6881
6978
  const externalFlowLayers = new Set(flowLayers.flatMap((_, layer) => {
6882
- const ids = [...flowLayerByNodeId].filter(([, index]) => index === layer);
6883
- return ids.length && ids.every(([id]) => {
6979
+ const ids = flowLayerIds[layer];
6980
+ return ids.length && ids.every((id) => {
6884
6981
  const node = nodeById.get(id);
6885
6982
  const side = node && externalPortDummyOf(node)?.side;
6886
6983
  return horizontal$1 ? side === "WEST" || side === "EAST" : side === "NORTH" || side === "SOUTH";
6887
6984
  }) ? [layer] : [];
6888
6985
  }));
6986
+ const shiftedIds = placedIdsByFlowLayer();
6889
6987
  for (const [layerNo, bounds] of flowLayers.entries()) {
6890
6988
  const shift = nextStart - bounds.start;
6891
- for (const [id, rect] of placement.rectByNodeId) {
6892
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
6989
+ for (const id of shiftedIds[layerNo]) {
6990
+ const rect = placement.rectByNodeId.get(id);
6893
6991
  mutableRects.set(id, horizontal$1 ? {
6894
6992
  ...rect,
6895
6993
  x: rect.x + shift
@@ -7103,10 +7201,11 @@ function routeEdges(style) {
7103
7201
  });
7104
7202
  }
7105
7203
  let nextStart = flowLayers[0]?.start ?? 0;
7204
+ const shiftedIds = placedIdsByFlowLayer();
7106
7205
  for (const [layerNo, bounds] of flowLayers.entries()) {
7107
7206
  const shift = nextStart - bounds.start;
7108
- for (const [id, rect] of placement.rectByNodeId) {
7109
- if (flowLayerByNodeId.get(id) !== layerNo) continue;
7207
+ for (const id of shiftedIds[layerNo]) {
7208
+ const rect = placement.rectByNodeId.get(id);
7110
7209
  mutableRects.set(id, horizontal$1 ? {
7111
7210
  ...rect,
7112
7211
  x: rect.x + shift
@@ -7979,7 +8078,7 @@ function normalizePlacementForPortExtents(input, placement, order, orientation)
7979
8078
  let trailingBoundary = Number.NEGATIVE_INFINITY;
7980
8079
  let leadingBoundary = Number.POSITIVE_INFINITY;
7981
8080
  for (const [ids, trailing] of [[layer, true], [nextLayer, false]]) for (const id of ids) {
7982
- const node = input.graph.nodes.find((candidate) => candidate.id === id);
8081
+ const node = findNode(input.graph.nodes, id);
7983
8082
  const rect = placement.rectByNodeId.get(id);
7984
8083
  if (!node || !rect) continue;
7985
8084
  const bodyStart = horizontal$1 ? rect.x : rect.y;
@@ -14799,10 +14898,16 @@ function buildNeighbors(input, order) {
14799
14898
  return (horizontal$1 ? port?.height : port?.width) ?? 0;
14800
14899
  });
14801
14900
  };
14901
+ const loopsByNodeId = /* @__PURE__ */ new Map();
14902
+ for (const edge of input.graph.edges) if (edge.sourceId === edge.targetId) {
14903
+ const loops = loopsByNodeId.get(edge.sourceId);
14904
+ if (loops) loops.push(edge);
14905
+ else loopsByNodeId.set(edge.sourceId, [edge]);
14906
+ }
14802
14907
  const loopTerminals = (id, side) => {
14803
14908
  const node = nodeById.get(id);
14804
14909
  if (!node) return 0;
14805
- return input.graph.edges.filter((edge) => edge.sourceId === id && edge.targetId === id).flatMap((edge) => [edge.sourcePort, edge.targetPort]).filter((name) => {
14910
+ return (loopsByNodeId.get(id) ?? []).flatMap((edge) => [edge.sourcePort, edge.targetPort]).filter((name) => {
14806
14911
  const port = node.ports?.find((candidate) => candidate.name === name);
14807
14912
  return port !== void 0 && input.portSettings?.(port, node)?.["port.side"] === side.toUpperCase() && usesSharedSideSlot(input, node, name);
14808
14913
  }).length;
@@ -14913,8 +15018,8 @@ function alignBlocks(input, order, bal, neighbors, markedEdges) {
14913
15018
  const adjacent = (bal.hdir === "LEFT" ? neighbors.right : neighbors.left).get(id) ?? [];
14914
15019
  const low = Math.floor((adjacent.length + 1) / 2) - 1;
14915
15020
  const high = Math.ceil((adjacent.length + 1) / 2) - 1;
14916
- const indices = bal.vdir === "UP" ? Array.from({ length: Math.max(0, high - low + 1) }, (_, i) => high - i) : Array.from({ length: Math.max(0, high - low + 1) }, (_, i) => low + i);
14917
- for (const median of indices) {
15021
+ for (let i = 0; i < high - low + 1; i++) {
15022
+ const median = bal.vdir === "UP" ? high - i : low + i;
14918
15023
  if (bal.align.get(id) !== id) break;
14919
15024
  const neighbor = adjacent[median];
14920
15025
  if (neighbor === void 0) continue;
@@ -18743,11 +18848,14 @@ function* createLayeredScopePipeline(graph, options, context) {
18743
18848
  settings: options.settings ?? {},
18744
18849
  ...options.nodeSettings === void 0 ? {} : { nodeSettings: options.nodeSettings },
18745
18850
  ...options.edgeSettings === void 0 ? {} : { edgeSettings: options.edgeSettings },
18746
- portSettings: (port, node) => ({
18747
- ...options.portSettings?.(port, node),
18748
- ...parallelPortIndexByKey.has(`${node.id}\0${port.name}`) ? { "port.index": parallelPortIndexByKey.get(`${node.id}\0${port.name}`) } : {},
18749
- ...switchedSideByPort.has(`${node.id}\0${port.name}`) ? { "port.side": switchedSideByPort.get(`${node.id}\0${port.name}`) } : {}
18750
- })
18851
+ portSettings: (port, node) => {
18852
+ const key = `${node.id}\0${port.name}`;
18853
+ return {
18854
+ ...options.portSettings?.(port, node),
18855
+ ...parallelPortIndexByKey.has(key) ? { "port.index": parallelPortIndexByKey.get(key) } : {},
18856
+ ...switchedSideByPort.has(key) ? { "port.side": switchedSideByPort.get(key) } : {}
18857
+ };
18858
+ }
18751
18859
  };
18752
18860
  const measure = (id, run) => {
18753
18861
  context?.throwIfAborted();
@@ -18893,8 +19001,10 @@ function* createLayeredScopePipeline(graph, options, context) {
18893
19001
  const northEdges = /* @__PURE__ */ new Set(), southEdges = /* @__PURE__ */ new Set();
18894
19002
  const upper = direction === "right" || direction === "left" ? "NORTH" : "WEST";
18895
19003
  const lower = direction === "right" || direction === "left" ? "SOUTH" : "EAST";
19004
+ const endpointNodes = /* @__PURE__ */ new Map();
19005
+ for (const node of expanded.input.graph.nodes) if (!endpointNodes.has(node.id)) endpointNodes.set(node.id, node);
18896
19006
  for (const edge of expanded.input.graph.edges) for (const [id, name] of [[edge.sourceId, edge.sourcePort], [edge.targetId, edge.targetPort]]) {
18897
- const node = expanded.input.graph.nodes.find((n) => n.id === id), port = node?.ports?.find((p) => p.name === name);
19007
+ const node = endpointNodes.get(id), port = node?.ports?.find((p) => p.name === name);
18898
19008
  const side = node && port ? expanded.input.portSettings?.(port, node)?.["port.side"] : void 0;
18899
19009
  if (side === upper) northEdges.add(id);
18900
19010
  if (side === lower) southEdges.add(id);