@tscircuit/schematic-trace-solver 0.0.186 → 0.0.188

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 (53) hide show
  1. package/dist/index.d.ts +40 -23
  2. package/dist/index.js +1041 -319
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +611 -230
  4. package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +2 -6
  5. package/lib/solvers/InlineNetLabelSolver/getInlineLabelObstacles.ts +64 -0
  6. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +407 -0
  7. package/lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions.ts +145 -0
  8. package/lib/solvers/InlineNetLabelSolver/isLabelRepresentedInline.ts +15 -0
  9. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +61 -31
  10. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +83 -14
  11. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +4 -3
  12. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +14 -11
  13. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +3 -0
  14. package/lib/types/InputProblem.ts +2 -2
  15. package/package.json +1 -1
  16. package/site/bug-reports/bug-report-20260907T083336Z.page.tsx +4 -0
  17. package/site/bug-reports/bug-report-20260907T110144Z.page.tsx +4 -0
  18. package/site/bug-reports/bug-report-20260907T145640Z.page.tsx +4 -0
  19. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +18 -18
  21. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +21 -0
  23. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +5 -38
  24. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +3 -9
  25. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +18 -30
  26. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +33 -0
  27. package/tests/bug-reports/bug-report-20260907T083336Z/__snapshots__/bug-report-20260907T083336Z.snap.svg +520 -0
  28. package/tests/bug-reports/bug-report-20260907T083336Z/bug-report-20260907T083336Z.json +2031 -0
  29. package/tests/bug-reports/bug-report-20260907T083336Z/bug-report-20260907T083336Z.test.ts +27 -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__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  37. package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +4 -4
  38. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
  39. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +20 -20
  40. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +37 -55
  41. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +44 -68
  42. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +3 -9
  43. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +50 -74
  44. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  45. package/tests/repros/repro-battery-management-bq24073.test.ts +13 -1
  46. package/tests/repros/repro-hub-usb-sheet.test.ts +25 -0
  47. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +59 -13
  48. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +296 -0
  49. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +7 -4
  50. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +2 -1
  51. package/tests/solvers/InlineNetLabelSolver/shared-direct-and-named-connection.test.ts +62 -0
  52. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +9 -3
  53. package/tests/solvers/SameNetJunctionAlignmentSolver/parallel-load-rails.test.ts +24 -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,3 +1,4 @@
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"
@@ -91,6 +92,10 @@ function sampleAnchorsAlongSegment(
91
92
  }
92
93
 
93
94
  export interface NetLabelNetLabelCollisionSolverParams {
95
+ /** Use the final horizontal tag envelope after inline conversion. */
96
+ useRenderedLabelBounds?: boolean
97
+ /** Already placed labels that are obstacles, but must not be moved. */
98
+ fixedNetLabelPlacements?: NetLabelPlacement[]
94
99
  inputProblem: InputProblem
95
100
  traces: SolvedTracePath[]
96
101
  netLabelPlacements: NetLabelPlacement[]
@@ -107,7 +112,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
107
112
  currentLabelToMove: NetLabelPlacement | null = null
108
113
  candidateResults: Candidate[] = []
109
114
 
110
- private chipIndex: ChipObstacleSpatialIndex
115
+ private chipIndex: ChipObstacleSpatialIndex | undefined
111
116
  private traceMap: Record<MspConnectionPairId, SolvedTracePath>
112
117
  private skippedCollisionKeys = new Set<string>()
113
118
 
@@ -115,7 +120,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
115
120
  private candidateQueue: Candidate[] = []
116
121
  private candidateIndex = 0
117
122
 
118
- constructor(params: NetLabelNetLabelCollisionSolverParams) {
123
+ constructor(private params: NetLabelNetLabelCollisionSolverParams) {
119
124
  super()
120
125
  this.inputProblem = params.inputProblem
121
126
  this.traces = params.traces
@@ -123,7 +128,9 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
123
128
  this.outputNetLabelPlacements = [...params.netLabelPlacements]
124
129
  this.chipIndex =
125
130
  params.inputProblem._chipObstacleSpatialIndex ??
126
- new ChipObstacleSpatialIndex(params.inputProblem.chips)
131
+ (params.inputProblem.chips.length > 0
132
+ ? new ChipObstacleSpatialIndex(params.inputProblem.chips)
133
+ : undefined)
127
134
  this.traceMap = Object.fromEntries(
128
135
  params.traces.map((t) => [t.mspPairId, t]),
129
136
  ) as Record<MspConnectionPairId, SolvedTracePath>
@@ -136,6 +143,8 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
136
143
  inputProblem: this.inputProblem,
137
144
  traces: this.traces,
138
145
  netLabelPlacements: this.netLabelPlacements,
146
+ fixedNetLabelPlacements: this.params.fixedNetLabelPlacements,
147
+ useRenderedLabelBounds: this.params.useRenderedLabelBounds,
139
148
  }
140
149
  }
141
150
 
@@ -144,7 +153,12 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
144
153
  }
