From 269bbc03caeac790ff6e9897ab602f12498b5170 Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Wed, 28 Oct 2026 21:58:27 +0100 Subject: [PATCH] Wizard DXF: anteprima immagine, anteprima DXF e Prova usano Refs #122 Co-Authored-By: Claude Sonnet 5.5 --- .../flask_app/static/js/dxf_recipe_wizard.js | 193 +----------------- .../templates/maker/task_editor.html | 108 +++------- .../flask_app/tests/js/test_dxf_zoom.js | 170 +++------------ .../flask_app/tests/test_dxf_recipe_routes.py | 20 +- .../flask_app/tests/test_wizard_salva_svg.py | 72 ++++--- 5 files changed, 120 insertions(+), 443 deletions(-) diff --git a/src/frontend/flask_app/static/js/dxf_recipe_wizard.js b/src/frontend/flask_app/static/js/dxf_recipe_wizard.js index e422a7f..0401213 100644 --- a/src/frontend/flask_app/static/js/dxf_recipe_wizard.js +++ b/src/frontend/flask_app/static/js/dxf_recipe_wizard.js @@ -39,11 +39,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) { result: null, // corpo B3 {result, saved, recipe_dir, outputs} imageSource: '', // #104: '' | 'file' | 'camera'; il campo mm/px vale solo per 'file' tested: false, // #104: true dopo una Prova (anche fallita): mostra la sezione del risultato - zoom: 1, // #104: scala dell'immagine di prova (1 = pixel nativi) - fitZoom: 1, // #104: zoom di Adatta (sotto o pari a questo il viewport segue l'immagine) - panning: false, // #104: trascinamento in corso - _pan: null, // #104: {x, y, left, top} al pointerdown - resPx: null, // #104: {w, h} in pixel dell'immagine di prova (per la dimensione del wrapper) + // #122: zoom, Adatta e pan delle tre viste (immagine, DXF, Prova) ora stanno nel componente runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione) runBandMm: null, // #104: banda usata nell'ultima Prova runMinPointsPct: null, // #104: soglia usata nell'ultima Prova @@ -56,11 +52,6 @@ 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() { @@ -204,25 +195,22 @@ 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 this.imageSource = file ? (source || '') : ''; // #104 this._imgPickSeq++; // #105: una scelta piu' recente batte i file della ricetta ancora in arrivo }, - // #102: l' dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile - onImagePreviewLoad(el) { + // #102: il ha caricato l'immagine (evento viewer-load): legge i pixel dal detail + // #122: lo zoom e' del componente; qui restano solo le dimensioni per il riepilogo + onImagePreviewLoad(d) { 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'); + this.imagePx = d && d.width ? { w: d.width, h: d.height } : null; }, + // #102: se il formato non e' mostrabile (sonda del template) compare l'avviso 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; }, @@ -255,7 +243,6 @@ 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 @@ -271,8 +258,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) { if (!resp.ok || body.error) this.dxfPreviewError = this._errorText(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; @@ -373,7 +358,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) { this.error = this._errorText(body); } else { this.result = body; - this.$nextTick(() => this.drawOverlay()); if (save && body.saved) { this.notice = this.msgs.saved || 'Saved'; // Il componente visionRecipe ricarica cartella, file e uscite delle quote @@ -391,7 +375,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) { this.busy = false; // #104: la sezione del risultato compare anche con Prova fallita (immagine a sinistra, errore a destra) this.tested = true; - this.$nextTick(() => this.fit()); }, // Le quattro uscite del confronto, nell'ordine fisso @@ -424,173 +407,9 @@ function dxfRecipeWizard(taskId, csrf, msgs) { return `${mm} mm/px (${this.imageSource === 'file' ? (this.msgs.scaleManual || 'manual') : (this.msgs.scaleStation || 'station calibration')})`; }, - // #104: zoom dell'immagine di prova. Il wrapper e' largo naturalWidth*zoom; il canvas resta in pixel immagine e lo scala il CSS - get zoomPct() { return Math.round(this.zoom * 100) + ' %'; }, - get zoomW() { return this.resPx ? this.resPx.w * this.zoom : 0; }, - get zoomH() { return this.resPx ? this.resPx.h * this.zoom : 0; }, - // #104: altezza massima del viewport = min(70vh, 520px) - _maxVpH() { return Math.min(0.7 * (typeof window !== 'undefined' && window.innerHeight ? window.innerHeight : 800), 520); }, - // #104: con zoom <= Adatta il viewport e' alto come l'immagine scalata (min 200px), altrimenti al massimo - get vpHeight() { - const max = this._maxVpH(); - if (!this.resPx || this.zoom > this.fitZoom * 1.0001) return max; - return Math.min(max, Math.max(200, this.zoomH)); - }, - // #104: l'immagine e' piu' grande dello spazio: si puo' trascinare - get canPan() { return !!this.resPx && this.zoom > this.fitZoom * 1.0001; }, - get panCursor() { return this.canPan ? (this.panning ? 'grabbing' : 'grab') : 'default'; }, - // #104: pan col puntatore (mouse e touch): scroll del viewport di quanto si muove il puntatore - onPanDown(ev) { - const vp = this.$refs.viewport; - if (!vp || (ev.pointerType === 'mouse' && ev.button)) return; - // #104: senza preventDefault Chromium manda pointercancel al primo movimento del mouse e il pan si ferma - if (ev.preventDefault) ev.preventDefault(); - this._pan = { x: ev.clientX, y: ev.clientY, left: vp.scrollLeft, top: vp.scrollTop }; - this.panning = true; - try { vp.setPointerCapture(ev.pointerId); } catch (e) { /* puntatore gia' rilasciato */ } - }, - onPanMove(ev) { - const vp = this.$refs.viewport; - if (!this._pan || !vp) return; - vp.scrollLeft = this._pan.left - (ev.clientX - this._pan.x); - vp.scrollTop = this._pan.top - (ev.clientY - this._pan.y); - }, - onPanEnd() { this._pan = null; this.panning = false; }, - zoomBy(f) { - const vp = this.$refs.viewport; - const old = this.zoom; - const z = Math.min(8, Math.max(0.05, old * f)); - if (z === old) return; - // centro visibile invariato: coordinate immagine del centro prima, scroll dopo il nuovo layout - let cx = 0, cy = 0; - if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; } - this.zoom = z; - this.drawOverlay(); // #121: ridisegno al nuovo zoom - if (vp) this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; }); - }, - // #104: Fit = tutta l'immagine nel viewport (altezza massima, non quella corrente che segue l'immagine) - fit() { - const vp = this.$refs.viewport, img = this.$refs.img; - if (!vp || !img || !img.naturalWidth || !vp.clientWidth) return; - const z = Math.min(8, Math.max(0.05, Math.min(vp.clientWidth / img.naturalWidth, this._maxVpH() / img.naturalHeight))); - this.zoom = z; - this.fitZoom = z; - this.drawOverlay(); // #121: ridisegno al nuovo zoom - 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 - // altezza utile del viewport (senza bordo), ripiego 240; con 240 secco il contenuto sborda e compare lo scroll - let cw = vp.clientWidth, ch = vp.clientHeight || 240; - // #113: Adatta premuto da ingrandito: client* escludono le barre di scorrimento che spariranno dopo il fit; - // l'area vera e' offset* meno i bordi (nei test senza DOM offsetWidth manca e restano client*) - 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) || 240; - } - let z = Math.min(cw / v.w, ch / 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; - ev.preventDefault(); - this.zoomBy(ev.deltaY < 0 ? 1.25 : 1 / 1.25); - }, - // #104: load dell'immagine di prova: dimensioni, overlay e adattamento allo spazio - onResultImgLoad() { - const img = this.$refs.img; - this.resPx = img && img.naturalWidth ? { w: img.naturalWidth, h: img.naturalHeight } : null; - this.drawOverlay(); - this.$nextTick(() => this.fit()); - }, - fmt(v) { return (typeof v === 'number') ? v.toLocaleString(document.documentElement.lang || 'it', { maximumFractionDigits: 3 }) : '-'; }, - - // #121: overlay SVG in pixel immagine (viewBox = dimensioni naturali); tratto a spessore costante a ogni zoom. - // Senza VisionOverlay.svg resta vuoto, senza errori. - drawOverlay() { - const img = this.$refs.img, sv = this.$refs.overlay; - if (!img || !sv || !img.naturalWidth) return; - sv.setAttribute('viewBox', '0 0 ' + img.naturalWidth + ' ' + img.naturalHeight); - const shapes = (this.result && this.result.result && this.result.result.overlay) || []; - const VO = (typeof window !== 'undefined' && window.VisionOverlay) || null; - sv.innerHTML = (VO && typeof VO.svg === 'function') ? VO.svg(shapes, { zoom: this.zoom }) : ''; - }, }; } diff --git a/src/frontend/flask_app/templates/maker/task_editor.html b/src/frontend/flask_app/templates/maker/task_editor.html index fe33193..e98e437 100644 --- a/src/frontend/flask_app/templates/maker/task_editor.html +++ b/src/frontend/flask_app/templates/maker/task_editor.html @@ -730,60 +730,36 @@
- -
-
- - - - {# #119: salva il frame grezzo mostrato (PNG, senza disegni) #} - - -
- -
-
{# #113: tutto in :style, Alpine con stringa sostituisce l'attributo style #} - -
-
+ {# #122: standard (zoom, Adatta, Salva, pan) al posto della barra e del viewport scritti a mano (#113); altezza fissa 240px #} +
{# #122: x-show sul contenitore, mai sul #} + + {# #122: sonda nascosta: il componente non propaga l'errore di caricamento, la sonda si'; se il formato non e' mostrabile compare l'avviso #} +

{{ _('Anteprima non disponibile per questo formato') }}

{# #104: resta a destra anche senza anteprima immagine #} - -
-
- - - - -
- -
-
- - - - -
-
-
+ {# #122: in modo contenuto: lo (viewBox in mm) va nello stage; width/height = misura del viewBox, ora anche Salva (PNG) #} + + + + + +

@@ -814,31 +790,14 @@
{# #104: risultati a destra gia' da 640px, come le altre griglie del wizard #}
- -
-
- - - - {# #119: salva il frame grezzo della Prova (PNG, senza overlay) #} - - -
- -
-
- - {# #121: overlay SVG (tratto a spessore costante a ogni zoom) al posto del canvas; il viewBox lo imposta drawOverlay #} - -
-
-
+ {# #122: al posto di barra, viewport, pan e overlay della Prova (#104 #121); overlay dalle forme del risultato #} + +

@@ -1523,6 +1482,7 @@ {# #121 #} {# #119 #} +{# #122: , dopo vision_overlay.js e save_image.js #} {# #97 #} diff --git a/src/frontend/flask_app/tests/js/test_dxf_zoom.js b/src/frontend/flask_app/tests/js/test_dxf_zoom.js index a057dc5..2815c37 100644 --- a/src/frontend/flask_app/tests/js/test_dxf_zoom.js +++ b/src/frontend/flask_app/tests/js/test_dxf_zoom.js @@ -1,5 +1,8 @@ /** - * #113: zoom, Adatta e pan sulle anteprime del wizard (immagine e DXF), con plain node. + * #122: lo zoom delle tre viste del wizard (immagine, DXF, Prova) e' nel componente . + * Qui si prova cio' che resta nel wizard (dimensioni dal detail di viewer-load, anteprima DXF) + * e che le funzioni di zoom/pan/fit/overlay non ci siano piu'. + * (Il nome del file resta quello di #113 per non rompere i riferimenti.) * * Run from src/frontend/flask_app: node tests/js/test_dxf_zoom.js */ @@ -18,109 +21,35 @@ function check(label, cond, extra) { if (cond) console.log('ok - ' + label); else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); } } -const near = (a, b) => Math.abs(a - b) < 1e-9 * Math.max(1, Math.abs(b)); -// Wizard con viewport finti e $nextTick sincrono -function make(cw) { - const w = dxfRecipeWizard(1, () => 'tok', {}); - const vp = () => ({ clientWidth: cw, clientHeight: 238, scrollLeft: 5, scrollTop: 5 }); - w.$refs = { vpImg: vp(), vpDxf: vp() }; - w.$nextTick = fn => fn(); - return w; -} +const w = dxfRecipeWizard(1, () => 'tok', {}); +w.$nextTick = fn => fn(); -// img: viewport 400 x 238 utili, immagine 2856 x 2848 -let w = make(400); -w.vSetContent('img', 2856, 2848); -check('#113 img: Adatta = clientHeight/2848', near(w.views.img.zoom, 238 / 2848) && near(w.views.img.fit, 238 / 2848), String(w.views.img.zoom)); -check('#113 img: scroll azzerato dopo Adatta', w.$refs.vpImg.scrollLeft === 0 && w.$refs.vpImg.scrollTop === 0); -check('#113 img: percentuale = zoom*100', w.vPct('img') === Math.round(238 / 2848 * 100) + ' %', w.vPct('img')); -const z0 = w.views.img.zoom; -w.vZoomBy('img', 1.25); -check('#113 img: zoomBy moltiplica', near(w.views.img.zoom, z0 * 1.25)); -check('#113 img: dopo zoom si puo trascinare', w.vCanPan('img') === true && w.vCursor('img') === 'grab'); -w.vZoomBy('img', 1e6); -check('#113 img: limite alto 8', w.views.img.zoom === 8); -w.vZoomBy('img', 1e-9); -check('#113 img: limite basso 0.05', w.views.img.zoom === 0.05); -w.vFit('img'); -check('#113 img: Adatta torna al fit', near(w.views.img.zoom, z0) && w.vCanPan('img') === false); -// clientHeight 0 (non misurabile): ripiego 240 -w = make(400); w.$refs.vpImg.clientHeight = 0; w.vSetContent('img', 2856, 2848); -check('#113 img: clientHeight 0 usa 240', near(w.views.img.fit, 240 / 2848)); -w = make(400); w.vSetContent('img', 2856, 2848); -check('#113 img: l\'altra vista non si muove', w.views.dxf.w === 0 && w.views.dxf.zoom === 1); -check('#113 img: wrapper w*zoom', near(w.vW('img'), 2856 * z0) && near(w.vH('img'), 2848 * z0)); +// #122: via tutto lo zoom/pan/fit/overlay del wizard +const gone = ['views', 'viewPanKey', 'zoom', 'fitZoom', 'panning', 'resPx', 'vZoomBy', 'vFit', 'vWheel', 'vPanDown', + 'vPanMove', 'vPanEnd', 'vCursor', 'vPct', 'vW', 'vH', 'vReset', 'vSetContent', 'zoomBy', 'fit', 'onWheel', + 'onPanDown', 'onPanMove', 'onPanEnd', 'onResultImgLoad', 'drawOverlay', 'zoomPct', 'zoomW', 'zoomH', 'vpHeight']; +for (const k of gone) check('#122 tolto dal wizard: ' + k, !(k in w)); +check('#122 sorgente senza $refs di zoom', !/\$refs\./.test(src)); -// dxf: viewBox 20 x 10 mm, viewport 400 x 238 -> fit = min(20, 23.8) = 20 -w = make(400); -w.vSetContent('dxf', 20, 10); -check('#113 dxf: fit = min(vpW/w, clientHeight/h)', near(w.views.dxf.fit, 20), String(w.views.dxf.fit)); -check('#113 dxf: dopo Adatta 100 %', w.vPct('dxf') === '100 %', w.vPct('dxf')); -w.vZoomBy('dxf', 2); -check('#113 dxf: zoom 2x = 200 %', w.vPct('dxf') === '200 %', w.vPct('dxf')); -w.vZoomBy('dxf', 1e9); -check('#113 dxf: si ferma a fit*50', near(w.views.dxf.zoom, 20 * 50) && w.vPct('dxf') === '5000 %', String(w.views.dxf.zoom)); -w.vZoomBy('dxf', 1e-9); -check('#113 dxf: si ferma a fit*0.5', near(w.views.dxf.zoom, 10) && w.vPct('dxf') === '50 %', String(w.views.dxf.zoom)); - -// vFit con viewport largo 0 (nascosto): stato invariato -w = make(0); -w.vSetContent('img', 1000, 500); -check('#113 clientWidth 0: nessun fit, stato invariato', w.views.img.zoom === 1 && w.views.img.fit === 1 && w.views.img.w === 1000); -w.$refs.vpImg.clientWidth = 500; -w.vFit('img'); -check('#113 viewport visibile: il fit parte', near(w.views.img.fit, 238 / 500), String(w.views.img.fit)); - -// Reset: nuova immagine / DXF tolto -w.vReset('img'); -check('#113 reset: contenuto e zoom azzerati', w.views.img.w === 0 && w.views.img.zoom === 1 && w.vCanPan('img') === false); -w.vZoomBy('img', 2); -check('#113 zoomBy senza contenuto: ignorato', w.views.img.zoom === 1); - -// Integrazione: onImagePreviewLoad e _setImage +// onImagePreviewLoad riceve il detail di viewer-load e alimenta il riepilogo global.URL.createObjectURL = () => 'blob:x'; global.URL.revokeObjectURL = () => {}; -w = make(400); w._setImage({ name: 'a.bmp' }, 'file'); -w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 }); -check('#113 onImagePreviewLoad: Adatta all\'arrivo', near(w.views.img.zoom, 0.5) && w.views.img.w === 800, String(w.views.img.zoom)); -w._setImage({ name: 'b.bmp' }, 'file'); -check('#113 _setImage: vista azzerata', w.views.img.w === 0 && w.views.img.zoom === 1); -w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 }); +w.onImagePreviewLoad({ width: 800, height: 240 }); +check('#122 viewer-load: dimensioni nel riepilogo', w.imagePx.w === 800 && w.imagePx.h === 240 && w.imageBroken === false); +check('#122 riepilogo «W × H px»', /800.*240 px/.test(w.imagePreviewSummary), w.imagePreviewSummary); +w.onImagePreviewLoad({ width: 0, height: 0 }); +check('#122 viewer-load senza dimensioni: imagePx null', w.imagePx === null); +w.onImagePreviewLoad({ width: 800, height: 240 }); w.onImagePreviewError(); -check('#113 errore : vista azzerata', w.views.img.w === 0); +check('#122 errore immagine: avviso e dimensioni azzerate', w.imageBroken === true && w.imagePx === null); +w._setImage({ name: 'b.bmp' }, 'file'); +check('#122 nuova immagine: avviso tolto', w.imageBroken === false && w.imagePx === null); +w._setImage(null); +check('#122 immagine tolta: src vuoto', w.imageSrc === '' && w.imagePreviewUrl === ''); -// Wheel: solo con Ctrl -w = make(400); -w.vSetContent('dxf', 20, 10); -let prevented = false; -w.vWheel('dxf', { ctrlKey: false, deltaY: -1, preventDefault() { prevented = true; } }); -check('#113 rotella senza Ctrl: niente zoom', w.views.dxf.zoom === 20 && !prevented); -w.vWheel('dxf', { ctrlKey: true, deltaY: -1, preventDefault() { prevented = true; } }); -check('#113 Ctrl+rotella: zoom avanti', near(w.views.dxf.zoom, 25) && prevented); - -// Pan -w = make(400); -w.vSetContent('dxf', 20, 10); -w.vZoomBy('dxf', 2); -const vp = w.$refs.vpDxf; -vp.scrollLeft = 100; vp.scrollTop = 50; vp.setPointerCapture = () => {}; -w.vPanDown('dxf', { pointerType: 'mouse', button: 0, clientX: 200, clientY: 100, pointerId: 1 }); -w.vPanMove('dxf', { clientX: 150, clientY: 90 }); -check('#113 pan: scroll di quanto si muove il puntatore', vp.scrollLeft === 150 && vp.scrollTop === 60, vp.scrollLeft + ',' + vp.scrollTop); -check('#113 pan: cursore grabbing', w.vCursor('dxf') === 'grabbing'); -w.vPanEnd(); -check('#113 pan finito', w.viewPanKey === '' && w._viewPan === null); - -// #113: Adatta da ingrandito, con barre di scorrimento presenti: area = offset* meno i bordi, non client* -global.getComputedStyle = () => ({ borderLeftWidth: '1px', borderRightWidth: '1px', borderTopWidth: '1px', borderBottomWidth: '1px' }); -w = make(432); w.$refs.vpImg.clientHeight = 228; w.$refs.vpImg.offsetWidth = 444; w.$refs.vpImg.offsetHeight = 240; -w.vSetContent('img', 2464, 2056); -check('#113 Adatta con barre: area senza barre (238/2056)', near(w.views.img.fit, 238 / 2056), String(w.views.img.fit)); -delete global.getComputedStyle; - -// Integrazione DXF: loadDxfPreview imposta la misura del viewBox e fa Adatta; file tolto azzera +// Anteprima DXF: il viewBox resta quello per il (width/height del template) (async () => { const PREVIEW = { contorni: [[[0, 0], [20, 0], [20, 10]]], @@ -129,54 +58,17 @@ delete global.getComputedStyle; }; global.FormData = class { append() {} }; global.fetch = async () => ({ ok: true, json: async () => PREVIEW }); - const wd = make(400); + const wd = dxfRecipeWizard(1, () => 'tok', {}); + wd.$nextTick = fn => fn(); wd.dxfFile = { name: 'a.dxf' }; await wd.loadDxfPreview(); const vb = wd.dxfPreviewViewBox().split(' ').map(Number); - check('#113 loadDxfPreview: misura = viewBox', near(wd.views.dxf.w, vb[2]) && near(wd.views.dxf.h, vb[3]), JSON.stringify(wd.views.dxf)); - check('#113 loadDxfPreview: Adatta = 100 %', wd.vPct('dxf') === '100 %' && wd.views.dxf.fit > 1); + check('#122 DXF: viewBox con margine 5 % (w 22 x h 12)', vb.length === 4 && Math.abs(vb[2] - 22) < 1e-6 && Math.abs(vb[3] - 12) < 1e-6, vb.join(' ')); + check('#122 DXF: anteprima caricata', wd.dxfPreview && wd.dxfPreviewD().startsWith('M 0 0')); wd.dxfFile = null; await wd.loadDxfPreview(); - check('#113 DXF tolto: vista azzerata', wd.views.dxf.w === 0 && wd.views.dxf.zoom === 1); - - // #121: overlay SVG al posto del canvas; ridisegno a ogni cambio di zoom (zoomBy, fit) - { - const calls = []; - global.window = { VisionOverlay: { svg: (shapes, o) => { calls.push({ n: shapes.length, zoom: o.zoom }); return ''; } } }; - const attrs = {}; - const sv = { innerHTML: '', setAttribute: (k, v) => { attrs[k] = v; } }; - const mk = () => { - const x = make(400); - x.$refs = { viewport: { clientWidth: 400, clientHeight: 300, scrollLeft: 0, scrollTop: 0 }, - img: { naturalWidth: 800, naturalHeight: 600 }, overlay: sv }; - x.result = { result: { overlay: [{ type: 'contour', role: 'found', rows: [1], cols: [2] }] } }; - return x; - }; - const wo = mk(); - wo.drawOverlay(); - check('#121 viewBox = dimensioni naturali', attrs.viewBox === '0 0 800 600', attrs.viewBox); - check('#121 innerHTML = VisionOverlay.svg', sv.innerHTML === '' && calls[0].n === 1); - wo.zoomBy(2); - check('#121 zoomBy ridisegna col nuovo zoom', calls[calls.length - 1].zoom === wo.zoom && wo.zoom === 2, JSON.stringify(calls)); - wo.fit(); - check('#121 fit ridisegna col nuovo zoom', calls[calls.length - 1].zoom === wo.zoom, JSON.stringify(calls)); - // senza modulo o senza svg: vuoto, nessun errore - global.window = {}; - sv.innerHTML = 'x'; - let err = null; - try { mk().drawOverlay(); } catch (e) { err = e; } - check('#121 senza VisionOverlay: vuoto senza errori', err === null && sv.innerHTML === '', String(err)); - global.window = { VisionOverlay: {} }; - sv.innerHTML = 'x'; - try { mk().drawOverlay(); } catch (e) { err = e; } - check('#121 senza svg(): vuoto senza errori', err === null && sv.innerHTML === '', String(err)); - // immagine non ancora caricata: non tocca nulla - const wn = mk(); wn.$refs.img.naturalWidth = 0; sv.innerHTML = 'y'; - wn.drawOverlay(); - check('#121 immagine non caricata: nessun disegno', sv.innerHTML === 'y'); - delete global.window; - } + check('#122 DXF tolto: anteprima azzerata', wd.dxfPreview === null && wd.dxfPreviewViewBox() === '0 0 1 1'); if (failures) { console.log(failures + ' test falliti'); process.exit(1); } - console.log('tutti i test #113 ok'); + console.log('tutti i test #122 ok'); })(); diff --git a/src/frontend/flask_app/tests/test_dxf_recipe_routes.py b/src/frontend/flask_app/tests/test_dxf_recipe_routes.py index 9eef3fc..d831e64 100644 --- a/src/frontend/flask_app/tests/test_dxf_recipe_routes.py +++ b/src/frontend/flask_app/tests/test_dxf_recipe_routes.py @@ -354,19 +354,13 @@ class TestPages: assert "@dxf-recipe-saved" in html assert 'x-model="mmPx"' in html # #98: campo mm/px a mano assert "dxfPreviewD()" in html and "view-box.camel" in html # #101: anteprima SVG del DXF - assert "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit - assert 'onPanDown($event)' in html # #104: pan col puntatore + assert "imageSource === 'file'" in html # #104: mm/px solo da file assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine - # #113: zoom, Adatta e pan anche sulle due anteprime (immagine e DXF), per chiave - assert 'x-ref="vpImg"' in html and 'x-ref="vpDxf"' in html - assert "vFit('img')" in html and "vFit('dxf')" in html - assert "vZoomBy('img', 1.25)" in html and "vZoomBy('dxf', 1.25)" in html - assert "vPanDown('img', $event)" in html and "vWheel('dxf', $event)" in html - # #113: Alpine :style a stringa sostituisce l'attributo style: niente style statico sullo stesso tag - assert 'style="margin:auto;flex:none;"' not in html - assert html.count("margin:auto;flex:none;") == 2 and html.count(":style=\"'margin:auto;flex:none;") == 2 - # #113: x-show e :style (display:flex) si pestano: x-ref="vpImg" non sta sul tag con x-show - tag = html[html.rindex("; zoom, Adatta, pan e Salva stanno nel componente + assert html.count(" (#122, prima #113 #119 #121). Legge task_editor.html come testo (come test_vision_panel_fit.py), senza renderlo. """ +import re from pathlib import Path TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "maker" / "task_editor.html" def _src() -> str: - return TEMPLATE.read_text(encoding="utf-8") + # senza commenti Jinja e HTML, che citano e i vecchi nomi + text = TEMPLATE.read_text(encoding="utf-8") + text = re.sub(r"\{#.*?#\}", "", text, flags=re.S) + return re.sub(r"", "", text, flags=re.S) -def _bar_of(src: str, marker: str) -> str: - """Testo della barra zoom che contiene il marker (dal div della barra al primo
).""" +def _tag_of(src: str, marker: str) -> str: + """Tag di apertura che contiene il marker.""" i = src.index(marker) - start = src.rfind('absolute top-2 right-2', 0, i) - return src[start:src.index("
", i)] + start = src.rfind("", i) + 1] -def test_prova_overlay_is_svg_not_canvas(): - # #121: l'overlay della Prova e' un con preserveAspectRatio none, non piu' un canvas +def test_three_viewers_image_dxf_prova(): + # #122: tre : immagine (src), DXF (modo contenuto, con ), Prova (fit-height) src = _src() - i = src.index('x-ref="overlay"') - tag = src[src.rfind("<", 0, i):src.index(">", i) + 1] - assert tag.startswith(" da disegnare, dentro il componente + i = src.index('save-name="dxf"') + assert src.index("", i) + assert "dxfPreviewD()" in src[i:src.index("", i)] -def test_save_button_in_image_and_prova_bars_only(): - # #119: Salva nella barra dell'immagine e in quella della Prova, non in quella del DXF +def test_old_viewports_gone(): + # #122: niente piu' viewport, barre e overlay scritti a mano src = _src() - img_bar = _bar_of(src, "vFit('img')") - prova_bar = _bar_of(src, 'zoomBy(1 / 1.25)') - dxf_bar = _bar_of(src, "vFit('dxf')") - assert "tmfSaveImage(imagePreviewUrl, 'immagine')" in img_bar - assert "tmfSaveImage(imageSrc, 'prova')" in prova_bar - assert "tmfSaveImage" not in dxf_bar - assert src.count("tmfSaveImage(") == 2 - # il pulsante sta dopo Adatta e prima della percentuale - assert img_bar.index("vFit('img')") < img_bar.index("tmfSaveImage") < img_bar.index("vPct('img')") - assert prova_bar.index("fit()") < prova_bar.index("tmfSaveImage") < prova_bar.index("zoomPct") + for old in ('x-ref="vpImg"', 'x-ref="vpDxf"', 'x-ref="overlay"', 'x-ref="viewport"', "tmfSaveImage(", + "vZoomBy", "vFit(", "zoomPct", "vPct("): + assert old not in src, old + + +def test_no_style_string_on_viewers(): + # #122: un :style a stringa sul cancella lo style statico; x-show non sta sul componente + src = _src() + for m in re.finditer(r"]*>", src): + tag = m.group(0) + assert ":style" not in tag and "x-show" not in tag, tag def test_scripts_loaded_before_wizard(): - # #119 #121: vision_overlay.js e save_image.js prima di dxf_recipe_wizard.js + # #119 #121 #122: vision_overlay.js e save_image.js prima di image_viewer.js, che sta prima di dxf_recipe_wizard.js src = _src() - wiz = src.index("js/dxf_recipe_wizard.js") - assert src.index("js/vision_overlay.js") < wiz - assert src.index("js/save_image.js") < wiz + viewer = src.index("js/image_viewer.js") + assert src.index("js/vision_overlay.js") < viewer + assert src.index("js/save_image.js") < viewer + assert viewer < src.index("js/dxf_recipe_wizard.js")