8c361c14ae
Rotella senza Ctrl = zoom sul punto del mouse su tutte le immagini. image_viewer.js e vision_overlay.js copiati byte per byte da TieVision-Station (lavoro/daitarn-nolayers b1af911); attributo no-layers su ogni <tmf-viewer> (task_editor 3, task_execute 1, calibrazione_pagina 4). Test JS e pagina demo allineati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019A9CSrh9rfM5WwqKtg8BSE
454 lines
28 KiB
JavaScript
454 lines
28 KiB
JavaScript
/**
|
|
* #116: overlay del check visione (static/js/vision_overlay.js) con plain node.
|
|
*
|
|
* Run from src/frontend/flask_app: node tests/js/test_vision_overlay.js
|
|
*/
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const VisionOverlay = require(path.join(__dirname, '..', '..', 'static', 'js', 'vision_overlay.js'));
|
|
const fixture = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, 'fixtures', 'vision_check_task14.json'), 'utf8'),
|
|
);
|
|
|
|
let failures = 0;
|
|
let total = 0;
|
|
function check(label, cond, extra) {
|
|
total++;
|
|
if (cond) console.log('ok - ' + label);
|
|
else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); }
|
|
}
|
|
const near = (a, b) => Math.abs(a - b) < 1e-9;
|
|
|
|
// Canvas finto: registra le chiamate e le proprieta' al momento dello stroke
|
|
function fakeCanvas() {
|
|
const calls = [];
|
|
const g = {
|
|
calls,
|
|
strokes: [],
|
|
dash: [],
|
|
strokeStyle: '',
|
|
lineWidth: 0,
|
|
beginPath() { calls.push(['beginPath']); },
|
|
moveTo(x, y) { calls.push(['moveTo', x, y]); },
|
|
lineTo(x, y) { calls.push(['lineTo', x, y]); },
|
|
arc(...a) { calls.push(['arc', ...a]); },
|
|
closePath() { calls.push(['closePath']); },
|
|
stroke() {
|
|
calls.push(['stroke']);
|
|
g.strokes.push({ style: g.strokeStyle, width: g.lineWidth, dash: g.dash.slice() });
|
|
},
|
|
setLineDash(d) { g.dash = d.slice(); calls.push(['setLineDash', d.slice()]); },
|
|
};
|
|
return g;
|
|
}
|
|
const onlyCalls = (g, name) => g.calls.filter((c) => c[0] === name);
|
|
|
|
// --- fixture vero: task 14 TEST1 ---
|
|
const overlay = fixture.overlay;
|
|
check('fixture: overlay con 25 elementi', overlay.length === 25, String(overlay.length));
|
|
{
|
|
const g = fakeCanvas();
|
|
const res = VisionOverlay.draw(g, overlay, 2);
|
|
check('draw sul fixture: drawn 25, skipped 0', res.drawn === 25 && res.skipped === 0, JSON.stringify(res));
|
|
const lastDash = onlyCalls(g, 'setLineDash').pop();
|
|
check('draw: ultimo setLineDash e\' []', lastDash && lastDash[1].length === 0);
|
|
check('draw: uno stroke per elemento', g.strokes.length === 25, String(g.strokes.length));
|
|
const roiIdx = overlay.findIndex((o) => o.role === 'roi');
|
|
const foundIdx = overlay.findIndex((o) => o.role === 'found');
|
|
check('roi: arancione, tratteggiata, spessore lw*0.75',
|
|
g.strokes[roiIdx].style === '#f59e0b' && g.strokes[roiIdx].width === 1.5 &&
|
|
JSON.stringify(g.strokes[roiIdx].dash) === '[6,4]', JSON.stringify(g.strokes[roiIdx]));
|
|
check('found: verde, piena, spessore lw',
|
|
g.strokes[foundIdx].style === '#16a34a' && g.strokes[foundIdx].width === 2 &&
|
|
g.strokes[foundIdx].dash.length === 0, JSON.stringify(g.strokes[foundIdx]));
|
|
}
|
|
|
|
// --- colori: reference e ruolo ignoto ---
|
|
{
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [
|
|
{ type: 'segment', role: 'reference', row1: 0, col1: 0, row2: 1, col2: 1 },
|
|
{ type: 'segment', role: 'boh', row1: 0, col1: 0, row2: 1, col2: 1 },
|
|
], 1);
|
|
check('reference blu', g.strokes[0].style === '#2563eb');
|
|
check('ruolo ignoto rosso', g.strokes[1].style === '#ef4444');
|
|
}
|
|
|
|
// --- rect2 ---
|
|
function xs(g) { return onlyCalls(g, 'moveTo').concat(onlyCalls(g, 'lineTo')).map((c) => c[1]); }
|
|
function ys(g) { return onlyCalls(g, 'moveTo').concat(onlyCalls(g, 'lineTo')).map((c) => c[2]); }
|
|
{
|
|
const g = fakeCanvas();
|
|
const r = VisionOverlay.draw(g, [{ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 0, length1: 10, length2: 4 }], 1);
|
|
const X = xs(g), Y = ys(g);
|
|
check('rect2 phi 0: 4 vertici chiusi', r.drawn === 1 && X.length === 4 && onlyCalls(g, 'closePath').length === 1);
|
|
check('rect2 phi 0: col 90/110', X.every((x) => near(x, 90) || near(x, 110)) && X.filter((x) => near(x, 90)).length === 2, String(X));
|
|
check('rect2 phi 0: row 46/54', Y.every((y) => near(y, 46) || near(y, 54)) && Y.filter((y) => near(y, 46)).length === 2, String(Y));
|
|
}
|
|
{
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [{ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 90, length1: 10, length2: 4 }], 1);
|
|
const X = xs(g), Y = ys(g);
|
|
check('rect2 phi 90: lati scambiati, col 96/104', X.every((x) => near(x, 96) || near(x, 104)), String(X));
|
|
check('rect2 phi 90: lati scambiati, row 40/60', Y.every((y) => near(y, 40) || near(y, 60)), String(Y));
|
|
}
|
|
|
|
// --- arc: argomenti come da contratto ---
|
|
{
|
|
const g = fakeCanvas();
|
|
const a = { type: 'arc', role: 'found', row: 30, col: 40, radius: 12, start_deg: 90, extent_deg: 90 };
|
|
VisionOverlay.draw(g, [a], 1);
|
|
const c = onlyCalls(g, 'arc')[0];
|
|
check('arc: g.arc(col, row, r, -rad(start), -rad(start+extent), extent>0)',
|
|
c && near(c[1], 40) && near(c[2], 30) && near(c[3], 12) &&
|
|
near(c[4], -Math.PI / 2) && near(c[5], -Math.PI) && c[6] === true, JSON.stringify(c));
|
|
const g2 = fakeCanvas();
|
|
VisionOverlay.draw(g2, [{ ...a, start_deg: 0, extent_deg: -45 }], 1);
|
|
const c2 = onlyCalls(g2, 'arc')[0];
|
|
check('arc: extent negativo = orario (anticlockwise false)',
|
|
c2 && near(c2[4], 0) && near(c2[5], Math.PI / 4) && c2[6] === false, JSON.stringify(c2));
|
|
}
|
|
|
|
// --- circle, points, contour ---
|
|
{
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [{ type: 'circle', role: 'found', row: 5, col: 7, radius: 3 }], 1);
|
|
const c = onlyCalls(g, 'arc')[0];
|
|
check('circle: cerchio pieno', c && c[1] === 7 && c[2] === 5 && c[3] === 3 && c[4] === 0 && near(c[5], 2 * Math.PI));
|
|
}
|
|
{
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [{ type: 'points', role: 'found', rows: [10], cols: [20] }], 1);
|
|
const m = onlyCalls(g, 'moveTo').concat(onlyCalls(g, 'lineTo'));
|
|
// lw 1 -> semi-lato max(3, 2) = 3
|
|
check('points: croce di semi-lato 3', m.length === 4 && m.some((c) => c[1] === 17 && c[2] === 10) &&
|
|
m.some((c) => c[1] === 23 && c[2] === 10) && m.some((c) => c[1] === 20 && c[2] === 7) &&
|
|
m.some((c) => c[1] === 20 && c[2] === 13), JSON.stringify(m));
|
|
const g2 = fakeCanvas();
|
|
VisionOverlay.draw(g2, [{ type: 'points', role: 'found', rows: [10], cols: [20] }], 5);
|
|
check('points: semi-lato lw*2 per lw grande', onlyCalls(g2, 'moveTo').some((c) => c[1] === 10 && c[2] === 10));
|
|
}
|
|
{
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [{ type: 'contour', role: 'found', rows: [1, 2, 3], cols: [4, 5, 6] }], 1);
|
|
check('contour: polilinea aperta di 3 punti',
|
|
onlyCalls(g, 'moveTo').length === 1 && onlyCalls(g, 'lineTo').length === 2 && onlyCalls(g, 'closePath').length === 0);
|
|
}
|
|
|
|
// --- elementi malformati: saltati, nessuna eccezione ---
|
|
{
|
|
const g = fakeCanvas();
|
|
let thrown = null;
|
|
let res = null;
|
|
try {
|
|
res = VisionOverlay.draw(g, [
|
|
{ type: 'circle', role: 'found', row: 1, col: 1 }, // manca radius
|
|
{ type: 'points', role: 'found', rows: [1, 2], cols: [1] }, // lunghezze diverse
|
|
{ type: 'contour', role: 'found', rows: [], cols: [] }, // vuoto
|
|
{ type: 'boh', role: 'found' }, // tipo ignoto
|
|
{ type: 'segment', role: 'found', row1: NaN, col1: 0, row2: 1, col2: 1 }, // non finito
|
|
null, // elemento nullo
|
|
{ type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 }, // valido
|
|
], 1);
|
|
} catch (e) { thrown = e; }
|
|
check('malformati: nessuna eccezione', thrown === null, String(thrown));
|
|
check('malformati: 6 saltati, 1 disegnato', res && res.drawn === 1 && res.skipped === 6, JSON.stringify(res));
|
|
check('malformati: draw con overlay non array', (() => {
|
|
try { const r = VisionOverlay.draw(fakeCanvas(), null, 1); return r.drawn === 0 && r.skipped === 0; } catch (e) { return false; }
|
|
})());
|
|
}
|
|
|
|
// --- anchor, valori calcolati a mano ---
|
|
{
|
|
const A = VisionOverlay.anchor;
|
|
const seg = A({ type: 'segment', row1: 10, col1: 0, row2: 20, col2: 40 });
|
|
check('anchor segment: punto medio', seg && seg.x === 20 && seg.y === 15);
|
|
const lin = A({ type: 'line', row1: 0, col1: 2, row2: 0, col2: 4 });
|
|
check('anchor line: punto medio', lin && lin.x === 3 && lin.y === 0);
|
|
const cir = A({ type: 'circle', row: 50, col: 100, radius: 8 });
|
|
check('anchor circle: centro + raggio a destra', cir && cir.x === 108 && cir.y === 50);
|
|
const arc = A({ type: 'arc', row: 50, col: 100, radius: 10, start_deg: 0, extent_deg: 180 });
|
|
check('anchor arc: punto medio (90 gradi = in alto a video)', arc && near(arc.x, 100) && near(arc.y, 40), JSON.stringify(arc));
|
|
const pts = A({ type: 'points', rows: [7, 8], cols: [3, 4] });
|
|
check('anchor points: primo punto', pts && pts.x === 3 && pts.y === 7);
|
|
const r2 = A({ type: 'rect2', row: 9, col: 11, phi_deg: 30, length1: 5, length2: 2 });
|
|
check('anchor rect2: centro', r2 && r2.x === 11 && r2.y === 9);
|
|
const con = A({ type: 'contour', rows: [0, 1, 2, 3], cols: [10, 11, 12, 13] });
|
|
check('anchor contour: punto a meta\' (indice 2)', con && con.x === 12 && con.y === 2);
|
|
check('anchor: tipo ignoto -> null', A({ type: 'boh' }) === null);
|
|
check('anchor: dati mancanti -> null', A({ type: 'circle', row: 1, col: 1 }) === null && A(null) === null);
|
|
}
|
|
|
|
// --- labels sul fixture, righe costruite come fara' vision_execute.js ---
|
|
const subById = {};
|
|
fixture._subtasks.forEach((s) => { subById[s.id] = s; });
|
|
const rows = fixture.measurements.map((m) => {
|
|
const s = subById[m.subtask_id];
|
|
return {
|
|
key: m.subtask_id, name: s.description, value: m.value, unit: s.unit,
|
|
status: m.pass_fail, vision_output: s.vision_output,
|
|
};
|
|
});
|
|
{
|
|
const L = VisionOverlay.labels(rows, overlay);
|
|
check('labels fixture: 6 etichette', L.length === 6, String(L.length));
|
|
const names = L.map((l) => l.text.split(' ')[0]).join(',');
|
|
check('labels fixture: larghezza, lunghezza e 4 raggi',
|
|
L[0].text.startsWith('Larghezza') && L[1].text.startsWith('Lunghezza') &&
|
|
L.slice(2).every((l) => l.text.startsWith('R ')), names);
|
|
check('labels fixture: nessuna per le differenze', L.every((l) => !/^Diff/.test(l.text)));
|
|
const l15 = L.find((l) => l.key === 15);
|
|
check('quota 15: testo con Larghezza e 1.668', l15 && l15.text.includes('Larghezza') && l15.text.includes('1.668'), l15 && l15.text);
|
|
check('quota 15: status riportato', l15 && l15.status === fixture.measurements[0].pass_fail);
|
|
const seg = overlay.find((o) => o.tool_id === 'larghezza' && o.role === 'found');
|
|
check('quota 15: ancorata al punto medio del segmento', l15 && near(l15.x, (seg.col1 + seg.col2) / 2) && near(l15.y, (seg.row1 + seg.row2) / 2));
|
|
}
|
|
{
|
|
const nuda = rows.map((r) => ({ ...r, vision_output: undefined }));
|
|
check('labels: righe senza vision_output -> nessuna etichetta', VisionOverlay.labels(nuda, overlay).length === 0);
|
|
const sola = VisionOverlay.labels([{ key: 1, name: 'X', value: 2, unit: 'mm', status: 'pass' }], overlay);
|
|
check('labels: riga senza vision_output (assente) -> nessuna', sola.length === 0);
|
|
const noTool = VisionOverlay.labels([{ key: 1, name: 'X', value: 2, unit: 'mm', status: 'pass', vision_output: 'nonesiste.value' }], overlay);
|
|
check('labels: tool senza overlay -> nessuna', noTool.length === 0);
|
|
// found preferito: la roi (per prima) ha un'altra ancora, ma vince il found
|
|
const ov = [
|
|
{ type: 'rect2', role: 'roi', tool_id: 't', row: 1, col: 1, phi_deg: 0, length1: 1, length2: 1 },
|
|
{ type: 'segment', role: 'found', tool_id: 't', row1: 10, col1: 10, row2: 10, col2: 20 },
|
|
];
|
|
const f = VisionOverlay.labels([{ key: 1, name: 'T', value: 1.5, unit: 'mm', status: 'warning', vision_output: 't.value' }], ov);
|
|
check('labels: preferisce l\'ancora del found', f.length === 1 && f[0].x === 15 && f[0].y === 10 && f[0].status === 'warning', JSON.stringify(f));
|
|
const solo = VisionOverlay.labels([{ key: 1, name: 'T', value: 1.5, unit: 'mm', status: 'fail', vision_output: 't.value' }], [ov[0]]);
|
|
check('labels: senza found ripiega sul primo elemento', solo.length === 1 && solo[0].x === 1 && solo[0].y === 1);
|
|
check('labels: testo name + valore a 3 decimali + unit', f[0].text === 'T 1.500 mm', f[0].text);
|
|
}
|
|
|
|
// --- #118: labels, valore null/undefined/non finito -> etichetta NaN con status della riga ---
|
|
{
|
|
const ov = [{ type: 'segment', role: 'found', tool_id: 't', row1: 10, col1: 10, row2: 10, col2: 20 }];
|
|
const mk = (value) => ({ key: 1, name: 'T', value, unit: 'mm', status: 'fail', vision_output: 't.value' });
|
|
const nan = (v) => { const l = VisionOverlay.labels([mk(v)], ov); return l.length === 1 && l[0].text === 'T NaN mm' && l[0].status === 'fail'; };
|
|
check('labels: value null -> etichetta NaN, status fail (Number(null) e 0)', nan(null));
|
|
check('labels: value undefined / stringa vuota -> etichetta NaN', nan(undefined) && nan(''));
|
|
check('labels: value NaN/Infinity -> etichetta NaN', nan(NaN) && nan(Infinity) && nan(-Infinity));
|
|
check('labels: value 0 resta un valore valido', VisionOverlay.labels([mk(0)], ov)[0].text === 'T 0.000 mm');
|
|
const misto = VisionOverlay.labels([{ ...mk(null), key: 1 }, { ...mk(2.5), key: 2 }], ov);
|
|
check('labels: null e valida entrambe presenti', misto.length === 2 && misto[0].text === 'T NaN mm' && misto[1].key === 2);
|
|
check('labels: riga NaN senza ancoraggio -> nessuna etichetta', VisionOverlay.labels([mk(null)], []).length === 0);
|
|
}
|
|
|
|
// --- #121: svg(overlay, opts) ---
|
|
const count = (str, re) => (str.match(re) || []).length;
|
|
const SHAPE_RE = /<(polygon|line|circle|path|polyline) /g;
|
|
{
|
|
const out = VisionOverlay.svg(overlay);
|
|
// il fixture vero ha 25 forme: 7 segment, 1 contour, 1 points, 4 rect2 roi, 4 arc roi, 4 arc found, 4 circle
|
|
check('svg fixture: 25 forme', count(out, SHAPE_RE) === 25, String(count(out, SHAPE_RE)));
|
|
check('svg fixture: ogni forma con vector-effect non-scaling-stroke',
|
|
count(out, /vector-effect="non-scaling-stroke"/g) === 25);
|
|
check('svg fixture: ogni forma fill none', count(out, /fill="none"/g) === 25);
|
|
check('svg fixture: 8 roi (4 rect2 + 4 arc) con stroke-dasharray 6 4', count(out, /stroke-dasharray="6 4"/g) === 8,
|
|
String(count(out, /stroke-dasharray="6 4"/g)));
|
|
check('svg fixture: nessun <svg> esterno', !/<svg/.test(out));
|
|
check('svg fixture: nessun NaN/undefined/null nel markup', !/NaN|undefined|null|Infinity/.test(out));
|
|
check('svg fixture: numeri con al piu\' 2 decimali', !/\d\.\d{3,}/.test(out));
|
|
}
|
|
{
|
|
const one = (o, opts) => VisionOverlay.svg([o], opts);
|
|
const roi = one({ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 0, length1: 10, length2: 4 });
|
|
check('svg roi: polygon 4 vertici, colore roi, dash 6 4, spessore lw*0.75',
|
|
roi.startsWith('<polygon ') && roi.includes('points="110,46 110,54 90,54 90,46"') && roi.includes('stroke="#f59e0b"') &&
|
|
roi.includes('stroke-dasharray="6 4"') && roi.includes('stroke-width="1.5"'), roi);
|
|
const roi4 = one({ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 0, length1: 10, length2: 4 }, { lw: 4 });
|
|
check('svg roi: lw 4 -> stroke-width 3', roi4.includes('stroke-width="3"'), roi4);
|
|
const fnd = one({ type: 'segment', role: 'found', row1: 1, col1: 2, row2: 3, col2: 4 });
|
|
check('svg found: line verde piena, spessore lw 2 predefinito',
|
|
fnd === '<line x1="2" y1="1" x2="4" y2="3" fill="none" stroke="#16a34a" stroke-width="2" vector-effect="non-scaling-stroke"/>', fnd);
|
|
check('svg reference blu, ruolo ignoto rosso',
|
|
one({ type: 'segment', role: 'reference', row1: 0, col1: 0, row2: 1, col2: 1 }).includes('stroke="#2563eb"') &&
|
|
one({ type: 'segment', role: 'boh', row1: 0, col1: 0, row2: 1, col2: 1 }).includes('stroke="#ef4444"'));
|
|
check('svg: spessore costante, non dipende dallo zoom',
|
|
one({ type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 }, { zoom: 4 }).includes('stroke-width="2"'));
|
|
const cir = one({ type: 'circle', role: 'found', row: 5, col: 7, radius: 3 });
|
|
check('svg circle: cx cy r', cir.startsWith('<circle cx="7" cy="5" r="3" '), cir);
|
|
const con = one({ type: 'contour', role: 'found', rows: [1, 2, 3], cols: [4, 5, 6] });
|
|
check('svg contour: polyline aperta', con.startsWith('<polyline points="4,1 5,2 6,3" ') && !/ Z/.test(con), con);
|
|
|
|
// points: croce di semi-braccio cross/zoom
|
|
const p1 = one({ type: 'points', role: 'found', rows: [10], cols: [20] }, { zoom: 1 });
|
|
check('svg points zoom 1: semi-braccio 6', p1.includes('d="M 14 10 L 26 10 M 20 4 L 20 16"'), p1);
|
|
const p2 = one({ type: 'points', role: 'found', rows: [10], cols: [20] }, { zoom: 2 });
|
|
check('svg points zoom 2: semi-braccio 3', p2.includes('d="M 17 10 L 23 10 M 20 7 L 20 13"'), p2);
|
|
const p3 = one({ type: 'points', role: 'found', rows: [10, 30], cols: [20, 40] }, { zoom: 1, cross: 2 });
|
|
check('svg points: cross personalizzato, 2 croci in un solo path',
|
|
count(p3, /<path /g) === 1 && count(p3, /M /g) === 4 && p3.includes('M 18 10 L 22 10'), p3);
|
|
|
|
// arc: estremi calcolati (y verso il basso, angoli antiorari)
|
|
const arcD = (o) => /d="([^"]+)"/.exec(one({ type: 'arc', role: 'found', ...o }))[1];
|
|
const parse = (d) => d.match(/-?\d+(\.\d+)?/g).map(Number); // [x0, y0, rx, ry, rot, large, sweep, x1, y1]
|
|
{
|
|
// start 90, extent 90 su centro (40,30) r 12: da (40,18) a (28,30), antiorario (sweep 0), arco piccolo
|
|
const d = arcD({ row: 30, col: 40, radius: 12, start_deg: 90, extent_deg: 90 });
|
|
const v = parse(d);
|
|
check('svg arc 90+90: M 40 18 A 12 12 0 0 0 28 30',
|
|
near(v[0], 40) && near(v[1], 18) && v[2] === 12 && v[3] === 12 && v[4] === 0 && v[5] === 0 && v[6] === 0 &&
|
|
near(v[7], 28) && near(v[8], 30), d);
|
|
}
|
|
{
|
|
// start 0, extent -90 (orario): da (10,0) a (0,10) a video, sweep 1
|
|
const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: -90 }));
|
|
check('svg arc extent negativo: sweep 1, fine in basso a video',
|
|
near(v[0], 10) && near(v[1], 0) && v[5] === 0 && v[6] === 1 && near(v[7], 0) && near(v[8], 10), JSON.stringify(v));
|
|
}
|
|
{
|
|
// 270 gradi antiorari da 0: fine a 270 = in basso a video (0, +10)
|
|
const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: 270 }));
|
|
check('svg arc 270: large-arc 1, sweep 0, fine (0,10) a video',
|
|
v[5] === 1 && v[6] === 0 && near(v[7], 0) && near(v[8], 10), JSON.stringify(v));
|
|
}
|
|
{
|
|
const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: 360 }));
|
|
check('svg arc 360: estensione limitata a 359.99 (large-arc, fine quasi sullo start)',
|
|
v[5] === 1 && Math.abs(v[8]) < 0.01 && v[7] > 9.99, JSON.stringify(v));
|
|
}
|
|
{
|
|
// coincide con draw: punto finale del g.arc del canvas (angolo -rad(start+extent))
|
|
const a = { type: 'arc', role: 'found', row: 30, col: 40, radius: 12, start_deg: 30, extent_deg: 100 };
|
|
const g = fakeCanvas();
|
|
VisionOverlay.draw(g, [a], 1);
|
|
const c = onlyCalls(g, 'arc')[0];
|
|
const ex = c[1] + c[3] * Math.cos(c[5]), ey = c[2] + c[3] * Math.sin(c[5]);
|
|
const sx = c[1] + c[3] * Math.cos(c[4]), sy = c[2] + c[3] * Math.sin(c[4]);
|
|
const v = parse(arcD(a));
|
|
check('svg arc: estremi uguali a quelli di draw su canvas',
|
|
Math.abs(v[7] - ex) < 0.01 && Math.abs(v[8] - ey) < 0.01 && Math.abs(v[0] - sx) < 0.01 && Math.abs(v[1] - sy) < 0.01,
|
|
JSON.stringify([v, ex, ey]));
|
|
}
|
|
|
|
// malformati e ignoti: saltati, nessuna eccezione, la forma valida resta
|
|
let thrown = null, out = '';
|
|
try {
|
|
out = VisionOverlay.svg([
|
|
{ type: 'circle', role: 'found', row: 1, col: 1 },
|
|
{ type: 'points', role: 'found', rows: [1, 2], cols: [1] },
|
|
{ type: 'contour', role: 'found', rows: [], cols: [] },
|
|
{ type: 'boh', role: 'found' },
|
|
{ type: 'segment', role: 'found', row1: NaN, col1: 0, row2: 1, col2: 1 },
|
|
{ type: 'arc', role: 'found', row: 0, col: 0, radius: 1, start_deg: 0 },
|
|
{ type: 'rect2', role: 'roi', row: 0, col: 0, phi_deg: 0, length1: Infinity, length2: 1 },
|
|
null,
|
|
{ type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 },
|
|
], { zoom: 1 });
|
|
} catch (e) { thrown = e; }
|
|
check('svg malformati: nessuna eccezione, una sola forma valida',
|
|
thrown === null && count(out, /<line /g) === 1 && count(out, /vector-effect/g) === 1, String(thrown) + out);
|
|
check('svg: overlay non array o vuoto -> stringa vuota',
|
|
VisionOverlay.svg(null) === '' && VisionOverlay.svg(undefined) === '' && VisionOverlay.svg([]) === '');
|
|
check('svg: opts non validi -> predefiniti',
|
|
VisionOverlay.svg([{ type: 'points', role: 'found', rows: [10], cols: [20] }], { zoom: 0, lw: -1, cross: NaN })
|
|
.includes('d="M 14 10 L 26 10 M 20 4 L 20 16"'));
|
|
|
|
// nessuna stringa dei dati nel markup
|
|
const evil = '<script>alert(1)</script>"onload="x';
|
|
const sicuro = VisionOverlay.svg([
|
|
{ type: 'segment', role: evil, tool_id: evil, name: evil, row1: 0, col1: 0, row2: 1, col2: 1 },
|
|
{ type: 'circle', role: 'found', tool_id: evil, row: 1, col: 1, radius: 2 },
|
|
]);
|
|
check('svg: tool_id e role dei dati non compaiono nel markup',
|
|
!/script|onload|alert/.test(sicuro) && count(sicuro, /vector-effect/g) === 2, sicuro);
|
|
const strNum = VisionOverlay.svg([{ type: 'segment', role: 'found', row1: '<b>', col1: 0, row2: 1, col2: 1 }]);
|
|
check('svg: coordinata stringa -> forma saltata', strNum === '');
|
|
}
|
|
|
|
// --- #12: livelli e quote CAD ---
|
|
{
|
|
const VO = VisionOverlay;
|
|
check('layerOf: layer esplicito vince sul ruolo',
|
|
VO.layerOf({ role: 'found', layer: 'quote' }) === 'quote' && VO.layerOf({ role: 'roi', layer: 'costruzioni' }) === 'costruzioni');
|
|
check('layerOf: senza campo found = costruzioni, altro = ricerca',
|
|
VO.layerOf({ role: 'found' }) === 'costruzioni' && VO.layerOf({ role: 'roi' }) === 'ricerca' &&
|
|
VO.layerOf({ role: 'reference' }) === 'ricerca' && VO.layerOf({}) === 'ricerca' && VO.layerOf(null) === 'ricerca');
|
|
check('layerOf: layer ignoto -> secondo il ruolo', VO.layerOf({ role: 'found', layer: 'boh' }) === 'costruzioni');
|
|
|
|
const lin = { tipo: 'lineare', p1: [0, 0], p2: [100, 0], testo: null };
|
|
const rag = { tipo: 'raggio', centro: [50, 50], r: 10, dir_deg: 90, testo: null };
|
|
const ang = { tipo: 'angolo', centro: [50, 50], r: 20, start_deg: 0, extent_deg: 90, testo: null };
|
|
// default
|
|
let p = VO.quotaPunto(lin);
|
|
check('quotaPunto lineare default = punto medio', near(p.x, 50) && near(p.y, 0), JSON.stringify(p));
|
|
p = VO.quotaPunto(rag);
|
|
check('quotaPunto raggio default = centro + 2r lungo dir (90 gradi = verso l\'alto a video)', near(p.x, 50) && near(p.y, 30), JSON.stringify(p));
|
|
p = VO.quotaPunto(ang);
|
|
{ // u a 45 gradi = (cos45, -sin45); T = centro + 1.5r u
|
|
const e = 30 * Math.SQRT1_2;
|
|
check('quotaPunto angolo default = centro + 1.5r a meta arco', near(p.x, 50 + e) && near(p.y, 50 - e), JSON.stringify(p));
|
|
}
|
|
// con testo: lineare p1->p2 lungo +x, n = (0, 1): dv positivo = verso il basso a video
|
|
p = VO.quotaPunto({ tipo: 'lineare', p1: [0, 0], p2: [100, 0], testo: [10, 20] });
|
|
check('quotaPunto lineare con testo [du,dv]', near(p.x, 60) && near(p.y, 20), JSON.stringify(p));
|
|
// inverse
|
|
[lin, rag, ang].forEach((q) => {
|
|
[[3, -7], [-12.5, 40], [0, 0]].forEach((t) => {
|
|
const q2 = Object.assign({}, q, { testo: t });
|
|
const pt = VO.quotaPunto(q2);
|
|
const back = VO.quotaTesto(q2, pt.x, pt.y);
|
|
check('quotaTesto inverso di quotaPunto (' + q.tipo + ' ' + t + ')', near(back[0], t[0]) && near(back[1], t[1]), JSON.stringify(back));
|
|
});
|
|
// dal punto di default si torna a testo di default
|
|
const d = VO.quotaPunto(q), back = VO.quotaTesto(q, d.x, d.y), p2 = VO.quotaPunto(Object.assign({}, q, { testo: back }));
|
|
check('quotaTesto dal punto di default riproduce il default (' + q.tipo + ')', near(p2.x, d.x) && near(p2.y, d.y));
|
|
});
|
|
check('quota malformata: quotaPunto/quotaTesto null, quotaSvg stringa vuota',
|
|
VO.quotaPunto(null) === null && VO.quotaPunto({ tipo: 'boh' }) === null &&
|
|
VO.quotaPunto({ tipo: 'lineare', p1: [0, 'a'], p2: [1, 1] }) === null &&
|
|
VO.quotaTesto({ tipo: 'raggio' }, 1, 1) === null &&
|
|
VO.quotaSvg({ tipo: 'angolo', centro: [0, 0] }) === '' && VO.quotaSvg(undefined) === '');
|
|
check('quotaPunto: testo malformato = default', near(VO.quotaPunto({ tipo: 'lineare', p1: [0, 0], p2: [10, 0], testo: ['x', 1] }).x, 5));
|
|
|
|
// quotaSvg lineare: linee di estensione solo con dv != 0
|
|
const conDv = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [200, 0], testo: [0, 30] }, { zoom: 1, color: '#16a34a' });
|
|
const senzaDv = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [200, 0], testo: [0, 0] }, { zoom: 1, color: '#16a34a' });
|
|
check('quotaSvg lineare dv=30: 2 estensioni + 1 linea di misura, 2 frecce', count(conDv, /<line /g) === 3 && count(conDv, /<polygon /g) === 2, conDv);
|
|
check('quotaSvg lineare dv=0: nessuna estensione (solo linea di misura), 2 frecce', count(senzaDv, /<line /g) === 1 && count(senzaDv, /<polygon /g) === 2, senzaDv);
|
|
check('quotaSvg: tratto non-scaling 1.5 e colore passato', /stroke-width="1.5" vector-effect="non-scaling-stroke"/.test(conDv) && conDv.includes('#16a34a'));
|
|
// frecce dentro/fuori: lineare lunga 200 a zoom 1 -> la punta sta in A (0, 30): primo vertice del poligono
|
|
{ // dentro: punta in A, coda verso +u (x crescente) -> il secondo vertice ha x > 0? no: dentro la freccia punta verso fuori, coda verso l'interno (x>0)
|
|
const m = conDv.match(/<polygon points="([^"]+)"/)[1].split(' ').map((s) => s.split(',').map(Number));
|
|
check('freccia dentro: punta in A, coda verso l\'interno della misura', near(m[0][0], 0) && near(m[0][1], 30) && m[1][0] > 0, m.join('|'));
|
|
const corta = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [10, 0], testo: [0, 30] }, { zoom: 1 });
|
|
const mc = corta.match(/<polygon points="([^"]+)"/)[1].split(' ').map((s) => s.split(',').map(Number));
|
|
check('freccia fuori (misura corta): punta in A, coda fuori (x < 0)', near(mc[0][0], 0) && mc[1][0] < 0, mc.join('|'));
|
|
const zz = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [200, 0], testo: [0, 30] }, { zoom: 4 });
|
|
const mz = zz.match(/<polygon points="([^"]+)"/)[1].split(' ').map((s) => s.split(',').map(Number));
|
|
check('freccia: lunghezza fissa a video (9 px / zoom)', near(mz[1][0], 9 / 4) && count(zz, /<line /g) === 3, mz.join('|'));
|
|
}
|
|
// T oltre B: la linea di misura si allunga
|
|
const oltre = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [100, 0], testo: [150, 30] }, { zoom: 1 });
|
|
check('quotaSvg lineare: scritta oltre B allunga la linea di misura fino a lei', oltre.includes('x2="200"'), oltre);
|
|
// raggio
|
|
const rFuori = VO.quotaSvg({ tipo: 'raggio', centro: [50, 50], r: 10, dir_deg: 0, testo: [30, 0] }, { zoom: 1 });
|
|
const rDentro = VO.quotaSvg({ tipo: 'raggio', centro: [50, 50], r: 10, dir_deg: 0, testo: [5, 0] }, { zoom: 1 });
|
|
check('quotaSvg raggio: crocetta + linea + freccia (fuori)', count(rFuori, /<line /g) === 3 && count(rFuori, /<polygon /g) === 1, rFuori);
|
|
{ // fuori: P = (60, 50), freccia verso il centro: coda a x > 60
|
|
const m = rFuori.match(/<polygon points="([^"]+)"/)[1].split(' ').map((s) => s.split(',').map(Number));
|
|
check('raggio fuori: punta in P sul cerchio, coda verso fuori', near(m[0][0], 60) && near(m[0][1], 50) && m[1][0] > 60, m.join('|'));
|
|
const d = rDentro.match(/<polygon points="([^"]+)"/)[1].split(' ').map((s) => s.split(',').map(Number));
|
|
check('raggio dentro: punta in P, coda verso il centro (x < 60)', near(d[0][0], 60) && d[1][0] < 60, d.join('|'));
|
|
}
|
|
// angolo
|
|
const a1 = VO.quotaSvg(ang, { zoom: 1 });
|
|
check('quotaSvg angolo: 2 estensioni radiali + arco + 2 frecce', count(a1, /<line /g) === 2 && count(a1, /<path /g) === 1 && count(a1, /<polygon /g) === 2, a1);
|
|
// markup senza stringhe dei dati
|
|
const evil = '<script>alert(1)</script>"onload="x';
|
|
const s1 = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [100, 0], testo: [0, 20], nome: evil, extra: evil }, { zoom: 1, color: evil });
|
|
const s2 = VO.quotaSvg({ tipo: 'lineare', p1: [0, 0], p2: [100, 0], testo: [0, evil] }, { zoom: 1 });
|
|
check('quotaSvg: nessuna stringa dei dati nel markup, colore non in lista -> rosso',
|
|
!/script|onload|alert/.test(s1) && s1.includes('#ef4444') && !/script|onload|alert/.test(s2), s1);
|
|
check('quotaSvg: colori ammessi (esiti)', ['#16a34a', '#d97706', '#dc2626'].every((c) => VO.quotaSvg(lin, { color: c }).includes(c)));
|
|
check('svg() invariato: uscita di un segmento', VO.svg([{ type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 }]).startsWith('<line x1="0"'));
|
|
}
|
|
|
|
console.log('\n' + (total - failures) + '/' + total + ' controlli ok');
|
|
process.exit(failures ? 1 : 0);
|