@tscircuit/schematic-trace-solver 0.0.100 → 0.0.102

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 (34) hide show
  1. package/dist/index.d.ts +13 -6
  2. package/dist/index.js +219 -67
  3. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +26 -7
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +5 -2
  5. package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts +168 -24
  6. package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +128 -48
  7. package/package.json +1 -1
  8. package/site/bug-reports/bug-report-20260716T144856Z.page.tsx +4 -0
  9. package/site/bug-reports/bug-report-20260717T022934Z.page.tsx +4 -0
  10. package/site/bug-reports/bug-report-20260717T031704Z.page.tsx +4 -0
  11. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
  12. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +3 -3
  13. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -5
  14. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
  15. package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +149 -0
  16. package/tests/bug-reports/bug-report-20260716T144856Z/bug-report-20260716T144856Z.json +693 -0
  17. package/tests/bug-reports/bug-report-20260716T144856Z/bug-report-20260716T144856Z.test.ts +12 -0
  18. package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +130 -0
  19. package/tests/bug-reports/bug-report-20260717T022934Z/bug-report-20260717T022934Z.json +543 -0
  20. package/tests/bug-reports/bug-report-20260717T022934Z/bug-report-20260717T022934Z.test.ts +12 -0
  21. package/tests/bug-reports/bug-report-20260717T031704Z/__snapshots__/bug-report-20260717T031704Z.snap.svg +48 -0
  22. package/tests/bug-reports/bug-report-20260717T031704Z/bug-report-20260717T031704Z.json +77 -0
  23. package/tests/bug-reports/bug-report-20260717T031704Z/bug-report-20260717T031704Z.test.ts +12 -0
  24. package/tests/examples/__snapshots__/example11.snap.svg +19 -23
  25. package/tests/examples/__snapshots__/example19.snap.svg +3 -5
  26. package/tests/examples/__snapshots__/example29.snap.svg +13 -13
  27. package/tests/examples/__snapshots__/example37.snap.svg +4 -2
  28. package/tests/examples/example16.test.ts +22 -0
  29. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +2 -8
  30. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +19 -23
  31. package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +10 -10
  32. package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +32 -2
  33. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +37 -0
  34. package/tests/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.test.ts +174 -0
package/dist/index.d.ts CHANGED
@@ -239,8 +239,10 @@ interface OverlappingTraceSegmentLocator {
239
239
  traceSegmentIndex: number;
240
240
  }>;
241
241
  }
