@tscircuit/schematic-trace-solver 0.0.189 → 0.0.191

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 (36) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +680 -428
  3. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +29 -0
  4. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +7 -0
  5. package/lib/solvers/LongDistancePairSolver/getParallelNetLabelPolicy.ts +98 -0
  6. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +24 -2
  7. package/lib/solvers/MspConnectionPairSolver/getParallelRailPairs.ts +178 -0
  8. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +13 -0
  9. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +4 -9
  10. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +10 -2
  11. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +75 -38
  12. package/package.json +1 -1
  13. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +837 -799
  14. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +904 -866
  15. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +533 -524
  16. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
  17. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +94 -94
  18. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +13 -2
  19. package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.test.ts +16 -2
  21. package/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +19 -58
  23. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +2 -2
  24. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +917 -933
  25. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +18 -18
  26. package/tests/repros/__snapshots__/repro-repeated-power-rail-junctions.snap.svg +255 -0
  27. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +111 -75
  28. package/tests/repros/assets/repro-repeated-power-rail-junctions.input.json +927 -0
  29. package/tests/repros/repro-repeated-power-rail-junctions.test.ts +22 -0
  30. package/tests/repros/repro-trellis-core-decoupling-snake-traces.test.ts +66 -0
  31. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +62 -0
  32. package/tests/solvers/LongDistancePairSolver/__snapshots__/decoupling-mixed-supplies.snap.svg +112 -0
  33. package/tests/solvers/LongDistancePairSolver/__snapshots__/horizontal-rails.snap.svg +79 -0
  34. package/tests/solvers/LongDistancePairSolver/__snapshots__/vertical-rails.snap.svg +86 -0
  35. package/tests/solvers/LongDistancePairSolver/parallel-decoupling-labels.test.ts +505 -0
  36. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-same-net-cycles.test.ts +30 -0
@@ -4,6 +4,8 @@ import type { InputProblem } from "lib/types/InputProblem"
4
4
  import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
5
5
  import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
6
6
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
7
+ import { minimizeTurnsWithFilteredLabels } from "lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels"
8
+ import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
7
9
  import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
8
10
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
9
11
  import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
@@ -274,6 +276,33 @@ export function* getLocalTraceLabelShifts(
274
276
  }
275
277
  }
276
278
  if (blocked) continue
