diff --git a/web/pose_3d.js b/web/pose_3d.js index 6bedc7e..3ad8f1c 100644 --- a/web/pose_3d.js +++ b/web/pose_3d.js @@ -784,6 +784,7 @@ async function pollState() { updateNuwaInputsFromCfg(); updateD435InputsFromCfg(); applyCloudParamsFromCfg(s.cloud_params); + applyCrossCameraFromCfg(s.cross_camera); applyCalibratedD435UCS(); // Applica TUTTI i giunti reali sul modello URDF (29 motori G1) state.joints = s.joints || null; // snapshot per tag locator save @@ -1972,6 +1973,75 @@ async function pollTags() { } } +// Costruisce una THREE.Matrix4 (row-major via .set) da {scale,R,t}: p' = s·R·p + t. +function matrixFromAlign(cc) { + if (!cc || !Array.isArray(cc.R) || cc.R.length !== 3 || !Array.isArray(cc.t)) return null; + const s = Number.isFinite(cc.scale) ? cc.scale : 1; + const R = cc.R, t = cc.t; + return new THREE.Matrix4().set( + s*R[0][0], s*R[0][1], s*R[0][2], t[0], + s*R[1][0], s*R[1][1], s*R[1][2], t[1], + s*R[2][0], s*R[2][1], s*R[2][2], t[2], + 0, 0, 0, 1, + ); +} + +async function doAlign() { + const ids = ($('p3FrontTagIds')?.value || '0-49').trim(); + const alignId = parseInt($('p3AlignTagId')?.value, 10) || 0; + const status = $('p3AlignStatus'); + if (status) { status.textContent = 'allineamento…'; status.style.color = 'var(--dim)'; } + try { + const r = await jsonFetch( + `/api/pose_3d/align?ids=${encodeURIComponent(ids)}&align_id=${alignId}`, + { method: 'POST' }); + if (!r.found_in_both) { + if (status) { status.textContent = r.error || 'tag non visto da entrambe'; status.style.color = 'var(--danger)'; } + return; + } + state.align.matrix = matrixFromAlign(r); + state.align.id = r.align_id; + if (status) { + status.textContent = `scala=${r.scale.toFixed(3)} · rmse=${r.rmse_mm.toFixed(1)}mm · n=${r.n_corners}`; + status.style.color = 'var(--ok)'; + } + applyNuwaCloudPlacement(); + } catch (e) { + if (status) { status.textContent = `errore: ${e?.message || e}`; status.style.color = 'var(--danger)'; } + } +} + +// Persistenza del solo flag + align_id (la matrice la salva il backend in /align). +async function saveCrossCameraFlag() { + try { + await jsonFetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pose_3d: { cross_camera: { + enabled: $('p3AlignEnabled')?.checked === true, + align_id: parseInt($('p3AlignTagId')?.value, 10) || 0, + } } }), + }); + } catch (e) { console.warn('[pose_3d] save cross_camera failed:', e?.message || e); } +} + +// Ripopola UI + matrice dalla config (guardato come applyCloudParamsFromCfg). +function applyCrossCameraFromCfg(cc) { + if (state.cross_camera_populated) return; + if (!cc || Object.keys(cc).length === 0) { state.cross_camera_populated = true; return; } + const en = $('p3AlignEnabled'); if (en) en.checked = cc.enabled === true; + const id = $('p3AlignTagId'); if (id && Number.isFinite(cc.align_id)) id.value = String(cc.align_id); + state.align.enabled = cc.enabled === true; + state.align.id = cc.align_id || 0; + const m = matrixFromAlign(cc); + if (m) state.align.matrix = m; + const status = $('p3AlignStatus'); + if (status && Number.isFinite(cc.rmse_mm)) { + status.textContent = `caricata · scala=${(cc.scale||1).toFixed(3)} · rmse=${cc.rmse_mm.toFixed(1)}mm`; + } + state.cross_camera_populated = true; +} + function bindFrontTagFinder() { const cb = $('p3ShowFrontTags'); if (!cb) return; @@ -1990,6 +2060,13 @@ function bindFrontTagFinder() { function bindCloudControls() { $('p3CloudAcquireBtn')?.addEventListener('click', acquireClouds); + $('p3AlignBtn')?.addEventListener('click', doAlign); + $('p3AlignEnabled')?.addEventListener('change', () => { + state.align.enabled = $('p3AlignEnabled').checked === true; + saveCrossCameraFlag(); + applyNuwaCloudPlacement(); + }); + $('p3AlignTagId')?.addEventListener('change', saveCrossCameraFlag); $('p3CloudClearBtn')?.addEventListener('click', clearClouds); $('p3ShowNuwaCloud')?.addEventListener('change', applyCloudVisibility); $('p3ShowD435Cloud')?.addEventListener('change', applyCloudVisibility);