diff --git a/src/fn/to220.ts b/src/fn/to220.ts
index 3cbe0a86..61820cc8 100644
--- a/src/fn/to220.ts
+++ b/src/fn/to220.ts
@@ -4,16 +4,21 @@ import {
type PcbSilkscreenPath,
length,
} from "circuit-json"
-import { platedhole } from "src/helpers/platedhole"
+import { mm } from "@tscircuit/mm"
+import { platedHolePill } from "src/helpers/platedHolePill"
+import { platedHoleWithRectPad } from "src/helpers/platedHoleWithRectPad"
import { z } from "zod"
import { type SilkscreenRef, silkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"
export const to220_def = base_def.extend({
fn: z.string(),
- p: length.optional().default("5.0mm"),
- id: length.optional().default("1.0mm"),
- od: length.optional().default("1.9mm"),
+ // JEDEC TO-220 lead pitch is a fixed 0.1" (2.54mm), the same value the
+ // to220f sibling and KiCad TO-220-3_Vertical use.
+ p: length.optional().default("2.54mm"),
+ id: length.optional().default("1.1mm"),
+ od: length.optional().default("1.905mm"),
+ ph: length.optional().default("2mm"),
w: length.optional().default("13mm"),
h: length.optional().default("7mm"),
num_pins: z.number().optional(),
@@ -26,7 +31,7 @@ export const to220 = (
raw_params: To220Def,
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const parameters = to220_def.parse(raw_params)
- const { id, od, w, h, string } = parameters
+ const { p, id, od, ph, w, h, string } = parameters
const numPins =
parameters.num_pins ??
@@ -36,17 +41,29 @@ 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))
-
- 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
- return platedhole(i + 1, x, holeY, id, od)
- })
+ // Pins sit on the fixed 2.54mm pitch, not a value derived from the body
+ // width. Pin 1 gets a rectangular pad (polarity marker) and the rest are
+ // pill pads, mirroring KiCad's TO-220-3_Vertical land pattern.
+ const plated_holes: AnyCircuitElement[] = Array.from(
+ { length: numPins },
+ (_, i) => {
+ const x =
+ numPins % 2 === 0
+ ? (i - numPins / 2 + 0.5) * p
+ : (i - Math.floor(numPins / 2)) * p
+ if (i === 0) {
+ return platedHoleWithRectPad({
+ pn: 1,
+ x,
+ y: holeY,
+ holeDiameter: id,
+ rectPadWidth: od,
+ rectPadHeight: ph,
+ })
+ }
+ return platedHolePill(i + 1, x, holeY, mm(id), mm(od), mm(ph))
+ },
+ )
const silkscreenBody: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
@@ -103,7 +120,10 @@ export const to220 = (
const silkscreenRefText: SilkscreenRef = silkscreenRef(0, h / 2 + 0.6, 0.5)
const pinToeHalfSpanX =
- Math.max(...plated_holes.map((hole) => Math.abs(hole.x))) + od / 2
+ Math.max(
+ ...plated_holes.map((hole) => Math.abs((hole as { x: number }).x)),
+ ) +
+ od / 2
const pinToeTopY = holeY + od / 2
const pinToeBottomY = holeY - od / 2
const courtyardHalfWidth = Math.max(
@@ -135,6 +155,6 @@ export const to220 = (
silkscreenRefText as AnyCircuitElement,
courtyard,
],
- parameters: { ...parameters, p: computedPitch },
+ parameters: { ...parameters, p },
}
}
diff --git a/tests/__snapshots__/to220_2.snap.svg b/tests/__snapshots__/to220_2.snap.svg
index 7844a8e6..565e88f1 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..4894c1a6 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..02387ea1 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..7f095858 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/kicad-parity/__snapshots__/to220_3.snap.svg b/tests/kicad-parity/__snapshots__/to220_3.snap.svg
new file mode 100644
index 00000000..0538b537
--- /dev/null
+++ b/tests/kicad-parity/__snapshots__/to220_3.snap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/kicad-parity/__snapshots__/to220_3_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/to220_3_boolean_difference.snap.svg
new file mode 100644
index 00000000..ed5921af
--- /dev/null
+++ b/tests/kicad-parity/__snapshots__/to220_3_boolean_difference.snap.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/tests/kicad-parity/__snapshots__/to220_4.snap.svg b/tests/kicad-parity/__snapshots__/to220_4.snap.svg
new file mode 100644
index 00000000..b79fd915
--- /dev/null
+++ b/tests/kicad-parity/__snapshots__/to220_4.snap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/kicad-parity/__snapshots__/to220_4_boolean_difference.snap.svg b/tests/kicad-parity/__snapshots__/to220_4_boolean_difference.snap.svg
new file mode 100644
index 00000000..b437a00e
--- /dev/null
+++ b/tests/kicad-parity/__snapshots__/to220_4_boolean_difference.snap.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/tests/kicad-parity/to220_3_kicad_parity.test.ts b/tests/kicad-parity/to220_3_kicad_parity.test.ts
new file mode 100644
index 00000000..405368dc
--- /dev/null
+++ b/tests/kicad-parity/to220_3_kicad_parity.test.ts
@@ -0,0 +1,32 @@
+import { expect, test } from "bun:test"
+import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad"
+import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
+
+test("parity/to220_3", async () => {
+ const {
+ avgRelDiff,
+ combinedFootprintElements,
+ booleanDifferenceSvg,
+ courtyardDiffPercent,
+ } = await compareFootprinterVsKicad(
+ "to220_3",
+ "Package_TO_SOT_THT.pretty/TO-220-3_Vertical.circuit.json",
+ )
+
+ const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements, {
+ showCourtyards: true,
+ })
+ // Copper now matches KiCad exactly: standard 2.54mm lead pitch with a
+ // rectangular pin-1 pad and pill lead pads (previously plain circular holes
+ // on a body-width-derived 2.6mm pitch).
+ expect(avgRelDiff).toBeLessThan(0.01)
+ // Residual courtyard delta is the shared TO-220 body outline (also used by
+ // to220f, which matches KiCad TO-220F-3 within 0.5%); KiCad draws a slightly
+ // tighter courtyard for the metal-tab TO-220-3 variant.
+ expect(courtyardDiffPercent).toBeLessThan(13)
+ expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220_3")
+ expect(booleanDifferenceSvg).toMatchSvgSnapshot(
+ import.meta.path,
+ "to220_3_boolean_difference",
+ )
+})
diff --git a/tests/kicad-parity/to220_4_kicad_parity.test.ts b/tests/kicad-parity/to220_4_kicad_parity.test.ts
new file mode 100644
index 00000000..06be1460
--- /dev/null
+++ b/tests/kicad-parity/to220_4_kicad_parity.test.ts
@@ -0,0 +1,31 @@
+import { expect, test } from "bun:test"
+import { compareFootprinterVsKicad } from "../fixtures/compareFootprinterVsKicad"
+import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
+
+test("parity/to220_4", async () => {
+ const {
+ avgRelDiff,
+ combinedFootprintElements,
+ booleanDifferenceSvg,
+ courtyardDiffPercent,
+ } = await compareFootprinterVsKicad(
+ "to220_4",
+ "Package_TO_SOT_THT.pretty/TO-220-4_Vertical.circuit.json",
+ )
+
+ const svgContent = convertCircuitJsonToPcbSvg(combinedFootprintElements, {
+ showCourtyards: true,
+ })
+ // Same 2.54mm JEDEC grid as TO-220-3, one extra pin. Copper matches KiCad
+ // exactly (rectangular pin-1 pad plus pill lead pads on a 2.54mm pitch).
+ expect(avgRelDiff).toBeLessThan(0.01)
+ // Residual courtyard delta is the shared TO-220 body outline, identical to
+ // the to220_3 case: KiCad draws a slightly tighter courtyard for the
+ // metal-tab TO-220 than footprinter's single body courtyard.
+ expect(courtyardDiffPercent).toBeLessThan(13)
+ expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220_4")
+ expect(booleanDifferenceSvg).toMatchSvgSnapshot(
+ import.meta.path,
+ "to220_4_boolean_difference",
+ )
+})