fix: harden week-two ordering, artwork and operations

This commit is contained in:
Cauê Faleiros
2026-09-23 10:40:18 -03:00
parent ccc25a2d5d
commit 24013458c9
43 changed files with 1064 additions and 228 deletions

View File

@@ -6,6 +6,23 @@
let draftId = localStorage.getItem('dtf-quote');
let requestKey = localStorage.getItem('dtf-request-key');
let requestBody = localStorage.getItem('dtf-request-body');
let quotedCart = localStorage.getItem('dtf-quote-cart');
let refreshVersion = 0;
function cartSnapshot() {
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
return JSON.stringify({customer:cliente,delivery:entrega,items:items.map(item=>({
mode:item.modo,metres:item.metros,grade:item.nota,production:item.production,
quality:item.qualityStatus,
acknowledged:item.qualityAcknowledged,
files:(item.localFiles||[]).map(file=>({name:file.name,size:file.size,lastModified:file.lastModified}))
}))});
}
function clearDraft() {
draftId=null;quotedCart=null;requestKey=null;requestBody=null;
for(const key of ['dtf-quote','dtf-quote-cart','dtf-request-key','dtf-request-body'])
localStorage.removeItem(key);
actions.replaceChildren();
}
const api = async (path, body) => {
const response = await fetch('/api'+path, {
credentials: 'same-origin', headers: {'Content-Type':'application/json'},
@@ -19,6 +36,12 @@
return data;
};
const ready = api('/session');
ready.then(session=>{
window.dtfUploadMaxBytes=session.max_upload_bytes;
const limit=document.getElementById('zLimite');
if(limit)limit.textContent='Até '+(session.max_upload_bytes/1048576).toFixed(0)+
' MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.';
}).catch(()=>{});
window.dtfSessionReady=ready;
window.dtfApi=api;
ready.catch(error => { status.textContent = error.message; });
@@ -50,8 +73,10 @@
if (busy) return;
if (draftId) { await refresh(); status.scrollIntoView({behavior:'smooth',block:'nearest'}); return; }
if (!clienteOk() || !entrega.cotado) return;
if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.');
const cart = [...pedido,...(itemAtual?[itemAtual]:[])];
if (!cart.length) return message('Adicione um item ao pedido.');
const initialCart=cartSnapshot();
busy = true;
$('bPagar').disabled = true;
try {
@@ -62,9 +87,15 @@
if (!item.localFiles?.length) throw new Error('Selecione novamente os arquivos deste item.');
const uploads=[];
for (const file of item.localFiles) uploads.push(await upload(file));
items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads});
if (item.production?.version!==2 || item.production.sources?.length!==uploads.length)
throw new Error('A montagem deste item precisa ser refeita antes da cotação.');
items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads,
production:{...item.production,sources:item.production.sources.map((source,index)=>({
upload_id:uploads[index],...source}))},
quality_status:item.qualityStatus,quality_acknowledged:item.qualityAcknowledged});
}
const content = {customer:{...cliente},items,freight:{service:entrega.tipo==='retira'?'pickup':'mock-standard',postal_code:entrega.tipo==='retira'?'':entrega.cep}};
if (cartSnapshot()!==initialCart) throw new Error('O carrinho mudou durante o envio. Confira os itens e envie de novo.');
const serialized = JSON.stringify(content);
if (!requestKey || serialized !== requestBody) {
requestKey = crypto.randomUUID(); requestBody = serialized;
@@ -72,6 +103,7 @@
localStorage.setItem('dtf-request-body',requestBody);
}
const quote = await api('/quotes',{request_key:requestKey,...content});
quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart);
draftId=quote.id; localStorage.setItem('dtf-quote',draftId);
await refresh();
status.scrollIntoView({behavior:'smooth',block:'nearest'});
@@ -80,10 +112,17 @@
};
async function refresh() {
if (!draftId) return;
const version=++refreshVersion, shownId=draftId;
try {
await ready;
const quote=await api('/quotes/'+draftId);
const quote=await api('/quotes/'+shownId);
if(version!==refreshVersion || draftId!==shownId) return;
actions.replaceChildren();
if (quote.status!=='paid' && (!quotedCart || quotedCart!==cartSnapshot())) {
message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.');
button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();});
return;
}
if (quote.status==='pending_review') {
message('Arquivos enviados. No Kanban, confira metragem e nota e aprove a cotação '+draftId.slice(0,8)+'.');
} else if (quote.status==='approved') {
@@ -93,6 +132,7 @@
return;
}
button('Criar pedido de teste',async event=>{
if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; }
event.target.disabled=true;
try {
const order=await api('/orders/dev-paid',{quote_id:draftId});
@@ -104,18 +144,23 @@
});
} else if (quote.status==='paid') {
message('Pedido #'+quote.order.number+' · etapa: '+quote.order.state+'.');
button('Novo pedido',()=>{localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');localStorage.removeItem('dtf-request-body');location.reload();});
button('Novo pedido',()=>{clearDraft();location.reload();});
} else {
message('Cotação expirada. Envie o carrinho para uma nova revisão.');
button('Nova cotação',()=>{draftId=null;requestKey=null;localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');actions.replaceChildren();});
button('Nova cotação',clearDraft);
}
} catch(error) {
if(version!==refreshVersion || draftId!==shownId) return;
message(error.message);
actions.replaceChildren();
button('Limpar referência e tentar de novo',()=>{draftId=null;requestKey=null;localStorage.removeItem('dtf-quote');localStorage.removeItem('dtf-request-key');actions.replaceChildren();});
button('Limpar referência e tentar de novo',clearDraft);
}
}
const quoteFromPortal=new URLSearchParams(location.search).get('quote');
if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);}
if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){
if(draftId!==quoteFromPortal){quotedCart=null;localStorage.removeItem('dtf-quote-cart');}
draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);
}
window.addEventListener('dtf-cart-changed',()=>{if(draftId) refresh();});
refresh();
})();

