Files
dtf-system/local/static/site-pdf.js
Cauê Faleiros 96f1d27221 refactor: split the Site's behaviour out of one 1,575-line inline script
dtf-site.html held commercial rules, the nesting engine, PDF analysis, the cart
and every handler in a single inline script, 42% of the runtime code in one
file, and the money logic lived in the middle of it.

It is now nine files under local/static, cut at the section markers the original
author left, so no function was split across a boundary: config, product modes,
upload, sheet analysis, PDF, quality, packing, cart, flow. They load as classic
scripts in the original order and share one global scope, so evaluation is
exactly what it was; the extraction was checked byte-identical against the
original before the tags replaced it. dtf-site.html is 1,394 lines of markup and
style.

With no inline script left anywhere, the policy no longer needs a hash
allowlist: script-src is now 'self' alone, which is stronger than what it
replaced and cannot drift as the page changes.

Three things depended on the old shape and were updated rather than worked
around. The pricing parity test read the ladder out of the HTML and now reads it
from site-config.js, still proving the server agrees with what the customer is
shown. The isolated artwork test served four hardcoded script paths and now
serves any script that resolves inside local/static, so the next file added does
not silently 404. The CSP assertion checked the whole policy for 'unsafe-inline'
and now checks the script-src directive alone, since style-src legitimately
carries it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 16:44:49 -03:00

401 lines
20 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 dtf-site.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 local/static/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;
// Alguns navegadores e iframes bloqueiam Worker de blob. Descobrimos antes de
// tentar, porque sem worker o PDF é lido na thread principal e precisa de outra
// estratégia: grade mais grossa e mais tempo.
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{
const codigo='importScripts("'+PDFJS_WORKER+'");';
const url=URL.createObjectURL(new Blob([codigo],{type:'application/javascript'}));
temWorker=testaWorker(url);
window.pdfjsLib.GlobalWorkerOptions.workerSrc = temWorker? url : 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/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
function rasterizarPdf(file, larguraCm, alturaCm){
let motivo=null;
const trabalho=carregarPdfJs().then(lib=>{
if(!lib){ motivo='não foi possível carregar o leitor de PDF'; return null; }
// sem worker tudo roda na thread principal · grade mais grossa para caber
const folga = temWorker===false ? 2.2 : 1;
return file.arrayBuffer().then(buf=>lib.getDocument({
data:buf, disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
verbosity:0 // sem worker é fallback, não erro
}).promise).then(doc=>
doc.getPage(1).then(page=>{
const vp1=page.getViewport({scale:1});
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
const alvo=Math.max(8, Math.round(larguraCm/passo));
const esc=alvo/vp1.width;
const vp=page.getViewport({scale:esc});
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});
return page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'})
.promise.then(()=>page.getOperatorList()).then(ops=>{
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=>{ motivo=(e&&e.message)? e.message : 'erro ao abrir o PDF'; return null; });
// sem worker o processo é lento: damos mais tempo antes de desistir
const espera = temWorker===false ? 90000 : 30000;
const tarde=new Promise(r=>setTimeout(()=>{ motivo='demorou demais neste navegador'; r('tempo'); }, espera));
return Promise.race([trabalho, tarde]).then(r=>{
const saida = r==='tempo'? null : r;
if(!saida) return {erro: motivo||'não deu para conferir', semWorker: temWorker===false};
return saida;
});
}
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)){
const pedaco=(inicio,fim)=>new Promise(r=>{
const fr=new FileReader();
fr.onload=()=>r(new TextDecoder('latin1').decode(new Uint8Array(fr.result)));
fr.onerror=()=>r('');
fr.readAsArrayBuffer(file.slice(inicio,fim));
});
const M=4*1048576;
Promise.all([pedaco(0,M), pedaco(Math.max(0,file.size-M), file.size)]).then(([a,b])=>{
const txt=a+b;
const re=/\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)/;
const m=txt.match(re);
if(!m) return res(null);
// UserUnit escala a página inteira · sem ele, 1 unidade = 1/72 pol
const uu=txt.match(/\/UserUnit\s+([\d.]+)/);
const esc=uu? Math.max(1,Math.min(75000,+uu[1])) : 1;
const ptW=Math.abs(+m[3]-+m[1]), ptH=Math.abs(+m[4]-+m[2]);
if(!(ptW>0&&ptH>0)) return res(null);
const w=ptW*PT_CM*esc, h=ptH*PT_CM*esc;
// acima de 14.400 unidades sem UserUnit o arquivo está fora da especificação
const foraDoPadrao = !uu && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT);
res({larg:+w.toFixed(1), alt:+h.toFixed(1),
fonte:'página do PDF'+(uu? ' · UserUnit '+esc : ''), foraDoPadrao});
});
return;
}
res(null);
});
}
function pintaFolha(){
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+0.5, 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{
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> '+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();
}));
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(){
$('lista').innerHTML=artes.map((a,i)=>{
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
if(a.cm>0){
const dpi=Math.round(a.px/(a.cm/2.54)), 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+' px</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.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.floor(+i.value||1));
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);
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((a.px/(a.cm/2.54))/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>':'');
}