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
imageName: '',
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,
dxfPreview: null, // #101: risposta di /dxf-preview {contorni, bbox, larghezza_mm, altezza_mm, entita, punti}
dxfPreviewBusy: false,
@@ -54,13 +57,17 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
// Cambio stazione: rilegge lo stato di calibrazione dall'acquisitore
async onStation() {
this.calState = ''; this.calLabel = ''; this.calUrl = '';
this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calPxMm = null;
this.result = null;
if (!this.stationId) return;
const r = await fetchStationCalibration(this.stationId);
this.calState = r.state || 'unreachable';
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'; },
@@ -81,12 +88,38 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
_setImage(file) {
if (this.imageSrc) URL.revokeObjectURL(this.imageSrc);
this.imageFile = file;
this.imageName = file.name || '';
this.imageSrc = URL.createObjectURL(file);
// #102: con file null (immagine tolta) si azzera anche l'anteprima
this.imageFile = file || null;
this.imageName = (file && file.name) || '';
this.imageSrc = file ? URL.createObjectURL(file) : '';
this.imagePx = null;
this.imageBroken = false;
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) {
const f = ev.target.files && ev.target.files[0];
if (f) this._setImage(f);
@@ -662,7 +662,7 @@
<!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== -->
<template x-if="task.task_type === 'dxf_compare'">
<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()">
{{ _('Crea ricetta da DXF') }}
</button>
@@ -710,6 +710,15 @@
</label>
</div>
<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>
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
@@ -85,6 +85,25 @@ const PREVIEW = {
await w2.loadDxfPreview();
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); }
console.log('tutto ok');
})();
@@ -280,4 +280,5 @@ 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 "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
@@ -3499,3 +3499,11 @@ msgstr "Reading DXF…"
msgid "entità"
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à"
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"