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>
354 lines
16 KiB
JavaScript
354 lines
16 KiB
JavaScript
/* 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>'; }
|
||
}
|
||
|