@tscircuit/schematic-trace-solver 0.0.178 → 0.0.180

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 +8 -2
  2. package/dist/index.js +109 -4
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +141 -6
  4. package/lib/solvers/AvailableNetOrientationSolver/traces.ts +36 -0
  5. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -0
  6. package/package.json +1 -1
  7. package/site/bug-reports/bug-report-20260902T092101Z.page.tsx +4 -0
  8. package/site/bug-reports/bug-report-20260902T092425Z.page.tsx +4 -0
  9. package/tests/bug-reports/bug-report-20260902T092101Z/__snapshots__/bug-report-20260902T092101Z.snap.svg +117 -0
  10. package/tests/bug-reports/bug-report-20260902T092101Z/bug-report-20260902T092101Z.json +162 -0
  11. package/tests/bug-reports/bug-report-20260902T092101Z/bug-report-20260902T092101Z.test.ts +28 -0
  12. package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +136 -0
  13. package/tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.json +238 -0
  14. package/tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.test.ts +12 -0
  15. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +545 -0
  16. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +10 -10
  17. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +4 -4
  18. package/tests/repros/assets/repro-hub-usb-sheet.input.json +3967 -0
  19. package/tests/repros/repro-battery-management-bq24073.test.ts +17 -0
  20. package/tests/repros/repro-hub-usb-sheet.test.ts +56 -0
  21. 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;
@@ -900,6 +900,12 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
900
900
  private evaluateCandidate;
901
901
  private recordCandidateResult;
902
902
  private getCandidateConnectorTrace;
903
+ /**
904
+ * A laterally shifted label can branch directly from the interior of its
905
+ * existing host trace. Prefer that tee over running a new connector beside
906
+ * the host trace from the old label anchor.
907
+ */
908
+ private findProjectedConnectorSourceOnHostTrace;
903
909
  private isStandaloneSinglePinNetLabel;
904
910
  private labelIntersectsDifferentNetTrace;
905
911
  private getSearchStartAnchor;
@@ -910,7 +916,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
910
916
  private createCandidate;
911
917
  private getNetLabelHeight;
912
918
  private getCandidateStatus;
913
- private isAcceptableTraceAnchorChipCollision;
919
+ private isAcceptableConnectedChipBoundaryOverlap;
914
920
  private staysOutsideConnectedPinRow;
915
921
  private getBoundsStatus;
916
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,
@@ -9784,11 +9841,16 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9784
9841
  x: baseAnchor.x + direction.x * distance7,
9785
9842
  y: baseAnchor.y + direction.y * distance7
9786
9843
  };
9844
+ const projectedConnectorSource = connectorSource ?? (phase === "lateral-shift" ? this.findProjectedConnectorSourceOnHostTrace(
9845
+ label,
9846
+ anchorPoint,
9847
+ orientation
9848
+ ) : void 0);
9787
9849
  const candidate = this.createCandidate(
9788
9850
  label,
9789
9851
  anchorPoint,
9790
9852
  orientation,
9791
- connectorSource
9853
+ projectedConnectorSource
9792
9854
  );
9793
9855
  const result = this.evaluateCandidate(
9794
9856
  candidate,
@@ -9842,6 +9904,13 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9842
9904
  candidate.anchorPoint
9843
9905
  ]);
9844
9906
  }
9907
+ if (candidate.connectorSource) {
9908
+ return getConnectorTracePath(
9909
+ candidate.connectorSource,
9910
+ candidate.anchorPoint,
9911
+ candidate.orientation
9912
+ );
9913
+ }
9845
9914
  if (candidate.phase === "lateral-shift") {
9846
9915
  const chipOutwardDir = this.crowdedPortOnlyLabelIndices.has(labelIndex) ? this.getChipOutwardDirection(label.anchorPoint) : (() => {
9847
9916
  const orientDir = dir(candidate.orientation);
@@ -9861,11 +9930,41 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
9861
9930
  ]);
9862
9931
  }
9863
9932
  return getConnectorTracePath(
9864
- candidate.connectorSource ?? label.anchorPoint,
9933
+ label.anchorPoint,
9865
9934
  candidate.anchorPoint,
9866
9935
  candidate.orientation
9867
9936
  );
9868
9937
  }
