From ef9a3c7c0573fe99d0ce354d7701f4618c6a3c1b Mon Sep 17 00:00:00 2001 From: gunitesh Date: Sun, 24 May 2026 11:06:58 +0530 Subject: [PATCH 1/2] feat: implement UTDFN-4-EP(1x1) footprint (fixes #183) --- src/fn/index.ts | 1 + src/fn/utdfn.ts | 107 ++++++++++++++++++++ src/footprinter.ts | 5 + tests/__snapshots__/utdfn-4-ep-1x1.snap.svg | 1 + tests/utdfn.test.ts | 46 +++++++++ 5 files changed, 160 insertions(+) create mode 100644 src/fn/utdfn.ts create mode 100644 tests/__snapshots__/utdfn-4-ep-1x1.snap.svg create mode 100644 tests/utdfn.test.ts diff --git a/src/fn/index.ts b/src/fn/index.ts index 94c0370e..75131465 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -81,3 +81,4 @@ export { sot343 } from "./sot343" export { m2host } from "./m2host" export { mountedpcbmodule } from "./mountedpcbmodule" export { to92l } from "./to92l" +export { utdfn } from "./utdfn" diff --git a/src/fn/utdfn.ts b/src/fn/utdfn.ts new file mode 100644 index 00000000..2537db31 --- /dev/null +++ b/src/fn/utdfn.ts @@ -0,0 +1,107 @@ +import type { AnyCircuitElement, PcbCourtyardRect, PcbSilkscreenPath } from "circuit-json" +import { z } from "zod" +import { rectpad } from "src/helpers/rectpad" +import { base_def } from "../helpers/zod/base_def" +import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef" + +export const utdfn_def = base_def.extend({ + fn: z.string(), + num_pins: z.number().default(4), + w: z.string().default("0.90mm"), + p: z.string().default("0.65mm"), + pl: z.string().default("0.40mm"), + pw: z.string().default("0.25mm"), + epw: z.string().default("0.48mm"), + eph: z.string().default("0.48mm"), + string: z.string().optional(), +}) + +export const utdfn = ( + raw_params: z.input, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + const parameters = utdfn_def.parse(raw_params) + const w = parseFloat(parameters.w) + const p = parseFloat(parameters.p) + const pl = parseFloat(parameters.pl) + const pw = parseFloat(parameters.pw) + const epw = parseFloat(parameters.epw) + const eph = parseFloat(parameters.eph) + + const pads: AnyCircuitElement[] = [] + + // Generate the 4 pins in CCW order starting from bottom-left (Pin 1) + pads.push(rectpad(1, -w / 2, -p / 2, pl, pw)) + pads.push(rectpad(2, -w / 2, p / 2, pl, pw)) + pads.push(rectpad(3, w / 2, p / 2, pl, pw)) + pads.push(rectpad(4, w / 2, -p / 2, pl, pw)) + + // Pin 5: Center Exposed Pad (EP) + pads.push(rectpad(5, 0, 0, epw, eph)) + + // Silkscreen: 2 horizontal lines + const sw = w + pl + const sh = p + pw + 0.3 + const silkscreenPaths: PcbSilkscreenPath[] = [] + + silkscreenPaths.push({ + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + type: "pcb_silkscreen_path", + route: [ + { x: -sw / 2 + 0.1, y: sh / 2 }, + { x: sw / 2 - 0.1, y: sh / 2 }, + ], + stroke_width: 0.08, + }) + silkscreenPaths.push({ + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + type: "pcb_silkscreen_path", + route: [ + { x: -sw / 2 + 0.1, y: -sh / 2 }, + { x: sw / 2 - 0.1, y: -sh / 2 }, + ], + stroke_width: 0.08, + }) + + // Pin 1 indicator line + silkscreenPaths.push({ + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + type: "pcb_silkscreen_path", + route: [ + { x: -sw / 2 - 0.1, y: -sh / 2 }, + { x: -sw / 2 - 0.2, y: -sh / 2 }, + ], + stroke_width: 0.08, + }) + + const silkscreenRefText: SilkscreenRef = silkscreenRef( + 0, + sh / 2 + 0.3, + 0.2, + ) + + const courtyard: PcbCourtyardRect = { + type: "pcb_courtyard_rect", + pcb_courtyard_rect_id: "", + pcb_component_id: "", + center: { x: 0, y: 0 }, + width: w + pl + 0.3, + height: p + pw + 0.5, + layer: "top", + } + + return { + circuitJson: [ + ...pads, + silkscreenRefText as AnyCircuitElement, + ...silkscreenPaths, + courtyard, + ], + parameters, + } +} diff --git a/src/footprinter.ts b/src/footprinter.ts index fa51a3b4..e33f28bc 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -40,6 +40,9 @@ export type Footprinter = { dip: ( num_pins?: number, ) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow"> + utdfn: ( + num_pins?: number, + ) => FootprinterParamsBuilder<"w" | "p" | "pl" | "pw" | "epw" | "eph"> cap: () => FootprinterParamsBuilder res: () => FootprinterParamsBuilder diode: () => FootprinterParamsBuilder @@ -275,6 +278,8 @@ const normalizeDefinition = (def: string): string => { .replace(/^sot-223-(\d+)(?=_|$)/i, "sot223_$1") .replace(/^to-220f-(\d+)(?=_|$)/i, "to220f_$1") .replace(/^jst_(ph|sh|zh)_(\d+)(?=_|$)/i, "jst$2_$1") + .replace(/^utdfn-4-ep\(1x1\)(?=_|$)/i, "utdfn4") + .replace(/^utdfn-4-ep(?=_|$)/i, "utdfn4") } export const string = (def: string): Footprinter => { diff --git a/tests/__snapshots__/utdfn-4-ep-1x1.snap.svg b/tests/__snapshots__/utdfn-4-ep-1x1.snap.svg new file mode 100644 index 00000000..817f935c --- /dev/null +++ b/tests/__snapshots__/utdfn-4-ep-1x1.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/utdfn.test.ts b/tests/utdfn.test.ts new file mode 100644 index 00000000..0f9a68d8 --- /dev/null +++ b/tests/utdfn.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("utdfn-4-ep(1x1)", () => { + const circuitJson = fp.string("utdfn-4-ep(1x1)").circuitJson() + + // Find all SMD pads + const pads = circuitJson.filter((e: any) => e.type === "pcb_smtpad") + expect(pads.length).toBe(5) + + // Verify exposed pad (pad 5) is at center + const ep = pads.find((p: any) => p.port_hints?.includes("5")) + expect(ep).toBeDefined() + expect(ep.x).toBe(0) + expect(ep.y).toBe(0) + expect(ep.width).toBeCloseTo(0.48, 3) + expect(ep.height).toBeCloseTo(0.48, 3) + + // Verify contact pad 1 (bottom-left) + const pad1 = pads.find((p: any) => p.port_hints?.includes("1")) + expect(pad1).toBeDefined() + expect(pad1.x).toBeCloseTo(-0.45, 3) + expect(pad1.y).toBeCloseTo(-0.325, 3) + + // Verify contact pad 2 (top-left) + const pad2 = pads.find((p: any) => p.port_hints?.includes("2")) + expect(pad2).toBeDefined() + expect(pad2.x).toBeCloseTo(-0.45, 3) + expect(pad2.y).toBeCloseTo(0.325, 3) + + // Verify contact pad 3 (top-right) + const pad3 = pads.find((p: any) => p.port_hints?.includes("3")) + expect(pad3).toBeDefined() + expect(pad3.x).toBeCloseTo(0.45, 3) + expect(pad3.y).toBeCloseTo(0.325, 3) + + // Verify contact pad 4 (bottom-right) + const pad4 = pads.find((p: any) => p.port_hints?.includes("4")) + expect(pad4).toBeDefined() + expect(pad4.x).toBeCloseTo(0.45, 3) + expect(pad4.y).toBeCloseTo(-0.325, 3) + + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn-4-ep-1x1") +}) From 6f64b6e884fad62c62dcedb8852ae31256af96dc Mon Sep 17 00:00:00 2001 From: gunitesh Date: Sun, 24 May 2026 21:44:13 +0530 Subject: [PATCH 2/2] style: format utdfn files using biome --- src/fn/utdfn.ts | 12 ++++++------ tests/utdfn.test.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/fn/utdfn.ts b/src/fn/utdfn.ts index 2537db31..570863a5 100644 --- a/src/fn/utdfn.ts +++ b/src/fn/utdfn.ts @@ -1,4 +1,8 @@ -import type { AnyCircuitElement, PcbCourtyardRect, PcbSilkscreenPath } from "circuit-json" +import type { + AnyCircuitElement, + PcbCourtyardRect, + PcbSilkscreenPath, +} from "circuit-json" import { z } from "zod" import { rectpad } from "src/helpers/rectpad" import { base_def } from "../helpers/zod/base_def" @@ -79,11 +83,7 @@ export const utdfn = ( stroke_width: 0.08, }) - const silkscreenRefText: SilkscreenRef = silkscreenRef( - 0, - sh / 2 + 0.3, - 0.2, - ) + const silkscreenRefText: SilkscreenRef = silkscreenRef(0, sh / 2 + 0.3, 0.2) const courtyard: PcbCourtyardRect = { type: "pcb_courtyard_rect", diff --git a/tests/utdfn.test.ts b/tests/utdfn.test.ts index 0f9a68d8..5e805db3 100644 --- a/tests/utdfn.test.ts +++ b/tests/utdfn.test.ts @@ -4,7 +4,7 @@ import { fp } from "../src/footprinter" test("utdfn-4-ep(1x1)", () => { const circuitJson = fp.string("utdfn-4-ep(1x1)").circuitJson() - + // Find all SMD pads const pads = circuitJson.filter((e: any) => e.type === "pcb_smtpad") expect(pads.length).toBe(5)