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..977980be --- /dev/null +++ b/src/fn/utdfn.ts @@ -0,0 +1,151 @@ +import type { + AnyCircuitElement, + PcbCourtyardOutline, + PcbSilkscreenPath, +} from "circuit-json" +import { z } from "zod" +import { length } from "circuit-json" +import { rectpad } from "../helpers/rectpad" +import { silkscreenRef, type SilkscreenRef } from "src/helpers/silkscreenRef" +import { base_def } from "../helpers/zod/base_def" +import { createRectUnionOutline } from "src/helpers/rect-union-outline" + +export const utdfn_def = base_def.extend({ + fn: z.string(), + num_pins: z.literal(4).default(4), + w: z.string().default("1mm"), + h: z.string().default("1mm"), + p: z.string().default("0.5mm"), + pl: z.string().default("0.25mm"), + pw: z.string().default("0.25mm"), + epw: z.string().default("0.54mm"), + eph: z.string().default("0.54mm"), + string: z.string().optional(), + ep: z.boolean().default(true), +}) + +const getUtdfn4PadCoord = ( + pn: number, + w: number, + p: number, + pl: number, +): { x: number; y: number } => { + const pinColumnX = w / 2 - pl / 2 + 0.025 + const pinRowY = p / 2 + + if (pn === 1) return { x: -pinColumnX, y: -pinRowY } + if (pn === 2) return { x: -pinColumnX, y: pinRowY } + if (pn === 3) return { x: pinColumnX, y: pinRowY } + return { x: pinColumnX, y: -pinRowY } +} + +export const utdfn = ( + raw_params: z.input, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + if (raw_params.string?.toLowerCase().includes("_ep")) { + raw_params = { ...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 pads: AnyCircuitElement[] = [] + + for (let i = 0; i < parameters.num_pins; i++) { + const { x, y } = getUtdfn4PadCoord(i + 1, w, p, pl) + pads.push(rectpad(i + 1, x, y, pl, pw)) + } + + if (parameters.ep) { + pads.push(rectpad(parameters.num_pins + 1, 0, 0, epw, eph)) + } + + const silkscreenTopLine: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + route: [ + { x: -w / 2, y: h / 2 }, + { x: w / 2, y: h / 2 }, + ], + stroke_width: 0.05, + pcb_silkscreen_path_id: "", + } + + const silkscreenBottomLine: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + route: [ + { x: -w / 2, y: -h / 2 }, + { x: w / 2, y: -h / 2 }, + ], + stroke_width: 0.05, + pcb_silkscreen_path_id: "", + } + + const pin1Position = getUtdfn4PadCoord(1, w, p, pl) + const pin1Marking: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "pin_marker_1", + route: [ + { x: pin1Position.x - 0.15, y: pin1Position.y }, + { x: pin1Position.x - 0.25, y: pin1Position.y + 0.1 }, + { x: pin1Position.x - 0.25, y: pin1Position.y - 0.1 }, + { x: pin1Position.x - 0.15, y: pin1Position.y }, + ], + stroke_width: 0.05, + pcb_silkscreen_path_id: "pin_marker_1", + } + + const silkscreenRefText: SilkscreenRef = silkscreenRef(0, h / 2 + 0.35, 0.2) + + const pinColumnCenterX = Math.abs(getUtdfn4PadCoord(1, w, p, pl).x) + const pinRowSpanY = p + pw + const pinRowSpanX = pinColumnCenterX * 2 + pl + const courtyardStepInnerHalfWidth = w / 2 + 0.25 + const courtyardStepOuterHalfWidth = pinRowSpanX / 2 + 0.25 + const courtyardStepInnerHalfHeight = pinRowSpanY / 2 + 0.25 + const courtyardStepOuterHalfHeight = h / 2 + 0.25 + + const courtyard: PcbCourtyardOutline = { + type: "pcb_courtyard_outline", + pcb_courtyard_outline_id: "", + pcb_component_id: "", + layer: "top", + outline: createRectUnionOutline([ + { + minX: -courtyardStepOuterHalfWidth, + maxX: courtyardStepOuterHalfWidth, + minY: -courtyardStepInnerHalfHeight, + maxY: courtyardStepInnerHalfHeight, + }, + { + minX: -courtyardStepInnerHalfWidth, + maxX: courtyardStepInnerHalfWidth, + minY: -courtyardStepOuterHalfHeight, + maxY: courtyardStepOuterHalfHeight, + }, + ]), + } + + return { + circuitJson: [ + ...pads, + silkscreenTopLine, + silkscreenBottomLine, + silkscreenRefText, + pin1Marking, + courtyard, + ], + parameters, + } +} diff --git a/src/footprinter.ts b/src/footprinter.ts index fa51a3b4..2b117a7e 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< @@ -275,6 +280,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, "utdfn_4_ep") + .replace(/^utdfn-(\d+)-ep/i, "utdfn_$1_ep") } export const string = (def: string): Footprinter => { diff --git a/tests/__snapshots__/utdfn_4_ep.snap.svg b/tests/__snapshots__/utdfn_4_ep.snap.svg new file mode 100644 index 00000000..4e837972 --- /dev/null +++ b/tests/__snapshots__/utdfn_4_ep.snap.svg @@ -0,0 +1 @@ +{REF} diff --git a/tests/__snapshots__/utdfn_4_ep_1x1_alias.snap.svg b/tests/__snapshots__/utdfn_4_ep_1x1_alias.snap.svg new file mode 100644 index 00000000..4e837972 --- /dev/null +++ b/tests/__snapshots__/utdfn_4_ep_1x1_alias.snap.svg @@ -0,0 +1 @@ +{REF} diff --git a/tests/utdfn.test.ts b/tests/utdfn.test.ts new file mode 100644 index 00000000..d52e79f0 --- /dev/null +++ b/tests/utdfn.test.ts @@ -0,0 +1,28 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("utdfn_4_ep", () => { + const circuitJson = fp.string("utdfn_4_ep").circuitJson() + const params = fp.string("utdfn_4_ep").json() as any + expect(params.num_pins).toBe(4) + expect(params.ep).toBe(true) + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn_4_ep") +}) + +test("UTDFN-4-EP(1x1) pretransform alias", () => { + const circuitJson = fp.string("UTDFN-4-EP(1x1)").circuitJson() + const params = fp.string("UTDFN-4-EP(1x1)").json() as any + expect(params.num_pins).toBe(4) + expect(params.ep).toBe(true) + const pads = circuitJson.filter( + (el: any) => el.type === "pcb_smtpad" || el.type === "pcb_plated_hole", + ) + expect(pads.length).toBe(5) + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot( + import.meta.path, + "utdfn_4_ep_1x1_alias", + ) +})