Skip to content

feat: implement TO-220 horizontal variant (to220h)#535

Closed
victorjzq wants to merge 1 commit intotscircuit:mainfrom
victorjzq:feat/to220h-horizontal
Closed

feat: implement TO-220 horizontal variant (to220h)#535
victorjzq wants to merge 1 commit intotscircuit:mainfrom
victorjzq:feat/to220h-horizontal

Conversation

@victorjzq
Copy link
Contributor

Closes #185

Summary

  • Implements to220h footprint for the TO-220 Horizontal (lying-down) package variant, matching KiCad's TO-220-3_Horizontal_TabDown and TO-220-3_Horizontal_TabUp footprints
  • Signal pins are in a horizontal row at 2.54 mm pitch (standard)
  • Mounting tab is a large plated hole with a silkscreen outline
  • tabup parameter controls tab direction: false (default) = tab toward −Y (TabDown), true = tab toward +Y (TabUp)
  • String normalization handles KiCad-style aliases: TO-220-3_Horizontalto220h_3, TO-220-3_Horizontal_TabUpto220h_3_tabup
  • Supports arbitrary pin counts via to220h_N naming

Supported strings

String Description
to220h_3 3-pin, tab down (default)
to220h_3_tabup 3-pin, tab up
TO-220-3_Horizontal KiCad alias → to220h_3
TO-220-3_Horizontal_TabUp KiCad alias → to220h_3_tabup

Test plan

  • to220h_3 – 3 signal holes + 1 tab hole, SVG snapshot
  • to220h_3_tabup – tab in opposite direction, SVG snapshot
  • to220h_2 – 2-pin variant, SVG snapshot
  • TO-220-3_Horizontal alias round-trips to canonical output
  • TO-220-3_Horizontal_TabUp alias round-trips to canonical output
  • Existing to220 and to220f tests still pass
  • footprint-completeness test passes

Closes tscircuit#185

Adds to220h footprint for the TO-220 horizontal (lying-down) package:
- Signal pins in a horizontal row at 2.54mm pitch
- Mounting tab as a plated hole with silkscreen outline
- tabup parameter: tab extends toward +Y (default: tabdown, toward -Y)
- String aliases: to220h_3, to220h_3_tabup, TO-220-3_Horizontal, TO-220-3_Horizontal_TabUp
- 5 tests with SVG snapshot coverage
@victorjzq
Copy link
Contributor Author

Closing duplicate - #533 covers the same implementation.

@victorjzq victorjzq closed this Mar 19, 2026
Comment on lines +1 to +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))
})
Copy link
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.

Comment on lines +93 to +109
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,
}
Copy link
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

implement TO-220 horizontal variant

2 participants