145
154
 
146
155
  private labelBounds(label: NetLabelPlacement) {
147
- return getRectBounds(label.center, label.width, label.height)
156
+ if (
157
+ !this.params.useRenderedLabelBounds ||
158
+ this.params.fixedNetLabelPlacements?.includes(label)
159
+ )
160
+ return getRectBounds(label.center, label.width, label.height)
161
+ return getAnchoredNetLabelRenderedBounds(label)
148
162
  }
149
163
 
150
164
  private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) {
@@ -154,12 +168,19 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
154
168
  private findNextCollidingPair():
155
169
  | [NetLabelPlacement, NetLabelPlacement]
156
170
  | null {
157
- const labels = this.outputNetLabelPlacements
158
- for (let i = 0; i < labels.length; i++) {
171
+ const labels = [
172
+ ...this.outputNetLabelPlacements,
173
+ ...(this.params.fixedNetLabelPlacements ?? []),
174
+ ]
175
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
159
176
  for (let j = i + 1; j < labels.length; j++) {
160
177
  const a = labels[i]!
161
178
  const b = labels[j]!
162
- if (a.globalConnNetId === b.globalConnNetId) continue
179
+ if (
180
+ a.globalConnNetId === b.globalConnNetId &&
181
+ !this.params.fixedNetLabelPlacements?.includes(b)
182
+ )
183
+ continue
163
184
  if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue
164
185
  if (boundsOverlap(this.labelBounds(a), this.labelBounds(b)))
165
186
  return [a, b]
@@ -208,7 +229,16 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
208
229
  center,
209
230
  width,
210
231
  height,
211
- bounds: getRectBounds(center, width, height),
232
+ bounds: this.params.useRenderedLabelBounds
233
+ ? getAnchoredNetLabelRenderedBounds({
234
+ ...label,
235
+ orientation,
236
+ anchorPoint: anchor,
237
+ center,
238
+ width,
239
+ height,
240
+ })
241
+ : getRectBounds(center, width, height),
212
242
  hostPairId,
213
243
  hostSegIndex,
214
244
  status: null,
@@ -271,7 +301,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
271
301
  ): CandidateStatus {
272
302
  const { bounds, hostPairId, hostSegIndex } = candidate
273
303
 
274
- if (this.chipIndex.getChipsInBounds(bounds).length > 0)
304
+ if ((this.chipIndex?.getChipsInBounds(bounds).length ?? 0) > 0)
275
305
  return "chip-collision"
276
306
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
277
307
  return "text-collision"
@@ -284,7 +314,11 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
284
314
  }
285
315
 
286
316
  for (const obstacle of obstacleLabels) {
287
- if (obstacle.globalConnNetId === movingLabelNetId) continue
317
+ if (
318
+ obstacle.globalConnNetId === movingLabelNetId &&
319
+ !this.params.fixedNetLabelPlacements?.includes(obstacle)
320
+ )
321
+ continue
288
322
  if (boundsOverlap(bounds, this.labelBounds(obstacle)))
289
323
  return "label-collision"
290
324
  }
@@ -299,6 +333,38 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
299
333
  this.candidateResults = []
300
334
  }
301
335
 
336
+ /** Reuse the normal anchor search for a local trace-clearance proposal. */
337
+ getNearbyValidPlacements(
338
+ label: NetLabelPlacement,
339
+ maxDistance: number,
340
+ ): NetLabelPlacement[] {
341
+ const distance = (candidate: Candidate) =>
342
+ Math.hypot(
343
+ candidate.anchor.x - label.anchorPoint.x,
344
+ candidate.anchor.y - label.anchorPoint.y,
345
+ )
346
+ const obstacles = [
347
+ ...this.outputNetLabelPlacements.filter((other) => other !== label),
348
+ ...(this.params.fixedNetLabelPlacements ?? []),
349
+ ]
350
+ return this.buildCandidatesForLabel(label)
351
+ .filter(
352
+ (candidate) =>
353
+ distance(candidate) <= maxDistance &&
354
+ this.checkCandidate(candidate, label.globalConnNetId, obstacles) ===
355
+ "ok",
356
+ )
357
+ .sort((a, b) => distance(a) - distance(b))
358
+ .map((candidate) => ({
359
+ ...label,
360
+ orientation: candidate.orientation,
361
+ anchorPoint: candidate.anchor,
362
+ center: candidate.center,
363
+ width: candidate.width,
364
+ height: candidate.height,
365
+ }))
366
+ }
367
+
302
368
  private clearActiveSearch() {
303
369
  this.currentCollision = null
304
370
  this.currentLabelToMove = null
@@ -316,7 +382,9 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
316
382
  return
317
383
  }
318
384
  this.currentCollision = pair
319
- this.labelsToTry = [pair[1], pair[0]]
385
+ this.labelsToTry = [pair[1], pair[0]].filter(
386
+ (label) => !(this.params.fixedNetLabelPlacements ?? []).includes(label),
387
+ )
320
388
  this.beginSearchForLabel(this.labelsToTry.shift()!)
321
389
  return
322
390
  }
@@ -342,9 +410,10 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
342
410
  fixedLabel = labelB
343
411
  }
