From 86830494040e83f31fc8aa36b647f468a6e1d05e Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 22:19:52 +0900 Subject: [PATCH] fix: keep quad corner silkscreen clear of the pads (#734) --- src/fn/quad.ts | 57 ++++++++++++++- ...65mm_startingpin(topside,leftpin).snap.svg | 2 +- tests/__snapshots__/tqfp32_w7.snap.svg | 2 +- tests/__snapshots__/tqfp48_w7.snap.svg | 2 +- .../__snapshots__/tqfp32.snap.svg | 2 +- .../__snapshots__/tqfp48.snap.svg | 2 +- tests/quad.test.ts | 72 +++++++++++++++++++ 7 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/fn/quad.ts b/src/fn/quad.ts index 7111d3530..fc39abfa4 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 1d0e7fedd..043511dd5 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 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/tqfp32_w7.snap.svg b/tests/__snapshots__/tqfp32_w7.snap.svg index b77408e29..b3c09f7dd 100644 --- a/tests/__snapshots__/tqfp32_w7.snap.svg +++ b/tests/__snapshots__/tqfp32_w7.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/tqfp48_w7.snap.svg b/tests/__snapshots__/tqfp48_w7.snap.svg index a9e4bbb82..88161342d 100644 --- a/tests/__snapshots__/tqfp48_w7.snap.svg +++ b/tests/__snapshots__/tqfp48_w7.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ 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 1dc06724d..f4f4772c2 100644 --- a/tests/kicad-parity/__snapshots__/tqfp32.snap.svg +++ b/tests/kicad-parity/__snapshots__/tqfp32.snap.svg @@ -1 +1 @@ -{REF}REF**Diff: 0.00% \ No newline at end of file +{REF}REF**Diff: 0.00% \ 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 18a267d90..a35d27e77 100644 --- a/tests/kicad-parity/__snapshots__/tqfp48.snap.svg +++ b/tests/kicad-parity/__snapshots__/tqfp48.snap.svg @@ -1 +1 @@ -{REF}REF**Diff: 0.00% \ No newline at end of file +{REF}REF**Diff: 0.00% \ No newline at end of file diff --git a/tests/quad.test.ts b/tests/quad.test.ts index f05ea2f57..1e5fe358c 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, + }) + } +})