From a8175bc13bd544ef17239b0ba82660ed20801bb8 Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 14:36:57 +0000 Subject: [PATCH 1/8] feat: add DRC check for vias too close to pads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements issue #44 — a new DRC check that validates via placement relative to pads (both SMT pads and plated holes). Uses rect-to-circle and circle-to-circle distance calculations for accurate gap measurement. Closes #44 Co-Authored-By: Claude Opus 4.6 --- index.ts | 1 + lib/check-via-to-pad-spacing.ts | 177 +++++++++++++++++++++ lib/drc-defaults.ts | 1 + lib/run-all-checks.ts | 2 + tests/lib/check-via-to-pad-spacing.test.ts | 175 ++++++++++++++++++++ 5 files changed, 356 insertions(+) create mode 100644 lib/check-via-to-pad-spacing.ts create mode 100644 tests/lib/check-via-to-pad-spacing.test.ts diff --git a/index.ts b/index.ts index cd67a83..7b89f29 100644 --- a/index.ts +++ b/index.ts @@ -17,3 +17,4 @@ export { } from "./lib/run-all-checks" export { checkConnectorAccessibleOrientation } from "./lib/check-connector-accessible-orientation" +export { checkViaToPadSpacing } from "./lib/check-via-to-pad-spacing" diff --git a/lib/check-via-to-pad-spacing.ts b/lib/check-via-to-pad-spacing.ts new file mode 100644 index 0000000..f91e74a --- /dev/null +++ b/lib/check-via-to-pad-spacing.ts @@ -0,0 +1,177 @@ +import type { + AnyCircuitElement, + PcbVia, + PcbSmtPad, + PcbPlatedHole, + PcbViaClearanceError, +} from "circuit-json" +import { getReadableNameForElement } from "@tscircuit/circuit-json-util" +import { DEFAULT_VIA_TO_PAD_MARGIN, EPSILON } from "lib/drc-defaults" + +type Pad = PcbSmtPad | PcbPlatedHole + +/** + * Get the effective radius of a pad for clearance calculations. + * For rectangular pads, returns the half-diagonal (conservative bounding circle). + * For circular pads/plated holes, returns the actual radius. + */ +function getPadRadius(pad: Pad): number { + if (pad.type === "pcb_smtpad") { + if (pad.shape === "circle") return pad.radius + if ( + pad.shape === "rect" || + pad.shape === "rotated_rect" || + pad.shape === "pill" || + pad.shape === "rotated_pill" + ) { + return Math.max(pad.width, pad.height) / 2 + } + return 0 + } + if (pad.type === "pcb_plated_hole") { + if (pad.shape === "circle") return pad.outer_diameter / 2 + if (pad.shape === "oval" || pad.shape === "pill") { + return Math.max(pad.outer_width, pad.outer_height) / 2 + } + if (pad.shape === "pill_hole_with_rect_pad") { + return Math.max(pad.rect_pad_width, pad.rect_pad_height) / 2 + } + return 0 + } + return 0 +} + +function getPadId(pad: Pad): string { + if (pad.type === "pcb_smtpad") return pad.pcb_smtpad_id + return pad.pcb_plated_hole_id +} + +/** + * Compute the minimum distance between a via (circle) and a rectangular pad. + * Returns the edge-to-edge gap (negative if overlapping). + */ +function distanceViaToRectPad( + via: PcbVia, + pad: { x: number; y: number; width: number; height: number }, +): number { + const halfW = pad.width / 2 + const halfH = pad.height / 2 + // Nearest point on rectangle to via center + const nearestX = Math.max(pad.x - halfW, Math.min(via.x, pad.x + halfW)) + const nearestY = Math.max(pad.y - halfH, Math.min(via.y, pad.y + halfH)) + const dist = Math.hypot(via.x - nearestX, via.y - nearestY) + return dist - via.outer_diameter / 2 +} + +/** + * Compute the edge-to-edge gap between a via and a circular pad. + */ +function distanceViaToCirclePad( + via: PcbVia, + padX: number, + padY: number, + padRadius: number, +): number { + const dist = Math.hypot(via.x - padX, via.y - padY) + return dist - via.outer_diameter / 2 - padRadius +} + +/** + * Compute the edge-to-edge gap between a via and any pad type. + */ +function computeGap(via: PcbVia, pad: Pad): number { + if (pad.type === "pcb_smtpad") { + if (pad.shape === "circle") { + return distanceViaToCirclePad(via, pad.x, pad.y, pad.radius) + } + if ( + pad.shape === "rect" || + pad.shape === "rotated_rect" || + pad.shape === "pill" || + pad.shape === "rotated_pill" + ) { + return distanceViaToRectPad(via, { + x: pad.x, + y: pad.y, + width: pad.width, + height: pad.height, + }) + } + // Fallback: use bounding circle + return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad)) + } + if (pad.type === "pcb_plated_hole") { + if (pad.shape === "circle") { + return distanceViaToCirclePad(via, pad.x, pad.y, pad.outer_diameter / 2) + } + if (pad.shape === "oval" || pad.shape === "pill") { + return distanceViaToRectPad(via, { + x: pad.x, + y: pad.y, + width: pad.outer_width, + height: pad.outer_height, + }) + } + if (pad.shape === "pill_hole_with_rect_pad") { + return distanceViaToRectPad(via, { + x: pad.x, + y: pad.y, + width: pad.rect_pad_width, + height: pad.rect_pad_height, + }) + } + return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad)) + } + return Number.POSITIVE_INFINITY +} + +export function checkViaToPadSpacing( + circuitJson: AnyCircuitElement[], + { + minSpacing = DEFAULT_VIA_TO_PAD_MARGIN, + }: { minSpacing?: number } = {}, +): PcbViaClearanceError[] { + const vias = circuitJson.filter((el) => el.type === "pcb_via") as PcbVia[] + const pads: Pad[] = [ + ...(circuitJson.filter((el) => el.type === "pcb_smtpad") as PcbSmtPad[]), + ...(circuitJson.filter( + (el) => el.type === "pcb_plated_hole", + ) as PcbPlatedHole[]), + ] + + if (vias.length === 0 || pads.length === 0) return [] + + const errors: PcbViaClearanceError[] = [] + + for (const via of vias) { + for (const pad of pads) { + const gap = computeGap(via, pad) + if (gap + EPSILON >= minSpacing) continue + + const padId = getPadId(pad) + const pairId = [via.pcb_via_id, padId].sort().join("_") + + errors.push({ + type: "pcb_via_clearance_error", + pcb_error_id: `via_pad_close_${pairId}`, + message: `Via ${getReadableNameForElement( + circuitJson, + via.pcb_via_id, + )} is too close to pad ${getReadableNameForElement( + circuitJson, + padId, + )} (gap: ${gap.toFixed(3)}mm, minimum: ${minSpacing}mm)`, + error_type: "pcb_via_clearance_error", + pcb_via_ids: [via.pcb_via_id], + minimum_clearance: minSpacing, + actual_clearance: gap, + pcb_center: { + x: (via.x + pad.x) / 2, + y: (via.y + pad.y) / 2, + }, + }) + } + } + + return errors +} diff --git a/lib/drc-defaults.ts b/lib/drc-defaults.ts index cf578e9..959be96 100644 --- a/lib/drc-defaults.ts +++ b/lib/drc-defaults.ts @@ -5,5 +5,6 @@ export const DEFAULT_VIA_BOARD_MARGIN = 0.3 export const DEFAULT_SAME_NET_VIA_MARGIN = 0.2 export const DEFAULT_DIFFERENT_NET_VIA_MARGIN = 0.3 +export const DEFAULT_VIA_TO_PAD_MARGIN = 0.2 export const EPSILON = 0.005 diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index b73bd1f..711a06e 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -8,6 +8,7 @@ import { checkPcbComponentOverlap } from "./check-pcb-components-overlap/checkPc import { checkConnectorAccessibleOrientation } from "./check-connector-accessible-orientation" import { checkPinMustBeConnected } from "./check-pin-must-be-connected" import { checkSameNetViaSpacing } from "./check-same-net-via-spacing" +import { checkViaToPadSpacing } from "./check-via-to-pad-spacing" import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-traces" import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceOutOfBoard" import { checkTracesAreContiguous } from "./check-traces-are-contiguous/check-traces-are-contiguous" @@ -32,6 +33,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) { ...checkEachPcbTraceNonOverlapping(circuitJson), ...checkSameNetViaSpacing(circuitJson), ...checkDifferentNetViaSpacing(circuitJson), + ...checkViaToPadSpacing(circuitJson), // ...checkTracesAreContiguous(circuitJson), ...checkPcbTracesOutOfBoard(circuitJson), ] diff --git a/tests/lib/check-via-to-pad-spacing.test.ts b/tests/lib/check-via-to-pad-spacing.test.ts new file mode 100644 index 0000000..c1ea442 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing.test.ts @@ -0,0 +1,175 @@ +import { expect, test, describe } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +describe("checkViaToPadSpacing", () => { + test("returns error when via is too close to a rectangular SMT pad", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 0.5, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + // Via edge at 0.3, pad left edge at 0.3 => gap = 0mm, well below 0.2mm default + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(1) + expect(errors[0].message).toContain("too close to pad") + }) + + test("no error when via is far from pad", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 2, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(0) + }) + + test("returns error when via is too close to a circular SMT pad", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "circle", + x: 0.7, + y: 0, + radius: 0.2, + layer: "top", + }, + ] + // center-to-center = 0.7, via radius = 0.3, pad radius = 0.2 => gap = 0.2mm + // gap + EPSILON (0.005) >= 0.2mm minSpacing => no error + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(0) + }) + + test("returns error when via is too close to a plated hole", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_plated_hole", + pcb_plated_hole_id: "hole1", + shape: "circle", + x: 0.6, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.5, + layers: ["top", "bottom"], + pcb_component_id: "comp1", + pcb_port_id: "port1", + }, + ] + // center-to-center = 0.6, via radius = 0.3, hole radius = 0.25 => gap = 0.05mm < 0.2mm + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(1) + expect(errors[0].message).toContain("too close to pad") + }) + + test("respects custom minSpacing parameter", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 1.0, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + // Via edge at 0.3, pad left edge at 0.8 => gap = 0.5mm + // With default 0.2mm: no error. With 0.6mm: error + expect(checkViaToPadSpacing(soup)).toHaveLength(0) + expect(checkViaToPadSpacing(soup, { minSpacing: 0.6 })).toHaveLength(1) + }) + + test("returns empty array when no vias", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 0, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + expect(checkViaToPadSpacing(soup)).toHaveLength(0) + }) + + test("returns empty array when no pads", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + ] + expect(checkViaToPadSpacing(soup)).toHaveLength(0) + }) +}) From 0783bc382f0968580f9ea25457e9943ae7202f48 Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 15:15:47 +0000 Subject: [PATCH 2/8] refactor: split via-to-pad-spacing tests into separate files One test per file per project convention. Moved tests from single file into check-via-to-pad-spacing/ directory. Co-Authored-By: Claude Opus 4.6 --- tests/lib/check-via-to-pad-spacing.test.ts | 175 ------------------ .../custom-min-spacing.test.ts | 31 ++++ .../check-via-to-pad-spacing/no-pads.test.ts | 18 ++ .../check-via-to-pad-spacing/no-vias.test.ts | 19 ++ .../via-close-to-circle-pad.test.ts | 30 +++ .../via-close-to-plated-hole.test.ts | 33 ++++ .../via-far-from-pad.test.ts | 29 +++ .../via-too-close-to-rect-pad.test.ts | 31 ++++ 8 files changed, 191 insertions(+), 175 deletions(-) delete mode 100644 tests/lib/check-via-to-pad-spacing.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/no-pads.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/no-vias.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts create mode 100644 tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts diff --git a/tests/lib/check-via-to-pad-spacing.test.ts b/tests/lib/check-via-to-pad-spacing.test.ts deleted file mode 100644 index c1ea442..0000000 --- a/tests/lib/check-via-to-pad-spacing.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { expect, test, describe } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -describe("checkViaToPadSpacing", () => { - test("returns error when via is too close to a rectangular SMT pad", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 0.5, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - // Via edge at 0.3, pad left edge at 0.3 => gap = 0mm, well below 0.2mm default - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(1) - expect(errors[0].message).toContain("too close to pad") - }) - - test("no error when via is far from pad", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 2, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(0) - }) - - test("returns error when via is too close to a circular SMT pad", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "circle", - x: 0.7, - y: 0, - radius: 0.2, - layer: "top", - }, - ] - // center-to-center = 0.7, via radius = 0.3, pad radius = 0.2 => gap = 0.2mm - // gap + EPSILON (0.005) >= 0.2mm minSpacing => no error - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(0) - }) - - test("returns error when via is too close to a plated hole", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_plated_hole", - pcb_plated_hole_id: "hole1", - shape: "circle", - x: 0.6, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.5, - layers: ["top", "bottom"], - pcb_component_id: "comp1", - pcb_port_id: "port1", - }, - ] - // center-to-center = 0.6, via radius = 0.3, hole radius = 0.25 => gap = 0.05mm < 0.2mm - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(1) - expect(errors[0].message).toContain("too close to pad") - }) - - test("respects custom minSpacing parameter", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 1.0, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - // Via edge at 0.3, pad left edge at 0.8 => gap = 0.5mm - // With default 0.2mm: no error. With 0.6mm: error - expect(checkViaToPadSpacing(soup)).toHaveLength(0) - expect(checkViaToPadSpacing(soup, { minSpacing: 0.6 })).toHaveLength(1) - }) - - test("returns empty array when no vias", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 0, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - expect(checkViaToPadSpacing(soup)).toHaveLength(0) - }) - - test("returns empty array when no pads", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - ] - expect(checkViaToPadSpacing(soup)).toHaveLength(0) - }) -}) diff --git a/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts b/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts new file mode 100644 index 0000000..e879562 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("respects custom minSpacing parameter", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 1.0, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + // Via edge at 0.3, pad left edge at 0.8 => gap = 0.5mm + // With default 0.2mm: no error. With 0.6mm: error + expect(checkViaToPadSpacing(soup)).toHaveLength(0) + expect(checkViaToPadSpacing(soup, { minSpacing: 0.6 })).toHaveLength(1) +}) diff --git a/tests/lib/check-via-to-pad-spacing/no-pads.test.ts b/tests/lib/check-via-to-pad-spacing/no-pads.test.ts new file mode 100644 index 0000000..8525280 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/no-pads.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("returns empty array when no pads", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + ] + expect(checkViaToPadSpacing(soup)).toHaveLength(0) +}) diff --git a/tests/lib/check-via-to-pad-spacing/no-vias.test.ts b/tests/lib/check-via-to-pad-spacing/no-vias.test.ts new file mode 100644 index 0000000..5d31b31 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/no-vias.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("returns empty array when no vias", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 0, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + expect(checkViaToPadSpacing(soup)).toHaveLength(0) +}) diff --git a/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts new file mode 100644 index 0000000..68a0d19 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("no error when via gap to circular SMT pad equals minSpacing", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "circle", + x: 0.7, + y: 0, + radius: 0.2, + layer: "top", + }, + ] + // center-to-center = 0.7, via radius = 0.3, pad radius = 0.2 => gap = 0.2mm + // gap + EPSILON (0.005) >= 0.2mm minSpacing => no error + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(0) +}) diff --git a/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts b/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts new file mode 100644 index 0000000..4a16f66 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("returns error when via is too close to a plated hole", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_plated_hole", + pcb_plated_hole_id: "hole1", + shape: "circle", + x: 0.6, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.5, + layers: ["top", "bottom"], + pcb_component_id: "comp1", + pcb_port_id: "port1", + }, + ] + // center-to-center = 0.6, via radius = 0.3, hole radius = 0.25 => gap = 0.05mm < 0.2mm + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(1) + expect(errors[0].message).toContain("too close to pad") +}) diff --git a/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts new file mode 100644 index 0000000..d19dc73 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("no error when via is far from pad", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 2, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(0) +}) diff --git a/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts new file mode 100644 index 0000000..f68fdd9 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "bun:test" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" + +test("returns error when via is too close to a rectangular SMT pad", () => { + const soup: AnyCircuitElement[] = [ + { + type: "pcb_via", + pcb_via_id: "via1", + x: 0, + y: 0, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad1", + shape: "rect", + x: 0.5, + y: 0, + width: 0.4, + height: 0.3, + layer: "top", + }, + ] + // Via edge at 0.3, pad left edge at 0.3 => gap = 0mm, well below 0.2mm default + const errors = checkViaToPadSpacing(soup) + expect(errors).toHaveLength(1) + expect(errors[0].message).toContain("too close to pad") +}) From 03342974cc64bea5d72ad9028fe1e2cefc92b1fa Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 17:17:42 +0000 Subject: [PATCH 3/8] fix: add PCB snapshots to via-to-pad spacing tests Address review feedback: add SVG snapshots with error rendering to the error-detecting test cases so reviewers can visually verify the DRC check behavior. Co-Authored-By: Claude Opus 4.6 --- .../__snapshots__/via-close-to-plated-hole.snap.svg | 1 + .../__snapshots__/via-too-close-to-rect-pad.snap.svg | 1 + .../via-close-to-plated-hole.test.ts | 8 +++++++- .../via-too-close-to-rect-pad.test.ts | 8 +++++++- 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg create mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg new file mode 100644 index 0000000..20cfe31 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg new file mode 100644 index 0000000..24e0a0a --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts b/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts index 4a16f66..496fd64 100644 --- a/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts +++ b/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts @@ -1,8 +1,9 @@ import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" import type { AnyCircuitElement } from "circuit-json" -test("returns error when via is too close to a plated hole", () => { +test("returns error when via is too close to a plated hole", async () => { const soup: AnyCircuitElement[] = [ { type: "pcb_via", @@ -30,4 +31,9 @@ test("returns error when via is too close to a plated hole", () => { const errors = checkViaToPadSpacing(soup) expect(errors).toHaveLength(1) expect(errors[0].message).toContain("too close to pad") + + const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { + shouldDrawErrors: true, + }) + await expect(svg).toMatchSvgSnapshot(import.meta.path) }) diff --git a/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts index f68fdd9..1ef318f 100644 --- a/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts +++ b/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts @@ -1,8 +1,9 @@ import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" import type { AnyCircuitElement } from "circuit-json" -test("returns error when via is too close to a rectangular SMT pad", () => { +test("returns error when via is too close to a rectangular SMT pad", async () => { const soup: AnyCircuitElement[] = [ { type: "pcb_via", @@ -28,4 +29,9 @@ test("returns error when via is too close to a rectangular SMT pad", () => { const errors = checkViaToPadSpacing(soup) expect(errors).toHaveLength(1) expect(errors[0].message).toContain("too close to pad") + + const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { + shouldDrawErrors: true, + }) + await expect(svg).toMatchSvgSnapshot(import.meta.path) }) From 0c0496851c8c0c7b792e3000a6b34d2711aa34fb Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 17:30:08 +0000 Subject: [PATCH 4/8] fix: add realistic PCB snapshot test and fix getPadRadius half-diagonal - Add circuit JSON asset with board, components, traces and vias for a realistic PCB snapshot showing via-to-pad spacing violations - Fix getPadRadius to use actual half-diagonal (sqrt) instead of max/2 for the fallback bounding circle on rectangular pads Co-Authored-By: Claude Opus 4.6 --- lib/check-via-to-pad-spacing.ts | 2 +- tests/assets/via-too-close-to-pad.json | 170 ++++++++++++++++++ .../via-too-close-to-pad-snapshot.snap.svg | 1 + .../via-too-close-to-pad-snapshot.test.ts | 19 ++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 tests/assets/via-too-close-to-pad.json create mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg create mode 100644 tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts diff --git a/lib/check-via-to-pad-spacing.ts b/lib/check-via-to-pad-spacing.ts index f91e74a..a40f58b 100644 --- a/lib/check-via-to-pad-spacing.ts +++ b/lib/check-via-to-pad-spacing.ts @@ -24,7 +24,7 @@ function getPadRadius(pad: Pad): number { pad.shape === "pill" || pad.shape === "rotated_pill" ) { - return Math.max(pad.width, pad.height) / 2 + return Math.sqrt(pad.width ** 2 + pad.height ** 2) / 2 } return 0 } diff --git a/tests/assets/via-too-close-to-pad.json b/tests/assets/via-too-close-to-pad.json new file mode 100644 index 0000000..a88bec7 --- /dev/null +++ b/tests/assets/via-too-close-to-pad.json @@ -0,0 +1,170 @@ +[ + { + "type": "pcb_board", + "pcb_board_id": "board1", + "center": { "x": 0, "y": 0 }, + "width": 10, + "height": 8 + }, + { + "type": "source_component", + "source_component_id": "sc1", + "ftype": "simple_resistor", + "name": "R1" + }, + { + "type": "source_port", + "source_port_id": "sp1", + "source_component_id": "sc1", + "name": "pin1" + }, + { + "type": "source_port", + "source_port_id": "sp2", + "source_component_id": "sc1", + "name": "pin2" + }, + { + "type": "pcb_component", + "pcb_component_id": "pc1", + "source_component_id": "sc1", + "center": { "x": -2, "y": 0 }, + "width": 2.4, + "height": 1.2, + "rotation": 0, + "layer": "top" + }, + { + "type": "pcb_smtpad", + "pcb_smtpad_id": "pad1", + "pcb_component_id": "pc1", + "pcb_port_id": "pp1", + "shape": "rect", + "x": -2.8, + "y": 0, + "width": 1.0, + "height": 0.8, + "layer": "top", + "port_hints": ["pin1"] + }, + { + "type": "pcb_smtpad", + "pcb_smtpad_id": "pad2", + "pcb_component_id": "pc1", + "pcb_port_id": "pp2", + "shape": "rect", + "x": -1.2, + "y": 0, + "width": 1.0, + "height": 0.8, + "layer": "top", + "port_hints": ["pin2"] + }, + { + "type": "pcb_port", + "pcb_port_id": "pp1", + "pcb_component_id": "pc1", + "source_port_id": "sp1", + "x": -2.8, + "y": 0, + "layers": ["top"] + }, + { + "type": "pcb_port", + "pcb_port_id": "pp2", + "pcb_component_id": "pc1", + "source_port_id": "sp2", + "x": -1.2, + "y": 0, + "layers": ["top"] + }, + { + "type": "source_trace", + "source_trace_id": "st1", + "connected_source_port_ids": ["sp2"] + }, + { + "type": "pcb_trace", + "pcb_trace_id": "trace1", + "source_trace_id": "st1", + "route": [ + { "route_type": "wire", "x": -1.2, "y": 0, "width": 0.15, "layer": "top" }, + { "route_type": "wire", "x": 0, "y": 0, "width": 0.15, "layer": "top" }, + { "route_type": "via", "x": 0, "y": 0, "to_layer": "bottom", "from_layer": "top" }, + { "route_type": "wire", "x": 0, "y": 0, "width": 0.15, "layer": "bottom" }, + { "route_type": "wire", "x": 2, "y": 0, "width": 0.15, "layer": "bottom" } + ] + }, + { + "type": "pcb_via", + "pcb_via_id": "via1", + "x": -0.5, + "y": 0, + "hole_diameter": 0.3, + "outer_diameter": 0.6, + "layers": ["top", "bottom"] + }, + { + "type": "source_component", + "source_component_id": "sc2", + "ftype": "simple_capacitor", + "name": "C1" + }, + { + "type": "source_port", + "source_port_id": "sp3", + "source_component_id": "sc2", + "name": "pin1" + }, + { + "type": "source_port", + "source_port_id": "sp4", + "source_component_id": "sc2", + "name": "pin2" + }, + { + "type": "pcb_component", + "pcb_component_id": "pc2", + "source_component_id": "sc2", + "center": { "x": 2, "y": -2 }, + "width": 2.0, + "height": 1.0, + "rotation": 0, + "layer": "top" + }, + { + "type": "pcb_smtpad", + "pcb_smtpad_id": "pad3", + "pcb_component_id": "pc2", + "pcb_port_id": "pp3", + "shape": "rect", + "x": 1.5, + "y": -2, + "width": 0.8, + "height": 0.6, + "layer": "top", + "port_hints": ["pin1"] + }, + { + "type": "pcb_smtpad", + "pcb_smtpad_id": "pad4", + "pcb_component_id": "pc2", + "pcb_port_id": "pp4", + "shape": "rect", + "x": 2.5, + "y": -2, + "width": 0.8, + "height": 0.6, + "layer": "top", + "port_hints": ["pin2"] + }, + { + "type": "pcb_via", + "pcb_via_id": "via2", + "x": 1.5, + "y": -1.2, + "hole_diameter": 0.3, + "outer_diameter": 0.6, + "layers": ["top", "bottom"] + } +] diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg new file mode 100644 index 0000000..49031a0 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts b/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts new file mode 100644 index 0000000..04eb416 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import type { AnyCircuitElement } from "circuit-json" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import circuitJson from "tests/assets/via-too-close-to-pad.json" + +test("renders PCB snapshot with via-to-pad spacing violations", async () => { + const soup = circuitJson as AnyCircuitElement[] + const errors = checkViaToPadSpacing(soup) + + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain("too close to pad") + + const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { + shouldDrawErrors: true, + }) + + await expect(svg).toMatchSvgSnapshot(import.meta.path) +}) From 1bc58147811d49ad22cfb4c857033f2199773685 Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 18:09:02 +0000 Subject: [PATCH 5/8] feat: add clear PCB snapshot test with real tscircuit circuit Renders SOIC8 chips with a via placed too close to a pad, showing the DRC error highlighted in the SVG snapshot. Co-Authored-By: Claude Opus 4.6 --- .../__snapshots__/pcb-snapshot.snap.svg | 1 + .../pcb-snapshot.test.tsx | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg create mode 100644 tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg new file mode 100644 index 0000000..595c524 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg @@ -0,0 +1 @@ +U1U2Trace too close to board edge (0.000mm < 0.275mm required, margin: 0.2mm)Component U2 extends outside board boundaries by 0.65mm. Try moving it 0.65mm left to fit within the board edge. \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx b/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx new file mode 100644 index 0000000..e3b279f --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx @@ -0,0 +1,57 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" + +test("pcb snapshot showing via too close to pad", async () => { + const circuit = new Circuit() + circuit.add( + + + + + , + ) + + await circuit.renderUntilSettled() + + const circuitJson = circuit.getCircuitJson() as any[] + + // Find a pad from U1 to place a via too close to it + const pad = circuitJson.find( + (el: any) => el.type === "pcb_smtpad" && el.pcb_component_id, + ) + + if (pad) { + // Place a via 0.2mm from the pad center (well within clearance) + circuitJson.push({ + type: "pcb_via", + pcb_via_id: "test_via_1", + x: pad.x + 0.3, + y: pad.y, + hole_diameter: 0.3, + outer_diameter: 0.6, + layers: ["top", "bottom"], + }) + } + + const errors = checkViaToPadSpacing(circuitJson) + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain("too close to pad") + + const pcbSvg = convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + }) + + await expect(pcbSvg).toMatchSvgSnapshot(import.meta.path) +}) From 4a243c07f16a5699d9cbee846986dd70b1bfba9c Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 18:42:21 +0000 Subject: [PATCH 6/8] fix: remove noisy pcb-snapshot test, keep clean JSON-fixture snapshot Remove pcb-snapshot.test.tsx which produced an SVG with unrelated DRC errors (trace-to-edge, component-outside-board) that obscured the via-to-pad check. Keep via-too-close-to-pad-snapshot.test.ts which uses a clean JSON fixture and produces a focused snapshot showing only the relevant elements. Co-Authored-By: Claude Opus 4.6 --- .../__snapshots__/pcb-snapshot.snap.svg | 1 - .../pcb-snapshot.test.tsx | 57 ------------------- 2 files changed, 58 deletions(-) delete mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg delete mode 100644 tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg deleted file mode 100644 index 595c524..0000000 --- a/tests/lib/check-via-to-pad-spacing/__snapshots__/pcb-snapshot.snap.svg +++ /dev/null @@ -1 +0,0 @@ -U1U2Trace too close to board edge (0.000mm < 0.275mm required, margin: 0.2mm)Component U2 extends outside board boundaries by 0.65mm. Try moving it 0.65mm left to fit within the board edge. \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx b/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx deleted file mode 100644 index e3b279f..0000000 --- a/tests/lib/check-via-to-pad-spacing/pcb-snapshot.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { expect, test } from "bun:test" -import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" -import { Circuit } from "tscircuit" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" - -test("pcb snapshot showing via too close to pad", async () => { - const circuit = new Circuit() - circuit.add( - - - - - , - ) - - await circuit.renderUntilSettled() - - const circuitJson = circuit.getCircuitJson() as any[] - - // Find a pad from U1 to place a via too close to it - const pad = circuitJson.find( - (el: any) => el.type === "pcb_smtpad" && el.pcb_component_id, - ) - - if (pad) { - // Place a via 0.2mm from the pad center (well within clearance) - circuitJson.push({ - type: "pcb_via", - pcb_via_id: "test_via_1", - x: pad.x + 0.3, - y: pad.y, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }) - } - - const errors = checkViaToPadSpacing(circuitJson) - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].message).toContain("too close to pad") - - const pcbSvg = convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { - shouldDrawErrors: true, - }) - - await expect(pcbSvg).toMatchSvgSnapshot(import.meta.path) -}) From 6cb31b6d69f39704ba2519042042e02c14e71a6d Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 19:55:06 +0000 Subject: [PATCH 7/8] refactor: simplify via-to-pad check using getBoundsOfPcbElements Use getBoundsOfPcbElements from circuit-json-util for generic pad shape handling instead of manually dispatching on every shape variant. Reduces to one clear test with a real tscircuit PCB snapshot per review feedback. Co-Authored-By: Claude Opus 4.6 --- lib/check-via-to-pad-spacing.ts | 123 +++--------------- .../via-close-to-plated-hole.snap.svg | 1 - .../via-too-close-to-pad-snapshot.snap.svg | 1 - .../via-too-close-to-rect-pad.snap.svg | 1 - .../custom-min-spacing.test.ts | 31 ----- .../check-via-to-pad-spacing/no-pads.test.ts | 18 --- .../check-via-to-pad-spacing/no-vias.test.ts | 19 --- .../via-close-to-circle-pad.test.ts | 30 ----- .../via-close-to-plated-hole.test.ts | 39 ------ .../via-far-from-pad.test.ts | 29 ----- .../via-too-close-to-pad-snapshot.test.ts | 19 --- .../via-too-close-to-rect-pad.test.ts | 37 ------ 12 files changed, 15 insertions(+), 333 deletions(-) delete mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg delete mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg delete mode 100644 tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg delete mode 100644 tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/no-pads.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/no-vias.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts delete mode 100644 tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts diff --git a/lib/check-via-to-pad-spacing.ts b/lib/check-via-to-pad-spacing.ts index a40f58b..0271aec 100644 --- a/lib/check-via-to-pad-spacing.ts +++ b/lib/check-via-to-pad-spacing.ts @@ -5,131 +5,38 @@ import type { PcbPlatedHole, PcbViaClearanceError, } from "circuit-json" -import { getReadableNameForElement } from "@tscircuit/circuit-json-util" +import { + getReadableNameForElement, + getBoundsOfPcbElements, +} from "@tscircuit/circuit-json-util" import { DEFAULT_VIA_TO_PAD_MARGIN, EPSILON } from "lib/drc-defaults" type Pad = PcbSmtPad | PcbPlatedHole -/** - * Get the effective radius of a pad for clearance calculations. - * For rectangular pads, returns the half-diagonal (conservative bounding circle). - * For circular pads/plated holes, returns the actual radius. - */ -function getPadRadius(pad: Pad): number { - if (pad.type === "pcb_smtpad") { - if (pad.shape === "circle") return pad.radius - if ( - pad.shape === "rect" || - pad.shape === "rotated_rect" || - pad.shape === "pill" || - pad.shape === "rotated_pill" - ) { - return Math.sqrt(pad.width ** 2 + pad.height ** 2) / 2 - } - return 0 - } - if (pad.type === "pcb_plated_hole") { - if (pad.shape === "circle") return pad.outer_diameter / 2 - if (pad.shape === "oval" || pad.shape === "pill") { - return Math.max(pad.outer_width, pad.outer_height) / 2 - } - if (pad.shape === "pill_hole_with_rect_pad") { - return Math.max(pad.rect_pad_width, pad.rect_pad_height) / 2 - } - return 0 - } - return 0 -} - function getPadId(pad: Pad): string { if (pad.type === "pcb_smtpad") return pad.pcb_smtpad_id return pad.pcb_plated_hole_id } /** - * Compute the minimum distance between a via (circle) and a rectangular pad. - * Returns the edge-to-edge gap (negative if overlapping). + * Compute the edge-to-edge gap between a via and a pad using bounding boxes. + * Uses getBoundsOfPcbElements to handle all pad shapes generically. */ -function distanceViaToRectPad( - via: PcbVia, - pad: { x: number; y: number; width: number; height: number }, -): number { - const halfW = pad.width / 2 - const halfH = pad.height / 2 - // Nearest point on rectangle to via center - const nearestX = Math.max(pad.x - halfW, Math.min(via.x, pad.x + halfW)) - const nearestY = Math.max(pad.y - halfH, Math.min(via.y, pad.y + halfH)) - const dist = Math.hypot(via.x - nearestX, via.y - nearestY) - return dist - via.outer_diameter / 2 -} +function computeGap(via: PcbVia, pad: Pad): number { + const padBounds = getBoundsOfPcbElements([pad as AnyCircuitElement]) + const viaRadius = via.outer_diameter / 2 -/** - * Compute the edge-to-edge gap between a via and a circular pad. - */ -function distanceViaToCirclePad( - via: PcbVia, - padX: number, - padY: number, - padRadius: number, -): number { - const dist = Math.hypot(via.x - padX, via.y - padY) - return dist - via.outer_diameter / 2 - padRadius -} + // Distance from via center to nearest point on pad bounding box + const nearestX = Math.max(padBounds.minX, Math.min(via.x, padBounds.maxX)) + const nearestY = Math.max(padBounds.minY, Math.min(via.y, padBounds.maxY)) + const dist = Math.hypot(via.x - nearestX, via.y - nearestY) -/** - * Compute the edge-to-edge gap between a via and any pad type. - */ -function computeGap(via: PcbVia, pad: Pad): number { - if (pad.type === "pcb_smtpad") { - if (pad.shape === "circle") { - return distanceViaToCirclePad(via, pad.x, pad.y, pad.radius) - } - if ( - pad.shape === "rect" || - pad.shape === "rotated_rect" || - pad.shape === "pill" || - pad.shape === "rotated_pill" - ) { - return distanceViaToRectPad(via, { - x: pad.x, - y: pad.y, - width: pad.width, - height: pad.height, - }) - } - // Fallback: use bounding circle - return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad)) - } - if (pad.type === "pcb_plated_hole") { - if (pad.shape === "circle") { - return distanceViaToCirclePad(via, pad.x, pad.y, pad.outer_diameter / 2) - } - if (pad.shape === "oval" || pad.shape === "pill") { - return distanceViaToRectPad(via, { - x: pad.x, - y: pad.y, - width: pad.outer_width, - height: pad.outer_height, - }) - } - if (pad.shape === "pill_hole_with_rect_pad") { - return distanceViaToRectPad(via, { - x: pad.x, - y: pad.y, - width: pad.rect_pad_width, - height: pad.rect_pad_height, - }) - } - return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad)) - } - return Number.POSITIVE_INFINITY + return dist - viaRadius } export function checkViaToPadSpacing( circuitJson: AnyCircuitElement[], - { - minSpacing = DEFAULT_VIA_TO_PAD_MARGIN, - }: { minSpacing?: number } = {}, + { minSpacing = DEFAULT_VIA_TO_PAD_MARGIN }: { minSpacing?: number } = {}, ): PcbViaClearanceError[] { const vias = circuitJson.filter((el) => el.type === "pcb_via") as PcbVia[] const pads: Pad[] = [ diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg deleted file mode 100644 index 20cfe31..0000000 --- a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-close-to-plated-hole.snap.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg deleted file mode 100644 index 49031a0..0000000 --- a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-pad-snapshot.snap.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg b/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg deleted file mode 100644 index 24e0a0a..0000000 --- a/tests/lib/check-via-to-pad-spacing/__snapshots__/via-too-close-to-rect-pad.snap.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts b/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts deleted file mode 100644 index e879562..0000000 --- a/tests/lib/check-via-to-pad-spacing/custom-min-spacing.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect, test } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("respects custom minSpacing parameter", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 1.0, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - // Via edge at 0.3, pad left edge at 0.8 => gap = 0.5mm - // With default 0.2mm: no error. With 0.6mm: error - expect(checkViaToPadSpacing(soup)).toHaveLength(0) - expect(checkViaToPadSpacing(soup, { minSpacing: 0.6 })).toHaveLength(1) -}) diff --git a/tests/lib/check-via-to-pad-spacing/no-pads.test.ts b/tests/lib/check-via-to-pad-spacing/no-pads.test.ts deleted file mode 100644 index 8525280..0000000 --- a/tests/lib/check-via-to-pad-spacing/no-pads.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect, test } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("returns empty array when no pads", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - ] - expect(checkViaToPadSpacing(soup)).toHaveLength(0) -}) diff --git a/tests/lib/check-via-to-pad-spacing/no-vias.test.ts b/tests/lib/check-via-to-pad-spacing/no-vias.test.ts deleted file mode 100644 index 5d31b31..0000000 --- a/tests/lib/check-via-to-pad-spacing/no-vias.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("returns empty array when no vias", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 0, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - expect(checkViaToPadSpacing(soup)).toHaveLength(0) -}) diff --git a/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts deleted file mode 100644 index 68a0d19..0000000 --- a/tests/lib/check-via-to-pad-spacing/via-close-to-circle-pad.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { expect, test } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("no error when via gap to circular SMT pad equals minSpacing", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "circle", - x: 0.7, - y: 0, - radius: 0.2, - layer: "top", - }, - ] - // center-to-center = 0.7, via radius = 0.3, pad radius = 0.2 => gap = 0.2mm - // gap + EPSILON (0.005) >= 0.2mm minSpacing => no error - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(0) -}) diff --git a/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts b/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts deleted file mode 100644 index 496fd64..0000000 --- a/tests/lib/check-via-to-pad-spacing/via-close-to-plated-hole.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect, test } from "bun:test" -import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("returns error when via is too close to a plated hole", async () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_plated_hole", - pcb_plated_hole_id: "hole1", - shape: "circle", - x: 0.6, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.5, - layers: ["top", "bottom"], - pcb_component_id: "comp1", - pcb_port_id: "port1", - }, - ] - // center-to-center = 0.6, via radius = 0.3, hole radius = 0.25 => gap = 0.05mm < 0.2mm - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(1) - expect(errors[0].message).toContain("too close to pad") - - const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { - shouldDrawErrors: true, - }) - await expect(svg).toMatchSvgSnapshot(import.meta.path) -}) diff --git a/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts deleted file mode 100644 index d19dc73..0000000 --- a/tests/lib/check-via-to-pad-spacing/via-far-from-pad.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from "bun:test" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("no error when via is far from pad", () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 2, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(0) -}) diff --git a/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts b/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts deleted file mode 100644 index 04eb416..0000000 --- a/tests/lib/check-via-to-pad-spacing/via-too-close-to-pad-snapshot.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test } from "bun:test" -import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" -import type { AnyCircuitElement } from "circuit-json" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import circuitJson from "tests/assets/via-too-close-to-pad.json" - -test("renders PCB snapshot with via-to-pad spacing violations", async () => { - const soup = circuitJson as AnyCircuitElement[] - const errors = checkViaToPadSpacing(soup) - - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].message).toContain("too close to pad") - - const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { - shouldDrawErrors: true, - }) - - await expect(svg).toMatchSvgSnapshot(import.meta.path) -}) diff --git a/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts b/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts deleted file mode 100644 index 1ef318f..0000000 --- a/tests/lib/check-via-to-pad-spacing/via-too-close-to-rect-pad.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from "bun:test" -import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" -import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" -import type { AnyCircuitElement } from "circuit-json" - -test("returns error when via is too close to a rectangular SMT pad", async () => { - const soup: AnyCircuitElement[] = [ - { - type: "pcb_via", - pcb_via_id: "via1", - x: 0, - y: 0, - hole_diameter: 0.3, - outer_diameter: 0.6, - layers: ["top", "bottom"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad1", - shape: "rect", - x: 0.5, - y: 0, - width: 0.4, - height: 0.3, - layer: "top", - }, - ] - // Via edge at 0.3, pad left edge at 0.3 => gap = 0mm, well below 0.2mm default - const errors = checkViaToPadSpacing(soup) - expect(errors).toHaveLength(1) - expect(errors[0].message).toContain("too close to pad") - - const svg = convertCircuitJsonToPcbSvg([...soup, ...errors], { - shouldDrawErrors: true, - }) - await expect(svg).toMatchSvgSnapshot(import.meta.path) -}) From b5254422efc725f8c3244dcfdd0176fc86180596 Mon Sep 17 00:00:00 2001 From: lucaferri-dev Date: Sun, 8 Mar 2026 20:07:33 +0000 Subject: [PATCH 8/8] test: add single via-to-pad spacing test with realistic circuit Co-Authored-By: Claude Opus 4.6 --- .../check-via-to-pad-spacing.snap.svg | 1 + tests/lib/check-via-to-pad-spacing.test.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/lib/__snapshots__/check-via-to-pad-spacing.snap.svg create mode 100644 tests/lib/check-via-to-pad-spacing.test.ts diff --git a/tests/lib/__snapshots__/check-via-to-pad-spacing.snap.svg b/tests/lib/__snapshots__/check-via-to-pad-spacing.snap.svg new file mode 100644 index 0000000..49031a0 --- /dev/null +++ b/tests/lib/__snapshots__/check-via-to-pad-spacing.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/lib/check-via-to-pad-spacing.test.ts b/tests/lib/check-via-to-pad-spacing.test.ts new file mode 100644 index 0000000..a9f08c2 --- /dev/null +++ b/tests/lib/check-via-to-pad-spacing.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { checkViaToPadSpacing } from "lib/check-via-to-pad-spacing" +import type { AnyCircuitElement } from "circuit-json" +import circuitJson from "tests/assets/via-too-close-to-pad.json" + +test("check-via-to-pad-spacing detects violations in realistic circuit", async () => { + const elements = circuitJson as AnyCircuitElement[] + const errors = checkViaToPadSpacing(elements) + + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].type).toBe("pcb_via_clearance_error") + expect(errors[0].message).toContain("too close to pad") + + const svg = convertCircuitJsonToPcbSvg([...elements, ...errors], { + shouldDrawErrors: true, + }) + await expect(svg).toMatchSvgSnapshot(import.meta.path) +})