344
412
  const obstacleLabels = [
345
- ...this.outputNetLabelPlacements.filter(
346
- (l) => l !== labelA && l !== labelB,
347
- ),
413
+ ...[
414
+ ...this.outputNetLabelPlacements,
415
+ ...(this.params.fixedNetLabelPlacements ?? []),
416
+ ].filter((l) => l !== labelA && l !== labelB),
348
417
  fixedLabel,
349
418
  ]
350
419
 
@@ -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",
@@ -648,20 +648,18 @@ const pathsIntersect = (firstPath: Point[], secondPath: Point[]) => {
648
648
  }
649
649
 
650
650
  /**
651
- * Keep generated label connectors joined when their host trace moves. Only a
651
+ * Keep same-net branches joined when their host trace moves. Only a
652
652
  * connector endpoint is projected, and only along its existing first/last
653
653
  * segment, so the label and the rest of its route stay fixed.
654
654
  */
655
- const preserveAttachedLabelConnectorJunctions = ({
655
+ const preserveAttachedTraceJunctions = ({
656
656
  originalTrace,
657
657
  candidateTrace,
658
658
  traces,
659
- netLabelConnectorTraceIds,
660
659
  }: {
661
660
  originalTrace: SolvedTracePath
662
661
  candidateTrace: SolvedTracePath
663
662
  traces: SolvedTracePath[]
664
- netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
665
663
  }): SolvedTracePath[] | null => {
666
664
  let outputTraces = traces.map((trace) =>
667
665
  trace.mspPairId === originalTrace.mspPairId ? candidateTrace : trace,
@@ -670,7 +668,6 @@ const preserveAttachedLabelConnectorJunctions = ({
670
668
  for (const connector of traces) {
671
669
  if (
672
670
  connector.mspPairId === originalTrace.mspPairId ||
673
- !netLabelConnectorTraceIds.has(connector.mspPairId) ||
674
671
  connector.globalConnNetId !== originalTrace.globalConnNetId ||
675
672
  !pathsIntersect(originalTrace.tracePath, connector.tracePath) ||
676
673
  pathsIntersect(candidateTrace.tracePath, connector.tracePath)
@@ -686,6 +683,16 @@ const preserveAttachedLabelConnectorJunctions = ({
686
683
  )
687
684
  if (endpointIndex === undefined) return null
688
685
  const originalJunction = connector.tracePath[endpointIndex]!
686
+ // Component pins are fixed; reject a rail move that would detach one.
687
+ if (
688
+ connector.pins.some(
689
+ (pin) =>
690
+ nearlyEqual(pin.x, originalJunction.x) &&
691
+ nearlyEqual(pin.y, originalJunction.y),
692
+ )
693
+ ) {
694
+ return null
695
+ }
689
696
  const movedJunction = getMovedAnchorPointForReroute(
690
697
  originalJunction,
691
698
  originalTrace.tracePath,
@@ -1250,11 +1257,10 @@ export const alignSameNetJunctions = ({
1250
1257
  netLabelPlacements: outputNetLabelPlacements,
1251
1258
  attachedLabelIndexes,
1252
1259
  })
1253
- const candidateTraces = preserveAttachedLabelConnectorJunctions({
1260
+ const candidateTraces = preserveAttachedTraceJunctions({
1254
1261
  originalTrace: branchTrace,
1255
1262
  candidateTrace,
1256
1263
  traces: outputTraces,
1257
- netLabelConnectorTraceIds,
1258
1264
  })
1259
1265
  if (!candidateTraces) continue
1260
1266
  if (
@@ -1272,10 +1278,7 @@ export const alignSameNetJunctions = ({
1272
1278
  }
1273
1279
 
1274
1280
  const movedConnectorsAreClear = candidateTraces.every((trace) => {
1275
- if (
1276
- trace.mspPairId === branchTrace.mspPairId ||
1277
- !netLabelConnectorTraceIds.has(trace.mspPairId)
1278
- ) {
1281
+ if (trace.mspPairId === branchTrace.mspPairId) {
1279
1282
  return true
1280
1283
  }
1281
1284
  const originalConnector = outputTraces.find(
@@ -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
  },
@@ -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.186",
8
+ "version": "0.0.188",
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-20260907T083336Z/bug-report-20260907T083336Z.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-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} />