Skip to content
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
84 changes: 84 additions & 0 deletions lib/check-via-to-pad-spacing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type {
AnyCircuitElement,
PcbVia,
PcbSmtPad,
PcbPlatedHole,
PcbViaClearanceError,
} from "circuit-json"
import {
getReadableNameForElement,
getBoundsOfPcbElements,
} from "@tscircuit/circuit-json-util"
import { DEFAULT_VIA_TO_PAD_MARGIN, EPSILON } from "lib/drc-defaults"

type Pad = PcbSmtPad | PcbPlatedHole

function getPadId(pad: Pad): string {
if (pad.type === "pcb_smtpad") return pad.pcb_smtpad_id
return pad.pcb_plated_hole_id
}

/**
* Compute the edge-to-edge gap between a via and a pad using bounding boxes.
* Uses getBoundsOfPcbElements to handle all pad shapes generically.
*/
function computeGap(via: PcbVia, pad: Pad): number {
const padBounds = getBoundsOfPcbElements([pad as AnyCircuitElement])
const viaRadius = via.outer_diameter / 2

// 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)

return dist - viaRadius
}

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
}
1 change: 1 addition & 0 deletions lib/drc-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions lib/run-all-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,6 +33,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) {
...checkEachPcbTraceNonOverlapping(circuitJson),
...checkSameNetViaSpacing(circuitJson),
...checkDifferentNetViaSpacing(circuitJson),
...checkViaToPadSpacing(circuitJson),
// ...checkTracesAreContiguous(circuitJson),
...checkPcbTracesOutOfBoard(circuitJson),
]
Expand Down
170 changes: 170 additions & 0 deletions tests/assets/via-too-close-to-pad.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
1 change: 1 addition & 0 deletions tests/lib/__snapshots__/check-via-to-pad-spacing.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions tests/lib/check-via-to-pad-spacing.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})