@tscircuit/schematic-trace-solver 0.0.113 → 0.0.115

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 (26) hide show
  1. package/dist/index.d.ts +7 -8
  2. package/dist/index.js +101 -7
  3. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +55 -1
  4. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +13 -4
  5. package/lib/solvers/MspConnectionPairSolver/isLabeledPeripheralConnection.ts +44 -0
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +9 -9
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +25 -9
  8. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +2 -0
  9. package/package.json +2 -2
  10. package/site/bug-reports/bug-report-20260728T144234Z.page.tsx +4 -0
  11. package/site/bug-reports/bug-report-20260728T225606Z.page.tsx +4 -0
  12. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
  13. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +108 -0
  14. package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.json +518 -0
  15. package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.test.ts +12 -0
  16. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +131 -0
  17. package/tests/bug-reports/bug-report-20260728T225606Z/bug-report-20260728T225606Z.json +1127 -0
  18. package/tests/bug-reports/bug-report-20260728T225606Z/bug-report-20260728T225606Z.test.ts +35 -0
  19. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +54 -0
  20. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +56 -0
  21. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +87 -0
  22. package/tests/repros/assets/repro48-555-timer-vcc-rail-label.input.json +276 -0
  23. package/tests/repros/repro-type-c-16pin-self-connection.test.ts +97 -0
  24. package/tests/repros/repro-type-c-16pin-to-resistor.test.ts +120 -0
  25. package/tests/repros/repro48-555-timer-vcc-rail-label.test.ts +23 -0
  26. package/tests/solvers/SchematicTraceSingleLineSolver2/text-box-obstacle-gap.test.ts +43 -0
package/dist/index.d.ts CHANGED
@@ -146,17 +146,11 @@ type RectPadding = {
146
146
  maxY?: number;
147
147
  };
148
148
 
