@tscircuit/schematic-trace-solver 0.0.168 → 0.0.170

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 (22) hide show
  1. package/dist/index.js +273 -90
  2. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +18 -5
  3. package/lib/solvers/NetLabelToTraceSolver/reduceTraceCrossings.ts +180 -0
  4. package/lib/solvers/NetLabelTraceRecovery/doesTraceRecoveryPathConflict.ts +112 -0
  5. package/package.json +1 -1
  6. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +60 -222
  7. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +46 -64
  8. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +7 -25
  9. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +68 -104
  10. package/tests/examples/__snapshots__/example35.snap.svg +5 -23
  11. package/tests/examples/__snapshots__/example36.snap.svg +27 -45
  12. package/tests/examples/__snapshots__/example37.snap.svg +7 -25
  13. package/tests/examples/__snapshots__/example51.snap.svg +47 -83
  14. package/tests/examples/example37.test.ts +13 -1
  15. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +5 -23
  16. package/tests/repros/__snapshots__/repro-bc107a-common-emitter-routing.snap.svg +141 -0
  17. package/tests/repros/__snapshots__/repro-dc-power-isolated-routing.snap.svg +6 -24
  18. package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +255 -0
  19. package/tests/repros/assets/repro-bc107a-common-emitter-routing.input.json +687 -0
  20. package/tests/repros/assets/repro-powerbank-3v-system-power.input.json +1772 -0
  21. package/tests/repros/repro-bc107a-common-emitter-routing.test.ts +16 -0
  22. package/tests/repros/repro-powerbank-3v-system-power.test.ts +17 -0
@@ -5,15 +5,17 @@ import {
5
5
  } from "@tscircuit/math-utils"
6
6
  import type { GraphicsObject } from "graphics-debug"
7
7
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
8
- import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
9
8
  import { doesPairCrossRestrictedCenterLines } from "lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines"
9
+ import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
10
10
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
11
+ import { doesTraceRecoveryPathConflict } from "lib/solvers/NetLabelTraceRecovery/doesTraceRecoveryPathConflict"
11
12
  import {
12
13
  getTraceRecoveryConnectivityMaps,
13
14
  type TraceRecoveryPin,
14
15
  } from "lib/solvers/NetLabelTraceRecovery/getTraceRecoveryConnectivityMaps"
15
16
  import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
16
17
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
18
+ import { findFirstCollision } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
17
19
  import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
