main
John Lauer LODs, models.json, verification log, provenance and tools, README, license c543194 16h ago
"""Build the Kyocera CX3225GA (2-pad, 3.2 x 2.5 mm) 3D model from the Kyocera catalog drawing.

Source: Kyocera "Crystal Units Surface Mount Type CX3225GA" catalog page (As of October 2025),
https://ele.kyocera.com/assets/products/crystal-device/cx3225ga_e.pdf, Dimensions and Recommended
Land Pattern panels. Printed values are used as printed; undimensioned features (electrode outer arc,
electrode height, lid inset, castellation depth) were measured off the drawing at 600 dpi with the
scale calibrated per axis on the printed 3.2 and 2.5 body edges (see provenance/3d-model.json).

Frame: Adom/KiCad Z-up, millimetres, seated on z = 0, centred on the footprint origin. +X is the
footprint's pad 2 side; -Y is the front edge (footprint +Y, KiCad's Y points down). Pad 1 is the
electrode with the R0.3 inner corner (Kyocera's bottom view, third-angle projection).

usage: python build_cx3225ga.py out.step [--simple]
  --simple: no castellations, no electrode corner rounding (for the super-low LOD source)
"""
import math
import sys

import cadquery as cq

OUT = sys.argv[1]
SIMPLE = "--simple" in sys.argv

# printed (catalog)
L, W, H = 3.2, 2.5, 0.85          # body 3.2 +-0.1, 2.5 +-0.1, height 0.85 +-0.1
BASE_TOP = 0.53                    # 0.53 +-0.1: ceramic base plus seal ring, from the seating plane
GAP = 0.6                          # electrode gap 0.6 +-0.1
R_INNER = 0.3                      # R0.3 on pad 1's inner front corner
R_BODY = 0.15                      # 4-R0.15 corner castellations
NOTCH_W = 0.5                      # 0.5 +-0.1 end castellation width
CAST_H = 0.25                      # 0.25 end castellation metallisation height
# measured off the drawing (600 dpi, per-axis calibration)
EL_HALF_H = 1.14                   # electrode half height (y)
EL_OUT_MID = 1.48                  # electrode outer edge at y = 0
EL_OUT_END = 1.24                  # electrode outer edge at y = +-EL_HALF_H
NOTCH_D = 0.10                     # end castellation depth (top and bottom views: 15 px at 157 px/mm)
LID_INSET = 0.08                   # seal ring inset; the lid is 0.02 further in (0.10, front and end views)
SEAL_T = 0.05                      # seal ring under the lid
EL_T = 0.02                        # electrode film thickness (bottom), body seated on the films
LID_R = 0.2                        # lid corner radius

base_z0 = EL_T
base_t = BASE_TOP - SEAL_T - base_z0
lid_z0 = BASE_TOP
lid_t = H - BASE_TOP

# ceramic base
base = cq.Workplane("XY").box(L, W, base_t, centered=(True, True, False)).translate((0, 0, base_z0))
if not SIMPLE:
    for sx in (-1, 1):
        for sy in (-1, 1):
            cut = cq.Workplane("XY").circle(R_BODY).extrude(base_t).translate((sx * L / 2, sy * W / 2, base_z0))
            base = base.cut(cut)
        notch = cq.Workplane("XY").box(2 * NOTCH_D, NOTCH_W, base_t, centered=(True, True, False)).translate((sx * L / 2, 0, base_z0))
        base = base.cut(notch)

# seal ring and lid
seal = (cq.Workplane("XY").rect(L - 2 * LID_INSET, W - 2 * LID_INSET).extrude(SEAL_T)
        .edges("|Z").fillet(LID_R).translate((0, 0, BASE_TOP - SEAL_T)))
lid = (cq.Workplane("XY").rect(L - 2 * LID_INSET - 0.04, W - 2 * LID_INSET - 0.04).extrude(lid_t)
       .edges("|Z").fillet(LID_R).translate((0, 0, lid_z0)))


def electrode(sign, rounded):
    """Bottom electrode: straight inner edge at |x| = GAP/2, circular outer arc through
    (EL_OUT_END, +-EL_HALF_H) and (EL_OUT_MID, 0)."""
    xi = GAP / 2
    s = EL_OUT_MID - EL_OUT_END
    R = (EL_HALF_H ** 2 + s ** 2) / (2 * s)
    cx = EL_OUT_MID - R
    if SIMPLE:
        pts = [(xi, -EL_HALF_H), (EL_OUT_END, -EL_HALF_H), (EL_OUT_MID, 0), (EL_OUT_END, EL_HALF_H), (xi, EL_HALF_H)]
        wp = cq.Workplane("XY").polyline(pts).close()
    else:
        wp = (cq.Workplane("XY").moveTo(xi, -EL_HALF_H).lineTo(EL_OUT_END, -EL_HALF_H)
              .threePointArc((EL_OUT_MID, 0), (EL_OUT_END, EL_HALF_H)).lineTo(xi, EL_HALF_H).close())
    e = wp.extrude(EL_T)
    if rounded and not SIMPLE:
        # R0.3 on the inner corner at the front edge (y = -EL_HALF_H in Z-up model coordinates)
        e = e.edges("|Z").edges(cq.selectors.NearestToPointSelector((xi, -EL_HALF_H, 0))).fillet(R_INNER)
    if sign < 0:
        e = e.mirror("YZ")
    return e


pad1 = electrode(-1, rounded=True)
pad2 = electrode(+1, rounded=False)
if not SIMPLE:
    # end castellation metallisation, 0.25 high, joined to each electrode
    for sx, p in ((-1, "pad1"), (1, "pad2")):
        c = (cq.Workplane("XY").box(0.02, NOTCH_W, CAST_H, centered=(True, True, False))
             .translate((sx * (L / 2 - NOTCH_D - 0.01), 0, 0)))
        bridge = (cq.Workplane("XY").box(abs(L / 2 - NOTCH_D - EL_OUT_MID) + 0.04, NOTCH_W, EL_T, centered=(True, True, False))
                  .translate((sx * (L / 2 - NOTCH_D + EL_OUT_MID) / 2, 0, 0)))
        if p == "pad1":
            pad1 = pad1.union(c).union(bridge)
        else:
            pad2 = pad2.union(c).union(bridge)

CERAMIC = cq.Color(0.20, 0.17, 0.24)   # dark purple-black alumina (Kyocera product photo)
SEAL = cq.Color(0.55, 0.55, 0.55)
LID = cq.Color(0.07, 0.07, 0.07)       # black top (product photo)
GOLD = cq.Color(0.83, 0.69, 0.22)

asm = cq.Assembly(name="CX3225GA08000D0PTVTT")
asm.add(base, name="base", color=CERAMIC)
asm.add(seal, name="seal_ring", color=SEAL)
asm.add(lid, name="lid", color=LID)
asm.add(pad1, name="electrode_1", color=GOLD)
asm.add(pad2, name="electrode_2", color=GOLD)
asm.save(OUT, exportType="STEP")
bb = asm.toCompound().BoundingBox()
print(f"OK {OUT}: bbox x {bb.xmin:.3f}..{bb.xmax:.3f} y {bb.ymin:.3f}..{bb.ymax:.3f} z {bb.zmin:.3f}..{bb.zmax:.3f}")