@statelyai/layout 0.4.0 → 0.4.1

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.
@@ -506,10 +506,18 @@ function createOrthogonalHypersegments(ports, connections, listRank) {
506
506
  segmentByPort
507
507
  };
508
508
  }
509
- /** ELK's hyperedge crossing estimate; positions are distinct ordinal port ranks on each side. */
510
- function countOrthogonalHypersegmentCrossings(ports, connections) {
511
- const { segments } = createOrthogonalHypersegments(ports, connections);
512
- const spans = segments.map((s) => ({
509
+ /**
510
+ * {@link countOrthogonalHypersegmentCrossings}, optionally separating shared
511
+ * tracks. ELK routes a hyperedge on one shared track. When it joins two or
512
+ * more ports on each side, its edges would run opposite ways along that
513
+ * track, which routing never allows; `separateShared` counts such a
514
+ * hyperedge's edges one by one instead. `shared` reports whether one exists.
515
+ */
516
+ function scoreOrthogonalHypersegments(ports, connections, separateShared = false) {
517
+ const { segments: merged, segmentByPort } = createOrthogonalHypersegments(ports, connections);
518
+ const sharedSegments = new Set(merged.flatMap((s, i) => s.incoming.length > 1 && s.outgoing.length > 1 ? [i] : []));
519
+ const separating = separateShared && sharedSegments.size > 0;
520
+ const spans = merged.filter((_, i) => !separating || !sharedSegments.has(i)).map((s) => ({
513
521
  leftStart: Math.min(...s.incoming),
514
522
  leftEnd: Math.max(...s.incoming),
515
523
  rightStart: Math.min(...s.outgoing),
@@ -522,6 +530,43 @@ function countOrthogonalHypersegmentCrossings(ports, connections) {
522
530
  if (a.leftStart < b.leftEnd && b.leftStart < a.leftEnd) count++;
523
531
  if (a.rightStart < b.rightEnd && b.rightStart < a.rightEnd) count++;
524
532
  }
533
+ if (separating) {
534
+ const position = new Map(ports.map((p) => [p.id, p.position]));
535
+ const edges = connections.filter((c$1) => sharedSegments.has(segmentByPort.get(c$1.source))).map((c$1) => [position.get(c$1.source), position.get(c$1.target)]);
536
+ for (const [left, right] of edges) for (const b of spans) {
537
+ if ((left - b.leftStart) * (right - b.rightStart) < 0) count++;
538
+ if (left < b.leftEnd && b.leftStart < left) count++;
539
+ if (right < b.rightEnd && b.rightStart < right) count++;
540
+ }
541
+ count += strictInversions(edges);
542
+ }
543
+ return {
544
+ count,
545
+ shared: sharedSegments.size > 0
546
+ };
547
+ }
548
+ /** Pairs whose ends are strictly inverted: two separated edges that cross, in O(n log n). */
549
+ function strictInversions(pairs) {
550
+ const ranks = [...new Set(pairs.map(([, right]) => right))].sort((a, b) => a - b);
551
+ const rankOf = new Map(ranks.map((value, index) => [value, index + 1]));
552
+ const tree = new Array(ranks.length + 1).fill(0);
553
+ const atMost = (rank) => {
554
+ let sum = 0;
555
+ for (let i = rank; i > 0; i -= i & -i) sum += tree[i];
556
+ return sum;
557
+ };
558
+ const sorted = [...pairs].sort((a, b) => a[0] - b[0]);
559
+ let inserted = 0, count = 0;
560
+ for (let start = 0; start < sorted.length;) {
561
+ let end = start;
562
+ while (end < sorted.length && sorted[end][0] === sorted[start][0]) end++;
563
+ for (let i = start; i < end; i++) count += inserted - atMost(rankOf.get(sorted[i][1]));
564
+ for (let i = start; i < end; i++) {
565
+ for (let r = rankOf.get(sorted[i][1]); r <= ranks.length; r += r & -r) tree[r]++;
566
+ inserted++;
567
+ }
568
+ start = end;
569
+ }
525
570
  return count;
526
571
  }
527
572
 
@@ -650,21 +695,22 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
650
695
  betweenLayers: 0,
651
696
  inLayer: 0,
652
697
  northSouth: 0,
653
- total: 0
698
+ total: 0,
699
+ sharedTracks: false
654
700
  };
655
701
  if (!graph.layers.length) return score;
656
702
  score.inLayer += countPorts(face(graph.layers[0], "WEST"), true);
657
703
  score.inLayer += countPorts(face(graph.layers.at(-1), "EAST"), true);
658
704
  for (const [index, layer] of graph.layers.entries()) {
659
705
  const next = graph.layers[index + 1];
660
- if (next) if ((layer.some((node) => node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1)) || next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1))) && boundaryMode === "hyperedges") {
706
+ if (next) if ((layer.some((node) => node.ports.some((port) => port.side === "EAST" && degree(port.id) > 1)) || next.some((node) => node.ports.some((port) => port.side === "WEST" && degree(port.id) > 1))) && boundaryMode !== "edges") {
661
707
  const sourcePorts = layer.flatMap((node) => node.ports).filter((port) => outgoing.get(port.id).some((peer) => layerByPort.get(peer) !== index));
662
708
  const targetPorts = next.flatMap((node) => {
663
709
  const connectedPorts = node.ports.filter((port) => incoming.get(port.id).some((peer) => layerByPort.get(peer) !== index + 1));
664
710
  return [...connectedPorts.filter((port) => port.side === "NORTH").reverse(), ...connectedPorts.filter((port) => port.side !== "NORTH").reverse()];
665
711
  });
666
712
  const sources = new Set(sourcePorts.map((port) => port.id)), targets = new Set(targetPorts.map((port) => port.id));
667
- score.betweenLayers += countOrthogonalHypersegmentCrossings([...sourcePorts.map((port, position) => ({
713
+ const hyperedges = scoreOrthogonalHypersegments([...sourcePorts.map((port, position) => ({
668
714
  id: port.id,
669
715
  side: "source",
670
716
  position
@@ -672,7 +718,9 @@ function countAllCrossings(graph, boundaryMode = "hyperedges") {
672
718
  id: port.id,
673
719
  side: "target",
674
720
  position
675
- }))], graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)));
721
+ }))], graph.edges.filter((edge) => sources.has(edge.source) && targets.has(edge.target)), boundaryMode === "separated");
722
+ score.betweenLayers += hyperedges.count;
723
+ score.sharedTracks ||= hyperedges.shared;
676
724
  score.inLayer += countPorts(face(layer, "EAST"), true) + countPorts(face(next, "WEST"), true);
677
725
  } else score.betweenLayers += countPorts([...face(layer, "EAST"), ...face(next, "WEST", false)], false);
678
726
  if (layer.some((node) => node.type === "NORTH_SOUTH_PORT")) score.northSouth += countNorthSouth(layer);
@@ -3641,6 +3689,18 @@ function cycleModelOrder(input) {
3641
3689
  const count = Math.max(1, input.graph.nodes.length);
3642
3690
  return new Map(input.graph.nodes.map((node, index) => [node.id, enforced ? Number(input.nodeSettings?.(node)?.["considerModelOrder.groupModelOrder.cycleBreakingId"] ?? 0) * count + index : index]));
3643
3691
  }
3692
+ /**
3693
+ * Internal setting: how crossing minimization models ports and tracks.
3694
+ * `mixedPorts` (default true) counts a port carrying both directions as two
3695
+ * ports and groups its neighbours by direction. `separateSharedTracks`
3696
+ * (default false) counts a hyperedge joining two or more ports on each side
3697
+ * of its gap edge by edge, since routing never lets edges run opposite ways
3698
+ * along one track. A `probe` records whether either choice can change the
3699
+ * order. Each helps on average but steers the sweeps into worse orders on
3700
+ * some graphs, so the elkjs facade also lays such graphs out with the
3701
+ * alternatives and keeps the best result.
3702
+ */
3703
+ const CROSSING_MODEL = "internal.crossingModel";
3644
3704
  const breakCyclesWithDepthFirstSearch = (input) => {
3645
3705
  const outgoing = /* @__PURE__ */ new Map();
3646
3706
  for (const node of input.graph.nodes) outgoing.set(node.id, []);
@@ -4128,8 +4188,7 @@ function applyLayerConstraintOrder(input, order) {
4128
4188
  return {
4129
4189
  ...order,
4130
4190
  layers: order.layers.map((layer) => {
4131
- const unconstrained = layer.filter((id) => !constrained.has(id));
4132
- return [...input.settings.mergeEdges === true ? [...unconstrained.filter((id) => id.startsWith("__layout_dummy:")), ...unconstrained.filter((id) => !id.startsWith("__layout_dummy:"))] : unconstrained, ...layer.filter((id) => constrained.has(id))];
4191
+ return [...layer.filter((id) => !constrained.has(id)), ...layer.filter((id) => constrained.has(id))];
4133
4192
  })
4134
4193
  };
4135
4194
  }
@@ -4896,6 +4955,18 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4896
4955
  }
4897
4956
  const nodeById = new Map(input.graph.nodes.map((node) => [node.id, node]));
4898
4957
  const edgeById = new Map(input.graph.edges.map((edge) => [edge.id, edge]));
4958
+ const boundaryPortIndex = (nodeId, edgeId, incoming) => {
4959
+ const edge = edgeById.get(edgeId);
4960
+ const name = incoming !== orientation.reversedEdgeIds.has(edgeId) ? edge.targetPort : edge.sourcePort;
4961
+ if (!name?.startsWith("__native_hierarchy")) return -1;
4962
+ return nodeById.get(nodeId).ports?.findIndex((port) => port.name === name) ?? -1;
4963
+ };
4964
+ for (const [orders, incoming] of [[outputPortOrder, false], [inputPortOrder, true]]) for (const [nodeId, edgeIds] of orders) {
4965
+ if (edgeIds.length < 2) continue;
4966
+ const slots = edgeIds.flatMap((id, slot) => boundaryPortIndex(nodeId, id, incoming) < 0 ? [] : [slot]);
4967
+ const sorted = slots.map((slot) => edgeIds[slot]).sort((a, b) => boundaryPortIndex(nodeId, a, incoming) - boundaryPortIndex(nodeId, b, incoming));
4968
+ slots.forEach((slot, index) => edgeIds[slot] = sorted[index]);
4969
+ }
4899
4970
  const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
4900
4971
  const hierarchicalNodes = /* @__PURE__ */ new Set();
4901
4972
  const fixedOrderNodes = new Set(input.graph.nodes.filter((node) => {
@@ -4933,10 +5004,24 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4933
5004
  if (edgeIds.length >= 4) edgeIds.splice(0, edgeIds.length - 1, ...edgeIds.slice(0, -1).reverse());
4934
5005
  }
4935
5006
  const buildCrossingGraph$1 = createCrossingGraphBuilder(input);
5007
+ const crossingModel = input.settings[CROSSING_MODEL] ?? {};
5008
+ const mixedPorts = crossingModel.mixedPorts !== false, probe = crossingModel.probe;
4936
5009
  const countCrossings = (candidateLayers) => {
4937
5010
  if (exactPortSweep) {
4938
5011
  const graph = buildCrossingGraph$1(orientation, candidateLayers, inputPortOrder, outputPortOrder);
4939
- let total = countAllCrossings(splitMixedCrossingPorts(graph, canonicalReversed)).total;
5012
+ for (const layer of graph.layers) for (const node of layer) {
5013
+ const physical = node.ports.length > 1 ? canonicalNodes.get(node.id)?.ports : void 0;
5014
+ 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));
5017
+ }
5018
+ const split = mixedPorts ? splitMixedCrossingPorts(graph, canonicalReversed) : graph;
5019
+ const score = countAllCrossings(split, crossingModel.separateSharedTracks ? "separated" : "hyperedges");
5020
+ if (probe) {
5021
+ if (split !== graph) probe.mixedPorts = true;
5022
+ if (score.sharedTracks) probe.sharedTracks = true;
5023
+ }
5024
+ let total = score.total;
4940
5025
  const nodeInfluence = Number(input.settings["considerModelOrder.crossingCounterNodeInfluence"] ?? 0);
4941
5026
  if ((input.settings["considerModelOrder.strategy"] ?? "NONE") !== "NONE" && nodeInfluence !== 0) {
4942
5027
  const changes = countModelOrderChanges(input, orientation, graph);
@@ -4986,11 +5071,12 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
4986
5071
  const nodes = fixedLayer.map((id) => canonicalNodes.get(id));
4987
5072
  canonicalGraph.layers[fixedIndex] = nodes;
4988
5073
  canonicalDistributor.calculatePortRanks(canonicalGraph, nodes, !forward, nodeRelativePortRanks);
4989
- const fixedIds = new Set(fixedLayer);
4990
- for (let index = 0; index < canonicalEdges.length; index++) {
5074
+ const byEnd = forward ? canonicalEdgesBySource : canonicalEdgesByTarget;
5075
+ const indices = fixedLayer.flatMap((id) => byEnd.get(id) ?? []).sort((a, b) => a - b);
5076
+ for (const index of indices) {
4991
5077
  const edge = canonicalEdges[index];
4992
5078
  const [source, target] = canonicalOrientedEndpoints[index];
4993
- if (!fixedIds.has(forward ? source : target) || !freeIds.has(forward ? target : source)) continue;
5079
+ if (!freeIds.has(forward ? target : source)) continue;
4994
5080
  const endpoint = canonicalEndpoints.get(edge.id);
4995
5081
  const rank = canonicalDistributor.state.ranks[forward ? endpoint.source : endpoint.target];
4996
5082
  ranks.get(forward ? target : source).push(rank);
@@ -5072,6 +5158,14 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5072
5158
  const canonicalEdges = input.graph.edges.filter((edge) => edge.sourceId !== edge.targetId);
5073
5159
  const canonicalReversed = canonicalEdges.map((edge) => orientation.reversedEdgeIds.has(edge.id));
5074
5160
  const canonicalOrientedEndpoints = canonicalEdges.map((edge) => getOrientedEndpoints(edge, orientation));
5161
+ const canonicalEdgesBySource = /* @__PURE__ */ new Map(), canonicalEdgesByTarget = /* @__PURE__ */ new Map();
5162
+ canonicalOrientedEndpoints.forEach(([source, target], index) => {
5163
+ for (const [byEnd, id] of [[canonicalEdgesBySource, source], [canonicalEdgesByTarget, target]]) {
5164
+ const list = byEnd.get(id);
5165
+ if (list) list.push(index);
5166
+ else byEnd.set(id, [index]);
5167
+ }
5168
+ });
5075
5169
  const canonicalEndpoints = new Map(canonicalEdges.map((edge, index) => [edge.id, canonicalGraph.edges[index]]));
5076
5170
  const canonicalFixedOrder = new Set(input.graph.nodes.filter((node) => {
5077
5171
  const constraint = input.nodeSettings?.(node)?.portConstraints;
@@ -5121,6 +5215,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5121
5215
  */
5122
5216
  const constrained = new Set([...units?.units.keys() ?? [], ...[...units?.successors ?? []].flatMap(([id, after]) => [id, ...after])]);
5123
5217
  const groupMixedPortDirections = (free, fixed, beyond = []) => {
5218
+ if (!mixedPorts) return;
5124
5219
  const fixedIds = new Set(fixed);
5125
5220
  const groups = /* @__PURE__ */ new Map();
5126
5221
  for (const id of free) {
@@ -5143,6 +5238,7 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5143
5238
  groups.set(key, group);
5144
5239
  }
5145
5240
  if (![...groups.values()].some((g) => g.some((m) => m.reversed) && g.some((m) => !m.reversed))) return;
5241
+ if (probe) probe.mixedPorts = true;
5146
5242
  const index = new Map(free.map((id, i) => [id, i]));
5147
5243
  const far = new Map(beyond.map((id, i) => [id, i]));
5148
5244
  const reach = (members) => {
@@ -5311,6 +5407,10 @@ function createLayerSweepSession(input, orientation, assignment, statistic = "me
5311
5407
  restore,
5312
5408
  finish,
5313
5409
  currentLayers: () => working,
5410
+ crossingGraph: () => ({
5411
+ layers: working.map((layer) => layer.map((id) => canonicalNodes.get(id))),
5412
+ edges: canonicalGraph.edges
5413
+ }),
5314
5414
  physicalPortOrder: (id) => canonicalNodes.get(id)?.ports.map((port) => port.id) ?? [],
5315
5415
  edgeOrders: (id) => ({
5316
5416
  incoming: inputPortOrder.get(id) ?? [],
@@ -6014,6 +6114,16 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6014
6114
  }
6015
6115
  const result = /* @__PURE__ */ new Map();
6016
6116
  const edgeModelOrder = new Map(input.graph.edges.map((edge, index) => [edge.id, index]));
6117
+ const orderIndexes = /* @__PURE__ */ new Map();
6118
+ const orderIndex = (list, id) => {
6119
+ let positions = orderIndexes.get(list);
6120
+ if (!positions) {
6121
+ positions = /* @__PURE__ */ new Map();
6122
+ for (const [index, item] of list.entries()) if (!positions.has(item)) positions.set(item, index);
6123
+ orderIndexes.set(list, positions);
6124
+ }
6125
+ return positions.get(id) ?? -1;
6126
+ };
6017
6127
  const placedPorts = /* @__PURE__ */ new Map();
6018
6128
  const portSideCounts = /* @__PURE__ */ new Map();
6019
6129
  const portSideCount = (node, side) => {
@@ -6055,7 +6165,7 @@ function implicitEdgeEndpoints(input, placement, orientation) {
6055
6165
  const leftIncoming = left.endpoint === "target" !== (orientation?.reversedEdgeIds.has(left.edge.id) ?? false);
6056
6166
  if (leftIncoming === (right.endpoint === "target" !== (orientation?.reversedEdgeIds.has(right.edge.id) ?? false))) {
6057
6167
  const selected = (leftIncoming ? sweptOrder?.inputPortOrderByNodeId : sweptOrder?.outputPortOrderByNodeId)?.get(nodeId);
6058
- const a = selected?.indexOf(left.edge.id) ?? -1, b = selected?.indexOf(right.edge.id) ?? -1;
6168
+ const a = selected ? orderIndex(selected, left.edge.id) : -1, b = selected ? orderIndex(selected, right.edge.id) : -1;
6059
6169
  if (a >= 0 && b >= 0 && a !== b) return (leftIncoming ? -1 : 1) * (a - b);
6060
6170
  }
6061
6171
  if (interactiveLeftSourceOrder) {
@@ -6753,8 +6863,9 @@ function routeEdges(style) {
6753
6863
  candidate.slot = increasing ? segment.slot : maximumSlot - segment.slot;
6754
6864
  if (segment.partner !== void 0) {
6755
6865
  const partner = result.segments[segment.partner];
6756
- candidate.secondSlot = increasing ? partner.slot : maximumSlot - segment.slot;
6757
- if (!increasing) candidate.slot = maximumSlot - partner.slot;
6866
+ const [first, second] = physicalPort(candidate, "source").graphSource ? [segment, partner] : [partner, segment];
6867
+ candidate.slot = increasing ? first.slot : maximumSlot - first.slot;
6868
+ candidate.secondSlot = increasing ? second.slot : maximumSlot - second.slot;
6758
6869
  const sourcePort = portByCandidate.get(candidate).source;
6759
6870
  const offset = (increasing ? candidate.sourceCross : candidate.targetCross) - byPort.get(sourcePort).position + (runShift.get(sourcePort) ?? 0);
6760
6871
  candidate.crossover = segment.outgoing[0] + offset;
@@ -9654,6 +9765,10 @@ function crossing(s, t) {
9654
9765
  const v = h === s ? t : s;
9655
9766
  return v.a.x > Math.min(h.a.x, h.b.x) + EPS && v.a.x < Math.max(h.a.x, h.b.x) - EPS && h.a.y > Math.min(v.a.y, v.b.y) + EPS && h.a.y < Math.max(v.a.y, v.b.y) - EPS;
9656
9767
  }
9768
+ const onBorder = (p, r) => {
9769
+ const inX = p.x >= r.x - EPS && p.x <= r.x + r.width + EPS;
9770
+ return p.y >= r.y - EPS && p.y <= r.y + r.height + EPS && (Math.abs(p.x - r.x) <= EPS || Math.abs(p.x - r.x - r.width) <= EPS) || inX && (Math.abs(p.y - r.y) <= EPS || Math.abs(p.y - r.y - r.height) <= EPS);
9771
+ };
9657
9772
  const segmentsOf = (points) => points.slice(1).flatMap((b, i) => {
9658
9773
  const s = {
9659
9774
  a: points[i],
@@ -9686,7 +9801,34 @@ function separateOpposingTracks(scene) {
9686
9801
  if (!rects) terminalCache.set(key, rects = terminals(a.route, b.route));
9687
9802
  return rects;
9688
9803
  };
9804
+ const boxes = /* @__PURE__ */ new WeakMap();
9805
+ const boxOf = (segments) => {
9806
+ let box = boxes.get(segments);
9807
+ if (!box) {
9808
+ box = [
9809
+ Infinity,
9810
+ Infinity,
9811
+ -Infinity,
9812
+ -Infinity
9813
+ ];
9814
+ for (const { a, b } of segments) {
9815
+ box[0] = Math.min(box[0], a.x, b.x);
9816
+ box[1] = Math.min(box[1], a.y, b.y);
9817
+ box[2] = Math.max(box[2], a.x, b.x);
9818
+ box[3] = Math.max(box[3], a.y, b.y);
9819
+ }
9820
+ boxes.set(segments, box);
9821
+ }
9822
+ return box;
9823
+ };
9824
+ const none = {
9825
+ opposing: 0,
9826
+ overlap: 0,
9827
+ crossings: 0
9828
+ };
9689
9829
  const pair = (mine, self, other) => {
9830
+ const [ax0, ay0, ax1, ay1] = boxOf(mine), [bx0, by0, bx1, by1] = boxOf(other.measured);
9831
+ if (ax1 < bx0 - EPS || bx1 < ax0 - EPS || ay1 < by0 - EPS || by1 < ay0 - EPS) return none;
9690
9832
  const rects = terminalsOf(self, other);
9691
9833
  const as = rects.length ? clip(mine, rects, true) : mine, bs = rects.length ? clip(other.measured, rects, true) : other.measured;
9692
9834
  let opposing = 0, overlap = 0, crossings$1 = 0;
@@ -9899,8 +10041,8 @@ function separateOpposingTracks(scene) {
9899
10041
  y: p.y
9900
10042
  };
9901
10043
  if (k >= 1 && k <= raw.length - 2) {
9902
- const before = raw[k - 1], after = raw[k + 1];
9903
- if (h ? !vertical(before) || !vertical(after) : !horizontal(before) || !horizontal(after)) continue;
10044
+ const before = raw[k - 1], after$1 = raw[k + 1];
10045
+ if (h ? !vertical(before) || !vertical(after$1) : !horizontal(before) || !horizontal(after$1)) continue;
9904
10046
  for (const delta of offsets) {
9905
10047
  const points = state.points.map((p, i) => i === k || i === k + 1 ? shift(p, delta) : p);
9906
10048
  const next = [
@@ -9911,11 +10053,11 @@ function separateOpposingTracks(scene) {
9911
10053
  a: points[i],
9912
10054
  b: points[i + 1]
9913
10055
  }));
9914
- if (!keeps(before, next[0]) || !keeps(after, next[2])) continue;
10056
+ if (!keeps(before, next[0]) || !keeps(after$1, next[2])) continue;
9915
10057
  consider(points, [
9916
10058
  before,
9917
10059
  segment,
9918
- after
10060
+ after$1
9919
10061
  ], next, next[1], 0, delta);
9920
10062
  }
9921
10063
  continue;
@@ -9938,6 +10080,32 @@ function separateOpposingTracks(scene) {
9938
10080
  a: port,
9939
10081
  b: corner
9940
10082
  }], [inflate(rect, TERMINAL_MARGIN)], true).reduce((sum, part) => sum + size(part), 0) : along;
10083
+ const after = {
10084
+ a: corner,
10085
+ b: beyond
10086
+ };
10087
+ const against = (s) => states.reduce((sum, other) => other === state ? sum : other.measured.reduce((n, t) => n + (opposite(s, t) ? shared(s, t) : 0), sum), 0);
10088
+ if (state.route.ends.length === 2 && state.route.ports?.[reversed ? 1 : 0] === void 0 && rect && onBorder(port, rect) && (h ? vertical(after) : horizontal(after))) {
10089
+ const lo = h ? rect.y : rect.x, hi = lo + (h ? rect.height : rect.width);
10090
+ for (const delta of offsets) {
10091
+ const moved = shift(port, delta);
10092
+ if ((h ? moved.y : moved.x) < lo - EPS || (h ? moved.y : moved.x) > hi + EPS) continue;
10093
+ const next = [{
10094
+ a: moved,
10095
+ b: shift(corner, delta)
10096
+ }, {
10097
+ a: shift(corner, delta),
10098
+ b: beyond
10099
+ }];
10100
+ if (!keeps(after, next[1]) || against(next[1]) > against(after) + EPS) continue;
10101
+ const replaced = [
10102
+ moved,
10103
+ shift(corner, delta),
10104
+ ...path.slice(2)
10105
+ ];
10106
+ consider(reversed ? replaced.reverse() : replaced, [segment, after], next, next[0], 0, delta);
10107
+ }
10108
+ }
9941
10109
  for (const stub of new Set([along - clearance, spacing])) for (const delta of offsets) {
9942
10110
  if (stub < 1 || along - stub < spacing) continue;
9943
10111
  const q = {
@@ -9995,14 +10163,34 @@ function separateOpposingTracks(scene) {
9995
10163
  * half a spacing either side of the port's centre.
9996
10164
  */
9997
10165
  function splitSharedPorts(tolerant) {
9998
- const ports = scene.ports;
9999
- if (!ports?.size) return;
10000
10166
  const groups = /* @__PURE__ */ new Map();
10167
+ const implicit = /* @__PURE__ */ new Map();
10001
10168
  for (const state of states) for (const end of [0, 1]) {
10002
- const id = state.route.ports?.[end];
10003
- const port = id === void 0 ? void 0 : ports.get(id);
10004
- if (!port || state.points.length < 2) continue;
10169
+ if (state.points.length < 2) continue;
10005
10170
  const [p, q] = end ? [...state.points].reverse() : state.points;
10171
+ let id = state.route.ports?.[end];
10172
+ let port = id === void 0 ? void 0 : scene.ports?.get(id);
10173
+ const node = state.route.ends.length === 2 ? state.route.ends[end] : void 0;
10174
+ const rect = node === void 0 ? void 0 : nodes.get(node);
10175
+ if (id === void 0 && rect && onBorder(p, rect)) {
10176
+ const points = implicit.get(node) ?? [];
10177
+ implicit.set(node, points);
10178
+ let at = points.find((o) => Math.abs(o.x - p.x) <= EPS && Math.abs(o.y - p.y) <= EPS);
10179
+ if (!at) points.push(at = {
10180
+ x: p.x,
10181
+ y: p.y
10182
+ });
10183
+ id = `${node}\0${points.indexOf(at)}`;
10184
+ port = {
10185
+ node,
10186
+ rect: {
10187
+ ...at,
10188
+ width: 0,
10189
+ height: 0
10190
+ }
10191
+ };
10192
+ }
10193
+ if (!port) continue;
10006
10194
  const s = {
10007
10195
  a: p,
10008
10196
  b: q
@@ -10024,7 +10212,13 @@ function separateOpposingTracks(scene) {
10024
10212
  end
10025
10213
  });
10026
10214
  }
10027
- for (const { port, normal, ends } of groups.values()) {
10215
+ for (const group of groups.values()) {
10216
+ const { port, normal } = group;
10217
+ const ends = group.ends.filter(({ state, end }) => {
10218
+ if (state.route.ports?.[end] !== void 0) return true;
10219
+ const p = end ? state.points.at(-1) : state.points[0];
10220
+ return Math.abs(p.x - port.rect.x) <= EPS && Math.abs(p.y - port.rect.y) <= EPS;
10221
+ });
10028
10222
  const incoming = ends.filter((e) => e.end === 1), outgoing = ends.filter((e) => e.end === 0);
10029
10223
  if (!incoming.length || !outgoing.length) continue;
10030
10224
  const stub = ({ state, end }) => {
@@ -10043,9 +10237,25 @@ function separateOpposingTracks(scene) {
10043
10237
  const sideLo = node[lateral], sideHi = node[lateral] + (lateral === "x" ? node.width : node.height);
10044
10238
  const wide = extent$2 >= spacing - EPS;
10045
10239
  const half = spacing / 2;
10046
- const shifted = (points, end, offset) => {
10240
+ const slid = (state, points, end, offset) => {
10241
+ if (state.route.ports?.some((id) => id !== void 0)) return void 0;
10242
+ if (Math.abs(points[0][across] - points[1][across]) <= EPS) return void 0;
10243
+ const far = nodes.get(state.route.ends[1 - end] ?? "");
10244
+ const moved = points.map((p) => ({
10245
+ ...p,
10246
+ [lateral]: p[lateral] + offset
10247
+ }));
10248
+ const within = (value$1, rect) => {
10249
+ const lo = rect[lateral];
10250
+ return value$1 >= lo - EPS && value$1 <= lo + (lateral === "x" ? rect.width : rect.height) + EPS;
10251
+ };
10252
+ const value = moved[0][lateral];
10253
+ return within(value, node) && far && within(value, far) ? moved : void 0;
10254
+ };
10255
+ const shifted = (state, points, end, offset) => {
10047
10256
  if (Math.abs(offset) <= EPS) return points;
10048
10257
  const path = end ? [...points].reverse() : [...points];
10258
+ if (path.length === 2) return slid(state, points, end, offset);
10049
10259
  if (path.length < 3) return void 0;
10050
10260
  const [p, q, r] = path;
10051
10261
  const along = r[lateral] - q[lateral];
@@ -10109,7 +10319,7 @@ function separateOpposingTracks(scene) {
10109
10319
  const next = /* @__PURE__ */ new Map();
10110
10320
  let feasible = true;
10111
10321
  for (const e of ends) {
10112
- const points = shifted(next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
10322
+ const points = shifted(e.state, next.get(e.state) ?? e.state.points, e.end, offsetOf(e));
10113
10323
  if (!points) {
10114
10324
  feasible = false;
10115
10325
  break;
@@ -18811,10 +19021,6 @@ function* createLayeredScopePipeline(graph, options, context) {
18811
19021
  switchedSideByPort.set(`${origin.node.id}\0${origin.port.name}`, opposite$2);
18812
19022
  }
18813
19023
  }
18814
- if (expanded.input.settings.mergeEdges === true) order = {
18815
- ...order,
18816
- layers: order.layers.map((layer) => [...layer.filter((id) => id.startsWith("__layout_dummy:")), ...layer.filter((id) => !id.startsWith("__layout_dummy:"))])
18817
- };
18818
19024
  const unzippingFanIn = graph.edges.length === graph.nodes.length - 1 && graph.nodes.some((node) => graph.edges.filter((edge) => edge.targetId === node.id).length === graph.nodes.length - 1);
18819
19025
  if ((options.settings?.["layerUnzipping.strategy"] ?? "NONE") === "ALTERNATING") if (unzippingFanIn) order = applyLayerUnzipping(expanded.input, order);
18820
19026
  else {
@@ -19717,6 +19923,12 @@ const elkjs0111LayeredAlgorithm = {
19717
19923
  };
19718
19924
 
19719
19925
  //#endregion
19926
+ Object.defineProperty(exports, 'CROSSING_MODEL', {
19927
+ enumerable: true,
19928
+ get: function () {
19929
+ return CROSSING_MODEL;
19930
+ }
19931
+ });
19720
19932
  Object.defineProperty(exports, 'JavaRandom', {
19721
19933
  enumerable: true,
19722
19934
  get: function () {
@@ -19903,12 +20115,6 @@ Object.defineProperty(exports, 'fromElkLayeredOptionId', {
19903
20115
  return fromElkLayeredOptionId;
19904
20116
  }
19905
20117
  });
19906
- Object.defineProperty(exports, 'getCrossingUnits', {
19907
- enumerable: true,
19908
- get: function () {
19909
- return getCrossingUnits;
19910
- }
19911
- });
19912
20118
  Object.defineProperty(exports, 'getLayeredLayout', {
19913
20119
  enumerable: true,
19914
20120
  get: function () {