← Commit history

Hero GLB per eda-component-hero: footprint pad outlines, silkscreen and pad numbers as nonphysical layers; plain STEP and board GLB unchanged; provenance

John Lauer ·a0f3a2944f ·5d ago ·parent 5b4c135
8 files changed +470−3
README.md+8
@@ -30,6 +30,8 @@ Datasheet: [abracon.com/datasheets/AMPLH5030S.pdf](https://abracon.com/datasheet | `AMPLH5030S-150MT.kicad_mod` | KiCad footprint drawn from the datasheet recommended land pattern (p3: pads 2.15 x 3.50 mm, 6.50 mm outer span, 2.20 mm gap) | | `AMPLH5030S-150MT.step` | Manufacturer 3D model (Abracon `AMPLH5030S.STEP`), original bytes | | `amplh5030s-150mt.glb` | GLB converted from the STEP with `step2glb convert`, posed Z-up |+| `amplh5030s-150mt-hero.glb` | Wiki hero: the same Z-up model plus footprint reference art (pad outlines, silkscreen, pad numbers). Page viewer only, not for boards |+| `provenance/` | Hero provenance: source hashes, pose, pin map, overlay report, the scripts used | | `AMPLH5030S-150MT.adom-lbr.json` | adom-lbr canonical part (symbol + footprint + pin-pad map) | | `AMPLH5030S-150MT.lbr` | Fusion / EAGLE library generated by `adom-lbr generate` | @@ -39,6 +41,12 @@ Datasheet: [abracon.com/datasheets/AMPLH5030S.pdf](https://abracon.com/datasheet  The Abracon STEP is Y-up and its terminals reach 0.05 mm below the body origin (sha256 `8e69d8c60ea0856c13e79bb2444bf254db519e46f3ae8384d6a67f9695b16e52`). The GLB wraps it in one root transform, +90 degrees about X plus a 0.05 mm lift, so the part is Z-up and seated on Z = 0. The footprint's KiCad model binding uses the equivalent `rotate (xyz -90 0 0)` with `offset (xyz 0 0 0.05)`. The model is the AMPLH5030S series body, shared by every inductance value in the series. +## Hero 3D model++The 3D viewer shows `amplh5030s-150mt-hero.glb`: the manufacturer model, unchanged, with nonphysical footprint reference art in the footprint seating frame. Teal dashed outlines (50 percent opacity) trace the copper pads, the footprint silkscreen is drawn at 30 percent and the pad numbers 1 and 2 at 50 percent. Each is a separate layer in the viewer's Layers panel. The top-face `AMPLH5030S` text is Abracon's own printed mark in the STEP; no virtual mark was added.++Boards should use `AMPLH5030S-150MT.step` or `amplh5030s-150mt.glb`, which carry no footprint art. Hashes, transforms and the tools are in `provenance/amplh5030s-150mt-hero.provenance.json`.+ ## License  The footprint, symbol and generated files are MIT. The STEP model remains subject to Abracon's CAD data terms.
amplh5030s-150mt-hero.glbadded
⋯ 1 unchanged line ⋯
package.json+12−1
@@ -2,7 +2,7 @@   "name": "amplh5030s-150mt",   "slug": "amplh5030s-150mt",   "title": "AMPLH5030S-150MT",-  "version": "1.0.0",+  "version": "1.1.0",   "type": "component",   "description": "Abracon AMPLH5030S-150MT: 15 uH 20% molded shielded power inductor, Isat 3.7 A, Irms 3.5 A, DCR 118 mOhm max, 5.7 x 5.3 x 2.8 mm.",   "tags": [@@ -23,6 +23,17 @@       "mouser": "815-AMPLH5030S-150T",       "digikey": "535-AMPLH5030S-150MTTR-ND",       "lcsc": false+    },+    "parts": {+      "symbol": "AMPLH5030S-150MT.kicad_sym",+      "footprint": "AMPLH5030S-150MT.kicad_mod",+      "model_3d": "amplh5030s-150mt-hero.glb",+      "model_3d_plain": "amplh5030s-150mt.glb",+      "step": "AMPLH5030S-150MT.step",+      "lbr": "AMPLH5030S-150MT.lbr"+    },+    "viewer_3d": {+      "tooltips": "authored"     }   },   "dependencies": {},
page.json+13−2
@@ -2,7 +2,7 @@   "name": "amplh5030s-150mt",   "slug": "amplh5030s-150mt",   "title": "AMPLH5030S-150MT",-  "version": "1.0.0",+  "version": "1.1.0",   "type": "component",   "description": "Abracon AMPLH5030S-150MT: 15 uH 20% molded shielded power inductor, Isat 3.7 A, Irms 3.5 A, DCR 118 mOhm max, 5.7 x 5.3 x 2.8 mm.",   "tags": [@@ -23,6 +23,17 @@       "mouser": "815-AMPLH5030S-150T",       "digikey": "535-AMPLH5030S-150MTTR-ND",       "lcsc": false+    },+    "parts": {+      "symbol": "AMPLH5030S-150MT.kicad_sym",+      "footprint": "AMPLH5030S-150MT.kicad_mod",+      "model_3d": "amplh5030s-150mt-hero.glb",+      "model_3d_plain": "amplh5030s-150mt.glb",+      "step": "AMPLH5030S-150MT.step",+      "lbr": "AMPLH5030S-150MT.lbr"+    },+    "viewer_3d": {+      "tooltips": "authored"     }   },   "dependencies": {},@@ -41,4 +52,4 @@     "name": "John Lauer",     "email": "[email protected]"   }-}\ No newline at end of file+}
provenance/amplh5030s-150mt-hero.pinmap.jsonadded+41
@@ -0,0 +1,41 @@+{+ "schema": "adom/hero-pinmap@1",+ "mpn": "AMPLH5030S-150MT",+ "derived_from": {+  "file": "AMPLH5030S-150MT.adom-lbr.json",+  "sha256": "b31b74ef86d55bc42cc96192760cb75911f460bb8cf422f730fd2c8da67c6071"+ },+ "note": "Two-terminal passive: labels are the pad numbers. electrical_type added for the overlay tool; the library symbol is unchanged.",+ "symbol": {+  "pins": [+   {+    "number": "1",+    "name": "1",+    "x": 0,+    "y": 150,+    "length": 50,+    "rotation": 270,+    "electrical_type": "passive"+   },+   {+    "number": "2",+    "name": "2",+    "x": 0,+    "y": -150,+    "length": 50,+    "rotation": 90,+    "electrical_type": "passive"+   }+  ]+ },+ "connect": [+  {+   "pin": "1",+   "pad": "1"+  },+  {+   "pin": "2",+   "pad": "2"+  }+ ]+}\ No newline at end of file
provenance/amplh5030s-150mt-hero.provenance.jsonadded+181
@@ -0,0 +1,181 @@+{+ "schema": "adom/component-hero-provenance@1",+ "mpn": "AMPLH5030S-150MT",+ "date": "2026-09-30",+ "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": "amplh5030s-150mt-hero.glb",+  "sha256": "c1451d1de65e8d2d4ac937780e164cb16003b9548c6685f064a75cf2ff013232",+  "bytes": 125860+ },+ "inputs": {+  "original_step": {+   "file": "AMPLH5030S-150MT.step",+   "sha256": "8e69d8c60ea0856c13e79bb2444bf254db519e46f3ae8384d6a67f9695b16e52",+   "status": "unchanged"+  },+  "board_glb": {+   "file": "amplh5030s-150mt.glb",+   "sha256": "b0dd68f96c1958723ab700c4c41f7c517a875d926c6859e19a123a3f94d0543e",+   "status": "unchanged (clean board-use model, no footprint art)"+  },+  "hero_base_glb_sha256": "b0dd68f96c1958723ab700c4c41f7c517a875d926c6859e19a123a3f94d0543e",+  "footprint": {+   "file": "AMPLH5030S-150MT.kicad_mod",+   "sha256": "8e19acacd48f3eba61ccffd5f5e1eae92473470d12b1ad7a0a93afedb1bce53f"+  },+  "library": {+   "file": "AMPLH5030S-150MT.adom-lbr.json",+   "sha256": "b31b74ef86d55bc42cc96192760cb75911f460bb8cf422f730fd2c8da67c6071"+  },+  "pin_map": {+   "file": "provenance/amplh5030s-150mt-hero.pinmap.json",+   "sha256": "a6c8cb99ca79063f7af6a84bacec52a633b737c550e0cac844bd7039510b5e51"+  },+  "font": {+   "file": "JetBrainsMono-Regular.ttf (Adom brand mono, OFL, converted from the adom-theme woff2)",+   "sha256": "c1bfaf2ccf7c56dc25af0cc595c4c6a064a8ebf88930d4543805a9d90692e581"+  }+ },+ "pose": {+  "frame": "Adom Z-up footprint seating frame, metres in GLB, seating plane Z = 0",+  "transform": "source STEP is Y-up; the unchanged page board GLB amplh5030s-150mt.glb already carries the verified pose (+90 deg about X, +0.05 mm Z, equal to KiCad model rotate -90 0 0 offset 0 0 0.05)"+ },+ "mark": {+  "kind": "manufacturer",+  "text": "AMPLH5030S",+  "note": "The Abracon STEP carries a printed AMPLH5030S mark on the top face (white faces in the model). Kept as is; no virtual etch was added.",+  "orientation": "along X (the footprint pad axis), readable from the -Y edge",+  "size_mm": "about 3.0 x 0.4 (measured from a top-view render of the vendor model)"+ },+ "reference_art": {+  "tool": "component-reference-overlay.py from the eda-engineering eda-component-hero PR (/home/adom/aiflow-esc-astra/eda-hero-pr, sha256 215e095ac1225c5920f99fcd8977c094db6d525878d0ede12fb5df46e9e8d7f6), newer than the published eda-engineering 0.2.3 which does not ship it; local change: --label-height option (default 0.17 mm unchanged)",+  "tool_sha256": "aad792e4550350dc175c098711cc77e6574a2b1191191bcf57865fa882580e18",+  "label_height_mm": 0.8,+  "plane_z_mm": -0.02,+  "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; pad numbers (unnamed passive terminals)",+  "overlay_report": {+   "font_file": "JetBrainsMono-Regular.ttf",+   "font_sha256": "c1bfaf2ccf7c56dc25af0cc595c4c6a064a8ebf88930d4543805a9d90692e581",+   "generator_sha256": "aad792e4550350dc175c098711cc77e6574a2b1191191bcf57865fa882580e18",+   "source_glb_sha256": "b0dd68f96c1958723ab700c4c41f7c517a875d926c6859e19a123a3f94d0543e",+   "footprint_sha256": "8e19acacd48f3eba61ccffd5f5e1eae92473470d12b1ad7a0a93afedb1bce53f",+   "derivative_sha256": "3da36fd227c989c249ce50d5b3a6d82c0792c5707da7e072a12f3ff0078f79f0",+   "manufacturer_geometry_buffer_prefix_unchanged": true,+   "units": "metre",+   "upAxis": "Z",+   "plane_z_mm": -0.02,+   "pad_outline": "copper perimeter, not solder mask or paste",+   "dash_mm": 0.07,+   "gap_mm": 0.045,+   "silkscreen_path_count": 2,+   "silkscreen_opacity": 0.3,+   "pad_outline_opacity": 0.5,+   "pad_outline_color": "teal #00E6DC",+   "uninstantiated_reference_placeholder": "omitted from component display",+   "pad_name_opacity": 0.5,+   "pad_label_height_mm": 0.8,+   "pad_names": "signal names resolved through canonical pin-to-pad mapping; active-low overbars preserved",+   "library_sha256": "a6c8cb99ca79063f7af6a84bacec52a633b737c550e0cac844bd7039510b5e51",+   "pads": [+    {+     "number": "1",+     "drill_boundary_drawn": false,+     "signal_name": "1",+     "display_name": "1",+     "pad_number_fallback": true,+     "active_low_overbar": false,+     "center_mm": [+      -2.175,+      -0.0+     ],+     "copper_size_mm": [+      2.15,+      3.5+     ],+     "copper_outline_bounds_mm": [+      [+       -3.25,+       -1.75+      ],+      [+       -1.0999999999999999,+       1.75+      ]+     ],+     "label_position_mm": [+      -3.8259999999999996,+      -0.0+     ]+    },+    {+     "number": "2",+     "drill_boundary_drawn": false,+     "signal_name": "2",+     "display_name": "2",+     "pad_number_fallback": true,+     "active_low_overbar": false,+     "center_mm": [+      2.175,+      -0.0+     ],+     "copper_size_mm": [+      2.15,+      3.5+     ],+     "copper_outline_bounds_mm": [+      [+       1.0999999999999999,+       -1.75+      ],+      [+       3.25,+       1.75+      ]+     ],+     "label_position_mm": [+      3.808933333333333,+      -0.0+     ]+    }+   ],+   "overlay_nodes": 3,+   "limitations": [+    "Nonphysical reference lines, not a manufacturing layer or manufacturer STEP geometry.",+    "Silkscreen text follows source text/placement but uses the supplied display font.",+    "Physical body can occlude artwork on the seating plane; inspect underside or hide the body."+   ]+  }+ },+ "finishing": {+  "tool": "finalize.py (JSON chunk only, binary buffer untouched)",+  "tool_sha256": "2b97fc95d2236b55aa97548a3bf0694fe7049257d9853f0b12812ec86f6f6ef5",+  "changes": [+   "declared extras.adomLayer: component, fp_pad_outlines, fp_silkscreen, fp_pad_labels",+   "authored extras.tooltip on the physical nodes (vendor CAD names are not reader-facing)"+  ]+ },+ "review": {+  "renderer": "headless Chromium, three.js r169 GLTFLoader, Z-up camera",+  "views": [+   "top",+   "oblique",+   "underside",+   "pad 1 corner",+   "grazing close-up of the mark"+  ],+  "checked": [+   "MPN readable",+   "model colours intact",+   "no z-fighting between mark and top face",+   "pad outlines frame the physical terminals from below",+   "pad 1 at -X"+  ]+ },+ "limitations": [+  "Reference art is display-only, not a manufacturing layer.",+  "Silkscreen and labels are hairline outlines; zoom in to read them."+ ]+}\ No newline at end of file
provenance/tools/component-reference-overlay.pyadded+184
@@ -0,0 +1,184 @@+#!/usr/bin/env python3+"""Add nonphysical footprint reference artwork to a Z-up component GLB.+Original geometry buffers and source files are preserved. Units: GLB metres,+KiCad footprint millimetres with Y down. Glyph outlines use a supplied TrueType font.+"""+import argparse,json,re,math,struct,hashlib+from pathlib import Path+from fontTools.ttLib import TTFont+from fontTools.pens.basePen import BasePen+class Atom(str):pass++def parse(s):+ stack=[];root=None+ for t in re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+',s):+  if t=='(':+   a=[]+   if stack:stack[-1].append(a)+   else:root=a+   stack.append(a)+  elif t==')':stack.pop()+  else:stack[-1].append(json.loads(t) if t.startswith('"') else Atom(t))+ assert not stack+ return root+def dump(n):+ return '('+' '.join(map(dump,n))+')' if isinstance(n,list) else str(n) if isinstance(n,Atom) else json.dumps(n,ensure_ascii=False)+def fields(n,key):return [x for x in n if isinstance(x,list) and x and x[0]==key]+def field(n,key,default=None):return next(iter(fields(n,key)),default)+def xy(n,key):return tuple(map(float,field(n,key)[1:3]))+def circle(c,r,start=0,span=2*math.pi,count=48):return [(c[0]+r*math.cos(start+span*i/count),c[1]+r*math.sin(start+span*i/count)) for i in range(count+1)]+def rounded(w,h,r):+ out=[]+ for cx,cy,a in [(w/2-r,h/2-r,0),(-w/2+r,h/2-r,math.pi/2),(-w/2+r,-h/2+r,math.pi),(w/2-r,-h/2+r,math.pi*1.5)]:out+=circle((cx,cy),r,a,math.pi/2,8)+ return out+[out[0]]+def dashed(points,on=.07,off=.045):+ out=[];distance=0+ for a,b in zip(points,points[1:]):+  length=math.dist(a,b);pos=0+  while pos<length-1e-12:+   phase=distance%(on+off);take=min(length-pos,(on-phase) if phase<on else (on+off-phase))+   if take<1e-10:distance+=1e-9;continue+   if phase<on:+    out.append([(a[0]+(b[0]-a[0])*u/length,a[1]+(b[1]-a[1])*u/length) for u in [pos,pos+take]])+   distance+=take;pos+=take+ return out+class Flatten(BasePen):+ def __init__(self,g):super().__init__(g);self.paths=[];self.path=[]+ def _moveTo(self,p):self.path=[p]+ def _lineTo(self,p):self.path.append(p)+ def _curveToOne(self,a,b,c):+  p=self._getCurrentPoint()+  for i in range(1,9):+   t=i/8;self.path.append(tuple((1-t)**3*p[j]+3*(1-t)**2*t*a[j]+3*(1-t)*t*t*b[j]+t**3*c[j] for j in range(2)))+ def _qCurveToOne(self,a,b):+  p=self._getCurrentPoint()+  for i in range(1,9):+   t=i/8;self.path.append(tuple((1-t)**2*p[j]+2*(1-t)*t*a[j]+t*t*b[j] for j in range(2)))+ def _closePath(self):self.path.append(self.path[0]);self.paths.append(self.path);self.path=[]+ def _endPath(self):self.paths.append(self.path);self.path=[]+def main():+ ap=argparse.ArgumentParser();ap.add_argument('--glb',type=Path,required=True);ap.add_argument('--footprint',type=Path,required=True);ap.add_argument('--library',type=Path,required=True);ap.add_argument('--font',type=Path,required=True);ap.add_argument('--out',type=Path,required=True);ap.add_argument('--label-height',type=float,default=.17,help='pad label glyph height in mm (default .17, sized for fine-pitch ICs)');args=ap.parse_args()+ fp=parse(args.footprint.read_text());library=json.loads(args.library.read_text());pin_names={p['number']:p['name'] for p in library['symbol']['pins']};pad_names={}+ for c in library['connect']:+  name=pin_names[c['pin']]+  if c['pad'] in pad_names:assert pad_names[c['pad']]==name,'Ambiguous pad name mapping'+  pad_names[c['pad']]=name+ font=TTFont(args.font);glyphs=font.getGlyphSet();cmap=font.getBestCmap();em=font['head'].unitsPerEm+ def textpaths(text,at,height):+  paths=[];cursor=0+  for ch in text:+   name=cmap.get(ord(ch));assert name,repr(ch)+   pen=Flatten(glyphs);glyphs[name].draw(pen);paths.extend([[(x+cursor,y) for x,y in line] for line in pen.paths]);cursor+=glyphs[name].width+  scale=height/(em*.75)+  return [[(at[0]+(x-cursor/2)*scale,at[1]+(y-em*.35)*scale) for x,y in line] for line in paths]+ pads=[];padlines=[];silk=[];labels=[]+ for p in fields(fp,'pad'):+  if not any(x in field(p,'layers')[1:] for x in ['F.Cu','*.Cu']):continue+  number=p[1];at=xy(p,'at');size=xy(p,'size');angle=math.radians(-float(field(p,'at')[3]) if len(field(p,'at'))>3 else 0);shape=p[3]+  if shape=='roundrect':r=min(size)*float(field(p,'roundrect_rratio')[1]);outline=rounded(*size,r)+  elif shape=='rect':outline=rounded(*size,0)+  elif shape=='circle':outline=circle((0,0),size[0]/2)+  elif shape=='oval':outline=rounded(*size,min(size)/2)+  elif shape=='custom':+   from shapely.geometry import Polygon,Point,LineString,box+   from shapely.ops import unary_union+   shapes=[]+   anchor=field(field(p,'options',[]),'anchor',['anchor','rect'])[1]+   shapes.append(Point(0,0).buffer(size[0]/2) if anchor=='circle' else box(-size[0]/2,-size[1]/2,size[0]/2,size[1]/2))+   for pr in field(p,'primitives',[])[1:]:+    width=float(field(pr,'width',['width',0])[1])+    if pr[0]=='gr_poly':+     pts=[tuple(map(float,q[1:3])) for q in field(pr,'pts')[1:]];shapes.append(Polygon(pts).buffer(width/2))+    elif pr[0]=='gr_line':shapes.append(LineString([xy(pr,'start'),xy(pr,'end')]).buffer(width/2))+    elif pr[0]=='gr_circle':shapes.append(Point(*xy(pr,'center')).buffer(math.dist(xy(pr,'center'),xy(pr,'end'))+width/2))+    else:raise ValueError('Unsupported custom copper primitive '+pr[0])+   merged=unary_union(shapes);assert merged.geom_type=='Polygon','Disconnected custom copper pad requires explicit review';outline=list(merged.exterior.coords)+  else:raise ValueError('Unsupported pad shape '+shape)+  def transform(q):return (at[0]+q[0]*math.cos(angle)-q[1]*math.sin(angle),-(at[1]+q[0]*math.sin(angle)+q[1]*math.cos(angle)))+  outline=list(map(transform,outline));padlines+=dashed(outline)+  drill=field(p,'drill',[])+  if drill:+   dims=[float(v) for v in drill[1:] if not isinstance(v,list) and v!='oval'];off=field(drill,'offset',['offset',0,0]);center=(float(off[1]),float(off[2]))+   hole=rounded(dims[0],dims[1],min(dims)/2) if drill[1]=='oval' else circle((0,0),dims[0]/2)+   hole=[transform((x+center[0],y+center[1])) for x,y in hole];padlines+=dashed(hole)+  bounds=([min(q[i] for q in outline) for i in range(2)],[max(q[i] for q in outline) for i in range(2)])+  raw_name=pad_names.get(number,number) or number;+  if not raw_name:raise ValueError('No electrical mapping for copper pad '+number)+  active_low=bool(re.fullmatch(r'~\{[^}]+\}',raw_name));display_name=raw_name[2:-1] if active_low else raw_name+  unnamed_passive=len(library['symbol']['pins'])==2 and all(q['electrical_type']=='passive' for q in library['symbol']['pins']) and raw_name in ['', '~', number]+  if unnamed_passive:display_name=number+  if not display_name:raise ValueError('Missing signal name for pad '+number)+  glyphs_local=textpaths(display_name,(0,0),args.label_height);points=[q for line in glyphs_local for q in line];xlo=min(q[0] for q in points);xhi=max(q[0] for q in points)+  if active_low:+   bar_y=max(q[1] for q in points)+.025;glyphs_local.append([(xlo,bar_y),(xhi,bar_y)])+  padcenters=[xy(q,'at') for q in fields(fp,'pad') if any(v in field(q,'layers')[1:] for v in ['F.Cu','*.Cu'])]+  maxx=max(abs(v[0]) for v in padcenters) or 1;maxy=max(abs(v[1]) for v in padcenters) or 1+  vertical=abs(at[1])/maxy > abs(at[0])/maxx+.05+  if vertical:+   # Radial labels outside top/bottom rows, with text along the outward axis.+   glyphs_local=[[(-y,x) for x,y in line] for line in glyphs_local];points=[q for line in glyphs_local for q in line];xlo=min(q[0] for q in points);xhi=max(q[0] for q in points)+   dx=at[0];label_y=(bounds[1][1]+.32-min(q[1] for q in points)) if at[1]<0 else bounds[0][1]-.32-max(q[1] for q in points)+  else:+   dx=(bounds[0][0]-.32-xhi) if at[0]<0 else (bounds[1][0]+.32-xlo);label_y=-at[1]+  if abs(at[0])<.01 and abs(at[1])<.01:+   ys=[-xy(q,'at')[1]+sign*xy(q,'size')[1]/2 for q in fields(fp,'pad') for sign in [-1,1]];dx=0;label_y=min(ys)-.65+  labelat=(dx,label_y);labels += [[(x+dx,y+label_y) for x,y in line] for line in glyphs_local]+  pads.append({'number':number,'drill_boundary_drawn':bool(drill),'signal_name':raw_name,'display_name':display_name,'pad_number_fallback':unnamed_passive,'active_low_overbar':active_low,'center_mm':[at[0],-at[1]],'copper_size_mm':size,'copper_outline_bounds_mm':bounds,'label_position_mm':labelat})+ for p in fp:+  if not isinstance(p,list) or field(p,'layer')!=['layer','F.SilkS']:continue+  kind=p[0]+  if kind=='fp_line':silk.append([(x,-y) for x,y in [xy(p,'start'),xy(p,'end')]])+  elif kind=='fp_circle':+   c=xy(p,'center');e=xy(p,'end');silk.append([(x,-y) for x,y in circle(c,math.dist(c,e))])+  elif kind=='fp_rect':+   a,b=xy(p,'start'),xy(p,'end');silk.append([(x,-y) for x,y in [a,(b[0],a[1]),b,(a[0],b[1]),a]])+  elif kind=='fp_poly':+   points=[tuple(map(float,q[1:3])) for q in field(p,'pts')[1:]];silk.append([(x,-y) for x,y in points+[points[0]]])+  elif kind=='fp_arc':+   import numpy as np+   a,m,z=[np.array(xy(p,k)) for k in ['start','mid','end']];center=np.linalg.solve(2*np.array([m-a,z-a]),np.array([m@m-a@a,z@z-a@a]));rad=float(np.linalg.norm(a-center));t0,t1,t2=[math.atan2(*(v-center)[::-1]) for v in [a,m,z]];span=(t2-t0)%(2*math.pi)+   if (t1-t0)%(2*math.pi)>span:span-=2*math.pi+   silk.append([(x,-y) for x,y in circle(center,rad,t0,span,48)])+  elif kind=='property':+   # Identity placeholders/hidden properties are not physical footprint silk.+   if field(p,'hide') or 'hide' in p or p[1]=='Reference' or p[2] in ['REF**','REF*','${REFERENCE}']:continue+   at=xy(p,'at');effects=field(p,'effects');height=float(field(field(effects,'font'),'size')[2]);silk+=textpaths(p[2],(at[0],-at[1]),height)+  elif kind=='fp_text':+   if 'hide' in p or (p[1]=='reference' and p[2] in ['REF**','REF*','${REFERENCE}']):continue+   at=xy(p,'at');effects=field(p,'effects');height=float(field(field(effects,'font'),'size')[2]);silk+=textpaths(p[2],(at[0],-at[1]),height)+  else:raise ValueError('Unsupported silk primitive '+kind)+ b=args.glb.read_bytes();jn,jt=struct.unpack_from('<II',b,12);d=json.loads(b[20:20+jn]);bn,bt=struct.unpack_from('<II',b,20+jn);assert bt==0x004e4942+ original=b[28+jn:28+jn+bn];binary=bytearray(original);assert len(d['buffers'])==1 and 'uri' not in d['buffers'][0]+ ext=d.setdefault('extensionsUsed',[])+ if 'KHR_materials_unlit' not in ext:ext.append('KHR_materials_unlit')+ parent={'name':'Adom reference artwork (nonphysical)','children':[],'extras':{'physical':False,'purpose':'footprint copper outlines, silkscreen and pad identifiers','sourceFootprint':args.footprint.name,'units':'metre','upAxis':'Z'}}+ parentid=len(d['nodes']);d['nodes'].append(parent)+ roots=d['scenes'][d.get('scene',0)]['nodes'];poses=[i for i in roots if d['nodes'][i].get('name','').startswith('Adom.Hero.')]+ if len(poses)==1:d['nodes'][poses[0]].setdefault('children',[]).append(parentid)+ elif not poses:roots.append(parentid)+ else:raise ValueError('Ambiguous hero presentation transform')+ def add(name,paths,color,alpha,width):+  vertices=[];z=-.00002+  for path in paths:+   for a,c in zip(path,path[1:]):+    length=math.dist(a,c)+    if length<1e-10:continue+    nx=-(c[1]-a[1])/length*width/2;ny=(c[0]-a[0])/length*width/2+    quad=[(a[0]+nx,a[1]+ny),(a[0]-nx,a[1]-ny),(c[0]-nx,c[1]-ny),(c[0]+nx,c[1]+ny)]+    for i in [0,1,2,0,2,3]:vertices.append((quad[i][0]/1000,quad[i][1]/1000,z))+  if not vertices:return+  binary.extend(b'\0'*((-len(binary))%4));offset=len(binary);raw=b''.join(struct.pack('<fff',*v) for v in vertices);binary.extend(raw)+  bv=len(d['bufferViews']);d['bufferViews'].append({'buffer':0,'byteOffset':offset,'byteLength':len(raw),'target':34962})+  ac=len(d['accessors']);d['accessors'].append({'bufferView':bv,'componentType':5126,'count':len(vertices),'type':'VEC3','min':[min(v[i] for v in vertices) for i in range(3)],'max':[max(v[i] for v in vertices) for i in range(3)]})+  material=len(d['materials']);d['materials'].append({'name':name,'pbrMetallicRoughness':{'baseColorFactor':color+[alpha],'metallicFactor':0,'roughnessFactor':1},'alphaMode':'BLEND' if alpha<1 else 'OPAQUE','doubleSided':True,'extensions':{'KHR_materials_unlit':{}},'extras':{'referenceArtwork':True,'opacity':alpha}})+  mesh=len(d['meshes']);d['meshes'].append({'name':name,'primitives':[{'attributes':{'POSITION':ac},'material':material,'mode':4}]})+  node=len(d['nodes']);d['nodes'].append({'name':name,'mesh':mesh,'extras':{'physical':False}});parent['children'].append(node)+ add('Footprint pads - teal dashed outlines - 50 percent opacity',padlines,[0,.902,.863],.5,.015)+ add('Silkscreen reference lines - 30 percent opacity',silk,[.92,.96,1],.3,.012)+ add('Signal names - 50 percent opacity',labels,[.92,.96,1],.5,.009)+ d['buffers'][0]['byteLength']=len(binary);assert binary[:len(original)]==original+ j=json.dumps(d,separators=(',',':')).encode();j+=b' '*((-len(j))%4);binary.extend(b'\0'*((-len(binary))%4))+ out=struct.pack('<III',0x46546c67,2,28+len(j)+len(binary))+struct.pack('<II',len(j),0x4e4f534a)+j+struct.pack('<II',len(binary),0x004e4942)+binary;args.out.write_bytes(out)+ report={'font_file':args.font.name,'font_sha256':hashlib.sha256(args.font.read_bytes()).hexdigest(),'generator_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'source_glb_sha256':hashlib.sha256(b).hexdigest(),'footprint_sha256':hashlib.sha256(args.footprint.read_bytes()).hexdigest(),'derivative_sha256':hashlib.sha256(out).hexdigest(),'manufacturer_geometry_buffer_prefix_unchanged':True,'units':'metre','upAxis':'Z','plane_z_mm':-.02,'pad_outline':'copper perimeter, not solder mask or paste','dash_mm':.07,'gap_mm':.045,'silkscreen_path_count':len(silk),'silkscreen_opacity':.3,'pad_outline_opacity':.5,'pad_outline_color':'teal #00E6DC','uninstantiated_reference_placeholder':'omitted from component display','pad_name_opacity':.5,'pad_label_height_mm':args.label_height,'pad_names':'signal names resolved through canonical pin-to-pad mapping; active-low overbars preserved','library_sha256':hashlib.sha256(args.library.read_bytes()).hexdigest(),'pads':pads,'overlay_nodes':len(parent['children']),'limitations':['Nonphysical reference lines, not a manufacturing layer or manufacturer STEP geometry.','Silkscreen text follows source text/placement but uses the supplied display font.','Physical body can occlude artwork on the seating plane; inspect underside or hide the body.']}+ args.out.with_suffix('.overlay.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report))+if __name__=='__main__':main()
provenance/tools/finalize.pyadded+31
@@ -0,0 +1,31 @@+#!/usr/bin/env python3+"""Hero GLB finishing pass (JSON chunk only; the binary buffer is untouched):+- the adom-chip-laser inlay node (found by its material colour) becomes Adom.LaserEtch.MPN with provenance extras+- the physical model root, pad outlines, silkscreen and pad labels are declared as extras.adomLayer+- authored extras.tooltip on physical meshes (vendor CAD names such as NAUO3 are not reader-facing)+Usage: finalize.py in.glb out.glb spec.json"""+import json, struct, sys, hashlib+src, dst, spec = sys.argv[1], sys.argv[2], json.load(open(sys.argv[3]))+b = open(src, 'rb').read(); jn = struct.unpack_from('<I', b, 12)[0]; d = json.loads(b[20:20+jn]); rest = b[20+jn:]+nodes = d['nodes']; roots = d['scenes'][d.get('scene', 0)]['nodes']+def near(c, t): return all(abs(x-y) < 2e-3 for x, y in zip(c, t))+etch_mats = [i for i, m in enumerate(d['materials']) if spec.get('etch_rgb') and near(m.get('pbrMetallicRoughness', {}).get('baseColorFactor', [9])[:3], spec['etch_rgb'])]+found = []+for n in nodes:+    if 'mesh' in n and etch_mats and all(p.get('material') in etch_mats for p in d['meshes'][n['mesh']]['primitives']):+        n['name'] = 'Adom.LaserEtch.MPN'; n['extras'] = dict(n.get('extras', {}), **spec['etch_extras']); found.append(n)+if spec.get('etch_rgb'): assert len(found) == 1, f'etch node match {len(found)}'+for i, n in enumerate(nodes):+    nm = n.get('name', '')+    if nm.startswith('Footprint pads'): n['extras'].update(adomLayer={'key': 'fp_pad_outlines', 'label': 'Pad outlines', 'group': 'Footprint (reference art)'}, tooltip='Footprint copper pad outline\nreference art, not part of the component')+    elif nm.startswith('Silkscreen reference'): n['extras'].update(adomLayer={'key': 'fp_silkscreen', 'label': 'Silkscreen', 'group': 'Footprint (reference art)'}, tooltip='Footprint silkscreen\nreference art, not part of the component')+    elif nm.startswith('Signal names'): n['extras'].update(adomLayer={'key': 'fp_pad_labels', 'label': 'Pad labels', 'group': 'Footprint (reference art)'}, tooltip=spec['label_tooltip'])+    for key, tip in spec.get('tooltips', {}).items():+        if nm == key: n.setdefault('extras', {})['tooltip'] = tip+phys = [r for r in roots if not nodes[r].get('name', '').startswith(('Adom reference artwork', 'Adom.LaserEtch'))]+assert len(phys) == 1, phys+nodes[phys[0]].setdefault('extras', {}).update(adomLayer={'key': 'component', 'label': spec['component_label'], 'group': 'Component'}, tooltip=spec['component_tooltip'])+d.setdefault('asset', {}).setdefault('extras', {}).update(spec.get('asset_extras', {}))+j = json.dumps(d, separators=(',', ':')).encode(); j += b' ' * ((-len(j)) % 4)+out = struct.pack('<III', 0x46546c67, 2, 20 + len(j) + len(rest)) + struct.pack('<II', len(j), 0x4e4f534a) + j + rest+open(dst, 'wb').write(out); print(dst, len(out), hashlib.sha256(out).hexdigest()[:16], 'etch nodes', len(found))