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 @@ -{REF} \ No newline at end of file +{REF} \ 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 @@ -{REF} \ No newline at end of file +{REF} \ 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 @@ -{REF} \ No newline at end of file +{REF} \ 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 @@ -{REF} \ No newline at end of file +{REF} \ 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 @@ -{REF} \ No newline at end of file +{REF} \ 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 @@ -{REF} \ No newline at end of file +{REF} \ 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) +})