@tscircuit/schematic-trace-solver 0.0.105 → 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.
Files changed (20) hide show
  1. package/dist/index.js +403 -166
  2. package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +78 -27
  3. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +40 -20
  4. package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +178 -9
  5. package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +59 -0
  6. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +64 -1
  7. package/package.json +1 -1
  8. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
  9. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
  10. package/tests/examples/__snapshots__/example13.snap.svg +3 -3
  11. package/tests/examples/__snapshots__/example29.snap.svg +23 -23
  12. package/tests/examples/example19.test.ts +4 -0
  13. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +256 -0
  14. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +221 -0
  15. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +1 -1
  16. package/tests/repros/assets/repro-example35-minimize-trace-crossing.input.json +1561 -0
  17. package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +200 -0
  18. package/tests/repros/repro-example35-minimize-trace-crossing.test.ts +22 -0
  19. package/tests/repros/repro-tps61222-trace-intersection.test.ts +14 -0
  20. package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +6 -0
@@ -3,13 +3,18 @@ import { minimizeTurns } from "./turnMinimization"
3
3
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
4
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
5
  import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
6
+ import { countTurns } from "./countTurns"
7
+ import {
8
+ getVisibleTraceLength,
9
+ getVisibleTraceSegmentCount,
10
+ RAIL_ALIGNMENT_EPSILON,
11
+ } from "./sameNetRailAlignment/geometry"
6
12
 
7
13
  /**
8
- * Minimizes the turns of a target trace while considering different-net traces and labels as obstacles.
9
- * This function first identifies the target trace and separates traces from other nets, which are then treated as obstacles.
10
- * It also filters out labels that belong to the target trace's net, so they don't act as obstacles.
11
- * The function then combines static obstacles (from the input problem) with the other traces and filtered labels to create a comprehensive set of obstacles.
12
- * Finally, it uses a turn minimization algorithm to find a new path for the target trace that avoids these combined obstacles.
14
+ * Minimizes turns with a strict pass that treats every other trace as an
15
+ * obstacle and a relaxed pass that permits joining endpoint-sharing same-net
16
+ * branches. Equivalent-turn routes are compared by their rendered same-net
17
+ * complexity so aligned rails win without arbitrarily shifting local routes.
13
18
  */
