diff --git a/src/fn/index.ts b/src/fn/index.ts index 94c0370e..9480466f 100644 --- a/src/fn/index.ts +++ b/src/fn/index.ts @@ -81,3 +81,4 @@ export { sot343 } from "./sot343" export { m2host } from "./m2host" export { mountedpcbmodule } from "./mountedpcbmodule" export { to92l } from "./to92l" +export { tripad } from "./tripad" diff --git a/src/fn/tripad.ts b/src/fn/tripad.ts new file mode 100644 index 00000000..7aa9a2de --- /dev/null +++ b/src/fn/tripad.ts @@ -0,0 +1,117 @@ +import { z } from "zod" +import { rectpad } from "../helpers/rectpad" +import { silkscreenRef } from "../helpers/silkscreenRef" +import type { AnyCircuitElement, PcbSilkscreenPath } from "circuit-json" +import { base_def } from "../helpers/zod/base_def" + +/** + * Tripad footprint generator + * + * A tripad is a 3-pad SMD configuration commonly used for transistors, MOSFETs, + * and other 3-terminal devices. It features two smaller signal pads on one side + * and a larger pad (often a thermal/tab pad) on the opposite side. + * + * Default dimensions are based on common tripad footprints found in the wild + * (similar to TO-252 / D-PAK style tripad SMD footprints): + * - w: overall width (pad center-to-center X span + pad widths) + * - h: overall height (body height) + * - p: pitch between the two small pads (Y axis) + * - pw: small pad width (X dimension) + * - ph: small pad height (Y dimension) + * - lw: large pad width (X dimension) + * - lh: large pad height (Y dimension) + * + * Pin numbering (CCW from bottom-left): + * Pin 1: top-left small pad + * Pin 2: large pad (right side) — often drain/tab + * Pin 3: bottom-left small pad + * + * Example string: "tripad", "tripad_w4_h4_p2.3" + */ +export const tripad_def = base_def.extend({ + fn: z.string(), + w: z.string().default("4.7mm"), + h: z.string().default("4.0mm"), + p: z.string().default("2.3mm"), + pw: z.string().default("1.5mm"), + ph: z.string().default("1.5mm"), + lw: z.string().default("2.5mm"), + lh: z.string().default("3.5mm"), + string: z.string().optional(), +}) + +export const tripad = ( + raw_params: z.input, +): { circuitJson: AnyCircuitElement[]; parameters: any } => { + const parameters = tripad_def.parse(raw_params) + + const w = Number.parseFloat(parameters.w) + const h = Number.parseFloat(parameters.h) + const p = Number.parseFloat(parameters.p) + const pw = Number.parseFloat(parameters.pw) + const ph = Number.parseFloat(parameters.ph) + const lw = Number.parseFloat(parameters.lw) + const lh = Number.parseFloat(parameters.lh) + + // Two small pads on the left side, large pad on the right + // The span from small pad centers to large pad center equals w/2 + const smallPadX = -(w / 2 - pw / 2) + const largePadX = w / 2 - lw / 2 + + const pads: AnyCircuitElement[] = [ + // Pin 1: top-left small pad + rectpad(1, smallPadX, p / 2, pw, ph), + // Pin 2: large pad (right side, thermal/tab) + rectpad(2, largePadX, 0, lw, lh), + // Pin 3: bottom-left small pad + rectpad(3, smallPadX, -p / 2, pw, ph), + ] + + // Silkscreen outline + const silkPadding = 0.2 + const bodyLeft = smallPadX - pw / 2 - silkPadding + const bodyRight = largePadX + lw / 2 + silkPadding + const bodyTop = h / 2 + const bodyBottom = -h / 2 + + const silkscreenOutline: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "silkscreen_outline", + route: [ + { x: bodyLeft, y: bodyTop }, + { x: bodyRight, y: bodyTop }, + { x: bodyRight, y: bodyBottom }, + { x: bodyLeft, y: bodyBottom }, + { x: bodyLeft, y: bodyTop }, + ], + stroke_width: 0.1, + } + + // Pin 1 indicator: notch at top-left corner + const notchSize = 0.4 + const pin1Indicator: PcbSilkscreenPath = { + type: "pcb_silkscreen_path", + layer: "top", + pcb_component_id: "", + pcb_silkscreen_path_id: "pin1_indicator", + route: [ + { x: bodyLeft, y: bodyTop - notchSize }, + { x: bodyLeft + notchSize, y: bodyTop }, + ], + stroke_width: 0.1, + } + + const silk = silkscreenRef(0, bodyTop + 0.4, 0.3) + + return { + circuitJson: [ + ...pads, + silkscreenOutline, + pin1Indicator, + silk as AnyCircuitElement, + ], + parameters, + } +} diff --git a/src/footprinter.ts b/src/footprinter.ts index 6af68a44..3a52c2e4 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -257,6 +257,9 @@ export type Footprinter = { solderjumper: ( num_pins?: number, ) => FootprinterParamsBuilder<"bridged" | "p" | "pw" | "ph"> + tripad: () => FootprinterParamsBuilder< + "w" | "h" | "p" | "pw" | "ph" | "lw" | "lh" + > params: () => any /** @deprecated use circuitJson() instead */ diff --git a/tests/__snapshots__/tripad_custom_call.snap.svg b/tests/__snapshots__/tripad_custom_call.snap.svg new file mode 100644 index 00000000..ff60b983 --- /dev/null +++ b/tests/__snapshots__/tripad_custom_call.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/tripad_custom_dimensions.snap.svg b/tests/__snapshots__/tripad_custom_dimensions.snap.svg new file mode 100644 index 00000000..05f61c3a --- /dev/null +++ b/tests/__snapshots__/tripad_custom_dimensions.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/tripad_default.snap.svg b/tests/__snapshots__/tripad_default.snap.svg new file mode 100644 index 00000000..4a66d5fa --- /dev/null +++ b/tests/__snapshots__/tripad_default.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/tripad1.test.ts b/tests/tripad1.test.ts new file mode 100644 index 00000000..cd1f9565 --- /dev/null +++ b/tests/tripad1.test.ts @@ -0,0 +1,11 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("tripad default", () => { + const circuitJson = fp.string("tripad").circuitJson() + const pads = circuitJson.filter((e) => e.type === "pcb_smtpad") + expect(pads.length).toBe(3) + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "tripad_default") +}) diff --git a/tests/tripad2.test.ts b/tests/tripad2.test.ts new file mode 100644 index 00000000..b2365b2a --- /dev/null +++ b/tests/tripad2.test.ts @@ -0,0 +1,14 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("tripad custom dimensions", () => { + const circuitJson = fp.string("tripad_w5_h4.5_p2.54").circuitJson() + const pads = circuitJson.filter((e) => e.type === "pcb_smtpad") + expect(pads.length).toBe(3) + const svgContent = convertCircuitJsonToPcbSvg(circuitJson) + expect(svgContent).toMatchSvgSnapshot( + import.meta.path, + "tripad_custom_dimensions", + ) +}) diff --git a/tests/tripad3.test.ts b/tests/tripad3.test.ts new file mode 100644 index 00000000..d99db2d8 --- /dev/null +++ b/tests/tripad3.test.ts @@ -0,0 +1,36 @@ +import { test, expect } from "bun:test" +import { tripad } from "../src/fn/tripad" + +test("tripad pin numbering", () => { + const result = tripad({ fn: "tripad" }) + const pads = result.circuitJson.filter((e) => e.type === "pcb_smtpad") + expect(pads.length).toBe(3) + + // Pin 1 is top-left small pad (highest y) + const pin1 = pads.find( + (e) => e.type === "pcb_smtpad" && e.port_hints?.includes("1"), + ) + const pin2 = pads.find( + (e) => e.type === "pcb_smtpad" && e.port_hints?.includes("2"), + ) + const pin3 = pads.find( + (e) => e.type === "pcb_smtpad" && e.port_hints?.includes("3"), + ) + + expect(pin1).toBeDefined() + expect(pin2).toBeDefined() + expect(pin3).toBeDefined() + + // Pin 1 and 3 are on the left (negative x), pin 2 is on the right (positive x) + if (pin1 && pin2 && pin3) { + expect((pin1 as any).x).toBeLessThan((pin2 as any).x) + expect((pin3 as any).x).toBeLessThan((pin2 as any).x) + // Pin 1 is above pin 3 + expect((pin1 as any).y).toBeGreaterThan((pin3 as any).y) + // Pin 2 (large pad) is centered vertically + expect((pin2 as any).y).toBe(0) + // Large pad is bigger than small pads + expect((pin2 as any).width).toBeGreaterThan((pin1 as any).width) + expect((pin2 as any).height).toBeGreaterThan((pin1 as any).height) + } +}) diff --git a/tests/tripad4.test.ts b/tests/tripad4.test.ts new file mode 100644 index 00000000..b57fe857 --- /dev/null +++ b/tests/tripad4.test.ts @@ -0,0 +1,11 @@ +import { test, expect } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { tripad } from "../src/fn/tripad" + +test("tripad direct call with custom params", () => { + const result = tripad({ fn: "tripad", w: "6mm", h: "5mm", p: "3mm" }) + const pads = result.circuitJson.filter((e) => e.type === "pcb_smtpad") + expect(pads.length).toBe(3) + const svgContent = convertCircuitJsonToPcbSvg(result.circuitJson) + expect(svgContent).toMatchSvgSnapshot(import.meta.path, "tripad_custom_call") +})