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()};
}