Skip to content

Check for vias inside pads - #180

Merged
seveibar merged 1 commit into
mainfrom
agent/check-vias-in-pads
Aug 2, 2026
Merged

Check for vias inside pads#180
seveibar merged 1 commit into
mainfrom
agent/check-vias-in-pads

Conversation

@seveibar

@seveibar seveibar commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add and export checkViasInPads
  • report a PCB placement error when a via center lies inside an SMD or plated-through pad on an overlapping copper layer
  • use the pad's true geometry rather than only its bounding box
  • include the rule in runAllPlacementChecks / runAllChecks
  • skip the rule when pcb_board.is_via_in_pad_allowed is explicitly true

Why

Via-in-pad usually requires an intentional manufacturing process. With the new circuit-json board flag, checks can reject accidental vias in pads while allowing designs that explicitly opt in.

The rule uses backward-compatible feature detection for the board flag so this focused PR does not need to pull in unrelated schema migrations through the repository's pinned development dependency.

Validation

  • bun test tests/lib/check-vias-in-pads.test.ts (4 passing)
  • bun test (141 passing)
  • bunx tsc --noEmit
  • bun run build
  • bunx @tscircuit/dependency-check
  • Biome formatting on all touched files
  • git diff --check

@seveibar
seveibar marked this pull request as ready for review August 2, 2026 20:42
@seveibar
seveibar merged commit fb8a2d3 into main Aug 2, 2026
5 checks passed
@seveibar
seveibar deleted the agent/check-vias-in-pads branch August 2, 2026 20:44
Comment on lines +49 to +157
test("reports a via whose center is inside an SMD pad", async () => {
const circuitJson: AnyCircuitElement[] = [makeBoard(), rectPad, viaInRectPad]

const errors = checkViasInPads(circuitJson)

expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({
type: "pcb_placement_error",
pcb_placement_error_id: "via_in_pad_pcb_via_1_pcb_smtpad_1",
error_type: "pcb_placement_error",
})
expect(errors[0].message).toContain("is inside SMD pad")
expect(containsCircuitJsonId(errors[0].message)).toBe(false)
expect(await runAllPlacementChecks(circuitJson)).toContainEqual(errors[0])
})

test("uses true pad geometry for circular, polygon, and plated-hole pads", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
{
type: "pcb_smtpad",
pcb_smtpad_id: "pcb_smtpad_circle",
shape: "circle",
x: -4,
y: 0,
radius: 0.5,
layer: "top",
},
{
type: "pcb_smtpad",
pcb_smtpad_id: "pcb_smtpad_polygon",
shape: "polygon",
points: [
{ x: -0.5, y: -0.5 },
{ x: 0.5, y: -0.5 },
{ x: 0, y: 0.5 },
],
layer: "top",
},
{
type: "pcb_plated_hole",
pcb_plated_hole_id: "pcb_plated_hole_1",
shape: "circle",
x: 4,
y: 0,
outer_diameter: 1,
hole_diameter: 0.5,
layers: ["top", "bottom"],
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_circle",
x: -4.2,
y: 0,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_polygon",
x: 0,
y: 0,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_plated_hole",
x: 4,
y: 0,
},
]

expect(checkViasInPads(circuitJson)).toHaveLength(3)
})

test("ignores vias outside pads and vias on non-overlapping layers", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
rectPad,
{
...viaInRectPad,
pcb_via_id: "pcb_via_outside",
x: 2,
y: 2,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_inner_layers",
x: 0,
y: 0,
layers: ["inner1", "inner2"],
},
]

expect(checkViasInPads(circuitJson)).toEqual([])
})

