diff --git a/src/fn/to220.ts b/src/fn/to220.ts index 3cbe0a86..c0ec8c61 100644 --- a/src/fn/to220.ts +++ b/src/fn/to220.ts @@ -4,6 +4,7 @@ import { type PcbSilkscreenPath, length, } from "circuit-json" +import { mm } from "@tscircuit/mm" import { platedhole } from "src/helpers/platedhole" import { z } from "zod" import { type SilkscreenRef, silkscreenRef } from "../helpers/silkscreenRef" @@ -11,7 +12,7 @@ import { base_def } from "../helpers/zod/base_def" export const to220_def = base_def.extend({ fn: z.string(), - p: length.optional().default("5.0mm"), + p: length.optional().default("2.54mm"), id: length.optional().default("1.0mm"), od: length.optional().default("1.9mm"), w: length.optional().default("13mm"), @@ -36,15 +37,15 @@ export const to220 = ( const halfWidth = w / 2 const halfHeight = h / 2 - const minPitch = 2.5 - const maxHoleWidth = w * 0.4 - const computedPitch = Math.max(minPitch, maxHoleWidth / (numPins - 1)) + // TO-220 lead pitch is fixed (JEDEC: 0.1in = 2.54mm) and must not scale with + // the plastic body width; `p` keeps the pitch explicit and overridable. + const pitch = mm(parameters.p) const plated_holes = Array.from({ length: numPins }, (_, i) => { const x = numPins % 2 === 0 - ? (i - numPins / 2 + 0.5) * computedPitch - : (i - Math.floor(numPins / 2)) * computedPitch + ? (i - numPins / 2 + 0.5) * pitch + : (i - Math.floor(numPins / 2)) * pitch return platedhole(i + 1, x, holeY, id, od) }) @@ -135,6 +136,6 @@ export const to220 = ( silkscreenRefText as AnyCircuitElement, courtyard, ], - parameters: { ...parameters, p: computedPitch }, + parameters: { ...parameters, p: pitch }, } } diff --git a/tests/__snapshots__/to220_2.snap.svg b/tests/__snapshots__/to220_2.snap.svg index 7844a8e6..bb65e0a7 100644 --- a/tests/__snapshots__/to220_2.snap.svg +++ b/tests/__snapshots__/to220_2.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/to220_3.snap.svg b/tests/__snapshots__/to220_3.snap.svg index 8a345fa7..ce43eb9a 100644 --- a/tests/__snapshots__/to220_3.snap.svg +++ b/tests/__snapshots__/to220_3.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/to220_4.snap.svg b/tests/__snapshots__/to220_4.snap.svg index e75600a9..af4cdcaa 100644 --- a/tests/__snapshots__/to220_4.snap.svg +++ b/tests/__snapshots__/to220_4.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/to220_5.snap.svg b/tests/__snapshots__/to220_5.snap.svg index 45c1d5ad..c8d5cca7 100644 --- a/tests/__snapshots__/to220_5.snap.svg +++ b/tests/__snapshots__/to220_5.snap.svg @@ -1 +1 @@ -{REF} \ No newline at end of file +{REF} \ No newline at end of file diff --git a/tests/to220-pitch.test.ts b/tests/to220-pitch.test.ts new file mode 100644 index 00000000..b04ee013 --- /dev/null +++ b/tests/to220-pitch.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "bun:test" +import { fp } from "src/footprinter" +import { to220 } from "src/fn/to220" + +/** + * TO-220 lead pitch is fixed by JEDEC at 0.1in (2.54mm). It used to be derived + * from the plastic body width (`max(2.5, w * 0.4 / (numPins - 1))` → 2.6mm at + * the default w=13mm), and the declared `p` parameter (default "5.0mm") was + * parsed but never read. + * + * Issue #790: pitch must be the standard 2.54mm regardless of body width, with + * `p` honored as an explicit override — matching the sibling `to220f`, which + * already hardcodes 2.54mm "to match KiCad". + */ + +const holeXs = (cj: any[]) => + cj + .filter((e: any) => e.type === "pcb_plated_hole") + .map((e: any) => e.x) + .sort((a, b) => a - b) + +const gaps = (xs: number[]) => + xs.slice(1).map((x, i) => Math.round((x - xs[i]!) * 1e6) / 1e6) + +test("to220_3 uses the standard 2.54mm JEDEC pitch by default", () => { + const xs = holeXs(fp.string("to220_3").circuitJson()) + expect(xs).toHaveLength(3) + expect(gaps(xs)).toEqual([2.54, 2.54]) +}) + +test("declared p parameter overrides the pitch", () => { + const xs = holeXs( + to220({ fn: "to220", num_pins: 3, p: "5mm" }).circuitJson as any, + ) + expect(gaps(xs)).toEqual([5, 5]) +}) + +test("pitch does not scale with body width", () => { + const xs = holeXs( + to220({ fn: "to220", num_pins: 3, w: "25.4mm" }).circuitJson as any, + ) + expect(gaps(xs)).toEqual([2.54, 2.54]) +}) + +test("even pin counts stay centered at the standard pitch", () => { + const xs = holeXs(fp.string("to220_4").circuitJson()) + expect(xs).toHaveLength(4) + expect((xs[0]! + xs[xs.length - 1]!) / 2).toBeCloseTo(0, 9) + expect(gaps(xs)).toEqual([2.54, 2.54, 2.54]) +}) + +test("5-pin variant uses the standard pitch", () => { + const xs = holeXs(fp.string("to220_5").circuitJson()) + expect(gaps(xs)).toEqual([2.54, 2.54, 2.54, 2.54]) +}) + +test("reported parameters reflect the effective pitch", () => { + const { parameters } = to220({ fn: "to220", num_pins: 3 }) + expect(parameters.p).toBe(2.54) +})