Pannello visione: integrazione overlay ed etichette quota (rows, drawOverlay, overlayLabels)

Refs #116

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 17:00:48 +01:00
parent 88b3f202b1
commit acd94a9d33
4 changed files with 101 additions and 1 deletions
@@ -261,6 +261,10 @@ function visionExecute() {
return ((this.result && this.result.measurements) || []).map((m) => { return ((this.result && this.result.measurements) || []).map((m) => {
const st = subs.find((s) => s.id === m.subtask_id) || {}; const st = subs.find((s) => s.id === m.subtask_id) || {};
return { return {
// #116: chiave stabile della quota, nome senza numero di marker e uscita ricetta, per le etichette sull'immagine
key: m.subtask_id,
name: st.description || st.measurement_type || '#' + m.subtask_id,
vision_output: st.vision_output,
// #114: il nome della quota e' description (prima: measurement_type/name, che per le quote DXF e' vuoto -> '#15') // #114: il nome della quota e' description (prima: measurement_type/name, che per le quote DXF e' vuoto -> '#15')
label: (st.marker_number ? st.marker_number + '. ' : '') + (st.description || st.measurement_type || '#' + m.subtask_id), label: (st.marker_number ? st.marker_number + '. ' : '') + (st.description || st.measurement_type || '#' + m.subtask_id),
value: m.value, value: m.value,
@@ -384,6 +388,30 @@ function visionExecute() {
// #117: tolleranza a 3 decimali, trattino se manca (come x-text="...?.toFixed(3) || '—'" dei calibri, ma 0 resta 0.000) // #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); }, fmtTol(v) { return v == null ? '\u2014' : Number(v).toFixed(3); },
// #116: etichette «nome valore unita» agganciate alla geometria dello strumento; posizione in % delle
// dimensioni naturali dell'immagine, cosi' seguono lo zoom (#115) senza ingrandirsi
get overlayLabels() {
if (typeof VisionOverlay === 'undefined' || !this.result) return [];
const img = this.$refs && this.$refs.img;
const w = this.z.w || (img && img.naturalWidth) || 0;
const h = this.z.h || (img && img.naturalHeight) || 0;
if (!w || !h) return [];
return VisionOverlay.labels(this.rows, this.result.overlay || []).map((l) => ({
key: l.key,
text: l.text,
cls: this.labelClass(l.status),
style: 'left:' + (l.x / w * 100) + '%;top:' + (l.y / h * 100) + '%;transform:translate(8px,-50%)',
}));
},
// #116: colori dell'etichetta per esito
labelClass(s) {
if (s === 'pass') return 'bg-green-600 text-white';
if (s === 'warning') return 'bg-amber-400 text-black';
if (s === 'fail') return 'bg-red-600 text-white';
return 'bg-gray-700 text-white';
},
statusClass(s) { statusClass(s) {
// Same palette as the measurement markers of the manual flow. // Same palette as the measurement markers of the manual flow.
if (s === 'pass') return 'bg-measure-pass/20 text-measure-pass border border-measure-pass/40'; if (s === 'pass') return 'bg-measure-pass/20 text-measure-pass border border-measure-pass/40';
@@ -401,8 +429,14 @@ function visionExecute() {
cv.height = img.naturalHeight; cv.height = img.naturalHeight;
const g = cv.getContext('2d'); const g = cv.getContext('2d');
g.clearRect(0, 0, cv.width, cv.height); g.clearRect(0, 0, cv.width, cv.height);
g.lineWidth = Math.max(2, cv.width / 400); const lw = Math.max(2, cv.width / 400);
g.lineWidth = lw;
g.lineJoin = 'round'; 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) || [])) { for (const item of ((this.result && this.result.overlay) || [])) {
const rows = item.rows || [], cols = item.cols || []; const rows = item.rows || [], cols = item.cols || [];
if (!rows.length || rows.length !== cols.length) continue; if (!rows.length || rows.length !== cols.length) continue;
@@ -594,6 +594,11 @@
<div class="relative" :style="'margin:auto;flex:none;' + zSize()"> <div class="relative" :style="'margin:auto;flex:none;' + zSize()">
<img x-ref="img" :src="shownImage" @load="onImgLoad()" alt="" draggable="false" class="absolute inset-0 w-full h-full object-contain"> <img x-ref="img" :src="shownImage" @load="onImgLoad()" alt="" draggable="false" class="absolute inset-0 w-full h-full object-contain">
<canvas x-ref="overlay" class="absolute inset-0 w-full h-full object-contain pointer-events-none"></canvas> <canvas x-ref="overlay" class="absolute inset-0 w-full h-full object-contain pointer-events-none"></canvas>
{# #116: etichette quota in HTML sopra il canvas (posizione in %, dimensione costante a ogni zoom); solo :style stringa #}
<template x-for="l in overlayLabels" :key="l.key">
<div class="absolute pointer-events-none whitespace-nowrap text-xs font-bold px-1.5 py-0.5 rounded shadow"
:class="l.cls" :style="l.style" x-text="l.text"></div>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -1208,6 +1213,8 @@
labels: { requestFailed: {{ _('Richiesta non riuscita')|tojson }} }, labels: { requestFailed: {{ _('Richiesta non riuscita')|tojson }} },
}; };
</script> </script>
{# #116: disegno overlay ed etichette quote; prima di vision_execute.js, che lo usa se presente #}
<script src="{{ url_for('static', filename='js/vision_overlay.js') }}"></script>
<script src="{{ url_for('static', filename='js/vision_execute.js') }}"></script> <script src="{{ url_for('static', filename='js/vision_execute.js') }}"></script>
{# #53 #54 #55: result of check / note tasks (urls and words for the component) #} {# #53 #54 #55: result of check / note tasks (urls and words for the component) #}
<script> <script>
@@ -71,4 +71,52 @@ check('#117 fmtTol(undefined) = trattino', c2.fmtTol(undefined) === '\u2014');
check('#117 fmtTol(0.05) = 0.050', c2.fmtTol(0.05) === '0.050', c2.fmtTol(0.05)); check('#117 fmtTol(0.05) = 0.050', c2.fmtTol(0.05) === '0.050', c2.fmtTol(0.05));
check('#117 fmtTol(0) = 0.000', c2.fmtTol(0) === '0.000', c2.fmtTol(0)); check('#117 fmtTol(0) = 0.000', c2.fmtTol(0) === '0.000', c2.fmtTol(0));
// #116: righe con key/name/vision_output, etichette in % con un VisionOverlay finto, labelClass
win.__visionCfg = {
subtasks: [
{ id: 30, marker_number: 1, description: 'Larghezza', unit: 'mm', vision_output: 'larg.value' },
{ id: 31, description: '', measurement_type: 'Diametro', unit: 'mm' },
],
};
const c3 = ctx.visionExecute();
c3.result = { overlay: [{ type: 'circle', tool_id: 'larg' }], measurements: [
{ subtask_id: 30, value: 1.5, pass_fail: 'pass' },
{ subtask_id: 31, value: 2, pass_fail: 'fail' },
] };
const r3 = c3.rows;
check('#116 riga: key = subtask_id', r3[0].key === 30 && r3[1].key === 31);
check('#116 riga: name senza marker', r3[0].name === 'Larghezza' && r3[1].name === 'Diametro', r3[0].name + '/' + r3[1].name);
check('#116 riga: vision_output', r3[0].vision_output === 'larg.value' && r3[1].vision_output == null);
check('#116 label invariato (#114)', r3[0].label === '1. Larghezza', r3[0].label);
check('#116 senza VisionOverlay: nessuna etichetta', c3.overlayLabels.length === 0);
let seen = null;
ctx.VisionOverlay = {
labels(rows, overlay) { seen = { rows, overlay }; return [{ key: 30, x: 500, y: 250, text: 'Larghezza 1.500 mm', status: 'pass' }]; },
};
check('#116 senza immagine: nessuna etichetta', c3.overlayLabels.length === 0);
c3.z.w = 2000; c3.z.h = 1000;
const ls = c3.overlayLabels;
check('#116 una etichetta', ls.length === 1 && ls[0].key === 30 && ls[0].text === 'Larghezza 1.500 mm', JSON.stringify(ls));
check('#116 posizione in percentuale', ls[0].style === 'left:25%;top:25%;transform:translate(8px,-50%)', ls[0].style);
check('#116 classe da esito', ls[0].cls === 'bg-green-600 text-white', ls[0].cls);
check('#116 labels riceve rows e overlay', seen.rows.length === 2 && seen.overlay.length === 1);
c3.result = null;
check('#116 senza risultato: nessuna etichetta', c3.overlayLabels.length === 0);
check('#116 labelClass pass', c3.labelClass('pass') === 'bg-green-600 text-white');
check('#116 labelClass warning', c3.labelClass('warning') === 'bg-amber-400 text-black');
check('#116 labelClass fail', c3.labelClass('fail') === 'bg-red-600 text-white');
check('#116 labelClass altro', c3.labelClass('boh') === 'bg-gray-700 text-white' && c3.labelClass(undefined) === 'bg-gray-700 text-white');
// drawOverlay usa VisionOverlay.draw con lw = max(2, w/400)
let drawn = null;
ctx.VisionOverlay = { draw(g, overlay, lw) { drawn = { g, overlay, lw }; return { drawn: 1, skipped: 0 }; } };
const gfake = { clearRect() {}, set lineWidth(v) {}, set lineJoin(v) {} };
const cv = { width: 0, height: 0, getContext() { return gfake; } };
c3.$refs = { img: { naturalWidth: 2000, naturalHeight: 1000 }, overlay: cv };
c3.result = { overlay: [{ type: 'circle' }] };
c3.drawOverlay();
check('#116 drawOverlay chiama VisionOverlay.draw', drawn && drawn.g === gfake && drawn.overlay.length === 1 && drawn.lw === 5, JSON.stringify(drawn && drawn.lw));
check('#116 canvas alla risoluzione naturale', cv.width === 2000 && cv.height === 1000);
process.exit(failures ? 1 : 0); process.exit(failures ? 1 : 0);
@@ -71,3 +71,14 @@ def test_quote_table_shows_nominal_and_tolerances():
for marker in ("fmtTol(r.nominal)", "fmtTol(r.ltl)", "fmtTol(r.utl)", "fmtTol(r.lwl)", "fmtTol(r.uwl)"): for marker in ("fmtTol(r.nominal)", "fmtTol(r.ltl)", "fmtTol(r.utl)", "fmtTol(r.lwl)", "fmtTol(r.uwl)"):
assert marker in panel, marker assert marker in panel, marker
assert "text-measure-fail" in panel and "text-measure-warning" in panel assert "text-measure-fail" in panel and "text-measure-warning" in panel
def test_overlay_script_loaded_before_vision_execute_and_labels_layer():
# #116: vision_overlay.js prima di vision_execute.js; strato etichette dentro il wrapper dello zoom, dopo il canvas
src = TEMPLATE.read_text(encoding="utf-8")
assert src.index("js/vision_overlay.js") < src.index("js/vision_execute.js")
panel = _panel()
assert panel.index('x-ref="overlay"') < panel.index('x-for="l in overlayLabels"')
label = _tag_with(panel, 'x-text="l.text"')
assert ':style="l.style"' in label and "pointer-events-none" in label
assert 'style="' not in label.replace(':style="', "") # niente style statico accanto a :style