@tscircuit/schematic-trace-solver 0.0.187 → 0.0.189

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 (60) hide show
  1. package/dist/index.d.ts +42 -23
  2. package/dist/index.js +1053 -316
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +612 -230
  4. package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +2 -6
  5. package/lib/solvers/InlineNetLabelSolver/getAnchoredNetLabelRenderedBounds.ts +13 -0
  6. package/lib/solvers/InlineNetLabelSolver/getInlineLabelObstacles.ts +64 -0
  7. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +407 -0
  8. package/lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions.ts +145 -0
  9. package/lib/solvers/InlineNetLabelSolver/isLabelRepresentedInline.ts +15 -0
  10. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +61 -31
  11. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +94 -16
  12. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +4 -3
  13. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +7 -7
  14. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +3 -0
  15. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +5 -1
  16. package/lib/types/InputProblem.ts +2 -2
  17. package/package.json +1 -1
  18. package/site/SchematicTracePipelineSolver/repro-robot-controller-imu-tof.page.tsx +7 -0
  19. package/site/bug-reports/bug-report-20260907T110144Z.page.tsx +4 -0
  20. package/site/bug-reports/bug-report-20260907T145640Z.page.tsx +4 -0
  21. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +2 -2
  23. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +18 -18
  24. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
  25. package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +21 -0
  26. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +5 -38
  27. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +3 -9
  28. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +18 -30
  29. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +33 -0
  30. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +282 -0
  31. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.json +1073 -0
  32. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +12 -0
  33. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +1107 -0
  34. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.json +3563 -0
  35. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.test.ts +117 -0
  36. package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +4 -4
  37. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
  38. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +20 -20
  39. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +37 -55
  40. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +44 -68
  41. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +3 -9
  42. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.snap.svg +199 -0
  43. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.source.svg +33 -0
  44. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +50 -74
  45. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  46. package/tests/repros/__snapshots__/rp2040-robot-controller-disconnected-gnd.snap.svg +103 -0
  47. package/tests/repros/assets/repro-robot-controller-imu-tof.input.json +540 -0
  48. package/tests/repros/assets/rp2040-robot-controller.input.json +349 -0
  49. package/tests/repros/repro-battery-management-bq24073.test.ts +13 -1
  50. package/tests/repros/repro-hub-usb-sheet.test.ts +25 -0
  51. package/tests/repros/repro-robot-controller-imu-tof.test.ts +83 -0
  52. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +59 -13
  53. package/tests/repros/rp2040-robot-controller-disconnected-gnd.test.ts +59 -0
  54. package/tests/solvers/InlineNetLabelSolver/get-anchored-net-label-rendered-bounds.test.ts +24 -0
  55. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +296 -0
  56. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +7 -4
  57. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +2 -1
  58. package/tests/solvers/InlineNetLabelSolver/shared-direct-and-named-connection.test.ts +62 -0
  59. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +9 -3
  60. package/tests/solvers/TraceCleanupSolver/perpendicular-detours-preserve-endpoints.test.ts +60 -0
