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) +})