@tscircuit/schematic-trace-solver 0.0.77 → 0.0.78

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 (23) hide show
  1. package/.github/ISSUE_TEMPLATE/json-bug-report.yml +24 -0
  2. package/.github/scripts/import-json-bug-report.ts +483 -0
  3. package/.github/workflows/json-bug-report.yml +154 -0
  4. package/dist/index.d.ts +32 -10
  5. package/dist/index.js +216 -74
  6. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +4 -0
  7. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -0
  8. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +6 -0
  9. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +21 -1
  10. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +9 -3
  11. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +27 -2
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +1 -0
  13. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +121 -30
  14. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +10 -10
  15. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +5 -8
  16. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +31 -6
  17. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +3 -0
  18. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +1 -0
  19. package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +3 -0
  20. package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +1 -0
  21. package/lib/utils/textBoxBounds.ts +40 -0
  22. package/package.json +1 -1
  23. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +12 -12
@@ -19,6 +19,7 @@ import { chooseHostTraceForGroup } from "./host"
19
19
  import { anchorsForSegment } from "./anchors"
20
20
  import { solveNetLabelPlacementForPortOnlyPin } from "./solvePortOnlyPin"
21
21
  import { visualizeSingleNetLabelPlacementSolver } from "./SingleNetLabelPlacementSolver_visualize"
22
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
22
23
 
23
24
  export {
24
25
  NET_LABEL_HORIZONTAL_WIDTH,
@@ -70,7 +71,12 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
70
71
  bounds: { minX: number; minY: number; maxX: number; maxY: number }
71
72
  anchor: { x: number; y: number }
72
73
  orientation: FacingDirection
73
- status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
74
+ status:
75
+ | "ok"
76
+ | "chip-collision"
77
+ | "trace-collision"
78
+ | "text-collision"
79
+ | "parallel-to-segment"
74
80
  hostSegIndex: number
75
81
  }> = []
76
82
 
@@ -276,6 +282,20 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
276
282
  continue
277
283
  }
278
284
 
285
+ if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
286
+ this.testedCandidates.push({
287
+ center: testCenter,
288
+ width,
289
+ height,
290
+ bounds,
291
+ anchor,
292
+ orientation,
293
+ status: "text-collision",
294
+ hostSegIndex: si,
295
+ })
296
+ continue
297
+ }
298
+
279
299
  // Trace collision check (ignore the host segment)
