Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions scripts/verify_utdfn.js
Original file line number Diff line number Diff line change
@@ -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)
}
75 changes: 75 additions & 0 deletions scripts/verify_utdfn.ts
Original file line number Diff line number Diff line change
@@ -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)
}
52 changes: 41 additions & 11 deletions src/fn/dfn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,51 @@ 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
*
* 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++) {
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions src/fn/jst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand Down
3 changes: 2 additions & 1 deletion src/fn/msop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const getMsopCoords = (
const half = pinCount / 2
const rowIndex = (pn - 1) % half
const col = pn <= half ? -1 : 1
const row = (half - 1) / 2 - rowIndex
const rowIndexInCol = pn <= half ? rowIndex : (half - 1 - rowIndex)
const row = (half - 1) / 2 - rowIndexInCol
const padBodyOverlap = length.parse("0.2mm")

return {
Expand Down
3 changes: 2 additions & 1 deletion src/fn/son.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export const getSonPadCoord = (
const half = num_pins / 2
const rowIndex = (pn - 1) % half
const col = pn <= half ? -1 : 1
const row = (half - 1) / 2 - rowIndex
const rowIndexInCol = pn <= half ? rowIndex : (half - 1 - rowIndex)
const row = (half - 1) / 2 - rowIndexInCol

return {
x: col * length.parse("1.4mm"),
Expand Down
16 changes: 8 additions & 8 deletions src/fn/sot343.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const sot343CourtyardOutline = [
{ x: -1.703, y: 0.98 },
{ x: -0.983, y: 0.98 },
{ x: -0.983, y: 1.1 },
{ x: 0.477, y: 1.1 },
{ x: 0.477, y: 0.98 },
{ x: 1.197, y: 0.98 },
{ x: 1.197, y: -0.98 },
{ x: 0.477, y: -0.98 },
{ x: 0.477, y: -1.1 },
{ x: 0.983, y: 1.1 },
{ x: 0.983, y: 0.98 },
{ x: 1.703, y: 0.98 },
{ x: 1.703, y: -0.98 },
{ x: 0.983, y: -0.98 },
{ x: 0.983, y: -1.1 },
{ x: -0.983, y: -1.1 },
{ x: -0.983, y: -0.98 },
{ x: -1.703, y: -0.98 },
Expand Down Expand Up @@ -66,8 +66,8 @@ export const getCcwSot343Coords = (parameters: {
const { pn, p } = parameters
if (pn === 1) return { x: -p * 1.92, y: -0.65 }
if (pn === 2) return { x: -p * 1.92, y: 0.65 }
if (pn === 3) return { x: p, y: 0.65 }
if (pn === 4) return { x: p, y: -0.65 }
if (pn === 3) return { x: p * 1.92, y: 0.65 }
if (pn === 4) return { x: p * 1.92, y: -0.65 }
return { x: 0, y: 0 }
}

Expand Down
4 changes: 2 additions & 2 deletions src/fn/sot723.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ export const getCcwSot723Coords = (parameters: {
const { pn, w, h, pl, p } = parameters

if (pn === 1) {
return { x: p, y: 0 }
return { x: -p, y: 0.4 }
}
if (pn === 2) {
return { x: -p, y: -0.4 }
}
return { x: -p, y: 0.4 }
return { x: p, y: 0 }
}

export const sot723WithoutParsing = (
Expand Down
7 changes: 7 additions & 0 deletions src/footprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion tests/__snapshots__/jst.test.tsjst_ph_4.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/__snapshots__/pad.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/__snapshots__/pad_3x2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading