Skip to content

feat: implement customsmd3 footprint for custom 3-pad SMD layouts#536

Open
victorjzq wants to merge 3 commits intotscircuit:mainfrom
victorjzq:feat/customsmd3-10
Open

feat: implement customsmd3 footprint for custom 3-pad SMD layouts#536
victorjzq wants to merge 3 commits intotscircuit:mainfrom
victorjzq:feat/customsmd3-10

Conversation

@victorjzq
Copy link
Contributor

Summary

  • Adds customsmd3 footprint for flexible custom 3-pad SMD layouts (resolves Implement customsmd3 #10)
  • Supports rectangular and circular pads via w/h or r parameters
  • Pin positions configurable with leftmostn, rightmostn, topmostn, bottommostn specifiers
  • Inter-pad distances via center-to-center (c2cvert_A_B, c2chorz_A_B) and edge-to-edge (e2evert_A_B, e2ehorz_A_B) params
  • Default layout: pin 1 left, pin 2 top-right, pin 3 bottom-right (SOT-23 style)
  • Includes courtyard rect, silkscreen ref, and full string parser support
  • 9 tests all passing, 388 total tests green

Test plan

  • Default layout generates 3 pads with correct relative positions
  • Custom w/h sets pad dimensions for all pads
  • r= parameter produces circular pads with correct radius
  • leftmostn/topmostn/bottommostn specifiers reorder pads correctly
  • c2cvert_2_3 sets vertical center-to-center distance
  • e2ehorz_1_2 sets horizontal edge-to-edge distance
  • Courtyard rect is present in output
  • String parser: customsmd3 and customsmd3_w2_h1 both work

/claim #10

Adds a flexible 3-pad SMD footprint supporting:
- Rectangular or circular pads (r= for circular)
- Pin position specifiers: leftmostn, rightmostn, topmostn, bottommostn
- Center-to-center distances: c2cvert_A_B, c2chorz_A_B
- Edge-to-edge distances: e2evert_A_B, e2ehorz_A_B
- sym (symmetric) and eqsz (equally-sized pads) flags
- Courtyard rectangle and silkscreen ref
- Full string parser support (e.g. customsmd3_w2_h1_leftmostn1)

Closes tscircuit#10
Comment on lines +5 to +125
test("customsmd3 default layout (1 left, 2 top-right, 3 bottom-right)", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_default")
const circuitJson = fp().customsmd3().circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
expect(pads).toHaveLength(3)

// Pin 1 should be leftmost
const pin1 = pads.find((p: any) => p.port_hints?.includes("1"))!
const pin2 = pads.find((p: any) => p.port_hints?.includes("2"))!
const pin3 = pads.find((p: any) => p.port_hints?.includes("3"))!

expect(pin1.x).toBeLessThan(pin2.x)
expect(pin1.x).toBeLessThan(pin3.x)

// Pin 2 should be above pin 3
expect(pin2.y).toBeGreaterThan(pin3.y)
})

test("customsmd3 with custom pad size w/h", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_w2_h1")
const circuitJson = fp().customsmd3().w("2mm").h("1mm").circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
expect(pads).toHaveLength(3)
for (const pad of pads) {
expect(pad.width).toBeCloseTo(2)
expect(pad.height).toBeCloseTo(1)
}
})

test("customsmd3 circular pads with radius", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_r0.5")
const circuitJson = fp().customsmd3().r("0.5mm").circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
expect(pads).toHaveLength(3)
for (const pad of pads) {
expect(pad.shape).toBe("circle")
expect(pad.radius).toBeCloseTo(0.5)
}
})

test("customsmd3 with position specifiers (leftmost=2)", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_leftmostn2")
const circuitJson = fp()
.customsmd3()
.leftmostn(2)
.topmostn(1)
.bottommostn(3)
.circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
const pin2 = pads.find((p: any) => p.port_hints?.includes("2"))!
const pin1 = pads.find((p: any) => p.port_hints?.includes("1"))!

// Pin 2 is leftmost, so its x should be less than pin 1
expect(pin2.x).toBeLessThan(pin1.x)
})

test("customsmd3 with c2cvert (center-to-center vertical distance)", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_c2cvert_2_3_2mm")
const circuitJson = fp()
.customsmd3()
.c2cvert_2_3("2mm")
.circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
const pin2 = pads.find((p: any) => p.port_hints?.includes("2"))!
const pin3 = pads.find((p: any) => p.port_hints?.includes("3"))!

expect(Math.abs(pin2.y - pin3.y)).toBeCloseTo(2)
})