280
300
  const traceIntersectionResult = rectIntersectsAnyTrace(
281
301
  bounds,
@@ -38,7 +38,9 @@ export function visualizeSingleNetLabelPlacementSolver(
38
38
  ? "rgba(220, 0, 0, 0.25)"
39
39
  : c.status === "trace-collision"
40
40
  ? "rgba(220, 140, 0, 0.25)"
41
- : "rgba(120, 120, 120, 0.15)"
41
+ : c.status === "text-collision"
42
+ ? "rgba(160, 0, 220, 0.2)"
43
+ : "rgba(120, 120, 120, 0.15)"
42
44
  const stroke =
43
45
  c.status === "ok"
44
46
  ? "green"
@@ -46,7 +48,9 @@ export function visualizeSingleNetLabelPlacementSolver(
46
48
  ? "red"
47
49
  : c.status === "trace-collision"
48
50
  ? "orange"
49
- : "gray"
51
+ : c.status === "text-collision"
52
+ ? "purple"
53
+ : "gray"
50
54
  const candidateLabel =
51
55
  c.status === "ok"
52
56
  ? "status: ok(valid net label candidate)"
@@ -54,7 +58,9 @@ export function visualizeSingleNetLabelPlacementSolver(
54
58
  ? "status: chip-collision"
55
59
  : c.status === "trace-collision"
56
60
  ? "status: trace-collision"
57
- : "status: parallel-to-segment"
61
+ : c.status === "text-collision"
62
+ ? "status: text-collision"
63
+ : "status: parallel-to-segment"
58
64
 
59
65
  graphics.rects!.push({
60
66
  center: {
@@ -14,6 +14,7 @@ import {
14
14
  getRectBounds,
15
15
  } from "./geometry"
16
16
  import { rectIntersectsAnyTrace } from "./collisions"
17
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
17
18
 
18
19
  export function solveNetLabelPlacementForPortOnlyPin(params: {
19
20
  inputProblem: InputProblem
@@ -32,7 +33,12 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
32
33
  bounds: { minX: number; minY: number; maxX: number; maxY: number }
33
34
  anchor: { x: number; y: number }
34
35
  orientation: FacingDirection
35
- status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
36
+ status:
37
+ | "ok"
38
+ | "chip-collision"
39
+ | "trace-collision"
40
+ | "text-collision"
41
+ | "parallel-to-segment"
36
42
  hostSegIndex: number
37
43
  }>
38
44
  error?: string
@@ -97,7 +103,12 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
97
103
  bounds: { minX: number; minY: number; maxX: number; maxY: number }
98
104
  anchor: { x: number; y: number }
99
105
  orientation: FacingDirection
100
- status: "ok" | "chip-collision" | "trace-collision" | "parallel-to-segment"
106
+ status:
107
+ | "ok"
108
+ | "chip-collision"
109
+ | "trace-collision"
110
+ | "text-collision"
111
+ | "parallel-to-segment"
101
112
  hostSegIndex: number
102
113
  }> = []
103
114
 
@@ -133,6 +144,20 @@ export function solveNetLabelPlacementForPortOnlyPin(params: {
133
144
  continue
134
145
  }
135
146
 
147
+ if (rectIntersectsAnyTextBox(bounds, inputProblem)) {
148
+ testedCandidates.push({
149
+ center,
150
+ width,
151
+ height,
152
+ bounds,
153
+ anchor,
154
+ orientation,
155
+ status: "text-collision",
156
+ hostSegIndex: -1,
157
+ })
158
+ continue
159
+ }
160
+
136
161
  // Trace collision check
137
162
  const traceIntersectionResult = rectIntersectsAnyTrace(
138
163
  bounds,
@@ -110,6 +110,7 @@ export class SchematicTraceLinesSolver extends BaseSolver {
110
110
  this.activeSubSolver = new SchematicTraceSingleLineSolver2({
111
111
  inputProblem: this.inputProblem,
112
112
  pins,
113
+ connectionPair,
113
114
  chipMap: this.chipMap,
114
115
  })
115
116
  }
@@ -2,16 +2,11 @@ import type { GraphicsObject } from "graphics-debug"
2
2
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
3
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
4
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"
5
+ import type { InputChip, InputProblem, PinId } from "lib/types/InputProblem"
11
6
  import type { Point } from "@tscircuit/math-utils"
12
7
  import { calculateElbow } from "calculate-elbow"
13
8
  import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
14
- import { getObstacleRects, type ChipWithBounds } from "./rect"
9
+ import { getObstacleRects, type ObstacleRect } from "./rect"
15
10
  import { findFirstCollision, isHorizontal, isVertical } from "./collisions"
16
11
  import {
17
12
  aabbFromPoints,
@@ -20,32 +15,39 @@ import {
20
15
  type Axis,
21
16
  } from "./mid"
22
17
  import { pathKey, shiftSegmentOrth } from "./pathOps"
18
+ import type { FacingDirection } from "lib/utils/dir"
19
+ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
20
+ import type { RectPadding } from "lib/utils/textBoxBounds"
23
21
 
24
22
  type PathKey = string
25
23
 
26
24
  export class SchematicTraceSingleLineSolver2 extends BaseSolver {
27
25
  pins: MspConnectionPair["pins"]
26
+ connectionPair?: MspConnectionPair
28
27
  inputProblem: InputProblem
29
28
  chipMap: Record<string, InputChip>
30
29
 
31
- obstacles: ChipWithBounds[]
32
- rectById: Map<string, ChipWithBounds>
30
+ obstacles: ObstacleRect[]
31
+ textObstacles: Set<ObstacleRect>
33
32
  aabb: { minX: number; maxX: number; minY: number; maxY: number }
34
33
 
35
34
  baseElbow: Point[]
36
35
 
37
36
  solvedTracePath: Point[] | null = null
38
37
 
39
- private queue: Array<{ path: Point[]; collisionChipIds: Set<ChipId> }> = []
38
+ private queue: Array<{ path: Point[]; collisionRects: Set<ObstacleRect> }> =
39
+ []
40
40
  private visited: Set<PathKey> = new Set()
41
41
 
42
42
  constructor(params: {
43
43
  pins: MspConnectionPair["pins"]
44
+ connectionPair?: MspConnectionPair
44
45
  inputProblem: InputProblem
45
46
  chipMap: Record<string, InputChip>
46
47
  }) {
47
48
  super()
48
49
  this.pins = params.pins
50
+ this.connectionPair = params.connectionPair
49
51
  this.inputProblem = params.inputProblem
50
52
  this.chipMap = params.chipMap
51
53
 
@@ -57,9 +59,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
57
59
  }
58
60
  }
59
61
 
60
- // Build obstacle rects from chips
61
- this.obstacles = getObstacleRects(this.inputProblem)
62
- this.rectById = new Map(this.obstacles.map((r) => [r.chipId, r]))
62
+ // Build obstacle rects from chips and schematic text boxes. Text boxes are
63
+ // padded by the label footprint for this net so labels have clearance too.
64
+ this.obstacles = getObstacleRects(this.inputProblem, {
65
+ textBoxPadding: this.getTextBoxPaddingForConnectionPair(),
66
+ })
67
+ this.textObstacles = new Set(
68
+ this.obstacles.filter((r) => r.kind === "text_box"),
69
+ )
63
70
 
64
71
  // Build initial elbow path
65
72
  const [pin1, pin2] = this.pins
@@ -84,7 +91,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
84
91
  )
85
92
 
86
93
  // Seed search
87
- this.queue.push({ path: this.baseElbow, collisionChipIds: new Set() })
94
+ this.queue.push({ path: this.baseElbow, collisionRects: new Set() })
88
95
  this.visited.add(pathKey(this.baseElbow))
89
96
  }
90
97
 
@@ -94,10 +101,92 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
94
101
  return {
95
102
  chipMap: this.chipMap,
96
103
  pins: this.pins,
104
+ connectionPair: this.connectionPair,
97
105
  inputProblem: this.inputProblem,
98
106
  }
99
107
  }
100
108
 
109
+ private getTextBoxPaddingForConnectionPair(): RectPadding {
110
+ if (!this.inputProblem.textBoxes?.length) return {}
111
+
112
+ const netId = this.connectionPair?.userNetId
113
+ if (!netId) return {}
114
+
115
+ const orientations =
116
+ this.inputProblem.availableNetLabelOrientations[netId] ??
117
+ (["x+", "x-", "y+", "y-"] as FacingDirection[])
118
+ const netLabelWidth = this.getNetLabelWidthForConnectionPair(netId)
119
+ const netLabelHeight = this.getNetLabelHeightForConnectionPair(netId)
120
+ const padding: Required<RectPadding> = {
121
+ minX: 0,
122
+ minY: 0,
123
+ maxX: 0,
124
+ maxY: 0,
125
+ }
126
+
127
+ for (const orientation of orientations) {
128
+ const { width, height } = getDimsForOrientation({
129
+ orientation,
130
+ netLabelWidth,
131
+ netLabelHeight,
132
+ })
133
+
134
+ if (orientation === "y+" || orientation === "y-") {
135
+ padding.minX = Math.max(padding.minX, width / 2)
136
+ padding.maxX = Math.max(padding.maxX, width / 2)
137
+ if (orientation === "y+") {
138
+ padding.minY = Math.max(padding.minY, height)
139
+ } else {
140
+ padding.maxY = Math.max(padding.maxY, height)
141
+ }
142
+ } else {
143
+ padding.minY = Math.max(padding.minY, height / 2)
144
+ padding.maxY = Math.max(padding.maxY, height / 2)
145
+ if (orientation === "x+") {
146
+ padding.minX = Math.max(padding.minX, width)
147
+ } else {
148
+ padding.maxX = Math.max(padding.maxX, width)
149
+ }
150
+ }
151
+ }
152
+
153
+ return padding
154
+ }
155
+
156
+ private getNetLabelWidthForConnectionPair(netId: string) {
157
+ const ncWidth = this.inputProblem.netConnections.find(
158
+ (nc) => nc.netId === netId,
159
+ )?.netLabelWidth
160
+ if (ncWidth !== undefined) return ncWidth
161
+
162
+ const dcWidthByNetId = this.inputProblem.directConnections.find(
163
+ (dc) => dc.netId === netId,
164
+ )?.netLabelWidth
165
+ if (dcWidthByNetId !== undefined) return dcWidthByNetId
166
+
167
+ const pinIds = this.pins.map((p) => p.pinId)
168
+ const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
169
+ dc.pinIds.some((pid) => pinIds.includes(pid)),
170
+ )?.netLabelWidth
171
+ if (dcWidthByPinId !== undefined) return dcWidthByPinId
172
+
173
+ return this.inputProblem.netConnections.find((nc) =>
174
+ nc.pinIds.some((pid) => pinIds.includes(pid)),
175
+ )?.netLabelWidth
176
+ }
177
+
178
+ private getNetLabelHeightForConnectionPair(netId: string) {
179
+ const ncHeight = this.inputProblem.netConnections.find(
180
+ (nc) => nc.netId === netId,
181
+ )?.netLabelHeight
182
+ if (ncHeight !== undefined) return ncHeight
183
+
184
+ const pinIds = this.pins.map((p) => p.pinId)
185
+ return this.inputProblem.netConnections.find((nc) =>
186
+ nc.pinIds.some((pid) => pinIds.includes(pid)),
187
+ )?.netLabelHeight
188
+ }
189
+
101
190
  private axisOfSegment(a: Point, b: Point): Axis | null {
102
191
  if (isVertical(a, b)) return "x"
103
192
  if (isHorizontal(a, b)) return "y"
@@ -127,10 +216,18 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
127
216
  return
128
217
  }
129
218
 
130
- const { path, collisionChipIds } = state
219
+ const { path, collisionRects } = state
131
220
 
132
221
  const [PA, PB] = this.pins
133
- const collision = findFirstCollision(path, this.obstacles)
222
+ const collision = findFirstCollision(path, this.obstacles, {
223
+ excludeRectsForSegment: (segIndex) => {
224
+ const lastSegIndex = path.length - 2
225
+ if (segIndex === 0 || segIndex === lastSegIndex) {
226
+ return this.textObstacles
227
+ }
228
+ return new Set<ObstacleRect>()
229
+ },
230
+ })
134
231
 
135
232
  if (!collision) {
136
233
  // Sanity check: ensure path still connects PA -> PB
@@ -182,7 +279,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
182
279
  // Note: PA and PB are already defined above
183
280
  const candidates: number[] = []
184
281
 
185
- if (collisionChipIds.size === 0) {
282
+ if (collisionRects.size === 0) {
186
283
  // First collision on this search branch: use mid(PA, C) and mid(PB, C)
187
284
  const m1 = midBetweenPointAndRect(axis, { x: PA.x, y: PA.y }, rect)
188
285
  const m2 = midBetweenPointAndRect(axis, { x: PB.x, y: PB.y }, rect)
@@ -193,20 +290,14 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
193
290
  candidates.push(...uniqueCandidates)
194
291
  } else {
195
292
  // Subsequent collisions: mid between C and nearest rect/bounds from the set
196
- const mids = candidateMidsFromSet(
197
- axis,
198
- rect,
199
- this.rectById,
200
- collisionChipIds,
201
- this.aabb,
202
- )
293
+ const mids = candidateMidsFromSet(axis, rect, collisionRects, this.aabb)
203
294
  candidates.push(...mids)
204
295
  }
205
296
 
206
297
  // Generate new shifted paths, order by total path length (shorter first)
207
298
  const newStates: Array<{
208
299
  path: Point[]
209
- collisionRectIds: Set<string>
300
+ collisionRects: Set<ObstacleRect>
210
301
  len: number
211
302
  }> = []
212
303
 
@@ -216,15 +307,15 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
216
307
  const key = pathKey(newPath)
217
308
  if (this.visited.has(key)) continue
218
309
  this.visited.add(key)
219
- const nextSet = new Set(collisionChipIds)
220
- nextSet.add(rect.chipId)
310
+ const nextSet = new Set(collisionRects)
311
+ nextSet.add(rect)
221
312
  const len = this.pathLength(newPath)
222
- newStates.push({ path: newPath, collisionRectIds: nextSet, len })
313
+ newStates.push({ path: newPath, collisionRects: nextSet, len })
223
314
  }
224
315
 
225
316
  newStates.sort((a, b) => a.len - b.len)
226
317
  for (const st of newStates) {
227
- this.queue.push({ path: st.path, collisionChipIds: st.collisionRectIds })
318
+ this.queue.push({ path: st.path, collisionRects: st.collisionRects })
228
319
  }
229
320
  }
230
321
 
@@ -254,7 +345,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
254
345
  })
255
346
 
256
347
  // Draw all the new candidates
257
- for (const { path, collisionChipIds: collisionRectIds } of this.queue) {
348
+ for (const { path } of this.queue) {
258
349
  g.lines!.push({ points: path, strokeColor: "teal", strokeDash: "2 2" })
259
350
  }
260
351
 
@@ -1,5 +1,5 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
- import type { ChipWithBounds } from "./rect"
2
+ import type { RectBounds } from "./rect"
3
3
 
4
4
  const EPS = 1e-9
5
5
 
@@ -8,10 +8,10 @@ export const isVertical = (a: Point, b: Point, eps = EPS) =>
8
8
  export const isHorizontal = (a: Point, b: Point, eps = EPS) =>
9
9
  Math.abs(a.y - b.y) < eps
10
10
 
11
- export const segmentIntersectsRect = (
11
+ export const segmentIntersectsRect = <TRect extends RectBounds>(
12
12
  a: Point,
13
13
  b: Point,
14
- r: ChipWithBounds,
14
+ r: TRect,
15
15
  eps = EPS,
16
16
  ): boolean => {
17
17
  const vert = isVertical(a, b, eps)
@@ -35,19 +35,19 @@ export const segmentIntersectsRect = (
35
35
  }
36
36
  }
37
37
 
38
- export const findFirstCollision = (
38
+ export const findFirstCollision = <TRect extends RectBounds>(
39
39
  pts: Point[],
40
- rects: ChipWithBounds[],
40
+ rects: TRect[],
41
41
  opts: {
42
- excludeRectIdsForSegment?: (segIndex: number) => Set<string>
42
+ excludeRectsForSegment?: (segIndex: number) => Set<TRect>
43
43
  } = {},
44
- ): { segIndex: number; rect: ChipWithBounds } | null => {
44
+ ): { segIndex: number; rect: TRect } | null => {
45
45
  for (let i = 0; i < pts.length - 1; i++) {
46
46
  const a = pts[i]!
47
47
  const b = pts[i + 1]!
48
- const excluded = opts.excludeRectIdsForSegment?.(i) ?? new Set<string>()
48
+ const excluded = opts.excludeRectsForSegment?.(i) ?? new Set<TRect>()
49
49
  for (const r of rects) {
50
- if (excluded.has(r.chipId)) continue
50
+ if (excluded.has(r)) continue
51
51
  if (segmentIntersectsRect(a, b, r)) {
52
52
  return { segIndex: i, rect: r }
53
53
  }
@@ -61,7 +61,7 @@ export const findFirstCollision = (
61
61
  */
62
62
  export const isPathCollidingWithObstacles = (
63
63
  path: Point[],
64
- obstacles: ChipWithBounds[],
64
+ obstacles: RectBounds[],
65
65
  ): boolean => {
66
66
  for (let i = 0; i < path.length - 1; i++) {
67
67
  for (const obstacle of obstacles) {
@@ -1,5 +1,5 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
- import type { ChipWithBounds } from "./rect"
2
+ import type { ObstacleRect } from "./rect"
3
3
 
4
4
  const EPS = 1e-9
5
5
 
@@ -15,7 +15,7 @@ export const aabbFromPoints = (a: Point, b: Point) => ({
15
15
  export const midBetweenPointAndRect = (
16
16
  axis: Axis,
17
17
  p: Point,
18
- r: ChipWithBounds,
18
+ r: ObstacleRect,
19
19
  eps = EPS,
20
20
  ): number[] => {
21
21
  if (axis === "x") {
@@ -41,15 +41,12 @@ export const midBetweenPointAndRect = (
41
41
 
42
42
  export const candidateMidsFromSet = (
43
43
  axis: Axis,
44
- colliding: ChipWithBounds,
45
- rectsById: Map<string, ChipWithBounds>,
46
- collisionRectIds: Set<string>,
44
+ colliding: ObstacleRect,
45
+ collisionRects: Set<ObstacleRect>,
47
46
  aabb: { minX: number; maxX: number; minY: number; maxY: number },
48
47
  eps = EPS,
49
48
  ): number[] => {
50
- const setRects = [...collisionRectIds]
51
- .map((id) => rectsById.get(id))
52
- .filter((r): r is ChipWithBounds => !!r)
49
+ const setRects = [...collisionRects]
53
50
 
54
51
  if (axis === "x") {
55
52
  const leftBoundaries = [aabb.minX, ...setRects.map((r) => r.maxX)].filter(
@@ -1,19 +1,44 @@
1
1
  import type { InputChip, InputProblem } from "lib/types/InputProblem"
2
2
  import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
3
+ import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
3
4
 
4
- export type ChipWithBounds = {
5
- chipId: string
5
+ export type RectBounds = {
6
6
  minX: number
7
7
  minY: number
8
8
  maxX: number
9
9
  maxY: number
10
10
  }
11
11
 
12
- export const chipToRect = (chip: InputChip): ChipWithBounds => {
12
+ export type ChipObstacleRect = RectBounds & {
13
+ kind: "chip"
14
+ chipId: string
15
+ }
16
+
17
+ export type TextBoxObstacleRect = RectBounds & {
18
+ kind: "text_box"
19
+ textBox: NonNullable<InputProblem["textBoxes"]>[number]
20
+ }
21
+
22
+ export type ObstacleRect = ChipObstacleRect | TextBoxObstacleRect
23
+
24
+ export const chipToRect = (chip: InputChip): ChipObstacleRect => {
13
25
  const b = getInputChipBounds(chip)
14
- return { chipId: chip.chipId, ...b }
26
+ return { kind: "chip", chipId: chip.chipId, ...b }
15
27
  }
16
28
 
17
- export const getObstacleRects = (problem: InputProblem): ChipWithBounds[] => {
18
- return problem.chips.map(chipToRect)
29
+ export const getObstacleRects = (
30
+ problem: InputProblem,
31
+ opts: { textBoxPadding?: RectPadding } = {},
32
+ ): ObstacleRect[] => {
33
+ const chipRects = problem.chips.map(chipToRect)
34
+ const textBoxRects = (problem.textBoxes ?? []).map((textBox) => {
35
+ const b = getTextBoxBounds(textBox, opts.textBoxPadding)
36
+ return {
37
+ kind: "text_box" as const,
38
+ textBox,
39
+ ...b,
40
+ }
41
+ })
42
+
43
+ return [...chipRects, ...textBoxRects]
19
44
  }
@@ -20,6 +20,7 @@ import type {
20
20
  TraceAnchoredNetLabelOverlapSolverParams,
21
21
  } from "./types"
22
22
  import { visualizeTraceAnchoredNetLabelOverlapSolver } from "./visualize"
23
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
23
24
 
24
25
  type ActiveOverlapSearch = {
25
26
  overlap: LabelOverlap
@@ -227,6 +228,8 @@ export class TraceAnchoredNetLabelOverlapSolver extends BaseSolver {
227
228
  ): CandidateStatus {
228
229
  const bounds = getLabelBounds(candidate)
229
230
  if (this.intersectsAnyChip(bounds)) return "chip-collision"
231
+ if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
232
+ return "text-collision"
230
233
  if (traceCrossesBoundsInterior(bounds, this.traces))
231
234
  return "trace-collision"
232
235
  if (this.intersectsAnyOtherLabel(bounds, labelIndex)) {
@@ -30,6 +30,7 @@ export type TraceLocation = {
30
30
  export type CandidateStatus =
31
31
  | "valid"
32
32
  | "chip-collision"
33
+ | "text-collision"
33
34
  | "trace-collision"
34
35
  | "netlabel-collision"
35
36
 
@@ -23,6 +23,7 @@ import type {
23
23
  VccNetLabelCornerPlacementSolverParams,
24
24
  } from "./types"
25
25
  import { visualizeVccNetLabelCornerPlacementSolver } from "./visualize"
26
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
26
27
 
27
28
  export class VccNetLabelCornerPlacementSolver extends BaseSolver {
28
29
  inputProblem: InputProblem
@@ -171,6 +172,8 @@ export class VccNetLabelCornerPlacementSolver extends BaseSolver {
171
172
  labelIndex: number,
172
173
  ): CornerCandidateStatus {
173
174
  if (this.intersectsAnyChip(bounds)) return "chip-collision"
175
+ if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
176
+ return "text-collision"
174
177
  if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
175
178
  return "trace-collision"
176
179
  }
@@ -25,6 +25,7 @@ export type TraceCornerCandidate = {
25
25
  export type CornerCandidateStatus =
26
26
  | "valid"
27
27
  | "chip-collision"
28
+ | "text-collision"
28
29
  | "trace-collision"
29
30
  | "netlabel-collision"
30
31
 
@@ -0,0 +1,40 @@
1
+ import type { Bounds } from "@tscircuit/math-utils"
2
+ import type { InputProblem, TextBoxes } from "lib/types/InputProblem"
3
+
4
+ export type RectPadding = {
5
+ minX?: number
6
+ minY?: number
7
+ maxX?: number
8
+ maxY?: number
9
+ }
10
+
11
+ export function getTextBoxBounds(
12
+ textBox: TextBoxes,
13
+ padding: RectPadding = {},
14
+ ): Bounds {
15
+ return {
16
+ minX: textBox.center.x - textBox.width / 2 - (padding.minX ?? 0),
17
+ minY: textBox.center.y - textBox.height / 2 - (padding.minY ?? 0),
18
+ maxX: textBox.center.x + textBox.width / 2 + (padding.maxX ?? 0),
19
+ maxY: textBox.center.y + textBox.height / 2 + (padding.maxY ?? 0),
20
+ }
21
+ }
22
+
23
+ export function boundsOverlap(a: Bounds, b: Bounds, eps = 1e-9): boolean {
24
+ return (
25
+ a.minX < b.maxX - eps &&
26
+ a.maxX > b.minX + eps &&
27
+ a.minY < b.maxY - eps &&
28
+ a.maxY > b.minY + eps
29
+ )
30
+ }
31
+
32
+ export function rectIntersectsAnyTextBox(
33
+ bounds: Bounds,
34
+ inputProblem: InputProblem,
35
+ ): boolean {
36
+ for (const textBox of inputProblem.textBoxes ?? []) {
37
+ if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true
38
+ }
39
+ return false
40
+ }
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.77",
4
+ "version": "0.0.78",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",