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 @@ -{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..4894c1a6 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..02387ea1 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..7f095858 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/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 @@ +{REF}REF**Diff: 0.00% \ 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 @@ +TO-220-3_Vertical - Alignment Analysis (Footprinter vs KiCad)to220_3KiCad: TO-220-3_VerticalPerfect alignment = complete overlap \ 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 @@ +{REF}REF**Diff: 0.00% \ 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 @@ +TO-220-4_Vertical - Alignment Analysis (Footprinter vs KiCad)to220_4KiCad: TO-220-4_VerticalPerfect alignment = complete overlap \ 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", + ) +})