main
John Lauer Fan component engineering assets and verified simulated rotor motion prototype 422c1bc 12d ago
"""Add a rigid rotor hierarchy and one-turn glTF animation to our authored model."""
from pathlib import Path
import json,struct,math,hashlib
r=Path(__file__).resolve().parents[1];src=r/'cad/MF40200V1-1000U-G99-ai-nominal.glb';b=src.read_bytes();n=struct.unpack_from('<I',b,12)[0];g=json.loads(b[20:20+n]);start=20+n;nb=struct.unpack_from('<I',b,start)[0];binary=bytearray(b[start+8:start+8+nb])
rotor=[i for i,v in enumerate(g['nodes']) if v.get('name','').startswith(('motor_hub_','illustrative_blade_'))]
assert len(rotor)==8
static=[i for i in g['nodes'][0]['children'] if i not in rotor]
ri=len(g['nodes']);g['nodes']+=[{'name':'Fan.Rotor','children':rotor,'rotation':[0,0,0,1],'extras':{'role':'rotor','axisLocal':[0,0,1],'pivotLocalMeters':[0,0,0]}},{'name':'Fan.Housing','children':static}]
g['nodes'][0]['name']='Fan.Assembly';g['nodes'][0]['children']=[ri,ri+1]
def accessor(values,kind):
 while len(binary)%4:binary.append(0)
 off=len(binary);flat=[x for v in values for x in (v if isinstance(v,list) else [v])];binary.extend(struct.pack('<'+'f'*len(flat),*flat));vi=len(g.setdefault('bufferViews',[]));g['bufferViews'].append({'buffer':0,'byteOffset':off,'byteLength':len(flat)*4});ai=len(g['accessors']);a={'bufferView':vi,'componentType':5126,'count':len(values),'type':kind};
 if kind=='SCALAR':a.update(min=[min(values)],max=[max(values)])
 g['accessors'].append(a);return ai
times=[0,.25,.5,.75,1];quats=[[0,0,math.sin(-math.pi*t),math.cos(-math.pi*t)] for t in times]
t=accessor(times,'SCALAR');q=accessor(quats,'VEC4')
g['animations']=[{'name':'Fan.Rotor.Revolution','samplers':[{'input':t,'output':q,'interpolation':'LINEAR'}],'channels':[{'sampler':0,'target':{'node':ri,'path':'rotation'}}],'extras':{'revolutionsPerCycle':1,'cycleDurationSeconds':1,'defaultPlayback':'paused'}}]
g['buffers'][0]['byteLength']=len(binary)
g['asset'].setdefault('extras',{})['adomComponentMotion']={'schema':'adom/component-motion@1','states':'../states.json','authoredUpAxis':'Z','defaultState':'off','geometryFidelity':'nominal envelope; rotor illustrative'}
js=json.dumps(g,separators=(',',':')).encode();js+=b' '*((-len(js))%4);binary+=b'\0'*((-len(binary))%4)
out=struct.pack('<III',0x46546c67,2,12+8+len(js)+8+len(binary))+struct.pack('<II',len(js),0x4e4f534a)+js+struct.pack('<II',len(binary),0x004e4942)+binary
p=r/'states/fan-animated.glb';p.write_bytes(out)
s={'schema':'adom/component-states@1','mpn':'MF40200V1-1000U-G99','kind':'fan','default':'off','variables':{'speed':{'type':'number','unit':'rpm','min':0,'max':10000,'default':0,'description':'Actual rotor speed in the declared data source, not requested supply or nominal speed.'}},'states':[{'id':'off','label':'Off','summary':'Rotor stopped. No implicit coast-down model.','models':{'step':'cad/MF40200V1-1000U-G99-ai-nominal.step','glb':'states/fan-animated.glb'},'values':{'speed':0},'motion':{'playing':False},'image':'artwork/fan-off.png','when':{'text':'Explicit stopped state; unknown telemetry must not be presented as verified off.'}},{'id':'on','label':'On','summary':'Rotating at the supplied speed variable.','models':{'step':'cad/MF40200V1-1000U-G99-ai-nominal.step','glb':'states/fan-animated.glb'},'values':{'speed':7000},'motion':{'playing':True},'image':'artwork/fan-on.png','when':{'text':'Explicit simulated rotor speed or valid measured tachometer RPM; supply presence alone is insufficient.'}}], 'motion':{'schema':'adom/component-motion@1','status':'proposed-extension','model':'states/fan-animated.glb','sha256':hashlib.sha256(out).hexdigest(),'requiredCapability':'component-motion@1','nodes':{'assembly':'Fan.Assembly','stationary':'Fan.Housing','rotor':'Fan.Rotor'},'bindings':[{'id':'rotor','node':'Fan.Rotor','animation':'Fan.Rotor.Revolution','variable':'speed','unit':'rpm','cycleDurationSeconds':1,'revolutionsPerCycle':1,'axisLocal':[0,0,1],'pivotLocalMeters':[0,0,0],'direction':'negative right-hand rotation about local Z; illustrative orientation','mapping':'rpm-to-cycle-rate','offBehavior':'pause-preserve-phase'}],'presentation':{'defaultTimeScale':1,'previewTimeScale':0.02,'labelRequiredWhenScaled':True,'highRpmAliasingWarning':True},'telemetry':{'defaultSource':'simulated','sources':['simulated','measured'],'validRequired':True,'staleBehavior':'pause-and-label-unknown','doesNotControlHardware':True}},'electrical':{'supply_V':5,'nominal_rpm':7000,'tach_pulses_per_revolution':2,'source':'Sunon D04114960G-01 pages 3 and 8'},'pins':{'supply':'1 (red lead, library convention)','ground':'2 (black lead)','tach':'3 (yellow lead)'},'limitations':['Motion extension pending ecosystem adoption','STEP is static mechanical geometry; playback lives in GLB/runtime','Rotor blades and supports are illustrative, not exact vendor geometry','Rotation direction requires physical verification','No airflow, inertia, thermal or safety proof from animation']}
s['states'][0]['models']['glb']='cad/MF40200V1-1000U-G99-ai-nominal.glb'
(r/'states.json').write_text(json.dumps(s,indent=2)+'\n')
print('Rotor node',ri,'animated GLB',len(out),'bytes')