@@ -76,33 +76,52 @@ const getRequiredOutwardDistance = (
76
76
  label: NetLabelPlacement,
77
77
  inlineBounds: Bounds[],
78
78
  ) => {
79
- const labelBounds = getAnchoredNetLabelRenderedBounds(label)
80
-
81
- if (label.orientation === "x-" || label.orientation === "x+") {
82
- const nearby = inlineBounds.filter(
83
- (bounds) =>
84
- labelBounds.minY < bounds.maxY && labelBounds.maxY > bounds.minY,
79
+ let distance = 0
80
+ // Only clear obstacles within the current proposal's clearance. A label elsewhere
81
+ // on the same row must not force this label across the whole schematic.
82
+ // Moving past one obstacle can encounter another, so continue until clear.
83
+ for (let iteration = 0; iteration < inlineBounds.length; iteration++) {
84
+ const labelBounds = getAnchoredNetLabelRenderedBounds(
85
+ moveLabel(label, label.orientation, distance),
86
+ )
87
+ const horizontal = label.orientation === "x-" || label.orientation === "x+"
88
+ const overlapping = inlineBounds.filter((bounds) =>
89
+ boundsOverlap(labelBounds, {
90
+ minX: bounds.minX - (horizontal ? LABEL_CLEARANCE : 0),
91
+ maxX: bounds.maxX + (horizontal ? LABEL_CLEARANCE : 0),
92
+ minY: bounds.minY - (horizontal ? 0 : LABEL_CLEARANCE),
93
+ maxY: bounds.maxY + (horizontal ? 0 : LABEL_CLEARANCE),
94
+ }),
85
95
  )
86
- if (nearby.length === 0) return 0
87
- if (label.orientation === "x-") {
88
- const targetMaxX = Math.min(...nearby.map((bounds) => bounds.minX))
89
- return Math.max(0, labelBounds.maxX - targetMaxX + LABEL_CLEARANCE)
96
+ if (overlapping.length === 0) break
97
+ switch (label.orientation) {
98
+ case "x-":
99
+ distance +=
100
+ labelBounds.maxX -
101
+ Math.min(...overlapping.map((b) => b.minX)) +
102
+ LABEL_CLEARANCE
103
+ break
104
+ case "x+":
105
+ distance +=
106
+ Math.max(...overlapping.map((b) => b.maxX)) -
107
+ labelBounds.minX +
108
+ LABEL_CLEARANCE
109
+ break
110
+ case "y-":
111
+ distance +=
112
+ labelBounds.maxY -
113
+ Math.min(...overlapping.map((b) => b.minY)) +
114
+ LABEL_CLEARANCE
115
+ break
116
+ case "y+":
117
+ distance +=
118
+ Math.max(...overlapping.map((b) => b.maxY)) -
119
+ labelBounds.minY +
120
+ LABEL_CLEARANCE
121
+ break
90
122
  }
91
- const targetMinX = Math.max(...nearby.map((bounds) => bounds.maxX))
92
- return Math.max(0, targetMinX - labelBounds.minX + LABEL_CLEARANCE)
93
123
  }
94
-
95
- const nearby = inlineBounds.filter(
96
- (bounds) =>
97
- labelBounds.minX < bounds.maxX && labelBounds.maxX > bounds.minX,
98
- )
99
- if (nearby.length === 0) return 0
100
- if (label.orientation === "y-") {
101
- const targetMaxY = Math.min(...nearby.map((bounds) => bounds.minY))
102
- return Math.max(0, labelBounds.maxY - targetMaxY + LABEL_CLEARANCE)
103
- }
104
- const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY))
105
- return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE)
124
+ return distance
106
125
  }
107
126
 
