@tscircuit/schematic-trace-solver 0.0.77 → 0.0.79

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 (40) 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 +37 -10
  5. package/dist/index.js +281 -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/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +87 -1
  13. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +1 -0
  14. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +121 -30
  15. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +10 -10
  16. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +5 -8
  17. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +31 -6
  18. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +3 -0
  19. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +1 -0
  20. package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +3 -0
  21. package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +1 -0
  22. package/lib/utils/textBoxBounds.ts +40 -0
  23. package/package.json +1 -1
  24. package/site/examples/example46.page.tsx +4 -0
  25. package/site/examples/example47.page.tsx +4 -0
  26. package/site/examples/example48.page.tsx +4 -0
  27. package/site/examples/example49.page.tsx +4 -0
  28. package/tests/assets/example46.json +236 -0
  29. package/tests/assets/example47.json +206 -0
  30. package/tests/assets/example48.json +104 -0
  31. package/tests/assets/example49.json +390 -0
  32. package/tests/examples/__snapshots__/example46.snap.svg +343 -0
  33. package/tests/examples/__snapshots__/example47.snap.svg +324 -0
  34. package/tests/examples/__snapshots__/example48.snap.svg +168 -0
  35. package/tests/examples/__snapshots__/example49.snap.svg +566 -0
  36. package/tests/examples/example46.test.ts +12 -0
  37. package/tests/examples/example47.test.ts +12 -0
  38. package/tests/examples/example48.test.ts +12 -0
  39. package/tests/examples/example49.test.ts +12 -0
  40. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +12 -12
@@ -36,6 +36,7 @@ import type {
36
36
  EvaluatedCandidate,
37
37
  } from "./types"
38
38
  import { visualizeAvailableNetOrientationSolver } from "./visualize"
39
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
39
40
 
40
41
  export class AvailableNetOrientationSolver extends BaseSolver {
41
42
  inputProblem: InputProblem
@@ -643,6 +644,9 @@ export class AvailableNetOrientationSolver extends BaseSolver {
643
644
  if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
644
645
  return "chip-collision"
645
646
  }
647
+ if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
648
+ return "text-collision"
649
+ }
646
650
  if (this.sharesChipBoundary(bounds)) {
647
651
  return "chip-collision"
648
652
  }
