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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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)
+})