From 3cd56601e5a7bdd4ac4dd181216f36d7b8e4d042 Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 17:54:18 +0900 Subject: [PATCH 1/8] add utdfn footprint support (utdfn.ts) --- src/fn/utdfn.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/fn/utdfn.ts diff --git a/src/fn/utdfn.ts b/src/fn/utdfn.ts new file mode 100644 index 00000000..2ca79d75 --- /dev/null +++ b/src/fn/utdfn.ts @@ -0,0 +1,33 @@ +import type { AnyCircuitElement } from "circuit-json" +import { dfn } from "./dfn" +import type { DfnInput } from "./dfn" + +const defined = (v: Record) => + Object.fromEntries( + Object.entries(v).filter(([, value]) => value !== undefined), + ) + +/** + * UDFN / UTDFN with an exposed thermal pad. + * + * Defaults follow KiCad UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm: + * four 0.22mm x 0.25mm pads at +-0.54mm on 0.65mm pitch and a + * 0.48mm x 0.48mm center exposed pad. `utdfn4ep`, `utdfn-4-ep`, and + * `UTDFN-4-EP(1x1)` all resolve to this generator with four pins. + */ +export const utdfn = ( + raw_params: DfnInput, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + return dfn({ + ...defined({ + num_pins: 4, + w: "1.3mm", + p: "0.65mm", + pl: "0.22mm", + pw: "0.25mm", + thermalpad: "0.48mmx0.48mm", + }), + ...defined(raw_params as Record), + fn: "utdfn", + } as DfnInput) +} From a9914d126b6ad83c1a61a703c3585f0290e0be11 Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 17:54:19 +0900 Subject: [PATCH 2/8] update src/fn/index.ts for utdfn --- src/fn/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fn/index.ts b/src/fn/index.ts index 37566bbd..a961b7d6 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -22,6 +22,7 @@ export { sot886 } from "./sot886" export { sot23 } from "./sot23" export { sot25 } from "./sot25" export { dfn } from "./dfn" +export { utdfn } from "./utdfn" export { do219ad } from "./do219ad" export { pinrow } from "./pinrow" export { headermodule } from "./headermodule" From 24d169fa08aae7fba7d923bf1da12620e2a00a27 Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 17:54:20 +0900 Subject: [PATCH 3/8] update src/footprinter.ts for utdfn --- src/footprinter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/footprinter.ts b/src/footprinter.ts index 10c78e9d..3337ecd1 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -650,6 +650,7 @@ const normalizeDefinition = (def: string): string => { .replace(/^sot-223-(\d+)(?=_|$)/i, "sot223_$1") .replace(/^to-220f-(\d+)(?=_|$)/i, "to220f_$1") .replace(/^jst_(ph|sh|zh|xh)_(\d+)(?=_|$)/i, "jst$2_$1") + .replace(/^u?t?dfn-(\d+)-ep(?:\([^)]*\))?(?=_|$)/i, "utdfn$1ep") } const normalizeMicrometerLengths = (value: string): string => From c6b09f8122475cb60882bba76588282848bf734f Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 17:54:21 +0900 Subject: [PATCH 4/8] add utdfn footprint support (utdfn.test.ts) --- tests/utdfn.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/utdfn.test.ts diff --git a/tests/utdfn.test.ts b/tests/utdfn.test.ts new file mode 100644 index 00000000..70e22366 --- /dev/null +++ b/tests/utdfn.test.ts @@ -0,0 +1,55 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import type { PcbSmtPad } from "circuit-json" +import { fp } from "../src/footprinter" + +const isSignalPad = (p: PcbSmtPad) => + p.port_hints?.some((h) => /^\d+$/.test(h)) ?? false + +test("utdfn4ep matches the KiCad UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm pad geometry", () => { + const soup = fp.string("utdfn4ep").circuitJson() + const pads = soup.filter( + (e): e is PcbSmtPad => e.type === "pcb_smtpad" && e.shape === "rect", + ) + + const signalPads = pads + .filter(isSignalPad) + .sort((a, b) => Number(a.port_hints?.[0]) - Number(b.port_hints?.[0])) + + expect(signalPads).toHaveLength(4) + for (const pad of signalPads) { + if (pad.shape !== "rect") throw new Error("expected rect pad") + expect(pad.width).toBeCloseTo(0.22) + expect(pad.height).toBeCloseTo(0.25) + } + const padXY = (p: PcbSmtPad) => { + if (!("x" in p) || !("y" in p)) throw new Error("pad has no x/y") + return [p.x, p.y] + } + expect(signalPads.map(padXY)).toEqual([ + [-0.54, 0.325], + [-0.54, -0.325], + [0.54, -0.325], + [0.54, 0.325], + ]) + + const thermalPad = pads.find((p) => p.port_hints?.includes("thermalpad")) + if (!thermalPad || thermalPad.shape !== "rect") { + throw new Error("expected a rect thermal pad") + } + expect(thermalPad.x).toBeCloseTo(0) + expect(thermalPad.y).toBeCloseTo(0) + expect(thermalPad.width).toBeCloseTo(0.48) + expect(thermalPad.height).toBeCloseTo(0.48) + + const svgContent = convertCircuitJsonToPcbSvg(soup) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn4ep") +}) + +test("utdfn accepts an explicit pin count", () => { + const soup = fp.string("utdfn6").circuitJson() + const signalPads = soup.filter( + (e): e is PcbSmtPad => e.type === "pcb_smtpad" && isSignalPad(e), + ) + expect(signalPads).toHaveLength(6) +}) From 078b8281685e66e3019793d45e4d87c7566f31d1 Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 17:54:22 +0900 Subject: [PATCH 5/8] add utdfn footprint support (utdfn4ep.snap.svg) --- tests/__snapshots__/utdfn4ep.snap.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/__snapshots__/utdfn4ep.snap.svg diff --git a/tests/__snapshots__/utdfn4ep.snap.svg b/tests/__snapshots__/utdfn4ep.snap.svg new file mode 100644 index 00000000..12f26e6f --- /dev/null +++ b/tests/__snapshots__/utdfn4ep.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file From 022dcc2013c4a5b18a0d4da3567cab7eb65ba351 Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 18:15:37 +0900 Subject: [PATCH 6/8] address copilot review: typed utdfn builder + utdfn6 svg snapshot --- src/footprinter.ts | 60 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/footprinter.ts b/src/footprinter.ts index 3337ecd1..4ef6135e 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -50,6 +50,34 @@ type CommonPassiveOptionKey = | "nonpolarized" | "textbottom" +type DfnOptionKey = + | "w" + | "p" + | "pw" + | "pl" + | "missing" + | "pillpads" + | "thermalpad" + | "thermalpadcenteroffsetx" + | "thermalpadcenteroffsety" + | "thermalvias" + | "thermalviapitch" + | "thermalviaid" + | "thermalviaod" + | "cornerpads" + | "cornerpadcutlength" + | "bodywidth" + | "bodylength" + | "bodythickness" + | "standoff" + | "terminalinset" + | "terminallength" + | "terminalwidth" + | "terminalpitch" + | "terminalthickness" + | "pin1terminalchamfer" + | "pin1markwidth" + export type Footprinter = { dip: ( num_pins?: number, @@ -196,36 +224,8 @@ export type Footprinter = { | "thermalpadcenteroffsetx" | "thermalpadcenteroffsety" > - dfn: ( - num_pins?: number, - ) => FootprinterParamsBuilder< - | "w" - | "p" - | "pw" - | "pl" - | "missing" - | "pillpads" - | "thermalpad" - | "thermalpadcenteroffsetx" - | "thermalpadcenteroffsety" - | "thermalvias" - | "thermalviapitch" - | "thermalviaid" - | "thermalviaod" - | "cornerpads" - | "cornerpadcutlength" - | "bodywidth" - | "bodylength" - | "bodythickness" - | "standoff" - | "terminalinset" - | "terminallength" - | "terminalwidth" - | "terminalpitch" - | "terminalthickness" - | "pin1terminalchamfer" - | "pin1markwidth" - > + dfn: (num_pins?: number) => FootprinterParamsBuilder + utdfn: (num_pins?: number) => FootprinterParamsBuilder do219ad: () => FootprinterParamsBuilder< | "p" | "pw" From a20944e1eb6a1302fc5e00fa621154c244033b9f Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 18:15:38 +0900 Subject: [PATCH 7/8] address copilot review: typed utdfn builder + utdfn6 svg snapshot --- tests/utdfn.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/utdfn.test.ts b/tests/utdfn.test.ts index 70e22366..3c225b31 100644 --- a/tests/utdfn.test.ts +++ b/tests/utdfn.test.ts @@ -52,4 +52,7 @@ test("utdfn accepts an explicit pin count", () => { (e): e is PcbSmtPad => e.type === "pcb_smtpad" && isSignalPad(e), ) expect(signalPads).toHaveLength(6) + + const svgContent = convertCircuitJsonToPcbSvg(soup) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn6") }) From b8ff6246a1bb948e64adcba82a8c21232e6e45ac Mon Sep 17 00:00:00 2001 From: halc8312 Date: Sat, 19 Sep 2026 18:15:39 +0900 Subject: [PATCH 8/8] address copilot review: typed utdfn builder + utdfn6 svg snapshot --- tests/__snapshots__/utdfn6.snap.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/__snapshots__/utdfn6.snap.svg diff --git a/tests/__snapshots__/utdfn6.snap.svg b/tests/__snapshots__/utdfn6.snap.svg new file mode 100644 index 00000000..cbb12020 --- /dev/null +++ b/tests/__snapshots__/utdfn6.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file