From 9d4dc437f33244908bba3736d25348eba3e3940a Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 2 Aug 2026 13:36:11 -0700 Subject: [PATCH] check for vias inside pads --- index.ts | 1 + lib/check-vias-in-pads.ts | 75 +++++++++++++ lib/run-all-checks.ts | 2 + tests/lib/check-vias-in-pads.test.ts | 157 +++++++++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 lib/check-vias-in-pads.ts create mode 100644 tests/lib/check-vias-in-pads.test.ts diff --git a/index.ts b/index.ts index 3c7d597..c4b87bb 100644 --- a/index.ts +++ b/index.ts @@ -14,6 +14,7 @@ export { checkPcbTraceLengths } from "./lib/check-pcb-trace-lengths" export { checkPadPadClearance } from "./lib/check-pad-pad-clearance" export { checkPadTraceClearance } from "./lib/check-pad-trace-clearance" export { checkViaTraceClearance } from "./lib/check-via-trace-clearance" +export { checkViasInPads } from "./lib/check-vias-in-pads" export { dedupePcbDrcErrors } from "./lib/dedupe-pcb-drc-errors" export { checkPinMustBeConnected } from "./lib/check-pin-must-be-connected" export { checkAllPinsInComponentAreUnderspecified } from "./lib/check-all-pins-in-component-are-underspecified" diff --git a/lib/check-vias-in-pads.ts b/lib/check-vias-in-pads.ts new file mode 100644 index 0000000..b37d604 --- /dev/null +++ b/lib/check-vias-in-pads.ts @@ -0,0 +1,75 @@ +import { getPrimaryId } from "@tscircuit/circuit-json-util" +import type { AnyCircuitElement, PcbPlacementError, PcbVia } from "circuit-json" +import { + type PadElement, + getPadBounds, + getPads, +} from "./check-pad-clearance/common" +import { isPointInPad } from "./check-traces-are-contiguous/is-point-in-pad" +import { SpatialObjectIndex } from "./data-structures/SpatialIndex" +import { getPcbBoard } from "./drc-defaults" +import { getLayersOfPcbElement } from "./util/getLayersOfPcbElement" +import { getReadableNameForFootprintPad } from "./util/get-readable-names" + +export function checkViasInPads( + circuitJson: AnyCircuitElement[], +): PcbPlacementError[] { + const board = getPcbBoard(circuitJson) + if ( + board && + "is_via_in_pad_allowed" in board && + board.is_via_in_pad_allowed === true + ) { + return [] + } + + const vias = circuitJson.filter( + (element): element is PcbVia => element.type === "pcb_via", + ) + const pads = getPads(circuitJson) + if (vias.length === 0 || pads.length === 0) return [] + + const padOrdinals = new Map( + pads.map((pad, index) => [getPrimaryId(pad), index]), + ) + const padIndex = new SpatialObjectIndex({ + objects: pads, + getBounds: getPadBounds, + getId: getPrimaryId, + }) + const errors: PcbPlacementError[] = [] + + for (const via of vias) { + const nearbyPads = padIndex.getObjectsInBounds({ + minX: via.x, + minY: via.y, + maxX: via.x, + maxY: via.y, + }) + + for (const pad of nearbyPads) { + const viaLayers = getLayersOfPcbElement(via) + const padLayers = getLayersOfPcbElement(pad) + if (!viaLayers.some((layer) => padLayers.includes(layer))) continue + if (!isPointInPad(via, pad)) continue + + const padId = getPrimaryId(pad) + const padOrdinal = padOrdinals.get(padId) ?? 0 + const padName = getReadableNameForFootprintPad( + circuitJson, + pad, + padOrdinal, + ) + + errors.push({ + type: "pcb_placement_error", + pcb_placement_error_id: `via_in_pad_${via.pcb_via_id}_${padId}`, + error_type: "pcb_placement_error", + message: `Via at (${via.x.toFixed(2)}mm, ${via.y.toFixed(2)}mm) is inside ${padName}`, + subcircuit_id: via.subcircuit_id ?? pad.subcircuit_id, + }) + } + } + + return errors +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index 184f908..e95824d 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -20,10 +20,12 @@ import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceO import { checkTracesAreContiguous } from "./check-traces-are-contiguous/check-traces-are-contiguous" import { checkTestPointAccessibility } from "./check-testpoint-accessibility" import { checkViaTraceClearance } from "./check-via-trace-clearance" +import { checkViasInPads } from "./check-vias-in-pads" export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { return [ ...checkViasOffBoard(circuitJson), + ...checkViasInPads(circuitJson), ...checkPcbComponentsOutOfBoard(circuitJson), ...checkPcbComponentOverlap(circuitJson), ...checkPadPadClearance(circuitJson), diff --git a/tests/lib/check-vias-in-pads.test.ts b/tests/lib/check-vias-in-pads.test.ts new file mode 100644 index 0000000..1b47485 --- /dev/null +++ b/tests/lib/check-vias-in-pads.test.ts @@ -0,0 +1,157 @@ +import { expect, test } from "bun:test" +import type { + AnyCircuitElement, + PcbBoard, + PcbSmtPad, + PcbVia, +} from "circuit-json" +import { checkViasInPads } from "lib/check-vias-in-pads" +import { runAllPlacementChecks } from "lib/run-all-checks" +import { containsCircuitJsonId } from "lib/util/get-readable-names" + +const makeBoard = ( + isViaInPadAllowed?: boolean, +): PcbBoard & { is_via_in_pad_allowed?: boolean } => ({ + type: "pcb_board", + pcb_board_id: "pcb_board_1", + center: { x: 0, y: 0 }, + width: 20, + height: 20, + thickness: 1.6, + num_layers: 4, + material: "fr4", + ...(isViaInPadAllowed === undefined + ? {} + : { is_via_in_pad_allowed: isViaInPadAllowed }), +}) + +const rectPad: PcbSmtPad = { + type: "pcb_smtpad", + pcb_smtpad_id: "pcb_smtpad_1", + shape: "rect", + x: 0, + y: 0, + width: 1, + height: 1, + layer: "top", +} + +const viaInRectPad: PcbVia = { + type: "pcb_via", + pcb_via_id: "pcb_via_1", + x: 0.2, + y: 0.1, + hole_diameter: 0.2, + outer_diameter: 0.4, + layers: ["top", "bottom"], +} + +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) +})