Pannello visione operatore: zoom, overlay e Salva passano a <tmf-viewer>
Refs #122 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,13 +38,6 @@ 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() {
|
||||
// #120: «Rimisura»/nuovo ciclo (task-result-reset): il pezzo va registrato di nuovo, anche senza camera
|
||||
@@ -53,8 +46,6 @@ function visionExecute() {
|
||||
this.preview = !!this.result;
|
||||
this.kick();
|
||||
});
|
||||
// #121: l'overlay SVG si ridisegna a ogni cambio di zoom (spessore e croci costanti a video)
|
||||
if (typeof this.$watch === 'function') this.$watch('z.zoom', () => this.drawOverlay());
|
||||
if (!this.hasCamera) return;
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) this.kick(); });
|
||||
const leave = () => { this.leaving = true; };
|
||||
@@ -132,7 +123,6 @@ function visionExecute() {
|
||||
this.result = body;
|
||||
this.preview = true;
|
||||
this.saved = false;
|
||||
this.$nextTick(() => this.drawOverlay());
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (gen === this.gen) this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
@@ -194,8 +184,6 @@ function visionExecute() {
|
||||
this.result = body;
|
||||
// #120: senza camera il file scelto e' salvato subito (nessun «Registra»): stesso evento
|
||||
if (!this.hasCamera && body.vision_result_id) this._emitRecorded();
|
||||
// Draw once Alpine has rendered the svg.
|
||||
this.$nextTick(() => this.drawOverlay());
|
||||
} catch (e) {
|
||||
this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
} finally {
|
||||
@@ -209,13 +197,6 @@ function visionExecute() {
|
||||
window.dispatchEvent(new CustomEvent('vision-recorded', { detail: { measurements: ms } }));
|
||||
},
|
||||
|
||||
// #119: «Salva» = frame grezzo PNG dell'immagine mostrata, senza disegni (save_image.js)
|
||||
saveImage() {
|
||||
const cfg = window.__visionCfg || {};
|
||||
if (!this.shownImage || typeof tmfSaveImage !== 'function') return Promise.resolve(false);
|
||||
return tmfSaveImage(this.shownImage, cfg.taskName || 'immagine');
|
||||
},
|
||||
|
||||
// Station camera (#72): the Flask proxy answers a PNG, which goes through
|
||||
// acquire() like a picked file. Errors show as the execute ones do.
|
||||
// #97: sends task_id (the task's camera wins over the station's); with a camera
|
||||
@@ -297,114 +278,7 @@ 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; },
|
||||
// #122: zoom, pan, resize e Salva li gestisce <tmf-viewer> (static/js/image_viewer.js)
|
||||
|
||||
// #118: valore della cella: una quota non misurata (null) e' «NaN», mai Number(null) = 0
|
||||
fmtValue(v, unit) {
|
||||
@@ -414,19 +288,15 @@ function visionExecute() {
|
||||
// #117: tolleranza a 3 decimali, trattino se manca (come x-text="...?.toFixed(3) || '—'" dei calibri, ma 0 resta 0.000)
|
||||
fmtTol(v) { return v == null ? '\u2014' : Number(v).toFixed(3); },
|
||||
|
||||
// #116: etichette «nome valore unita» agganciate alla geometria dello strumento; posizione in % delle
|
||||
// dimensioni naturali dell'immagine, cosi' seguono lo zoom (#115) senza ingrandirsi
|
||||
get overlayLabels() {
|
||||
// #119 #122: nome proposto da «Salva» (titolo del task) per <tmf-viewer save-name>
|
||||
get saveName() { return (window.__visionCfg || {}).taskName || 'immagine'; },
|
||||
|
||||
// #116 #122: etichette «nome valore unita» per <tmf-viewer>: x/y in px logici dell'immagine
|
||||
// (il componente le posiziona in % e le tiene a dimensione costante a ogni zoom)
|
||||
get viewerLabels() {
|
||||
if (typeof VisionOverlay === 'undefined' || !this.result) return [];
|
||||
const img = this.$refs && this.$refs.img;
|
||||
const w = this.z.w || (img && img.naturalWidth) || 0;
|
||||
const h = this.z.h || (img && img.naturalHeight) || 0;
|
||||
if (!w || !h) return [];
|
||||
return VisionOverlay.labels(this.rows, this.result.overlay || []).map((l) => ({
|
||||
key: l.key,
|
||||
text: l.text,
|
||||
cls: this.labelClass(l.status),
|
||||
style: 'left:' + (l.x / w * 100) + '%;top:' + (l.y / h * 100) + '%;transform:translate(8px,-50%)',
|
||||
key: l.key, x: l.x, y: l.y, text: l.text, cls: this.labelClass(l.status),
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -445,15 +315,5 @@ function visionExecute() {
|
||||
if (s === 'warning') return 'bg-amber-100 dark:bg-amber-900/30 text-measure-warning border border-measure-warning/40';
|
||||
return 'bg-[var(--bg-secondary)] text-[var(--text-muted)] border border-[var(--border-color)]';
|
||||
},
|
||||
|
||||
// #121: overlay SVG (VisionOverlay.svg) nel wrapper dello zoom: viewBox = dimensioni naturali, quindi
|
||||
// le coordinate (px immagine) valgono cosi' come sono; spessore e croci restano costanti a ogni zoom
|
||||
drawOverlay() {
|
||||
const img = this.$refs.img, sv = this.$refs.overlay;
|
||||
if (!sv) return;
|
||||
if (img && img.naturalWidth) sv.setAttribute('viewBox', '0 0 ' + img.naturalWidth + ' ' + img.naturalHeight);
|
||||
const ok = typeof VisionOverlay !== 'undefined' && typeof VisionOverlay.svg === 'function';
|
||||
sv.innerHTML = ok && this.result ? VisionOverlay.svg(this.result.overlay || [], { zoom: this.z.zoom }) : '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user