Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions lib/check-pad-clearance/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const getPadRadius = (pad: PadElement) => {

export const isCircularPad = (pad: PadElement) => pad.shape === "circle"

const getCircleShape = (pad: PadElement) => {
export const getCircleShape = (pad: PadElement) => {
const center = getPadCenter(pad)
return {
kind: "circle" as const,
Expand All @@ -51,7 +51,7 @@ const getCircleShape = (pad: PadElement) => {
}
}

const getPolygonShape = (pad: PadElement) => {
export const getPolygonShape = (pad: PadElement) => {
const bounds = getPadBounds(pad)
return {
kind: "polygon" as const,
Expand Down
156 changes: 156 additions & 0 deletions lib/check-via-pad-clearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
getPrimaryId,
getReadableNameForElement,
} from "@tscircuit/circuit-json-util"
import {
distanceBetweenCircleAndCircle,
distanceBetweenCircleAndPolygon,
} from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement, PcbVia } from "circuit-json"
import {
type ConnectivityMap,
getFullConnectivityMapFromCircuitJson,
} from "circuit-json-to-connectivity-map"
import { SpatialObjectIndex } from "lib/data-structures/SpatialIndex"
import { EPSILON, getBoardDrcValue, getPcbBoard } from "lib/drc-defaults"
import { getLayersOfPcbElement } from "lib/util/getLayersOfPcbElement"
import {
type PadElement,
formatMm,
getCircleShape,
getPadBounds,
getPadCenter,
getPads,
isCircularPad,
getPolygonShape,
} from "./check-pad-clearance/common"
import { jlcMinTolerances } from "@tscircuit/jlcpcb-manufacturing-specs"

/**
* Error emitted when a via is closer to a pad than the allowed clearance.
*
* `circuit-json` does not yet define a dedicated `pcb_via_pad_clearance_error`
* type, so it is declared locally. Promoting it 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 of
* outer_diameter) and a pad. Uses `getBoundsOfPcbElements` for the pad
* so that all pad shapes (circle, rect, pill, rotated_pill, polygon, etc.)
* are handled correctly - the pad is conservatively approximated as either
* a circle or an axis-aligned bounding-box polygon.
*/
const getViaToPadGap = (
via: PcbVia,
pad: PadElement,
): number => {
const viaCircle = {
kind: "circle" as const,
x: via.x,
y: via.y,
radius: via.outer_diameter / 2,
}

if (isCircularPad(pad)) {
return distanceBetweenCircleAndCircle(viaCircle, getCircleShape(pad))
}

return distanceBetweenCircleAndPolygon(viaCircle, getPolygonShape(pad))
}

/**
* Checks that vias are not placed too close to pads (SMT pads and plated
* holes). A via connected to the same net as the pad is ignored.
*/
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 spatialIndex = new SpatialObjectIndex<PadElement>({
objects: pads,
getBounds: getPadBounds,
getId: (pad) => getPrimaryId(pad),
})

const errors = new Map<
string,
{ error: PcbViaPadClearanceError; gap: number }
>()

for (const via of vias) {
const viaLayers = getLayersOfPcbElement(via)
const viaBounds = {
minX: via.x - via.outer_diameter / 2,
minY: via.y - via.outer_diameter / 2,
maxX: via.x + via.outer_diameter / 2,
maxY: via.y + via.outer_diameter / 2,
}
const nearbyPads = spatialIndex.getObjectsInBounds(
viaBounds,
minClearance,
)

for (const pad of nearbyPads) {
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(via, pad)
if (gap + EPSILON >= minClearance!) continue

const pairId = `${via.pcb_via_id}_${padId}`
const padCenter = getPadCenter(pad)
const nextError: PcbViaPadClearanceError = {
type: "pcb_via_pad_clearance_error",
pcb_via_pad_clearance_error_id: `via_pad_clearance_${pairId}`,
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,
},
}

const current = errors.get(pairId)
if (!current || gap < current.gap) {
errors.set(pairId, { error: nextError, gap })
}
}
}

return Array.from(errors.values()).map(({ error }) => error)
}
2 changes: 2 additions & 0 deletions lib/run-all-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down Expand Up @@ -50,6 +51,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) {
...checkEachPcbPortConnectedToPcbTraces(circuitJson),
...checkSourceTracesHavePcbTraces(circuitJson),
...checkEachPcbTraceNonOverlapping(circuitJson),
...checkViaPadClearance(circuitJson),
...checkViaTraceClearance(circuitJson),
...checkSameNetViaSpacing(circuitJson),
...checkDifferentNetViaSpacing(circuitJson),
Expand Down
148 changes: 148 additions & 0 deletions tests/lib/check-via-pad-clearance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { expect, test } from "bun:test"
import type { AnyCircuitElement } from "circuit-json"
import { checkViaPadClearance } from "../../lib/check-via-pad-clearance"

const makeBoard = (): AnyCircuitElement =>
({
type: "pcb_board",
pcb_board_id: "board1",
width: 100,
height: 100,
x: 0,
y: 0,
}) as AnyCircuitElement

const makeVia = (id: string, x: number, y: number): AnyCircuitElement =>
({
type: "pcb_via",
pcb_via_id: id,
x,
y,
outer_diameter: 1.0,
hole_diameter: 0.4,
layers: ["top", "bottom"],
}) as AnyCircuitElement

const makeCirclePad = (id: string, x: number, y: number): AnyCircuitElement =>
({
type: "pcb_smtpad",
pcb_smtpad_id: id,
pcb_component_id: "comp1",
pcb_port_id: `port_${id}`,
shape: "circle",
x,
y,
radius: 0.5,
layer: "top",
}) as AnyCircuitElement

const makeRectPad = (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.0,
height: 1.0,
layer: "top",
}) as AnyCircuitElement

const makePlatedHole = (id: string, x: number, y: number): AnyCircuitElement =>
({
type: "pcb_plated_hole",
pcb_plated_hole_id: id,
pcb_component_id: "comp1",
pcb_port_id: `port_${id}`,
shape: "circle",
x,
y,
hole_diameter: 0.8,
outer_diameter: 1.2,
layers: ["top", "bottom"],
}) as AnyCircuitElement

test("via too close to circle pad should produce error", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
// via radius=0.5, pad radius=0.5, distance=1.05 => gap=0.05mm < 0.1mm default
makeCirclePad("pad1", 1.05, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBeGreaterThan(0)
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")
})

test("via far from pad should not produce error", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
makeCirclePad("pad1", 10, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBe(0)
})

test("via too close to rect pad should produce error", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
// via radius=0.5, rect half-width=0.5, center at 1.04
// rect nearest edge at x=0.54, gap≈0.04mm < 0.1mm default
makeRectPad("pad1", 1.04, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBeGreaterThan(0)
})

test("via too close to plated hole should produce error", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
// via radius=0.5, plated_hole outer radius=0.6, distance=1.14
// gap=1.14-0.5-0.6=0.04mm < 0.1mm default
makePlatedHole("ph1", 1.14, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBeGreaterThan(0)
})

test("custom minClearance is respected", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
makeCirclePad("pad1", 3, 0), // gap = 3 - 0.5 - 0.5 = 2.0mm (passes default)
]
// Default clearance (~0.1mm) should pass
const defaultErrors = checkViaPadClearance(circuitJson)
expect(defaultErrors.length).toBe(0)

// Very large minClearance should fail
const customErrors = checkViaPadClearance(circuitJson, {
minClearance: 5,
})
expect(customErrors.length).toBeGreaterThan(0)
})

test("no vias returns empty", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeCirclePad("pad1", 0, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBe(0)
})

test("no pads returns empty", () => {
const circuitJson: AnyCircuitElement[] = [
makeBoard(),
makeVia("via1", 0, 0),
]
const errors = checkViaPadClearance(circuitJson)
expect(errors.length).toBe(0)
})
Loading