From 930cc2d75b399428abe9cbd4a625bec40d0596ec Mon Sep 17 00:00:00 2001 From: chengdazhi Date: Mon, 13 Jul 2026 15:55:44 +0800 Subject: [PATCH] feat: implement UTDFN-4-EP (1x1mm) footprint Add utdfn for the ultra-thin DFN 4-pin 1.0x1.0mm package with exposed pad, used by JLCPCB parts such as FP6182-28X7 and MIC5366-xYMT. Dimensions follow the Diodes X2-DFN1010-4 (Type B) package outline and suggested pad layout (pitch 0.65mm, 0.35x0.35mm pads with the inner corner chamfered for exposed-pad clearance, 0.53x0.53mm center pad), which also matches the Micrel 4-pin 1x1mm Thin MLF (MT) package. - src/fn/utdfn.ts: chamfered polygon signal pads + rect exposed pad, corner silkscreen marks and pin-1 arrow, courtyard - footprinter string support: utdfn_4_ep, utdfn4_ep and hyphenated aliases UTDFN-4-EP(1x1) / UTDFN-4-EP - snapshot and alias tests, plus a pad/exposed-pad overlap check --- src/fn/index.ts | 1 + src/fn/utdfn.ts | 187 ++++++++++++++++++++++++ src/footprinter.ts | 7 + tests/__snapshots__/utdfn_4_ep.snap.svg | 1 + tests/utdfn.test.ts | 58 ++++++++ 5 files changed, 254 insertions(+) create mode 100644 src/fn/utdfn.ts create mode 100644 tests/__snapshots__/utdfn_4_ep.snap.svg create mode 100644 tests/utdfn.test.ts diff --git a/src/fn/index.ts b/src/fn/index.ts index 94c0370e..0eb306c2 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -66,6 +66,7 @@ export { msop } from "./msop" export { sod323w } from "./sod323w" export { sod323fl } from "./sod323fl" export { son } from "./son" +export { utdfn } from "./utdfn" export { vson } from "./vson" export { solderjumper } from "./solderjumper" export { sot457 } from "./sot457" diff --git a/src/fn/utdfn.ts b/src/fn/utdfn.ts new file mode 100644 index 00000000..6bfdd419 --- /dev/null +++ b/src/fn/utdfn.ts @@ -0,0 +1,187 @@ +import type { + AnyCircuitElement, + PcbCourtyardRect, + PcbSilkscreenPath, + Point, +} from "circuit-json" +import { z } from "zod" +import { length } from "circuit-json" +import { rectpad } from "../helpers/rectpad" +import { polygonpad } from "../helpers/polygonpad" +import { silkscreenRef, type SilkscreenRef } from "src/helpers/silkscreenRef" +import { base_def } from "../helpers/zod/base_def" +import { CORNERS } from "src/helpers/corner" + +export const utdfn_def = base_def.extend({ + fn: z.string(), + num_pins: z.literal(4).optional().default(4), + w: z.string().default("1mm"), + h: z.string().default("1mm"), + p: z.string().default("0.65mm"), + pl: z.string().default("0.35mm"), + pw: z.string().default("0.35mm"), + epw: z.string().default("0.53mm"), + eph: z.string().default("0.53mm"), + string: z.string().optional(), + ep: z.boolean().default(true), +}) + +/** + * How far the signal pads extend past the body edge (toe fillet). + * Matches the Diodes X2-DFN1010-4 (Type B) suggested pad layout (Y2 = 1.10mm). + */ +const PAD_TOE_EXTENSION = 0.05 + +/** + * 45-degree chamfer leg on the inner corner of each signal pad, keeps + * clearance to the exposed pad (per the X2-DFN1010-4 (Type B) land pattern). + */ +const PAD_CHAMFER = 0.24 + +const getUtdfnPadCoord = (pn: number, p: number, rowY: number) => { + // CCW starting at the top left, pads on the top and bottom rows + if (pn === 1) return { x: -p / 2, y: rowY } + if (pn === 2) return { x: -p / 2, y: -rowY } + if (pn === 3) return { x: p / 2, y: -rowY } + return { x: p / 2, y: rowY } +} + +/** + * Signal pad polygon: rectangle with the inner corner (the one facing the + * exposed pad) chamfered at 45 degrees, points returned CCW. + */ +const getUtdfnPadPolygon = ( + pn: number, + p: number, + rowY: number, + pl: number, + pw: number, +): Point[] => { + const { x: cx, y: cy } = getUtdfnPadCoord(pn, p, rowY) + const sx = Math.sign(cx) + const sy = Math.sign(cy) + const xInner = cx - (sx * pw) / 2 + const xOuter = cx + (sx * pw) / 2 + const yInner = cy - (sy * pl) / 2 + const yOuter = cy + (sy * pl) / 2 + + const points: Point[] = [ + { x: xInner + sx * PAD_CHAMFER, y: yInner }, + { x: xOuter, y: yInner }, + { x: xOuter, y: yOuter }, + { x: xInner, y: yOuter }, + { x: xInner, y: yInner + sy * PAD_CHAMFER }, + ] + + let signedArea = 0 + for (let i = 0; i < points.length; i++) { + const a = points[i]! + const b = points[(i + 1) % points.length]! + signedArea += a.x * b.y - b.x * a.y + } + return signedArea < 0 ? points.reverse() : points +} + +/** + * UTDFN-4-EP (1x1mm) - Ultra Thin Dual Flat No-lead, 4 pins + exposed pad + * + * 1.0 x 1.0mm body, 0.65mm pitch, pads on the top and bottom rows and a + * centered exposed pad. Dimensions follow the Diodes X2-DFN1010-4 (Type B) + * package outline and suggested pad layout (e = 0.65mm, X/Y = 0.35mm, + * X3 = 1.00mm, Y2 = 1.10mm, center pad = 0.53mm) as used by JLCPCB parts + * such as FP6182-28X7 and MIC5366-xYMT (4-pin 1x1mm Thin MLF). + */ +export const utdfn = ( + raw_params: z.input, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + if (raw_params.string?.toLowerCase().includes("_ep")) { + raw_params.ep = true + } + + const parameters = utdfn_def.parse(raw_params) + + const w = length.parse(parameters.w) + const h = length.parse(parameters.h) + const p = length.parse(parameters.p) + const pl = length.parse(parameters.pl) + const pw = length.parse(parameters.pw) + const epw = length.parse(parameters.epw) + const eph = length.parse(parameters.eph) + + const padRowY = h / 2 + PAD_TOE_EXTENSION - pl / 2 + + const pads: AnyCircuitElement[] = [] + for (let i = 0; i < parameters.num_pins; i++) { + pads.push(polygonpad(i + 1, getUtdfnPadPolygon(i + 1, p, padRowY, pl, pw))) + } + + if (parameters.ep) { + pads.push(rectpad(parameters.num_pins + 1, 0, 0, epw, eph)) + } + + // The silkscreen is 4 corner marks outside the pads and a pin 1 arrow + const sw = w + 0.1 + const sh = h + 0.2 + const arm = 0.2 + const silkscreenPaths: PcbSilkscreenPath[] = [] + + for (const corner of CORNERS) { + const { dx, dy } = corner + silkscreenPaths.push({ + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + route: [ + { x: (dx * sw) / 2 - dx * arm, y: (dy * sh) / 2 }, + { x: (dx * sw) / 2, y: (dy * sh) / 2 }, + { x: (dx * sw) / 2, y: (dy * sh) / 2 - dy * arm }, + ], + type: "pcb_silkscreen_path", + stroke_width: 0.1, + }) + } + + // Pin 1 arrow pointing at the top left pad + const as = 0.1 + const atx = -sw / 2 - as / 2 + const aty = padRowY + silkscreenPaths.push({ + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "", + type: "pcb_silkscreen_path", + route: [ + { x: atx, y: aty }, + { x: atx - as, y: aty + as }, + { x: atx - as, y: aty - as }, + { x: atx, y: aty }, + ], + stroke_width: 0.1, + }) + + const silkscreenRefText: SilkscreenRef = silkscreenRef(0, sh / 2 + 0.35, 0.2) + + const pinRowSpanX = p + pw + const pinRowSpanY = 2 * padRowY + pl + const courtyardHalfWidth = Math.max(pinRowSpanX, w) / 2 + 0.25 + const courtyardHalfHeight = Math.max(pinRowSpanY, h) / 2 + 0.25 + const courtyard: PcbCourtyardRect = { + type: "pcb_courtyard_rect", + pcb_courtyard_rect_id: "", + pcb_component_id: "", + center: { x: 0, y: 0 }, + width: courtyardHalfWidth * 2, + height: courtyardHalfHeight * 2, + layer: "top", + } + + return { + circuitJson: [ + ...pads, + ...silkscreenPaths, + silkscreenRefText, + courtyard, + ] as AnyCircuitElement[], + parameters, + } +} diff --git a/src/footprinter.ts b/src/footprinter.ts index fa51a3b4..aed20f71 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -177,6 +177,11 @@ export type Footprinter = { ) => FootprinterParamsBuilder< "w" | "h" | "p" | "pl" | "pw" | "epw" | "eph" | "ep" > + utdfn: ( + num_pins?: number, + ) => FootprinterParamsBuilder< + "w" | "h" | "p" | "pl" | "pw" | "epw" | "eph" | "ep" + > vson: ( num_pins?: number, ) => FootprinterParamsBuilder< @@ -274,6 +279,8 @@ const normalizeDefinition = (def: string): string => { .replace(/^sot23-(\d+)(?=_|$)/i, "sot23_$1") .replace(/^sot-223-(\d+)(?=_|$)/i, "sot223_$1") .replace(/^to-220f-(\d+)(?=_|$)/i, "to220f_$1") + .replace(/^utdfn-(\d+)-ep\(1x1\)(?=_|$)/i, "utdfn$1_ep") + .replace(/^utdfn-(\d+)-ep(?=_|$)/i, "utdfn$1_ep") .replace(/^jst_(ph|sh|zh)_(\d+)(?=_|$)/i, "jst$2_$1") } diff --git a/tests/__snapshots__/utdfn_4_ep.snap.svg b/tests/__snapshots__/utdfn_4_ep.snap.svg new file mode 100644 index 00000000..ba92714c --- /dev/null +++ b/tests/__snapshots__/utdfn_4_ep.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..699b3b79 --- /dev/null +++ b/tests/utdfn.test.ts @@ -0,0 +1,58 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" +import type { AnyCircuitElement } from "circuit-json" + +test("utdfn_4_ep", () => { + const circuitJson = fp + .string("utdfn_4_ep") + .circuitJson() as AnyCircuitElement[] + const json = fp.string("utdfn_4_ep").json() as any + + expect(json.num_pins).toBe(4) + expect(json.ep).toBe(true) + + const pads = circuitJson.filter((el) => el.type === "pcb_smtpad") + expect(pads.length).toBe(5) + + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn_4_ep") +}) + +test("UTDFN-4-EP(1x1) (alias)", () => { + const aliasJson = fp.string("UTDFN-4-EP(1x1)").circuitJson() + const canonicalJson = fp.string("utdfn_4_ep").circuitJson() + expect(aliasJson).toEqual(canonicalJson) + + const json = fp.string("UTDFN-4-EP(1x1)").json() as any + expect(json.num_pins).toBe(4) + expect(json.ep).toBe(true) +}) + +test("UTDFN-4-EP (alias)", () => { + const aliasJson = fp.string("UTDFN-4-EP").circuitJson() + const canonicalJson = fp.string("utdfn_4_ep").circuitJson() + expect(aliasJson).toEqual(canonicalJson) +}) + +test("utdfn signal pads do not overlap the exposed pad", () => { + const circuitJson = fp + .string("utdfn_4_ep") + .circuitJson() as AnyCircuitElement[] + const ep = circuitJson.find( + (el: any) => el.type === "pcb_smtpad" && el.port_hints?.includes("5"), + ) as any + const signalPads = circuitJson.filter( + (el: any) => el.type === "pcb_smtpad" && !el.port_hints?.includes("5"), + ) as any[] + + const epHalfW = ep.width / 2 + const epHalfH = ep.height / 2 + for (const pad of signalPads) { + // every polygon vertex must lie outside the exposed pad rectangle + for (const point of pad.points) { + const inside = Math.abs(point.x) < epHalfW && Math.abs(point.y) < epHalfH + expect(inside).toBe(false) + } + } +})