diff --git a/README.md b/README.md
index 25a06c4..2830f7f 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ jscad-electronics includes models for various components, including:
- ICs (DIP, SOIC, TSSOP, QFN, QFP, BGA)
- Diodes (SOD-123)
- Transistors (SOT-23, SOT-563, SOT-723)
+- Connectors (JST PH, SH, ZH, generic SMD, FPC, RJ45, pin headers)
- And more!
Check the `lib` directory for a full list of available components.
@@ -76,6 +77,15 @@ Refer to the individual component files for available customization options.
jscad-electronics is designed to work seamlessly with tscircuit. You can use these 3D models in your tscircuit projects to create accurate 3D representations of your PCB designs just by
using the `footprint` prop
+### JST footprint models
+
+`Footprinter3d` supports every JST variant exposed by footprinter:
+
+- `jst4` or `jst4_ph` for PH-style through-hole connectors
+- `jst4_sh` for SH 1 mm surface-mount connectors
+- `jst4_zh` for ZH 1.5 mm through-hole connectors
+- `jst4_smd` for generic parameterized surface-mount connectors
+
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
diff --git a/bun.lock b/bun.lock
index 94f0b94..d54b089 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,7 +6,7 @@
"name": "jscad-electronics",
"devDependencies": {
"@biomejs/biome": "^1.9.3",
- "@tscircuit/footprinter": "^0.0.400",
+ "@tscircuit/footprinter": "^0.0.401",
"@types/react": "19",
"@types/react-dom": "19",
"@types/three": "^0.179.0",
@@ -179,7 +179,7 @@
"@tscircuit/alphabet": ["@tscircuit/alphabet@0.0.24", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-vYYLwDwpTULFDwuqB5+U4RlTiHJ3Ez4Sf5OEv7rB1oVbhmh8p3Xl7h3vsi5eD4SE+x+0e8eSXKSlwSLCN94vZA=="],
- "@tscircuit/footprinter": ["@tscircuit/footprinter@0.0.400", "", { "dependencies": { "@tscircuit/mm": "^0.0.8", "zod": "^3.23.8" }, "peerDependencies": { "circuit-json": "*" } }, "sha512-wGs4RoR0a/ncIHSp522bc7cvLt1KcrmDpAsZGH5E1nJTBdXA7ltEt750bWaxRwzP1pv60YeiaDTCnFHjkOrC8g=="],
+ "@tscircuit/footprinter": ["@tscircuit/footprinter@0.0.401", "", { "dependencies": { "@tscircuit/mm": "^0.0.8", "zod": "^3.23.8" }, "peerDependencies": { "circuit-json": "*" } }, "sha512-C/Z7RQ2lGt2xTirwLC13yrs1d3IHMQ6ryNWKeVuKjN/ETUWYT+Zv6QJJZKjCoMdX5oxaopbHwX3ZcPFxJME21Q=="],
"@tscircuit/mm": ["@tscircuit/mm@0.0.8", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-nl7nxE7AhARbKuobflI0LUzoir7+wJyvwfPw6bzA/O0Q3YTcH3vBkU/Of+V/fp6ht+AofiCXj7YAH9E446138Q=="],
diff --git a/examples/jst-family.example.tsx b/examples/jst-family.example.tsx
new file mode 100644
index 0000000..c49c4ba
--- /dev/null
+++ b/examples/jst-family.example.tsx
@@ -0,0 +1,21 @@
+import { JsCadView, Translate } from "jscad-fiber"
+import { JSTPH2mm, JSTSH1mm, JSTSmd, JSTZH1_5mm } from "../lib"
+
+export default function Example() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/lib/Footprinter3d.tsx b/lib/Footprinter3d.tsx
index 60bf6b6..a43508c 100644
--- a/lib/Footprinter3d.tsx
+++ b/lib/Footprinter3d.tsx
@@ -56,7 +56,10 @@ import { AxialCapacitor } from "./AxialCapacitor"
import { StampBoard } from "./stampboard"
import { MountedPcbModule } from "./MountedPcbModule"
import SOD723 from "./SOD723"
+import { JSTPH2mm } from "./JSTPH2mm"
+import { JSTSH1mm } from "./JSTSH1mm"
import { JSTZH1_5mm } from "./JSTZH1_5mm"
+import { JSTSmd } from "./JSTSmd"
import { FPC } from "./FPC"
import { SmdPinHeader } from "./SmdPinHeader"
import { RJ45 } from "./RJ45"
@@ -83,6 +86,8 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => {
pw: number
num_pins: number
fn: string
+ ph?: boolean
+ sh?: boolean
zh?: boolean
thermalpad?: { x: number; y: number }
imperial: String
@@ -319,10 +324,47 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => {
/>
)
case "jst":
+ if (fpJson.smd) {
+ return (
+
+ )
+ }
+ if (fpJson.sh) {
+ return (
+
+ )
+ }
if (fpJson.zh) {
return
}
- break
+ return (
+
+ )
case "fpc":
return (
{
+ const pinSpan = (numPins - 1) * pitch
+ const resolvedBodyWidth = Math.max(bodyWidth ?? 0, pinSpan + 3.9)
+ const bodyCenterY = pinRowY - bodyDepth / 2 + 0.5
+ const bodyBottom = 0.2
+ const cavityDepth = Math.min(bodyDepth * 0.56, 2.8)
+ const cavityCenterY = bodyCenterY - bodyDepth / 2 + cavityDepth / 2 - 0.05
+ const cavityHeight = Math.max(bodyHeight - 1.5, 2)
+ const startX = -pinSpan / 2
+ const pinRadius = Math.max(0.2, Math.min(holeDiameter * 0.37, 0.32))
+ const horizontalPinEndY = bodyCenterY + bodyDepth * 0.05
+ const horizontalPinLength = Math.max(pinRowY - horizontalPinEndY, 0.8)
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {showPins &&
+ Array.from({ length: numPins }).map((_, index) => {
+ const x = startX + index * pitch
+ return (
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ {showFootprint &&
+ Array.from({ length: numPins }).map((_, index) => {
+ const hole: PcbPlatedHole = {
+ type: "pcb_plated_hole",
+ pcb_plated_hole_id: `jstph_${index}`,
+ shape: "circular_hole_with_rect_pad",
+ x: startX + index * pitch,
+ y: pinRowY,
+ hole_diameter: holeDiameter,
+ hole_shape: "circle",
+ pad_shape: "rect",
+ rect_pad_width: padWidth,
+ rect_pad_height: padLength,
+ layers: ["top", "bottom"],
+ port_hints: [`${index + 1}`],
+ }
+ return (
+
+ )
+ })}
+ >
+ )
+}
+
+export default JSTPH2mm
diff --git a/lib/JSTSH1mm.tsx b/lib/JSTSH1mm.tsx
new file mode 100644
index 0000000..2ffaf2a
--- /dev/null
+++ b/lib/JSTSH1mm.tsx
@@ -0,0 +1,147 @@
+import type { PcbSmtPadRect } from "circuit-json"
+import { Colorize, Cuboid, RoundedCuboid, Subtract } from "jscad-fiber"
+import { FootprintPad } from "./FootprintPad"
+
+export interface JSTSH1mmProps {
+ numPins?: number
+ pitch?: number
+ bodyDepth?: number
+ bodyHeight?: number
+ signalPadY?: number
+ padWidth?: number
+ padLength?: number
+ mountPadY?: number
+ mountPadWidth?: number
+ mountPadLength?: number
+ showPins?: boolean
+ showFootprint?: boolean
+ bodyColor?: string
+ pinColor?: string
+}
+
+/**
+ * Parametric JST SH-style, top-entry surface-mount header.
+ */
+export const JSTSH1mm = ({
+ numPins = 2,
+ pitch = 1,
+ bodyDepth = 4.25,
+ bodyHeight = 2.9,
+ signalPadY = -1.325,
+ padWidth = 0.6,
+ padLength = 1.55,
+ mountPadY = 1.22,
+ mountPadWidth = 1.2,
+ mountPadLength = 1.8,
+ showPins = true,
+ showFootprint = true,
+ bodyColor = "#eee9d8",
+ pinColor = "#b8a062",
+}: JSTSH1mmProps) => {
+ const pinSpan = (numPins - 1) * pitch
+ const bodyWidth = pinSpan + 3
+ const startX = -pinSpan / 2
+ const mountPadX = pinSpan / 2 + 1.3
+ const bodyCenterY = (signalPadY + mountPadY) / 2
+ const cavityWidth = Math.max(bodyWidth - 0.95, pitch * 0.8)
+ const cavityDepth = Math.max(bodyDepth - 1.05, 1.5)
+
+ const signalPads: PcbSmtPadRect[] = Array.from(
+ { length: numPins },
+ (_, index) => ({
+ type: "pcb_smtpad",
+ pcb_smtpad_id: `jstsh_signal_${index}`,
+ shape: "rect",
+ x: startX + index * pitch,
+ y: signalPadY,
+ width: padWidth,
+ height: padLength,
+ layer: "top",
+ port_hints: [`${index + 1}`],
+ }),
+ )
+ const mountPads: PcbSmtPadRect[] = [-mountPadX, mountPadX].map(
+ (x, index) => ({
+ type: "pcb_smtpad",
+ pcb_smtpad_id: `jstsh_mount_${index}`,
+ shape: "rect",
+ x,
+ y: mountPadY,
+ width: mountPadWidth,
+ height: mountPadLength,
+ layer: "top",
+ port_hints: [`${numPins + index + 1}`],
+ }),
+ )
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {showPins && (
+
+ {signalPads.map((pad, index) => (
+
+ ))}
+ {signalPads.map((pad, index) => (
+
+ ))}
+ {mountPads.map((pad, index) => (
+
+ ))}
+
+ )}
+
+ {showFootprint &&
+ [...signalPads, ...mountPads].map((pad, index) => (
+
+ ))}
+ >
+ )
+}
+
+export default JSTSH1mm
diff --git a/lib/JSTSmd.tsx b/lib/JSTSmd.tsx
new file mode 100644
index 0000000..1edfd1b
--- /dev/null
+++ b/lib/JSTSmd.tsx
@@ -0,0 +1,170 @@
+import type { PcbSmtPadRect } from "circuit-json"
+import { Colorize, Cuboid, RoundedCuboid, Subtract } from "jscad-fiber"
+import { FootprintPad } from "./FootprintPad"
+
+export interface JSTSmdProps {
+ numPins?: number
+ pitch?: number
+ bodyWidth?: number
+ bodyDepth?: number
+ bodyHeight?: number
+ signalPadY?: number
+ padWidth?: number
+ padLength?: number
+ mountPadPitchX?: number
+ mountPadRowDistance?: number
+ mountPadWidth?: number
+ mountPadLength?: number
+ mountTop?: boolean
+ showPins?: boolean
+ showFootprint?: boolean
+ bodyColor?: string
+ pinColor?: string
+}
+
+/**
+ * Generic parametric surface-mount JST-style wire-to-board header.
+ *
+ * This represents footprinter's configurable `jst*_smd` family rather than a
+ * single JST series. Its signal and hold-down pads are fully parameterized.
+ */
+export const JSTSmd = ({
+ numPins = 2,
+ pitch = 2,
+ bodyWidth = 8,
+ bodyDepth = 5,
+ bodyHeight = 3.8,
+ signalPadY = 0,
+ padWidth = 1,
+ padLength = 3,
+ mountPadPitchX = 5.4,
+ mountPadRowDistance = 2.5,
+ mountPadWidth = 1.8,
+ mountPadLength = 3,
+ mountTop = false,
+ showPins = true,
+ showFootprint = true,
+ bodyColor = "#eee9dc",
+ pinColor = "#b7a167",
+}: JSTSmdProps) => {
+ const pinSpan = (numPins - 1) * pitch
+ const resolvedBodyWidth = Math.max(
+ bodyWidth,
+ pinSpan + padWidth + 1.4,
+ mountPadPitchX + mountPadWidth * 0.55,
+ )
+ const startX = -pinSpan / 2
+ const mountPadY =
+ mountPadRowDistance === 0
+ ? signalPadY
+ : signalPadY + (mountTop ? 1 : -1) * mountPadRowDistance
+ const bodyCenterY = (signalPadY + mountPadY) / 2
+ const openingDirection = mountPadY <= signalPadY ? 1 : -1
+ const openingDepth = Math.min(bodyDepth * 0.54, 3)
+ const openingCenterY =
+ bodyCenterY + openingDirection * (bodyDepth / 2 - openingDepth / 2 + 0.05)
+
+ const signalPads: PcbSmtPadRect[] = Array.from(
+ { length: numPins },
+ (_, index) => ({
+ type: "pcb_smtpad",
+ pcb_smtpad_id: `jstsmd_signal_${index}`,
+ shape: "rect",
+ x: startX + index * pitch,
+ y: signalPadY,
+ width: padWidth,
+ height: padLength,
+ layer: "top",
+ port_hints: [`${index + 1}`],
+ }),
+ )
+ const mountPads: PcbSmtPadRect[] = [
+ -mountPadPitchX / 2,
+ mountPadPitchX / 2,
+ ].map((x, index) => ({
+ type: "pcb_smtpad",
+ pcb_smtpad_id: `jstsmd_mount_${index}`,
+ shape: "rect",
+ x,
+ y: mountPadY,
+ width: mountPadWidth,
+ height: mountPadLength,
+ layer: "top",
+ port_hints: [`${numPins + index + 1}`],
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {showPins && (
+
+ {signalPads.map((pad, index) => (
+
+ ))}
+ {signalPads.map((pad, index) => (
+
+ ))}
+ {mountPads.map((pad, index) => (
+
+ ))}
+
+ )}
+
+ {showFootprint &&
+ [...signalPads, ...mountPads].map((pad, index) => (
+
+ ))}
+ >
+ )
+}
+
+export default JSTSmd
diff --git a/lib/index.ts b/lib/index.ts
index a30e911..d136769 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -70,6 +70,9 @@ export * from "./TO220"
export * from "./TO92"
export * from "./MountedPcbModule"
export * from "./Screen"
+export * from "./JSTPH2mm"
+export * from "./JSTSH1mm"
+export * from "./JSTSmd"
export * from "./JSTZH1_5mm"
export * from "./FPC"
export * from "./RJ45"
diff --git a/package.json b/package.json
index 7ab6431..0f9ec8a 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.3",
- "@tscircuit/footprinter": "^0.0.400",
+ "@tscircuit/footprinter": "^0.0.401",
"@types/react": "19",
"@types/react-dom": "19",
"@types/three": "^0.179.0",
diff --git a/tests/snapshots/__snapshots__/jst-ph.snap.png b/tests/snapshots/__snapshots__/jst-ph.snap.png
new file mode 100644
index 0000000..37bf7a9
Binary files /dev/null and b/tests/snapshots/__snapshots__/jst-ph.snap.png differ
diff --git a/tests/snapshots/__snapshots__/jst-sh.snap.png b/tests/snapshots/__snapshots__/jst-sh.snap.png
new file mode 100644
index 0000000..648f120
Binary files /dev/null and b/tests/snapshots/__snapshots__/jst-sh.snap.png differ
diff --git a/tests/snapshots/__snapshots__/jst-smd.snap.png b/tests/snapshots/__snapshots__/jst-smd.snap.png
new file mode 100644
index 0000000..ed3cf46
Binary files /dev/null and b/tests/snapshots/__snapshots__/jst-smd.snap.png differ
diff --git a/tests/snapshots/__snapshots__/jst-zh.snap.png b/tests/snapshots/__snapshots__/jst-zh.snap.png
new file mode 100644
index 0000000..aae15aa
Binary files /dev/null and b/tests/snapshots/__snapshots__/jst-zh.snap.png differ
diff --git a/tests/snapshots/jst-family.test.ts b/tests/snapshots/jst-family.test.ts
new file mode 100644
index 0000000..bca2da2
--- /dev/null
+++ b/tests/snapshots/jst-family.test.ts
@@ -0,0 +1,42 @@
+import { expect, test } from "bun:test"
+import "../fixtures/png-matcher"
+import { renderFootprint } from "../helpers/render-footprint"
+
+const variants = [
+ {
+ name: "jst-ph",
+ footprint: "jst4_ph",
+ renderOptions: {
+ camPos: [11, 16, 12] as [number, number, number],
+ lookAt: [0, 1.5, 1.8] as [number, number, number],
+ },
+ },
+ {
+ name: "jst-sh",
+ footprint: "jst4_sh",
+ renderOptions: { cameraPreset: "top-left-corner" as const },
+ },
+ {
+ name: "jst-zh",
+ footprint: "jst4_zh",
+ renderOptions: { cameraPreset: "top-left-corner" as const },
+ },
+ {
+ name: "jst-smd",
+ footprint: "jst4_smd_p2mm_mpx8mm_mpy3mm_mpw2mm_mpl3.4mm_mounttop",
+ renderOptions: {
+ camPos: [11, 16, 12] as [number, number, number],
+ lookAt: [0, 1.5, 1.8] as [number, number, number],
+ },
+ },
+] as const
+
+for (const variant of variants) {
+ test(`${variant.name} footprint model`, async () => {
+ const pngBuffer = await renderFootprint(
+ variant.footprint,
+ variant.renderOptions,
+ )
+ await expect(pngBuffer).toMatchPngSnapshot(import.meta.path, variant.name)
+ })
+}
diff --git a/tests/vanilla-jst.test.ts b/tests/vanilla-jst.test.ts
new file mode 100644
index 0000000..bfb5e47
--- /dev/null
+++ b/tests/vanilla-jst.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from "bun:test"
+import * as jscadModeling from "@jscad/modeling"
+import { importVanilla } from "./fixtures/importVanilla.js"
+
+const supportedJstFootprints = [
+ "jst2",
+ "jst2_ph",
+ "jst2_sh",
+ "jst2_zh",
+ "jst2_smd",
+] as const
+
+for (const footprint of supportedJstFootprints) {
+ test(`vanilla build returns geometries for ${footprint}`, async () => {
+ const { getJscadModelForFootprint } = await importVanilla()
+ const result = getJscadModelForFootprint(footprint, jscadModeling)
+
+ expect(result.geometries.length).toBeGreaterThan(0)
+ expect(
+ result.geometries.some(({ color }: { color?: unknown }) => color != null),
+ ).toBe(true)
+ })
+}
+
+test("generic JST SMD honors custom signal and mounting pad parameters", async () => {
+ const { getJscadModelForFootprint } = await importVanilla()
+ const result = getJscadModelForFootprint(
+ "jst4_smd_p2mm_mpx8mm_mpy3mm_mpw2mm_mpl3.4mm_mounttop",
+ jscadModeling,
+ )
+ const [minimum, maximum] =
+ jscadModeling.measurements.measureAggregateBoundingBox(
+ ...result.geometries.map(({ geom }: { geom: any }) => geom),
+ )
+
+ expect(result.geometries.length).toBeGreaterThan(0)
+ expect(minimum[0] <= -4.99).toBe(true)
+ expect(maximum[0] >= 4.99).toBe(true)
+ expect(maximum[1] >= 4.69).toBe(true)
+})