@@ -469,6 +493,7 @@ function cambioSorgente(salva) {
if (A.bloccata) rilasciaBlocco(); // #94: casella spenta e DELETE sulla sorgente vecchia (prima di svuotare i riquadri)
azzeraIndicatori();
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
+ tornaAdatta(); // #111: nuova sorgente = visori ad Adatta
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
annullaRimuovi();
$("rimuoviImmagine").hidden = !sorgenteImmagine();
@@ -485,6 +510,131 @@ function cambioSorgente(salva) {
// #91: ris = {w, h} dell'ultimo frame; #92: calAttiva = la sorgente ha una calibrazione attiva (null = non nota), bloccoCal = un 409 ha fermato le richieste calibrata; urlCal/seqCal = riquadro calibrata, bloccata/sorgenteBloccata/urlBlocco/seqBlocco = #94 immagine bloccata per il calcolo (il riquadro della tab Calibrazione non segue piu' il ciclo), lato = ultimo lato_max
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, bloccata: false, bloccando: 0, sorgenteBloccata: null, urlBlocco: null, seqBlocco: 0, lato: 1280, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
+// ---- #111: visori con zoom +/-, Adatta e pan (come il wizard DXF #104), uno per riquadro immagine; ognuno ha il suo zoom
+// z = pixel CSS per pixel di camera (100 % = pixel della camera), limiti [zFit, ZMAX]; le dimensioni del frame pieno sono A.ris
+var ZMAX = 8, PASSO = 1.25;
+var V = {};
+var sondaAlt = null;
+// #111: altezza massima del palco = max-height che il CSS da' alle immagini (letta da una sonda nascosta: nessun numero duplicato, segue la media query)
+function altezzaMax() {
+ if (!sondaAlt) {
+ sondaAlt = document.createElement("div");
+ sondaAlt.className = "cornice";
+ sondaAlt.hidden = true;
+ sondaAlt.appendChild(document.createElement("img"));
+ document.body.appendChild(sondaAlt);
+ }
+ var h = parseFloat(getComputedStyle(sondaAlt.firstChild).maxHeight);
+ return isFinite(h) && h > 0 ? h : 400;
+}
+// #111: zoom che fa stare tutto il frame nel palco (0 = non calcolabile: palco nascosto o risoluzione ignota)
+function zFit(v) {
+ var larg = v.palco.offsetWidth - 2 * v.palco.clientLeft; // larghezza utile senza la barra di scorrimento
+ if (!A.ris || larg <= 0) return 0;
+ return Math.min(larg / A.ris.w, altezzaMax() / A.ris.h);
+}
+function zoomata(v) { return v.palco.classList.contains("zoomata"); }
+// #111: applica lo stato al DOM. Adatta (z == zFit): nessuna regola nuova, layout di prima. Zoomato: palco ad altezza fissa che scorre, cornice W*z x H*z
+function applica(v) {
+ var zf = zFit(v);
+ if (!zf) return;
+ var z = v.adatta ? zf : Math.max(zf, Math.min(ZMAX, v.z));
+ v.z = z;
+ if (z > zf * 1.0001) {
+ v.palco.classList.add("zoomata");
+ v.palco.style.height = altezzaMax() + "px";
+ v.cornice.style.width = (A.ris.w * z) + "px";
+ v.cornice.style.height = (A.ris.h * z) + "px";
+ } else {
+ v.palco.classList.remove("zoomata", "panning");
+ v.palco.style.height = v.cornice.style.width = v.cornice.style.height = "";
+ }
+ v.pct.textContent = Math.round(z * 100) + " %";
+}
+// #111: zoom che tiene fermo il centro visibile (stessa idea del wizard), centro misurato in pixel di camera
+function zoomBy(v, f) {
+ var zf = zFit(v);
+ if (!zf) return;
+ var vecchio = v.z || zf;
+ var z = Math.max(zf, Math.min(ZMAX, vecchio * f));
+ if (z === vecchio) return;
+ var p = v.palco, pr = p.getBoundingClientRect(), cr = v.cornice.getBoundingClientRect();
+ var cx = A.ris.w / 2, cy = A.ris.h / 2;
+ if (cr.width > 0 && cr.height > 0) {
+ cx = Math.max(0, Math.min(A.ris.w, (pr.left + p.clientLeft + p.clientWidth / 2 - cr.left) / (cr.width / A.ris.w)));
+ cy = Math.max(0, Math.min(A.ris.h, (pr.top + p.clientTop + p.clientHeight / 2 - cr.top) / (cr.height / A.ris.h)));
+ }
+ v.z = z;
+ v.adatta = z <= zf * 1.0001;
+ applica(v);
+ if (zoomata(v)) { p.scrollLeft = cx * z - p.clientWidth / 2; p.scrollTop = cy * z - p.clientHeight / 2; }
+}
+function adatta(v) {
+ v.adatta = true;
+ v.z = 0;
+ applica(v);
+ v.palco.scrollLeft = 0;
+ v.palco.scrollTop = 0;
+}
+// #111: tutti i visori tornano ad Adatta (cambio sorgente, risoluzione del frame diversa); con A.ris ignoto si toglie anche lo stato zoomato
+function tornaAdatta() {
+ Object.keys(V).forEach(function (k) {
+ var v = V[k];
+ v.adatta = true;
+ v.z = 0;
+ v.palco.classList.remove("zoomata", "panning");
+ v.palco.style.height = v.cornice.style.width = v.cornice.style.height = "";
+ v.palco.scrollLeft = v.palco.scrollTop = 0;
+ applica(v);
+ });
+}
+// #111: lato_max dell'anteprima: 1280 come prima se nessun visore dell'Acquisizione e' zoomato, altrimenti abbastanza pixel per lo zoom piu' alto
+function latoAnteprima() {
+ var zm = 0;
+ ["acq", "cal"].forEach(function (k) {
+ var v = V[k];
+ if (v.palco.offsetWidth > 0 && zoomata(v) && v.z > zm) zm = v.z;
+ });
+ if (!zm || !A.ris) return 1280;
+ return Math.min(4096, Math.max(1280, Math.ceil(Math.max(A.ris.w, A.ris.h) * zm * (window.devicePixelRatio || 1))));
+}
+function creaVisore(chiave, suf, idCornice, idImg) {
+ var cornice = $(idCornice);
+ var v = { cornice: cornice, img: $(idImg), palco: cornice.parentNode, pct: $("zoomPct" + suf), z: 0, adatta: true, pan: null };
+ V[chiave] = v;
+ $("zoomMeno" + suf).onclick = function () { zoomBy(v, 1 / PASSO); };
+ $("zoomPiu" + suf).onclick = function () { zoomBy(v, PASSO); };
+ $("zoomAdatta" + suf).onclick = function () { adatta(v); };
+ // #111: Ctrl+rotella = zoom; senza Ctrl la rotella scorre la pagina
+ v.palco.addEventListener("wheel", function (ev) {
+ if (!ev.ctrlKey) return;
+ ev.preventDefault();
+ zoomBy(v, ev.deltaY < 0 ? PASSO : 1 / PASSO);
+ }, { passive: false });
+ // #111: pan col puntatore (solo zoomato): scroll del palco di quanto si muove il puntatore
+ v.palco.addEventListener("pointerdown", function (ev) {
+ if (!zoomata(v) || (ev.pointerType === "mouse" && ev.button)) return;
+ ev.preventDefault(); // senza, Chromium manda pointercancel al primo movimento e il pan si ferma
+ v.pan = { x: ev.clientX, y: ev.clientY, left: v.palco.scrollLeft, top: v.palco.scrollTop };
+ v.palco.classList.add("panning");
+ try { v.palco.setPointerCapture(ev.pointerId); } catch (e) { /* puntatore gia' rilasciato */ }
+ });
+ v.palco.addEventListener("pointermove", function (ev) {
+ if (!v.pan) return;
+ v.palco.scrollLeft = v.pan.left - (ev.clientX - v.pan.x);
+ v.palco.scrollTop = v.pan.top - (ev.clientY - v.pan.y);
+ });
+ var fine = function () { v.pan = null; v.palco.classList.remove("panning"); };
+ v.palco.addEventListener("pointerup", fine);
+ v.palco.addEventListener("pointercancel", fine);
+ // #111: palco ridimensionato (finestra, griglia a due riquadri, tab mostrata): ricalcola zFit e riapplica
+ if (window.ResizeObserver) new ResizeObserver(function () { applica(v); }).observe(v.palco);
+ else window.addEventListener("resize", function () { applica(v); });
+}
+creaVisore("acq", "", "cornice", "immagine");
+creaVisore("cal", "Cal", "corniceCal", "immagineCal");
+creaVisore("tab", "CalTab", "corniceCalTab", "immagineCalTab");
+
function azzeraIndicatori() {
A.nitMax = 0;
["nitidezza", "saturati", "media", "dimensioni", "nitidezzaMax"].forEach(function (i) { $(i).textContent = "-"; });
@@ -594,7 +744,7 @@ function aggiornaIndicatori(h) {
$("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0);
// #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano
var nw = parseInt(w, 10), na = parseInt(a, 10);
- if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); }
+ if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); tornaAdatta(); } // #111: risoluzione diversa = Adatta
}
$("raddrizzato").textContent = "frame grezzo"; // #92: l'indicatore viene sempre dal frame grezzo
}
@@ -639,7 +789,7 @@ async function ciclo(g) {
while (A.bloccando) await pausa(20); // #94: durante «Blocca immagine» nessuna nuova anteprima
if (!A.attiva || g !== A.gen) return;
var t0 = Date.now();
- var esito = await unaAnteprima(1280);
+ var esito = await unaAnteprima(latoAnteprima()); // #111: 1280 salvo zoom sull'Acquisizione
if (g !== A.gen) return;
if (esito === "fatale") { fermaAnteprima(); return; }
await pausa(esito === "errore" ? 1500 : Math.max(100 - (Date.now() - t0), 0));
diff --git a/scripts/tests/test_pagina_acquisizione.py b/scripts/tests/test_pagina_acquisizione.py
index 5d5404c..8ceaa52 100644
--- a/scripts/tests/test_pagina_acquisizione.py
+++ b/scripts/tests/test_pagina_acquisizione.py
@@ -59,3 +59,15 @@ def test_specchiatura_per_sorgente(): # #110
assert {"specchiaO", "specchiaV", "msgSpecchia"} <= ids
assert '"/camera/orientamento' in PAGINA
assert "Specchia orizzontale" in PAGINA and "Specchia verticale" in PAGINA
+
+
+def test_zoom_visori(): # #111
+ ids = set(re.findall(r'id="([^"]+)"', PAGINA))
+ for suf in ("", "Cal", "CalTab"): # tre riquadri: grezza, calibrata, tab Calibrazione
+ for base in ("zoomMeno", "zoomAdatta", "zoomPiu", "zoomPct"):
+ assert base + suf in ids, base + suf
+ assert "zoomata" in PAGINA
+ assert "latoAnteprima()" in PAGINA and "unaAnteprima(1280)" not in PAGINA # lato_max segue lo zoom
+ # la barra e' sorella del palco che scorre (non dentro): resta ferma durante lo scorrimento
+ assert PAGINA.count('
') == 3