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

type Pad = PcbSmtPad | PcbPlatedHole

/**
* Get the effective radius of a pad for clearance calculations.
* For rectangular pads, returns the half-diagonal (conservative bounding circle).
* For circular pads/plated holes, returns the actual radius.
*/
function getPadRadius(pad: Pad): number {
if (pad.type === "pcb_smtpad") {
if (pad.shape === "circle") return pad.radius
if (
pad.shape === "rect" ||
pad.shape === "rotated_rect" ||
pad.shape === "pill" ||
pad.shape === "rotated_pill"
) {
return Math.sqrt(pad.width ** 2 + pad.height ** 2) / 2
}
return 0
}
if (pad.type === "pcb_plated_hole") {
if (pad.shape === "circle") return pad.outer_diameter / 2
if (pad.shape === "oval" || pad.shape === "pill") {
return Math.max(pad.outer_width, pad.outer_height) / 2
}
if (pad.shape === "pill_hole_with_rect_pad") {
return Math.max(pad.rect_pad_width, pad.rect_pad_height) / 2
}
return 0
}
return 0
}

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

/**
* Compute the minimum distance between a via (circle) and a rectangular pad.
* Returns the edge-to-edge gap (negative if overlapping).
*/
function distanceViaToRectPad(
via: PcbVia,
pad: { x: number; y: number; width: number; height: number },
): number {
const halfW = pad.width / 2
const halfH = pad.height / 2
// Nearest point on rectangle to via center
const nearestX = Math.max(pad.x - halfW, Math.min(via.x, pad.x + halfW))
const nearestY = Math.max(pad.y - halfH, Math.min(via.y, pad.y + halfH))
const dist = Math.hypot(via.x - nearestX, via.y - nearestY)
return dist - via.outer_diameter / 2
}

/**
* Compute the edge-to-edge gap between a via and a circular pad.
*/
function distanceViaToCirclePad(
via: PcbVia,
padX: number,
padY: number,
padRadius: number,
): number {
const dist = Math.hypot(via.x - padX, via.y - padY)
return dist - via.outer_diameter / 2 - padRadius
}

/**
* Compute the edge-to-edge gap between a via and any pad type.
*/
function computeGap(via: PcbVia, pad: Pad): number {
if (pad.type === "pcb_smtpad") {
if (pad.shape === "circle") {
return distanceViaToCirclePad(via, pad.x, pad.y, pad.radius)
}
if (
pad.shape === "rect" ||
pad.shape === "rotated_rect" ||
pad.shape === "pill" ||
pad.shape === "rotated_pill"
) {
return distanceViaToRectPad(via, {
x: pad.x,
y: pad.y,
width: pad.width,
height: pad.height,
})
}
// Fallback: use bounding circle
return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad))
}
if (pad.type === "pcb_plated_hole") {
if (pad.shape === "circle") {
return distanceViaToCirclePad(via, pad.x, pad.y, pad.outer_diameter / 2)
}
if (pad.shape === "oval" || pad.shape === "pill") {
return distanceViaToRectPad(via, {
x: pad.x,
y: pad.y,
width: pad.outer_width,
height: pad.outer_height,
})
}
if (pad.shape === "pill_hole_with_rect_pad") {
return distanceViaToRectPad(via, {
x: pad.x,
y: pad.y,
width: pad.rect_pad_width,
height: pad.rect_pad_height,
})
}
return distanceViaToCirclePad(via, pad.x, pad.y, getPadRadius(pad))
}
return Number.POSITIVE_INFINITY
}

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"]
}
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading