@tscircuit/schematic-trace-solver 0.0.179 → 0.0.181

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 (142) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +88 -11
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +63 -4
  4. package/lib/solvers/AvailableNetOrientationSolver/traces.ts +36 -0
  5. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -0
  6. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +29 -11
  7. package/package.json +1 -1
  8. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +2 -114
  9. package/tests/bug-reports/bug-report-20260706T213649Z/bug-report-20260706T213649Z.json +1 -0
  10. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +3 -94
  11. package/tests/bug-reports/bug-report-20260706T220324Z/bug-report-20260706T220324Z.json +1 -0
  12. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +533 -578
  13. package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.json +1 -0
  14. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -37
  15. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.json +1 -0
  16. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +4 -25
  17. package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.json +1 -0
  18. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +1 -50
  19. package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.json +1 -0
  20. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +1 -15
  21. package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.json +1 -0
  22. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +5 -33
  23. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.json +1 -0
  24. package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +1 -15
  25. package/tests/bug-reports/bug-report-20260707T230831Z/bug-report-20260707T230831Z.json +1 -0
  26. package/tests/bug-reports/bug-report-20260708T055430Z/__snapshots__/bug-report-20260708T055430Z.snap.svg +65 -79
  27. package/tests/bug-reports/bug-report-20260708T055430Z/bug-report-20260708T055430Z.json +1 -0
  28. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +1 -15
  29. package/tests/bug-reports/bug-report-20260708T095725Z/bug-report-20260708T095725Z.json +1 -0
  30. package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +1 -43
  31. package/tests/bug-reports/bug-report-20260716T144856Z/bug-report-20260716T144856Z.json +1 -0
  32. package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +2 -30
  33. package/tests/bug-reports/bug-report-20260717T022934Z/bug-report-20260717T022934Z.json +1 -0
  34. package/tests/bug-reports/bug-report-20260717T031704Z/__snapshots__/bug-report-20260717T031704Z.snap.svg +1 -8
  35. package/tests/bug-reports/bug-report-20260717T031704Z/bug-report-20260717T031704Z.json +1 -0
  36. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -92
  37. package/tests/bug-reports/bug-report-20260721T221026Z/bug-report-20260721T221026Z.json +1 -0
  38. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -29
  39. package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.json +1 -0
  40. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +1 -22
  41. package/tests/bug-reports/bug-report-20260728T225606Z/bug-report-20260728T225606Z.json +1 -0
  42. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +1 -8
  43. package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.json +1 -0
  44. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +1 -36
  45. package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.json +1 -0
  46. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -121
  47. package/tests/bug-reports/bug-report-20260804T171919Z/bug-report-20260804T171919Z.json +1 -0
  48. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +1 -8
  49. package/tests/bug-reports/bug-report-20260804T225912Z/bug-report-20260804T225912Z.json +1 -0
  50. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +1 -15
  51. package/tests/bug-reports/bug-report-20260805T061316Z/bug-report-20260805T061316Z.json +1 -0
  52. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +1 -15
  53. package/tests/bug-reports/bug-report-20260806T061548Z/bug-report-20260806T061548Z.json +1 -0
  54. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +1 -22
  55. package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +1 -0
  56. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +1 -8
  57. package/tests/bug-reports/bug-report-20260811T075142Z/bug-report-20260811T075142Z.json +1 -0
  58. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +1 -22
  59. package/tests/bug-reports/bug-report-20260815T073240Z/bug-report-20260815T073240Z.json +1 -0
  60. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +1 -15
  61. package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.json +1 -0
  62. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +1 -29
  63. package/tests/bug-reports/bug-report-20260825T103621Z/bug-report-20260825T103621Z.json +1 -0
  64. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +31 -95
  65. package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.json +1 -0
  66. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +1 -113
  67. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +1 -29
  68. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +64 -56
  69. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +1 -15
  70. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +1 -22
  71. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +185 -186
  72. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +1 -15
  73. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +1 -15
  74. package/tests/repros/__snapshots__/repro-board-8090-power-section.snap.svg +126 -0
  75. package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +1 -29
  76. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +1 -8
  77. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +1 -15
  78. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +1 -8
  79. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +545 -0
  80. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +1 -22
  81. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +2 -2
  82. package/tests/repros/__snapshots__/repro-mcp73831-charger-traces.snap.svg +131 -0
  83. package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +1 -15
  84. package/tests/repros/__snapshots__/repro-mouse-switch-ground-unnecessary-jog.snap.svg +83 -0
  85. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +1 -29
  86. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +1 -8
  87. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +1 -29
  88. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +2 -2
  89. package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +1 -29
  90. package/tests/repros/__snapshots__/repro-rf1-unnecessary-dogleg.snap.svg +72 -0
  91. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +1 -8
  92. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +1 -8
  93. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +4 -4
  94. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +1 -8
  95. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +991 -1091
  96. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +2 -2
  97. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +1 -15
  98. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +1 -22
  99. package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +1 -15
  100. package/tests/repros/__snapshots__/small-variant-resistor-facing-direction.snap.svg +65 -79
  101. package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +1 -8
  102. package/tests/repros/assets/board-1273-trace-overlap-cycle.input.json +1 -0
  103. package/tests/repros/assets/board-15984-schematic-trace-input.json +1 -0
  104. package/tests/repros/assets/bugreport-001-gnd-overlap.input.json +1 -0
  105. package/tests/repros/assets/repro-atmega328p-fault-pullup.input.json +1 -0
  106. package/tests/repros/assets/repro-atmega328p-missing-gnd-netlabel.input.json +1 -0
  107. package/tests/repros/assets/repro-board-589-regulator-section.input.json +1 -0
  108. package/tests/repros/assets/repro-board-648-esp12f-section.input.json +1 -0
  109. package/tests/repros/assets/repro-board-8090-power-section.input.json +322 -0
  110. package/tests/repros/assets/repro-bq24074-battery-charger.input.json +1 -0
  111. package/tests/repros/assets/repro-bq25895-cross-net-trace-overlap.input.json +1 -0
  112. package/tests/repros/assets/repro-cc2340r5.input.json +1 -0
  113. package/tests/repros/assets/repro-example35-minimize-trace-crossing.input.json +1 -0
  114. package/tests/repros/assets/repro-hub-usb-sheet.input.json +3967 -0
  115. package/tests/repros/assets/repro-ina237-current-monitor.input.json +1 -0
  116. package/tests/repros/assets/repro-mcp73831-charger-traces.input.json +256 -0
  117. package/tests/repros/assets/repro-missing-trace-netlabel.input.json +1 -0
  118. package/tests/repros/assets/repro-mouse-switch-ground-unnecessary-jog.input.json +158 -0
  119. package/tests/repros/assets/repro-mspm0l1306-capacitor-symmetry.input.json +1 -0
  120. package/tests/repros/assets/repro-netlabel-connector-through-label.input.json +1 -0
  121. package/tests/repros/assets/repro-netlabel-overlap-trace.input.json +1 -0
  122. package/tests/repros/assets/repro-nrf52810-clock-routing.input.json +1 -0
  123. package/tests/repros/assets/repro-powerbank-3v-system-power.input.json +1 -0
  124. package/tests/repros/assets/repro-rf1-unnecessary-dogleg.input.json +90 -0
  125. package/tests/repros/assets/repro-rp2040-gamepad-trace-alignment.input.json +1 -0
  126. package/tests/repros/assets/repro-rp2040-usb-cc2-ground-overlap.input.json +1 -0
  127. package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +1 -0
  128. package/tests/repros/assets/repro-usb-power-vbus-label-detour.input.json +1 -0
  129. package/tests/repros/assets/repro126-trace-overlap-box-resistor.input.json +1 -0
  130. package/tests/repros/assets/repro161-power-section-autolayout.input.json +1 -0
  131. package/tests/repros/assets/repro48-555-timer-vcc-rail-label.input.json +1 -0
  132. package/tests/repros/assets/repro51-overlap-junction-crossing.input.json +1 -0
  133. package/tests/repros/assets/small-variant-resistor-facing-direction.input.json +1 -0
  134. package/tests/repros/board1096-usb-label-overlap-iteration.json +1 -0
  135. package/tests/repros/board1096-usb-label-overlap-iteration.test.ts +1 -1
  136. package/tests/repros/repro-battery-management-bq24073.test.ts +17 -0
  137. package/tests/repros/repro-board-8090-power-section.test.ts +18 -0
  138. package/tests/repros/repro-hub-usb-sheet.test.ts +56 -0
  139. package/tests/repros/repro-mcp73831-charger-traces.test.ts +16 -0
  140. package/tests/repros/repro-mouse-switch-ground-unnecessary-jog.test.ts +15 -0
  141. package/tests/repros/repro-rf1-unnecessary-dogleg.test.ts +15 -0
  142. package/tests/repros/repro-smartwatch-power-sheet.test.ts +31 -5
