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:
@@ -261,6 +261,10 @@ function visionExecute() {
|
||||
return ((this.result && this.result.measurements) || []).map((m) => {
|
||||
const st = subs.find((s) => s.id === m.subtask_id) || {};
|
||||
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')
|
||||
label: (st.marker_number ? st.marker_number + '. ' : '') + (st.description || st.measurement_type || '#' + m.subtask_id),
|
||||
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)
|
||||
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) {
|
||||
// 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';
|
||||
@@ -401,8 +429,14 @@ function visionExecute() {
|
||||
cv.height = img.naturalHeight;
|
||||
const g = cv.getContext('2d');
|
||||
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';
|
||||
// #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;
|
||||
|
||||
@@ -594,6 +594,11 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -1208,6 +1213,8 @@
|
||||
labels: { requestFailed: {{ _('Richiesta non riuscita')|tojson }} },
|
||||
};
|
||||
</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>
|
||||
{# #53 #54 #55: result of check / note tasks (urls and words for the component) #}
|
||||
<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) = 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);
|
||||
|
||||
@@ -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)"):
|
||||
assert marker in panel, marker
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user