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
3 changes: 3 additions & 0 deletions src/fn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ export { sot343 } from "./sot343"
export { m2host } from "./m2host"
export { mountedpcbmodule } from "./mountedpcbmodule"
export { to92l } from "./to92l"
export { pdip } from "./pdip"
export { spdip } from "./spdip"
export { utdfn } from "./utdfn"
24 changes: 24 additions & 0 deletions src/fn/pdip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { AnyCircuitElement } from "circuit-json"
import { dip, dip_def } from "./dip"
import { z } from "zod"

export const pdip_def = dip_def

/**
* PDIP (Plastic Dual In-line Package) — identical layout to DIP.
* Aliases to the standard DIP footprint generator.
*/
export const pdip = (
raw_params: z.input<typeof pdip_def> & { pdip?: boolean },
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const match = (raw_params as any).string?.match(/^pdip_?(\d+)/)
const numPins = match
? Number.parseInt(match[1]!, 10)
: ((raw_params as any).num_pins ?? 8)

return dip({
dip: true,
...raw_params,
num_pins: numPins,
} as any)
}
115 changes: 115 additions & 0 deletions src/fn/spdip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type {
AnyCircuitElement,
PcbCourtyardRect,
PcbSilkscreenPath,
} from "circuit-json"
import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"
import { z } from "zod"
import { platedhole } from "../helpers/platedhole"
import { platedHoleWithRectPad } from "../helpers/platedHoleWithRectPad"
import { u_curve } from "../helpers/u-curve"
import { getCcwDipCoords } from "./dip"

/**
* SPDIP — Shrink Plastic DIP.
* Uses 70 mil (1.778 mm) pitch instead of standard 100 mil (2.54 mm).
* Row spacing: 300 mil (7.62 mm).
*/
export const spdip_def = base_def.extend({
fn: z.string(),
num_pins: z.number().optional().default(28),
w: z.number().optional().default(7.62),
p: z.number().optional().default(1.778),
id: z.number().optional().default(0.6),
od: z.number().optional().default(1.1),
})

export const spdip = (
raw_params: z.input<typeof spdip_def> & { spdip?: boolean },
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const match = (raw_params as any).string?.match(/^spdip_?(\d+)/)
const numPins = match
? Number.parseInt(match[1]!, 10)
: ((raw_params as any).num_pins ?? 28)

const parameters = spdip_def.parse({ ...raw_params, num_pins: numPins })

const { w, p, id, od } = parameters as {
w: number
p: number
id: number
od: number
num_pins: number
}
const num_pins = parameters.num_pins!

const platedHoles: AnyCircuitElement[] = []
for (let i = 0; i < num_pins; i++) {
const { x, y } = getCcwDipCoords(num_pins, i + 1, w, p, false)
if (i === 0) {
platedHoles.push(
platedHoleWithRectPad({
pn: i + 1,
x,
y,
holeDiameter: id,
rectPadWidth: od,
rectPadHeight: od,
}),
)
continue
}
platedHoles.push(platedhole(i + 1, x, y, id, od))
}

const padEdgeHeight = (num_pins / 2 - 1) * p + od
const innerGap = w - od
const sw = innerGap - 1
const sh = (num_pins / 2 - 1) * p + od + 0.4

const silkscreenBorder: PcbSilkscreenPath = {
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "silkscreen_path_1",
route: [
{ x: -sw / 2, y: -sh / 2 },
{ x: -sw / 2, y: sh / 2 },
...u_curve.map(({ x, y }) => ({
x: (x * sw) / 6,
y: (y * sw) / 6 + sh / 2,
})),
{ x: sw / 2, y: sh / 2 },
{ x: sw / 2, y: -sh / 2 },
{ x: -sw / 2, y: -sh / 2 },
],
type: "pcb_silkscreen_path",
stroke_width: 0.1,
}

const silkscreenRefText: SilkscreenRef = silkscreenRef(0, sh / 2 + 0.5, 0.4)

const pinRowSpanX = w + od
const pinRowSpanY = padEdgeHeight
const courtyardHalfWidth = pinRowSpanX / 2 + 0.25
const courtyardHalfHeight = pinRowSpanY / 2 + 0.72
const courtyard: PcbCourtyardRect = {
type: "pcb_courtyard_rect",
pcb_courtyard_rect_id: "",
pcb_component_id: "",
center: { x: 0, y: 0 },
width: courtyardHalfWidth * 2,
height: courtyardHalfHeight * 2,
layer: "top",
}

return {
circuitJson: [
...platedHoles,
silkscreenBorder,
silkscreenRefText,
courtyard,
],
parameters,
}
}
121 changes: 121 additions & 0 deletions src/fn/utdfn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type {
AnyCircuitElement,
PcbCourtyardRect,
PcbSilkscreenPath,
} from "circuit-json"
import { type SilkscreenRef, silkscreenRef } from "src/helpers/silkscreenRef"
import { z } from "zod"
import { rectpad } from "../helpers/rectpad"
import { base_def } from "../helpers/zod/base_def"
import { CORNERS } from "src/helpers/corner"

/**
* UTDFN — Ultra-Thin Dual Flat No-lead with Exposed Pad.
*
* Default dimensions match UTDFN-4-EP(1x1):
* body 1×1 mm, 4 signal pins (pitch 0.5 mm), 1 exposed pad 0.48×0.48 mm.
*/
export const utdfn_def = base_def.extend({
fn: z.string(),
num_pins: z.number().optional().default(4),
w: z.number().optional().default(1.0),
h: z.number().optional().default(1.0),
p: z.number().optional().default(0.5),
pl: z.number().optional().default(0.3),
pw: z.number().optional().default(0.25),
epw: z.number().optional().default(0.48),
eph: z.number().optional().default(0.48),
})

