@tscircuit/schematic-trace-solver 0.0.106 → 0.0.108

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 (21) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +470 -213
  3. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +58 -27
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +1 -10
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateInternalSegmentCollisionDetours.ts +94 -0
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +10 -0
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +5 -1
  8. package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +178 -9
  9. package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +59 -0
  10. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +64 -1
  11. package/package.json +1 -1
  12. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
  13. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +5 -5
  14. package/tests/bug-reports/bug-report-20260708T095725Z/bug-report-20260708T095725Z.test.ts +15 -0
  15. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
  16. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +64 -0
  17. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +1 -1
  18. package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +200 -0
  19. package/tests/repros/repro-tps61222-trace-intersection.test.ts +41 -0
  20. package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +6 -0
  21. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-internal-segment-collision-detours.test.ts +49 -0
@@ -7,16 +7,18 @@ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/Singl
7
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
8
  import type { InputChip, InputProblem } from "lib/types/InputProblem"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
- import type { RectPadding } from "lib/utils/textBoxBounds"
10
+ import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
11
11
  import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
12
12
  import { calculateDirectShortPath } from "./calculateDirectShortPath"
13
13
  import {
14
14
  findFirstCollision,
15
15
  isHorizontal,
16
16
  isVertical,
17
+ segmentIntersectsRect,
17
18
  segmentOverlapsRectBoundary,
18
19
  } from "./collisions"
19
20
  import { generateEndpointCollisionDetours } from "./generateEndpointCollisionDetours"
21
+ import { generateInternalSegmentCollisionDetours } from "./generateInternalSegmentCollisionDetours"
20
22
  import {
21
23
  type Axis,
22
24
  aabbFromPoints,
@@ -24,7 +26,12 @@ import {
24
26
  midBetweenPointAndRect,
25
27
  } from "./mid"
26
28
  import { pathKey, shiftSegmentOrth } from "./pathOps"
27
- import { getObstacleRects, type ObstacleRect } from "./rect"
29
+ import {
30
+ getObstacleRects,
31
+ isTextBoxObstacle,
32
+ type ObstacleRect,
33
+ type TextBoxObstacleRect,
34
+ } from "./rect"
28
35
 
29
36
  type PathKey = string
30
37
 
@@ -58,8 +65,8 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
58
65
  chipMap: Record<string, InputChip>
59
66
 
60
67
  obstacles: ObstacleRect[]
61
- textObstacles: Set<ObstacleRect>
62
- endpointTextObstacles: Set<ObstacleRect>
68
+ textObstacles: Set<TextBoxObstacleRect>
69
+ endpointTextObstacles: Set<TextBoxObstacleRect>
63
70
  aabb: { minX: number; maxX: number; minY: number; maxY: number }
64
71
 
65
72
  baseElbow: Point[]
@@ -95,18 +102,17 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
95
102
  this.obstacles = getObstacleRects(this.inputProblem, {
96
103
  textBoxPadding: this.getTextBoxPaddingForConnectionPair(),
97
104
  })
98
- this.textObstacles = new Set(
99
- this.obstacles.filter((r) => r.kind === "text_box"),
100
- )
105
+ this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle))
101
106
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
102
107
  this.endpointTextObstacles = new Set(
103
108
  endpointChipIds.size > 1
104
- ? this.obstacles.filter(
105
- (r) =>
106
- r.kind === "text_box" &&
107
- r.textBox.chipId !== undefined &&
108
- endpointChipIds.has(r.textBox.chipId),
109
- )
109
+ ? this.obstacles
110
+ .filter(isTextBoxObstacle)
111
+ .filter(
112
+ (obstacle) =>
113
+ obstacle.textBox.chipId !== undefined &&
114
+ endpointChipIds.has(obstacle.textBox.chipId),
115
+ )
110
116
  : [],
111
117
  )
112
118
 
@@ -356,7 +362,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
356
362
  )
357
363
  ) {
358
364
  for (const textObstacle of this.endpointTextObstacles) {
359
- excludedRects.add(textObstacle)
365
+ // Outside-pin-band routing may cross label padding around endpoint
366
+ // text, but the actual text bounds remain a hard obstacle.
367
+ const textBounds = getTextBoxBounds(textObstacle.textBox)
368
+ if (!segmentIntersectsRect(segmentStart, segmentEnd, textBounds)) {
369
+ excludedRects.add(textObstacle)
370
+ }
360
371
  }
361
372
  }
