@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.
@@ -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 { findIntersectionsWithObstacles } from "./findIntersectionsWithObstacles"
10
- import { generateLShapeRerouteCandidates } from "./generateLShapeRerouteCandidates"
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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.106",
4
+ "version": "0.0.107",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",