board
Network cabinet fan controller
Public Unreviewedby matt
Two independently controlled 12 V fans, Ethernet and Wi-Fi, and an RGB speed display in a custom printed puck. Routed development preview, not approved for manufacture.
main
matt
Use decoder-independent CAD assembly in sandboxed interactive preview
03d133a
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>
*{box-sizing:border-box}body{margin:0;background:#0c1019;color:#e9edf5;font:16px Satoshi,system-ui,sans-serif}header,main,footer{max-width:1280px;margin:auto;padding:32px}header{display:flex;align-items:center;justify-content:space-between}.tag{font:12px monospace;letter-spacing:.12em;color:#89b9ff}.badge{border:1px solid #3d4c65;border-radius:30px;padding:8px 14px;font-size:12px;color:#aabbcf}h1{font-size:clamp(36px,5vw,64px);line-height:1.05;letter-spacing:-.04em;max-width:850px;margin:18px 0}h2{font-size:28px;margin:0 0 15px}.lead{font-size:20px;color:#a6b5cc;max-width:800px;line-height:1.6}p{line-height:1.65;color:#b8c5d9}section{margin-bottom:52px}.stage{position:relative;height:650px;background:#111827;border:1px solid #29364b;border-radius:20px;overflow:hidden}#viewer{width:100%;height:100%}.overlay{position:absolute;pointer-events:none;left:30px;top:26px;max-width:510px;text-shadow:0 2px 12px #000}.overlay h2{font-size:26px}#caption{position:absolute;bottom:28px;left:14%;right:14%;text-align:center;color:white;font-size:21px;line-height:1.45;text-shadow:0 2px 6px #000;background:#0b101cce;padding:14px 24px;border-radius:12px;pointer-events:none;display:none}#scene-note{font-size:12px;color:#9cabc0;letter-spacing:.04em}.controls{display:flex;gap:8px;flex-wrap:wrap;padding-top:16px}button,a.button{background:#1e2d45;color:#e4edff;border:1px solid #3c5172;border-radius:9px;padding:10px 16px;font:inherit;cursor:pointer;text-decoration:none}button:hover,a.button:hover{background:#2a4264}button.primary{background:#3879e6;border-color:#3879e6}input{accent-color:#4b93ff}#layers{display:flex;gap:18px;flex-wrap:wrap;margin-top:16px;color:#aebfd7;font-size:13px}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.card{background:#131c2c;border:1px solid #26364e;border-radius:14px;padding:22px}.number{font-size:30px;color:#84b7ff;font-weight:700}.card p{font-size:14px;margin-bottom:0}.gallery{display:grid;grid-template-columns:repeat(2,1fr);gap:20px}.gallery img{width:100%;border:1px solid #29364b;border-radius:12px}.gallery p{margin-top:7px;font-size:13px}.status{border-left:3px solid #e4b763;padding:18px 24px;background:#241f17;border-radius:0 12px 12px 0}a{color:#9bc3ff}table{border-collapse:collapse;width:100%;font-size:14px}td,th{padding:14px;border-bottom:1px solid #26364e;text-align:left}#video{width:100%;border-radius:16px;background:black}footer{font-size:12px;color:#8498b6}#loadstate{color:#b3c9e9}body.film header,body.film .intro,body.film main>section:not(.model-section),body.film .controls,body.film #layers,body.film footer{display:none}body.film main{padding:0;max-width:none}body.film section{margin:0}body.film .stage{height:100vh;border:0;border-radius:0}body.film .overlay{top:42px;left:48px}body.film .overlay h2{font-size:36px}body.film #caption{display:block}body.film #scene-note{font-size:15px}body.film #loadstate{display:none}#labels{position:absolute;inset:0;pointer-events:none}.part-label{position:absolute;padding:5px 9px;background:#111c2fe8;border:1px solid #658dc2;border-radius:5px;font-size:13px;white-space:nowrap;transform:translate(-50%,-130%);color:#e4efff}.film .part-label{font-size:17px}@media(max-width:750px){header,main,footer{padding:20px}.stage{height:500px}.grid{grid-template-columns:1fr}.gallery{grid-template-columns:1fr}h1{font-size:38px}}
</style><script>try{localStorage}catch(e){let m={};Object.defineProperty(window,'localStorage',{value:{getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>m={}}});}</script></head><body><main><section class="intro"><span class="tag">DEVELOPMENT PREVIEW / PCB b675a28</span><h1>Built around the cabinet.</h1><p class="lead">Two fans. Wired control. A display you can read at a glance. Explore the actual board and the puck designed around it.</p></section><section class="model-section"><div class="stage"><div id="viewer"></div><div class="overlay"><span class="tag">EXPLORE THE ASSEMBLY</span><h2 id="scene-title">The finished puck</h2><div id="scene-note">CAD illustration · PCB b675a28</div><p id="loadstate">Loading the actual assembly...</p></div><div id="labels"></div><div id="caption"></div></div><div class="controls"><button class="primary" id="reveal">Play opening reveal</button><button data-view="assembled">Assembled</button><button data-view="exploded">Exploded</button><button data-view="pipes">Light pipes</button><button data-view="chips">Board tour</button><button data-view="connectors">Connectors</button><button data-view="power">Power</button></div><div id="layers"></div><p>Drag to rotate, scroll to zoom. The opening motion and exploded views explain the CAD assembly; they are not a validated service procedure. Illumination is illustrative, not an optical simulation.</p></section>
</main><script type="module">import 'https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js';
const film=false;if(film)document.body.classList.add('film');
const timeline=await(await fetch('https://wiki.adom.inc/matt/fan-controller/render/timeline.json')).json();
const viewer=window.Adom3DViewerBabylon9.init(document.querySelector('#viewer'),{zUp:true,showGround:false,axes:false,shadows:false,animate:false,showHelp:!film,showViewCube:!film,background:'#111827',tooltips:'authored'});
window.loadStage='model';const modelUrl=new URL('https://wiki.adom.inc/matt/fan-controller/render/assets/puck-assembly-interactive.glb');if(window.__wikiCap)modelUrl.searchParams.set('cap',window.__wikiCap);const modelResponse=await fetch(modelUrl);if(!modelResponse.ok)throw new Error('Model download: '+modelResponse.status);await viewer.loadModelBytes(new Uint8Array(await modelResponse.arrayBuffer()),{extension:'.glb'});window.loadStage='ready';await viewer.whenReady();window.loadStage='babylon';
const B=await viewer.loadBabylon(),scene=viewer.getScene();window.loadStage='pose';window.viewer=viewer;window.scene=scene;
const nodes={};for(const name of ['PCB','body','cap','bezel','knob','pipes','hardware']){const n=scene.getNodeByName(name);if(!n)throw Error('Missing node '+name);nodes[name]=n;n._base=n.position.clone();}
const camera=scene.activeCamera;camera.minZ=.0001;camera.maxZ=10;
const smooth=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x)};
const viewConfig={assembled:[-.9,.75,.22,[0,0,.016]],opening:[-.9,.8,.245,[0,0,.03]],exploded:[-.95,.93,.29,[0,0,.052]],pipes:[-1.1,.65,.16,[0,0,.022]],leds:[Math.PI/2,.13,.115,[0,0,.008]],chips:[Math.PI/2,.25,.115,[0,0,.009]],board:[Math.PI/2,.32,.12,[0,0,.009]],connectors:[Math.PI/2,.5,.12,[0,0,.009]],power:[Math.PI/2,.35,.115,[0,-.01,.009]],thermal:[Math.PI/2,.25,.12,[0,0,.009]],closing:[-.9,.75,.22,[0,0,.016]]};
const descriptions={assembled:'The finished puck',opening:'A threaded reveal',exploded:'Inside the display',pipes:'From LED to visible dot',leds:'11 RGB indicators',chips:'Control and networking',board:'The routed board',connectors:'Designed for the harness',power:'Power and service',thermal:'Temperature sensing',closing:'Built around the cabinet'};
let activeLabels=[],playing=false;
function reset(){for(const n of Object.values(nodes)){n.setEnabled(true);n.position.copyFrom(n._base);}nodes.cap.rotationQuaternion=null;nodes.cap.rotation.set(0,0,0);}
function pose(view,progress=1){reset();let c=viewConfig[view]||viewConfig.board;
if(view==='opening'){let q=smooth(progress),dz=q*.04;nodes.cap.position.z+=dz;nodes.cap.rotation.z=-Math.PI*2*Math.min(dz,.008)/.004;}
if(view==='exploded'){nodes.cap.position.z+=.095;nodes.bezel.position.z+=.055;nodes.knob.position.z+=.075;nodes.pipes.position.z+=.025;nodes.hardware.position.z+=.025;}
if(view==='pipes'){nodes.cap.setEnabled(false);nodes.bezel.setEnabled(false);nodes.knob.setEnabled(false);nodes.hardware.setEnabled(false);nodes.body.setEnabled(false);}
if(['leds','chips','board','connectors','power','thermal'].includes(view)){for(const n of ['cap','bezel','knob','pipes','hardware','body'])nodes[n].setEnabled(false);}
viewer.moveCamera({alpha:c[0],beta:c[1],radius:c[2],target:c[3]},{ms:0});document.querySelector('#scene-title').textContent=descriptions[view];
document.querySelector('#scene-note').textContent=view==='exploded'?'Exploded CAD illustration · Bezel fasteners attach from below':view==='pipes'?'PLP1-500-F · 12.7 mm body · Illustrative illumination':'Actual source geometry · PCB b675a28';
activeLabels=view==='chips'?[['U2 · ESP32-S3',.031,.0035,.012],['U4 · W5500',.01775,-.0072,.012],['U5 · LP5036',-.024,.0105,.012]]:view==='thermal'?[['RT1 · Onboard NTC · board-heated',-.034,.019,.009]]:[];
for(const input of document.querySelectorAll('[data-layer]'))input.checked=nodes[input.dataset.layer].isEnabled();updateLabels();scene.render();}
function updateLabels(){const el=document.querySelector('#labels');el.innerHTML='';const rect=document.querySelector('#viewer').getBoundingClientRect();for(const [text,x,y,z]of activeLabels){const p=B.Vector3.Project(new B.Vector3(x,y,z),B.Matrix.Identity(),scene.getTransformMatrix(),camera.viewport.toGlobal(rect.width,rect.height));const d=document.createElement('div');d.className='part-label';d.textContent=text;d.style.left=p.x+'px';d.style.top=p.y+'px';el.append(d);}}
scene.onAfterRenderObservable.add(updateLabels);
window.setView=v=>{playing=false;pose(v);document.querySelector('#caption').style.display='none'};
window.setFilmTime=t=>{const b=timeline.beats.find(b=>t>=b.start&&t<b.end)||timeline.beats.at(-1);pose(b.view,(t-b.start)/(b.end-b.start));document.querySelector('#scene-title').textContent=b.title;document.querySelector('#caption').textContent=b.text;document.querySelector('#caption').style.display='block';};
for(const b of document.querySelectorAll('[data-view]'))b.onclick=()=>window.setView(b.dataset.view);
document.querySelector('#reveal').onclick=()=>{playing=true;const start=performance.now();function tick(now){if(!playing)return;const t=(now-start)/1000;pose(t<1?'assembled':t<7?'opening':'exploded',(t-1)/6);if(t<9)requestAnimationFrame(tick);else playing=false;}requestAnimationFrame(tick)};
for(const name of ['body','cap','bezel','knob','pipes','hardware','PCB']){const label=document.createElement('label'),input=document.createElement('input');input.type='checkbox';input.dataset.layer=name;input.checked=true;input.onchange=()=>nodes[name].setEnabled(input.checked);label.append(input,' '+name);document.querySelector('#layers').append(label);}
pose('assembled');document.querySelector('#loadstate').style.display='none';window.previewReady=true;
</script><script>(()=>{let last=0;function report(){let h=Math.ceil(document.documentElement.scrollHeight)+2;if(Math.abs(h-last)>2){last=h;parent.postMessage({type:'readme-height',height:h},'*')}}addEventListener('load',report);new ResizeObserver(report).observe(document.documentElement);setInterval(report,1200);report()})();</script></body></html>