// Development-only, read-only browser evidence. No forced render or streaming changes. export function attachModelDiagnostics(scene, host, readState, {now=()=>performance.now(),isMoving=()=>false}={}) { const node=host.ownerDocument.createElement('details'),summary=host.ownerDocument.createElement('summary'),output=host.ownerDocument.createElement('output') node.setAttribute('data-model-diagnostics','');summary.textContent='模型加载诊断(开发)' node.style.cssText='position:absolute;bottom:4px;left:4px;z-index:8;max-width:420px;font:11px monospace;background:#061827dd;color:#bad4e0;padding:4px;pointer-events:auto' output.style.cssText='display:block;white-space:pre-wrap;max-height:180px;overflow:auto' output.setAttribute('aria-label','模型加载诊断读数');node.append(summary,output);host.appendChild(node) let start=null,lastFrame=null,lastPaint=-Infinity,total=0 const durations=[],intervals=[] const add=(list,n)=>{list.push(n);if(list.length>120)list.shift()} const stats=list=>{const sorted=[...list].sort((a,b)=>a-b);return{samples:list.length,p50Ms:sorted.length?+sorted[Math.floor(sorted.length*.5)].toFixed(2):null,p95Ms:sorted.length?+sorted[Math.min(sorted.length-1,Math.floor(sorted.length*.95))].toFixed(2):null}} const pre=scene.preRender.addEventListener(()=>{start=now()}) function paint(){const state=readState();output.textContent=JSON.stringify({...state,renderEvents:total,renderSubmissionDuration:stats(durations),movingFrameIntervals:stats(intervals),gpuTimeMeasured:false,fpsDuringIdleMeaningful:false},null,2)} const post=scene.postRender.addEventListener(()=>{const t=now();total++ if(!host.ownerDocument.hidden){if(start!==null)add(durations,t-start);if(lastFrame!==null&&isMoving()&&t-lastFrame<1000)add(intervals,t-lastFrame)} start=null;lastFrame=t;if(t-lastPaint>=500){lastPaint=t;paint()} }) summary.addEventListener('click',paint);paint() return()=>{pre();post();node.remove()} }