diff --git a/src/fn/quad.ts b/src/fn/quad.ts
index 7111d353..fc39abfa 100644
--- a/src/fn/quad.ts
+++ b/src/fn/quad.ts
@@ -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
@@ -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)
@@ -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],
@@ -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,
},
{
@@ -243,7 +296,7 @@ export const quad = (
},
{
x: corner_x,
- y: corner_y - csz * dy,
+ y: corner_y - armY * dy,
},
],
type: "pcb_silkscreen_path",
diff --git a/tests/__snapshots__/qfp80_w14_h14_p0.65mm_startingpin(topside,leftpin).snap.svg b/tests/__snapshots__/qfp80_w14_h14_p0.65mm_startingpin(topside,leftpin).snap.svg
index 1d0e7fed..043511dd 100644
--- a/tests/__snapshots__/qfp80_w14_h14_p0.65mm_startingpin(topside,leftpin).snap.svg
+++ b/tests/__snapshots__/qfp80_w14_h14_p0.65mm_startingpin(topside,leftpin).snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/tqfp32_w7.snap.svg b/tests/__snapshots__/tqfp32_w7.snap.svg
index b77408e2..b3c09f7d 100644
--- a/tests/__snapshots__/tqfp32_w7.snap.svg
+++ b/tests/__snapshots__/tqfp32_w7.snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/tqfp48_w7.snap.svg b/tests/__snapshots__/tqfp48_w7.snap.svg
index a9e4bbb8..88161342 100644
--- a/tests/__snapshots__/tqfp48_w7.snap.svg
+++ b/tests/__snapshots__/tqfp48_w7.snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/kicad-parity/__snapshots__/tqfp32.snap.svg b/tests/kicad-parity/__snapshots__/tqfp32.snap.svg
index 1dc06724..f4f4772c 100644
--- a/tests/kicad-parity/__snapshots__/tqfp32.snap.svg
+++ b/tests/kicad-parity/__snapshots__/tqfp32.snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/kicad-parity/__snapshots__/tqfp48.snap.svg b/tests/kicad-parity/__snapshots__/tqfp48.snap.svg
index 18a267d9..a35d27e7 100644
--- a/tests/kicad-parity/__snapshots__/tqfp48.snap.svg
+++ b/tests/kicad-parity/__snapshots__/tqfp48.snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/quad.test.ts b/tests/quad.test.ts
index f05ea2f5..1e5fe358 100644
--- a/tests/quad.test.ts
+++ b/tests/quad.test.ts
@@ -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,
+ })
+ }
+})