Pannello visione: modulo vision_overlay.js (forme overlay, ancore, etichette) con test node e fixture task 14
Refs #116 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* #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}].
|
||||
* 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;
|
||||
}
|
||||
|
||||
// #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;
|
||||
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 };
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') window.VisionOverlay = VisionOverlay;
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = VisionOverlay;
|
||||
Reference in New Issue
Block a user