@tscircuit/schematic-trace-solver 0.0.154 → 0.0.156

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.
Files changed (21) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.js +331 -114
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +60 -16
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +38 -0
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +19 -11
  6. package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +17 -0
  7. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +32 -4
  8. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +28 -5
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +114 -0
  10. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +98 -32
  11. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +2 -0
  12. package/package.json +1 -1
  13. package/site/bug-reports/bug-report-20260825T045913Z.page.tsx +4 -0
  14. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
  15. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  16. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +271 -0
  17. package/tests/bug-reports/bug-report-20260825T045913Z/bug-report-20260825T045913Z.json +874 -0
  18. package/tests/bug-reports/bug-report-20260825T045913Z/bug-report-20260825T045913Z.test.ts +23 -0
  19. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +2 -2
  20. package/tests/examples/__snapshots__/example51.snap.svg +2 -2
  21. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +45 -45
package/dist/index.js CHANGED
@@ -1255,6 +1255,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1255
1255
  endpointTextObstacles;
1256
1256
  aabb;
1257
1257
  baseElbow;
1258
+ preferExteriorDetours;
1258
1259
  solvedTracePath = null;
1259
1260
  queue = [];
1260
1261
  visited = /* @__PURE__ */ new Set();
@@ -1264,6 +1265,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1264
1265
  this.connectionPair = params.connectionPair;
1265
1266
  this.inputProblem = params.inputProblem;
1266
1267
  this.chipMap = params.chipMap;
1268
+ this.preferExteriorDetours = params.preferExteriorDetours ?? true;
1267
1269
  for (const pin of this.pins) {
1268
1270
  if (!pin._facingDirection) {
1269
1271
  const chip = this.chipMap[pin.chipId];
@@ -1316,7 +1318,8 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1316
1318
  chipMap: this.chipMap,
1317
1319
  pins: this.pins,
1318
1320
  connectionPair: this.connectionPair,
1319
- inputProblem: this.inputProblem
1321
+ inputProblem: this.inputProblem,
1322
+ preferExteriorDetours: this.preferExteriorDetours
1320
1323
  };
1321
1324
  }
1322
1325
  getTextBoxPaddingForConnectionPair() {
@@ -1481,7 +1484,6 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1481
1484
  const isEndpointChipObstacle = rect.kind === "chip" && this.pins.some((pin) => pin.chipId === rect.chipId);
1482
1485
  const canGenerateEndpointDetour = path.length === 3 || path.length === 4 && this.connectionPair !== void 0 && !isEndpointChipObstacle;
1483
1486
  if (canGenerateEndpointDetour && (isFirstSegment || isLastSegment)) {
1484
- const compareDetours = path.length === 4 ? (a2, b2) => this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2) || this.pathLength(a2) - this.pathLength(b2) : (a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2);
1485
1487
  const detours = generateEndpointCollisionDetours({
1486
1488
  path,
1487
1489
  collidingSegmentIndex: segIndex,
@@ -1491,7 +1493,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1491
1493
  if (this.visited.has(key)) return false;
1492
1494
  this.visited.add(key);
1493
1495
  return true;
1494
- }).sort(compareDetours);
1496
+ }).sort((a2, b2) => {
1497
+ if (path.length === 4 && this.preferExteriorDetours) {
1498
+ return this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2) || this.pathLength(a2) - this.pathLength(b2);
1499
+ }
1500
+ return this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2);
1501
+ });
1495
1502
  for (const detour of detours) {
1496
1503
  const nextCollisionRects = new Set(collisionRects);
1497
1504
  nextCollisionRects.add(rect);
@@ -1644,6 +1651,24 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1644
1651
  import { getBounds } from "graphics-debug";
1645
1652
 
1646
1653
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts
1654
+ var shouldPreferExteriorDetours = ({
1655
+ connectionPair,
1656
+ allConnectionPairs,
1657
+ inputProblem
1658
+ }) => {
1659
+ const [firstPin, secondPin] = connectionPair.pins;
1660
+ const belongsToNetConnection = inputProblem.netConnections.some(
1661
+ (netConnection) => netConnection.pinIds.includes(firstPin.pinId) && netConnection.pinIds.includes(secondPin.pinId)
1662
+ );
1663
+ if (belongsToNetConnection) return true;
1664
+ return allConnectionPairs.some((otherPair) => {
1665
+ if (otherPair === connectionPair) return false;
1666
+ const [otherFirstPin, otherSecondPin] = otherPair.pins;
1667
+ const sameOrder = firstPin.chipId === otherFirstPin.chipId && secondPin.chipId === otherSecondPin.chipId;
1668
+ if (sameOrder) return true;
1669
+ return firstPin.chipId === otherSecondPin.chipId && secondPin.chipId === otherFirstPin.chipId;
1670
+ });
1671
+ };
1647
1672
  var SchematicTraceLinesSolver = class extends BaseSolver {
1648
1673
  inputProblem;
1649
1674
  mspConnectionPairs;
@@ -1711,7 +1736,12 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
1711
1736
  inputProblem: this.inputProblem,
1712
1737
  pins,
1713
1738
  connectionPair,
1714
- chipMap: this.chipMap
1739
+ chipMap: this.chipMap,
1740
+ preferExteriorDetours: shouldPreferExteriorDetours({
1741
+ connectionPair,
1742
+ allConnectionPairs: this.mspConnectionPairs,
1743
+ inputProblem: this.inputProblem
1744
+ })
1715
1745
  });
1716
1746
  }