@@ -30,6 +30,7 @@ export type CandidateLabel = {
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
 
@@ -14,10 +14,12 @@ import { rectIntersectsAnyTrace } from "lib/solvers/NetLabelPlacementSolver/Sing
14
14
  import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
15
15
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
16
16
  import { getColorFromString } from "lib/utils/getColorFromString"
17
+ import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
17
18
 
18
19
  type CandidateStatus =
19
20
  | "ok"
20
21
  | "chip-collision"
22
+ | "text-collision"
21
23
  | "trace-collision"
22
24
  | "label-collision"
23
25
 
@@ -37,6 +39,7 @@ const CANDIDATE_STATUS_COLOR: Record<CandidateStatus, string> = {
37
39
  "label-collision": "orange",
38
40
  "trace-collision": "darkorange",
39
41
  "chip-collision": "red",
42
+ "text-collision": "purple",
40
43
  }
41
44
 
42
45
  const CANDIDATE_STATUS_FILL: Record<CandidateStatus, string> = {
@@ -44,6 +47,7 @@ const CANDIDATE_STATUS_FILL: Record<CandidateStatus, string> = {
44
47
  "label-collision": "rgba(255, 160, 0, 0.2)",
45
48
  "trace-collision": "rgba(200, 80, 0, 0.2)",
46
49
  "chip-collision": "rgba(220, 0, 0, 0.15)",
50
+ "text-collision": "rgba(128, 0, 128, 0.15)",
47
51
  }
48
52
 
49
53
  type Candidate = {
@@ -269,6 +273,8 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
269
273
 
270
274
  if (this.chipIndex.getChipsInBounds(bounds).length > 0)
271
275
  return "chip-collision"
276
+ if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
277
+ return "text-collision"
272
278
 
273
279
  if (
274
280
  rectIntersectsAnyTrace(bounds, this.traceMap, hostPairId, hostSegIndex)
@@ -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,
@@ -1,14 +1,67 @@
1
+ import type { Point } from "@tscircuit/math-utils"
1
2
  import type { GraphicsObject } from "graphics-debug"
2
3
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
4
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
5
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
6
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
6
7
  import { SingleOverlapSolver } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver"
7
- import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
8
+ import {
9
+ getCenterFromAnchor,
10
+ getRectBounds,
11
+ } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
8
12
  import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
9
13
  import { getColorFromString } from "lib/utils/getColorFromString"
10
14
  import type { InputProblem } from "lib/types/InputProblem"
11
15
 
16
+ const ON_PATH_EPS = 1e-6
17
+
18
+ function getClosestPointOnSegment(params: {
19
+ point: Point
20
+ segmentStart: Point
21
+ segmentEnd: Point
22
+ }): Point {
23
+ const { point, segmentStart, segmentEnd } = params
24
+ const dx = segmentEnd.x - segmentStart.x
25
+ const dy = segmentEnd.y - segmentStart.y
26
+ const segmentLengthSq = dx * dx + dy * dy
27
+ if (segmentLengthSq === 0) return { x: segmentStart.x, y: segmentStart.y }
28
+ const projection =
29
+ ((point.x - segmentStart.x) * dx + (point.y - segmentStart.y) * dy) /
30
+ segmentLengthSq
31
+ const clampedProjection = Math.max(0, Math.min(1, projection))
32
+ return {
33
+ x: segmentStart.x + clampedProjection * dx,
34
+ y: segmentStart.y + clampedProjection * dy,
35
+ }
36
+ }
37
+
38
+ function getClosestPointOnPath(point: Point, path: Point[]): Point {
39
+ let closestPoint: Point = path[0]!
40
+ let closestDistSq = Infinity
41
+ for (let i = 0; i < path.length - 1; i++) {
42
+ const candidatePoint = getClosestPointOnSegment({
43
+ point,
44
+ segmentStart: path[i]!,
45
+ segmentEnd: path[i + 1]!,
46
+ })
47
+ const dx = candidatePoint.x - point.x
48
+ const dy = candidatePoint.y - point.y
49
+ const candidateDistSq = dx * dx + dy * dy
50
+ if (candidateDistSq < closestDistSq) {
51
+ closestDistSq = candidateDistSq
52
+ closestPoint = candidatePoint
53
+ }
54
+ }
55
+ return closestPoint
56
+ }
57
+
58
+ function isPointOnPath(point: Point, path: Point[]): boolean {
59
+ const closestPoint = getClosestPointOnPath(point, path)
60
+ return (
61
+ Math.hypot(closestPoint.x - point.x, closestPoint.y - point.y) < ON_PATH_EPS
62
+ )
63
+ }
64
+
12
65
  /**
13
66
  * Merges all labels that touch/overlap with each other (transitively) around
14
67
  * a seed label into a single bounding-box label. This prevents the situation
@@ -159,10 +212,12 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
159
212
  (t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
160
213
  )
161
214
  if (idx !== -1) {
215
+ const oldPath = this.outputTraces[idx]!.tracePath
162
216
  this.outputTraces[idx] = {
163
217
  ...this.outputTraces[idx],
164
218
  tracePath: solvedPath,
165
219
  }
220
+ this.reanchorLabelsOnMovedTrace(this.outputTraces[idx]!, oldPath)
166
221
  }
167
222
  }
168
223
  this.activeSubSolver = null
@@ -229,6 +284,37 @@ export class NetLabelTraceCollisionSolver extends BaseSolver {
229
284
  })
230
285
  }
231
286
 
287
+ /**
288
+ * When a trace is rerouted, net labels that were anchored on the old path
289
+ * can be left floating in space. Snap their anchor back onto the new path.
290
+ */
291
+ private reanchorLabelsOnMovedTrace(
292
+ movedTrace: SolvedTracePath,
293
+ oldPath: Point[],
294
+ ) {
295
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
296
+ const label = this.outputNetLabelPlacements[i]!
297
+ if (label.globalConnNetId !== movedTrace.globalConnNetId) continue
298
+ if (isPointOnPath(label.anchorPoint, movedTrace.tracePath)) continue
299
+ if (!isPointOnPath(label.anchorPoint, oldPath)) continue
300
+
301
+ const newAnchor = getClosestPointOnPath(
302
+ label.anchorPoint,
303
+ movedTrace.tracePath,
304
+ )
305
+ this.outputNetLabelPlacements[i] = {
306
+ ...label,
307
+ anchorPoint: newAnchor,
308
+ center: getCenterFromAnchor(
309
+ newAnchor,
310
+ label.orientation,
311
+ label.width,
312
+ label.height,
313
+ ),
314
+ }
315
+ }
316
+ }
317
+
232
318
  getOutput() {
233
319
  return {
234
320
  traces: this.outputTraces,
@@ -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(