From f846ad9047f291cf62b14b6829ee1ab61427c337 Mon Sep 17 00:00:00 2001 From: Dasdebsankar54 Date: Tue, 28 Jul 2026 22:51:25 +0530 Subject: [PATCH 1/2] feat: full footprinter coverage in Footprinter3d + coverage test Problem - Footprinter3d silently returned null for ~40 of the 97 footprint functions exposed by @tscircuit/footprinter (about 100 models when counting pin/size variants), so components vanished from boards with no error. JST connectors parsed fine but only rendered for the ZH series; SH and PH fell through. Several components already existed in lib/ but were never wired into the switch or exported, and every new footprinter release added more silent gaps. Root cause - No machine-checkable coverage: a null render does not throw, no test enumerated footprinter names, so gaps were invisible and the backlog regrew automatically. 15 component files were not even exported from index.ts. Fix - Add tests/footprinter-coverage.test.ts: iterates all 97 names from fp.getFootprintNames() and asserts each renders non-null, turning the backlog into a self-updating checklist that fails CI when footprinter adds packages. 'pad' and 'vson' are broken upstream in footprinter itself (parser throws) and are pinned by a companion test until fixed there. - New parametric JST component (series zh/sh/ph presets) replacing the ZH-only wiring; legacy jstzh1_5mm strings still work. - 13 new components: JST, SmdDiode (sod110/sod80/sod323w/sod882d/ smbf), DPAK (dpak/to252/d2pak/to263), SOT89, SOT343, Crystal, ElectrolyticCapacitor, Potentiometer, SmdPushButton, LED2835, LED5050, SolderJumper, M2Host. - Wire existing-but-unreachable components: BGA (bga/lga), SOD123, SOT233P (sot23), SOT563, RadialCapacitor (radial), USB_C (usbcmidmount), QFN for quad/mlp/son/vson/wson, SOIC for sop8/ssop, StampBoard for breakoutheaders, FootprintPad/ FootprintPlatedHole for smtpad/pad/platedhole, TO220/TO92 for their f/l/s variants. - Complete index.ts exports for all previously unexported components. - Add Ellipsoid to the vanilla primitive shim (required by USB-C, which previously broke the vanilla bundle once reachable). - 15 cosmos examples for the new models. Verification - bun test: 82/82 pass, including all 73 pre-existing snapshot tests, so existing model output is unchanged (purely additive change). - bun run build succeeds for both index and vanilla entries. --- examples/Crystal.example.tsx | 10 + examples/DPAK.example.tsx | 10 + examples/ElectrolyticCapacitor.example.tsx | 10 + examples/JST-PH.example.tsx | 10 + examples/JST-SH.example.tsx | 10 + examples/JST-ZH.example.tsx | 10 + examples/LED2835.example.tsx | 10 + examples/LED5050.example.tsx | 10 + examples/M2Host.example.tsx | 10 + examples/Potentiometer.example.tsx | 10 + examples/SOT343.example.tsx | 10 + examples/SOT89.example.tsx | 10 + examples/SmdDiode.example.tsx | 10 + examples/SmdPushButton.example.tsx | 10 + examples/SolderJumper.example.tsx | 10 + lib/Crystal.tsx | 60 +++++ lib/DPAK.tsx | 92 +++++++ lib/ElectrolyticCapacitor.tsx | 77 ++++++ lib/Footprinter3d.tsx | 273 ++++++++++++++++++++- lib/JST.tsx | 263 ++++++++++++++++++++ lib/LED2835.tsx | 50 ++++ lib/LED5050.tsx | 60 +++++ lib/M2Host.tsx | 67 +++++ lib/Potentiometer.tsx | 65 +++++ lib/SOT343.tsx | 69 ++++++ lib/SOT89.tsx | 70 ++++++ lib/SmdDiode.tsx | 77 ++++++ lib/SmdPushButton.tsx | 69 ++++++ lib/SolderJumper.tsx | 45 ++++ lib/index.ts | 28 +++ lib/vanilla/primitives.ts | 1 + lib/vanilla/render.ts | 6 + tests/footprinter-coverage.test.ts | 69 ++++++ 33 files changed, 1585 insertions(+), 6 deletions(-) create mode 100644 examples/Crystal.example.tsx create mode 100644 examples/DPAK.example.tsx create mode 100644 examples/ElectrolyticCapacitor.example.tsx create mode 100644 examples/JST-PH.example.tsx create mode 100644 examples/JST-SH.example.tsx create mode 100644 examples/JST-ZH.example.tsx create mode 100644 examples/LED2835.example.tsx create mode 100644 examples/LED5050.example.tsx create mode 100644 examples/M2Host.example.tsx create mode 100644 examples/Potentiometer.example.tsx create mode 100644 examples/SOT343.example.tsx create mode 100644 examples/SOT89.example.tsx create mode 100644 examples/SmdDiode.example.tsx create mode 100644 examples/SmdPushButton.example.tsx create mode 100644 examples/SolderJumper.example.tsx create mode 100644 lib/Crystal.tsx create mode 100644 lib/DPAK.tsx create mode 100644 lib/ElectrolyticCapacitor.tsx create mode 100644 lib/JST.tsx create mode 100644 lib/LED2835.tsx create mode 100644 lib/LED5050.tsx create mode 100644 lib/M2Host.tsx create mode 100644 lib/Potentiometer.tsx create mode 100644 lib/SOT343.tsx create mode 100644 lib/SOT89.tsx create mode 100644 lib/SmdDiode.tsx create mode 100644 lib/SmdPushButton.tsx create mode 100644 lib/SolderJumper.tsx create mode 100644 tests/footprinter-coverage.test.ts diff --git a/examples/Crystal.example.tsx b/examples/Crystal.example.tsx new file mode 100644 index 00000000..cda7a465 --- /dev/null +++ b/examples/Crystal.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { Crystal } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/DPAK.example.tsx b/examples/DPAK.example.tsx new file mode 100644 index 00000000..bbdd227a --- /dev/null +++ b/examples/DPAK.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { DPAK } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/ElectrolyticCapacitor.example.tsx b/examples/ElectrolyticCapacitor.example.tsx new file mode 100644 index 00000000..cfb802c9 --- /dev/null +++ b/examples/ElectrolyticCapacitor.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { ElectrolyticCapacitor } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/JST-PH.example.tsx b/examples/JST-PH.example.tsx new file mode 100644 index 00000000..076160f6 --- /dev/null +++ b/examples/JST-PH.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { JST } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/JST-SH.example.tsx b/examples/JST-SH.example.tsx new file mode 100644 index 00000000..53e69228 --- /dev/null +++ b/examples/JST-SH.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { JST } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/JST-ZH.example.tsx b/examples/JST-ZH.example.tsx new file mode 100644 index 00000000..4419decf --- /dev/null +++ b/examples/JST-ZH.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { JST } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/LED2835.example.tsx b/examples/LED2835.example.tsx new file mode 100644 index 00000000..e09dcf46 --- /dev/null +++ b/examples/LED2835.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { LED2835 } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/LED5050.example.tsx b/examples/LED5050.example.tsx new file mode 100644 index 00000000..1f6e235d --- /dev/null +++ b/examples/LED5050.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { LED5050 } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/M2Host.example.tsx b/examples/M2Host.example.tsx new file mode 100644 index 00000000..e06af912 --- /dev/null +++ b/examples/M2Host.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { M2Host } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/Potentiometer.example.tsx b/examples/Potentiometer.example.tsx new file mode 100644 index 00000000..08502d77 --- /dev/null +++ b/examples/Potentiometer.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { Potentiometer } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/SOT343.example.tsx b/examples/SOT343.example.tsx new file mode 100644 index 00000000..d18d0a5e --- /dev/null +++ b/examples/SOT343.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { SOT343 } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/SOT89.example.tsx b/examples/SOT89.example.tsx new file mode 100644 index 00000000..71d59917 --- /dev/null +++ b/examples/SOT89.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { SOT89 } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/SmdDiode.example.tsx b/examples/SmdDiode.example.tsx new file mode 100644 index 00000000..d408890a --- /dev/null +++ b/examples/SmdDiode.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { SmdDiode } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/SmdPushButton.example.tsx b/examples/SmdPushButton.example.tsx new file mode 100644 index 00000000..25dab116 --- /dev/null +++ b/examples/SmdPushButton.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { SmdPushButton } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/examples/SolderJumper.example.tsx b/examples/SolderJumper.example.tsx new file mode 100644 index 00000000..a6edecd4 --- /dev/null +++ b/examples/SolderJumper.example.tsx @@ -0,0 +1,10 @@ +import { JsCadView } from "jscad-fiber" +import { SolderJumper } from "../lib" + +export default function Example() { + return ( + + + + ) +} diff --git a/lib/Crystal.tsx b/lib/Crystal.tsx new file mode 100644 index 00000000..141c9191 --- /dev/null +++ b/lib/Crystal.tsx @@ -0,0 +1,60 @@ +import { Colorize, Cuboid } from "jscad-fiber" + +interface CrystalProps { + /** pad pitch X (footprinter px) */ + padPitchX?: number + /** pad pitch Y (footprinter py) */ + padPitchY?: number + padWidth?: number + padLength?: number + bodyWidth?: number + bodyLength?: number + bodyHeight?: number +} + +/** + * 4-pad SMD quartz crystal: metal can over a dark base. + */ +export const Crystal = ({ + padPitchX = 2.2, + padPitchY = 1.7, + padWidth = 1.4, + padLength = 1.2, + bodyWidth = 3.2, + bodyLength = 2.5, + bodyHeight = 0.8, +}: CrystalProps) => { + return ( + <> + {/* Corner pads */} + {[-1, 1].map((sx) => + [-1, 1].map((sy) => ( + + + + )), + )} + + {/* Dark base */} + + + + + {/* Metal can */} + + + + + ) +} + +export default Crystal diff --git a/lib/DPAK.tsx b/lib/DPAK.tsx new file mode 100644 index 00000000..c7f9ff1a --- /dev/null +++ b/lib/DPAK.tsx @@ -0,0 +1,92 @@ +import { ChipBody } from "./ChipBody" +import { SmdChipLead } from "./SmdChipLead" + +interface DPAKProps { + /** number of gull-wing leads (tab side excluded), default 3 */ + numPins?: number + /** package width across leads (footprint w) */ + bodyWidth?: number + /** package length (footprint h) */ + bodyLength?: number + bodyHeight?: number + /** pitch between gull-wing leads */ + pitch?: number + leadWidth?: number + leadThickness?: number + leadHeight?: number + padContactLength?: number + /** heatsink tab width (footprint tabw) */ + tabWidth?: number + /** heatsink tab length (footprint tabh) */ + tabLength?: number +} + +/** + * Parametric DPAK-family package: DPAK / TO-252, D2PAK / TO-263. + * Gull-wing leads on one side, wide heatsink tab on the other. + */ +export const DPAK = ({ + numPins = 3, + bodyWidth = 6.6, + bodyLength = 6.5, + bodyHeight = 2.3, + pitch = 2.29, + leadWidth = 0.9, + leadThickness = 0.25, + leadHeight = 1.15, + padContactLength = 0.6, + tabWidth = 6.2, + tabLength = 2.5, +}: DPAKProps) => { + const leadSpan = (numPins - 1) * pitch + const startY = -leadSpan / 2 + + return ( + <> + {/* Gull-wing leads on the left side */} + {Array.from({ length: numPins }).map((_, i) => ( + + ))} + + {/* Heatsink tab on the right side */} + + + {/* Package body */} + + + ) +} + +export default DPAK diff --git a/lib/ElectrolyticCapacitor.tsx b/lib/ElectrolyticCapacitor.tsx new file mode 100644 index 00000000..957dac6f --- /dev/null +++ b/lib/ElectrolyticCapacitor.tsx @@ -0,0 +1,77 @@ +import { Colorize, Cylinder, Subtract, Cuboid, Translate } from "jscad-fiber" + +interface ElectrolyticCapacitorProps { + /** can diameter in mm (footprinter `d`) */ + diameter?: number + height?: number + pitch?: number + bodyColor?: string + topColor?: string +} + +/** + * Radial aluminum electrolytic capacitor (vertical can). + */ +export const ElectrolyticCapacitor = ({ + diameter = 10.5, + height = 12.5, + pitch = 7.5, + bodyColor = "#1a1a5e", + topColor = "#c0c0c0", +}: ElectrolyticCapacitorProps) => { + const radius = diameter / 2 + const legRadius = 0.25 + + return ( + <> + {/* Can body */} + + + + + {/* Metal top with score lines */} + + + + + + + + + {/* Black base ring */} + + + + + {/* Leads */} + + + + + + ) +} + +export default ElectrolyticCapacitor diff --git a/lib/Footprinter3d.tsx b/lib/Footprinter3d.tsx index 60bf6b67..4794a9fc 100644 --- a/lib/Footprinter3d.tsx +++ b/lib/Footprinter3d.tsx @@ -52,14 +52,42 @@ import { SOT886 } from "./SOT-886" import { SOD323 } from "./sod-323" import { SOD323F } from "./sod-323F" import { SOD323FL } from "./sod-323FL" +import { SOD123 } from "./sod-123" import { AxialCapacitor } from "./AxialCapacitor" import { StampBoard } from "./stampboard" import { MountedPcbModule } from "./MountedPcbModule" import SOD723 from "./SOD723" import { JSTZH1_5mm } from "./JSTZH1_5mm" +import { JST } from "./JST" import { FPC } from "./FPC" import { SmdPinHeader } from "./SmdPinHeader" import { RJ45 } from "./RJ45" +import { BGA } from "./BGA" +import { SOT233P } from "./SOT-23-3P" +import { SOT563 } from "./SOT-563" +import { SOT89 } from "./SOT89" +import { SOT343 } from "./SOT343" +import { SmdDiode } from "./SmdDiode" +import { DPAK } from "./DPAK" +import { RadialCapacitor } from "./RadialCapacitor" +import { ElectrolyticCapacitor } from "./ElectrolyticCapacitor" +import { Potentiometer } from "./Potentiometer" +import { Crystal } from "./Crystal" +import { SmdPushButton } from "./SmdPushButton" +import { LED2835 } from "./LED2835" +import { LED5050 } from "./LED5050" +import { SolderJumper } from "./SolderJumper" +import { M2Host } from "./M2Host" +import { USB_C } from "./USB-C" +import { FootprintPad } from "./FootprintPad" +import { FootprintPlatedHole } from "./FootprintPlatedHole" +import type { PcbPlatedHole, PcbSmtPad } from "circuit-json" + +/** Parse footprinter values that may be numbers or strings like "3mm" */ +const fpNum = (v: unknown, fallback = 0): number => { + const n = typeof v === "number" ? v : Number.parseFloat(String(v)) + return Number.isFinite(n) ? n : fallback +} /** * Outputs a 3d model for any [footprinter string](https://github.com/tscircuit/footprinter) @@ -84,6 +112,8 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { num_pins: number fn: string zh?: boolean + sh?: boolean + ph?: boolean thermalpad?: { x: number; y: number } imperial: String male: boolean @@ -131,6 +161,8 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { ledp?: number ledy?: number bodyy?: number + // Allow any additional footprinter fields (grid, tabw, epw, ...) + [key: string]: unknown } switch (fpJson.fn) { @@ -304,8 +336,6 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { return case "sod323fl": return - case "sot363": - return case "sot886": return case "sot963": @@ -318,11 +348,242 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { innerDiameter={fpJson.id} /> ) - case "jst": - if (fpJson.zh) { - return + case "jst": { + const series = fpJson.zh ? "zh" : fpJson.sh ? "sh" : "ph" + return + } + case "bga": + case "lga": + return + case "quad": + case "mlp": { + const hasThermalPad = fpJson.fn === "mlp" || !!fpJson.thermalpad + return ( + + ) + } + case "son": + case "vson": + case "wson": { + const epw = fpNum(fpJson.epw, 0) + const eph = fpNum(fpJson.eph, 0) + const hasThermalPad = epw > 0 && eph > 0 + return ( + + ) + } + case "sop8": + case "ssop": + return ( + + ) + case "sot": + case "sot363": + return + case "sot23": + return + case "sot25": + return + case "sot563": + return + case "sot343": + return + case "sot89": + return + case "dpak": + case "to252": + return ( + + ) + case "d2pak": + case "to263": + return ( + + ) + case "to220f": + return + case "to92l": + case "to92s": + return + case "radial": + return + case "electrolytic": + return ( + + ) + case "potentiometer": + return ( + + ) + case "crystal": + return ( + + ) + case "smdpushbutton": + return ( + + ) + case "led2835": + return + case "led5050": + return ( + + ) + case "solderjumper": + return + case "m2host": + return + case "usbcmidmount": + return + case "breakoutheaders": + return ( + + ) + case "smbf": + return ( + + ) + case "sod110": + return ( + + ) + case "sod80": + return ( + + ) + case "sod323w": + return ( + + ) + case "sod882d": + return ( + + ) + case "sod123": + return + case "smtpad": + case "pad": { + const pad: PcbSmtPad = { + type: "pcb_smtpad", + pcb_smtpad_id: "smtpad_0", + shape: "rect", + x: 0, + y: 0, + width: fpNum(fpJson.width ?? fpJson.pw, 1), + height: fpNum(fpJson.height ?? fpJson.ph, 1), + layer: "top", } - break + return + } + case "platedhole": { + const hole: PcbPlatedHole = { + type: "pcb_plated_hole", + pcb_plated_hole_id: "platedhole_0", + shape: "circle", + x: 0, + y: 0, + hole_diameter: fpNum(fpJson.d, 1), + outer_diameter: fpNum(fpJson.pd, 1.5), + layers: ["top", "bottom"], + } + return + } case "fpc": return ( = { + // JST ZH 1.5mm through-hole (top entry) + zh: { + pitch: 1.5, + bodyHeight: 6, + bodyDepth: 3.5, + sideMargin: 2, + pinRadius: 0.35, + pinLength: 6, + smd: false, + holeDiameter: 0.73, + padWidth: 1.03, + padHeight: 1.73, + }, + // JST SH 1.0mm surface-mount (top entry) + sh: { + pitch: 1.0, + bodyHeight: 3.9, + bodyDepth: 4.2, + sideMargin: 1.9, + pinRadius: 0.25, + pinLength: 3.5, + smd: true, + holeDiameter: 0, + padWidth: 0.6, + padHeight: 1.6, + }, + // JST PH 2.0mm through-hole (top entry) + ph: { + pitch: 2.0, + bodyHeight: 6.5, + bodyDepth: 4.5, + sideMargin: 2.25, + pinRadius: 0.35, + pinLength: 6.5, + smd: false, + holeDiameter: 0.75, + padWidth: 1.1, + padHeight: 1.8, + }, +} + +interface JSTProps { + numPins?: number + series?: JstSeries + showPins?: boolean + showFootprint?: boolean + bodyColor?: string + pinColor?: string +} + +/** + * Parametric JST connector (ZH / SH / PH series). + * ZH and PH are through-hole top-entry, SH is surface-mount top-entry. + */ +export const JST = ({ + numPins = 4, + series = "ph", + showPins = true, + showFootprint = true, + bodyColor = "#f5f5f5", + pinColor = "#635959", +}: JSTProps) => { + const dims = SERIES_DIMS[series] + const { pitch, bodyHeight, bodyDepth, sideMargin, pinRadius, pinLength } = + dims + const wallThickness = 0.5 + const hollowHeight = bodyHeight * 0.6 + const bodyWidth = (numPins - 1) * pitch + sideMargin * 2 + const startX = -((numPins - 1) * pitch) / 2 + + return ( + <> + + + + + + + + + + + + + + + + + + + {showPins && + Array.from({ length: numPins }).map((_, i) => + dims.smd ? ( + // SMD gull-wing contact: short vertical stub + horizontal foot + + + + + ) : ( + + + + ), + )} + + {showFootprint && + !dims.smd && + Array.from({ length: numPins }).map((_, i) => { + const isPin1 = i === 0 + const hole: PcbPlatedHole = isPin1 + ? { + type: "pcb_plated_hole", + pcb_plated_hole_id: `jst${series}_${i}`, + shape: "circular_hole_with_rect_pad", + x: startX + i * pitch, + y: 0, + hole_diameter: dims.holeDiameter, + rect_pad_width: dims.padWidth, + rect_pad_height: dims.padHeight, + hole_shape: "circle", + pad_shape: "rect", + layers: ["top", "bottom"], + port_hints: [`${i + 1}`], + } + : { + type: "pcb_plated_hole", + pcb_plated_hole_id: `jst${series}_${i}`, + shape: "pill", + x: startX + i * pitch, + y: 0, + hole_height: dims.holeDiameter, + hole_width: dims.holeDiameter, + outer_height: dims.padHeight, + outer_width: dims.padWidth, + layers: ["top", "bottom"], + port_hints: [`${i + 1}`], + } + return ( + + ) + })} + + {showFootprint && + dims.smd && + Array.from({ length: numPins }).map((_, i) => { + const pad: PcbSmtPad = { + type: "pcb_smtpad", + pcb_smtpad_id: `jst${series}_${i}`, + shape: "rect", + x: startX + i * pitch, + y: bodyDepth / 2 + dims.padHeight / 2 - 0.2, + width: dims.padWidth, + height: dims.padHeight, + layer: "top", + port_hints: [`${i + 1}`], + } + return ( + + ) + })} + + ) +} + +export default JST diff --git a/lib/LED2835.tsx b/lib/LED2835.tsx new file mode 100644 index 00000000..88acf550 --- /dev/null +++ b/lib/LED2835.tsx @@ -0,0 +1,50 @@ +import { Colorize, Cuboid } from "jscad-fiber" + +interface LED2835Props { + bodyWidth?: number + bodyLength?: number + bodyHeight?: number +} + +/** + * 2835 SMD LED (2.8 x 3.5mm): white package with yellow phosphor area. + */ +export const LED2835 = ({ + bodyWidth = 3.5, + bodyLength = 2.8, + bodyHeight = 0.8, +}: LED2835Props) => { + return ( + <> + {/* Two pads underneath (asymmetric, anode wider) */} + + + + + + {/* White body */} + + + + + {/* Phosphor / emitting area */} + + + + + ) +} + +export default LED2835 diff --git a/lib/LED5050.tsx b/lib/LED5050.tsx new file mode 100644 index 00000000..81cba691 --- /dev/null +++ b/lib/LED5050.tsx @@ -0,0 +1,60 @@ +import { Colorize, Cuboid, Cylinder } from "jscad-fiber" + +interface LED5050Props { + bodyWidth?: number + bodyLength?: number + bodyHeight?: number + pitch?: number + rowSpan?: number +} + +/** + * 5050 SMD RGB LED (5 x 5mm): white package, 6 pads (3 per side), + * three emitting dots for R/G/B dies. + */ +export const LED5050 = ({ + bodyWidth = 5, + bodyLength = 5, + bodyHeight = 1.6, + pitch = 1.7, + rowSpan = 4.8, +}: LED5050Props) => { + const startY = -pitch + + return ( + <> + {/* 3 pads per side */} + {[-1, 1].map((sx) => + [0, 1, 2].map((i) => ( + + + + )), + )} + + {/* White body */} + + + + + {/* Emitting dots (R/G/B) */} + {["#e74c3c", "#2ecc71", "#3498db"].map((color, i) => ( + + + + ))} + + ) +} + +export default LED5050 diff --git a/lib/M2Host.tsx b/lib/M2Host.tsx new file mode 100644 index 00000000..262e89ea --- /dev/null +++ b/lib/M2Host.tsx @@ -0,0 +1,67 @@ +import { Colorize, Cuboid, Subtract } from "jscad-fiber" + +interface M2HostProps { + bodyWidth?: number + bodyDepth?: number + bodyHeight?: number + pinCount?: number + pinPitch?: number +} + +/** + * M.2 (NGFF) host connector: low elongated body with a card slot. + */ +export const M2Host = ({ + bodyWidth = 22, + bodyDepth = 8.7, + bodyHeight = 3.2, + pinCount = 67, + pinPitch = 0.5, +}: M2HostProps) => { + const slotDepth = bodyDepth * 0.55 + + return ( + <> + {/* Body with card slot */} + + + + {/* card slot opening on the front face */} + + + + + {/* Contact fingers inside the slot */} + {Array.from({ length: Math.min(pinCount, 32) }).map((_, i) => ( + + + + ))} + + {/* Mounting posts at both ends */} + {[-1, 1].map((s) => ( + + + + ))} + + ) +} + +export default M2Host diff --git a/lib/Potentiometer.tsx b/lib/Potentiometer.tsx new file mode 100644 index 00000000..fab4efa8 --- /dev/null +++ b/lib/Potentiometer.tsx @@ -0,0 +1,65 @@ +import { Colorize, Cuboid, Cylinder, Subtract } from "jscad-fiber" + +interface PotentiometerProps { + /** body diameter (footprinter `ca`) */ + diameter?: number + /** body height (footprinter `h`) */ + height?: number + /** pin pitch (footprinter `p`) */ + pitch?: number + numPins?: number +} + +/** + * Through-hole trimmer/rotary potentiometer: cylindrical body, + * shaft on top, pins along one edge. + */ +export const Potentiometer = ({ + diameter = 14, + height = 4, + pitch = 5, + numPins = 3, +}: PotentiometerProps) => { + const radius = diameter / 2 + const startX = -((numPins - 1) * pitch) / 2 + + return ( + <> + {/* Body */} + + + + + {/* Shaft with slot */} + + + + + + + + {/* Pins */} + {Array.from({ length: numPins }).map((_, i) => ( + + + + ))} + + ) +} + +export default Potentiometer diff --git a/lib/SOT343.tsx b/lib/SOT343.tsx new file mode 100644 index 00000000..3dd797ae --- /dev/null +++ b/lib/SOT343.tsx @@ -0,0 +1,69 @@ +import { ChipBody } from "./ChipBody" +import { SmdChipLead } from "./SmdChipLead" + +/** + * SOT-343 package: 4 gull-wing leads, 2 per side. + */ +export const SOT343 = () => { + const fullWidth = 3.2 + const bodyWidth = 1.9 + const bodyLength = 2.1 + const bodyHeight = 1.1 + const leadWidth = 0.4 + const leadThickness = 0.15 + const leadHeight = 0.55 + const padContactLength = 0.4 + const padPitch = 1.3 + + const bodyDistance = (fullWidth - bodyWidth) / 2 + + return ( + <> + {/* 2 leads on the left side */} + {[-padPitch / 2, padPitch / 2].map((y, i) => ( + + ))} + + {/* 2 leads on the right side */} + {[-padPitch / 2, padPitch / 2].map((y, i) => ( + + ))} + + {/* Chip Body */} + + + ) +} + +export default SOT343 diff --git a/lib/SOT89.tsx b/lib/SOT89.tsx new file mode 100644 index 00000000..e7bc51a3 --- /dev/null +++ b/lib/SOT89.tsx @@ -0,0 +1,70 @@ +import { ChipBody } from "./ChipBody" +import { SmdChipLead } from "./SmdChipLead" + +/** + * SOT-89 package: 3 gull-wing leads on one side, wide tab on the other. + */ +export const SOT89 = () => { + const fullWidth = 4.2 + const bodyWidth = 2.6 + const bodyLength = 4.5 + const bodyHeight = 1.5 + const leadWidth = 0.5 + const tabLeadWidth = 1.9 + const leadThickness = 0.2 + const leadHeight = 0.75 + const padContactLength = 0.5 + const padPitch = 1.5 + + const extendedBodyDistance = fullWidth - bodyWidth + + return ( + <> + {/* Tab on the right side */} + + + {/* 3 leads on the left side */} + {[-padPitch, 0, padPitch].map((y, i) => ( + + ))} + + {/* Chip Body */} + + + ) +} + +export default SOT89 diff --git a/lib/SmdDiode.tsx b/lib/SmdDiode.tsx new file mode 100644 index 00000000..12bd4a68 --- /dev/null +++ b/lib/SmdDiode.tsx @@ -0,0 +1,77 @@ +import { ChipBody } from "./ChipBody" +import { SmdChipLead } from "./SmdChipLead" + +interface SmdDiodeProps { + /** total width including leads (footprint w) */ + fullWidth?: number + bodyWidth?: number + bodyLength?: number + bodyHeight?: number + leadWidth?: number + leadThickness?: number + leadHeight?: number + padContactLength?: number +} + +/** + * Generic parametric 2-pin SMD diode/small-2-terminal package + * (covers SOD-110, SOD-80, SOD-323W, SOD-882D, SMBF and similar). + */ +export const SmdDiode = ({ + fullWidth = 3.3, + bodyWidth = fullWidth * 0.72, + bodyLength = 1.7, + bodyHeight = bodyLength * 0.7, + leadWidth = 0.8, + leadThickness = 0.15, + leadHeight = 0.5, + padContactLength = 0.4, +}: SmdDiodeProps) => { + const padThickness = leadThickness / 2 + const bodyDistance = (fullWidth - bodyWidth) / 2 + + return ( + <> + {/* Lead on the left side */} + + + {/* Lead on the right side */} + + + {/* Chip Body */} + + + ) +} + +export default SmdDiode diff --git a/lib/SmdPushButton.tsx b/lib/SmdPushButton.tsx new file mode 100644 index 00000000..c106e7e1 --- /dev/null +++ b/lib/SmdPushButton.tsx @@ -0,0 +1,69 @@ +import { Colorize, Cuboid, Cylinder } from "jscad-fiber" + +interface SmdPushButtonProps { + /** pad pitch X (footprinter px) */ + padPitchX?: number + /** pad pitch Y (footprinter py) */ + padPitchY?: number + padWidth?: number + padLength?: number + bodyWidth?: number + bodyLength?: number + bodyHeight?: number +} + +/** + * SMD tactile push button: square base, round actuator, 4 corner pads. + */ +export const SmdPushButton = ({ + padPitchX = 4.2, + padPitchY = 2.15, + padWidth = 1.05, + padLength = 0.7, + bodyWidth = 6, + bodyLength = 6, + bodyHeight = 3.5, +}: SmdPushButtonProps) => { + return ( + <> + {/* 4 corner gull-wing pads */} + {[-1, 1].map((sx) => + [-1, 1].map((sy) => ( + + + + )), + )} + + {/* Base */} + + + + + {/* Top cover */} + + + + + {/* Actuator */} + + + + + ) +} + +export default SmdPushButton diff --git a/lib/SolderJumper.tsx b/lib/SolderJumper.tsx new file mode 100644 index 00000000..a150d5b1 --- /dev/null +++ b/lib/SolderJumper.tsx @@ -0,0 +1,45 @@ +import type { PcbSmtPad } from "circuit-json" +import { FootprintPad } from "./FootprintPad" + +interface SolderJumperProps { + /** number of pads (2 = jumper2, 3 = jumper3) */ + numPads?: number + padWidth?: number + padHeight?: number + gap?: number +} + +/** + * Solder jumper: 2 (or 3) SMD pads separated by a small gap, + * meant to be bridged with a blob of solder. + */ +export const SolderJumper = ({ + numPads = 2, + padWidth = 1, + padHeight = 1.5, + gap = 0.3, +}: SolderJumperProps) => { + const totalWidth = numPads * padWidth + (numPads - 1) * gap + const startX = -totalWidth / 2 + padWidth / 2 + + return ( + <> + {Array.from({ length: numPads }).map((_, i) => { + const pad: PcbSmtPad = { + type: "pcb_smtpad", + pcb_smtpad_id: `solderjumper_${i}`, + shape: "rect", + x: startX + i * (padWidth + gap), + y: 0, + width: padWidth, + height: padHeight, + layer: "top", + port_hints: [`${i + 1}`], + } + return + })} + + ) +} + +export default SolderJumper diff --git a/lib/index.ts b/lib/index.ts index a30e911c..8223d68c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -73,3 +73,31 @@ export * from "./Screen" export * from "./JSTZH1_5mm" export * from "./FPC" export * from "./RJ45" +export * from "./JST" +export * from "./SmdDiode" +export * from "./DPAK" +export * from "./SOT89" +export * from "./SOT343" +export * from "./Crystal" +export * from "./ElectrolyticCapacitor" +export * from "./Potentiometer" +export * from "./SmdPushButton" +export * from "./LED2835" +export * from "./LED5050" +export * from "./SolderJumper" +export * from "./M2Host" +export * from "./AxialLed" +export * from "./AxialResistor" +export * from "./BGA" +export * from "./ChipBody" +export * from "./DualInlinePackage" +export * from "./PushButton" +export * from "./RadialCapacitor" +export * from "./SOIC" +export * from "./SOT-23-3P" +export * from "./SOT-235" +export * from "./SOT-563" +export * from "./SmdChipLead" +export * from "./USB-A" +export * from "./USB-C" +export * from "./smdLED" diff --git a/lib/vanilla/primitives.ts b/lib/vanilla/primitives.ts index 9be88e78..d9f6a2ef 100644 --- a/lib/vanilla/primitives.ts +++ b/lib/vanilla/primitives.ts @@ -2,6 +2,7 @@ export const Cuboid = Symbol("Cuboid") export const Cube = Symbol("Cube") export const Cylinder = Symbol("Cylinder") export const Sphere = Symbol("Sphere") +export const Ellipsoid = Symbol("Ellipsoid") export const RoundedCuboid = Symbol("RoundedCuboid") export const Translate = Symbol("Translate") diff --git a/lib/vanilla/render.ts b/lib/vanilla/render.ts index 274f763a..80fbe0fa 100644 --- a/lib/vanilla/render.ts +++ b/lib/vanilla/render.ts @@ -7,6 +7,7 @@ import { Cuboid, Cylinder, Sphere, + Ellipsoid, Hull, Rotate, RoundedCuboid, @@ -160,6 +161,7 @@ function renderNode( type === Cube || type === Cylinder || type === Sphere || + type === Ellipsoid || type === RoundedCuboid || type === RoundedCylinder ) { @@ -187,6 +189,10 @@ function renderNode( const radius = props?.radius ?? 1 const center = props?.center ?? [0, 0, 0] g = primitives.sphere({ radius, center }) + } else if (type === Ellipsoid) { + const radius = props?.radius ?? [1, 1, 1] + const center = props?.center ?? [0, 0, 0] + g = primitives.ellipsoid({ radius, center }) } else if (type === RoundedCylinder) { const height = props?.height ?? 1 const radius = props?.radius ?? 1 diff --git a/tests/footprinter-coverage.test.ts b/tests/footprinter-coverage.test.ts new file mode 100644 index 00000000..13b47302 --- /dev/null +++ b/tests/footprinter-coverage.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from "bun:test" +import { fp } from "@tscircuit/footprinter" +import { Footprinter3d } from "../lib/Footprinter3d" + +/** + * Footprinter coverage test. + * + * Every footprint name exposed by @tscircuit/footprinter must produce a + * non-null 3D model from . If footprinter adds a new + * footprint function, it automatically appears here and fails until a + * model is wired up in lib/Footprinter3d.tsx — this is the checklist for + * the "missing models" backlog. + */ + +// Footprint names that need parameters to parse into valid footprint +// strings. Everything else is used as-is. +const FOOTPRINT_STRING_OVERRIDES: Record = { + cap: "cap0603", + diode: "diode0603", + led: "led0603", + res: "res0603", + fpc: "fpc4", + jst: "jst4_sh", +} + +// Footprint names where fp.string(...) itself throws inside +// @tscircuit/footprinter (broken upstream). Tracked separately so the +// test starts failing if footprinter fixes them and they become +// renderable — at that point wire them into Footprinter3d and move them +// out of this list. +const BROKEN_IN_FOOTPRINTER = new Set(["pad", "vson"]) + +const footprintNames = fp.getFootprintNames() + +test("every footprinter footprint renders a non-null 3D model", () => { + const missing: string[] = [] + + for (const name of footprintNames) { + if (BROKEN_IN_FOOTPRINTER.has(name)) continue + const footprint = FOOTPRINT_STRING_OVERRIDES[name] ?? name + + let element: unknown + try { + element = Footprinter3d({ footprint }) + } catch (error) { + missing.push(`${name} (${footprint}) threw: ${(error as Error).message}`) + continue + } + if (element === null || element === undefined) { + missing.push(`${name} (${footprint}) returned null`) + } + } + + expect(missing).toEqual([]) +}) + +test("JST connector series all render", () => { + for (const footprint of ["jst2_zh", "jst4_sh", "jst6_ph", "jst4"]) { + expect(Footprinter3d({ footprint })).not.toBeNull() + } + // legacy jstzh1_5mm string normalization still works + expect(Footprinter3d({ footprint: "jstzh1_5mm4" })).not.toBeNull() +}) + +test("footprints broken upstream in footprinter are still broken", () => { + for (const name of BROKEN_IN_FOOTPRINTER) { + expect(() => fp.string(name).json()).toThrow() + } +}) From fbabb5e87ad77c67856da9fe95f79cee7d7950b1 Mon Sep 17 00:00:00 2001 From: Dasdebsankar54 Date: Wed, 29 Jul 2026 11:23:26 +0530 Subject: [PATCH 2/2] style: apply biome formatting to new components Fixes the Format Check CI failure on PR #315: collapse JSX props onto single lines per biome's canonical style in ElectrolyticCapacitor, LED2835, Potentiometer, and Footprinter3d. No behavior change; bun test still 82/82. --- lib/ElectrolyticCapacitor.tsx | 12 ++---------- lib/Footprinter3d.tsx | 4 +--- lib/LED2835.tsx | 10 ++-------- lib/Potentiometer.tsx | 6 +----- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/lib/ElectrolyticCapacitor.tsx b/lib/ElectrolyticCapacitor.tsx index 957dac6f..fc861560 100644 --- a/lib/ElectrolyticCapacitor.tsx +++ b/lib/ElectrolyticCapacitor.tsx @@ -59,16 +59,8 @@ export const ElectrolyticCapacitor = ({ {/* Leads */} - - + + ) diff --git a/lib/Footprinter3d.tsx b/lib/Footprinter3d.tsx index 4794a9fc..39b419fa 100644 --- a/lib/Footprinter3d.tsx +++ b/lib/Footprinter3d.tsx @@ -366,9 +366,7 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { pitch={fpNum(fpJson.p, 0.5)} padLength={fpNum(fpJson.pl, 0.25)} padWidth={fpNum(fpJson.pw, 0.25)} - thermalPadSize={ - hasThermalPad ? { width: 5, length: 5 } : undefined - } + thermalPadSize={hasThermalPad ? { width: 5, length: 5 } : undefined} /> ) } diff --git a/lib/LED2835.tsx b/lib/LED2835.tsx index 88acf550..fed167e0 100644 --- a/lib/LED2835.tsx +++ b/lib/LED2835.tsx @@ -18,14 +18,8 @@ export const LED2835 = ({ <> {/* Two pads underneath (asymmetric, anode wider) */} - - + + {/* White body */} diff --git a/lib/Potentiometer.tsx b/lib/Potentiometer.tsx index fab4efa8..97ae2a0c 100644 --- a/lib/Potentiometer.tsx +++ b/lib/Potentiometer.tsx @@ -27,11 +27,7 @@ export const Potentiometer = ({ <> {/* Body */} - + {/* Shaft with slot */}