@tscircuit/schematic-trace-solver 0.0.182 → 0.0.184

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 (112) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +542 -132
  3. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +30 -12
  4. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseRedundantSharedEndpointStubs.ts +267 -0
  5. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +1 -1
  6. package/lib/solvers/SameNetJunctionAlignmentSolver/getSharedPin.ts +13 -0
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +45 -2
  8. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection.ts +58 -10
  9. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +166 -57
  10. package/lib/solvers/TraceCleanupSolver/hasCollisionsWithLabels.ts +108 -3
  11. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +62 -15
  12. package/lib/solvers/TraceCleanupSolver/turnMinimization.ts +5 -0
  13. package/package.json +1 -1
  14. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +2 -2
  15. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +13 -20
  16. package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +2 -2
  17. package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.test.ts +10 -0
  18. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +2 -2
  19. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +2 -2
  21. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +2 -2
  23. package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +2 -2
  24. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +2 -2
  25. package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +2 -2
  26. package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +2 -2
  27. package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +2 -2
  28. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  29. package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +2 -2
  30. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  31. package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.test.ts +2 -0
  32. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  33. package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.test.ts +1 -1
  34. package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +2 -2
  35. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
  36. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +2 -2
  37. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +2 -2
  38. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +2 -2
  39. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +2 -2
  40. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
  41. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +2 -2
  42. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +2 -2
  43. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +2 -2
  44. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +2 -2
  45. package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +2 -2
  46. package/tests/bug-reports/bug-report-20260901T134241Z/__snapshots__/bug-report-20260901T134241Z.snap.svg +2 -2
  47. package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +2 -2
  48. package/tests/examples/__snapshots__/example02.snap.svg +2 -2
  49. package/tests/examples/__snapshots__/example13.snap.svg +2 -2
  50. package/tests/examples/__snapshots__/example14.snap.svg +2 -2
  51. package/tests/examples/__snapshots__/example15.snap.svg +2 -2
  52. package/tests/examples/__snapshots__/example18.snap.svg +2 -2
  53. package/tests/examples/__snapshots__/example19.snap.svg +2 -2
  54. package/tests/examples/__snapshots__/example21.snap.svg +2 -2
  55. package/tests/examples/__snapshots__/example27.snap.svg +2 -2
  56. package/tests/examples/__snapshots__/example29.snap.svg +2 -2
  57. package/tests/examples/__snapshots__/example42.snap.svg +2 -2
  58. package/tests/examples/__snapshots__/example45.snap.svg +2 -2
  59. package/tests/examples/__snapshots__/example46.snap.svg +2 -2
  60. package/tests/examples/__snapshots__/example47.snap.svg +2 -2
  61. package/tests/examples/__snapshots__/example51.snap.svg +2 -2
  62. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  63. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  64. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +13 -13
  65. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +2 -2
  66. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +2 -2
  67. package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +2 -2
  68. package/tests/repros/__snapshots__/repro-bc107a-common-emitter-routing.snap.svg +2 -2
  69. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +2 -2
  70. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +2 -2
  71. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
  72. package/tests/repros/__snapshots__/repro-board-8090-power-section.snap.svg +2 -2
  73. package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +2 -2
  74. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +2 -2
  75. package/tests/repros/__snapshots__/repro-bq40z60-current-sense.snap.svg +2 -2
  76. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +2 -2
  77. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +2 -2
  78. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +508 -0
  79. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +2 -2
  80. package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +2 -2
  81. package/tests/repros/__snapshots__/repro-hdc3020-ground-label-alignment.snap.svg +2 -2
  82. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +2 -2
  83. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +2 -2
  84. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +2 -2
  85. package/tests/repros/__snapshots__/repro-mouse-switch-ground-unnecessary-jog.snap.svg +2 -2
  86. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +2 -2
  87. package/tests/repros/__snapshots__/repro-pga300-c6-trace-alignment.snap.svg +2 -2
  88. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +2 -2
  89. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +26 -26
  90. package/tests/repros/__snapshots__/repro-power-group-rail-labels.snap.svg +2 -2
  91. package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +2 -2
  92. package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +2 -2
  93. package/tests/repros/__snapshots__/repro-rf1-unnecessary-dogleg.snap.svg +5 -5
  94. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +2 -2
  95. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +2 -2
  96. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +2 -2
  97. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +2 -2
  98. package/tests/repros/__snapshots__/repro-tida-01389-hbridge.snap.svg +2 -2
  99. package/tests/repros/__snapshots__/repro-tida00553-j4-cell-divider.snap.svg +2 -2
  100. package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +2 -2
  101. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +2 -2
  102. package/tests/repros/__snapshots__/repro-tmp1075-ground-label-overlap.snap.svg +2 -2
  103. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +2 -2
  104. package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +2 -2
  105. package/tests/repros/__snapshots__/repro-type-c-vbus-led-ground-label.snap.svg +2 -2
  106. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +2 -2
  107. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +2 -2
  108. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +2 -2
  109. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  110. package/tests/repros/assets/repro-esp12f-status-section.input.json +1394 -0
  111. package/tests/repros/repro-esp12f-section.test.ts +12 -0
  112. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-redundant-shared-endpoint-stubs.test.ts +101 -0
