← Commit history

Serve motion adapter with wiki-supported JavaScript media type

John Lauer ·9330288b5f ·12d ago ·parent d6e69af
2 files changed +23−1
states/controller.jsadded+22
@@ -0,0 +1,22 @@+// 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];+  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 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(!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');+    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()};+}
viewer/index.html+1−1
@@ -1,5 +1,5 @@ <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Adom · Fan component states</title><link rel="icon" href="data:,"><link rel="stylesheet" href="https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/style.css"><script type="module" src="https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js"></script><style>body{margin:0;background:#10161d;color:#e6edf3;font:15px system-ui}header{height:44px;box-sizing:border-box;padding:10px 18px;border-bottom:1px solid #35424c}#view{position:absolute;top:44px;bottom:0;left:320px;right:0}aside{width:280px;padding:20px}h1{font-size:23px}p{color:#aabac6;line-height:1.5}button,input,select{font:inherit}button{background:#253847;color:#fff;border:1px solid #618092;border-radius:6px;padding:9px 20px;margin-right:6px;cursor:pointer}button.active{background:#087c76}input[type=range]{width:100%}strong{font-size:30px;color:#5ee4ce}label{display:block;margin:18px 0}#badge{color:#ffd17b}#status{font-size:12px;white-space:pre-wrap}small{color:#aabac6}</style></head><body><header>Adom / Component states / Sunon fan</header><aside><h1>MF40200V1-1000U-G99</h1><p>Fan state and speed preview</p><div id="badge">SIMULATED · no hardware connection</div><p><strong id="rpm">0</strong> RPM</p><button id="off" class="active">Off</button><button id="on">On</button><label>Simulated rotor speed <input id="speed" type="range" min="0" max="10000" step="100" value="7000"><output id="requested">7000 RPM</output></label><label>Playback<select id="scale"><option value="0.02">Slow motion · 0.02×</option><option value="1">Real time · 1×</option></select></label><p id="rate">Rotor stopped</p><small>Nominal envelope. Blade and support geometry are illustrative. Animation demonstrates state control; it does not prove airflow or cooling.</small><p><small>At high RPM, screen sampling can make blades appear slower or reversed.</small></p><div id="status">Loading canonical Adom viewer…</div></aside><div id="view"></div><script type="module">-import {bindFanMotion} from '../states/controller.mjs';+import {bindFanMotion} from '../states/controller.js'; const $=id=>document.getElementById(id);let state='off'; try{ const V=window.Adom3DViewerBabylon9,B=window.BABYLON;const v=V.init($('view'),{zUp:true,showViewCube:true,showGround:false});const scene=v.getScene();window.fanViewer=v;scene.lights.filter(l=>l.name==='light').forEach(l=>l.intensity=1.4);