by adom
0402 SMD Red LED · 2.1 V drop · 25 mA max
master
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
#!/usr/bin/env python3
"""Rebuild a page's composite LED scene GLB (<MPN>.glb / <MPN>.insertion.glb: fr4_board, pad_top, silk,
solder_top, the LED, LED_lit glow, fov_cone, LED_light) with the LED geometry and colours taken from the
coloured STEP's step2glb conversion. Everything else in the scene (board, pads, silk, solder, glow, FOV
cone, light, node names, node order, the LED node's name/children) is copied unchanged.
- LED geometry: every step2glb primitive is dequantized, the step2glb node transform is baked in (metres,
Z-up, same frame as the old mesh), and primitives are merged per material (like the old composite).
- LED node Z scale is set so the part keeps the old composite's rendered height
(new_scale = old_scale * old_mesh_top / new_mesh_top); a child light keeps its world position.
- Materials: the old slots keep their names/PBR settings; baseColor comes from the STEP colour
(linear, as step2glb emits it). The lens slot is renamed lens_<colour>, baseColor = STEP lens colour,
emissiveFactor = 0.35 x that colour (old slots used a similar dim self-glow), emissive strength kept,
roughness 0.35 + KHR_materials_clearcoat (1.0, roughness 0.08) so it reads as tinted epoxy.
Usage: rebuild_composite.py COMPOSITE.glb STEP2GLB.glb OUT.glb --mpn MPN --colour red --lens R,G,B(srgb)"""
import sys, json, argparse, hashlib
import numpy as np
sys.path.insert(0, __import__('os').path.dirname(__file__))
import gltfio
def lin(c): return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
KNOWN = {'body': (0.7776, 0.7697, 0.6261), 'terminal': (0.7087, 0.5040, 0.2104), 'cathode_mark': (0.0946, 0.4200, 0.1666)}
def main():
ap = argparse.ArgumentParser(); ap.add_argument('composite'); ap.add_argument('s2g'); ap.add_argument('out')
ap.add_argument('--mpn', required=True); ap.add_argument('--colour', required=True); ap.add_argument('--lens', required=True)
a = ap.parse_args()
lens_lin = tuple(lin(float(v)) for v in a.lens.split(','))
d, binc = gltfio.load(a.composite); arrays = gltfio.explode(d, binc)
ni = next(i for i, n in enumerate(d['nodes']) if n.get('name') == a.mpn); node = d['nodes'][ni]
mold = node['mesh']; old_prims = d['meshes'][mold]['primitives']
old_top = max(arrays[p['attributes']['POSITION']][0][:, 2].max() for p in old_prims)
old_tris = sum(len(arrays[p['indices']][0]) // 3 for p in old_prims)
# role of each old material slot
slot = {}
for p in old_prims:
nm = d['materials'][p['material']]['name']
role = 'lens' if nm.startswith('lens') else {'mat_0': 'body', 'mat_1': 'terminal', 'mat_2': 'cathode_mark'}[nm]
slot[role] = p['material']
# new LED geometry from step2glb (any node tree: flattened with world matrices, grouped by material colour)
s, sb = gltfio.load(a.s2g)
G = gltfio.led_groups(s, sb, KNOWN, lens_lin)
prims = []
for role in ('body', 'terminal', 'cathode_mark', 'lens'):
P, N, I = G[role]; key = f'led_{role}'
arrays[key + '_POSITION'] = (P.astype(np.float32), 34962); arrays[key + '_NORMAL'] = (N.astype(np.float32), 34962); arrays[key + '_IDX'] = (I, 34963)
prims.append({'attributes': {'POSITION': key + '_POSITION', 'NORMAL': key + '_NORMAL'}, 'indices': key + '_IDX', 'material': slot[role], 'mode': 4})
groups = {r: [[G[r][0]], None, [G[r][2]]] for r in G}
d['meshes'][mold]['primitives'] = prims
new_top = float(max(G[r][0][:, 2].max() for r in G if r != 'laser_etch'))
new_tris = sum(len(G[r][2]) // 3 for r in G)
# materials
for role in ('body', 'terminal', 'cathode_mark'):
m = d['materials'][slot[role]]; m['pbrMetallicRoughness']['baseColorFactor'] = [round(v, 4) for v in KNOWN[role]] + [1]
m = d['materials'][slot['lens']]; old_lens = json.loads(json.dumps(m))
m['name'] = 'lens_' + a.colour; m['pbrMetallicRoughness']['baseColorFactor'] = [round(v, 4) for v in lens_lin] + [1]
m['emissiveFactor'] = [round(0.35 * v, 4) for v in lens_lin]
# tinted epoxy, not paint: smoother lens with a clear coat
m['pbrMetallicRoughness'].update(metallicFactor=0, roughnessFactor=0.35)
m.setdefault('extensions', {})['KHR_materials_clearcoat'] = {'clearcoatFactor': 1.0, 'clearcoatRoughnessFactor': 0.08}
if 'KHR_materials_clearcoat' not in d.setdefault('extensionsUsed', []): d['extensionsUsed'].append('KHR_materials_clearcoat')
# keep rendered height: node Z scale, child light world position
sc = node.get('scale', [1, 1, 1]); old_sz = sc[2]; new_sz = old_sz * float(old_top) / new_top
node['scale'] = [sc[0], sc[1], new_sz]
for ci in node.get('children', []):
c = d['nodes'][ci]
if 'translation' in c: c['translation'] = [c['translation'][0], c['translation'][1], c['translation'][2] * old_sz / new_sz]
etch = None
if 'laser_etch' in G: # side MPN mark: child of the LED node (rides the insertion animation) with the inverse Z scale,
P, N, I = G['laser_etch'] # so the glyphs keep their true height while the body is scaled to the datasheet height
zc = float((P[:, 2].min() + P[:, 2].max()) / 2); c = P.mean(0)
arrays['etch_POSITION'] = ((P - [0, 0, zc]).astype(np.float32), 34962); arrays['etch_NORMAL'] = (N.astype(np.float32), 34962); arrays['etch_IDX'] = (I, 34963)
rgb = d0 = None
d['materials'].append({'name': 'laser_etch', 'pbrMetallicRoughness': {'baseColorFactor': list(s['materials'][[mi for mi, *_ in gltfio.flatten(s, sb) if any(max(abs(x - y) for x, y in zip(s['materials'][mi]['pbrMetallicRoughness']['baseColorFactor'][:3], k)) < 3e-3 for k in gltfio.ETCH_RGB.values())][0]]['pbrMetallicRoughness']['baseColorFactor'][:3]) + [1], 'metallicFactor': 0, 'roughnessFactor': 0.6}, 'doubleSided': True})
d['meshes'].append({'name': 'laser_etch', 'primitives': [{'attributes': {'POSITION': 'etch_POSITION', 'NORMAL': 'etch_NORMAL'}, 'indices': 'etch_IDX', 'material': len(d['materials']) - 1, 'mode': 4}]})
d['nodes'].append({'name': 'Adom.LaserEtch.MPN', 'mesh': len(d['meshes']) - 1, 'translation': [0, 0, zc], 'scale': [1, 1, 1 / new_sz],
'extras': {'adomRole': 'laser_etch', 'virtualMark': True, 'text': a.mpn, 'face': 'side (-Y, lens band)', 'tooltip': f'Virtual laser-etched MPN ({a.mpn})\nidentification aid, not the manufacturer marking'}})
node.setdefault('children', []).append(len(d['nodes']) - 1)
etch = dict(tris=len(I) // 3, z_mm=[round(float(P[:, 2].min()) * 1000, 4), round(float(P[:, 2].max()) * 1000, 4)])
d.setdefault('asset', {})['generator'] = 'adom led-pass rebuild_composite.py (scene from ' + a.composite.split('/')[-1] + ', LED from step2glb)'
n = gltfio.save(a.out, d, arrays)
rep = dict(out=a.out.split('/')[-1], bytes=n, sha256=hashlib.sha256(open(a.out, 'rb').read()).hexdigest(),
led_node=a.mpn, old_led_tris=old_tris, new_led_tris=new_tris, old_mesh_top_m=float(old_top), new_mesh_top_m=new_top,
old_node_scale_z=old_sz, new_node_scale_z=new_sz, rendered_height_mm=round(new_top * new_sz * 1000, 4), laser_etch=etch,
lens_material={'name': m['name'], 'baseColor_linear': m['pbrMetallicRoughness']['baseColorFactor'], 'replaced': old_lens['name'], 'old_baseColor': old_lens['pbrMetallicRoughness']['baseColorFactor']})
print(json.dumps(rep))
if __name__ == '__main__': main()