@tscircuit/schematic-trace-solver 0.0.31 → 0.0.33

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 (32) hide show
  1. package/CLAUDE.md +8 -0
  2. package/dist/index.d.ts +24 -53
  3. package/dist/index.js +494 -807
  4. package/lib/index.ts +1 -0
  5. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +54 -3
  6. package/lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines.ts +62 -0
  7. package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +7 -2
  8. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +5 -11
  9. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +37 -7
  10. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/generateElbowVariants.ts +7 -0
  11. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +239 -0
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +57 -0
  13. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +97 -0
  14. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +65 -0
  15. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +19 -0
  16. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +14 -14
  17. package/package.json +1 -1
  18. package/site/SchematicTracePipelineSolver/SchematicTracePipelineSolver03.page.tsx +486 -0
  19. package/site/examples/example09.page.tsx +1 -1
  20. package/tests/examples/__snapshots__/example01.snap.svg +29 -29
  21. package/tests/examples/__snapshots__/example02.snap.svg +13 -10
  22. package/tests/examples/__snapshots__/example04.snap.svg +12 -12
  23. package/tests/examples/__snapshots__/example05.snap.svg +38 -38
  24. package/tests/examples/__snapshots__/example06.snap.svg +14 -14
  25. package/tests/examples/__snapshots__/example08.snap.svg +29 -23
  26. package/tests/examples/__snapshots__/example09.snap.svg +119 -149
  27. package/tests/examples/__snapshots__/example11.snap.svg +39 -33
  28. package/tests/examples/__snapshots__/example12.snap.svg +32 -29
  29. package/tests/examples/__snapshots__/example13.snap.svg +87 -84
  30. package/tests/examples/__snapshots__/example15.snap.svg +57 -57
  31. package/tests/examples/__snapshots__/example16.snap.svg +3 -3
  32. package/tests/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver_repro03.test.ts +491 -0
package/lib/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
2
2
  export * from "./types/InputProblem"
3
+ export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
@@ -1,8 +1,14 @@
1
1
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
2
- import type { InputChip, InputPin, InputProblem } from "lib/types/InputProblem"
2
+ import type {
3
+ InputChip,
4
+ InputPin,
5
+ InputProblem,
6
+ PinId,
7
+ } from "lib/types/InputProblem"
3
8
  import { ConnectivityMap } from "connectivity-map"
4
9
  import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
5
10
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
11
+ import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
6
12
  import type { GraphicsObject } from "graphics-debug"
7
13
  import { getColorFromString } from "lib/utils/getColorFromString"
8
14
  import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
@@ -104,6 +110,22 @@ export class MspConnectionPairSolver extends BaseSolver {
104
110
  // Too far apart; skip creating an MSP pair for this net
105
111
  return
106
112
  }
113
+ // Avoid pairs that would cross restricted center lines
114
+ const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
115
+ PinId,
116
+ InputPin & { chipId: string }
117
+ >
118
+ if (
119
+ doesPairCrossRestrictedCenterLines({
120
+ inputProblem: this.inputProblem,
121
+ chipMap: this.chipMap,
122
+ pinIdMap,
123
+ p1,
124
+ p2,
125
+ })
126
+ ) {
127
+ return
128
+ }
107
129
 
108
130
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
109
131
  const userNetId =
@@ -122,12 +144,41 @@ export class MspConnectionPairSolver extends BaseSolver {
122
144
 
123
145
  // There are more than 3 pins, so we need to run MSP to find the best pairs
124
146
 
147
+ const pinIdMap = new Map(Object.entries(this.pinMap)) as Map<
148
+ PinId,
149
+ InputPin & { chipId: string }
150
+ >
125
151
  const msp = getOrthogonalMinimumSpanningTree(
126
152
  directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean),
127
- { maxDistance: this.maxMspPairDistance },
153
+ {
154
+ maxDistance: this.maxMspPairDistance,
155
+ forbidEdge: (a, b) =>
156
+ doesPairCrossRestrictedCenterLines({
157
+ inputProblem: this.inputProblem,
158
+ chipMap: this.chipMap,
159
+ pinIdMap,
160
+ p1: a as InputPin & { chipId: string },
161
+ p2: b as InputPin & { chipId: string },
162
+ }),
163
+ },
128
164
  )