149
- type RectBounds = {
150
- minX: number;
151
- minY: number;
152
- maxX: number;
153
- maxY: number;
154
- };
155
- type ChipObstacleRect = RectBounds & {
149
+ type ChipObstacleRect = Bounds & {
156
150
  kind: "chip";
157
151
  chipId: string;
158
152
  };
159
- type TextBoxObstacleRect = RectBounds & {
153
+ type TextBoxObstacleRect = Bounds & {
160
154
  kind: "text_box";
161
155
  textBox: NonNullable<InputProblem["textBoxes"]>[number];
162
156
  };
@@ -586,6 +580,8 @@ declare class LongDistancePairSolver extends BaseSolver {
586
580
  solvedLongDistanceTraces: SolvedTracePath[];
587
581
  private queuedCandidatePairs;
588
582
  private currentCandidatePair;
583
+ private queuedFailedConnectionPairs;
584
+ private currentFailedConnectionPair;
589
585
  private subSolver;
590
586
  private chipMap;
591
587
  private inputProblem;
@@ -596,12 +592,15 @@ declare class LongDistancePairSolver extends BaseSolver {
596
592
  inputProblem: InputProblem;
597
593
  alreadySolvedTraces: SolvedTracePath[];
598
594
  primaryMspConnectionPairs: MspConnectionPair[];
595
+ failedConnectionPairs: MspConnectionPair[];
599
596
  });
600
597
  getConstructorParams(): {
601
598
  inputProblem: InputProblem;
602
599
  alreadySolvedTraces: SolvedTracePath[];
603
600
  primaryMspConnectionPairs: MspConnectionPair[];
601
+ failedConnectionPairs: MspConnectionPair[];
604
602
  };
603
+ private acceptFailedConnectionPair;
605
604
  _step(): void;
606
605
  visualize(): graphics_debug.GraphicsObject;
607
606
  getOutput(): {
package/dist/index.js CHANGED
@@ -420,6 +420,33 @@ function getOrthogonalMinimumSpanningTree(pins, opts = {}) {
420
420
  return edges;
421
421
  }
422
422
 
423
+ // lib/solvers/MspConnectionPairSolver/isLabeledPeripheralConnection.ts
424
+ var isLabeledPeripheralConnection = ({
425
+ inputProblem,
426
+ chipMap,
427
+ pins
428
+ }) => {
429
+ const [firstPin, secondPin] = pins;
430
+ const directConnection = inputProblem.directConnections.find(
431
+ (connection) => connection.pinIds.includes(firstPin.pinId) && connection.pinIds.includes(secondPin.pinId)
432
+ );
433
+ if (directConnection?.netLabelWidth === void 0) return false;
434
+ const firstChip = chipMap[firstPin.chipId];
435
+ const secondChip = chipMap[secondPin.chipId];
436
+ if (!firstChip || !secondChip) return false;
437
+ const hasSinglePinPeripheral = firstChip.pins.length === 1 || secondChip.pins.length === 1;
438
+ if (!hasSinglePinPeripheral) return false;
439
+ let firstFacingDirection = firstPin._facingDirection;
440
+ if (!firstFacingDirection) {
441
+ firstFacingDirection = getPinDirection(firstPin, firstChip);
442
+ }
443
+ let secondFacingDirection = secondPin._facingDirection;
444
+ if (!secondFacingDirection) {
445
+ secondFacingDirection = getPinDirection(secondPin, secondChip);
446
+ }
447
+ return firstFacingDirection === "x-" && secondFacingDirection === "x+" || firstFacingDirection === "x+" && secondFacingDirection === "x-";
448
+ };
449
+
423
450
  // lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts
424
451
  var DEFAULT_MAX_MSP_PAIR_DISTANCE = 1;
425
452
  var getPinPairKey = (pinIds) => [...pinIds].sort().join("::");
@@ -489,8 +516,16 @@ var MspConnectionPairSolver = class extends BaseSolver {
489
516
  const p1 = this.pinMap[pin1];
490
517
  const p2 = this.pinMap[pin2];
491
518
  const pinPairKey = getPinPairKey([pin1, pin2]);
492
- const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey) ? distance(p1, p2) : Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
493
- if (pairDistance > this.maxMspPairDistance) {
519
+ let pairDistance = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
520
+ if (this.directConnectionPinPairKeys.has(pinPairKey)) {
521
+ pairDistance = distance(p1, p2);
522
+ }
523
+ const isLabeledPeripheral = isLabeledPeripheralConnection({
524
+ inputProblem: this.inputProblem,
525
+ chipMap: this.chipMap,
526
+ pins: [p1, p2]
527
+ });
528
+ if (pairDistance > this.maxMspPairDistance && !isLabeledPeripheral) {
494
529
  return;
495
530
  }
496
531
  if (arePinsInDifferentSchematicSections(this.inputProblem, p1, p2)) {
@@ -1081,6 +1116,9 @@ var generateInternalSegmentCollisionDetours = ({
1081
1116
  ]).filter(hasOnlyNonzeroOrthogonalSegments);
1082
1117
  };
1083
1118
 
1119
+ // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts
1120
+ import { boundsDistance } from "@tscircuit/math-utils";
1121
+
1084
1122
  // lib/solvers/GuidelinesSolver/getInputChipBounds.ts
1085
1123
  function getInputChipBounds(chip) {
1086
1124
  const halfWidth = chip.width / 2;
@@ -1094,6 +1132,7 @@ function getInputChipBounds(chip) {
1094
1132
  }
1095
1133
 
1096
1134
  // lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts
1135
+ var MIN_ROUTABLE_OBSTACLE_GAP = 0.05;
1097
1136
  var isTextBoxObstacle = (obstacle) => obstacle.kind === "text_box";
1098
1137
  var chipToRect = (chip) => {
1099
1138
  const b = getInputChipBounds(chip);
@@ -1102,7 +1141,17 @@ var chipToRect = (chip) => {
1102
1141
  var getObstacleRects = (problem, opts = {}) => {
1103
1142
  const chipRects = problem.chips.map(chipToRect);
1104
1143
  const textBoxRects = (problem.textBoxes ?? []).map((textBox) => {
1105
- const b = getTextBoxBounds(textBox, opts.textBoxPadding);
1144
+ const textBounds = getTextBoxBounds(textBox, opts.textBoxPadding);
1145
+ const attachedChipBounds = chipRects.find(
1146
+ (chip) => chip.chipId === textBox.chipId
1147
+ );
1148
+ const gap = attachedChipBounds ? boundsDistance(textBounds, attachedChipBounds) : Number.POSITIVE_INFINITY;
1149
+ const b = attachedChipBounds && gap > 0 && gap < MIN_ROUTABLE_OBSTACLE_GAP ? {
1150
+ minX: Math.min(textBounds.minX, attachedChipBounds.maxX),
1151
+ maxX: Math.max(textBounds.maxX, attachedChipBounds.minX),
1152
+ minY: Math.min(textBounds.minY, attachedChipBounds.maxY),
1153
+ maxY: Math.max(textBounds.maxY, attachedChipBounds.minY)
1154
+ } : textBounds;
1106
1155
  return {
1107
1156
  kind: "text_box",
1108
1157
  textBox,
@@ -1172,9 +1221,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1172
1221
  this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle));
1173
1222
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId));
1174
1223
  this.endpointTextObstacles = new Set(
1175
- endpointChipIds.size > 1 ? this.obstacles.filter(isTextBoxObstacle).filter(
1224
+ this.obstacles.filter(isTextBoxObstacle).filter(
1176
1225
  (obstacle) => obstacle.textBox.chipId !== void 0 && endpointChipIds.has(obstacle.textBox.chipId)
1177
- ) : []
1226
+ )
1178
1227
  );
1179
1228
  const [pin1, pin2] = this.pins;
1180
1229
  const directShortPath = calculateDirectShortPath(pin1, pin2);
@@ -4495,6 +4544,13 @@ var LongDistancePairSolver = class extends BaseSolver {
4495
4544
  pinMap.set(pin.pinId, { ...pin, chipId: chip.chipId });
4496
4545
  }
4497
4546
  }
4547
+ this.queuedFailedConnectionPairs = this.params.failedConnectionPairs.filter(
4548
+ (connectionPair) => isLabeledPeripheralConnection({
4549
+ inputProblem: this.inputProblem,
4550
+ chipMap: this.chipMap,
4551
+ pins: connectionPair.pins
4552
+ })
4553
+ );
4498
4554
  const candidatePairs = [];
4499
4555
  const addedPairKeys = /* @__PURE__ */ new Set();
4500
4556
  for (const netId of Object.keys(netConnMap.netMap)) {
@@ -4535,6 +4591,8 @@ var LongDistancePairSolver = class extends BaseSolver {
4535
4591
  solvedLongDistanceTraces = [];
4536
4592
  queuedCandidatePairs = [];
4537
4593
  currentCandidatePair = null;
4594
+ queuedFailedConnectionPairs = [];
4595
+ currentFailedConnectionPair = null;
4538
4596
  subSolver = null;
4539
4597
  chipMap = {};
4540
4598
  inputProblem;
@@ -4544,8 +4602,31 @@ var LongDistancePairSolver = class extends BaseSolver {
4544
4602
  getConstructorParams() {
4545
4603
  return this.params;
4546
4604
  }
4605
+ acceptFailedConnectionPair(tracePath) {
4606
+ const connectionPair = this.currentFailedConnectionPair;
4607
+ if (!connectionPair) return;
4608
+ const solvedTrace = {
4609
+ ...connectionPair,
4610
+ tracePath,
4611
+ mspConnectionPairIds: [connectionPair.mspPairId],
4612
+ pinIds: connectionPair.pins.map((pin) => pin.pinId)
4613
+ };
4614
+ this.solvedLongDistanceTraces.push(solvedTrace);
4615
+ this.allSolvedTraces.push(solvedTrace);
4616
+ for (const pin of connectionPair.pins) {
4617
+ this.newlyConnectedPinIds.add(pin.pinId);
4618
+ }
4619
+ }
4547
4620
  _step() {
4548
- if (this.subSolver?.solved) {
4621
+ if (this.subSolver?.solved && this.currentFailedConnectionPair) {
4622
+ const tracePath = this.subSolver.solvedTracePath;
4623
+ if (tracePath) {
4624
+ this.acceptFailedConnectionPair(tracePath);
4625
+ }
4626
+ this.subSolver = null;
4627
+ this.currentCandidatePair = null;
4628
+ this.currentFailedConnectionPair = null;
4629
+ } else if (this.subSolver?.solved) {
4549
4630
  const newTracePath = this.subSolver.solvedTracePath;
4550
4631
  if (newTracePath && this.currentCandidatePair) {
4551
4632
  const isTraceClear = !doesTraceOverlapWithExistingTraces(
@@ -4576,11 +4657,23 @@ var LongDistancePairSolver = class extends BaseSolver {
4576
4657
  } else if (this.subSolver?.failed) {
4577
4658
  this.subSolver = null;
4578
4659
  this.currentCandidatePair = null;
4660
+ this.currentFailedConnectionPair = null;
4579
4661
  }
4580
4662
  if (this.subSolver) {
4581
4663
  this.subSolver.step();
4582
4664
  return;
4583
4665
  }
4666
+ const failedConnectionPair = this.queuedFailedConnectionPairs.shift();
4667
+ if (failedConnectionPair) {
4668
+ this.currentFailedConnectionPair = failedConnectionPair;
4669
+ this.currentCandidatePair = failedConnectionPair.pins;
4670
+ this.subSolver = new SchematicTraceSingleLineSolver2({
4671
+ inputProblem: this.params.inputProblem,
4672
+ pins: failedConnectionPair.pins,
4673
+ chipMap: this.chipMap
4674
+ });
4675
+ return;
4676
+ }
4584
4677
  while (this.queuedCandidatePairs.length > 0) {
4585
4678
  const nextPair = this.queuedCandidatePairs.shift();
4586
4679
  const [p1, p2] = nextPair;
@@ -10703,7 +10796,8 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
10703
10796
  {
10704
10797
  inputProblem: instance.inputProblem,
10705
10798
  primaryMspConnectionPairs: instance.mspConnectionPairSolver.mspConnectionPairs,
10706
- alreadySolvedTraces: instance.schematicTraceLinesSolver.solvedTracePaths
10799
+ alreadySolvedTraces: instance.schematicTraceLinesSolver.solvedTracePaths,
10800
+ failedConnectionPairs: instance.schematicTraceLinesSolver.failedConnectionPairs
10707
10801
  }
10708
10802
  ],
10709
10803
  {
@@ -13,6 +13,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
13
13
  import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
14
14
  import type { ConnectivityMap } from "connectivity-map"
15
15
  import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
16
+ import { isLabeledPeripheralConnection } from "../MspConnectionPairSolver/isLabeledPeripheralConnection"
16
17
 
17
18
  const NEAREST_NEIGHBOR_COUNT = 3
18
19
 
@@ -28,6 +29,8 @@ export class LongDistancePairSolver extends BaseSolver {
28
29
  private currentCandidatePair:
29
30
  | [InputPin & { chipId: string }, InputPin & { chipId: string }]
30
31
  | null = null
32
+ private queuedFailedConnectionPairs: MspConnectionPair[] = []
33
+ private currentFailedConnectionPair: MspConnectionPair | null = null
31
34
  private subSolver: SchematicTraceSingleLineSolver2 | null = null
32
35
  private chipMap: Record<string, InputChip> = {}
33
36
  private inputProblem: InputProblem
@@ -40,6 +43,7 @@ export class LongDistancePairSolver extends BaseSolver {
40
43
  inputProblem: InputProblem
41
44
  alreadySolvedTraces: SolvedTracePath[]
42
45
  primaryMspConnectionPairs: MspConnectionPair[]
46
+ failedConnectionPairs: MspConnectionPair[]
43
47
  },
44
48
  ) {
45
49
  super()
@@ -66,6 +70,16 @@ export class LongDistancePairSolver extends BaseSolver {
66
70
  pinMap.set(pin.pinId, { ...pin, chipId: chip.chipId })
67
71
  }
68
72
  }
73
+ // Retry failed MSP pairs with their existing identity before creating
74
+ // new nearest-neighbor candidates.
75
+ this.queuedFailedConnectionPairs = this.params.failedConnectionPairs.filter(
76
+ (connectionPair) =>
77
+ isLabeledPeripheralConnection({
78
+ inputProblem: this.inputProblem,
79
+ chipMap: this.chipMap,
80
+ pins: connectionPair.pins,
81
+ }),
82
+ )
69
83
 
70
84
  // 2. Generate candidate pairs using N-Nearest-Neighbors approach
71
85
  const candidatePairs: Array<
@@ -129,9 +143,36 @@ export class LongDistancePairSolver extends BaseSolver {
129
143
  return this.params
130
144
  }
131
145
 
146
+ private acceptFailedConnectionPair(tracePath: SolvedTracePath["tracePath"]) {
147
+ const connectionPair = this.currentFailedConnectionPair
148
+ if (!connectionPair) return
149
+
150
+ // Reuse the original pair so downstream solvers retain its connectivity
151
+ // IDs, user net ID, pins, and MSP pair ID.
152
+ const solvedTrace: SolvedTracePath = {
153
+ ...connectionPair,
154
+ tracePath,
155
+ mspConnectionPairIds: [connectionPair.mspPairId],
156
+ pinIds: connectionPair.pins.map((pin) => pin.pinId),
157
+ }
158
+ this.solvedLongDistanceTraces.push(solvedTrace)
159
+ this.allSolvedTraces.push(solvedTrace)
160
+ for (const pin of connectionPair.pins) {
161
+ this.newlyConnectedPinIds.add(pin.pinId)
162
+ }
163
+ }
164
+
132
165
  override _step() {
133
166
  // 1. Check if a sub-solver has finished and process its result
134
- if (this.subSolver?.solved) {
167
+ if (this.subSolver?.solved && this.currentFailedConnectionPair) {
168
+ const tracePath = this.subSolver.solvedTracePath
169
+ if (tracePath) {
170
+ this.acceptFailedConnectionPair(tracePath)
171
+ }
172
+ this.subSolver = null
173
+ this.currentCandidatePair = null
174
+ this.currentFailedConnectionPair = null
175
+ } else if (this.subSolver?.solved) {
135
176
  const newTracePath = this.subSolver.solvedTracePath
136
177
  if (newTracePath && this.currentCandidatePair) {
137
178
  const isTraceClear = !doesTraceOverlapWithExistingTraces(
@@ -166,6 +207,7 @@ export class LongDistancePairSolver extends BaseSolver {
166
207
  } else if (this.subSolver?.failed) {
167
208
  this.subSolver = null
168
209
  this.currentCandidatePair = null
210
+ this.currentFailedConnectionPair = null
169
211
  }
170
212
 
171
213
  // 2. If a sub-solver is already running, let it continue
@@ -174,6 +216,18 @@ export class LongDistancePairSolver extends BaseSolver {
174
216
  return
175
217
  }
176
218
 
219
+ const failedConnectionPair = this.queuedFailedConnectionPairs.shift()
220
+ if (failedConnectionPair) {
221
+ this.currentFailedConnectionPair = failedConnectionPair
222
+ this.currentCandidatePair = failedConnectionPair.pins
223
+ this.subSolver = new SchematicTraceSingleLineSolver2({
224
+ inputProblem: this.params.inputProblem,
225
+ pins: failedConnectionPair.pins,
226
+ chipMap: this.chipMap,
227
+ })
228
+ return
229
+ }
230
+
177
231
  // 3. Find the next valid candidate pair and start a new sub-solver
178
232
  while (this.queuedCandidatePairs.length > 0) {
179
233
  const nextPair = this.queuedCandidatePairs.shift()!
@@ -14,6 +14,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
14
14
  import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
15
15
  import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
16
16
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
17
+ import { isLabeledPeripheralConnection } from "./isLabeledPeripheralConnection"
17
18
 
18
19
  export type MspConnectionPairId = string
19
20
  export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
@@ -116,10 +117,18 @@ export class MspConnectionPairSolver extends BaseSolver {
116
117
  const pinPairKey = getPinPairKey([pin1!, pin2!])
117
118
  // Explicit source traces are classified by straight-line distance when
118
119
  // their input is created; named nets retain the orthogonal route metric.
119
- const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey)
120
- ? distance(p1, p2)
121
- : Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
122
- if (pairDistance > this.maxMspPairDistance) {
120
+ let pairDistance = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
121
+ if (this.directConnectionPinPairKeys.has(pinPairKey)) {
122
+ pairDistance = distance(p1, p2)
123
+ }
124
+ // Labeled one-pin peripherals need a real trace even when they are far
125
+ // apart; skipping the MSP pair would leave only the fallback path.
126
+ const isLabeledPeripheral = isLabeledPeripheralConnection({
127
+ inputProblem: this.inputProblem,
128
+ chipMap: this.chipMap,
129
+ pins: [p1, p2],
130
+ })
131
+ if (pairDistance > this.maxMspPairDistance && !isLabeledPeripheral) {
123
132
  // Too far apart; skip creating an MSP pair for this net
124
133
  return
125
134
  }
@@ -0,0 +1,44 @@
1
+ import type { InputChip, InputPin, InputProblem } from "lib/types/InputProblem"
2
+ import { getPinDirection } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
3
+
4
+ type PinWithChipId = InputPin & { chipId: string }
5
+
6
+ export const isLabeledPeripheralConnection = ({
7
+ inputProblem,
8
+ chipMap,
9
+ pins,
10
+ }: {
11
+ inputProblem: InputProblem
12
+ chipMap: Record<string, InputChip>
13
+ pins: [PinWithChipId, PinWithChipId]
14
+ }): boolean => {
15
+ const [firstPin, secondPin] = pins
16
+ const directConnection = inputProblem.directConnections.find(
17
+ (connection) =>
18
+ connection.pinIds.includes(firstPin.pinId) &&
19
+ connection.pinIds.includes(secondPin.pinId),
20
+ )
21
+ if (directConnection?.netLabelWidth === undefined) return false
22
+
23
+ const firstChip = chipMap[firstPin.chipId]
24
+ const secondChip = chipMap[secondPin.chipId]
25
+ if (!firstChip || !secondChip) return false
26
+
27
+ const hasSinglePinPeripheral =
28
+ firstChip.pins.length === 1 || secondChip.pins.length === 1
29
+ if (!hasSinglePinPeripheral) return false
30
+
31
+ let firstFacingDirection = firstPin._facingDirection
32
+ if (!firstFacingDirection) {
33
+ firstFacingDirection = getPinDirection(firstPin, firstChip)
34
+ }
35
+ let secondFacingDirection = secondPin._facingDirection
36
+ if (!secondFacingDirection) {
37
+ secondFacingDirection = getPinDirection(secondPin, secondChip)
38
+ }
39
+
40
+ return (
41
+ (firstFacingDirection === "x-" && secondFacingDirection === "x+") ||
42
+ (firstFacingDirection === "x+" && secondFacingDirection === "x-")
43
+ )
44
+ }
@@ -123,16 +123,16 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
123
123
  })
124
124
  this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle))
125
125
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
126
+ // Same-chip routes also need these obstacles so they can escape around
127
+ // attached text after a narrow chip-to-text channel is closed.
126
128
  this.endpointTextObstacles = new Set(
127
- endpointChipIds.size > 1
128
- ? this.obstacles
129
- .filter(isTextBoxObstacle)
130
- .filter(
131
- (obstacle) =>
132
- obstacle.textBox.chipId !== undefined &&
133
- endpointChipIds.has(obstacle.textBox.chipId),
134
- )
135
- : [],
129
+ this.obstacles
130
+ .filter(isTextBoxObstacle)
131
+ .filter(
132
+ (obstacle) =>
133
+ obstacle.textBox.chipId !== undefined &&
134
+ endpointChipIds.has(obstacle.textBox.chipId),
135
+ ),
136
136
  )
137
137
 
138
138
  const [pin1, pin2] = this.pins
@@ -1,20 +1,21 @@
1
+ import { boundsDistance, type Bounds } from "@tscircuit/math-utils"
1
2
  import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
2
3
  import type { InputChip, InputProblem } from "lib/types/InputProblem"
3
4
  import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
4
5
 
5
- export type RectBounds = {
6
- minX: number
7
- minY: number
8
- maxX: number
9
- maxY: number
10
- }
6
+ // Component text is normally placed 0.04 units from its chip. A trace
7
+ // centerline can fit mathematically, but its rendered stroke cannot usefully
8
+ // occupy that channel.
9
+ const MIN_ROUTABLE_OBSTACLE_GAP = 0.05
10
+
11
+ export type { Bounds as RectBounds } from "@tscircuit/math-utils"
11
12
 
12
- export type ChipObstacleRect = RectBounds & {
13
+ export type ChipObstacleRect = Bounds & {
13
14
  kind: "chip"
14
15
  chipId: string
15
16
  }
16
17
 
17
- export type TextBoxObstacleRect = RectBounds & {
18
+ export type TextBoxObstacleRect = Bounds & {
18
19
  kind: "text_box"
19
20
  textBox: NonNullable<InputProblem["textBoxes"]>[number]
20
21
  }
@@ -36,7 +37,22 @@ export const getObstacleRects = (
36
37
  ): ObstacleRect[] => {
37
38
  const chipRects = problem.chips.map(chipToRect)
38
39
  const textBoxRects = (problem.textBoxes ?? []).map((textBox) => {
39
- const b = getTextBoxBounds(textBox, opts.textBoxPadding)
40
+ const textBounds = getTextBoxBounds(textBox, opts.textBoxPadding)
41
+ const attachedChipBounds = chipRects.find(
42
+ (chip) => chip.chipId === textBox.chipId,
43
+ )
44
+ const gap = attachedChipBounds
45
+ ? boundsDistance(textBounds, attachedChipBounds)
46
+ : Number.POSITIVE_INFINITY
47
+ const b =
48
+ attachedChipBounds && gap > 0 && gap < MIN_ROUTABLE_OBSTACLE_GAP
49
+ ? {
50
+ minX: Math.min(textBounds.minX, attachedChipBounds.maxX),
51
+ maxX: Math.max(textBounds.maxX, attachedChipBounds.minX),
52
+ minY: Math.min(textBounds.minY, attachedChipBounds.maxY),
53
+ maxY: Math.max(textBounds.maxY, attachedChipBounds.minY),
54
+ }
55
+ : textBounds
40
56
  return {
41
57
  kind: "text_box" as const,
42
58
  textBox,
@@ -140,6 +140,8 @@ export class SchematicTracePipelineSolver extends BaseSolver {
140
140
  instance.mspConnectionPairSolver!.mspConnectionPairs,
141
141
  alreadySolvedTraces:
142
142
  instance.schematicTraceLinesSolver!.solvedTracePaths,
143
+ failedConnectionPairs:
144
+ instance.schematicTraceLinesSolver!.failedConnectionPairs,
143
145
  },
144
146
  ],
145
147
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.113",
4
+ "version": "0.0.115",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -13,7 +13,7 @@
13
13
  "devDependencies": {
14
14
  "@biomejs/biome": "^2.2.2",
15
15
  "@react-hook/resize-observer": "^2.0.2",
16
- "@tscircuit/math-utils": "^0.0.19",
16
+ "@tscircuit/math-utils": "^0.0.36",
17
17
  "@types/bun": "^1.2.21",
18
18
  "bun-match-svg": "^0.0.13",
19
19
  "calculate-elbow": "^0.0.12",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.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-20260728T225606Z/bug-report-20260728T225606Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />