molecule
Drone Motor Carrier
Public Made by Adomby adom
Passive baseplate for the single-motor drone test cell: mounts the BLDC motor, passes its three phases through, routes two load-cell channels, and nests the RP2350A/ADS1220 acquisition molecule and a PiProbe over a 96 x 96 mm machine-pin datum. 104 x 104 mm, 4-layer.
← Commit history
README viewer: Motor assembly toggle loads the fitted thrust-stand stack (viewer/motor-assembly.glb) into the board viewer
2 files changed
+192−173
README.html+192−173@@ -79,10 +79,10 @@ <!-- ============================ VIEWER (the centrepiece) ============================ --> <div class="viewer-card"> <iframe id="board-viewer" title="Feeder Reader interactive board viewer"- sandbox="allow-scripts" data-viewer-src="<meta charset="utf-8">-<title>Feeder Reader — interactive board viewer</title>-<meta name="viewport" content="width=device-width, initial-scale=1">-<style>+ sandbox="allow-scripts" data-viewer-src="<meta charset="utf-8">+<title>Feeder Reader — interactive board viewer</title>+<meta name="viewport" content="width=device-width, initial-scale=1">+<style> :root{ --bg:#2a3340; --ink:#e8eef5; --dim:#9fb0c3; --line:rgba(255,255,255,.14); } *{box-sizing:border-box} html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);@@ -143,80 +143,80 @@ /* embedded in a short frame the legend (top-left) and the caption (bottom-left) collide — the caption is the one that carries the narrative, so the legend yields */ @media (max-height:640px){ #legend{display:none} }-</style>--<div id="app"></div>-<div id="status">loading board…</div>-<div id="topbar"><div id="topbar-fill"></div></div>-<div id="marks"></div>-<div class="panel" id="modes"></div>-<div class="panel" id="legend"><h3>Sections</h3><div id="legend-list"></div></div>-<div class="panel" id="caption"><div class="eyebrow"></div><div class="head"></div><div class="rows"></div><div class="note"></div></div>-<div class="btn-row">- <button class="btn" id="btn-power" style="display:none">⏻ Power</button>- <button class="btn on" id="btn-play">⏸ Pause</button>- <button class="btn" id="btn-replay">↺ Replay</button>- <button class="btn on" id="btn-ground">Ground</button>- <button class="btn on" id="btn-labels">Labels</button>-</div>-<div id="tip"></div>--<script>-(async () => {-const statusEl = document.getElementById('status');+</style>++<div id="app"></div>+<div id="status">loading board…</div>+<div id="topbar"><div id="topbar-fill"></div></div>+<div id="marks"></div>+<div class="panel" id="modes"></div>+<div class="panel" id="legend"><h3>Sections</h3><div id="legend-list"></div></div>+<div class="panel" id="caption"><div class="eyebrow"></div><div class="head"></div><div class="rows"></div><div class="note"></div></div>+<div class="btn-row">+ <button class="btn" id="btn-power" style="display:none">⏻ Power</button>+ <button class="btn on" id="btn-play">⏸ Pause</button>+ <button class="btn" id="btn-replay">↺ Replay</button>+ <button class="btn on" id="btn-ground">Ground</button>+ <button class="btn on" id="btn-labels">Labels</button><button class="btn" id="btn-motor" title="Show or hide the fitted thrust-stand stack (load cell, EMAX 2808, APC 7x11E)">Motor assembly</button>+</div>+<div id="tip"></div>++<script>+(async () => {+const statusEl = document.getElementById('status'); // Wiki blobs render SANDBOXED (no allow-same-origin) → localStorage/sessionStorage throw on-// ACCESS, which aborts the bundle's SceneBuilder (no lights/IBL/shadows). Shim before load.-for (const k of ['localStorage','sessionStorage']) {- try { window[k] && window[k].getItem('x'); }- catch (e) { try { Object.defineProperty(window,k,{configurable:true,value:{getItem:()=>null,setItem:()=>{},removeItem:()=>{},clear:()=>{},key:()=>null,length:0}}); } catch(_){} }+// ACCESS, which aborts the bundle's SceneBuilder (no lights/IBL/shadows). Shim before load.+for (const k of ['localStorage','sessionStorage']) {+ try { window[k] && window[k].getItem('x'); }+ catch (e) { try { Object.defineProperty(window,k,{configurable:true,value:{getItem:()=>null,setItem:()=>{},removeItem:()=>{},clear:()=>{},key:()=>null,length:0}}); } catch(_){} } }-// Blob storage serves .js as application/octet-stream + nosniff → <script src> is refused.+// Blob storage serves .js as application/octet-stream + nosniff → <script src> is refused. // Fetch as text, inject inline. if (!window.Adom3DViewer) { try {- const code = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/adom-3d-viewer.min.js?cap=__WIKICAP__')).text();- const s = document.createElement('script'); s.textContent = code; document.head.appendChild(s);- } catch (e) { statusEl.textContent = 'viewer bundle fetch failed: ' + e.message; return; }+ const code = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/adom-3d-viewer.min.js?cap=__WIKICAP__')).text();+ const s = document.createElement('script'); s.textContent = code; document.head.appendChild(s);+ } catch (e) { statusEl.textContent = 'viewer bundle fetch failed: ' + e.message; return; } } const V = window.Adom3DViewer;-if (!V) { statusEl.textContent = 'Adom3DViewer bundle failed to load'; return; }+if (!V) { statusEl.textContent = 'Adom3DViewer bundle failed to load'; return; } const B = V.BABYLON; try { // ---------------------------------------------------------------- config + index-const CFG = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/viewer-config.json?cap=__WIKICAP__',{cache:'no-store'})).json();-const IDX = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/board-index.json?cap=__WIKICAP__',{cache:'no-store'})).json();+const CFG = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/viewer-config.json?cap=__WIKICAP__',{cache:'no-store'})).json();+const IDX = await (await fetch('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/board-index.json?cap=__WIKICAP__',{cache:'no-store'})).json(); const BLOCKS = CFG.blocks;-const BCOL = Object.fromEntries(BLOCKS.map(b => [b.id, b.color]));-const BLAB = Object.fromEntries(BLOCKS.map(b => [b.id, b.label]));+const BCOL = Object.fromEntries(BLOCKS.map(b => [b.id, b.color]));+const BLAB = Object.fromEntries(BLOCKS.map(b => [b.id, b.label])); // ---------------------------------------------------------------- scene-const viewer = V.init(document.getElementById('app'), { zUp:true, showViewCube:true, showGround:true });-await viewer.loadModel('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/board-detailed.glb?cap=__WIKICAP__');+const viewer = V.init(document.getElementById('app'), { zUp:true, showViewCube:true, showGround:true });+await viewer.loadModel('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/board-detailed.glb?cap=__WIKICAP__'); const scene = viewer.getScene();-scene.animationGroups.slice().forEach(g => g.dispose()); // we drive all motion+scene.animationGroups.slice().forEach(g => g.dispose()); // we drive all motion -// Match Colby's current native viewer: flat #2a3340, no skybox mesh, soft radial ground.+// Match Colby's current native viewer: flat #2a3340, no skybox mesh, soft radial ground. const bgBot = new B.Color3(0.165,0.200,0.251); const Color4 = scene.clearColor && scene.clearColor.constructor; let _fade=null;-const groundFadeTex = () => {+const groundFadeTex = () => { if (_fade) return _fade;- const t=new B.DynamicTexture('groundFadeTex',256,scene,true), c=t.getContext();+ const t=new B.DynamicTexture('groundFadeTex',256,scene,true), c=t.getContext(); const g=c.createRadialGradient(128,128,6,128,128,128);- g.addColorStop(0,'rgba(255,255,255,1)'); g.addColorStop(.55,'rgba(255,255,255,.6)'); g.addColorStop(1,'rgba(255,255,255,0)');+ g.addColorStop(0,'rgba(255,255,255,1)'); g.addColorStop(.55,'rgba(255,255,255,.6)'); g.addColorStop(1,'rgba(255,255,255,0)'); c.fillStyle=g; c.fillRect(0,0,256,256); t.update(); t.hasAlpha=true; _fade=t; return t; };-const matchColby = () => {+const matchColby = () => { if (Color4) scene.clearColor = new Color4(bgBot.r,bgBot.g,bgBot.b,1); scene.environmentIntensity = 1.0;- scene.meshes.filter(m=>/^Background/.test(m.name)).forEach(m=>m.setEnabled(false));- const gnd = scene.getMeshByName('shadowGround');+ scene.meshes.filter(m=>/^Background/.test(m.name)).forEach(m=>m.setEnabled(false));+ const gnd = scene.getMeshByName('shadowGround'); if (gnd && gnd.material) { const m = gnd.material;- if (!(m.baseTexture && m.baseTexture.name==='groundFadeTex')) {- if ('baseTexture' in m) m.baseTexture = groundFadeTex();+ if (!(m.baseTexture && m.baseTexture.name==='groundFadeTex')) {+ if ('baseTexture' in m) m.baseTexture = groundFadeTex(); m.alpha=0.2; m.transparencyMode=2; m.backFaceCulling=false; } gnd.receiveShadows = true;@@ -225,14 +225,14 @@ const matchColby = () => { matchColby(); // ---------------------------------------------------------------- group meshes-// Every mesh is named "<block>|<ref>|<i>" (transforms baked at export, so a plain+// Every mesh is named "<block>|<ref>|<i>" (transforms baked at export, so a plain // position offset is a rigid move — see bake2.py).-const parts = new Map(); // ref -> {ref, blk, meshes[], home:Vector3, centre}-const byBlock = new Map(); // blk -> [part]+const parts = new Map(); // ref -> {ref, blk, meshes[], home:Vector3, centre}+const byBlock = new Map(); // blk -> [part] for (const m of scene.meshes) {- const nm = (m.name||'').split('_primitive')[0];- if (nm.indexOf('|') < 0) continue;- const [blk, ref] = nm.split('|');+ const nm = (m.name||'').split('_primitive')[0];+ if (nm.indexOf('|') < 0) continue;+ const [blk, ref] = nm.split('|'); m.rotationQuaternion = null; // glTF quaternion would override Euler let p = parts.get(ref); if (!p) { p = { ref, blk, meshes:[], home:m.position.clone() }; parts.set(ref, p); (byBlock.get(blk)||byBlock.set(blk,[]).get(blk)).push(p); }@@ -248,7 +248,7 @@ for (const p of parts.values()) { } p.lo=lo; p.hi=hi; p.centre=lo.add(hi).scale(0.5); p.size=hi.subtract(lo); }-const blockBox = blk => {+const blockBox = blk => { let lo=null,hi=null; for (const p of (byBlock.get(blk)||[])) { lo = lo ? B.Vector3.Minimize(lo,p.lo) : p.lo.clone();@@ -262,14 +262,14 @@ const blockBox = blk => { } return lo ? {lo,hi,c:lo.add(hi).scale(.5),s:hi.subtract(lo)} : null; };-const SUB = ['PCB','COPPER','MASK','SILK'].map(r=>parts.get(r)).filter(Boolean);-const COMPONENTS = [...parts.values()].filter(p => p.blk!=='substrate');-statusEl.textContent = '';+const SUB = ['PCB','COPPER','MASK','SILK'].map(r=>parts.get(r)).filter(Boolean);+const COMPONENTS = [...parts.values()].filter(p => p.blk!=='substrate');+statusEl.textContent = ''; // ---------------------------------------------------------------- camera const cam = scene.activeCamera; const home = { a:-Math.PI/2 - 0.55, b:0.92, r:0.165, t:new B.Vector3(0,0,0.004) };-const setCam = (o,ease) => {+const setCam = (o,ease) => { const tgt = { alpha:o.a, beta:o.b, radius:o.r, target:o.t }; if (!ease) { cam.alpha=tgt.alpha; cam.beta=tgt.beta; cam.radius=tgt.radius; cam.target.copyFrom(tgt.target); return; } camGoal = tgt;@@ -277,33 +277,33 @@ const setCam = (o,ease) => { let camGoal = null, userHold = 0; setCam(home,false); cam.lowerRadiusLimit = 0.03; cam.upperRadiusLimit = 1.2; cam.wheelDeltaPercentage = 0.02;-// Depth-precision fix: the bundle's camera sets minZ = radius/1000, which makes one 24-bit+// Depth-precision fix: the bundle's camera sets minZ = radius/1000, which makes one 24-bit // depth step ~10 um at board-viewing distance — exactly the gap between mask/silk/copper // sheets, so coplanar-ish layers shimmer. radius/100 gives ~1 um steps and the near plane is // still only 0.3 mm out at the closest zoom. (Bake-side the layers are also nudged apart.)-scene.onBeforeRenderObservable.add(() => { cam.minZ = Math.max(cam.radius / 100, 2e-4); });+scene.onBeforeRenderObservable.add(() => { cam.minZ = Math.max(cam.radius / 100, 2e-4); }); cam.panningSensibility = 6000; // The camera is "yours for 5 s": any drag/wheel pauses the scripted chase (assembly only), // which then eases back — same convention as the single-component Timeline viewer. In static // mode there is no scripted camera at all, so input is simply never fought.-['pointerdown','wheel'].forEach(ev => scene.getEngine().getRenderingCanvas()- .addEventListener(ev, () => { userHold = 5; }, {passive:true}));+['pointerdown','wheel'].forEach(ev => scene.getEngine().getRenderingCanvas()+ .addEventListener(ev, () => { userHold = 5; }, {passive:true})); // ---------------------------------------------------------------- helpers-const emissive = (col, alpha) => {- const m = new B.StandardMaterial('em'+Math.random(), scene);+const emissive = (col, alpha) => {+ const m = new B.StandardMaterial('em'+Math.random(), scene); m.emissiveColor = col; m.diffuseColor = new B.Color3(0,0,0); m.specularColor = new B.Color3(0,0,0); m.disableLighting = true; m.alpha = alpha==null?1:alpha; if (alpha!=null) m.alphaMode = 1; return m; };-const hex2c3 = h => new B.Color3(parseInt(h.slice(1,3),16)/255, parseInt(h.slice(3,5),16)/255, parseInt(h.slice(5,7),16)/255);+const hex2c3 = h => new B.Color3(parseInt(h.slice(1,3),16)/255, parseInt(h.slice(3,5),16)/255, parseInt(h.slice(5,7),16)/255); // Section outline boxes (wireframe) — one per block, hidden by default. const outlines = new Map(); for (const b of BLOCKS) { const bb = blockBox(b.id); if (!bb) continue; const pad = 0.0018;- const box = B.MeshBuilder.CreateBox('outline_'+b.id, {+ const box = B.MeshBuilder.CreateBox('outline_'+b.id, { width: Math.max(bb.s.x,0.002)+pad*2, height: Math.max(bb.s.y,0.002)+pad*2, depth: Math.max(bb.s.z,0.002)+pad*2 }, scene); box.position.copyFrom(bb.c); const mt = emissive(hex2c3(b.color)); mt.wireframe = true; mt.alpha = .45; mt.alphaMode = 1;@@ -318,22 +318,22 @@ const glows = new Map(); let powerOn = !!(FN && FN.default); for (const ref of (FN ? FN.refs : [])) { const f = IDX.fps[ref]; if (!f) continue;- const d = B.MeshBuilder.CreateBox('glow_'+ref, {width:.0028,height:.0028,depth:.0009}, scene);+ const d = B.MeshBuilder.CreateBox('glow_'+ref, {width:.0028,height:.0028,depth:.0009}, scene); d.position.set(f.x, f.y, 0.0022); d.material = emissive(hex2c3(FN.color), .95); d.isPickable=false; d.setEnabled(false); glows.set(ref, d); } // DOM labels for sections-const labelLayer = document.createElement('div');-labelLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:4';+const labelLayer = document.createElement('div');+labelLayer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:4'; document.body.appendChild(labelLayer); const labels = new Map(); for (const b of BLOCKS) { const bb = blockBox(b.id); if (!bb) continue;- const el = document.createElement('div');- el.style.cssText = 'position:absolute;transform:translate(-50%,-50%);font:650 10.5px/1 ui-sans-serif,system-ui;'+- 'letter-spacing:.03em;padding:3px 7px;border-radius:6px;white-space:nowrap;opacity:0;transition:opacity .3s;'+- 'background:rgba(10,15,22,.82);border:1px solid '+b.color+'66;color:'+b.color;+ const el = document.createElement('div');+ el.style.cssText = 'position:absolute;transform:translate(-50%,-50%);font:650 10.5px/1 ui-sans-serif,system-ui;'++ 'letter-spacing:.03em;padding:3px 7px;border-radius:6px;white-space:nowrap;opacity:0;transition:opacity .3s;'++ 'background:rgba(10,15,22,.82);border:1px solid '+b.color+'66;color:'+b.color; el.textContent = b.label; labelLayer.appendChild(el); labels.set(b.id, {el, pos:bb.c.clone()}); }@@ -344,57 +344,57 @@ let labelsOn = true; let focusBlk = null, hoverBlk = null; // ---------------------------------------------------------------- visibility control-const setPartVis = (p, v) => { for (const m of p.meshes) m.visibility = v; };-const showAllComponents = v => COMPONENTS.forEach(p => setPartVis(p, v));+const setPartVis = (p, v) => { for (const m of p.meshes) m.visibility = v; };+const showAllComponents = v => COMPONENTS.forEach(p => setPartVis(p, v)); const layerOn = { PCB:true, COPPER:true, SILK:true, MASK:true };-const blockOn = Object.fromEntries(BLOCKS.map(b=>[b.id,true]));-const applyLayers = () => {- for (const p of SUB) { const on = layerOn[p.ref]!==false; p.meshes.forEach(m=>m.setEnabled(on)); }- for (const p of COMPONENTS) { const on = blockOn[p.blk]!==false; p.meshes.forEach(m=>m.setEnabled(on)); }+const blockOn = Object.fromEntries(BLOCKS.map(b=>[b.id,true]));+const applyLayers = () => {+ for (const p of SUB) { const on = layerOn[p.ref]!==false; p.meshes.forEach(m=>m.setEnabled(on)); }+ for (const p of COMPONENTS) { const on = blockOn[p.blk]!==false; p.meshes.forEach(m=>m.setEnabled(on)); } }; // ---------------------------------------------------------------- legend-const legendList = document.getElementById('legend-list');+const legendList = document.getElementById('legend-list'); const counts = {}; for (const f of Object.values(IDX.fps)) counts[f.blk] = (counts[f.blk]||0)+1; for (const b of BLOCKS) {- const row = document.createElement('div'); row.className='row';- row.innerHTML = `<span class="sw" style="background:${b.color}"></span><span>${b.label}</span><span class="n">${counts[b.id]||0}</span>`;- row.onclick = () => { blockOn[b.id] = !blockOn[b.id]; row.classList.toggle('off', !blockOn[b.id]); applyLayers(); };- row.onmouseenter = () => { hoverBlk = b.id; outlines.get(b.id)?.setEnabled(true); };- row.onmouseleave = () => { hoverBlk = null; if (focusBlk!==b.id) outlines.get(b.id)?.setEnabled(false); };+ const row = document.createElement('div'); row.className='row';+ row.innerHTML = `<span class="sw" style="background:${b.color}"></span><span>${b.label}</span><span class="n">${counts[b.id]||0}</span>`;+ row.onclick = () => { blockOn[b.id] = !blockOn[b.id]; row.classList.toggle('off', !blockOn[b.id]); applyLayers(); };+ row.onmouseenter = () => { hoverBlk = b.id; outlines.get(b.id)?.setEnabled(true); };+ row.onmouseleave = () => { hoverBlk = null; if (focusBlk!==b.id) outlines.get(b.id)?.setEnabled(false); }; legendList.appendChild(row); }-const subRow = document.createElement('div'); subRow.className='row';-subRow.innerHTML = `<span class="sw" style="background:#b78227"></span><span>Copper &amp; silkscreen</span><span class="n">4</span>`;-subRow.onclick = () => { const on = !(layerOn.COPPER && layerOn.SILK); layerOn.COPPER=layerOn.SILK=on; subRow.classList.toggle('off',!on); applyLayers(); };+const subRow = document.createElement('div'); subRow.className='row';+subRow.innerHTML = `<span class="sw" style="background:#b78227"></span><span>Copper &amp; silkscreen</span><span class="n">4</span>`;+subRow.onclick = () => { const on = !(layerOn.COPPER && layerOn.SILK); layerOn.COPPER=layerOn.SILK=on; subRow.classList.toggle('off',!on); applyLayers(); }; legendList.appendChild(subRow); // ---------------------------------------------------------------- caption-const capEl = document.getElementById('caption');-const showCap = (c) => {- if (!c) { capEl.classList.remove('show'); return; }- const dot = c.block ? `<span class="chip" style="background:${BCOL[c.block]}"></span>` : '';- capEl.querySelector('.eyebrow').innerHTML = dot + (c.eyebrow||'');- capEl.querySelector('.head').textContent = c.head||'';- capEl.querySelector('.rows').innerHTML = (c.rows||[]).map(r=>`<b>${r[0]}</b><span>${r[1]}</span>`).join('');- capEl.querySelector('.note').textContent = c.note||'';- capEl.classList.add('show');+const capEl = document.getElementById('caption');+const showCap = (c) => {+ if (!c) { capEl.classList.remove('show'); return; }+ const dot = c.block ? `<span class="chip" style="background:${BCOL[c.block]}"></span>` : '';+ capEl.querySelector('.eyebrow').innerHTML = dot + (c.eyebrow||'');+ capEl.querySelector('.head').textContent = c.head||'';+ capEl.querySelector('.rows').innerHTML = (c.rows||[]).map(r=>`<b>${r[0]}</b><span>${r[1]}</span>`).join('');+ capEl.querySelector('.note').textContent = c.note||'';+ capEl.classList.add('show'); }; let lastCapKey = null;-const showCapOnce = (key,c) => { if (key!==lastCapKey) { lastCapKey=key; showCap(c); } };+const showCapOnce = (key,c) => { if (key!==lastCapKey) { lastCapKey=key; showCap(c); } }; // ---------------------------------------------------------------- modes const A = CFG.modes.assembly, S = CFG.modes.static;-// Assembly schedule — the GROUP is the unit: all of a grouping's parts fall together-// (tiny in-group stagger for texture) while that grouping's caption explains it.+// Assembly schedule — the GROUP is the unit: all of a grouping's parts fall together+// (tiny in-group stagger for texture) while that grouping's caption explains it. const T = A.timing; const sched = []; // {p, t, blk} — individual part drops const groups = []; // {id, t0, t1} — caption/camera windows let tA = (A.intro && A.intro.d) || 6; for (const bid of A.order) {- const ps = (byBlock.get(bid)||[]).slice().sort((p,q)=>p.centre.x-q.centre.x);- ps.forEach((p,i)=> sched.push({p, t: tA + i*T.stagger_s, blk:bid}));+ const ps = (byBlock.get(bid)||[]).slice().sort((p,q)=>p.centre.x-q.centre.x);+ ps.forEach((p,i)=> sched.push({p, t: tA + i*T.stagger_s, blk:bid})); const dwell = (A.captions[bid] && A.captions[bid].d) || T.dwell_s; groups.push({ id: bid, t0: tA, t1: tA + dwell }); tA += dwell;@@ -402,36 +402,36 @@ for (const bid of A.order) { const A_LEN = tA + T.rest_s; const LEN = { assembly: A_LEN, static: 1 }; // static has NO timeline -let mode = 'assembly', t = 0, playing = true;+let mode = 'assembly', t = 0, playing = true; -const modesEl = document.getElementById('modes');-for (const k of ['assembly','static']) {- const b = document.createElement('button'); b.textContent = CFG.modes[k].label; b.dataset.k = k;- b.onclick = () => setMode(k);+const modesEl = document.getElementById('modes');+for (const k of ['assembly','static']) {+ const b = document.createElement('button'); b.textContent = CFG.modes[k].label; b.dataset.k = k;+ b.onclick = () => setMode(k); modesEl.appendChild(b); }-const marksEl = document.getElementById('marks');+const marksEl = document.getElementById('marks'); function drawMarks() {- marksEl.innerHTML = '';- if (mode!=='assembly') return;- for (const g of groups) marksEl.insertAdjacentHTML('beforeend', `<i style="left:${100*g.t0/A_LEN}%"></i>`);+ marksEl.innerHTML = '';+ if (mode!=='assembly') return;+ for (const g of groups) marksEl.insertAdjacentHTML('beforeend', `<i style="left:${100*g.t0/A_LEN}%"></i>`); } function setMode(k) { mode = k; t = 0; lastCapKey = null; camGoal = null; userHold = 0; focusBlk = null;- [...modesEl.children].forEach(b => b.classList.toggle('on', b.dataset.k===k));- outlines.forEach(o => o.setEnabled(false));- COMPONENTS.forEach(p => { setPartVis(p, k==='assembly' ? 0 : 1);- p.meshes.forEach(m => m.position.copyFrom(p.home)); });- // static: no timeline, no scripted camera — the camera is entirely the user's- const isA = k==='assembly';- document.getElementById('topbar').style.display = isA ? 'block' : 'none';- document.getElementById('btn-play').style.display = isA ? '' : 'none';- document.getElementById('btn-replay').style.display = isA ? '' : 'none';- document.getElementById('btn-power').style.display = (!isA && FN) ? '' : 'none';- glows.forEach(g => g.setEnabled(!isA && powerOn));+ [...modesEl.children].forEach(b => b.classList.toggle('on', b.dataset.k===k));+ outlines.forEach(o => o.setEnabled(false));+ COMPONENTS.forEach(p => { setPartVis(p, k==='assembly' ? 0 : 1);+ p.meshes.forEach(m => m.position.copyFrom(p.home)); });+ // static: no timeline, no scripted camera — the camera is entirely the user's+ const isA = k==='assembly';+ document.getElementById('topbar').style.display = isA ? 'block' : 'none';+ document.getElementById('btn-play').style.display = isA ? '' : 'none';+ document.getElementById('btn-replay').style.display = isA ? '' : 'none';+ document.getElementById('btn-power').style.display = (!isA && FN) ? '' : 'none';+ glows.forEach(g => g.setEnabled(!isA && powerOn)); setCam(home,false); drawMarks(); applyLayers(); showCap(null);- if (!isA && S.blurb) showCap({ eyebrow:'Static', head:'Inspect the board', note:S.blurb });+ if (!isA && S.blurb) showCap({ eyebrow:'Static', head:'Inspect the board', note:S.blurb }); } // ---------------------------------------------------------------- render loop@@ -439,7 +439,7 @@ function setMode(k) { // on it (§5b). Harvest it from a live instance instead of reaching for B.Matrix. const Matrix = scene.getTransformMatrix().constructor; const IDM = Matrix.Identity();-const worldToScreen = (v) => {+const worldToScreen = (v) => { try { const e = scene.getEngine(); return B.Vector3.Project(v, IDM, scene.getTransformMatrix(),@@ -447,13 +447,13 @@ const worldToScreen = (v) => { } catch (_) { return null; } }; let last = performance.now();-scene.onBeforeRenderObservable.add(() => {+scene.onBeforeRenderObservable.add(() => { const now = performance.now(); const dt = Math.min((now-last)/1000, .05); last = now; if (playing) t = (t + dt) % LEN[mode];- if (userHold > 0) userHold -= dt;+ if (userHold > 0) userHold -= dt; // camera easing — assembly only; static never steers- if (mode==='assembly' && camGoal && userHold <= 0 &&+ if (mode==='assembly' && camGoal && userHold <= 0 && Number.isFinite(camGoal.alpha) && Number.isFinite(camGoal.beta) && Number.isFinite(camGoal.radius)) { const k = 1 - Math.pow(0.0016, dt); cam.alpha += (camGoal.alpha - cam.alpha) * k;@@ -463,36 +463,36 @@ scene.onBeforeRenderObservable.add(() => { cam.target.y += (camGoal.target.y - cam.target.y) * k; cam.target.z += (camGoal.target.z - cam.target.z) * k; }- if (mode==='assembly' && userHold <= 0) cam.alpha += dt * 0.005; // gentle drift+ if (mode==='assembly' && userHold <= 0) cam.alpha += dt * 0.005; // gentle drift - if (mode === 'assembly') {+ if (mode === 'assembly') { // drops for (const d of sched) { const u = (t - d.t) / T.fall_s;- if (u < 0) { setPartVis(d.p, 0); continue; }+ if (u < 0) { setPartVis(d.p, 0); continue; } const k = Math.min(u, 1), e = 1 - Math.pow(1-k, 3); // easeOutCubic setPartVis(d.p, Math.min(1, k*2.2)); const dz = (1-e) * T.drop_height_mm/1000; for (const m of d.p.meshes) { m.position.copyFrom(d.p.home); m.position.z += dz; } } // caption + focus + camera per GROUP window (intro before, rest after)- const g = groups.find(g => t>=g.t0 && t<g.t1);- if (t < groups[0].t0) {- showCapOnce('a-intro', A.intro); focusBlk = null;- outlines.forEach((o,id)=>o.setEnabled(id===hoverBlk));+ const g = groups.find(g => t>=g.t0 && t<g.t1);+ if (t < groups[0].t0) {+ showCapOnce('a-intro', A.intro); focusBlk = null;+ outlines.forEach((o,id)=>o.setEnabled(id===hoverBlk)); camGoal = { alpha:home.a, beta:home.b, radius:home.r, target:home.t }; } else if (g) { const c = A.captions[g.id] || {};- showCapOnce('a'+g.id, { ...c, block:g.id });+ showCapOnce('a'+g.id, { ...c, block:g.id }); focusBlk = g.id;- outlines.forEach((o,id)=>o.setEnabled(id===g.id || id===hoverBlk));+ outlines.forEach((o,id)=>o.setEnabled(id===g.id || id===hoverBlk)); const bb = blockBox(g.id); if (bb) camGoal = { alpha: home.a + 0.25*Math.sin(t*0.12), beta: 0.86, radius: Math.max(0.105, Math.min(0.24, bb.s.length()*2.6)), target: bb.c }; } else { // rest: whole board seated- showCapOnce('a-rest', null); focusBlk = null;- outlines.forEach((o,id)=>o.setEnabled(id===hoverBlk));+ showCapOnce('a-rest', null); focusBlk = null;+ outlines.forEach((o,id)=>o.setEnabled(id===hoverBlk)); camGoal = { alpha:home.a, beta:home.b, radius:home.r, target:home.t }; } }@@ -500,53 +500,72 @@ scene.onBeforeRenderObservable.add(() => { // Hover tooltips, legend isolate, labels and the Power toggle all work event-driven. // scrub + labels- if (mode==='assembly') document.getElementById('topbar-fill').style.width = (100*t/A_LEN) + '%';+ if (mode==='assembly') document.getElementById('topbar-fill').style.width = (100*t/A_LEN) + '%'; for (const [id, L] of labels) { const vis = labelsOn && blockOn[id]!==false && (id===focusBlk || id===hoverBlk); if (!vis) { L.el.style.opacity = 0; continue; } const p = worldToScreen(L.pos);- if (!p || p.z > 1 || p.z < 0) { L.el.style.opacity = 0; continue; }- L.el.style.left = p.x+'px'; L.el.style.top = p.y+'px'; L.el.style.opacity = 0.92;+ if (!p || p.z > 1 || p.z < 0) { L.el.style.opacity = 0; continue; }+ L.el.style.left = p.x+'px'; L.el.style.top = p.y+'px'; L.el.style.opacity = 0.92; } }); // ---------------------------------------------------------------- hover tooltip-const tipEl = document.getElementById('tip');-scene.getEngine().getRenderingCanvas().addEventListener('pointermove', ev => {- const pick = scene.pick(scene.pointerX, scene.pointerY, m => (m.name||'').indexOf('|')>=0);+const tipEl = document.getElementById('tip');+scene.getEngine().getRenderingCanvas().addEventListener('pointermove', ev => {+ const pick = scene.pick(scene.pointerX, scene.pointerY, m => (m.name||'').indexOf('|')>=0); if (!pick || !pick.hit) { tipEl.style.opacity = 0; return; }- const nm = pick.pickedMesh.name.split('_primitive')[0];- const [blk, ref] = nm.split('|');+ const nm = pick.pickedMesh.name.split('_primitive')[0];+ const [blk, ref] = nm.split('|'); const f = IDX.fps[ref]; const p = parts.get(ref);- const mm = v => (v*1000).toFixed(2);- if (blk === 'substrate') {- tipEl.innerHTML = `<div class="t">Bare board</div><div class="m">130.00 × 50.00 mm · 1.6 mm · 4 layers<br>`+- `${{PCB:'FR-4 substrate',COPPER:'copper (all 4 layers)',MASK:'soldermask',SILK:'silkscreen'}[ref]||ref}</div>`;+ const mm = v => (v*1000).toFixed(2);+ if (blk === 'substrate') {+ tipEl.innerHTML = `<div class="t">Bare board</div><div class="m">130.00 × 50.00 mm · 1.6 mm · 4 layers<br>`++ `${{PCB:'FR-4 substrate',COPPER:'copper (all 4 layers)',MASK:'soldermask',SILK:'silkscreen'}[ref]||ref}</div>`; } else if (f) {- tipEl.innerHTML = `<div class="t">${ref} · ${f.val}</div>`+- `<div class="m">${f.fp}<br>${BLAB[blk]||blk} · ${f.layer==='F.Cu'?'top':'bottom'} · ${f.rot?f.rot+'°':'0°'}`+- (p? `<br>extent ${mm(p.size.x)} × ${mm(p.size.y)} × ${mm(p.size.z)} mm` : '')+`</div>`;+ tipEl.innerHTML = `<div class="t">${ref} · ${f.val}</div>`++ `<div class="m">${f.fp}<br>${BLAB[blk]||blk} · ${f.layer==='F.Cu'?'top':'bottom'} · ${f.rot?f.rot+'°':'0°'}`++ (p? `<br>extent ${mm(p.size.x)} × ${mm(p.size.y)} × ${mm(p.size.z)} mm` : '')+`</div>`; } else return; tipEl.style.opacity = 1;- tipEl.style.left = Math.min(ev.clientX+14, innerWidth-244)+'px';- tipEl.style.top = Math.min(ev.clientY+14, innerHeight-90)+'px';+ tipEl.style.left = Math.min(ev.clientX+14, innerWidth-244)+'px';+ tipEl.style.top = Math.min(ev.clientY+14, innerHeight-90)+'px'; });-scene.getEngine().getRenderingCanvas().addEventListener('pointerleave', ()=> tipEl.style.opacity=0);+scene.getEngine().getRenderingCanvas().addEventListener('pointerleave', ()=> tipEl.style.opacity=0); // ---------------------------------------------------------------- controls-document.getElementById('btn-play').onclick = e => { playing=!playing; e.target.classList.toggle('on',playing); e.target.textContent = playing?'⏸ Pause':'▶ Play'; };-document.getElementById('btn-replay').onclick = () => { t=0; lastCapKey=null; if(mode==='assembly') COMPONENTS.forEach(p=>setPartVis(p,0)); };-document.getElementById('btn-ground').onclick = e => {- const g = scene.getMeshByName('shadowGround'); if (!g) return;- const on = !g.isEnabled(); g.setEnabled(on); e.target.classList.toggle('on',on); matchColby();+document.getElementById('btn-play').onclick = e => { playing=!playing; e.target.classList.toggle('on',playing); e.target.textContent = playing?'⏸ Pause':'▶ Play'; };+document.getElementById('btn-replay').onclick = () => { t=0; lastCapKey=null; if(mode==='assembly') COMPONENTS.forEach(p=>setPartVis(p,0)); };+document.getElementById('btn-ground').onclick = e => {+ const g = scene.getMeshByName('shadowGround'); if (!g) return;+ const on = !g.isEnabled(); g.setEnabled(on); e.target.classList.toggle('on',on); matchColby();+};+// ---- Motor assembly toggle: the fitted thrust-stand stack (Loadcell Thrust Stand Assembly.step), lazily loaded on first use.+// viewer.loadModel APPENDS (it never clears), so the stack lands in the same frame handling as the board.+let motorRoot = null, motorOn = false, motorLoading = null;+const motorBtn = document.getElementById('btn-motor');+async function ensureMotor() {+ if (motorRoot || motorLoading) return motorLoading;+ const before = new Set(scene.meshes);+ motorLoading = viewer.loadModel('https://wiki.adom.inc/blob/component/drone-motor-carrier/viewer/motor-assembly.glb?cap=__WIKICAP__').then(() => {+ const added = scene.meshes.filter(m => !before.has(m));+ motorRoot = added.find(m => m.name === '__root__') || added[0] || null;+ added.forEach(m => { m.isPickable = false; });+ }).catch(e => { console.warn('motor assembly failed to load', e); motorBtn.textContent = 'Motor assembly (unavailable)'; });+ return motorLoading;+}+motorBtn.onclick = async () => {+ motorBtn.disabled = true; await ensureMotor(); motorBtn.disabled = false;+ if (!motorRoot) return;+ motorOn = !motorOn; motorRoot.setEnabled(motorOn); motorBtn.classList.toggle('on', motorOn); };-document.getElementById('btn-labels').onclick = e => { labelsOn=!labelsOn; e.target.classList.toggle('on',labelsOn); };-document.getElementById('btn-power').onclick = e => {- powerOn = !powerOn; e.target.classList.toggle('on', powerOn);- glows.forEach(g => g.setEnabled(mode!=='assembly' && powerOn));+document.getElementById('btn-labels').onclick = e => { labelsOn=!labelsOn; e.target.classList.toggle('on',labelsOn); };+document.getElementById('btn-power').onclick = e => {+ powerOn = !powerOn; e.target.classList.toggle('on', powerOn);+ glows.forEach(g => g.setEnabled(mode!=='assembly' && powerOn)); };-document.getElementById('topbar').onclick = ev => { if (mode==='assembly') { t = A_LEN * (ev.clientX/innerWidth); lastCapKey=null; } };+document.getElementById('topbar').onclick = ev => { if (mode==='assembly') { t = A_LEN * (ev.clientX/innerWidth); lastCapKey=null; } }; // debug handle (harmless; also handy from the console) window.__dbg = { parts, COMPONENTS, byBlock, sched, groups, outlines, labels, cam,@@ -559,15 +578,15 @@ window.__dbg = { parts, COMPONENTS, byBlock, sched, groups, outlines, labels, ca cam.radius=camGoal.radius; cam.target.copyFrom(camGoal.target); } }, pause(){ playing=false; }, play(){ playing=true; } }; -setMode('assembly');-addEventListener('resize', () => scene.getEngine().resize());+setMode('assembly');+addEventListener('resize', () => scene.getEngine().resize()); } catch (err) {- statusEl.textContent = 'viewer error: ' + (err && err.message ? err.message : err);+ statusEl.textContent = 'viewer error: ' + (err && err.message ? err.message : err); console.error(err); } })();-</script>+</script> "></iframe> <script> /* wiki#73 caps: the injected bootstrap patches THIS document, but a sandboxed srcdoc child is a
viewer/motor-assembly.glbadded⋯ 1 unchanged line ⋯