9938
+ /**
9939
+ * A laterally shifted label can branch directly from the interior of its
9940
+ * existing host trace. Prefer that tee over running a new connector beside
9941
+ * the host trace from the old label anchor.
9942
+ */
9943
+ findProjectedConnectorSourceOnHostTrace(label, targetAnchor, orientation) {
9944
+ const orientationDirection = dir(orientation);
9945
+ const projectedSources = [];
9946
+ for (const traceId of label.mspConnectionPairIds) {
9947
+ const hostTrace = this.traceMap[traceId];
9948
+ if (!hostTrace) continue;
9949
+ for (let pointIndex = 0; pointIndex < hostTrace.tracePath.length - 1; pointIndex++) {
9950
+ const start = hostTrace.tracePath[pointIndex];
9951
+ const end = hostTrace.tracePath[pointIndex + 1];
9952
+ const segmentIsPerpendicularToLabel = isYOrientation(orientation) ? Math.abs(start.y - end.y) <= EPS13 : Math.abs(start.x - end.x) <= EPS13;
9953
+ if (!segmentIsPerpendicularToLabel) continue;
9954
+ const source = isYOrientation(orientation) ? { x: targetAnchor.x, y: start.y } : { x: start.x, y: targetAnchor.y };
9955
+ if (!tracePathContainsPoint([start, end], label.anchorPoint) || !tracePathContainsPoint([start, end], source)) {
9956
+ continue;
9957
+ }
9958
+ const targetDistanceAlongOrientation = (targetAnchor.x - source.x) * orientationDirection.x + (targetAnchor.y - source.y) * orientationDirection.y;
9959
+ if (targetDistanceAlongOrientation >= -EPS13) {
9960
+ projectedSources.push(source);
9961
+ }
9962
+ }
9963
+ }
9964
+ return projectedSources.sort(
9965
+ (first, second) => Math.abs(first.x - targetAnchor.x) + Math.abs(first.y - targetAnchor.y) - (Math.abs(second.x - targetAnchor.x) + Math.abs(second.y - targetAnchor.y))
9966
+ )[0];
9967
+ }
9869
9968
  isStandaloneSinglePinNetLabel(label) {
9870
9969
  return this.inputProblem.netConnections.some(
9871
9970
  (connection) => connection.netId === label.netId && connection.pinIds.length === 1 && connection.pinIds[0] === label.pinIds[0]
@@ -10054,7 +10153,12 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10054
10153
  label
10055
10154
  });
10056
10155
  if (boundsStatus !== "valid") {
10057
- 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 })) {
10058
10162
  return boundsStatus;
10059
10163
  }
10060
10164
  const nonChipBoundsStatus = this.getBoundsStatus({
@@ -10069,6 +10173,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10069
10173
  label,
10070
10174
  {
10071
10175
  anchorPoint: candidate.anchorPoint,
10176
+ connectorSource: phase === "lateral-shift" ? candidate.connectorSource : void 0,
10072
10177
  orientation: candidate.orientation,
10073
10178
  phase
10074
10179
  },
@@ -10097,7 +10202,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
10097
10202
  }
10098
10203
  return "valid";
10099
10204
  }
10100
- isAcceptableTraceAnchorChipCollision(candidate, label, bounds) {
10205
+ isAcceptableConnectedChipBoundaryOverlap(candidate, label, bounds) {
10101
10206
  const collidingChips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds);
10102
10207
  if (collidingChips.length === 0) return false;
10103
10208
  const labelChipIds = new Set(
@@ -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(
@@ -1076,11 +1127,20 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1076
1127
  x: baseAnchor.x + direction.x * distance,
1077
1128
  y: baseAnchor.y + direction.y * distance,
1078
1129
  }
1130
+ const projectedConnectorSource =
1131
+ connectorSource ??
1132
+ (phase === "lateral-shift"
1133
+ ? this.findProjectedConnectorSourceOnHostTrace(
1134
+ label,
1135
+ anchorPoint,
1136
+ orientation,
1137
+ )
1138
+ : undefined)
1079
1139
  const candidate = this.createCandidate(
1080
1140
  label,
1081
1141
  anchorPoint,
1082
1142
  orientation,
1083
- connectorSource,
1143
+ projectedConnectorSource,
1084
1144
  )
1085
1145
  const result = this.evaluateCandidate(
1086
1146
  candidate,
@@ -1163,6 +1223,14 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1163
1223
  ])
1164
1224
  }
1165
1225
 
1226
+ if (candidate.connectorSource) {
1227
+ return getConnectorTracePath(
1228
+ candidate.connectorSource,
1229
+ candidate.anchorPoint,
1230
+ candidate.orientation,
1231
+ )
1232
+ }
1233
+
1166
1234
  if (candidate.phase === "lateral-shift") {
1167
1235
  const chipOutwardDir = this.crowdedPortOnlyLabelIndices.has(labelIndex)
1168
1236
  ? this.getChipOutwardDirection(label.anchorPoint)
@@ -1185,12 +1253,69 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1185
1253
  }
1186
1254
 
1187
1255
  return getConnectorTracePath(
1188
- candidate.connectorSource ?? label.anchorPoint,
1256
+ label.anchorPoint,
1189
1257
  candidate.anchorPoint,
1190
1258
  candidate.orientation,
1191
1259
  )
