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
128 changes: 128 additions & 0 deletions lib/check-via-pad-clearance.ts
Original file line number Diff line number Diff line change
@@ -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
}
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 @@ -51,6 +52,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) {
...checkSourceTracesHavePcbTraces(circuitJson),
...checkEachPcbTraceNonOverlapping(circuitJson),
...checkViaTraceClearance(circuitJson),
...checkViaPadClearance(circuitJson),
...checkSameNetViaSpacing(circuitJson),
...checkDifferentNetViaSpacing(circuitJson),
...checkTracesAreContiguous(circuitJson),
Expand Down
92 changes: 92 additions & 0 deletions tests/lib/check-via-pad-clearance.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
Loading