Pannello visione: zoom, Adatta e trascinamento sull'immagine

Refs #115

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 16:54:13 +01:00
parent 7a1df0bca4
commit 4a11d2595f
5 changed files with 292 additions and 3 deletions
@@ -41,6 +41,13 @@ function visionExecute() {
looping: false, // an in-line loop is running
gen: 0, // bumped by «Registra»: answers of older requests are dropped
// #115: zoom del riquadro immagine (stesso schema del wizard DXF, #113). zoom = px schermo per px immagine;
// fit = zoom di «Adatta»; w/h = dimensioni naturali; auto = la vista e' ad Adatta (segue il resize del riquadro)
z: { zoom: 1, fit: 1, w: 0, h: 0, auto: true },
zPanning: false, // #115: trascinamento in corso (cursore grabbing)
_zPan: null,
_zRO: null,
// #97: start on a frame already acquired and analysed; hook visibility and exit.
init() {
if (!this.hasCamera) return;
@@ -263,6 +270,115 @@ function visionExecute() {
});
},
// #115: load dell'immagine (anche a ogni frame in linea): dimensioni naturali, fit solo se serve, poi overlay
onImgLoad() {
const img = this.$refs.img;
if (img && img.naturalWidth) {
const z = this.z;
const changed = z.w !== img.naturalWidth || z.h !== img.naturalHeight;
z.w = img.naturalWidth;
z.h = img.naturalHeight;
// dimensioni uguali e vista ingrandita: il frame nuovo resta ingrandito (in linea ogni frame ricarica l'immagine)
if (changed || z.auto) this.zFit();
this._zObserve();
}
this.drawOverlay();
},
// #115: osservatore di resize sul viewport: se la vista e' ad Adatta (o il riquadro era nascosto) rifa il fit
_zObserve() {
const vp = this.$refs.vp;
if (!vp || this._zRO || typeof ResizeObserver === 'undefined') return;
this._zRO = new ResizeObserver(() => { if (this.z.w > 0 && this.z.auto) this.zFit(); });
this._zRO.observe(vp);
},
// #115: area utile del viewport; offset meno i bordi (client* escludono le barre di scorrimento che spariranno dopo il fit, come #113)
_zArea() {
const vp = this.$refs.vp;
if (!vp) return { cw: 0, ch: 0 };
let cw = vp.clientWidth, ch = vp.clientHeight;
if (vp.offsetWidth && typeof getComputedStyle === 'function') {
const cs = getComputedStyle(vp);
cw = vp.offsetWidth - (parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth) || 0);
ch = vp.offsetHeight - (parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) || 0);
}
return { cw, ch };
},
// #115: Adatta = tutta l'immagine nel riquadro (come il fit di #96)
zFit() {
const vp = this.$refs.vp, z = this.z;
if (!vp || !z.w || !z.h) return;
const a = this._zArea();
if (!(a.cw > 0) || !(a.ch > 0)) return; // riquadro nascosto o vuoto: niente, ci pensa l'osservatore
const f = Math.min(a.cw / z.w, a.ch / z.h);
z.fit = f;
z.zoom = f;
z.auto = true;
vp.scrollLeft = 0;
vp.scrollTop = 0;
},
// #115: zoom di un fattore tenendo fermo il punto (cx, cy) del viewport (default: il centro)
zoomBy(f, cx, cy) {
const vp = this.$refs.vp, z = this.z;
if (!vp || !z.w) return;
const old = z.zoom;
const nz = Math.min(Math.max(8, z.fit), Math.max(z.fit * 0.5, old * f));
if (nz === old) return;
if (cx == null) cx = vp.clientWidth / 2;
if (cy == null) cy = vp.clientHeight / 2;
const left = vp.scrollLeft, top = vp.scrollTop;
z.zoom = nz;
z.auto = false;
const apply = () => {
vp.scrollLeft = (left + cx) * nz / old - cx;
vp.scrollTop = (top + cy) * nz / old - cy;
};
if (typeof this.$nextTick === 'function') this.$nextTick(apply); else apply();
},
// #115: Ctrl+rotella = zoom sul punto del cursore; senza Ctrl la pagina scorre come sempre
zWheel(ev) {
if (!ev.ctrlKey) return;
ev.preventDefault();
const vp = this.$refs.vp;
let cx, cy;
if (vp && vp.getBoundingClientRect) {
const r = vp.getBoundingClientRect();
cx = ev.clientX - r.left;
cy = ev.clientY - r.top;
}
this.zoomBy(ev.deltaY < 0 ? 1.25 : 1 / 1.25, cx, cy);
},
zCanPan() { const z = this.z; return z.w > 0 && z.zoom > z.fit * 1.0001; },
zCursor() { return this.zCanPan() ? (this.zPanning ? 'grabbing' : 'grab') : 'default'; },
zPct() { return Math.round(this.z.zoom * 100) + '%'; },
// #115: dimensione del wrapper: naturale x zoom; prima del load riempie il riquadro
zSize() {
const z = this.z;
return z.w ? 'width:' + (z.w * z.zoom) + 'px;height:' + (z.h * z.zoom) + 'px' : 'width:100%;height:100%';
},
// #115: trascinamento = scorrimento del viewport (solo se ingrandito)
zPanDown(ev) {
const vp = this.$refs.vp;
if (!vp || !this.zCanPan() || (ev.pointerType === 'mouse' && ev.button)) return;
if (ev.preventDefault) ev.preventDefault();
this._zPan = { x: ev.clientX, y: ev.clientY, left: vp.scrollLeft, top: vp.scrollTop };
this.zPanning = true;
try { vp.setPointerCapture(ev.pointerId); } catch (e) { /* puntatore gia' rilasciato */ }
},
zPanMove(ev) {
const vp = this.$refs.vp, p = this._zPan;
if (!p || !vp) return;
vp.scrollLeft = p.left - (ev.clientX - p.x);
vp.scrollTop = p.top - (ev.clientY - p.y);
},
zPanEnd() { this._zPan = null; this.zPanning = false; },
statusClass(s) {
// Same palette as the measurement markers of the manual flow.
if (s === 'pass') return 'bg-measure-pass/20 text-measure-pass border border-measure-pass/40';