Files
dtf-system/web/site-pdf.js
2026-09-23 10:40:18 -03:00

436 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Site DTF — PDF measurement and rasterisation, and the DPI of images inside one.
Extracted verbatim from the single inline script in web/index.html.
Loaded as classic scripts in the order listed there: they share one global
scope and run top to bottom, exactly as the original did. */
// ── PDF também é conferido: rasteriza a página e mede o DPI das imagens de dentro.
// Arte vetorial não tem resolução — nesses casos a nota é máxima, e isso é correto.
// Served from this origin, not a CDN: the Site keeps working when a third party
// does not, and the CSP can name only 'self' for scripts and workers. Provenance
// and hashes are recorded in web/vendor/README.md.
const PDFJS_URL='/vendor/pdf.min.js';
const PDFJS_WORKER=new URL('/vendor/pdf.worker.min.js', location.origin).href;
let pdfLibP=null, temWorker=null;
// Check whether this browser can create a same-origin Worker. The worker file
// is served alongside this script and does not need a blob URL.
function testaWorker(url){
try{ const w=new Worker(url); w.terminate(); return true; }catch(e){ return false; }
}
function carregarPdfJs(){
if(typeof window==='undefined') return Promise.resolve(null);
if(window.pdfjsLib) return Promise.resolve(window.pdfjsLib);
if(pdfLibP) return pdfLibP;
pdfLibP=new Promise(res=>{
const sc=document.createElement('script');
sc.src=PDFJS_URL;
sc.onload=()=>{
try{
temWorker=testaWorker(PDFJS_WORKER);
window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER;
}catch(e){
temWorker=false;
try{ window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e2){}
}
res(window.pdfjsLib||null);
};
sc.onerror=()=>res(null);
document.head.appendChild(sc);
});
return pdfLibP;
}
// DPI real de cada imagem embutida: acompanha a matriz de transformação e compara
// o tamanho em que a imagem foi colocada com quantos pixels ela tem.
function dpiDasImagens(page, ops){
const mul=(a,b)=>[a[0]*b[0]+a[2]*b[1], a[1]*b[0]+a[3]*b[1],
a[0]*b[2]+a[2]*b[3], a[1]*b[2]+a[3]*b[3],
a[0]*b[4]+a[2]*b[5]+a[4], a[1]*b[4]+a[3]*b[5]+a[5]];
const O=window.pdfjsLib.OPS;
let ctm=[1,0,0,1,0,0]; const pilha=[]; const dpis=[];
for(let i=0;i<ops.fnArray.length;i++){
const fn=ops.fnArray[i], ar=ops.argsArray[i];
if(fn===O.save) pilha.push(ctm.slice());
else if(fn===O.restore) ctm=pilha.pop()||[1,0,0,1,0,0];
else if(fn===O.transform) ctm=mul(ctm,ar);
else if(fn===O.paintImageXObject||fn===O.paintJpegXObject||fn===O.paintImageXObjectRepeat){
let im=null;
try{ im=page.objs.get(ar[0]); }catch(e){}
if(!im||!im.width) continue;
const larguraPt=Math.hypot(ctm[0],ctm[1]);
if(larguraPt<1) continue;
const areaPt=Math.abs(ctm[0]*ctm[3]-ctm[1]*ctm[2]);
dpis.push({dpi: im.width/(larguraPt*page.userUnit/72), area: areaPt});
}
}
return dpis;
}
// A régua de 96 DPI vale por arte, não pela folha inteira. Numa folha de 250 peças,
// um selo pequeno em 65 DPI não pode barrar o pedido: o que decide é quanto da
// área impressa está abaixo do limite.
function resumoDpi(lista){
if(!lista.length) return null;
const total=lista.reduce((t,x)=>t+x.area,0) || 1;
const abaixoPiso=lista.filter(x=>x.dpi<DPI_RECUSA);
const abaixoAviso=lista.filter(x=>x.dpi<DPI_AVISA);
const pond=lista.reduce((t,x)=>t+x.dpi*x.area,0)/total;
return {
pior: Math.round(Math.min(...lista.map(x=>x.dpi))),
ponderado: Math.round(pond),
fracBaixa: abaixoPiso.reduce((t,x)=>t+x.area,0)/total,
fracAviso: abaixoAviso.reduce((t,x)=>t+x.area,0)/total,
quantasBaixas: abaixoPiso.length,
quantasAviso: abaixoAviso.length,
total: lista.length
};
}
// Rasteriza a primeira página a 1 mm por pixel e devolve a tela para a análise
async function rasterizarPdf(file, larguraCm, alturaCm){
const lib=await carregarPdfJs();
if(!lib) return {erro:'não foi possível carregar o leitor de PDF'};
// Decide after the worker check; the old timeout always used 30 seconds.
const espera=temWorker===false ? 90000 : 30000;
const folga=temWorker===false ? 2.2 : 1;
let loading=null, doc=null, rendering=null, expired=false, timer=null;
const trabalho=(async()=>{
try{
const buf=await file.arrayBuffer();
if(expired) return null;
loading=lib.getDocument({data:buf, disableFontFace:true,
isEvalSupported:false, useSystemFonts:false, verbosity:0});
doc=await loading.promise;
if(expired || doc.numPages!==1) return null;
const page=await doc.getPage(1);
if(expired) return null;
const vp1=page.getViewport({scale:1});
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
const alvo=Math.max(8,Math.round(larguraCm/passo));
const vp=page.getViewport({scale:alvo/vp1.width});
const cv=document.createElement('canvas');
cv.width=Math.max(8,Math.round(vp.width)); cv.height=Math.max(8,Math.round(vp.height));
const ctx=cv.getContext('2d',{willReadFrequently:true});
rendering=page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'});
await rendering.promise;
if(expired) return null;
const ops=await page.getOperatorList();
if(expired) return null;
let dpis=[];
try{ dpis=dpiDasImagens(page,ops); }catch(e){}
const r=resumoDpi(dpis);
return {tela:cv, dpi:r? r.ponderado:null, res:r};
}catch(e){
if(expired) return null;
return {erro:(e&&e.message)||'erro ao abrir o PDF', semWorker:temWorker===false};
}finally{
if(doc) doc.destroy().catch(()=>{});
else if(loading) loading.destroy().catch(()=>{});
}
})();
const tardio=new Promise(resolve=>{
timer=setTimeout(()=>{
expired=true;
try{ if(rendering) rendering.cancel(); }catch(e){}
if(loading) loading.destroy().catch(()=>{});
resolve({erro:'demorou demais neste navegador',semWorker:temWorker===false});
},espera);
});
const result=await Promise.race([trabalho,tardio]);
clearTimeout(timer);
return result||{erro:'não deu para conferir',semWorker:temWorker===false};
}
function medirFolha(file){
return new Promise(res=>{
const nome=(file.name||'').toLowerCase();
if(RENDERIZA.test(nome)){
carregarImagem(file).then(img=>{
if(!img || !img.width) return res(null);
const L=larguraFilme();
// Uma imagem só é folha montada se tiver pixel para ocupar a largura do
// filme. Sem esta conferência, qualquer arte pequena vira "folha" e leva
// o preço por metro sem nunca ter sido montada.
res({larg:L, alt:+(img.height/img.width*L).toFixed(1), fonte:'proporção da imagem',
dpiFolha:Math.round(img.width/(L/2.54)), px:img.width});
});
return;
}
if(/\.pdf$/.test(nome)){
// PDF boxes can be inherited, compressed, rotated, and scaled by UserUnit.
// Searching raw bytes for /MediaBox can read the wrong object or miss it.
let doc=null;
carregarPdfJs().then(async lib=>{
if(!lib) throw new Error('Não foi possível carregar o leitor de PDF.');
doc=await lib.getDocument({data:await file.arrayBuffer(),
disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
verbosity:0}).promise;
if(doc.numPages!==1)
return {rejected:true, reason:'O PDF tem '+doc.numPages+
' páginas. Envie cada folha como um PDF de uma página para calcular o preço correto.'};
const page=await doc.getPage(1);
const view=page.view, unit=page.userUnit, vp=page.getViewport({scale:1});
if(!Array.isArray(view) || view.length!==4 ||
!Number.isFinite(unit) || unit<=0 ||
!Number.isFinite(vp.width) || !Number.isFinite(vp.height) ||
vp.width<=0 || vp.height<=0)
return {rejected:true, reason:'Não conseguimos determinar o tamanho desta página PDF. Exporte-a novamente.'};
const ptW=Math.abs(view[2]-view[0]), ptH=Math.abs(view[3]-view[1]);
if(!(ptW>0 && ptH>0) ||
(unit===1 && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT)))
return {rejected:true, reason:'A página passa do limite de 508 cm sem UserUnit. Divida a folha ou exporte em PNG.'};
// This pinned PDF.js build exposes the page's rotated view in points;
// userUnit is separate and must scale both physical dimensions.
const w=vp.width*unit*PT_CM, h=vp.height*unit*PT_CM;
if(!Number.isFinite(w) || !Number.isFinite(h) || w<=0 || h<=0 || h>6000)
return {rejected:true, reason:'O tamanho desta página PDF não é suportado. Divida a folha em partes menores.'};
// Keep precise geometry for the fit and quote; the UI rounds only
// when displaying dimensions.
return {larg:+w.toFixed(3), alt:+h.toFixed(3),
fonte:'página do PDF'+(unit!==1 ? ' · UserUnit '+unit : '')};
}).then(res).catch(()=>res({rejected:true,
reason:'Não conseguimos ler este PDF. Exporte-o novamente como PDF de uma página ou PNG.'}))
.finally(()=>{ if(doc) doc.destroy().catch(()=>{}); });
return;
}
res(null);
});
}
function pintaFolha(){
invalidaItemAtual();
const L=larguraFilme();
pintaTipoEnvio(); // trava o seletor enquanto houver folha
if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; }
$('lista').innerHTML = folhas.map((x,i)=>{
const lido=!!x.med, larga=lido && x.med.larg>L, suspeito=lido && x.med.foraDoPadrao;
const sub=(x.m||0)*(x.rep||1);
let medida='';
if(lido){
const a=x.an;
const conferido = a
? '<div class="conf"><b>Conferido de verdade</b>'+
'<span>'+a.artes+' peça'+(a.artes===1?'':'s')+' · '+a.aproveitamento+
'% da folha virou arte · '+
(a.residuos? a.residuos+' resíduo'+(a.residuos>1?'s':'') : 'sem resíduo')+' · '+
(a.fundoChapado? 'fundo chapado' : 'fundo transparente')+'</span>'+
'<span>resolução '+a.dpi+' DPI · '+
(a.fonteDpi==='imagens'? 'medida imagem por imagem dentro do PDF'
: a.fonteDpi==='vetor'? 'arte vetorial, sem imagem embutida'
: 'lida do arquivo')+
' · grade de '+n1(a.grade)+' mm</span></div>'
: '';
medida='<div class="med'+(larga||suspeito?' alerta':'')+'">'+
'<b>'+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm</b>'+
'<span>'+x.med.fonte+' · '+fmtM(x.m)+' m de folha</span>'+
conferido+
(suspeito? '<em><b>Este PDF passa do tamanho que o formato comporta.</b> '+
'A especificação para em 508 cm e o seu tem '+n1(x.med.alt)+' cm. '+
'Ele abre certo só em quem o gerou — aqui vai precisar de tratamento. '+
'Manda em <b>PNG ou TIFF</b>, ou divide em partes de até 5 m, que sai na hora.</em>':'')+
(larga? '<em><b>Esta folha não roda neste filme.</b> Ela tem '+n1(x.med.larg)+
' cm de largura e o filme aqui tem '+n1(L)+' cm. Não dá para reduzir: '+
'encolher a folha encolheria cada arte junto.'+
(x.med.larg<=57.5
? ' Ela cabe no <b>DTF têxtil de 57 cm</b> — troque de produto ali em cima.'
: ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'</em>':'')+
'</div>';
}else if(x.measurementError){
medida='<div class="med alerta"><b>Este PDF não pode ser orçado</b><span>'+
escapeHTML(x.measurementError)+'</span></div>';
}else{
const teto=TABELA[modo], piso=pisoEscada();
const dif=(x.m>0)? (teto-piso)*cobrar(x.m*(x.rep||1)) : 0;
medida='<div class="med pede">'+
'<b>'+escapeHTML(extDe(x.f.name))+' não abre no navegador</b>'+
'<span>Sem conseguir conferir o arquivo, não temos como dar nota — e sem nota '+
'o metro sai pela tabela, <b>'+rs(teto)+'</b>. Alguém vai abrir esse arquivo na mão '+
'antes de imprimir.</span>'+
'<div class="troca">Exportando a mesma folha em <b>PNG ou PDF</b>, a conferência é '+
'automática e o metro pode cair até <b>'+rs(piso)+'</b>'+
(dif>0.5? ' — <b>'+rs(dif)+' a menos</b> só nesta folha' : '')+'.</div>'+
'<span style="margin-top:9px">Informe o comprimento para seguir:</span>'+
'<input type="number" min="0.5" max="60" step="0.01" data-comp="'+i+'" '+
'placeholder="metros" value="'+(x.m? x.m.toFixed(2):'')+'">'+
'</div>';
}
const barra = x.pct!=null
? '<div class="prog"><i style="width:'+x.pct+'%"></i></div>'+
'<span class="progT">'+(x.pct<50?'lendo o arquivo…':
x.pct<70?'medindo a folha…':'conferindo a arte…')+'</span>'
: x.semAnalise
? '<div class="progF"><b>Não conseguimos conferir esta folha.</b> '+escapeHTML(x.semAnalise)+
'. Seguimos com o preço de tabela — se quiser a nota e o desconto, exporte a mesma '+
'folha em <b>PNG</b>, que a conferência é bem mais leve.</div>'
: '';
return '<div class="rep"><div class="l1"><b>'+
(folhas.length>1? (i+1)+'. ':'')+escapeHTML(x.f.name)+'</b>'+
'<span style="color:var(--fraco);font-size:10.5px">'+fmt(x.f.size)+'</span>'+
'<button data-rmf="'+i+'">×</button></div>'+
barra+medida+
(x.m>0 ?
'<div class="cps" style="margin-top:10px">'+
'<div><label>quantas vezes repetir</label>'+
'<input type="number" min="1" max="200" data-repf="'+i+'" value="'+x.rep+'"></div>'+
'<div class="conta"><b>'+fmtM(sub)+' m</b>'+
(x.rep>1? ' · '+x.rep+' × '+fmtM(x.m)+' m':'')+'</div>'+
'</div>' : '')+
'</div>';
}).join('');
// total do pedido, quando há mais de uma folha
const tot=folhaTotalM(), partes=Math.ceil(tot/20);
if(folhas.length>1 && tot>0){
$('lista').innerHTML += '<div class="somaF">'+
'<span>'+folhas.length+' folhas neste pedido</span>'+
'<b>'+fmtM(tot)+' m no total</b>'+
(partes>1? '<em>vai em '+partes+' partes de até 20 m</em>':'')+'</div>';
}else if(partes>1 && tot>0){
$('lista').innerHTML += '<div class="somaF"><span>folha longa</span>'+
'<b>'+fmtM(tot)+' m</b><em>vai em '+partes+' partes de até 20 m</em></div>';
}
$('lista').querySelectorAll('[data-rmf]').forEach(b=>b.addEventListener('click',()=>{
folhas.splice(+b.dataset.rmf,1); pintaFolha();
}));
$('lista').querySelectorAll('[data-repf]').forEach(el=>el.addEventListener('change',e=>{
folhas[+el.dataset.repf].rep=Math.max(1,Math.min(200,+e.target.value||1)); pintaFolha();
}));
$('lista').querySelectorAll('[data-comp]').forEach(el=>el.addEventListener('change',e=>{
folhas[+el.dataset.comp].m=Math.max(0,Math.min(60,+e.target.value||0)); pintaFolha();
}));
$('lista').querySelectorAll('[data-repf], [data-comp]').forEach(el=>
el.addEventListener('input',invalidaItemAtual));
agendaAvaliacao(); previaAoVivo();
}
// sugestão de largura
const cabemNaLinha=w=>Math.max(1,Math.floor((larguraFilme()+GAP_CM)/(w+GAP_CM)));
const larguraPara=n=>(larguraFilme()-(n-1)*GAP_CM)/n;
// Só sugere se a folha realmente encolher. Caber mais um por fileira não adianta
// nada se a quantidade pedida continuar ocupando o mesmo número de fileiras.
function sugestao(a){
const w=a.cm, q=Math.max(1,a.q||1), prop=propDe(a);
const n=cabemNaLinha(w), alvo=Math.floor(larguraPara(n+1)*10)/10;
if(!(alvo>0) || alvo>=w) return null;
if((w-alvo)/w>0.12) return null; // não corta mais que 12% da peça
const fa=Math.ceil(q/n), fd=Math.ceil(q/(n+1));
if(fd>=fa) return null; // mesmo número de fileiras: não muda nada
const economiaCm=fa*w*prop - fd*alvo*prop;
if(economiaCm<0.5) return null; // menos de meio centímetro não é notícia
return {alvo, de:n, para:n+1, fileiras:fa-fd, cm:economiaCm};
}
// A cota em escala: o cliente ve a peca dentro da largura do filme, quantas cabem
// lado a lado e quanto sobra. Numero em texto ninguem le; retangulo todo mundo entende.
function vizPeca(a){
const BASE=300, L=larguraFilme();
const e0=BASE/L, alt0=a.cm*propDe(a)*e0;
const k = alt0>132 ? 132/alt0 : 1; // tira nunca fica alta demais
const e = e0*k;
const n=cabemNaLinha(a.cm), pw=a.cm*e, ph=a.cm*propDe(a)*e, gp=GAP_CM*e;
const sobra=L-(n*a.cm+(n-1)*GAP_CM);
const ext=escapeHTML(a.f.name.split('.').pop().toUpperCase());
let pecas='';
for(let i=0;i<n;i++) pecas+='<div class="pc" style="width:'+pw.toFixed(1)+'px;height:'+
ph.toFixed(1)+'px">'+(a.src?'<img src="'+a.src+'" alt="" style="transform:'+
(a.giro?'rotate(90deg) ':'')+(a.esp?'scaleX(-1)':'')+'">':'<span>'+ext+'</span>')+'</div>';
const Wt=(L*e+6).toFixed(1); // +6 = borda e respiro da tira
return '<div class="viz">'+
'<div class="vizRow">'+
'<div class="vizH" style="height:'+ph.toFixed(1)+'px"><span>'+n1(a.cm*propDe(a))+' cm</span></div>'+
'<div class="vizTira" style="width:'+Wt+'px">'+
'<div class="vizL"><i></i><b>'+n1(L)+' cm de filme</b><i></i></div>'+
'<div class="tira" style="height:'+ph.toFixed(1)+'px;gap:'+gp.toFixed(1)+'px">'+pecas+
(sobra>0.25?'<div class="folga"><span>sobra '+n1(sobra)+' cm</span></div>':'')+
'</div>'+
'</div>'+
'</div>'+
'<div class="vizPe"><b>'+n1(a.cm)+' cm</b> de largura · <b>'+n+
(n>1?' peças':' peça')+'</b> por fileira'+
(sobra>0.25?' · sobram <b>'+n1(sobra)+' cm</b>':' · largura toda aproveitada')+'</div>'+
'</div>';
}
const n1=v=>v.toFixed(1).replace('.',',');
function pintaArtes(){
invalidaItemAtual();
$('lista').innerHTML=artes.map((a,i)=>{
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
if(a.decodeError){
calc='<span class="er">Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG</span>';
}else if(a.cm>larguraFilme()){
calc='<span class="er">Largura maior que o filme de '+n1(larguraFilme())+' cm</span>';
}else if(a.cm>0 && a.src){
const dpi=Math.round(dpiDe(a)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
barra='<div class="qb2"><i class="'+cls(n)+'" style="width:'+n+'%"></i></div>';
calc='<span class="'+cls(n)+'"><b>'+n+'%</b> · '+dpi+' DPI</span><span>'+
a.q+' × '+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</span>';
const sug=sugestao(a);
dica = sug
? '<div class="dica"><b>Reduzindo para '+n1(sug.alvo)+' cm você economiza '+
n1(sug.cm)+' cm de folha</b>Passam de '+sug.de+' para '+sug.para+
' por fileira, e as suas '+a.q+' peças ocupam '+sug.fileiras+
' fileira'+(sug.fileiras>1?'s':'')+' a menos.<button data-usar="'+i+'" data-cm="'+
sug.alvo+'">Usar sugerido</button></div>'
: '';
dica = vizPeca(a) + dica;
}
return '<div class="art'+(a===recemChegada?' nova':'')+'"><div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+
'<span style="color:var(--fraco);font-size:10.5px">'+
(a.px>0?a.px+' × '+a.py+' px':'a ler')+'</span>'+
'<button data-rm="'+i+'">×</button></div><div class="cps">'+
'<div><label>largura na peça</label><input type="number" min="1" max="'+larguraFilme()+
'" placeholder="cm" data-cm="'+i+'" value="'+(a.cm||'')+'"></div>'+
'<div><label>quantas vezes</label><input type="number" min="1" placeholder="1" data-q="'+i+
'" value="'+a.q+'"></div></div>'+
'<div class="atalhos">'+
(TAMANHOS[larguraFilme()]||[]).map(t=>'<button class="tm'+
(Math.abs(a.cm-t)<0.05?' on':'')+'" data-tam="'+i+'" data-t="'+t+'">'+
n1(t)+' cm</button>').join('')+
'<span class="sep"></span>'+
'<button class="gr'+(a.giro?' on':'')+'" data-giro="'+i+'" title="Girar 90°">⟳</button>'+
'<button class="gr'+(a.esp?' on':'')+'" data-esp="'+i+'" title="Espelhar">⇋</button>'+
'</div>'+barra+'<div class="calc">'+calc+'</div>'+dica+'</div>';
}).join('');
$('lista').querySelectorAll('[data-rm]').forEach(b=>b.addEventListener('click',()=>{artes.splice(+b.dataset.rm,1);pintaArtes();}));
$('lista').querySelectorAll('[data-cm]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.cm].cm=+i.value||0;pintaArtes();}));
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.min(200,Math.floor(+i.value||1)));pintaArtes();}));
$('lista').querySelectorAll('[data-cm], [data-q]').forEach(i=>i.addEventListener('input',()=>{
const largura=i.hasAttribute('data-cm');
const a=artes[+(largura?i.dataset.cm:i.dataset.q)];
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.min(200,Math.floor(+i.value||1)));
invalidaItemAtual();
previaAoVivo();
}));
$('lista').querySelectorAll('[data-usar]').forEach(b=>b.addEventListener('click',()=>{
artes[+b.dataset.usar].cm=+b.dataset.cm; pintaArtes();
}));
$('lista').querySelectorAll('[data-tam]').forEach(b=>b.addEventListener('click',()=>{
artes[+b.dataset.tam].cm=+b.dataset.t; pintaArtes();
}));
$('lista').querySelectorAll('[data-giro]').forEach(b=>b.addEventListener('click',()=>{
const a=artes[+b.dataset.giro]; a.giro=a.giro?0:90; pintaArtes();
}));
$('lista').querySelectorAll('[data-esp]').forEach(b=>b.addEventListener('click',()=>{
const a=artes[+b.dataset.esp]; a.esp=!a.esp; pintaArtes();
}));
resumoArtes();
previaAoVivo();
}
function resumoArtes(){
const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
if(!prontas.length){ r.style.display='none'; return; }
const m=montagemCm/100; // a mesma folha que aparece ao lado
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
const falta=artes.length-prontas.length;
r.style.display='block'; r.className='resumoA on';
r.innerHTML='<div class="r"><span>artes com tamanho</span><b>'+prontas.length+' de '+artes.length+'</b></div>'+
'<div class="r"><span>qualidade média</span><b style="color:'+
(media>=85?'var(--verde)':media>=50?'var(--laranja)':'var(--vermelho)')+'">'+media+'%</b></div>'+
(pior<media?'<div class="r"><span>a pior arte</span><b>'+pior+'%</b></div>':'')+
(m>0?'<div class="r"><span>folha montada</span><b>'+fmtM(m)+' m</b></div>'+
(cobrar(m)>m+0.001?'<div class="r"><span>cobrança</span><b>'+fmtM(cobrar(m))+
' m'+(cobrar(m)===MINIMO_M?' · mínimo':'')+'</b></div>':''):'')+
(falta?'<div class="r" style="color:var(--laranja)"><span>faltam informar</span><b>'+falta+'</b></div>':'');
}