/** * #116: disegno dell'overlay del check visione sul canvas del pannello operatore e * ancoraggio delle etichette delle quote alla geometria del loro strumento. * * Script classico (niente import/export): lo carica task_execute.html prima di * vision_execute.js, che lo usa via window.VisionOverlay. In node (test) si carica * con require. * * API: draw(g, overlay, lw) -> {drawn, skipped}; anchor(item) -> {x, y} | null; * labels(rows, overlay) -> [{key, x, y, text, status}]; * svg(overlay, opts) -> stringa di figli SVG (#121, vedi sotto). * Convenzioni (vs-tools): row = y, col = x; angoli in gradi positivi antiorari a video. * Porting da TieVision-Studio frontend/src/comune/overlay.js (verticiRect2, forma) * e frontend/src/editor/misure.js (ancoraOverlay). */ var VisionOverlay = (function () { 'use strict'; // #116: colore per ruolo, uguale a quello dello Studio var COLORS = { reference: '#2563eb', found: '#16a34a', roi: '#f59e0b' }; var COLOR_OTHER = '#ef4444'; function rad(d) { return (d * Math.PI) / 180; } function num(v) { return typeof v === 'number' && isFinite(v); } function allNum(o) { for (var i = 1; i < arguments.length; i++) { if (!o || !num(o[arguments[i]])) return false; } return true; } // #116: serie rows/cols valida = entrambe array, stessa lunghezza, non vuote, tutti numeri finiti function validSeries(o) { if (!o || !Array.isArray(o.rows) || !Array.isArray(o.cols)) return false; if (o.rows.length === 0 || o.rows.length !== o.cols.length) return false; for (var i = 0; i < o.rows.length; i++) { if (!num(o.rows[i]) || !num(o.cols[i])) return false; } return true; } // #116: i 4 vertici del rect2 (x = col, y = row), come verticiRect2 dello Studio function rect2Vertices(o) { var a = rad(o.phi_deg); var u = [Math.cos(a), -Math.sin(a)]; var v = [-Math.sin(a), -Math.cos(a)]; // u ruotato di +90 antiorario return [[1, 1], [1, -1], [-1, -1], [-1, 1]].map(function (st) { return [ o.col + st[0] * o.length1 * u[0] + st[1] * o.length2 * v[0], o.row + st[0] * o.length1 * u[1] + st[1] * o.length2 * v[1], ]; }); } // #116: disegna una forma; true se disegnata, false se saltata (tipo ignoto o dati malformati) function drawShape(g, o, lw) { var type = o && o.type; var i; switch (type) { case 'rect2': { if (!allNum(o, 'row', 'col', 'phi_deg', 'length1', 'length2')) return false; var vs = rect2Vertices(o); g.beginPath(); g.moveTo(vs[0][0], vs[0][1]); for (i = 1; i < vs.length; i++) g.lineTo(vs[i][0], vs[i][1]); g.closePath(); g.stroke(); return true; } case 'segment': case 'line': if (!allNum(o, 'row1', 'col1', 'row2', 'col2')) return false; g.beginPath(); g.moveTo(o.col1, o.row1); g.lineTo(o.col2, o.row2); g.stroke(); return true; case 'circle': if (!allNum(o, 'row', 'col', 'radius')) return false; g.beginPath(); g.arc(o.col, o.row, o.radius, 0, 2 * Math.PI); g.stroke(); return true; case 'arc': if (!allNum(o, 'row', 'col', 'radius', 'start_deg', 'extent_deg')) return false; g.beginPath(); // #116: y canvas verso il basso, quindi angoli negati; extent > 0 = antiorario a video g.arc(o.col, o.row, o.radius, -rad(o.start_deg), -rad(o.start_deg + o.extent_deg), o.extent_deg > 0); g.stroke(); return true; case 'points': { if (!validSeries(o)) return false; var m = Math.max(3, lw * 2); // semi-lato della croce g.beginPath(); for (i = 0; i < o.rows.length; i++) { g.moveTo(o.cols[i] - m, o.rows[i]); g.lineTo(o.cols[i] + m, o.rows[i]); g.moveTo(o.cols[i], o.rows[i] - m); g.lineTo(o.cols[i], o.rows[i] + m); } g.stroke(); return true; } case 'contour': if (!validSeries(o)) return false; g.beginPath(); g.moveTo(o.cols[0], o.rows[0]); for (i = 1; i < o.rows.length; i++) g.lineTo(o.cols[i], o.rows[i]); g.stroke(); // polilinea aperta, come nello Studio return true; default: return false; } } function draw(g, overlay, lw) { var res = { drawn: 0, skipped: 0 }; var items = Array.isArray(overlay) ? overlay : []; for (var k = 0; k < items.length; k++) { var o = items[k]; var role = o && o.role; var isRoi = role === 'roi'; g.strokeStyle = COLORS[role] || COLOR_OTHER; g.lineWidth = isRoi ? lw * 0.75 : lw; g.setLineDash(isRoi ? [lw * 3, lw * 2] : []); // roi tratteggiata var ok = false; try { ok = drawShape(g, o, lw); } catch (e) { ok = false; } // un elemento rotto non ferma gli altri if (ok) res.drawn++; else res.skipped++; } g.setLineDash([]); return res; } // #121: numero per il markup SVG, al piu' 2 decimali (niente stringhe dei dati nel markup) function n2(v) { return String(Math.round(v * 100) / 100); } function pos(v, dflt) { return num(v) && v > 0 ? v : dflt; } // #121: path SVG di un arco, porting di percorsoArco dello Studio; extent > 0 = antiorario a video function arcPath(o) { var ext = Math.max(-359.99, Math.min(359.99, o.extent_deg)); // 360 pieni darebbero start == end var a0 = rad(o.start_deg); var a1 = rad(o.start_deg + ext); var x0 = o.col + o.radius * Math.cos(a0), y0 = o.row - o.radius * Math.sin(a0); var x1 = o.col + o.radius * Math.cos(a1), y1 = o.row - o.radius * Math.sin(a1); var large = Math.abs(ext) > 180 ? 1 : 0; var sweep = ext > 0 ? 0 : 1; // y verso il basso: antiorario a video = sweep-flag 0 return 'M ' + n2(x0) + ' ' + n2(y0) + ' A ' + n2(o.radius) + ' ' + n2(o.radius) + ' 0 ' + large + ' ' + sweep + ' ' + n2(x1) + ' ' + n2(y1); } // #121: [tag, attributi geometrici] di una forma, o null se tipo ignoto o dati malformati (stessi criteri di drawShape) function shapeSvg(o, cross) { var i, pts; switch (o && o.type) { case 'rect2': if (!allNum(o, 'row', 'col', 'phi_deg', 'length1', 'length2')) return null; pts = rect2Vertices(o).map(function (v) { return n2(v[0]) + ',' + n2(v[1]); }).join(' '); return ['polygon', 'points="' + pts + '"']; case 'segment': case 'line': if (!allNum(o, 'row1', 'col1', 'row2', 'col2')) return null; return ['line', 'x1="' + n2(o.col1) + '" y1="' + n2(o.row1) + '" x2="' + n2(o.col2) + '" y2="' + n2(o.row2) + '"']; case 'circle': if (!allNum(o, 'row', 'col', 'radius')) return null; return ['circle', 'cx="' + n2(o.col) + '" cy="' + n2(o.row) + '" r="' + n2(o.radius) + '"']; case 'arc': if (!allNum(o, 'row', 'col', 'radius', 'start_deg', 'extent_deg')) return null; return ['path', 'd="' + arcPath(o) + '"']; case 'points': { if (!validSeries(o)) return null; var d = []; for (i = 0; i < o.rows.length; i++) { var x = o.cols[i], y = o.rows[i]; d.push('M ' + n2(x - cross) + ' ' + n2(y) + ' L ' + n2(x + cross) + ' ' + n2(y) + ' M ' + n2(x) + ' ' + n2(y - cross) + ' L ' + n2(x) + ' ' + n2(y + cross)); } return ['path', 'd="' + d.join(' ') + '"']; } case 'contour': if (!validSeries(o)) return null; pts = o.rows.map(function (r, k) { return n2(o.cols[k]) + ',' + n2(r); }).join(' '); return ['polyline', 'points="' + pts + '"']; // aperta, come draw default: return null; } } // #121: overlay -> figli SVG (senza esterno) in px immagine. Tratto a spessore costante a video // (vector-effect non-scaling-stroke), croci dei points a semi-braccio costante a video (cross / zoom). function svg(overlay, opts) { opts = opts || {}; var zoom = pos(opts.zoom, 1), lw = pos(opts.lw, 2), cross = pos(opts.cross, 6); var items = Array.isArray(overlay) ? overlay : []; var out = []; for (var k = 0; k < items.length; k++) { var o = items[k]; var sh = null; try { sh = shapeSvg(o, cross / zoom); } catch (e) { sh = null; } // un elemento rotto non ferma gli altri if (!sh) continue; var role = o.role; var isRoi = role === 'roi'; // solo colori fissi e numeri: nessuna stringa dei dati finisce nel markup out.push('<' + sh[0] + ' ' + sh[1] + ' fill="none" stroke="' + (COLORS[role] || COLOR_OTHER) + '" stroke-width="' + n2(isRoi ? lw * 0.75 : lw) + '"' + (isRoi ? ' stroke-dasharray="6 4"' : '') + ' vector-effect="non-scaling-stroke"/>'); } return out.join(''); } // #116: punto di ancoraggio dell'etichetta (px immagine), come ancoraOverlay dello Studio function anchor(o) { switch (o && o.type) { case 'segment': case 'line': return allNum(o, 'row1', 'col1', 'row2', 'col2') ? { x: (o.col1 + o.col2) / 2, y: (o.row1 + o.row2) / 2 } : null; case 'circle': return allNum(o, 'row', 'col', 'radius') ? { x: o.col + o.radius, y: o.row } : null; // punto a destra case 'arc': { if (!allNum(o, 'row', 'col', 'radius', 'start_deg', 'extent_deg')) return null; var a = rad(o.start_deg + o.extent_deg / 2); // punto medio dell'arco return { x: o.col + o.radius * Math.cos(a), y: o.row - o.radius * Math.sin(a) }; } case 'points': return Array.isArray(o.rows) && Array.isArray(o.cols) && num(o.rows[0]) && num(o.cols[0]) ? { x: o.cols[0], y: o.rows[0] } : null; case 'rect2': return allNum(o, 'row', 'col') ? { x: o.col, y: o.row } : null; case 'contour': { if (!Array.isArray(o.rows) || !Array.isArray(o.cols)) return null; var i = Math.floor(Math.min(o.rows.length, o.cols.length) / 2); return num(o.rows[i]) && num(o.cols[i]) ? { x: o.cols[i], y: o.rows[i] } : null; } default: return null; } } // #116: un'etichetta per riga con vision_output 'tool_id.proprieta' che ha un overlay ancorabile function labels(rows, overlay) { var out = []; var items = Array.isArray(overlay) ? overlay : []; (rows || []).forEach(function (r) { if (!r || typeof r.vision_output !== 'string' || r.vision_output.indexOf('.') < 0) return; var toolId = r.vision_output.split('.')[0]; var mine = items.filter(function (o) { return o && o.tool_id === toolId; }); // prima gli elementi 'found', poi qualsiasi altro var order = mine.filter(function (o) { return o.role === 'found'; }) .concat(mine.filter(function (o) { return o.role !== 'found'; })); var pt = null; for (var i = 0; i < order.length && !pt; i++) pt = anchor(order[i]); if (!pt) return; // #118: valore null/undefined/vuoto = quota non misurata (Number(null) e' 0): nessuna etichetta if (r.value === null || r.value === undefined || r.value === '' || typeof r.value === 'boolean') return; var v = Number(r.value); if (!isFinite(v)) return; out.push({ key: r.key, x: pt.x, y: pt.y, text: r.name + ' ' + v.toFixed(3) + ' ' + (r.unit || ''), status: r.status, }); }); return out; } return { draw: draw, anchor: anchor, labels: labels, svg: svg }; })(); if (typeof window !== 'undefined') window.VisionOverlay = VisionOverlay; if (typeof module !== 'undefined' && module.exports) module.exports = VisionOverlay;