@tscircuit/schematic-trace-solver 0.0.169 → 0.0.171
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.
- package/dist/index.js +216 -107
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +6 -1
- package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +17 -4
- package/lib/solvers/NetLabelToTraceSolver/reduceTraceCrossings.ts +180 -0
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +10 -10
- package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +10 -10
- package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +10 -10
- package/tests/examples/__snapshots__/example37.snap.svg +2 -2
- package/tests/examples/example37.test.ts +13 -1
- package/tests/repros/__snapshots__/repro-hdc3020-ground-label-alignment.snap.svg +110 -0
- package/tests/repros/assets/repro-hdc3020-ground-label-alignment.input.json +282 -0
- package/tests/repros/repro-hdc3020-ground-label-alignment.test.ts +25 -0
|
@@ -5,16 +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
|
-
import { doesTraceRecoveryPathConflict } from "lib/solvers/NetLabelTraceRecovery/doesTraceRecoveryPathConflict"
|
|
16
16
|
import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
|
|
17
17
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
18
|
+
import { findFirstCollision } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
18
19
|
import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
19
20
|
import type {
|
|
20
21
|
ChipId,
|
|
@@ -24,10 +25,11 @@ import type {
|
|
|
24
25
|
} from "lib/types/InputProblem"
|
|
25
26
|
import { arePinsInDifferentSchematicSections } from "lib/utils/arePinsInDifferentSchematicSections"
|
|
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
|
-
|
|
425
|
+
let tracePath = this.activeSubSolver?.solvedTracePath
|
|
424
426
|
if (!candidate || !tracePath) return
|
|
425
427
|
|
|
426
428
|
const retainedTraces = this.outputTraces.filter(
|
|
@@ -440,6 +442,17 @@ export class NetLabelToTraceSolver extends BaseSolver {
|
|
|
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
|
+
}
|