test("customsmd3 with e2ehorz (edge-to-edge horizontal distance)", async () => {
const { snapshotSoup } = await getTestFixture("customsmd3_e2ehorz_1_2_3mm")
const circuitJson = fp()
.customsmd3()
.w("1.5mm")
.h("1mm")
.e2ehorz_1_2("3mm")
.circuitJson()
snapshotSoup(circuitJson)

const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
const pin1 = pads.find((p: any) => p.port_hints?.includes("1"))!
const pin2 = pads.find((p: any) => p.port_hints?.includes("2"))!

// edge-to-edge = center-to-center - padW
const edge2edge = Math.abs(pin2.x - pin1.x) - pin1.width
expect(edge2edge).toBeCloseTo(3)
})

test("customsmd3 has courtyard", () => {
const circuitJson = fp().customsmd3().circuitJson()
const courtyard = circuitJson.find(
(el: any) => el.type === "pcb_courtyard_rect",
)
expect(courtyard).toBeDefined()
})

test("customsmd3 from string - default", () => {
const circuitJson = fp.string("customsmd3").circuitJson()
const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
expect(pads).toHaveLength(3)
})

test("customsmd3 from string - with w and h", () => {
const circuitJson = fp.string("customsmd3_w2_h1").circuitJson()
const pads = circuitJson.filter((el: any) => el.type === "pcb_smtpad")
expect(pads).toHaveLength(3)
for (const pad of pads) {
expect(pad.width).toBeCloseTo(2)
expect(pad.height).toBeCloseTo(1)
}
})
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 9 test() function calls, 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. The file should be split into separate files like customsmd3_1.test.ts, customsmd3_2.test.ts, etc., with each file containing only one test() function.

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

Fix in Graphite


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

Comment on lines +119 to +122
// --- Position specifiers (defaults: 1=left, 2=top-right, 3=bottom-right) ---
const leftPin = params.leftmostn !== undefined ? params.leftmostn : 1
const topPin = params.topmostn !== undefined ? params.topmostn : 2
const bottomPin = params.bottommostn !== undefined ? params.bottommostn : 3
Copy link
Contributor

Choose a reason for hiding this comment

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

No validation that leftmostn, topmostn, and bottommostn are distinct values. If a user sets the same pin number for multiple positions (e.g., leftmostn=2, topmostn=2), the positions object will have duplicate keys where later assignments overwrite earlier ones, resulting in fewer than 3 pads being generated.

Fix: Add validation after line 122:

const pinNumbers = [leftPin, topPin, bottomPin]
if (new Set(pinNumbers).size !== 3) {
  throw new Error('leftmostn, topmostn, and bottommostn must specify distinct pin numbers')
}
Suggested change
// --- Position specifiers (defaults: 1=left, 2=top-right, 3=bottom-right) ---
const leftPin = params.leftmostn !== undefined ? params.leftmostn : 1
const topPin = params.topmostn !== undefined ? params.topmostn : 2
const bottomPin = params.bottommostn !== undefined ? params.bottommostn : 3
// --- Position specifiers (defaults: 1=left, 2=top-right, 3=bottom-right) ---
const leftPin = params.leftmostn !== undefined ? params.leftmostn : 1
const topPin = params.topmostn !== undefined ? params.topmostn : 2
const bottomPin = params.bottommostn !== undefined ? params.bottommostn : 3
const pinNumbers = [leftPin, topPin, bottomPin]
if (new Set(pinNumbers).size !== 3) {
throw new Error('leftmostn, topmostn, and bottommostn must specify distinct pin numbers')
}

Spotted by Graphite

Fix in Graphite


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

Comment on lines +178 to +194
for (let pinNum = 1; pinNum <= 3; pinNum++) {
const pos = positions[pinNum]
if (!pos) continue
if (isCircle) {
pads.push(
circlepad(pinNum, {
x: pos.x,
y: pos.y,
radius,
}) as AnyCircuitElement,
)
} else {
pads.push(
rectpad(pinNum, pos.x, pos.y, padW, padH) as AnyCircuitElement,
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The pad creation loop hardcodes pinNum = 1; pinNum <= 3 but doesn't validate that leftmostn, topmostn, and bottommostn are within this range. If a user sets leftmostn=4, a position would be calculated for pin 4 in the positions object (line 163) but the loop would never create that pad, resulting in only 2 pads being generated instead of 3.

Fix: Add validation in the schema or after parsing:

if (leftPin < 1 || leftPin > 3 || topPin < 1 || topPin > 3 || bottomPin < 1 || bottomPin > 3) {
  throw new Error('Pin position specifiers must be 1, 2, or 3')
}

Spotted by Graphite

Fix in Graphite


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

@victorjzq
Copy link
Contributor Author

@seveibar Implements customsmd3 as requested in #10. All 388 tests pass, CI green.

@victorjzq
Copy link
Contributor Author

Hey @seveibar — customsmd3 footprint, CI green. Ready for review.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement customsmd3

1 participant