diff --git a/web/app.js b/web/app.js
index 1ac8ccc..24c42dd 100644
--- a/web/app.js
+++ b/web/app.js
@@ -114,14 +114,9 @@ const graspCanvas = $('graspCanvas');
const graspCtx = graspCanvas.getContext('2d');
let displayedGrasp = null;
let graspRgbInFlight = false;
-const cropCanvas = $('cropCanvas');
-const cropCtx = cropCanvas.getContext('2d');
-const scatterXY = $('scatterXY');
-const scatterXZ = $('scatterXZ');
+// (rimossi cropCanvas/scatter — ora c'è findResultCanvas)
let lastDetections = []; // all detections, sorted by confidence desc
let lastSelectedClass = null; // class name whose point cloud is displayed
-let lastCropBbox = null;
-let lastPoints = null; // array of [x,y,z]
let knownClasses = []; // names discovered from /api/perception/classes
// Stable per-class color palette (Okabe-Ito, colour-blind safe).
@@ -161,60 +156,6 @@ function drawGraspFrame() {
});
}
-function drawCrop() {
- cropCtx.fillStyle = '#000';
- cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
- if (!lastCropBbox || !displayedGrasp) return;
- const { x1, y1, x2, y2 } = lastCropBbox;
- const sw = Math.max(1, x2 - x1), sh = Math.max(1, y2 - y1);
- const scale = Math.min(cropCanvas.width / sw, cropCanvas.height / sh);
- const dw = sw * scale, dh = sh * scale;
- const dx = (cropCanvas.width - dw) / 2, dy = (cropCanvas.height - dh) / 2;
- cropCtx.drawImage(displayedGrasp, x1, y1, sw, sh, dx, dy, dw, dh);
- cropCtx.strokeStyle = '#4da3ff';
- cropCtx.lineWidth = 2;
- cropCtx.strokeRect(dx, dy, dw, dh);
-}
-
-function drawScatter(canvas, pts, axU, axV, label) {
- const ctx = canvas.getContext('2d');
- ctx.fillStyle = '#0d1014';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- if (!pts || pts.length === 0) {
- ctx.fillStyle = '#666';
- ctx.font = '12px sans-serif';
- ctx.fillText('(no points)', 10, 20);
- return;
- }
- let uMin = Infinity, uMax = -Infinity, vMin = Infinity, vMax = -Infinity;
- for (const p of pts) {
- if (p[axU] < uMin) uMin = p[axU]; if (p[axU] > uMax) uMax = p[axU];
- if (p[axV] < vMin) vMin = p[axV]; if (p[axV] > vMax) vMax = p[axV];
- }
- const pad = 20;
- const du = Math.max(1e-6, uMax - uMin), dv = Math.max(1e-6, vMax - vMin);
- const s = Math.min((canvas.width - 2*pad) / du, (canvas.height - 2*pad) / dv);
- // axes
- ctx.strokeStyle = '#2a2f37';
- ctx.strokeRect(pad, pad, canvas.width - 2*pad, canvas.height - 2*pad);
- // points
- ctx.fillStyle = '#3ecf8e';
- for (const p of pts) {
- const x = pad + (p[axU] - uMin) * s;
- const y = canvas.height - pad - (p[axV] - vMin) * s;
- ctx.fillRect(x - 1, y - 1, 2, 2);
- }
- ctx.fillStyle = '#9aa0a6';
- ctx.font = '11px ui-monospace, monospace';
- ctx.fillText(label, pad + 2, 14);
- ctx.fillText(`u:[${uMin.toFixed(2)},${uMax.toFixed(2)}] v:[${vMin.toFixed(2)},${vMax.toFixed(2)}]`, pad, canvas.height - 4);
-}
-
-function drawScatters() {
- drawScatter(scatterXY, lastPoints, 0, 1, 'Top-down (X→, Y↑)');
- drawScatter(scatterXZ, lastPoints, 0, 2, 'Laterale (X→, Z↑)');
-}
-
function refreshGraspRgb() {
if (graspRgbInFlight) return;
graspRgbInFlight = true;
@@ -223,7 +164,6 @@ function refreshGraspRgb() {
displayedGrasp = img;
graspRgbInFlight = false;
drawGraspFrame();
- drawCrop();
};
img.onerror = () => { graspRgbInFlight = false; };
img.src = `/api/calibration/rgb.jpg?t=${Date.now()}`;
@@ -266,11 +206,9 @@ function applyScanResponse(r) {
lastDetections = Array.isArray(r.detections) ? r.detections : [];
const sel = r.selected || null;
lastSelectedClass = sel ? sel.class : null;
- lastCropBbox = sel ? sel.bbox : null;
- lastPoints = r.points_base || r.points_camera || null;
refreshObjectPickOptions(lastDetections);
renderGraspLegend(lastDetections);
- drawGraspFrame(); drawCrop(); drawScatters();
+ drawGraspFrame();
}
$('detectBtn').addEventListener('click', async () => {
@@ -295,27 +233,6 @@ $('detectBtn').addEventListener('click', async () => {
} catch (e) { setStatus(e.message, 'err'); }
});
-$('planBtn').addEventListener('click', async () => {
- try {
- setStatus('Pianifico…');
- const body = { conf: +$('graspConf').value || null, class_name: pickedClass() };
- const r = await api('POST', '/api/grasp/plan', body);
- if (r.detection) {
- // keep the multi-class view, but highlight the planned class
- lastSelectedClass = r.detection.class || lastSelectedClass;
- lastCropBbox = r.detection.bbox;
- if (!lastDetections.some(d => d.class === r.detection.class)) {
- lastDetections = [r.detection, ...lastDetections];
- }
- }
- $('graspOut').textContent = JSON.stringify(r, null, 2);
- renderGraspLegend(lastDetections);
- drawGraspFrame(); drawCrop();
- const src = r.source ? `source=${r.source}${r.sam21_fallback_reason ? ` (${r.sam21_fallback_reason})` : ''} · ` : '';
- setStatus(`Plan ok · ${src}n=${r.n_points ?? '?'} · ${fmtTimings(r.timings_ms)}`, 'ok');
- } catch (e) { setStatus(e.message, 'err'); }
-});
-
// --- continuous detection loop ---
const STATS_WINDOW = 5;
const MISS_LIMIT = 5; // consecutive frames without detection before clearing the view
@@ -327,12 +244,8 @@ let missStreak = 0;
function clearDetectionState(reason) {
lastDetections = [];
lastSelectedClass = null;
- lastCropBbox = null;
- lastPoints = null;
recentCentroids = [];
drawGraspFrame(); // redraws RGB without bbox overlay
- drawCrop(); // wipes crop thumbnail
- drawScatters(); // wipes scatter plots
updateStats(); // resets table to dashes
refreshObjectPickOptions([]);
renderGraspLegend([]);
@@ -385,11 +298,9 @@ async function loopTick() {
const frame = r.points_base ? 'base' : 'camera';
lastDetections = Array.isArray(r.detections) ? r.detections : [];
lastSelectedClass = r.selected ? r.selected.class : null;
- lastCropBbox = r.selected ? r.selected.bbox : null;
- lastPoints = pts;
refreshObjectPickOptions(lastDetections);
renderGraspLegend(lastDetections);
- drawGraspFrame(); drawCrop(); drawScatters();
+ drawGraspFrame();
const c = centroidOf(pts);
if (c) {
recentCentroids.push(c);
@@ -467,21 +378,6 @@ document.querySelectorAll('.tab').forEach(btn => btn.addEventListener('click', (
if (btn.dataset.tab !== 'grasp' && loopTimer) stopLoop();
}));
-$('executeBtn').addEventListener('click', async () => {
- if (!$('graspDryRun').checked && !confirm('Eseguire realmente la presa sul robot?')) return;
- try {
- setStatus('Esecuzione…');
- const body = {
- conf: +$('graspConf').value || null,
- class_name: pickedClass(),
- dry_run: $('graspDryRun').checked,
- };
- const r = await api('POST', '/api/grasp/execute', body);
- $('graspOut').textContent = JSON.stringify(r, null, 2);
- setStatus(`Grasp eseguito · ${fmtTimings(r.timings_ms)}`, 'ok');
- } catch (e) { setStatus(e.message, 'err'); }
-});
-
// --- object pick select + legend + class discovery ---
// Build per-class summary: best (highest-confidence) detection and instance count.
@@ -575,6 +471,102 @@ $('graspObjectPick').addEventListener('change', () => {
if (!loopTimer) $('detectBtn').click();
});
+// ==================== FIT MODELLO ====================
+const findCanvas = $('findResultCanvas');
+const findCtx = findCanvas.getContext('2d');
+let findResult = null;
+let findRgbImg = null;
+
+async function loadRecipes() {
+ const sel = $('findModelPick');
+ try {
+ const r = await api('GET', '/api/models/recipes');
+ sel.innerHTML = '';
+ (r.recipes || []).forEach(rec => {
+ const opt = document.createElement('option');
+ opt.value = rec.id;
+ opt.textContent = `${rec.name} (${rec.symmetry_type})`;
+ sel.appendChild(opt);
+ });
+ sel.disabled = (r.recipes || []).length === 0;
+ $('findBtn').disabled = !sel.value;
+ } catch (e) {
+ sel.innerHTML = ``;
+ }
+}
+
+$('findModelPick').addEventListener('change', () => {
+ $('findBtn').disabled = !$('findModelPick').value;
+});
+
+$('findRecipesRefreshBtn').addEventListener('click', loadRecipes);
+
+$('findBtn').addEventListener('click', async () => {
+ const recipeId = $('findModelPick').value;
+ if (!recipeId) return;
+ $('findBtn').disabled = true;
+ $('findStatus').textContent = 'Cerco posa…';
+ $('findStatus').className = 'status-line';
+ try {
+ const body = {
+ recipe_id: recipeId,
+ conf: +$('graspConf').value || null,
+ class_name: pickedClass(),
+ };
+ const r = await api('POST', '/api/grasp/find_model', body);
+ findResult = r;
+ findRgbImg = new Image();
+ findRgbImg.onload = drawFindResult;
+ findRgbImg.src = 'data:image/jpeg;base64,' + r.rgb_jpeg_b64;
+ const fit = r.fit_ok
+ ? `OK · fitness=${(r.fitness ?? 0).toFixed(2)} · rmse=${(r.inlier_rmse_mm ?? 0).toFixed(2)}mm`
+ : `Fit fallito: ${r.fit_reason}`;
+ $('findStatus').textContent = `${fit} · ${fmtTimings(r.timings_ms)}`;
+ $('findStatus').className = 'status-line ' + (r.fit_ok ? 'ok' : 'warn');
+ $('findDetails').textContent = JSON.stringify({
+ detection: r.detection,
+ recipe: r.recipe,
+ n_accepted: r.n_accepted,
+ n_excluded: r.n_excluded,
+ fitness: r.fitness,
+ rmse_mm: r.inlier_rmse_mm,
+ timings: r.timings_ms,
+ }, null, 2);
+ } catch (e) {
+ $('findStatus').textContent = `errore: ${e.message}`;
+ $('findStatus').className = 'status-line err';
+ } finally {
+ $('findBtn').disabled = !$('findModelPick').value;
+ }
+});
+
+['findShowModel', 'findShowAccepted', 'findShowExcluded'].forEach(id =>
+ $(id).addEventListener('change', drawFindResult));
+
+function drawFindResult() {
+ if (!findResult || !findRgbImg || !findRgbImg.complete) return;
+ const [W, H] = findResult.rgb_size;
+ if (findCanvas.width !== W) { findCanvas.width = W; findCanvas.height = H; }
+ findCtx.drawImage(findRgbImg, 0, 0);
+ const dot = (pts, color, r) => {
+ findCtx.fillStyle = color;
+ for (const [u, v] of pts) findCtx.fillRect(u - r, v - r, r*2+1, r*2+1);
+ };
+ if ($('findShowExcluded').checked) dot(findResult.excluded_points_px, '#e74c3c', 1);
+ if ($('findShowAccepted').checked) dot(findResult.accepted_points_px, '#3ecf8e', 1);
+ if ($('findShowModel').checked) dot(findResult.model_points_px, '#4da3ff', 1);
+}
+
+function loadRecipesIfNeeded() {
+ if ($('tab-grasp').classList.contains('active') && !$('findModelPick').dataset.loaded) {
+ loadRecipes();
+ $('findModelPick').dataset.loaded = '1';
+ }
+}
+document.querySelectorAll('.tab').forEach(t =>
+ t.addEventListener('click', () => setTimeout(loadRecipesIfNeeded, 50)));
+loadRecipesIfNeeded();
+
// ==================== EE ====================
async function refreshWrist() {
try {