242
+ type TraceInteractionKind = "overlap" | "point_contact";
242
243
  declare class TraceOverlapIssueSolver extends BaseSolver {
243
244
  overlappingTraceSegments: OverlappingTraceSegmentLocator[];
245
+ interactionKind: TraceInteractionKind;
244
246
  traceNetIslands: Record<ConnNetId$1, Array<SolvedTracePath>>;
245
247
  obstacleRects: ReturnType<typeof getObstacleRects>;
246
248
  SHIFT_DISTANCE: number;
@@ -248,9 +250,12 @@ declare class TraceOverlapIssueSolver extends BaseSolver {
248
250
  constructor(params: {
249
251
  inputProblem: InputProblem;
250
252
  overlappingTraceSegments: OverlappingTraceSegmentLocator[];
253
+ interactionKind: TraceInteractionKind;
251
254
  traceNetIslands: Record<ConnNetId$1, Array<SolvedTracePath>>;
252
255
  });
253
256
  _step(): void;
257
+ private applyOffsets;
258
+ private countDifferentNetIntersections;
254
259
  private getObstacleAwareOffset;
255
260
  private getBlockingCollisions;
256
261
  visualize(): GraphicsObject;
@@ -258,14 +263,15 @@ declare class TraceOverlapIssueSolver extends BaseSolver {
258
263
 
259
264
  type ConnNetId = string;
260
265
  /**
261
- * This solver finds traces that overlap (coincident and parallel) and aren't
262
- * connected via the globalConnMap, then shifts them to avoid the overlap in
263
- * such a way that minimizes the resulting intersections
266
+ * This solver finds traces that overlap or meet collinearly and aren't
267
+ * connected via the globalConnMap, then shifts them apart in the direction
268
+ * that minimizes the resulting intersections.
264
269
  *
265
270
  * All traces are orthogonal, so for traces to be considered overlapping, they
266
- * need to each have a segment where both are horizontal or both are vertical
267
- * AND the segments must be within 1e-6 of each other in X (if vertical) or
268
- * Y (if horizontal)
271
+ * need to each have a segment where both are horizontal or both are vertical,
272
+ * the segments must be on the same axis, and their ranges must overlap. A
273
+ * point contact is also handled when both segments are internal rails, where
274
+ * it can be separated without moving an anchored terminal segment.
269
275
  *
270
276
  * Each iteration, we find overlapping traces that aren't part of the same net.
271
277
  * This is the same as finding two "trace net islands" that have an overlap.
@@ -294,6 +300,7 @@ declare class TraceOverlapShiftSolver extends BaseSolver {
294
300
  computeTraceNetIslands(): Record<ConnNetId, Array<SolvedTracePath>>;
295
301
  findNextOverlapIssue(): {
296
302
  overlappingTraceSegments: Array<OverlappingTraceSegmentLocator>;
303
+ interactionKind: TraceInteractionKind;
297
304
  } | null;
298
305
  private findNextDiagonalSegment;
299
306
  private findAndFixNextDiagonalSegment;
package/dist/index.js CHANGED
@@ -894,7 +894,7 @@ var generateEndpointCollisionDetours = ({
894
894
  collidingSegmentIndex,
895
895
  obstacle
896
896
  }) => {
897
- if (path.length !== 3) return [];
897
+ if (path.length < 3 || path.length > 4) return [];
898
898
  const lastSegmentIndex = path.length - 2;
899
899
  if (collidingSegmentIndex !== 0 && collidingSegmentIndex !== lastSegmentIndex) {
900
900
  return [];
@@ -917,7 +917,7 @@ var generateEndpointCollisionDetours = ({
917
917
  const detours = [];
918
918
  for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
919
919
  for (const detourCoordinate of [...new Set(detourCoordinates)]) {
920
- const orderedDetour = firstSegmentAxis === "y" ? [
920
+ const orderedDetourPrefix = firstSegmentAxis === "y" ? [
921
921
  start,
922
922
  { x: escapeCoordinate, y: start.y },
923
923
  { x: escapeCoordinate, y: detourCoordinate },
@@ -930,6 +930,7 @@ var generateEndpointCollisionDetours = ({
930
930
  { x: detourCoordinate, y: end.y },
931
931
  end
932
932
  ];
933
+ const orderedDetour = [...orderedDetourPrefix, ...orderedPath.slice(3)];
933
934
  const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour;
934
935
  if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour);
935
936
  }
@@ -1287,7 +1288,10 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1287
1288
  let { segIndex, rect } = collision;
1288
1289
  const isFirstSegment = segIndex === 0;
1289
1290
  const isLastSegment = segIndex === path.length - 2;
1290
- if (path.length === 3 && (isFirstSegment || isLastSegment)) {
1291
+ const isEndpointChipObstacle = rect.kind === "chip" && this.pins.some((pin) => pin.chipId === rect.chipId);
1292
+ const canGenerateEndpointDetour = path.length === 3 || path.length === 4 && this.connectionPair !== void 0 && !isEndpointChipObstacle;
1293
+ if (canGenerateEndpointDetour && (isFirstSegment || isLastSegment)) {
1294
+ 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);
1291
1295
  const detours = generateEndpointCollisionDetours({
1292
1296
  path,
1293
1297
  collidingSegmentIndex: segIndex,
@@ -1297,9 +1301,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1297
1301
  if (this.visited.has(key)) return false;
1298
1302
  this.visited.add(key);
1299
1303
  return true;
1300
- }).sort(
1301
- (a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2)
1302
- );
1304
+ }).sort(compareDetours);
1303
1305
  for (const detour of detours) {
1304
1306
  const nextCollisionRects = new Set(collisionRects);
1305
1307
  nextCollisionRects.add(rect);
@@ -1539,6 +1541,9 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
1539
1541
  }
1540
1542
  };
1541
1543
 
1544
+ // lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts
1545
+ import { doSegmentsIntersect } from "@tscircuit/math-utils";
1546
+
1542
1547
  // lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/applyJogToTrace.ts
1543
1548
  var applyJogToTerminalSegment = ({
1544
1549
  pts,
@@ -1627,6 +1632,7 @@ var applyJogToTerminalSegment = ({
1627
1632
  var EPS4 = 1e-6;
1628
1633
  var TraceOverlapIssueSolver = class extends BaseSolver {
1629
1634
  overlappingTraceSegments;
1635
+ interactionKind;
1630
1636
  traceNetIslands;
1631
1637
  obstacleRects;
1632
1638
  SHIFT_DISTANCE = 0.1;
@@ -1634,6 +1640,7 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1634
1640
  constructor(params) {
1635
1641
  super();
1636
1642
  this.overlappingTraceSegments = params.overlappingTraceSegments;
1643
+ this.interactionKind = params.interactionKind;
1637
1644
  this.traceNetIslands = params.traceNetIslands;
1638
1645
  this.obstacleRects = getObstacleRects(params.inputProblem);
1639
1646
  for (const { connNetId, pathsWithOverlap } of this.overlappingTraceSegments) {
@@ -1657,19 +1664,64 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1657
1664
  const someGroupCanShift = groupShouldStayInPlace.some(
1658
1665
  (shouldStay) => !shouldStay
1659
1666
  );
1660
- const offsets = this.overlappingTraceSegments.map((group, idx) => {
1661
- if (someGroupCanShift && groupShouldStayInPlace[idx]) return 0;
1662
- const n = Math.floor(idx / 2) + 1;
1663
- const signed = idx % 2 === 0 ? -n : n;
1667
+ const getSeparationOffsets = (firstDirection) => this.overlappingTraceSegments.map((group, groupIndex) => {
1668
+ if (someGroupCanShift && groupShouldStayInPlace[groupIndex]) return 0;
1669
+ const n = Math.floor(groupIndex / 2) + 1;
1670
+ const direction = groupIndex % 2 === 0 ? firstDirection : -firstDirection;
1664
1671
  return this.getObstacleAwareOffset({
1665
1672
  group,
1666
- offset: signed * this.SHIFT_DISTANCE
1673
+ offset: direction * n * this.SHIFT_DISTANCE
1667
1674
  });
1668
1675
  });
1676
+ const establishedOffsets = getSeparationOffsets(-1);
1677
+ const isCompoundOverlap = this.interactionKind === "overlap" && this.overlappingTraceSegments.some(
1678
+ (group) => group.pathsWithOverlap.length > 1
1679
+ );
1680
+ if (isCompoundOverlap) {
1681
+ this.correctedTraceMap = this.applyOffsets(establishedOffsets);
1682
+ this.solved = true;
1683
+ return;
1684
+ }
1685
+ const offsetCandidates = this.interactionKind === "overlap" ? [establishedOffsets, getSeparationOffsets(1)] : [];
1686
+ if (this.interactionKind === "point_contact") {
1687
+ const movableGroupIndexes = this.overlappingTraceSegments.map((_, groupIndex) => groupIndex).filter(
1688
+ (groupIndex) => !someGroupCanShift || !groupShouldStayInPlace[groupIndex]
1689
+ ).reverse();
1690
+ for (const groupIndex of movableGroupIndexes) {
1691
+ const group = this.overlappingTraceSegments[groupIndex];
1692
+ for (const direction of [-1, 1]) {
1693
+ const offsets = this.overlappingTraceSegments.map(() => 0);
1694
+ offsets[groupIndex] = this.getObstacleAwareOffset({
1695
+ group,
1696
+ offset: direction * this.SHIFT_DISTANCE
1697
+ });
1698
+ offsetCandidates.push(offsets);
1699
+ }
1700
+ }
1701
+ }
1702
+ const candidates = offsetCandidates.map((offsets) => {
1703
+ const correctedTraceMap = this.applyOffsets(offsets);
1704
+ return {
1705
+ correctedTraceMap,
1706
+ intersectionCount: this.countDifferentNetIntersections(correctedTraceMap),
1707
+ totalDisplacement: offsets.reduce(
1708
+ (total, offset) => total + Math.abs(offset),
1709
+ 0
1710
+ )
1711
+ };
1712
+ });
1713
+ candidates.sort(
1714
+ (a, b) => a.intersectionCount - b.intersectionCount || a.totalDisplacement - b.totalDisplacement
1715
+ );
1716
+ this.correctedTraceMap = candidates[0].correctedTraceMap;
1717
+ this.solved = true;
1718
+ }
1719
+ applyOffsets(offsets) {
1720
+ const correctedTraceMap = { ...this.correctedTraceMap };
1669
1721
  const eq = (a, b) => Math.abs(a - b) < EPS4;
1670
1722
  const samePoint = (p, q) => !!p && !!q && eq(p.x, q.x) && eq(p.y, q.y);
1671
- this.overlappingTraceSegments.forEach((group, gidx) => {
1672
- const offset = offsets[gidx];
1723
+ this.overlappingTraceSegments.forEach((group, groupIndex) => {
1724
+ const offset = offsets[groupIndex];
1673
1725
  if (offset === 0) return;
1674
1726
  const byPath = /* @__PURE__ */ new Map();
1675
1727
  for (const loc of group.pathsWithOverlap) {
@@ -1680,9 +1732,22 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1680
1732
  }
1681
1733
  for (const [pathIdx, segIdxSet] of byPath) {
1682
1734
  const original = this.traceNetIslands[group.connNetId][pathIdx];
1683
- const current = this.correctedTraceMap[original.mspPairId] ?? original;
1735
+ const current = correctedTraceMap[original.mspPairId] ?? original;
1684
1736
  const pts = current.tracePath.map((p) => ({ ...p }));
1685
1737
  const segIdxsRev = Array.from(segIdxSet).sort((a, b) => a - b).reverse();
1738
+ const segmentAxes = /* @__PURE__ */ new Map();
1739
+ for (const si of segIdxsRev) {
1740
+ const start = pts[si];
1741
+ const end = pts[si + 1];
1742
+ if (!start || !end) continue;
1743
+ if (Math.abs(start.x - end.x) < EPS4) {
1744
+ segmentAxes.set(si, "vertical");
1745
+ } else if (Math.abs(start.y - end.y) < EPS4) {
1746
+ segmentAxes.set(si, "horizontal");
1747
+ }
1748
+ }
1749
+ const shiftedXPointIndexes = /* @__PURE__ */ new Set();
1750
+ const shiftedYPointIndexes = /* @__PURE__ */ new Set();
1686
1751
  const JOG_SIZE = this.SHIFT_DISTANCE;
1687
1752
  for (const si of segIdxsRev) {
1688
1753
  if (si < 0 || si >= pts.length - 1) continue;
@@ -1697,15 +1762,25 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1697
1762
  } else {
1698
1763
  const start = pts[si];
1699
1764
  const end = pts[si + 1];
1700
- const isVertical5 = Math.abs(start.x - end.x) < EPS4;
1701
- const isHorizontal4 = Math.abs(start.y - end.y) < EPS4;
1702
- if (!isVertical5 && !isHorizontal4) continue;
1703
- if (isVertical5) {
1704
- start.x += offset;
1705
- end.x += offset;
1706
- } else {
1707
- start.y += offset;
1708
- end.y += offset;
1765
+ const axis = segmentAxes.get(si);
1766
+ if (axis === "vertical") {
1767
+ if (!shiftedXPointIndexes.has(si)) {
1768
+ start.x += offset;
1769
+ shiftedXPointIndexes.add(si);
1770
+ }
1771
+ if (!shiftedXPointIndexes.has(si + 1)) {
1772
+ end.x += offset;
1773
+ shiftedXPointIndexes.add(si + 1);
1774
+ }
1775
+ } else if (axis === "horizontal") {
1776
+ if (!shiftedYPointIndexes.has(si)) {
1777
+ start.y += offset;
1778
+ shiftedYPointIndexes.add(si);
1779
+ }
1780
+ if (!shiftedYPointIndexes.has(si + 1)) {
1781
+ end.y += offset;
1782
+ shiftedYPointIndexes.add(si + 1);
1783
+ }
1709
1784
  }
1710
1785
  }
1711
1786
  }
@@ -1715,13 +1790,39 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1715
1790
  cleaned.push(p);
1716
1791
  }
1717
1792
  }
1718
- this.correctedTraceMap[original.mspPairId] = {
1793
+ correctedTraceMap[original.mspPairId] = {
1719
1794
  ...current,
1720
1795
  tracePath: cleaned
1721
1796
  };
1722
1797
  }
1723
1798
  });