View File

@@ -1035,6 +1035,7 @@ footer a:hover{color:var(--laranja2)}
<div class="zona" id="zona" tabindex="0" role="button">
<div class="ico">↑</div>
<b id="zTit">Arraste aqui</b><span id="zSub"></span>
<span id="zLimite">Até 128 MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.</span>
<span class="bt">ou escolher no computador</span>
</div>
<div class="recusa" id="recusa" style="display:none"></div>
@@ -1098,9 +1099,8 @@ footer a:hover{color:var(--laranja2)}
<li class="s"><b>PDF é o que conferimos melhor.</b> Nele medimos a resolução de
cada imagem dentro da folha, uma por uma. Se a sua arte é boa, é onde a nota
aparece com mais precisão</li>
<li class="a"><b>PDF só até 5 m.</b> O limite é do próprio formato: a página do PDF
para em 508 cm. Acima disso o arquivo sai fora do padrão e só abre certo em quem
o gerou — divida em partes ou mande PNG</li>
<li class="a"><b>PDF grande.</b> Sem UserUnit, a página passa do limite de 508 cm
do formato; divida a folha ou mande PNG. Envie um PDF de uma página por folha.</li>
<li class="a"><b>TIFF, PSD, AI e CDR</b> a gente aceita, mas ninguém consegue
conferir sozinho: alguém abre na mão, sai mais devagar e o metro vai pela
tabela, sem desconto de nota</li>

View File

