component
RC0402FR-0733KL - YAGEO
Public Made by Adomby adom
Thick film chip resistor 33 kOhm +/-1 %, 1/16 W, 50 V, 0402 (1005 metric)
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
"""Board-use LOD GLBs (high, medium, low, super-low) for a small two-terminal chip.
Input: the page's board-use STEP (Z-up, mm, seated on z=0, centred).
Output (in <outdir>/lod/): high.glb medium.glb low.glb super-low.glb, a 400x300
PNG preview of each rendered from that GLB's own triangles, manifest.json
(schema adom/component-lod@1).
Every level: Z-up, metres, the same origin and pose as the STEP (no recentring,
no rotation), primitives batched by material (one per material), the STEP's own
colours. No footprint, pad outline, label or silkscreen helpers exist in the
source, so nothing is removed. high/medium/low are OCCT tessellations of the
exact B-rep at decreasing deflection; super-low replaces each material group by
its bounding box per side of the part (terminals, body, overcoat), which keeps
the silhouette, the terminal bands and the seating plane.
usage: lod.py part.step MPN outdir [source_label]
"""
import sys, os, json, struct, hashlib
import numpy as np
from PIL import Image, ImageDraw
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ortho
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRepTools import BRepTools
step, mpn, outdir = sys.argv[1], sys.argv[2], sys.argv[3]
src_label = sys.argv[4] if len(sys.argv) > 4 else os.path.basename(step)
# optional: the unmarked STEP; colours present only in the marked source are the marking,
# kept in high and medium, dropped in low and super-low (too small to read there)
plain_step = sys.argv[5] if len(sys.argv) > 5 else None
MARK_COLS = set()
lod_dir = os.path.join(outdir, "lod"); os.makedirs(lod_dir, exist_ok=True)
LEVELS = [("high", 0.0005, 0.1), ("medium", 0.004, 0.35), ("low", 0.02, 0.9)]
def tris_at(lin, ang):
# ortho.load_tris meshes with fixed params; clean and remesh by patching BRepMesh call
orig = ortho.BRepMesh_IncrementalMesh
def mesh(shp, _l, rel, _a, par):
BRepTools.Clean_s(shp)
return orig(shp, lin, False, ang, True)
ortho.BRepMesh_IncrementalMesh = mesh
try:
return ortho.load_tris(step)
finally:
ortho.BRepMesh_IncrementalMesh = orig
def boxes_from(tris):
groups = {}
for p, c in tris:
groups.setdefault(c, []).append(p)
allp = np.concatenate([p for p, _ in tris]); xmax = allp[:, 0].max()
endc = {}
for p, c in tris:
if np.allclose(p[:, 0], xmax, atol=1e-4): endc[c] = endc.get(c, 0) + 1
tcol = max(endc, key=endc.get)
boxes = []
for c, ps in groups.items():
T = np.stack(ps)
crosses = c != tcol and ((T[:, :, 0].min(1) < -1e-6) & (T[:, :, 0].max(1) > 1e-6)).any() # terminals: always two boxes
P = T.reshape(-1, 3)
parts = [P] if crosses else [P[P[:, 0] < 0], P[P[:, 0] > 0]]
for Q in parts:
if len(Q):
boxes.append([Q.min(0), Q.max(0), c])
tin = min(min(abs(lo[0]), abs(hi[0])) for lo, hi, c in boxes if c == tcol)
# the body box reaches the same top as the overcoat box: coplanar tops z-fight, so stop the body at the overcoat's underside
dark = min({c for _, _, c in boxes}, key=lambda c: sum(c))
dk = [(lo, hi) for lo, hi, c in boxes if c == dark]
if dark != tcol and dk:
dlo = min(lo[2] for lo, hi in dk); dhi = max(hi[2] for lo, hi in dk)
for b in boxes:
if b[2] not in (dark, tcol) and b[1][2] >= dhi - 1e-6:
b[1] = b[1].copy(); b[1][2] = dlo
out = []
for lo, hi, c in boxes:
if c != tcol:
lo = lo.copy(); hi = hi.copy(); lo[0] = max(lo[0], -tin); hi[0] = min(hi[0], tin)
v = np.array([[x, y, z] for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])])
for a_, b_, cc, d in [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]:
out.append((v[[a_, b_, cc]], c)); out.append((v[[a_, cc, d]], c))
return out
def write_glb(tris, path, name):
mats = []; groups = {}
for p, c in tris:
if c not in groups:
groups[c] = []; mats.append(c)
groups[c].append(p)
bin_ = b""; views = []; accs = []; prims = []
def add_view(data, target):
nonlocal bin_
off = len(bin_); bin_ += data
while len(bin_) % 4: bin_ += b"\0"
views.append({"buffer": 0, "byteOffset": off, "byteLength": len(data), "target": target})
return len(views) - 1
decoded = 0; ntri = 0
for mi, c in enumerate(mats):
P = np.concatenate(groups[c]).reshape(-1, 3, 3)
n = np.cross(P[:, 1] - P[:, 0], P[:, 2] - P[:, 0]); n /= np.linalg.norm(n, axis=1, keepdims=True) + 1e-30
pos = (P.reshape(-1, 3) / 1000.0).astype(np.float32) # mm -> metres
nor = np.repeat(n, 3, axis=0).astype(np.float32)
# weld identical (position, normal) vertices
key = np.concatenate([np.round(pos, 9), np.round(nor, 5)], 1)
uniq, inv = np.unique(key, axis=0, return_inverse=True)
upos = uniq[:, :3].astype(np.float32); unor = uniq[:, 3:].astype(np.float32)
idx = inv.astype(np.uint16 if len(uniq) < 65535 else np.uint32).ravel()
vp = add_view(upos.tobytes(), 34962); vn = add_view(unor.tobytes(), 34962); vi = add_view(idx.tobytes(), 34963)
accs.append({"bufferView": vp, "componentType": 5126, "count": len(upos), "type": "VEC3",
"min": upos.min(0).tolist(), "max": upos.max(0).tolist()})
accs.append({"bufferView": vn, "componentType": 5126, "count": len(unor), "type": "VEC3"})
accs.append({"bufferView": vi, "componentType": 5123 if idx.dtype == np.uint16 else 5125, "count": len(idx), "type": "SCALAR"})
prims.append({"attributes": {"POSITION": len(accs) - 3, "NORMAL": len(accs) - 2}, "indices": len(accs) - 1, "material": mi})
decoded += upos.nbytes + unor.nbytes + idx.nbytes; ntri += len(idx) // 3
gl = {"asset": {"version": "2.0", "generator": "capencoder passives lod.py (OCCT tessellation)"},
"scene": 0, "scenes": [{"nodes": [0]}],
"nodes": [{"name": name, "mesh": 0, "extras": {"adomUpAxis": "Z", "units": "metres"}}],
"meshes": [{"name": name, "primitives": prims}],
"materials": [{"name": f"mat_{i}", "pbrMetallicRoughness": {"baseColorFactor": [*c, 1.0], "metallicFactor": 0.0, "roughnessFactor": 0.6}} for i, c in enumerate(mats)],
"accessors": accs, "bufferViews": views, "buffers": [{"byteLength": len(bin_)}]}
js = json.dumps(gl, separators=(",", ":")).encode()
while len(js) % 4: js += b" "
glb = struct.pack("<III", 0x46546C67, 2, 12 + 8 + len(js) + 8 + len(bin_)) + struct.pack("<II", len(js), 0x4E4F534A) + js + struct.pack("<II", len(bin_), 0x004E4942) + bin_
open(path, "wb").write(glb)
return {"decodedBufferBytes": decoded, "triangles": ntri, "primitives": len(prims)}
def read_glb_tris(path):
b = open(path, "rb").read(); n = struct.unpack("<I", b[12:16])[0]
j = json.loads(b[20:20 + n]); binc = b[20 + n + 8:]
def acc(i, dt, w):
a = j["accessors"][i]; v = j["bufferViews"][a["bufferView"]]
return np.frombuffer(binc, dt, a["count"] * w, v["byteOffset"]).reshape(-1, w) if w > 1 else np.frombuffer(binc, dt, a["count"], v["byteOffset"])
out = []
for p in j["meshes"][0]["primitives"]:
pos = acc(p["attributes"]["POSITION"], np.float32, 3) * 1000.0
ia = j["accessors"][p["indices"]]
idx = acc(p["indices"], np.uint16 if ia["componentType"] == 5123 else np.uint32, 1)
col = tuple(j["materials"][p["material"]]["pbrMetallicRoughness"]["baseColorFactor"][:3])
for t in idx.reshape(-1, 3):
out.append((pos[t].astype(float), col))
return out
ortho.VIEWS["iso"] = (tuple(np.array([-0.7071, 0.7071, 0])), tuple(np.array([-0.4082, -0.4082, 0.8165])), tuple(np.array([0.5774, 0.5774, 0.5774])))
def preview(path, png):
tris = read_glb_tris(path)
allp = np.concatenate([t[0] for t in tris]); ext = np.abs(allp).max()
img = ortho.render(tris, "iso", 300 / (3.3 * ext), size=(400, 300))
# painter's sorting by triangle centroid can bury faces in the top plane (and the marking 5 um above it);
# faces in those planes cannot be hidden in a view from above, so draw them again, plane by plane, bottom first
near = lambda c: any(max(abs(a - b) for a, b in zip(c, m)) < 2e-3 for m in MARK_COLS)
zt = allp[:, 2].max()
marked = [t for t in tris if MARK_COLS and near(t[1])]
top = zt - 0.005 if marked else zt
plane = [t for t in tris if not (MARK_COLS and near(t[1])) and np.abs(t[0][:, 2] - top).max() < 1e-4]
for layer in (plane, [t for t in marked if t[0][:, 2].min() > top + 1e-4]):
if layer:
img.alpha_composite(ortho.render(layer, "iso", 300 / (3.3 * ext), size=(400, 300)))
bg = Image.new("RGBA", img.size, (48, 57, 68, 255)); bg.alpha_composite(img); bg.convert("RGB").save(png)
sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()
levels = []
src_tris = None
if plain_step:
MARK_COLS = {c for _, c in ortho.load_tris(step)} - {c for _, c in ortho.load_tris(plain_step)}
print("marking colours", MARK_COLS)
for name, lin, ang in LEVELS + [("super-low", None, None)]:
if name == "super-low":
tris = boxes_from(src_tris)
else:
tris = tris_at(lin, ang)
if name == "high":
src_tris = [t for t in tris if t[1] not in MARK_COLS]
if name == "low" and MARK_COLS:
tris = [t for t in tris if t[1] not in MARK_COLS]
path = os.path.join(lod_dir, f"{name}.glb")
st = write_glb(tris, path, mpn)
back = read_glb_tris(path); P = np.concatenate([t[0] for t in back])
png = os.path.join(lod_dir, f"{name}.png"); preview(path, png)
rec = {"level": name, "path": f"lod/{name}.glb", "sha256": sha(path), "sizeBytes": os.path.getsize(path), **st,
"bounds": {"min": (P.min(0) / 1000).round(7).tolist(), "max": (P.max(0) / 1000).round(7).tolist()},
"approximation": ({"method": "OCCT BRepMesh_IncrementalMesh", "linearDeflectionMm": lin, "angularDeflectionRad": ang}
if lin else {"method": "per-material bounding boxes split at the part centre (terminals, body, overcoat)"}),
"detailVisibility": ({"virtualMpn": False, "manufacturerMarking": name in ("high", "medium"),
"note": "datasheet marking code shown on the top coat" if name in ("high", "medium") else "datasheet marking omitted: unreadable at this level"}
if MARK_COLS else {"virtualMpn": False, "note": "no marking: the datasheet specifies none"}),
"review": "Candidate: rendered preview checked by AI (silhouette, terminals, seat z=0); not human-reviewed",
"preview": {"path": f"lod/{name}.png", "sha256": sha(png), "sizeBytes": os.path.getsize(png), "width": 400, "height": 300,
"view": "iso", "sourceLevelSha256": sha(path)}}
levels.append(rec)
print(name, st, rec["bounds"])
manifest = {"schema": "adom/component-lod@1", "mpn": mpn, "upAxis": "Z", "units": "metres",
"status": "candidate (AI-checked, not human-reviewed)",
"purpose": "clean board-use component models: no footprint pads, outlines, labels or silkscreen; no marking (none per datasheet)",
"source": {"path": src_label, "sha256": sha(step)},
"sourceToDisplay": "uniform scale 0.001 (mm to m); no rotation, no translation; origin = footprint origin, seat z = 0",
"generator": "lod.py (capencoder passives, OCCT via cadquery-ocp)",
"removedHelpers": [], "levels": levels,
"hints": ["All four levels share pose and units; super-low is boxes per material group, chosen for board overviews."]}
json.dump(manifest, open(os.path.join(lod_dir, "manifest.json"), "w"), indent=1)