14
19
  export const minimizeTurnsWithFilteredLabels = ({
15
20
  targetMspConnectionPairId,
@@ -33,27 +38,31 @@ export const minimizeTurnsWithFilteredLabels = ({
33
38
  throw new Error(`Target trace ${targetMspConnectionPairId} not found`)
34
39
  }
35
40
 
36
- // Same-net traces are valid connection targets: letting the candidate path
37
- // coincide with them removes unnecessary detours and forms clean junctions.
38
- const obstacleTraces = traces.filter(
39
- (t) =>
40
- t.mspPairId !== targetMspConnectionPairId &&
41
- t.globalConnNetId !== targetTrace.globalConnNetId,
41
+ const targetPinIds = new Set(targetTrace.pinIds)
42
+ const otherTraces = traces.filter(
43
+ (trace) => trace.mspPairId !== targetMspConnectionPairId,
42
44
  )
45
+ const relaxedObstacleTraces = otherTraces.filter((trace) => {
46
+ const sharesEndpoint = trace.pinIds.some((pinId) => targetPinIds.has(pinId))
47
+ return (
48
+ trace.globalConnNetId !== targetTrace.globalConnNetId || !sharesEndpoint
49
+ )
50
+ })
43
51
 
44
52
  const TRACE_WIDTH = 0.01
45
- const traceObstacles = obstacleTraces.flatMap((trace, i) =>
46
- trace.tracePath.slice(0, -1).map((p1, pi) => {
47
- const p2 = trace.tracePath[pi + 1]!
48
- return {
49
- chipId: `trace-obstacle-${i}-${pi}`,
50
- minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2,
51
- minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2,
52
- maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2,
53
- maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2,
54
- }
55
- }),
56
- )
53
+ const getTraceObstacles = (obstacleTraces: SolvedTracePath[]) =>
54
+ obstacleTraces.flatMap((trace, i) =>
55
+ trace.tracePath.slice(0, -1).map((p1, pi) => {
56
+ const p2 = trace.tracePath[pi + 1]!
57
+ return {
58
+ chipId: `trace-obstacle-${i}-${pi}`,
59
+ minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2,
60
+ minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2,
61
+ maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2,
62
+ maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2,
63
+ }
64
+ }),
65
+ )
57
66
 
58
67
  const staticObstaclesRaw = getObstacleRects(inputProblem)
59
68
  const PADDING = 0.01
@@ -65,8 +74,6 @@ export const minimizeTurnsWithFilteredLabels = ({
65
74
  maxY: obs.maxY + PADDING,
66
75
  }))
67
76
 
68
- const combinedObstacles = [...staticObstacles, ...traceObstacles]
69
-
70
77
  const originalPath = targetTrace.tracePath
71
78
  const filteredLabels = allLabelPlacements.filter((label) => {
72
79
  const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId]
@@ -83,13 +90,57 @@ export const minimizeTurnsWithFilteredLabels = ({
83
90
  maxY: nl.center.y + nl.height / 2 + paddingBuffer,
84
91
  }))
85
92
 
86
- const newPath = minimizeTurns({
93
+ const strictPath = minimizeTurns({
87
94
  path: originalPath,
88
- obstacles: combinedObstacles,
95
+ obstacles: [...staticObstacles, ...getTraceObstacles(otherTraces)],
89
96
  labelBounds,
90
97
  originalPath: originalPath,
91
98
  })
92
99
 
100
+ const relaxedPath = minimizeTurns({
101
+ path: originalPath,
102
+ obstacles: [
103
+ ...staticObstacles,
104
+ ...getTraceObstacles(relaxedObstacleTraces),
105
+ ],
106
+ labelBounds,
107
+ originalPath: originalPath,
108
+ })
109
+
110
+ const sameNetTraces = otherTraces.filter(
111
+ (trace) => trace.globalConnNetId === targetTrace.globalConnNetId,
112
+ )
113
+ const getSameNetReadability = (tracePath: SolvedTracePath["tracePath"]) => {
114
+ const tracesWithCandidate = [
115
+ ...sameNetTraces,
116
+ { ...targetTrace, tracePath },
117
+ ]
118
+ return {
119
+ segmentCount: getVisibleTraceSegmentCount(tracesWithCandidate),
120
+ visibleLength: getVisibleTraceLength(tracesWithCandidate),
121
+ }
122
+ }
123
+
124
+ const strictTurns = countTurns(strictPath)
125
+ const relaxedTurns = countTurns(relaxedPath)
126
+ let newPath = strictPath
127
+
128
+ if (relaxedTurns < strictTurns) {
129
+ newPath = relaxedPath
130
+ } else if (relaxedTurns === strictTurns) {
131
+ const strictReadability = getSameNetReadability(strictPath)
132
+ const relaxedReadability = getSameNetReadability(relaxedPath)
133
+
134
+ if (
135
+ relaxedReadability.segmentCount < strictReadability.segmentCount ||
136
+ (relaxedReadability.segmentCount === strictReadability.segmentCount &&
137
+ relaxedReadability.visibleLength <
138
+ strictReadability.visibleLength - RAIL_ALIGNMENT_EPSILON)
139
+ ) {
140
+ newPath = relaxedPath
141
+ }
142
+ }
143
+
93
144
  return {
94
145
  ...targetTrace,
95
146
  tracePath: newPath,
@@ -48,7 +48,7 @@ interface Interval {
48
48
  max: number
49
49
  }
50
50
 
51
- const getMergedIntervalLength = (intervals: Interval[]) => {
51
+ const getMergedIntervalMetrics = (intervals: Interval[]) => {
52
52
  const groups: Interval[][] = []
53
53
  for (const interval of intervals) {
54
54
  const group = groups.find((items) =>
@@ -58,28 +58,35 @@ const getMergedIntervalLength = (intervals: Interval[]) => {
58
58
  else groups.push([interval])
59
59
  }
60
60
 
61
- return groups.reduce((total, group) => {
62
- const sorted = [...group].sort((a, b) => a.min - b.min)
63
- let groupLength = 0
64
- let currentMin = sorted[0]!.min
65
- let currentMax = sorted[0]!.max
66
-
67
- for (const interval of sorted.slice(1)) {
68
- if (interval.min <= currentMax + RAIL_ALIGNMENT_EPSILON) {
69
- currentMax = Math.max(currentMax, interval.max)
70
- } else {
71
- groupLength += currentMax - currentMin
72
- currentMin = interval.min
73
- currentMax = interval.max
61
+ return groups.reduce(
62
+ (total, group) => {
63
+ const sorted = [...group].sort((a, b) => a.min - b.min)
64
+ let groupLength = 0
65
+ let segmentCount = 1
66
+ let currentMin = sorted[0]!.min
67
+ let currentMax = sorted[0]!.max
68
+
69
+ for (const interval of sorted.slice(1)) {
70
+ if (interval.min <= currentMax + RAIL_ALIGNMENT_EPSILON) {
71
+ currentMax = Math.max(currentMax, interval.max)
72
+ } else {
73
+ groupLength += currentMax - currentMin
74
+ segmentCount++
75
+ currentMin = interval.min
76
+ currentMax = interval.max
77
+ }
74
78
  }
75
- }
76
79
 
77
- return total + groupLength + currentMax - currentMin
78
- }, 0)
80
+ return {
81
+ length: total.length + groupLength + currentMax - currentMin,
82
+ segmentCount: total.segmentCount + segmentCount,
83
+ }
84
+ },
85
+ { length: 0, segmentCount: 0 },
86
+ )
79
87
  }
80
88
 
81
- /** Returns rendered length after overlapping collinear runs are merged. */
82
- export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
89
+ const getVisibleTraceMetrics = (traces: SolvedTracePath[]) => {
83
90
  const horizontal: Interval[] = []
84
91
  const vertical: Interval[] = []
85
92
 
@@ -104,5 +111,18 @@ export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
104
111
  }
105
112
  }
106
113
 
107
- return getMergedIntervalLength(horizontal) + getMergedIntervalLength(vertical)
114
+ const horizontalMetrics = getMergedIntervalMetrics(horizontal)
115
+ const verticalMetrics = getMergedIntervalMetrics(vertical)
116
+ return {
117
+ length: horizontalMetrics.length + verticalMetrics.length,
118
+ segmentCount: horizontalMetrics.segmentCount + verticalMetrics.segmentCount,
119
+ }
108
120
  }
121
+
122
+ /** Returns rendered length after overlapping collinear runs are merged. */
123
+ export const getVisibleTraceLength = (traces: SolvedTracePath[]) =>
124
+ getVisibleTraceMetrics(traces).length
125
+
126
+ /** Returns the number of distinct collinear runs visible on the schematic. */
127
+ export const getVisibleTraceSegmentCount = (traces: SolvedTracePath[]) =>
128
+ getVisibleTraceMetrics(traces).segmentCount
@@ -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.105",
4
+ "version": "0.0.107",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",