diff --git a/examples/Crystal.example.tsx b/examples/Crystal.example.tsx new file mode 100644 index 0000000..f19de2a --- /dev/null +++ b/examples/Crystal.example.tsx @@ -0,0 +1,11 @@ +import { JsCadView } from "jscad-fiber" +import { Crystal, ExtrudedPads } from "../lib" + +export default function CrystalExample() { + return ( + + + + + ) +} diff --git a/lib/Crystal.tsx b/lib/Crystal.tsx new file mode 100644 index 0000000..11559e3 --- /dev/null +++ b/lib/Crystal.tsx @@ -0,0 +1,174 @@ +import { Colorize, Cylinder, ExtrudeLinear, Hull, Polygon } from "jscad-fiber" +import { ChipBody } from "./ChipBody" + +export interface CrystalProps { + horizontalPadPitch?: number + verticalPadPitch?: number + padWidth?: number + padHeight?: number + bodyHeight?: number +} + +type Point = [number, number] + +const getTerminalPoints = ( + [centerX, centerY]: Point, + width: number, + length: number, + chamfer: number, + isPinOne: boolean, +): Point[] => { + const xDirection = Math.sign(centerX) + const yDirection = Math.sign(centerY) + const halfWidth = width / 2 + const halfLength = length / 2 + const innerChamfer = isPinOne ? Math.min(0.32, length * 0.36) : 0 + const outerChamfer = Math.min(chamfer, halfWidth, halfLength) + const points: Point[] = [ + [-halfWidth, -halfLength], + [halfWidth, -halfLength], + [halfWidth, halfLength - outerChamfer], + [halfWidth - outerChamfer, halfLength], + [-halfWidth, halfLength], + ] + if (innerChamfer) { + points[0] = [-halfWidth + innerChamfer, -halfLength] + points.push([-halfWidth, -halfLength + innerChamfer]) + } + + const oriented = points.map( + ([x, y]) => [centerX + x * xDirection, centerY + y * yDirection] as Point, + ) + + return xDirection * yDirection < 0 ? oriented.reverse() : oriented +} + +type RoundedRectProfileOptions = { + width: number + length: number + height: number + radius: number + z: number +} + +const getRoundedRectProfile = ({ + width, + length, + radius, + height, + z, +}: RoundedRectProfileOptions) => { + const x = width / 2 - radius + const y = length / 2 - radius + + return ( + [ + [-x, -y], + [x, -y], + [x, y], + [-x, y], + ] as const + ).map(([cornerX, cornerY], index) => ( + + )) +} + +export const Crystal = ({ + horizontalPadPitch = 2.2, + verticalPadPitch = 1.7, + padWidth = 1.4, + padHeight = 1.2, + bodyHeight = 0.7, +}: CrystalProps) => { + const bodyWidth = horizontalPadPitch + padWidth * (5 / 7) + const bodyLength = verticalPadPitch + padHeight * (2 / 3) + const terminalThickness = Math.min(0.01, bodyHeight * 0.02) + const terminalWidth = padWidth * (5 / 7) + const terminalLength = padHeight * 0.75 + const ceramicTopZ = bodyHeight * (5 / 7) + const baseHeight = ceramicTopZ - terminalThickness + const sealHeight = bodyHeight * (2 / 35) + const sealWidth = bodyWidth * 0.95 + const sealLength = bodyLength * 0.94 + const lidShoulderWidth = bodyWidth * 0.905 + const lidShoulderLength = bodyLength * 0.9 + const lidTopWidth = bodyWidth * 0.83 + const lidTopLength = bodyLength * 0.82 + const lidBaseZ = ceramicTopZ + sealHeight + const lidProfileHeight = Math.min(0.01, bodyHeight * 0.015) + const bodyChamfer = Math.min(0.12, bodyLength * 0.05) + const terminalX = (bodyWidth - terminalWidth) / 2 + const terminalY = (bodyLength - terminalLength) / 2 + const terminalPositions = [ + [-terminalX, -terminalY], + [terminalX, -terminalY], + [terminalX, terminalY], + [-terminalX, terminalY], + ] satisfies Point[] + const sealProfile = getRoundedRectProfile({ + width: sealWidth, + length: sealLength, + radius: Math.min(0.13, sealLength * 0.08), + height: sealHeight, + z: ceramicTopZ, + }) + const lidProfiles = [ + ...getRoundedRectProfile({ + width: lidShoulderWidth, + length: lidShoulderLength, + radius: Math.min(0.14, lidShoulderLength * 0.08), + height: lidProfileHeight, + z: lidBaseZ, + }), + ...getRoundedRectProfile({ + width: lidTopWidth, + length: lidTopLength, + radius: Math.min(0.15, lidTopLength * 0.09), + height: lidProfileHeight, + z: bodyHeight - lidProfileHeight, + }), + ] + + return ( + <> + {terminalPositions.map((center, index) => ( + + + + + + ))} + + + {sealProfile} + + + {lidProfiles} + + + ) +} diff --git a/lib/Footprinter3d.tsx b/lib/Footprinter3d.tsx index 3caada4..5f5015f 100644 --- a/lib/Footprinter3d.tsx +++ b/lib/Footprinter3d.tsx @@ -57,6 +57,7 @@ import { StampBoard } from "./stampboard" import { MountedPcbModule } from "./MountedPcbModule" import SOD723 from "./SOD723" import { JSTZH1_5mm } from "./JSTZH1_5mm" +import { Crystal } from "./Crystal" import { FPC } from "./FPC" import { SmdPinHeader } from "./SmdPinHeader" @@ -80,6 +81,9 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { h: number pl: number pw: number + ph?: number + px?: number + py?: number num_pins: number fn: string zh?: boolean @@ -108,7 +112,6 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { screencenteroffsety?: number staggered?: boolean reverse?: boolean - py?: number toppl?: number bottompl?: number mpx?: number @@ -119,6 +122,15 @@ export const Footprinter3d = ({ footprint }: { footprint: string }) => { } switch (fpJson.fn) { + case "crystal": + return ( + + ) case "dip": return ( diff --git a/lib/index.ts b/lib/index.ts index f575588..3a337bd 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -9,6 +9,7 @@ export * from "./A2010" export * from "./A2512" export * from "./BGA" export * from "./ChipBody" +export * from "./Crystal" // export * from "./Dip" export * from "./ExtrudedPads" export * from "./FootprintPad" diff --git a/tests/snapshots/__snapshots__/crystal4.snap.png b/tests/snapshots/__snapshots__/crystal4.snap.png new file mode 100644 index 0000000..3ebe57c Binary files /dev/null and b/tests/snapshots/__snapshots__/crystal4.snap.png differ diff --git a/tests/snapshots/crystal4.test.ts b/tests/snapshots/crystal4.test.ts new file mode 100644 index 0000000..ea39a9e --- /dev/null +++ b/tests/snapshots/crystal4.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from "bun:test" +import "../fixtures/png-matcher" +import { renderFootprint } from "../helpers/render-footprint" + +test("four-pad SMD crystal package", async () => { + const pngBuffer = await renderFootprint("crystal4") + await expect(pngBuffer).toMatchPngSnapshot(import.meta.path) +})