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
57 changes: 55 additions & 2 deletions src/fn/quad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ export const quad = (
const pads: AnyCircuitElement[] = []
let padOuterHalfX = 0
let padOuterHalfY = 0
/** Bounds of every pad, kept so the silkscreen can be clamped clear of them. */
const padBoxes: {
x: number
y: number
width: number
height: number
}[] = []
const pin_map = getQuadPinMap(parameters)
/** Side pin count */
const spc = parameters.num_pins / 4
Expand Down Expand Up @@ -151,6 +158,7 @@ export const quad = (
const pn = pin_map[i + 1]!
padOuterHalfX = Math.max(padOuterHalfX, Math.abs(x) + padWidth / 2)
padOuterHalfY = Math.max(padOuterHalfY, Math.abs(y) + padHeight / 2)
padBoxes.push({ x, y, width: padWidth, height: padHeight })
pads.push(
parameters.pillpads
? pillpad(pn, x, y, padWidth, padHeight)
Expand Down Expand Up @@ -182,6 +190,49 @@ export const quad = (

// Silkscreen corners
const silkscreen_corners: PcbSilkscreenPath[] = []
const SILK_STROKE_WIDTH = 0.1
/** IPC silkscreen-to-pad clearance, plus half the stroke we draw with. */
const silkPadClearance = 0.2 + SILK_STROKE_WIDTH / 2

/**
* How far a corner arm may run before it would touch a pad.
*
* The corner sits at (±w/2, ±h/2), which is the pad-row centre line, so an
* arm running inward along one axis passes straight through the band the
* outer pads occupy on the perpendicular side. Without this the mark is drawn
* over copper whenever the outermost pad reaches far enough along the arm,
* which is what happens on narrow-body variants such as `tqfp32_w7`.
*/
const maxArmLength = (
cornerX: number,
cornerY: number,
axis: "x" | "y",
sign: number,
requested: number,
) => {
let limit = requested
for (const pad of padBoxes) {
const left = pad.x - pad.width / 2 - silkPadClearance
const right = pad.x + pad.width / 2 + silkPadClearance
const bottom = pad.y - pad.height / 2 - silkPadClearance
const top = pad.y + pad.height / 2 + silkPadClearance

if (axis === "x") {
// Arm runs horizontally at y = cornerY; only pads spanning that y matter.
if (cornerY < bottom || cornerY > top) continue
const reach = sign < 0 ? cornerX - right : left - cornerX
if (reach >= 0) limit = Math.min(limit, reach)
else limit = 0
} else {
if (cornerX < left || cornerX > right) continue
const reach = sign < 0 ? cornerY - top : bottom - cornerY
if (reach >= 0) limit = Math.min(limit, reach)
else limit = 0
}
}
return Math.max(0, limit)
}

for (const [corner, dx, dy] of [
["top-left", -1, 1],
["bottom-left", -1, -1],
Expand Down Expand Up @@ -228,13 +279,15 @@ export const quad = (

// Normal Corner
if (arrow === "none" || parameters.legsoutside) {
const armX = maxArmLength(corner_x, corner_y, "x", -dx, csz)
const armY = maxArmLength(corner_x, corner_y, "y", -dy, csz)
silkscreen_corners.push({
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: `pcb_silkscreen_path_${corner}`,
route: [
{
x: corner_x - csz * dx,
x: corner_x - armX * dx,
y: corner_y,
},
{
Expand All @@ -243,7 +296,7 @@ export const quad = (
},
{
x: corner_x,
y: corner_y - csz * dy,
y: corner_y - armY * dy,
},
],
type: "pcb_silkscreen_path",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/__snapshots__/tqfp32_w7.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/__snapshots__/tqfp48_w7.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/kicad-parity/__snapshots__/tqfp32.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/kicad-parity/__snapshots__/tqfp48.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions tests/quad.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,75 @@ test("quad16_w4_l4_p0.4_pw0.25_pl0.4_thermalpad_startingpin(bottomside,leftpin)"
"quad16_w4_l4_p0.4_pw0.25_pl0.4_thermalpad_startingpin(bottomside,leftpin)",
)
})

test("quad corner silkscreen keeps clear of every pad", () => {
// The corner marks sit at (±w/2, ±h/2) — the pad-row centre line — so an arm
// running inward passes through the band the outer pads occupy. Narrow-body
// variants like tqfp32_w7 used to draw the mark straight over copper.
const footprints = [
"tqfp32_w7",
"tqfp48_w7",
"tqfp32",
"tqfp44",
"lqfp32",
"lqfp48",
"qfn16",
"qfn32",
"qfp80_w14_h14_p0.65mm",
]

const distanceToPad = (point: { x: number; y: number }, pad: any) => {
const dx = Math.max(
pad.x - pad.width / 2 - point.x,
0,
point.x - (pad.x + pad.width / 2),
)
const dy = Math.max(
pad.y - pad.height / 2 - point.y,
0,
point.y - (pad.y + pad.height / 2),
)
return Math.sqrt(dx * dx + dy * dy)
}

for (const name of footprints) {
const circuitJson = fp.string(name).circuitJson()
const pads = circuitJson.filter((e) => e.type === "pcb_smtpad") as any[]
// Only the corner marks; the pin 1 arrow is deliberately placed against the
// pad it identifies.
const cornerPaths = circuitJson.filter(
(e) =>
e.type === "pcb_silkscreen_path" &&
/pcb_silkscreen_path_(top|bottom)-(left|right)$/.test(
(e as any).pcb_silkscreen_path_id ?? "",
),
) as any[]

expect(cornerPaths.length).toBeGreaterThan(0)

let minClearance = Number.POSITIVE_INFINITY
for (const path of cornerPaths) {
const strokeHalfWidth = (path.stroke_width ?? 0) / 2
for (let i = 0; i < path.route.length - 1; i++) {
const a = path.route[i]
const b = path.route[i + 1]
for (let t = 0; t <= 1; t += 0.01) {
const point = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }
for (const pad of pads) {
minClearance = Math.min(
minClearance,
distanceToPad(point, pad) - strokeHalfWidth,
)
}
}
}
}

expect(Number.isFinite(minClearance)).toBe(true)
// IPC silkscreen-to-pad clearance, the same 0.2mm KiCad leaves.
expect({ name, minClearance: minClearance >= 0.2 - 1e-6 }).toEqual({
name,
minClearance: true,
})
}
})
Loading