129
165
 
130
166
  for (const [pin1, pin2] of msp) {
167
+ const p1Obj = this.pinMap[pin1!]!
168
+ const p2Obj = this.pinMap[pin2!]!
169
+ // Skip any edge that would cross a restricted center line (safety filter)
170
+ if (
171
+ doesPairCrossRestrictedCenterLines({
172
+ inputProblem: this.inputProblem,
173
+ chipMap: this.chipMap,
174
+ pinIdMap,
175
+ p1: p1Obj,
176
+ p2: p2Obj,
177
+ })
178
+ ) {
179
+ continue
180
+ }
181
+
131
182
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
132
183
  const userNetId =
133
184
  this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
@@ -136,7 +187,7 @@ export class MspConnectionPairSolver extends BaseSolver {
136
187
  dcConnNetId: dcNetId,
137
188
  globalConnNetId,
138
189
  userNetId,
139
- pins: [this.pinMap[pin1!]!, this.pinMap[pin2!]!],
190
+ pins: [p1Obj, p2Obj],
140
191
  })
141
192
  }
142
193
  }
@@ -0,0 +1,62 @@
1
+ import type {
2
+ InputChip,
3
+ InputPin,
4
+ InputProblem,
5
+ PinId,
6
+ } from "lib/types/InputProblem"
7
+ import { getRestrictedCenterLines } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines"
8
+
9
+ export const doesPairCrossRestrictedCenterLines = (params: {
10
+ inputProblem: InputProblem
11
+ chipMap: Record<string, InputChip>
12
+ pinIdMap: Map<PinId, InputPin & { chipId: string }>
13
+ p1: InputPin & { chipId: string }
14
+ p2: InputPin & { chipId: string }
15
+ }): boolean => {
16
+ const { inputProblem, chipMap, pinIdMap, p1, p2 } = params
17
+
18
+ const restrictedCenterLines = getRestrictedCenterLines({
19
+ pins: [p1, p2],
20
+ inputProblem,
21
+ pinIdMap,
22
+ chipMap,
23
+ })
24
+
25
+ if (restrictedCenterLines.size === 0) return false
26
+
27
+ const EPS = 1e-9
28
+
29
+ const crossesSegment = (
30
+ a: { x: number; y: number },
31
+ b: { x: number; y: number },
32
+ ): boolean => {
33
+ for (const [, rcl] of restrictedCenterLines) {
34
+ if (rcl.axes.has("x") && typeof rcl.x === "number") {
35
+ if ((a.x - rcl.x) * (b.x - rcl.x) < -EPS) return true
36
+ }
37
+ if (rcl.axes.has("y") && typeof rcl.y === "number") {
38
+ if ((a.y - rcl.y) * (b.y - rcl.y) < -EPS) return true
39
+ }
40
+ }
41
+ return false
42
+ }
43
+
44
+ // If already aligned on one axis, just check that single segment
45
+ if (Math.abs(p1.x - p2.x) < EPS || Math.abs(p1.y - p2.y) < EPS) {
46
+ return crossesSegment({ x: p1.x, y: p1.y }, { x: p2.x, y: p2.y })
47
+ }
48
+
49
+ // Two L-shape possibilities: horizontal-then-vertical or vertical-then-horizontal
50
+ const elbowHV = { x: p2.x, y: p1.y }
51
+ const elbowVH = { x: p1.x, y: p2.y }
52
+
53
+ const hvCrosses =
54
+ crossesSegment({ x: p1.x, y: p1.y }, elbowHV) ||
55
+ crossesSegment(elbowHV, { x: p2.x, y: p2.y })
56
+ const vhCrosses =
57
+ crossesSegment({ x: p1.x, y: p1.y }, elbowVH) ||
58
+ crossesSegment(elbowVH, { x: p2.x, y: p2.y })
59
+
60
+ // Forbid the pair only if both L-shape routes would cross a restricted center line
61
+ return hvCrosses && vhCrosses
62
+ }
@@ -18,7 +18,10 @@ import type { InputPin, PinId } from "lib/types/InputProblem"
18
18
  */
