app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
// Opt-in LOD inspection, built on the canonical Adom Babylon viewer.
// No module, engine or model is loaded until the user enables this preview.
const VIEWER='/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';
async function loadAssetBytes(info,resolveAsset,signal) {
if(info.transport?.kind!=='wiki-sandbox-frame@1'){
const r=await fetch(resolveAsset(info.path),{credentials:'include',signal});
if(!r.ok)throw Error('LOD request returned '+r.status);return r.arrayBuffer();
}
if(info.transport.sourceLevelSha256!==info.sha256||info.sizeBytes>8*1024*1024)throw Error('Invalid or oversized LOD transport');
const nonce=[...crypto.getRandomValues(new Uint8Array(16))].map(x=>x.toString(16).padStart(2,'0')).join('');
const frame=document.createElement('iframe');frame.hidden=true;frame.setAttribute('aria-hidden','true');frame.title='Component LOD asset transfer';
const url=new URL(resolveAsset(info.transport.path));url.hash=new URLSearchParams({nonce,sha256:info.sha256}).toString();
return new Promise((resolve,reject)=>{
let timer;
const cleanup=()=>{clearTimeout(timer);window.removeEventListener('message',receive);signal?.removeEventListener('abort',abort);frame.remove();};
const fail=e=>{cleanup();reject(e);};
const abort=()=>fail(new DOMException('LOD load cancelled','AbortError'));
const receive=e=>{
if(e.source!==frame.contentWindow||e.data?.type!=='adom-lod-asset'||e.data.nonce!==nonce)return;
const d=e.data;
if(d.sha256!==info.sha256||d.sizeBytes!==info.sizeBytes||!(d.buffer instanceof ArrayBuffer)||d.buffer.byteLength!==info.sizeBytes)return fail(Error('LOD transport payload does not match manifest'));
cleanup();resolve(d.buffer);
};
if(signal?.aborted)return abort();
window.addEventListener('message',receive);signal?.addEventListener('abort',abort,{once:true});
timer=setTimeout(()=>fail(Error('LOD asset transfer timed out; check wiki login and transport URL')),12000);
frame.src=url.href;document.body.append(frame);
});
}
export async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) {
let viewer,engine,scene,root,camera,raf=0,timer,observer,channel,stopped=false;
let current=null,wanted='super-low',loading=null,candidate=null,since=0,frames=0,lastFrame=0,awakeUntil=0,lastCamera='';
const cache=new Map(),levels=['super-low','low','medium','high'],thresholds=[90,190,340],budget=16*1024*1024;
const stats={active:true,loading:true,level:null,wanted,frames:0,downloads:0,glbRequests:0,evictions:0,residentLevels:[],decodedBufferBytes:0,errors:[],initialLevel:'super-low'};
function report(){stats.sceneReady=!!scene?.isReady();const overlay=engine?.loadingScreen?._loadingDiv;stats.loadingOverlayVisible=!!overlay&&getComputedStyle(overlay).display!=='none'&&overlay.getBoundingClientRect().height>0;stats.frames=frames;stats.wanted=wanted;stats.loading=!!loading;stats.residentLevels=[...cache.values()].map(e=>e.info.level);stats.decodedBufferBytes=[...cache.values()].reduce((s,e)=>s+e.info.decodedBufferBytes,0);onState({...stats});}
function invalidate(){awakeUntil=performance.now()+1100;if(!raf&&!stopped&&!document.hidden)raf=requestAnimationFrame(render);}
function render(now){raf=0;if(stopped||document.hidden)return;if(now-lastFrame>=1000/30){engine.beginFrame();try{scene.render();}finally{engine.endFrame();}frames++;lastFrame=now;}if(now<awakeUntil)raf=requestAnimationFrame(render);}
function dispose(reason='turned off'){
if(stopped)return;stopped=true;loading?.controller.abort();clearInterval(timer);cancelAnimationFrame(raf);observer?.disconnect();channel?.close();
document.removeEventListener('visibilitychange',hidden);signal?.removeEventListener('abort',aborted);
for(const e of cache.values())e.container.dispose();cache.clear();
const canvas=engine?.getRenderingCanvas();const gl=canvas&&(canvas.getContext('webgl2')||canvas.getContext('webgl'));
viewer?.destroy();gl?.getExtension('WEBGL_lose_context')?.loseContext();host.replaceChildren();
stats.active=false;stats.loading=false;stats.level=null;stats.residentLevels=[];stats.decodedBufferBytes=0;stats.stopReason=reason;onState({...stats,frames});
}
const hidden=()=>{if(document.hidden)dispose('tab hidden');},aborted=()=>dispose('turned off');
signal?.addEventListener('abort',aborted,{once:true});
try{
if(signal?.aborted)throw new DOMException('Preview cancelled','AbortError');
if(manifest.upAxis!=='Z'||!['metres','metre'].includes(manifest.units))throw Error('Preview requires a declared Z-up, metre-scale display model.');
if(levels.some(level=>!manifest.levels.find(l=>l.level===level)?.path))throw Error('All four LOD files must be available before interactive inspection.');
// Canonical viewer camera preferences use Web Storage. Opaque wiki widgets
// have none; provide document-local ephemeral storage only when access throws.
for(const key of ['localStorage','sessionStorage']){
try{void window[key].length;}catch{
const values=new Map();Object.defineProperty(window,key,{configurable:true,value:{getItem:k=>values.get(String(k))??null,setItem:(k,v)=>values.set(String(k),String(v)),removeItem:k=>values.delete(String(k)),clear:()=>values.clear(),key:i=>[...values.keys()][i]??null,get length(){return values.size;}}});
}
}
await import(VIEWER);if(stopped||signal?.aborted)return {dispose,state:()=>({...stats})};
const api=window.Adom3DViewerBabylon9,B=api.BABYLON;
viewer=api.init(host,{zUp:true,showViewCube:true});engine=viewer.getEngine();scene=viewer.getScene();scene.useRightHandedSystem=true;camera=viewer.getCamera();
engine.stopRenderLoop();engine.runRenderLoop=()=>invalidate();
viewer.setGroundVisible(false);scene.shadowsEnabled=false;viewer.getShadowGenerator()?.dispose();root=new B.TransformNode('component-z-up-mm',scene);root.scaling.setAll(1000);viewer.addContentRoot(root);
const fill=new B.HemisphericLight('lod-inspection-fill',new B.Vector3(0,0,1),scene);fill.intensity=.9;fill.groundColor=new B.Color3(.5,.5,.5);
const canvas=engine.getRenderingCanvas();for(const event of ['pointerdown','pointermove','pointerup','wheel','keydown'])canvas.addEventListener(event,invalidate,{passive:true});
document.addEventListener('visibilitychange',hidden);
observer=new IntersectionObserver(entries=>{if(!entries[0].isIntersecting)dispose('preview scrolled offscreen');});observer.observe(host);
try{channel=new BroadcastChannel('adom-component-lod-preview');channel.onmessage=()=>dispose('another component preview enabled');channel.postMessage({enabled:true});}catch{channel=null;}
function evict(){for(const [key,e] of cache)if(e!==current&&e.info.level!=='super-low'){e.container.dispose();cache.delete(key);stats.evictions++;}}
function apply(e,level){
if(current!==e){current?.container.removeAllFromScene();e.container.addAllToScene();for(const n of e.container.rootNodes)if(!n.parent)n.parent=root;current=e;}
stats.level=level;evict();invalidate();report();
}
async function load(level){
const info=manifest.levels.find(l=>l.level===level);if(cache.has(info.sha256)){apply(cache.get(info.sha256),level);return;}
if(loading)return;
evict();if(info.decodedBufferBytes+[...cache.values()].reduce((s,e)=>s+e.info.decodedBufferBytes,0)>budget)throw Error('Component preview exceeds the 16 MiB decoded-geometry budget.');
const request={level,controller:new AbortController()};loading=request;report();
let container;
try{
stats.glbRequests++;const data=await loadAssetBytes(info,resolveAsset,request.controller.signal);if(data.byteLength!==info.sizeBytes)throw Error('LOD byte count does not match manifest.');
const hash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',data))].map(x=>x.toString(16).padStart(2,'0')).join('');if(hash!==info.sha256)throw Error('LOD SHA-256 does not match manifest.');
if(stopped)return;
// Feed verified bytes directly; blob fetches are disallowed in wiki widgets.
container=await B.SceneLoader.LoadAssetContainerAsync('',new Uint8Array(data),scene,undefined,'.glb');
if(stopped||wanted!==level){container.dispose();return;}
const e={info,container};cache.set(info.sha256,e);stats.downloads++;apply(e,level);
}catch(e){container?.dispose();if(e.name!=='AbortError'&&!stopped){stats.errors.push(String(e));dispose('model loading failed');throw e;}}
finally{if(loading===request)loading=null;if(!stopped)report();}
}
await load('super-low');if(stopped)return {dispose,state:()=>({...stats})};
const box=manifest.levels.find(l=>l.level==='super-low').bounds;
const min=B.Vector3.FromArray(box.min).scale(1000),max=B.Vector3.FromArray(box.max).scale(1000),diameter=max.subtract(min).length();
const center=min.add(max).scale(.5);camera.minZ=.001;camera.lowerRadiusLimit=Math.max(.01,diameter*.35);camera.upperRadiusLimit=diameter*35;
camera.target=center;camera.setPosition(center.add(new B.Vector3(2,-3,3).normalize().scale(diameter*12)));
const tick=()=>{
if(stopped)return;
const sig=JSON.stringify([camera.alpha,camera.beta,camera.radius,...camera.target.asArray(),host.clientWidth,host.clientHeight].map(n=>Math.round(n*1e7)/1e7));
if(sig!==lastCamera){lastCamera=sig;engine.resize();invalidate();}
const distance=B.Vector3.Distance(camera.globalPosition,center),pixels=diameter*host.clientHeight/(2*Math.max(.01,distance)*Math.tan(camera.fov/2));
let index=levels.indexOf(wanted);while(index<3&&pixels>thresholds[index]*1.2)index++;while(index>0&&pixels<thresholds[index-1]*.8)index--;
if(levels[index]!==candidate){candidate=levels[index];since=performance.now();}
if(performance.now()-since>350){wanted=candidate;if(loading&&loading.level!==wanted)loading.controller.abort();if(!loading&&stats.level!==wanted)void load(wanted).catch(e=>{stats.errors.push(String(e));dispose('preview error');});}
report();
};
timer=setInterval(tick,150);invalidate();report();
return {dispose,state:()=>({...stats}),inspectDistance:multiple=>{camera.radius=diameter*multiple;invalidate();},camera:()=>({radius:camera.radius,diameter,alpha:camera.alpha,beta:camera.beta,target:camera.target.asArray(),size:[host.clientWidth,host.clientHeight]})};
}catch(e){dispose(e.name==='AbortError'?'turned off':String(e));throw e;}
}