component
RC0402FR-0710KL - Yageo
Public Unreviewedby John Lauer
Thick film chip resistor, 10 kohm, 1%, 1/16 W, 0402 (1005 metric), RC_L series
main
John Lauer
Remove the AI-added MPN-etched model (the part has no marking per its datasheet; John's rule: real marking only). Add d…
9bdf1dc
18h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
"""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)
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 = ((T[:, :, 0].min(1) < -1e-6) & (T[:, :, 0].max(1) > 1e-6)).any()
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)
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))
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
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 = tris
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, "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)