Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/fn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ export { sot343 } from "./sot343"
export { m2host } from "./m2host"
export { mountedpcbmodule } from "./mountedpcbmodule"
export { to92l } from "./to92l"
export { to220h } from "./to220h"
146 changes: 146 additions & 0 deletions src/fn/to220h.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {
type AnyCircuitElement,
type PcbSilkscreenPath,
length,
} from "circuit-json"
import { platedhole } from "src/helpers/platedhole"
import { rectpad } from "src/helpers/rectpad"
import { z } from "zod"
import { type SilkscreenRef, silkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"

/**
* TO-220 Horizontal variant footprint.
*
* Matches KiCad TO-220-3_Horizontal_TabDown / TO-220-3_Horizontal_TabUp.
*
* Signal pins emerge horizontally in a row; the mounting tab pad extends
* either downward (tabdown, default) or upward (tabup).
*
* Parameters:
* tabup – tab extends toward +Y (default: false → TabDown, tab toward -Y)
* p – pitch between signal pins (default 2.54mm)
* id – signal pin drill diameter (default 1.0mm)
* od – signal pin annular ring outer diameter (default 1.8mm)
* w – body width (default 10.16mm, KiCad standard)
* h – body height (default 4.0mm, KiCad standard)
* tabid – tab mounting hole drill diameter (default 3.2mm)
* tabod – tab mounting hole outer diameter (default 5.4mm)
* tabw – tab SMT pad width (default 6.6mm)
* tabh – tab SMT pad height (default 4.6mm)
*/
export const to220h_def = base_def.extend({
fn: z.string(),
tabup: z.boolean().optional().default(false),
p: length.optional().default("2.54mm"),
id: length.optional().default("1.0mm"),
od: length.optional().default("1.8mm"),
w: length.optional().default("10.16mm"),
h: length.optional().default("4.0mm"),
tabid: length.optional().default("3.2mm"),
tabod: length.optional().default("5.4mm"),
tabw: length.optional().default("6.6mm"),
tabh: length.optional().default("4.6mm"),
num_pins: z.number().optional(),
string: z.string().optional(),
})

export type To220hDef = z.input<typeof to220h_def>

export const to220h = (
raw_params: To220hDef,
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const parameters = to220h_def.parse(raw_params)
const { tabup, p, id, od, w, h, tabid, tabod, tabw, tabh, string } =
parameters

const numPins =
parameters.num_pins ??
Number.parseInt(string?.match(/^to220h(?:_|-)(\d+)/i)?.[1] ?? "3")

// Signal pins in a horizontal row at y = 0
const signalPins: AnyCircuitElement[] = Array.from(
{ length: numPins },
(_, i) => {
const x =
numPins % 2 === 0
? (i - numPins / 2 + 0.5) * p
: (i - Math.floor(numPins / 2)) * p
return platedhole(i + 1, x, 0, id, od)
},
)

// Tab pad: large rectangular SMT pad with mounting hole
// TabDown: tab extends below pins (negative Y); TabUp: above pins (positive Y)
const tabSign = tabup ? 1 : -1
const tabCenterY = tabSign * (h / 2 + tabh / 2 + 0.5)

// Mounting hole in the tab (through-hole, no electrical connection – pin "tab")
const tabHole: AnyCircuitElement = platedhole(
numPins + 1,
0,
tabCenterY,
tabid,
tabod,
)

// Silkscreen body outline
const halfW = w / 2
const pinsTop = od / 2 + 0.3
const pinsBottom = -(od / 2 + 0.3)

// Draw an outline around the body region (between pins and tab)
const bodyBottom = tabSign > 0 ? pinsBottom : tabCenterY - tabh / 2 - 0.3
const bodyTop = tabSign > 0 ? tabCenterY + tabh / 2 + 0.3 : pinsTop

const silkBody: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "",
route: [
{ x: -halfW, y: pinsTop },
{ x: halfW, y: pinsTop },
{ x: halfW, y: pinsBottom },
{ x: -halfW, y: pinsBottom },
{ x: -halfW, y: pinsTop },
],
stroke_width: 0.1,
}
Comment on lines +93 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables bodyBottom and bodyTop are calculated but never used. The silkBody silkscreen path uses pinsTop and pinsBottom instead, causing the body outline to always draw around just the pins regardless of tab direction. This contradicts the comment stating it should draw "around the body region (between pins and tab)".

Fix:

const silkBody: PcbSilkscreenPath = {
  type: "pcb_silkscreen_path",
  layer: "top",
  pcb_component_id: "",
  pcb_silkscreen_path_id: "",
  route: [
    { x: -halfW, y: bodyTop },
    { x: halfW, y: bodyTop },
    { x: halfW, y: bodyBottom },
    { x: -halfW, y: bodyBottom },
    { x: -halfW, y: bodyTop },
  ],
  stroke_width: 0.1,
}
Suggested change
const bodyBottom = tabSign > 0 ? pinsBottom : tabCenterY - tabh / 2 - 0.3
const bodyTop = tabSign > 0 ? tabCenterY + tabh / 2 + 0.3 : pinsTop
const silkBody: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "",
route: [
{ x: -halfW, y: pinsTop },
{ x: halfW, y: pinsTop },
{ x: halfW, y: pinsBottom },
{ x: -halfW, y: pinsBottom },
{ x: -halfW, y: pinsTop },
],
stroke_width: 0.1,
}
const bodyBottom = tabSign > 0 ? pinsBottom : tabCenterY - tabh / 2 - 0.3
const bodyTop = tabSign > 0 ? tabCenterY + tabh / 2 + 0.3 : pinsTop
const silkBody: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "",
route: [
{ x: -halfW, y: bodyTop },
{ x: halfW, y: bodyTop },
{ x: halfW, y: bodyBottom },
{ x: -halfW, y: bodyBottom },
{ x: -halfW, y: bodyTop },
],
stroke_width: 0.1,
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


