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