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.
← Commit history
Publish 1.1.9
11 files changed
+359−29
SKILL.md+27−2files/chip-thumbnail-widget.js+23−5files/component-lod-preview.js+77package.json+7−4page.json+12−5readme.html+1−1skills/component-widgets/SKILL.md+12−2skills/component-widgets/widgets/chip-thumbnail-widget.js+23−5skills/component-widgets/widgets/component-lod-preview.js+77widgets/chip-thumbnail-widget.js+23−5widgets/component-lod-preview.js+77SKILL.md+27−2@@ -78,6 +78,21 @@ to light its parts; hover a part to light its nets. `src` is a rendered PNG (in production: `adom-glb2png` / the chip-thumbnailer off the part's GLB). Falls back to a clean placeholder when there is no render. +### `<adom-moleculize>`: board to workcell molecule · new+A BOARD-page widget. Drop a `.kicad_pcb` on it (read entirely client-side, no backend)+and it shows the molecule version `kicad-to-molecule` would produce: the original board+wrapped in workcell wings with 4 corner machine pins, grid-snapped. Only *generating*+the real `.molecule.kicad_pcb` needs compute, so that is an AI-thread hook, not on-page.+```+<adom-moleculize></adom-moleculize> <!-- shows a drop zone; or seed dims: -->+{ "name": "board", "width_mm": 35, "height_mm": 85, "kicad_pcb": "board.kicad_pcb" }+```+It parses the Edge.Cuts bounding box in JS, then applies the engine's rule verbatim:+`max(w,h) > 60mm → LARGE` (16mm grid, 3mm offset) else MEDIUM (8mm grid, 1mm offset);+`MC_span = ceil(orig/grid)·grid`, board centered, 4 corner MPs inset. Hover a machine+pin for its coordinate; the "Ask the Adom AI to build it" button hands the actual+conversion to a thread.+ ## The cross-highlight bus Every widget speaks one channel: `BroadcastChannel('lbr-hover')` (with a@@ -141,8 +156,7 @@ for a compact four-level listing. The `adom/component-lod@1` manifest records high, medium, low and super-low files, sizes, decoded attribute/index estimates, triangles, primitives, hashes and review status. Missing levels remain visible as not generated; candidates never become visually approved through this widget.-The widget uses the existing cache implementation and creates no WebGL context,-fetches no GLB, and duplicates no shaded chip viewer. The chip outline widget+By default the widget uses cached images, creates no WebGL context and fetches no GLB. Interactive 3D is an explicit opt-in, described below. The chip outline widget remains focused on cached symbol artwork. Reuse the shared `wiki-component-lod` skill for generation, coordinate/MPN@@ -157,3 +171,14 @@ The legacy `adom-chip-thumbnail` tag also accepts this LOD manifest. In a network-restricted HTML README, inline the widget script and JSON instead of fetching. Use `selfCheck()` for available levels, source hash, axes, review state and AI hints. Metrics are manifest-reported, not live hash or GPU validation.+++## Image-first LOD inspection++Use a cached PNG or WebP screenshot for each available level, generated from the actual high/medium/low/super-low GLB with the same camera and reviewed source pose. The per-level `preview` entry records `path`, image `sha256`, `sizeBytes`, dimensions, view name and `sourceLevelSha256`; a source/model change invalidates its preview. Keep these small (approximately 400 pixels wide) and lazy-decode them. Never synthesize a model preview or use one level's image to impersonate another.++No 3D viewer, Babylon bundle or GLB may load on the default page path. A user must choose **Turn on 3D preview**. The optional preview extends the canonical Adom Babylon viewer, starts with super-low, and uses screen size plus hysteresis to promote detail during zoom. Show the active level. Fetch one model at a time, verify its hash and byte count, retain the coarse fallback and current detail only, and dispose unused detail. Download bytes and decoded geometry estimates are not total GPU memory.++Turning the preview off, hiding the tab, scrolling it offscreen or disconnecting the widget must abort pending work, dispose model containers and the viewer/engine, release the WebGL context, and stop timers/render loops. A later activation starts coarse again. Retaining the downloaded JavaScript module is normal; retaining a live engine/model is not. The shared runtime permits one active LOD preview per same-origin BroadcastChannel and uses a 16 MiB decoded geometry budget. Runtime imports can be blocked by a restrictive HTML sandbox; use the registered Markdown widget frame, and do not weaken page authentication or CSP to work around it.++Before publication, test image-only network requests, all four zoom levels, idle rendering, repeat activation/deactivation and live GPU-buffer/context cleanup. Screenshot the real authenticated widget and record purposeful inspection motion. A passing runtime check is separate from native CAD or manufacturing qualification.
files/chip-thumbnail-widget.js+23−5@@ -1,5 +1,6 @@ /* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {+ const widgetBase=new URL('.',document.currentScript?.src||'https://wiki.adom.inc/adom/component-widgets/render/widgets/chip-thumbnail-widget.js'); 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}}`; class AdomChipThumbnail extends HTMLElement {@@ -8,9 +9,10 @@ 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(){this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}} _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');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;@@ -38,6 +40,18 @@ 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{+ const module=await import(new URL('component-lod-preview.js?v=1.1.9',widgetBase));+ 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();@@ -48,21 +62,25 @@ 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);- return {...l,available:true,url:this._url(l.path,src),status:l.review||'Review not recorded'};+ 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. Download size is not GPU memory.','Missing or candidate detail is not visually or physically qualified.',...(d.hints||[])]};+ 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}- .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}+ .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. This widget loads no 3D models.</p>+ <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');
files/component-lod-preview.jsadded+77@@ -0,0 +1,77 @@+// 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='https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';+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.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.');+ 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 r=await fetch(resolveAsset(info.path),{credentials:'include',signal:request.controller.signal});if(!r.ok)throw Error('LOD request returned '+r.status);+ const data=await r.arrayBuffer();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;+ const url=URL.createObjectURL(new Blob([data],{type:'model/gltf-binary'}));+ try{container=await B.SceneLoader.LoadAssetContainerAsync('',url,scene,undefined,'.glb');}finally{URL.revokeObjectURL(url);}+ 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;}+}
package.json+7−4@@ -2,8 +2,8 @@ "slug": "component-widgets", "title": "Component Widgets", "type": "app",- "version": "1.1.8",- "description": "Reusable, dependency-free web-component widgets to drop into any component or molecule page: <adom-pinout> (datasheet-style pin diagram + synced table), <adom-heatsink-via> (exposed-pad thermal via array, only when the part has an EP), <adom-molecule> (ganged part set + nets), and <adom-chip-thumbnail> (rendered part image). Each is one custom element, self-contained (Shadow DOM, no deps), theme-aware (light + dark), fed by inline JSON or el.data, and all on one lbr-hover cross-highlight bus so a pin hovered in one lights the same pin everywhere. Symbol, footprint, and 3D stay with adom-lbr; the datasheet renders via the native viewer. Installs the widget JS, iframe-embeddable viewers, example data, and an AI skill to ~/.claude/skills/component-widgets. Live reference page renders them all as a readme.html.",+ "version": "1.1.9",+ "description": "Reusable component-page widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and image-first LOD caches with an optional zoom-driven 3D preview.", "license": "MIT", "dependencies": {}, "scripts": {@@ -46,9 +46,12 @@ "component LOD widget", "LOD listing", "LOD manifest",- "high medium low super-low"+ "high medium low super-low",+ "LOD preview screenshots",+ "image-first LOD widget",+ "turn on 3D preview" ],- "discovery_pitch": "Reach for component-widgets when a component or molecule page needs a live pinout, an exposed-pad thermal via pattern, a ganged molecule/sub-circuit view, or a part thumbnail: each is one self-contained <adom-*> tag fed by inline JSON, theme-aware, and cross-linked with adom-lbr's symbol/footprint/3D on one hover bus.",+ "discovery_pitch": "Show cached component artwork and LOD images without loading 3D. Enable the canonical Adom viewer only when the user asks to inspect zoom-driven detail.", "sample_prompts": [ { "label": "Add a pinout",
page.json+12−5@@ -4,7 +4,7 @@ "type": "app", "title": "Component Widgets", "brief": "Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.",- "version": "1.1.8",+ "version": "1.1.9", "visibility": { "public": true },@@ -60,12 +60,19 @@ "drop a pinout into a page", "reusable component widget", "cross-highlight pins on a page",+ "cached chip thumbnails",+ "reuse chip outline",+ "reviewed MPN placement",+ "chip artwork cache", "component LOD widget", "LOD listing", "LOD manifest",- "high medium low super-low"+ "high medium low super-low",+ "LOD preview screenshots",+ "image-first LOD widget",+ "turn on 3D preview" ],- "discovery_pitch": "Reach for component-widgets when a component or molecule page needs a live pinout, an exposed-pad thermal via pattern, a ganged molecule/sub-circuit view, or a part thumbnail: each is one self-contained <adom-*> tag fed by inline JSON, theme-aware, and cross-linked with adom-lbr's symbol/footprint/3D on one hover bus.",+ "discovery_pitch": "Show cached component artwork and LOD images without loading 3D. Enable the canonical Adom viewer only when the user asks to inspect zoom-driven detail.", "metadata": {}, "created_at": "2026-08-06T15:51:32.326Z", "updated_at": "2026-08-06T15:51:32.326Z",@@ -77,7 +84,7 @@ }, "skills": [], "org": "adom",- "description": "Reusable, dependency-free web-component widgets to drop into any component or molecule page: <adom-pinout> (datasheet-style pin diagram + synced table), <adom-heatsink-via> (exposed-pad thermal via array, only when the part has an EP), <adom-molecule> (ganged part set + nets), and <adom-chip-thumbnail> (rendered part image). Each is one custom element, self-contained (Shadow DOM, no deps), theme-aware (light + dark), fed by inline JSON or el.data, and all on one lbr-hover cross-highlight bus so a pin hovered in one lights the same pin everywhere. Symbol, footprint, and 3D stay with adom-lbr; the datasheet renders via the native viewer. Installs the widget JS, iframe-embeddable viewers, example data, and an AI skill to ~/.claude/skills/component-widgets. Live reference page renders them all as a readme.html.",+ "description": "Reusable component-page widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and image-first LOD caches with an optional zoom-driven 3D preview.", "dependencies": {}, "scripts": { "install": "./install.sh",@@ -87,4 +94,4 @@ "hero", "readme" ]-}+}
readme.html+1−1@@ -1184,4 +1184,4 @@ h2 .anchor:hover{opacity:1;color:var(--teal)} <section style="padding:20px;border:1px solid #30363d;border-radius:12px;margin:24px 0"><h2>Reuse reviewed chip artwork</h2><p>The chip-thumbnail widget now accepts a cached-artwork manifest as well as an image. Its cache mode shows vector chip outlines, download links, exact virtual MPN placement, Z-up orientation and review state. The page’s existing 3D viewer shows the full chip. Existing image-only pages continue to work.</p><p>Read the component’s <code>artwork/MPN-thumbs.json</code> before generating anything. Compare source hashes and reuse its reviewed placement. Hash verification is recorded at publication time; native pin-1 and model-seating qualification remain separate.</p><pre>::adom-chip-thumbnail{src="/OWNER/COMPONENT/render/artwork/MPN-thumbs.json"}</pre></section></body> </html> -<section id="component-lods" style="padding:20px;border:1px solid #30363d;border-radius:12px;margin:24px 0"><h2>Component LOD listings</h2><p>The <code>adom-component-lod</code> widget lists high, medium, low and super-low model derivatives, download sizes, decoded geometry buffer estimates, triangles, primitives, source hashes and review states. It makes no GLB requests and creates no WebGL context. Missing levels remain visible as missing.</p><pre>::adom-component-lod{src="/OWNER/COMPONENT/render/lod/manifest.json"}</pre><p>Feed the <code>adom/component-lod@1</code> manifest through the registered Markdown tag. For sandboxed HTML, inline the shared script and manifest JSON. Reuse the reviewed pose and marking placement; verify hashes before downstream use. Candidate files and a listing alone do not establish native seating or manufacturing qualification.</p></section>+<section id="component-lods" style="padding:20px;border:1px solid #30363d;border-radius:12px;margin:24px 0"><h2>Component LOD listings</h2><p>The <code>adom-component-lod</code> widget lists high, medium, low and super-low model derivatives, download sizes, decoded geometry buffer estimates, triangles, primitives, source hashes and review states. By default it shows small cached images of each level and creates no WebGL context. Turn on the optional 3D preview to inspect zoom-driven detail changes, then turn it off to release the model and viewer resources. Missing levels remain visible as missing.</p><pre>::adom-component-lod{src="/OWNER/COMPONENT/render/lod/manifest.json"}</pre><p>Feed the <code>adom/component-lod@1</code> manifest through the registered Markdown tag. For sandboxed HTML, inline the shared script and manifest JSON. Reuse the reviewed pose and marking placement; verify hashes before downstream use. Candidate files and a listing alone do not establish native seating or manufacturing qualification.</p></section>
skills/component-widgets/SKILL.md+12−2@@ -156,8 +156,7 @@ for a compact four-level listing. The `adom/component-lod@1` manifest records high, medium, low and super-low files, sizes, decoded attribute/index estimates, triangles, primitives, hashes and review status. Missing levels remain visible as not generated; candidates never become visually approved through this widget.-The widget uses the existing cache implementation and creates no WebGL context,-fetches no GLB, and duplicates no shaded chip viewer. The chip outline widget+By default the widget uses cached images, creates no WebGL context and fetches no GLB. Interactive 3D is an explicit opt-in, described below. The chip outline widget remains focused on cached symbol artwork. Reuse the shared `wiki-component-lod` skill for generation, coordinate/MPN@@ -172,3 +171,14 @@ The legacy `adom-chip-thumbnail` tag also accepts this LOD manifest. In a network-restricted HTML README, inline the widget script and JSON instead of fetching. Use `selfCheck()` for available levels, source hash, axes, review state and AI hints. Metrics are manifest-reported, not live hash or GPU validation.+++## Image-first LOD inspection++Use a cached PNG or WebP screenshot for each available level, generated from the actual high/medium/low/super-low GLB with the same camera and reviewed source pose. The per-level `preview` entry records `path`, image `sha256`, `sizeBytes`, dimensions, view name and `sourceLevelSha256`; a source/model change invalidates its preview. Keep these small (approximately 400 pixels wide) and lazy-decode them. Never synthesize a model preview or use one level's image to impersonate another.++No 3D viewer, Babylon bundle or GLB may load on the default page path. A user must choose **Turn on 3D preview**. The optional preview extends the canonical Adom Babylon viewer, starts with super-low, and uses screen size plus hysteresis to promote detail during zoom. Show the active level. Fetch one model at a time, verify its hash and byte count, retain the coarse fallback and current detail only, and dispose unused detail. Download bytes and decoded geometry estimates are not total GPU memory.++Turning the preview off, hiding the tab, scrolling it offscreen or disconnecting the widget must abort pending work, dispose model containers and the viewer/engine, release the WebGL context, and stop timers/render loops. A later activation starts coarse again. Retaining the downloaded JavaScript module is normal; retaining a live engine/model is not. The shared runtime permits one active LOD preview per same-origin BroadcastChannel and uses a 16 MiB decoded geometry budget. Runtime imports can be blocked by a restrictive HTML sandbox; use the registered Markdown widget frame, and do not weaken page authentication or CSP to work around it.++Before publication, test image-only network requests, all four zoom levels, idle rendering, repeat activation/deactivation and live GPU-buffer/context cleanup. Screenshot the real authenticated widget and record purposeful inspection motion. A passing runtime check is separate from native CAD or manufacturing qualification.
skills/component-widgets/widgets/chip-thumbnail-widget.js+23−5@@ -1,5 +1,6 @@ /* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {+ const widgetBase=new URL('.',document.currentScript?.src||'https://wiki.adom.inc/adom/component-widgets/render/widgets/chip-thumbnail-widget.js'); 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}}`; class AdomChipThumbnail extends HTMLElement {@@ -8,9 +9,10 @@ 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(){this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}} _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');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;@@ -38,6 +40,18 @@ 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{+ const module=await import(new URL('component-lod-preview.js?v=1.1.9',widgetBase));+ 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();@@ -48,21 +62,25 @@ 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);- return {...l,available:true,url:this._url(l.path,src),status:l.review||'Review not recorded'};+ 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. Download size is not GPU memory.','Missing or candidate detail is not visually or physically qualified.',...(d.hints||[])]};+ 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}- .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}+ .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. This widget loads no 3D models.</p>+ <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');
skills/component-widgets/widgets/component-lod-preview.jsadded+77@@ -0,0 +1,77 @@+// 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='https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';+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.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.');+ 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 r=await fetch(resolveAsset(info.path),{credentials:'include',signal:request.controller.signal});if(!r.ok)throw Error('LOD request returned '+r.status);+ const data=await r.arrayBuffer();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;+ const url=URL.createObjectURL(new Blob([data],{type:'model/gltf-binary'}));+ try{container=await B.SceneLoader.LoadAssetContainerAsync('',url,scene,undefined,'.glb');}finally{URL.revokeObjectURL(url);}+ 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;}+}
widgets/chip-thumbnail-widget.js+23−5@@ -1,5 +1,6 @@ /* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {+ const widgetBase=new URL('.',document.currentScript?.src||'https://wiki.adom.inc/adom/component-widgets/render/widgets/chip-thumbnail-widget.js'); 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}}`; class AdomChipThumbnail extends HTMLElement {@@ -8,9 +9,10 @@ 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(){this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}} _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');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;@@ -38,6 +40,18 @@ 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{+ const module=await import(new URL('component-lod-preview.js?v=1.1.9',widgetBase));+ 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();@@ -48,21 +62,25 @@ 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);- return {...l,available:true,url:this._url(l.path,src),status:l.review||'Review not recorded'};+ 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. Download size is not GPU memory.','Missing or candidate detail is not visually or physically qualified.',...(d.hints||[])]};+ 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}- .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}+ .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. This widget loads no 3D models.</p>+ <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');
widgets/component-lod-preview.jsadded+77@@ -0,0 +1,77 @@+// 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='https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';+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.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.');+ 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 r=await fetch(resolveAsset(info.path),{credentials:'include',signal:request.controller.signal});if(!r.ok)throw Error('LOD request returned '+r.status);+ const data=await r.arrayBuffer();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;+ const url=URL.createObjectURL(new Blob([data],{type:'model/gltf-binary'}));+ try{container=await B.SceneLoader.LoadAssetContainerAsync('',url,scene,undefined,'.glb');}finally{URL.revokeObjectURL(url);}+ 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;}+}