const getUtdfnPadCoord = (
num_pins: number,
pn: number,
w: number,
p: number,
): { x: number; y: number } => {
const halfCount = num_pins / 2
const offset = ((halfCount - 1) / 2) * p

if (pn <= halfCount) {
return { x: -w / 2, y: offset - (pn - 1) * p }
}
return { x: w / 2, y: -offset + (pn - halfCount - 1) * p }
}

export const utdfn = (
raw_params: z.input<typeof utdfn_def> & { utdfn?: boolean },
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const match = (raw_params as any).string?.match(/^utdfn_?(\d+)/)
const numPins = match
? Number.parseInt(match[1]!, 10)
: ((raw_params as any).num_pins ?? 4)

const parameters = utdfn_def.parse({ ...raw_params, num_pins: numPins })

const { w, h, p, pl, pw, epw, eph } = parameters as {
w: number
h: number
p: number
pl: number
pw: number
epw: number
eph: number
num_pins: number
}
const num_pins = parameters.num_pins!

const pads: AnyCircuitElement[] = []
let padOuterHalfWidth = 0
let padOuterHalfHeight = 0

for (let i = 1; i <= num_pins; i++) {
const { x, y } = getUtdfnPadCoord(num_pins, i, w, p)
padOuterHalfWidth = Math.max(padOuterHalfWidth, Math.abs(x) + pl / 2)
padOuterHalfHeight = Math.max(padOuterHalfHeight, Math.abs(y) + pw / 2)
pads.push(rectpad(i, x, y, pl, pw))
}

// Exposed pad centred on the component
pads.push(rectpad(num_pins + 1, 0, 0, epw, eph))

// Corner-only silkscreen (like DFN style)
const m = Math.min(0.3, p / 2)
const sw = w + m
const sh = h + m
const silkscreenPaths: PcbSilkscreenPath[] = []

for (const corner of CORNERS) {
const { dx, dy } = corner
silkscreenPaths.push({
layer: "top",
pcb_component_id: "",
pcb_silkscreen_path_id: "",
route: [
{ x: (dx * sw) / 2 - dx * p * 0.6, y: (dy * sh) / 2 },
{ x: (dx * sw) / 2, y: (dy * sh) / 2 },
{ x: (dx * sw) / 2, y: (dy * sh) / 2 - dy * p * 0.6 },
],
type: "pcb_silkscreen_path",
stroke_width: 0.1,
})
}

const silkscreenRefText: SilkscreenRef = silkscreenRef(0, sh / 2 + 0.3, 0.3)

const courtyardHalfWidth = Math.max(padOuterHalfWidth + 0.15, w / 2 + 0.15)
const courtyardHalfHeight = Math.max(padOuterHalfHeight + 0.15, h / 2 + 0.15)
const courtyard: PcbCourtyardRect = {
type: "pcb_courtyard_rect",
pcb_courtyard_rect_id: "",
pcb_component_id: "",
center: { x: 0, y: 0 },
width: courtyardHalfWidth * 2,
height: courtyardHalfHeight * 2,
layer: "top",
}

return {
circuitJson: [...pads, ...silkscreenPaths, silkscreenRefText, courtyard],
parameters,
}
}
18 changes: 18 additions & 0 deletions tests/pdip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test, expect } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "../src/footprinter"
import type { AnyCircuitElement } from "circuit-json"

test("pdip8", () => {
const circuitJson = fp.string("pdip8").circuitJson() as AnyCircuitElement[]
expect(circuitJson.length).toBeGreaterThan(0)
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip8")
})

test("pdip14", () => {
const circuitJson = fp.string("pdip14").circuitJson() as AnyCircuitElement[]
expect(circuitJson.length).toBeGreaterThan(0)
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "pdip14")
})
18 changes: 18 additions & 0 deletions tests/spdip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test, expect } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "../src/footprinter"
import type { AnyCircuitElement } from "circuit-json"

test("spdip28", () => {
const circuitJson = fp.string("spdip28").circuitJson() as AnyCircuitElement[]
expect(circuitJson.length).toBeGreaterThan(0)
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "spdip28")
})

test("spdip16", () => {
const circuitJson = fp.string("spdip16").circuitJson() as AnyCircuitElement[]
expect(circuitJson.length).toBeGreaterThan(0)
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "spdip16")
})
14 changes: 14 additions & 0 deletions tests/utdfn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { test, expect } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "../src/footprinter"
import type { AnyCircuitElement } from "circuit-json"

test("utdfn4 (UTDFN-4-EP 1x1)", () => {
const circuitJson = fp.string("utdfn4").circuitJson() as AnyCircuitElement[]
// 4 signal pads + 1 exposed pad + silkscreen paths + ref + courtyard
expect(circuitJson.length).toBeGreaterThan(0)
const smtpads = circuitJson.filter((e) => e.type === "pcb_smtpad")
expect(smtpads.length).toBe(5) // 4 pins + EP
const svgContent = convertCircuitJsonToPcbSvg(circuitJson)
expect(svgContent).toMatchSvgSnapshot(import.meta.path, "utdfn4")
})
Loading