Pagina camera 8765: i quattro visori diventano tmf-viewer, JS serviti su /visore/
Refs #122 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+56
-164
@@ -17,7 +17,9 @@ un'opzione della calibrazione: la calibrazione usa sempre il frame grezzo della
|
||||
scelta nella tab Acquisizione. Sorgente in cima alla tab; riquadri «Immagine» (grezza) e,
|
||||
con la casella, «Calibrata» (/camera/anteprima/calibrata) affiancati; il ciclo anteprima
|
||||
si sospende mentre la tab Calibrazione e' aperta.
|
||||
Dipende da: niente. E' una stringa, cosi' il pacchetto compilato non ha file dati.
|
||||
#122: i quattro visori sono tmf-viewer; i tre file JS del frontend (vision_overlay, save_image, image_viewer)
|
||||
li serve camera_service su /visore/.
|
||||
Dipende da: i tre file JS su /visore/ (camera_service). E' una stringa, cosi' il pacchetto compilato non ha file dati.
|
||||
"""
|
||||
|
||||
PAGINA = r"""<!doctype html>
|
||||
@@ -66,9 +68,7 @@ PAGINA = r"""<!doctype html>
|
||||
/* anteprima */
|
||||
.palco { background: #020617; border: 1px solid var(--bordo); border-radius: 6px; min-height: 340px;
|
||||
display: flex; align-items: center; justify-content: center; margin-top: 10px; overflow: hidden; }
|
||||
.cornice { position: relative; display: inline-block; line-height: 0; max-width: 100%; }
|
||||
.cornice img { max-width: 100%; max-height: calc(100vh - 330px); min-height: 200px; border: 0; margin: 0; }
|
||||
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
|
||||
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri; #122: sta dentro lo stage del tmf-viewer */
|
||||
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
||||
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
||||
#vuoto, #vuotoCal, #vuotoCalTab { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
||||
@@ -78,21 +78,10 @@ PAGINA = r"""<!doctype html>
|
||||
.riquadro .titolo { font-size: .88rem; font-weight: 600; }
|
||||
.riquadro .titolo span { font-weight: 400; font-size: .78rem; color: var(--secondario); margin-left: 6px; }
|
||||
.riquadro .palco { margin-top: 4px; }
|
||||
/* #111: visore zoom. .visore (position:relative) NON scorre: la barra e' sua figlia e resta ferma; a scorrere e' il .palco da zoomato */
|
||||
.visore { position: relative; margin-top: 10px; }
|
||||
.riquadro .visore { margin-top: 4px; }
|
||||
.visore > .palco { margin-top: 0; }
|
||||
.barraZoom { position: absolute; top: 8px; right: 8px; z-index: 3; display: flex; align-items: center; gap: 4px;
|
||||
padding: 3px 5px; border-radius: 6px; background: rgba(0, 0, 0, .6); color: #fff; font-size: .8rem; }
|
||||
.barraZoom button { padding: 2px 9px; font-size: .85rem; background: rgba(255, 255, 255, .15); color: #fff; }
|
||||
.barraZoom span { min-width: 3.4em; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.palco.zoomata + .barraZoom { right: 26px; } /* #111: lascia libera la barra di scorrimento verticale */
|
||||
.visore:has(.cornice[hidden]) .barraZoom { display: none; } /* #111: barra nascosta finche' la cornice e' nascosta */
|
||||
/* #111: stato zoomato: palco ad altezza fissa che scorre, cornice a dimensioni esplicite (impostate da JS); centrata con margin:auto solo se piu' piccola */
|
||||
.palco.zoomata { display: flex; align-items: flex-start; justify-content: flex-start; overflow: auto; cursor: grab; touch-action: none; }
|
||||
.palco.zoomata.panning { cursor: grabbing; }
|
||||
.palco.zoomata .cornice { display: block; flex: none; margin: auto; max-width: none; }
|
||||
.palco.zoomata .cornice img { display: block; width: 100%; height: 100%; max-width: none; max-height: none; min-height: 0; }
|
||||
/* #122: i visori sono tmf-viewer (static/js/image_viewer.js servito su /visore/): barra, zoom e pan vengono dal componente.
|
||||
Altezza data dalla pagina (come l'altezza massima che avevano le immagini); [hidden] vale gia' sopra */
|
||||
.palco tmf-viewer { width: 100%; height: max(340px, calc(100vh - 330px)); }
|
||||
.angoli { display: block; width: 100%; margin-top: 8px; border: 1px solid var(--bordo); border-radius: 6px; } /* #122: immagine degli angoli, alta quanto serve (fit-height) */
|
||||
.sorgente-riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
|
||||
.sorgente-riga .campo-sorgente { flex: 1 1 260px; min-width: 0; }
|
||||
.sorgente-riga select { width: 100%; }
|
||||
@@ -128,7 +117,7 @@ PAGINA = r"""<!doctype html>
|
||||
@media (max-width: 900px) {
|
||||
.griglia { grid-template-columns: minmax(0, 1fr); }
|
||||
.riquadri.doppio { grid-template-columns: minmax(0, 1fr); } /* #92: in colonna */
|
||||
.cornice img { max-height: 60vh; }
|
||||
.palco tmf-viewer { height: max(340px, 60vh); } /* #122: era max-height delle immagini */
|
||||
}
|
||||
@media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */
|
||||
main { padding: 8px; }
|
||||
@@ -179,28 +168,18 @@ PAGINA = r"""<!doctype html>
|
||||
<div class="riquadri" id="riquadri">
|
||||
<div class="riquadro">
|
||||
<div class="titolo">Immagine<span>grezza, senza correzione</span></div>
|
||||
<div class="visore"> <!-- #111 -->
|
||||
<div class="palco">
|
||||
<div class="palco"> <!-- #122: tmf-viewer al posto del visore #111; width/height = risoluzione della camera, messe da tornaAdatta() -->
|
||||
<div id="vuoto">Anteprima ferma. Premere «Avvia anteprima» o «Scatta».</div>
|
||||
<div class="cornice" id="cornice" hidden>
|
||||
<img id="immagine" alt="Anteprima grezza della sorgente">
|
||||
<div id="mirino" class="mirino" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="barraZoom"><button id="zoomMeno" class="secondario" type="button" title="Riduci">−</button><button id="zoomAdatta" class="secondario" type="button" title="Adatta allo spazio">Adatta</button><button id="zoomPiu" class="secondario" type="button" title="Ingrandisci">+</button><span id="zoomPct">100 %</span></div>
|
||||
<tmf-viewer id="immagine" save-name="grezza" title-save="Salva l'immagine grezza" hidden></tmf-viewer>
|
||||
<div id="mirino" class="mirino" hidden></div> <!-- #122: spostato nello stage del componente da creaVisore() -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="riquadro" id="riquadroCal" hidden>
|
||||
<div class="titolo">Calibrata<span id="nomeCalibrata"></span></div>
|
||||
<div class="visore"> <!-- #111 -->
|
||||
<div class="palco">
|
||||
<div class="palco"> <!-- #122 -->
|
||||
<div id="vuotoCal">Premere «Avvia anteprima» o «Scatta».</div>
|
||||
<div class="cornice" id="corniceCal" hidden>
|
||||
<img id="immagineCal" alt="Anteprima della sorgente raddrizzata dalla calibrazione">
|
||||
<div id="mirinoCal" class="mirino" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="barraZoom"><button id="zoomMenoCal" class="secondario" type="button" title="Riduci">−</button><button id="zoomAdattaCal" class="secondario" type="button" title="Adatta allo spazio">Adatta</button><button id="zoomPiuCal" class="secondario" type="button" title="Ingrandisci">+</button><span id="zoomPctCal">100 %</span></div>
|
||||
<tmf-viewer id="immagineCal" save-name="calibrata" title-save="Salva l'immagine calibrata" hidden></tmf-viewer>
|
||||
<div id="mirinoCal" class="mirino" hidden></div>
|
||||
</div>
|
||||
<!-- #93: scelta della calibrazione da attivare sulla sorgente, con CONFERMA -->
|
||||
<div class="riga scelta-cal">
|
||||
@@ -260,14 +239,9 @@ PAGINA = r"""<!doctype html>
|
||||
<h2>Calibrazione</h2>
|
||||
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
||||
<!-- #94: immagine viva (stesso frame dell'anteprima) e «Blocca immagine» per calcolare su quella -->
|
||||
<div class="visore" style="margin-top:0"> <!-- #111 -->
|
||||
<div class="palco" style="margin-top:0">
|
||||
<div class="palco" style="margin-top:0"> <!-- #122 -->
|
||||
<div id="vuotoCalTab">Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione</div>
|
||||
<div class="cornice" id="corniceCalTab" hidden>
|
||||
<img id="immagineCalTab" alt="Immagine grezza della sorgente">
|
||||
</div>
|
||||
</div>
|
||||
<div class="barraZoom"><button id="zoomMenoCalTab" class="secondario" type="button" title="Riduci">−</button><button id="zoomAdattaCalTab" class="secondario" type="button" title="Adatta allo spazio">Adatta</button><button id="zoomPiuCalTab" class="secondario" type="button" title="Ingrandisci">+</button><span id="zoomPctCalTab">100 %</span></div>
|
||||
<tmf-viewer id="immagineCalTab" save-name="calibrazione" title-save="Salva l'immagine della calibrazione" hidden></tmf-viewer>
|
||||
</div>
|
||||
<div class="riga" style="margin-top:8px">
|
||||
<button id="scattaCal" class="secondario" type="button">Scatta</button>
|
||||
@@ -294,7 +268,7 @@ PAGINA = r"""<!doctype html>
|
||||
<button id="salva">Salva</button>
|
||||
</div>
|
||||
<div id="msgSalva" class="messaggio"></div>
|
||||
<img id="overlay" alt="Angoli trovati sulla scacchiera" style="max-width:100%;margin-top:8px;border:1px solid var(--bordo);border-radius:6px">
|
||||
<tmf-viewer id="overlay" class="angoli" fit-height save-name="angoli" title-save="Salva l'immagine degli angoli"></tmf-viewer> <!-- #122: era un <img> senza zoom -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -323,6 +297,10 @@ PAGINA = r"""<!doctype html>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- #122: file del frontend serviti da camera_service su /visore/ (locali: la pagina resta offline); l'ordine conta -->
|
||||
<script src="/visore/vision_overlay.js"></script>
|
||||
<script src="/visore/save_image.js"></script>
|
||||
<script src="/visore/image_viewer.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
@@ -510,130 +488,44 @@ 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;
|
||||
// ---- #122: visori = tmf-viewer (zoom, Adatta, Salva, pan e Ctrl+rotella vengono dal componente, come nel resto di TMF)
|
||||
// width/height del componente = risoluzione del frame pieno (A.ris): 100 % = pixel della camera, anche se l'anteprima scaricata e' piu' piccola
|
||||
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 adattaVisore(v) { // #111: nome distinto da adatta(d, v) dei parametri camera
|
||||
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
|
||||
// #122: tutti i visori dell'anteprima tornano ad Adatta con le dimensioni di A.ris (cambio sorgente, risoluzione del frame diversa); A.ris ignoto = dimensioni naturali
|
||||
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);
|
||||
var el = V[k].el;
|
||||
if (A.ris) { el.setAttribute("width", A.ris.w); el.setAttribute("height", A.ris.h); }
|
||||
else { el.removeAttribute("width"); el.removeAttribute("height"); }
|
||||
if (el.fit) el.fit();
|
||||
});
|
||||
}
|
||||
// #111: lato_max dell'anteprima: 1280 come prima se nessun visore dell'Acquisizione e' zoomato, altrimenti abbastanza pixel per lo zoom piu' alto
|
||||
// #122: lato_max dell'anteprima: 1280 come prima se nessun visore dell'Acquisizione e' ingrandito, altrimenti abbastanza pixel per lo zoom piu' alto (zoom dall'evento viewer-zoom)
|
||||
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 (v.el.offsetWidth > 0 && v.fit && v.z > v.fit * 1.0001 && 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 };
|
||||
function creaVisore(chiave, idViewer, idMirino) {
|
||||
var el = $(idViewer);
|
||||
var v = { el: el, z: 0, fit: 0 };
|
||||
V[chiave] = v;
|
||||
$("zoomMeno" + suf).onclick = function () { zoomBy(v, 1 / PASSO); };
|
||||
$("zoomPiu" + suf).onclick = function () { zoomBy(v, PASSO); };
|
||||
$("zoomAdatta" + suf).onclick = function () { adattaVisore(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); });
|
||||
el.addEventListener("viewer-zoom", function (ev) { v.z = ev.detail.zoom; v.fit = ev.detail.fit; });
|
||||
// #122: il mirino sta dentro lo stage del componente (segue immagine e zoom); lo stage esiste dopo il connectedCallback, si riprova a ogni viewer-load
|
||||
var monta = function () {
|
||||
var stage = el.querySelector(".tmfv-stage"), m = idMirino ? $(idMirino) : null;
|
||||
if (stage && m && m.parentNode !== stage) stage.appendChild(m);
|
||||
};
|
||||
monta();
|
||||
el.addEventListener("viewer-load", monta);
|
||||
}
|
||||
creaVisore("acq", "", "cornice", "immagine");
|
||||
creaVisore("cal", "Cal", "corniceCal", "immagineCal");
|
||||
creaVisore("tab", "CalTab", "corniceCalTab", "immagineCalTab");
|
||||
creaVisore("acq", "immagine", "mirino");
|
||||
creaVisore("cal", "immagineCal", "mirinoCal");
|
||||
creaVisore("tab", "immagineCalTab", "");
|
||||
|
||||
function azzeraIndicatori() {
|
||||
A.nitMax = 0;
|
||||
@@ -653,17 +545,17 @@ function pulsantiAnteprima() {
|
||||
function mostraImmagine(blob) {
|
||||
var vecchio = A.url;
|
||||
A.url = URL.createObjectURL(blob);
|
||||
$("immagine").src = A.url;
|
||||
$("immagine").setAttribute("src", A.url); // #122
|
||||
if (vecchio && vecchio !== A.urlBlocco) URL.revokeObjectURL(vecchio); // niente memoria che cresce a ogni frame; #94: non quello bloccato
|
||||
$("cornice").hidden = false;
|
||||
$("immagine").hidden = false;
|
||||
$("vuoto").hidden = true;
|
||||
if (!A.bloccata) mostraInTabCal(A.url); // #94: lo stesso blob URL va anche nella tab Calibrazione, salvo immagine bloccata
|
||||
}
|
||||
|
||||
// #94: riquadro «immagine viva» della tab Calibrazione (url null = vuoto)
|
||||
function mostraInTabCal(url) {
|
||||
$("immagineCalTab").src = url || "";
|
||||
$("corniceCalTab").hidden = !url;
|
||||
if (url) $("immagineCalTab").setAttribute("src", url); else $("immagineCalTab").removeAttribute("src"); // #122
|
||||
$("immagineCalTab").hidden = !url;
|
||||
$("vuotoCalTab").hidden = !!url;
|
||||
}
|
||||
|
||||
@@ -671,14 +563,14 @@ function mostraInTabCal(url) {
|
||||
function mostraCalibrata(blob, nome, soloScala) {
|
||||
var vecchio = A.urlCal;
|
||||
A.urlCal = URL.createObjectURL(blob);
|
||||
$("immagineCal").src = A.urlCal;
|
||||
$("immagineCal").setAttribute("src", A.urlCal); // #122
|
||||
if (vecchio) URL.revokeObjectURL(vecchio);
|
||||
$("corniceCal").hidden = false;
|
||||
$("immagineCal").hidden = false;
|
||||
$("vuotoCal").hidden = true;
|
||||
$("nomeCalibrata").textContent = (nome ? "«" + nome + "»" : "") + (soloScala ? " — solo scala, nessuna correzione ottica" : ""); // #95: senza mappa il frame e' grezzo
|
||||
}
|
||||
function messaggioCalibrata(testo) {
|
||||
$("corniceCal").hidden = true;
|
||||
$("immagineCal").hidden = true;
|
||||
$("vuotoCal").hidden = false;
|
||||
$("vuotoCal").textContent = testo;
|
||||
$("nomeCalibrata").textContent = "";
|
||||
@@ -686,7 +578,7 @@ function messaggioCalibrata(testo) {
|
||||
// #92: cambio sorgente: via i due frame, nessuna immagine di un'altra sorgente
|
||||
function svuotaRiquadri() {
|
||||
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
||||
$("cornice").hidden = true;
|
||||
$("immagine").hidden = true;
|
||||
$("vuoto").hidden = false;
|
||||
mostraInTabCal(null); // #94: svuotato anche il riquadro della tab Calibrazione
|
||||
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
||||
@@ -832,7 +724,7 @@ function sbloccaLocale() { // casella spenta, il riquadro della tab Calibrazion
|
||||
if (b && b !== A.url) URL.revokeObjectURL(b);
|
||||
$("bloccaImg").checked = false;
|
||||
$("scattaCal").disabled = $("scatta").disabled;
|
||||
mostraInTabCal($("cornice").hidden ? null : A.url);
|
||||
mostraInTabCal($("immagine").hidden ? null : A.url);
|
||||
}
|
||||
function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
||||
var s = A.sorgenteBloccata;
|
||||
@@ -843,7 +735,7 @@ function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
||||
$("bloccaImg").onchange = async function () {
|
||||
if (!this.checked) { rilasciaBlocco(); return; }
|
||||
var sorgente = $("sorgente").value;
|
||||
if (!sorgente || $("cornice").hidden) {
|
||||
if (!sorgente || $("immagine").hidden) {
|
||||
$("bloccaImg").checked = false;
|
||||
dire("msgBlocco", "Nessuna immagine da bloccare: «Scatta» o avviare l'anteprima.", "errore");
|
||||
return;
|
||||
@@ -1514,7 +1406,7 @@ async function calcola() {
|
||||
var d = await r.json();
|
||||
ultimo = { dati: d };
|
||||
tabella($("tabella"), d);
|
||||
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
||||
$("overlay").setAttribute("src", "data:image/png;base64," + d.overlay_png); // #122
|
||||
$("avviso").textContent = avvisi(d);
|
||||
$("origineCalcolo").textContent = "Origine: " + (d.bloccata ? "immagine bloccata — " : "") + descriviOrigine(d.origine); // #94
|
||||
$("risultato").hidden = false;
|
||||
|
||||
@@ -45,6 +45,7 @@ Calibrazione a scacchiera (#83, logica in scripts/calibrazione.py).
|
||||
la libreria resta)
|
||||
GET /calibrazione -> pagina HTML autonoma (offline) per fare tutto a mano
|
||||
GET /acquisizione -> stessa pagina (#87)
|
||||
GET /visore/{image_viewer.js|save_image.js|vision_overlay.js} -> i JS del <tmf-viewer> del frontend (#122), altri nomi 404
|
||||
Acquisizione (#87): anteprima, esposizione/guadagno, settaggi di default:
|
||||
POST /camera/anteprima json {source, grezzo=false, lato_max=1280 (320..4096),
|
||||
timeout_ms=5000} -> JPEG ridotto (lato lungo <= lato_max, mai ingrandito).
|
||||
@@ -125,6 +126,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import calibrazione # noqa: E402
|
||||
from calibrazione_pagina import PAGINA # noqa: E402
|
||||
|
||||
# #122: i tre file JS del frontend che la pagina carica su /visore/<nome> (solo questi nomi: nessun percorso arbitrario dall'URL).
|
||||
# Cartelle cercate nell'ordine: pacchetto Nuitka (static/ accanto al modulo, copiata da build.ps1) e sorgenti (src/frontend/flask_app/static/js)
|
||||
_FILE_VISORE = ("image_viewer.js", "save_image.js", "vision_overlay.js")
|
||||
_QUI = Path(__file__).resolve().parent
|
||||
_CARTELLE_VISORE = [_QUI / "static" / "js", _QUI.parent / "src" / "frontend" / "flask_app" / "static" / "js"]
|
||||
|
||||
_TRIGGER = ("continuo",)
|
||||
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
||||
_CACHE_CAMERE_S = 60.0 # #92: durata della cache della ricerca camere
|
||||
@@ -780,6 +787,17 @@ def create_app(folder: Path | str | None = None,
|
||||
def calibrazione_pagina():
|
||||
return HTMLResponse(PAGINA)
|
||||
|
||||
@app.get("/visore/{nome}")
|
||||
def visore_file(nome: str):
|
||||
"""#122: i file JS del <tmf-viewer> letti dal frontend Flask; la pagina resta offline."""
|
||||
if nome not in _FILE_VISORE: # lista chiusa: niente percorsi dall'URL
|
||||
raise HTTPException(404, "file non trovato")
|
||||
for cartella in _CARTELLE_VISORE:
|
||||
f = cartella / nome
|
||||
if f.is_file():
|
||||
return Response(f.read_bytes(), media_type="application/javascript", headers={"Cache-Control": "no-cache"})
|
||||
raise HTTPException(404, "file non trovato")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -1,80 +1,87 @@
|
||||
"""Test della pagina Acquisizione camera (#87): contenuto statico di PAGINA, senza servizio."""
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
_FILE = Path(__file__).resolve().parents[1] / "calibrazione_pagina.py"
|
||||
_spec = importlib.util.spec_from_file_location("calibrazione_pagina", _FILE)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
PAGINA = _mod.PAGINA
|
||||
|
||||
|
||||
def test_titolo_e_stile_scuro():
|
||||
assert "<title>Acquisizione camera</title>" in PAGINA
|
||||
assert "#0f172a" in PAGINA and "#1e293b" in PAGINA and "#3b82f6" in PAGINA
|
||||
|
||||
|
||||
def test_rotte_usate():
|
||||
for rotta in ("/camera/sources", "/camera/anteprima", "/camera/parametri",
|
||||
"/camera/parametri/salva", "/camera/calibrate", "/camera/calibration"):
|
||||
assert '"' + rotta in PAGINA, rotta
|
||||
|
||||
|
||||
def test_offline_e_senza_finestre_del_browser():
|
||||
for vietato in ("alert(", "confirm(", "prompt(", "http://", "https://"):
|
||||
assert vietato not in PAGINA, vietato
|
||||
|
||||
|
||||
def test_id_principali():
|
||||
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||
attesi = {"sorgente", "avvia", "ferma", "scatta", "mirinoCasella", "immagine",
|
||||
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
||||
"parametri", "salvaParametri", "quadrato", "acquisisci", "caricaImmagine", "fileImmagine",
|
||||
"mostraCal", "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga; #92: file e calibrata nell'Acquisizione
|
||||
assert attesi <= ids, attesi - ids
|
||||
# ogni id usato dal JS con $("...") esiste nel markup
|
||||
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
|
||||
assert usati <= ids, usati - ids
|
||||
|
||||
|
||||
def test_sezioni_e_testi_italiani():
|
||||
for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
|
||||
"Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili",
|
||||
"Attiva calibrazione"): # #92 #93
|
||||
assert testo in PAGINA, testo
|
||||
assert "it-IT" in PAGINA # numeri con la virgola
|
||||
assert "Choose File" not in PAGINA
|
||||
|
||||
|
||||
def test_localstorage_protetto():
|
||||
# ogni uso di localStorage sta dentro un try
|
||||
for m in re.finditer(r"localStorage[.]", PAGINA):
|
||||
assert "try {" in PAGINA[max(0, m.start() - 200):m.start()]
|
||||
|
||||
|
||||
def test_specchiatura_per_sorgente(): # #110
|
||||
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||
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('<div class="visore"') == 3
|
||||
assert PAGINA.count('<div class="barraZoom">') == 3
|
||||
|
||||
|
||||
def test_nessuna_funzione_dichiarata_due_volte(): # #111: in JS vince l'ultima dichiarazione, in silenzio
|
||||
script = re.search(r"<script>(.*?)</script>", PAGINA, re.S).group(1)
|
||||
nomi = re.findall(r"function\s+([A-Za-z0-9_$]+)\s*\(", script)
|
||||
doppi = sorted({n for n in nomi if nomi.count(n) > 1})
|
||||
assert not doppi, "funzioni dichiarate piu' volte: " + ", ".join(doppi)
|
||||
"""Test della pagina Acquisizione camera (#87): contenuto statico di PAGINA, senza servizio."""
|
||||
|
||||
|
||||
|
||||
import importlib.util
|
||||
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
_FILE = Path(__file__).resolve().parents[1] / "calibrazione_pagina.py"
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("calibrazione_pagina", _FILE)
|
||||
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
PAGINA = _mod.PAGINA
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_titolo_e_stile_scuro():
|
||||
|
||||
assert "<title>Acquisizione camera</title>" in PAGINA
|
||||
|
||||
assert "#0f172a" in PAGINA and "#1e293b" in PAGINA and "#3b82f6" in PAGINA
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_rotte_usate():
|
||||
|
||||
for rotta in ("/camera/sources", "/camera/anteprima", "/camera/parametri",
|
||||
|
||||
"/camera/parametri/salva", "/camera/calibrate", "/camera/calibration"):
|
||||
|
||||
assert '"' + rotta in PAGINA, rotta
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_offline_e_senza_finestre_del_browser():
|
||||
|
||||
for vietato in ("alert(", "confirm(", "prompt(", "http://", "https://"):
|
||||
|
||||
assert vietato not in PAGINA, vietato
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_id_principali():
|
||||
|
||||
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||
|
||||
attesi = {"sorgente", "avvia", "ferma", "scatta", "mirinoCasella", "immagine",
|
||||
|
||||
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
||||
|
||||
"parametri", "salvaParametri", "quadrato", "acquisisci", "caricaImmagine", "fileImmagine",
|
||||
|
||||
"mostraCal", "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga; #92: file e calibrata nell'Acquisizione
|
||||
|
||||
assert attesi <= ids, attesi - ids
|
||||
|
||||
# ogni id usato dal JS con $("...") esiste nel markup
|
||||
|
||||
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
|
||||
|
||||
assert usati <= ids, usati - ids
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_sezioni_e_testi_italiani():
|
||||
|
||||
for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
|
||||
|
||||
"Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Test delle rotte /visore/*.js di camera_service e della pagina che le usa (#122).
|
||||
|
||||
Uso: python -m pytest scripts/tests/test_visore_rotte.py -q
|
||||
Dipende da: venv con fastapi, httpx, opencv (come test_camera_service.py).
|
||||
image_viewer.js lo scrive un altro piano (V1): se manca nei sorgenti il test della rotta usa una cartella temporanea con file finti.
|
||||
"""
|
||||
import importlib.util
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
MODULO = Path(__file__).resolve().parents[1] / "camera_service.py"
|
||||
NOMI = ("image_viewer.js", "save_image.js", "vision_overlay.js")
|
||||
|
||||
|
||||
def _carica():
|
||||
spec = importlib.util.spec_from_file_location("camera_service", MODULO)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path):
|
||||
mod = _carica()
|
||||
with TestClient(mod.create_app(tmp_path)) as c:
|
||||
c.mod = mod
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cartella_finta(client, tmp_path):
|
||||
# file finti in una cartella temporanea: la rotta si prova senza dipendere da image_viewer.js del frontend
|
||||
d = tmp_path / "js"
|
||||
d.mkdir()
|
||||
for n in NOMI:
|
||||
(d / n).write_text("/* finto " + n + " */\n", encoding="utf-8")
|
||||
client.mod._CARTELLE_VISORE[:] = [d]
|
||||
return d
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", NOMI)
|
||||
def test_rotta_serve_il_file(client, cartella_finta, nome):
|
||||
r = client.get("/visore/" + nome)
|
||||
assert r.status_code == 200
|
||||
assert r.headers["content-type"].startswith("application/javascript")
|
||||
assert r.content == (cartella_finta / nome).read_bytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", ["altro.js", "camera_service.py", "..%2Fcamera_service.py", "image_viewer.JS", "image_viewer.js.map"])
|
||||
def test_altri_nomi_danno_404(client, cartella_finta, nome):
|
||||
assert client.get("/visore/" + nome).status_code == 404
|
||||
|
||||
|
||||
def test_file_mancante_nella_cartella_da_404(client, tmp_path):
|
||||
client.mod._CARTELLE_VISORE[:] = [tmp_path / "vuota"]
|
||||
assert client.get("/visore/save_image.js").status_code == 404
|
||||
|
||||
|
||||
def test_cartelle_di_default_sono_static_js_del_frontend(client):
|
||||
# sviluppo: sorgenti del frontend; pacchetto Nuitka: static/ accanto al modulo (build.ps1 la include)
|
||||
cartelle = _carica()._CARTELLE_VISORE
|
||||
assert any(c.parts[-5:] == ("src", "frontend", "flask_app", "static", "js") for c in cartelle)
|
||||
assert any(c.parts[-2:] == ("static", "js") and c.parent.parent == MODULO.parent for c in cartelle)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nome", ("save_image.js", "vision_overlay.js", "image_viewer.js"))
|
||||
def test_file_reali_del_frontend(client, nome):
|
||||
reale = next((c / nome for c in client.mod._CARTELLE_VISORE if (c / nome).is_file()), None)
|
||||
if reale is None:
|
||||
pytest.skip(nome + " non e' ancora nei sorgenti del frontend (lo scrive il piano V1)")
|
||||
r = client.get("/visore/" + nome)
|
||||
assert r.status_code == 200 and r.content == reale.read_bytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rotta", ("/acquisizione", "/calibrazione"))
|
||||
def test_pagina_usa_quattro_visori_e_i_tre_script(client, rotta):
|
||||
html = client.get(rotta).text
|
||||
assert html.count("<tmf-viewer") == 4
|
||||
src = re.findall(r'<script src="(/visore/[^"]+)"', html)
|
||||
assert src == ["/visore/vision_overlay.js", "/visore/save_image.js", "/visore/image_viewer.js"]
|
||||
assert "zoomAdatta" not in html
|
||||
Reference in New Issue
Block a user