1724
- this.solved = true;
1799
+ return correctedTraceMap;
1800
+ }
1801
+ countDifferentNetIntersections(correctedTraceMap) {
1802
+ const traces = Object.values(this.traceNetIslands).flatMap(
1803
+ (island) => island.map((trace) => correctedTraceMap[trace.mspPairId] ?? trace)
1804
+ );
1805
+ let intersectionCount = 0;
1806
+ for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
1807
+ const trace = traces[traceIndex];
1808
+ for (let otherTraceIndex = traceIndex + 1; otherTraceIndex < traces.length; otherTraceIndex++) {
1809
+ const otherTrace = traces[otherTraceIndex];
1810
+ if (trace.globalConnNetId === otherTrace.globalConnNetId) continue;
1811
+ for (let segmentIndex = 0; segmentIndex < trace.tracePath.length - 1; segmentIndex++) {
1812
+ for (let otherSegmentIndex = 0; otherSegmentIndex < otherTrace.tracePath.length - 1; otherSegmentIndex++) {
1813
+ if (doSegmentsIntersect(
1814
+ trace.tracePath[segmentIndex],
1815
+ trace.tracePath[segmentIndex + 1],
1816
+ otherTrace.tracePath[otherSegmentIndex],
1817
+ otherTrace.tracePath[otherSegmentIndex + 1]
1818
+ )) {
1819
+ intersectionCount++;
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+ }
1825
+ return intersectionCount;
1725
1826
  }
1726
1827
  getObstacleAwareOffset({
1727
1828
  group,
@@ -1883,15 +1984,58 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
1883
1984
  const pathsB = this.traceNetIslands[netB] || [];
1884
1985
  const overlapsA = [];
1885
1986
  const overlapsB = [];
1987
+ const pointContacts = [];
1886
1988
  const seenA = /* @__PURE__ */ new Set();
1887
1989
  const seenB = /* @__PURE__ */ new Set();
1888
- const overlaps1D = (a1, a2, b1, b2) => {
1990
+ const getRangeOverlap1D = (a1, a2, b1, b2) => {
1889
1991
  const minA = Math.min(a1, a2);
1890
1992
  const maxA = Math.max(a1, a2);
1891
1993
  const minB = Math.min(b1, b2);
1892
1994
  const maxB = Math.max(b1, b2);
1893
- const overlap = Math.min(maxA, maxB) - Math.max(minA, minB);
1894
- return overlap > EPS11;
1995
+ return Math.min(maxA, maxB) - Math.max(minA, minB);
1996
+ };
1997
+ const recordCollinearInteraction = ({
1998
+ pathAIndex,
1999
+ segmentAIndex,
2000
+ pathBIndex,
2001
+ segmentBIndex,
2002
+ rangeOverlap
2003
+ }) => {
2004
+ if (rangeOverlap > EPS11) {
2005
+ const keyA = `${pathAIndex}:${segmentAIndex}`;
2006
+ const keyB = `${pathBIndex}:${segmentBIndex}`;
2007
+ if (!seenA.has(keyA)) {
2008
+ overlapsA.push({
2009
+ solvedTracePathIndex: pathAIndex,
2010
+ traceSegmentIndex: segmentAIndex
2011
+ });
2012
+ seenA.add(keyA);
2013
+ }
2014
+ if (!seenB.has(keyB)) {
2015
+ overlapsB.push({
2016
+ solvedTracePathIndex: pathBIndex,
2017
+ traceSegmentIndex: segmentBIndex
2018
+ });
2019
+ seenB.add(keyB);
2020
+ }
2021
+ return;
2022
+ }
2023
+ if (rangeOverlap < -EPS11) return;
2024
+ const pathA = pathsA[pathAIndex].tracePath;
2025
+ const pathB = pathsB[pathBIndex].tracePath;
2026
+ const segmentAStart = pathA[segmentAIndex];
2027
+ const segmentAEnd = pathA[segmentAIndex + 1];
2028
+ const segmentBStart = pathB[segmentBIndex];
2029
+ const segmentBEnd = pathB[segmentBIndex + 1];
2030
+ const hasTerminalSegment = segmentAIndex === 0 || segmentAIndex === pathA.length - 2 || segmentBIndex === 0 || segmentBIndex === pathB.length - 2;
2031
+ if (hasTerminalSegment) return;
2032
+ pointContacts.push({
2033
+ pathAIndex,
2034
+ segmentAIndex,
2035
+ pathBIndex,
2036
+ segmentBIndex,
2037
+ combinedLength: Math.abs(segmentAStart.x - segmentAEnd.x) + Math.abs(segmentAStart.y - segmentAEnd.y) + Math.abs(segmentBStart.x - segmentBEnd.x) + Math.abs(segmentBStart.y - segmentBEnd.y)
2038
+ });
1895
2039
  };
1896
2040
  for (let pa = 0; pa < pathsA.length; pa++) {
1897
2041
  const pathA = pathsA[pa];
@@ -1913,45 +2057,23 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
1913
2057
  if (!bVert && !bHorz) continue;
1914
2058
  if (aVert && bVert) {
1915
2059
  if (Math.abs(a1.x - b1.x) < EPS11) {
1916
- if (overlaps1D(a1.y, a2.y, b1.y, b2.y)) {
1917
- const keyA = `${pa}:${sa}`;
1918
- const keyB = `${pb}:${sb}`;
1919
- if (!seenA.has(keyA)) {
1920
- overlapsA.push({
1921
- solvedTracePathIndex: pa,
1922
- traceSegmentIndex: sa
1923
- });
1924
- seenA.add(keyA);
1925
- }
1926
- if (!seenB.has(keyB)) {
1927
- overlapsB.push({
1928
- solvedTracePathIndex: pb,
1929
- traceSegmentIndex: sb
1930
- });
1931
- seenB.add(keyB);
1932
- }
1933
- }
2060
+ recordCollinearInteraction({
2061
+ pathAIndex: pa,
2062
+ segmentAIndex: sa,
2063
+ pathBIndex: pb,
2064
+ segmentBIndex: sb,
2065
+ rangeOverlap: getRangeOverlap1D(a1.y, a2.y, b1.y, b2.y)
2066
+ });
1934
2067
  }
1935
2068
  } else if (aHorz && bHorz) {
1936
2069
  if (Math.abs(a1.y - b1.y) < EPS11) {
1937
- if (overlaps1D(a1.x, a2.x, b1.x, b2.x)) {
1938
- const keyA = `${pa}:${sa}`;
1939
- const keyB = `${pb}:${sb}`;
1940
- if (!seenA.has(keyA)) {
1941
- overlapsA.push({
1942
- solvedTracePathIndex: pa,
1943
- traceSegmentIndex: sa
1944
- });
1945
- seenA.add(keyA);
1946
- }
1947
- if (!seenB.has(keyB)) {
1948
- overlapsB.push({
1949
- solvedTracePathIndex: pb,
1950
- traceSegmentIndex: sb
1951
- });
1952
- seenB.add(keyB);
1953
- }
1954
- }
2070
+ recordCollinearInteraction({
2071
+ pathAIndex: pa,
2072
+ segmentAIndex: sa,
2073
+ pathBIndex: pb,
2074
+ segmentBIndex: sb,
2075
+ rangeOverlap: getRangeOverlap1D(a1.x, a2.x, b1.x, b2.x)
2076
+ });
1955
2077
  }
1956
2078
  }
1957
2079
  }
@@ -1960,12 +2082,41 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
1960
2082
  }
1961
2083
  if (overlapsA.length > 0 && overlapsB.length > 0) {
1962
2084
  return {
2085
+ interactionKind: "overlap",
1963
2086
  overlappingTraceSegments: [
1964
2087
  { connNetId: netA, pathsWithOverlap: overlapsA },
1965
2088
  { connNetId: netB, pathsWithOverlap: overlapsB }
1966
2089
  ]
1967
2090
  };
1968
2091
  }
2092
+ const bestPointContact = pointContacts.sort(
2093
+ (a, b) => b.combinedLength - a.combinedLength
2094
+ )[0];
2095
+ if (bestPointContact) {
2096
+ return {
2097
+ interactionKind: "point_contact",
2098
+ overlappingTraceSegments: [
2099
+ {
2100
+ connNetId: netA,
2101
+ pathsWithOverlap: [
2102
+ {
2103
+ solvedTracePathIndex: bestPointContact.pathAIndex,
2104
+ traceSegmentIndex: bestPointContact.segmentAIndex
2105
+ }
2106
+ ]
2107
+ },
2108
+ {
2109
+ connNetId: netB,
2110
+ pathsWithOverlap: [
2111
+ {
2112
+ solvedTracePathIndex: bestPointContact.pathBIndex,
2113
+ traceSegmentIndex: bestPointContact.segmentBIndex
2114
+ }
2115
+ ]
2116
+ }
2117
+ ]
2118
+ };
2119
+ }
1969
2120
  }
1970
2121
  }
1971
2122
  return null;
@@ -2041,9 +2192,10 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
2041
2192
  this.solved = true;
2042
2193
  return;
2043
2194
  }
2044
- const { overlappingTraceSegments } = overlapIssue;
2195
+ const { interactionKind, overlappingTraceSegments } = overlapIssue;
2045
2196
  this.activeSubSolver = new TraceOverlapIssueSolver({
2046
2197
  inputProblem: this.inputProblem,
2198
+ interactionKind,
2047
2199
  overlappingTraceSegments,
2048
2200
  traceNetIslands: this.traceNetIslands
2049
2201
  });
@@ -4162,7 +4314,7 @@ var expandChipsToFitPins = (problem) => {
4162
4314
  };
4163
4315
 
4164
4316
  // lib/utils/does-trace-overlap-with-existing-traces.ts
4165
- import { doSegmentsIntersect } from "@tscircuit/math-utils";
4317
+ import { doSegmentsIntersect as doSegmentsIntersect2 } from "@tscircuit/math-utils";
4166
4318
  function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
4167
4319
  for (let i = 0; i < newTracePath.length - 1; i++) {
4168
4320
  const newSegmentP1 = newTracePath[i];
@@ -4171,7 +4323,7 @@ function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
4171
4323
  for (let j = 0; j < existingTrace.tracePath.length - 1; j++) {
4172
4324
  const existingSegmentP1 = existingTrace.tracePath[j];
4173
4325
  const existingSegmentP2 = existingTrace.tracePath[j + 1];
4174
- if (doSegmentsIntersect(
4326
+ if (doSegmentsIntersect2(
4175
4327
  newSegmentP1,
4176
4328
  newSegmentP2,
4177
4329
  existingSegmentP1,
@@ -357,8 +357,31 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
357
357
  // Never move the first or last segments - move adjacent segment instead
358
358
  const isFirstSegment = segIndex === 0
359
359
  const isLastSegment = segIndex === path.length - 2
360
-
361
- if (path.length === 3 && (isFirstSegment || isLastSegment)) {
360
+ const isEndpointChipObstacle =
361
+ rect.kind === "chip" &&
362
+ this.pins.some((pin) => pin.chipId === rect.chipId)
363
+ // U-shaped detours are local repairs for fixed MSP pairs obstructed by an
364
+ // intermediate obstacle. Endpoint-chip and optional long-distance routing
365
+ // require multi-trace or net-level selection instead.
366
+ const canGenerateEndpointDetour =
367
+ path.length === 3 ||
368
+ (path.length === 4 &&
369
+ this.connectionPair !== undefined &&
370
+ !isEndpointChipObstacle)
371
+
372
+ if (canGenerateEndpointDetour && (isFirstSegment || isLastSegment)) {
373
+ // A three-point detour replaces an L elbow, so the shortest valid route
374
+ // remains the best choice. A four-point detour preserves the far side of
375
+ // a U elbow; prefer the candidate that keeps its new internal rails out
376
+ // of the pin band, then use length to choose between equivalent routes.
377
+ const compareDetours =
378
+ path.length === 4
379
+ ? (a: Point[], b: Point[]) =>
380
+ this.getPinBandPenalty(a) - this.getPinBandPenalty(b) ||
381
+ this.pathLength(a) - this.pathLength(b)
382
+ : (a: Point[], b: Point[]) =>
383
+ this.pathLength(a) - this.pathLength(b) ||
384
+ this.getPinBandPenalty(a) - this.getPinBandPenalty(b)
362
385
  const detours = generateEndpointCollisionDetours({
363
386
  path,
364
387
  collidingSegmentIndex: segIndex,
@@ -370,11 +393,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
370
393
  this.visited.add(key)
371
394
  return true
372
395
  })
373
- .sort(
374
- (a, b) =>
375
- this.pathLength(a) - this.pathLength(b) ||
376
- this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
377
- )
396
+ .sort(compareDetours)
378
397
 
379
398
  for (const detour of detours) {
380
399
  const nextCollisionRects = new Set(collisionRects)
@@ -28,7 +28,9 @@ export const generateEndpointCollisionDetours = ({
28
28
  collidingSegmentIndex: number
29
29
  obstacle: ObstacleRect
30
30
  }): Point[][] => {
31
- if (path.length !== 3) return []
31
+ // Endpoint detours expand the initial L- and U-shaped elbow candidates.
32
+ // Longer paths have already been expanded and are handled by segment shifts.
33
+ if (path.length < 3 || path.length > 4) return []
32
34
 
33
35
  const lastSegmentIndex = path.length - 2
34
36
  if (
@@ -58,7 +60,7 @@ export const generateEndpointCollisionDetours = ({
58
60
  const detours: Point[][] = []
59
61
  for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
60
62
  for (const detourCoordinate of [...new Set(detourCoordinates)]) {
61
- const orderedDetour =
63
+ const orderedDetourPrefix =
62
64
  firstSegmentAxis === "y"
63
65
  ? [
64
66
  start!,
@@ -75,6 +77,7 @@ export const generateEndpointCollisionDetours = ({
75
77
  end!,
76
78
  ]
77
79
 
80
+ const orderedDetour = [...orderedDetourPrefix, ...orderedPath.slice(3)]
78
81
  const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour
79
82
  if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour)
80
83
  }