279
+ // A shifted leg can coincide with a redundant bend on another net.
280
+ // Try removing that bend with the existing cleanup, then validate
281
+ // both changed routes together below before yielding the proposal.
282
+ for (const [index, other] of traces.entries()) {
283
+ if (
284
+ other.globalConnNetId === trace.globalConnNetId ||
285
+ simplifyPath(other.tracePath).length <= 4 ||
286
+ !shiftedPaths.some((path) =>
287
+ doesPathCoincideWithTraces(path, [other]),
288
+ )
289
+ )
290
+ continue
291
+ const simplified = minimizeTurnsWithFilteredLabels({
292
+ inputProblem,
293
+ traces: [...traces, ...terminalTraces],
294
+ targetMspConnectionPairId: other.mspPairId,
295
+ allLabelPlacements: [...labels, ...fixedLabels],
296
+ mergedLabelNetIdMap: {},
297
+ paddingBuffer: CLEARANCE,
298
+ })
299
+ if (
300
+ simplifyPath(simplified.tracePath).length <
301
+ simplifyPath(other.tracePath).length &&
302
+ preservesLabelAnchors(labels, [other], [simplified])
303
+ )
304
+ traces[index] = simplified
305
+ }
277
306
  for (const [index, proposed] of traces.entries()) {
278
307
  const original = output.traces[index]!
279
308
  if (proposed === original) continue
@@ -14,6 +14,7 @@ import type {
14
14
  import { doesTraceOverlapWithExistingTraces } from "lib/utils/does-trace-overlap-with-existing-traces"
15
15
  import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
16
16
  import { BaseSolver } from "../BaseSolver/BaseSolver"
17
+ import { getParallelNetLabelPolicy } from "./getParallelNetLabelPolicy"
17
18
  import { getGroundConnectionPolicy } from "../MspConnectionPairSolver/getGroundConnectionPolicy"
18
19
  import { isLabeledPeripheralConnection } from "../MspConnectionPairSolver/isLabeledPeripheralConnection"
19
20
  import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
@@ -180,6 +181,11 @@ export class LongDistancePairSolver extends BaseSolver {
180
181
 
181
182
  const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem)
182
183
  this.netConnMap = netConnMap
184
+ const canRecoverParallelPair = getParallelNetLabelPolicy(
185
+ inputProblem,
186
+ netConnMap,
187
+ primaryConnectedPinIds,
188
+ )
183
189
  const pinMap = new Map<PinId, InputPin & { chipId: string }>()
184
190
  for (const chip of inputProblem.chips) {
185
191
  this.chipMap[chip.chipId] = chip
@@ -226,6 +232,7 @@ export class LongDistancePairSolver extends BaseSolver {
226
232
  const targetPin = pinMap.get(otherPinId)
227
233
  if (!targetPin) return [] // Gracefully handle missing pins
228
234
  if (!canRouteGroundPair(sourcePin.pinId, targetPin.pinId)) return []
235
+ if (!canRecoverParallelPair(sourcePin, targetPin)) return []
229
236
  const isNamedTwoPinConnection = inputProblem.netConnections.some(
230
237
  (connection) =>
231
238
  connection.pinIds.length === 2 &&
@@ -0,0 +1,98 @@
1
+ import { ConnectivityMap } from "connectivity-map"
2
+ import type { InputPin, InputProblem, PinId } from "lib/types/InputProblem"
3
+ import { DEFAULT_MAX_MSP_PAIR_DISTANCE } from "../MspConnectionPairSolver/MspConnectionPairSolver"
4
+ import { getParallelRailPairs } from "../MspConnectionPairSolver/getParallelRailPairs"
5
+
6
+ /** Keep distant parallel rail/ground branches local during trace recovery. */
7
+ export const getParallelNetLabelPolicy = (
8
+ inputProblem: InputProblem,
9
+ netConnMap: ConnectivityMap,
10
+ connectedPinIds: ReadonlySet<PinId>,
11
+ ) => {
12
+ const maxDistance =
13
+ inputProblem.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE
14
+ const { extendedRailPinIds, separatedRailPinIds } = getParallelRailPairs(
15
+ inputProblem,
16
+ netConnMap,
17
+ maxDistance,
18
+ )
19
+ const groundNetIds = new Set<string>()
20
+ const namedPinIds = new Set<PinId>()
21
+ for (const connection of inputProblem.netConnections) {
22
+ const netId = netConnMap.getNetConnectedToId(connection.netId)
23
+ if (!netId) continue
24
+ for (const pinId of connection.pinIds) namedPinIds.add(pinId)
25
+ if (connection.isGround) {
26
+ groundNetIds.add(netId)
27
+ }
28
+ }
29
+
30
+ // Older inputs omit symbol metadata. Identify parallel two-terminal
31
+ // branches by their two named nets, without inferring capacitor types or
32
+ // requiring identical pin positions, sizes, or orientations.
33
+ const banks = new Map<string, PinId[][]>()
34
+ for (const chip of inputProblem.chips) {
35
+ // Preserve branches that use source wires or already participate in
36
+ // local routing. This policy only selects standalone named-net branches.
37
+ if (
38
+ chip.pins.length !== 2 ||
39
+ !chip.pins.every((pin) => namedPinIds.has(pin.pinId)) ||
40
+ chip.pins.some(
41
+ (pin) =>
42
+ connectedPinIds.has(pin.pinId) && !extendedRailPinIds.has(pin.pinId),
43
+ )
44
+ )
45
+ continue
46
+ const [first, second] = chip.pins.map((pin) =>
47
+ netConnMap.getNetConnectedToId(pin.pinId),
48
+ )
49
+ if (
50
+ !first ||
51
+ !second ||
52
+ first === second ||
53
+ groundNetIds.has(first) === groundNetIds.has(second)
54
+ ) {
55
+ continue
56
+ }
57
+ const key = JSON.stringify([
58
+ chip.sectionId ?? null,
59
+ ...[first, second].sort(),
60
+ ])
61
+ const bank = banks.get(key) ?? []
62
+ bank.push(chip.pins.map((pin) => pin.pinId))
63
+ banks.set(key, bank)
64
+ }
65
+ const bankPinIds = new Set(
66
+ [...banks.values()].filter((bank) => bank.length > 1).flat(2),
67
+ )
68
+ // Both shared rails and terminals separated by a large gap stay local.
69
+ // Do not recover wires between them or to distant IC pins.
70
+ for (const pinId of [...extendedRailPinIds, ...separatedRailPinIds]) {
71
+ bankPinIds.add(pinId)
72
+ }
73
+
74
+ // Explicit source wires may be recovered even beyond the local distance.
75
+ // Sharing a net ID alone does not request a physical wire between branches.
76
+ const physicalConnMap = new ConnectivityMap({})
77
+ for (const connection of inputProblem.directConnections) {
78
+ if (connection.netLabelWidth === undefined) {
79
+ physicalConnMap.addConnections([connection.pinIds])
80
+ }
81
+ }
82
+
83
+ return (first: InputPin, second: InputPin): boolean => {
84
+ if (!bankPinIds.has(first.pinId) && !bankPinIds.has(second.pinId))
85
+ return true
86
+ if (
87
+ Math.abs(first.x - second.x) + Math.abs(first.y - second.y) <=
88
+ maxDistance
89
+ ) {
90
+ return true
91
+ }
92
+ const physicalNetId = physicalConnMap.getNetConnectedToId(first.pinId)
93
+ return (
94
+ physicalNetId !== undefined &&
95
+ physicalNetId === physicalConnMap.getNetConnectedToId(second.pinId)
96
+ )
97
+ }
98
+ }
@@ -17,6 +17,7 @@ import { getGroundConnectionPolicy } from "./getGroundConnectionPolicy"
17
17
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
18
18
  import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
19
19
  import { shouldSeparateGroundNetRows } from "./shouldSeparateGroundNetRows"
20
+ import { getParallelRailPairs, getRailPairKey } from "./getParallelRailPairs"
20
21
 
21
22
  export type MspConnectionPairId = string
22
23
  export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
@@ -48,6 +49,7 @@ export class MspConnectionPairSolver extends BaseSolver {
48
49
  userNetIdByPinId: Record<string, string | undefined>
49
50
  directConnectionPinPairKeys: Set<string>
50
51
  private canRouteGroundPair: (firstPinId: PinId, secondPinId: PinId) => boolean
52
+ private parallelRailPairKeys: Set<string>
51
53
 
52
54
  constructor({ inputProblem }: { inputProblem: InputProblem }) {
53
55
  super()
@@ -61,6 +63,11 @@ export class MspConnectionPairSolver extends BaseSolver {
61
63
  getConnectivityMapsFromInputProblem(inputProblem)
62
64
  this.dcConnMap = directConnMap
63
65
  this.globalConnMap = netConnMap
66
+ this.parallelRailPairKeys = getParallelRailPairs(
67
+ inputProblem,
68
+ netConnMap,
69
+ this.maxMspPairDistance,
70
+ ).pairKeys
64
71
 
65
72
  this.pinMap = {}
66
73
  for (const chip of inputProblem.chips) {
@@ -145,6 +152,7 @@ export class MspConnectionPairSolver extends BaseSolver {
145
152
  })
146
153
  if (
147
154
  pairDistance > this.maxMspPairDistance &&
155
+ !this.parallelRailPairKeys.has(getRailPairKey(pin1!, pin2!)) &&
148
156
  !labeledConnectionRouteReason
149
157
  ) {
150
158
  // Too far apart; skip creating an MSP pair for this net
@@ -203,8 +211,9 @@ export class MspConnectionPairSolver extends BaseSolver {
203
211
  (pinId) => this.pinMap[pinId]!,
204
212
  )
205
213
  const msp = getOrthogonalMinimumSpanningTree(directlyConnectedPinObjects, {
206
- maxDistance: this.maxMspPairDistance,
207
214
  forbidEdge: (a, b) =>
215
+ (Math.abs(a.x - b.x) + Math.abs(a.y - b.y) > this.maxMspPairDistance &&
216
+ !this.parallelRailPairKeys.has(getRailPairKey(a.pinId, b.pinId))) ||
208
217
  !this.canRouteGroundPair(a.pinId, b.pinId) ||
209
218
  shouldSeparateGroundNetRows({
210
219
  netConnection,
@@ -222,7 +231,20 @@ export class MspConnectionPairSolver extends BaseSolver {
222
231
  }),
223
232
  })
224
233
 
225
- for (const [pin1, pin2] of msp) {
234
+ for (let [pin1, pin2] of msp) {
235
+ // Extended rails replace long-distance recovery, which visits pins in
236
+ // connectivity order. Preserve that order so an unchanged rail keeps
237
+ // its label at the same endpoint.
238
+ if (
239
+ this.parallelRailPairKeys.has(getRailPairKey(pin1!, pin2!)) &&
240
+ Math.abs(this.pinMap[pin1!]!.x - this.pinMap[pin2!]!.x) +
241
+ Math.abs(this.pinMap[pin1!]!.y - this.pinMap[pin2!]!.y) >
242
+ this.maxMspPairDistance &&
243
+ directlyConnectedPins.indexOf(pin1!) >
244
+ directlyConnectedPins.indexOf(pin2!)
245
+ ) {
246
+ ;[pin1, pin2] = [pin2, pin1]
247
+ }
226
248
  const p1Obj = this.pinMap[pin1!]!
227
249
  const p2Obj = this.pinMap[pin2!]!
228
250
  if (
@@ -0,0 +1,178 @@
1
+ import type { ConnectivityMap } from "connectivity-map"
2
+ import type { InputPin, InputProblem, PinId } from "lib/types/InputProblem"
3
+ import { getPinDirection } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
4
+
5
+ const EPS = 1e-6
6
+ // Component scale supplies a minimum allowance, including two-capacitor rows
7
+ // with no neighboring pitch to compare against.
8
+ const MAX_RAIL_SPACING_IN_TERMINAL_SPANS = 4
9
+ const MAX_RAIL_SPACING_MULTIPLIER = 2
10
+
11
+ export const getRailPairKey = (first: PinId, second: PinId) =>
12
+ JSON.stringify([first, second].sort())
13
+
14
+ /** Adjacent, outward-facing terminals can share a straight decoupling rail. */
15
+ export const getParallelRailPairs = (
16
+ inputProblem: InputProblem,
17
+ netConnMap: ConnectivityMap,
18
+ maxMspPairDistance: number,
19
+ ) => {
20
+ const groundNetIds = new Set<string>()
21
+ const namedPinIds = new Set<PinId>()
22
+ const wiredPinIds = new Set<PinId>()
23
+ for (const connection of inputProblem.netConnections) {
24
+ for (const pinId of connection.pinIds) namedPinIds.add(pinId)
25
+ const netId = netConnMap.getNetConnectedToId(connection.netId)
26
+ if (netId && connection.isGround) {
27
+ groundNetIds.add(netId)
28
+ }
29
+ }
30
+ for (const connection of inputProblem.directConnections) {
31
+ const pinIds =
32
+ connection.netLabelWidth === undefined ? wiredPinIds : namedPinIds
33
+ for (const pinId of connection.pinIds) pinIds.add(pinId)
34
+ }
35
+
36
+ const rows: Array<{
37
+ sectionId?: string
38
+ direction: string
39
+ coordinate: number
40
+ axis: "x" | "y"
41
+ pins: Array<InputPin & { chipId: string; oppositeCoordinate: number }>
42
+ }> = []
43
+ for (const chip of inputProblem.chips) {
44
+ if (
45
+ chip.pins.length !== 2 ||
46
+ (chip.symbolName && !chip.symbolName.startsWith("capacitor")) ||
47
+ !chip.pins.every((pin) => namedPinIds.has(pin.pinId)) ||
48
+ chip.pins.some((pin) => wiredPinIds.has(pin.pinId))
49
+ )
50
+ continue
51
+ const nets = chip.pins.map((pin) =>
52
+ netConnMap.getNetConnectedToId(pin.pinId),
53
+ )
54
+ if (
55
+ !nets[0] ||
56
+ !nets[1] ||
57
+ nets[0] === nets[1] ||
58
+ groundNetIds.has(nets[0]) === groundNetIds.has(nets[1])
59
+ )
60
+ continue
61
+ const directions = chip.pins.map(
62
+ (pin) => pin._facingDirection ?? getPinDirection(pin, chip),
63
+ )
64
+ if (
65
+ directions[0]![0] !== directions[1]![0] ||
66
+ directions[0] === directions[1]
67
+ )
68
+ continue
69
+ const axis = directions[0]![0] as "x" | "y"
70
+ const along = axis === "x" ? "y" : "x"
71
+ if (Math.abs(chip.pins[0]![along] - chip.pins[1]![along]) > EPS) continue
72
+
73
+ for (const [index, pin] of chip.pins.entries()) {
74
+ const direction = directions[index]!
75
+ let row = rows.find(
76
+ (candidate) =>
77
+ candidate.sectionId === chip.sectionId &&
78
+ candidate.direction === direction &&
79
+ Math.abs(candidate.coordinate - pin[axis]) <= EPS,
80
+ )
81
+ if (!row) {
82
+ row = {
83
+ sectionId: chip.sectionId,
84
+ direction,
85
+ coordinate: pin[axis],
86
+ axis,
87
+ pins: [],
88
+ }
89
+ rows.push(row)
90
+ }
91
+ row.pins.push({
92
+ ...pin,
93
+ chipId: chip.chipId,
94
+ oppositeCoordinate: chip.pins[1 - index]![axis],
95
+ })
96
+ }
97
+ }
98
+
99
+ const pairKeys = new Set<string>()
100
+ const extendedRailPinIds = new Set<PinId>()
101
+ const separatedRailPinIds = new Set<PinId>()
102
+ for (const row of rows) {
103
+ const along = row.axis === "x" ? "y" : "x"
104
+ row.pins.sort(
105
+ (a, b) => a[along] - b[along] || a.pinId.localeCompare(b.pinId),
106
+ )
107
+ for (let index = 1; index < row.pins.length; index++) {
108
+ const first = row.pins[index - 1]!
109
+ const second = row.pins[index]!
110
+ const spacing = second[along] - first[along]
111
+ // A regular row may be widely spaced. Split at gaps that are much
112
+ // larger than the neighboring pitch, rather than imposing a fixed cap.
113
+ const neighboringSpacings = [
114
+ first[along] - (row.pins[index - 2]?.[along] ?? first[along]),
115
+ (row.pins[index + 1]?.[along] ?? second[along]) - second[along],
116
+ ].filter((gap) => gap > EPS)
117
+ const maxRailSpacing = Math.max(
118
+ MAX_RAIL_SPACING_MULTIPLIER * maxMspPairDistance,
119
+ MAX_RAIL_SPACING_MULTIPLIER *
120
+ (neighboringSpacings.length ? Math.min(...neighboringSpacings) : 0),
121
+ MAX_RAIL_SPACING_IN_TERMINAL_SPANS *
122
+ Math.max(
123
+ Math.abs(first[row.axis] - first.oppositeCoordinate),
124
+ Math.abs(second[row.axis] - second.oppositeCoordinate),
125
+ ),
126
+ )
127
+ // A different supply in between ends the supply rail; GND can continue.
128
+ if (
129
+ spacing <= EPS ||
130
+ netConnMap.getNetConnectedToId(first.pinId) !==
131
+ netConnMap.getNetConnectedToId(second.pinId)
132
+ )
133
+ continue
134
+ if (spacing > maxRailSpacing + EPS) {
135
+ // Recovery must retain this separation even when each capacitor has
136
+ // a different supply and does not belong to a same-net branch bank.
137
+ separatedRailPinIds.add(first.pinId)
138
+ separatedRailPinIds.add(second.pinId)
139
+ continue
140
+ }
141
+ // A component between the terminals breaks the row. Leave obstacle
142
+ // detours to ordinary local routing instead of extending this exception.
143
+ const minAcross = Math.min(
144
+ first[row.axis],
145
+ second[row.axis],
146
+ first.oppositeCoordinate,
147
+ second.oppositeCoordinate,
148
+ )
149
+ const maxAcross = Math.max(
150
+ first[row.axis],
151
+ second[row.axis],
152
+ first.oppositeCoordinate,
153
+ second.oppositeCoordinate,
154
+ )
155
+ if (
156
+ inputProblem.chips.some((chip) => {
157
+ if (chip.chipId === first.chipId || chip.chipId === second.chipId)
158
+ return false
159
+ const acrossSize = row.axis === "x" ? chip.width : chip.height
160
+ const alongSize = along === "x" ? chip.width : chip.height
161
+ return (
162
+ chip.center[row.axis] + acrossSize / 2 >= minAcross - EPS &&
163
+ chip.center[row.axis] - acrossSize / 2 <= maxAcross + EPS &&
164
+ chip.center[along] + alongSize / 2 > first[along] + EPS &&
165
+ chip.center[along] - alongSize / 2 < second[along] - EPS
166
+ )
167
+ })
168
+ )
169
+ continue
170
+ pairKeys.add(getRailPairKey(first.pinId, second.pinId))
171
+ if (spacing > maxMspPairDistance) {
172
+ extendedRailPinIds.add(first.pinId)
173
+ extendedRailPinIds.add(second.pinId)
174
+ }
175
+ }
176
+ }
177
+ return { pairKeys, extendedRailPinIds, separatedRailPinIds }
178
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
+ import { getParallelNetLabelPolicy } from "lib/solvers/LongDistancePairSolver/getParallelNetLabelPolicy"
4
5
  import { doesPairCrossRestrictedCenterLines } from "lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines"
5
6
  import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
6
7
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
@@ -84,6 +85,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
84
85
  private readonly recoveredTraceIds = new Set<string>()
85
86
  private chipMap: Record<ChipId, InputChip>
86
87
  private pinMap: Map<PinId, TraceRecoveryPin>
88
+ private canRecoverParallelPair: ReturnType<typeof getParallelNetLabelPolicy>
87
89
  private queuedCandidates: CandidatePair[]
88
90
  private currentCandidate: CandidatePair | null = null
89
91
  declare activeSubSolver: SchematicTraceSingleLineSolver2 | null
@@ -99,6 +101,15 @@ export class NetLabelToTraceSolver extends BaseSolver {
99
101
  )
100
102
  this.chipMap = chipMap
101
103
  this.pinMap = pinMap
104
+ this.canRecoverParallelPair = getParallelNetLabelPolicy(
105
+ this.inputProblem,
106
+ getConnectivityMapsFromInputProblem(this.inputProblem).netConnMap,
107
+ new Set(
108
+ input.traces
109
+ .filter((trace) => trace.pinIds.length > 1)
110
+ .flatMap((trace) => trace.pinIds),
111
+ ),
112
+ )
102
113
 
103
114
  this.queuedCandidates = this.buildCandidatePairs()
104
115
  this.stats.candidateCount = this.queuedCandidates.length
@@ -180,6 +191,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
180
191
  const firstPin = this.pinMap.get(firstLabel.pinIds[0]!)
181
192
  const secondPin = this.pinMap.get(secondLabel.pinIds[0]!)
182
193
  if (!firstPin || !secondPin) continue
194
+ if (!this.canRecoverParallelPair(firstPin, secondPin)) continue
183
195
  const perpendicularOffset = getPerpendicularOffset(
184
196
  firstPin,
185
197
  secondPin,
@@ -354,6 +366,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
354
366
  const firstPin = this.pinMap.get(firstPinId)
355
367
  const secondPin = this.pinMap.get(secondPinId)
356
368
  if (!firstPin || !secondPin) continue
369
+ if (!this.canRecoverParallelPair(firstPin, secondPin)) continue
357
370
  if (isDirectConnection && firstPin.chipId === secondPin.chipId)
358
371
  continue
359
372
  const perpendicularOffset = getPerpendicularOffset(
@@ -7,7 +7,6 @@ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/
7
7
  import type { InputProblem } from "lib/types/InputProblem"
8
8
  import { getColorFromString } from "lib/utils/getColorFromString"
9
9
  import { alignSameNetJunctions } from "./alignSameNetJunctions"
10
- import { collapseSameNetCycles } from "./collapseSameNetCycles"
11
10
  import { placeGroundRailLabelsAtOuterEnd } from "./placeGroundRailLabelsAtOuterEnd"
12
11
 
13
12
  interface SameNetJunctionAlignmentSolverInput {
@@ -32,18 +31,14 @@ export class SameNetJunctionAlignmentSolver extends BaseSolver {
32
31
 
33
32
  override _step() {
34
33
  const alignment = alignSameNetJunctions(this.input)
35
- const cycleCollapse = collapseSameNetCycles({
36
- traces: alignment.traces,
37
- netLabelPlacements: alignment.netLabelPlacements,
38
- })
39
- this.outputTraces = cycleCollapse.traces
34
+ this.outputTraces = alignment.traces
40
35
  this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
41
36
  inputProblem: this.input.inputProblem,
42
- traces: cycleCollapse.traces,
43
- netLabelPlacements: cycleCollapse.netLabelPlacements,
37
+ traces: alignment.traces,
38
+ netLabelPlacements: alignment.netLabelPlacements,
44
39
  })
45
40
  this.stats.alignedJunctionCount = alignment.alignedJunctionCount
46
- this.stats.collapsedCycleCount = cycleCollapse.collapsedCycleCount
41
+ this.stats.collapsedCycleCount = alignment.collapsedCycleCount
47
42
  this.solved = true
48
43
  }
49
44
 
@@ -34,6 +34,7 @@ import {
34
34
  isCoordinateOnPinFacingSide,
35
35
  } from "./findNearestSharedPinExitRail"
36
36
  import { collapseRedundantSharedEndpointStubs } from "./collapseRedundantSharedEndpointStubs"
37
+ import { collapseSameNetCycles } from "./collapseSameNetCycles"
37
38
  import { getSharedPin } from "./getSharedPin"
38
39
 
39
40
  interface AlignSameNetJunctionsInput {
@@ -1314,17 +1315,24 @@ export const alignSameNetJunctions = ({
1314
1315
  }
1315
1316
  }
1316
1317
 
1317
- const collapsedSharedEndpointStubs = collapseRedundantSharedEndpointStubs({
1318
+ // Shared-stub collapse can remove the common pin from one trace path, so
1319
+ // collapse cycles while both paths still expose their shared endpoint.
1320
+ const cycleCollapse = collapseSameNetCycles({
1318
1321
  traces: outputTraces,
1319
1322
  netLabelPlacements: outputNetLabelPlacements,
1323
+ })
1324
+ outputTraces = collapseRedundantSharedEndpointStubs({
1325
+ traces: cycleCollapse.traces,
1326
+ netLabelPlacements: cycleCollapse.netLabelPlacements,
1320
1327
  netLabelConnectorTraceIds,
1321
1328
  multiPinNetPinIds: getMultiPinNetPinIds(inputProblem),
1322
1329
  })
1323
- outputTraces = collapsedSharedEndpointStubs
1330
+ outputNetLabelPlacements = cycleCollapse.netLabelPlacements
1324
1331
 
1325
1332
  return {
1326
1333
  traces: outputTraces,
1327
1334
  netLabelPlacements: outputNetLabelPlacements,
1328
1335
  alignedJunctionCount,
1336
+ collapsedCycleCount: cycleCollapse.collapsedCycleCount,
1329
1337
  }
1330
1338
  }
@@ -11,6 +11,7 @@ import {
11
11
  pointsEqual,
12
12
  } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
13
13
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
14
+ import { findPerpendicularPathCrossings } from "lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles"
14
15
  import { getSharedPin } from "./getSharedPin"
15
16
  import {
16
17
  pathEntersAnyNetLabel,
@@ -100,6 +101,25 @@ const getPerpendicularPathCrossings = (
100
101
  return crossings
101
102
  }
102
103
 
104
+ const buildCollapsedSelfCyclePath = (
105
+ path: Point[],
106
+ firstSegmentIndex: number,
107
+ secondSegmentIndex: number,
108
+ ) => {
109
+ const intersectionPoint = getSegmentIntersection(
110
+ path[firstSegmentIndex]!,
111
+ path[firstSegmentIndex + 1]!,
112
+ path[secondSegmentIndex]!,
113
+ path[secondSegmentIndex + 1]!,
114
+ )
115
+ if (!intersectionPoint) return path
116
+ return simplifyPath([
117
+ ...path.slice(0, firstSegmentIndex + 1),
118
+ intersectionPoint,
119
+ ...path.slice(secondSegmentIndex + 1),
120
+ ])
121
+ }
122
+
103
123
  const buildCollapsedCyclePath = ({
104
124
  targetTrace,
105
125
  targetPath,
@@ -209,6 +229,60 @@ const getBestCycleCollapseCandidate = ({
209
229
  const baselineTargetVisibleLength = getVisibleTraceLength([targetTrace])
210
230
  let bestCandidate: CycleCollapseCandidate | null = null
211
231
 
232
+ const considerTracePath = (tracePath: Point[]) => {
233
+ const candidateTrace = { ...targetTrace, tracePath }
234
+ const candidateTargetVisibleLength = getVisibleTraceLength([candidateTrace])
235
+ if (
236
+ candidateTargetVisibleLength >
237
+ baselineTargetVisibleLength + TRACE_LENGTH_EPSILON
238
+ ) {
239
+ return
240
+ }
241
+ const candidateNetLabelPlacements = getNetLabelPlacementsForCycleCollapse({
242
+ targetTrace,
243
+ tracePath,
244
+ netLabelPlacements,
245
+ })
246
+ if (!candidateNetLabelPlacements) return
247
+
248
+ const candidateNetTraces = sameNetTraces.map((trace) => {
249
+ if (trace.mspPairId === targetTrace.mspPairId) return candidateTrace
250
+ return trace
251
+ })
252
+ const netVisibleLength = getVisibleTraceLength(candidateNetTraces)
253
+ if (netVisibleLength >= baselineNetVisibleLength - TRACE_LENGTH_EPSILON) {
254
+ return
255
+ }
256
+ const candidate: CycleCollapseCandidate = {
257
+ tracePath,
258
+ netLabelPlacements: candidateNetLabelPlacements,
259
+ netVisibleLength,
260
+ netVisibleSegmentCount: getVisibleTraceSegmentCount(candidateNetTraces),
261
+ }
262
+ if (candidateIsBetter(candidate, bestCandidate)) {
263
+ bestCandidate = candidate
264
+ }
265
+ }
266
+
267
+ // Comparing a path with itself reports each crossing in both directions.
268
+ const selfCrossings = findPerpendicularPathCrossings(
269
+ targetTrace.tracePath,
270
+ targetTrace.tracePath,
271
+ { includeTerminalSegments: true },
272
+ ).filter(
273
+ ({ pathSegmentIndex, otherPathSegmentIndex }) =>
274
+ pathSegmentIndex < otherPathSegmentIndex,
275
+ )
276
+ for (const crossing of selfCrossings) {
277
+ considerTracePath(
278
+ buildCollapsedSelfCyclePath(
279
+ targetTrace.tracePath,
280
+ crossing.pathSegmentIndex,
281
+ crossing.otherPathSegmentIndex,
282
+ ),
283
+ )
284
+ }
285
+
212
286
  for (const donorTrace of sameNetTraces) {
213
287
  if (donorTrace.mspPairId === targetTrace.mspPairId) continue
214
288
  const sharedPin = getSharedPin({
@@ -228,44 +302,7 @@ const getBestCycleCollapseCandidate = ({
228
302
  donorPath,
229
303
  crossing,
230
304
  })
231
- const candidateTrace = { ...targetTrace, tracePath }
232
- const candidateTargetVisibleLength = getVisibleTraceLength([
233
- candidateTrace,
234
- ])
235
- if (
236
- candidateTargetVisibleLength >
237
- baselineTargetVisibleLength + TRACE_LENGTH_EPSILON
238
- ) {
239
- continue
240
- }
241
- const candidateNetLabelPlacements = getNetLabelPlacementsForCycleCollapse(
242
- {
243
- targetTrace,
244
- tracePath,
245
- netLabelPlacements,
246
- },
247
- )
248
- if (!candidateNetLabelPlacements) continue
249
-
250
- const candidateNetTraces = sameNetTraces.map((trace) => {
251
- if (trace.mspPairId === targetTrace.mspPairId) return candidateTrace
252
- return trace
253
- })
254
- const netVisibleLength = getVisibleTraceLength(candidateNetTraces)
255
- if (netVisibleLength >= baselineNetVisibleLength - TRACE_LENGTH_EPSILON) {
256
- continue
257
- }
258
- const netVisibleSegmentCount =
259
- getVisibleTraceSegmentCount(candidateNetTraces)
260
- const candidate = {
261
- tracePath,
262
- netLabelPlacements: candidateNetLabelPlacements,
263
- netVisibleLength,
264
- netVisibleSegmentCount,
265
- }
266
- if (candidateIsBetter(candidate, bestCandidate)) {
267
- bestCandidate = candidate
268
- }
305
+ considerTracePath(tracePath)
269
306
  }
270
307
  }
271
308
 
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.189",
8
+ "version": "0.0.191",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",