1717
1747
  visualize() {
@@ -5690,22 +5720,8 @@ var getRailAlignmentFallbackCoordinates = ({
5690
5720
  return getDistinctCoordinates(coordinates);
5691
5721
  };
5692
5722
 
5693
- // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
5694
- var moveRailSegments = (trace, segments, coordinate) => {
5695
- const pointsToMove = /* @__PURE__ */ new Set();
5696
- for (const segment of segments) {
5697
- pointsToMove.add(segment.segmentIndex);
5698
- pointsToMove.add(segment.segmentIndex + 1);
5699
- }
5700
- const orientation = segments[0].orientation;
5701
- const tracePath = simplifyPath(
5702
- trace.tracePath.map((point, index) => {
5703
- if (!pointsToMove.has(index)) return point;
5704
- return orientation === "vertical" ? { ...point, x: coordinate } : { ...point, y: coordinate };
5705
- })
5706
- );
5707
- return { ...trace, tracePath };
5708
- };
5723
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts
5724
+ import { distance as distance4 } from "@tscircuit/math-utils";
5709
5725
 
5710
5726
  // lib/solvers/RailNetLabelCornerPlacementSolver/geometry.ts
5711
5727
  var EPS7 = 1e-6;
@@ -5740,6 +5756,95 @@ var isPointOnSegment = (point, start, end) => {
5740
5756
  };
5741
5757
  var getSegmentOrientation = (a, b) => Math.abs(a.y - b.y) <= EPS7 ? "horizontal" : "vertical";
5742
5758
 
5759
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts
5760
+ var getTransitivelyConnectedTraceIds = (group, traces) => {
5761
+ const groupNetId = group[0].globalConnNetId;
5762
+ const sameNetTraces = traces.filter(
5763
+ (trace) => trace.globalConnNetId === groupNetId
5764
+ );
5765
+ const connectedTraceIds = new Set(group.map((segment) => segment.traceId));
5766
+ const connectedPinIds = new Set(
5767
+ sameNetTraces.filter((trace) => connectedTraceIds.has(trace.mspPairId)).flatMap((trace) => trace.pinIds)
5768
+ );
5769
+ for (let changed = true; changed; ) {
5770
+ changed = false;
5771
+ for (const trace of sameNetTraces) {
5772
+ if (connectedTraceIds.has(trace.mspPairId)) continue;
5773
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue;
5774
+ connectedTraceIds.add(trace.mspPairId);
5775
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
5776
+ changed = true;
5777
+ }
5778
+ }
5779
+ return connectedTraceIds;
5780
+ };
5781
+ var getLabelsLinkedToRailGroup = (group, netLabelPlacements, traces) => {
5782
+ const traceIds = getTransitivelyConnectedTraceIds(group, traces);
5783
+ return netLabelPlacements.filter(
5784
+ (label) => label.mspConnectionPairIds.some((traceId) => traceIds.has(traceId))
5785
+ );
5786
+ };
5787
+ var getFixedLabelCoordinate = (group, netLabelPlacements, traces) => {
5788
+ const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
5789
+ const groupTraceIds = new Set(group.map((segment) => segment.traceId));
5790
+ const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
5791
+ (traceId) => traceMap.get(traceId)?.tracePath.length === 4
5792
+ );
5793
+ if (!groupHasOnlySingleRailTraces) return null;
5794
+ const orientation = group[0].orientation;
5795
+ const linkedLabels = getLabelsLinkedToRailGroup(
5796
+ group,
5797
+ netLabelPlacements,
5798
+ traces
5799
+ );
5800
+ const anchoringLabels = linkedLabels.filter(
5801
+ (label) => label.orientation === group[0].componentFacingDirection && label.mspConnectionPairIds.some((traceId) => {
5802
+ const trace = traceMap.get(traceId);
5803
+ return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint);
5804
+ })
5805
+ );
5806
+ const coordinates = getDistinctCoordinates(
5807
+ anchoringLabels.map(
5808
+ (label) => orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y
5809
+ )
5810
+ );
5811
+ if (coordinates.length !== 1) return null;
5812
+ const labelCoordinate = coordinates[0];
5813
+ const coordinate = group.find(
5814
+ (segment) => Math.abs(segment.coordinate - labelCoordinate) <= RAIL_ALIGNMENT_EPSILON
5815
+ )?.coordinate ?? labelCoordinate;
5816
+ const labelIsAnchoredToRoutedBackbone = anchoringLabels.some(
5817
+ (label) => label.mspConnectionPairIds.some(
5818
+ (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4
5819
+ )
5820
+ );
5821
+ if (labelIsAnchoredToRoutedBackbone) return coordinate;
5822
+ const coordinateIsLocalToEveryTrace = group.every((segment) => {
5823
+ const trace = traceMap.get(segment.traceId);
5824
+ if (!trace) return false;
5825
+ return Math.abs(segment.coordinate - coordinate) <= distance4(trace.pins[0], trace.pins[1]) + RAIL_ALIGNMENT_EPSILON;
5826
+ });
5827
+ return coordinateIsLocalToEveryTrace ? coordinate : null;
5828
+ };
5829
+
5830
+ // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
5831
+ var moveRailSegments = (trace, segments, coordinate) => {
5832
+ const pointsToMove = /* @__PURE__ */ new Set();
5833
+ for (const segment of segments) {
5834
+ if (nearlyEqual(segment.coordinate, coordinate)) continue;
5835
+ pointsToMove.add(segment.segmentIndex);
5836
+ pointsToMove.add(segment.segmentIndex + 1);
5837
+ }
5838
+ const orientation = segments[0].orientation;
5839
+ const tracePath = simplifyPath(
5840
+ trace.tracePath.map((point, index) => {
5841
+ if (!pointsToMove.has(index)) return point;
5842
+ return orientation === "vertical" ? { ...point, x: coordinate } : { ...point, y: coordinate };
5843
+ })
5844
+ );
5845
+ return { ...trace, tracePath };
5846
+ };
5847
+
5743
5848
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors.ts
5744
5849
  var getAnchoredTraceIds = (label, traces) => new Set(
5745
5850
  traces.filter(
@@ -5855,10 +5960,10 @@ var projectPointToPath = (point, path) => {
5855
5960
  let bestDistance = Number.POSITIVE_INFINITY;
5856
5961
  for (let i = 0; i < path.length - 1; i++) {
5857
5962
  const projectedPoint = projectPointToSegment(point, path[i], path[i + 1]);
5858
- const distance5 = getDistance2(point, projectedPoint);
5859
- if (distance5 < bestDistance) {
5963
+ const distance7 = getDistance2(point, projectedPoint);
5964
+ if (distance7 < bestDistance) {
5860
5965
  bestPoint = projectedPoint;
5861
- bestDistance = distance5;
5966
+ bestDistance = distance7;
5862
5967
  }
5863
5968
  }
5864
5969
  return bestPoint;
@@ -5964,13 +6069,18 @@ var evaluateRailGroup = ({
5964
6069
  const originalCoordinates = getDistinctCoordinates(
5965
6070
  group.map((segment) => segment.coordinate)
5966
6071
  );
6072
+ const fixedLabelCoordinate = getFixedLabelCoordinate(
6073
+ group,
6074
+ netLabelPlacements,
6075
+ traces
6076
+ );
5967
6077
  const otherNetTraces = traces.filter(
5968
6078
  (trace) => trace.globalConnNetId !== group[0].globalConnNetId
5969
6079
  );
5970
6080
  const immutableSameNetTraces = traces.filter(
5971
6081
  (trace) => trace.globalConnNetId === group[0].globalConnNetId && !eligibleTraceIds.has(trace.mspPairId)
5972
6082
  );
5973
- const evaluateCoordinates = (coordinates) => {
6083
+ const evaluateCoordinates = (coordinates, options) => {
5974
6084
  let best = null;
5975
6085
  for (const coordinate of coordinates) {
5976
6086
  const candidateMap = /* @__PURE__ */ new Map();
@@ -6006,7 +6116,9 @@ var evaluateRailGroup = ({
6006
6116
  allCandidateTraces
6007
6117
  );
6008
6118
  if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
6009
- if (!isReadabilityImprovement(metrics, baseline)) continue;
6119
+ if (options?.coordinateIsFixedByLabel ? metrics.turnCount > baseline.turnCount : !isReadabilityImprovement(metrics, baseline)) {
6120
+ continue;
6121
+ }
6010
6122
  const score = {
6011
6123
  ...metrics,
6012
6124
  displacement: group.reduce(
@@ -6022,13 +6134,21 @@ var evaluateRailGroup = ({
6022
6134
  return tracePathChanged(original, candidate2);
6023
6135
  }).map((trace) => trace.mspPairId);
6024
6136
  if (changedTraceIds.length === 0) continue;
6025
- const candidate = { traces: allCandidateTraces, changedTraceIds, score };
6137
+ const candidate = {
6138
+ traces: allCandidateTraces,
6139
+ changedTraceIds,
6140
+ score
6141
+ };
6026
6142
  if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate;
6027
6143
  }
6028
6144
  return best;
6029
6145
  };
6030
- const originalCandidate = evaluateCoordinates(originalCoordinates);
6146
+ const originalCandidate = evaluateCoordinates(
6147
+ fixedLabelCoordinate === null ? originalCoordinates : [fixedLabelCoordinate],
6148
+ { coordinateIsFixedByLabel: fixedLabelCoordinate !== null }
6149
+ );
6031
6150
  if (originalCandidate) return originalCandidate;
6151
+ if (fixedLabelCoordinate !== null) return null;
6032
6152
  return evaluateCoordinates(
6033
6153
  getRailAlignmentFallbackCoordinates({
6034
6154
  group,
@@ -6039,6 +6159,7 @@ var evaluateRailGroup = ({
6039
6159
  };
6040
6160
 
6041
6161
  // lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts
6162
+ import { distance as distance5 } from "@tscircuit/math-utils";
6042
6163
  var getMovableRailSegments = (trace) => {
6043
6164
  const segments = [];
6044
6165
  const path = trace.tracePath;
@@ -6083,8 +6204,9 @@ var railIsOutsideComponent = (segment, chip, facingDirection) => {
6083
6204
  return segment.orientation === "horizontal" && segment.coordinate <= minY + RAIL_ALIGNMENT_EPSILON;
6084
6205
  }
6085
6206
  };
6086
- var getComponentSideRailSegments = (trace, chipMap) => {
6207
+ var getComponentSideRailSegments = (trace, chipMap, options) => {
6087
6208
  const segments = [];
6209
+ const isLocalConnection = distance5(trace.pins[0], trace.pins[1]) <= (options?.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE);
6088
6210
  for (const segment of getMovableRailSegments(trace)) {
6089
6211
  const associations = trace.pins.flatMap((pin, pinIndex) => {
6090
6212
  const chip = chipMap.get(pin.chipId);
@@ -6102,9 +6224,18 @@ var getComponentSideRailSegments = (trace, chipMap) => {
6102
6224
  ];
6103
6225
  });
6104
6226
  associations.sort((a, b) => a.distanceFromEndpoint - b.distanceFromEndpoint);
6105
- const association = associations[0];
6106
- if (!association) continue;
6107
- segments.push({ ...segment, ...association });
6227
+ const minimumDistance = associations[0]?.distanceFromEndpoint;
6228
+ const nearestAssociationKeys = /* @__PURE__ */ new Set();
6229
+ for (const association of associations) {
6230
+ if (nearestAssociationKeys.size > 0 && (!options?.includeTiedEndpointAssociations || !isLocalConnection)) {
6231
+ break;
6232
+ }
6233
+ if (association.distanceFromEndpoint !== minimumDistance) continue;
6234
+ const associationKey = `${association.componentId}:${association.componentFacingDirection}`;
6235
+ if (nearestAssociationKeys.has(associationKey)) continue;
6236
+ nearestAssociationKeys.add(associationKey);
6237
+ segments.push({ ...segment, ...association });
6238
+ }
6108
6239
  }
6109
6240
  return segments;
6110
6241
  };
@@ -6135,41 +6266,90 @@ var tracesSharePin = (a, b, traceMap) => {
6135
6266
  return traceMap.get(b.traceId).pins.some((pin) => aPinIds.has(pin.pinId));
6136
6267
  };
6137
6268
  var canJoinRailGroup = (start, current, candidate, traceMap, obstacles) => candidate.globalConnNetId === start.globalConnNetId && candidate.orientation === start.orientation && candidate.componentId === start.componentId && candidate.componentFacingDirection === start.componentFacingDirection && (rangesTouchOrOverlap(current, candidate) || tracesSharePin(current, candidate, traceMap)) && corridorIsClear(current, candidate, obstacles);
6138
- var getRailGroups = (traces, eligibleTraceIds, inputProblem, obstacles) => {
6269
+ var getRailGroups = (traces, eligibleTraceIds, inputProblem, obstacles, netLabelPlacements) => {
6139
6270
  const chipMap = new Map(inputProblem.chips.map((chip) => [chip.chipId, chip]));
6140
- const segments = traces.filter((trace) => eligibleTraceIds.has(trace.mspPairId)).flatMap((trace) => getComponentSideRailSegments(trace, chipMap));
6141
6271
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]));
6142
- const visited = /* @__PURE__ */ new Set();
6143
- const groups = [];
6144
- for (let startIndex = 0; startIndex < segments.length; startIndex++) {
6145
- if (visited.has(startIndex)) continue;
6146
- const start = segments[startIndex];
6147
- const queue = [startIndex];
6148
- const group = [];
6149
- visited.add(startIndex);
6150
- for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
6151
- const current = segments[queue[queueIndex]];
6152
- group.push(current);
6153
- for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex++) {
6154
- if (visited.has(candidateIndex)) continue;
6155
- const candidate = segments[candidateIndex];
6156
- if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
6157
- continue;
6272
+ const eligibleTraces = traces.filter(
6273
+ (trace) => eligibleTraceIds.has(trace.mspPairId)
6274
+ );
6275
+ const collectConnectedGroups = (segments) => {
6276
+ const visited = /* @__PURE__ */ new Set();
6277
+ const connectedGroups = [];
6278
+ for (let startIndex = 0; startIndex < segments.length; startIndex++) {
6279
+ if (visited.has(startIndex)) continue;
6280
+ const start = segments[startIndex];
6281
+ const queue = [startIndex];
6282
+ const group = [];
6283
+ visited.add(startIndex);
6284
+ for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
6285
+ const current = segments[queue[queueIndex]];
6286
+ group.push(current);
6287
+ for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex++) {
6288
+ if (visited.has(candidateIndex)) continue;
6289
+ const candidate = segments[candidateIndex];
6290
+ if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
6291
+ continue;
6292
+ }
6293
+ visited.add(candidateIndex);
6294
+ queue.push(candidateIndex);
6158
6295
  }
6159
- visited.add(candidateIndex);
6160
- queue.push(candidateIndex);
6161
6296
  }
6297
+ connectedGroups.push(group);
6162
6298
  }
6299
+ return connectedGroups;
6300
+ };
6301
+ const groupKey = (group) => [
6302
+ group[0].componentId,
6303
+ group[0].componentFacingDirection,
6304
+ group[0].orientation,
6305
+ ...group.map((segment) => `${segment.traceId}:${segment.segmentIndex}`).sort()
6306
+ ].join("|");
6307
+ const selectedGroups = [];
6308
+ const selectedGroupKeys = /* @__PURE__ */ new Set();
6309
+ const addEligibleGroup = (group, options) => {
6163
6310
  const traceCount = new Set(group.map((segment) => segment.traceId)).size;
6311
+ if (traceCount < 2) return;
6312
+ const fixedLabelCoordinate = getFixedLabelCoordinate(
6313
+ group,
6314
+ netLabelPlacements,
6315
+ traces
6316
+ );
6317
+ if (options?.requireFixedLabel && fixedLabelCoordinate === null) return;
6164
6318
  const hasDifferentCoordinates = group.some(
6165
6319
  (segment) => !nearlyEqual(segment.coordinate, group[0].coordinate)
6166
6320
  );
6167
- if (traceCount >= 2 && hasDifferentCoordinates) groups.push(group);
6321
+ const hasDifferentFixedLabelCoordinate = fixedLabelCoordinate !== null && !nearlyEqual(fixedLabelCoordinate, group[0].coordinate);
6322
+ if (!hasDifferentCoordinates && !hasDifferentFixedLabelCoordinate) return;
6323
+ const key = groupKey(group);
6324
+ if (selectedGroupKeys.has(key)) return;
6325
+ selectedGroupKeys.add(key);
6326
+ selectedGroups.push(group);
6327
+ };
6328
+ const primarySegments = eligibleTraces.flatMap(
6329
+ (trace) => getComponentSideRailSegments(trace, chipMap)
6330
+ );
6331
+ for (const group of collectConnectedGroups(primarySegments)) {
6332
+ addEligibleGroup(group);
6333
+ }
6334
+ const tiedEndpointSegments = eligibleTraces.flatMap(
6335
+ (trace) => getComponentSideRailSegments(trace, chipMap, {
6336
+ includeTiedEndpointAssociations: true,
6337
+ maxMspPairDistance: inputProblem.maxMspPairDistance
6338
+ })
6339
+ );
6340
+ for (const group of collectConnectedGroups(tiedEndpointSegments)) {
6341
+ addEligibleGroup(group, { requireFixedLabel: true });
6168
6342
  }
6169
- return groups;
6343
+ return selectedGroups;
6170
6344
  };
6171
6345
 
6172
6346
  // lib/solvers/TraceCleanupSolver/alignSameNetRails.ts
6347
+ var getTraceStateKey = (traces) => JSON.stringify(
6348
+ traces.map((trace) => ({
6349
+ mspPairId: trace.mspPairId,
6350
+ tracePath: trace.tracePath
6351
+ }))
6352
+ );
6173
6353
  var alignSameNetRails = ({
6174
6354
  inputProblem,
6175
6355
  traces,
@@ -6177,6 +6357,7 @@ var alignSameNetRails = ({
6177
6357
  eligibleTraceIds
6178
6358
  }) => {
6179
6359
  let outputTraces = [...traces];
6360
+ const seenTraceStates = /* @__PURE__ */ new Set([getTraceStateKey(outputTraces)]);
6180
6361
  const obstacles = getObstacleRects(inputProblem);
6181
6362
  const alignedTraceIds = /* @__PURE__ */ new Set();
6182
6363
  let alignedRailGroupCount = 0;
@@ -6189,7 +6370,8 @@ var alignSameNetRails = ({
6189
6370
  outputTraces,
6190
6371
  eligibleTraceIds,
6191
6372
  inputProblem,
6192
- obstacles
6373
+ obstacles,
6374
+ netLabelPlacements
6193
6375
  );
6194
6376
  let applied = null;
6195
6377
  for (const group of groups) {
@@ -6203,9 +6385,13 @@ var alignSameNetRails = ({
6203
6385
  if (applied) break;
6204
6386
  }
6205
6387
  if (!applied) break;
6388
+ const candidateStateKey = getTraceStateKey(applied.traces);
6389
+ const repeatsSeenState = seenTraceStates.has(candidateStateKey);
6390
+ if (!repeatsSeenState) seenTraceStates.add(candidateStateKey);
6206
6391
  outputTraces = applied.traces;
6207
6392
  alignedRailGroupCount++;
6208
6393
  for (const traceId of applied.changedTraceIds) alignedTraceIds.add(traceId);
6394
+ if (repeatsSeenState) break;
6209
6395
  }
6210
6396
  return {
6211
6397
  traces: outputTraces,
@@ -7801,11 +7987,11 @@ var getLabelHugDistance = (tracePath, obstacleLabel) => {
7801
7987
  obstacleLabel.width,
7802
7988
  obstacleLabel.height
7803
7989
  );
7804
- let distance5 = 0;
7990
+ let distance7 = 0;
7805
7991
  for (const point of tracePath) {
7806
- distance5 += getPointDistanceFromRect(point, bounds);
7992
+ distance7 += getPointDistanceFromRect(point, bounds);
7807
7993
  }
7808
- return distance5;
7994
+ return distance7;
7809
7995
  };
7810
7996
  var getPointDistanceFromRect = (point, rect) => {
7811
7997
  const dx = Math.max(rect.minX - point.x, 0, point.x - rect.maxX);
@@ -8082,16 +8268,16 @@ var Example28Solver = class extends BaseSolver {
8082
8268
  const outward = dir(label.orientation);
8083
8269
  if (outward.x === 0 && outward.y === 0) return null;
8084
8270
  for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
8085
- const distance5 = step * LABEL_OUTWARD_STEP;
8271
+ const distance7 = step * LABEL_OUTWARD_STEP;
8086
8272
  const candidate = {
8087
8273
  ...label,
8088
8274
  anchorPoint: {
8089
- x: label.anchorPoint.x + outward.x * distance5,
8090
- y: label.anchorPoint.y + outward.y * distance5
8275
+ x: label.anchorPoint.x + outward.x * distance7,
8276
+ y: label.anchorPoint.y + outward.y * distance7
8091
8277
  },
8092
8278
  center: {
8093
- x: label.center.x + outward.x * distance5,
8094
- y: label.center.y + outward.y * distance5
8279
+ x: label.center.x + outward.x * distance7,
8280
+ y: label.center.y + outward.y * distance7
8095
8281
  }
8096
8282
  };
8097
8283
  const candidateWithClearance = {
@@ -8960,10 +9146,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8960
9146
  }
8961
9147
  findValidTraceAnchorCandidate(label, orientation, labelIndex) {
8962
9148
  const direction = dir(orientation);
8963
- const candidatePoints = this.getTraceAnchorCandidatePoints(
8964
- label,
8965
- orientation
8966
- ).sort((a, b) => {
9149
+ const { points } = this.getTraceAnchorCandidates(label, orientation);
9150
+ const candidatePoints = points.sort((a, b) => {
8967
9151
  const aAlongDirection = a.x * direction.x + a.y * direction.y;
8968
9152
  const bAlongDirection = b.x * direction.x + b.y * direction.y;
8969
9153
  return bAlongDirection - aAlongDirection;
@@ -8986,19 +9170,19 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8986
9170
  }
8987
9171
  findValidOutwardTraceAnchorCandidate(label, orientation, labelIndex) {
8988
9172
  const direction = dir(orientation);
8989
- const candidatePoints = this.getTraceAnchorCandidatePoints(
8990
- label,
8991
- orientation
8992
- ).sort((a, b) => {
9173
+ const preservedColumnAnchor = this.getSearchStartAnchor(label, orientation);
9174
+ const { points, preferNearestBendOnOutwardRow } = this.getTraceAnchorCandidates(label, orientation);
9175
+ const candidatePoints = points.sort((a, b) => {
8993
9176
  const aAlongDirection = a.x * direction.x + a.y * direction.y;
8994
9177
  const bAlongDirection = b.x * direction.x + b.y * direction.y;
8995
- return bAlongDirection - aAlongDirection;
9178
+ if (!preferNearestBendOnOutwardRow) {
9179
+ return bAlongDirection - aAlongDirection;
9180
+ }
9181
+ const aPerpendicularDistance = isYOrientation(orientation) ? Math.abs(a.x - preservedColumnAnchor.x) : Math.abs(a.y - preservedColumnAnchor.y);
9182
+ const bPerpendicularDistance = isYOrientation(orientation) ? Math.abs(b.x - preservedColumnAnchor.x) : Math.abs(b.y - preservedColumnAnchor.y);
9183
+ return bAlongDirection - aAlongDirection || aPerpendicularDistance - bPerpendicularDistance;
8996
9184
  });
8997
9185
  for (const connectorSource of candidatePoints) {
8998
- const preservedColumnAnchor = this.getSearchStartAnchor(
8999
- label,
9000
- orientation
9001
- );
9002
9186
  const preservedColumnCandidate = this.createCandidate(
9003
9187
  label,
9004
9188
  {
@@ -9024,10 +9208,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9024
9208
  orientation
9025
9209
  );
9026
9210
  if (outwardDirection.x === 0 && outwardDirection.y === 0) continue;
9027
- for (let distance5 = LABEL_SEARCH_STEP; distance5 <= this.maxSearchDistance + EPS12; distance5 += LABEL_SEARCH_STEP) {
9211
+ for (let distance7 = LABEL_SEARCH_STEP; distance7 <= this.maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9028
9212
  const anchorPoint = {
9029
- x: connectorSource.x + outwardDirection.x * distance5,
9030
- y: connectorSource.y + outwardDirection.y * distance5
9213
+ x: connectorSource.x + outwardDirection.x * distance7,
9214
+ y: connectorSource.y + outwardDirection.y * distance7
9031
9215
  };
9032
9216
  const candidate = this.createCandidate(
9033
9217
  label,
@@ -9040,7 +9224,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9040
9224
  label,
9041
9225
  labelIndex,
9042
9226
  "outward-trace-anchor",
9043
- distance5
9227
+ distance7
9044
9228
  );
9045
9229
  this.recordCandidateResult(result);
9046
9230
  if (result.status === "valid") {
@@ -9051,10 +9235,11 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9051
9235
  }
9052
9236
  return null;
9053
9237
  }
9054
- getTraceAnchorCandidatePoints(label, orientation) {
9238
+ getTraceAnchorCandidates(label, orientation) {
9055
9239
  const seen = /* @__PURE__ */ new Set();
9056
9240
  const points = [];
9057
- const connectedTraceIds = new Set(label.mspConnectionPairIds ?? []);
9241
+ const directHostTraceIds = new Set(label.mspConnectionPairIds ?? []);
9242
+ const connectedTraceIds = new Set(directHostTraceIds);
9058
9243
  if (isYOrientation(orientation)) {
9059
9244
  const connectedPinIds = /* @__PURE__ */ new Set();
9060
9245
  for (const traceId of connectedTraceIds) {
@@ -9094,7 +9279,39 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9094
9279
  points.push(point);
9095
9280
  }
9096
9281
  }
9097
- return points;
9282
+ const direction = dir(orientation);
9283
+ const furthestAlong = Math.max(
9284
+ ...points.map((point) => point.x * direction.x + point.y * direction.y)
9285
+ );
9286
+ const furthestRowPoints = points.filter(
9287
+ (point) => Math.abs(
9288
+ point.x * direction.x + point.y * direction.y - furthestAlong
9289
+ ) <= EPS12
9290
+ );
9291
+ const directHostPinIds = new Set(
9292
+ [...directHostTraceIds].flatMap(
9293
+ (traceId) => this.traceMap[traceId]?.pinIds ?? []
9294
+ )
9295
+ );
9296
+ const adjacentTraces = [...connectedTraceIds].flatMap((traceId) => {
9297
+ if (directHostTraceIds.has(traceId)) return [];
9298
+ const trace = this.traceMap[traceId];
9299
+ if (!trace?.pinIds.some((pinId) => directHostPinIds.has(pinId))) return [];
9300
+ return [trace];
9301
+ });
9302
+ return {
9303
+ points,
9304
+ // Prefer the nearest bend when the outward row is on a trace directly
9305
+ // adjacent to the label's host. Deeper transitive chains keep their
9306
+ // stable trace ordering.
9307
+ preferNearestBendOnOutwardRow: adjacentTraces.some(
9308
+ (trace) => furthestRowPoints.some(
9309
+ (point) => trace.tracePath.some(
9310
+ (tracePoint) => Math.abs(tracePoint.x - point.x) <= EPS12 && Math.abs(tracePoint.y - point.y) <= EPS12
9311
+ )
9312
+ )
9313
+ )
9314
+ };
9098
9315
  }
9099
9316
  sharesVerticalRailWithAny(trace, otherTraces) {
9100
9317
  const verticalRailXs = /* @__PURE__ */ new Set();
@@ -9203,10 +9420,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9203
9420
  phase = "shift",
9204
9421
  stopOnTraceCollision = true
9205
9422
  } = params;
9206
- for (let distance5 = LABEL_SEARCH_STEP; distance5 <= maxSearchDistance + EPS12; distance5 += LABEL_SEARCH_STEP) {
9423
+ for (let distance7 = LABEL_SEARCH_STEP; distance7 <= maxSearchDistance + EPS12; distance7 += LABEL_SEARCH_STEP) {
9207
9424
  const anchorPoint = {
9208
- x: baseAnchor.x + direction.x * distance5,
9209
- y: baseAnchor.y + direction.y * distance5
9425
+ x: baseAnchor.x + direction.x * distance7,
9426
+ y: baseAnchor.y + direction.y * distance7
9210
9427
  };
9211
9428
  const candidate = this.createCandidate(label, anchorPoint, orientation);
9212
9429
  const result = this.evaluateCandidate(
@@ -9214,7 +9431,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9214
9431
  label,
9215
9432
  labelIndex,
9216
9433
  phase,
9217
- distance5,
9434
+ distance7,
9218
9435
  outwardDistance
9219
9436
  );
9220
9437
  this.recordCandidateResult(result);
@@ -9226,11 +9443,11 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9226
9443
  }
9227
9444
  return null;
9228
9445
  }
9229
- evaluateCandidate(candidate, label, labelIndex, phase, distance5, outwardDistance) {
9446
+ evaluateCandidate(candidate, label, labelIndex, phase, distance7, outwardDistance) {
9230
9447
  return {
9231
9448
  ...candidate,
9232
9449
  phase,
9233
- distance: distance5,
9450
+ distance: distance7,
9234
9451
  outwardDistance,
9235
9452
  selected: false,
9236
9453
  status: this.getCandidateStatus({
@@ -9736,10 +9953,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9736
9953
  if (point.x < bounds.minX - EPS12 || point.x > bounds.maxX + EPS12 || point.y < bounds.minY - EPS12 || point.y > bounds.maxY + EPS12) {
9737
9954
  continue;
9738
9955
  }
9739
- for (const [side, distance5] of getSideDistances(point, bounds)) {
9740
- if (distance5 < nearestDistance) {
9956
+ for (const [side, distance7] of getSideDistances(point, bounds)) {
9957
+ if (distance7 < nearestDistance) {
9741
9958
  nearestSide = side;
9742
- nearestDistance = distance5;
9959
+ nearestDistance = distance7;
9743
9960
  }
9744
9961
  }
9745
9962
  }
@@ -9768,10 +9985,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9768
9985
  let nearestSide = null;
9769
9986
  let nearestDistance = Number.POSITIVE_INFINITY;
9770
9987
  for (const chip of this.chipObstacleSpatialIndex.chips) {
9771
- for (const [side, distance5] of getSideDistances(point, chip.bounds)) {
9772
- if (distance5 < nearestDistance) {
9988
+ for (const [side, distance7] of getSideDistances(point, chip.bounds)) {
9989
+ if (distance7 < nearestDistance) {
9773
9990
  nearestSide = side;
9774
- nearestDistance = distance5;
9991
+ nearestDistance = distance7;
9775
9992
  }
9776
9993
  }
9777
9994
  }
@@ -10354,17 +10571,17 @@ var getTraceLength = (trace) => {
10354
10571
  }
10355
10572
  return length;
10356
10573
  };
10357
- var getPointAtTraceDistance = (trace, distance5) => {
10574
+ var getPointAtTraceDistance = (trace, distance7) => {
10358
10575
  let pathDistance = 0;
10359
10576
  for (let i = 0; i < trace.tracePath.length - 1; i++) {
10360
10577
  const start = trace.tracePath[i];
10361
10578
  const end = trace.tracePath[i + 1];
10362
10579
  const segmentLength = getManhattanDistance(start, end);
10363
10580
  const nextDistance = pathDistance + segmentLength;
10364
- if (distance5 <= nextDistance + EPS13) {
10581
+ if (distance7 <= nextDistance + EPS13) {
10365
10582
  const offset = Math.max(
10366
10583
  0,
10367
- Math.min(segmentLength, distance5 - pathDistance)
10584
+ Math.min(segmentLength, distance7 - pathDistance)
10368
10585
  );
10369
10586
  const direction = getSegmentDirection(start, end);
10370
10587
  return {
@@ -10489,13 +10706,13 @@ var getCandidateDistances = (traceLength, vertexDistances) => {
10489
10706
  const distances = /* @__PURE__ */ new Set();
10490
10707
  const maxSteps = Math.ceil(traceLength / CANDIDATE_STEP);
10491
10708
  for (let i = 0; i <= maxSteps; i++) {
10492
- const distance5 = Math.min(traceLength, i * CANDIDATE_STEP);
10493
- distances.add(roundDistance(distance5));
10709
+ const distance7 = Math.min(traceLength, i * CANDIDATE_STEP);
10710
+ distances.add(roundDistance(distance7));
10494
10711
  }
10495
- for (const distance5 of vertexDistances) {
10496
- distances.add(roundDistance(distance5));
10712
+ for (const distance7 of vertexDistances) {
10713
+ distances.add(roundDistance(distance7));
10497
10714
  }
10498
- return [...distances].filter((distance5) => distance5 >= -EPS13 && distance5 <= traceLength + EPS13).sort((a, b) => a - b);
10715
+ return [...distances].filter((distance7) => distance7 >= -EPS13 && distance7 <= traceLength + EPS13).sort((a, b) => a - b);
10499
10716
  };
10500
10717
  var getOrientationsForPoint = (params) => {
10501
10718
  const { inputProblem, label, point, orientationConstraint } = params;
@@ -10668,7 +10885,7 @@ var getNetLabelHeight = (inputProblem, label) => {
10668
10885
  (nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid))
10669
10886
  )?.netLabelHeight;
10670
10887
  };
10671
- var roundDistance = (distance5) => Number(distance5.toFixed(6));
10888
+ var roundDistance = (distance7) => Number(distance7.toFixed(6));
10672
10889
  var isSamePlacement = (label, point, orientation) => Math.abs(point.x - label.anchorPoint.x) <= EPS13 && Math.abs(point.y - label.anchorPoint.y) <= EPS13 && orientation === label.orientation;
10673
10890
 
10674
10891
  // lib/solvers/TraceAnchoredNetLabelOverlapSolver/visualize.ts
@@ -11649,7 +11866,7 @@ ${c.status}`
11649
11866
  };
11650
11867
 
11651
11868
  // lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts
11652
- import { distance as distance4, doSegmentsIntersect as doSegmentsIntersect3 } from "@tscircuit/math-utils";
11869
+ import { distance as distance6, doSegmentsIntersect as doSegmentsIntersect3 } from "@tscircuit/math-utils";
11653
11870
  var ROUTE_CLEARANCE = 0.2;
11654
11871
  var COORDINATE_TOLERANCE = 1e-9;
11655
11872
  var GROUND_NET_ID = "GND";
@@ -11811,7 +12028,7 @@ var getJunctionCandidates = ({
11811
12028
  facingDirection: pin._facingDirection
11812
12029
  });
11813
12030
  for (const junctionPoint of junctionPoints) {
11814
- if (distance4(pin, junctionPoint) > maxConnectionDistance) {
12031
+ if (distance6(pin, junctionPoint) > maxConnectionDistance) {
11815
12032
  continue;
11816
12033
  }
11817
12034
  candidates.push(
@@ -12027,7 +12244,7 @@ var UnroutedTraceRecoverySolver = class extends BaseSolver {
12027
12244
  if (connectionPair.globalConnNetId === this.groundGlobalConnNetId) {
12028
12245
  return;
12029
12246
  }
12030
- if (distance4(connectionPair.pins[0], connectionPair.pins[1]) > this.maxConnectionDistance) {
12247
+ if (distance6(connectionPair.pins[0], connectionPair.pins[1]) > this.maxConnectionDistance) {
12031
12248
  return;
12032
12249
  }
12033
12250
  const obstacles = getObstacleRects(this.inputProblem);
@@ -13010,17 +13227,17 @@ var getRequiredOutwardDistance = (label, inlineBounds) => {
13010
13227
  const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY));
13011
13228
  return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE2);
13012
13229
  };
13013
- var moveLabel = (label, orientation, distance5) => {
13230
+ var moveLabel = (label, orientation, distance7) => {
13014
13231
  const direction = dir(orientation);
13015
13232
  return {
13016
13233
  ...label,
13017
13234
  anchorPoint: {
13018
- x: label.anchorPoint.x + direction.x * distance5,
13019
- y: label.anchorPoint.y + direction.y * distance5
13235
+ x: label.anchorPoint.x + direction.x * distance7,
13236
+ y: label.anchorPoint.y + direction.y * distance7
13020
13237
  },
13021
13238
  center: {
13022
- x: label.center.x + direction.x * distance5,
13023
- y: label.center.y + direction.y * distance5
13239
+ x: label.center.x + direction.x * distance7,
13240
+ y: label.center.y + direction.y * distance7
13024
13241
  }
13025
13242
  };
13026
13243
  };
@@ -13145,15 +13362,15 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
13145
13362
  const movedLabelIndices = /* @__PURE__ */ new Set();
13146
13363
  for (let triggerIndex = 0; triggerIndex < outputLabels.length; triggerIndex++) {
13147
13364
  const trigger = outputLabels[triggerIndex];
13148
- const distance5 = getRequiredOutwardDistance(trigger, inlineBounds);
13149
- if (distance5 <= POINT_EPSILON || distance5 > MAX_OUTWARD_DISTANCE) continue;
13365
+ const distance7 = getRequiredOutwardDistance(trigger, inlineBounds);
13366
+ if (distance7 <= POINT_EPSILON || distance7 > MAX_OUTWARD_DISTANCE) continue;
13150
13367
  const { group, ownerChipIds } = getContiguousLabelGroup({
13151
13368
  triggerIndex,
13152
13369
  labels: outputLabels,
13153
13370
  chipIdByPinId
13154
13371
  });
13155
13372
  const distances = new Map(
13156
- [...group].map((labelIndex) => [labelIndex, distance5])
13373
+ [...group].map((labelIndex) => [labelIndex, distance7])
13157
13374
  );
13158
13375
  let failed = false;
13159
13376
  for (let iteration = 0; iteration < outputLabels.length; iteration++) {