package/dist/index.d.ts CHANGED
@@ -831,7 +831,7 @@ type CandidateLabel = {
831
831
  height: number;
832
832
  };
833
833
  type CandidateStatus$2 = "valid" | "chip-collision" | "text-collision" | "trace-collision" | "trace-clearance-violation" | "netlabel-collision";
834
- type CandidatePhase = "rotate" | "trace-anchor" | "connected-rail-shift" | "outward-trace-anchor" | "shift" | "lateral-shift";
834
+ type CandidatePhase = "rotate" | "trace-anchor" | "connected-rail-shift" | "downward-ground-rail-extension" | "outward-trace-anchor" | "shift" | "lateral-shift";
835
835
  type EvaluatedCandidate = CandidateLabel & {
836
836
  status: CandidateStatus$2;
837
837
  selected: boolean;
@@ -916,7 +916,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
916
916
  private createCandidate;
917
917
  private getNetLabelHeight;
918
918
  private getCandidateStatus;
919
- private isAcceptableTraceAnchorChipCollision;
919
+ private isAcceptableConnectedChipBoundaryOverlap;
920
920
  private staysOutsideConnectedPinRow;
921
921
  private getBoundsStatus;
922
922
  private isTraceTooCloseToLabel;
package/dist/index.js CHANGED
@@ -7709,6 +7709,27 @@ var TraceCleanupSolver = class extends BaseSolver {
7709
7709
  };
7710
7710
 
7711
7711
  // lib/solvers/AvailableNetOrientationSolver/traces.ts
7712
+ var extendVerticalTracePathAtInteriorPoint = ({
7713
+ tracePath,
7714
+ sourcePoint,
7715
+ extensionEndPoint
7716
+ }) => {
7717
+ const sourceIndex = tracePath.findIndex(
7718
+ (point) => pointsEqual(point, sourcePoint)
7719
+ );
7720
+ if (sourceIndex <= 0 || sourceIndex >= tracePath.length - 1) return null;
7721
+ const extensionDirectionY = extensionEndPoint.y - sourcePoint.y;
7722
+ const adjacentIndex = [sourceIndex - 1, sourceIndex + 1].find((index) => {
7723
+ const point = tracePath[index];
7724
+ return isVertical2(point, sourcePoint) && (point.y - sourcePoint.y) * extensionDirectionY < 0;
7725
+ });
7726
+ if (adjacentIndex === void 0) return null;
7727
+ return tracePath.toSpliced(
7728
+ Math.max(sourceIndex, adjacentIndex),
7729
+ 0,
7730
+ extensionEndPoint
7731
+ );
7732
+ };
7712
7733
  var getPinMap = (inputProblem) => {
7713
7734
  const pinMap = {};
7714
7735
  for (const chip of inputProblem.chips) {
@@ -9028,6 +9049,7 @@ function getPaddedBounds(bounds, padding) {
9028
9049
 
9029
9050
  // lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts
9030
9051
  var LABEL_TRACE_CLEARANCE2 = 0.1;
9052
+ var MIN_DOWNWARD_GROUND_RAIL_EXTENSION = 0.2;
9031
9053
  var CONNECTED_PIN_ROW_OVERLAP_TOLERANCE = 0.1;
9032
9054
  var AvailableNetOrientationSolver = class extends BaseSolver {
9033
9055
  inputProblem;
@@ -9267,6 +9289,20 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9267
9289
  }
9268
9290
  addConnectorTrace(label, candidate, labelIndex) {
9269
9291
  if (candidate.phase === "trace-anchor") return;
9292
+ if (candidate.phase === "downward-ground-rail-extension") {
9293
+ for (const mspPairId2 of label.mspConnectionPairIds) {
9294
+ const hostTrace = this.traceMap[mspPairId2];
9295
+ if (!hostTrace) continue;
9296
+ const extendedTracePath = extendVerticalTracePathAtInteriorPoint({
9297
+ tracePath: hostTrace.tracePath,
9298
+ sourcePoint: label.anchorPoint,
9299
+ extensionEndPoint: candidate.anchorPoint
9300
+ });
9301
+ if (!extendedTracePath) continue;
9302
+ hostTrace.tracePath = extendedTracePath;
9303
+ return;
9304
+ }
9305
+ }
9270
9306
  const tracePath = this.getCandidateConnectorTrace(
9271
9307
  label,
9272
9308
  candidate,
@@ -9313,6 +9349,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9313
9349
  (connection) => connection.netId === label.netId
9314
9350
  );
9315
9351
  const isTwoPinNet = netConnection?.pinIds.length === 2;
9352
+ const isDownwardGroundRail = netConnection?.isGround && isXOrientation(label.orientation) && orientations.length === 1 && requiredOrientation === "y-" && this.hasTraceContinuingInOrientation(label, "y+");
9316
9353
  const isDistanceSplitVerticalRail = (netConnection?.pinIds.length ?? 0) > 2 && isYOrientation(requiredOrientation) && this.hasPortOnlyLabelOnSameNet(label);
9317
9354
  const textBlockedMultiPinRailTraces = this.getTextBlockedMultiPinRailTraces(
9318
9355
  label,
@@ -9370,6 +9407,26 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9370
9407
  labelIndex,
9371
9408
  orientations
9372
9409
  );
9410
+ const rotatedCandidateNeedsLateralBranch = rotatedCandidate !== null && Math.abs(rotatedCandidate.anchorPoint.x - label.anchorPoint.x) > EPS13;
9411
+ if (isDownwardGroundRail && rotatedCandidateNeedsLateralBranch) {
9412
+ const downwardRailCandidate = this.findValidCandidateInShiftColumn({
9413
+ label,
9414
+ labelIndex,
9415
+ orientation: requiredOrientation,
9416
+ direction: dir(requiredOrientation),
9417
+ baseAnchor: label.anchorPoint,
9418
+ maxSearchDistance: this.getSearchDistanceLimit(
9419
+ label,
9420
+ requiredOrientation
9421
+ ),
9422
+ outwardDistance: 0,
9423
+ phase: "downward-ground-rail-extension",
9424
+ stopOnTraceCollision: false,
9425
+ connectorSource: label.anchorPoint,
9426
+ startDistance: MIN_DOWNWARD_GROUND_RAIL_EXTENSION
9427
+ });
9428
+ if (downwardRailCandidate) return downwardRailCandidate;
9429
+ }
9373
9430
  if (rotatedCandidate) return rotatedCandidate;
9374
9431
  const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
9375
9432
  label,
@@ -10096,7 +10153,12 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10096
10153
  label
10097
10154
  });
10098
10155
  if (boundsStatus !== "valid") {
10099
- if (phase !== "trace-anchor" && phase !== "connected-rail-shift" || boundsStatus !== "chip-collision" || !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds) || phase === "connected-rail-shift" && !this.staysOutsideConnectedPinRow({ candidate, label, bounds })) {
10156
+ const canOverlapConnectedChipBoundary = phase === "trace-anchor" || phase === "connected-rail-shift" || phase === "downward-ground-rail-extension";
10157
+ if (!canOverlapConnectedChipBoundary || boundsStatus !== "chip-collision" || !this.isAcceptableConnectedChipBoundaryOverlap(
10158
+ candidate,
10159
+ label,
10160
+ bounds
10161
+ ) || phase === "connected-rail-shift" && !this.staysOutsideConnectedPinRow({ candidate, label, bounds })) {
10100
10162
  return boundsStatus;
10101
10163
  }
10102
10164
  const nonChipBoundsStatus = this.getBoundsStatus({
@@ -10140,7 +10202,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10140
10202
  }
10141
10203
  return "valid";
10142
10204
  }
10143
- isAcceptableTraceAnchorChipCollision(candidate, label, bounds) {
10205
+ isAcceptableConnectedChipBoundaryOverlap(candidate, label, bounds) {
10144
10206
  const collidingChips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
10145
10207
  if (collidingChips.length === 0) return false;
10146
10208
  const labelChipIds = new Set(
@@ -13220,11 +13282,26 @@ var getAlignedSharedEndpointRailPath = ({
13220
13282
  const donorDeparture = donorPath[1][donorDepartureAxis] - donorPath[0][donorDepartureAxis];
13221
13283
  const branchDeparture = branchPath[1][branchDepartureAxis] - branchPath[0][branchDepartureAxis];
13222
13284
  if (Math.sign(donorDeparture) !== Math.sign(branchDeparture)) return null;
13223
- const donorRailAxis = getSegmentAxis3(donorPath[1], donorPath[2]);
13224
- const branchRailAxis = getSegmentAxis3(branchPath[1], branchPath[2]);
13285
+ let railIndex = 1;
13286
+ let donorRailAxis = getSegmentAxis3(donorPath[1], donorPath[2]);
13287
+ let branchRailAxis = getSegmentAxis3(branchPath[1], branchPath[2]);
13225
13288
  if (!donorRailAxis || donorRailAxis !== branchRailAxis || donorRailAxis === donorDepartureAxis) {
13226
13289
  return null;
13227
13290
  }
13291
+ const firstRailsAreAligned = donorRailAxis === "x" && nearlyEqual(donorPath[1].y, branchPath[1].y) || donorRailAxis === "y" && nearlyEqual(donorPath[1].x, branchPath[1].x);
13292
+ if (firstRailsAreAligned) {
13293
+ railIndex++;
13294
+ if (!donorPath[railIndex + 2] || !branchPath[railIndex + 2]) return null;
13295
+ donorRailAxis = getSegmentAxis3(
13296
+ donorPath[railIndex],
13297
+ donorPath[railIndex + 1]
13298
+ );
13299
+ branchRailAxis = getSegmentAxis3(
13300
+ branchPath[railIndex],
13301
+ branchPath[railIndex + 1]
13302
+ );
13303
+ if (!donorRailAxis || donorRailAxis !== branchRailAxis) return null;
13304
+ }
13228
13305
  let branchParallelRailCount = 0;
13229
13306
  for (let index = 1; index < branchPath.length - 2; index++) {
13230
13307
  if (getSegmentAxis3(branchPath[index], branchPath[index + 1]) === branchRailAxis) {
@@ -13233,25 +13310,25 @@ var getAlignedSharedEndpointRailPath = ({
13233
13310
  }
13234
13311
  if (branchParallelRailCount !== 1) return null;
13235
13312
  const railCoordinateAxis = donorRailAxis === "x" ? "y" : "x";
13236
- const donorCoordinate = donorPath[1][railCoordinateAxis];
13237
- const branchCoordinate = branchPath[1][railCoordinateAxis];
13313
+ const donorCoordinate = donorPath[railIndex][railCoordinateAxis];
13314
+ const branchCoordinate = branchPath[railIndex][railCoordinateAxis];
13238
13315
  const railOffset = Math.abs(donorCoordinate - branchCoordinate);
13239
13316
  if (nearlyEqual(donorCoordinate, branchCoordinate) || railOffset > MAX_SHARED_ENDPOINT_RAIL_OFFSET) {
13240
13317
  return null;
13241
13318
  }
13242
13319
  const donorRange = [
13243
- donorPath[1][donorRailAxis],
13244
- donorPath[2][donorRailAxis]
13320
+ donorPath[railIndex][donorRailAxis],
13321
+ donorPath[railIndex + 1][donorRailAxis]
13245
13322
  ].sort((a, b) => a - b);
13246
13323
  const branchRange = [
13247
- branchPath[1][branchRailAxis],
13248
- branchPath[2][branchRailAxis]
13324
+ branchPath[railIndex][branchRailAxis],
13325
+ branchPath[railIndex + 1][branchRailAxis]
13249
13326
  ].sort((a, b) => a - b);
13250
13327
  const railsTouchAfterAlignment = Math.min(donorRange[1], branchRange[1]) >= Math.max(donorRange[0], branchRange[0]) - 1e-6;
13251
13328
  if (!railsTouchAfterAlignment) return null;
13252
13329
  const candidateFromShared = simplifyPath(
13253
13330
  branchPath.map(
13254
- (point, index) => index === 1 || index === 2 ? { ...point, [railCoordinateAxis]: donorCoordinate } : point
13331
+ (point, index) => index === railIndex || index === railIndex + 1 ? { ...point, [railCoordinateAxis]: donorCoordinate } : point
13255
13332
  )
13256
13333
  );
13257
13334
  return branchTrace.pins[0].pinId === sharedPin.pinId ? candidateFromShared : candidateFromShared.reverse();
@@ -41,7 +41,12 @@ import {
41
41
  tracePathIntersectsBounds,
42
42
  } from "./geometry"
43
43
  import { orderRoutedLabelsBeforeOverlappingPortLabels } from "./orderRoutedLabelsBeforeOverlappingPortLabels"
44
- import { getPinMap, getTracePins, toNetLabelPlacementPatch } from "./traces"
44
+ import {
45
+ extendVerticalTracePathAtInteriorPoint,
46
+ getPinMap,
47
+ getTracePins,
48
+ toNetLabelPlacementPatch,
49
+ } from "./traces"
45
50
  import type {
46
51
  AvailableNetOrientationSolverParams,
47
52
  Bounds,
@@ -55,6 +60,7 @@ import { visualizeAvailableNetOrientationSolver } from "./visualize"
55
60
  import { AvailableNetOrientationObstacleIndex } from "./AvailableNetOrientationObstacleIndex"
56
61
 
57
62
  const LABEL_TRACE_CLEARANCE = 0.1
63
+ const MIN_DOWNWARD_GROUND_RAIL_EXTENSION = 0.2
58
64
  const CONNECTED_PIN_ROW_OVERLAP_TOLERANCE = 0.1
59
65
 
60
66
  export class AvailableNetOrientationSolver extends BaseSolver {
@@ -357,6 +363,21 @@ export class AvailableNetOrientationSolver extends BaseSolver {
357
363
  ) {
358
364
  if (candidate.phase === "trace-anchor") return
359
365
 
366
+ if (candidate.phase === "downward-ground-rail-extension") {
367
+ for (const mspPairId of label.mspConnectionPairIds) {
368
+ const hostTrace = this.traceMap[mspPairId]
369
+ if (!hostTrace) continue
370
+ const extendedTracePath = extendVerticalTracePathAtInteriorPoint({
371
+ tracePath: hostTrace.tracePath,
372
+ sourcePoint: label.anchorPoint,
373
+ extensionEndPoint: candidate.anchorPoint,
374
+ })
375
+ if (!extendedTracePath) continue
376
+ hostTrace.tracePath = extendedTracePath
377
+ return
378
+ }
379
+ }
380
+
360
381
  const tracePath = this.getCandidateConnectorTrace(
361
382
  label,
362
383
  candidate,
@@ -416,6 +437,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
416
437
  (connection) => connection.netId === label.netId,
417
438
  )
418
439
  const isTwoPinNet = netConnection?.pinIds.length === 2
440
+ const isDownwardGroundRail =
441
+ netConnection?.isGround &&
442
+ isXOrientation(label.orientation) &&
443
+ orientations.length === 1 &&
444
+ requiredOrientation === "y-" &&
445
+ this.hasTraceContinuingInOrientation(label, "y+")
419
446
  const isDistanceSplitVerticalRail =
420
447
  (netConnection?.pinIds.length ?? 0) > 2 &&
421
448
  isYOrientation(requiredOrientation) &&
@@ -495,6 +522,30 @@ export class AvailableNetOrientationSolver extends BaseSolver {
495
522
  labelIndex,
496
523
  orientations,
497
524
  )
525
+ const rotatedCandidateNeedsLateralBranch =
526
+ rotatedCandidate !== null &&
527
+ Math.abs(rotatedCandidate.anchorPoint.x - label.anchorPoint.x) > EPS
528
+
529
+ if (isDownwardGroundRail && rotatedCandidateNeedsLateralBranch) {
530
+ const downwardRailCandidate = this.findValidCandidateInShiftColumn({
531
+ label,
532
+ labelIndex,
533
+ orientation: requiredOrientation,
534
+ direction: dir(requiredOrientation),
535
+ baseAnchor: label.anchorPoint,
536
+ maxSearchDistance: this.getSearchDistanceLimit(
537
+ label,
538
+ requiredOrientation,
539
+ ),
540
+ outwardDistance: 0,
541
+ phase: "downward-ground-rail-extension",
542
+ stopOnTraceCollision: false,
543
+ connectorSource: label.anchorPoint,
544
+ startDistance: MIN_DOWNWARD_GROUND_RAIL_EXTENSION,
545
+ })
546
+ if (downwardRailCandidate) return downwardRailCandidate
547
+ }
548
+
498
549
  if (rotatedCandidate) return rotatedCandidate
499
550
 
500
551
  const traceAnchorCandidate = this.findValidTraceAnchorCandidate(
@@ -1525,10 +1576,18 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1525
1576
  label,
1526
1577
  })
1527
1578
  if (boundsStatus !== "valid") {
1579
+ const canOverlapConnectedChipBoundary =
1580
+ phase === "trace-anchor" ||
1581
+ phase === "connected-rail-shift" ||
1582
+ phase === "downward-ground-rail-extension"
1528
1583
  if (
1529
- (phase !== "trace-anchor" && phase !== "connected-rail-shift") ||
1584
+ !canOverlapConnectedChipBoundary ||
1530
1585
  boundsStatus !== "chip-collision" ||
1531
- !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds) ||
1586
+ !this.isAcceptableConnectedChipBoundaryOverlap(
1587
+ candidate,
1588
+ label,
1589
+ bounds,
1590
+ ) ||
1532
1591
  (phase === "connected-rail-shift" &&
1533
1592
  !this.staysOutsideConnectedPinRow({ candidate, label, bounds }))
1534
1593
  ) {
@@ -1587,7 +1646,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1587
1646
  return "valid"
1588
1647
  }
1589
1648
 
1590
- private isAcceptableTraceAnchorChipCollision(
1649
+ private isAcceptableConnectedChipBoundaryOverlap(
1591
1650
  candidate: CandidateLabel,
1592
1651
  label: NetLabelPlacement,
1593
1652
  bounds: Bounds,
@@ -1,8 +1,44 @@
1
+ import type { Point } from "@tscircuit/math-utils"
1
2
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
3
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import {
5
+ isVertical,
6
+ pointsEqual,
7
+ } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
3
8
  import type { InputPin, InputProblem } from "lib/types/InputProblem"
4
9
  import type { CandidateLabel } from "./types"
5
10
 
11
+ export const extendVerticalTracePathAtInteriorPoint = ({
12
+ tracePath,
13
+ sourcePoint,
14
+ extensionEndPoint,
15
+ }: {
16
+ tracePath: Point[]
17
+ sourcePoint: Point
18
+ extensionEndPoint: Point
19
+ }) => {
20
+ const sourceIndex = tracePath.findIndex((point) =>
21
+ pointsEqual(point, sourcePoint),
22
+ )
23
+ if (sourceIndex <= 0 || sourceIndex >= tracePath.length - 1) return null
24
+
25
+ const extensionDirectionY = extensionEndPoint.y - sourcePoint.y
26
+ const adjacentIndex = [sourceIndex - 1, sourceIndex + 1].find((index) => {
27
+ const point = tracePath[index]!
28
+ return (
29
+ isVertical(point, sourcePoint) &&
30
+ (point.y - sourcePoint.y) * extensionDirectionY < 0
31
+ )
32
+ })
33
+ if (adjacentIndex === undefined) return null
34
+
35
+ return tracePath.toSpliced(
36
+ Math.max(sourceIndex, adjacentIndex),
37
+ 0,
38
+ extensionEndPoint,
39
+ )
40
+ }
41
+
6
42
  export const getPinMap = (inputProblem: InputProblem) => {
7
43
  const pinMap: Record<string, InputPin & { chipId: string }> = {}
8
44
  for (const chip of inputProblem.chips) {
@@ -40,6 +40,7 @@ export type CandidatePhase =
40
40
  | "rotate"
41
41
  | "trace-anchor"
42
42
  | "connected-rail-shift"
43
+ | "downward-ground-rail-extension"
43
44
  | "outward-trace-anchor"
44
45
  | "shift"
45
46
  | "lateral-shift"
@@ -375,8 +375,8 @@ const getAlignedParallelPinExitPath = ({
375
375
  }
376
376
 
377
377
  /**
378
- * Move the first movable rail of a branch onto the first rail of another
379
- * trace leaving the same endpoint. The two perpendicular rail intervals must
378
+ * Move the first divergent movable rail of a branch onto the matching rail of
379
+ * another trace leaving the same endpoint. The two rail intervals must
380
380
  * touch after the move; the caller then verifies that the combined visible
381
381
  * trace loses a segment and that the reroute is clear.
382
382
  */
@@ -419,8 +419,9 @@ const getAlignedSharedEndpointRailPath = ({
419
419
  branchPath[1]![branchDepartureAxis] - branchPath[0]![branchDepartureAxis]
420
420
  if (Math.sign(donorDeparture) !== Math.sign(branchDeparture)) return null
421
421
 
422
- const donorRailAxis = getSegmentAxis(donorPath[1]!, donorPath[2]!)
423
- const branchRailAxis = getSegmentAxis(branchPath[1]!, branchPath[2]!)
422
+ let railIndex = 1
423
+ let donorRailAxis = getSegmentAxis(donorPath[1]!, donorPath[2]!)
424
+ let branchRailAxis = getSegmentAxis(branchPath[1]!, branchPath[2]!)
424
425
  if (
425
426
  !donorRailAxis ||
426
427
  donorRailAxis !== branchRailAxis ||
@@ -428,6 +429,23 @@ const getAlignedSharedEndpointRailPath = ({
428
429
  ) {
429
430
  return null
430
431
  }
432
+ const firstRailsAreAligned =
433
+ (donorRailAxis === "x" && nearlyEqual(donorPath[1]!.y, branchPath[1]!.y)) ||
434
+ (donorRailAxis === "y" && nearlyEqual(donorPath[1]!.x, branchPath[1]!.x))
435
+ // An already-shared rail can lead into the nearby parallel pair to merge.
436
+ if (firstRailsAreAligned) {
437
+ railIndex++
438
+ if (!donorPath[railIndex + 2] || !branchPath[railIndex + 2]) return null
439
+ donorRailAxis = getSegmentAxis(
440
+ donorPath[railIndex]!,
441
+ donorPath[railIndex + 1]!,
442
+ )
443
+ branchRailAxis = getSegmentAxis(
444
+ branchPath[railIndex]!,
445
+ branchPath[railIndex + 1]!,
446
+ )
447
+ if (!donorRailAxis || donorRailAxis !== branchRailAxis) return null
448
+ }
431
449
  // A return bus has another rail with this orientation near its far
432
450
  // endpoint. Moving only the first rail would partially rewrite a shape that
433
451
  // must be evaluated as a whole by the endpoint-bus transformation.
@@ -443,8 +461,8 @@ const getAlignedSharedEndpointRailPath = ({
443
461
  if (branchParallelRailCount !== 1) return null
444
462
 
445
463
  const railCoordinateAxis = donorRailAxis === "x" ? "y" : "x"
446
- const donorCoordinate = donorPath[1]![railCoordinateAxis]
447
- const branchCoordinate = branchPath[1]![railCoordinateAxis]
464
+ const donorCoordinate = donorPath[railIndex]![railCoordinateAxis]
465
+ const branchCoordinate = branchPath[railIndex]![railCoordinateAxis]
448
466
  const railOffset = Math.abs(donorCoordinate - branchCoordinate)
449
467
  if (
450
468
  nearlyEqual(donorCoordinate, branchCoordinate) ||
@@ -454,12 +472,12 @@ const getAlignedSharedEndpointRailPath = ({
454
472
  }
455
473
 
456
474
  const donorRange = [
457
- donorPath[1]![donorRailAxis],
458
- donorPath[2]![donorRailAxis],
475
+ donorPath[railIndex]![donorRailAxis],
476
+ donorPath[railIndex + 1]![donorRailAxis],
459
477
  ].sort((a, b) => a - b)
460
478
  const branchRange = [
461
- branchPath[1]![branchRailAxis],
462
- branchPath[2]![branchRailAxis],
479
+ branchPath[railIndex]![branchRailAxis],
480
+ branchPath[railIndex + 1]![branchRailAxis],
463
481
  ].sort((a, b) => a - b)
464
482
  const railsTouchAfterAlignment =
465
483
  Math.min(donorRange[1]!, branchRange[1]!) >=
@@ -468,7 +486,7 @@ const getAlignedSharedEndpointRailPath = ({
468
486
 
469
487
  const candidateFromShared = simplifyPath(
470
488
  branchPath.map((point, index) =>
471
- index === 1 || index === 2
489
+ index === railIndex || index === railIndex + 1
472
490
  ? { ...point, [railCoordinateAxis]: donorCoordinate }
473
491
  : point,
474
492
  ),
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.179",
8
+ "version": "0.0.181",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",