Files
dtf-system/local/static/site-packing.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

354 lines
16 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 — Masks, the nesting engine, and drawing the assembled film.
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. */
// ── imagens e prévia
function carregarImagem(file){
return new Promise(res=>{
if(!RENDERIZA.test(file.name)) return res(null);
const fr=new FileReader();
fr.onload=()=>{ const im=new Image(); im.onload=()=>res(im); im.onerror=()=>res(null); im.src=fr.result; };
fr.onerror=()=>res(null);
fr.readAsDataURL(file);
});
}
// A folha precisa parecer uma folha: borda, largura marcada e régua de metros.
// Sem isso o cliente não tem noção de escala e acha que está mal encaixado.
function molduraFolha(ctx,W,H,larguraCm,metrosReais){
const REG=34; // faixa da régua, à direita
ctx.save();
// fundo da régua
ctx.fillStyle='#FAFAF8'; ctx.fillRect(W,0,REG,H);
ctx.strokeStyle='#D5DAE3'; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(W+.5,0); ctx.lineTo(W+.5,H); ctx.stroke();
// marcas a cada 10 cm, número a cada metro
const pxPorM = H/Math.max(metrosReais,0.01);
ctx.font='9px "Roboto", sans-serif'; ctx.textAlign='left';
for(let cm=0; cm<=metrosReais*100; cm+=10){
const y=cm/100*pxPorM;
if(y>H) break;
const metro = cm%100===0;
ctx.strokeStyle = metro? '#B4BAC5' : '#E1E5EC';
ctx.beginPath(); ctx.moveTo(W+1,y+.5); ctx.lineTo(W+(metro?12:6),y+.5); ctx.stroke();
if(metro && cm>0){
ctx.fillStyle='#8A8F98';
ctx.fillText((cm/100)+' m', W+14, y+3);
}
}
// borda da folha
ctx.strokeStyle='#B4BAC5'; ctx.lineWidth=1;
ctx.strokeRect(.5,.5,W-1,H-1);
// cota da largura, no topo
ctx.fillStyle='#FFFFFFDD'; ctx.fillRect(0,0,W,18);
ctx.strokeStyle='#8A8F98'; ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(8,9); ctx.lineTo(W-8,9);
ctx.moveTo(8,5); ctx.lineTo(8,13);
ctx.moveTo(W-8,5); ctx.lineTo(W-8,13);
ctx.stroke();
const t=String(larguraCm).replace('.',',')+' cm';
ctx.font='600 10px "Roboto", sans-serif'; ctx.textAlign='center';
const tw=ctx.measureText(t).width;
ctx.fillStyle='#FAFAF8'; ctx.fillRect(W/2-tw/2-6,2,tw+12,14);
ctx.fillStyle='#5A6570'; ctx.fillText(t,W/2,13);
ctx.restore();
}
const LOGO_URL='https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286';
const logoImg=new Image(); logoImg.crossOrigin='anonymous'; logoImg.src=LOGO_URL;
function marcaDagua(ctx,w,h){
ctx.save(); ctx.globalAlpha=.14;
const passo=Math.max(90,w/2.1);
ctx.translate(w/2,h/2); ctx.rotate(-Math.PI/7); ctx.translate(-w/2,-h/2);
if(logoImg.complete && logoImg.naturalWidth){
const lw=Math.max(70,w/3.4), lh=lw*(logoImg.naturalHeight/logoImg.naturalWidth);
for(let y=-h;y<h*2;y+=passo) for(let x=-w;x<w*2;x+=passo*1.7)
ctx.drawImage(logoImg,x,y,lw,lh);
}else{
ctx.fillStyle='#111';
ctx.font='700 '+Math.max(13,Math.round(w/13))+'px Inter, sans-serif'; ctx.textAlign='center';
for(let y=-h;y<h*2;y+=passo) for(let x=-w;x<w*2;x+=passo*1.5) ctx.fillText('PRÉVIA',x,y);
}
ctx.restore();
}
const CEL=GAP_CM; // célula da máscara, em cm = a folga de 5 mm
// Dilata 1 célula para a direita e para baixo. Como toda peça é dilatada igual,
// duas vizinhas ficam separadas por exatamente 1 célula — os 5 mm da regra.
function dilatar(bruto,cw,ch,cheias){
const cw2=cw+1, ch2=ch+1;
const out=new Uint8Array(cw2*ch2);
for(let y=0;y<ch;y++) for(let x=0;x<cw;x++){
if(!bruto[y*cw+x]) continue;
out[y*cw2+x]=1; out[y*cw2+x+1]=1; out[(y+1)*cw2+x]=1; out[(y+1)*cw2+x+1]=1;
}
return {cw:cw2,ch:ch2,bits:out,cheias}; // cheias = área de arte, sem a folga
}
function mascara(img,w,h,rot,esp=false){
const cw=Math.max(1,Math.ceil(w/CEL)), ch=Math.max(1,Math.ceil(h/CEL));
const cv=document.createElement('canvas');
cv.width=cw; cv.height=ch;
const ctx=cv.getContext('2d',{willReadFrequently:true});
let bruto, cheias;
if(!img){ // sem imagem: caixa cheia
bruto=new Uint8Array(cw*ch).fill(1); cheias=cw*ch;
return dilatar(bruto,cw,ch,cheias);
}
ctx.save();
ctx.translate(cw/2,ch/2);
ctx.rotate(rot*Math.PI/180);
if(esp) ctx.scale(-1,1);
const dw=rot%180===90?ch:cw, dh=rot%180===90?cw:ch;
ctx.drawImage(img,-dw/2,-dh/2,dw,dh);
ctx.restore();
const d=ctx.getImageData(0,0,cw,ch).data;
bruto=new Uint8Array(cw*ch); cheias=0;
for(let i=0,j=3;i<bruto.length;i++,j+=4){ bruto[i] = d[j]>16 ? 1 : 0; cheias+=bruto[i]; }
return dilatar(bruto,cw,ch,cheias);
}
// Encaixe pela forma, como o motor do servidor: mapa de ocupação real,
// máscara por arte e candidatos numa grade ao longo da largura.
// pecas em centímetros: {w,h,img}. W é a largura do filme em cm.
function encaixar(pecas,W){
const cols=Math.ceil(W/CEL)+1; // +1 = a coluna de folga da dilatação
let linhas=0; const mapa=[];
function garantir(ate){ while(linhas<ate){ mapa.push(new Uint8Array(cols)); linhas++; } }
function colide(m,cx,cy){
if(cx<0||cy<0||cx+m.cw>cols) return true;
for(let y=0;y<m.ch;y++){
const gy=cy+y;
if(gy>=linhas) continue; // vazio abaixo do que já existe
const L=mapa[gy], off=y*m.cw;
for(let x=0;x<m.cw;x++)
if(m.bits[off+x] && L[cx+x]) return true;
}
return false;
}
function ocupar(m,cx,cy){
garantir(cy+m.ch);
for(let y=0;y<m.ch;y++){
const L=mapa[cy+y], off=y*m.cw;
for(let x=0;x<m.cw;x++) if(m.bits[off+x]) L[cx+x]=1;
}
}
const postas=[];
const ordem=[...pecas].sort((a,b)=>(b.w*b.h)-(a.w*a.h));
ordem.forEach(p=>{
const oris=[{w:p.w,h:p.h,rot:0}];
if(!p.mPronta && !p.travado && Math.abs(p.w-p.h)>1 && p.h<=W) oris.push({w:p.h,h:p.w,rot:90});
let melhor=null;
oris.forEach(o=>{
if(o.w>W) return;
// a máscara segue a medida da orientação escolhida · girada, largura e altura trocam
const m = (p.mPronta && o.rot===0) ? p.mPronta : mascara(p.img, o.w, o.h,
o.rot+(p.a?.giro||0), p.a?.esp);
const xs=new Set([0]), ys=new Set([0]);
postas.forEach(q=>{ xs.add(q.cx+q.m.cw); xs.add(q.cx); ys.add(q.cy+q.m.ch); ys.add(q.cy); });
const passoX=Math.max(1,Math.round(cols/34));
for(let x=0;x+m.cw<=cols;x+=passoX) xs.add(x);
xs.add(cols-m.cw);
const passoY=Math.max(1,Math.round(Math.max(linhas,1)/50));
for(let y=0;y<=linhas;y+=passoY) ys.add(y);
const XS=[...xs].filter(x=>x>=0 && x+m.cw<=cols && x*CEL+o.w<=W+0.01).sort((a,b)=>a-b);
const YS=[...ys].filter(y=>y>=0).sort((a,b)=>a-b);
for(const cy of YS){
if(melhor && cy*CEL > melhor.y + CEL) break;
for(const cx of XS){
if(cx*CEL + o.w > W + 0.01) continue; // não deixa a peça sair do filme
if(colide(m,cx,cy)) continue;
const cand={cx,cy,m,x:cx*CEL,y:cy*CEL,w:o.w,h:o.h,rot:o.rot,ref:p};
// menor y, depois menor x · em empate, mantém a orientação original.
// Desempatar por largura gira tudo, e texto deitado na prévia parece erro.
const ganha = !melhor || cand.y<melhor.y ||
(cand.y===melhor.y && cand.x<melhor.x) ||
(cand.y===melhor.y && cand.x===melhor.x && cand.rot===0 && melhor.rot!==0);
if(ganha) melhor=cand;
break;
}
}
});
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};
}
// a folga só existe entre peças, não é folha cobrada
ocupar(melhor.m,melhor.cx,melhor.cy);
postas.push(melhor);
});
const altura=Math.max(...postas.map(p=>p.y+p.h),1);
return {pos:postas, altura};
}
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}));
if(fora) fora.innerHTML='';
if(!itens.length){
area.innerHTML='<div class="semmont"><b>A montagem aparece aqui</b>'+
'<span>informe a largura de cada arte e a folha se desenha sozinha</span></div>';
if(aoTerminar) aoTerminar({alturaCm:0, uso:0, girou:0});
return;
}
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})));
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);
c.travado = !!c.a.giro; // giro do cliente manda no encaixe
});
const enc=encaixar(cargas,filme);
const alturaCm=enc.altura;
const pos=enc.pos.map(p=>({...p, x:p.x*PX_CM, y:p.y*PX_CM, w:p.w*PX_CM, h:p.h*PX_CM}));
const altura=alturaCm*PX_CM;
const usado=Math.max(altura,10);
const REG=34;
const cv=document.createElement('canvas');
cv.width=W+REG; cv.height=Math.round(usado);
const ctx=cv.getContext('2d');
ctx.fillStyle='#fff'; ctx.fillRect(0,0,cv.width,cv.height);
const pal=['#C3CEDD','#B7A9C8','#A9B7C8','#A8BFA9','#C8C0A0','#CBB6B6'];
let girou=0;
pos.forEach((p,j)=>{
const c=p.ref;
if(p.rot===90) girou++;
ctx.save();
const giro = (p.rot===90?90:0) + (c.a && c.a.giro? 90:0);
ctx.translate(p.x+p.w/2, p.y+p.h/2);
ctx.rotate(giro*Math.PI/180);
if(c.a && c.a.esp) ctx.scale(-1,1);
const dw = giro%180===90 ? p.h : p.w, dh = giro%180===90 ? p.w : p.h;
if(c.img) ctx.drawImage(c.img,-dw/2,-dh/2,dw,dh);
else{ ctx.fillStyle=pal[j%pal.length]; ctx.fillRect(-dw/2,-dh/2,dw,dh); }
ctx.restore();
if(!c.img){
ctx.fillStyle='#5A6570'; ctx.font='11px "Roboto", sans-serif'; ctx.textAlign='center';
ctx.fillText(c.a.f.name.split('.').pop().toUpperCase(),p.x+p.w/2,p.y+p.h/2+4);
}
});
molduraFolha(ctx,W,usado,filme,alturaCm/100);
marcaDagua(ctx,W,cv.height);
area.innerHTML=''; area.appendChild(cv);
// O que virou arte é a área pintada, não a soma das caixas: com encaixe por forma
// as caixas se sobrepõem de propósito e a soma delas passa de 100%.
const areaArteCm=enc.pos.reduce((s,p)=>
s + (p.m && p.m.cheias!=null ? p.m.cheias*CEL*CEL : p.w*p.h), 0);
const uso=Math.max(1,Math.min(100,
Math.round(areaArteCm/(filme*Math.max(alturaCm,0.1))*100)));
const el=document.createElement('div');
el.className = uso>=70? 'usoBom' : 'usoRuim';
el.innerHTML = (uso>=70
? '<b>'+uso+'% da folha virou arte</b> bom aproveitamento'
: '<b>'+uso+'% da folha virou arte</b> dá para melhorar: artes de larguras parecidas '+
'se encaixam melhor lado a lado')
+ (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});
});
}
function previaArtes(){ desenhaMontagem($('pArea'), artes, metros, 560); }
// montagem ao vivo, ao lado da fila · redesenha 140 ms depois da ultima mexida
let tMont=null;
function previaAoVivo(){
clearTimeout(tMont);
$('vArea').montagemVersao=($('vArea').montagemVersao||0)+1;
if(ehFolha()){
$('vUso').innerHTML='';
const tot=folhaTotalM();
$('vMt').textContent = tot>0 ? fmtM(cobrar(tot))+' m' : '—';
const medidas=folhas.filter(x=>x.med);
const previews=folhas.filter(x=>x.previewSrc);
if(previews.length){
const painel=document.createElement('div'); painel.className='folhaPrevias';
previews.forEach((x,index)=>{
const figura=document.createElement('figure'); figura.className='folhaPrevia';
const imagem=new Image(); imagem.src=x.previewSrc; imagem.alt=x.f.name;
const legenda=document.createElement('figcaption');
legenda.textContent=(previews.length>1 ? (index+1)+'. ' : '')+x.f.name+
(x.med ? ' · '+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm' : '');
figura.append(imagem,legenda); painel.append(figura);
});
$('vArea').replaceChildren(painel);
}else{
$('vArea').innerHTML = medidas.length
? '<div class="semmont"><b>'+(medidas.length>1
? medidas.length+' folhas · '+fmtM(tot)+' m'
: n1(medidas[0].med.larg)+' × '+n1(medidas[0].med.alt)+' cm')+'</b>'+
'<span>'+(medidas.length>1? 'medidas lidas de cada arquivo' : medidas[0].med.fonte)+
' · vão para a máquina do jeito que você montou</span></div>'
: '<div class="semmont"><b>Sua folha aparece aqui</b>'+
'<span>a medida é lida do próprio arquivo, não do peso dele</span></div>';
}
return;
}
tMont=setTimeout(()=>{
const prontas=artes.filter(a=>a.cm>0);
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
$('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
});
$('vArea').scrollTop=0;
},140);
}
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);
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);
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';
}
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ê';
}
function previa(){
// Na folha pronta o painel não tem o que mostrar: a folha vem fechada, ninguém
// olhou dentro dela. Largura e metragem — o que de fato medimos — já aparecem
// na coluna ao lado e no carrinho.
if(ehFolha()){ $('prev').classList.remove('on'); return; }
const cob=cobrar(metros), sobra=+(cob-metros).toFixed(2);
$('pMt').innerHTML=fmtM(cob)+'<small> m</small>';
$('pSub').textContent='folha montada por nós com as artes que você enviou';
const linhas=[
['var(--verde)','✓','Largura',larguraFilme()+' cm · dentro do padrão da máquina'],
['var(--verde)','✓','Espaço entre artes','5 mm garantidos'],
[nota>=75?'var(--verde)':'var(--laranja)', nota>=75?'✓':'!','Qualidade da folha',motivoNota()],
['var(--verde)','✓','Metragem proporcional',
'sua folha tem '+fmtM(metros)+' m e você paga '+fmtM(cob)+
' m · arredondamos só até os 10 cm mais próximos'],
];
linhas.push(['var(--verde)','+','Identificação do pedido',
'aplicamos seu nome e o número do pedido no fim da folha · por nossa conta']);
$('pInfo').innerHTML=linhas.map(l=>
'<div class="li"><span class="c" style="background:'+l[0]+
(l[0]==='var(--laranja)'?';color:#241703':'')+'">'+l[1]+'</span>'+
'<div><b>'+escapeHTML(l[2])+'</b><span>'+escapeHTML(l[3])+'</span></div></div>').join('');
$('prev').classList.add('on');
$('pArea').innerHTML='<div class="carregando">montando a prévia…</div>';
try{ previaArtes(); }
catch(e){ $('pArea').innerHTML='<div class="semprev"><b>Prévia indisponível</b>'+
'O arquivo será processado normalmente.</div>'; }
}