<tmf-viewer>: clamp fit*8, viewer-error, etichette autonome, pagina di prova
Refs #122 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,9 +30,9 @@
|
||||
var f = Math.min(boxW / w, boxH / h);
|
||||
return isFinite(f) && f > 0 ? f : 0;
|
||||
},
|
||||
// zoom ristretto a [fit * 0.5, max(8, fit)] (come #115)
|
||||
// zoom ristretto a [fit * 0.5, max(8, fit * 8)]: il massimo segue il fit, cosi' un DXF in mm (fit ~10) si ingrandisce ancora
|
||||
clampZoom: function (z, fit) {
|
||||
var lo = fit * 0.5, hi = Math.max(8, fit);
|
||||
var lo = fit * 0.5, hi = Math.max(8, fit * 8);
|
||||
return Math.min(hi, Math.max(lo, z));
|
||||
},
|
||||
// nuovo scroll che tiene fermo il punto (px, py) del viewport passando dallo zoom z0 a z1
|
||||
@@ -54,8 +54,10 @@
|
||||
'tmf-viewer .tmfv-stage>svg:not(.tmfv-ov){display:block;width:100%;height:100%}',
|
||||
'tmf-viewer .tmfv-ov{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}',
|
||||
'tmf-viewer .tmfv-labels{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}',
|
||||
'tmf-viewer .tmfv-label{position:absolute;transform:translate(8px,-50%);white-space:nowrap;font:600 12px/1.3 system-ui,sans-serif;',
|
||||
'color:#fff;background:rgba(0,0,0,.6);padding:1px 5px;border-radius:3px;pointer-events:none}',
|
||||
// #122: aspetto completo dell'etichetta; le classi `cls` della pagina (es. bg-red-600 text-white) cambiano solo i colori
|
||||
'tmf-viewer .tmfv-label{position:absolute;transform:translate(8px,-50%);white-space:nowrap;font:700 12px/1.3 system-ui,sans-serif;',
|
||||
'padding:2px 6px;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.4);pointer-events:none}',
|
||||
':where(tmf-viewer .tmfv-label){color:#fff;background:rgba(0,0,0,.6)}',
|
||||
'tmf-viewer .tmfv-bar{position:absolute;top:6px;right:6px;z-index:3;display:flex;align-items:center;gap:2px;padding:2px;',
|
||||
'background:rgba(0,0,0,.55);color:#fff;border-radius:6px;font:12px/1.4 system-ui,sans-serif}',
|
||||
'tmf-viewer .tmfv-bar button{appearance:none;border:0;border-radius:4px;background:transparent;color:#fff;',
|
||||
@@ -135,6 +137,11 @@
|
||||
this._bFit.onclick = function () { self.fit(); };
|
||||
this._bSave.onclick = function () { self._save(); };
|
||||
img.addEventListener('load', function () { self._onLoad(); });
|
||||
// #122: errore di caricamento -> evento viewer-error (bubbling) con l'URL che non si e' caricato
|
||||
img.addEventListener('error', function () {
|
||||
if (!self._hasSrc()) return; // l'errore di un src appena tolto non conta
|
||||
self.dispatchEvent(new CustomEvent('viewer-error', { bubbles: true, detail: { src: self.getAttribute('src') } }));
|
||||
});
|
||||
// Ctrl+rotella = zoom intorno al puntatore; senza Ctrl la pagina scorre come sempre
|
||||
vp.addEventListener('wheel', function (ev) { self._wheel(ev); }, { passive: false });
|
||||
vp.addEventListener('pointerdown', function (ev) { self._panDown(ev); });
|
||||
@@ -143,7 +150,12 @@
|
||||
vp.addEventListener('pointercancel', function () { self._panEnd(); });
|
||||
// resize del viewport: in stato «adatta» rifa Adatta; altrimenti aggiorna solo il fit
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this._ro = new ResizeObserver(function () { self._onResize(); });
|
||||
// #122: la callback cambia l'altezza del viewport (fit-height): rimando a un task dopo, senza «ResizeObserver loop»
|
||||
this._ro = new ResizeObserver(function () {
|
||||
if (self._raf) return;
|
||||
var run = function () { self._raf = 0; self._onResize(); };
|
||||
self._raf = setTimeout(run, 0);
|
||||
});
|
||||
this._ro.observe(vp);
|
||||
}
|
||||
// contenuto aggiunto dopo (x-html, template): lo adotto
|
||||
@@ -247,6 +259,7 @@
|
||||
if (this._content) this._content.style.display = this._hasSrc() ? 'none' : '';
|
||||
if (!w) { this._z = 0; this._fit = 0; this._auto = true; this._apply(); return; }
|
||||
if (changed || this._auto || !this._z) this.fit(); else this._apply();
|
||||
if (!this._z) this._apply(true); // riquadro nascosto: Adatta rimandata, ma overlay ed etichette (in %) si disegnano gia'
|
||||
}
|
||||
|
||||
// #122: area utile del viewport: offset meno i bordi (client* escludono le barre che spariranno dopo l'Adatta)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<!doctype html>
|
||||
<!-- #122: pagina di prova di <tmf-viewer> da aprire in Chrome (file:// va bene): immagine generata col canvas,
|
||||
overlay vs-tools finto, etichette e modo contenuto (svg). Il riquadro #log riporta i controlli automatici. -->
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Prova tmf-viewer</title>
|
||||
<style>
|
||||
body { font: 14px system-ui, sans-serif; margin: 16px; background: #f3f4f6; }
|
||||
.griglia { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.box { height: 320px; }
|
||||
h2 { font-size: 13px; margin: 12px 0 4px; }
|
||||
#log { white-space: pre; font: 12px ui-monospace, monospace; background: #fff; padding: 8px; margin-top: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Prova di <tmf-viewer></h1>
|
||||
<div class="griglia">
|
||||
<div><h2>Immagine 800x500 + overlay + etichette (box fisso, Adatta nel box)</h2>
|
||||
<tmf-viewer id="a" class="box" save-name="demo_immagine" width="800" height="500"></tmf-viewer></div>
|
||||
<div><h2>fit-height, no-save: alto quanto l'immagine</h2>
|
||||
<tmf-viewer id="b" fit-height no-save label-fit="Intera"></tmf-viewer></div>
|
||||
<div><h2>Modo contenuto: svg (DXF) senza src</h2>
|
||||
<tmf-viewer id="c" class="box" save-name="demo_dxf">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><rect x="5" y="5" width="190" height="90" fill="none" stroke="#fff" stroke-width="1"/><circle cx="100" cy="50" r="30" fill="none" stroke="#0ff" stroke-width="1"/></svg>
|
||||
</tmf-viewer></div>
|
||||
<div><h2>Vuoto: barra nascosta</h2><tmf-viewer id="d" class="box"></tmf-viewer></div>
|
||||
</div>
|
||||
<div id="log">in corso...</div>
|
||||
<script src="../../../static/js/vision_overlay.js"></script>
|
||||
<script src="../../../static/js/save_image.js"></script>
|
||||
<script src="../../../static/js/image_viewer.js"></script>
|
||||
<script>
|
||||
window.onerror = function (m, u, l) { document.getElementById('log').textContent += '\nERRORE ' + m + ' riga ' + l; }; // errori visibili nel riquadro
|
||||
(function () {
|
||||
var out = [], fails = 0;
|
||||
function check(l, c, x) { if (!c) fails++; out.push((c ? 'ok - ' : 'FAIL - ') + l + (c || x === undefined ? '' : ' :: ' + x)); document.getElementById('log').textContent = out.join('\n'); } // log aggiornato a ogni controllo
|
||||
function img(w, h, col) {
|
||||
var c = document.createElement('canvas'); c.width = w; c.height = h;
|
||||
var g = c.getContext('2d'); g.fillStyle = col; g.fillRect(0, 0, w, h);
|
||||
g.fillStyle = '#fff'; for (var x = 0; x < w; x += 100) g.fillRect(x, 0, 2, h);
|
||||
for (var y = 0; y < h; y += 100) g.fillRect(0, y, w, 2);
|
||||
return c.toDataURL('image/png');
|
||||
}
|
||||
var a = document.getElementById('a'), b = document.getElementById('b'), c = document.getElementById('c'), d = document.getElementById('d');
|
||||
var ev = { load: 0, zoom: 0 };
|
||||
a.addEventListener('viewer-load', function (e) { ev.load++; ev.det = e.detail; });
|
||||
a.addEventListener('viewer-zoom', function () { ev.zoom++; });
|
||||
a.overlay = [{ type: 'segment', role: 'found', row1: 50, col1: 50, row2: 450, col2: 750 },
|
||||
{ type: 'points', role: 'reference', rows: [100, 200], cols: [100, 300] }];
|
||||
a.labels = [{ key: 'q1', x: 400, y: 250, text: 'Quota 1 12.345 mm', cls: 'ok' }];
|
||||
a.setAttribute('src', img(800, 500, '#334155'));
|
||||
b.setAttribute('src', img(1000, 400, '#475569'));
|
||||
function btn(v, i) { return v.querySelectorAll('.tmfv-bar button')[i]; }
|
||||
setTimeout(function () {
|
||||
var vpa = a.querySelector('.tmfv-vp'), st = a.querySelector('.tmfv-stage');
|
||||
var fitA = function () { return Math.min(vpa.clientWidth / 800, vpa.clientHeight / 500); };
|
||||
check('a: viewer-load una volta con 800x500', ev.load === 1 && ev.det && ev.det.width === 800 && ev.det.height === 500, JSON.stringify(ev));
|
||||
check('a: Adatta = min(box/img), stage entra nel box', Math.abs(a.zoom - fitA()) < 0.01 && st.offsetWidth <= vpa.clientWidth + 1 && st.offsetHeight <= vpa.clientHeight + 1,
|
||||
a.zoom + ' ' + st.offsetWidth + 'x' + st.offsetHeight + ' vp ' + vpa.clientWidth + 'x' + vpa.clientHeight);
|
||||
check('a: percentuale mostrata', /^\d+ %$/.test(a.querySelector('.tmfv-pct').textContent), a.querySelector('.tmfv-pct').textContent);
|
||||
check('a: overlay con 2 forme e viewBox 0 0 800 500', a.querySelector('.tmfv-ov').children.length >= 2 && a.querySelector('.tmfv-ov').getAttribute('viewBox') === '0 0 800 500', a.querySelector('.tmfv-ov').innerHTML.slice(0, 80));
|
||||
var lab = a.querySelector('.tmfv-label');
|
||||
check('a: etichetta al 50 % / 50 %, testo, classe', lab && lab.style.left === '50%' && lab.style.top === '50%' && lab.textContent === 'Quota 1 12.345 mm' && lab.classList.contains('ok'));
|
||||
check('a: barra visibile con -, +, Adatta, Salva', !a.querySelector('.tmfv-bar').hidden && a.querySelectorAll('.tmfv-bar button').length === 4 && btn(a, 3).textContent === 'Salva');
|
||||
var z0 = a.zoom; btn(a, 1).click();
|
||||
check('a: + ingrandisce di 1.25', Math.abs(a.zoom / z0 - 1.25) < 1e-9, a.zoom + '/' + z0);
|
||||
for (var i = 0; i < 6; i++) a.zoomBy(1.25);
|
||||
check('a: ingrandito -> scorrimento possibile e cursore grab', vpa.scrollWidth > vpa.clientWidth && vpa.style.cursor === 'grab', vpa.style.cursor);
|
||||
var rect = vpa.getBoundingClientRect(), px = 120, py = 90;
|
||||
var zb = a.zoom, lx = (vpa.scrollLeft + px) / zb, ly = (vpa.scrollTop + py) / zb;
|
||||
vpa.dispatchEvent(new WheelEvent('wheel', { ctrlKey: true, deltaY: -100, clientX: rect.left + vpa.clientLeft + px, clientY: rect.top + vpa.clientTop + py, bubbles: true, cancelable: true }));
|
||||
var za = a.zoom, lx2 = (vpa.scrollLeft + px) / za, ly2 = (vpa.scrollTop + py) / za;
|
||||
check('a: Ctrl+rotella zooma e tiene fermo il punto', za > zb && Math.abs(lx - lx2) < 1 && Math.abs(ly - ly2) < 1, [zb, za, lx, lx2, ly, ly2].join(' '));
|
||||
for (i = 0; i < 40; i++) a.zoomBy(1.25);
|
||||
check('a: zoom limitato a max(8, fit)', a.zoom <= 8.0000001, a.zoom);
|
||||
var we = new WheelEvent('wheel', { deltaY: 100, bubbles: true, cancelable: true }); vpa.dispatchEvent(we);
|
||||
check('a: rotella senza Ctrl non intercettata', we.defaultPrevented === false);
|
||||
btn(a, 2).click();
|
||||
check('a: Adatta torna al fit e scroll a 0', Math.abs(a.zoom - fitA()) < 0.01 && vpa.scrollLeft === 0, a.zoom);
|
||||
check('a: viewer-zoom emesso a ogni cambio', ev.zoom > 3, ev.zoom);
|
||||
a.zoomBy(1.25); var zz = a.zoom; ev.load = 0;
|
||||
a.setAttribute('src', img(800, 500, '#7f1d1d'));
|
||||
setTimeout(function () {
|
||||
check('a: nuovo src, stesse dimensioni -> zoom mantenuto', ev.load === 1 && Math.abs(a.zoom - zz) < 1e-9, a.zoom + ' vs ' + zz);
|
||||
var vpb = b.querySelector('.tmfv-vp');
|
||||
check('b: fit-height: viewport alto quanto l\'immagine adattata alla larghezza', Math.abs(vpb.offsetHeight - 400 * b.zoom) < 1.5 && Math.abs(b.zoom - vpb.clientWidth / 1000) < 0.01,
|
||||
vpb.offsetHeight + ' vs ' + 400 * b.zoom + ' zoom ' + b.zoom);
|
||||
check('b: no-save nasconde Salva, label-fit "Intera"', btn(b, 3).hidden && btn(b, 2).textContent === 'Intera');
|
||||
b.style.width = '300px';
|
||||
// il resize e' consegnato dal ResizeObserver al prossimo frame: in una scheda in background puo' tardare, quindi attendo (max 5 s)
|
||||
var t0 = Date.now();
|
||||
(function attendi() { if (b.zoom < 0.5 || Date.now() - t0 > 5000) dopo(); else setTimeout(attendi, 100); })();
|
||||
function dopo() {
|
||||
check('b: resize in stato adatta -> rifa Adatta (300 px)', Math.abs(b.zoom - vpb.clientWidth / 1000) < 0.01 && Math.abs(vpb.offsetHeight - 400 * b.zoom) < 1.5, b.zoom + ' ' + vpb.offsetHeight);
|
||||
var stc = c.querySelector('.tmfv-stage');
|
||||
check('c: svg nello stage, dimensioni dal viewBox 200x100, Adatta', stc.querySelector('svg:not(.tmfv-ov)') && !c.querySelector(':scope > svg') && c.zoom > 0 && Math.abs(stc.offsetWidth / stc.offsetHeight - 2) < 0.05, c.zoom + ' ' + stc.offsetWidth + 'x' + stc.offsetHeight);
|
||||
// #122: DXF in mm (fit ~ 10): si ingrandisce oltre Adatta; width/height cambiati dopo il connect rifanno Adatta
|
||||
c.setAttribute('width', '50'); c.setAttribute('height', '30'); // contenuto 50 x 30 mm nel box ~ 560 x 320
|
||||
var zc = c.zoom; c.zoomBy(1.25);
|
||||
check('c: width/height 50x30 -> fit > 8 e zoom oltre Adatta', zc > 8 && c.zoom > zc * 1.2, zc + ' -> ' + c.zoom);
|
||||
// #122: viewer-error su immagine non caricabile; src tolto -> componente vuoto senza errori
|
||||
var e = document.createElement('tmf-viewer'); e.style.height = '100px'; document.body.appendChild(e);
|
||||
var gotErr = null; e.addEventListener('viewer-error', function (x) { gotErr = x.detail; });
|
||||
e.overlay = [{ type: 'segment', role: 'found', row1: 1, col1: 1, row2: 5, col2: 9 }]; e.labels = [{ x: 5, y: 5, text: 'x' }]; // prima del load
|
||||
e.setAttribute('src', 'inesistente.png');
|
||||
setTimeout(function () {
|
||||
check('e: viewer-error con src', gotErr && gotErr.src === 'inesistente.png', JSON.stringify(gotErr));
|
||||
e.setAttribute('src', img(100, 50, '#000'));
|
||||
setTimeout(function () {
|
||||
check('e: overlay ed etichette impostati prima del load disegnati dopo', e.querySelector('.tmfv-ov').children.length === 1 && e.querySelectorAll('.tmfv-label').length === 1);
|
||||
var lb = getComputedStyle(e.querySelector('.tmfv-label'));
|
||||
check('e: etichetta con aspetto proprio (absolute, bold, padding)', lb.position === 'absolute' && lb.fontWeight === '700' && lb.paddingLeft === '6px' && lb.pointerEvents === 'none');
|
||||
e.removeAttribute('src');
|
||||
check('e: src tolto -> vuoto, barra nascosta', e.querySelector('.tmfv-bar').hidden === true && e.zoom === 0);
|
||||
}, 300);
|
||||
}, 300);
|
||||
check('d: vuoto -> barra nascosta', d.querySelector('.tmfv-bar').hidden === true);
|
||||
check('stile iniettato una sola volta', document.querySelectorAll('#tmf-viewer-css').length === 1);
|
||||
tmfSvgToPngBlob(c.querySelector('.tmfv-stage > svg:not(.tmfv-ov)'), 200, 100).then(function (bl) {
|
||||
check('c: tmfSvgToPngBlob produce un PNG', bl && bl.type === 'image/png' && bl.size > 100, bl && (bl.type + ' ' + bl.size));
|
||||
return createImageBitmap(bl);
|
||||
}).then(function (bm) {
|
||||
check('c: PNG 1600x800', bm.width === 1600 && bm.height === 800, bm.width + 'x' + bm.height);
|
||||
}).catch(function (er) { check('c: rasterizzazione', false, String(er)); }).then(function () { return new Promise(function (r) { setTimeout(r, 900); }); }).then(function () {
|
||||
document.getElementById('log').textContent = out.join('\n') + '\n\n' + (out.length - fails) + '/' + out.length + ' controlli ok';
|
||||
document.title = fails ? 'FALLITO' : 'TUTTO OK';
|
||||
});
|
||||
}
|
||||
}, 400);
|
||||
}, 600);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,7 +44,8 @@ check('fitZoom: 0 con NaN, negativi, undefined',
|
||||
check('clampZoom: dentro i limiti resta uguale', near(M.clampZoom(1, 0.5), 1));
|
||||
check('clampZoom: sotto fit*0.5 -> fit*0.5', near(M.clampZoom(0.01, 0.5), 0.25));
|
||||
check('clampZoom: sopra 8 -> 8', near(M.clampZoom(20, 0.5), 8));
|
||||
check('clampZoom: fit sopra 8 alza il massimo a fit', near(M.clampZoom(20, 12), 12) && near(M.clampZoom(11, 12), 11));
|
||||
check('clampZoom: fit sopra 1 alza il massimo a fit*8 (DXF in mm, fit ~10)', near(M.clampZoom(20, 12), 20) && near(M.clampZoom(200, 12), 96) && near(M.clampZoom(11, 12), 11));
|
||||
check('clampZoom: con fit piccolo il massimo resta 8', near(M.clampZoom(20, 0.5), 8) && near(M.clampZoom(20, 1), 8) && near(M.clampZoom(100, 2), 16));
|
||||
check('clampZoom: limiti esatti', near(M.clampZoom(0.25, 0.5), 0.25) && near(M.clampZoom(8, 0.5), 8));
|
||||
|
||||
// --- zoomAround ---
|
||||
|
||||
Reference in New Issue
Block a user