Wizard DXF: anteprima immagine, anteprima DXF e Prova usano <tmf-viewer>

Refs #122

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:58:27 +01:00
parent 43ef22079d
commit 269bbc03ca
5 changed files with 120 additions and 443 deletions
@@ -39,11 +39,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
result: null, // corpo B3 {result, saved, recipe_dir, outputs} result: null, // corpo B3 {result, saved, recipe_dir, outputs}
imageSource: '', // #104: '' | 'file' | 'camera'; il campo mm/px vale solo per 'file' 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 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) // #122: zoom, Adatta e pan delle tre viste (immagine, DXF, Prova) ora stanno nel componente <tmf-viewer>
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)
runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione) runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione)
runBandMm: null, // #104: banda usata nell'ultima Prova runBandMm: null, // #104: banda usata nell'ultima Prova
runMinPointsPct: null, // #104: soglia 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 _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 _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) _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) // #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone)
init() { init() {
@@ -204,25 +195,22 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.imageSrc = file ? URL.createObjectURL(file) : ''; this.imageSrc = file ? URL.createObjectURL(file) : '';
this.imagePx = null; this.imagePx = null;
this.imageBroken = false; this.imageBroken = false;
this.vReset('img'); // #113: nuova immagine, vista da rifare
this.result = null; this.result = null;
this.tested = false; // #104 this.tested = false; // #104
this.resPx = null; // #104
this.imageSource = file ? (source || '') : ''; // #104 this.imageSource = file ? (source || '') : ''; // #104
this._imgPickSeq++; // #105: una scelta piu' recente batte i file della ricetta ancora in arrivo this._imgPickSeq++; // #105: una scelta piu' recente batte i file della ricetta ancora in arrivo
}, },
// #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile // #102: il <tmf-viewer> ha caricato l'immagine (evento viewer-load): legge i pixel dal detail
onImagePreviewLoad(el) { // #122: lo zoom e' del componente; qui restano solo le dimensioni per il riepilogo
onImagePreviewLoad(d) {
this.imageBroken = false; this.imageBroken = false;
this.imagePx = el && el.naturalWidth ? { w: el.naturalWidth, h: el.naturalHeight } : null; this.imagePx = d && d.width ? { w: d.width, h: d.height } : null;
if (this.imagePx) this.vSetContent('img', this.imagePx.w, this.imagePx.h); // #113: Adatta al caricamento
else this.vReset('img');
}, },
// #102: se il formato non e' mostrabile (sonda <img hidden> del template) compare l'avviso
onImagePreviewError() { onImagePreviewError() {
this.imageBroken = !!this.imageSrc; this.imageBroken = !!this.imageSrc;
this.imagePx = null; this.imagePx = null;
this.vReset('img'); // #113
}, },
// #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati) // #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati)
get imagePreviewUrl() { return this.imageSrc; }, get imagePreviewUrl() { return this.imageSrc; },
@@ -255,7 +243,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
async loadDxfPreview() { async loadDxfPreview() {
const seq = ++this._dxfPreviewSeq; const seq = ++this._dxfPreviewSeq;
this.dxfPreview = null; this.dxfPreview = null;
this.vReset('dxf'); // #113: nuova anteprima in arrivo
this.dxfPreviewError = ''; this.dxfPreviewError = '';
this.dxfPreviewBusy = false; this.dxfPreviewBusy = false;
if (!this.dxfFile) return; // file tolto: tutto azzerato 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); if (!resp.ok || body.error) this.dxfPreviewError = this._errorText(body);
else { else {
this.dxfPreview = body; 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) { } catch (e) {
if (seq !== this._dxfPreviewSeq) return; if (seq !== this._dxfPreviewSeq) return;
@@ -373,7 +358,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.error = this._errorText(body); this.error = this._errorText(body);
} else { } else {
this.result = body; this.result = body;
this.$nextTick(() => this.drawOverlay());
if (save && body.saved) { if (save && body.saved) {
this.notice = this.msgs.saved || 'Saved'; this.notice = this.msgs.saved || 'Saved';
// Il componente visionRecipe ricarica cartella, file e uscite delle quote // Il componente visionRecipe ricarica cartella, file e uscite delle quote
@@ -391,7 +375,6 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.busy = false; this.busy = false;
// #104: la sezione del risultato compare anche con Prova fallita (immagine a sinistra, errore a destra) // #104: la sezione del risultato compare anche con Prova fallita (immagine a sinistra, errore a destra)
this.tested = true; this.tested = true;
this.$nextTick(() => this.fit());
}, },
// Le quattro uscite del confronto, nell'ordine fisso // 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')})`; 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) { fmt(v) {
return (typeof v === 'number') ? v.toLocaleString(document.documentElement.lang || 'it', return (typeof v === 'number') ? v.toLocaleString(document.documentElement.lang || 'it',
{ maximumFractionDigits: 3 }) : '-'; { 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 }) : '';
},
}; };
} }
@@ -730,50 +730,28 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">
<div x-show="imagePreviewUrl"> <div x-show="imagePreviewUrl">
<!-- #102: anteprima dell'immagine --> <!-- #102: anteprima dell'immagine -->
<!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'img'; x-show sul contenitore (sul viewport :style lo pesterebbe) --> {# #122: <tmf-viewer> standard (zoom, Adatta, Salva, pan) al posto della barra e del viewport scritti a mano (#113); altezza fissa 240px #}
<div class="relative" x-show="!imageBroken"> <div x-show="!imageBroken">{# #122: x-show sul contenitore, mai sul <tmf-viewer> #}
<div x-show="!imageBroken && views.img.w" class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5" <tmf-viewer :src="imagePreviewUrl" save-name="immagine"
style="background: rgba(0,0,0,.55); color: #fff;"> label-fit="{{ _('Adatta') }}" label-save="{{ _('Salva') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button> title-in="{{ _('Ingrandisci') }}" title-out="{{ _('Riduci') }}" title-save="{{ _('Salva immagine') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1.25)" title="{{ _('Ingrandisci') }}">+</button> @viewer-load="onImagePreviewLoad($event.detail)"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('img')">{{ _('Adatta') }}</button>
{# #119: salva il frame grezzo mostrato (PNG, senza disegni) #}
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="tmfSaveImage(imagePreviewUrl, 'immagine')" title="{{ _('Salva immagine') }}">{{ _('Salva') }}</button>
<span class="text-xs px-1" x-text="vPct('img')"></span>
</div>
<!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande -->
<div x-ref="vpImg" @wheel="vWheel('img', $event)"
@pointerdown="vPanDown('img', $event)" @pointermove="vPanMove('img', $event)"
@pointerup="vPanEnd()" @pointercancel="vPanEnd()"
class="rounded border border-[var(--border-color)]" class="rounded border border-[var(--border-color)]"
:style="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('img')"> style="height: 240px; background: var(--bg-primary);"></tmf-viewer>
<div :style="'margin:auto;flex:none;' + (views.img.w ? ('width:' + vW('img') + 'px;height:' + vH('img') + 'px') : 'width:100%;height:240px')">{# #113: tutto in :style, Alpine con stringa sostituisce l'attributo style #} {# #122: sonda nascosta: il componente non propaga l'errore di caricamento, la sonda si'; se il formato non e' mostrabile compare l'avviso #}
<img :src="imagePreviewUrl" alt="" draggable="false" <img :src="imagePreviewUrl" alt="" hidden @error="onImagePreviewError()">
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
class="block" style="width: 100%; height: 100%; object-fit: contain; user-select: none;">
</div>
</div>
</div> </div>
<p x-show="imageBroken" class="text-xs text-amber-600">{{ _('Anteprima non disponibile per questo formato') }}</p> <p x-show="imageBroken" class="text-xs text-amber-600">{{ _('Anteprima non disponibile per questo formato') }}</p>
<p x-show="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p> <p x-show="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p>
</div> </div>
<div x-show="dxfPreview" class="sm:col-start-2">{# #104: resta a destra anche senza anteprima immagine #} <div x-show="dxfPreview" class="sm:col-start-2">{# #104: resta a destra anche senza anteprima immagine #}
<!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'dxf' --> {# #122: <tmf-viewer> in modo contenuto: lo <svg> (viewBox in mm) va nello stage; width/height = misura del viewBox, ora anche Salva (PNG) #}
<div class="relative"> <tmf-viewer save-name="dxf"
<div class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5" :width="dxfPreviewViewBox().split(' ')[2]" :height="dxfPreviewViewBox().split(' ')[3]"
style="background: rgba(0,0,0,.55); color: #fff;"> label-fit="{{ _('Adatta') }}" label-save="{{ _('Salva') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('dxf', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button> title-in="{{ _('Ingrandisci') }}" title-out="{{ _('Riduci') }}" title-save="{{ _('Salva immagine') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('dxf', 1.25)" title="{{ _('Ingrandisci') }}">+</button>
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('dxf')">{{ _('Adatta') }}</button>
<span class="text-xs px-1" x-text="vPct('dxf')"></span>
</div>
<!-- #113: stesso viewport dell'immagine; svg 100% del wrapper, viewBox in mm -->
<div x-ref="vpDxf" @wheel="vWheel('dxf', $event)"
@pointerdown="vPanDown('dxf', $event)" @pointermove="vPanMove('dxf', $event)"
@pointerup="vPanEnd()" @pointercancel="vPanEnd()"
class="rounded border border-[var(--border-color)]" class="rounded border border-[var(--border-color)]"
:style="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('dxf')"> style="height: 240px; background: var(--bg-primary);">
<div :style="'margin:auto;flex:none;width:' + vW('dxf') + 'px;height:' + vH('dxf') + 'px'">
<svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet" <svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet"
class="block rounded" style="width: 100%; height: 100%;"> class="block rounded" style="width: 100%; height: 100%;">
<!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox --> <!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox -->
@@ -781,9 +759,7 @@
stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-width="1.5" vector-effect="non-scaling-stroke"
stroke-linejoin="round"></path> stroke-linejoin="round"></path>
</svg> </svg>
</div> </tmf-viewer>
</div>
</div>
<p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p> <p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p>
</div> </div>
</div> </div>
@@ -814,30 +790,13 @@
<!-- #104: risultato della Prova su due colonne: immagine con zoom a sinistra, dati ed errori a destra --> <!-- #104: risultato della Prova su due colonne: immagine con zoom a sinistra, dati ed errori a destra -->
<div x-show="tested" class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">{# #104: risultati a destra gia' da 640px, come le altre griglie del wizard #} <div x-show="tested" class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">{# #104: risultati a destra gia' da 640px, come le altre griglie del wizard #}
<div> <div>
<!-- #104: barra zoom in sovrimpressione, fuori dal contenuto scorrevole --> {# #122: <tmf-viewer fit-height> al posto di barra, viewport, pan e overlay della Prova (#104 #121); overlay dalle forme del risultato #}
<div class="relative"> <tmf-viewer fit-height :src="imageSrc" save-name="prova"
<div class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5" x-effect="$el.overlay = (result && result.result && result.result.overlay) || []"
style="background: rgba(0,0,0,.55); color: #fff;"> label-fit="{{ _('Adatta') }}" label-save="{{ _('Salva') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button> title-in="{{ _('Ingrandisci') }}" title-out="{{ _('Riduci') }}" title-save="{{ _('Salva immagine') }}"
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1.25)" title="{{ _('Ingrandisci') }}">+</button>
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="fit()">{{ _('Adatta') }}</button>
{# #119: salva il frame grezzo della Prova (PNG, senza overlay) #}
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="tmfSaveImage(imageSrc, 'prova')" title="{{ _('Salva immagine') }}">{{ _('Salva') }}</button>
<span class="text-xs px-1" x-text="zoomPct"></span>
</div>
<!-- #104: pan col puntatore (mouse e dito), cursore grab; altezza che segue l'immagine finche' zoom <= Adatta -->
<div x-ref="viewport" @wheel="onWheel($event)"
@pointerdown="onPanDown($event)" @pointermove="onPanMove($event)"
@pointerup="onPanEnd()" @pointercancel="onPanEnd()"
class="rounded border border-[var(--border-color)]" class="rounded border border-[var(--border-color)]"
:style="'overflow:auto;touch-action:none;user-select:none;background:var(--bg-primary);height:' + vpHeight + 'px;cursor:' + panCursor"> style="background: var(--bg-primary);"></tmf-viewer>
<div class="relative" :style="zoomW ? ('width:' + zoomW + 'px;height:' + zoomH + 'px') : 'width:100%'">
<img x-ref="img" :src="imageSrc" alt="" class="block" draggable="false" style="width: 100%; height: 100%; user-select: none;"
@load="onResultImgLoad()">
{# #121: overlay SVG (tratto a spessore costante a ogni zoom) al posto del canvas; il viewBox lo imposta drawOverlay #}
<svg x-ref="overlay" class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="none"></svg>
</div>
</div>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
@@ -1523,6 +1482,7 @@
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script> <script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
<script src="{{ url_for('static', filename='js/vision_overlay.js') }}"></script>{# #121 #} <script src="{{ url_for('static', filename='js/vision_overlay.js') }}"></script>{# #121 #}
<script src="{{ url_for('static', filename='js/save_image.js') }}"></script>{# #119 #} <script src="{{ url_for('static', filename='js/save_image.js') }}"></script>{# #119 #}
<script src="{{ url_for('static', filename='js/image_viewer.js') }}"></script>{# #122: <tmf-viewer>, dopo vision_overlay.js e save_image.js #}
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script> <script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
<script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #} <script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #}
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script> <script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
+29 -137
View File
@@ -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 <tmf-viewer>.
* 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 * 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); if (cond) console.log('ok - ' + label);
else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); } 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 w = dxfRecipeWizard(1, () => 'tok', {});
const vp = () => ({ clientWidth: cw, clientHeight: 238, scrollLeft: 5, scrollTop: 5 });
w.$refs = { vpImg: vp(), vpDxf: vp() };
w.$nextTick = fn => fn(); w.$nextTick = fn => fn();
return w;
}
// img: viewport 400 x 238 utili, immagine 2856 x 2848 // #122: via tutto lo zoom/pan/fit/overlay del wizard
let w = make(400); const gone = ['views', 'viewPanKey', 'zoom', 'fitZoom', 'panning', 'resPx', 'vZoomBy', 'vFit', 'vWheel', 'vPanDown',
w.vSetContent('img', 2856, 2848); 'vPanMove', 'vPanEnd', 'vCursor', 'vPct', 'vW', 'vH', 'vReset', 'vSetContent', 'zoomBy', 'fit', 'onWheel',
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)); 'onPanDown', 'onPanMove', 'onPanEnd', 'onResultImgLoad', 'drawOverlay', 'zoomPct', 'zoomW', 'zoomH', 'vpHeight'];
check('#113 img: scroll azzerato dopo Adatta', w.$refs.vpImg.scrollLeft === 0 && w.$refs.vpImg.scrollTop === 0); for (const k of gone) check('#122 tolto dal wizard: ' + k, !(k in w));
check('#113 img: percentuale = zoom*100', w.vPct('img') === Math.round(238 / 2848 * 100) + ' %', w.vPct('img')); check('#122 sorgente senza $refs di zoom', !/\$refs\./.test(src));
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));
// dxf: viewBox 20 x 10 mm, viewport 400 x 238 -> fit = min(20, 23.8) = 20 // onImagePreviewLoad riceve il detail di viewer-load e alimenta il riepilogo
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
global.URL.createObjectURL = () => 'blob:x'; global.URL.createObjectURL = () => 'blob:x';
global.URL.revokeObjectURL = () => {}; global.URL.revokeObjectURL = () => {};
w = make(400);
w._setImage({ name: 'a.bmp' }, 'file'); w._setImage({ name: 'a.bmp' }, 'file');
w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 }); w.onImagePreviewLoad({ width: 800, height: 240 });
check('#113 onImagePreviewLoad: Adatta all\'arrivo', near(w.views.img.zoom, 0.5) && w.views.img.w === 800, String(w.views.img.zoom)); check('#122 viewer-load: dimensioni nel riepilogo', w.imagePx.w === 800 && w.imagePx.h === 240 && w.imageBroken === false);
w._setImage({ name: 'b.bmp' }, 'file'); check('#122 riepilogo «W × H px»', /800.*240 px/.test(w.imagePreviewSummary), w.imagePreviewSummary);
check('#113 _setImage: vista azzerata', w.views.img.w === 0 && w.views.img.zoom === 1); w.onImagePreviewLoad({ width: 0, height: 0 });
w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 }); check('#122 viewer-load senza dimensioni: imagePx null', w.imagePx === null);
w.onImagePreviewLoad({ width: 800, height: 240 });
w.onImagePreviewError(); w.onImagePreviewError();
check('#113 errore <img>: 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 // Anteprima DXF: il viewBox resta quello per il <tmf-viewer> (width/height del template)
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
(async () => { (async () => {
const PREVIEW = { const PREVIEW = {
contorni: [[[0, 0], [20, 0], [20, 10]]], contorni: [[[0, 0], [20, 0], [20, 10]]],
@@ -129,54 +58,17 @@ delete global.getComputedStyle;
}; };
global.FormData = class { append() {} }; global.FormData = class { append() {} };
global.fetch = async () => ({ ok: true, json: async () => PREVIEW }); 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' }; wd.dxfFile = { name: 'a.dxf' };
await wd.loadDxfPreview(); await wd.loadDxfPreview();
const vb = wd.dxfPreviewViewBox().split(' ').map(Number); 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('#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('#113 loadDxfPreview: Adatta = 100 %', wd.vPct('dxf') === '100 %' && wd.views.dxf.fit > 1); check('#122 DXF: anteprima caricata', wd.dxfPreview && wd.dxfPreviewD().startsWith('M 0 0'));
wd.dxfFile = null; wd.dxfFile = null;
await wd.loadDxfPreview(); await wd.loadDxfPreview();
check('#113 DXF tolto: vista azzerata', wd.views.dxf.w === 0 && wd.views.dxf.zoom === 1); check('#122 DXF tolto: anteprima azzerata', wd.dxfPreview === null && wd.dxfPreviewViewBox() === '0 0 1 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 '<path d="M0 0"/>'; } } };
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 === '<path d="M0 0"/>' && 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;
}
if (failures) { console.log(failures + ' test falliti'); process.exit(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');
})(); })();
@@ -354,19 +354,13 @@ class TestPages:
assert "@dxf-recipe-saved" in html assert "@dxf-recipe-saved" in html
assert 'x-model="mmPx"' in html # #98: campo mm/px a mano 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 "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 "imageSource === 'file'" in html # #104: mm/px solo da file
assert 'onPanDown($event)' in html # #104: pan col puntatore
assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine 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 # #122: le tre viste (immagine, DXF, Prova) sono <tmf-viewer>; zoom, Adatta, pan e Salva stanno nel componente
assert 'x-ref="vpImg"' in html and 'x-ref="vpDxf"' in html assert html.count("<tmf-viewer") == 3 and "js/image_viewer.js" in html
assert "vFit('img')" in html and "vFit('dxf')" in html assert '@viewer-load="onImagePreviewLoad($event.detail)"' in html # #122: dimensioni per il riepilogo
assert "vZoomBy('img', 1.25)" in html and "vZoomBy('dxf', 1.25)" in html assert "fit-height" in html and 'save-name="prova"' in html # #122: la Prova segue l'immagine
assert "vPanDown('img', $event)" in html and "vWheel('dxf', $event)" in html for old in ('x-ref="vpImg"', 'x-ref="vpDxf"', "vZoomBy(", "vFit(", "vPanDown(", "onPanDown(", "zoomBy(", 'x-ref="viewport"'):
# #113: Alpine :style a stringa sostituisce l'attributo style: niente style statico sullo stesso tag assert old not in html, old # #122: zoom a mano tolto dal wizard
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("<div", 0, html.index('x-ref="vpImg"')):html.index('x-ref="vpImg"')]
assert "x-show" not in tag
assert "existing ? msgs.editLabel : msgs.createLabel" in html and "Modifica ricetta da DXF" in html # #105: etichetta dinamica assert "existing ? msgs.editLabel : msgs.createLabel" in html and "Modifica ricetta da DXF" in html # #105: etichetta dinamica
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
@@ -1,52 +1,64 @@
"""Controllo del template del wizard DXF: Salva in due barre zoom e overlay SVG (#119, #121). """Controllo del template del wizard DXF: le tre viste sono <tmf-viewer> (#122, prima #113 #119 #121).
Legge task_editor.html come testo (come test_vision_panel_fit.py), senza renderlo. Legge task_editor.html come testo (come test_vision_panel_fit.py), senza renderlo.
""" """
import re
from pathlib import Path from pathlib import Path
TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "maker" / "task_editor.html" TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "maker" / "task_editor.html"
def _src() -> str: def _src() -> str:
return TEMPLATE.read_text(encoding="utf-8") # senza commenti Jinja e HTML, che citano <tmf-viewer> 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: def _tag_of(src: str, marker: str) -> str:
"""Testo della barra zoom che contiene il marker (dal div della barra al primo </div>).""" """Tag di apertura <tmf-viewer ...> che contiene il marker."""
i = src.index(marker) i = src.index(marker)
start = src.rfind('absolute top-2 right-2', 0, i) start = src.rfind("<tmf-viewer", 0, i)
return src[start:src.index("</div>", i)] return src[start:src.index(">", i) + 1]
def test_prova_overlay_is_svg_not_canvas(): def test_three_viewers_image_dxf_prova():
# #121: l'overlay della Prova e' un <svg> con preserveAspectRatio none, non piu' un canvas # #122: tre <tmf-viewer>: immagine (src), DXF (modo contenuto, con <svg>), Prova (fit-height)
src = _src() src = _src()
i = src.index('x-ref="overlay"') assert src.count("<tmf-viewer") == 3
tag = src[src.rfind("<", 0, i):src.index(">", i) + 1] img = _tag_of(src, 'save-name="immagine"')
assert tag.startswith("<svg") assert ':src="imagePreviewUrl"' in img
assert 'preserveAspectRatio="none"' in tag dxf = _tag_of(src, 'save-name="dxf"')
assert "pointer-events-none" in tag assert ":src" not in dxf and ":width=" in dxf and ":height=" in dxf
assert "<canvas" not in src[i - 200:i + 200] prova = _tag_of(src, 'save-name="prova"')
assert "fit-height" in prova and ':src="imageSrc"' in prova
assert "result.result.overlay" in prova # overlay delle forme del risultato
# il DXF contiene lo <svg> da disegnare, dentro il componente
i = src.index('save-name="dxf"')
assert src.index("<svg", i) < src.index("</tmf-viewer>", i)
assert "dxfPreviewD()" in src[i:src.index("</tmf-viewer>", i)]
def test_save_button_in_image_and_prova_bars_only(): def test_old_viewports_gone():
# #119: Salva nella barra dell'immagine e in quella della Prova, non in quella del DXF # #122: niente piu' viewport, barre e overlay scritti a mano
src = _src() src = _src()
img_bar = _bar_of(src, "vFit('img')") for old in ('x-ref="vpImg"', 'x-ref="vpDxf"', 'x-ref="overlay"', 'x-ref="viewport"', "tmfSaveImage(",
prova_bar = _bar_of(src, 'zoomBy(1 / 1.25)') "vZoomBy", "vFit(", "zoomPct", "vPct("):
dxf_bar = _bar_of(src, "vFit('dxf')") assert old not in src, old
assert "tmfSaveImage(imagePreviewUrl, 'immagine')" in img_bar
assert "tmfSaveImage(imageSrc, 'prova')" in prova_bar
assert "tmfSaveImage" not in dxf_bar def test_no_style_string_on_viewers():
assert src.count("tmfSaveImage(") == 2 # #122: un :style a stringa sul <tmf-viewer> cancella lo style statico; x-show non sta sul componente
# il pulsante sta dopo Adatta e prima della percentuale src = _src()
assert img_bar.index("vFit('img')") < img_bar.index("tmfSaveImage") < img_bar.index("vPct('img')") for m in re.finditer(r"<tmf-viewer[^>]*>", src):
assert prova_bar.index("fit()") < prova_bar.index("tmfSaveImage") < prova_bar.index("zoomPct") tag = m.group(0)
assert ":style" not in tag and "x-show" not in tag, tag
def test_scripts_loaded_before_wizard(): 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() src = _src()
wiz = src.index("js/dxf_recipe_wizard.js") viewer = src.index("js/image_viewer.js")
assert src.index("js/vision_overlay.js") < wiz assert src.index("js/vision_overlay.js") < viewer
assert src.index("js/save_image.js") < wiz assert src.index("js/save_image.js") < viewer
assert viewer < src.index("js/dxf_recipe_wizard.js")