import { mountShare, viewerURL } from './share.js?v=1'; import { bindCamera } from './interactions.js'; import { layerVisible, presetLayers } from './pcb-layers.js'; const comparison=location.pathname.startsWith('/compare/pcb/'); const id=location.pathname.split('/').pop(), state=document.querySelector('#state'); const key=`pcb:${comparison?'comparison':'render'}:${id}`; const token=new URLSearchParams(location.hash.slice(1)).get('token')||sessionStorage.getItem(key); if(token)sessionStorage.setItem(key,token); if(location.hash)history.replaceState(null,'',location.pathname); const headers=token?{'X-Viewer-Token':token}:{}, controller=new AbortController(), views=[]; const base=comparison?`/api/v1/pcbs/comparisons/${id}`:`/api/v1/pcbs/${id}`; const menu=document.querySelector('#menu-toggle'); function setMenu(open){ document.body.classList.toggle('menu-open',open); for(const panel of document.querySelectorAll('[data-pcb-menu]'))panel.hidden=!open; menu.setAttribute('aria-expanded',String(open));menu.setAttribute('aria-label',open?'Hide menu':'Show menu'); } setMenu(false); menu.addEventListener('click',()=>setMenu(menu.getAttribute('aria-expanded')!=='true')); addEventListener('keydown',e=>{if(e.key==='Escape'){setMenu(false);menu.focus();}}); addEventListener('pagehide',()=>{controller.abort();for(const view of views)view.destroy();},{once:true}); document.body.classList.toggle('pcb-comparison',comparison); document.querySelector('#new-pane').hidden=!comparison; for(const selector of ['#sync-control','#highlight-control','#diff-controls'])document.querySelector(selector).hidden=!comparison; async function request(path='',text=false){ const r=await fetch(base+path,{headers,signal:controller.signal,cache:'no-store'}); if(!r.ok){const b=await r.json();throw new Error(b.error?.message||'PCB unavailable');} return text?r.text():r.json(); } function union(boxes){ const x=Math.min(...boxes.map(b=>b.x)),y=Math.min(...boxes.map(b=>b.y)); return {x,y,width:Math.max(...boxes.map(b=>b.x+b.width))-x,height:Math.max(...boxes.map(b=>b.y+b.height))-y}; } async function start(){ let result; for(;;){result=await request(comparison?'':'/status');if(result.status==='completed')break;if(result.status==='failed')throw new Error(result.error?.message||'PCB processing failed');await new Promise(r=>setTimeout(r,1000));} const texts=await Promise.all((comparison?['/svg/old','/svg/new']:['/svg']).map(path=>request(path,true))); const svgs=texts.map(text=>{ const xml=new DOMParser().parseFromString(text,'image/svg+xml'); if(xml.querySelector('parsererror')||xml.documentElement.localName!=='svg')throw new Error('Invalid PCB artifact'); return document.importNode(xml.documentElement,true); }); const boxes=comparison?[result.old.bounds,result.new.bounds]:svgs.map(svg=>{const b=svg.viewBox.baseVal;return {x:b.x,y:b.y,width:b.width,height:b.height};}); const bounds=union(boxes); for(const [i,svg]of svgs.entries()){ const surface=document.querySelector(i?'#new-surface':'#old-surface');surface.append(svg); views.push(bindCamera(surface,svg,bounds,source=>{if(comparison&&document.querySelector('#sync').checked)for(const view of views)if(view!==source)view.apply(source.pose());})); } await document.fonts.ready; state.classList.remove('loading');state.textContent=''; document.querySelector('#fit').addEventListener('click',()=>{for(const view of views)view.fit();}); document.querySelector('#sync').addEventListener('change',()=>{if(views[1]&&document.querySelector('#sync').checked)views[1].apply(views[0].pose());}); const layers=comparison?[...new Map([...result.old.layers,...result.new.layers].map(l=>[l.id,l])).values()]:result.layers; let selected=new Set(layers.filter(l=>l.visible).map(l=>l.id)); const layerNodes=svgs.flatMap(svg=>[...svg.querySelectorAll('[data-layer-id]')]); function paintLayers(){for(const node of layerNodes)node.setAttribute('display',layerVisible(node.dataset.layerId,(node.dataset.layerSpan||'').split(' ').filter(Boolean),selected)?'inline':'none');} const checks=new Map(); for(const layer of layers){ const label=document.createElement('label'),input=document.createElement('input');input.type='checkbox';input.checked=selected.has(layer.id); checks.set(layer.id,input);label.append(input,document.createTextNode(layer.name));document.querySelector('#layers').append(label); input.addEventListener('change',()=>{if(input.checked)selected.add(layer.id);else selected.delete(layer.id);paintLayers();}); } for(const button of document.querySelectorAll('[data-preset]'))button.addEventListener('click',()=>{selected=presetLayers(layers,button.dataset.preset);for(const [id,input]of checks)input.checked=selected.has(id);paintLayers();}); paintLayers(); const diagnostics=comparison?[...result.old.diagnostics,...result.new.diagnostics]:result.diagnostics||[]; document.querySelector('#compatibility').textContent=[...new Set(diagnostics.map(d=>d.message))].join('\n'); if(comparison){ const changes=result.changes,filter=document.querySelector('#filter'),rows=new Map();let active=null; document.querySelector('#summary').textContent=Object.entries(result.summary).map(([k,v])=>`${v} ${k}`).join(' · '); function focus(change){active=change.id;const boxes=[change.old_bounds,change.new_bounds].filter(Boolean);if(boxes.length)for(const view of views)view.focus(union(boxes));document.querySelector('#details').textContent=JSON.stringify({kind:change.kind,match:change.match,movement:change.movement,fields:change.fields},null,2);for(const [id,row]of rows)row.setAttribute('aria-current',String(id===active));} for(const change of changes){const button=document.createElement('button');button.type='button';button.className=change.status;button.textContent=`${change.status} · ${change.label}`;button.addEventListener('click',()=>focus(change));document.querySelector('#changes').append(button);rows.set(change.id,button);} if(!changes.length)document.querySelector('#changes').textContent='No structural changes detected.'; const byId=new Map(changes.map(c=>[c.id,c])),nodes=svgs.flatMap(svg=>[...svg.querySelectorAll('[data-change-id]')]); function apply(){ for(const node of nodes){const c=byId.get(node.dataset.changeId);node.toggleAttribute('data-diff-hidden',c?(filter.value!=='all'&&filter.value!==c.status):(filter.value!=='all'||document.querySelector('#only-changes').checked));} for(const change of changes)rows.get(change.id).hidden=filter.value!=='all'&&filter.value!==change.status; document.body.classList.toggle('no-highlights',!document.querySelector('#highlights').checked); } for(const input of [filter,document.querySelector('#only-changes'),document.querySelector('#highlights')])input.addEventListener('change',apply); for(const [selector,step]of [['#previous',-1],['#next',1]])document.querySelector(selector).addEventListener('click',()=>{const visible=changes.filter(c=>filter.value==='all'||c.status===filter.value);if(!visible.length)return;const i=visible.findIndex(c=>c.id===active);focus(visible[(i<0?(step>0?0:visible.length-1):(i+step+visible.length)%visible.length)]);}); for(const svg of svgs)svg.addEventListener('click',e=>{const c=byId.get(e.target.closest('[data-change-id]')?.dataset.changeId);if(c)focus(c);}); apply(); } mountShare(document.querySelector('#pcb-share'),viewerURL(location.pathname,token)); function expire(){if(result.expires_at===-1)return;const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('#pcb-workspace').replaceChildren();state.textContent='This PCB link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire(); } start().catch(e=>{state.classList.remove('loading');state.textContent=e.name==='AbortError'?'':e.message;});