Wizard DXF: zoom, Adatta e pan sulle anteprime immagine e DXF. Refs #113
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
_srcSeq: 0, // #105: una risposta di /source arrivata dopo una lettura piu' recente si scarta
|
||||
_dxfPickSeq: 0, // #105: sale a ogni scelta manuale del DXF; i file della ricetta arrivati dopo si scartano
|
||||
_imgPickSeq: 0, // #105: idem per l'immagine (_setImage)
|
||||
// #113: zoom delle due anteprime (img = immagine caricata/acquisita, dxf = disegno); w,h = misura del contenuto (px immagine; mm del viewBox per il DXF)
|
||||
views: { img: { zoom: 1, fit: 1, w: 0, h: 0 }, dxf: { zoom: 1, fit: 1, w: 0, h: 0 } },
|
||||
viewPanKey: '', // #113: anteprima in trascinamento ('' = nessuna)
|
||||
_viewPan: null, // #113: {k, x, y, left, top} al pointerdown
|
||||
_viewRO: {}, // #113: ResizeObserver per chiave, rifanno Adatta se il viewport cambia larghezza
|
||||
|
||||
// #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone)
|
||||
init() {
|
||||
@@ -200,6 +205,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.imageSrc = file ? URL.createObjectURL(file) : '';
|
||||
this.imagePx = null;
|
||||
this.imageBroken = false;
|
||||
this.vReset('img'); // #113: nuova immagine, vista da rifare
|
||||
this.result = null;
|
||||
this.tested = false; // #104
|
||||
this.resPx = null; // #104
|
||||
@@ -211,10 +217,13 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
onImagePreviewLoad(el) {
|
||||
this.imageBroken = false;
|
||||
this.imagePx = el && el.naturalWidth ? { w: el.naturalWidth, h: el.naturalHeight } : null;
|
||||
if (this.imagePx) this.vSetContent('img', this.imagePx.w, this.imagePx.h); // #113: Adatta al caricamento
|
||||
else this.vReset('img');
|
||||
},
|
||||
onImagePreviewError() {
|
||||
this.imageBroken = !!this.imageSrc;
|
||||
this.imagePx = null;
|
||||
this.vReset('img'); // #113
|
||||
},
|
||||
// #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati)
|
||||
get imagePreviewUrl() { return this.imageSrc; },
|
||||
@@ -247,6 +256,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
async loadDxfPreview() {
|
||||
const seq = ++this._dxfPreviewSeq;
|
||||
this.dxfPreview = null;
|
||||
this.vReset('dxf'); // #113: nuova anteprima in arrivo
|
||||
this.dxfPreviewError = '';
|
||||
this.dxfPreviewBusy = false;
|
||||
if (!this.dxfFile) return; // file tolto: tutto azzerato
|
||||
@@ -260,7 +270,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
if (seq !== this._dxfPreviewSeq) return; // file cambiato nel frattempo
|
||||
if (!resp.ok || body.error) this.dxfPreviewError = this._errorText(body);
|
||||
else this.dxfPreview = body;
|
||||
else {
|
||||
this.dxfPreview = body;
|
||||
const vb = this.dxfPreviewViewBox().split(' ').map(Number); // #113: misura del viewBox (mm) per lo zoom
|
||||
this.vSetContent('dxf', vb[2], vb[3]);
|
||||
}
|
||||
} catch (e) {
|
||||
if (seq !== this._dxfPreviewSeq) return;
|
||||
this.dxfPreviewError = this.msgs.requestFailed || String(e);
|
||||
@@ -463,6 +477,82 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.fitZoom = z;
|
||||
vp.scrollLeft = 0; vp.scrollTop = 0;
|
||||
},
|
||||
// #113: zoom per chiave ('img' | 'dxf'), stessa logica di #104 ma con stato in views[k]. Viewport fisso di 240px.
|
||||
_vpEl(k) { return this.$refs ? this.$refs[k === 'img' ? 'vpImg' : 'vpDxf'] : null; },
|
||||
_vLimits(k) {
|
||||
const v = this.views[k];
|
||||
// img: pixel nativi 5 %..800 %; dxf: da meta' a 50 volte Adatta
|
||||
return k === 'img' ? [0.05, 8] : [v.fit * 0.5, v.fit * 50];
|
||||
},
|
||||
vPct(k) {
|
||||
const v = this.views[k];
|
||||
return Math.round((k === 'img' ? v.zoom : (v.fit ? v.zoom / v.fit : 1)) * 100) + ' %';
|
||||
},
|
||||
vW(k) { const v = this.views[k]; return v.w * v.zoom; },
|
||||
vH(k) { const v = this.views[k]; return v.h * v.zoom; },
|
||||
vCanPan(k) { const v = this.views[k]; return v.w > 0 && v.zoom > v.fit * 1.0001; },
|
||||
vCursor(k) { return this.vCanPan(k) ? (this.viewPanKey === k ? 'grabbing' : 'grab') : 'default'; },
|
||||
// #113: nessun contenuto: zoom di nuovo a 1 e misura azzerata (il wrapper sparisce)
|
||||
vReset(k) { this.views[k] = { zoom: 1, fit: 1, w: 0, h: 0 }; },
|
||||
// #113: nuovo contenuto: vista da Adatta (appena il viewport ha una larghezza) e osservatore di resize
|
||||
vSetContent(k, w, h) {
|
||||
if (!(w > 0) || !(h > 0)) { this.vReset(k); return; }
|
||||
this.views[k] = { zoom: 1, fit: 1, w, h };
|
||||
const run = () => { this._vObserve(k); this.vFit(k); };
|
||||
if (typeof this.$nextTick === 'function') this.$nextTick(run); else run();
|
||||
},
|
||||
// #113: se il viewport era nascosto (larghezza 0) o cambia larghezza, rifa Adatta finche' la vista e' ad Adatta
|
||||
_vObserve(k) {
|
||||
const vp = this._vpEl(k);
|
||||
if (!vp || this._viewRO[k] || typeof ResizeObserver === 'undefined') return;
|
||||
this._viewRO[k] = new ResizeObserver(() => {
|
||||
const v = this.views[k];
|
||||
if (v.w > 0 && Math.abs(v.zoom - v.fit) < 1e-9 * Math.max(1, v.fit)) this.vFit(k);
|
||||
});
|
||||
this._viewRO[k].observe(vp);
|
||||
},
|
||||
vFit(k) {
|
||||
const vp = this._vpEl(k), v = this.views[k];
|
||||
if (!vp || !v.w || !v.h || !vp.clientWidth) return; // nascosto o vuoto: niente
|
||||
let z = Math.min(vp.clientWidth / v.w, 240 / v.h);
|
||||
if (k === 'img') z = Math.min(8, Math.max(0.05, z));
|
||||
v.zoom = z;
|
||||
v.fit = z;
|
||||
vp.scrollLeft = 0; vp.scrollTop = 0;
|
||||
},
|
||||
vZoomBy(k, f) {
|
||||
const vp = this._vpEl(k), v = this.views[k];
|
||||
if (!v.w) return;
|
||||
const old = v.zoom, lim = this._vLimits(k);
|
||||
const z = Math.min(lim[1], Math.max(lim[0], old * f));
|
||||
if (z === old) return;
|
||||
// centro visibile invariato, come zoomBy (#104)
|
||||
let cx = 0, cy = 0;
|
||||
if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; }
|
||||
v.zoom = z;
|
||||
if (vp && typeof this.$nextTick === 'function') this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; });
|
||||
},
|
||||
vWheel(k, ev) {
|
||||
if (!ev.ctrlKey) return;
|
||||
ev.preventDefault();
|
||||
this.vZoomBy(k, ev.deltaY < 0 ? 1.25 : 1 / 1.25);
|
||||
},
|
||||
vPanDown(k, ev) {
|
||||
const vp = this._vpEl(k);
|
||||
if (!vp || (ev.pointerType === 'mouse' && ev.button)) return;
|
||||
if (ev.preventDefault) ev.preventDefault();
|
||||
this._viewPan = { k, x: ev.clientX, y: ev.clientY, left: vp.scrollLeft, top: vp.scrollTop };
|
||||
this.viewPanKey = k;
|
||||
try { vp.setPointerCapture(ev.pointerId); } catch (e) { /* puntatore gia' rilasciato */ }
|
||||
},
|
||||
vPanMove(k, ev) {
|
||||
const vp = this._vpEl(k), p = this._viewPan;
|
||||
if (!p || p.k !== k || !vp) return;
|
||||
vp.scrollLeft = p.left - (ev.clientX - p.x);
|
||||
vp.scrollTop = p.top - (ev.clientY - p.y);
|
||||
},
|
||||
vPanEnd() { this._viewPan = null; this.viewPanKey = ''; },
|
||||
|
||||
// #104: Ctrl+rotella = zoom
|
||||
onWheel(ev) {
|
||||
if (!ev.ctrlKey) return;
|
||||
|
||||
Reference in New Issue
Block a user