diff --git a/index.ts b/index.ts index a24e833..fcf062f 100644 --- a/index.ts +++ b/index.ts @@ -11,6 +11,7 @@ export { checkPcbTracesOutOfBoard } from "./lib/check-trace-out-of-board/checkTr export { checkPcbComponentOverlap } from "./lib/check-pcb-components-overlap/checkPcbComponentOverlap" export { checkPadPadClearance } from "./lib/check-pad-pad-clearance" export { checkPadTraceClearance } from "./lib/check-pad-trace-clearance" +export { checkViaPadClearance } from "./lib/check-via-pad-clearance" export { checkViaTraceClearance } from "./lib/check-via-trace-clearance" 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-via-pad-clearance.ts b/lib/check-via-pad-clearance.ts new file mode 100644 index 0000000..ab73ef4 --- /dev/null +++ b/lib/check-via-pad-clearance.ts @@ -0,0 +1,128 @@ +import { + getPrimaryId, + getReadableNameForElement, +} from "@tscircuit/circuit-json-util" +import { jlcMinTolerances } from "@tscircuit/jlcpcb-manufacturing-specs" +import type { AnyCircuitElement, PcbVia } from "circuit-json" +import { + type ConnectivityMap, + getFullConnectivityMapFromCircuitJson, +} from "circuit-json-to-connectivity-map" +import { EPSILON, getBoardDrcValue, getPcbBoard } from "lib/drc-defaults" +import { getLayersOfPcbElement } from "lib/util/getLayersOfPcbElement" +import { + type PadElement, + formatMm, + getPadBounds, + getPadCenter, + getPadRadius, + getPads, + isCircularPad, +} from "./check-pad-clearance/common" + +/** + * Error emitted when a via's copper is closer than the allowed clearance to a + * pad (an SMT pad or a plated hole). + * + * `circuit-json` does not (yet) define a dedicated `pcb_via_pad_clearance_error` + * type, so it is declared locally here. Promoting this type to `circuit-json` + * would be a natural follow-up. + */ +export interface PcbViaPadClearanceError { + type: "pcb_via_pad_clearance_error" + pcb_via_pad_clearance_error_id: string + error_type: "pcb_via_pad_clearance_error" + message: string + pcb_via_id: string + pcb_pad_id: string + minimum_clearance: number + actual_clearance: number + center: { x: number; y: number } +} + +/** + * Minimum edge-to-edge distance between a via (treated as a circle) and a pad. + * A negative value means the two overlap. + */ +const getViaToPadGap = ( + via: { x: number; y: number; radius: number }, + pad: PadElement, +): number => { + if (isCircularPad(pad)) { + const center = getPadCenter(pad) + return ( + Math.hypot(via.x - center.x, via.y - center.y) - + via.radius - + getPadRadius(pad) + ) + } + + // Rectangular pad: distance from the via centre to the pad's axis-aligned + // bounding box, then subtract the via radius. This matches how the existing + // pad-to-pad check treats rectangular pads. + const bounds = getPadBounds(pad) + const dx = Math.max(bounds.minX - via.x, 0, via.x - bounds.maxX) + const dy = Math.max(bounds.minY - via.y, 0, via.y - bounds.maxY) + return Math.hypot(dx, dy) - via.radius +} + +/** + * Checks that vias are not placed too close to pads. A via that is connected to + * the same net as the pad is ignored, since that proximity is intentional. + */ +export function checkViaPadClearance( + circuitJson: AnyCircuitElement[], + { + connMap, + minClearance, + }: { connMap?: ConnectivityMap; minClearance?: number } = {}, +): PcbViaPadClearanceError[] { + const vias = circuitJson.filter((el) => el.type === "pcb_via") as PcbVia[] + const pads = getPads(circuitJson) + if (vias.length === 0 || pads.length === 0) return [] + + const board = getPcbBoard(circuitJson) + minClearance ??= + getBoardDrcValue(board, "min_pad_edge_to_pad_edge_clearance") ?? + jlcMinTolerances.min_pad_edge_to_pad_edge_clearance + connMap ??= getFullConnectivityMapFromCircuitJson(circuitJson) + + const errors: PcbViaPadClearanceError[] = [] + + for (const via of vias) { + const viaCircle = { x: via.x, y: via.y, radius: via.outer_diameter / 2 } + const viaLayers = getLayersOfPcbElement(via) + + for (const pad of pads) { + const padId = getPrimaryId(pad) + + // Only compare elements that share at least one copper layer. + const padLayers = getLayersOfPcbElement(pad as any) + if (!viaLayers.some((layer) => padLayers.includes(layer))) continue + + // A via intentionally connected to the pad's net is not a violation. + if (connMap.areIdsConnected(via.pcb_via_id, padId)) continue + + const gap = getViaToPadGap(viaCircle, pad) + if (gap + EPSILON >= minClearance!) continue + + const padCenter = getPadCenter(pad) + errors.push({ + type: "pcb_via_pad_clearance_error", + pcb_via_pad_clearance_error_id: `via_pad_clearance_${via.pcb_via_id}_${padId}`, + error_type: "pcb_via_pad_clearance_error", + message: `Via ${getReadableNameForElement(circuitJson, via.pcb_via_id)} and pad ${getReadableNameForElement(circuitJson, padId)} are too close (clearance: ${formatMm(gap)}, minimum: ${formatMm(minClearance!)})`, + pcb_via_id: via.pcb_via_id, + pcb_pad_id: padId, + minimum_clearance: minClearance!, + actual_clearance: gap, + center: { + x: (via.x + padCenter.x) / 2, + y: (via.y + padCenter.y) / 2, + }, + }) + } + } + + return errors +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index ec5aa16..7a730ac 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -17,6 +17,7 @@ import { checkSameNetViaSpacing } from "./check-same-net-via-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" +import { checkViaPadClearance } from "./check-via-pad-clearance" import { checkViaTraceClearance } from "./check-via-trace-clearance" export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { @@ -51,6 +52,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) { ...checkSourceTracesHavePcbTraces(circuitJson), ...checkEachPcbTraceNonOverlapping(circuitJson), ...checkViaTraceClearance(circuitJson), + ...checkViaPadClearance(circuitJson), ...checkSameNetViaSpacing(circuitJson), ...checkDifferentNetViaSpacing(circuitJson), ...checkTracesAreContiguous(circuitJson), diff --git a/tests/lib/check-via-pad-clearance.test.ts b/tests/lib/check-via-pad-clearance.test.ts new file mode 100644 index 0000000..2fd638e --- /dev/null +++ b/tests/lib/check-via-pad-clearance.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkViaPadClearance } from "../../lib/check-via-pad-clearance" + +const rectPad = (id: string, x: number, y: number): AnyCircuitElement => + ({ + type: "pcb_smtpad", + pcb_smtpad_id: id, + pcb_component_id: "comp1", + pcb_port_id: `port_${id}`, + shape: "rect", + x, + y, + width: 1, + height: 1, + layer: "top", + }) as AnyCircuitElement + +const via = (id: string, x: number, y: number): AnyCircuitElement => + ({ + type: "pcb_via", + pcb_via_id: id, + x, + y, + hole_diameter: 0.3, + outer_diameter: 0.4, + layers: ["top", "bottom"], + }) as AnyCircuitElement + +// Stub: nothing is connected unless the two ids are identical. +const unconnected = { + areIdsConnected: (a: string, b: string) => a === b, +} as any + +test("checkViaPadClearance flags a via too close to an unrelated pad", () => { + const circuitJson: AnyCircuitElement[] = [ + rectPad("pad1", 0, 0), + via("via1", 0.55, 0), + ] + + const errors = checkViaPadClearance(circuitJson, { connMap: unconnected }) + + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_via_pad_clearance_error") + expect(errors[0].pcb_via_id).toBe("via1") + expect(errors[0].pcb_pad_id).toBe("pad1") + expect(errors[0].actual_clearance).toBeLessThan(errors[0].minimum_clearance) +}) + +test("checkViaPadClearance ignores a via far away from the pad", () => { + const circuitJson: AnyCircuitElement[] = [ + rectPad("pad1", 0, 0), + via("via1", 5, 5), + ] + + expect( + checkViaPadClearance(circuitJson, { connMap: unconnected }), + ).toHaveLength(0) +}) + +test("checkViaPadClearance ignores a via connected to the pad's net", () => { + const circuitJson: AnyCircuitElement[] = [ + rectPad("pad1", 0, 0), + via("via1", 0.55, 0), + ] + const connected = { areIdsConnected: () => true } as any + + expect( + checkViaPadClearance(circuitJson, { connMap: connected }), + ).toHaveLength(0) +}) + +test("checkViaPadClearance respects a custom minClearance", () => { + const circuitJson: AnyCircuitElement[] = [ + rectPad("pad1", 0, 0), + // Edge-to-edge gap is ~0.3mm here. + via("via1", 1.0, 0), + ] + + expect( + checkViaPadClearance(circuitJson, { + connMap: unconnected, + minClearance: 0.1, + }), + ).toHaveLength(0) + expect( + checkViaPadClearance(circuitJson, { + connMap: unconnected, + minClearance: 1, + }), + ).toHaveLength(1) +})