108
127
  const moveLabel = (
@@ -159,17 +178,14 @@ const sharesOwnerChip = (
159
178
  ) =>
160
179
  label.pinIds.some((pinId) => ownerChipIds.has(chipIdByPinId.get(pinId) ?? ""))
161
180
 
162
- const isGeneratedLabelConnector = (trace: SolvedTracePath) =>
163
- trace.mspPairId.startsWith("available-net-orientation-") ||
164
- trace.mspPairId.startsWith("inline-net-label-clearance-")
165
-
166
181
  const findConnectorTraceIndex = (
167
182
  label: NetLabelPlacement,
168
183
  traces: SolvedTracePath[],
184
+ connectorTraceIds: ReadonlySet<string>,
169
185
  ) =>
170
186
  traces.findIndex((trace) => {
171
187
  if (trace.globalConnNetId !== label.globalConnNetId) return false
172
- if (!isGeneratedLabelConnector(trace)) return false
188
+ if (!connectorTraceIds.has(trace.mspPairId)) return false
173
189
  const first = trace.tracePath[0]
174
190
  const last = trace.tracePath.at(-1)
175
191
  return Boolean(
@@ -298,7 +314,9 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
298
314
  traces,
299
315
  netLabelPlacements,
300
316
  inlineNetLabelPlacements,
317
+ netLabelConnectorTraceIds = new Set<string>(),
301
318
  }: {
319
+ netLabelConnectorTraceIds?: ReadonlySet<string>
302
320
  inputProblem: InputProblem
303
321
  traces: SolvedTracePath[]
304
322
  netLabelPlacements: NetLabelPlacement[]
@@ -307,7 +325,9 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
307
325
  traces: SolvedTracePath[]
308
326
  netLabelPlacements: NetLabelPlacement[]
309
327
  movedLabelCount: number
328
+ netLabelConnectorTraceIds: ReadonlySet<string>
310
329
  } => {
330
+ const connectorTraceIds = new Set(netLabelConnectorTraceIds)
311
331
  const outputTraces = traces.map((trace) => ({
312
332
  ...trace,
313
333
  tracePath: trace.tracePath.map((point) => ({ ...point })),
@@ -364,7 +384,11 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
364
384
  if (!boundsOverlap(movingBounds, obstacleBounds)) continue
365
385
  if (
366
386
  !sharesOwnerChip(obstacle, ownerChipIds, chipIdByPinId) ||
367
- (findConnectorTraceIndex(obstacle, outputTraces) === -1 &&
387
+ (findConnectorTraceIndex(
388
+ obstacle,
389
+ outputTraces,
390
+ connectorTraceIds,
391
+ ) === -1 &&
368
392
  !canAddConnectorAtAnchor(obstacle, outputTraces, pinMap))
369
393
  ) {
370
394
  failed = true
@@ -460,7 +484,11 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
460
484
  }> = []
461
485
  for (const [labelIndex, movedLabel] of proposals) {
462
486
  const label = outputLabels[labelIndex]!
463
- const connectorIndex = findConnectorTraceIndex(label, outputTraces)
487
+ const connectorIndex = findConnectorTraceIndex(
488
+ label,
489
+ outputTraces,
490
+ connectorTraceIds,
491
+ )
464
492
  if (
465
493
  connectorIndex === -1 &&
466
494
  !canAddConnectorAtAnchor(label, outputTraces, pinMap)
@@ -528,6 +556,7 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
528
556
  movedLabelIndices.add(labelIndex)
529
557
  }
530
558
  for (const update of connectorUpdates) {
559
+ connectorTraceIds.add(update.trace.mspPairId)
531
560
  if (update.connectorIndex === -1) outputTraces.push(update.trace)
532
561
  else outputTraces[update.connectorIndex] = update.trace
533
562
  }
@@ -537,5 +566,6 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
537
566
  traces: outputTraces,
538
567
  netLabelPlacements: outputLabels,
539
568
  movedLabelCount: movedLabelIndices.size,
569
+ netLabelConnectorTraceIds: connectorTraceIds,
540
570
  }
541
571
  }
@@ -1,6 +1,8 @@
1
+ import { getAnchoredNetLabelRenderedBounds } from "../InlineNetLabelSolver/getAnchoredNetLabelRenderedBounds"
1
2
  import type { GraphicsObject } from "graphics-debug"
2
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
4
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
4
6
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
7
  import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
6
8
  import type { InputProblem } from "lib/types/InputProblem"
@@ -91,6 +93,12 @@ function sampleAnchorsAlongSegment(
91
93
  }
92
94
 
93
95
  export interface NetLabelNetLabelCollisionSolverParams {
96
+ /** Use the final horizontal tag envelope after inline conversion. */
97
+ useRenderedLabelBounds?: boolean
98
+ /** Already placed labels that are obstacles, but must not be moved. */
99
+ fixedNetLabelPlacements?: NetLabelPlacement[]
100
+ /** Existing connector identities from AvailableNetOrientationSolver. */
101
+ netLabelConnectorTraceIds?: ReadonlySet<MspConnectionPairId>
94
102
  inputProblem: InputProblem
95
103
  traces: SolvedTracePath[]
96
104
  netLabelPlacements: NetLabelPlacement[]
@@ -107,7 +115,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
107
115
  currentLabelToMove: NetLabelPlacement | null = null
108
116
  candidateResults: Candidate[] = []
109
117
 
110
- private chipIndex: ChipObstacleSpatialIndex
118
+ private chipIndex: ChipObstacleSpatialIndex | undefined
111
119
  private traceMap: Record<MspConnectionPairId, SolvedTracePath>
112
120
  private skippedCollisionKeys = new Set<string>()
113
121
 
@@ -115,7 +123,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
115
123
  private candidateQueue: Candidate[] = []
116
124
  private candidateIndex = 0
117
125
 
118
- constructor(params: NetLabelNetLabelCollisionSolverParams) {
126
+ constructor(private params: NetLabelNetLabelCollisionSolverParams) {
119
127
  super()
120
128
  this.inputProblem = params.inputProblem
121
129
  this.traces = params.traces
@@ -123,7 +131,9 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
123
131
  this.outputNetLabelPlacements = [...params.netLabelPlacements]
124
132
  this.chipIndex =
125
133
  params.inputProblem._chipObstacleSpatialIndex ??
126
- new ChipObstacleSpatialIndex(params.inputProblem.chips)
134
+ (params.inputProblem.chips.length > 0
135
+ ? new ChipObstacleSpatialIndex(params.inputProblem.chips)
136
+ : undefined)
127
137
  this.traceMap = Object.fromEntries(
128
138
  params.traces.map((t) => [t.mspPairId, t]),
129
139
  ) as Record<MspConnectionPairId, SolvedTracePath>
@@ -136,6 +146,8 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
136
146
  inputProblem: this.inputProblem,
137
147
  traces: this.traces,
138
148
  netLabelPlacements: this.netLabelPlacements,
149
+ fixedNetLabelPlacements: this.params.fixedNetLabelPlacements,
150
+ useRenderedLabelBounds: this.params.useRenderedLabelBounds,
139
151
  }
140
152
  }
141
153
 
@@ -144,7 +156,12 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
144
156
  }
145
157
 
146
158
  private labelBounds(label: NetLabelPlacement) {
147
- return getRectBounds(label.center, label.width, label.height)
159
+ if (
160
+ !this.params.useRenderedLabelBounds ||
161
+ this.params.fixedNetLabelPlacements?.includes(label)
162
+ )
163
+ return getRectBounds(label.center, label.width, label.height)
164
+ return getAnchoredNetLabelRenderedBounds(label)
148
165
  }
149
166
 
150
167
  private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) {
@@ -154,12 +171,19 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
154
171
  private findNextCollidingPair():
155
172
  | [NetLabelPlacement, NetLabelPlacement]
156
173
  | null {
157
- const labels = this.outputNetLabelPlacements
158
- for (let i = 0; i < labels.length; i++) {
174
+ const labels = [
175
+ ...this.outputNetLabelPlacements,
176
+ ...(this.params.fixedNetLabelPlacements ?? []),
177
+ ]
178
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
159
179
  for (let j = i + 1; j < labels.length; j++) {
160
180
  const a = labels[i]!
161
181
  const b = labels[j]!
162
- if (a.globalConnNetId === b.globalConnNetId) continue
182
+ if (
183
+ a.globalConnNetId === b.globalConnNetId &&
184
+ !this.params.fixedNetLabelPlacements?.includes(b)
185
+ )
186
+ continue
163
187
  if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue
164
188
  if (boundsOverlap(this.labelBounds(a), this.labelBounds(b)))
165
189
  return [a, b]
@@ -208,16 +232,31 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
208
232
  center,
209
233
  width,
210
234
  height,
211
- bounds: getRectBounds(center, width, height),
235
+ bounds: this.params.useRenderedLabelBounds
236
+ ? getAnchoredNetLabelRenderedBounds({
237
+ ...label,
238
+ orientation,
239
+ anchorPoint: anchor,
240
+ center,
241
+ width,
242
+ height,
243
+ })
244
+ : getRectBounds(center, width, height),
212
245
  hostPairId,
213
246
  hostSegIndex,
214
247
  status: null,
215
248
  }
216
249
  }
217
250
 
218
- const isPortOnly = label.mspConnectionPairIds.length === 0
251
+ // A label on an extended connector must keep that attachment point.
252
+ const isAnchoredOnConnector = this.traces.some(
253
+ (trace) =>
254
+ this.params.netLabelConnectorTraceIds?.has(trace.mspPairId) &&
255
+ trace.globalConnNetId === label.globalConnNetId &&
256
+ tracePathContainsPoint(trace.tracePath, label.anchorPoint),
257
+ )
219
258
 
220
- if (isPortOnly) {
259
+ if (label.mspConnectionPairIds.length === 0 || isAnchoredOnConnector) {
221
260
  const allOrientations: FacingDirection[] = ["x+", "x-", "y+", "y-"]
222
261
  const orderedOrientations = [
223
262
  label.orientation,
@@ -271,7 +310,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
271
310
  ): CandidateStatus {
272
311
  const { bounds, hostPairId, hostSegIndex } = candidate
273
312
 
274
- if (this.chipIndex.getChipsInBounds(bounds).length > 0)
313
+ if ((this.chipIndex?.getChipsInBounds(bounds).length ?? 0) > 0)
275
314
  return "chip-collision"
276
315
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
277
316
  return "text-collision"
@@ -284,7 +323,11 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
284
323
  }
285
324
 
286
325
  for (const obstacle of obstacleLabels) {
287
- if (obstacle.globalConnNetId === movingLabelNetId) continue
326
+ if (
327
+ obstacle.globalConnNetId === movingLabelNetId &&
328
+ !this.params.fixedNetLabelPlacements?.includes(obstacle)
329
+ )
330
+ continue
288
331
  if (boundsOverlap(bounds, this.labelBounds(obstacle)))
289
332
  return "label-collision"
290
333
  }
@@ -299,6 +342,38 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
299
342
  this.candidateResults = []
300
343
  }
301
344
 
345
+ /** Reuse the normal anchor search for a local trace-clearance proposal. */
346
+ getNearbyValidPlacements(
347
+ label: NetLabelPlacement,
348
+ maxDistance: number,
349
+ ): NetLabelPlacement[] {
350
+ const distance = (candidate: Candidate) =>
351
+ Math.hypot(
352
+ candidate.anchor.x - label.anchorPoint.x,
353
+ candidate.anchor.y - label.anchorPoint.y,
354
+ )
355
+ const obstacles = [
356
+ ...this.outputNetLabelPlacements.filter((other) => other !== label),
357
+ ...(this.params.fixedNetLabelPlacements ?? []),
358
+ ]
359
+ return this.buildCandidatesForLabel(label)
360
+ .filter(
361
+ (candidate) =>
362
+ distance(candidate) <= maxDistance &&
363
+ this.checkCandidate(candidate, label.globalConnNetId, obstacles) ===
364
+ "ok",
365
+ )
366
+ .sort((a, b) => distance(a) - distance(b))
367
+ .map((candidate) => ({
368
+ ...label,
369
+ orientation: candidate.orientation,
370
+ anchorPoint: candidate.anchor,
371
+ center: candidate.center,
372
+ width: candidate.width,
373
+ height: candidate.height,
374
+ }))
375
+ }
376
+
302
377
  private clearActiveSearch() {
303
378
  this.currentCollision = null
304
379
  this.currentLabelToMove = null
@@ -316,7 +391,9 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
316
391
  return
317
392
  }
318
393
  this.currentCollision = pair
319
- this.labelsToTry = [pair[1], pair[0]]
394
+ this.labelsToTry = [pair[1], pair[0]].filter(
395
+ (label) => !(this.params.fixedNetLabelPlacements ?? []).includes(label),
396
+ )
320
397
  this.beginSearchForLabel(this.labelsToTry.shift()!)
321
398
  return
322
399
  }
@@ -342,9 +419,10 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
342
419
  fixedLabel = labelB
343
420
  }
344
421
  const obstacleLabels = [
345
- ...this.outputNetLabelPlacements.filter(
346
- (l) => l !== labelA && l !== labelB,
347
- ),
422
+ ...[
423
+ ...this.outputNetLabelPlacements,
424
+ ...(this.params.fixedNetLabelPlacements ?? []),
425
+ ].filter((l) => l !== labelA && l !== labelB),
348
426
  fixedLabel,
349
427
  ]
350
428
 
@@ -44,7 +44,6 @@ interface CandidatePair {
44
44
  netConnectionPinIds?: PinId[]
45
45
  }
46
46
 
47
- const AVAILABLE_NET_ORIENTATION_PREFIX = "available-net-orientation-"
48
47
  const RECOVERED_TRACE_PREFIX = "net-label-to-trace-"
49
48
  const MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET = 0.05
50
49
  const MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25
@@ -82,6 +81,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
82
81
  outputTraces: SolvedTracePath[]
83
82
  outputNetLabelPlacements: NetLabelPlacement[]
84
83
 
84
+ private readonly recoveredTraceIds = new Set<string>()
85
85
  private chipMap: Record<ChipId, InputChip>
86
86
  private pinMap: Map<PinId, TraceRecoveryPin>
87
87
  private queuedCandidates: CandidatePair[]
@@ -424,7 +424,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
424
424
  trace: SolvedTracePath,
425
425
  candidate: CandidatePair,
426
426
  ) {
427
- if (!trace.mspPairId.startsWith(AVAILABLE_NET_ORIENTATION_PREFIX)) {
427
+ if (!this.input.netLabelConnectorTraceIds?.has(trace.mspPairId)) {
428
428
  return false
429
429
  }
430
430
  if (trace.pinIds.length !== 1) return false
@@ -491,6 +491,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
491
491
  pinIds: [firstPin.pinId, secondPin.pinId],
492
492
  }
493
493
 
494
+ this.recoveredTraceIds.add(mspPairId)
494
495
  this.outputTraces = [...retainedTraces, recoveredTrace]
495
496
  if (candidate.recoveryMode !== "routed_components") {
496
497
  this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
@@ -566,7 +567,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
566
567
  ...this.getOutput(),
567
568
  })
568
569
  for (const trace of this.outputTraces) {
569
- if (!trace.mspPairId.startsWith(RECOVERED_TRACE_PREFIX)) continue
570
+ if (!this.recoveredTraceIds.has(trace.mspPairId)) continue
570
571
  graphics.lines!.push({
571
572
  points: trace.tracePath,
572
573
  strokeColor: "green",
@@ -8,6 +8,7 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
8
8
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
9
9
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
10
10
  import { getMovedAnchorPointForReroute } from "lib/solvers/Example28Solver/getMovedAnchorPointForReroute"
11
+ import { isLabelAttachedToTrace } from "lib/solvers/Example28Solver/isLabelAttachedToTrace"
11
12
  import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
12
13
  import {
13
14
  rectsOverlap,
@@ -182,14 +183,13 @@ const getAttachedLabelIndexes = (
182
183
  netLabelPlacements: NetLabelPlacement[],
183
184
  ) =>
184
185
  netLabelPlacements.flatMap((label, index) => {
185
- const labelOwnsTrace =
186
- label.mspConnectionPairIds.includes(trace.mspPairId) ||
187
- (label.mspConnectionPairIds.length === 0 &&
188
- label.globalConnNetId === trace.globalConnNetId)
189
- return labelOwnsTrace &&
186
+ if (
187
+ isLabelAttachedToTrace(label, trace) &&
190
188
  tracePathContainsPoint(trace.tracePath, label.anchorPoint)
191
- ? [index]
192
- : []
189
+ ) {
190
+ return [index]
191
+ }
192
+ return []
193
193
  })
194
194
 
195
195
  const moveAttachedLabels = ({
@@ -598,6 +598,9 @@ export class SchematicTracePipelineSolver extends BaseSolver {
598
598
  traces: junctionOutput.traces,
599
599
  netLabelPlacements: junctionOutput.netLabelPlacements,
600
600
  completedReroutes,
601
+ resolveTraceCollisions: true,
602
+ netLabelConnectorTraceIds:
603
+ instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
601
604
  },
602
605
  ]
603
606
  },
@@ -232,13 +232,17 @@ export const generatePerpendicularTraceDetours = ({
232
232
  ]),
233
233
  ].filter((coordinate): coordinate is number => coordinate !== undefined)
234
234
 
235
+ const remainingPath = path.slice(index + 2)
236
+ // A terminal segment has no following leg to reconnect the detour.
237
+ if (remainingPath.length === 0) remainingPath.push(end)
238
+
235
239
  return [...new Set(detourCoordinates)].map((detour) =>
236
240
  simplifyPath([
237
241
  ...path.slice(0, index + 1),
238
242
  { ...start, [movingAxis]: gate },
239
243
  { ...start, [movingAxis]: gate, [detourAxis]: detour },
240
244
  { ...end, [detourAxis]: detour },
241
- ...path.slice(index + 2),
245
+ ...remainingPath,
242
246
  ]),
243
247
  )
244
248
  }
@@ -120,8 +120,8 @@ export interface InputNetConnection {
120
120
  * When true, a named net may use inline labels. Each routed connected
121
121
  * component gets a label along its representative trace, while each
122
122
  * disconnected endpoint gets an outward inline-label stub. Conversion is
123
- * atomic for the whole net so inline and anchored representations are not
124
- * mixed when any label cannot be placed.
123
+ * atomic within each routed component. An obstructed component retains its
124
+ * anchored label without preventing inline labels on disconnected components.
125
125
  */
126
126
  allowInlineNetLabel?: boolean
127
127
 
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.187",
8
+ "version": "0.0.189",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,7 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import type { InputProblem } from "lib/types/InputProblem"
3
+ import inputProblem from "../../tests/repros/assets/repro-robot-controller-imu-tof.input.json"
4
+
5
+ export default () => (
6
+ <PipelineDebugger inputProblem={inputProblem as InputProblem} />
7
+ )
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.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-20260907T145640Z/bug-report-20260907T145640Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />