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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
/* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */
(() => {
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const CSS = `:host{display:block;color:#dbe2ec;font:14px system-ui;--accent:#00b8b0}*{box-sizing:border-box}.cache{background:#0d1117;border:1px solid #30363d;border-radius:12px;padding:20px}h3{margin:0 0 8px;font-size:20px}.muted{color:#a1abb7;line-height:1.5}.badges{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.badge{padding:5px 9px;border:1px solid #394550;border-radius:6px;font-size:12px}.row{display:grid;grid-template-columns:minmax(220px,1fr) minmax(250px,1fr);gap:24px}.preview{background:#faf8f0;border-radius:9px;overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:250px}.preview img{width:100%;max-height:370px;object-fit:contain;transition:transform .3s}.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}button,a.download{font:inherit;border:1px solid #45505d;border-radius:6px;background:#17202a;color:#e5edf5;padding:8px 12px;cursor:pointer;text-decoration:none}button:hover,a:hover{border-color:var(--accent)}button[aria-pressed=true]{border-color:var(--accent);color:#60e1d6}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}code{overflow-wrap:anywhere;font-size:12px}dl{margin:0}dt{color:#9da8b4;font-size:12px;margin-top:12px}dd{margin:4px 0;line-height:1.5}details{margin-top:16px}summary{cursor:pointer;padding:8px 0}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#161b22;padding:12px;border-radius:6px;font-size:12px}.files{display:grid;gap:9px;margin:12px 0}.files a{color:#6edfd5}.error{color:#ffb7a5}.legacy{width:200px;text-align:center;margin:0}.legacy img{width:100%}.legacy figcaption{padding:8px} @media(max-width:580px){.row{grid-template-columns:1fr}.cache{padding:14px}}`;
// BEGIN bundled opt-in runtime (generated from component-lod-preview.js)
// 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);
});
}
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;}
}
// END bundled opt-in runtime
class AdomChipThumbnail extends HTMLElement {
static get observedAttributes(){return ['src','mpn','package'];}
set data(value){this._data=value;if(this.shadowRoot)this._load();}
get data(){return this._data;}
connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(!this._commandHandler){this._commandHandler=e=>{if(e.source!==parent||e.data?.type!=='adom-chip-thumbnail-command')return;const {action,index,open}=e.data,s=this.shadowRoot;let button;if(action==='selectVariant'&&Number.isInteger(index))button=s.querySelectorAll('.variants button')[index];else if(action==='zoom')button=s.querySelector('.zoom');else if(action==='fit')button=s.querySelector('.reset');else if(action==='showFiles'&&typeof open==='boolean'){const details=s.querySelector('details');if(details)details.open=open;}if(button)button.click();this._height();};window.addEventListener('message',this._commandHandler);}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();}
attributeChangedCallback(){if(this.shadowRoot)this._load();}
disconnectedCallback(){cancelAnimationFrame(this._heightFrame);this._heightFrame=0;this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}
_height(){
if(this._heightFrame)return;
this._heightFrame=requestAnimationFrame(()=>{
this._heightFrame=0;if(!this.isConnected)return;
// Measure content, never the iframe viewport: scrollHeight + 2 can grow
// its parent's iframe repeatedly and trigger scrolling/layout feedback.
const height=Math.ceil(this.getBoundingClientRect().height)+2;
// Only the wiki's sandboxed widget frame reports to its parent; inline, the parent may be a Hydrogen webview.
if(!/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname))return;
if(height!==this._lastHeight){this._lastHeight=height;parent.postMessage({type:'readme-height',height},'*');}
parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');
});
}
async _load(){
this._stopPreview();
const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';
try{
let d=this._data;
if(!d){const inline=this.querySelector('script[type="application/json"]');if(inline)d=JSON.parse(inline.textContent);}
if(!d && /\.json(?:[?#]|$)/i.test(src)){
this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache muted">Loading cached chip artwork…</div>`;
const r=await fetch(src,{credentials:'include'});if(!r.ok)throw new Error('Manifest request returned '+r.status);d=await r.json();
}
if(token!==this._request)return;
if(d?.schema==='adom/chip-artwork-cache@1')this._cache(d,src);
else if(d?.schema==='adom/component-lod@1')this._lod(d,src);
else if(d?.schema)throw new Error('Unsupported cache schema: '+d.schema);
else this._legacy(d||{src,mpn:this.getAttribute('mpn'),package:this.getAttribute('package')});
}catch(e){if(token===this._request){this._state={ok:false,error:String(e),hints:['Check the manifest URL, permissions and schema. No regeneration was attempted.']};this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache error">${esc(e.message)}</div>`;}}
this._height();
}
_url(path,src){
const base=new URL(src||location.href,location.href),marker=base.pathname.indexOf('/render/');
if(marker>=0){base.pathname=base.pathname.slice(0,marker+8);base.search='';base.hash='';}
const u=new URL(path,base);if(!['https:','http:'].includes(u.protocol)||u.origin!==base.origin)throw new Error('Asset must use the manifest origin');
if(marker>=0&&!u.pathname.startsWith(base.pathname))throw new Error('Asset path escapes the component repository');return u.href;
}
_legacy(d){
const src=d.src||'',valid=/^(https?:|data:image\/|\/|\.\/|[^:]+$)/i.test(src);
this.shadowRoot.innerHTML=`<style>${CSS}</style><figure class="cache legacy">${src&&valid?`<img alt="${esc(d.mpn)}" src="${esc(src)}">`:'<div class="muted">No render cached</div>'}<figcaption>${esc(d.mpn)} ${esc(d.package)}</figcaption></figure>`;
const img=this.shadowRoot.querySelector('img');if(img){img.onerror=()=>{img.replaceWith(document.createTextNode('Render unavailable'));this._height();};img.onload=()=>this._height();}this._state={ok:true,mode:'legacy-image'};
}
_stopPreview(){this._previewAbort?.abort();this._previewController?.dispose();this._previewController=null;this._previewActive=false;}
async _togglePreview(d,src){
if(this._previewActive){this._stopPreview();return;}
this._previewActive=true;const abort=this._previewAbort=new AbortController(),host=this.shadowRoot.querySelector('.lod-live'),button=this.shadowRoot.querySelector('.preview-toggle'),status=this.shadowRoot.querySelector('.preview-status');
host.hidden=false;button.textContent='Turn off 3D preview';status.textContent='Loading super-low detail…';this._height();
const update=state=>{if(this._previewAbort!==abort)return;this._state.preview=state;this._state.glbRequests=state.glbRequests||0;this._previewActive=state.active;button.textContent=state.active?'Turn off 3D preview':'Turn on 3D preview';host.hidden=!state.active;status.textContent=state.active?`${state.level||'Loading'}${state.loading?' · loading '+state.wanted:''} · zoom to change detail`:'3D preview off'+(state.stopReason?' · '+state.stopReason:'');this._height();};
try{
if(document.hidden)throw new Error('Activate this page before enabling 3D');
await new Promise(resolve=>requestAnimationFrame(()=>{host.scrollIntoView({block:'center',behavior:'instant'});requestAnimationFrame(resolve);}));
const module={mountLodPreview};
if(abort.signal.aborted){update({active:false,stopReason:'turned off'});return;}
const controller=await module.mountLodPreview({host,manifest:d,resolveAsset:p=>this._url(p,src),onState:update,signal:abort.signal});if(this._previewAbort===abort)this._previewController=controller;else controller.dispose();
}catch(e){update({active:false,error:String(e),stopReason:'Could not start preview'});status.textContent=String(e.message);}
}
_lod(d,src){
if(!Array.isArray(d.levels)||d.levels.length>4)throw new Error('LOD manifest must list at most four detail levels');
const order=['high','medium','low','super-low'],seen=new Set();
for(const l of d.levels){if(!order.includes(l.level)||seen.has(l.level))throw new Error('Unknown or duplicate LOD level');seen.add(l.level);}
const sourceHash=d.source?.sha256,validSource=/^[a-f0-9]{64}$/i.test(sourceHash||'');
const rows=order.map(level=>{
const l=d.levels.find(x=>x.level===level);
if(!l||!l.path)return {level,available:false,status:l?.review||d.missingReason||'Not generated'};
const fields=['sizeBytes','decodedBufferBytes','triangles','primitives'];
if(!/^[a-f0-9]{64}$/i.test(l.sha256||'')||fields.some(k=>!Number.isSafeInteger(l[k])||l[k]<0))throw new Error('Invalid hash or metrics for '+level);
if(l.preview&&(!/\.(png|webp)$/i.test(l.preview.path||'')||l.preview.sourceLevelSha256!==l.sha256||!/^[a-f0-9]{64}$/i.test(l.preview.sha256||'')))throw new Error('Stale or invalid cached preview for '+level);
return {...l,available:true,url:this._url(l.path,src),previewUrl:l.preview?.path?this._url(l.preview.path,src):null,status:l.review||'Review not recorded'};
});
const count=rows.filter(r=>r.available).length,num=n=>Number(n).toLocaleString('en-US'),kib=n=>(n/1024).toFixed(1)+' KiB';
this._state={ok:true,mode:'lod',mpn:d.mpn,manifest:src,sourceHash:sourceHash||null,sourceHashRecorded:validSource,
upAxis:d.upAxis||'unverified',units:d.units||'unverified',status:d.status||'unreviewed',levels:rows.map(({url,...r})=>r),
availableLevels:count,glbRequests:0,hints:['Read this manifest and verify exact source/level hashes before reuse.','This listing does not fetch or decode GLBs until the user enables the 3D preview. Download size is not GPU memory.','Missing or candidate detail is not visually or physically qualified.',...(d.hints||[])]};
const sourceLink=d.source?.path?this._url(d.source.path,src):null;
this.shadowRoot.innerHTML=`<style>${CSS}
.lod-previews{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.lod-previews figure{margin:0;border:1px solid #30363d;border-radius:8px;overflow:hidden}.lod-previews img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block;background:#303944}.lod-previews figcaption{padding:8px}.lod-previews .unavailable{padding:25px 8px;min-height:100px;color:#a1abb7}.lod-live{height:380px;position:relative;margin-top:12px}.lod-live[hidden]{display:none}@media(max-width:580px){.lod-previews{grid-template-columns:repeat(2,minmax(0,1fr))}}.preview-status{padding:10px 0}.table-scroll{overflow-x:auto;margin:18px 0}table{border-collapse:collapse;width:100%;text-align:left;font-size:13px;color:#dbe2ec;font-family:system-ui,sans-serif}caption{text-align:left;padding:0 0 10px;color:#a1abb7}th,td{padding:10px 12px;border-bottom:1px solid #30363d;vertical-align:top}th{color:#a1abb7;font-weight:500;white-space:nowrap}th:first-child,td:first-child{padding-left:0}td:nth-child(2),td:nth-child(3),td:nth-child(4),td:nth-child(5){white-space:nowrap;font-variant-numeric:tabular-nums}.lod a{color:#6edfd5}.lod details code{display:block;margin:8px 0}.status{max-width:340px;line-height:1.5}.missing{color:#e8b66f}.lod .manifest-link{display:inline-block;margin-top:8px}
</style><section class="cache lod"><h3>Component LODs</h3><div class="muted">${esc(d.mpn)} · Cached levels of detail for board and scaffold viewers.</div>
<div class="badges"><span class="badge">${count}/4 levels available</span><span class="badge">${esc(d.upAxis?d.upAxis+'-up':'Orientation unverified')} · ${esc(d.units||'units unverified')}</span><span class="badge">${esc(d.status||'Review not recorded')}</span></div>
<div class="lod-previews">${rows.map(r=>`<figure>${r.previewUrl?`<img src="${esc(r.previewUrl)}" alt="${esc(d.mpn)} ${r.level} cached model preview" loading="lazy" decoding="async" width="400" height="300">`:'<div class="unavailable">Preview not cached</div>'}<figcaption>${r.level}</figcaption></figure>`).join('')}</div>
${count===4?'<button class="preview-toggle" aria-label="Toggle interactive component LOD preview">Turn on 3D preview</button><div class="preview-status muted">Images only. 3D stays off until you enable it.</div><div class="lod-live" hidden></div>':''}
<div class="table-scroll"><table><caption>Start with super-low; load more detail when inspection needs it.</caption><thead><tr><th scope="col">Detail</th><th scope="col">Download</th><th scope="col">Decoded buffers*</th><th scope="col">Triangles</th><th scope="col">Primitives</th><th scope="col">Review</th></tr></thead><tbody>${rows.map(r=>r.available?`<tr data-level="${r.level}"><th scope="row">${esc(r.level)}</th><td><a href="${esc(r.url)}" download="${esc(r.path.split('/').pop())}">${kib(r.sizeBytes)} GLB</a></td><td>${kib(r.decodedBufferBytes)}</td><td>${num(r.triangles)}</td><td>${num(r.primitives)}</td><td class="status">${esc(r.status)}${r.detailVisibility?`<div class="muted">MPN: ${r.detailVisibility.virtualMpn?'included':'omitted at this scale'}</div>`:''}</td></tr>`:`<tr data-level="${r.level}"><th scope="row">${esc(r.level)}</th><td colspan="5" class="missing">${esc(r.status)}</td></tr>`).join('')}</tbody></table></div>
<p class="muted">*Manifest-reported attribute/index bytes. Excludes textures, the board, renderer overhead and driver allocations. Cached previews are images. 3D models load only after you turn on the interactive preview.</p>
<details><summary>Source hashes and reuse contract</summary><p class="muted">${validSource?'Source SHA-256 is recorded; verify it before reuse.':'Source hash is missing; resolve provenance before generation.'}</p>${sourceLink?`<a href="${esc(sourceLink)}" download>Source component asset</a>`:''}<code>${esc(sourceHash||'No source hash recorded')}</code>${rows.filter(r=>r.available).map(r=>`<code>${esc(r.level)}: ${esc(r.sha256)}</code>`).join('')}<pre>${esc((d.hints||[]).join('\n'))}</pre></details>${src?`<a class="manifest-link" href="${esc(new URL(src,location.href).href)}" download>LOD manifest for AI and viewer reuse</a>`:''}</section>`;
this.shadowRoot.querySelector('details').addEventListener('toggle',()=>this._height());
this.shadowRoot.querySelector('.preview-toggle')?.addEventListener('click',()=>this._togglePreview(d,src));
}
_cache(d,src){
if(!Array.isArray(d.assets)||d.assets.length>100)throw new Error('Invalid asset list');
const assets=d.assets.filter(a=>a.kind!=='chip-shaded').map(a=>({...a,url:this._url(a.path,src)}));
const previews=assets.filter(a=>a.kind==='chip-outline'&&a.format==='svg');
if(!previews.length)throw new Error('No cached previews');
const verified=d.verification?.status==='matched-at-publication',review=d.review||{};
this._state={ok:true,mode:'cache',mpn:d.mpn,cacheKey:d.cacheKey,assets:assets.length,sourceVerification:verified?'matched-at-publication':'not-verified',review,hints:d.hints||[],manifest:src};
this.shadowRoot.innerHTML=`<style>${CSS}</style><section class="cache"><h3>Chip outline cache</h3><div class="muted">${esc(d.mpn)} · Reuse reviewed chip outlines in symbols and libraries.</div><div class="badges"><span class="badge">${esc(d.recipe?.upAxis||'Unknown')}-up display</span><span class="badge">${assets.length} cached files</span><span class="badge">${verified?'Hashes matched when published':'Source check not recorded'}</span></div><div class="row"><div><div class="preview"><img alt="${esc(d.mpn)} cached chip artwork"></div><div class="controls variants"></div><div class="controls"><button class="zoom">Inspect closer</button><button class="reset">Fit</button></div></div><div><dl><dt>Virtual MPN placement</dt><dd>${esc(d.marking?.placement)}</dd><dt>Artwork review</dt><dd>${esc(review.projectedArtwork||'Not recorded')}</dd><dt>Display orientation</dt><dd>${esc(review.orientation)}</dd><dt>Source freshness</dt><dd class="muted">${verified?'Source and asset hashes matched '+esc(d.verification.checkedAtIso)+'.':'Source hashes have not been checked.'} This records a point-in-time check; compare the manifest hashes before reuse.</dd></dl><details><summary>Reuse files and generation recipe</summary><div class="files"></div><pre>${esc((d.hints||[]).join('\n'))}</pre><code>Cache key: ${esc(d.cacheKey)}</code></details></div></div></section>`;
const img=this.shadowRoot.querySelector('img'),controls=this.shadowRoot.querySelector('.variants');
previews.forEach((a,i)=>{const b=document.createElement('button');b.textContent=({'symbol':'Schematic ink','teal':'Teal','dark':'Dark ink'})[a.style]||a.label||a.style||a.kind;b.setAttribute('aria-pressed',String(i===0));b.onclick=()=>{img.src=a.url;img.style.transform='';controls.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));this._state.selected=a.path;};controls.append(b);});
img.src=previews[0].url;img.onload=()=>this._height();img.onerror=()=>{this._state.ok=false;this._state.error='Preview failed to load';img.alt='Cached preview unavailable';this._height();};
this.shadowRoot.querySelector('.zoom').onclick=()=>{img.style.transform='scale(1.55)';};this.shadowRoot.querySelector('.reset').onclick=()=>{img.style.transform='';};
const files=this.shadowRoot.querySelector('.files');for(const a of assets){const link=document.createElement('a');link.href=a.url.replace(/https:\/\/wiki\.adom\.inc\/([^/]+)\/([^/]+)\/render\//,'https://wiki.adom.inc/api/pages/$1/$2/files/');link.download=a.path.split('/').pop();link.textContent=`${a.path.split('/').pop()} · ${(a.sizeBytes/1024).toFixed(1)} KB`;files.append(link);}
if(src){const a=document.createElement('a');a.href=src;a.download='';a.textContent='Cache manifest and source hashes';files.append(a);}
this.shadowRoot.querySelector('details').addEventListener('toggle',()=>this._height());
}
selfCheck(){
const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');
if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);
const host=this.shadowRoot?.querySelector('.lod-live');
s.resources={viewerModuleLoaded:!!window.Adom3DViewerBabylon9,previewActive:!!this._previewActive,
viewerCanvases:host?.querySelectorAll('canvas').length||0,viewerFrames:host?.querySelectorAll('iframe').length||0,
assetTransferFrames:document.querySelectorAll('iframe[title="Component LOD asset transfer"]').length,
viewerModuleRequests:performance.getEntriesByType('resource').filter(r=>/adom-3d-viewer-babylon9/.test(r.name)).length,
modelRequests:s.glbRequests||0,activation:'explicit-click',scope:'This widget document only; inspect sibling page viewers separately'};
return s;
}
}
if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail);
if(!customElements.get('adom-component-lod'))customElements.define('adom-component-lod',class extends AdomChipThumbnail {});
})();