diff --git a/src/frontend/flask_app/static/js/image_viewer.js b/src/frontend/flask_app/static/js/image_viewer.js index d43c5ed..f6744f8 100644 --- a/src/frontend/flask_app/static/js/image_viewer.js +++ b/src/frontend/flask_app/static/js/image_viewer.js @@ -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) diff --git a/src/frontend/flask_app/tests/js/fixtures/image_viewer_demo.html b/src/frontend/flask_app/tests/js/fixtures/image_viewer_demo.html new file mode 100644 index 0000000..89aa3be --- /dev/null +++ b/src/frontend/flask_app/tests/js/fixtures/image_viewer_demo.html @@ -0,0 +1,135 @@ + + + + + +Prova tmf-viewer + + + +

Prova di <tmf-viewer>

+
+

Immagine 800x500 + overlay + etichette (box fisso, Adatta nel box)

+
+

fit-height, no-save: alto quanto l'immagine

+
+

Modo contenuto: svg (DXF) senza src

+ + +
+

Vuoto: barra nascosta

+
+
in corso...
+ + + + + + diff --git a/src/frontend/flask_app/tests/js/test_image_viewer.js b/src/frontend/flask_app/tests/js/test_image_viewer.js index aaacc24..cc4bb55 100644 --- a/src/frontend/flask_app/tests/js/test_image_viewer.js +++ b/src/frontend/flask_app/tests/js/test_image_viewer.js @@ -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 ---