From 716dfeb8902c58222fb452cf6af4f746debee79e Mon Sep 17 00:00:00 2001 From: Raghav Arora Date: Mon, 10 Nov 2025 23:03:34 +0530 Subject: [PATCH] fix: QFN footprint pad sizing to match KiCad IPC standards - Update QFN pad dimensions to use 0.8mm length (not 2x pitch) - Set pad width to 0.5x pitch as per IPC standards - Add padoffset parameter to quad function for fine-tuning pad position - Position QFN pads to extend 0.35mm beyond package edge for proper soldering - Add QFN32 KiCad parity test (now passes with <5% difference) - Update QFN16 snapshot with corrected pad dimensions Fixes #413 --- src/fn/qfn.ts | 27 ++++++++++++++++--- src/fn/quad.ts | 7 +++-- .../qfn16_w4_h4_p0.65mm.snap.svg | 2 +- .../__snapshots__/qfn32_kicad_parity.snap.svg | 1 + .../qfn32_kicad_parity_boolean_diff.snap.svg | 1 + tests/kicad-parity/qfn32_kicad_parity.test.ts | 20 ++++++++++++++ 6 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 tests/kicad-parity/__snapshots__/qfn32_kicad_parity.snap.svg create mode 100644 tests/kicad-parity/__snapshots__/qfn32_kicad_parity_boolean_diff.snap.svg create mode 100644 tests/kicad-parity/qfn32_kicad_parity.test.ts diff --git a/src/fn/qfn.ts b/src/fn/qfn.ts index 0810384a..35f905df 100644 --- a/src/fn/qfn.ts +++ b/src/fn/qfn.ts @@ -1,12 +1,31 @@ import type { AnySoupElement } from "circuit-json" -import { base_quad_def, quad, quad_def, quadTransform } from "./quad" +import { base_quad_def, quad, quadTransform } from "./quad" import type { z } from "zod" export const qfn_def = base_quad_def.extend({}).transform(quadTransform) export const qfn = ( - parameters: z.input, + raw_params: z.input, ): { circuitJson: AnySoupElement[]; parameters: any } => { - parameters.legsoutside = false - return quad(parameters) + raw_params.legsoutside = false + + // Apply QFN-specific pad sizing if not explicitly provided + // Based on KiCad IPC standards for QFN packages + if (!raw_params.pw && !raw_params.pl) { + const pitchValue = + typeof raw_params.p === "string" ? parseFloat(raw_params.p) : raw_params.p + + if (pitchValue) { + // KiCad IPC standard for QFN: + // - Pad width = 0.5 × pitch + // - Pad length = 0.8mm + // - Pads extend ~0.35mm beyond package edge for proper soldering + raw_params.pw = pitchValue * 0.5 + raw_params.pl = 0.8 + // Offset pads 0.35mm further from center so they extend beyond package edge + raw_params.padoffset = -0.35 + } + } + + return quad(raw_params) } diff --git a/src/fn/quad.ts b/src/fn/quad.ts index 161892d1..51933391 100644 --- a/src/fn/quad.ts +++ b/src/fn/quad.ts @@ -26,6 +26,7 @@ export const base_quad_def = z.object({ pl: length.optional(), thermalpad: z.union([z.literal(true), dim2d]).optional(), legsoutside: z.boolean().default(false), + padoffset: length.optional(), // Additional offset for pad position (positive = further from center) }) export const quadTransform = >( @@ -80,8 +81,9 @@ export const getQuadCoords = (params: { p: number // pitch between pins pl: number // length of the pin legsoutside?: boolean + padoffset?: number // Additional offset for pad position }) => { - const { pin_count, pn, w, h, p, pl, legsoutside } = params + const { pin_count, pn, w, h, p, pl, legsoutside, padoffset = 0 } = params const sidePinCount = pin_count / 4 const side = SIDES_CCW[Math.floor((pn - 1) / sidePinCount)] const pos = (pn - 1) % sidePinCount @@ -92,7 +94,7 @@ export const getQuadCoords = (params: { const ibh = p * (sidePinCount - 1) /** pad center distance from edge (negative is inside, positive is outside) */ - const pcdfe = legsoutside ? pl / 2 : -pl / 2 + const pcdfe = (legsoutside ? pl / 2 : -pl / 2) - padoffset switch (side) { case "left": @@ -129,6 +131,7 @@ export const quad = ( p: parameters.p ?? 0.5, pl: parameters.pl, legsoutside: parameters.legsoutside, + padoffset: parameters.padoffset, }) let pw = parameters.pw diff --git a/tests/__snapshots__/qfn16_w4_h4_p0.65mm.snap.svg b/tests/__snapshots__/qfn16_w4_h4_p0.65mm.snap.svg index 2f61da8e..e5df35ef 100644 --- a/tests/__snapshots__/qfn16_w4_h4_p0.65mm.snap.svg +++ b/tests/__snapshots__/qfn16_w4_h4_p0.65mm.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__/qfn32_kicad_parity.snap.svg b/tests/kicad-parity/__snapshots__/qfn32_kicad_parity.snap.svg new file mode 100644 index 00000000..b0145679 --- /dev/null +++ b/tests/kicad-parity/__snapshots__/qfn32_kicad_parity.snap.svg @@ -0,0 +1 @@ +{REF}Diff: 4.95% \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/qfn32_kicad_parity_boolean_diff.snap.svg b/tests/kicad-parity/__snapshots__/qfn32_kicad_parity_boolean_diff.snap.svg new file mode 100644 index 00000000..c2e26639 --- /dev/null +++ b/tests/kicad-parity/__snapshots__/qfn32_kicad_parity_boolean_diff.snap.svg @@ -0,0 +1 @@ +QFN-32-1EP_4x4mm_P0.4mm_EP2.65x2.65mm - Alignment Analysis (Footprinter vs KiCad)qfn32_w4_h4_p0.4mm_thermalpad2.65x2.65KiCad: QFN-32-1EP_4x4mm_P0.4mm_EP2.65x2.65mmPerfect alignment = complete overlap \ No newline at end of file diff --git a/tests/kicad-parity/qfn32_kicad_parity.test.ts b/tests/kicad-parity/qfn32_kicad_parity.test.ts new file mode 100644 index 00000000..65c6e2c4 --- /dev/null +++ b/tests/kicad-parity/qfn32_kicad_parity.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test" +import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" + +test("parity/qfn32", async () => { + const { avgRelDiff, combinedFootprintElements, booleanDifferenceSvg } = + await compareFootprinterVsKicad( + "qfn32_w4_h4_p0.4mm_thermalpad2.65x2.65", + "Package_DFN_QFN.pretty/QFN-32-1EP_4x4mm_P0.4mm_EP2.65x2.65mm.circuit.json", + ) + + expect(avgRelDiff).toBeLessThan(0.05) + expect( + convertCircuitJsonToPcbSvg(combinedFootprintElements), + ).toMatchSvgSnapshot(import.meta.path, "qfn32_kicad_parity") + expect(booleanDifferenceSvg).toMatchSvgSnapshot( + import.meta.path, + "qfn32_kicad_parity_boolean_diff", + ) +})