component
CX3225GA08000D0PTVTT - 8 MHz Automotive Crystal, 3.2 x 2.5 mm
Public Made by Adomby adom
Kyocera CX3225GA08000D0PTVTT: 8 MHz fundamental quartz crystal, 8 pF load, +-50 ppm, -40 to +150 C, AEC-Q200, 3.2 x 2.5 x 0.85 mm ceramic SMD with 2 pads. KiCad symbol and footprint from Kyocera's land pattern, datasheet-built 3D model, Fusion and Altium libraries.
main
John Lauer
LODs, models.json, verification log, provenance and tools, README, license
c543194
17h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
#!/usr/bin/env python3
"""Depth-buffered orthographic render of a chip GLB (or STEP), for LOD previews and close-ups.
Reads geometry with the page's glbtool loader (quantized accessors handled), shades with a key + fill
light, Lambert plus a Blinn specular that follows each material's metallic/roughness (so matte tin
reads as tin and epoxy as epoxy). Orthographic, 3x supersampled.
usage: render.py model.glb|model.step out.png [--view iso|top|front|side|corner] [--size 400]
[--box x0,x1,y0,y1,z0,z1 (mm: frame only this region)] [--bg 36,40,46] [--alpha]
views: top, bottom, iso = from front-left-up (pin-1 side); corner = low 3/4 view onto a lead corner;
front = along +Y; side = along -X."""
import sys, math, argparse
import numpy as np
from PIL import Image
sys.path.insert(0, sys.argv[0].rsplit("/", 1)[0])
def load(path):
if path.lower().endswith((".step", ".stp")):
import cadquery as cq
s = cq.importers.importStep(path).val()
v, t = s.tessellate(0.002, 0.1)
P = np.array([[p.x, p.y, p.z] for p in v]) / 1000.0
return [dict(path="step", P=P, I=np.array(t), col=[0.6, 0.6, 0.6, 1], metal=0.0, rough=0.6)]
from pygltflib import GLTF2
import importlib.util
spec = importlib.util.spec_from_file_location("glbtool", ARGS.glbtool)
gt = importlib.util.module_from_spec(spec); spec.loader.exec_module(gt)
g, res = gt.tris(path)
# attach metallic/roughness per primitive (glbtool keeps only the colour)
i = 0
for n, M, p in gt.walk(g):
for pr in g.meshes[n.mesh].primitives:
m = g.materials[pr.material] if pr.material is not None else None
pbr = m.pbrMetallicRoughness if m else None
res[i]["metal"] = (pbr.metallicFactor if pbr and pbr.metallicFactor is not None else 1.0)
res[i]["rough"] = (pbr.roughnessFactor if pbr and pbr.roughnessFactor is not None else 1.0)
i += 1
return res
def view_matrix(name):
def rz(a): a = math.radians(a); return np.array([[math.cos(a), -math.sin(a), 0], [math.sin(a), math.cos(a), 0], [0, 0, 1]])
def rx(a): a = math.radians(a); return np.array([[1, 0, 0], [0, math.cos(a), -math.sin(a)], [0, math.sin(a), math.cos(a)]])
if name == "top": return np.eye(3)
if name == "bottom": return rx(180) # from below, X still to the right
if name == "front": return rx(-90) # look along +Y: X right, Z up
if name == "side": return rx(-90) @ rz(-90) # look along -X: Y right, Z up
if name == "corner": return rx(-68) @ rz(-38) # low 3/4 onto the -X/-Y lead corner
return rx(-55) @ rz(-35) # iso
ap = argparse.ArgumentParser()
ap.add_argument("model"); ap.add_argument("out")
ap.add_argument("--view", default="iso"); ap.add_argument("--size", type=int, default=400)
ap.add_argument("--box"); ap.add_argument("--bg", default="36,40,46")
ap.add_argument("--turn", type=float, default=0.0, help="extra turn about Z before the view (deg)")
ap.add_argument("--alpha", action="store_true", help="transparent background (RGBA PNG)")
ap.add_argument("--glbtool", default="/tmp/claude-1001/-home-adom-project/43add640-14a6-413a-b15b-ffdadeb7cf16/scratchpad/stm32/provenance/tools/glbtool.py")
ARGS = ap.parse_args()
parts = load(ARGS.model)
R = view_matrix(ARGS.view)
if ARGS.turn:
_a = math.radians(ARGS.turn)
R = R @ np.array([[math.cos(_a), -math.sin(_a), 0], [math.sin(_a), math.cos(_a), 0], [0, 0, 1]])
allP = np.vstack([p["P"] for p in parts]) * 1000.0
if ARGS.box:
b = [float(v) for v in ARGS.box.split(",")]
corners = np.array([[x, y, z] for x in b[0:2] for y in b[2:4] for z in b[4:6]])
else:
corners = allP
Q = corners @ R.T
lo, hi = Q.min(0), Q.max(0); ctr = (lo + hi) / 2; ext = max(hi[0] - lo[0], hi[1] - lo[1]) * 1.08
SS = 3; N = ARGS.size * SS; s = N / ext
img = np.zeros((N, N, 3)); zb = np.full((N, N), -1e9)
L1 = np.array([0.35, 0.45, 0.82]); L1 /= np.linalg.norm(L1) # key, in view space
L2 = np.array([-0.6, -0.2, 0.5]); L2 /= np.linalg.norm(L2) # fill
V = np.array([0, 0, 1.0])
for p in parts:
X = (p["P"] * 1000.0) @ R.T
sx = (X[:, 0] - ctr[0]) * s + N / 2; sy = N / 2 - (X[:, 1] - ctr[1]) * s; z = X[:, 2]
col = np.array(p["col"][:3]); metal, rough = p.get("metal", 0.0), max(p.get("rough", 0.6), 0.08)
shin = 2.0 / (rough ** 4) - 2.0
for t in p["I"]:
a, b2, c = t
v0, v1, v2 = X[a], X[b2], X[c]
nrm = np.cross(v1 - v0, v2 - v0); ln = np.linalg.norm(nrm)
if ln == 0: continue
nrm /= ln
if nrm[2] < 0: nrm = -nrm
diff = 0.18 + 0.62 * max(0, nrm @ L1) + 0.2 * max(0, nrm @ L2)
H = (L1 + V); H /= np.linalg.norm(H)
spec = max(0, nrm @ H) ** min(shin, 400) * (0.9 if metal > 0.5 else 0.25) * (1 - rough * 0.6)
base = col * (0.55 + 0.45 * (1 - metal)) if metal > 0.5 else col
shade = np.clip(base * diff + spec * (col if metal > 0.5 else np.ones(3)), 0, 1)
shade = shade ** (1 / 2.2)
xs = np.array([sx[a], sx[b2], sx[c]]); ys = np.array([sy[a], sy[b2], sy[c]]); zs = np.array([z[a], z[b2], z[c]])
x0, x1 = int(max(0, np.floor(xs.min()))), int(min(N - 1, np.ceil(xs.max())))
y0, y1 = int(max(0, np.floor(ys.min()))), int(min(N - 1, np.ceil(ys.max())))
if x0 > x1 or y0 > y1: continue
gx, gy = np.meshgrid(np.arange(x0, x1 + 1) + 0.5, np.arange(y0, y1 + 1) + 0.5)
d = (ys[1] - ys[2]) * (xs[0] - xs[2]) + (xs[2] - xs[1]) * (ys[0] - ys[2])
if abs(d) < 1e-12: continue
w0 = ((ys[1] - ys[2]) * (gx - xs[2]) + (xs[2] - xs[1]) * (gy - ys[2])) / d
w1 = ((ys[2] - ys[0]) * (gx - xs[2]) + (xs[0] - xs[2]) * (gy - ys[2])) / d
w2 = 1 - w0 - w1
m = (w0 >= -1e-6) & (w1 >= -1e-6) & (w2 >= -1e-6)
if not m.any(): continue
zz = w0 * zs[0] + w1 * zs[1] + w2 * zs[2]
sub = zb[y0:y1 + 1, x0:x1 + 1]
upd = m & (zz > sub)
sub[upd] = zz[upd]
img[y0:y1 + 1, x0:x1 + 1][upd] = shade
bg = np.array([int(v) for v in ARGS.bg.split(",")]) / 255.0
img[zb < -1e8] = bg
if ARGS.alpha:
a = (zb > -1e8).astype(np.float64)[..., None]
rgba = np.concatenate([img * a, a], 2) # premultiplied so the LANCZOS edge stays clean
out = np.asarray(Image.fromarray((rgba * 255).astype(np.uint8), "RGBA").resize((ARGS.size, ARGS.size), Image.LANCZOS)).astype(np.float64)
al = out[..., 3:] / 255.0
out[..., :3] = np.where(al > 0, out[..., :3] / np.maximum(al, 1e-6), 0)
Image.fromarray(np.clip(out, 0, 255).astype(np.uint8), "RGBA").save(ARGS.out)
else:
Image.fromarray((img * 255).astype(np.uint8)).resize((ARGS.size, ARGS.size), Image.LANCZOS).save(ARGS.out)
print("wrote", ARGS.out)