18
20
  import type {
19
21
  ChipId,
@@ -22,12 +24,12 @@ import type {
22
24
  PinId,
23
25
  } from "lib/types/InputProblem"
24
26
  import { arePinsInDifferentSchematicSections } from "lib/utils/arePinsInDifferentSchematicSections"
25
- import { doesTraceOverlapWithExistingTraces } from "lib/utils/does-trace-overlap-with-existing-traces"
26
27
  import {
27
- type InlineNetLabelPlacement,
28
28
  type InlineNetLabelOutput,
29
+ type InlineNetLabelPlacement,
29
30
  visualizeInlineNetLabelOutput,
30
31
  } from "../InlineNetLabelSolver/InlineNetLabelSolver"
32
+ import { reduceTraceCrossings } from "./reduceTraceCrossings"
31
33
 
32
34
  type GlobalConnNetId = NetLabelPlacement["globalConnNetId"]
33
35
 
@@ -420,7 +422,7 @@ export class NetLabelToTraceSolver extends BaseSolver {
420
422
 
421
423
  private tryAcceptCurrentRoute() {
422
424
  const candidate = this.currentCandidate
423
- const tracePath = this.activeSubSolver?.solvedTracePath
425
+ let tracePath = this.activeSubSolver?.solvedTracePath
424
426
  if (!candidate || !tracePath) return
425
427
 
426
428
  const retainedTraces = this.outputTraces.filter(
@@ -434,12 +436,23 @@ export class NetLabelToTraceSolver extends BaseSolver {
434
436
  )
435
437
  }
436
438
  if (
437
- doesTraceOverlapWithExistingTraces(tracePath, collisionTraces) ||
439
+ doesTraceRecoveryPathConflict(tracePath, collisionTraces) ||
438
440
  this.routeIntersectsRemainingLabels(tracePath, candidate)
439
441
  ) {
440
442
  return
441
443
  }
442
444
 
445
+ tracePath = reduceTraceCrossings({
446
+ tracePath,
447
+ globalConnNetId: candidate.firstLabel.globalConnNetId,
448
+ otherTraces: collisionTraces,
449
+ isCandidateValid: (candidatePath) =>
450
+ findFirstCollision(candidatePath, this.activeSubSolver!.obstacles) ===
451
+ null &&
452
+ !doesTraceRecoveryPathConflict(candidatePath, collisionTraces) &&
453
+ !this.routeIntersectsRemainingLabels(candidatePath, candidate),
454
+ })
455
+
443
456
  const [firstPin, secondPin] = candidate.pins
444
457
  const mspPairId = `${RECOVERED_TRACE_PREFIX}${candidate.key}`
445
458
  const recoveredTrace: SolvedTracePath = {
@@ -0,0 +1,180 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import {
3
+ countPathIntersections,
4
+ getPathLength,
5
+ segmentsIntersect,
6
+ } from "lib/solvers/Example28Solver/geometry"
7
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
8
+
9
+ const EPS = 1e-9
10
+
11
+ const isHorizontal = (start: Point, end: Point) =>
12
+ Math.abs(start.y - end.y) < EPS
13
+
14
+ const isVertical = (start: Point, end: Point) => Math.abs(start.x - end.x) < EPS
15
+
16
+ const compactConsecutivePoints = (path: Point[]) =>
17
+ path.filter((point, index) => {
18
+ const previousPoint = path[index - 1]
19
+ return (
20
+ !previousPoint ||
21
+ Math.abs(point.x - previousPoint.x) >= EPS ||
22
+ Math.abs(point.y - previousPoint.y) >= EPS
23
+ )
24
+ })
25
+
26
+ const isValidOrthogonalPath = (path: Point[]) => {
27
+ if (
28
+ path.length < 2 ||
29
+ !path.every((point, index) => {
30
+ const nextPoint = path[index + 1]
31
+ if (!nextPoint) return true
32
+ return isHorizontal(point, nextPoint) || isVertical(point, nextPoint)
33
+ })
34
+ ) {
35
+ return false
36
+ }
37
+
38
+ for (
39
+ let firstSegmentIndex = 0;
40
+ firstSegmentIndex < path.length - 1;
41
+ firstSegmentIndex++
42
+ ) {
43
+ for (
44
+ let secondSegmentIndex = firstSegmentIndex + 2;
45
+ secondSegmentIndex < path.length - 1;
46
+ secondSegmentIndex++
47
+ ) {
48
+ if (
49
+ segmentsIntersect(
50
+ path[firstSegmentIndex]!,
51
+ path[firstSegmentIndex + 1]!,
52
+ path[secondSegmentIndex]!,
53
+ path[secondSegmentIndex + 1]!,
54
+ )
55
+ ) {
56
+ return false
57
+ }
58
+ }
59
+ }
60
+
61
+ return true
62
+ }
63
+
64
+ const getEndpointAlignedSegmentCandidates = (tracePath: Point[]) => {
65
+ const candidates: Point[][] = []
66
+
67
+ // Preserve the terminal segment at each pin. For an interior segment whose
68
+ // neighboring segments are perpendicular, align it with either neighboring
69
+ // endpoint corridor. This keeps the same orthogonal topology while allowing
70
+ // a midpoint elbow to move away from avoidable trace crossings.
71
+ for (
72
+ let segmentIndex = 2;
73
+ segmentIndex <= tracePath.length - 4;
74
+ segmentIndex++
75
+ ) {
76
+ const previousPoint = tracePath[segmentIndex - 1]!
77
+ const start = tracePath[segmentIndex]!
78
+ const end = tracePath[segmentIndex + 1]!
79
+ const nextPoint = tracePath[segmentIndex + 2]!
80
+ const segmentIsVertical = isVertical(start, end)
81
+ const segmentIsHorizontal = isHorizontal(start, end)
82
+
83
+ if (
84
+ (!segmentIsVertical && !segmentIsHorizontal) ||
85
+ (segmentIsVertical &&
86
+ (!isHorizontal(previousPoint, start) ||
87
+ !isHorizontal(end, nextPoint))) ||
88
+ (segmentIsHorizontal &&
89
+ (!isVertical(previousPoint, start) || !isVertical(end, nextPoint)))
90
+ ) {
91
+ continue
92
+ }
93
+
94
+ const alignedCoordinates = segmentIsVertical
95
+ ? [previousPoint.x, nextPoint.x]
96
+ : [previousPoint.y, nextPoint.y]
97
+ for (const alignedCoordinate of alignedCoordinates) {
98
+ if (
99
+ Math.abs(alignedCoordinate - (segmentIsVertical ? start.x : start.y)) <
100
+ EPS
101
+ ) {
102
+ continue
103
+ }
104
+
105
+ const candidate = tracePath.map((point) => ({ ...point }))
106
+ if (segmentIsVertical) {
107
+ candidate[segmentIndex]!.x = alignedCoordinate
108
+ candidate[segmentIndex + 1]!.x = alignedCoordinate
109
+ } else {
110
+ candidate[segmentIndex]!.y = alignedCoordinate
111
+ candidate[segmentIndex + 1]!.y = alignedCoordinate
112
+ }
113
+
114
+ const compactedCandidate = compactConsecutivePoints(candidate)
115
+ if (isValidOrthogonalPath(compactedCandidate)) {
116
+ candidates.push(compactedCandidate)
117
+ }
118
+ }
119
+ }
120
+
121
+ return candidates
122
+ }
123
+
124
+ const countOtherNetCrossings = ({
125
+ tracePath,
126
+ globalConnNetId,
127
+ otherTraces,
128
+ }: {
129
+ tracePath: Point[]
130
+ globalConnNetId: string
131
+ otherTraces: SolvedTracePath[]
132
+ }) =>
133
+ otherTraces.reduce(
134
+ (crossingCount, otherTrace) =>
135
+ crossingCount +
136
+ (otherTrace.globalConnNetId === globalConnNetId
137
+ ? 0
138
+ : countPathIntersections(tracePath, otherTrace.tracePath)),
139
+ 0,
140
+ )
141
+
142
+ export const reduceTraceCrossings = ({
143
+ tracePath,
144
+ globalConnNetId,
145
+ otherTraces,
146
+ isCandidateValid,
147
+ }: {
148
+ tracePath: Point[]
149
+ globalConnNetId: string
150
+ otherTraces: SolvedTracePath[]
151
+ isCandidateValid: (candidate: Point[]) => boolean
152
+ }) => {
153
+ let bestPath = tracePath
154
+ let bestCrossingCount = countOtherNetCrossings({
155
+ tracePath,
156
+ globalConnNetId,
157
+ otherTraces,
158
+ })
159
+ let bestPathLength = getPathLength(tracePath)
160
+
161
+ for (const candidate of getEndpointAlignedSegmentCandidates(tracePath)) {
162
+ if (!isCandidateValid(candidate)) continue
163
+ const crossingCount = countOtherNetCrossings({
164
+ tracePath: candidate,
165
+ globalConnNetId,
166
+ otherTraces,
167
+ })
168
+ const pathLength = getPathLength(candidate)
169
+ if (
170
+ crossingCount < bestCrossingCount ||
171
+ (crossingCount === bestCrossingCount && pathLength < bestPathLength)
172
+ ) {
173
+ bestPath = candidate
174
+ bestCrossingCount = crossingCount
175
+ bestPathLength = pathLength
176
+ }
177
+ }
178
+
179
+ return bestPath
180
+ }
@@ -0,0 +1,112 @@
1
+ import { doSegmentsIntersect, type Point } from "@tscircuit/math-utils"
2
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
3
+
4
+ const EPSILON = 1e-6
5
+
6
+ const isStrictlyBetween = ({
7
+ coordinate,
8
+ segmentStartCoordinate,
9
+ segmentEndCoordinate,
10
+ }: {
11
+ coordinate: number
12
+ segmentStartCoordinate: number
13
+ segmentEndCoordinate: number
14
+ }) =>
15
+ coordinate >
16
+ Math.min(segmentStartCoordinate, segmentEndCoordinate) + EPSILON &&
17
+ coordinate < Math.max(segmentStartCoordinate, segmentEndCoordinate) - EPSILON
18
+
19
+ const isStrictInteriorPerpendicularCrossing = ({
20
+ firstSegmentStart,
21
+ firstSegmentEnd,
22
+ secondSegmentStart,
23
+ secondSegmentEnd,
24
+ }: {
25
+ firstSegmentStart: Point
26
+ firstSegmentEnd: Point
27
+ secondSegmentStart: Point
28
+ secondSegmentEnd: Point
29
+ }) => {
30
+ const firstIsHorizontal =
31
+ Math.abs(firstSegmentStart.y - firstSegmentEnd.y) <= EPSILON
32
+ const firstIsVertical =
33
+ Math.abs(firstSegmentStart.x - firstSegmentEnd.x) <= EPSILON
34
+ const secondIsHorizontal =
35
+ Math.abs(secondSegmentStart.y - secondSegmentEnd.y) <= EPSILON
36
+ const secondIsVertical =
37
+ Math.abs(secondSegmentStart.x - secondSegmentEnd.x) <= EPSILON
38
+
39
+ let horizontalStart: Point
40
+ let horizontalEnd: Point
41
+ if (firstIsHorizontal) {
42
+ horizontalStart = firstSegmentStart
43
+ horizontalEnd = firstSegmentEnd
44
+ } else if (secondIsHorizontal) {
45
+ horizontalStart = secondSegmentStart
46
+ horizontalEnd = secondSegmentEnd
47
+ } else {
48
+ return false
49
+ }
50
+
51
+ let verticalStart: Point
52
+ let verticalEnd: Point
53
+ if (firstIsVertical) {
54
+ verticalStart = firstSegmentStart
55
+ verticalEnd = firstSegmentEnd
56
+ } else if (secondIsVertical) {
57
+ verticalStart = secondSegmentStart
58
+ verticalEnd = secondSegmentEnd
59
+ } else {
60
+ return false
61
+ }
62
+
63
+ return (
64
+ isStrictlyBetween({
65
+ coordinate: verticalStart.x,
66
+ segmentStartCoordinate: horizontalStart.x,
67
+ segmentEndCoordinate: horizontalEnd.x,
68
+ }) &&
69
+ isStrictlyBetween({
70
+ coordinate: horizontalStart.y,
71
+ segmentStartCoordinate: verticalStart.y,
72
+ segmentEndCoordinate: verticalEnd.y,
73
+ })
74
+ )
75
+ }
76
+
77
+ export const doesTraceRecoveryPathConflict = (
78
+ tracePath: Point[],
79
+ collisionTraces: SolvedTracePath[],
80
+ ) => {
81
+ for (let pathIndex = 0; pathIndex < tracePath.length - 1; pathIndex++) {
82
+ const pathStart = tracePath[pathIndex]!
83
+ const pathEnd = tracePath[pathIndex + 1]!
84
+
85
+ for (const trace of collisionTraces) {
86
+ for (
87
+ let traceIndex = 0;
88
+ traceIndex < trace.tracePath.length - 1;
89
+ traceIndex++
90
+ ) {
91
+ const traceStart = trace.tracePath[traceIndex]!
92
+ const traceEnd = trace.tracePath[traceIndex + 1]!
93
+ if (!doSegmentsIntersect(pathStart, pathEnd, traceStart, traceEnd)) {
94
+ continue
95
+ }
96
+ if (
97
+ isStrictInteriorPerpendicularCrossing({
98
+ firstSegmentStart: pathStart,
99
+ firstSegmentEnd: pathEnd,
100
+ secondSegmentStart: traceStart,
101
+ secondSegmentEnd: traceEnd,
102
+ })
103
+ ) {
104
+ continue
105
+ }
106
+ return true
107
+ }
108
+ }
109
+ }
110
+
111
+ return false
112
+ }
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.168",
8
+ "version": "0.0.170",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",