Skip to content
Open
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 @@ -86,6 +86,7 @@ export { fpc } from "./fpc"
export { smbf } from "./smbf"
export { sot323 } from "./sot323"
export { smtpad } from "./smtpad"
export { smtpadpair } from "./smtpadpair"
export { smdpushbutton } from "./smdpushbutton"
export { smdslideswitch } from "./smdslideswitch"
export { smdpinheader } from "./smdpinheader"
Expand Down
120 changes: 120 additions & 0 deletions src/fn/smtpadpair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
type AnyCircuitElement,
type PcbCourtyardRect,
type PcbSilkscreenPath,
length,
} from "circuit-json"
import { z } from "zod"
import { rectpad } from "../helpers/rectpad"
import { type SilkscreenRef, silkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"

const positiveLength = length.refine((value) => value > 0, {
message: "pad dimensions must be positive",
})

export const smtpadpair_def = base_def
.extend({
fn: z.literal("smtpadpair"),
num_pins: z.literal(2).default(2),
px: length.default("2mm").describe("pin 2 x offset from pin 1"),
py: length.default("0mm").describe("pin 2 y offset from pin 1"),
pw: positiveLength.default("1mm").describe("shared pad width"),
ph: positiveLength.default("1mm").describe("shared pad height"),
p1w: positiveLength.optional().describe("pin 1 pad width override"),
p1h: positiveLength.optional().describe("pin 1 pad height override"),
p2w: positiveLength.optional().describe("pin 2 pad width override"),
p2h: positiveLength.optional().describe("pin 2 pad height override"),
})
.superRefine(({ px, py }, ctx) => {
if (px === 0 && py === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "smtpadpair pads must have a non-zero center offset",
path: ["px", "py"],
})
}
})
.transform((parameters) => ({
...parameters,
p1h: parameters.p1h ?? parameters.ph,
p1w: parameters.p1w ?? parameters.pw,
p2h: parameters.p2h ?? parameters.ph,
p2w: parameters.p2w ?? parameters.pw,
}))