1192
1260
  }
1193
1261
 
1262
+ /**
1263
+ * A laterally shifted label can branch directly from the interior of its
1264
+ * existing host trace. Prefer that tee over running a new connector beside
1265
+ * the host trace from the old label anchor.
1266
+ */
1267
+ private findProjectedConnectorSourceOnHostTrace(
1268
+ label: NetLabelPlacement,
1269
+ targetAnchor: Point,
1270
+ orientation: FacingDirection,
1271
+ ): Point | undefined {
1272
+ const orientationDirection = dir(orientation)
1273
+ const projectedSources: Point[] = []
1274
+
1275
+ for (const traceId of label.mspConnectionPairIds) {
1276
+ const hostTrace = this.traceMap[traceId]
1277
+ if (!hostTrace) continue
1278
+
1279
+ for (
1280
+ let pointIndex = 0;
1281
+ pointIndex < hostTrace.tracePath.length - 1;
1282
+ pointIndex++
1283
+ ) {
1284
+ const start = hostTrace.tracePath[pointIndex]!
1285
+ const end = hostTrace.tracePath[pointIndex + 1]!
1286
+ const segmentIsPerpendicularToLabel = isYOrientation(orientation)
1287
+ ? Math.abs(start.y - end.y) <= EPS
1288
+ : Math.abs(start.x - end.x) <= EPS
1289
+ if (!segmentIsPerpendicularToLabel) continue
1290
+
1291
+ const source = isYOrientation(orientation)
1292
+ ? { x: targetAnchor.x, y: start.y }
1293
+ : { x: start.x, y: targetAnchor.y }
1294
+ if (
1295
+ !tracePathContainsPoint([start, end], label.anchorPoint) ||
1296
+ !tracePathContainsPoint([start, end], source)
1297
+ ) {
1298
+ continue
1299
+ }
1300
+
1301
+ const targetDistanceAlongOrientation =
1302
+ (targetAnchor.x - source.x) * orientationDirection.x +
1303
+ (targetAnchor.y - source.y) * orientationDirection.y
1304
+ if (targetDistanceAlongOrientation >= -EPS) {
1305
+ projectedSources.push(source)
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ return projectedSources.sort(
1311
+ (first, second) =>
1312
+ Math.abs(first.x - targetAnchor.x) +
1313
+ Math.abs(first.y - targetAnchor.y) -
1314
+ (Math.abs(second.x - targetAnchor.x) +
1315
+ Math.abs(second.y - targetAnchor.y)),
1316
+ )[0]
1317
+ }
1318
+
1194
1319
  private isStandaloneSinglePinNetLabel(label: NetLabelPlacement) {
1195
1320
  return this.inputProblem.netConnections.some(
1196
1321
  (connection) =>
@@ -1451,10 +1576,18 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1451
1576
  label,
1452
1577
  })
1453
1578
  if (boundsStatus !== "valid") {
1579
+ const canOverlapConnectedChipBoundary =
1580
+ phase === "trace-anchor" ||
1581
+ phase === "connected-rail-shift" ||
1582
+ phase === "downward-ground-rail-extension"
1454
1583
  if (
1455
- (phase !== "trace-anchor" && phase !== "connected-rail-shift") ||
1584
+ !canOverlapConnectedChipBoundary ||
1456
1585
  boundsStatus !== "chip-collision" ||
1457
- !this.isAcceptableTraceAnchorChipCollision(candidate, label, bounds) ||
1586
+ !this.isAcceptableConnectedChipBoundaryOverlap(
1587
+ candidate,
1588
+ label,
1589
+ bounds,
1590
+ ) ||
1458
1591
  (phase === "connected-rail-shift" &&
1459
1592
  !this.staysOutsideConnectedPinRow({ candidate, label, bounds }))
1460
1593
  ) {
@@ -1474,6 +1607,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1474
1607
  label,
1475
1608
  {
1476
1609
  anchorPoint: candidate.anchorPoint,
1610
+ connectorSource:
1611
+ phase === "lateral-shift" ? candidate.connectorSource : undefined,
1477
1612
  orientation: candidate.orientation,
1478
1613
  phase,
1479
1614
  },
@@ -1511,7 +1646,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1511
1646
  return "valid"
1512
1647
  }
1513
1648
 
1514
- private isAcceptableTraceAnchorChipCollision(
1649
+ private isAcceptableConnectedChipBoundaryOverlap(
1515
1650
  candidate: CandidateLabel,
1516
1651
  label: NetLabelPlacement,
1517
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"
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.178",
8
+ "version": "0.0.180",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260902T092101Z/bug-report-20260902T092101Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />