master
John Lauer Add optional LED on/off STEP and emissive GLB variants with independent MPN group and provenance 9231724 1mo ago
import sys,json,pathlib,struct,hashlib,subprocess,math
B=pathlib.Path(__file__).resolve().parent; A=B.parent/'all-marked';sys.path.insert(0,str(B.parent.parent/'cad-python'))
import cadquery as cq
from OCP.TDocStd import TDocStd_Document
from OCP.TCollection import TCollection_ExtendedString
from OCP.STEPCAFControl import STEPCAFControl_Reader,STEPCAFControl_Writer
from OCP.XCAFDoc import XCAFDoc_DocumentTool,XCAFDoc_ColorType
from OCP.TDataStd import TDataStd_Name
from OCP.Quantity import Quantity_Color,Quantity_TOC_RGB
from OCP.STEPControl import STEPControl_AsIs

def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def readglb(p):
 d=p.read_bytes();n=struct.unpack_from('<I',d,12)[0];return json.loads(d[20:20+n]),d[20+n:]
def writeglb(p,j,rest):
 raw=json.dumps(j,separators=(',',':')).encode();raw+=b' '*((-len(raw))%4);p.write_bytes(struct.pack('<III',0x46546c67,2,20+len(raw)+len(rest))+struct.pack('<II',len(raw),0x4e4f534a)+raw+rest)
def descendants(j,node):
 yield node
 for i in node.get('children',[]):yield from descendants(j,j['nodes'][i])

for slug,color,rgb in [('ncd0402g1','green',[.015,1,.07]),('ncd0402b1','blue',[.025,.14,1])]:
 d=B/slug;d.mkdir(exist_ok=True);src=A/slug/(slug+'-mpn.step');out=d/(slug+'-mpn-led-on.step')
 doc=TDocStd_Document(TCollection_ExtendedString('BinXCAF'));r=STEPCAFControl_Reader();r.SetColorMode(True);r.SetNameMode(True);r.ReadFile(str(src));assert r.Transfer(doc)
 original=cq.importers.importStep(str(src)).val();plates=[]
 for face in original.Faces():
  bb=face.BoundingBox()
  if face.geomType()=='PLANE' and face.Area()>.01 and .4999<=bb.zmax<=.50001 and bb.zmin>=.18 and face.normalAt().z>=-.1:
   normal=face.normalAt();skin=cq.Solid.extrudeLinear(face.outerWire(),face.innerWires(),normal.multiply(.0002)).translate(normal.multiply(.00005));plates.append(skin)
 assert len(plates)>=5,len(plates)
 shape=cq.Compound.makeCompound(plates)
 st=XCAFDoc_DocumentTool.ShapeTool_s(doc.Main());ct=XCAFDoc_DocumentTool.ColorTool_s(doc.Main());label=st.AddShape(shape.wrapped,False);TDataStd_Name.Set_s(label,TCollection_ExtendedString('Adom.LED.Emission'));ct.SetColor(label,Quantity_Color(*rgb,Quantity_TOC_RGB),XCAFDoc_ColorType.XCAFDoc_ColorSurf)
 w=STEPCAFControl_Writer();w.SetNameMode(True);w.SetColorMode(True);assert w.Transfer(doc,STEPControl_AsIs);w.Write(str(out))
 cq.Assembly(shape,name='Adom.LED.Emission',color=cq.Color(*rgb)).export(str(d/(slug+'-led-overlay.step')))
 glb=out.with_suffix('.glb');subprocess.run(['step2glb','convert',str(out),'-o',str(glb)],check=True)
 j,rest=readglb(glb);found=[]
 for node in j['nodes']:
  name=node.get('name','')
  if name=='Adom.LED.Emission':
   node['extras']={'adomRole':'led-emission','defaultEnabled':True,'emittingColor':color,'illustrative':True};found.append(name)
   for p in [p for n in descendants(j,node) if 'mesh' in n for p in j['meshes'][n['mesh']]['primitives']]:
    mi=len(j['materials']);m={'name':'Adom.LED.Emissive.'+color,'doubleSided':True,'pbrMetallicRoughness':{'baseColorFactor':rgb+[1],'metallicFactor':0,'roughnessFactor':.4},'emissiveFactor':rgb};j['materials'].append(m);p['material']=mi
  elif name=='Adom.LaserEtch.MPN':node['extras']={'adomRole':'mpn-marking','defaultEnabled':True};found.append(name)
 assert len(set(found))==2,found
 # Fit a smooth glTF cone to the datasheet's full half-intensity angle 130deg.
 outer=math.radians(85);inner=math.acos(math.cos(outer)+(math.cos(math.radians(65))-math.cos(outer))/math.sqrt(.5))
 intensity=.000075 if color=='green' else .000015
 light={'name':'Adom.LED.Light','type':'spot','color':rgb,'intensity':intensity,'range':.015,'spot':{'innerConeAngle':inner,'outerConeAngle':outer},'extras':{'viewingAngleFwhmDegrees':130,'datasheetTypicalCandelaAt5mA':.15 if color=='green' else .03,'illustrationGain':.0005,'profile':'Two-angle smooth cosine approximation, not measured far-field distribution'}}
 j.setdefault('extensions',{})['KHR_lights_punctual']={'lights':[light]};j.setdefault('extensionsUsed',[]).append('KHR_lights_punctual')
 li=len(j['nodes']);j['nodes'].append({'name':'Adom.LED.Light','translation':[0,0,.0005004],'rotation':[1,0,0,0],'extensions':{'KHR_lights_punctual':{'light':0}},'extras':{'adomRole':'led-light'}})
 for sc in j['scenes']:
  roots=[i for i in sc['nodes'] if j['nodes'][i].get('name')=='Adom.LED.Emission'];assert roots
  gi=len(j['nodes']);j['nodes'].append({'name':'Adom.LED.State','children':roots+[li],'extras':{'adomRole':'led-state','defaultEnabled':True}});sc['nodes']=[i for i in sc['nodes'] if i not in roots]+[gi]
 j.setdefault('asset',{})['extras']={'adomDisplayVariants':{'led':'on','mpn':'on'},'notice':'Illustrative LED state, not circuit simulation or calibrated photometry. Toggle named node visibility in a capable viewer.'};writeglb(glb,j,rest)
 off=d/(slug+'-mpn-led-off.glb');jo=json.loads(json.dumps(j));lit={i for i,n in enumerate(jo['nodes']) if n.get('name')=='Adom.LED.State'}
 for sc in jo['scenes']:sc['nodes']=[i for i in sc['nodes'] if i not in lit]
 for n in jo['nodes']:
  if 'children' in n:n['children']=[i for i in n['children'] if i not in lit]
 jo['asset']['extras']['adomDisplayVariants']['led']='off';writeglb(off,jo,rest)
 record={'mpn':slug.upper(),'emittingColor':color,'sourceStep':str(src),'sourceSha256':sha(src),'classification':'AI-authored illustrative light overlay on KiCad-derived generic LED body; manufacturer body fidelity not established','generator':'Codex Astra; build.py with CadQuery/OpenCascade and glTF emissive materials','lightGeometryMm':{'skinThickness':.0002,'outwardClearance':.00005,'method':'Extrude actual upper-lens planar faces along their outward normals','faceCount':len(plates)},'punctualLight':light,'linearRgbIllustration':rgb,'defaultLED':'on','defaultMPN':'on','groups':found,'physicalLimitation':'Illustrative emission, not a calibrated optical or circuit simulation. GLB includes a spot light; STEP has colored geometry only. The lens skin adds at most 0.0002mm to XY bounds. Original dimensions and polarity geometry unchanged.','files':{p.name:sha(p) for p in d.iterdir() if p.suffix in ['.step','.glb']}}
 (d/'led-provenance.json').write_text(json.dumps(record,indent=2));print('READY',slug,flush=True)