اضافه شدن قابلیت رندر پی سی بی
Verify / test (push) Failing after 20s

This commit is contained in:
2026-11-09 12:34:50 +03:30
والد a599086c11
کامیت 4eb2cb3d6f
33فایلهای تغییر یافته به همراه2470 افزوده شده و 28 حذف شده
+9
مشاهده پرونده
@@ -0,0 +1,9 @@
/** Native layer identities; drill visibility also depends on its recorded copper span. */
export function layerVisible(id, span, selected) {
if (!selected.has(id)) return false;
return id !== '81' || !span.length || span.some(layer => selected.has(layer));
}
export function presetLayers(layers, preset) {
return new Set(layers.filter(layer => preset === 'all' || layer.id === '0' || layer.id === '81' ||
(preset === 'top' ? ['1','33'].includes(layer.id) : ['32','34'].includes(layer.id))).map(layer => layer.id));
}
@@ -0,0 +1,15 @@
import assert from 'node:assert/strict';
import {test} from 'node:test';
import {layerVisible,presetLayers} from './pcb-layers.js';
test('blind via drills follow actual selected copper span',()=>{
assert.equal(layerVisible('81',['2','3'],new Set(['1','81'])),false);
assert.equal(layerVisible('81',['2','3'],new Set(['2','81'])),true);
assert.equal(layerVisible('81',['1'],new Set(['1'])),false);
assert.equal(layerVisible('0',[],new Set(['0'])),true);
});
test('layer presets preserve native IDs and outline',()=>{
const layers=['0','1','2','32','33','34','81'].map(id=>({id}));
assert.deepEqual([...presetLayers(layers,'top')],['0','1','33','81']);
assert.deepEqual([...presetLayers(layers,'bottom')],['0','32','34','81']);
assert.equal(presetLayers(layers,'all').size,7);
});
+20
مشاهده پرونده
@@ -0,0 +1,20 @@
[hidden] { display: none !important; }
#pcb-workspace { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0,1fr); }
body.pcb-comparison #pcb-workspace { grid-template-columns: minmax(0,1fr) minmax(0,1fr); }
body.menu-open #pcb-workspace { grid-template-columns: minmax(0,1fr) 270px; }
body.pcb-comparison.menu-open #pcb-workspace { grid-template-columns: minmax(0,1fr) minmax(0,1fr) 270px; }
#pcb-menu { padding: 10px; overflow: auto; font-size: 12px; }
#pcb-menu h2 { padding: 5px 0; background: transparent; }
#layers { display: grid; gap: 5px; margin: 12px 0; }
#layer-presets { display: flex; gap: 5px; }
#compatibility { color: #60716b; font-size: 11px; overflow-wrap: anywhere; }
#diff-controls { min-height: 100px; }
#changes { max-height: 35vh; padding: 0; margin-top: 10px; }
#summary { font-size: 12px; }
.surface { background: #08151b; }
@media (max-width: 700px) {
body.pcb-comparison #pcb-workspace { grid-template-columns: 1fr; grid-template-rows: minmax(0,1fr) minmax(0,1fr); }
body.menu-open #pcb-workspace { grid-template-columns: minmax(0,1fr) 210px; }
body.pcb-comparison.menu-open #pcb-workspace { grid-template-columns: minmax(0,1fr) 210px; grid-template-rows: minmax(0,1fr) minmax(0,1fr); }
#pcb-menu { grid-column: 2; grid-row: 1 / -1; }
}
+5
مشاهده پرونده
@@ -0,0 +1,5 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PCB Viewer</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><link rel="stylesheet" href="/assets/compare.css?v=pcb-1"><link rel="stylesheet" href="/assets/pcb.css?v=1"><script type="module" src="/assets/pcb.js?v=1"></script></head><body>
<button id="menu-toggle" type="button" aria-label="Show menu" aria-expanded="false" aria-controls="pcb-toolbar pcb-menu"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
<header id="pcb-toolbar" data-pcb-menu hidden><strong>PCB</strong><div id="summary"></div><div class="controls"><label id="sync-control"><input id="sync" type="checkbox" checked> Sync views</label><label id="highlight-control"><input id="highlights" type="checkbox" checked> Highlights</label><button id="fit" type="button">Fit</button></div></header>
<main id="pcb-workspace"><section class="pane"><div id="old-surface" class="surface" tabindex="0" aria-label="PCB"></div></section><section id="new-pane" class="pane" hidden><div id="new-surface" class="surface" tabindex="0" aria-label="Modified PCB"></div></section><aside id="pcb-menu" data-pcb-menu hidden><section><h2>Layers</h2><div id="layer-presets"><button data-preset="top" type="button">Top</button><button data-preset="bottom" type="button">Bottom</button><button data-preset="all" type="button">All</button></div><div id="layers"></div></section><section id="diff-controls" hidden><div class="change-controls"><select id="filter" aria-label="Change filter"><option value="all">All changes</option><option value="added">Added</option><option value="removed">Removed</option><option value="modified">Modified</option><option value="moved">Moved</option></select><button id="previous" aria-label="Previous change">↑</button><button id="next" aria-label="Next change">↓</button></div><label><input id="only-changes" type="checkbox"> Changes only</label><nav id="changes" aria-label="PCB changes"></nav><pre id="details"></pre></section><p id="compatibility"></p></aside></main><output id="state" class="loading" role="status">Loading PCB…</output></body></html>
+85
مشاهده پرونده
@@ -0,0 +1,85 @@
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();
}
function expire(){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;});