@tscircuit/schematic-trace-solver 0.0.106 → 0.0.107
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 +270 -86
- package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +178 -9
- package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +59 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +64 -1
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +221 -0
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +1 -1
- package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +200 -0
- package/tests/repros/repro-tps61222-trace-intersection.test.ts +14 -0
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +6 -0
|
@@ -6,8 +6,14 @@ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatia
|
|
|
6
6
|
|
|
7
7
|
import { findAllLShapedTurns, type LShape } from "./findAllLShapedTurns"
|
|
8
8
|
import { getTraceObstacles } from "./getTraceObstacles"
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
findIntersectionsWithObstacles,
|
|
11
|
+
findPerpendicularPathCrossings,
|
|
12
|
+
} from "./findIntersectionsWithObstacles"
|
|
13
|
+
import {
|
|
14
|
+
generateLShapeRerouteCandidates,
|
|
15
|
+
generatePerpendicularTraceDetours,
|
|
16
|
+
} from "./generateLShapeRerouteCandidates"
|
|
11
17
|
import { isPathColliding, type CollisionInfo } from "./isPathColliding"
|
|
12
18
|
import {
|
|
13
19
|
generateRectangleCandidates,
|
|
@@ -24,6 +30,19 @@ import { visualizeTightRectangle } from "../visualizeTightRectangle"
|
|
|
24
30
|
import { visualizeCandidates } from "./visualizeCandidates"
|
|
25
31
|
import { mergeGraphicsObjects } from "../mergeGraphicsObjects"
|
|
26
32
|
import { visualizeCollision } from "./visualizeCollision"
|
|
33
|
+
import {
|
|
34
|
+
getPathLength,
|
|
35
|
+
isPathCollidingWithChipInterior,
|
|
36
|
+
} from "../../Example28Solver/geometry"
|
|
37
|
+
import { getObstacleRects } from "../../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
38
|
+
|
|
39
|
+
interface TraceCrossing {
|
|
40
|
+
trace: SolvedTracePath
|
|
41
|
+
segmentIndex: number
|
|
42
|
+
otherTrace: SolvedTracePath
|
|
43
|
+
otherSegmentIndex: number
|
|
44
|
+
isInitialBundleCrossing: boolean
|
|
45
|
+
}
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
48
|
* Defines the input structure for the UntangleTraceSubsolver.
|
|
@@ -60,6 +79,9 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
60
79
|
private input: UntangleTraceSubsolverInput
|
|
61
80
|
private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
62
81
|
private lShapesToProcess: LShape[] = []
|
|
82
|
+
private ignoredCrossings = new Set<string>()
|
|
83
|
+
private reroutedTraceIds = new Set<string>()
|
|
84
|
+
private processingCrossings = true
|
|
63
85
|
private visualizationMode: VisualizationMode = "l_shapes"
|
|
64
86
|
|
|
65
87
|
private currentLShape: LShape | null = null
|
|
@@ -95,13 +117,6 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
95
117
|
this.input.inputProblem._chipObstacleSpatialIndex =
|
|
96
118
|
this.chipObstacleSpatialIndex
|
|
97
119
|
}
|
|
98
|
-
|
|
99
|
-
for (const trace of this.input.allTraces) {
|
|
100
|
-
const lShapes = findAllLShapedTurns(trace.tracePath)
|
|
101
|
-
this.lShapesToProcess.push(
|
|
102
|
-
...lShapes.map((l) => ({ ...l, traceId: trace.mspPairId as string })),
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
override _step(): void {
|
|
@@ -110,6 +125,21 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
110
125
|
return
|
|
111
126
|
}
|
|
112
127
|
|
|
128
|
+
if (this.processingCrossings) {
|
|
129
|
+
// The L-shape pass below only reacts when both arms intersect obstacles.
|
|
130
|
+
// Resolve strict crossings on merged-label bundles before entering it.
|
|
131
|
+
const crossing = this._findCrossing()
|
|
132
|
+
if (crossing) {
|
|
133
|
+
if (!this._resolveCrossing(crossing)) {
|
|
134
|
+
this.ignoredCrossings.add(this._crossingKey(crossing))
|
|
135
|
+
}
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
this.processingCrossings = false
|
|
139
|
+
this._initializeLShapes()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
if (this.lShapeJustProcessed) {
|
|
114
144
|
this._resetAfterLShapProcessing()
|
|
115
145
|
return
|
|
@@ -136,6 +166,145 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
168
|
|
|
169
|
+
private _initializeLShapes() {
|
|
170
|
+
for (const trace of this.input.allTraces) {
|
|
171
|
+
this.lShapesToProcess.push(
|
|
172
|
+
...findAllLShapedTurns(trace.tracePath).map((lShape) => ({
|
|
173
|
+
...lShape,
|
|
174
|
+
traceId: trace.mspPairId,
|
|
175
|
+
})),
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _crossingKey(crossing: TraceCrossing) {
|
|
181
|
+
return `${crossing.trace.mspPairId}:${crossing.segmentIndex}:${crossing.otherTrace.mspPairId}:${crossing.otherSegmentIndex}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _isTraceBundle(first: SolvedTracePath, second: SolvedTracePath) {
|
|
185
|
+
const componentPair = (trace: SolvedTracePath) =>
|
|
186
|
+
trace.pins
|
|
187
|
+
.map((pin) => pin.chipId)
|
|
188
|
+
.sort()
|
|
189
|
+
.join(":")
|
|
190
|
+
if (componentPair(first) !== componentPair(second)) return false
|
|
191
|
+
|
|
192
|
+
return Object.values(this.input.mergedLabelNetIdMap).some(
|
|
193
|
+
(netIds) =>
|
|
194
|
+
netIds.has(first.globalConnNetId) && netIds.has(second.globalConnNetId),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _findCrossing(): TraceCrossing | null {
|
|
199
|
+
const traces = this.input.allTraces
|
|
200
|
+
for (let firstIndex = 0; firstIndex < traces.length; firstIndex++) {
|
|
201
|
+
const trace = traces[firstIndex]!
|
|
202
|
+
for (
|
|
203
|
+
let secondIndex = firstIndex + 1;
|
|
204
|
+
secondIndex < traces.length;
|
|
205
|
+
secondIndex++
|
|
206
|
+
) {
|
|
207
|
+
const otherTrace = traces[secondIndex]!
|
|
208
|
+
if (trace.globalConnNetId === otherTrace.globalConnNetId) continue
|
|
209
|
+
const isInitialBundleCrossing = this._isTraceBundle(trace, otherTrace)
|
|
210
|
+
if (
|
|
211
|
+
!isInitialBundleCrossing &&
|
|
212
|
+
!this.reroutedTraceIds.has(trace.mspPairId) &&
|
|
213
|
+
!this.reroutedTraceIds.has(otherTrace.mspPairId)
|
|
214
|
+
) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const crossings = findPerpendicularPathCrossings(
|
|
219
|
+
trace.tracePath,
|
|
220
|
+
otherTrace.tracePath,
|
|
221
|
+
)
|
|
222
|
+
for (const { pathSegmentIndex, otherPathSegmentIndex } of crossings) {
|
|
223
|
+
const crossing = {
|
|
224
|
+
trace,
|
|
225
|
+
segmentIndex: pathSegmentIndex,
|
|
226
|
+
otherTrace,
|
|
227
|
+
otherSegmentIndex: otherPathSegmentIndex,
|
|
228
|
+
isInitialBundleCrossing,
|
|
229
|
+
}
|
|
230
|
+
if (!this.ignoredCrossings.has(this._crossingKey(crossing))) {
|
|
231
|
+
return crossing
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _resolveCrossing(crossing: TraceCrossing) {
|
|
240
|
+
const chipBounds = this.chipObstacleSpatialIndex.chips.map(
|
|
241
|
+
(chip) => chip.bounds,
|
|
242
|
+
)
|
|
243
|
+
const candidates = [
|
|
244
|
+
...generatePerpendicularTraceDetours({
|
|
245
|
+
trace: crossing.trace,
|
|
246
|
+
segmentIndex: crossing.segmentIndex,
|
|
247
|
+
obstacleStart:
|
|
248
|
+
crossing.otherTrace.tracePath[crossing.otherSegmentIndex]!,
|
|
249
|
+
obstacleEnd:
|
|
250
|
+
crossing.otherTrace.tracePath[crossing.otherSegmentIndex + 1]!,
|
|
251
|
+
chipBounds,
|
|
252
|
+
clearance: this.input.paddingBuffer,
|
|
253
|
+
}),
|
|
254
|
+
...generatePerpendicularTraceDetours({
|
|
255
|
+
trace: crossing.otherTrace,
|
|
256
|
+
segmentIndex: crossing.otherSegmentIndex,
|
|
257
|
+
obstacleStart: crossing.trace.tracePath[crossing.segmentIndex]!,
|
|
258
|
+
obstacleEnd: crossing.trace.tracePath[crossing.segmentIndex + 1]!,
|
|
259
|
+
chipBounds,
|
|
260
|
+
clearance: this.input.paddingBuffer,
|
|
261
|
+
}),
|
|
262
|
+
]
|
|
263
|
+
const chipObstacles = getObstacleRects(this.input.inputProblem).filter(
|
|
264
|
+
(obstacle) => obstacle.kind === "chip",
|
|
265
|
+
)
|
|
266
|
+
// Prefer a globally clear route. If the initial bundle detour transfers
|
|
267
|
+
// the crossing, continue rip-up/reroute from that moved trace until clear.
|
|
268
|
+
const validCandidates = candidates
|
|
269
|
+
.map((candidate) => ({
|
|
270
|
+
...candidate,
|
|
271
|
+
collision: isPathColliding(
|
|
272
|
+
candidate.path,
|
|
273
|
+
this.input.allTraces,
|
|
274
|
+
candidate.traceId,
|
|
275
|
+
),
|
|
276
|
+
}))
|
|
277
|
+
.filter(
|
|
278
|
+
(candidate) =>
|
|
279
|
+
!isPathCollidingWithChipInterior(candidate.path, chipObstacles) &&
|
|
280
|
+
!isPathColliding(
|
|
281
|
+
candidate.path,
|
|
282
|
+
[crossing.trace, crossing.otherTrace],
|
|
283
|
+
candidate.traceId,
|
|
284
|
+
).isColliding &&
|
|
285
|
+
(crossing.isInitialBundleCrossing ||
|
|
286
|
+
!candidate.collision.isColliding),
|
|
287
|
+
)
|
|
288
|
+
.sort(
|
|
289
|
+
(first, second) =>
|
|
290
|
+
Number(first.collision.isColliding) -
|
|
291
|
+
Number(second.collision.isColliding) ||
|
|
292
|
+
getPathLength(first.path) - getPathLength(second.path),
|
|
293
|
+
)
|
|
294
|
+
const bestCandidate = validCandidates[0]
|
|
295
|
+
if (!bestCandidate) return false
|
|
296
|
+
|
|
297
|
+
const traceIndex = this.input.allTraces.findIndex(
|
|
298
|
+
(trace) => trace.mspPairId === bestCandidate.traceId,
|
|
299
|
+
)
|
|
300
|
+
this.input.allTraces[traceIndex] = {
|
|
301
|
+
...this.input.allTraces[traceIndex]!,
|
|
302
|
+
tracePath: bestCandidate.path,
|
|
303
|
+
}
|
|
304
|
+
this.reroutedTraceIds.add(bestCandidate.traceId)
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
139
308
|
private _resetAfterLShapProcessing() {
|
|
140
309
|
this.lShapeProcessingStep = "idle"
|
|
141
310
|
this.currentLShape = null
|
|
@@ -2,6 +2,13 @@ import type { Point } from "@tscircuit/math-utils"
|
|
|
2
2
|
import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
|
|
3
3
|
import type { TraceObstacle } from "./getTraceObstacles"
|
|
4
4
|
|
|
5
|
+
const EPS = 1e-6
|
|
6
|
+
|
|
7
|
+
export interface PerpendicularPathCrossing {
|
|
8
|
+
pathSegmentIndex: number
|
|
9
|
+
otherPathSegmentIndex: number
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
|
|
7
14
|
* It iterates through each segment of every obstacle and checks for intersections with the input segment.
|
|
@@ -34,3 +41,55 @@ export const findIntersectionsWithObstacles = (
|
|
|
34
41
|
|
|
35
42
|
return intersections
|
|
36
43
|
}
|
|
44
|
+
|
|
45
|
+
const isSamePoint = (first: Point, second: Point) =>
|
|
46
|
+
Math.abs(first.x - second.x) < EPS && Math.abs(first.y - second.y) < EPS
|
|
47
|
+
|
|
48
|
+
export const findPerpendicularPathCrossings = (
|
|
49
|
+
path: Point[],
|
|
50
|
+
otherPath: Point[],
|
|
51
|
+
): PerpendicularPathCrossing[] => {
|
|
52
|
+
const crossings: PerpendicularPathCrossing[] = []
|
|
53
|
+
|
|
54
|
+
// Terminal segments connect to pins and are allowed to meet other traces at
|
|
55
|
+
// their endpoints. Only internal, strict crossings need to be untangled.
|
|
56
|
+
for (
|
|
57
|
+
let pathSegmentIndex = 1;
|
|
58
|
+
pathSegmentIndex < path.length - 2;
|
|
59
|
+
pathSegmentIndex++
|
|
60
|
+
) {
|
|
61
|
+
const start = path[pathSegmentIndex]!
|
|
62
|
+
const end = path[pathSegmentIndex + 1]!
|
|
63
|
+
const isVertical = Math.abs(start.x - end.x) < EPS
|
|
64
|
+
|
|
65
|
+
for (
|
|
66
|
+
let otherPathSegmentIndex = 1;
|
|
67
|
+
otherPathSegmentIndex < otherPath.length - 2;
|
|
68
|
+
otherPathSegmentIndex++
|
|
69
|
+
) {
|
|
70
|
+
const otherStart = otherPath[otherPathSegmentIndex]!
|
|
71
|
+
const otherEnd = otherPath[otherPathSegmentIndex + 1]!
|
|
72
|
+
const otherIsVertical = Math.abs(otherStart.x - otherEnd.x) < EPS
|
|
73
|
+
if (isVertical === otherIsVertical) continue
|
|
74
|
+
|
|
75
|
+
const intersection = getSegmentIntersection(
|
|
76
|
+
start,
|
|
77
|
+
end,
|
|
78
|
+
otherStart,
|
|
79
|
+
otherEnd,
|
|
80
|
+
)
|
|
81
|
+
if (
|
|
82
|
+
!intersection ||
|
|
83
|
+
[start, end, otherStart, otherEnd].some((point) =>
|
|
84
|
+
isSamePoint(point, intersection),
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
crossings.push({ pathSegmentIndex, otherPathSegmentIndex })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return crossings
|
|
95
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Point } from "@tscircuit/math-utils"
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
2
|
import type { LShape } from "./findAllLShapedTurns"
|
|
3
3
|
import type { Rectangle } from "./generateRectangleCandidates"
|
|
4
|
+
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { simplifyPath } from "../simplifyPath"
|
|
4
6
|
|
|
5
7
|
const EPS = 1e-6
|
|
6
8
|
|
|
@@ -105,3 +107,64 @@ export const generateLShapeRerouteCandidates = ({
|
|
|
105
107
|
|
|
106
108
|
return [[i1_padded, c2, i2_padded]]
|
|
107
109
|
}
|
|
110
|
+
|
|
111
|
+
export interface PerpendicularTraceDetourInput {
|
|
112
|
+
trace: SolvedTracePath
|
|
113
|
+
segmentIndex: number
|
|
114
|
+
obstacleStart: Point
|
|
115
|
+
obstacleEnd: Point
|
|
116
|
+
chipBounds: Bounds[]
|
|
117
|
+
clearance: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface TraceDetourCandidate {
|
|
121
|
+
traceId: string
|
|
122
|
+
path: Point[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const generatePerpendicularTraceDetours = ({
|
|
126
|
+
trace,
|
|
127
|
+
segmentIndex,
|
|
128
|
+
obstacleStart,
|
|
129
|
+
obstacleEnd,
|
|
130
|
+
chipBounds,
|
|
131
|
+
clearance,
|
|
132
|
+
}: PerpendicularTraceDetourInput): TraceDetourCandidate[] => {
|
|
133
|
+
const buildDetours = (path: Point[], index: number) => {
|
|
134
|
+
const start = path[index]!
|
|
135
|
+
const end = path[index + 1]!
|
|
136
|
+
const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x"
|
|
137
|
+
const detourAxis = movingAxis === "x" ? "y" : "x"
|
|
138
|
+
const gate =
|
|
139
|
+
obstacleStart[movingAxis] +
|
|
140
|
+
Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance
|
|
141
|
+
const obstacleRange = [obstacleStart[detourAxis], obstacleEnd[detourAxis]]
|
|
142
|
+
const lowBound = detourAxis === "x" ? "minX" : "minY"
|
|
143
|
+
const highBound = detourAxis === "x" ? "maxX" : "maxY"
|
|
144
|
+
const detourCoordinates = [
|
|
145
|
+
Math.min(...obstacleRange) - clearance,
|
|
146
|
+
Math.max(...obstacleRange) + clearance,
|
|
147
|
+
...chipBounds.flatMap((bounds) => [
|
|
148
|
+
bounds[lowBound] - clearance,
|
|
149
|
+
bounds[highBound] + clearance,
|
|
150
|
+
]),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
return [...new Set(detourCoordinates)].map((detour) =>
|
|
154
|
+
simplifyPath([
|
|
155
|
+
...path.slice(0, index + 1),
|
|
156
|
+
{ ...start, [movingAxis]: gate },
|
|
157
|
+
{ ...start, [movingAxis]: gate, [detourAxis]: detour },
|
|
158
|
+
{ ...end, [detourAxis]: detour },
|
|
159
|
+
...path.slice(index + 2),
|
|
160
|
+
]),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const reversedPath = [...trace.tracePath].reverse()
|
|
165
|
+
const reversedIndex = trace.tracePath.length - 2 - segmentIndex
|
|
166
|
+
return [
|
|
167
|
+
...buildDetours(trace.tracePath, segmentIndex),
|
|
168
|
+
...buildDetours(reversedPath, reversedIndex).map((path) => path.reverse()),
|
|
169
|
+
].map((path) => ({ traceId: trace.mspPairId, path }))
|
|
170
|
+
}
|