diff --git a/scripts/verify_utdfn.js b/scripts/verify_utdfn.js
new file mode 100644
index 00000000..5d8f2b50
--- /dev/null
+++ b/scripts/verify_utdfn.js
@@ -0,0 +1,61 @@
+require("esbuild-register")
+const { fp } = require("../src/footprinter")
+const assert = require("assert")
+
+console.log("=== Verifying UTDFN-4-EP(1x1) Footprint ===")
+
+try {
+ const circuitJson = fp.string("UTDFN-4-EP(1x1)").circuitJson()
+
+ // Verify total elements
+ console.log(`Total circuit elements: ${circuitJson.length}`)
+
+ // Filter pads
+ const pads = circuitJson.filter((x) => x.type === "pcb_pad")
+ console.log(`Found ${pads.length} pads`)
+ assert.strictEqual(
+ pads.length,
+ 5,
+ "Should have exactly 5 pads (4 pins + 1 EP)",
+ )
+
+ // Verify EP pad (pin 5) is at center (0, 0)
+ const epPad = pads.find((x) => x.pcb_pad_id === "5")
+ assert.ok(epPad, "Pin 5 (Exposed Pad) should exist")
+ assert.strictEqual(epPad.x, 0, "EP x coordinate must be 0")
+ assert.strictEqual(epPad.y, 0, "EP y coordinate must be 0")
+ assert.strictEqual(epPad.width, 0.5, "EP width should be 0.5mm")
+ assert.strictEqual(epPad.height, 0.5, "EP height should be 0.5mm")
+
+ // Verify regular pads (pins 1 to 4)
+ const pad1 = pads.find((x) => x.pcb_pad_id === "1")
+ assert.ok(pad1, "Pin 1 should exist")
+ // dfn4 width is 1.0mm, pin length pl is 0.3mm.
+ // getCcwSoicCoords logic: widthincludeslegs is true, so legsoutside is false, so legoffset = -pl/2 = -0.15mm
+ // Left side pins (1 & 2): x = -w/2 - legoffset = -0.5 - (-0.15) = -0.35mm.
+ assert.strictEqual(
+ Math.abs(pad1.x - -0.35) < 0.001,
+ true,
+ "Pin 1 x coordinate should be -0.35mm",
+ )
+ assert.strictEqual(pad1.width, 0.3, "Pin 1 width should be 0.3mm")
+ assert.strictEqual(pad1.height, 0.25, "Pin 1 height should be 0.25mm")
+
+ // Verify alias parity
+ const canonical = fp
+ .string(
+ "dfn4_w1.00mm_h1.00mm_p0.65mm_pl0.30mm_pw0.25mm_ep_epw0.50mm_eph0.50mm",
+ )
+ .circuitJson()
+ assert.deepStrictEqual(
+ circuitJson,
+ canonical,
+ "Alias 'UTDFN-4-EP(1x1)' must yield identical circuit JSON to canonical representation",
+ )
+
+ console.log("\n✅ ALL LOCAL VERIFICATIONS PASSED SUCCESSFULLY!")
+} catch (err) {
+ console.error("\n❌ VERIFICATION FAILED:")
+ console.error(err)
+ process.exit(1)
+}
diff --git a/scripts/verify_utdfn.ts b/scripts/verify_utdfn.ts
new file mode 100644
index 00000000..205533ac
--- /dev/null
+++ b/scripts/verify_utdfn.ts
@@ -0,0 +1,75 @@
+import { fp } from "../src/footprinter"
+import assert from "assert"
+
+console.log("=== Verifying UTDFN-4-EP(1x1) Footprint ===")
+
+try {
+ const circuitJson = fp.string("UTDFN-4-EP(1x1)").circuitJson()
+
+ // Verify total elements
+ console.log(`Total circuit elements: ${circuitJson.length}`)
+ console.log("Element types:", circuitJson.map((x) => x.type).join(", "))
+
+ // Filter pads (type is pcb_smtpad, not pcb_pad)
+ const pads = circuitJson.filter((x) => x.type === "pcb_smtpad")
+ console.log(`Found ${pads.length} pads`)
+ assert.strictEqual(
+ pads.length,
+ 5,
+ "Should have exactly 5 pads (4 pins + 1 EP)",
+ )
+
+ // Find EP pad - it's pin number 5
+ const epPad: any = pads.find((x: any) => x.port_hints?.includes("5"))
+ assert.ok(epPad, "Pin 5 (Exposed Pad) should exist")
+ assert.strictEqual(epPad.x, 0, "EP x coordinate must be 0")
+ assert.strictEqual(epPad.y, 0, "EP y coordinate must be 0")
+ assert.ok(Math.abs(epPad.width - 0.5) < 0.001, "EP width should be ~0.5mm")
+ assert.ok(Math.abs(epPad.height - 0.5) < 0.001, "EP height should be ~0.5mm")
+
+ // Verify regular pads (pins 1 to 4)
+ const pad1: any = pads.find((x: any) => x.port_hints?.includes("1"))
+ assert.ok(pad1, "Pin 1 should exist")
+ console.log(
+ "Pad 1 details:",
+ JSON.stringify({
+ x: pad1.x,
+ y: pad1.y,
+ width: pad1.width,
+ height: pad1.height,
+ }),
+ )
+ // dfn4 width is 1.0mm, pin length pl is 0.3mm.
+ // getCcwSoicCoords: widthincludeslegs=true → legsoutside=false → legoffset = -pl/2 = -0.15mm
+ // Left side pins: x = -w/2 - legoffset = -0.5 - (-0.15) = -0.35mm
+ assert.ok(
+ Math.abs(pad1.x - -0.35) < 0.01,
+ `Pin 1 x should be ~-0.35mm, got ${pad1.x}`,
+ )
+ assert.ok(
+ Math.abs(pad1.width - 0.3) < 0.01,
+ `Pin 1 width should be ~0.3mm, got ${pad1.width}`,
+ )
+ assert.ok(
+ Math.abs(pad1.height - 0.25) < 0.01,
+ `Pin 1 height should be ~0.25mm, got ${pad1.height}`,
+ )
+
+ // Verify alias parity
+ const canonical = fp
+ .string(
+ "dfn4_w1.00mm_h1.00mm_p0.65mm_pl0.30mm_pw0.25mm_ep_epw0.50mm_eph0.50mm",
+ )
+ .circuitJson()
+ assert.deepStrictEqual(
+ circuitJson,
+ canonical,
+ "Alias 'UTDFN-4-EP(1x1)' must yield identical circuit JSON to canonical representation",
+ )
+
+ console.log("\n✅ ALL LOCAL VERIFICATIONS PASSED SUCCESSFULLY!")
+} catch (err) {
+ console.error("\n❌ VERIFICATION FAILED:")
+ console.error(err)
+ process.exit(1)
+}
diff --git a/src/fn/dfn.ts b/src/fn/dfn.ts
index 1a5b5936..c48dc355 100644
--- a/src/fn/dfn.ts
+++ b/src/fn/dfn.ts
@@ -3,18 +3,38 @@ import type {
PcbCourtyardRect,
PcbSilkscreenPath,
} from "circuit-json"
-import {
- extendSoicDef,
- soicWithoutParsing,
- type SoicInput,
- getCcwSoicCoords,
-} from "./soic"
+import { getCcwSoicCoords } from "./soic"
import { rectpad } from "src/helpers/rectpad"
import { z } from "zod"
import { CORNERS } from "src/helpers/corner"
import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef"
+import { base_def } from "../helpers/zod/base_def"
+import { length } from "circuit-json"
-export const dfn_def = extendSoicDef({})
+export const dfn_def = base_def
+ .extend({
+ fn: z.string(),
+ num_pins: z.number().optional().default(8),
+ w: length.default("5.3mm"),
+ p: length.default("1.27mm"),
+ pw: length.default("0.6mm"),
+ pl: length.default("1.0mm"),
+ ep: z.boolean().optional().default(false),
+ epw: length.optional(),
+ eph: length.optional(),
+ silkscreen_stroke_width: z.number().optional().default(0.1),
+ })
+ .transform((v) => {
+ if (!v.pw && !v.pl) {
+ v.pw = 0.6
+ v.pl = 1.0
+ } else if (!v.pw) {
+ v.pw = v.pl! * (0.6 / 1.0)
+ } else if (!v.pl) {
+ v.pl = v.pw! * (1.0 / 0.6)
+ }
+ return v
+ })
/**
* Dual Flat No-lead
@@ -22,8 +42,12 @@ export const dfn_def = extendSoicDef({})
* Similar to SOIC but different silkscreen
*/
export const dfn = (
- raw_params: SoicInput,
+ raw_params: any,
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
+ if (raw_params.string && raw_params.string.includes("_ep")) {
+ raw_params.ep = true
+ }
+
const parameters = dfn_def.parse(raw_params)
const pads: AnyCircuitElement[] = []
for (let i = 0; i < parameters.num_pins; i++) {
@@ -35,9 +59,15 @@ export const dfn = (
pl: parameters.pl,
widthincludeslegs: true,
})
- pads.push(
- rectpad(i + 1, x, y, parameters.pl ?? "1mm", parameters.pw ?? "0.6mm"),
- )
+ pads.push(rectpad(i + 1, x, y, parameters.pl, parameters.pw))
+ }
+
+ if (parameters.ep) {
+ const epw = parameters.epw ?? parameters.w * 0.5
+ const eph =
+ parameters.eph ??
+ ((parameters.num_pins / 2 - 1) * parameters.p + parameters.pw) * 0.5
+ pads.push(rectpad(parameters.num_pins + 1, 0, 0, epw, eph))
}
// The silkscreen is 4 corners and an arrow identifier for pin1
diff --git a/src/fn/jst.ts b/src/fn/jst.ts
index 27434461..4b012399 100644
--- a/src/fn/jst.ts
+++ b/src/fn/jst.ts
@@ -245,16 +245,19 @@ function generateSilkscreenBody({
p?: number
}): PcbSilkscreenPath {
if (variant === "ph") {
+ const pinSpan = numPins && p ? (numPins - 1) * p : 2.2
+ const bodyLeft = -pinSpan / 2 - 1.9
+ const bodyRight = pinSpan / 2 + 1.9
return {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
route: [
- { x: -3, y: 3 },
- { x: 3, y: 3 },
- { x: 3, y: -2 },
- { x: -3, y: -2 },
- { x: -3, y: 3 },
+ { x: bodyLeft, y: 3 },
+ { x: bodyRight, y: 3 },
+ { x: bodyRight, y: -2 },
+ { x: bodyLeft, y: -2 },
+ { x: bodyLeft, y: 3 },
],
stroke_width: 0.1,
pcb_silkscreen_path_id: "",
diff --git a/src/footprinter.ts b/src/footprinter.ts
index fa51a3b4..87dc43d8 100644
--- a/src/footprinter.ts
+++ b/src/footprinter.ts
@@ -271,10 +271,17 @@ const normalizeDefinition = (def: string): string => {
return def
.trim()
.replace(/^pinheader(?=[\d_]|$)/i, "pinrow")
+ .replace(/^pdip[-_]?(\d+)/i, "dip$1")
+ .replace(/^spdip[-_]?(\d+)/i, "dip$1_p1.778mm")
.replace(/^sot23-(\d+)(?=_|$)/i, "sot23_$1")
.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,
+ "dfn4_w1.00mm_h1.00mm_p0.65mm_pl0.30mm_pw0.25mm_ep_epw0.50mm_eph0.50mm",
+ )
+ .replace(/^utdfn(?=[\d_]|$)/i, "dfn")
}
export const string = (def: string): Footprinter => {
diff --git a/tests/__snapshots__/jst.test.tsjst_ph_4.snap.svg b/tests/__snapshots__/jst.test.tsjst_ph_4.snap.svg
index 39acfdd3..f38f3cc5 100644
--- a/tests/__snapshots__/jst.test.tsjst_ph_4.snap.svg
+++ b/tests/__snapshots__/jst.test.tsjst_ph_4.snap.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/pad.svg b/tests/__snapshots__/pad.svg
index 901c3e4a..e8fbc7e4 100644
--- a/tests/__snapshots__/pad.svg
+++ b/tests/__snapshots__/pad.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/pad_3x2.svg b/tests/__snapshots__/pad_3x2.svg
index 1c7d507a..1d6d2fb4 100644
--- a/tests/__snapshots__/pad_3x2.svg
+++ b/tests/__snapshots__/pad_3x2.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/smtpad_circle_d1.2.svg b/tests/__snapshots__/smtpad_circle_d1.2.svg
index bb5f9409..104245ed 100644
--- a/tests/__snapshots__/smtpad_circle_d1.2.svg
+++ b/tests/__snapshots__/smtpad_circle_d1.2.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/smtpad_pill_w3_h1.svg b/tests/__snapshots__/smtpad_pill_w3_h1.svg
index f50daa40..46ab266c 100644
--- a/tests/__snapshots__/smtpad_pill_w3_h1.svg
+++ b/tests/__snapshots__/smtpad_pill_w3_h1.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/__snapshots__/smtpad_rect_w2_h1.svg b/tests/__snapshots__/smtpad_rect_w2_h1.svg
index 901c3e4a..e8fbc7e4 100644
--- a/tests/__snapshots__/smtpad_rect_w2_h1.svg
+++ b/tests/__snapshots__/smtpad_rect_w2_h1.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/dfn.test.ts b/tests/dfn.test.ts
index 71548d29..e9e61371 100644
--- a/tests/dfn.test.ts
+++ b/tests/dfn.test.ts
@@ -7,3 +7,25 @@ test("dfn8_w5.3mm_p1.27mm", () => {
const svgContent = convertCircuitJsonToPcbSvg(soup)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "dfn8_w5.3mm_p1.27mm")
})
+
+test("UTDFN-4-EP(1x1) footprint normalization & rendering", () => {
+ const circuitJson = fp.string("UTDFN-4-EP(1x1)").circuitJson()
+ const pads = circuitJson.filter((x: any) => x.type === "pcb_smtpad")
+ expect(pads.length).toBe(5) // 4 pins + 1 exposed pad
+
+ // Verify exposed pad (pin 5) is at 0, 0
+ const epPad: any = pads.find((x: any) => x.port_hints?.includes("5"))
+ expect(epPad).toBeDefined()
+ expect(epPad.x).toBe(0)
+ expect(epPad.y).toBe(0)
+})
+
+test("utdfn_4_ep(1x1) alias parity", () => {
+ const canonical = fp
+ .string(
+ "dfn4_w1.00mm_h1.00mm_p0.65mm_pl0.30mm_pw0.25mm_ep_epw0.50mm_eph0.50mm",
+ )
+ .circuitJson()
+ const alias = fp.string("UTDFN-4-EP(1x1)").circuitJson()
+ expect(alias).toEqual(canonical)
+})
diff --git a/tests/dip.test.ts b/tests/dip.test.ts
index ccc0d214..35a0eca3 100644
--- a/tests/dip.test.ts
+++ b/tests/dip.test.ts
@@ -103,3 +103,16 @@ test("dip_0.1in", () => {
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "dip_0.1in")
})
+
+test("PDIP-8 string resolves to dip8", () => {
+ const json = fp.string("PDIP-8").json()
+ expect(json.fn).toBe("dip")
+ expect(json.num_pins).toBe(8)
+})
+
+test("SPDIP-28 string resolves to dip28 with shrink pitch", () => {
+ const json = fp.string("SPDIP-28").json()
+ expect(json.fn).toBe("dip")
+ expect(json.num_pins).toBe(28)
+ expect(json.p).toBe(1.778)
+})
diff --git a/tests/jst.test.ts b/tests/jst.test.ts
index b02c7f9a..3c38dc79 100644
--- a/tests/jst.test.ts
+++ b/tests/jst.test.ts
@@ -66,3 +66,33 @@ test("jst_zh_2 (pretransform)", () => {
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path + "jst_zh_2")
})
+
+test("jst_ph silkscreen scales dynamically with pin count", () => {
+ const circuitJson2 = fp.string("jst2_ph").circuitJson()
+ const circuitJson4 = fp.string("jst_ph_4").circuitJson()
+
+ const silkscreen2 = circuitJson2.find(
+ (x: any) => x.type === "pcb_silkscreen_path",
+ ) as any
+ const silkscreen4 = circuitJson4.find(
+ (x: any) => x.type === "pcb_silkscreen_path",
+ ) as any
+
+ expect(silkscreen2).toBeDefined()
+ expect(silkscreen4).toBeDefined()
+
+ const xs2 = silkscreen2.route.map((p: any) => p.x)
+ const xs4 = silkscreen4.route.map((p: any) => p.x)
+
+ const width2 = Math.max(...xs2) - Math.min(...xs2)
+ const width4 = Math.max(...xs4) - Math.min(...xs4)
+
+ // 2-pin width should be exactly 6mm (or very close: 2.2 + 3.8 = 6.0)
+ expect(width2).toBeCloseTo(6.0, 1)
+
+ // 4-pin width should be exactly 10.4mm (3 * 2.2 + 3.8 = 10.4)
+ expect(width4).toBeCloseTo(10.4, 1)
+
+ // 4-pin should be wider than 2-pin
+ expect(width4).toBeGreaterThan(width2)
+})