362
373
 
@@ -500,18 +511,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
500
511
  pinBandPenalty: number
501
512
  }> = []
502
513
 
503
- const addShiftedCandidate = (
504
- candidateSegIndex: number,
505
- candidateAxis: Axis,
506
- coord: number,
507
- ) => {
508
- const newPath = shiftSegmentOrth(
509
- path,
510
- candidateSegIndex,
511
- candidateAxis,
512
- coord,
513
- )
514
- if (!newPath) return
514
+ const addPathCandidate = (newPath: Point[]) => {
515
515
  const key = pathKey(newPath)
516
516
  if (this.visited.has(key)) return
517
517
  this.visited.add(key)
@@ -525,11 +525,42 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
525
525
  })
526
526
  }
527
527
 
528
+ const addShiftedCandidate = (
529
+ candidateSegIndex: number,
530
+ candidateAxis: Axis,
531
+ coord: number,
532
+ ) => {
533
+ const newPath = shiftSegmentOrth(
534
+ path,
535
+ candidateSegIndex,
536
+ candidateAxis,
537
+ coord,
538
+ )
539
+ if (newPath) addPathCandidate(newPath)
540
+ }
541
+
542
+ const lastSegIndex = path.length - 2
543
+ if (
544
+ this.connectionPair === undefined &&
545
+ rect.kind === "text_box" &&
546
+ this.endpointTextObstacles.has(rect) &&
547
+ !collisionRects.has(rect) &&
548
+ originalSegIndex > 0 &&
549
+ originalSegIndex < lastSegIndex
550
+ ) {
551
+ for (const detour of generateInternalSegmentCollisionDetours({
552
+ path,
553
+ collidingSegmentIndex: originalSegIndex,
554
+ obstacle: rect,
555
+ })) {
556
+ addPathCandidate(detour)
557
+ }
558
+ }
559
+
528
560
  for (const coord of candidates) {
529
561
  addShiftedCandidate(segIndex, axis, coord)
530
562
  }
531
563
 
532
- const lastSegIndex = path.length - 2
533
564
  const adjacentSegmentIndexes =
534
565
  originalSegIndex === 1
535
566
  ? [2]
@@ -1,6 +1,7 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import { isHorizontal, isVertical } from "./collisions"
3
3
  import { type Axis, midBetweenPointAndRect } from "./mid"
4
+ import { hasOnlyNonzeroOrthogonalSegments } from "./pathOps"
4
5
  import type { ObstacleRect } from "./rect"
5
6
 
