@tscircuit/schematic-trace-solver 0.0.32 → 0.0.34

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 (42) hide show
  1. package/dist/index.d.ts +24 -53
  2. package/dist/index.js +526 -847
  3. package/lib/index.ts +1 -0
  4. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +54 -3
  5. package/lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines.ts +62 -0
  6. package/lib/solvers/MspConnectionPairSolver/getMspConnectionPairsFromPins.ts +7 -2
  7. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +25 -4
  8. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +3 -10
  9. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +239 -0
  10. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +57 -0
  11. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +97 -0
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +65 -0
  13. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +19 -0
  14. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +14 -14
  15. package/package.json +1 -1
  16. package/site/examples/example09.page.tsx +1 -1
  17. package/site/examples/example15-rp2040-caps.page.tsx +623 -0
  18. package/site/examples/example16-core-repro51.page.tsx +107 -0
  19. package/site/examples/example17-straight-line-trace.page.tsx +165 -0
  20. package/site/examples/example18.page.tsx +181 -0
  21. package/site/examples/example19.page.tsx +169 -0
  22. package/tests/examples/__snapshots__/example01.snap.svg +29 -29
  23. package/tests/examples/__snapshots__/example02.snap.svg +13 -10
  24. package/tests/examples/__snapshots__/example04.snap.svg +12 -12
  25. package/tests/examples/__snapshots__/example05.snap.svg +38 -38
  26. package/tests/examples/__snapshots__/example06.snap.svg +14 -14
  27. package/tests/examples/__snapshots__/example08.snap.svg +29 -23
  28. package/tests/examples/__snapshots__/example09.snap.svg +119 -149
  29. package/tests/examples/__snapshots__/example11.snap.svg +39 -33
  30. package/tests/examples/__snapshots__/example12.snap.svg +32 -29
  31. package/tests/examples/__snapshots__/example13.snap.svg +87 -84
  32. package/tests/examples/__snapshots__/example15.snap.svg +800 -71
  33. package/tests/examples/__snapshots__/example16.snap.svg +40 -86
  34. package/tests/examples/__snapshots__/example17.snap.svg +190 -0
  35. package/tests/examples/__snapshots__/example18.snap.svg +235 -0
  36. package/tests/examples/__snapshots__/example19.snap.svg +195 -0
  37. package/tests/examples/example15.test.tsx +524 -82
  38. package/tests/examples/example16.test.tsx +56 -118
  39. package/tests/examples/example17.test.tsx +171 -0
  40. package/tests/examples/example18.test.tsx +187 -0
  41. package/tests/examples/example19.test.tsx +175 -0
  42. package/tests/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver_repro03.test.ts +1 -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)
@@ -107,6 +107,13 @@ export class NetLabelPlacementSolver extends BaseSolver {
107
107
  this.inputProblem,
108
108
  )
109
109
 
110
+ const pinIdToPinMap = new Map<string, unknown>()
111
+ for (const chip of this.inputProblem.chips) {
112
+ for (const pin of chip.pins) {
113
+ pinIdToPinMap.set(pin.pinId, pin)
114
+ }
115
+ }
116
+
110
117
  // Map pins to user-provided netIds (if any)
111
118
  const userNetIdByPinId: Record<string, string | undefined> = {}
