/* Site DTF — Masks, the nesting engine, and drawing the assembled film. 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. */ // ── 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); }); } // The image each artwork was measured with, loaded once and reused on every // repaint: the packing used to read and decode the whole file again each // time. Kept outside the artwork, which is saved with the cart. const imagensDasArtes=new WeakMap(); function imagemDaArte(a){ if(!a.src) return Promise.resolve(null); const guardada=imagensDasArtes.get(a); if(guardada && guardada.src===a.src) return guardada.img; const img=new Promise(res=>{ const im=new Image(); im.onload=()=>res(im); im.onerror=()=>res(null); im.src=a.src; }); imagensDasArtes.set(a,{src:a.src,img}); return img; } // 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;y16 ? 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(linhascols) return true; for(let y=0;y=linhas) continue; // vazio abaixo do que já existe const L=mapa[gy], off=y*m.cw; for(let x=0;x!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=>{ 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.yp.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,sourceIndex)=>({...a,sourceIndex})); if(fora) fora.innerHTML=''; if(!itens.length){ area.innerHTML='
A montagem aparece aqui'+ 'informe a largura de cada arte e a folha se desenha sozinha
'; if(aoTerminar) aoTerminar({alturaCm:0, uso:0, girou:0}); return; } Promise.all(itens.map(a=>imagemDaArte(a).then(img=>({a,img})))).then(imagens=>{ if(area.montagemVersao!==versao) return; 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 // The file's own proportion: a large artwork is drawn from a smaller copy. const prop = c.a.prop>0 ? c.a.prop : c.img ? c.img.height/c.img.width : 1; c.h = c.w * (c.a.giro? 1/prop : prop); 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 ? ''+uso+'% da folha virou arte bom aproveitamento' : ''+uso+'% da folha virou arte dá para melhorar: artes de larguras parecidas '+ 'se encaixam melhor lado a lado') + (girou? ' · '+girou+ (girou>1?' artes giradas':' arte girada')+' 90° para caber melhor':''); (fora||area).appendChild(el); 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}))}); }); } 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); montagemLayout=null; $('vArea').montagemVersao=($('vArea').montagemVersao||0)+1; if(!ehFolha()) $('bAmpliar').hidden=true; if(ehFolha()){ $('vUso').innerHTML=''; const tot=folhaTotalM(); $('vMt').textContent = tot>0 ? fmtM(cobrar(tot))+' m' : '—'; const medidas=folhas.filter(x=>x.med); // Sheets still being read show a placeholder; a sheet printed N times is // shown N times (up to REPETE_MAX, then a count). const REPETE_MAX=6; const previews=folhas.filter(x=>x.previewSrc || (x.pct!=null && x.med && x.med.grande)); if(previews.length){ const painel=document.createElement('div'); painel.className='folhaPrevias'; previews.forEach((x,index)=>{ const figura=document.createElement('figure'); figura.className='folhaPrevia'; const legenda=document.createElement('figcaption'); const vezes=Math.max(1,x.rep||1); legenda.textContent=(previews.length>1 ? (index+1)+'. ' : '')+x.f.name+ (x.med ? ' · '+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm' : '')+(vezes>1 ? ' · '+vezes+' cópias' : ''); if(!x.previewSrc){ const esqueleto=document.createElement('div'); esqueleto.className='skel folhaSkel'; esqueleto.style.aspectRatio=x.med ? x.med.larg+' / '+Math.min(x.med.alt,x.med.larg*4) : '1 / 2'; esqueleto.append(Object.assign(document.createElement('span'),{textContent:'Gerando a prévia da folha…'})); figura.append(esqueleto,legenda); painel.append(figura); return; } const pilha=document.createElement('div'); pilha.className='folhaPilha'; for(let k=0;kREPETE_MAX) pilha.append(Object.assign(document.createElement('div'), {className:'folhaMais', textContent:'+ '+(vezes-REPETE_MAX)+' cópia'+(vezes-REPETE_MAX>1?'s':'')})); figura.append(pilha,legenda); painel.append(figura); }); $('vArea').replaceChildren(painel); $('bAmpliar').hidden=false; }else{ $('bAmpliar').hidden=true; $('vArea').innerHTML = medidas.length ? '
'+(medidas.length>1 ? medidas.length+' folhas · '+fmtM(tot)+' m' : n1(medidas[0].med.larg)+' × '+n1(medidas[0].med.alt)+' cm')+''+ ''+(medidas.length>1? 'medidas lidas de cada arquivo' : medidas[0].med.fonte)+ ' · vão para a máquina do jeito que você montou
' : '
Sua folha aparece aqui'+ 'a medida é lida do próprio arquivo, não do peso dele
'; } return; } tMont=setTimeout(()=>{ // o produto pode ter sido fechado nesses 140 ms (item foi para o carrinho) if(!modo) return; const prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && (a.src||a.semPrevia)); 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 }); $('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 && dpiDe(a)<300); if(!ruins.length) return 'nota '+nota+' · todas as artes em 300 DPI ou mais'; const pior=ruins.reduce((p,a)=>dpiDe(a)1?' artes abaixo':' arte abaixo')+ ' de 300 DPI · a menor é "'+pior.f.name+'" com '+ 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ê'; } 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)+' m'; $('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=> '
'+l[1]+''+ '
'+escapeHTML(l[2])+''+escapeHTML(l[3])+'
').join(''); $('prev').classList.add('on'); $('pArea').innerHTML='
montando a prévia…
'; try{ previaArtes(); } catch(e){ $('pArea').innerHTML='
Prévia indisponível'+ 'O arquivo será processado normalmente.
'; } }