19
19
  export function getOrthogonalMinimumSpanningTree(
20
20
  pins: InputPin[],
21
- opts: { maxDistance?: number } = {},
21
+ opts: {
22
+ maxDistance?: number
23
+ forbidEdge?: (a: InputPin, b: InputPin) => boolean
24
+ } = {},
22
25
  ): Array<[PinId, PinId]> {
23
26
  const n = pins.length
24
27
  const maxDistance = opts?.maxDistance ?? Number.POSITIVE_INFINITY
@@ -82,7 +85,9 @@ export function getOrthogonalMinimumSpanningTree(
82
85
  for (let v = 0; v < n; v++) {
83
86
  if (!inTree[v]) {
84
87
  const d0 = manhattan(pins[u], pins[v])
85
- const d = d0 > maxDistance ? Number.POSITIVE_INFINITY : d0
88
+ const isForbidden = opts?.forbidEdge?.(pins[u], pins[v]) ?? false
89
+ const d =
90
+ d0 > maxDistance || isForbidden ? Number.POSITIVE_INFINITY : d0
86
91
  if (
87
92
  d < bestDist[v] ||
88
93
  (d === bestDist[v] && pins[u].pinId < pins[parent[v]]?.pinId)
@@ -7,7 +7,7 @@ import type {
7
7
  MspConnectionPairId,
8
8
  } from "../MspConnectionPairSolver/MspConnectionPairSolver"
9
9
  import type { ConnectivityMap } from "connectivity-map"
10
- import { SchematicTraceSingleLineSolver } from "./SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver"
10
+ import { SchematicTraceSingleLineSolver2 } from "./SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
11
11
  import type { Guideline } from "../GuidelinesSolver/GuidelinesSolver"
12
12
  import { visualizeGuidelines } from "../GuidelinesSolver/visualizeGuidelines"
13
13
  import type { Point } from "@tscircuit/math-utils"
@@ -20,7 +20,6 @@ export interface SolvedTracePath extends MspConnectionPair {
20
20
 
21
21
  export class SchematicTraceLinesSolver extends BaseSolver {
22
22
  inputProblem: InputProblem
23
- guidelines: Guideline[]
24
23
  mspConnectionPairs: MspConnectionPair[]
25
24
 
26
25
  dcConnMap: ConnectivityMap
@@ -34,7 +33,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
34
33
  solvedTracePaths: Array<SolvedTracePath> = []
35
34
  failedConnectionPairs: Array<MspConnectionPair & { error?: string }> = []
36
35
 
37
- declare activeSubSolver: SchematicTraceSingleLineSolver | null
36
+ declare activeSubSolver: SchematicTraceSingleLineSolver2 | null
38
37
 
39
38
  constructor(params: {
40
39
  mspConnectionPairs: MspConnectionPair[]
@@ -42,14 +41,12 @@ export class SchematicTraceLinesSolver extends BaseSolver {
42
41
  dcConnMap: ConnectivityMap
43
42
  globalConnMap: ConnectivityMap
44
43
  inputProblem: InputProblem
45
- guidelines: Guideline[]
46
44
  }) {
47
45
  super()
48
46
  this.inputProblem = params.inputProblem
49
47
  this.mspConnectionPairs = params.mspConnectionPairs
50
48
  this.dcConnMap = params.dcConnMap
51
49
  this.globalConnMap = params.globalConnMap
52
- this.guidelines = params.guidelines
53
50
  this.chipMap = params.chipMap
54
51
 
55
52
  this.queuedConnectionPairs = [...this.mspConnectionPairs]
@@ -64,7 +61,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
64
61
  mspConnectionPairs: this.mspConnectionPairs,
65
62
  dcConnMap: this.dcConnMap,
66
63
  globalConnMap: this.globalConnMap,
67
- guidelines: this.guidelines,
68
64
  }
69
65
  }
70
66
 
@@ -101,20 +97,20 @@ export class SchematicTraceLinesSolver extends BaseSolver {
101
97
  }
102
98
 
103
99
  const connectionPair = this.queuedConnectionPairs.shift()
104
- this.currentConnectionPair = connectionPair!
105
100
 
106
101
  if (!connectionPair) {
107
102
  this.solved = true
108
103
  return
109
104
  }
110
105
 
106
+ this.currentConnectionPair = connectionPair
107
+
111
108
  const { pins } = connectionPair
112
109
 
113
- this.activeSubSolver = new SchematicTraceSingleLineSolver({
110
+ this.activeSubSolver = new SchematicTraceSingleLineSolver2({
114
111
  inputProblem: this.inputProblem,
115
112
  pins,
116
113
  chipMap: this.chipMap,
117
- guidelines: this.guidelines,
118
114
  })
119
115
  }
120
116
 
@@ -127,8 +123,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
127
123
  connectionAlpha: 0.1,
128
124
  })
129
125
 
130
- visualizeGuidelines({ guidelines: this.guidelines, graphics })
131
-
132
126
  for (const { mspPairId, tracePath } of this.solvedTracePaths) {
133
127
  graphics.lines!.push({
134
128
  points: tracePath,
@@ -97,6 +97,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
97
97
  const { elbowVariants, movableSegments } = generateElbowVariants({
98
98
  baseElbow: this.baseElbow,
99
99
  guidelines: this.guidelines,
100
+ maxVariants: 1000,
100
101
  })
101
102
 
102
103
  this.movableSegments = movableSegments
@@ -144,6 +145,9 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
144
145
  chipMap: this.chipMap,
145
146
  })
146
147
 
148
+ // Check if this candidate path is valid
149
+ let pathIsValid = true
150
+
147
151
  for (let i = 0; i < nextCandidatePath.length - 1; i++) {
148
152
  const start = nextCandidatePath[i]
149
153
  const end = nextCandidatePath[i + 1]
@@ -156,14 +160,22 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
156
160
  for (const [, rcl] of restrictedCenterLines) {
157
161
  if (rcl.axes.has("x") && typeof rcl.x === "number") {
158
162
  // segment strictly crosses vertical center line
159
- if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) return
163
+ if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) {
164
+ pathIsValid = false
165
+ break
166
+ }
160
167
  }
161
168
  if (rcl.axes.has("y") && typeof rcl.y === "number") {
162
169
  // segment strictly crosses horizontal center line
163
- if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) return
170
+ if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) {
171
+ pathIsValid = false
172
+ break
173
+ }
164
174
  }
165
175
  }
166
176
 
177
+ if (!pathIsValid) break
178
+
167
179
  // Always exclude chips that contain the start or end points of this segment
168
180
  // if those points are actually pin locations
169
181
  const isStartPin = this.pins.some(
@@ -197,13 +209,18 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
197
209
  (onRight && dx < -EPS) ||
198
210
  (onBottom && dy > EPS) ||
199
211
  (onTop && dy < -EPS)
200
- if (entersInterior) return
212
+ if (entersInterior) {
213
+ pathIsValid = false
214
+ break
215
+ }
201
216
  if (!excludeChipIds.includes(startPin.chipId)) {
202
217
  excludeChipIds.push(startPin.chipId)
203
218
  }
204
219
  }
205
220
  }
206
221
 
222
+ if (!pathIsValid) break
223
+
207
224
  if (isEndPin) {
208
225
  const endPin = this.pins.find(
209
226
  (pin) =>
@@ -224,24 +241,37 @@ export class SchematicTraceSingleLineSolver extends BaseSolver {
224
241
  (onRight && dx < -EPS) ||
225
242
  (onBottom && dy > EPS) ||
226
243
  (onTop && dy < -EPS)
227
- if (entersInterior) return
244
+ if (entersInterior) {
245
+ pathIsValid = false
246
+ break
247
+ }
228
248
  if (!excludeChipIds.includes(endPin.chipId)) {
229
249
  excludeChipIds.push(endPin.chipId)
230
250
  }
231
251
  }
232
252
  }
233
253
 
254
+ if (!pathIsValid) break
255
+
234
256
  const obstacleOps = { excludeChipIds }
235
257
  const intersects =
236
258
  this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip(
237
259
  [start, end],
238
260
  obstacleOps,
239
261
  )
240
- if (intersects) return
262
+ if (intersects) {
263
+ pathIsValid = false
264
+ break
265
+ }
241
266
  }
242
267
 
243
- this.solvedTracePath = nextCandidatePath
244
- this.solved = true
268
+ // If this path is valid, use it as the solution
269
+ if (pathIsValid) {
270
+ this.solvedTracePath = nextCandidatePath
271
+ this.solved = true
272
+ }
273
+ // If this path is invalid, continue to next step to try the next candidate
274
+ // The next _step() call will try the next candidate path
245
275
  }
246
276
 
247
277
  override visualize(): GraphicsObject {
@@ -260,9 +260,11 @@ const cartesian = <T>(arrays: T[][]): T[][] =>
260
260
  export const generateElbowVariants = ({
261
261
  baseElbow,
262
262
  guidelines,
263
+ maxVariants = 10000,
263
264
  }: {
264
265
  baseElbow: Point[]
265
266
  guidelines: Guideline[]
267
+ maxVariants?: number
266
268
  }): {
267
269
  elbowVariants: Array<Point[]>
268
270
  movableSegments: Array<MovableSegment>
@@ -395,6 +397,11 @@ export const generateElbowVariants = ({
395
397
  if (!seen.has(key)) {
396
398
  seen.add(key)
397
399
  elbowVariants.push(variant)
400
+
401
+ // Stop if we've hit the maximum number of variants
402
+ if (elbowVariants.length >= maxVariants) {
403
+ break
404
+ }
398
405
  }
399
406
  }
400
407
 
@@ -0,0 +1,239 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
3
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
+ import type { MspConnectionPair } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
+ import type {
6
+ ChipId,
7
+ InputChip,
8
+ InputProblem,
9
+ PinId,
10
+ } from "lib/types/InputProblem"
11
+ import type { Point } from "@tscircuit/math-utils"
12
+ import { calculateElbow } from "calculate-elbow"
13
+ import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
14
+ import { getObstacleRects, type ChipWithBounds } from "./rect"
15
+ import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
16
+ import {
17
+ aabbFromPoints,
18
+ candidateMidsFromSet,
19
+ midBetweenPointAndRect,
20
+ type Axis,
21
+ } from "./mid"
22
+ import { pathKey, shiftSegmentOrth } from "./pathOps"
23
+
24
+ type PathKey = string
25
+
26
+ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
27
+ pins: MspConnectionPair["pins"]
28
+ inputProblem: InputProblem
29
+ chipMap: Record<string, InputChip>
30
+
31
+ obstacles: ChipWithBounds[]
32
+ rectById: Map<string, ChipWithBounds>
33
+ aabb: { minX: number; maxX: number; minY: number; maxY: number }
34
+
35
+ baseElbow: Point[]
36
+
37
+ solvedTracePath: Point[] | null = null
38
+
39
+ private queue: Array<{ path: Point[]; collisionChipIds: Set<ChipId> }> = []
40
+ private visited: Set<PathKey> = new Set()
41
+
42
+ constructor(params: {
43
+ pins: MspConnectionPair["pins"]
44
+ inputProblem: InputProblem
45
+ chipMap: Record<string, InputChip>
46
+ }) {
47
+ super()
48
+ this.pins = params.pins
49
+ this.inputProblem = params.inputProblem
50
+ this.chipMap = params.chipMap
51
+
52
+ // Ensure facing directions are present
53
+ for (const pin of this.pins) {
54
+ if (!pin._facingDirection) {
55
+ const chip = this.chipMap[pin.chipId]
56
+ pin._facingDirection = getPinDirection(pin, chip)
57
+ }
58
+ }
59
+
60
+ // Build obstacle rects from chips
61
+ this.obstacles = getObstacleRects(this.inputProblem)
62
+ this.rectById = new Map(this.obstacles.map((r) => [r.chipId, r]))
63
+
64
+ // Build initial elbow path
65
+ const [pin1, pin2] = this.pins
66
+ this.baseElbow = calculateElbow(
67
+ {
68
+ x: pin1.x,
69
+ y: pin1.y,
70
+ facingDirection: pin1._facingDirection!,
71
+ },
72
+ {
73
+ x: pin2.x,
74
+ y: pin2.y,
75
+ facingDirection: pin2._facingDirection!,
76
+ },
77
+ { overshoot: 0.2 },
78
+ )
79
+
80
+ // Bounds defined by PA and PB
81
+ this.aabb = aabbFromPoints(
82
+ { x: pin1.x, y: pin1.y },
83
+ { x: pin2.x, y: pin2.y },
84
+ )
85
+
86
+ // Seed search
87
+ this.queue.push({ path: this.baseElbow, collisionChipIds: new Set() })
88
+ this.visited.add(pathKey(this.baseElbow))
89
+ }
90
+
91
+ override getConstructorParams(): ConstructorParameters<
92
+ typeof SchematicTraceSingleLineSolver2
93
+ >[0] {
94
+ return {
95
+ chipMap: this.chipMap,
96
+ pins: this.pins,
97
+ inputProblem: this.inputProblem,
98
+ }
99
+ }
100
+
101
+ private axisOfSegment(a: Point, b: Point): Axis | null {
102
+ if (isVertical(a, b)) return "x"
103
+ if (isHorizontal(a, b)) return "y"
104
+ return null
105
+ }
106
+
107
+ private pathLength(pts: Point[]): number {
108
+ let sum = 0
109
+ for (let i = 0; i < pts.length - 1; i++) {
110
+ sum +=
111
+ Math.abs(pts[i + 1]!.x - pts[i]!.x) +
112
+ Math.abs(pts[i + 1]!.y - pts[i]!.y)
113
+ }
114
+ return sum
115
+ }
116
+
117
+ override _step() {
118
+ if (this.solvedTracePath) {
119
+ this.solved = true
120
+ return
121
+ }
122
+
123
+ const state = this.queue.shift()
124
+ if (!state) {
125
+ this.failed = true
126
+ this.error = "No collision-free path found"
127
+ return
128
+ }
129
+
130
+ const { path, collisionChipIds } = state
131
+
132
+ const collision = findFirstCollision(path, this.obstacles)
133
+
134
+ if (!collision) {
135
+ this.solvedTracePath = path
136
+ this.solved = true
137
+ return
138
+ }
139
+
140
+ let { segIndex, rect } = collision
141
+
142
+ // Never move the first or last segments - move adjacent segment instead
143
+ const isFirstSegment = segIndex === 0
144
+ const isLastSegment = segIndex === path.length - 2
145
+
146
+ if (isFirstSegment) {
147
+ // If first segment collides, move the second segment instead
148
+ if (path.length < 3) {
149
+ // Path too short to have an adjacent segment to move
150
+ return
151
+ }
152
+ segIndex = 1
153
+ } else if (isLastSegment) {
154
+ // If last segment collides, move the second-to-last segment instead
155
+ if (path.length < 3) {
156
+ // Path too short to have an adjacent segment to move
157
+ return
158
+ }
159
+ segIndex = path.length - 3
160
+ }
161
+
162
+ const a = path[segIndex]!
163
+ const b = path[segIndex + 1]!
164
+ const axis = this.axisOfSegment(a, b)
165
+ if (!axis) {
166
+ // Should not happen for elbow paths
167
+ return
168
+ }
169
+
170
+ const [PA, PB] = this.pins
171
+ const candidates: number[] = []
172
+
173
+ if (collisionChipIds.size === 0) {
174
+ // First collision on this search branch: use mid(PA, C) and mid(PB, C)
175
+ const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect)
176
+ const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect)
177
+
178
+ // Combine and deduplicate candidates
179
+ const allCandidates = [...m1, ...m2]
180
+ const uniqueCandidates = [...new Set(allCandidates)]
181
+ candidates.push(...uniqueCandidates)
182
+ } else {
183
+ // Subsequent collisions: mid between C and nearest rect/bounds from the set
184
+ const mids = candidateMidsFromSet(
185
+ axis,
186
+ rect,
187
+ this.rectById,
188
+ collisionChipIds,
189
+ this.aabb,
190
+ )
191
+ candidates.push(...mids)
192
+ }
193
+
194
+ // Generate new shifted paths, order by total path length (shorter first)
195
+ const newStates: Array<{
196
+ path: Point[]
197
+ collisionRectIds: Set<string>
198
+ len: number
199
+ }> = []
200
+
201
+ for (const coord of candidates) {
202
+ const newPath = shiftSegmentOrth(path, segIndex, axis, coord)
203
+ if (!newPath) continue
204
+ const key = pathKey(newPath)
205
+ if (this.visited.has(key)) continue
206
+ this.visited.add(key)
207
+ const nextSet = new Set(collisionChipIds)
208
+ nextSet.add(rect.chipId)
209
+ const len = this.pathLength(newPath)
210
+ newStates.push({ path: newPath, collisionRectIds: nextSet, len })
211
+ }
212
+
213
+ newStates.sort((a, b) => a.len - b.len)
214
+ for (const st of newStates) {
215
+ this.queue.push({ path: st.path, collisionChipIds: st.collisionRectIds })
216
+ }
217
+ }
218
+
219
+ override visualize(): GraphicsObject {
220
+ const g = visualizeInputProblem(this.inputProblem, {
221
+ chipAlpha: 0.1,
222
+ connectionAlpha: 0.1,
223
+ })
224
+
225
+ // Draw all the new candidates
226
+ for (const { path, collisionChipIds: collisionRectIds } of this.queue) {
227
+ g.lines!.push({ points: path, strokeColor: "teal", strokeDash: "2 2" })
228
+ }
229
+
230
+ if (this.solvedTracePath) {
231
+ g.lines!.push({ points: this.solvedTracePath, strokeColor: "green" })
232
+ } else if (this.queue.length > 0) {
233
+ // Show next candidate to be explored
234
+ g.lines!.push({ points: this.queue[0]!.path, strokeColor: "orange" })
235
+ }
236
+
237
+ return g
238
+ }
239
+ }
@@ -0,0 +1,57 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { ChipWithBounds } from "./rect"
3
+
4
+ const EPS = 1e-9
5
+
6
+ export const isVertical = (a: Point, b: Point, eps = EPS) =>
7
+ Math.abs(a.x - b.x) < eps
8
+ export const isHorizontal = (a: Point, b: Point, eps = EPS) =>
9
+ Math.abs(a.y - b.y) < eps
10
+
11
+ export const segmentIntersectsRect = (
12
+ a: Point,
13
+ b: Point,
14
+ r: ChipWithBounds,
15
+ eps = EPS,
16
+ ): boolean => {
17
+ const vert = isVertical(a, b, eps)
18
+ const horz = isHorizontal(a, b, eps)
19
+ if (!vert && !horz) return false
20
+
21
+ if (vert) {
22
+ const x = a.x
23
+ if (x < r.minX - eps || x > r.maxX + eps) return false
24
+ const segMinY = Math.min(a.y, b.y)
25
+ const segMaxY = Math.max(a.y, b.y)
26
+ const overlap = Math.min(segMaxY, r.maxY) - Math.max(segMinY, r.minY)
27
+ return overlap > eps
28
+ } else {
29
+ const y = a.y
30
+ if (y < r.minY - eps || y > r.maxY + eps) return false
31
+ const segMinX = Math.min(a.x, b.x)
32
+ const segMaxX = Math.max(a.x, b.x)
33
+ const overlap = Math.min(segMaxX, r.maxX) - Math.max(segMinX, r.minX)
34
+ return overlap > eps
35
+ }
36
+ }
37
+
38
+ export const findFirstCollision = (
39
+ pts: Point[],
40
+ rects: ChipWithBounds[],
41
+ opts: {
42
+ excludeRectIdsForSegment?: (segIndex: number) => Set<string>
43
+ } = {},
44
+ ): { segIndex: number; rect: ChipWithBounds } | null => {
45
+ for (let i = 0; i < pts.length - 1; i++) {
46
+ const a = pts[i]!
47
+ const b = pts[i + 1]!
48
+ const excluded = opts.excludeRectIdsForSegment?.(i) ?? new Set<string>()
49
+ for (const r of rects) {
50
+ if (excluded.has(r.chipId)) continue
51
+ if (segmentIntersectsRect(a, b, r)) {
52
+ return { segIndex: i, rect: r }
53
+ }
54
+ }
55
+ }
56
+ return null
57
+ }