112
119
  for (const dc of this.inputProblem.directConnections) {
@@ -124,11 +131,24 @@ export class NetLabelPlacementSolver extends BaseSolver {
124
131
 
125
132
  const groups: Array<OverlappingSameNetTraceGroup> = []
126
133
 
134
+ const allPinIds = this.inputProblem.chips.flatMap((c) =>
135
+ c.pins.map((p) => p.pinId),
136
+ )
137
+
138
+ const allGlobalConnNetIds = new Set<string>()
139
+ for (const pinId of allPinIds) {
140
+ const netId = netConnMap.getNetConnectedToId(pinId)
141
+ if (netId) {
142
+ allGlobalConnNetIds.add(netId)
143
+ }
144
+ }
145
+
127
146
  // Consider every global connectivity net id
128
- for (const globalConnNetId of Object.keys((netConnMap as any).netMap)) {
129
- const pinsInNet = netConnMap.getIdsConnectedToNet(
147
+ for (const globalConnNetId of allGlobalConnNetIds) {
148
+ const allIdsInNet = netConnMap.getIdsConnectedToNet(
130
149
  globalConnNetId,
131
150
  ) as string[]
151
+ const pinsInNet = allIdsInNet.filter((id) => pinIdToPinMap.has(id))
132
152
 
133
153
  // Build adjacency from solved traces (edges)
134
154
  const adj: Record<string, Set<string>> = {}
@@ -205,12 +225,13 @@ export class NetLabelPlacementSolver extends BaseSolver {
205
225
  ),
206
226
  )
207
227
 
208
- groups.push({
228
+ const group = {
209
229
  globalConnNetId,
210
230
  netId: userNetId,
211
231
  overlappingTraces: rep,
212
232
  mspConnectionPairIds,
213
- })
233
+ }
234
+ groups.push(group)
214
235
  } else {
215
236
  // No traces in this component: place label at each pin that has a user net id
216
237
  for (const p of component) {
@@ -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
 
@@ -111,11 +107,10 @@ export class SchematicTraceLinesSolver extends BaseSolver {
111
107
 
112
108
  const { pins } = connectionPair
113
109
 
114
- this.activeSubSolver = new SchematicTraceSingleLineSolver({
110
+ this.activeSubSolver = new SchematicTraceSingleLineSolver2({
115
111
  inputProblem: this.inputProblem,
116
112
  pins,
117
113
  chipMap: this.chipMap,
118
- guidelines: this.guidelines,
119
114
  })
120
115
  }
121
116
 
@@ -128,8 +123,6 @@ export class SchematicTraceLinesSolver extends BaseSolver {
128
123
  connectionAlpha: 0.1,
129
124
  })
130
125
 
131
- visualizeGuidelines({ guidelines: this.guidelines, graphics })
132
-
133
126
  for (const { mspPairId, tracePath } of this.solvedTracePaths) {
134
127
  graphics.lines!.push({
135
128
  points: tracePath,
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { ChipWithBounds } from "./rect"
3
+
4
+ const EPS = 1e-9
5
+
6
+ export type Axis = "x" | "y"
7
+
8
+ export const aabbFromPoints = (a: Point, b: Point) => ({
9
+ minX: Math.min(a.x, b.x),
10
+ maxX: Math.max(a.x, b.x),
11
+ minY: Math.min(a.y, b.y),
12
+ maxY: Math.max(a.y, b.y),
13
+ })
14
+
15
+ export const midBetweenPointAndRect = (
16
+ axis: Axis,
17
+ p: Point,
18
+ r: ChipWithBounds,
19
+ eps = EPS,
20
+ ): number[] => {
21
+ if (axis === "x") {
22
+ if (p.x < r.minX - eps) {
23
+ return [(p.x + r.minX) / 2]
24
+ }
25
+ if (p.x > r.maxX + eps) {
26
+ return [(p.x + r.maxX) / 2]
27
+ }
28
+ // Point is within rect bounds on this axis - generate candidates on both sides
29
+ return [r.minX - 0.2, r.maxX + 0.2]
30
+ } else {
31
+ if (p.y < r.minY - eps) {
32
+ return [(p.y + r.minY) / 2]
33
+ }
34
+ if (p.y > r.maxY + eps) {
35
+ return [(p.y + r.maxY) / 2]
36
+ }
37
+ // Point is within rect bounds on this axis - generate candidates on both sides
38
+ return [r.minY - 0.2, r.maxY + 0.2]
39
+ }
40
+ }
41
+
42
+ export const candidateMidsFromSet = (
43
+ axis: Axis,
44
+ colliding: ChipWithBounds,
45
+ rectsById: Map<string, ChipWithBounds>,
46
+ collisionRectIds: Set<string>,
47
+ aabb: { minX: number; maxX: number; minY: number; maxY: number },
48
+ eps = EPS,
49
+ ): number[] => {
50
+ const setRects = [...collisionRectIds]
51
+ .map((id) => rectsById.get(id))
52
+ .filter((r): r is ChipWithBounds => !!r)
53
+
54
+ if (axis === "x") {
55
+ const leftBoundaries = [aabb.minX, ...setRects.map((r) => r.maxX)].filter(
56
+ (v) => v < colliding.minX - eps,
57
+ )
58
+ const rightBoundaries = [aabb.maxX, ...setRects.map((r) => r.minX)].filter(
59
+ (v) => v > colliding.maxX + eps,
60
+ )
61
+
62
+ const leftNeighbor =
63
+ leftBoundaries.length > 0 ? Math.max(...leftBoundaries) : undefined
64
+ const rightNeighbor =
65
+ rightBoundaries.length > 0 ? Math.min(...rightBoundaries) : undefined
66
+
67
+ const out: number[] = []
68
+ if (leftNeighbor !== undefined) {
69
+ out.push((leftNeighbor + colliding.minX) / 2)
70
+ }
71
+ if (rightNeighbor !== undefined) {
72
+ out.push((colliding.maxX + rightNeighbor) / 2)
73
+ }
74
+ return out
75
+ } else {
76
+ const bottomBoundaries = [aabb.minY, ...setRects.map((r) => r.maxY)].filter(
77
+ (v) => v < colliding.minY - eps,
78
+ )
79
+ const topBoundaries = [aabb.maxY, ...setRects.map((r) => r.minY)].filter(
80
+ (v) => v > colliding.maxY + eps,
81
+ )
82
+
83
+ const bottomNeighbor =
84
+ bottomBoundaries.length > 0 ? Math.max(...bottomBoundaries) : undefined
85
+ const topNeighbor =
86
+ topBoundaries.length > 0 ? Math.min(...topBoundaries) : undefined
87
+
88
+ const out: number[] = []
89
+ if (bottomNeighbor !== undefined) {
90
+ out.push((bottomNeighbor + colliding.minY) / 2)
91
+ }
92
+ if (topNeighbor !== undefined) {
93
+ out.push((colliding.maxY + topNeighbor) / 2)
94
+ }
95
+ return out
96
+ }
97
+ }