master
John Lauer MPN laser etch on the side face (window kept clear) 1f2f3d4 20d ago
#!/usr/bin/env python3
"""Build <slug>-hero.glb for an LED page, the same way john/eee-fk1v470p's hero was built:
  1. base = `step2glb convert` of the coloured <MPN>.step (Z-up, metres), wrapped (JSON chunk only, the
     binary buffer untouched) in one root node named <MPN> whose Z scale gives the datasheet package height
     (the same factor the page's own composite GLBs already apply to this KiCad model);
  2. component-reference-overlay.py: teal dashed copper-pad outlines 50 %, the page's CURRENT F.SilkS
     (Colby's cathode bracket) 30 %, pad labels 50 %, from <MPN>.kicad_mod + a hero pin map;
  3. finalize.py: extras.adomLayer on component + the three reference layers, authored tooltips.
No laser etch (real parts are unmarked and far too small).
Usage: build_hero.py SLUG MPN S2G.glb --zscale Z --height-mm H --label-height MM --colour-note TEXT"""
import sys, os, json, struct, hashlib, subprocess, argparse
T = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser(); ap.add_argument('slug'); ap.add_argument('mpn'); ap.add_argument('s2g')
ap.add_argument('--zscale', type=float, required=True); ap.add_argument('--height-mm', type=float, required=True)
ap.add_argument('--label-height', type=float, required=True); ap.add_argument('--colour-note', required=True)
ap.add_argument('--outdir'); ap.add_argument('--lens', required=True, help='lens sRGB r,g,b as in the STEP'); a = ap.parse_args()
page = os.path.join(os.path.dirname(T), a.slug); out_dir = a.outdir or page; prov = os.path.join(out_dir, 'provenance'); os.makedirs(prov, exist_ok=True)
work = os.path.join(os.path.dirname(T), 'work'); sha = lambda p: hashlib.sha256(open(p, 'rb').read()).hexdigest()

# 1. base: the step2glb conversion flattened (any node tree) and regrouped by role, under one root node <MPN>
#    whose Z scale gives the datasheet package height; the side MPN mark (if any) is a child node with the
#    inverse Z scale, so the glyphs keep their true height (finalize.py names it Adom.LaserEtch.MPN).
import numpy as np
sys.path.insert(0, T); import gltfio
def lin(c): return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
LENS = [lin(float(v)) for v in a.lens.split(',')]
KNOWN = {'substrate': (0.7776, 0.7697, 0.6261), 'terminal': (0.7087, 0.5040, 0.2104), 'cathode_mark': (0.0946, 0.4200, 0.1666)}
FINISH = {'lens': (0, 0.35), 'substrate': (0, 0.6), 'terminal': (0.7, 0.35), 'cathode_mark': (0, 0.6), 'laser_etch': (0, 0.6)}
s0, sb0 = gltfio.load(a.s2g); G = gltfio.led_groups(s0, sb0, KNOWN, LENS)
etch_rgb = None
for mi, *_ in gltfio.flatten(s0, sb0):
    c = s0['materials'][mi]['pbrMetallicRoughness']['baseColorFactor'][:3]
    if any(max(abs(x - y) for x, y in zip(c, k)) < 3e-3 for k in gltfio.ETCH_RGB.values()): etch_rgb = [round(v, 4) for v in c]
d = {'asset': {'version': '2.0', 'generator': 'adom led-pass build_hero.py (from step2glb)'}, 'scene': 0, 'scenes': [{'nodes': [0]}],
     'nodes': [{'name': a.mpn, 'scale': [1, 1, a.zscale], 'children': [1]}, {'name': a.mpn + ' (STEP body)', 'mesh': 0}],
     'meshes': [{'name': a.mpn, 'primitives': []}], 'materials': [], 'extensionsUsed': ['KHR_materials_clearcoat']}
