maker: wizard DXF con anteprime sotto le scelte, mm/px solo da file, risultato su due colonne con zoom e Fit (Refs #104)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
function dxfRecipeWizard(taskId, csrf, msgs) {
|
function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||||
// Colori overlay per ruolo, uguali a vision_execute.js (reference, roi, found)
|
// Colori overlay per ruolo, uguali a vision_execute.js (reference, roi, found)
|
||||||
const ROLE_COLORS = { reference: '#2563eb', found: '#16a34a', roi: '#f59e0b' };
|
const ROLE_COLORS = { reference: '#2563eb', found: '#16a34a', roi: '#f59e0b' };
|
||||||
|
const VALUE_UNITS = { 'dxf.points_in_pct': '%', 'dxf.distance_avg': 'mm', 'dxf.distance_min': 'mm', 'dxf.distance_max': 'mm' }; // #104
|
||||||
const VALUE_KEYS = ['dxf.points_in_pct', 'dxf.distance_avg', 'dxf.distance_min', 'dxf.distance_max'];
|
const VALUE_KEYS = ['dxf.points_in_pct', 'dxf.distance_avg', 'dxf.distance_min', 'dxf.distance_max'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -37,6 +38,13 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
error: '',
|
error: '',
|
||||||
notice: '',
|
notice: '',
|
||||||
result: null, // corpo B3 {result, saved, recipe_dir, outputs}
|
result: null, // corpo B3 {result, saved, recipe_dir, outputs}
|
||||||
|
imageSource: '', // #104: '' | 'file' | 'camera'; il campo mm/px vale solo per 'file'
|
||||||
|
tested: false, // #104: true dopo una Prova (anche fallita): mostra la sezione del risultato
|
||||||
|
zoom: 1, // #104: scala dell'immagine di prova (1 = pixel nativi)
|
||||||
|
resPx: null, // #104: {w, h} in pixel dell'immagine di prova (per la dimensione del wrapper)
|
||||||
|
runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione)
|
||||||
|
runBandMm: null, // #104: banda usata nell'ultima Prova
|
||||||
|
runMinPointsPct: null, // #104: soglia usata nell'ultima Prova
|
||||||
|
|
||||||
async toggle() {
|
async toggle() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
@@ -59,6 +67,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
async onStation() {
|
async onStation() {
|
||||||
this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calPxMm = null;
|
this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calPxMm = null;
|
||||||
this.result = null;
|
this.result = null;
|
||||||
|
this.tested = false; // #104
|
||||||
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';
|
||||||
@@ -81,12 +90,14 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
},
|
},
|
||||||
// #98: compilato ma non valido -> errore in pagina
|
// #98: compilato ma non valido -> errore in pagina
|
||||||
get mmPxInvalid() { return String(this.mmPx || '').trim() !== '' && this.manualPxMm === null; },
|
get mmPxInvalid() { return String(this.mmPx || '').trim() !== '' && this.manualPxMm === null; },
|
||||||
|
// #104: la scala a mano vale solo per un'immagine da file; con la camera vale la calibrazione della stazione
|
||||||
|
get useManualScale() { return this.imageSource === 'file' && this.manualPxMm !== null; },
|
||||||
get canRun() {
|
get canRun() {
|
||||||
// #98: la scala a mano sostituisce la calibrazione della stazione
|
// #98: la scala a mano sostituisce la calibrazione della stazione (#104: solo per immagini da file)
|
||||||
return !!(this.stationId && (this.calibrated || this.manualPxMm !== null) && this.imageFile && this.dxfFile) && !this.busy;
|
return !!(this.stationId && (this.calibrated || this.useManualScale) && this.imageFile && this.dxfFile) && !this.busy;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setImage(file) {
|
_setImage(file, source) {
|
||||||
if (this.imageSrc) URL.revokeObjectURL(this.imageSrc);
|
if (this.imageSrc) URL.revokeObjectURL(this.imageSrc);
|
||||||
// #102: con file null (immagine tolta) si azzera anche l'anteprima
|
// #102: con file null (immagine tolta) si azzera anche l'anteprima
|
||||||
this.imageFile = file || null;
|
this.imageFile = file || null;
|
||||||
@@ -95,6 +106,9 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
this.imagePx = null;
|
this.imagePx = null;
|
||||||
this.imageBroken = false;
|
this.imageBroken = false;
|
||||||
this.result = null;
|
this.result = null;
|
||||||
|
this.tested = false; // #104
|
||||||
|
this.resPx = null; // #104
|
||||||
|
this.imageSource = file ? (source || '') : ''; // #104
|
||||||
},
|
},
|
||||||
|
|
||||||
// #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile
|
// #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile
|
||||||
@@ -115,18 +129,19 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
const lang = document.documentElement.lang || 'it';
|
const lang = document.documentElement.lang || 'it';
|
||||||
const f = (v, d) => Number(v).toLocaleString(lang, { minimumFractionDigits: d, maximumFractionDigits: d });
|
const f = (v, d) => Number(v).toLocaleString(lang, { minimumFractionDigits: d, maximumFractionDigits: d });
|
||||||
let text = `${f(p.w, 0)} × ${f(p.h, 0)} px`;
|
let text = `${f(p.w, 0)} × ${f(p.h, 0)} px`;
|
||||||
const pxMm = this.manualPxMm !== null ? this.manualPxMm : (this.calibrated ? this.calPxMm : null);
|
const pxMm = this.useManualScale ? this.manualPxMm : (this.calibrated ? this.calPxMm : null); // #104
|
||||||
if (pxMm) text += ` · ${this.msgs.imageField || 'field'} ≈ ${f(p.w / pxMm, 2)} × ${f(p.h / pxMm, 2)} mm`;
|
if (pxMm) text += ` · ${this.msgs.imageField || 'field'} ≈ ${f(p.w / pxMm, 2)} × ${f(p.h / pxMm, 2)} mm`;
|
||||||
return text;
|
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, 'file'); // #104
|
||||||
},
|
},
|
||||||
onDxf(ev) {
|
onDxf(ev) {
|
||||||
this.dxfFile = (ev.target.files && ev.target.files[0]) || null;
|
this.dxfFile = (ev.target.files && ev.target.files[0]) || null;
|
||||||
this.result = null;
|
this.result = null;
|
||||||
|
this.tested = false; // #104
|
||||||
this.loadDxfPreview(); // #101: anteprima appena si sceglie il file
|
this.loadDxfPreview(); // #101: anteprima appena si sceglie il file
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -200,7 +215,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
this.error = body.detail || this.msgs.requestFailed || 'Error';
|
this.error = body.detail || this.msgs.requestFailed || 'Error';
|
||||||
} else {
|
} else {
|
||||||
const blob = await resp.blob();
|
const blob = await resp.blob();
|
||||||
this._setImage(new File([blob], 'camera.png', { type: blob.type || 'image/png' }));
|
this._setImage(new File([blob], 'camera.png', { type: blob.type || 'image/png' }), 'camera'); // #104
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = this.msgs.requestFailed || String(e);
|
this.error = this.msgs.requestFailed || String(e);
|
||||||
@@ -224,13 +239,17 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.error = '';
|
this.error = '';
|
||||||
this.notice = '';
|
this.notice = '';
|
||||||
|
// #104: parametri usati, mostrati accanto ai risultati
|
||||||
|
this.runPxPerMm = this.useManualScale ? this.manualPxMm : (this.calibrated ? this.calPxMm : null);
|
||||||
|
this.runBandMm = this.bandMm;
|
||||||
|
this.runMinPointsPct = this.minPointsPct;
|
||||||
try {
|
try {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('image', this.imageFile, this.imageFile.name || 'capture.png');
|
fd.append('image', this.imageFile, this.imageFile.name || 'capture.png');
|
||||||
fd.append('dxf', this.dxfFile, this.dxfFile.name);
|
fd.append('dxf', this.dxfFile, this.dxfFile.name);
|
||||||
fd.append('station_id', this.stationId);
|
fd.append('station_id', this.stationId);
|
||||||
// #98: scala a mano; il backend la preferisce alla calibrazione della stazione
|
// #98: scala a mano; il backend la preferisce alla calibrazione della stazione (#104: solo con immagine da file)
|
||||||
if (this.manualPxMm !== null) fd.append('px_per_mm', String(this.manualPxMm));
|
if (this.useManualScale) fd.append('px_per_mm', String(this.manualPxMm));
|
||||||
fd.append('band_mm', String(this.bandMm));
|
fd.append('band_mm', String(this.bandMm));
|
||||||
fd.append('min_points_pct', String(this.minPointsPct));
|
fd.append('min_points_pct', String(this.minPointsPct));
|
||||||
fd.append('polarity', this.dark ? 'dark' : 'light');
|
fd.append('polarity', this.dark ? 'dark' : 'light');
|
||||||
@@ -254,12 +273,75 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
this.error = this.msgs.requestFailed || String(e);
|
this.error = this.msgs.requestFailed || String(e);
|
||||||
}
|
}
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
// #104: la sezione del risultato compare anche con Prova fallita (immagine a sinistra, errore a destra)
|
||||||
|
this.tested = true;
|
||||||
|
this.$nextTick(() => this.fit());
|
||||||
},
|
},
|
||||||
|
|
||||||
// Le quattro uscite del confronto, nell'ordine fisso
|
// Le quattro uscite del confronto, nell'ordine fisso
|
||||||
get values() {
|
get values() {
|
||||||
const out = (this.result && this.result.result && this.result.result.outputs) || {};
|
const out = (this.result && this.result.result && this.result.result.outputs) || {};
|
||||||
return VALUE_KEYS.map(k => ({ key: k, value: out[k] }));
|
return VALUE_KEYS.map(k => ({ key: k, value: out[k], label: this._valueLabel(k), unit: VALUE_UNITS[k] })); // #104
|
||||||
|
},
|
||||||
|
|
||||||
|
// #104: etichette leggibili (testi passati da msgs), chiave grezza come ripiego
|
||||||
|
_valueLabel(k) {
|
||||||
|
const m = { 'dxf.points_in_pct': this.msgs.pointsInBand, 'dxf.distance_avg': this.msgs.distAvg,
|
||||||
|
'dxf.distance_min': this.msgs.distMin, 'dxf.distance_max': this.msgs.distMax };
|
||||||
|
return m[k] || k;
|
||||||
|
},
|
||||||
|
|
||||||
|
// #104: esito della ricetta sul riferimento: pass / warning / fail (src/vision/runner.py); altro = valore grezzo
|
||||||
|
get outcome() { return (this.result && this.result.result && this.result.result.outcome) || ''; },
|
||||||
|
get outcomeLabel() {
|
||||||
|
const m = { pass: this.msgs.outcomePass, warning: this.msgs.outcomeWarning, fail: this.msgs.outcomeFail };
|
||||||
|
return m[this.outcome] || this.outcome;
|
||||||
|
},
|
||||||
|
get outcomeClass() {
|
||||||
|
return { pass: 'text-emerald-600', warning: 'text-amber-600', fail: 'text-red-500' }[this.outcome] || 'text-[var(--text-muted)]';
|
||||||
|
},
|
||||||
|
// #104: scala usata nell'ultima Prova, con la fonte
|
||||||
|
get scaleSummary() {
|
||||||
|
if (!this.runPxPerMm) return this.msgs.scaleStation || 'station calibration';
|
||||||
|
const lang = document.documentElement.lang || 'it';
|
||||||
|
const mm = (1 / this.runPxPerMm).toLocaleString(lang, { maximumFractionDigits: 6 });
|
||||||
|
return `${mm} mm/px (${this.imageSource === 'file' ? (this.msgs.scaleManual || 'manual') : (this.msgs.scaleStation || 'station calibration')})`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// #104: zoom dell'immagine di prova. Il wrapper e' largo naturalWidth*zoom; il canvas resta in pixel immagine e lo scala il CSS
|
||||||
|
get zoomPct() { return Math.round(this.zoom * 100) + ' %'; },
|
||||||
|
get zoomW() { return this.resPx ? this.resPx.w * this.zoom : 0; },
|
||||||
|
get zoomH() { return this.resPx ? this.resPx.h * this.zoom : 0; },
|
||||||
|
zoomBy(f) {
|
||||||
|
const vp = this.$refs.viewport;
|
||||||
|
const old = this.zoom;
|
||||||
|
const z = Math.min(8, Math.max(0.05, old * f));
|
||||||
|
if (z === old) return;
|
||||||
|
// centro visibile invariato: coordinate immagine del centro prima, scroll dopo il nuovo layout
|
||||||
|
let cx = 0, cy = 0;
|
||||||
|
if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; }
|
||||||
|
this.zoom = z;
|
||||||
|
if (vp) this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; });
|
||||||
|
},
|
||||||
|
// #104: Fit = tutta l'immagine nel viewport
|
||||||
|
fit() {
|
||||||
|
const vp = this.$refs.viewport, img = this.$refs.img;
|
||||||
|
if (!vp || !img || !img.naturalWidth || !vp.clientWidth || !vp.clientHeight) return;
|
||||||
|
this.zoom = Math.min(8, Math.max(0.05, Math.min(vp.clientWidth / img.naturalWidth, vp.clientHeight / img.naturalHeight)));
|
||||||
|
vp.scrollLeft = 0; vp.scrollTop = 0;
|
||||||
|
},
|
||||||
|
// #104: Ctrl+rotella = zoom
|
||||||
|
onWheel(ev) {
|
||||||
|
if (!ev.ctrlKey) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
this.zoomBy(ev.deltaY < 0 ? 1.25 : 1 / 1.25);
|
||||||
|
},
|
||||||
|
// #104: load dell'immagine di prova: dimensioni, overlay e adattamento allo spazio
|
||||||
|
onResultImgLoad() {
|
||||||
|
const img = this.$refs.img;
|
||||||
|
this.resPx = img && img.naturalWidth ? { w: img.naturalWidth, h: img.naturalHeight } : null;
|
||||||
|
this.drawOverlay();
|
||||||
|
this.$nextTick(() => this.fit());
|
||||||
},
|
},
|
||||||
|
|
||||||
fmt(v) {
|
fmt(v) {
|
||||||
|
|||||||
@@ -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à'), 'imageField': _('campo')}|tojson_attr }})">
|
x-data="dxfRecipeWizard(task.id, () => csrfToken(), {{ {'requestFailed': _('Richiesta non riuscita'), 'saved': _('Ricetta salvata'), 'dxfEntities': _('entità'), 'imageField': _('campo'), 'pointsInBand': _('Punti in banda'), 'distAvg': _('Distanza media'), 'distMin': _('Distanza minima'), 'distMax': _('Distanza massima'), 'outcomePass': _('Conforme'), 'outcomeWarning': _('Con avvisi'), 'outcomeFail': _('Non conforme'), 'scaleManual': _('mm/px a mano'), 'scaleStation': _('calibrazione stazione')}|tojson_attr }})">{# #104: etichette del risultato della Prova #}
|
||||||
<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>
|
||||||
@@ -680,24 +680,14 @@
|
|||||||
<p x-show="stationId && calibrated" class="mt-1 text-xs text-emerald-600">
|
<p x-show="stationId && calibrated" class="mt-1 text-xs text-emerald-600">
|
||||||
{{ _('Calibrazione') }}: <span x-text="calLabel"></span>
|
{{ _('Calibrazione') }}: <span x-text="calLabel"></span>
|
||||||
</p>
|
</p>
|
||||||
<p x-show="stationId && calState && !calibrated && manualPxMm === null" class="mt-1 text-xs text-amber-600">{# #98: nascosto con mm/px valido #}
|
<p x-show="stationId && calState && !calibrated && !useManualScale" class="mt-1 text-xs text-amber-600">{# #98: nascosto con mm/px valido (#104: solo per immagine da file) #}
|
||||||
<span x-text="calState === 'uncalibrated' ? {{ _('Stazione non calibrata: calibrala prima di creare la ricetta.')|tojson_attr }} : {{ _('Acquisitore non raggiungibile: impossibile leggere la calibrazione.')|tojson_attr }}"></span>
|
<span x-text="calState === 'uncalibrated' ? {{ _('Stazione non calibrata: calibrala prima di creare la ricetta.')|tojson_attr }} : {{ _('Acquisitore non raggiungibile: impossibile leggere la calibrazione.')|tojson_attr }}"></span>
|
||||||
<a :href="calUrl" target="_blank" rel="noopener" x-show="calUrl"
|
<a :href="calUrl" target="_blank" rel="noopener" x-show="calUrl"
|
||||||
class="font-medium text-primary hover:underline">{{ _('Calibra') }}</a>
|
class="font-medium text-primary hover:underline">{{ _('Calibra') }}</a>
|
||||||
<span class="text-[var(--text-muted)]">({{ _('oppure vedi la pagina Stazioni') }})</span>
|
<span class="text-[var(--text-muted)]">({{ _('oppure vedi la pagina Stazioni') }})</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- #98: mm/px a mano per immagini da file, sostituisce la calibrazione della stazione -->
|
<!-- Immagine: camera o file. #104: prima le scelte, poi le anteprime affiancate -->
|
||||||
<div>
|
|
||||||
<label class="tmf-label text-xs">{{ _('mm/px (immagine da file)') }}</label>
|
|
||||||
<input type="text" inputmode="decimal" class="tmf-input text-xs" x-model="mmPx" placeholder="0,01406">
|
|
||||||
<p x-show="manualPxMm !== null" class="mt-1 text-xs text-emerald-600">
|
|
||||||
= <span x-text="fmt(manualPxMm)"></span> px/mm
|
|
||||||
</p>
|
|
||||||
<p x-show="mmPxInvalid" class="mt-1 text-xs text-amber-600">{{ _('mm/px non valido') }}</p>
|
|
||||||
<p class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Se compilato sostituisce la calibrazione della stazione; la scala resta salvata nella ricetta.') }}</p>
|
|
||||||
</div>
|
|
||||||
<!-- Immagine: camera o file -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="tmf-label text-xs">{{ _('Immagine del pezzo') }}</label>
|
<label class="tmf-label text-xs">{{ _('Immagine del pezzo') }}</label>
|
||||||
@@ -710,33 +700,46 @@
|
|||||||
</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) -->
|
<!-- #98: mm/px a mano, #104: solo per un'immagine da file; sostituisce la calibrazione della stazione -->
|
||||||
<div x-show="imagePreviewUrl" class="mt-2">
|
<div x-show="imageSource === 'file'" class="mt-2">
|
||||||
<img x-show="!imageBroken" :src="imagePreviewUrl" alt=""
|
<label class="tmf-label text-xs">{{ _('mm/px (immagine da file)') }}</label>
|
||||||
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
|
<input type="text" inputmode="decimal" class="tmf-input text-xs" x-model="mmPx" placeholder="0,01406">
|
||||||
class="block w-full rounded border border-[var(--border-color)]"
|
<p x-show="manualPxMm !== null" class="mt-1 text-xs text-emerald-600">
|
||||||
style="max-height: 240px; object-fit: contain; background: var(--bg-primary);">
|
= <span x-text="fmt(manualPxMm)"></span> px/mm
|
||||||
<p x-show="imageBroken" class="text-xs text-amber-600">{{ _('Anteprima non disponibile per questo formato') }}</p>
|
</p>
|
||||||
<p x-show="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p>
|
<p x-show="mmPxInvalid" class="mt-1 text-xs text-amber-600">{{ _('mm/px non valido') }}</p>
|
||||||
|
<p class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Se compilato sostituisce la calibrazione della stazione; la scala resta salvata nella ricetta.') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
|
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
|
||||||
<input type="file" accept=".dxf" class="tmf-input text-xs" @change="onDxf($event)">
|
<input type="file" accept=".dxf" class="tmf-input text-xs" @change="onDxf($event)">
|
||||||
<!-- #101: anteprima del disegno appena scelto il .dxf -->
|
<!-- #101: lettura del disegno appena scelto il .dxf -->
|
||||||
<p x-show="dxfPreviewBusy" class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Lettura DXF…') }}</p>
|
<p x-show="dxfPreviewBusy" class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Lettura DXF…') }}</p>
|
||||||
<p x-show="dxfPreviewError" class="mt-1 text-xs text-red-500" x-text="dxfPreviewError"></p>
|
<p x-show="dxfPreviewError" class="mt-1 text-xs text-red-500" x-text="dxfPreviewError"></p>
|
||||||
<div x-show="dxfPreview" class="mt-2">
|
</div>
|
||||||
<svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet"
|
</div>
|
||||||
class="block w-full rounded border border-[var(--border-color)]"
|
<!-- #104: anteprime sotto le scelte, scatole alte uguali (240px) per stare allineate -->
|
||||||
style="max-height: 240px; background: var(--bg-primary);">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">
|
||||||
<!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox -->
|
<div x-show="imagePreviewUrl">
|
||||||
<path :d="dxfPreviewD()" fill="none" stroke="var(--color-primary, #2563eb)"
|
<!-- #102: anteprima dell'immagine -->
|
||||||
stroke-width="1.5" vector-effect="non-scaling-stroke"
|
<img x-show="!imageBroken" :src="imagePreviewUrl" alt=""
|
||||||
stroke-linejoin="round"></path>
|
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
|
||||||
</svg>
|
class="block w-full rounded border border-[var(--border-color)]"
|
||||||
<p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p>
|
style="height: 240px; object-fit: contain; background: var(--bg-primary);">
|
||||||
</div>
|
<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 x-show="dxfPreview">
|
||||||
|
<svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet"
|
||||||
|
class="block w-full rounded border border-[var(--border-color)]"
|
||||||
|
style="height: 240px; background: var(--bg-primary);">
|
||||||
|
<!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox -->
|
||||||
|
<path :d="dxfPreviewD()" fill="none" stroke="var(--color-primary, #2563eb)"
|
||||||
|
stroke-width="1.5" vector-effect="non-scaling-stroke"
|
||||||
|
stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Pochi parametri -->
|
<!-- Pochi parametri -->
|
||||||
@@ -761,21 +764,50 @@
|
|||||||
<button type="button" class="btn btn-primary text-xs" @click="run(true)"
|
<button type="button" class="btn btn-primary text-xs" @click="run(true)"
|
||||||
:disabled="!canRun || !result">{{ _('Salva ricetta') }}</button>
|
:disabled="!canRun || !result">{{ _('Salva ricetta') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<p x-show="error" class="text-xs text-red-500" x-text="error"></p>
|
<!-- #104: errore di acquisizione prima di una Prova (dopo, sta nella colonna dei dati) -->
|
||||||
<p x-show="notice" class="text-xs text-emerald-600" x-text="notice"></p>
|
<p x-show="error && !tested" class="text-xs text-red-500" x-text="error"></p>
|
||||||
<!-- Anteprima con overlay e quattro valori -->
|
<!-- #104: risultato della Prova su due colonne: immagine con zoom a sinistra, dati ed errori a destra -->
|
||||||
<div x-show="imageSrc" class="relative">
|
<div x-show="tested" class="grid grid-cols-1 lg:grid-cols-2 gap-3 items-start">
|
||||||
<img x-ref="img" :src="imageSrc" alt="" class="block w-full rounded border border-[var(--border-color)]"
|
<div>
|
||||||
@load="drawOverlay()">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<canvas x-ref="overlay" class="absolute inset-0 w-full h-full pointer-events-none"></canvas>
|
<button type="button" class="btn btn-secondary text-xs" @click="zoomBy(1 / 1.25)" title="{{ _('Riduci') }}">−</button>
|
||||||
|
<button type="button" class="btn btn-secondary text-xs" @click="zoomBy(1.25)" title="{{ _('Ingrandisci') }}">+</button>
|
||||||
|
<button type="button" class="btn btn-secondary text-xs" @click="fit()">{{ _('Adatta') }}</button>
|
||||||
|
<span class="text-xs text-[var(--text-muted)]" x-text="zoomPct"></span>
|
||||||
|
</div>
|
||||||
|
<div x-ref="viewport" @wheel="onWheel($event)"
|
||||||
|
class="rounded border border-[var(--border-color)]"
|
||||||
|
style="overflow: auto; height: min(70vh, 520px); background: var(--bg-primary);">
|
||||||
|
<div class="relative" :style="zoomW ? ('width:' + zoomW + 'px;height:' + zoomH + 'px') : 'width:100%'">
|
||||||
|
<img x-ref="img" :src="imageSrc" alt="" class="block" style="width: 100%; height: 100%;"
|
||||||
|
@load="onResultImgLoad()">
|
||||||
|
<canvas x-ref="overlay" class="absolute inset-0 w-full h-full pointer-events-none"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p x-show="error" class="text-xs text-red-500" x-text="error"></p>
|
||||||
|
<p x-show="notice" class="text-xs text-emerald-600" x-text="notice"></p>
|
||||||
|
<div x-show="result" class="space-y-2">
|
||||||
|
<p class="text-xs">{{ _('Esito') }}: <span class="font-semibold" :class="outcomeClass" x-text="outcomeLabel"></span></p>
|
||||||
|
<table class="tmf-table text-xs">
|
||||||
|
<tbody>
|
||||||
|
<template x-for="v in values" :key="v.key">
|
||||||
|
<tr>
|
||||||
|
<td :title="v.key" x-text="v.label"></td>
|
||||||
|
<td class="text-right"><span x-text="fmt(v.value)"></span> <span class="text-[var(--text-muted)]" x-text="v.unit"></span></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="text-xs text-[var(--text-muted)] space-y-0.5">
|
||||||
|
<li>{{ _('Scala') }}: <span x-text="scaleSummary"></span></li>
|
||||||
|
<li>{{ _('Banda ± mm') }}: <span x-text="fmt(runBandMm)"></span></li>
|
||||||
|
<li>{{ _('Soglia punti in banda %%') }}: <span x-text="fmt(runMinPointsPct)"></span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table x-show="result" class="tmf-table text-xs">
|
|
||||||
<tbody>
|
|
||||||
<template x-for="v in values" :key="v.key">
|
|
||||||
<tr><td class="font-mono" x-text="v.key"></td><td class="text-right" x-text="fmt(v.value)"></td></tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const PREVIEW = {
|
|||||||
check('#102 senza immagine: summary vuoto', wi.imagePreviewSummary === '');
|
check('#102 senza immagine: summary vuoto', wi.imagePreviewSummary === '');
|
||||||
global.URL.createObjectURL = () => 'blob:x';
|
global.URL.createObjectURL = () => 'blob:x';
|
||||||
global.URL.revokeObjectURL = () => {};
|
global.URL.revokeObjectURL = () => {};
|
||||||
wi._setImage({ name: 'a.bmp' });
|
wi._setImage({ name: 'a.bmp' }, 'file'); // #104: la scala a mano vale solo per file
|
||||||
check('#102 _setImage imposta URL e nome', wi.imagePreviewUrl === 'blob:x' && wi.imageName === 'a.bmp');
|
check('#102 _setImage imposta URL e nome', wi.imagePreviewUrl === 'blob:x' && wi.imageName === 'a.bmp');
|
||||||
wi.onImagePreviewLoad({ naturalWidth: 2000, naturalHeight: 1000 });
|
wi.onImagePreviewLoad({ naturalWidth: 2000, naturalHeight: 1000 });
|
||||||
check('#102 senza scala: solo pixel', /2[. ]?000 × 1[. ]?000 px$/.test(wi.imagePreviewSummary), wi.imagePreviewSummary);
|
check('#102 senza scala: solo pixel', /2[. ]?000 × 1[. ]?000 px$/.test(wi.imagePreviewSummary), wi.imagePreviewSummary);
|
||||||
@@ -104,6 +104,69 @@ const PREVIEW = {
|
|||||||
check('#102 _setImage(null) azzera', wi.imagePreviewUrl === '' && wi.imageFile === null && wi.imageName === ''
|
check('#102 _setImage(null) azzera', wi.imagePreviewUrl === '' && wi.imageFile === null && wi.imageName === ''
|
||||||
&& wi.imagePx === null && wi.imageBroken === false && wi.imagePreviewSummary === '');
|
&& wi.imagePx === null && wi.imageBroken === false && wi.imagePreviewSummary === '');
|
||||||
|
|
||||||
|
// #104: scala a mano solo per immagini da file
|
||||||
|
const w4 = dxfRecipeWizard(1, () => 'tok', {});
|
||||||
|
w4._setImage({ name: 'a.bmp' }, 'file');
|
||||||
|
w4.mmPx = '0,01406';
|
||||||
|
check('#104 useManualScale vero con file', w4.useManualScale === true);
|
||||||
|
w4._setImage({ name: 'camera.png' }, 'camera');
|
||||||
|
check('#104 useManualScale falso con camera anche se mmPx valido', w4.manualPxMm !== null && w4.useManualScale === false);
|
||||||
|
w4.stationId = '1'; w4.dxfFile = { name: 'd.dxf' };
|
||||||
|
check('#104 canRun con camera richiede la calibrazione', w4.canRun === false);
|
||||||
|
w4.calState = 'calibrated'; w4.calPxMm = 50;
|
||||||
|
check('#104 canRun con camera e stazione calibrata', w4.canRun === true);
|
||||||
|
|
||||||
|
// #104: run() con la camera non invia px_per_mm; con il file si
|
||||||
|
const sent = [];
|
||||||
|
global.FormData = class { append(k, v) { sent.push(k); } };
|
||||||
|
global.fetch = async () => ({ ok: true, json: async () => ({ result: { outputs: {}, overlay: [], outcome: 'pass' } }) });
|
||||||
|
const tick = [];
|
||||||
|
w4.$nextTick = f => tick.push(f);
|
||||||
|
w4.$dispatch = () => {};
|
||||||
|
w4.$refs = {};
|
||||||
|
await w4.run(false);
|
||||||
|
check('#104 run con camera: niente px_per_mm', !sent.includes('px_per_mm') && sent.includes('station_id'), sent.join(','));
|
||||||
|
check('#104 run: tested true e scala = stazione', w4.tested === true && w4.runPxPerMm === 50);
|
||||||
|
sent.length = 0;
|
||||||
|
w4._setImage({ name: 'a.bmp' }, 'file');
|
||||||
|
w4.mmPx = '0,02';
|
||||||
|
await w4.run(false);
|
||||||
|
check('#104 run con file: px_per_mm inviato', sent.includes('px_per_mm') && Math.abs(w4.runPxPerMm - 50) < 1e-9, sent.join(','));
|
||||||
|
check('#104 esito pass', w4.outcome === 'pass' && w4.outcomeClass.includes('emerald'));
|
||||||
|
|
||||||
|
// #104: tested torna false dopo _setImage / onDxf / onStation
|
||||||
|
w4.tested = true; w4._setImage(null);
|
||||||
|
check('#104 tested falso dopo _setImage', w4.tested === false && w4.imageSource === '');
|
||||||
|
w4.tested = true; w4.onDxf({ target: { files: [] } });
|
||||||
|
check('#104 tested falso dopo onDxf', w4.tested === false);
|
||||||
|
w4.tested = true; w4.stationId = ''; await w4.onStation();
|
||||||
|
check('#104 tested falso dopo onStation', w4.tested === false);
|
||||||
|
|
||||||
|
// #104: zoomBy rispetta i limiti 0.05..8
|
||||||
|
const w5 = dxfRecipeWizard(1, () => 'tok', {});
|
||||||
|
w5.$nextTick = f => f();
|
||||||
|
w5.$refs = {};
|
||||||
|
for (let i = 0; i < 40; i++) w5.zoomBy(1.25);
|
||||||
|
check('#104 zoom massimo 8', w5.zoom === 8, String(w5.zoom));
|
||||||
|
for (let i = 0; i < 80; i++) w5.zoomBy(1 / 1.25);
|
||||||
|
check('#104 zoom minimo 0.05', w5.zoom === 0.05, String(w5.zoom));
|
||||||
|
w5.zoom = 1;
|
||||||
|
const vp = { scrollLeft: 100, scrollTop: 50, clientWidth: 200, clientHeight: 100 };
|
||||||
|
w5.$refs = { viewport: vp };
|
||||||
|
w5.zoomBy(2);
|
||||||
|
check('#104 zoomBy tiene il centro', w5.zoom === 2 && vp.scrollLeft === 300 && vp.scrollTop === 150, vp.scrollLeft + ',' + vp.scrollTop);
|
||||||
|
check('#104 zoomPct', w5.zoomPct === '200 %');
|
||||||
|
|
||||||
|
// #104: fit = min(viewport/immagine)
|
||||||
|
w5.$refs = { viewport: { clientWidth: 500, clientHeight: 400, scrollLeft: 9, scrollTop: 9 }, img: { naturalWidth: 2000, naturalHeight: 1000 } };
|
||||||
|
w5.fit();
|
||||||
|
check('#104 fit limitato dalla larghezza', Math.abs(w5.zoom - 0.25) < 1e-9, String(w5.zoom));
|
||||||
|
w5.$refs.img = { naturalWidth: 1000, naturalHeight: 2000 };
|
||||||
|
w5.fit();
|
||||||
|
check('#104 fit limitato dall altezza', Math.abs(w5.zoom - 0.2) < 1e-9, String(w5.zoom));
|
||||||
|
w5.$refs.viewport.clientWidth = 0; w5.zoom = 1; w5.fit();
|
||||||
|
check('#104 fit con viewport nascosto non cambia', w5.zoom === 1);
|
||||||
|
|
||||||
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,5 +280,6 @@ 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 "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit
|
||||||
assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine
|
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
|
||||||
|
|||||||
@@ -3507,3 +3507,55 @@ msgstr "field"
|
|||||||
#: templates/maker/task_editor.html (#102)
|
#: templates/maker/task_editor.html (#102)
|
||||||
msgid "Anteprima non disponibile per questo formato"
|
msgid "Anteprima non disponibile per questo formato"
|
||||||
msgstr "Preview not available for this format"
|
msgstr "Preview not available for this format"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Riduci"
|
||||||
|
msgstr "Zoom out"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Ingrandisci"
|
||||||
|
msgstr "Zoom in"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Adatta"
|
||||||
|
msgstr "Fit"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Scala"
|
||||||
|
msgstr "Scale"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Punti in banda"
|
||||||
|
msgstr "Points in band"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza media"
|
||||||
|
msgstr "Average distance"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza minima"
|
||||||
|
msgstr "Minimum distance"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza massima"
|
||||||
|
msgstr "Maximum distance"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Conforme"
|
||||||
|
msgstr "Pass"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Con avvisi"
|
||||||
|
msgstr "Warning"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Non conforme"
|
||||||
|
msgstr "Fail"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "mm/px a mano"
|
||||||
|
msgstr "manual mm/px"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "calibrazione stazione"
|
||||||
|
msgstr "station calibration"
|
||||||
|
|||||||
@@ -3491,3 +3491,55 @@ msgstr "campo"
|
|||||||
#: templates/maker/task_editor.html (#102)
|
#: templates/maker/task_editor.html (#102)
|
||||||
msgid "Anteprima non disponibile per questo formato"
|
msgid "Anteprima non disponibile per questo formato"
|
||||||
msgstr "Anteprima non disponibile per questo formato"
|
msgstr "Anteprima non disponibile per questo formato"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Riduci"
|
||||||
|
msgstr "Riduci"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Ingrandisci"
|
||||||
|
msgstr "Ingrandisci"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Adatta"
|
||||||
|
msgstr "Adatta"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Scala"
|
||||||
|
msgstr "Scala"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Punti in banda"
|
||||||
|
msgstr "Punti in banda"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza media"
|
||||||
|
msgstr "Distanza media"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza minima"
|
||||||
|
msgstr "Distanza minima"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Distanza massima"
|
||||||
|
msgstr "Distanza massima"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Conforme"
|
||||||
|
msgstr "Conforme"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Con avvisi"
|
||||||
|
msgstr "Con avvisi"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "Non conforme"
|
||||||
|
msgstr "Non conforme"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "mm/px a mano"
|
||||||
|
msgstr "mm/px a mano"
|
||||||
|
|
||||||
|
#: templates/maker/task_editor.html (#104)
|
||||||
|
msgid "calibrazione stazione"
|
||||||
|
msgstr "calibrazione stazione"
|
||||||
|
|||||||
Reference in New Issue
Block a user