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) {
|
||||
// Colori overlay per ruolo, uguali a vision_execute.js (reference, roi, found)
|
||||
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'];
|
||||
|
||||
return {
|
||||
@@ -37,6 +38,13 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
error: '',
|
||||
notice: '',
|
||||
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() {
|
||||
this.open = !this.open;
|
||||
@@ -59,6 +67,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
async onStation() {
|
||||
this.calState = ''; this.calLabel = ''; this.calUrl = ''; this.calPxMm = null;
|
||||
this.result = null;
|
||||
this.tested = false; // #104
|
||||
if (!this.stationId) return;
|
||||
const r = await fetchStationCalibration(this.stationId);
|
||||
this.calState = r.state || 'unreachable';
|
||||
@@ -81,12 +90,14 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
},
|
||||
// #98: compilato ma non valido -> errore in pagina
|
||||
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() {
|
||||
// #98: la scala a mano sostituisce la calibrazione della stazione
|
||||
return !!(this.stationId && (this.calibrated || this.manualPxMm !== null) && this.imageFile && this.dxfFile) && !this.busy;
|
||||
// #98: la scala a mano sostituisce la calibrazione della stazione (#104: solo per immagini da file)
|
||||
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);
|
||||
// #102: con file null (immagine tolta) si azzera anche l'anteprima
|
||||
this.imageFile = file || null;
|
||||
@@ -95,6 +106,9 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.imagePx = null;
|
||||
this.imageBroken = false;
|
||||
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
|
||||
@@ -115,18 +129,19 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
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);
|
||||
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`;
|
||||
return text;
|
||||
},
|
||||
|
||||
onImage(ev) {
|
||||
const f = ev.target.files && ev.target.files[0];
|
||||
if (f) this._setImage(f);
|
||||
if (f) this._setImage(f, 'file'); // #104
|
||||
},
|
||||
onDxf(ev) {
|
||||
this.dxfFile = (ev.target.files && ev.target.files[0]) || null;
|
||||
this.result = null;
|
||||
this.tested = false; // #104
|
||||
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';
|
||||
} else {
|
||||
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) {
|
||||
this.error = this.msgs.requestFailed || String(e);
|
||||
@@ -224,13 +239,17 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.busy = true;
|
||||
this.error = '';
|
||||
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 {
|
||||
const fd = new FormData();
|
||||
fd.append('image', this.imageFile, this.imageFile.name || 'capture.png');
|
||||
fd.append('dxf', this.dxfFile, this.dxfFile.name);
|
||||
fd.append('station_id', this.stationId);
|
||||
// #98: scala a mano; il backend la preferisce alla calibrazione della stazione
|
||||
if (this.manualPxMm !== null) fd.append('px_per_mm', String(this.manualPxMm));
|
||||
// #98: scala a mano; il backend la preferisce alla calibrazione della stazione (#104: solo con immagine da file)
|
||||
if (this.useManualScale) fd.append('px_per_mm', String(this.manualPxMm));
|
||||
fd.append('band_mm', String(this.bandMm));
|
||||
fd.append('min_points_pct', String(this.minPointsPct));
|
||||
fd.append('polarity', this.dark ? 'dark' : 'light');
|
||||
@@ -254,12 +273,75 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.error = this.msgs.requestFailed || String(e);
|
||||
}
|
||||
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
|
||||
get values() {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user