by adom
0603 MLCC · 100 nF ±5% X7R · 50 V
master
John Lauer
MPN laser etch: etched STEP is the default, plain kept as -unetched.step; hero GLB with footprint and silkscreen; hero…
b2b8af3
21d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
#!/usr/bin/env python3
"""Build a PROPOSED hero GLB for one slug (local only, nothing published).
Usage: build_hero.py <slug>"""
import json, hashlib, os, sys, subprocess, glob, struct
sys.path.insert(0, os.path.dirname(__file__)); from config import C, LIGHT, DARK
HP = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); T = os.path.join(HP, '_tools')
PY = os.path.expanduser('~/.local/share/adom-aiflow/occ/bin/python')
slug = sys.argv[1]; c = C[slug]; D = os.path.join(HP, slug); W = os.path.join(D, 'work'); os.makedirs(W, exist_ok=True)
sha = lambda p: hashlib.sha256(open(p, 'rb').read()).hexdigest()
def run(*a, py=True):
r = subprocess.run(([PY] if py is True else [py] if py else []) + list(a), capture_output=True, text=True)
if r.returncode: sys.exit(f'FAIL {a[:2]}: {r.stderr[-1500:]}{r.stdout[-800:]}')
return r.stdout
src = os.path.join(D, 'src'); mpn = c['mpn']
vendor = [p for p in glob.glob(f'{src}/*.step') if 'etched' not in p.lower()]; assert len(vendor) == 1, vendor; vendor = vendor[0]
fp = glob.glob(f'{src}/*.kicad_mod')[0]
etched = os.path.join(D, 'etched.step'); inp = etched if os.path.exists(etched) else vendor
# 1 pose into the Adom Z-up seating frame
args = [os.path.join(T, 'xcaf_transform.py'), inp, f'{W}/posed.step'] + sum([['--rot', r] for r in c['pose']], []) + ['--tr', c['tr']]
pose_out = run(*args).strip().splitlines()[-1]
# 2 STEP -> GLB on the shared service
conv = run('step2glb', 'convert', f'{W}/posed.step', '-o', f'{W}/base.glb', py=False)
# 3 pin map
pm = {'schema': 'adom/hero-pinmap@1', 'mpn': mpn, 'derived_from': {'file': os.path.basename(glob.glob(f'{src}/*.kicad_sym')[0]), 'sha256': sha(glob.glob(f'{src}/*.kicad_sym')[0])},
'symbol': {'pins': [{'number': n, 'name': nm, 'electrical_type': et} for n, nm, et in c['pins']]}, 'connect': [{'pin': n, 'pad': n} for n, _, _ in c['pins']]}
pmf = f'{D}/{slug}-hero.pinmap.json'; json.dump(pm, open(pmf, 'w'), indent=1)
# 4 reference art
font = os.path.join(T, 'JetBrainsMono-Regular.ttf')
run(os.path.join(T, 'component-reference-overlay.py'), '--glb', f'{W}/base.glb', '--footprint', fp, '--library', pmf, '--font', font, '--out', f'{W}/hero-raw.glb', '--label-height', str(c['label_h']), py='python3')
rep = json.load(open(f'{W}/hero-raw.overlay.json'))
# 5 finalize
rgb = None if c['mark'] is None else (LIGHT if c['mark'] == 'light' else DARK)
tip = f"{c['mfr']} {mpn}\n{c['pkg']} {c['kind'].lower()}"
names = [p['display_name'] for p in rep['pads']]
spec = {'etch_rgb': rgb, 'label_tooltip': ('Pad labels ' + ', '.join(names) if any(n not in '123456789' for n in names) else 'Pad numbers ' + ', '.join(names)) + '\nreference art, not part of the component',
'component_label': c['kind'], 'component_tooltip': tip, 'tooltips': {},
'etch_extras': {'virtual': True, 'physical': False, 'mpn': mpn, 'tooltip': f'{mpn}\nvirtual MPN mark (identification aid, not the manufacturer marking)',
'source': 'adom-chip-laser STEP inlay (flat top faces), merged onto the original coloured model', 'mark': {'text': mpn, 'color_linear_rgb': rgb}}}
json.dump(spec, open(f'{W}/spec.json', 'w'), indent=1)
fin = run(os.path.join(T, 'finalize.py'), f'{W}/hero-raw.glb', f'{D}/hero.glb', f'{W}/spec.json', py='python3')
# 6 node + triangle check
def glbinfo(p):
b = open(p, 'rb').read(); jn = struct.unpack_from('<I', b, 12)[0]; d = json.loads(b[20:20+jn]); tri = 0
for m in d['meshes']:
for pr in m['primitives']:
tri += (d['accessors'][pr['indices']]['count'] if 'indices' in pr else d['accessors'][pr['attributes']['POSITION']]['count']) // 3
return d, tri
d, tri = glbinfo(f'{D}/hero.glb'); nn = [n.get('name', '') for n in d['nodes']]
n_etch = nn.count('Adom.LaserEtch.MPN'); layers = sorted(n['extras']['adomLayer']['key'] for n in d['nodes'] if n.get('extras', {}).get('adomLayer'))
assert n_etch == (0 if rgb is None else 1), n_etch
assert layers == sorted(['component', 'fp_pad_outlines', 'fp_pad_labels'] + (['fp_silkscreen'] if rep['silkscreen_path_count'] else [])), layers
_, tri_base = glbinfo(f'{W}/base.glb'); _, tri_before = glbinfo(f'{D}/before.glb')
prov = {'schema': 'adom/component-hero-provenance@1', 'mpn': mpn, 'date': '2026-09-30', 'status': 'PROPOSAL (local only, not published)',
'purpose': 'Annotated wiki hero GLB: the component plus nonphysical footprint reference art. Not a board asset: boards use the plain STEP/GLB, which carry no footprint art.',
'hero_glb': {'file': 'hero.glb', 'sha256': sha(f'{D}/hero.glb'), 'bytes': os.path.getsize(f'{D}/hero.glb')},
'inputs': {'original_step': {'file': 'src/' + os.path.basename(vendor), 'sha256': sha(vendor), 'status': 'unchanged'},
'hero_base_glb_sha256': sha(f'{W}/base.glb'),
'footprint': {'file': 'src/' + os.path.basename(fp), 'sha256': sha(fp)},
'pin_map': {'file': os.path.basename(pmf), 'sha256': sha(pmf)},
'font': {'file': 'JetBrainsMono-Regular.ttf (Adom brand mono, OFL, converted from the adom-theme woff2)', 'sha256': rep['font_sha256']}},
'pose': {'frame': 'Adom Z-up footprint seating frame, metres in GLB, seating plane Z = 0', 'transform': c['pose_note'], 'xcaf_transform': pose_out},
'mark': ({'kind': 'manufacturer', 'text': 'AMPLH5030S', 'note': c['etch_note']} if rgb is None else
{'kind': 'virtual', 'text': mpn, 'color_linear_rgb': rgb, 'color_reason': 'light mark on a dark body' if c['mark'] == 'light' else 'dark mark on a light body', 'flat': 'zero-thickness sheet (glyph top faces only)', 'note': c.get('etch_note', '')}),
'reference_art': {'tool': 'component-reference-overlay.py (as shipped in john/eee-fk1v470p provenance/tools)', 'tool_sha256': rep['generator_sha256'], 'label_height_mm': c['label_h'], 'plane_z_mm': rep['plane_z_mm'],
'pad_outline': 'teal #00E6DC dashed (0.07 on / 0.045 off), alpha 0.50, from F.Cu copper pads', 'silkscreen': 'F.SilkS contours, alpha 0.30; REF** and hidden properties omitted', 'pad_labels': 'alpha 0.50; ' + ', '.join(f"{p['display_name']} on pad {p['number']}" for p in rep['pads']), 'overlay_report': rep},
'finishing': {'tool': 'finalize.py (JSON chunk only, binary buffer untouched)', 'tool_sha256': sha(os.path.join(T, 'finalize.py')), 'changes': ['declared extras.adomLayer: component, fp_pad_outlines, fp_silkscreen, fp_pad_labels', 'authored extras.tooltip on the physical nodes'] + (['laser inlay node renamed Adom.LaserEtch.MPN with virtual-mark extras'] if rgb else [])},
'conversion': {'service': 'step2glb convert (shared service-step2glb)', 'posed_step_sha256': sha(f'{W}/posed.step')},
'counts': {'triangles_before_glb': tri_before, 'triangles_base_glb': tri_base, 'triangles_hero_glb': tri, 'etch_nodes': n_etch, 'adom_layers': layers},
'limitations': ['Reference art is display-only, not a manufacturing layer.', 'Silkscreen and labels are hairline outlines; zoom in to read them.'] + (['The MPN mark is a virtual identification aid, not the manufacturer marking.'] if rgb else []),
}
if rgb is not None:
prov['etched_step'] = {'file': 'etched.step', 'sha256': sha(etched), 'frame': 'same as the original STEP (drop-in)', 'source_sha256': sha(vendor),
'tool': 'aiflow-work/merged/tools/laser-etch/etch_part.py (adom-chip-laser service + merge_etch.py flat) [+ _tools/seat_mark.py where the body top sits below the terminations]', 'note': c.get('etch_note', '')}
json.dump(prov, open(f'{D}/hero.provenance.json', 'w'), indent=1)
print(json.dumps({'slug': slug, 'tri_before': tri_before, 'tri_base': tri_base, 'tri_hero': tri, 'etch_nodes': n_etch, 'layers': layers, 'pads': [(p['number'], p['display_name']) for p in rep['pads']], 'silk_paths': rep['silkscreen_path_count']}))