@@ -2,6 +2,7 @@ import type { Point } from "@tscircuit/math-utils"
2
2
  import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
3
3
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
4
  import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
+ import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
5
6
  import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
6
7
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
8
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
@@ -20,7 +21,7 @@ import {
20
21
  isVertical,
21
22
  nearlyEqual,
22
23
  } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
23
- import type { InputPin, InputProblem } from "lib/types/InputProblem"
24
+ import type { InputPin, InputProblem, PinId } from "lib/types/InputProblem"
24
25
  import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
25
26
  import {
26
27
  pathEntersAnyNetLabel,
@@ -31,6 +32,8 @@ import {
31
32
  getPinFacingAxis,
32
33
  isCoordinateOnPinFacingSide,
33
34
  } from "./findNearestSharedPinExitRail"
35
+ import { collapseRedundantSharedEndpointStubs } from "./collapseRedundantSharedEndpointStubs"
36
+ import { getSharedPin } from "./getSharedPin"
34
37
 
35
38
  interface AlignSameNetJunctionsInput {
36
39
  inputProblem: InputProblem
@@ -57,23 +60,30 @@ const MAX_SHARED_PIN_RAIL_OFFSET = 0.05
57
60
  const MAX_SHARED_ENDPOINT_RAIL_OFFSET = 0.8
58
61
  const MIN_RETURN_STEM_LENGTH = 0.05
59
62
 
63
+ const getMultiPinNetPinIds = (inputProblem: InputProblem) => {
64
+ const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem)
65
+ const inputPinIds = new Set<PinId>(
66
+ inputProblem.chips.flatMap((chip) => chip.pins.map((pin) => pin.pinId)),
67
+ )
68
+ return new Set(
69
+ [...inputPinIds].filter((pinId) => {
70
+ const globalNetId = netConnMap.getNetConnectedToId(pinId)
71
+ if (!globalNetId) return false
72
+ return (
73
+ netConnMap
74
+ .getIdsConnectedToNet(globalNetId)
75
+ .filter((connectedId) => inputPinIds.has(connectedId)).length > 2
76
+ )
77
+ }),
78
+ )
79
+ }
80
+
60
81
  const getSegmentAxis = (start: Point, end: Point) => {
61
82
  if (isHorizontal(start, end)) return "x" as const
62
83
  if (isVertical(start, end)) return "y" as const
63
84
  return null
64
85
  }
65
86
 
66
- export const getSharedPin = ({
67
- donorTrace,
68
- branchTrace,
69
- }: {
70
- donorTrace: SolvedTracePath
71
- branchTrace: SolvedTracePath
72
- }): (InputPin & { chipId: string }) | null => {
73
- const branchPinIds = new Set(branchTrace.pins.map((pin) => pin.pinId))
74
- return donorTrace.pins.find((pin) => branchPinIds.has(pin.pinId)) ?? null
75
- }
76
-
77
87
  const getOtherPin = ({
78
88
  trace,
79
89
  sharedPin,
@@ -1301,6 +1311,14 @@ export const alignSameNetJunctions = ({
1301
1311
  }
1302
1312
  }
1303
1313
 
1314
+ const collapsedSharedEndpointStubs = collapseRedundantSharedEndpointStubs({
1315
+ traces: outputTraces,
1316
+ netLabelPlacements: outputNetLabelPlacements,
1317
+ netLabelConnectorTraceIds,
1318
+ multiPinNetPinIds: getMultiPinNetPinIds(inputProblem),
1319
+ })
1320
+ outputTraces = collapsedSharedEndpointStubs
1321
+
1304
1322
  return {
1305
1323
  traces: outputTraces,
1306
1324
  netLabelPlacements: outputNetLabelPlacements,
@@ -0,0 +1,267 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { PinId } from "lib/types/InputProblem"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import {
7
+ nearlyEqual,
8
+ pointsEqual,
9
+ } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
10
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
11
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
12
+ import { getSharedPin } from "./getSharedPin"
13
+
14
+ const getSegmentAxis = (start: Point, end: Point) => {
15
+ if (nearlyEqual(start.x, end.x)) return "y" as const
16
+ if (nearlyEqual(start.y, end.y)) return "x" as const
17
+ return null
18
+ }
19
+
20
+ const getPathFromSharedPin = ({
21
+ trace,
22
+ sharedPinId,
23
+ }: {
24
+ trace: SolvedTracePath
25
+ sharedPinId: PinId
26
+ }) => {
27
+ const tracePath = simplifyPath(trace.tracePath)
28
+ if (trace.pins[0]?.pinId === sharedPinId) return tracePath
29
+ return tracePath.reverse()
30
+ }
31
+
32
+ const traceCanLoseSharedPrefix = ({
33
+ trace,
34
+ sharedPrefix,
35
+ netLabelPlacements,
36
+ }: {
37
+ trace: SolvedTracePath
38
+ sharedPrefix: Point[]
39
+ netLabelPlacements: NetLabelPlacement[]
40
+ }) => {
41
+ return !netLabelPlacements.some(
42
+ (label) =>
43
+ label.mspConnectionPairIds.includes(trace.mspPairId) &&
44
+ tracePathContainsPoint(sharedPrefix, label.anchorPoint),
45
+ )
46
+ }
47
+
48
+ const getTraceWithoutSharedPrefix = ({
49
+ trace,
50
+ pathFromSharedPin,
51
+ sharedPinId,
52
+ branchPoint,
53
+ }: {
54
+ trace: SolvedTracePath
55
+ pathFromSharedPin: Point[]
56
+ sharedPinId: PinId
57
+ branchPoint: Point
58
+ }) => {
59
+ const branchSegmentIndex = pathFromSharedPin.findIndex((point, index) => {
60
+ const nextPoint = pathFromSharedPin[index + 1]
61
+ if (!nextPoint) return false
62
+ return tracePathContainsPoint([point, nextPoint], branchPoint)
63
+ })
64
+ if (branchSegmentIndex < 0) return null
65
+ const pathFromJunction = simplifyPath([
66
+ branchPoint,
67
+ ...pathFromSharedPin.slice(branchSegmentIndex + 1),
68
+ ])
69
+ let tracePath = pathFromJunction
70
+ if (trace.pins[0]?.pinId !== sharedPinId) {
71
+ tracePath = pathFromJunction.reverse()
72
+ }
73
+ return { ...trace, tracePath }
74
+ }
75
+
76
+ export const collapseRedundantSharedEndpointStubs = ({
77
+ traces,
78
+ netLabelPlacements,
79
+ netLabelConnectorTraceIds,
80
+ multiPinNetPinIds,
81
+ }: {
82
+ traces: SolvedTracePath[]
83
+ netLabelPlacements: NetLabelPlacement[]
84
+ netLabelConnectorTraceIds: ReadonlySet<MspConnectionPairId>
85
+ multiPinNetPinIds: ReadonlySet<PinId>
86
+ }) => {
87
+ const outputTraces = [...traces]
88
+
89
+ for (let firstIndex = 0; firstIndex < outputTraces.length; firstIndex++) {
90
+ const firstTrace = outputTraces[firstIndex]!
91
+ if (netLabelConnectorTraceIds.has(firstTrace.mspPairId)) continue
92
+ for (
93
+ let secondIndex = firstIndex + 1;
94
+ secondIndex < outputTraces.length;
95
+ secondIndex++
96
+ ) {
97
+ const secondTrace = outputTraces[secondIndex]!
98
+ if (
99
+ netLabelConnectorTraceIds.has(secondTrace.mspPairId) ||
100
+ firstTrace.globalConnNetId !== secondTrace.globalConnNetId
101
+ ) {
102
+ continue
103
+ }
104
+ const sharedPin = getSharedPin({
105
+ donorTrace: firstTrace,
106
+ branchTrace: secondTrace,
107
+ })
108
+ if (!sharedPin || !multiPinNetPinIds.has(sharedPin.pinId)) continue
109
+ const firstPath = getPathFromSharedPin({
110
+ trace: firstTrace,
111
+ sharedPinId: sharedPin.pinId,
112
+ })
113
+ const secondPath = getPathFromSharedPin({
114
+ trace: secondTrace,
115
+ sharedPinId: sharedPin.pinId,
116
+ })
117
+ if (firstPath.length < 2 || secondPath.length < 2) continue
118
+
119
+ const departureAxis = getSegmentAxis(firstPath[0]!, firstPath[1]!)
120
+ const secondDepartureAxis = getSegmentAxis(secondPath[0]!, secondPath[1]!)
121
+ if (!departureAxis || departureAxis !== secondDepartureAxis) continue
122
+
123
+ const firstDepartureDirection =
124
+ firstPath[1]![departureAxis] - firstPath[0]![departureAxis]
125
+ const secondDepartureDirection =
126
+ secondPath[1]![departureAxis] - secondPath[0]![departureAxis]
127
+ if (
128
+ !pointsEqual(firstPath[1]!, secondPath[1]!) &&
129
+ Math.sign(firstDepartureDirection) ===
130
+ Math.sign(secondDepartureDirection)
131
+ ) {
132
+ let traceIndexToCollapse = secondIndex
133
+ let traceToCollapse = secondTrace
134
+ let pathToCollapse = secondPath
135
+ if (
136
+ Math.abs(firstDepartureDirection) < Math.abs(secondDepartureDirection)
137
+ ) {
138
+ traceIndexToCollapse = firstIndex
139
+ traceToCollapse = firstTrace
140
+ pathToCollapse = firstPath
141
+ }
142
+ const branchPoint = pathToCollapse[1]!
143
+ if (
144
+ !traceCanLoseSharedPrefix({
145
+ trace: traceToCollapse,
146
+ sharedPrefix: [pathToCollapse[0]!, branchPoint],
147
+ netLabelPlacements,
148
+ })
149
+ ) {
150
+ if (traceIndexToCollapse === firstIndex) {
151
+ traceIndexToCollapse = secondIndex
152
+ traceToCollapse = secondTrace
153
+ pathToCollapse = secondPath
154
+ } else {
155
+ traceIndexToCollapse = firstIndex
156
+ traceToCollapse = firstTrace
157
+ pathToCollapse = firstPath
158
+ }
159
+ }
160
+ if (
161
+ !traceCanLoseSharedPrefix({
162
+ trace: traceToCollapse,
163
+ sharedPrefix: [pathToCollapse[0]!, branchPoint],
164
+ netLabelPlacements,
165
+ })
166
+ ) {
167
+ continue
168
+ }
169
+ const collapsedTrace = getTraceWithoutSharedPrefix({
170
+ trace: traceToCollapse,
171
+ pathFromSharedPin: pathToCollapse,
172
+ sharedPinId: sharedPin.pinId,
173
+ branchPoint,
174
+ })
175
+ if (!collapsedTrace) continue
176
+ outputTraces[traceIndexToCollapse] = collapsedTrace
177
+ break
178
+ }
179
+ if (firstPath.length < 3 || secondPath.length < 3) continue
180
+ if (!pointsEqual(firstPath[1]!, secondPath[1]!)) continue
181
+
182
+ const railAxis = getSegmentAxis(firstPath[1]!, firstPath[2]!)
183
+ const secondRailAxis = getSegmentAxis(secondPath[1]!, secondPath[2]!)
184
+ if (
185
+ !railAxis ||
186
+ railAxis !== secondRailAxis ||
187
+ railAxis === departureAxis
188
+ ) {
189
+ continue
190
+ }
191
+ const firstRailDirection =
192
+ firstPath[2]![railAxis] - firstPath[1]![railAxis]
193
+ const secondRailDirection =
194
+ secondPath[2]![railAxis] - secondPath[1]![railAxis]
195
+ const railsHaveSameDirection =
196
+ Math.sign(firstRailDirection) === Math.sign(secondRailDirection)
197
+ const sharedStub = firstPath.slice(0, 2)
198
+ if (
199
+ !railsHaveSameDirection &&
200
+ !nearlyEqual(
201
+ Math.abs(firstRailDirection),
202
+ Math.abs(secondRailDirection),
203
+ ) &&
204
+ netLabelPlacements.some(
205
+ (label) =>
206
+ label.globalConnNetId === firstTrace.globalConnNetId &&
207
+ tracePathContainsPoint(sharedStub, label.anchorPoint),
208
+ )
209
+ ) {
210
+ continue
211
+ }
212
+ let traceIndexToCollapse = secondIndex
213
+ let traceToCollapse = secondTrace
214
+ let pathToCollapse = secondPath
215
+ let branchPoint = firstPath[1]!
216
+ if (
217
+ railsHaveSameDirection &&
218
+ Math.abs(firstRailDirection) < Math.abs(secondRailDirection)
219
+ ) {
220
+ traceIndexToCollapse = firstIndex
221
+ traceToCollapse = firstTrace
222
+ pathToCollapse = firstPath
223
+ branchPoint = firstPath[2]!
224
+ } else if (railsHaveSameDirection) {
225
+ branchPoint = secondPath[2]!
226
+ }
227
+ const sharedPrefix = [pathToCollapse[0]!, pathToCollapse[1]!, branchPoint]
228
+ if (
229
+ !traceCanLoseSharedPrefix({
230
+ trace: traceToCollapse,
231
+ sharedPrefix,
232
+ netLabelPlacements,
233
+ })
234
+ ) {
235
+ if (traceIndexToCollapse === firstIndex) {
236
+ traceIndexToCollapse = secondIndex
237
+ traceToCollapse = secondTrace
238
+ pathToCollapse = secondPath
239
+ } else {
240
+ traceIndexToCollapse = firstIndex
241
+ traceToCollapse = firstTrace
242
+ pathToCollapse = firstPath
243
+ }
244
+ }
245
+ if (
246
+ !traceCanLoseSharedPrefix({
247
+ trace: traceToCollapse,
248
+ sharedPrefix: [pathToCollapse[0]!, pathToCollapse[1]!, branchPoint],
249
+ netLabelPlacements,
250
+ })
251
+ ) {
252
+ continue
253
+ }
254
+ const collapsedTrace = getTraceWithoutSharedPrefix({
255
+ trace: traceToCollapse,
256
+ pathFromSharedPin: pathToCollapse,
257
+ sharedPinId: sharedPin.pinId,
258
+ branchPoint,
259
+ })
260
+ if (!collapsedTrace) continue
261
+ outputTraces[traceIndexToCollapse] = collapsedTrace
262
+ break
263
+ }
264
+ }
265
+
266
+ return outputTraces
267
+ }
@@ -11,7 +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 { getSharedPin } from "./alignSameNetJunctions"
14
+ import { getSharedPin } from "./getSharedPin"
15
15
  import {
16
16
  pathEntersAnyNetLabel,
17
17
  pathIntersectsAnyNetLabel,
@@ -0,0 +1,13 @@
1
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import type { InputPin } from "lib/types/InputProblem"
3
+
4
+ export const getSharedPin = ({
5
+ donorTrace,
6
+ branchTrace,
7
+ }: {
8
+ donorTrace: SolvedTracePath
9
+ branchTrace: SolvedTracePath
10
+ }): (InputPin & { chipId: string }) | null => {
11
+ const branchPinIds = new Set(branchTrace.pins.map((pin) => pin.pinId))
12
+ return donorTrace.pins.find((pin) => branchPinIds.has(pin.pinId)) ?? null
13
+ }
@@ -11,6 +11,7 @@ import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolve
11
11
  import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
12
12
  import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
13
13
  import type { Point } from "@tscircuit/math-utils"
14
+ import { getPinDirectionCandidates } from "./SchematicTraceSingleLineSolver/getPinDirection"
14
15
 
15
16
  const shouldPreferExteriorDetours = ({
16
17
  connectionPair,
@@ -62,6 +63,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
62
63
  chipMap: Record<string, InputChip>
63
64
 
64
65
  currentConnectionPair: MspConnectionPair | null = null
66
+ retryingWithoutNetLabelClearance = false
65
67
 
66
68
  solvedTracePaths: Array<SolvedTracePath> = []
67
69
  failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
@@ -101,6 +103,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
101
103
  if (this.activeSubSolver?.solved) {
102
104
  this.solvedTracePaths.push({
103
105
  ...this.currentConnectionPair!,
106
+ pins: this.activeSubSolver.pins,
104
107
  tracePath: this.activeSubSolver!.solvedTracePath!,
105
108
  mspConnectionPairIds: [this.currentConnectionPair!.mspPairId],
106
109
  pinIds: [
@@ -110,8 +113,33 @@ export class SchematicTraceLinesSolver extends BaseSolver {
110
113
  })
111
114
  this.activeSubSolver = null
112
115
  this.currentConnectionPair = null
116
+ this.retryingWithoutNetLabelClearance = false
113
117
  }
114
118
  if (this.activeSubSolver?.failed) {
119
+ if (
120
+ this.currentConnectionPair &&
121
+ !this.retryingWithoutNetLabelClearance &&
122
+ this.activeSubSolver.hasAmbiguousPinDirections
123
+ ) {
124
+ const connectionPair = this.currentConnectionPair
125
+ this.retryingWithoutNetLabelClearance = true
126
+ this.activeSubSolver = new SchematicTraceSingleLineSolver2({
127
+ inputProblem: this.inputProblem,
128
+ pins: connectionPair.pins.map((pin) => ({
129
+ ...pin,
130
+ })) as MspConnectionPair["pins"],
131
+ connectionPair,
132
+ chipMap: this.chipMap,
133
+ preferExteriorDetours: shouldPreferExteriorDetours({
134
+ connectionPair,
135
+ allConnectionPairs: this.mspConnectionPairs,
136
+ inputProblem: this.inputProblem,
137
+ }),
138
+ reserveNetLabelClearance: false,
139
+ })
140
+ return
141
+ }
142
+
115
143
  // Record the failure for this connection and continue to the next pair
116
144
  if (this.currentConnectionPair) {
117
145
  this.failedConnectionPairs.push({
@@ -121,6 +149,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
121
149
  }
122
150
  this.activeSubSolver = null
123
151
  this.currentConnectionPair = null
152
+ this.retryingWithoutNetLabelClearance = false
124
153
  // Do not fail the whole solver; proceed to schedule the next pair
125
154
  }
126
155
 
@@ -137,8 +166,22 @@ export class SchematicTraceLinesSolver extends BaseSolver {
137
166
  }
138
167
 
139
168
  this.currentConnectionPair = connectionPair
140
-
141
- const { pins } = connectionPair
169
+ this.retryingWithoutNetLabelClearance = false
170
+
171
+ // Corner pins can legitimately face either adjacent edge. Keep their
172
+ // inferred direction local to this route so solving one branch cannot
173
+ // constrain a later branch that shares the same corner pin. Pins with one
174
+ // geometric direction retain the established shared representation.
175
+ const hasAmbiguousCornerPin = connectionPair.pins.some((pin) => {
176
+ if (pin._facingDirection) return false
177
+ const chip = this.chipMap[pin.chipId]
178
+ return chip && getPinDirectionCandidates(pin, chip).length > 1
179
+ })
180
+ const pins = hasAmbiguousCornerPin
181
+ ? (connectionPair.pins.map((pin) => ({
182
+ ...pin,
183
+ })) as MspConnectionPair["pins"])
184
+ : connectionPair.pins
142
185
 
143
186
  this.activeSubSolver = new SchematicTraceSingleLineSolver2({
144
187
  inputProblem: this.inputProblem,
@@ -1,9 +1,12 @@
1
1
  import type { InputChip, InputPin } from "lib/types/InputProblem"
2
2
 
3
- export const getPinDirection = (
3
+ export type PinDirection = "x+" | "x-" | "y+" | "y-"
4
+
5
+ export const getPinDirectionCandidates = (
4
6
  pin: InputPin,
5
7
  chip: InputChip,
6
- ): "x+" | "x-" | "y+" | "y-" => {
8
+ connectedPin?: InputPin,
9
+ ): PinDirection[] => {
7
10
  // Determine what edge the pin lies on
8
11
  const { x, y } = pin
9
12
  const { center, width, height } = chip
@@ -26,17 +29,62 @@ export const getPinDirection = (
26
29
  xMinusDistance,
27
30
  )
28
31
 
29
- if (minDistance === yPlusDistance) {
30
- return "y+"
31
- }
32
+ // Preserve the established single-direction result as the primary choice.
33
+ // Candidate routing may treat nearly equal edge distances as an ambiguous
34
+ // corner, but context-free callers (such as visualization) should not change
35
+ // direction solely because the candidate comparison uses a tolerance.
36
+ const primaryDirection: PinDirection =
37
+ minDistance === yPlusDistance
38
+ ? "y+"
39
+ : minDistance === yMinusDistance
40
+ ? "y-"
41
+ : minDistance === xPlusDistance
42
+ ? "x+"
43
+ : "x-"
44
+
45
+ const matchingDirections = [
46
+ { direction: "y+" as const, distance: yPlusDistance },
47
+ { direction: "y-" as const, distance: yMinusDistance },
48
+ { direction: "x+" as const, distance: xPlusDistance },
49
+ { direction: "x-" as const, distance: xMinusDistance },
50
+ ]
51
+ .filter(({ distance }) => Math.abs(distance - minDistance) <= 1e-9)
52
+ .map(({ direction }) => direction)
53
+
54
+ const closestDirections = [
55
+ primaryDirection,
56
+ ...matchingDirections.filter((direction) => direction !== primaryDirection),
57
+ ]
32
58
 
33
- if (minDistance === yMinusDistance) {
34
- return "y-"
59
+ if (!connectedPin || closestDirections.length <= 1) {
60
+ return closestDirections
35
61
  }
36
62
 
37
- if (minDistance === xPlusDistance) {
38
- return "x+"
63
+ const xDistance = connectedPin.x - pin.x
64
+ const yDistance = connectedPin.y - pin.y
65
+ const directionTowardConnectedPin: PinDirection =
66
+ Math.abs(xDistance) >= Math.abs(yDistance)
67
+ ? xDistance >= 0
68
+ ? "x+"
69
+ : "x-"
70
+ : yDistance >= 0
71
+ ? "y+"
72
+ : "y-"
73
+
74
+ if (!closestDirections.includes(directionTowardConnectedPin)) {
75
+ return closestDirections
39
76
  }
40
77
 
41
- return "x-"
78
+ return [
79
+ directionTowardConnectedPin,
80
+ ...closestDirections.filter(
81
+ (direction) => direction !== directionTowardConnectedPin,
82
+ ),
83
+ ]
42
84
  }
85
+
86
+ export const getPinDirection = (
87
+ pin: InputPin,
88
+ chip: InputChip,
89
+ connectedPin?: InputPin,
90
+ ): PinDirection => getPinDirectionCandidates(pin, chip, connectedPin)[0]!