diff --git a/src/fn/index.ts b/src/fn/index.ts index 94c0370e..f8435ea6 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -17,6 +17,7 @@ export { sot886 } from "./sot886" export { sot23 } from "./sot23" export { sot25 } from "./sot25" export { dfn } from "./dfn" +export { utdfn4ep } from "./utdfn4ep" export { pinrow } from "./pinrow" export { sot563 } from "./sot563" export { ms012 } from "./ms012" @@ -48,6 +49,7 @@ export { sod128 } from "./sod128" export { sot89 } from "./sot89" export { to220 } from "./to220" export { to220f } from "./to220f" +export { to220h } from "./to220h" export { minimelf } from "./minimelf" export { sod882d } from "./sod882d" export { melf } from "./melf" diff --git a/src/fn/to220h.ts b/src/fn/to220h.ts new file mode 100644 index 00000000..80fb34dc --- /dev/null +++ b/src/fn/to220h.ts @@ -0,0 +1,164 @@ +import type { AnyCircuitElement, PcbSilkscreenPath } from "circuit-json" +import { mm } from "@tscircuit/mm" +import { length } from "circuit-json" +import { z } from "zod" +import { platedHoleWithRectPad } from "../helpers/platedHoleWithRectPad" +import { platedHolePill } from "../helpers/platedHolePill" +import { type SilkscreenRef, silkscreenRef } from "../helpers/silkscreenRef" +import { base_def } from "../helpers/zod/base_def" + +// TO-220 Horizontal uses 2.54mm standard pitch (matches KiCad) +const TO220H_PITCH_MM = 2.54 + +export const to220h_def = base_def.extend({ + fn: z.string(), + id: length.optional().default("1.1mm"), + od: length.optional().default("1.905mm"), + ph: length.optional().default("2mm"), + num_pins: z.number().optional(), + // tabup / tabdown can be passed as boolean flags (from footprinter string parser) + // or "tab" can be set explicitly + tabup: z.boolean().optional(), + tabdown: z.boolean().optional(), + string: z.string().optional(), +}) + +export type To220hDef = z.input + +export const to220h = ( + raw_params: To220hDef, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + const parameters = to220h_def.parse(raw_params) + + const numPins = + parameters.num_pins ?? + Number.parseInt( + parameters.string?.match(/^to220h(?:[_-](\d+))?/i)?.[1] ?? "3", + ) + + // Determine tab direction: tabup flag takes precedence, default is "down" + const isTabUp = + parameters.tabup === true || + (parameters.string !== undefined && /tabup/i.test(parameters.string)) + const sign = isTabUp ? -1 : 1 + + // Holes: centered at x=0 with 2.54mm pitch (matches to220f) + const holes: AnyCircuitElement[] = Array.from({ length: numPins }, (_, i) => { + const x = + numPins % 2 === 0 + ? (i - numPins / 2 + 0.5) * TO220H_PITCH_MM + : (i - Math.floor(numPins / 2)) * TO220H_PITCH_MM + + if (i === 0) { + return platedHoleWithRectPad({ + pn: 1, + x, + y: 0, + holeDiameter: parameters.id, + rectPadWidth: parameters.od, + rectPadHeight: parameters.ph, + }) as AnyCircuitElement + } + + return platedHolePill( + i + 1, + x, + 0, + mm(parameters.id), + mm(parameters.od), + mm(parameters.ph), + ) as AnyCircuitElement + }) + + // Body silkscreen dimensions derived from KiCad TO-220-3_Horizontal_TabDown + // KiCad pins at x=0,2.54,5.08 (our pins centered at x=0: -2.54,0,2.54) + // KiCad body x: -2.57 to 7.65 → width = 10.22mm, center at 2.54 + // Translating to our center-at-0: x from -5.11 to 5.11 + // KiCad body y (TabDown): near=3.7, far=13.17, tab far=19.57 + // Lead clearance: 3.7mm; body height: 9.47mm; tab: 6.4mm + const bodyXLeft = -5.11 + const bodyXRight = 5.11 + const leadClearance = 3.7 - TO220H_PITCH_MM // 1.16mm (pad edge to body) + const bodyH = 9.47 + const tabH = 6.4 + + const bodyNearY = sign * leadClearance + const bodyFarY = sign * (leadClearance + bodyH) + const tabFarY = sign * (leadClearance + bodyH + tabH) + + // Body outline + const silkBody: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_body", + stroke_width: 0.12, + route: [ + { x: bodyXLeft, y: bodyNearY }, + { x: bodyXRight, y: bodyNearY }, + { x: bodyXRight, y: bodyFarY }, + { x: bodyXLeft, y: bodyFarY }, + { x: bodyXLeft, y: bodyNearY }, + ], + } + + // Tab outline + const silkTab: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_tab", + stroke_width: 0.12, + route: [ + { x: bodyXLeft, y: bodyFarY }, + { x: bodyXRight, y: bodyFarY }, + { x: bodyXRight, y: tabFarY }, + { x: bodyXLeft, y: tabFarY }, + { x: bodyXLeft, y: bodyFarY }, + ], + } + + // Lead lines from body edge toward pin holes + const halfPw = mm(parameters.od) / 2 + const leadLines: PcbSilkscreenPath[] = Array.from( + { length: numPins }, + (_, i) => { + const x = + numPins % 2 === 0 + ? (i - numPins / 2 + 0.5) * TO220H_PITCH_MM + : (i - Math.floor(numPins / 2)) * TO220H_PITCH_MM + return { + type: "pcb_silkscreen_path" as const, + layer: "top" as const, + pcb_component_id: "", + pcb_silkscreen_path_id: `silkscreen_lead_${i + 1}`, + stroke_width: 0.12, + route: [ + { x: x - halfPw, y: bodyNearY }, + { x: x - halfPw, y: sign * halfPw }, + ], + } + }, + ) + + const silkscreenRefText: SilkscreenRef = silkscreenRef( + 0, + isTabUp ? leadClearance + 0.8 : -(leadClearance + 0.8), + 0.5, + ) + + return { + circuitJson: [ + ...holes, + silkBody, + silkTab, + ...leadLines, + silkscreenRefText as AnyCircuitElement, + ], + parameters: { + ...parameters, + p: TO220H_PITCH_MM, + num_pins: numPins, + }, + } +} diff --git a/src/fn/utdfn4ep.ts b/src/fn/utdfn4ep.ts new file mode 100644 index 00000000..f4a2095c --- /dev/null +++ b/src/fn/utdfn4ep.ts @@ -0,0 +1,101 @@ +import type { AnyCircuitElement, PcbSilkscreenPath } from "circuit-json" +import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef" +import { rectpad } from "src/helpers/rectpad" +import { z } from "zod" +import { base_def } from "../helpers/zod/base_def" + +/** + * UTDFN-4-EP (1x1 mm) footprint + * + * Ultra-Thin Dual Flat No-Lead, 4 corner pads + 1 central exposed thermal pad. + * + * References (JLCPCB parts with this footprint): + * - Microchip MIC5366-1.8YMT-TZ https://jlcpcb.com/partdetail/C621364 + * - Fitipower FP6182-28X7 https://jlcpcb.com/partdetail/C498349 + * + * Land pattern dimensions based on: + * Microchip DS20005619G, page 11 (recommended courtyard 1.4 × 1.3 mm). + */ +export const utdfn4ep_def = base_def.extend({ + fn: z.string(), + /** Pad width (mm string, e.g. "0.28mm") */ + pw: z.string().default("0.28mm"), + /** Pad height (mm string, e.g. "0.4mm") */ + ph: z.string().default("0.4mm"), + /** Exposed-pad width (mm string) */ + epw: z.string().default("0.5mm"), + /** Exposed-pad height (mm string) */ + eph: z.string().default("0.4mm"), + string: z.string().optional(), +}) + +export const utdfn4ep = ( + raw_params: z.input, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + const parameters = utdfn4ep_def.parse(raw_params) + + const pw = Number.parseFloat(parameters.pw) + const ph = Number.parseFloat(parameters.ph) + const epw = Number.parseFloat(parameters.epw) + const eph = Number.parseFloat(parameters.eph) + + // Body is 1.0 × 1.0 mm; pads sit at the four corners. + // Pad centres at ±(0.5 - pw/2) in x, ±(0.5 - ph/2) in y. + const cx = 0.5 - pw / 2 + const cy = 0.5 - ph / 2 + + const pads: AnyCircuitElement[] = [ + // CCW numbering starting top-left + rectpad(1, -cx, cy, pw, ph), + rectpad(2, cx, cy, pw, ph), + rectpad(3, cx, -cy, pw, ph), + rectpad(4, -cx, -cy, pw, ph), + // Exposed thermal pad (EP) + rectpad(5, 0, 0, epw, eph), + ] + + // Silkscreen outline: two L-shaped edge marks (top and bottom) + const bx = 0.7 + const by = 0.65 + const notch = 0.2 + + const silkscreenTop: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_top", + route: [ + { x: -bx, y: by - notch }, + { x: -bx, y: by }, + { x: bx, y: by }, + { x: bx, y: by - notch }, + ], + stroke_width: 0.05, + } + + const silkscreenBottom: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_bottom", + route: [ + { x: -bx, y: -(by - notch) }, + { x: -bx, y: -by }, + { x: bx, y: -by }, + { x: bx, y: -(by - notch) }, + ], + stroke_width: 0.05, + } + + const refText: SilkscreenRef = silkscreenRef(0, by + 0.3, 0.3) + + return { + circuitJson: [ + ...pads, + silkscreenTop, + silkscreenBottom, + refText as AnyCircuitElement, + ], + parameters, + } +} diff --git a/src/footprinter.ts b/src/footprinter.ts index 6af68a44..75a0f84a 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -79,6 +79,7 @@ export type Footprinter = { ssop: (num_pins?: number) => FootprinterParamsBuilder<"w" | "p"> tssop: (num_pins?: number) => FootprinterParamsBuilder<"w" | "p"> dfn: (num_pins?: number) => FootprinterParamsBuilder<"w" | "p"> + utdfn4ep: () => FootprinterParamsBuilder<"pw" | "ph" | "epw" | "eph"> pinrow: ( num_pins?: number, ) => FootprinterParamsBuilder< @@ -110,6 +111,7 @@ export type Footprinter = { hc49: () => FootprinterParamsBuilder<"p" | "id" | "od" | "w" | "h"> to220: () => FootprinterParamsBuilder<"w" | "h" | "p" | "id" | "od"> to220f: () => FootprinterParamsBuilder<"w" | "h" | "p" | "id" | "od"> + to220h: () => FootprinterParamsBuilder<"id" | "od" | "ph" | "tab"> sot363: () => FootprinterParamsBuilder<"w" | "h" | "p" | "pl" | "pw"> sot886: () => FootprinterParamsBuilder<"w" | "h" | "p" | "pl" | "pw"> sot457: () => FootprinterParamsBuilder< @@ -271,6 +273,10 @@ const normalizeDefinition = (def: string): string => { .trim() .replace(/^sot-223-(\d+)(?=_|$)/i, "sot223_$1") .replace(/^to-220f-(\d+)(?=_|$)/i, "to220f_$1") + .replace(/^to-220-(\d+)[_-]horizontal/i, "to220h_$1") + .replace(/^to-220-horizontal(?:-(\d+))?/i, (_, n) => + n ? `to220h_${n}` : "to220h", + ) } export const string = (def: string): Footprinter => { diff --git a/tests/__snapshots__/to220h_3.snap.svg b/tests/__snapshots__/to220h_3.snap.svg new file mode 100644 index 00000000..067f4c6d --- /dev/null +++ b/tests/__snapshots__/to220h_3.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/to220h_3_tabup.snap.svg b/tests/__snapshots__/to220h_3_tabup.snap.svg new file mode 100644 index 00000000..57a70ff4 --- /dev/null +++ b/tests/__snapshots__/to220h_3_tabup.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/utdfn4ep.snap.svg b/tests/__snapshots__/utdfn4ep.snap.svg new file mode 100644 index 00000000..467d06bb --- /dev/null +++ b/tests/__snapshots__/utdfn4ep.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm.snap.svg b/tests/__snapshots__/utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm.snap.svg new file mode 100644 index 00000000..327efcc6 --- /dev/null +++ b/tests/__snapshots__/utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/to220h_3.snap.svg b/tests/kicad-parity/__snapshots__/to220h_3.snap.svg new file mode 100644 index 00000000..4adee352 --- /dev/null +++ b/tests/kicad-parity/__snapshots__/to220h_3.snap.svg @@ -0,0 +1 @@ +{REF}Diff: 0.00% \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/to220h_3_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/to220h_3_boolean_difference.snap.svg new file mode 100644 index 00000000..c94d55bc --- /dev/null +++ b/tests/kicad-parity/__snapshots__/to220h_3_boolean_difference.snap.svg @@ -0,0 +1,7 @@ +TO-220-3_Horizontal_TabDown - Alignment Analysis (Footprinter vs KiCad)to220h_3KiCad: TO-220-3_Horizontal_TabDownPerfect alignment = complete overlap \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/to220h_3_tabup.snap.svg b/tests/kicad-parity/__snapshots__/to220h_3_tabup.snap.svg new file mode 100644 index 00000000..4de9ef8c --- /dev/null +++ b/tests/kicad-parity/__snapshots__/to220h_3_tabup.snap.svg @@ -0,0 +1 @@ +{REF}Diff: 0.00% \ No newline at end of file diff --git a/tests/kicad-parity/__snapshots__/to220h_3_tabup_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/to220h_3_tabup_boolean_difference.snap.svg new file mode 100644 index 00000000..f9dbbb1c --- /dev/null +++ b/tests/kicad-parity/__snapshots__/to220h_3_tabup_boolean_difference.snap.svg @@ -0,0 +1,7 @@ +TO-220-3_Horizontal_TabUp - Alignment Analysis (Footprinter vs KiCad)to220h_3_tabupKiCad: TO-220-3_Horizontal_TabUpPerfect alignment = complete overlap \ No newline at end of file diff --git a/tests/kicad-parity/to220h_3_kicad_parity1.test.ts b/tests/kicad-parity/to220h_3_kicad_parity1.test.ts new file mode 100644 index 00000000..b75b2220 --- /dev/null +++ b/tests/kicad-parity/to220h_3_kicad_parity1.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test" +import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" + +test("parity/to220h_3 (TabDown)", async () => { + const { combinedFootprintElements, booleanDifferenceSvg } = + await compareFootprinterVsKicad( + "to220h_3", + "Package_TO_SOT_THT.pretty/TO-220-3_Horizontal_TabDown.circuit.json", + ) + + const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements, { + showCourtyards: true, + }) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3") + expect(booleanDifferenceSvg).toMatchSvgSnapshot( + import.meta.path, + "to220h_3_boolean_difference", + ) +}) diff --git a/tests/kicad-parity/to220h_3_kicad_parity2.test.ts b/tests/kicad-parity/to220h_3_kicad_parity2.test.ts new file mode 100644 index 00000000..a52de644 --- /dev/null +++ b/tests/kicad-parity/to220h_3_kicad_parity2.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test" +import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" + +test("parity/to220h_3_tabup (TabUp)", async () => { + const { combinedFootprintElements, booleanDifferenceSvg } = + await compareFootprinterVsKicad( + "to220h_3_tabup", + "Package_TO_SOT_THT.pretty/TO-220-3_Horizontal_TabUp.circuit.json", + ) + + const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements, { + showCourtyards: true, + }) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3_tabup") + expect(booleanDifferenceSvg).toMatchSvgSnapshot( + import.meta.path, + "to220h_3_tabup_boolean_difference", + ) +}) diff --git a/tests/to220h.test.ts b/tests/to220h.test.ts new file mode 100644 index 00000000..7c5ebb0f --- /dev/null +++ b/tests/to220h.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "src/footprinter" + +test("to220h_3 (tab down, default)", () => { + const circuitJson = fp.string("to220h_3").circuitJson() + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(circuitJson).toBeDefined() + expect(circuitJson.length).toBeGreaterThan(0) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3") +}) + +test("to220h_3_tabup", () => { + const circuitJson = fp.string("to220h_3_tabup").circuitJson() + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3_tabup") +}) + +test("TO-220-3_Horizontal_TabDown (alias → to220h_3)", () => { + const aliasSvg = convertCircuitJsonToPcbSvg( + fp.string("TO-220-3_Horizontal_TabDown").circuitJson(), + ) + const canonicalSvg = convertCircuitJsonToPcbSvg( + fp.string("to220h_3").circuitJson(), + ) + expect(aliasSvg).toEqual(canonicalSvg) +}) + +test("TO-220-3_Horizontal_TabUp (alias → to220h_3_tabup)", () => { + const aliasSvg = convertCircuitJsonToPcbSvg( + fp.string("TO-220-3_Horizontal_TabUp").circuitJson(), + ) + const canonicalSvg = convertCircuitJsonToPcbSvg( + fp.string("to220h_3_tabup").circuitJson(), + ) + expect(aliasSvg).toEqual(canonicalSvg) +}) diff --git a/tests/utdfn4ep.test.ts b/tests/utdfn4ep.test.ts new file mode 100644 index 00000000..42fba758 --- /dev/null +++ b/tests/utdfn4ep.test.ts @@ -0,0 +1,20 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("utdfn4ep", () => { + const circuitJson = fp.string("utdfn4ep").circuitJson() + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn4ep") +}) + +test("utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm", () => { + const circuitJson = fp + .string("utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm") + .circuitJson() + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot( + import.meta.path, + "utdfn4ep_pw0.3mm_ph0.5mm_epw0.55mm_eph0.45mm", + ) +})