@@ -3,6 +3,8 @@ const $ = id=>document.getElementById(id);
// Remove credentials saved by older local builds. Only HttpOnly sessions now.
sessionStorage.removeItem('dtf-operator');
let board;
let extraQuotes={pending:[],approved:[]};
let moreQuotes={pending:false,approved:false};
const money=cents=>(cents/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
function node(tag,text,className){const e=document.createElement(tag);if(text!==undefined)e.textContent=text;if(className)e.className=className;return e;}
function action(text,fn){const b=node('button',text);b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){$('status').textContent=e.message;}finally{b.disabled=false;}};return b;}
@@ -16,25 +18,67 @@ function files(container,items){
const link=node('a');link.href=result.url;link.referrerPolicy='no-referrer';link.download=result.name;link.click();
}));
}
function productionLines(container,item){
if(!item.production){container.append(node('p','Instruções de produção ausentes; não produzir este item.','meta'));return;}
container.append(node('p','Especificação v'+item.production.version+' · qualidade '+item.quality_status+
(item.quality_status==='warning'?' · ressalva '+(item.quality_acknowledged?'aceita':'não aceita'):''),'meta'));
if(item.production.placements){
container.append(node('p',item.production.placements.length+' peças posicionadas em '+
item.production.film_width_cm+' × '+item.production.height_cm+' cm de filme','meta'));
const download=node('button','Baixar manifesto da montagem');
download.type='button';
download.onclick=()=>{
const url=URL.createObjectURL(new Blob([JSON.stringify(item.production,null,2)],{type:'application/json'}));
const link=node('a');link.href=url;
link.download='dtf-layout-'+item.production.sources[0].upload_id.slice(0,8)+'.json';
link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
};
container.append(download);
}
item.production.sources.forEach((source,index)=>container.append(node('p',
(index+1)+'. '+source.kind+' · '+source.copies+' × '+source.width_cm+' × '+source.length_cm+
' cm · giro '+source.rotation_degrees+'°'+(source.mirrored?' · espelhada':'')+
' · medida '+source.measurement+' · arquivo '+source.upload_id.slice(0,8),'meta')));
}
async function load(){
try{
board=await api('/board');$('login').hidden=true;
board=await api('/board');
extraQuotes={pending:[],approved:[]};
moreQuotes={pending:board.pending_total>board.quotes.filter(q=>!q.approved).length,
approved:board.approved_total>board.quotes.filter(q=>!!q.approved).length};
$('login').hidden=true;
$('status').textContent='Atualizado às '+new Date().toLocaleTimeString();
render();
}catch(e){$('status').textContent=e.message;$('login').hidden=false;}
}
async function loadMoreQuotes(kind){
const shown=[...board.quotes.filter(q=>kind==='pending'?!q.approved:!!q.approved),...extraQuotes[kind]];
const last=shown.at(-1);
const params=new URLSearchParams({kind,limit:'50'});
if(last){params.set('before_created_at',last.created_at);params.set('before_id',last.id);}
const page=await api('/quotes?'+params);
const known=new Set(shown.map(q=>q.id));
extraQuotes[kind].push(...page.quotes.filter(q=>!known.has(q.id)));
moreQuotes[kind]=page.has_more;
render();
}
function render(){
$('reviews').replaceChildren();
if(board.quotes.length)$('reviews').append(node('h2','Cotações · conferência comercial'));
for(const quote of board.quotes){
if(board.pending_total || board.approved_total)
$('reviews').append(node('h2','Cotações · '+board.pending_total+' pendentes · '+
board.approved_total+' aprovadas sem pedido'));
for(const quote of [...board.quotes,...extraQuotes.pending,...extraQuotes.approved]){
const card=node('article',undefined,'review');
card.append(node('b',quote.id.slice(0,8)+' · '+quote.draft.customer.mail));
if(quote.status!=='pending_review'){
if(quote.status==='pending_review' && quote.draft.items.some(item=>item.production?.version!==2)){
card.append(node('p','Cotação com montagem antiga. Peça ao cliente para enviar uma nova cotação.'));
}else if(quote.status!=='pending_review'){
card.append(node('p',quote.status==='expired'?'Cotação expirada. O cliente precisa solicitar outra.':'Aprovada: '+money(quote.approved.total_cents)+'. Volte ao Site, atualize o pedido e confirme o pagamento local.'));
}else{
const form=node('form');
const edits=quote.draft.items.map((item,index)=>{
const row=node('div');row.append(node('strong',(index+1)+'. '+item.mode+' '));
productionLines(row,item);
const metres=node('input');metres.type='number';metres.min='0.001';metres.max='12000';metres.step='any';metres.value=item.metres;metres.required=true;
const grade=node('input');grade.type='number';grade.min='0';grade.max='100';grade.step='1';grade.value=item.grade;grade.required=true;
const ml=node('label','Metros conferidos');ml.append(metres);const gl=node('label','Nota conferida');gl.append(grade);row.append(ml,gl);form.append(row);
@@ -47,6 +91,10 @@ function render(){
}
const downloads=node('div',undefined,'actions');files(downloads,quote.draft.items);card.append(downloads);$('reviews').append(card);
}
for(const [kind,label] of [['pending','Carregar cotações pendentes mais antigas'],
['approved','Carregar cotações aprovadas mais antigas']]){
if(moreQuotes[kind])$('reviews').append(action(label,()=>loadMoreQuotes(kind)));
}
$('kan').replaceChildren();
const colors=['#00b8da','#e0357c','#efb700','#edebe6','#e0642a','#48b072'];
Object.entries(board.states).forEach(([state,title],index)=>{
@@ -60,7 +108,10 @@ function render(){
for(const order of orders){
const card=node('article',undefined,'cd');card.draggable=true;card.dataset.order=order.id;
card.append(node('b','#'+order.number+' · Pago local'),node('p',order.snapshot.customer.mail,'meta'),node('p',money(order.snapshot.total_cents)));
for(const item of order.snapshot.items)card.append(node('p',item.mode+' · '+item.billed_metres+' m · nota '+item.grade,'meta'));
for(const item of order.snapshot.items){
card.append(node('p',item.mode+' · '+item.billed_metres+' m · nota '+item.grade,'meta'));
productionLines(card,item);
}
const actions=node('div',undefined,'actions');files(actions,order.snapshot.items);
const artwork=node('div');
actions.append(action('Arquivos de produção',()=>artworkPanel(order,artwork)));

View File

@@ -4,7 +4,23 @@
scope and run top to bottom, exactly as the original did. */
// ── carrinho
let itemAtual=null;
function invalidaItemAtual(){
if(!itemAtual) return;
itemAtual=null;
$('atual').style.display='none';
$('carrLin').innerHTML='';
pintaPedido();
}
function itemPodeEnviar(item){
return item.qualityStatus==='ok' || item.qualityStatus==='unverified' ||
(item.qualityStatus==='warning' && item.qualityAcknowledged===true);
}
function cartPodeEnviar(){
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
return items.length>0 && items.every(itemPodeEnviar);
}
function limpaPaineis(){
invalidaItemAtual();
['qual','prev'].forEach(id=>$(id).classList.remove('on'));
if(!pedido.length){ $('carr').classList.remove('on'); }
}
@@ -37,6 +53,7 @@ function pintaPedido(){
$('bPagar').textContent = (pedido.length + (itemAtual?1:0))>1
? 'Pagar os '+(pedido.length+(itemAtual?1:0))+' itens de uma vez'
: 'Ir para o pagamento';
$('bPagar').disabled = !cartPodeEnviar() || !entrega.cotado || !clienteOk();
if(pedido.length || itemAtual) $('carr').classList.add('on');
if($('cartLink')) $('cartLink').textContent='Carrinho ('+n+')';
window.dispatchEvent(new Event('dtf-cart-changed'));
@@ -62,8 +79,36 @@ function carrinho(){
'<div class="l" style="color:var(--laranja2)"><span>Revisão e reencaixe</span><b>grátis</b></div>';
$('carrLin').innerHTML=l;
$('atual').style.display='';
const sources=(ehFolha()?folhas:artes).map(x=>ehFolha()
? {kind:'sheet',width_cm:x.med?.larg||larguraFilme(),length_cm:+(x.m*100).toFixed(4),
copies:x.rep||1,rotation_degrees:0,mirrored:false,
measurement:x.med?'file':'customer'}
: {kind:'artwork',width_cm:x.cm,length_cm:+(x.cm*propDe(x)).toFixed(4),
copies:x.q,rotation_degrees:x.giro||0,mirrored:!!x.esp,
measurement:x.src?'file':'customer'});
let layout;
if(ehFolha()){
let y=0;
const placements=[];
sources.forEach((source,source_index)=>{
for(let copy_index=0;copy_index<source.copies;copy_index++){
placements.push({source_index,copy_index,x_cm:0,y_cm:+y.toFixed(4),
width_cm:source.width_cm,length_cm:source.length_cm,
rotation_degrees:0,mirrored:false});
y+=source.length_cm;
}
});
layout={height_cm:+y.toFixed(4),placements};
}else{
if(!montagemLayout) return;
layout=montagemLayout;
}
itemAtual={modo, tit:MODOS[modo].tit, nota, metros, cob, unit, total:tot,
localFiles:(ehFolha()?folhas:artes).map(x=>x.f),
production:{version:2,film_width_cm:larguraFilme(),sources,...layout},
qualityStatus:$('ciente').classList.contains('recusa')?'rejected':
$('ciente').classList.contains('on')?'warning':nota===0?'unverified':'ok',
qualityAcknowledged:false,
desc:fmtM(cob)+' m · '+rs(unit)+'/m · nota '+nota};
pintaPedido();
previa();
@@ -74,4 +119,3 @@ function carrinho(){
}
}
function qm(t,c){ const m=$('qmsg'); m.style.display='block'; m.style.color=c||'var(--verde)'; m.innerHTML=t; }

View File

@@ -69,6 +69,7 @@ const folhaTotalM=()=>folhas.reduce((t,x)=>t+(x.m||0)*(x.rep||1),0);
// Cada modo por metro tem o seu par de artes avulsas, e o seletor anda nos dois sentidos.
const PAR={file:'avulsa', avulsa:'file', uvfile:'uv', uv:'uvfile'};
let montagemCm=0; // altura da folha montada pelo motor, em cm
let montagemLayout=null; // posições aprovadas de cada cópia, em cm
const ZOOMS=[1,1.5,2,3,4]; let zoomI=0; // ampliação da montagem ao vivo
let pedido=[]; // itens já fechados · o pagamento é um só
// A escolha de entrega vale para o pedido inteiro, não por item. No Tiny, retirada
@@ -100,9 +101,7 @@ const precoBase=n=>
(FAIXAS[modo].find(f=>n>=f[0])||FAIXAS[modo][FAIXAS[modo].length-1])[1];
const descontoMetragem=()=>0; // sem progressão por quantidade
const cls=n=>n>=85?'ok':n>=50?'av':'er';
const dpiDe=a=>a.cm>0? a.px/(a.cm/2.54) : 0;
const dpiDe=a=>a.cm>0? (a.giro?a.py:a.px)/(a.cm/2.54) : 0;
// girar 90° inverte a proporção · espelhar não muda medida, só o desenho
const propDe=a=>{ const p=a.prop||1; return a.giro? 1/p : p; };
const TAMANHOS={57:[10,15,20,25,28.2], 28.5:[5,8,10,14,28.2]};
const px=f=>Math.round(Math.min(6000,Math.max(600,Math.sqrt(f.size/1024)*95)));

View File

@@ -87,7 +87,7 @@ function pintaEntrega(){
? 'DTF é impresso depois que você paga. Avisamos no WhatsApp quando estiver '+
'<b>pronto para retirar</b> — não venha antes do aviso.'
: '';
$('bPagar').disabled = !entrega.cotado || !clienteOk();
$('bPagar').disabled = !entrega.cotado || !clienteOk() || !cartPodeEnviar();
pintaPedido();
}
@@ -156,6 +156,10 @@ $('bCep').addEventListener('click',async()=>{
});
$('bMais').addEventListener('click',()=>{
if(itemAtual && !itemPodeEnviar(itemAtual)){
$('qual').scrollIntoView({behavior:'smooth',block:'nearest'});
return;
}
if(itemAtual){ pedido.push(itemAtual); itemAtual=null; }
$('atual').style.display='none'; $('carrLin').innerHTML='';
$('foco').classList.remove('on'); $('cards').style.display='';
@@ -170,4 +174,3 @@ $('bPagar').addEventListener('click',()=>{
else alert('O pedido online está indisponível no momento. Tente novamente em instantes.');
});
pintaEntrega();

View File

@@ -134,6 +134,8 @@ function encaixar(pecas,W){
}
const postas=[];
if(pecas.some(p=>!Number.isFinite(p.w) || p.w<=0 || p.w>W))
throw new RangeError('A largura solicitada não cabe no filme.');
const ordem=[...pecas].sort((a,b)=>(b.w*b.h)-(a.w*a.h));
ordem.forEach(p=>{
@@ -173,10 +175,8 @@ function encaixar(pecas,W){
}
});
if(!melhor){
const k=Math.min(1, W/p.w); // cabe na largura, nem que seja reduzindo
const w=p.w*k, h=p.h*k;
const m=mascara(p.img,w,h,p.a?.giro||0,p.a?.esp);
melhor={cx:0,cy:linhas,m,x:0,y:linhas*CEL,w,h,rot:0,ref:p};
const m=mascara(p.img,p.w,p.h,p.a?.giro||0,p.a?.esp);
melhor={cx:0,cy:linhas,m,x:0,y:linhas*CEL,w:p.w,h:p.h,rot:0,ref:p};
}
// a folga só existe entre peças, não é folha cobrada
ocupar(melhor.m,melhor.cx,melhor.cy);
@@ -190,7 +190,7 @@ function encaixar(pecas,W){
function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
const versao=area.montagemVersao=(area.montagemVersao||0)+1;
const filme=larguraFilme(), PX_CM=W/filme;
const itens=lista.map(a=>({...a}));
const itens=lista.map((a,sourceIndex)=>({...a,sourceIndex}));
if(fora) fora.innerHTML='';
if(!itens.length){
area.innerHTML='<div class="semmont"><b>A montagem aparece aqui</b>'+
@@ -201,7 +201,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(imagens=>{
if(area.montagemVersao!==versao) return;
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},()=>({...c})));
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},(_,copyIndex)=>({...c,copyIndex})));
cargas.forEach(c=>{
c.w = c.a.cm; // o motor trabalha em centímetros
c.h = c.w * (c.img ? (c.a.giro? c.img.width/c.img.height : c.img.height/c.img.width) : 1);
@@ -255,7 +255,12 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
+ (girou? ' · <b style="display:inline">'+girou+
(girou>1?' artes giradas':' arte girada')+' 90°</b> para caber melhor':'');
(fora||area).appendChild(el);
if(aoTerminar) aoTerminar({alturaCm, uso, girou});
if(aoTerminar) aoTerminar({alturaCm, uso, girou,
placements:enc.pos.map(p=>({source_index:p.ref.a.sourceIndex,
copy_index:p.ref.copyIndex,x_cm:p.x,y_cm:p.y,
width_cm:+p.w.toFixed(4),length_cm:+p.h.toFixed(4),
rotation_degrees:(p.rot+(p.ref.a.giro||0))%360,
mirrored:!!p.ref.a.esp}))});
});
}
@@ -265,6 +270,7 @@ function previaArtes(){ desenhaMontagem($('pArea'), artes, metros, 560); }
let tMont=null;
function previaAoVivo(){
clearTimeout(tMont);
montagemLayout=null;
$('vArea').montagemVersao=($('vArea').montagemVersao||0)+1;
if(ehFolha()){
$('vUso').innerHTML='';
@@ -296,11 +302,13 @@ function previaAoVivo(){
return;
}
tMont=setTimeout(()=>{
const prontas=artes.filter(a=>a.cm>0);
const prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
const W=Math.round(300*ZOOMS[zoomI]);
$('vArea').classList.toggle('ampliado', zoomI>0);
desenhaMontagem($('vArea'), prontas, 0, W, $('vUso'), r=>{
montagemCm = r.alturaCm; // altura real da folha montada
montagemLayout=r.placements ? {height_cm:+r.alturaCm.toFixed(4),
placements:r.placements} : null;
$('vMt').textContent = r.alturaCm? fmtM(cobrar(r.alturaCm/100))+' m' : '—';
resumoArtes(); // o resumo lê a mesma folha
agendaAvaliacao(); // a nota só sai com a metragem pronta
@@ -312,12 +320,12 @@ function previaAoVivo(){
function motivoNota(){
if(!nota) return 'sem nota ainda · a conferência roda na sala antes de imprimir';
if(!ehFolha()){
const ruins=artes.filter(a=>a.cm>0 && (a.px/(a.cm/2.54))<300);
const ruins=artes.filter(a=>a.cm>0 && dpiDe(a)<300);
if(!ruins.length) return 'nota '+nota+' · todas as artes em 300 DPI ou mais';
const pior=ruins.reduce((p,a)=>(a.px/(a.cm/2.54))<(p.px/(p.cm/2.54))?a:p);
const pior=ruins.reduce((p,a)=>dpiDe(a)<dpiDe(p)?a:p);
return 'nota '+nota+' · '+ruins.length+(ruins.length>1?' artes abaixo':' arte abaixo')+
' de 300 DPI · a menor é "'+pior.f.name+'" com '+
Math.round(pior.px/(pior.cm/2.54))+' DPI em '+pior.cm.toFixed(1).replace('.',',')+' cm';
Math.round(dpiDe(pior))+' DPI em '+pior.cm.toFixed(1).replace('.',',')+' cm';
}
return nota>=90 ? 'nota '+nota+' · resolução ótima em toda a folha'
: 'nota '+nota+' · a resolução é o único ponto — o resto corrigimos por você';
@@ -350,4 +358,3 @@ function previa(){
catch(e){ $('pArea').innerHTML='<div class="semprev"><b>Prévia indisponível</b>'+
'O arquivo será processado normalmente.</div>'; }
}

View File

@@ -10,9 +10,8 @@
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.
// 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; }
}
@@ -25,10 +24,8 @@ function carregarPdfJs(){
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;
temWorker=testaWorker(PDFJS_WORKER);
window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER;
}catch(e){
temWorker=false;
try{ window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e2){}
@@ -61,7 +58,7 @@ function dpiDasImagens(page, ops){
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});
dpis.push({dpi: im.width/(larguraPt*page.userUnit/72), area: areaPt});
}
}
return dpis;
@@ -88,43 +85,58 @@ function resumoDpi(lista){
}
// 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;
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){
@@ -143,29 +155,40 @@ function medirFolha(file){
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});
});
// 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);
@@ -173,12 +196,13 @@ function medirFolha(file){
}
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+0.5, suspeito=lido && x.med.foraDoPadrao;
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){
@@ -210,6 +234,9 @@ function pintaFolha(){
? ' 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;
@@ -231,7 +258,7 @@ function pintaFolha(){
'<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+
? '<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>'
: '';
@@ -271,6 +298,8 @@ function 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();
}
@@ -323,10 +352,15 @@ function vizPeca(a){
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.cm>0){
const dpi=Math.round(a.px/(a.cm/2.54)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
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>';
@@ -341,7 +375,8 @@ function pintaArtes(){
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>'+
'<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>'+
@@ -358,11 +393,12 @@ function pintaArtes(){
}).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-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.floor(+i.value||1));
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',()=>{
@@ -381,10 +417,10 @@ function pintaArtes(){
previaAoVivo();
}
function resumoArtes(){
const r=$('rA'), prontas=artes.filter(a=>a.cm>0);
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((a.px/(a.cm/2.54))/300*100))));
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';
@@ -397,4 +433,3 @@ function resumoArtes(){
' 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>':'');
}

View File

@@ -8,7 +8,9 @@
function avaliar(){
if(ehFolha()){
if(!folhas.length) return {pronto:false, falta:'Arraste sua folha montada para começar.'};
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme()+0.5);
const pdfInvalido=folhas.find(x=>x.measurementError);
if(pdfInvalido) return {pronto:false, falta:pdfInvalido.f.name+': '+pdfInvalido.measurementError};
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme());
if(larga) return {pronto:false, falta:'A folha '+larga.f.name+' tem '+n1(larga.med.larg)+
' cm e não cabe no filme de '+n1(larguraFilme())+' cm.'};
const semMedida=folhas.filter(x=>!(x.m>0)).length;
@@ -17,12 +19,14 @@ function avaliar(){
metros=folhaTotalM();
reencaixado=false;
const ans=folhas.map(x=>x.an).filter(Boolean);
if(!ans.length){
// sem análise possível (PDF, CDR, PSD): não inventa nota
const exts=[...new Set(folhas.map(f=>extDe(f.f.name)))].join(', ');
if(ans.length!==folhas.length){
// A nota precisa cobrir cada folha faturada. Uma folha sem análise não
// herda a nota/desconto de outra folha que o navegador conseguiu abrir.
const ausentes=folhas.filter(f=>!f.an);
const exts=[...new Set(ausentes.map(f=>extDe(f.f.name)))].join(', ');
mostraNota([
['av','Sem nota · '+exts+' não abre no navegador',
'não temos como conferir a arte antes de imprimir'],
['av','Sem nota · '+exts+' sem análise completa',
ausentes.length+' de '+folhas.length+' folha(s) precisam de conferência manual'],
['er','Metro pela tabela · '+rs(TABELA[modo]),
'exportando em PNG ou PDF, cai até '+rs(pisoEscada())],
['fix','Alguém abre seu arquivo','a conferência é feita na mão, na sala']
@@ -58,13 +62,18 @@ function avaliar(){
return {pronto:true};
}
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'};
if(artes.some(a=>a.decodeError || !a.src || !(a.px>0) || !(a.py>0)))
return {pronto:false, falta:'Não conseguimos ler uma das imagens. Exporte-a novamente em PNG ou JPG antes de continuar.'};
if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme()))
return {pronto:false, falta:'A largura da arte precisa caber no filme de '+
n1(larguraFilme())+' cm. Reduza a medida solicitada antes de continuar.'};
const sem=artes.filter(a=>!a.cm).length;
if(sem) return {pronto:false, falta:'Informe a largura de '+sem+' arte'+(sem>1?'s':'')+
' para ver a nota e o preço.'};
// metragem = altura da folha depois de montada, com os 5 mm entre peças
metros=+(montagemCm/100).toFixed(4);
if(!(metros>0)) return {pronto:false, falta:'Montando a folha…'};
const notas=artes.map(a=>Math.max(6,Math.min(100,Math.round((a.px/(a.cm/2.54))/300*100))));
const notas=artes.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);
mostraNota([
['ok','Montagem por nossa conta','5 mm garantidos'],
@@ -182,5 +191,8 @@ function ressalvaResolucao(){
$('cienteOk').addEventListener('change',e=>{
if($('ciente').classList.contains('recusa')) return; // recusa não se aceita
$('qOk').disabled=!e.target.checked;
if(itemAtual && itemAtual.qualityStatus==='warning'){
itemAtual.qualityAcknowledged=e.target.checked;
pintaEntrega();
}
});

View File

@@ -37,6 +37,16 @@ function sel(fs){
const fora = fs.filter(f=>!regra.test(f.name));
fs = fs.filter(f=>regra.test(f.name));
recusa(fora);
const max=window.dtfUploadMaxBytes||128*1048576;
const grandes=fs.filter(f=>f.size>max);
if(grandes.length){
const el=$('recusa');
el.style.display='block';
el.innerHTML='<b>Arquivo acima do limite de '+(max/1048576).toFixed(0)+
' MB.</b> A verificação de segurança ainda não consegue liberar arquivos maiores. '+
'Divida ou compacte a arte antes de enviar.';
fs=fs.filter(f=>f.size<=max);
}
if(!fs.length) return;
if(ehFolha()){
const auto=fs.filter(f=>AUTO.test(f.name)).length;
@@ -60,6 +70,9 @@ function sel(fs){
novas.forEach(x=>{
x.pct=5; pintaFolha();
medirFolha(x.f).then(md=>{
if(md && md.rejected){
x.measurementError=md.reason; x.pct=null; pintaFolha(); return;
}
if(md && md.dpiFolha!=null && md.dpiFolha<DPI_RECUSA) return recusaFolha(x, md);
x.med=md; if(md) x.m=md.alt/100; x.pct=55; pintaFolha();
if(md && RENDERIZA.test(x.f.name)){
@@ -93,7 +106,7 @@ function sel(fs){
});
return;
}
const novos=fs.map(f=>({f,px:px(f),cm:0,q:1,prop:1,giro:0,esp:false}));
const novos=fs.map(f=>({f,px:0,py:0,cm:0,q:1,prop:1,giro:0,esp:false}));
artes=novos.concat(artes); // a mais recente fica no topo da fila
recemChegada=novos[0];
pintaArtes();
@@ -102,11 +115,12 @@ function sel(fs){
}
function medir(a){
return carregarImagem(a.f).then(img=>{
if(img){ a.prop=img.height/img.width; a.px=img.width; a.src=img.src; } else a.prop=a.prop||1;
if(img){ a.prop=img.height/img.width; a.px=img.width; a.py=img.height; a.src=img.src; }
else a.decodeError=true;
return a;
});
}
// A metragem sai do arquivo, nunca do peso em bytes.
// imagem → proporção da imagem aplicada à largura do filme
// PDF → MediaBox da primeira página, em pontos (1 pt = 1/72 pol)
// PDF → página única lida pelo PDF.js, com boxes, rotação e UserUnit
// TIFF, PSD, AI, CDR → o navegador não abre; o cliente informa o comprimento