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
425 lines
19 KiB
JavaScript
425 lines
19 KiB
JavaScript
/**
|
|
* #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);
|
|
* layerOf(o), quotaPunto(q), quotaTesto(q, x, y), quotaSvg(q, opts): livelli e quote CAD (#12).
|
|
* 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 <svg> 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/non finito = quota non misurata (Number(null) e' 0): etichetta NaN, status della riga (fail)
|
|
var v = (r.value === null || r.value === undefined || r.value === '' || typeof r.value === 'boolean')
|
|
? NaN : Number(r.value);
|
|
var txt = isFinite(v) ? v.toFixed(3) : 'NaN';
|
|
out.push({
|
|
key: r.key,
|
|
x: pt.x,
|
|
y: pt.y,
|
|
text: r.name + ' ' + txt + ' ' + (r.unit || ''),
|
|
status: r.status,
|
|
});
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// ---- #12: livelli e quote stile CAD ------------------------------------------------------------
|
|
// Coordinate in px immagine; angoli in gradi antiorari a video, versore u(a) = (cos a, -sin a),
|
|
// normale n = (-u.y, u.x). Contratto: gitea:Adriano/TieVision-Station#12.
|
|
|
|
// #12: colori ammessi per le quote (esiti del visore + rosso di default); mai una stringa dei dati nel markup
|
|
var QUOTA_COLORS = ['#16a34a', '#d97706', '#dc2626', '#ef4444'];
|
|
var QUOTA_ARROW = 9, QUOTA_GAP = 3, QUOTA_OVER = 6, QUOTA_LW = 1.5; // px a video
|
|
|
|
// #12: livello di una forma: campo "layer" se valido, altrimenti found = costruzioni, il resto = ricerca
|
|
function layerOf(o) {
|
|
var l = o && o.layer;
|
|
if (l === 'costruzioni' || l === 'ricerca' || l === 'quote') return l;
|
|
return o && o.role === 'found' ? 'costruzioni' : 'ricerca';
|
|
}
|
|
|
|
function pt2(p) { return Array.isArray(p) && p.length === 2 && num(p[0]) && num(p[1]); }
|
|
function uVec(deg) { var a = rad(deg); return [Math.cos(a), -Math.sin(a)]; }
|
|
|
|
// #12: riferimento locale della quota {b, u, n, td, ...} o null se malformata
|
|
function quotaFrame(q) {
|
|
if (!q || typeof q !== 'object') return null;
|
|
var f = { tipo: q.tipo };
|
|
switch (q.tipo) {
|
|
case 'lineare': {
|
|
if (!pt2(q.p1) || !pt2(q.p2)) return null;
|
|
var dx = q.p2[0] - q.p1[0], dy = q.p2[1] - q.p1[1], L = Math.sqrt(dx * dx + dy * dy);
|
|
f.u = L > 1e-12 ? [dx / L, dy / L] : [1, 0];
|
|
f.b = [(q.p1[0] + q.p2[0]) / 2, (q.p1[1] + q.p2[1]) / 2];
|
|
f.L = L; f.dflt = [0, 0];
|
|
break;
|
|
}
|
|
case 'raggio':
|
|
if (!pt2(q.centro) || !num(q.r) || !num(q.dir_deg)) return null;
|
|
f.u = uVec(q.dir_deg); f.b = [q.centro[0], q.centro[1]]; f.dflt = [2 * q.r, 0];
|
|
break;
|
|
case 'angolo':
|
|
if (!pt2(q.centro) || !num(q.r) || !num(q.start_deg) || !num(q.extent_deg)) return null;
|
|
f.u = uVec(q.start_deg + q.extent_deg / 2); f.b = [q.centro[0], q.centro[1]]; f.dflt = [1.5 * q.r, 0];
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
f.n = [-f.u[1], f.u[0]];
|
|
f.td = pt2(q.testo) ? [q.testo[0], q.testo[1]] : f.dflt; // testo null o malformato = posizione di default
|
|
return f;
|
|
}
|
|
|
|
// #12: punto T della scritta (px immagine) = B + du*u + dv*n; null se la quota e' malformata
|
|
function quotaPunto(q) {
|
|
var f = quotaFrame(q);
|
|
if (!f) return null;
|
|
return { x: f.b[0] + f.td[0] * f.u[0] + f.td[1] * f.n[0], y: f.b[1] + f.td[0] * f.u[1] + f.td[1] * f.n[1] };
|
|
}
|
|
|
|
// #12: inverso di quotaPunto: [du, dv] che porta la scritta nel punto (x, y); serve al trascinamento
|
|
function quotaTesto(q, x, y) {
|
|
var f = quotaFrame(q);
|
|
if (!f || !num(x) || !num(y)) return null;
|
|
var rx = x - f.b[0], ry = y - f.b[1];
|
|
return [rx * f.u[0] + ry * f.u[1], rx * f.n[0] + ry * f.n[1]];
|
|
}
|
|
|
|
function qLine(x1, y1, x2, y2, color) {
|
|
return '<line x1="' + n2(x1) + '" y1="' + n2(y1) + '" x2="' + n2(x2) + '" y2="' + n2(y2) + '" stroke="' + color +
|
|
'" stroke-width="' + QUOTA_LW + '" vector-effect="non-scaling-stroke"/>';
|
|
}
|
|
// #12: freccia piena con la punta in (x, y) rivolta verso (dx, dy) (versore); lunghezza fissa a video
|
|
function qArrow(x, y, dx, dy, zoom, color) {
|
|
var len = QUOTA_ARROW / zoom, w = len / 3;
|
|
var bx = x - dx * len, by = y - dy * len;
|
|
return '<polygon points="' + n2(x) + ',' + n2(y) + ' ' + n2(bx - dy * w) + ',' + n2(by + dx * w) + ' ' +
|
|
n2(bx + dy * w) + ',' + n2(by - dx * w) + '" fill="' + color + '" stroke="none"/>';
|
|
}
|
|
|
|
function quotaSvgLineare(q, f, zoom, color) {
|
|
var out = [], u = f.u, n = f.n, dv = f.td[1], du = f.td[0], i;
|
|
var sg = dv >= 0 ? 1 : -1, gap = QUOTA_GAP / zoom, over = QUOTA_OVER / zoom;
|
|
var ps = [q.p1, q.p2];
|
|
if (Math.abs(dv) * zoom >= 3) { // linee di estensione: assenti se la linea di misura sta sul pezzo
|
|
for (i = 0; i < 2; i++) {
|
|
out.push(qLine(ps[i][0] + sg * gap * n[0], ps[i][1] + sg * gap * n[1],
|
|
ps[i][0] + (dv + sg * over) * n[0], ps[i][1] + (dv + sg * over) * n[1], color));
|
|
}
|
|
}
|
|
var m0 = [f.b[0] + dv * n[0], f.b[1] + dv * n[1]]; // punto medio della linea di misura
|
|
var h = f.L / 2, inside = f.L * zoom >= 2.5 * QUOTA_ARROW;
|
|
var lo = Math.min(-h, du), hi = Math.max(h, du); // la linea arriva fino alla scritta se sta oltre A o B
|
|
if (!inside) { lo = Math.min(lo, -h - 12 / zoom); hi = Math.max(hi, h + 12 / zoom); }
|
|
out.push(qLine(m0[0] + lo * u[0], m0[1] + lo * u[1], m0[0] + hi * u[0], m0[1] + hi * u[1], color));
|
|
var k = inside ? 1 : -1; // frecce corte: fuori, rivolte verso l'interno
|
|
out.push(qArrow(m0[0] - h * u[0], m0[1] - h * u[1], -k * u[0], -k * u[1], zoom, color));
|
|
out.push(qArrow(m0[0] + h * u[0], m0[1] + h * u[1], k * u[0], k * u[1], zoom, color));
|
|
return out.join('');
|
|
}
|
|
|
|
function quotaSvgRaggio(q, f, zoom, color) {
|
|
var out = [], c = q.centro, T = quotaPunto(q);
|
|
var vx = T.x - c[0], vy = T.y - c[1], d = Math.sqrt(vx * vx + vy * vy);
|
|
var w = d > 1e-9 ? [vx / d, vy / d] : uVec(q.dir_deg);
|
|
var px = c[0] + q.r * w[0], py = c[1] + q.r * w[1], cr = 4 / zoom;
|
|
out.push(qLine(c[0] - cr, c[1], c[0] + cr, c[1], color)); // crocetta sul centro
|
|
out.push(qLine(c[0], c[1] - cr, c[0], c[1] + cr, color));
|
|
if (d > q.r) { // scritta fuori dal cerchio: da P a T, freccia in P verso il centro
|
|
out.push(qLine(px, py, T.x, T.y, color));
|
|
out.push(qArrow(px, py, -w[0], -w[1], zoom, color));
|
|
} else { // scritta dentro: dal centro a P, freccia in P verso fuori
|
|
out.push(qLine(c[0], c[1], px, py, color));
|
|
out.push(qArrow(px, py, w[0], w[1], zoom, color));
|
|
}
|
|
return out.join('');
|
|
}
|
|
|
|
function quotaSvgAngolo(q, f, zoom, color) {
|
|
var out = [], c = q.centro, T = quotaPunto(q), i;
|
|
var rho = Math.sqrt((T.x - c[0]) * (T.x - c[0]) + (T.y - c[1]) * (T.y - c[1]));
|
|
if (!(rho > 1e-9)) rho = q.r > 0 ? 1.5 * q.r : 1;
|
|
var gap = QUOTA_GAP / zoom, over = QUOTA_OVER / zoom;
|
|
var ends = [q.start_deg, q.start_deg + q.extent_deg];
|
|
var sgn = q.extent_deg >= 0 ? 1 : -1;
|
|
var inside = rho * rad(Math.abs(q.extent_deg)) * zoom >= 2.5 * QUOTA_ARROW;
|
|
for (i = 0; i < 2; i++) {
|
|
var u = uVec(ends[i]);
|
|
var a = rho >= q.r ? q.r + gap : rho - over, b = rho >= q.r ? rho + over : q.r - gap;
|
|
out.push(qLine(c[0] + a * u[0], c[1] + a * u[1], c[0] + b * u[0], c[1] + b * u[1], color)); // estensione radiale
|
|
}
|
|
out.push('<path d="' + arcPath({ col: c[0], row: c[1], radius: rho, start_deg: q.start_deg, extent_deg: q.extent_deg }) +
|
|
'" fill="none" stroke="' + color + '" stroke-width="' + QUOTA_LW + '" vector-effect="non-scaling-stroke"/>');
|
|
for (i = 0; i < 2; i++) {
|
|
var ang = rad(ends[i]), uu = uVec(ends[i]);
|
|
var tg = [-Math.sin(ang), -Math.cos(ang)]; // tangente nel verso degli angoli crescenti
|
|
var dir = (i === 0 ? -1 : 1) * sgn * (inside ? 1 : -1); // dentro: verso fuori dall'arco; corte: verso l'interno
|
|
out.push(qArrow(c[0] + rho * uu[0], c[1] + rho * uu[1], dir * tg[0], dir * tg[1], zoom, color));
|
|
}
|
|
return out.join('');
|
|
}
|
|
|
|
// #12: quota -> figli SVG (px immagine). opts = {zoom, color}; misure fisse a video divise per lo zoom.
|
|
// Solo numeri e colori da lista fissa nel markup.
|
|
function quotaSvg(q, opts) {
|
|
opts = opts || {};
|
|
var zoom = pos(opts.zoom, 1);
|
|
var color = QUOTA_COLORS.indexOf(opts.color) >= 0 ? opts.color : COLOR_OTHER;
|
|
try {
|
|
var f = quotaFrame(q);
|
|
if (!f) return '';
|
|
if (f.tipo === 'lineare') return quotaSvgLineare(q, f, zoom, color);
|
|
if (f.tipo === 'raggio') return quotaSvgRaggio(q, f, zoom, color);
|
|
return quotaSvgAngolo(q, f, zoom, color);
|
|
} catch (e) { return ''; } // una quota rotta non ferma le altre
|
|
}
|
|
|
|
return { draw: draw, anchor: anchor, labels: labels, svg: svg,
|
|
layerOf: layerOf, quotaPunto: quotaPunto, quotaTesto: quotaTesto, quotaSvg: quotaSvg };
|
|
})();
|
|
|
|
if (typeof window !== 'undefined') window.VisionOverlay = VisionOverlay;
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = VisionOverlay;
|