Reduzindo para '+n1(sug.alvo)+' cm você economiza '+
@@ -2051,7 +2067,13 @@ 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=+i.value||1;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();
}));
@@ -2348,7 +2370,7 @@ function dilatar(bruto,cw,ch,cheias){
}
return {cw:cw2,ch:ch2,bits:out,cheias}; // cheias = área de arte, sem a folga
}
-function mascara(img,w,h,rot){
+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;
@@ -2359,8 +2381,11 @@ function mascara(img,w,h,rot){
return dilatar(bruto,cw,ch,cheias);
}
ctx.save();
- if(rot===90){ ctx.translate(cw,0); ctx.rotate(Math.PI/2); ctx.drawImage(img,0,0,ch,cw); }
- else ctx.drawImage(img,0,0,cw,ch);
+ 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;
@@ -2406,7 +2431,8 @@ function encaixar(pecas,W){
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);
+ 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));
@@ -2436,7 +2462,7 @@ 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,0);
+ 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
@@ -2449,8 +2475,9 @@ function encaixar(pecas,W){
}
function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
- const PX_CM=W/larguraFilme();
- const itens=[]; lista.forEach(a=>{ for(let k=0;k<(a.q||1);k++) itens.push(a); });
+ 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='A montagem aparece aqui'+
@@ -2459,13 +2486,15 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
return;
}
- Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(cargas=>{
+ 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,larguraFilme());
+ 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;
@@ -2483,7 +2512,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
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);
- if(giro%180===90) ctx.rotate(Math.PI/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);
@@ -2494,7 +2523,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
ctx.fillText(c.a.f.name.split('.').pop().toUpperCase(),p.x+p.w/2,p.y+p.h/2+4);
}
});
- molduraFolha(ctx,W,usado,larguraFilme(),alturaCm/100);
+ molduraFolha(ctx,W,usado,filme,alturaCm/100);
marcaDagua(ctx,W,cv.height);
area.innerHTML=''; area.appendChild(cv);
@@ -2503,7 +2532,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
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/(larguraFilme()*Math.max(alturaCm,0.1))*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
@@ -2522,6 +2551,8 @@ 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();
@@ -2551,7 +2582,6 @@ function previaAoVivo(){
}
return;
}
- clearTimeout(tMont);
tMont=setTimeout(()=>{
const prontas=artes.filter(a=>a.cm>0);
const W=Math.round(300*ZOOMS[zoomI]);
diff --git a/local/artwork_browser_test.mjs b/local/artwork_browser_test.mjs
new file mode 100644
index 0000000..f23af6d
--- /dev/null
+++ b/local/artwork_browser_test.mjs
@@ -0,0 +1,187 @@
+// Isolated upload/editor regression: no API, database, storage, or Docker services.
+// Run: node local/artwork_browser_test.mjs
+// Optional: ARTWORK_FILE=/absolute/path/to/image.jpg to also inspect a real image.
+import assert from 'node:assert/strict';
+import {spawn} from 'node:child_process';
+import {createServer} from 'node:http';
+import {createHash} from 'node:crypto';
+import {mkdtemp, readFile, writeFile, mkdir} from 'node:fs/promises';
+import {tmpdir} from 'node:os';
+import {resolve} from 'node:path';
+
+const html=await readFile('dtf-site.html','utf8');
+const hashes=[...html.matchAll(/