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 @@ -12,6 +12,7 @@ export { checkPcbComponentOverlap } from "./lib/check-pcb-components-overlap/che
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 { checkViaPadClearance } from "./lib/check-via-pad-clearance"
export { checkPinMustBeConnected } from "./lib/check-pin-must-be-connected"
export { checkAllPinsInComponentAreUnderspecified } from "./lib/check-all-pins-in-component-are-underspecified"
export { checkNoPowerPinDefined } from "./lib/check-no-power-pin-defined"
Expand Down
147 changes: 147 additions & 0 deletions lib/check-via-pad-clearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { getReadableNameForElement } from "@tscircuit/circuit-json-util"
import {
distanceBetweenCircleAndCircle,
distanceBetweenCircleAndPolygon,
} from "@tscircuit/circuit-json-util"
import type {
AnyCircuitElement,
PcbPadPadClearanceError,
PcbVia,
} from "circuit-json"
import {
type ConnectivityMap,
getFullConnectivityMapFromCircuitJson,
} from "circuit-json-to-connectivity-map"
import { EPSILON, getBoardDrcValue, getPcbBoard } from "lib/drc-defaults"
import {
formatMm,
getPadCenter,
getPadRadius,
getPads,
isCircularPad,
type PadElement,
} from "./check-pad-clearance/common"
import { jlcMinTolerances } from "@tscircuit/jlcpcb-manufacturing-specs"

const getPadId = (pad: PadElement): string => {
if ("pcb_smtpad_id" in pad) return pad.pcb_smtpad_id
if ("pcb_plated_hole_id" in pad) return pad.pcb_plated_hole_id
return ""
}

const getViaToPadGap = (via: PcbVia, pad: PadElement): number => {
const viaRadius = via.outer_diameter / 2
const viaCircle = {
kind: "circle" as const,
x: via.x,
y: via.y,
radius: viaRadius,
}

const padCenter = getPadCenter(pad)
const padRadius = getPadRadius(pad)

if (isCircularPad(pad)) {
return distanceBetweenCircleAndCircle(viaCircle, {
kind: "circle" as const,
x: padCenter.x,
y: padCenter.y,
radius: padRadius,
})
}

// For rectangular / polygon pads, use circle-to-polygon distance.
// We approximate using the pad bounding box, which is consistent with
// how check-pad-clearance handles non-circular pads.
return distanceBetweenCircleAndPolygon(viaCircle, {
kind: "polygon" as const,
points: (() => {
// Build a bounding-box polygon from the pad center + radius.
// getPadRadius returns half of the smaller bounding dimension, so this
// is conservative (tighter than the actual shape).
const cx = padCenter.x
const cy = padCenter.y
const r = padRadius
return [
{ x: cx - r, y: cy - r },
{ x: cx + r, y: cy - r },
{ x: cx + r, y: cy + r },
{ x: cx - r, y: cy + r },
]
})(),
})
}

/**
* Check that vias are not too close to pads on different nets.
*
* A via's copper annular ring must maintain minimum clearance from nearby SMT
* pads and plated holes that belong to a different electrical net. Violating
* this rule risks short-circuits during soldering or manufacturing.
*
* Related issue: https://github.com/tscircuit/checks/issues/44
*/
export function checkViaPadClearance(
circuitJson: AnyCircuitElement[],
{
connMap,
minClearance,
}: { connMap?: ConnectivityMap; minClearance?: number } = {},
): PcbPadPadClearanceError[] {
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_trace_to_pad_edge_clearance") ??
jlcMinTolerances.min_trace_to_pad_edge_clearance

connMap ??= getFullConnectivityMapFromCircuitJson(circuitJson)

const errors: PcbPadPadClearanceError[] = []
const reported = new Set<string>()

for (const via of vias) {
for (const pad of pads) {
const padId = getPadId(pad)
if (!padId) continue

// Skip pads electrically connected to this via
if (connMap.areIdsConnected(via.pcb_via_id, padId)) continue

// TODO: use flatbush for spatial indexing to avoid O(n*m) loop
const gap = getViaToPadGap(via, pad)
if (gap + EPSILON >= minClearance!) continue

const pairId = [via.pcb_via_id, padId].sort().join("_")
if (reported.has(pairId)) continue
reported.add(pairId)

const padCenter = getPadCenter(pad)

errors.push({
type: "pcb_pad_pad_clearance_error",
pcb_pad_pad_clearance_error_id: `via_pad_clearance_${pairId}`,
error_type: "pcb_pad_pad_clearance_error",
message: `Via ${getReadableNameForElement(
circuitJson,
via.pcb_via_id,
)} is too close to pad ${getReadableNameForElement(
circuitJson,
padId,
)} (gap: ${formatMm(gap)}, minimum: ${formatMm(minClearance!)})`,
pcb_pad_ids: [via.pcb_via_id, padId],
minimum_clearance: minClearance,
actual_clearance: gap,
center: {
x: (via.x + padCenter.x) / 2,
y: (via.y + padCenter.y) / 2,
},
})
}
}

return errors
}
2 changes: 2 additions & 0 deletions lib/run-all-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-t
import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceOutOfBoard"
import { checkTracesAreContiguous } from "./check-traces-are-contiguous/check-traces-are-contiguous"
import { checkViaTraceClearance } from "./check-via-trace-clearance"
import { checkViaPadClearance } from "./check-via-pad-clearance"