test("respects the board via-in-pad allowance", () => {
const allowedCircuit: AnyCircuitElement[] = [
makeBoard(true),
rectPad,
viaInRectPad,
]
const disallowedCircuit: AnyCircuitElement[] = [
makeBoard(false),
rectPad,
viaInRectPad,
]

expect(checkViasInPads(allowedCircuit)).toEqual([])
expect(checkViasInPads(disallowedCircuit)).toHaveLength(1)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains 4 test(...) calls (at lines 49, 65, 121, and 143), but the style guide requires that a *.test.ts file may have AT MOST one test(...). After the first test, the remaining tests should be split into separate, numbered files. For example: check-vias-in-pads1.test.ts, check-vias-in-pads2.test.ts, check-vias-in-pads3.test.ts, and check-vias-in-pads4.test.ts.

Suggested change
test("reports a via whose center is inside an SMD pad", async () => {
const circuitJson: AnyCircuitElement[] = [makeBoard(), rectPad, viaInRectPad]
const errors = checkViasInPads(circuitJson)
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({
type: "pcb_placement_error",
pcb_placement_error_id: "via_in_pad_pcb_via_1_pcb_smtpad_1",
error_type: "pcb_placement_error",
})
expect(errors[0].message).toContain("is inside SMD pad")
expect(containsCircuitJsonId(errors[0].message)).toBe(false)
expect(await runAllPlacementChecks(circuitJson)).toContainEqual(errors[0])
})
test("uses true pad geometry for circular, polygon, and plated-hole pads", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
{
type: "pcb_smtpad",
pcb_smtpad_id: "pcb_smtpad_circle",
shape: "circle",
x: -4,
y: 0,
radius: 0.5,
layer: "top",
},
{
type: "pcb_smtpad",
pcb_smtpad_id: "pcb_smtpad_polygon",
shape: "polygon",
points: [
{ x: -0.5, y: -0.5 },
{ x: 0.5, y: -0.5 },
{ x: 0, y: 0.5 },
],
layer: "top",
},
{
type: "pcb_plated_hole",
pcb_plated_hole_id: "pcb_plated_hole_1",
shape: "circle",
x: 4,
y: 0,
outer_diameter: 1,
hole_diameter: 0.5,
layers: ["top", "bottom"],
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_circle",
x: -4.2,
y: 0,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_polygon",
x: 0,
y: 0,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_plated_hole",
x: 4,
y: 0,
},
]
expect(checkViasInPads(circuitJson)).toHaveLength(3)
})
test("ignores vias outside pads and vias on non-overlapping layers", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
rectPad,
{
...viaInRectPad,
pcb_via_id: "pcb_via_outside",
x: 2,
y: 2,
},
{
...viaInRectPad,
pcb_via_id: "pcb_via_inner_layers",
x: 0,
y: 0,
layers: ["inner1", "inner2"],
},
]
expect(checkViasInPads(circuitJson)).toEqual([])
})
test("respects the board via-in-pad allowance", () => {
const allowedCircuit: AnyCircuitElement[] = [
makeBoard(true),
rectPad,
viaInRectPad,
]
const disallowedCircuit: AnyCircuitElement[] = [
makeBoard(false),
rectPad,
viaInRectPad,
]
expect(checkViasInPads(allowedCircuit)).toEqual([])
expect(checkViasInPads(disallowedCircuit)).toHaveLength(1)
})
test("reports a via whose center is inside an SMD pad", async () => {
const circuitJson: AnyCircuitElement[] = [makeBoard(), rectPad, viaInRectPad]
const errors = checkViasInPads(circuitJson)
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({
type: "pcb_placement_error",
pcb_placement_error_id: "via_in_pad_pcb_via_1_pcb_smtpad_1",
error_type: "pcb_placement_error",
})
expect(errors[0].message).toContain("is inside SMD pad")
expect(containsCircuitJsonId(errors[0].message)).toBe(false)
expect(await runAllPlacementChecks(circuitJson)).toContainEqual(errors[0])
})

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@tscircuitbot

Copy link
Copy Markdown
Contributor

Thank you for your contribution! 🎉

PR Rating: ⭐⭐
Impact: Minor

Track your contributions and see the leaderboard at: tscircuit Contribution Tracker


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants