refactor: give the frontend its own directory and split the API into routers
The Site's page sat at the repository root while its scripts lived in local/static, a split with no reason behind it. They are together in web/ now, with the page as index.html, which is also what the image serves. app.py held the adapters, the configuration, the shared query helpers and nineteen routes; customer.py held fourteen more but could not import from it without a cycle, so it was wired by passing nine callables into install_routes. Configuration and shared helpers move to local/runtime.py, the rules for attaching artwork to an order move to local/artwork.py where a customer correction and an operator final-file set can share them, and the routes become seven routers under local/api. app.py is 48 lines that create the application, apply the middleware and include them. Routers import downwards only. Three faults came out of the extraction and are worth recording, because each passed a check that looked sufficient. ast reports a function's line at the def, so every decorator on the line above fell outside the extracted range: twelve routes and the security middleware were defined but never registered, and the files still imported and parsed cleanly. Names the old closure renamed on the way in, and a Jsonb import, were missing in three modules. A name-resolution pass over every new module found those; the route count matching the original exactly, 32, is what confirmed the first. The release gate's marker for the fake payment adapter pointed at app.py and the adapter moved to runtime.py, so the gate passed while the condition it guards was unchanged. That is the same silent decay 2.5 set out to fix. A test now asserts every marker still matches something in its file, so the next move fails loudly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
400
web/site-pdf.js
Normal file
400
web/site-pdf.js
Normal file
@@ -0,0 +1,400 @@
|
||||
/* 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;
|
||||
// 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>':'');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user