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.
← Commit history
Pause visualization on invalid RPM requests and validate motion rate bindings
3 files changed
+12−6
states/controller.js+4−2@@ -2,6 +2,7 @@ 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');@@ -9,10 +10,11 @@ export function bindFanMotion(imported, manifest) { 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)throw Error('invalid_motion_request');+ 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))throw Error('invalid_rpm');+ 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);}}
states/controller.mjs+4−2@@ -2,6 +2,7 @@ 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');@@ -9,10 +10,11 @@ export function bindFanMotion(imported, manifest) { 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)throw Error('invalid_motion_request');+ 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))throw Error('invalid_rpm');+ 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);}}
viewer/index.html+4−2@@ -3,6 +3,7 @@ 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');@@ -10,10 +11,11 @@ function bindFanMotion(imported, manifest) { 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)throw Error('invalid_motion_request');+ 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))throw Error('invalid_rpm');+ 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);}}