diff --git a/src/fn/stampboard.ts b/src/fn/stampboard.ts index 6e12f4bb..dd599c9f 100644 --- a/src/fn/stampboard.ts +++ b/src/fn/stampboard.ts @@ -1,15 +1,16 @@ import { - length, type AnyCircuitElement, type PcbPlatedHole, type PcbSilkscreenPath, type PcbSilkscreenText, + length, } from "circuit-json" +import { platedhole } from "src/helpers/platedhole" +import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef" import { z } from "zod" import { rectpad } from "../helpers/rectpad" -import { platedhole } from "src/helpers/platedhole" -import { silkscreenRef, type SilkscreenRef } from "src/helpers/silkscreenRef" import { base_def } from "../helpers/zod/base_def" +import { dim2d } from "../helpers/zod/dim-2d" export const stampboard_def = base_def.extend({ fn: z.string(), @@ -22,6 +23,22 @@ export const stampboard_def = base_def.extend({ p: length.default(length.parse("2.54mm")), pw: length.default(length.parse("1.6mm")), pl: length.default(length.parse("2.4mm")), + leftrowoffsety: length + .default(0) + .describe("left pad row y offset from its default placement"), + rightrowoffsety: length + .default(0) + .describe("right pad row y offset from its default placement"), + innergrid: dim2d.optional().describe("inner SMT pad columns by rows"), + innerp: length.default("1mm").describe("inner SMT pad pitch"), + innerpw: length.default("1mm").describe("inner SMT pad width"), + innerph: length.default("1mm").describe("inner SMT pad height"), + innergridoffsetx: length + .default(0) + .describe("inner SMT pad grid x offset from the footprint origin"), + innergridoffsety: length + .default(0) + .describe("inner SMT pad grid y offset from the footprint origin"), innerhole: z.boolean().default(false), innerholeedgedistance: length.default(length.parse("1.61mm")), silkscreenlabels: z.boolean().default(false), @@ -29,19 +46,17 @@ export const stampboard_def = base_def.extend({ }) export type Stampboard_def = z.input +type StampboardParams = z.output -const getHeight = (parameters: Stampboard_def) => { - const params = stampboard_def.parse(parameters) - if (params.left && params.right) { - return Math.max(params.left, params.right) * params.p - } - if (params.left) { - return params.left * params.p - } - if (params.right) { - return params.right * params.p - } - return 51 // Default height if no pins are provided +const getHeight = (params: StampboardParams) => { + const leftHalfHeight = params.left + ? Math.abs(params.leftrowoffsety) + (params.left * params.p) / 2 + : 0 + const rightHalfHeight = params.right + ? Math.abs(params.rightrowoffsety) + (params.right * params.p) / 2 + : 0 + const halfHeight = Math.max(leftHalfHeight, rightHalfHeight) + return halfHeight > 0 ? halfHeight * 2 : 51 } const getTriangleDir = (x: number, y: number, side: string) => { let routes: { x: number; y: number }[] = [] @@ -141,12 +156,16 @@ export const stampboard = ( let routes: { x: number; y: number }[] = [] const innerDiameter = 1 const outerDiameter = innerDiameter - const totalPadsNumber = + const perimeterPadCount = params.left + params.right + (params.bottom ?? 0) + (params.top ?? 0) + const innerPadCount = params.innergrid + ? params.innergrid.x * params.innergrid.y + : 0 + const totalPadsNumber = perimeterPadCount + innerPadCount const maxLabelLength = `pin${totalPadsNumber}`.length const textHalf = (maxLabelLength * 0.7) / 2 if (params.right) { - const yoff = -((params.right - 1) / 2) * params.p + const yoff = -((params.right - 1) / 2) * params.p + params.rightrowoffsety for (let i = 0; i < params.right; i++) { if ( i === 0 && @@ -213,7 +232,7 @@ export const stampboard = ( } } if (params.left) { - const yoff = ((params.left - 1) / 2) * params.p + const yoff = ((params.left - 1) / 2) * params.p + params.leftrowoffsety for (let i = 0; i < params.left; i++) { if (i === 0 && !params.silkscreenlabels) { routes = getTriangleDir( @@ -417,6 +436,23 @@ export const stampboard = ( } } } + if (params.innergrid) { + const { x: columns, y: rows } = params.innergrid + for (let row = 0; row < rows; row++) { + for (let column = 0; column < columns; column++) { + rectpads.push( + rectpad( + perimeterPadCount + row * columns + column + 1, + params.innergridoffsetx + + (column - (columns - 1) / 2) * params.innerp, + params.innergridoffsety + (row - (rows - 1) / 2) * params.innerp, + params.innerpw, + params.innerph, + ), + ) + } + } + } const silkscreenTriangle: PcbSilkscreenPath = { type: "pcb_silkscreen_path", diff --git a/src/footprinter.ts b/src/footprinter.ts index 73cd0595..a25eec88 100644 --- a/src/footprinter.ts +++ b/src/footprinter.ts @@ -430,6 +430,14 @@ export type Footprinter = { | "p" | "pw" | "pl" + | "leftrowoffsety" + | "rightrowoffsety" + | "innergrid" + | "innerp" + | "innerpw" + | "innerph" + | "innergridoffsetx" + | "innergridoffsety" | "innerhole" | "innerholeedgedistance" | "silkscreenlabels" diff --git a/tests/__snapshots__/stampboard_custom_side_rows_and_inner_pad_grid.snap.svg b/tests/__snapshots__/stampboard_custom_side_rows_and_inner_pad_grid.snap.svg new file mode 100644 index 00000000..5bbd919d --- /dev/null +++ b/tests/__snapshots__/stampboard_custom_side_rows_and_inner_pad_grid.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/__snapshots__/stampboard_offset_side_rows_inner_smt_grid.snap.svg b/tests/__snapshots__/stampboard_offset_side_rows_inner_smt_grid.snap.svg new file mode 100644 index 00000000..45565df2 --- /dev/null +++ b/tests/__snapshots__/stampboard_offset_side_rows_inner_smt_grid.snap.svg @@ -0,0 +1 @@ +{REF} \ No newline at end of file diff --git a/tests/stampboard.test.ts b/tests/stampboard.test.ts index 0235a000..3a91151c 100644 --- a/tests/stampboard.test.ts +++ b/tests/stampboard.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from "bun:test" +import { expect, test } from "bun:test" import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" import { fp } from "../src/footprinter" @@ -23,6 +23,118 @@ test("stampboard", () => { ) }) +test("stampboard with offset side rows and inner SMT grid", () => { + const def = + "stampboard_left14_right14_bottom12_top0_w19.000089mm_h18.9899036mm_p1.27mm_pw0.8999982mm_pl1.499997mm_leftrowoffsety0.7899527mm_rightrowoffsety0.7899527mm_innergrid3x3_innerp1.400048mm_innerpw0.8999982mm_innerph0.8999982mm_innergridoffsetx-1.500124mm_innergridoffsety1.3248767mm" + const soup = fp.string(def).circuitJson() + const pads = soup.filter((element) => element.type === "pcb_smtpad") + const pad = (pinNumber: number) => + pads.find((element) => element.port_hints.includes(pinNumber.toString())) + const expectPad = ( + pinNumber: number, + expected: { x: number; y: number; width: number; height: number }, + ) => { + const actual = pad(pinNumber) + expect(actual).toBeDefined() + expect(actual!.x).toBeCloseTo(expected.x, 6) + expect(actual!.y).toBeCloseTo(expected.y, 6) + expect(actual!.width).toBeCloseTo(expected.width, 6) + expect(actual!.height).toBeCloseTo(expected.height, 6) + } + + expect(pads).toHaveLength(49) + expectPad(1, { + x: -8.750046, + y: 9.0449527, + width: 1.499997, + height: 0.8999982, + }) + expectPad(15, { + x: -6.985, + y: -8.7449533, + width: 0.8999982, + height: 1.499997, + }) + expectPad(27, { + x: 8.750046, + y: -7.4650473, + width: 1.499997, + height: 0.8999982, + }) + expectPad(41, { + x: -2.900172, + y: -0.0751713, + width: 0.8999982, + height: 0.8999982, + }) + expectPad(49, { + x: -0.100076, + y: 2.7249247, + width: 0.8999982, + height: 0.8999982, + }) + + const svgContent = convertCircuitJsonToPcbSvg(soup) + expect(svgContent).toMatchSvgSnapshot( + import.meta.path, + "stampboard_offset_side_rows_inner_smt_grid", + ) +}) + +test("stampboard supports the builder API for custom side rows and inner pads", () => { + const soup = fp() + .stampboard() + .left(2) + .right(2) + .top(0) + .bottom(0) + .w("10mm") + .p("2mm") + .pw("1mm") + .pl("2mm") + .leftrowoffsety("1mm") + .rightrowoffsety("-1mm") + .innergrid("2x1") + .innerp("1.5mm") + .innerpw("0.8mm") + .innerph("1.2mm") + .innergridoffsetx("0.25mm") + .innergridoffsety("-0.5mm") + .circuitJson() + const pads = soup.filter((element) => element.type === "pcb_smtpad") + const pad = (pinNumber: number) => { + const pad = pads.find((element) => + element.port_hints.includes(pinNumber.toString()), + ) + expect(pad).toBeDefined() + return pad! + } + + expect(pads).toHaveLength(6) + expect(pad(1).y).toBeCloseTo(2) + expect(pad(2).y).toBeCloseTo(0) + expect(pad(3).y).toBeCloseTo(-2) + expect(pad(4).y).toBeCloseTo(0) + expect(pad(5)).toMatchObject({ + x: -0.5, + y: -0.5, + width: 0.8, + height: 1.2, + }) + expect(pad(6)).toMatchObject({ + x: 1, + y: -0.5, + width: 0.8, + height: 1.2, + }) + + const svgContent = convertCircuitJsonToPcbSvg(soup) + expect(svgContent).toMatchSvgSnapshot( + import.meta.path, + "stampboard_custom_side_rows_and_inner_pad_grid", + ) +}) + test("stampboard silkscreen labels", () => { const def = "stampboard_left10_right10_bottom4_top4_w21mm_p2.54mm_silkscreenlabels"