diff --git a/src/frontend/flask_app/static/js/vision_execute.js b/src/frontend/flask_app/static/js/vision_execute.js index 9f4703d..df60194 100644 --- a/src/frontend/flask_app/static/js/vision_execute.js +++ b/src/frontend/flask_app/static/js/vision_execute.js @@ -20,9 +20,6 @@ * Extra config: cameraAvailable, modo, checkUrl. */ function visionExecute() { - // Overlay colour per role (VisionSuite roles: reference, roi, found). - const ROLE_COLORS = { reference: '#2563eb', found: '#16a34a', roi: '#f59e0b' }; - return { busy: false, grabbing: false, // frame requested from the station camera (#72) @@ -50,6 +47,14 @@ function visionExecute() { // #97: start on a frame already acquired and analysed; hook visibility and exit. init() { + // #120: «Rimisura»/nuovo ciclo (task-result-reset): il pezzo va registrato di nuovo, anche senza camera + window.addEventListener('task-result-reset', () => { + this.saved = false; + this.preview = !!this.result; + this.kick(); + }); + // #121: l'overlay SVG si ridisegna a ogni cambio di zoom (spessore e croci costanti a video) + if (typeof this.$watch === 'function') this.$watch('z.zoom', () => this.drawOverlay()); if (!this.hasCamera) return; document.addEventListener('visibilitychange', () => { if (!document.hidden) this.kick(); }); const leave = () => { this.leaving = true; }; @@ -150,6 +155,7 @@ function visionExecute() { if (!this.error && this.result && this.result.vision_result_id) { this.preview = false; this.saved = true; + this._emitRecorded(); // #120: la pagina conta le quote registrate } } finally { this.registering = false; @@ -186,7 +192,9 @@ function visionExecute() { return; } this.result = body; - // Draw once Alpine has rendered the canvas. + // #120: senza camera il file scelto e' salvato subito (nessun «Registra»): stesso evento + if (!this.hasCamera && body.vision_result_id) this._emitRecorded(); + // Draw once Alpine has rendered the svg. this.$nextTick(() => this.drawOverlay()); } catch (e) { this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error'; @@ -195,6 +203,19 @@ function visionExecute() { } }, + // #120: misure salvate (risposta di execute) alla pagina: conteggio quote e «Completato» + _emitRecorded() { + const ms = (this.result && this.result.measurements) || []; + window.dispatchEvent(new CustomEvent('vision-recorded', { detail: { measurements: ms } })); + }, + + // #119: «Salva» = frame grezzo PNG dell'immagine mostrata, senza disegni (save_image.js) + saveImage() { + const cfg = window.__visionCfg || {}; + if (!this.shownImage || typeof tmfSaveImage !== 'function') return Promise.resolve(false); + return tmfSaveImage(this.shownImage, cfg.taskName || 'immagine'); + }, + // Station camera (#72): the Flask proxy answers a PNG, which goes through // acquire() like a picked file. Errors show as the execute ones do. // #97: sends task_id (the task's camera wins over the station's); with a camera @@ -385,6 +406,11 @@ function visionExecute() { }, zPanEnd() { this._zPan = null; this.zPanning = false; }, + // #118: valore della cella: una quota non misurata (null) e' «NaN», mai Number(null) = 0 + fmtValue(v, unit) { + return (v == null || !Number.isFinite(Number(v)) ? 'NaN' : Number(v).toFixed(3)) + ' ' + unit; + }, + // #117: tolleranza a 3 decimali, trattino se manca (come x-text="...?.toFixed(3) || '—'" dei calibri, ma 0 resta 0.000) fmtTol(v) { return v == null ? '\u2014' : Number(v).toFixed(3); }, @@ -420,32 +446,14 @@ function visionExecute() { return 'bg-[var(--bg-secondary)] text-[var(--text-muted)] border border-[var(--border-color)]'; }, - // Canvas is sized to the natural image, so overlay coordinates (image px) - // are used as they are and CSS scales the whole thing to the displayed size. + // #121: overlay SVG (VisionOverlay.svg) nel wrapper dello zoom: viewBox = dimensioni naturali, quindi + // le coordinate (px immagine) valgono cosi' come sono; spessore e croci restano costanti a ogni zoom drawOverlay() { - const img = this.$refs.img, cv = this.$refs.overlay; - if (!img || !cv || !img.naturalWidth) return; - cv.width = img.naturalWidth; - cv.height = img.naturalHeight; - const g = cv.getContext('2d'); - g.clearRect(0, 0, cv.width, cv.height); - const lw = Math.max(2, cv.width / 400); - g.lineWidth = lw; - g.lineJoin = 'round'; - // #116: tutte le forme (rect2, segment, circle, arc...) le disegna vision_overlay.js; il ciclo sotto resta come ripiego - if (typeof VisionOverlay !== 'undefined') { - VisionOverlay.draw(g, (this.result && this.result.overlay) || [], lw); - return; - } - for (const item of ((this.result && this.result.overlay) || [])) { - const rows = item.rows || [], cols = item.cols || []; - if (!rows.length || rows.length !== cols.length) continue; - g.strokeStyle = ROLE_COLORS[item.role] || '#ef4444'; - g.beginPath(); - g.moveTo(cols[0], rows[0]); - for (let i = 1; i < rows.length; i++) g.lineTo(cols[i], rows[i]); - g.stroke(); - } + const img = this.$refs.img, sv = this.$refs.overlay; + if (!sv) return; + if (img && img.naturalWidth) sv.setAttribute('viewBox', '0 0 ' + img.naturalWidth + ' ' + img.naturalHeight); + const ok = typeof VisionOverlay !== 'undefined' && typeof VisionOverlay.svg === 'function'; + sv.innerHTML = ok && this.result ? VisionOverlay.svg(this.result.overlay || [], { zoom: this.z.zoom }) : ''; }, }; } diff --git a/src/frontend/flask_app/templates/measure/task_execute.html b/src/frontend/flask_app/templates/measure/task_execute.html index 27cf0ed..fd9fb0b 100644 --- a/src/frontend/flask_app/templates/measure/task_execute.html +++ b/src/frontend/flask_app/templates/measure/task_execute.html @@ -69,7 +69,8 @@ @numpad-confirm.window="handleMeasurement($event.detail.value, $event.detail.inputMethod)" @numpad-rejected.window="onNumpadRejected($event.detail)" @numpad-error.window="onNumpadError($event.detail)" - @marker-click.window="goToSubtaskByMarker($event.detail.marker_number)"> + @marker-click.window="goToSubtaskByMarker($event.detail.marker_number)" + @vision-recorded.window="onVisionRecorded($event.detail)"> {# ================================================================ HEADER — Compact task info bar @@ -539,13 +540,14 @@
- {# On a tablet this opens the camera; on a PC it picks a file. «Riprova» reuses the same input. #} + {% if not camera_enabled %} + {# #119: l'input file solo senza camera. On a tablet this opens the camera; on a PC it picks a file. «Riprova» reuses the same input. #} - {% if camera_enabled %} + {% else %} {# #97: camera del task (o della stazione). Impulsiva: «Acquisisci» rifa grab+check; in linea: «Pausa»/«Riprendi» sul ciclo #} {% if camera_mode == 'in_linea' %} {% endif %} {# #97: salva l'ULTIMO frame mostrato (execute); fino ad allora e' solo un'anteprima #} @@ -576,7 +578,7 @@
- {# #96: riquadro flex-1 min-h-0; #115: dentro, barra zoom in overlay e viewport scorrevole con il wrapper (naturale x zoom) che contiene img e canvas; il canvas ha la stessa risoluzione naturale dell'immagine, quindi l'overlay resta allineato #} + {# #96: riquadro flex-1 min-h-0; #115: dentro, barra zoom in overlay e viewport scorrevole con il wrapper (naturale x zoom) che contiene img e svg; #121: l'svg ha il viewBox delle dimensioni naturali dell'immagine, quindi l'overlay resta allineato #}
{# #115: x-show sul contenitore della barra, mai sul viewport (con :style stringa Alpine lo pesterebbe) #}
− + {# #119: salva il frame grezzo PNG (senza disegni); la barra compare solo con un'immagine (x-show="z.w") #} +
{# #115: viewport = tutto il riquadro; solo :style stringa (uno style statico verrebbe cancellato da Alpine) #} @@ -593,8 +597,9 @@ :style="'overflow:auto;touch-action:none;user-select:none;display:flex;cursor:' + zCursor()">
- - {# #116: etichette quota in HTML sopra il canvas (posizione in %, dimensione costante a ogni zoom); solo :style stringa #} + {# #121: overlay SVG al posto del canvas: viewBox = dimensioni naturali (le imposta drawOverlay), tratto costante a ogni zoom #} + + {# #116: etichette quota in HTML sopra l'svg (posizione in %, dimensione costante a ogni zoom); solo :style stringa #}