Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34c251aa90 | |||
| c70d29d6d7 | |||
| 96aca975d0 | |||
| b3c56ef5b5 | |||
| 9111eb2741 | |||
| b46a70347f | |||
| 155abfd15c | |||
| 2c2d601495 |
@@ -78,6 +78,21 @@ 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 */
|
||||||
|
.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; }
|
||||||
.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%; }
|
||||||
@@ -164,6 +179,7 @@ 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">
|
<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>
|
<div class="cornice" id="cornice" hidden>
|
||||||
@@ -171,9 +187,12 @@ PAGINA = r"""<!doctype html>
|
|||||||
<div id="mirino" class="mirino" hidden></div>
|
<div id="mirino" class="mirino" hidden></div>
|
||||||
</div>
|
</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 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">
|
<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>
|
<div class="cornice" id="corniceCal" hidden>
|
||||||
@@ -181,6 +200,8 @@ PAGINA = r"""<!doctype html>
|
|||||||
<div id="mirinoCal" class="mirino" hidden></div>
|
<div id="mirinoCal" class="mirino" hidden></div>
|
||||||
</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>
|
||||||
@@ -220,6 +241,12 @@ PAGINA = r"""<!doctype html>
|
|||||||
<section class="scheda" id="sezioneParametri">
|
<section class="scheda" id="sezioneParametri">
|
||||||
<h2>Parametri camera</h2>
|
<h2>Parametri camera</h2>
|
||||||
<div id="parametri"><div class="nota">-</div></div>
|
<div id="parametri"><div class="nota">-</div></div>
|
||||||
|
<!-- #110: specchiatura per sorgente, applicata dal servizio a ogni frame -->
|
||||||
|
<div class="riga" id="specchiaRiga" style="margin-top:10px">
|
||||||
|
<label style="margin:0 0 6px"><input id="specchiaO" type="checkbox" disabled> Specchia orizzontale</label>
|
||||||
|
<label style="margin:0 0 6px"><input id="specchiaV" type="checkbox" disabled> Specchia verticale</label>
|
||||||
|
</div>
|
||||||
|
<div id="msgSpecchia" class="messaggio"></div>
|
||||||
<div class="riga" id="salvaParametriRiga" hidden>
|
<div class="riga" id="salvaParametriRiga" hidden>
|
||||||
<button id="salvaParametri" class="secondario">Salva come predefiniti</button>
|
<button id="salvaParametri" class="secondario">Salva come predefiniti</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,12 +260,15 @@ 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">
|
<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>
|
<div class="cornice" id="corniceCalTab" hidden>
|
||||||
<img id="immagineCalTab" alt="Immagine grezza della sorgente">
|
<img id="immagineCalTab" alt="Immagine grezza della sorgente">
|
||||||
</div>
|
</div>
|
||||||
</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 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>
|
||||||
<label style="margin:0 0 6px"><input id="bloccaImg" type="checkbox"> Blocca immagine</label>
|
<label style="margin:0 0 6px"><input id="bloccaImg" type="checkbox"> Blocca immagine</label>
|
||||||
@@ -463,6 +493,7 @@ function cambioSorgente(salva) {
|
|||||||
if (A.bloccata) rilasciaBlocco(); // #94: casella spenta e DELETE sulla sorgente vecchia (prima di svuotare i riquadri)
|
if (A.bloccata) rilasciaBlocco(); // #94: casella spenta e DELETE sulla sorgente vecchia (prima di svuotare i riquadri)
|
||||||
azzeraIndicatori();
|
azzeraIndicatori();
|
||||||
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
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
|
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
||||||
annullaRimuovi();
|
annullaRimuovi();
|
||||||
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
||||||
@@ -470,6 +501,7 @@ function cambioSorgente(salva) {
|
|||||||
dire("msgElenco", ""); // #91
|
dire("msgElenco", ""); // #91
|
||||||
aggiornaStato();
|
aggiornaStato();
|
||||||
caricaParametri();
|
caricaParametri();
|
||||||
|
caricaOrientamento(); // #110: specchiatura della nuova sorgente
|
||||||
if (era) avviaAnteprima();
|
if (era) avviaAnteprima();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,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
|
// #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
|
||||||
|
// 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 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() {
|
||||||
|
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 () { 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); });
|
||||||
|
}
|
||||||
|
creaVisore("acq", "", "cornice", "immagine");
|
||||||
|
creaVisore("cal", "Cal", "corniceCal", "immagineCal");
|
||||||
|
creaVisore("tab", "CalTab", "corniceCalTab", "immagineCalTab");
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
["nitidezza", "saturati", "media", "dimensioni", "nitidezzaMax"].forEach(function (i) { $(i).textContent = "-"; });
|
["nitidezza", "saturati", "media", "dimensioni", "nitidezzaMax"].forEach(function (i) { $(i).textContent = "-"; });
|
||||||
@@ -587,7 +744,7 @@ function aggiornaIndicatori(h) {
|
|||||||
$("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0);
|
$("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0);
|
||||||
// #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano
|
// #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano
|
||||||
var nw = parseInt(w, 10), na = parseInt(a, 10);
|
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
|
$("raddrizzato").textContent = "frame grezzo"; // #92: l'indicatore viene sempre dal frame grezzo
|
||||||
}
|
}
|
||||||
@@ -632,7 +789,7 @@ async function ciclo(g) {
|
|||||||
while (A.bloccando) await pausa(20); // #94: durante «Blocca immagine» nessuna nuova anteprima
|
while (A.bloccando) await pausa(20); // #94: durante «Blocca immagine» nessuna nuova anteprima
|
||||||
if (!A.attiva || g !== A.gen) return;
|
if (!A.attiva || g !== A.gen) return;
|
||||||
var t0 = Date.now();
|
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 (g !== A.gen) return;
|
||||||
if (esito === "fatale") { fermaAnteprima(); return; }
|
if (esito === "fatale") { fermaAnteprima(); return; }
|
||||||
await pausa(esito === "errore" ? 1500 : Math.max(100 - (Date.now() - t0), 0));
|
await pausa(esito === "errore" ? 1500 : Math.max(100 - (Date.now() - t0), 0));
|
||||||
@@ -793,6 +950,80 @@ async function caricaParametri() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #110: specchiatura per sorgente; gen scarta le risposte di una sorgente ormai lasciata, dati = ultimo stato noto del servizio
|
||||||
|
var O = { gen: 0, dati: { orizzontale: false, verticale: false } };
|
||||||
|
|
||||||
|
function caselleSpecchia(attive) { // #110: abilita o disabilita le due caselle
|
||||||
|
$("specchiaO").disabled = $("specchiaV").disabled = !attive;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function caricaOrientamento() { // #110
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
var g = ++O.gen;
|
||||||
|
dire("msgSpecchia", "");
|
||||||
|
O.dati = { orizzontale: false, verticale: false };
|
||||||
|
$("specchiaO").checked = $("specchiaV").checked = false;
|
||||||
|
caselleSpecchia(false); // spente finche' non si sa lo stato (e senza sorgente)
|
||||||
|
if (!sorgente) return;
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/orientamento?source=" + encodeURIComponent(sorgente));
|
||||||
|
if (g !== O.gen) return;
|
||||||
|
if (!r.ok) { dire("msgSpecchia", await dettaglio(r), "errore"); return; }
|
||||||
|
var j = await r.json();
|
||||||
|
if (g !== O.gen) return;
|
||||||
|
O.dati = { orizzontale: !!j.orizzontale, verticale: !!j.verticale };
|
||||||
|
$("specchiaO").checked = O.dati.orizzontale;
|
||||||
|
$("specchiaV").checked = O.dati.verticale;
|
||||||
|
caselleSpecchia(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (g === O.gen) dire("msgSpecchia", "Servizio non raggiungibile.", "errore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function salvaOrientamento() { // #110: onchange di una casella -> PUT
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
if (!sorgente) return;
|
||||||
|
var g = ++O.gen;
|
||||||
|
var nuovo = { orizzontale: $("specchiaO").checked, verticale: $("specchiaV").checked };
|
||||||
|
caselleSpecchia(false); // durante la PUT niente altri cambi
|
||||||
|
dire("msgSpecchia", "");
|
||||||
|
var rimetti = function () { // #110: casella com'era prima del clic
|
||||||
|
$("specchiaO").checked = O.dati.orizzontale;
|
||||||
|
$("specchiaV").checked = O.dati.verticale;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/orientamento", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ source: sorgente, orizzontale: nuovo.orizzontale, verticale: nuovo.verticale })
|
||||||
|
});
|
||||||
|
if (g !== O.gen) return; // sorgente cambiata: ci pensa caricaOrientamento
|
||||||
|
if (!r.ok) {
|
||||||
|
var d = await dettaglio(r);
|
||||||
|
if (g !== O.gen) return;
|
||||||
|
rimetti();
|
||||||
|
caselleSpecchia(true);
|
||||||
|
dire("msgSpecchia", d, "errore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var j = await r.json();
|
||||||
|
if (g !== O.gen) return;
|
||||||
|
O.dati = nuovo;
|
||||||
|
caselleSpecchia(true);
|
||||||
|
if (j.avviso) dire("msgSpecchia", j.avviso, "avviso"); // es. calibrazione da rifare
|
||||||
|
else dire("msgSpecchia", "Specchiatura salvata.", "ok");
|
||||||
|
if (!A.attiva) { // anteprima ferma: un frame solo per vedere l'effetto (se e' attiva, il prossimo esce gia' specchiato)
|
||||||
|
while (A.inVolo) await pausa(50);
|
||||||
|
if (g === O.gen && !A.attiva) unaAnteprima(A.lato);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (g !== O.gen) return;
|
||||||
|
rimetti();
|
||||||
|
caselleSpecchia(true);
|
||||||
|
dire("msgSpecchia", "Servizio non raggiungibile.", "errore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function costruisciParametri(j) {
|
function costruisciParametri(j) {
|
||||||
var box = $("parametri");
|
var box = $("parametri");
|
||||||
box.textContent = "";
|
box.textContent = "";
|
||||||
@@ -1393,6 +1624,7 @@ $("acquisisci").onclick = calcola;
|
|||||||
$("salva").onclick = salvaNome;
|
$("salva").onclick = salvaNome;
|
||||||
$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); };
|
$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); };
|
||||||
$("sorgente").onchange = function () { dire("msgSorgente", ""); cambioSorgente(true); }; // #92
|
$("sorgente").onchange = function () { dire("msgSorgente", ""); cambioSorgente(true); }; // #92
|
||||||
|
$("specchiaO").onchange = $("specchiaV").onchange = salvaOrientamento; // #110
|
||||||
scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata
|
scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata
|
||||||
caricaSorgenti();
|
caricaSorgenti();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,7 +61,17 @@ Acquisizione (#87): anteprima, esposizione/guadagno, settaggi di default:
|
|||||||
POST /camera/parametri/salva json {source} -> {source, salvati, file}
|
POST /camera/parametri/salva json {source} -> {source, salvati, file}
|
||||||
scrive <config-dir>/settaggi/<nome>-<sha1>.json; alla prima apertura
|
scrive <config-dir>/settaggi/<nome>-<sha1>.json; alla prima apertura
|
||||||
della camera nel servizio il file, se c'e', viene ricaricato.
|
della camera nel servizio il file, se c'e', viene ricaricato.
|
||||||
Le calibrazioni stanno in <config-dir>/calibrazioni, le attive in attive.json (default
|
#110: specchiatura per sorgente, in software (cv2.flip) su OGNI frame letto dalla
|
||||||
|
sorgente, prima di raddrizzatura/calibrazione (anteprima, grab, calibrazione da camera,
|
||||||
|
copia bloccata); non tocca l'immagine caricata a mano in /camera/calibrate.
|
||||||
|
GET /camera/orientamento?source=S -> {source, orizzontale, verticale} (default false/false);
|
||||||
|
400 se manca source
|
||||||
|
PUT /camera/orientamento json {source, orizzontale: bool, verticale: bool} -> come GET +
|
||||||
|
"avviso" (stringa o null: la calibrazione attiva ha una mappa fatta con l'orientamento
|
||||||
|
di prima e va rifatta). 400 se source manca o i due campi non sono bool. Salva subito
|
||||||
|
su disco; scarta i frame tenuti (anteprima/blocca).
|
||||||
|
Le calibrazioni stanno in <config-dir>/calibrazioni, le attive in attive.json, l'orientamento
|
||||||
|
per sorgente in orientamento.json (#110) (default
|
||||||
%ProgramData%/Tielogic/TieMeasureFlow/acquisitore su Windows, ./acquisitore altrove).
|
%ProgramData%/Tielogic/TieMeasureFlow/acquisitore su Windows, ./acquisitore altrove).
|
||||||
|
|
||||||
Serve perche' vs-gui ascolta solo su 127.0.0.1 e il container `client` non lo
|
Serve perche' vs-gui ascolta solo su 127.0.0.1 e il container `client` non lo
|
||||||
@@ -87,6 +97,7 @@ camera a chiunque raggiunga la porta: solo su rete isolata.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json # #110: orientamento.json
|
||||||
import logging
|
import logging
|
||||||
import math # #95: mm/px finito nella calibrazione manuale
|
import math # #95: mm/px finito nella calibrazione manuale
|
||||||
import os
|
import os
|
||||||
@@ -167,6 +178,36 @@ def create_app(folder: Path | str | None = None,
|
|||||||
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
||||||
calcoli: OrderedDict = OrderedDict() # #91: calcolo_id -> (dati, mappa); gli ultimi _MAX_CALCOLI
|
calcoli: OrderedDict = OrderedDict() # #91: calcolo_id -> (dati, mappa); gli ultimi _MAX_CALCOLI
|
||||||
lock_calcoli = threading.Lock()
|
lock_calcoli = threading.Lock()
|
||||||
|
file_orientamento = archivio.cartella / "orientamento.json" # #110: {sorgente: {orizzontale, verticale}}
|
||||||
|
|
||||||
|
def _carica_orientamenti() -> dict:
|
||||||
|
"""#110: orientamenti salvati; file assente o rotto -> {} (mai eccezione)."""
|
||||||
|
if not file_orientamento.is_file():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
dati = json.loads(file_orientamento.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(dati, dict):
|
||||||
|
raise ValueError("non e' un oggetto JSON")
|
||||||
|
return {n: {"orizzontale": v.get("orizzontale") is True, "verticale": v.get("verticale") is True}
|
||||||
|
for n, v in dati.items() if isinstance(n, str) and isinstance(v, dict)}
|
||||||
|
except (OSError, ValueError) as e:
|
||||||
|
_log.warning("orientamento non caricato (%s): %s", file_orientamento, e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
orientamenti: dict = _carica_orientamenti() # #110: sorgente -> {"orizzontale": bool, "verticale": bool}; protetto da `lock`
|
||||||
|
|
||||||
|
def _specchia(nome: str, frame):
|
||||||
|
"""#110: applica l'orientamento della sorgente al frame letto; nessuno -> lo stesso oggetto, senza copia."""
|
||||||
|
with lock:
|
||||||
|
o = orientamenti.get(nome) or {}
|
||||||
|
orizz, vert = o.get("orizzontale", False), o.get("verticale", False)
|
||||||
|
if orizz and vert:
|
||||||
|
return cv2.flip(frame, -1) # entrambi = rotazione di 180 gradi
|
||||||
|
if orizz:
|
||||||
|
return cv2.flip(frame, 1)
|
||||||
|
if vert:
|
||||||
|
return cv2.flip(frame, 0)
|
||||||
|
return frame
|
||||||
|
|
||||||
def _config() -> dict:
|
def _config() -> dict:
|
||||||
if folder is None:
|
if folder is None:
|
||||||
@@ -317,7 +358,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
|
|
||||||
def _grab(nome: str, timeout: int):
|
def _grab(nome: str, timeout: int):
|
||||||
"""Un frame dalla sorgente (apre e tiene aperta); errori camera -> HTTP."""
|
"""Un frame dalla sorgente (apre e tiene aperta); errori camera -> HTTP."""
|
||||||
return _con_sorgente(nome, lambda s: s.grab(timeout)) # #87: corpo in _con_sorgente
|
return _con_sorgente(nome, lambda s: _specchia(nome, s.grab(timeout))) # #87: corpo in _con_sorgente; #110: specchiato prima di tutto il resto
|
||||||
|
|
||||||
@app.post("/camera/grab")
|
@app.post("/camera/grab")
|
||||||
def camera_grab(corpo: dict = Body(default=None)):
|
def camera_grab(corpo: dict = Body(default=None)):
|
||||||
@@ -544,7 +585,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 10000:
|
if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 10000:
|
||||||
raise HTTPException(400, "«timeout_ms» deve essere un intero fra 1 e 10000")
|
raise HTTPException(400, "«timeout_ms» deve essere un intero fra 1 e 10000")
|
||||||
def leggi(s): # #92: il frame grezzo si tiene sotto il lock della sorgente
|
def leggi(s): # #92: il frame grezzo si tiene sotto il lock della sorgente
|
||||||
f = s.grab(timeout)
|
f = _specchia(nome, s.grab(timeout)) # #110: specchiato PRIMA di tenerlo, cosi' blocca/calibrata lo ricevono gia' specchiato
|
||||||
with lock:
|
with lock:
|
||||||
grezzi[nome] = f.copy()
|
grezzi[nome] = f.copy()
|
||||||
return f
|
return f
|
||||||
@@ -686,6 +727,54 @@ def create_app(folder: Path | str | None = None,
|
|||||||
|
|
||||||
return _con_sorgente(nome, salva)
|
return _con_sorgente(nome, salva)
|
||||||
|
|
||||||
|
# ---- #110: specchiatura orizzontale/verticale per sorgente
|
||||||
|
|
||||||
|
def _orientamento_di(nome: str) -> dict:
|
||||||
|
with lock:
|
||||||
|
o = orientamenti.get(nome) or {}
|
||||||
|
return {"source": nome, "orizzontale": bool(o.get("orizzontale")), "verticale": bool(o.get("verticale"))}
|
||||||
|
|
||||||
|
@app.get("/camera/orientamento")
|
||||||
|
def orientamento_leggi(source: str = ""):
|
||||||
|
return _orientamento_di(_sorgente(source))
|
||||||
|
|
||||||
|
@app.put("/camera/orientamento")
|
||||||
|
def orientamento_scrivi(corpo: dict = Body(default=None)):
|
||||||
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
|
nome = _sorgente(corpo.get("source"))
|
||||||
|
orizz, vert = corpo.get("orizzontale"), corpo.get("verticale")
|
||||||
|
if not isinstance(orizz, bool) or not isinstance(vert, bool):
|
||||||
|
raise HTTPException(400, "«orizzontale» e «verticale» devono essere true o false")
|
||||||
|
with lock:
|
||||||
|
prima = orientamenti.get(nome) or {}
|
||||||
|
prima_voce = orientamenti.get(nome) # #110: voce di prima (None = non c'era), per il ripristino se il salvataggio fallisce
|
||||||
|
cambiato = (bool(prima.get("orizzontale")), bool(prima.get("verticale"))) != (orizz, vert)
|
||||||
|
if orizz or vert:
|
||||||
|
orientamenti[nome] = {"orizzontale": orizz, "verticale": vert}
|
||||||
|
else:
|
||||||
|
orientamenti.pop(nome, None) # tutti false: la sorgente esce dal file
|
||||||
|
# scrittura atomica di tutto il dizionario (tmp + replace)
|
||||||
|
try:
|
||||||
|
file_orientamento.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = file_orientamento.with_name(file_orientamento.name + ".tmp")
|
||||||
|
tmp.write_text(json.dumps(orientamenti, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
os.replace(tmp, file_orientamento)
|
||||||
|
except OSError as e:
|
||||||
|
_log.warning("orientamento non salvato (%s): %s", file_orientamento, e)
|
||||||
|
if prima_voce is None: # #110: salvataggio fallito -> in memoria resta l'orientamento di prima
|
||||||
|
orientamenti.pop(nome, None)
|
||||||
|
else:
|
||||||
|
orientamenti[nome] = prima_voce
|
||||||
|
raise HTTPException(500, "salvataggio dell'orientamento non riuscito")
|
||||||
|
# i frame tenuti hanno l'orientamento di prima
|
||||||
|
grezzi.pop(nome, None)
|
||||||
|
bloccati.pop(nome, None)
|
||||||
|
avviso = None
|
||||||
|
if cambiato and archivio.leggi_mappa(nome) is not None:
|
||||||
|
avviso = (f"la calibrazione attiva «{archivio.attiva_di(nome)}» corregge la distorsione "
|
||||||
|
"con l'orientamento di prima: va rifatta")
|
||||||
|
return {**_orientamento_di(nome), "avviso": avviso}
|
||||||
|
|
||||||
@app.get("/calibrazione", response_class=HTMLResponse)
|
@app.get("/calibrazione", response_class=HTMLResponse)
|
||||||
@app.get("/acquisizione", response_class=HTMLResponse) # #87: stessa pagina
|
@app.get("/acquisizione", response_class=HTMLResponse) # #87: stessa pagina
|
||||||
def calibrazione_pagina():
|
def calibrazione_pagina():
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ def test_pagina_blocco_attende_l_anteprima_in_volo():
|
|||||||
assert "k < 150" in blocco and "pausa(20)" in blocco
|
assert "k < 150" in blocco and "pausa(20)" in blocco
|
||||||
assert blocco.index("A.inVolo") < blocco.index('fetch("/camera/blocca"')
|
assert blocco.index("A.inVolo") < blocco.index('fetch("/camera/blocca"')
|
||||||
ciclo = PAGINA.split("async function ciclo")[1].split("function avviaAnteprima")[0]
|
ciclo = PAGINA.split("async function ciclo")[1].split("function avviaAnteprima")[0]
|
||||||
assert "while (A.bloccando)" in ciclo and ciclo.index("A.bloccando") < ciclo.index("unaAnteprima(1280)")
|
# #111: il lato dell'anteprima segue lo zoom (latoAnteprima(), 1280 senza zoom)
|
||||||
|
assert "while (A.bloccando)" in ciclo and ciclo.index("A.bloccando") < ciclo.index("unaAnteprima(latoAnteprima())")
|
||||||
scatta = PAGINA.split("async function scatta")[1].split("// #94: «Blocca immagine»")[0]
|
scatta = PAGINA.split("async function scatta")[1].split("// #94: «Blocca immagine»")[0]
|
||||||
assert "while (A.bloccando)" in scatta and scatta.index("A.bloccando") < scatta.index("unaAnteprima(4096)")
|
assert "while (A.bloccando)" in scatta and scatta.index("A.bloccando") < scatta.index("unaAnteprima(4096)")
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""Test della specchiatura orizzontale/verticale per sorgente (#110): scripts/camera_service.py.
|
||||||
|
|
||||||
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
||||||
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
||||||
|
Copre: GET/PUT /camera/orientamento, effetto su /camera/grab e /camera/anteprima,
|
||||||
|
persistenza al riavvio, file rotto, validazione, scarto dei frame tenuti, avviso
|
||||||
|
sulla calibrazione densa. Sorgente: camera:folder su un'immagine 4x3 asimmetrica.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from test_calibrazione import ( # calibrazione densa reale (#84), per l'avviso
|
||||||
|
SORGENTE as SORGENTE_DENSA, cartella_densa, carica_su, client_denso, salva_con_nome,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["cartella_densa", "client_denso"] # fixture importate, restano usate
|
||||||
|
|
||||||
|
MODULO = Path(__file__).resolve().parents[1] / "camera_service.py"
|
||||||
|
SORGENTE = "camera:folder"
|
||||||
|
|
||||||
|
|
||||||
|
def _carica():
|
||||||
|
spec = importlib.util.spec_from_file_location("camera_service_orient", MODULO)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cartella(tmp_path):
|
||||||
|
# #110: 4x3 tutta nera con UN pixel acceso in alto a sinistra (riga 0, colonna 0)
|
||||||
|
img = tmp_path / "foto"
|
||||||
|
img.mkdir()
|
||||||
|
f = np.zeros((3, 4, 3), np.uint8)
|
||||||
|
f[0, 0] = 255
|
||||||
|
cv2.imwrite(str(img / "a.png"), f)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config(tmp_path):
|
||||||
|
return tmp_path / "cfg"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(cartella, config):
|
||||||
|
mod = _carica()
|
||||||
|
with TestClient(mod.create_app(cartella, config)) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
def _acceso(png: bytes):
|
||||||
|
"""(riga, colonna) del pixel acceso nel PNG; controlla che sia uno solo."""
|
||||||
|
f = cv2.imdecode(np.frombuffer(png, np.uint8), cv2.IMREAD_COLOR)
|
||||||
|
assert f.shape[:2] == (3, 4)
|
||||||
|
r, c = np.nonzero(f.max(axis=2) > 128)
|
||||||
|
assert len(r) == 1
|
||||||
|
return int(r[0]), int(c[0])
|
||||||
|
|
||||||
|
|
||||||
|
def _imposta(c, orizz, vert, source=SORGENTE):
|
||||||
|
return c.put("/camera/orientamento", json={"source": source, "orizzontale": orizz, "verticale": vert})
|
||||||
|
|
||||||
|
|
||||||
|
def _grab(c):
|
||||||
|
r = c.post("/camera/grab", json={"source": SORGENTE})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
return _acceso(r.content)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_senza_file_false_false(client):
|
||||||
|
r = client.get("/camera/orientamento", params={"source": SORGENTE})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"source": SORGENTE, "orizzontale": False, "verticale": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_senza_source_400(client):
|
||||||
|
assert client.get("/camera/orientamento").status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("orizz,vert,atteso", [
|
||||||
|
(False, False, (0, 0)), # originale: alto a sinistra
|
||||||
|
(True, False, (0, 3)), # orizzontale: alto a destra
|
||||||
|
(False, True, (2, 0)), # verticale: basso a sinistra
|
||||||
|
(True, True, (2, 3)), # entrambi: basso a destra (180 gradi)
|
||||||
|
])
|
||||||
|
def test_grab_specchiato(client, orizz, vert, atteso):
|
||||||
|
r = _imposta(client, orizz, vert)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert (r.json()["orizzontale"], r.json()["verticale"]) == (orizz, vert)
|
||||||
|
assert client.get("/camera/orientamento", params={"source": SORGENTE}).json()["orizzontale"] is orizz
|
||||||
|
assert _grab(client) == atteso
|
||||||
|
|
||||||
|
|
||||||
|
def test_tutti_false_toglie_la_sorgente_dal_file(client, config):
|
||||||
|
_imposta(client, True, False)
|
||||||
|
assert SORGENTE in (config / "orientamento.json").read_text(encoding="utf-8")
|
||||||
|
_imposta(client, False, False)
|
||||||
|
assert SORGENTE not in (config / "orientamento.json").read_text(encoding="utf-8")
|
||||||
|
assert _grab(client) == (0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_specchiata_dimensioni_invariate(client):
|
||||||
|
_imposta(client, True, True)
|
||||||
|
r = client.post("/camera/anteprima", json={"source": SORGENTE, "lato_max": 4096})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert (r.headers["x-larghezza"], r.headers["x-altezza"]) == ("4", "3")
|
||||||
|
f = cv2.imdecode(np.frombuffer(r.content, np.uint8), cv2.IMREAD_GRAYSCALE)
|
||||||
|
assert f.shape == (3, 4)
|
||||||
|
# JPEG con perdite: il pixel acceso e' il piu' chiaro, in basso a destra
|
||||||
|
assert np.unravel_index(int(np.argmax(f)), f.shape) == (2, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_persistenza_al_riavvio(cartella, config):
|
||||||
|
mod = _carica()
|
||||||
|
with TestClient(mod.create_app(cartella, config)) as c1:
|
||||||
|
assert _imposta(c1, True, False).status_code == 200
|
||||||
|
with TestClient(_carica().create_app(cartella, config)) as c2:
|
||||||
|
j = c2.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
||||||
|
assert (j["orizzontale"], j["verticale"]) == (True, False)
|
||||||
|
assert _grab(c2) == (0, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_rotto_non_blocca_l_avvio(cartella, config):
|
||||||
|
config.mkdir(parents=True)
|
||||||
|
(config / "orientamento.json").write_text("{non e json", encoding="utf-8")
|
||||||
|
with TestClient(_carica().create_app(cartella, config)) as c:
|
||||||
|
j = c.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
||||||
|
assert (j["orizzontale"], j["verticale"]) == (False, False)
|
||||||
|
assert _imposta(c, False, True).status_code == 200 # e si puo' riscrivere
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_campi_non_bool_400(client):
|
||||||
|
for corpo in ({"source": SORGENTE, "orizzontale": 1, "verticale": False},
|
||||||
|
{"source": SORGENTE, "orizzontale": True, "verticale": "si"},
|
||||||
|
{"source": SORGENTE, "orizzontale": True},
|
||||||
|
{"orizzontale": True, "verticale": True}):
|
||||||
|
assert client.put("/camera/orientamento", json=corpo).status_code == 400, corpo
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_scarta_i_frame_tenuti(client):
|
||||||
|
assert client.post("/camera/anteprima", json={"source": SORGENTE}).status_code == 200
|
||||||
|
assert client.post("/camera/blocca", json={"source": SORGENTE}).status_code == 200
|
||||||
|
_imposta(client, True, False)
|
||||||
|
assert client.post("/camera/blocca", json={"source": SORGENTE}).status_code == 404
|
||||||
|
# col frame tenuto scartato anche l'ultimo grezzo dell'anteprima e' sparito
|
||||||
|
assert client.post("/camera/anteprima/calibrata", json={"source": SORGENTE}).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_avviso_senza_mappa_e_null(client):
|
||||||
|
j = _imposta(client, True, False).json()
|
||||||
|
assert j["avviso"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_avviso_con_mappa_attiva(client_denso):
|
||||||
|
densa = client_denso.post("/camera/calibrate", data={"source": SORGENTE_DENSA, "square_mm": "2"}).json()
|
||||||
|
assert salva_con_nome(client_denso, densa, "Densa").json()["metodo"] == "densa"
|
||||||
|
assert carica_su(client_denso, SORGENTE_DENSA, "Densa").status_code == 200
|
||||||
|
r = _imposta(client_denso, True, False, source=SORGENTE_DENSA)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert "«Densa»" in r.json()["avviso"] and "va rifatta" in r.json()["avviso"]
|
||||||
|
# stesso valore di nuovo: nulla e' cambiato, nessun avviso
|
||||||
|
assert _imposta(client_denso, True, False, source=SORGENTE_DENSA).json()["avviso"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_scrittura_fallita_500_e_orientamento_di_prima(cartella, config, monkeypatch):
|
||||||
|
# #110: se orientamento.json non si scrive, in memoria resta l'orientamento di prima
|
||||||
|
mod = _carica()
|
||||||
|
with TestClient(mod.create_app(cartella, config)) as c:
|
||||||
|
assert _imposta(c, True, False).status_code == 200 # stato di partenza: orizzontale
|
||||||
|
def rotto(*a, **k):
|
||||||
|
raise OSError("disco pieno")
|
||||||
|
monkeypatch.setattr(mod.os, "replace", rotto)
|
||||||
|
assert _imposta(c, False, True).status_code == 500
|
||||||
|
j = c.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
||||||
|
assert (j["orizzontale"], j["verticale"]) == (True, False)
|
||||||
|
assert _grab(c) == (0, 3) # ancora solo orizzontale
|
||||||
|
# sorgente che prima non c'era: la voce non resta
|
||||||
|
assert c.put("/camera/orientamento", json={"source": "altra", "orizzontale": True, "verticale": True}).status_code == 500
|
||||||
|
j = c.get("/camera/orientamento", params={"source": "altra"}).json()
|
||||||
|
assert (j["orizzontale"], j["verticale"]) == (False, False)
|
||||||
@@ -52,3 +52,29 @@ def test_localstorage_protetto():
|
|||||||
# ogni uso di localStorage sta dentro un try
|
# ogni uso di localStorage sta dentro un try
|
||||||
for m in re.finditer(r"localStorage[.]", PAGINA):
|
for m in re.finditer(r"localStorage[.]", PAGINA):
|
||||||
assert "try {" in PAGINA[max(0, m.start() - 200):m.start()]
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user