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
+31 -139
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
*/
@@ -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 <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
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 <tmf-viewer> (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 '<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;
}
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');
})();