feat: implement customsmd3 footprint for custom 3-pad SMD layouts#536
feat: implement customsmd3 footprint for custom 3-pad SMD layouts#536victorjzq wants to merge 3 commits intotscircuit:mainfrom
Conversation
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
tests/customsmd3.test.ts
Outdated
| 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) | ||
| } | ||
| }) |
There was a problem hiding this comment.
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)
Is this helpful? React 👍 or 👎 to let us know.
| // --- 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 |
There was a problem hiding this comment.
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')
}| // --- 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
Is this helpful? React 👍 or 👎 to let us know.
| 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, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
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
Is this helpful? React 👍 or 👎 to let us know.
|
Hey @seveibar — customsmd3 footprint, CI green. Ready for review. |
Summary
customsmd3footprint for flexible custom 3-pad SMD layouts (resolves Implement customsmd3 #10)w/horrparametersleftmostn,rightmostn,topmostn,bottommostnspecifiersc2cvert_A_B,c2chorz_A_B) and edge-to-edge (e2evert_A_B,e2ehorz_A_B) paramsTest plan
w/hsets pad dimensions for all padsr=parameter produces circular pads with correct radiusleftmostn/topmostn/bottommostnspecifiers reorder pads correctlyc2cvert_2_3sets vertical center-to-center distancee2ehorz_1_2sets horizontal edge-to-edge distancecustomsmd3andcustomsmd3_w2_h1both work/claim #10