Wizard DXF: correzioni revisione zoom anteprime (style statico, x-show, altezza utile). Refs #113

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 12:39:06 +01:00
parent 45c9e6c3f3
commit 04b81ad2f2
4 changed files with 25 additions and 13 deletions
@@ -514,7 +514,9 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
vFit(k) { vFit(k) {
const vp = this._vpEl(k), v = this.views[k]; const vp = this._vpEl(k), v = this.views[k];
if (!vp || !v.w || !v.h || !vp.clientWidth) return; // nascosto o vuoto: niente if (!vp || !v.w || !v.h || !vp.clientWidth) return; // nascosto o vuoto: niente
let z = Math.min(vp.clientWidth / v.w, 240 / v.h); // altezza utile del viewport (senza bordo), ripiego 240; con 240 secco il contenuto sborda e compare lo scroll
const ch = vp.clientHeight || 240;
let z = Math.min(vp.clientWidth / v.w, ch / v.h);
if (k === 'img') z = Math.min(8, Math.max(0.05, z)); if (k === 'img') z = Math.min(8, Math.max(0.05, z));
v.zoom = z; v.zoom = z;
v.fit = z; v.fit = z;
@@ -730,8 +730,8 @@
<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' --> <!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'img'; x-show sul contenitore (sul viewport :style lo pesterebbe) -->
<div class="relative"> <div class="relative" x-show="!imageBroken">
<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" <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"
style="background: rgba(0,0,0,.55); color: #fff;"> style="background: rgba(0,0,0,.55); color: #fff;">
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button> <button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button>
@@ -740,12 +740,12 @@
<span class="text-xs px-1" x-text="vPct('img')"></span> <span class="text-xs px-1" x-text="vPct('img')"></span>
</div> </div>
<!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande --> <!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande -->
<div x-show="!imageBroken" x-ref="vpImg" @wheel="vWheel('img', $event)" <div x-ref="vpImg" @wheel="vWheel('img', $event)"
@pointerdown="vPanDown('img', $event)" @pointermove="vPanMove('img', $event)" @pointerdown="vPanDown('img', $event)" @pointermove="vPanMove('img', $event)"
@pointerup="vPanEnd()" @pointercancel="vPanEnd()" @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="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('img')">
<div style="margin:auto;flex:none;" :style="views.img.w ? ('width:' + vW('img') + 'px;height:' + vH('img') + 'px') : 'width:100%;height:240px'"> <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 #}
<img :src="imagePreviewUrl" alt="" draggable="false" <img :src="imagePreviewUrl" alt="" draggable="false"
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()" @load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
class="block" style="width: 100%; height: 100%; object-fit: contain; user-select: none;"> class="block" style="width: 100%; height: 100%; object-fit: contain; user-select: none;">
@@ -771,7 +771,7 @@
@pointerup="vPanEnd()" @pointercancel="vPanEnd()" @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="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('dxf')">
<div style="margin:auto;flex:none;" :style="'width:' + vW('dxf') + 'px;height:' + vH('dxf') + 'px'"> <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 -->
@@ -23,18 +23,18 @@ const near = (a, b) => Math.abs(a - b) < 1e-9 * Math.max(1, Math.abs(b));
// Wizard con viewport finti e $nextTick sincrono // Wizard con viewport finti e $nextTick sincrono
function make(cw) { function make(cw) {
const w = dxfRecipeWizard(1, () => 'tok', {}); const w = dxfRecipeWizard(1, () => 'tok', {});
const vp = () => ({ clientWidth: cw, clientHeight: 240, scrollLeft: 5, scrollTop: 5 }); const vp = () => ({ clientWidth: cw, clientHeight: 238, scrollLeft: 5, scrollTop: 5 });
w.$refs = { vpImg: vp(), vpDxf: vp() }; w.$refs = { vpImg: vp(), vpDxf: vp() };
w.$nextTick = fn => fn(); w.$nextTick = fn => fn();
return w; return w;
} }
// img: viewport 400 x 240, immagine 2856 x 2848 // img: viewport 400 x 238 utili, immagine 2856 x 2848
let w = make(400); let w = make(400);
w.vSetContent('img', 2856, 2848); w.vSetContent('img', 2856, 2848);
check('#113 img: Adatta = 240/2848', near(w.views.img.zoom, 240 / 2848) && near(w.views.img.fit, 240 / 2848), String(w.views.img.zoom)); 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: 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(240 / 2848 * 100) + ' %', w.vPct('img')); check('#113 img: percentuale = zoom*100', w.vPct('img') === Math.round(238 / 2848 * 100) + ' %', w.vPct('img'));
const z0 = w.views.img.zoom; const z0 = w.views.img.zoom;
w.vZoomBy('img', 1.25); w.vZoomBy('img', 1.25);
check('#113 img: zoomBy moltiplica', near(w.views.img.zoom, z0 * 1.25)); check('#113 img: zoomBy moltiplica', near(w.views.img.zoom, z0 * 1.25));
@@ -45,13 +45,17 @@ w.vZoomBy('img', 1e-9);
check('#113 img: limite basso 0.05', w.views.img.zoom === 0.05); check('#113 img: limite basso 0.05', w.views.img.zoom === 0.05);
w.vFit('img'); w.vFit('img');
check('#113 img: Adatta torna al fit', near(w.views.img.zoom, z0) && w.vCanPan('img') === false); 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: 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)); 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 240 -> fit = min(20, 24) = 20 // dxf: viewBox 20 x 10 mm, viewport 400 x 238 -> fit = min(20, 23.8) = 20
w = make(400); w = make(400);
w.vSetContent('dxf', 20, 10); w.vSetContent('dxf', 20, 10);
check('#113 dxf: fit = min(vpW/w, 240/h)', near(w.views.dxf.fit, 20), String(w.views.dxf.fit)); 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')); check('#113 dxf: dopo Adatta 100 %', w.vPct('dxf') === '100 %', w.vPct('dxf'));
w.vZoomBy('dxf', 2); w.vZoomBy('dxf', 2);
check('#113 dxf: zoom 2x = 200 %', w.vPct('dxf') === '200 %', w.vPct('dxf')); check('#113 dxf: zoom 2x = 200 %', w.vPct('dxf') === '200 %', w.vPct('dxf'));
@@ -66,7 +70,7 @@ 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); 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.$refs.vpImg.clientWidth = 500;
w.vFit('img'); w.vFit('img');
check('#113 viewport visibile: il fit parte', near(w.views.img.fit, 0.48), String(w.views.img.fit)); check('#113 viewport visibile: il fit parte', near(w.views.img.fit, 238 / 500), String(w.views.img.fit));
// Reset: nuova immagine / DXF tolto // Reset: nuova immagine / DXF tolto
w.vReset('img'); w.vReset('img');
@@ -362,5 +362,11 @@ class TestPages:
assert "vFit('img')" in html and "vFit('dxf')" 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 "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 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("<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