// Tab outline
const tabLeft = -(tabw / 2)
const tabRight = tabw / 2
const tabTop = tabCenterY + tabh / 2
const tabBot = tabCenterY - tabh / 2

const silkTab: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "",
route: [
{ x: tabLeft, y: tabTop },
{ x: tabRight, y: tabTop },
{ x: tabRight, y: tabBot },
{ x: tabLeft, y: tabBot },
{ x: tabLeft, y: tabTop },
],
stroke_width: 0.1,
}

// Reference designator text: place opposite side from tab
const refY = tabSign > 0 ? pinsBottom - 0.8 : pinsTop + 0.8
const silkscreenRefText: SilkscreenRef = silkscreenRef(0, refY, 0.5)

return {
circuitJson: [
...signalPins,
tabHole,
silkBody,
silkTab,
silkscreenRefText as AnyCircuitElement,
],
parameters: { ...parameters, p, num_pins: numPins },
}
}
8 changes: 8 additions & 0 deletions src/footprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export type Footprinter = {
hc49: () => FootprinterParamsBuilder<"p" | "id" | "od" | "w" | "h">
to220: () => FootprinterParamsBuilder<"w" | "h" | "p" | "id" | "od">
to220f: () => FootprinterParamsBuilder<"w" | "h" | "p" | "id" | "od">
to220h: () => FootprinterParamsBuilder<
"w" | "h" | "p" | "id" | "od" | "tabup" | "tabid" | "tabod" | "tabw" | "tabh"
>
sot363: () => FootprinterParamsBuilder<"w" | "h" | "p" | "pl" | "pw">
sot886: () => FootprinterParamsBuilder<"w" | "h" | "p" | "pl" | "pw">
sot457: () => FootprinterParamsBuilder<
Expand Down Expand Up @@ -271,6 +274,11 @@ const normalizeDefinition = (def: string): string => {
.trim()
.replace(/^sot-223-(\d+)(?=_|$)/i, "sot223_$1")
.replace(/^to-220f-(\d+)(?=_|$)/i, "to220f_$1")
.replace(/^to-220-(\d+)_horizontal(?:_tabup)?(?=_|$)/i, (m, n) =>
m.toLowerCase().includes("tabup")
? `to220h_${n}_tabup`
: `to220h_${n}`,
)
}

export const string = (def: string): Footprinter => {
Expand Down
1 change: 1 addition & 0 deletions tests/__snapshots__/to220h_2.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/__snapshots__/to220h_3.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/__snapshots__/to220h_3_tabup.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions tests/to220h.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { test, expect } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "src/footprinter"

test("to220h_3 (3 pins, tab down, default)", () => {
const circuitJson = fp.string("to220h_3").circuitJson()
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)

expect(circuitJson).toBeDefined()
expect(circuitJson.length).toBeGreaterThan(0)

// Should have 3 signal plated holes + 1 tab hole
const holes = circuitJson.filter((e: any) => e.type === "pcb_plated_hole")
expect(holes).toHaveLength(4)

expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3")
})

test("to220h_3_tabup (3 pins, tab up)", () => {
const circuitJson = fp.string("to220h_3_tabup").circuitJson()
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)

expect(circuitJson).toBeDefined()
expect(circuitJson.length).toBeGreaterThan(0)

expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_3_tabup")
})

test("to220h_2 (2 pins, tab down)", () => {
const circuitJson = fp.string("to220h_2").circuitJson()
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)

expect(circuitJson).toBeDefined()
expect(circuitJson.length).toBeGreaterThan(0)

expect(svgContent).toMatchSvgSnapshot(import.meta.path, "to220h_2")
})

test("TO-220-3_Horizontal (KiCad alias)", () => {
const circuitJson = fp.string("TO-220-3_Horizontal").circuitJson()

expect(circuitJson).toBeDefined()
expect(circuitJson.length).toBeGreaterThan(0)

// Must match the canonical to220h_3 output
const canonical = fp.string("to220h_3").circuitJson()
expect(JSON.stringify(circuitJson)).toEqual(JSON.stringify(canonical))
})

test("TO-220-3_Horizontal_TabUp (KiCad alias)", () => {
const circuitJson = fp.string("TO-220-3_Horizontal_TabUp").circuitJson()

expect(circuitJson).toBeDefined()
expect(circuitJson.length).toBeGreaterThan(0)

const canonical = fp.string("to220h_3_tabup").circuitJson()
expect(JSON.stringify(circuitJson)).toEqual(JSON.stringify(canonical))
})
Comment on lines +1 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file contains 5 test() calls (lines 5, 19, 29, 39, and 50), which violates the rule that a *.test.ts file may have AT MOST one test(...). After the first test, the user should split into multiple, numbered files. This file should be split into separate files like to220h1.test.ts, to220h2.test.ts, to220h3.test.ts, to220h4.test.ts, and to220h5.test.ts, with each file containing only one test() call.

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Loading