Files
Presa-Mano/web/work_plane.js
T
Adriano Dal PastroandClaude Opus 4.7 b4db712cd0 fix(gui): work_plane RGB feed via same-origin proxy
Direct fetch to http://localhost:8877/api/rgb fails from the browser due
to CORS (page is served from :8890). Switch to /api/calibration/rgb.jpg
which presa-mano already proxies same-origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:36:56 +02:00

284 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Calibrazione Piano tab: shift+drag ROI on RGB, fit preview, offset
// slider, save / clear, and a 3D scatter with the plane mesh.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const $ = (id) => document.getElementById(id);
const RGB_REFRESH_MS = 1000;
let roi = null; // {u0,v0,u1,v1} in image pixel coords
let previewPlane = null; // last response.plane (preview, not saved)
let previewPoints = null; // list of [x,y,z] in CV camera frame, meters
let savedPlane = null; // last GET /api/work_plane response.plane
function setChip(text, level) {
const el = $('wpStatusChip');
if (!el) return;
el.textContent = text;
el.className = 'post-health ' + (level || '');
}
function setRoiInfo() {
const el = $('wpRoiInfo');
if (!roi) { el.textContent = '— · disegna con shift+drag'; el.classList.add('dim'); return; }
el.classList.remove('dim');
el.textContent = `u=${roi.u0}…${roi.u1} v=${roi.v0}…${roi.v1} (${roi.u1 - roi.u0}×${roi.v1 - roi.v0} px)`;
}
function updateButtonStates() {
$('wpFitBtn').disabled = !roi;
$('wpSaveBtn').disabled = !previewPlane;
$('wpClearBtn').disabled = !savedPlane;
}
function rgbUrl() {
// Same-origin RGB proxy exposed by presa-mano (no CORS issues).
return `/api/calibration/rgb.jpg?t=${Date.now()}`;
}
function startRgbLoop() {
const tick = () => { $('wpImg').src = rgbUrl(); };
tick();
setInterval(tick, RGB_REFRESH_MS);
}
// ---------------- ROI drawing on the RGB image ----------------
(function setupRoiDrawing() {
const viewport = $('wpViewport');
const img = $('wpImg');
const overlay = $('wpOverlay');
let drawing = null;
function imgPixelFromEvent(e) {
const rect = img.getBoundingClientRect();
const sx = img.naturalWidth / rect.width;
const sy = img.naturalHeight / rect.height;
return {
u: Math.round((e.clientX - rect.left) * sx),
v: Math.round((e.clientY - rect.top) * sy),
};
}
function syncOverlaySize() {
overlay.width = img.naturalWidth || 640;
overlay.height = img.naturalHeight || 480;
overlay.style.width = img.clientWidth + 'px';
overlay.style.height = img.clientHeight + 'px';
}
img.addEventListener('load', syncOverlaySize);
function drawOverlay() {
const ctx = overlay.getContext('2d');
ctx.clearRect(0, 0, overlay.width, overlay.height);
if (roi) {
ctx.strokeStyle = '#9b6dff';
ctx.lineWidth = 2;
ctx.strokeRect(roi.u0, roi.v0, roi.u1 - roi.u0, roi.v1 - roi.v0);
}
}
viewport.addEventListener('mousedown', (e) => {
if (!e.shiftKey) return; // shift+drag = ROI; plain drag = pan (no-op here)
const p = imgPixelFromEvent(e);
drawing = { u0: p.u, v0: p.v, u1: p.u, v1: p.v };
e.preventDefault();
});
viewport.addEventListener('mousemove', (e) => {
if (!drawing) return;
const p = imgPixelFromEvent(e);
drawing.u1 = p.u;
drawing.v1 = p.v;
roi = {
u0: Math.min(drawing.u0, drawing.u1),
v0: Math.min(drawing.v0, drawing.v1),
u1: Math.max(drawing.u0, drawing.u1),
v1: Math.max(drawing.v0, drawing.v1),
};
drawOverlay();
setRoiInfo();
updateButtonStates();
});
viewport.addEventListener('mouseup', () => { drawing = null; });
viewport.addEventListener('mouseleave', () => { drawing = null; });
})();
// ---------------- Three.js 3D viewer ----------------
const three = (function setupThree() {
const canvas = $('wp3dCanvas');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.01, 20);
camera.position.set(0.5, -0.5, -1.2);
camera.lookAt(0, 0, 1);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(canvas.width, canvas.height);
const controls = new OrbitControls(camera, canvas);
scene.add(new THREE.AxesHelper(0.1));
const planeMesh = new THREE.Mesh(
new THREE.PlaneGeometry(0.4, 0.4),
new THREE.MeshBasicMaterial({ color: 0x9b6dff, transparent: true, opacity: 0.3, side: THREE.DoubleSide }),
);
planeMesh.visible = false;
scene.add(planeMesh);
let scatter = null;
function render() { renderer.render(scene, camera); }
(function loop() { controls.update(); render(); requestAnimationFrame(loop); })();
function setPlane(plane) {
if (!plane) { planeMesh.visible = false; return; }
const c = plane.centroid;
const n = plane.normal;
planeMesh.position.set(c[0], c[1], c[2]);
// Align plane to its normal.
planeMesh.lookAt(c[0] + n[0], c[1] + n[1], c[2] + n[2]);
planeMesh.visible = true;
}
function setScatter(pointsCv, plane, offsetMm) {
if (scatter) { scene.remove(scatter); scatter.geometry.dispose(); scatter.material.dispose(); scatter = null; }
if (!pointsCv || pointsCv.length === 0) return;
const n = pointsCv.length;
const positions = new Float32Array(n * 3);
const colors = new Float32Array(n * 3);
const nx = plane ? plane.normal[0] : 0;
const ny = plane ? plane.normal[1] : 0;
const nz = plane ? plane.normal[2] : 0;
const d = plane ? plane.offset : 0;
const offsetM = (offsetMm || 0) / 1000.0;
const rmsM = plane ? (plane.fit_rms_mm / 1000.0) * 2.0 : 0.01;
for (let i = 0; i < n; i++) {
const x = pointsCv[i][0], y = pointsCv[i][1], z = pointsCv[i][2];
positions[3 * i] = x; positions[3 * i + 1] = y; positions[3 * i + 2] = z;
const signed = plane ? (nx * x + ny * y + nz * z + d) : 0;
let r = 0.5, g = 0.5, b = 0.5;
if (plane) {
if (signed > offsetM) { r = 1.0; g = 0.83; b = 0.31; } // yellow: above plane+offset
else if (signed < -offsetM) { r = 0.93; g = 0.27; b = 0.27; } // red: below plane-offset
else if (Math.abs(signed) <= rmsM) { r = 0.30; g = 0.78; b = 0.40; } // green: inlier
else { r = 0.55; g = 0.55; b = 0.55; } // grey: buffer
}
colors[3 * i] = r; colors[3 * i + 1] = g; colors[3 * i + 2] = b;
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({ size: 0.005, vertexColors: true });
scatter = new THREE.Points(geom, mat);
scene.add(scatter);
}
return { setPlane, setScatter };
})();
// ---------------- State + button handlers ----------------
async function fetchSaved() {
try {
const r = await fetch('/api/work_plane').then((r) => r.json());
savedPlane = r.plane || null;
} catch (e) {
savedPlane = null;
}
if (savedPlane && !previewPlane) {
three.setPlane(savedPlane);
renderDetails(savedPlane);
$('wpStateBlock').innerHTML = `piano salvato · RMS ${savedPlane.fit_rms_mm.toFixed(1)} mm · n_inliers ${savedPlane.n_inliers} · offset ${savedPlane.offset_mm} mm`;
setChip('piano salvato', 'ok');
} else if (!savedPlane && !previewPlane) {
$('wpStateBlock').innerHTML = '<div class="dim">nessun piano</div>';
setChip('nessun piano');
}
updateButtonStates();
}
function renderDetails(plane) {
$('wpDetails').textContent = JSON.stringify(plane, null, 2);
}
async function doFit() {
if (!roi) return;
setChip('fit…');
$('wpFitBtn').disabled = true;
try {
const body = {
u_min: roi.u0, v_min: roi.v0, u_max: roi.u1, v_max: roi.v1,
offset_mm: parseFloat($('wpOffset').value || '5'),
};
const r = await fetch('/api/work_plane/fit', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then((r) => r.json());
if (r.detail) throw new Error(r.detail);
previewPlane = r.plane;
previewPoints = r.points_camera;
three.setPlane(previewPlane);
three.setScatter(previewPoints, previewPlane, previewPlane.offset_mm);
renderDetails(previewPlane);
$('wpStateBlock').innerHTML = `<strong>preview</strong> · RMS ${previewPlane.fit_rms_mm.toFixed(1)} mm · n_inliers ${previewPlane.n_inliers} · offset ${previewPlane.offset_mm} mm · <span class="warn">non salvato</span>`;
setChip('preview pronto', 'ok');
} catch (e) {
setChip('fit failed: ' + e.message, 'err');
} finally {
updateButtonStates();
}
}
async function doSave() {
if (!previewPlane) return;
setChip('save…');
try {
const r = await fetch('/api/work_plane/save', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plane: previewPlane }),
}).then((r) => r.json());
if (r.detail) throw new Error(r.detail);
savedPlane = r.plane;
previewPlane = null;
$('wpStateBlock').innerHTML = `piano salvato · RMS ${savedPlane.fit_rms_mm.toFixed(1)} mm · n_inliers ${savedPlane.n_inliers} · offset ${savedPlane.offset_mm} mm`;
renderDetails(savedPlane);
setChip('piano salvato', 'ok');
} catch (e) {
setChip('save failed: ' + e.message, 'err');
} finally {
updateButtonStates();
}
}
async function doClear() {
if (!confirm('Rimuovere il piano salvato?')) return;
setChip('clear…');
try {
await fetch('/api/work_plane', { method: 'DELETE' }).then((r) => r.json());
savedPlane = null;
previewPlane = null;
previewPoints = null;
three.setPlane(null);
three.setScatter(null);
$('wpStateBlock').innerHTML = '<div class="dim">nessun piano</div>';
$('wpDetails').textContent = '—';
setChip('nessun piano');
} catch (e) {
setChip('clear failed: ' + e.message, 'err');
} finally {
updateButtonStates();
}
}
// ---------------- Offset slider: re-color in real time ----------------
$('wpOffset').addEventListener('input', () => {
if (previewPlane && previewPoints) {
const off = parseFloat($('wpOffset').value || '0');
previewPlane.offset_mm = off;
three.setScatter(previewPoints, previewPlane, off);
$('wpStateBlock').innerHTML = `<strong>preview</strong> · RMS ${previewPlane.fit_rms_mm.toFixed(1)} mm · n_inliers ${previewPlane.n_inliers} · offset ${previewPlane.offset_mm.toFixed(1)} mm · <span class="warn">non salvato</span>`;
}
});
$('wpFitBtn').addEventListener('click', doFit);
$('wpSaveBtn').addEventListener('click', doSave);
$('wpClearBtn').addEventListener('click', doClear);
// Initial load.
startRgbLoop();
fetchSaved();