component
Sunon MF40200V1-1000U-G99 · 5 V tachometer fan
Public Made by Adomby adom
Tachometer fan with factory flying leads, an explicit Adom connector harness and gentle GLB state demonstration.
main
John Lauer
Pause visualization on invalid RPM requests and validate motion rate bindings
3be0ac3
12d ago
// Reusable adapter for one imported component instance. No network or hardware I/O.
export function bindFanMotion(imported, manifest) {
if(manifest.motion?.schema!=='adom/component-motion@1') throw Error('unsupported_motion_schema');
const binding=manifest.motion.bindings[0];
if(!binding || binding.mapping!=='rpm-to-cycle-rate' || !Number.isFinite(binding.cycleDurationSeconds) || binding.cycleDurationSeconds<=0 || !Number.isFinite(binding.revolutionsPerCycle) || binding.revolutionsPerCycle<=0 || !Number.isFinite(manifest.variables?.speed?.max))throw Error('invalid_motion_binding');
const groups=imported.animationGroups.filter(g=>g.name===binding.animation);
const nodes=[...(imported.transformNodes||[]),...(imported.meshes||[])].filter(n=>n.name===binding.node);
if(groups.length!==1||nodes.length!==1) throw Error('ambiguous_or_missing_motion_binding');
const group=groups[0], rotor=nodes[0];
if(!group.targetedAnimations.length)throw Error('empty_animation');
if(group.targetedAnimations.some(a=>a.target!==rotor))throw Error('animation_targets_outside_rotor');
group.stop();let last={state:'off',speed:0,source:'simulated',valid:true,timeScale:1};
function fail(message){group.pause();last={state:'unknown',speed:null,source:last.source,valid:false};throw Error(message);}
function apply({state,speed,source='simulated',valid=true,timeScale=1}) {
if(!['off','on'].includes(state)||!['simulated','measured'].includes(source)||typeof valid!=='boolean'||!Number.isFinite(timeScale)||timeScale<=0||timeScale>1)return fail('invalid_motion_request');
if(!valid){group.pause();last={state:'unknown',speed:null,source,valid:false,timeScale};return last;}
if(!Number.isFinite(speed)||speed<0||speed>manifest.variables.speed.max||(state==='off'&&speed!==0))return fail('invalid_rpm');
const rate=speed/60*binding.cycleDurationSeconds/binding.revolutionsPerCycle*timeScale;
if(state==='off'||speed===0)group.pause();
else {if(!group.isStarted)group.start(true,rate);else {group.speedRatio=rate;group.play(true);}}
last={state,speed,source,valid,timeScale,playbackRate:rate};return last;
}
return {apply,status:()=>({...last}),pause:()=>group.pause(),dispose:()=>group.stop()};
}