diff --git a/README.md b/README.md index fa6d47f6..08ab8340 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Here are some example footprinter strings: cap0402 res0805 soic8_p1.27mm +soic8_package(D0008A) dip16 pinrow10 pinrow6_rows2_cols5_p2.54mm_py5.08mm_missing(3,4,8,9) @@ -33,6 +34,18 @@ do219ad sod323he ``` +For the TI D0008A SOIC-8 package (for example, TLC555CD), use `soic8_package(D0008A)` +or `fp().soic(8).package("D0008A")`. Its default copper lands follow the example board layout +in the [TLC555 datasheet](https://www.ti.com/lit/ds/symlink/tlc555.pdf), +drawing 4214825/C: 1.55 × 0.60 mm pads, 1.27 mm pitch, 5.40 mm between +pad-row centers, and 0.05 mm corner radius. It reuses the SOIC silkscreen +and courtyard generator; it does not define solder-mask or paste openings. +Explicit dimensions override package defaults; for example, +`soic8_package(D0008A)_pl1.6mm`. The package option requires eight pins. +Use generic `soic8` with explicit dimensions for other land patterns. +Existing `soic8` defaults are unchanged. Common options such as +`soic8_package(D0008A)_norefdes` and `soic8_package(D0008A)_pin1location(topside,left)` also work. + You can use these like so: ```tsx diff --git a/src/fn/soic.ts b/src/fn/soic.ts index fa3488e0..e778dbaf 100644 --- a/src/fn/soic.ts +++ b/src/fn/soic.ts @@ -64,7 +64,38 @@ export const extendSoicDef = (newDefaults: { > }) -export const soic_def = extendSoicDef({}) +const soicPackage = z.preprocess( + (value) => + typeof value === "string" + ? value.replace(/^\((.*)\)$/, "$1").toUpperCase() + : value, + z.literal("D0008A").optional(), +) + +export const soic_def = z.preprocess( + (raw) => { + if (!raw || typeof raw !== "object") return raw + const input = raw as Record + const selectedPackage = soicPackage.safeParse(input.package) + if (!selectedPackage.success || selectedPackage.data !== "D0008A") + return raw + // TI drawing 4214825/C, example board layout: + // https://www.ti.com/lit/ds/symlink/tlc555.pdf + // w is the outer copper span: 5.4 mm row spacing + 1.55 mm pad length. + return { w: 6.95, p: 1.27, pl: 1.55, pw: 0.6, ...input } + }, + extendSoicDef({}) + .and(z.object({ package: soicPackage })) + .superRefine((value, ctx) => { + if (value.package === "D0008A" && value.num_pins !== 8) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["num_pins"], + message: "The TI D0008A package requires 8 pins", + }) + } + }), +) export type SoicInput = z.infer export const getCcwSoicCoords = (parameters: { @@ -111,6 +142,7 @@ export const soic = (raw_params: { num_pins: number w: number p?: number + package?: string id?: string | number od?: string | number }): { circuitJson: AnyCircuitElement[]; parameters: SoicInput } => { @@ -123,7 +155,10 @@ export const soic = (raw_params: { export const soicWithoutParsing = (parameters: z.infer) => { const pads: AnyCircuitElement[] = [] - const cornerRadius = Math.min(parameters.pl, parameters.pw) / 8 + const cornerRadius = + parameters.package === "D0008A" + ? Math.min(0.05, parameters.pl / 2, parameters.pw / 2) + : Math.min(parameters.pl, parameters.pw) / 8 let maxPadExtentX = 0 let maxPadExtentY = 0 for (let i = 0; i < parameters.num_pins; i++) { diff --git a/src/footprinter.ts b/src/footprinter.ts index 2c60ae78..5daa3f2e 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -167,6 +167,7 @@ export type Footprinter = { soic: ( num_pins?: number, ) => FootprinterParamsBuilder< + | "package" | "w" | "p" | "pw" diff --git a/tests/__snapshots__/soic8-ti-d0008a-rightside-norefdes.snap.svg b/tests/__snapshots__/soic8-ti-d0008a-rightside-norefdes.snap.svg new file mode 100644 index 00000000..3bcb67ea --- /dev/null +++ b/tests/__snapshots__/soic8-ti-d0008a-rightside-norefdes.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/__snapshots__/soic8-ti-d0008a.snap.svg b/tests/__snapshots__/soic8-ti-d0008a.snap.svg new file mode 100644 index 00000000..cab54997 --- /dev/null +++ b/tests/__snapshots__/soic8-ti-d0008a.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/soic-ti-d0008a.test.ts b/tests/soic-ti-d0008a.test.ts new file mode 100644 index 00000000..3f0dc1f3 --- /dev/null +++ b/tests/soic-ti-d0008a.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { fp } from "../src/footprinter" + +test("soic8_package(D0008A) matches TI drawing 4214825/C example copper lands", () => { + const circuit = fp.string("soic8_package(D0008A)").circuitJson() + const pads = circuit.filter((el) => el.type === "pcb_smtpad") + // Independent top-view coordinates from TI's 5.4 mm row spacing and + // 1.27 mm pitch, with counterclockwise numbering starting at top left. + const expected = [ + [-2.7, 1.905], + [-2.7, 0.635], + [-2.7, -0.635], + [-2.7, -1.905], + [2.7, -1.905], + [2.7, -0.635], + [2.7, 0.635], + [2.7, 1.905], + ] + expect(pads).toHaveLength(8) + pads.forEach((pad, i) => { + expect(pad.shape).toBe("rect") + if (pad.shape !== "rect") throw new Error("Expected rectangular pad") + expect(pad.port_hints).toEqual([String(i + 1)]) + expect(pad.layer).toBe("top") + expect(pad.x).toBeCloseTo(expected[i]![0]!, 6) + expect(pad.y).toBeCloseTo(expected[i]![1]!, 6) + expect(pad.width).toBeCloseTo(1.55, 6) + expect(pad.height).toBeCloseTo(0.6, 6) + expect(pad.corner_radius).toBeCloseTo(0.05, 6) + }) + expect(convertCircuitJsonToPcbSvg(circuit)).toMatchSvgSnapshot( + import.meta.path, + "soic8-ti-d0008a", + ) +}) + +test("soic8_package(D0008A) supports string and typed builder APIs", () => { + expect(fp().soic(8).package("D0008A").circuitJson()).toEqual( + fp.string("soic8_package(D0008A)").circuitJson(), + ) + expect(fp.string("soic8_package(D0008A)").json()).toMatchObject({ + fn: "soic", + package: "D0008A", + num_pins: 8, + w: 6.95, + p: 1.27, + pl: 1.55, + pw: 0.6, + }) + expect(() => fp.string("soic16_package(D0008A)").circuitJson()).toThrow( + "requires 8 pins", + ) + expect(() => fp.string("soic8_package(UNKNOWN)").circuitJson()).toThrow() + expect(fp.string("soic8_package(d0008a)").json()).toEqual( + fp.string("soic8_package(D0008A)").json(), + ) + for (const definition of [ + "soic8_package(D0008A)_pl1.6mm", + "soic8_pl1.6mm_package(D0008A)", + ]) { + expect(fp.string(definition).json()).toMatchObject({ + package: "D0008A", + pl: 1.6, + w: 6.95, + }) + } +}) + +test("soic8_package(D0008A) supports common footprint options", () => { + const circuit = fp() + .soic(8) + .package("D0008A") + .norefdes() + .pin1location("rightside", "bottom") + .circuitJson() + expect(circuit).toEqual( + fp + .string("soic8_package(D0008A)_norefdes_pin1location(rightside,bottom)") + .circuitJson(), + ) + expect( + circuit + .filter((el) => el.type === "pcb_silkscreen_text") + .every((el) => el.text === ""), + ).toBe(true) + const pin1 = circuit.find( + (el) => el.type === "pcb_smtpad" && el.port_hints?.includes("1"), + ) + expect(pin1).toMatchObject({ x: 2.7, y: -1.905 }) + expect(convertCircuitJsonToPcbSvg(circuit)).toMatchSvgSnapshot( + import.meta.path, + "soic8-ti-d0008a-rightside-norefdes", + ) +})