arrays = {}
for role in ('substrate', 'terminal', 'cathode_mark', 'lens', 'laser_etch'):
    if role not in G: continue
    P, N, I = G[role]; bc = (LENS if role == 'lens' else etch_rgb if role == 'laser_etch' else list(KNOWN[role])) + [1]
    mt, rg = FINISH[role]; m = {'name': role, 'pbrMetallicRoughness': {'baseColorFactor': bc, 'metallicFactor': mt, 'roughnessFactor': rg}}
    if role == 'lens': m['extensions'] = {'KHR_materials_clearcoat': {'clearcoatFactor': 1.0, 'clearcoatRoughnessFactor': 0.08}}
    d['materials'].append(m); mi = len(d['materials']) - 1
    if role == 'laser_etch':
        zc = float((P[:, 2].min() + P[:, 2].max()) / 2); P = P - [0, 0, zc]
        d['meshes'].append({'name': 'laser_etch', 'primitives': []}); d['nodes'].append({'name': 'laser_etch', 'mesh': 1, 'translation': [0, 0, zc], 'scale': [1, 1, 1 / a.zscale]})
        d['nodes'][0]['children'].append(2); tgt = d['meshes'][1]
    else: tgt = d['meshes'][0]
    arrays[f'{role}_P'] = (P.astype(np.float32), 34962); arrays[f'{role}_N'] = (N.astype(np.float32), 34962); arrays[f'{role}_I'] = (I, 34963)
    tgt['primitives'].append({'attributes': {'POSITION': f'{role}_P', 'NORMAL': f'{role}_N'}, 'indices': f'{role}_I', 'material': mi, 'mode': 4})
base = os.path.join(work, f'{a.mpn}.hero-base.glb'); gltfio.save(base, d, arrays)

# 2. pin map + overlay
lbr = json.load(open(os.path.join(page, f'{a.mpn}.adom-lbr.json')))
pins = [dict(p, electrical_type='passive') for p in lbr['symbol']['pins']]
pinmap = {'schema': 'adom/hero-pinmap@1', 'mpn': a.mpn, 'derived_from': {'file': f'{a.mpn}.adom-lbr.json', 'sha256': sha(os.path.join(page, f'{a.mpn}.adom-lbr.json'))},
          'note': 'Pin names and pin-to-pad connects copied from the adom-lbr part; electrical_type added (passive) for the hero only so the overlay labels pads by signal name. The library files are unchanged.',
          'symbol': {'pins': pins}, 'connect': lbr['connect']}
pm = os.path.join(prov, f'{a.slug}-hero.pinmap.json'); json.dump(pinmap, open(pm, 'w'), indent=1)
ov = os.path.join(work, f'{a.slug}-hero.overlay.glb')
r = subprocess.run([sys.executable, os.path.join(T, 'component-reference-overlay.py'), '--glb', base, '--footprint', os.path.join(page, f'{a.mpn}.kicad_mod'),
                    '--library', pm, '--font', os.path.join(T, 'JetBrainsMono-Regular.ttf'), '--out', ov, '--label-height', str(a.label_height)], capture_output=True, text=True)
assert r.returncode == 0, r.stderr
overlay_report = json.loads(open(ov[:-4] + '.overlay.json').read())

# 3. finalize
names = {p['number']: p['name'] for p in lbr['symbol']['pins']}
spec = {'component_label': f'{a.mpn} (coloured model)', 'component_tooltip': f'{a.mpn}\n{a.colour_note}',
        'label_tooltip': 'Pad labels (signal names)\nreference art, not part of the component',
        'tooltips': {a.mpn + ' (STEP body)': f'{a.mpn}\n{a.colour_note}'},
        'asset_extras': {'adomHero': {'mpn': a.mpn, 'kind': 'component + footprint reference art', 'board_use': False}}}
if etch_rgb:
    spec['etch_rgb'] = etch_rgb
    spec['etch_extras'] = {'adomRole': 'laser_etch', 'virtualMark': True, 'text': a.mpn, 'face': 'side (-Y), on the lens band, window kept clear',
                           'tooltip': f'Virtual laser-etched MPN ({a.mpn}) on the side face\nidentification aid: the real part is unmarked'}
sp = os.path.join(work, f'{a.slug}-hero.finalize.json'); json.dump(spec, open(sp, 'w'))
hero = os.path.join(out_dir, f'{a.slug}-hero.glb')
r = subprocess.run([sys.executable, os.path.join(T, 'finalize.py'), ov, hero, sp], capture_output=True, text=True); assert r.returncode == 0, r.stderr
print(r.stdout.strip())
json.dump({'overlay_report': overlay_report, 'base_glb_sha256': sha(base), 'finalize_spec': spec, 'hero_sha256': sha(hero), 'hero_bytes': os.path.getsize(hero),
           'zscale': a.zscale, 'height_mm': a.height_mm}, open(os.path.join(work, f'{a.slug}-hero.build.json'), 'w'), indent=1)