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:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user