436 lines
22 KiB
JavaScript
436 lines
22 KiB
JavaScript
/* 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>':'');
|
||
}
|