@tscircuit/schematic-trace-solver 0.0.115 → 0.0.117
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.d.ts +22 -1
- package/dist/index.js +281 -0
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +75 -0
- package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +71 -0
- package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +223 -0
- package/lib/solvers/SameNetJunctionAlignmentSolver/pathIntersectsAnyNetLabel.ts +22 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +17 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260730T061837Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260731T052229Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260728T144234Z/bug-report-20260728T144234Z.test.ts +6 -0
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +123 -0
- package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.json +614 -0
- package/tests/bug-reports/bug-report-20260730T061837Z/bug-report-20260730T061837Z.test.ts +34 -0
- package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +93 -0
- package/tests/bug-reports/bug-report-20260731T052229Z/bug-report-20260731T052229Z.json +299 -0
- package/tests/bug-reports/bug-report-20260731T052229Z/bug-report-20260731T052229Z.test.ts +43 -0
- package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +110 -0
- package/tests/repros/assets/board-15984-schematic-trace-input.json +518 -0
- package/tests/repros/board-15984-same-net-junction.test.ts +30 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
5
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
6
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
7
|
+
import {
|
|
8
|
+
getVisibleTraceLength,
|
|
9
|
+
getVisibleTraceSegmentCount,
|
|
10
|
+
isHorizontal,
|
|
11
|
+
nearlyEqual,
|
|
12
|
+
} from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
|
|
13
|
+
import type { InputPin, InputProblem } from "lib/types/InputProblem"
|
|
14
|
+
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
|
|
15
|
+
import { pathIntersectsAnyNetLabel } from "./pathIntersectsAnyNetLabel"
|
|
16
|
+
|
|
17
|
+
interface AlignSameNetJunctionsInput {
|
|
18
|
+
inputProblem: InputProblem
|
|
19
|
+
traces: SolvedTracePath[]
|
|
20
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface HorizontalSegment {
|
|
24
|
+
start: Point
|
|
25
|
+
end: Point
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_ALIGNED_LOAD_PIN_OFFSET = 0.2
|
|
29
|
+
|
|
30
|
+
const getSharedPin = ({
|
|
31
|
+
donorTrace,
|
|
32
|
+
branchTrace,
|
|
33
|
+
}: {
|
|
34
|
+
donorTrace: SolvedTracePath
|
|
35
|
+
branchTrace: SolvedTracePath
|
|
36
|
+
}): (InputPin & { chipId: string }) | null => {
|
|
37
|
+
const branchPinIds = new Set(branchTrace.pins.map((pin) => pin.pinId))
|
|
38
|
+
return donorTrace.pins.find((pin) => branchPinIds.has(pin.pinId)) ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getOtherPin = ({
|
|
42
|
+
trace,
|
|
43
|
+
sharedPin,
|
|
44
|
+
}: {
|
|
45
|
+
trace: SolvedTracePath
|
|
46
|
+
sharedPin: InputPin
|
|
47
|
+
}) => trace.pins.find((pin) => pin.pinId !== sharedPin.pinId) ?? null
|
|
48
|
+
|
|
49
|
+
const getLongestHorizontalSegment = (
|
|
50
|
+
trace: SolvedTracePath,
|
|
51
|
+
): HorizontalSegment | null => {
|
|
52
|
+
let longest: HorizontalSegment | null = null
|
|
53
|
+
for (let index = 0; index < trace.tracePath.length - 1; index++) {
|
|
54
|
+
const start = trace.tracePath[index]!
|
|
55
|
+
const end = trace.tracePath[index + 1]!
|
|
56
|
+
if (!isHorizontal(start, end)) continue
|
|
57
|
+
if (
|
|
58
|
+
!longest ||
|
|
59
|
+
Math.abs(end.x - start.x) > Math.abs(longest.end.x - longest.start.x)
|
|
60
|
+
) {
|
|
61
|
+
longest = { start, end }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return longest
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const getJunctionPoint = ({
|
|
68
|
+
segment,
|
|
69
|
+
sharedPin,
|
|
70
|
+
}: {
|
|
71
|
+
segment: HorizontalSegment
|
|
72
|
+
sharedPin: Point
|
|
73
|
+
}) => {
|
|
74
|
+
const startDistance = Math.abs(segment.start.x - sharedPin.x)
|
|
75
|
+
const endDistance = Math.abs(segment.end.x - sharedPin.x)
|
|
76
|
+
if (startDistance <= endDistance) return segment.start
|
|
77
|
+
return segment.end
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const railIsOnFacingSide = ({
|
|
81
|
+
railY,
|
|
82
|
+
pin,
|
|
83
|
+
}: {
|
|
84
|
+
railY: number
|
|
85
|
+
pin: InputPin
|
|
86
|
+
}) => {
|
|
87
|
+
if (pin._facingDirection === "y+") return railY > pin.y
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const getAlignedBranchPath = ({
|
|
92
|
+
donorTrace,
|
|
93
|
+
branchTrace,
|
|
94
|
+
}: {
|
|
95
|
+
donorTrace: SolvedTracePath
|
|
96
|
+
branchTrace: SolvedTracePath
|
|
97
|
+
}): Point[] | null => {
|
|
98
|
+
const sharedPin = getSharedPin({ donorTrace, branchTrace })
|
|
99
|
+
if (!sharedPin) return null
|
|
100
|
+
const donorOtherPin = getOtherPin({ trace: donorTrace, sharedPin })
|
|
101
|
+
if (!donorOtherPin) return null
|
|
102
|
+
const otherPin = getOtherPin({ trace: branchTrace, sharedPin })
|
|
103
|
+
if (!otherPin) return null
|
|
104
|
+
if (Math.abs(sharedPin.y - otherPin.y) > MAX_ALIGNED_LOAD_PIN_OFFSET) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const donorRail = getLongestHorizontalSegment(donorTrace)
|
|
109
|
+
if (!donorRail) return null
|
|
110
|
+
const branchRail = getLongestHorizontalSegment(branchTrace)
|
|
111
|
+
if (branchRail && nearlyEqual(branchRail.start.y, donorRail.start.y)) {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
if (!railIsOnFacingSide({ railY: donorRail.start.y, pin: otherPin })) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const junction = getJunctionPoint({ segment: donorRail, sharedPin })
|
|
119
|
+
const extendsDonorRail =
|
|
120
|
+
(donorOtherPin.x < junction.x && otherPin.x > junction.x) ||
|
|
121
|
+
(donorOtherPin.x > junction.x && otherPin.x < junction.x)
|
|
122
|
+
if (!extendsDonorRail) return null
|
|
123
|
+
|
|
124
|
+
const sharedToOther = simplifyPath([
|
|
125
|
+
{ x: sharedPin.x, y: sharedPin.y },
|
|
126
|
+
{ x: junction.x, y: sharedPin.y },
|
|
127
|
+
{ x: junction.x, y: junction.y },
|
|
128
|
+
{ x: otherPin.x, y: junction.y },
|
|
129
|
+
{ x: otherPin.x, y: otherPin.y },
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
if (branchTrace.pins[0].pinId === sharedPin.pinId) return sharedToOther
|
|
133
|
+
return [...sharedToOther].reverse()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const candidateIsClear = ({
|
|
137
|
+
candidateTrace,
|
|
138
|
+
traces,
|
|
139
|
+
inputProblem,
|
|
140
|
+
netLabelPlacements,
|
|
141
|
+
}: {
|
|
142
|
+
candidateTrace: SolvedTracePath
|
|
143
|
+
traces: SolvedTracePath[]
|
|
144
|
+
inputProblem: InputProblem
|
|
145
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
146
|
+
}) => {
|
|
147
|
+
const obstacles = getObstacleRects(inputProblem)
|
|
148
|
+
if (isPathCollidingWithObstacles(candidateTrace.tracePath, obstacles)) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const otherNetTraces = traces.filter(
|
|
153
|
+
(trace) => trace.globalConnNetId !== candidateTrace.globalConnNetId,
|
|
154
|
+
)
|
|
155
|
+
if (doesPathCoincideWithTraces(candidateTrace.tracePath, otherNetTraces)) {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return !pathIntersectsAnyNetLabel({
|
|
160
|
+
path: candidateTrace.tracePath,
|
|
161
|
+
netLabelPlacements,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const alignSameNetJunctions = ({
|
|
166
|
+
inputProblem,
|
|
167
|
+
traces,
|
|
168
|
+
netLabelPlacements,
|
|
169
|
+
}: AlignSameNetJunctionsInput) => {
|
|
170
|
+
let outputTraces = [...traces]
|
|
171
|
+
const alignedBranchTraceIds = new Set<string>()
|
|
172
|
+
let alignedJunctionCount = 0
|
|
173
|
+
|
|
174
|
+
// Reuse each aligned branch as the rail for the next load in the chain.
|
|
175
|
+
for (const donorTraceId of traces.map((trace) => trace.mspPairId)) {
|
|
176
|
+
const donorTrace = outputTraces.find(
|
|
177
|
+
(trace) => trace.mspPairId === donorTraceId,
|
|
178
|
+
)!
|
|
179
|
+
for (const branchTrace of outputTraces) {
|
|
180
|
+
if (alignedBranchTraceIds.has(branchTrace.mspPairId)) continue
|
|
181
|
+
if (donorTrace.mspPairId === branchTrace.mspPairId) continue
|
|
182
|
+
if (donorTrace.globalConnNetId !== branchTrace.globalConnNetId) continue
|
|
183
|
+
|
|
184
|
+
const candidatePath = getAlignedBranchPath({ donorTrace, branchTrace })
|
|
185
|
+
if (!candidatePath) continue
|
|
186
|
+
const candidateTrace = { ...branchTrace, tracePath: candidatePath }
|
|
187
|
+
const originalPair = [donorTrace, branchTrace]
|
|
188
|
+
const candidatePair = [donorTrace, candidateTrace]
|
|
189
|
+
const removesVisibleSegment =
|
|
190
|
+
getVisibleTraceSegmentCount(candidatePair) <
|
|
191
|
+
getVisibleTraceSegmentCount(originalPair)
|
|
192
|
+
const shortensVisibleTrace =
|
|
193
|
+
getVisibleTraceLength(candidatePair) <
|
|
194
|
+
getVisibleTraceLength(originalPair) &&
|
|
195
|
+
!nearlyEqual(
|
|
196
|
+
getVisibleTraceLength(candidatePair),
|
|
197
|
+
getVisibleTraceLength(originalPair),
|
|
198
|
+
)
|
|
199
|
+
if (!removesVisibleSegment && !shortensVisibleTrace) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
!candidateIsClear({
|
|
204
|
+
candidateTrace,
|
|
205
|
+
traces: outputTraces,
|
|
206
|
+
inputProblem,
|
|
207
|
+
netLabelPlacements,
|
|
208
|
+
})
|
|
209
|
+
) {
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
outputTraces = outputTraces.map((trace) => {
|
|
214
|
+
if (trace.mspPairId === branchTrace.mspPairId) return candidateTrace
|
|
215
|
+
return trace
|
|
216
|
+
})
|
|
217
|
+
alignedBranchTraceIds.add(branchTrace.mspPairId)
|
|
218
|
+
alignedJunctionCount++
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { traces: outputTraces, alignedJunctionCount }
|
|
223
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
|
|
4
|
+
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
5
|
+
|
|
6
|
+
export const pathIntersectsAnyNetLabel = ({
|
|
7
|
+
path,
|
|
8
|
+
netLabelPlacements,
|
|
9
|
+
}: {
|
|
10
|
+
path: Point[]
|
|
11
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
12
|
+
}) => {
|
|
13
|
+
for (const label of netLabelPlacements) {
|
|
14
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
15
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
16
|
+
if (segmentIntersectsRect(path[index], path[index + 1], labelBounds)) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
@@ -28,6 +28,7 @@ import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOver
|
|
|
28
28
|
import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
|
|
29
29
|
import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
|
|
30
30
|
import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
31
|
+
import { SameNetJunctionAlignmentSolver } from "../SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver"
|
|
31
32
|
|
|
32
33
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
33
34
|
solverName: string
|
|
@@ -86,6 +87,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
86
87
|
netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
|
|
87
88
|
traceCleanupSolver2?: TraceCleanupSolver
|
|
88
89
|
netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
|
|
90
|
+
sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver
|
|
89
91
|
|
|
90
92
|
startTimeOfPhase: Record<string, number>
|
|
91
93
|
endTimeOfPhase: Record<string, number>
|
|
@@ -402,6 +404,21 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
402
404
|
},
|
|
403
405
|
],
|
|
404
406
|
),
|
|
407
|
+
definePipelineStep(
|
|
408
|
+
"sameNetJunctionAlignmentSolver",
|
|
409
|
+
SameNetJunctionAlignmentSolver,
|
|
410
|
+
(instance) => {
|
|
411
|
+
const collisionOutput =
|
|
412
|
+
instance.netLabelNetLabelCollisionSolver!.getOutput()
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
inputProblem: instance.inputProblem,
|
|
416
|
+
traces: instance.netLabelNetLabelCollisionSolver!.traces,
|
|
417
|
+
netLabelPlacements: collisionOutput.netLabelPlacements,
|
|
418
|
+
},
|
|
419
|
+
]
|
|
420
|
+
},
|
|
421
|
+
),
|
|
405
422
|
]
|
|
406
423
|
|
|
407
424
|
constructor(inputProblem: InputProblem, opts?: Options) {
|
package/package.json
CHANGED