Unisce lavoro/daitarn-tmf-v-camera: visore standard <tmf-viewer> (Refs #122)
This commit is contained in:
+55
-163
@@ -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,
|
scelta nella tab Acquisizione. Sorgente in cima alla tab; riquadri «Immagine» (grezza) e,
|
||||||
con la casella, «Calibrata» (/camera/anteprima/calibrata) affiancati; il ciclo anteprima
|
con la casella, «Calibrata» (/camera/anteprima/calibrata) affiancati; il ciclo anteprima
|
||||||
si sospende mentre la tab Calibrazione e' aperta.
|
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>
|
PAGINA = r"""<!doctype html>
|
||||||
@@ -66,9 +68,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
/* anteprima */
|
/* anteprima */
|
||||||
.palco { background: #020617; border: 1px solid var(--bordo); border-radius: 6px; min-height: 340px;
|
.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; }
|
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%; }
|
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri; #122: sta dentro lo stage del tmf-viewer */
|
||||||
.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 */
|
|
||||||
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
||||||
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
||||||
#vuoto, #vuotoCal, #vuotoCalTab { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
#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 { font-size: .88rem; font-weight: 600; }
|
||||||
.riquadro .titolo span { font-weight: 400; font-size: .78rem; color: var(--secondario); margin-left: 6px; }
|
.riquadro .titolo span { font-weight: 400; font-size: .78rem; color: var(--secondario); margin-left: 6px; }
|
||||||
.riquadro .palco { margin-top: 4px; }
|
.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 */
|
/* #122: i visori sono tmf-viewer (static/js/image_viewer.js servito su /visore/): barra, zoom e pan vengono dal componente.
|
||||||
.visore { position: relative; margin-top: 10px; }
|
Altezza data dalla pagina (come l'altezza massima che avevano le immagini); [hidden] vale gia' sopra */
|
||||||
.riquadro .visore { margin-top: 4px; }
|
.palco tmf-viewer { width: 100%; height: max(340px, calc(100vh - 330px)); }
|
||||||
.visore > .palco { margin-top: 0; }
|
.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) */
|
||||||
.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; }
|
|
||||||
.sorgente-riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
|
.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 .campo-sorgente { flex: 1 1 260px; min-width: 0; }
|
||||||
.sorgente-riga select { width: 100%; }
|
.sorgente-riga select { width: 100%; }
|
||||||
@@ -128,7 +117,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.griglia { grid-template-columns: minmax(0, 1fr); }
|
.griglia { grid-template-columns: minmax(0, 1fr); }
|
||||||
.riquadri.doppio { grid-template-columns: minmax(0, 1fr); } /* #92: in colonna */
|
.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) */
|
@media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */
|
||||||
main { padding: 8px; }
|
main { padding: 8px; }
|
||||||
@@ -179,29 +168,19 @@ PAGINA = r"""<!doctype html>
|
|||||||
<div class="riquadri" id="riquadri">
|
<div class="riquadri" id="riquadri">
|
||||||
<div class="riquadro">
|
<div class="riquadro">
|
||||||
<div class="titolo">Immagine<span>grezza, senza correzione</span></div>
|
<div class="titolo">Immagine<span>grezza, senza correzione</span></div>
|
||||||
<div class="visore"> <!-- #111 -->
|
<div class="palco"> <!-- #122: tmf-viewer al posto del visore #111; width/height = risoluzione della camera, messe da tornaAdatta() -->
|
||||||
<div class="palco">
|
|
||||||
<div id="vuoto">Anteprima ferma. Premere «Avvia anteprima» o «Scatta».</div>
|
<div id="vuoto">Anteprima ferma. Premere «Avvia anteprima» o «Scatta».</div>
|
||||||
<div class="cornice" id="cornice" hidden>
|
<tmf-viewer id="immagine" save-name="grezza" title-save="Salva l'immagine grezza" hidden></tmf-viewer>
|
||||||
<img id="immagine" alt="Anteprima grezza della sorgente">
|
<div id="mirino" class="mirino" hidden></div> <!-- #122: spostato nello stage del componente da creaVisore() -->
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="riquadro" id="riquadroCal" hidden>
|
<div class="riquadro" id="riquadroCal" hidden>
|
||||||
<div class="titolo">Calibrata<span id="nomeCalibrata"></span></div>
|
<div class="titolo">Calibrata<span id="nomeCalibrata"></span></div>
|
||||||
<div class="visore"> <!-- #111 -->
|
<div class="palco"> <!-- #122 -->
|
||||||
<div class="palco">
|
|
||||||
<div id="vuotoCal">Premere «Avvia anteprima» o «Scatta».</div>
|
<div id="vuotoCal">Premere «Avvia anteprima» o «Scatta».</div>
|
||||||
<div class="cornice" id="corniceCal" hidden>
|
<tmf-viewer id="immagineCal" save-name="calibrata" title-save="Salva l'immagine calibrata" hidden></tmf-viewer>
|
||||||
<img id="immagineCal" alt="Anteprima della sorgente raddrizzata dalla calibrazione">
|
|
||||||
<div id="mirinoCal" class="mirino" hidden></div>
|
<div id="mirinoCal" class="mirino" hidden></div>
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<!-- #93: scelta della calibrazione da attivare sulla sorgente, con CONFERMA -->
|
<!-- #93: scelta della calibrazione da attivare sulla sorgente, con CONFERMA -->
|
||||||
<div class="riga scelta-cal">
|
<div class="riga scelta-cal">
|
||||||
<select id="sceltaCal" aria-label="Calibrazione da attivare" disabled></select>
|
<select id="sceltaCal" aria-label="Calibrazione da attivare" disabled></select>
|
||||||
@@ -260,14 +239,9 @@ PAGINA = r"""<!doctype html>
|
|||||||
<h2>Calibrazione</h2>
|
<h2>Calibrazione</h2>
|
||||||
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
<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 -->
|
<!-- #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"> <!-- #122 -->
|
||||||
<div class="palco" style="margin-top:0">
|
|
||||||
<div id="vuotoCalTab">Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione</div>
|
<div id="vuotoCalTab">Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione</div>
|
||||||
<div class="cornice" id="corniceCalTab" hidden>
|
<tmf-viewer id="immagineCalTab" save-name="calibrazione" title-save="Salva l'immagine della calibrazione" hidden></tmf-viewer>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="riga" style="margin-top:8px">
|
<div class="riga" style="margin-top:8px">
|
||||||
<button id="scattaCal" class="secondario" type="button">Scatta</button>
|
<button id="scattaCal" class="secondario" type="button">Scatta</button>
|
||||||
@@ -294,7 +268,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<button id="salva">Salva</button>
|
<button id="salva">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="msgSalva" class="messaggio"></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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -323,6 +297,10 @@ PAGINA = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
var $ = function (id) { return document.getElementById(id); };
|
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
|
// #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 };
|
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
|
// ---- #122: visori = tmf-viewer (zoom, Adatta, Salva, pan e Ctrl+rotella vengono dal componente, come nel resto di TMF)
|
||||||
// z = pixel CSS per pixel di camera (100 % = pixel della camera), limiti [zFit, ZMAX]; le dimensioni del frame pieno sono A.ris
|
// width/height del componente = risoluzione del frame pieno (A.ris): 100 % = pixel della camera, anche se l'anteprima scaricata e' piu' piccola
|
||||||
var ZMAX = 8, PASSO = 1.25;
|
|
||||||
var V = {};
|
var V = {};
|
||||||
var sondaAlt = null;
|
// #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
|
||||||
// #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
|
|
||||||
function tornaAdatta() {
|
function tornaAdatta() {
|
||||||
Object.keys(V).forEach(function (k) {
|
Object.keys(V).forEach(function (k) {
|
||||||
var v = V[k];
|
var el = V[k].el;
|
||||||
v.adatta = true;
|
if (A.ris) { el.setAttribute("width", A.ris.w); el.setAttribute("height", A.ris.h); }
|
||||||
v.z = 0;
|
else { el.removeAttribute("width"); el.removeAttribute("height"); }
|
||||||
v.palco.classList.remove("zoomata", "panning");
|
if (el.fit) el.fit();
|
||||||
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
|
// #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() {
|
function latoAnteprima() {
|
||||||
var zm = 0;
|
var zm = 0;
|
||||||
["acq", "cal"].forEach(function (k) {
|
["acq", "cal"].forEach(function (k) {
|
||||||
var v = V[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;
|
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))));
|
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) {
|
function creaVisore(chiave, idViewer, idMirino) {
|
||||||
var cornice = $(idCornice);
|
var el = $(idViewer);
|
||||||
var v = { cornice: cornice, img: $(idImg), palco: cornice.parentNode, pct: $("zoomPct" + suf), z: 0, adatta: true, pan: null };
|
var v = { el: el, z: 0, fit: 0 };
|
||||||
V[chiave] = v;
|
V[chiave] = v;
|
||||||
$("zoomMeno" + suf).onclick = function () { zoomBy(v, 1 / PASSO); };
|
el.addEventListener("viewer-zoom", function (ev) { v.z = ev.detail.zoom; v.fit = ev.detail.fit; });
|
||||||
$("zoomPiu" + suf).onclick = function () { zoomBy(v, PASSO); };
|
// #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
|
||||||
$("zoomAdatta" + suf).onclick = function () { adattaVisore(v); };
|
var monta = function () {
|
||||||
// #111: Ctrl+rotella = zoom; senza Ctrl la rotella scorre la pagina
|
var stage = el.querySelector(".tmfv-stage"), m = idMirino ? $(idMirino) : null;
|
||||||
v.palco.addEventListener("wheel", function (ev) {
|
if (stage && m && m.parentNode !== stage) stage.appendChild(m);
|
||||||
if (!ev.ctrlKey) return;
|
};
|
||||||
ev.preventDefault();
|
monta();
|
||||||
zoomBy(v, ev.deltaY < 0 ? PASSO : 1 / PASSO);
|
el.addEventListener("viewer-load", monta);
|
||||||
}, { 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("acq", "immagine", "mirino");
|
||||||
creaVisore("cal", "Cal", "corniceCal", "immagineCal");
|
creaVisore("cal", "immagineCal", "mirinoCal");
|
||||||
creaVisore("tab", "CalTab", "corniceCalTab", "immagineCalTab");
|
creaVisore("tab", "immagineCalTab", "");
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
@@ -653,17 +545,17 @@ function pulsantiAnteprima() {
|
|||||||
function mostraImmagine(blob) {
|
function mostraImmagine(blob) {
|
||||||
var vecchio = A.url;
|
var vecchio = A.url;
|
||||||
A.url = URL.createObjectURL(blob);
|
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
|
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;
|
$("vuoto").hidden = true;
|
||||||
if (!A.bloccata) mostraInTabCal(A.url); // #94: lo stesso blob URL va anche nella tab Calibrazione, salvo immagine bloccata
|
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)
|
// #94: riquadro «immagine viva» della tab Calibrazione (url null = vuoto)
|
||||||
function mostraInTabCal(url) {
|
function mostraInTabCal(url) {
|
||||||
$("immagineCalTab").src = url || "";
|
if (url) $("immagineCalTab").setAttribute("src", url); else $("immagineCalTab").removeAttribute("src"); // #122
|
||||||
$("corniceCalTab").hidden = !url;
|
$("immagineCalTab").hidden = !url;
|
||||||
$("vuotoCalTab").hidden = !!url;
|
$("vuotoCalTab").hidden = !!url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,14 +563,14 @@ function mostraInTabCal(url) {
|
|||||||
function mostraCalibrata(blob, nome, soloScala) {
|
function mostraCalibrata(blob, nome, soloScala) {
|
||||||
var vecchio = A.urlCal;
|
var vecchio = A.urlCal;
|
||||||
A.urlCal = URL.createObjectURL(blob);
|
A.urlCal = URL.createObjectURL(blob);
|
||||||
$("immagineCal").src = A.urlCal;
|
$("immagineCal").setAttribute("src", A.urlCal); // #122
|
||||||
if (vecchio) URL.revokeObjectURL(vecchio);
|
if (vecchio) URL.revokeObjectURL(vecchio);
|
||||||
$("corniceCal").hidden = false;
|
$("immagineCal").hidden = false;
|
||||||
$("vuotoCal").hidden = true;
|
$("vuotoCal").hidden = true;
|
||||||
$("nomeCalibrata").textContent = (nome ? "«" + nome + "»" : "") + (soloScala ? " — solo scala, nessuna correzione ottica" : ""); // #95: senza mappa il frame e' grezzo
|
$("nomeCalibrata").textContent = (nome ? "«" + nome + "»" : "") + (soloScala ? " — solo scala, nessuna correzione ottica" : ""); // #95: senza mappa il frame e' grezzo
|
||||||
}
|
}
|
||||||
function messaggioCalibrata(testo) {
|
function messaggioCalibrata(testo) {
|
||||||
$("corniceCal").hidden = true;
|
$("immagineCal").hidden = true;
|
||||||
$("vuotoCal").hidden = false;
|
$("vuotoCal").hidden = false;
|
||||||
$("vuotoCal").textContent = testo;
|
$("vuotoCal").textContent = testo;
|
||||||
$("nomeCalibrata").textContent = "";
|
$("nomeCalibrata").textContent = "";
|
||||||
@@ -686,7 +578,7 @@ function messaggioCalibrata(testo) {
|
|||||||
// #92: cambio sorgente: via i due frame, nessuna immagine di un'altra sorgente
|
// #92: cambio sorgente: via i due frame, nessuna immagine di un'altra sorgente
|
||||||
function svuotaRiquadri() {
|
function svuotaRiquadri() {
|
||||||
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
||||||
$("cornice").hidden = true;
|
$("immagine").hidden = true;
|
||||||
$("vuoto").hidden = false;
|
$("vuoto").hidden = false;
|
||||||
mostraInTabCal(null); // #94: svuotato anche il riquadro della tab Calibrazione
|
mostraInTabCal(null); // #94: svuotato anche il riquadro della tab Calibrazione
|
||||||
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
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);
|
if (b && b !== A.url) URL.revokeObjectURL(b);
|
||||||
$("bloccaImg").checked = false;
|
$("bloccaImg").checked = false;
|
||||||
$("scattaCal").disabled = $("scatta").disabled;
|
$("scattaCal").disabled = $("scatta").disabled;
|
||||||
mostraInTabCal($("cornice").hidden ? null : A.url);
|
mostraInTabCal($("immagine").hidden ? null : A.url);
|
||||||
}
|
}
|
||||||
function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
||||||
var s = A.sorgenteBloccata;
|
var s = A.sorgenteBloccata;
|
||||||
@@ -843,7 +735,7 @@ function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
|||||||
$("bloccaImg").onchange = async function () {
|
$("bloccaImg").onchange = async function () {
|
||||||
if (!this.checked) { rilasciaBlocco(); return; }
|
if (!this.checked) { rilasciaBlocco(); return; }
|
||||||
var sorgente = $("sorgente").value;
|
var sorgente = $("sorgente").value;
|
||||||
if (!sorgente || $("cornice").hidden) {
|
if (!sorgente || $("immagine").hidden) {
|
||||||
$("bloccaImg").checked = false;
|
$("bloccaImg").checked = false;
|
||||||
dire("msgBlocco", "Nessuna immagine da bloccare: «Scatta» o avviare l'anteprima.", "errore");
|
dire("msgBlocco", "Nessuna immagine da bloccare: «Scatta» o avviare l'anteprima.", "errore");
|
||||||
return;
|
return;
|
||||||
@@ -1514,7 +1406,7 @@ async function calcola() {
|
|||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
ultimo = { dati: d };
|
ultimo = { dati: d };
|
||||||
tabella($("tabella"), 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);
|
$("avviso").textContent = avvisi(d);
|
||||||
$("origineCalcolo").textContent = "Origine: " + (d.bloccata ? "immagine bloccata — " : "") + descriviOrigine(d.origine); // #94
|
$("origineCalcolo").textContent = "Origine: " + (d.bloccata ? "immagine bloccata — " : "") + descriviOrigine(d.origine); // #94
|
||||||
$("risultato").hidden = false;
|
$("risultato").hidden = false;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Calibrazione a scacchiera (#83, logica in scripts/calibrazione.py).
|
|||||||
la libreria resta)
|
la libreria resta)
|
||||||
GET /calibrazione -> pagina HTML autonoma (offline) per fare tutto a mano
|
GET /calibrazione -> pagina HTML autonoma (offline) per fare tutto a mano
|
||||||
GET /acquisizione -> stessa pagina (#87)
|
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:
|
Acquisizione (#87): anteprima, esposizione/guadagno, settaggi di default:
|
||||||
POST /camera/anteprima json {source, grezzo=false, lato_max=1280 (320..4096),
|
POST /camera/anteprima json {source, grezzo=false, lato_max=1280 (320..4096),
|
||||||
timeout_ms=5000} -> JPEG ridotto (lato lungo <= lato_max, mai ingrandito).
|
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
|
import calibrazione # noqa: E402
|
||||||
from calibrazione_pagina import PAGINA # 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",)
|
_TRIGGER = ("continuo",)
|
||||||
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
_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
|
_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():
|
def calibrazione_pagina():
|
||||||
return HTMLResponse(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
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,16 +61,23 @@ def test_specchiatura_per_sorgente(): # #110
|
|||||||
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||||
|
|
||||||
attesi = {"sorgente", "avvia", "ferma", "scatta", "mirinoCasella", "immagine",
|
attesi = {"sorgente", "avvia", "ferma", "scatta", "mirinoCasella", "immagine",
|
||||||
def test_zoom_visori(): # #111
|
|
||||||
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
"nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni",
|
||||||
for suf in ("", "Cal", "CalTab"): # tre riquadri: grezza, calibrata, tab Calibrazione
|
|
||||||
for base in ("zoomMeno", "zoomAdatta", "zoomPiu", "zoomPct"):
|
"parametri", "salvaParametri", "quadrato", "acquisisci", "caricaImmagine", "fileImmagine",
|
||||||
assert base + suf in ids, base + suf
|
|
||||||
assert "zoomata" in PAGINA
|
"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
|
# ogni id usato dal JS con $("...") esiste nel markup
|
||||||
# la barra e' sorella del palco che scorre (non dentro): resta ferma durante lo scorrimento
|
|
||||||
assert PAGINA.count('<div class="visore"') == 3
|
usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA))
|
||||||
assert PAGINA.count('<div class="barraZoom">') == 3
|
|
||||||
|
assert usati <= ids, usati - ids
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_sezioni_e_testi_italiani():
|
def test_sezioni_e_testi_italiani():
|
||||||
|
|||||||
@@ -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