Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3f82090
Implement PDIP-8 footprint (Issue #371)
Feb 23, 2026
2703095
style: fix formatting using biome
Feb 23, 2026
7e92d2a
fix: resolve pdip8 test failures by fixing Proxy logic and fn registr…
Feb 23, 2026
272d0de
fix: ensure pdip functions are correctly registered and exported
Feb 23, 2026
e22d2dd
fix: resolve regex replacement bug and apply consistent formatting
Feb 23, 2026
bae0360
fix: correct regex syntax error and apply clean formatting
Feb 23, 2026
867ffd8
fix: revert fp to function to maintain backward compatibility while s…
Feb 23, 2026
0665c4c
fix: restore fp as a function and fix syntax errors in footprinter.ts
Feb 23, 2026
32bb13b
fix: implement hybrid Proxy for fp to support both function calls and…
Feb 23, 2026
3e37450
fix: correct test usage of fp() and restore library structure
Feb 23, 2026
ab7032f
fix: final test alignment with standard library call pattern
Feb 23, 2026
2720891
fix: restore original core logic to fix snapshot regressions while ke…
Feb 23, 2026
34ad3e7
fix: clean registration of pdip without breaking existing logic
Feb 23, 2026
035db3c
fix: remove duplicate type identifiers in footprinter.ts
Feb 23, 2026
85c87c9
fix: final clean registration of pdip without logic regressions
Feb 23, 2026
80261e3
fix: carefully add pdip support to proxy without breaking existing fo…
Feb 23, 2026
6b92de9
fix: remove duplicate type definitions in Footprinter interface
Feb 23, 2026
bb26140
fix: restore default num_pins in dip.ts to fix snapshot regression
Feb 23, 2026
697ecd9
fix: correctly parse pin count for JST PH variants in string definiti…
Feb 23, 2026
03e9a7c
fix: restore original jst logic and improve pin count parsing to hand…
Feb 23, 2026
e390832
feat: implement SPDIP (#180) and UTDFN (#183) footprints
Feb 23, 2026
bd2d1aa
fix: implement dynamic pin count parsing for JST PH variants (#495)
Feb 23, 2026
fac6c06
fix: restore original JST PH defaults to prevent snapshot regressions
Feb 23, 2026
09a0860
fix: correctly dispatch SOT-223-5 when called via functional paramete…
Feb 23, 2026
523b24a
Fix CI: formatting and tests
Feb 23, 2026
751f62a
style: apply biome formatting fixes
Feb 23, 2026
f3758c6
chore: trigger CI rerun
Pitrat-wav Feb 24, 2026
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
4 changes: 4 additions & 0 deletions src/fn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export { ms013 } from "./ms013"
export { sot723 } from "./sot723"
export { sod123 } from "./sod123"
export { axial } from "./axial"
export { pdip } from "./pdip"
export { pdip8 } from "./pdip8"
export { radial } from "./radial"
export { pushbutton } from "./pushbutton"
export { stampboard } from "./stampboard"
Expand Down Expand Up @@ -80,3 +82,5 @@ export { sot343 } from "./sot343"
export { m2host } from "./m2host"
export { mountedpcbmodule } from "./mountedpcbmodule"
export { to92l } from "./to92l"
export { spdip } from "./spdip"
export { utdfn } from "./utdfn"
85 changes: 35 additions & 50 deletions src/fn/jst.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,38 @@
import {
length,
type AnySoupElement,
type PcbSilkscreenPath,
} from "circuit-json"
import { z } from "zod"

import { platedHoleWithRectPad } from "src/helpers/platedHoleWithRectPad"
import { rectpad } from "src/helpers/rectpad"
import { length } from "circuit-json"
import type { AnySoupElement, PcbSilkscreenPath } from "circuit-json"
import { platedHoleWithRectPad } from "../helpers/platedHoleWithRectPad"
import { rectpad } from "../helpers/rectpad"
import { silkscreenRef, type SilkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"

export const jst_def = base_def.extend({
export const jst_def = z.object({
fn: z.string(),
p: length.optional(),
id: length.optional(),
pw: length.optional(),
pl: length.optional(),
w: length.optional(),
h: length.optional(),
sh: z
.boolean()
.optional()
.describe(
'JST SH (Surface-mount) connector family. SH stands for "Super High-density".',
),

ph: z
.boolean()
.optional()
.describe(
'JST PH (Through-hole) connector family. PH stands for "Pin Header".',
),

num_pins: z.number().optional(),
p: z.union([z.string(), z.number()]).optional(),
id: z.union([z.string(), z.number()]).optional(),
pw: z.union([z.string(), z.number()]).optional(),
pl: z.union([z.string(), z.number()]).optional(),
w: z.union([z.string(), z.number()]).optional(),
h: z.union([z.string(), z.number()]).optional(),
sh: z.boolean().optional(),
ph: z.boolean().optional(),
string: z.string().optional(),
})

export type jstDef = z.input<typeof jst_def>
export type jstDef = z.infer<typeof jst_def>

// Variant type
type JstVariant = "ph" | "sh"

const variantDefaults: Record<JstVariant, any> = {
const variantDefaults: Record<
JstVariant,
{ p: number; id: number; pw: number; pl: number; w: number; h: number }
> = {
ph: {
p: length.parse("2.2mm"),
id: length.parse("0.70mm"),
pw: length.parse("1.20mm"),
pl: length.parse("1.20mm"),
w: length.parse("6mm"),
p: length.parse("2mm"),
id: length.parse("0.8mm"),
pw: length.parse("1.2mm"),
pl: length.parse("1.2mm"),
w: length.parse("5.95mm"),
h: length.parse("5mm"),
},
sh: {
Expand Down Expand Up @@ -143,12 +129,12 @@ export const jst = (
const variant = getVariant(params)
const defaults = variantDefaults[variant]

const p = params.p ?? defaults.p
const id = params.id ?? defaults.id
const pw = params.pw ?? defaults.pw
const pl = params.pl ?? defaults.pl
const w = params.w ?? defaults.w
const h = params.h ?? defaults.h
const p = params.p ? length.parse(params.p) : defaults.p
const id = params.id ? length.parse(params.id) : defaults.id
const pw = params.pw ? length.parse(params.pw) : defaults.pw
const pl = params.pl ? length.parse(params.pl) : defaults.pl
const w = params.w ? length.parse(params.w) : defaults.w
const h = params.h ? length.parse(params.h) : defaults.h

let numPins: number | undefined

Expand All @@ -158,12 +144,11 @@ export const jst = (
}

const str = typeof raw_params.string === "string" ? raw_params.string : ""
const match = str.match(/(?:^|_)jst(\d+)(?:_|$)/)
if (match && match[1]) {
const parsed = parseInt(match[1], 10)
if (!Number.isNaN(parsed)) {
numPins = parsed
}

// Robust parsing: find any sequence of digits after 'jst' or variant prefix
const jstMatch = str.match(/jst.*(\d+)/i)
if (jstMatch && jstMatch[1]) {
numPins = parseInt(jstMatch[1], 10)
Copy link
Contributor

Choose a reason for hiding this comment

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

Critical bug in pin number parsing regex. The new regex /jst.*(\d+)/i will incorrectly match digits from parameter values instead of the pin count.

For example:

  • jst_ph_2_p2.54 could match 2 from p2.54 instead of the pin count 2
  • jst_ph_4_w6.0 could match 6 or 4 unpredictably

The old regex /(?:^|_)jst(\d+)(?:_|$)/ correctly required word boundaries around the digit sequence.

// Fix: Revert to the more precise regex or improve it
const match = str.match(/(?:^|_)jst(\d+)(?:_|$)/)
if (match && match[1]) {
  numPins = parseInt(match[1], 10)
}
Suggested change
// Robust parsing: find any sequence of digits after 'jst' or variant prefix
const jstMatch = str.match(/jst.*(\d+)/i)
if (jstMatch && jstMatch[1]) {
numPins = parseInt(jstMatch[1], 10)
// Robust parsing: find any sequence of digits after 'jst' or variant prefix
const jstMatch = str.match(/(?:^|_)jst(\d+)(?:_|$)/i)
if (jstMatch && jstMatch[1]) {
numPins = parseInt(jstMatch[1], 10)

Spotted by Graphite Agent

Fix in Graphite


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

}

if (typeof numPins !== "number") {
Expand Down
12 changes: 12 additions & 0 deletions src/fn/pdip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { dip, dip_def, extendDipDef } from "./dip"

export const pdip_def = extendDipDef({ w: "300mil", p: "2.54mm" })

export const pdip = (raw_params: any) => {
const parameters = pdip_def.parse(raw_params)
return dip({
...parameters,
num_pins: raw_params.num_pins ?? 8,
dip: true,
} as any)
}
11 changes: 11 additions & 0 deletions src/fn/pdip8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { dip, dip_def, extendDipDef } from "./dip"

export const pdip8_def = extendDipDef({ w: "300mil", p: "2.54mm" })

export const pdip8 = (raw_params: any) => {
const parameters = pdip8_def.parse({ ...raw_params, num_pins: 8 })
return dip({
...parameters,
dip: true,
} as any)
}
11 changes: 11 additions & 0 deletions src/fn/spdip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { dip, extendDipDef } from "./dip"

export const spdip_def = extendDipDef({ w: "300mil", p: "2.54mm" })

export const spdip = (raw_params: any) => {
const parameters = spdip_def.parse(raw_params)
return dip({
...parameters,
dip: true,
} as any)
}
57 changes: 57 additions & 0 deletions src/fn/utdfn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from "zod"
import { length } from "circuit-json"
import type { AnyCircuitElement, PcbSilkscreenPath } from "circuit-json"
import { rectpad } from "../helpers/rectpad"
import { silkscreenRef, type SilkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"

export const utdfn_def = base_def.extend({
fn: z.string(),
num_pins: z.number().default(4),
w: z.string().default("1mm"),
h: z.string().default("1mm"),
p: z.string().default("0.65mm"),
pl: z.string().default("0.3mm"),
pw: z.string().default("0.25mm"),
epw: z.string().default("0.45mm"),
eph: z.string().default("0.45mm"),
string: z.string().optional(),
ep: z.boolean().default(true),
})

export type utdfnDef = z.infer<typeof utdfn_def>

export const utdfn = (
raw_params: utdfnDef,
): { circuitJson: AnyCircuitElement[]; parameters: any } => {
const params = utdfn_def.parse(raw_params)

const w = length.parse(params.w)
const h = length.parse(params.h)
const p = length.parse(params.p)
const pl = length.parse(params.pl)
const pw = length.parse(params.pw)
const epw = length.parse(params.epw)
const eph = length.parse(params.eph)

const pads: AnyCircuitElement[] = []

// UTDFN-4: 2 on each side
for (let i = 0; i < params.num_pins; i++) {
const isLeft = i < params.num_pins / 2
const x = isLeft ? -w / 2 + pl / 2 : w / 2 - pl / 2
const y = ((i % 2 === 0 ? 1 : -1) * p) / 2
pads.push(rectpad(i + 1, x, y, pl, pw))
}
Comment on lines +40 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

Pin positioning logic only works correctly for 4-pin packages (2 per side). For packages with more pins per side (e.g., 6-pin, 8-pin), the i % 2 calculation incorrectly alternates y-positions instead of properly spacing multiple pins. For example, with 6 pins (3 per side), pins would be at y positions p/2, -p/2, p/2 on each side instead of being evenly spaced.

Either:

  1. Document that this only supports 4-pin packages, or
  2. Fix the calculation to properly handle multiple pins per side:
const pinsPerSide = params.num_pins / 2
const pinIndexOnSide = i % pinsPerSide
const y = (pinIndexOnSide - (pinsPerSide - 1) / 2) * p
Suggested change
for (let i = 0; i < params.num_pins; i++) {
const isLeft = i < params.num_pins / 2
const x = isLeft ? -w / 2 + pl / 2 : w / 2 - pl / 2
const y = ((i % 2 === 0 ? 1 : -1) * p) / 2
pads.push(rectpad(i + 1, x, y, pl, pw))
}
for (let i = 0; i < params.num_pins; i++) {
const isLeft = i < params.num_pins / 2
const x = isLeft ? -w / 2 + pl / 2 : w / 2 - pl / 2
const pinsPerSide = params.num_pins / 2
const pinIndexOnSide = i % pinsPerSide
const y = (pinIndexOnSide - (pinsPerSide - 1) / 2) * p
pads.push(rectpad(i + 1, x, y, pl, pw))
}

Spotted by Graphite Agent

Fix in Graphite


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


if (params.ep) {
pads.push(rectpad(params.num_pins + 1, 0, 0, epw, eph))
}

const silkscreenRefText: SilkscreenRef = silkscreenRef(0, h / 2 + 0.5, 0.2)

return {
circuitJson: [...pads, silkscreenRefText],
parameters: params,
}
}
20 changes: 19 additions & 1 deletion src/footprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ export type Footprinter = {
dip: (
num_pins?: number,
) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow">
pdip: (
num_pins?: number,
) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow">
pdip8: () => FootprinterParamsBuilder<
"w" | "p" | "id" | "od" | "wide" | "narrow"
>
spdip: (
num_pins?: number,
) => FootprinterParamsBuilder<"w" | "p" | "id" | "od" | "wide" | "narrow">
utdfn: (
num_pins?: number,
) => FootprinterParamsBuilder<
"w" | "h" | "p" | "pl" | "pw" | "epw" | "eph" | "ep"
>
cap: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
res: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
diode: () => FootprinterParamsBuilder<CommonPassiveOptionKey>
Expand Down Expand Up @@ -386,7 +400,11 @@ export const footprinter = (): Footprinter & {
}
}
return (v: any) => {
if (Object.keys(target).length === 0) {
if (
Object.keys(target).length === 0 ||
prop === "pdip" ||
prop === "pdip8"
) {
Comment on lines +403 to +407
Copy link
Contributor

Choose a reason for hiding this comment

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

The special case for pdip and pdip8 breaks the footprinter's chain logic. This condition allows these functions to be called even when target already contains a function definition, which would overwrite an existing footprint mid-chain.

For example, fp().dip(8).pdip() would incorrectly change from a dip to a pdip footprint instead of setting a parameter.

Fix:

if (Object.keys(target).length === 0) {
  if (`${prop}${v}` in FOOTPRINT_FN) {
    target[`${prop}${v}`] = true
    target.fn = `${prop}${v}`
  } else {
    target[prop] = true
    target.fn = prop
    if (prop === "res" || prop === "cap") {
      // ... existing logic
    } else {
      target.num_pins = Number.isNaN(Number.parseFloat(v))
        ? undefined
        : Number.parseFloat(v)
    }
  }
}

The special case should be removed since pdip and pdip8 are properly registered in FOOTPRINT_FN and will work correctly with the normal flow.

Suggested change
if (
Object.keys(target).length === 0 ||
prop === "pdip" ||
prop === "pdip8"
) {
if (Object.keys(target).length === 0) {

Spotted by Graphite Agent

Fix in Graphite


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

if (`${prop}${v}` in FOOTPRINT_FN) {
target[`${prop}${v}`] = true
target.fn = `${prop}${v}`
Expand Down
Loading
Loading