maker: anteprima immagine nel wizard Crea ricetta da DXF (Refs #102)

Anteprima dell'immagine scelta/acquisita accanto a quella del DXF, con
pixel e campo in mm (scala a mano o calibrazione stazione); fallback se
il formato non e' mostrabile. Test JS e di pagina, voci it/en.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:14:10 +01:00
parent 4791c34205
commit 20f33234a8
6 changed files with 84 additions and 6 deletions
@@ -20,6 +20,9 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
imageFile: null, // immagine caricata o acquisita imageFile: null, // immagine caricata o acquisita
imageName: '', imageName: '',
imageSrc: '', imageSrc: '',
imagePx: null, // #102: {w, h} in pixel, letto dall'<img> dell'anteprima
imageBroken: false, // #102: il browser non sa mostrare il formato (il file resta valido per l'invio)
calPxMm: null, // #102: px/mm della calibrazione della stazione
dxfFile: null, dxfFile: null,
dxfPreview: null, // #101: risposta di /dxf-preview {contorni, bbox, larghezza_mm, altezza_mm, entita, punti} dxfPreview: null, // #101: risposta di /dxf-preview {contorni, bbox, larghezza_mm, altezza_mm, entita, punti}
dxfPreviewBusy: false, dxfPreviewBusy: false,
@@ -54,13 +57,17 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
// Cambio stazione: rilegge lo stato di calibrazione dall'acquisitore // Cambio stazione: rilegge lo stato di calibrazione dall'acquisitore
async onStation() { async onStation() {
this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calPxMm = null;
this.result = null; this.result = null;
if (!this.stationId) return; if (!this.stationId) return;
const r = await fetchStationCalibration(this.stationId); const r = await fetchStationCalibration(this.stationId);
this.calState = r.state || 'unreachable'; this.calState = r.state || 'unreachable';
this.calUrl = r.calibration_url || ''; this.calUrl = r.calibration_url || '';
if (this.calState === 'calibrated') this.calLabel = formatCalibration(r.calibration || {}); if (this.calState === 'calibrated') {
this.calLabel = formatCalibration(r.calibration || {});
const c = Number((r.calibration || {}).px_per_mm); // #102: scala per il campo in mm dell'anteprima
this.calPxMm = Number.isFinite(c) && c > 0 ? c : null;
}
}, },
get calibrated() { return this.calState === 'calibrated'; }, get calibrated() { return this.calState === 'calibrated'; },
@@ -81,12 +88,38 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
_setImage(file) { _setImage(file) {
if (this.imageSrc) URL.revokeObjectURL(this.imageSrc); if (this.imageSrc) URL.revokeObjectURL(this.imageSrc);
this.imageFile = file; // #102: con file null (immagine tolta) si azzera anche l'anteprima
this.imageName = file.name || ''; this.imageFile = file || null;
this.imageSrc = URL.createObjectURL(file); this.imageName = (file && file.name) || '';
this.imageSrc = file ? URL.createObjectURL(file) : '';
this.imagePx = null;
this.imageBroken = false;
this.result = null; this.result = null;
}, },
// #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile
onImagePreviewLoad(el) {
this.imageBroken = false;
this.imagePx = el && el.naturalWidth ? { w: el.naturalWidth, h: el.naturalHeight } : null;
},
onImagePreviewError() {
this.imageBroken = !!this.imageSrc;
this.imagePx = null;
},
// #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati)
get imagePreviewUrl() { return this.imageSrc; },
// #102: «W × H px · campo ≈ L × H mm»; scala = mm/px a mano se valido, altrimenti calibrazione della stazione
get imagePreviewSummary() {
const p = this.imagePx;
if (!p) return '';
const lang = document.documentElement.lang || 'it';
const f = (v, d) => Number(v).toLocaleString(lang, { minimumFractionDigits: d, maximumFractionDigits: d });
let text = `${f(p.w, 0)} × ${f(p.h, 0)} px`;
const pxMm = this.manualPxMm !== null ? this.manualPxMm : (this.calibrated ? this.calPxMm : null);
if (pxMm) text += ` · ${this.msgs.imageField || 'field'} ≈ ${f(p.w / pxMm, 2)} × ${f(p.h / pxMm, 2)} mm`;
return text;
},
onImage(ev) { onImage(ev) {
const f = ev.target.files && ev.target.files[0]; const f = ev.target.files && ev.target.files[0];
if (f) this._setImage(f); if (f) this._setImage(f);
@@ -662,7 +662,7 @@
<!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== --> <!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== -->
<template x-if="task.task_type === 'dxf_compare'"> <template x-if="task.task_type === 'dxf_compare'">
<div class="mt-4 pt-3 border-t border-[var(--border-color)]" <div class="mt-4 pt-3 border-t border-[var(--border-color)]"
x-data="dxfRecipeWizard(task.id, () => csrfToken(), {{ {'requestFailed': _('Richiesta non riuscita'), 'saved': _('Ricetta salvata'), 'dxfEntities': _('entità')}|tojson_attr }})"> x-data="dxfRecipeWizard(task.id, () => csrfToken(), {{ {'requestFailed': _('Richiesta non riuscita'), 'saved': _('Ricetta salvata'), 'dxfEntities': _('entità'), 'imageField': _('campo')}|tojson_attr }})">
<button type="button" class="btn btn-secondary text-xs" @click="toggle()"> <button type="button" class="btn btn-secondary text-xs" @click="toggle()">
{{ _('Crea ricetta da DXF') }} {{ _('Crea ricetta da DXF') }}
</button> </button>
@@ -710,6 +710,15 @@
</label> </label>
</div> </div>
<p class="mt-1 text-xs text-[var(--text-muted)]" x-text="imageName"></p> <p class="mt-1 text-xs text-[var(--text-muted)]" x-text="imageName"></p>
<!-- #102: anteprima dell'immagine, affiancata a quella del DXF (#101) -->
<div x-show="imagePreviewUrl" class="mt-2">
<img x-show="!imageBroken" :src="imagePreviewUrl" alt=""
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
class="block w-full rounded border border-[var(--border-color)]"
style="max-height: 240px; object-fit: contain; background: var(--bg-primary);">
<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>
</div>
</div> </div>
<div> <div>
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label> <label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
@@ -85,6 +85,25 @@ const PREVIEW = {
await w2.loadDxfPreview(); await w2.loadDxfPreview();
check('errore col detail', w2.dxfPreviewError === 'DXF illeggibile: x' && w2.dxfPreview === null, w2.dxfPreviewError); check('errore col detail', w2.dxfPreviewError === 'DXF illeggibile: x' && w2.dxfPreview === null, w2.dxfPreviewError);
// #102: anteprima dell'immagine
const wi = dxfRecipeWizard(1, () => 'tok', { imageField: 'campo' });
check('#102 senza immagine: summary vuoto', wi.imagePreviewSummary === '');
global.URL.createObjectURL = () => 'blob:x';
global.URL.revokeObjectURL = () => {};
wi._setImage({ name: 'a.bmp' });
check('#102 _setImage imposta URL e nome', wi.imagePreviewUrl === 'blob:x' && wi.imageName === 'a.bmp');
wi.onImagePreviewLoad({ naturalWidth: 2000, naturalHeight: 1000 });
check('#102 senza scala: solo pixel', /2[. ]?000 × 1[. ]?000 px$/.test(wi.imagePreviewSummary), wi.imagePreviewSummary);
wi.mmPx = '0,01';
check('#102 con mm/px a mano: campo in mm', /px · campo ≈ 20,00 × 10,00 mm$/.test(wi.imagePreviewSummary), wi.imagePreviewSummary);
wi.mmPx = ''; wi.calState = 'calibrated'; wi.calPxMm = 100;
check('#102 con calibrazione stazione: campo in mm', /campo ≈ 20,00 × 10,00 mm$/.test(wi.imagePreviewSummary), wi.imagePreviewSummary);
wi.onImagePreviewError();
check('#102 errore <img>: imageBroken e file ancora valido', wi.imageBroken === true && wi.imageFile && wi.imagePx === null);
wi._setImage(null);
check('#102 _setImage(null) azzera', wi.imagePreviewUrl === '' && wi.imageFile === null && wi.imageName === ''
&& wi.imagePx === null && wi.imageBroken === false && wi.imagePreviewSummary === '');
if (failures) { console.log(failures + ' fallimenti'); process.exit(1); } if (failures) { console.log(failures + ' fallimenti'); process.exit(1); }
console.log('tutto ok'); console.log('tutto ok');
})(); })();
@@ -280,4 +280,5 @@ 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 "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine
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
@@ -3499,3 +3499,11 @@ msgstr "Reading DXF…"
msgid "entità" msgid "entità"
msgstr "entities" msgstr "entities"
#: templates/maker/task_editor.html (#102)
msgid "campo"
msgstr "field"
#: templates/maker/task_editor.html (#102)
msgid "Anteprima non disponibile per questo formato"
msgstr "Preview not available for this format"
@@ -3483,3 +3483,11 @@ msgstr "Lettura DXF…"
msgid "entità" msgid "entità"
msgstr "entità" msgstr "entità"
#: templates/maker/task_editor.html (#102)
msgid "campo"
msgstr "campo"
#: templates/maker/task_editor.html (#102)
msgid "Anteprima non disponibile per questo formato"
msgstr "Anteprima non disponibile per questo formato"