export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) {
return [
Expand All @@ -26,6 +27,7 @@ export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) {
...checkPcbComponentOverlap(circuitJson),
...checkPadPadClearance(circuitJson),
...checkPadTraceClearance(circuitJson),
...checkViaPadClearance(circuitJson),
...checkCourtyardOverlap(circuitJson),
...checkConnectorAccessibleOrientation(circuitJson),
]
Expand Down
138 changes: 138 additions & 0 deletions tests/lib/check-via-pad-clearance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, test } from "bun:test"
import type { AnyCircuitElement } from "circuit-json"
import { checkViaPadClearance } from "lib/check-via-pad-clearance"

// Via outer_diameter = 0.6mm → radius = 0.3mm
// SMT pad radius ≈ 0.1mm (0.2×0.2 circle)
// Min clearance default ≈ 0.1mm (JLCPCB min_trace_to_pad_edge_clearance)

describe("checkViaPadClearance", () => {
test("returns error when via is too close to pad on a different net", () => {
const soup: AnyCircuitElement[] = [
// Two independent source traces so the via and pad are on different nets
{ type: "source_trace", source_trace_id: "st1", connected_source_port_ids: [], connected_source_net_ids: [] },
{ type: "source_trace", source_trace_id: "st2", connected_source_port_ids: [], connected_source_net_ids: [] },
{
type: "pcb_via",
pcb_via_id: "via1",
pcb_trace_id: "trace1",
x: 0,
y: 0,
hole_diameter: 0.3,
outer_diameter: 0.6,
layers: ["top", "bottom"],
},
// Place pad 0.35mm away — via radius (0.3) + pad radius (~0.1) = 0.4mm edge-to-edge clearance would be needed;
// gap = 0.35 - 0.3 - 0.1 = -0.05mm → violation
{
type: "pcb_smtpad",
pcb_smtpad_id: "pad1",
pcb_component_id: "comp1",
pcb_port_id: "port1",
x: 0.35,
y: 0,
width: 0.2,
height: 0.2,
shape: "circle" as const,
layer: "top",
port_hints: [],
},
]

const errors = checkViaPadClearance(soup, { minClearance: 0.1 })
expect(errors.length).toBeGreaterThan(0)
expect(errors[0].message).toContain("too close")
expect(errors[0].actual_clearance).toBeLessThan(0.1)
})

test("no error when via is sufficiently far from pad on a different net", () => {
const soup: AnyCircuitElement[] = [
{
type: "pcb_via",
pcb_via_id: "via1",
pcb_trace_id: "trace1",
x: 0,
y: 0,
hole_diameter: 0.3,
outer_diameter: 0.6,
layers: ["top", "bottom"],
},
{
type: "pcb_smtpad",
pcb_smtpad_id: "pad1",
pcb_component_id: "comp1",
pcb_port_id: "port1",
x: 2,
y: 0,
width: 0.2,
height: 0.2,
shape: "circle" as const,
layer: "top",
port_hints: [],
},
]

const errors = checkViaPadClearance(soup, { minClearance: 0.1 })
expect(errors).toHaveLength(0)
})

test("no error when via and pad are on the same net (via connMap)", () => {
const soup: AnyCircuitElement[] = [
{
type: "pcb_via",
pcb_via_id: "via1",
pcb_trace_id: "trace1",
x: 0,
y: 0,
hole_diameter: 0.3,
outer_diameter: 0.6,
layers: ["top", "bottom"],
},
{
type: "pcb_smtpad",
pcb_smtpad_id: "pad1",
pcb_component_id: "comp1",
pcb_port_id: "port1",
x: 0.35,
y: 0,
width: 0.2,
height: 0.2,
shape: "circle" as const,
layer: "top",
port_hints: [],
},
]

// Inject a connMap that marks via1 and pad1 as connected (same net)
const connMap = {
areIdsConnected: (a: string, b: string) =>
(a === "via1" && b === "pad1") || (a === "pad1" && b === "via1"),
getIdsConnectedToId: () => [],
getNetIdForId: () => undefined,
} as any

const errors = checkViaPadClearance(soup, { minClearance: 0.1, connMap })
expect(errors).toHaveLength(0)
})

test("returns no errors when there are no vias", () => {
const soup: AnyCircuitElement[] = [
{
type: "pcb_smtpad",
pcb_smtpad_id: "pad1",
pcb_component_id: "comp1",
pcb_port_id: "port1",
x: 0,
y: 0,
width: 0.5,
height: 0.5,
shape: "rect" as const,
layer: "top",
port_hints: [],
},
]

const errors = checkViaPadClearance(soup, { minClearance: 0.1 })
expect(errors).toHaveLength(0)
})
})
Loading