export const smtpadpair = (
rawParams: z.input<typeof smtpadpair_def>,
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const parameters = smtpadpair_def.parse(rawParams)
const { px, py, p1w, p1h, p2w, p2h } = parameters
const pin1 = { x: -px / 2, y: py === 0 ? 0 : -py / 2 }
const pin2 = { x: px / 2, y: py / 2 }
const pads = [
rectpad(1, pin1.x, pin1.y, p1w, p1h),
rectpad(2, pin2.x, pin2.y, p2w, p2h),
]

const centerDistance = Math.hypot(px, py)
const outward = {
x: -px / centerDistance,
y: -py / centerDistance,
}
const perpendicular = { x: -outward.y, y: outward.x }
const pin1Radius =
Math.abs(outward.x) * (p1w / 2) + Math.abs(outward.y) * (p1h / 2)
const arrowTip = {
x: pin1.x + outward.x * (pin1Radius + 0.12),
y: pin1.y + outward.y * (pin1Radius + 0.12),
}
const arrowBaseCenter = {
x: arrowTip.x + outward.x * 0.3,
y: arrowTip.y + outward.y * 0.3,
}
const pin1Marker: PcbSilkscreenPath = {
type: "pcb_silkscreen_path",
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "pin1_marker",
route: [
{
x: arrowBaseCenter.x + perpendicular.x * 0.16,
y: arrowBaseCenter.y + perpendicular.y * 0.16,
},
arrowTip,
{
x: arrowBaseCenter.x - perpendicular.x * 0.16,
y: arrowBaseCenter.y - perpendicular.y * 0.16,
},
],
stroke_width: 0.1,
}

const copperMinX = Math.min(pin1.x - p1w / 2, pin2.x - p2w / 2)
const copperMaxX = Math.max(pin1.x + p1w / 2, pin2.x + p2w / 2)
const copperMinY = Math.min(pin1.y - p1h / 2, pin2.y - p2h / 2)
const copperMaxY = Math.max(pin1.y + p1h / 2, pin2.y + p2h / 2)
const courtyard: PcbCourtyardRect = {
type: "pcb_courtyard_rect",
pcb_courtyard_rect_id: "",
pcb_component_id: "",
center: {
x: (copperMinX + copperMaxX) / 2,
y: (copperMinY + copperMaxY) / 2,
},
width: copperMaxX - copperMinX + 0.5,
height: copperMaxY - copperMinY + 0.5,
layer: "top",
}
const refY = outward.y > 0 ? copperMinY - 0.8 : copperMaxY + 0.8
const ref: SilkscreenRef = silkscreenRef(
(copperMinX + copperMaxX) / 2,
refY,
0.5,
)

return {
circuitJson: [...pads, pin1Marker, ref, courtyard],
parameters,
}
}
12 changes: 12 additions & 0 deletions src/footprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ export type Footprinter = {
soup: () => AnySoupElement[]
circuitJson: () => AnyCircuitElement[]
}
smtpadpair: () => FootprinterParamsBuilder<
"px" | "py" | "pw" | "ph" | "p1w" | "p1h" | "p2w" | "p2h"
>
platedhole: () => FootprinterParamsBuilder<
"d" | "hd" | "r" | "hr" | "pd" | "pr"
>
Expand Down Expand Up @@ -565,6 +568,15 @@ export const string = (def: string): Footprinter => {
v: pin1LocationMatch[2],
}
}
const numberedPadParameterMatch = s.match(
/^(p[12][whxy])([\(\d\.\+\-\?].*)$/i,
)
if (numberedPadParameterMatch) {
return {
fn: numberedPadParameterMatch[1]!.toLowerCase(),
v: numberedPadParameterMatch[2],
}
}
const m = s.match(/([a-zA-Z]+)([\(\d\.\+\-\?].*)?/)
if (!m) return null
const [, rawFn, v] = m
Expand Down
1 change: 1 addition & 0 deletions tests/__snapshots__/asymmetric.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__/staggered.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions tests/smtpadpair.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "../src/footprinter"

const getPadGeometry = (footprint: string) =>
fp
.string(footprint)
.circuitJson()
.filter((element) => element.type === "pcb_smtpad")
.map(({ x, y, width, height, port_hints }) => ({
x,
y,
width,
height,
port_hints,
}))

test("smtpadpair supports independently sized pads", () => {
const footprint = "smtpadpair_px2.1mm_ph1mm_p1w2mm_p2w1.2mm"
const circuitJson = fp.string(footprint).circuitJson()

expect(getPadGeometry(footprint)).toEqual([
{ x: -1.05, y: 0, width: 2, height: 1, port_hints: ["1"] },
{ x: 1.05, y: 0, width: 1.2, height: 1, port_hints: ["2"] },
])
expect(convertCircuitJsonToPcbSvg(circuitJson)).toMatchSvgSnapshot(
import.meta.path,
"asymmetric",
)
})

test("smtpadpair supports a signed vertical pad offset", () => {
const footprint = "smtpadpair_px13.45mm_py-2.54mm_pw2.5mm_ph2.55mm"
const circuitJson = fp.string(footprint).circuitJson()

expect(getPadGeometry(footprint)).toEqual([
{ x: -6.725, y: 1.27, width: 2.5, height: 2.55, port_hints: ["1"] },
{ x: 6.725, y: -1.27, width: 2.5, height: 2.55, port_hints: ["2"] },
])
expect(convertCircuitJsonToPcbSvg(circuitJson)).toMatchSvgSnapshot(
import.meta.path,
"staggered",
)
})

test("smtpadpair rejects coincident pads", () => {
expect(() => fp.string("smtpadpair_px0mm_py0mm").circuitJson()).toThrow(
"non-zero center offset",
)
})
Comment on lines +18 to +50

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 file contains three test(...) calls (at lines 18, 32, and 46), but the rule states that a *.test.ts file may have AT MOST one test(...). The additional tests should be split into separate, numbered files — for example: smtpadpair1.test.ts, smtpadpair2.test.ts, and smtpadpair3.test.ts.

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

Fix in Graphite


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

Loading