← Commit history

Pause visualization on invalid RPM requests and validate motion rate bindings

John Lauer ·3be0ac3de0 ·12d ago ·parent e73bd0e
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);}}