6
7
  const getSegmentAxis = (start: Point, end: Point): Axis | null => {
@@ -9,16 +10,6 @@ const getSegmentAxis = (start: Point, end: Point): Axis | null => {
9
10
  return null
10
11
  }
11
12
 
12
- const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
13
- path.every((point, index) => {
14
- const nextPoint = path[index + 1]
15
- if (!nextPoint) return true
16
- if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
17
- return false
18
- }
19
- return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
20
- })
21
-
22
13
  export const generateEndpointCollisionDetours = ({
23
14
  path,
24
15
  collidingSegmentIndex,
@@ -0,0 +1,94 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { isHorizontal, isVertical } from "./collisions"
3
+ import { hasOnlyNonzeroOrthogonalSegments } from "./pathOps"
4
+ import type { RectBounds } from "./rect"
5
+
6
+ const EPS = 1e-9
7
+ const DEFAULT_CLEARANCE = 0.2
8
+
9
+ const isStrictlyBetween = (value: number, a: number, b: number) =>
10
+ value > Math.min(a, b) + EPS && value < Math.max(a, b) - EPS
11
+
12
+ export const generateInternalSegmentCollisionDetours = ({
13
+ path,
14
+ collidingSegmentIndex,
15
+ obstacle,
16
+ clearance = DEFAULT_CLEARANCE,
17
+ }: {
18
+ path: Point[]
19
+ collidingSegmentIndex: number
20
+ obstacle: RectBounds
21
+ clearance?: number
22
+ }): Point[][] => {
23
+ if (collidingSegmentIndex <= 0 || collidingSegmentIndex >= path.length - 2) {
24
+ return []
25
+ }
26
+
27
+ const start = path[collidingSegmentIndex]!
28
+ const end = path[collidingSegmentIndex + 1]!
29
+ const detourPoints: Point[][] = []
30
+
31
+ if (isHorizontal(start, end)) {
32
+ const movingRight = start.x < end.x
33
+ const entryX = movingRight
34
+ ? obstacle.minX - clearance
35
+ : obstacle.maxX + clearance
36
+ const exitX = movingRight
37
+ ? obstacle.maxX + clearance
38
+ : obstacle.minX - clearance
39
+
40
+ if (
41
+ !isStrictlyBetween(entryX, start.x, end.x) ||
42
+ !isStrictlyBetween(exitX, start.x, end.x)
43
+ ) {
44
+ return []
45
+ }
46
+
47
+ for (const detourY of [
48
+ obstacle.minY - clearance,
49
+ obstacle.maxY + clearance,
50
+ ]) {
51
+ detourPoints.push([
52
+ { x: entryX, y: start.y },
53
+ { x: entryX, y: detourY },
54
+ { x: exitX, y: detourY },
55
+ { x: exitX, y: end.y },
56
+ ])
57
+ }
58
+ } else if (isVertical(start, end)) {
59
+ const movingUp = start.y < end.y
60
+ const entryY = movingUp
61
+ ? obstacle.minY - clearance
62
+ : obstacle.maxY + clearance
63
+ const exitY = movingUp
64
+ ? obstacle.maxY + clearance
65
+ : obstacle.minY - clearance
66
+
67
+ if (
68
+ !isStrictlyBetween(entryY, start.y, end.y) ||
69
+ !isStrictlyBetween(exitY, start.y, end.y)
70
+ ) {
71
+ return []
72
+ }
73
+
74
+ for (const detourX of [
75
+ obstacle.minX - clearance,
76
+ obstacle.maxX + clearance,
77
+ ]) {
78
+ detourPoints.push([
79
+ { x: start.x, y: entryY },
80
+ { x: detourX, y: entryY },
81
+ { x: detourX, y: exitY },
82
+ { x: end.x, y: exitY },
83
+ ])
84
+ }
85
+ }
86
+
87
+ return detourPoints
88
+ .map((points) => [
89
+ ...path.slice(0, collidingSegmentIndex + 1),
90
+ ...points,
91
+ ...path.slice(collidingSegmentIndex + 1),
92
+ ])
93
+ .filter(hasOnlyNonzeroOrthogonalSegments)
94
+ }
@@ -5,6 +5,16 @@ const EPS = 1e-9
5
5
 
6
6
  export type Axis = "x" | "y"
7
7
 
8
+ export const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
9
+ path.every((point, index) => {
10
+ const nextPoint = path[index + 1]
11
+ if (!nextPoint) return true
12
+ if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
13
+ return false
14
+ }
15
+ return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
16
+ })
17
+
8
18
  export const shiftSegmentOrth = (
9
19
  pts: Point[],
10
20
  segIndex: number,
@@ -1,5 +1,5 @@
1
- import type { InputChip, InputProblem } from "lib/types/InputProblem"
2
1
  import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
2
+ import type { InputChip, InputProblem } from "lib/types/InputProblem"
3
3
  import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
4
4
 
5
5
  export type RectBounds = {
@@ -21,6 +21,10 @@ export type TextBoxObstacleRect = RectBounds & {
21
21
 
22
22
  export type ObstacleRect = ChipObstacleRect | TextBoxObstacleRect
23
23
 
24
+ export const isTextBoxObstacle = (
25
+ obstacle: ObstacleRect,
26
+ ): obstacle is TextBoxObstacleRect => obstacle.kind === "text_box"
27
+
24
28
  export const chipToRect = (chip: InputChip): ChipObstacleRect => {
25
29
  const b = getInputChipBounds(chip)
26
30
  return { kind: "chip", chipId: chip.chipId, ...b }
@@ -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.108",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",