diff --git a/public/boardViewer.html b/public/boardViewer.html index 62efad3..e0ecbb5 100644 --- a/public/boardViewer.html +++ b/public/boardViewer.html @@ -666,6 +666,54 @@ function buildCompareLines() { if (onlyCompare.length) parts.push(`nur Vergleich: ${onlyCompare.join(' ')}`); if (noMatch.length) parts.push(`nur Basis: ${noMatch.join(' ')}`); vlog(parts.join(' | '), matchedIds.length ? 'ok' : 'warn'); + + // ── Bewegungsanalyse: mittlerer Verschiebungsvektor → Abweichung von X-Achse ── + if (matchedIds.length > 0) { + // Summe aller Verschiebungsvektoren (in Roboter-Koordinaten mm) + let sx = 0, sy = 0, sz = 0; + for (const cm of _compareFremdMarkers) { + const pm = primaryMap.get(cm.marker_id); + if (!pm) continue; + const [pmx, pmy, pmz] = pm.position_mm.map(Number); + const [cmx, cmy, cmz] = cm.position_mm.map(Number); + sx += cmx - pmx; sy += cmy - pmy; sz += cmz - pmz; + } + const n = matchedIds.length; + const dx = sx / n, dy = sy / n, dz = sz / n; + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + + if (dist > 0.01) { // mindestens 0.01 mm Bewegung + // Einheitsvektor – immer in Richtung positives X zeigen (konsistente Vorzeichen) + let vx = dx/dist, vy = dy/dist, vz = dz/dist; + if (vx < 0) { vx = -vx; vy = -vy; vz = -vz; } + + // Abweichungswinkel zur X-Achse [1,0,0] in Roboter-Koordinaten + // horizontal (XY-Ebene, Rotation um Z): positiv = nach Y (rückwärts) verschoben + // vertikal (XZ-Ebene, Rotation um Y): positiv = nach oben (+Z) verschoben + const degXY = Math.atan2(vy, vx) * 180 / Math.PI; + const degXZ = Math.atan2(vz, vx) * 180 / Math.PI; + + const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°'; + const good = Math.abs(degXY) < 0.5 && Math.abs(degXZ) < 0.5; + + vlog(`Bewegung: ⌀${dist.toFixed(2)} mm dir=[${vx.toFixed(4)}, ${vy.toFixed(4)}, ${vz.toFixed(4)}] (${n} Marker)`); + vlog(`Abw. von X-Achse: horizontal(XY) ${fmt(degXY)} vertikal(XZ) ${fmt(degXZ)}`, good ? 'ok' : 'warn'); + + // Parent informieren → aktiviert "X-Achse übernehmen"-Button + window.parent.postMessage({ + type: 'xaxis-measurement', + direction: [vx, vy, vz], + angleXY: degXY, + angleXZ: degXZ, + numMarkers: n, + distMm: dist, + }, '*'); + } else { + vlog(`Bewegung zu klein (${dist.toFixed(3)} mm) – Winkelberechnung übersprungen`, 'warn'); + // Messung ungültig → Parent mitteilen + window.parent.postMessage({ type: 'xaxis-measurement', direction: null }, '*'); + } + } } // ── Daten laden ─────────────────────────────────────────────────────────────── diff --git a/public/calibration.js b/public/calibration.js index f8beea6..dfda969 100644 --- a/public/calibration.js +++ b/public/calibration.js @@ -420,6 +420,80 @@ function initXAxis() { btn.disabled = false; } }); + + // ── X-Achse übernehmen ──────────────────────────────────────────────────── + // Empfängt postMessage aus dem eingebetteten boardViewer-iframe. + let _xaxisDirection = null; // zuletzt gemessene Richtung [vx,vy,vz] + + const adoptBtn = document.getElementById('btn-xaxis-adopt'); + + function onXaxisMessage(e) { + // Nachricht muss vom boardViewer-iframe stammen + const frame = document.getElementById('xaxis-viewer-frame'); + if (!frame || e.source !== frame.contentWindow) return; + const msg = e.data; + if (!msg || msg.type !== 'xaxis-measurement') return; + + if (Array.isArray(msg.direction)) { + _xaxisDirection = msg.direction; + const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°'; + logX(`📐 Messung empfangen: dir=[${msg.direction.map(v => v.toFixed(4)).join(', ')}]` + + ` XY=${fmt(msg.angleXY)} XZ=${fmt(msg.angleXZ)}` + + ` (${msg.numMarkers} Marker, Ø${msg.distMm.toFixed(1)} mm)`); + if (adoptBtn) { + adoptBtn.disabled = false; + adoptBtn.style.opacity = '1'; + adoptBtn.style.cursor = 'pointer'; + adoptBtn.title = `X-Achse übernehmen (dir=[${_xaxisDirection.map(v => v.toFixed(4)).join(', ')}])`; + } + } else { + // Ungültige / zu kleine Bewegung → Button sperren + _xaxisDirection = null; + if (adoptBtn) { + adoptBtn.disabled = true; + adoptBtn.style.opacity = '.45'; + adoptBtn.style.cursor = 'not-allowed'; + adoptBtn.title = 'Noch keine gültige Messung verfügbar'; + } + } + } + + window.addEventListener('message', onXaxisMessage); + + if (adoptBtn) { + adoptBtn.addEventListener('click', async () => { + if (!_xaxisDirection) return; + const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(4); + logX(`🔄 Übernehme X-Achse: dir=[${_xaxisDirection.map(fmt).join(', ')}] …`); + adoptBtn.disabled = true; + adoptBtn.style.opacity = '.45'; + try { + const r = await fetch('/api/robot/adopt-x-axis', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction: _xaxisDirection }), + }); + const data = await r.json(); + if (!r.ok) { + logX(`❌ Fehler: ${data.error ?? r.status}`); + return; + } + logX(`✅ X-Achse gespeichert — ${data.numChanged} Marker rotiert`); + logX(` Ursprung (A0-Schwerpunkt): [${data.origin.join(', ')}] mm`); + logX(` Neue X-Achse: [${data.newXAxis.join(', ')}]` + + ` Korr. XY=${(data.angleXYdeg >= 0 ? '+' : '') + data.angleXYdeg}°` + + ` XZ=${(data.angleXZdeg >= 0 ? '+' : '') + data.angleXZdeg}°`); + // Viewer neu laden damit die aktualisierten Positionen sichtbar werden + const frame = document.getElementById('xaxis-viewer-frame'); + if (frame?.contentWindow) frame.contentWindow.postMessage({ type: 'reload' }, '*'); + } catch (err) { + logX(`❌ Netzwerkfehler: ${err}`); + } finally { + adoptBtn.disabled = false; + adoptBtn.style.opacity = '1'; + } + }); + } } // ── Tab: Board (shared helpers) ─────────────────────────────────────────────── diff --git a/public/calibration_xaxis.html b/public/calibration_xaxis.html index 53572b4..b8cfaa5 100644 --- a/public/calibration_xaxis.html +++ b/public/calibration_xaxis.html @@ -45,6 +45,16 @@ Rechts ➡ +
+ + + Übernimmt die gemessene Richtung (aus Basis- und Vergleichs-Run) als X-Achse in robot.json + +
diff --git a/server/editRobot.js b/server/editRobot.js index 723942e..ced4116 100644 --- a/server/editRobot.js +++ b/server/editRobot.js @@ -389,3 +389,102 @@ export async function assignMarkerId(robotPath, { markerId, set, link, extraMark change: { action: 'added', markerId: id, oldLink: null, oldSet: '', newLink: link, newSet: set ?? '' }, }; } + +// ── Aktion 5: X-Achse übernehmen ───────────────────────────────────────────── + +/** + * Rotiert alle Marker-Positionen in robot.json so, dass die übergebene Richtung + * zur neuen X-Achse [1,0,0] wird. Rotation um den Schwerpunkt aller A0-Marker + * (Origin bleibt erhalten). + * + * direction: [vx, vy, vz] – gemessene Ist-X-Richtung in Roboter-Koordinaten + * + * Algorithmus: + * 1. Normalisiere direction, ggf. Vorzeichen so dass vx > 0 + * 2. Baue Orthonormalbasis: x_new = v, + * z_new = Gram-Schmidt([0,0,1] gegen v), + * y_new = cross(z_new, x_new) + * 3. Rotationsmatrix R (old→new): Zeilen = [x_new, y_new, z_new] + * 4. p_new = origin + R * (p_old − origin) + */ +export async function adoptXAxis(robotPath, { direction }) { + const [vx, vy, vz] = direction.map(Number); + const len = Math.sqrt(vx * vx + vy * vy + vz * vz); + if (len < 1e-9) throw new Error('Richtungsvektor zu klein (fast Null-Vektor).'); + + // Normalisieren, immer positive X-Komponente + let nx = vx / len, ny = vy / len, nz = vz / len; + if (nx < 0) { nx = -nx; ny = -ny; nz = -nz; } + + // ── Orthonormalbasis ────────────────────────────────────────────────────── + // Z_new: Gram-Schmidt von [0,0,1] gegen x_new + const dotZ = nz; // dot([0,0,1], [nx,ny,nz]) + let zx = -dotZ * nx, zy = -dotZ * ny, zz = 1 - dotZ * nz; + let zlen = Math.sqrt(zx * zx + zy * zy + zz * zz); + if (zlen < 1e-9) { + // Sonderfall: x_new fast parallel zu Z – Fallback auf [0,1,0] + const dotY = ny; + zx = -dotY * nx; zy = 1 - dotY * ny; zz = -dotY * nz; + zlen = Math.sqrt(zx * zx + zy * zy + zz * zz); + } + zx /= zlen; zy /= zlen; zz /= zlen; + + // Y_new = cross(z_new, x_new) [rechte-Hand-Regel: ẑ × x̂ = ŷ] + const yx = zy * nz - zz * ny; + const yy = zz * nx - zx * nz; + const yz = zx * ny - zy * nx; + + // Rotationsfunktion: p_rot = R * p (R hat Zeilen = neue Achsen in alten Koordinaten) + function rotVec(px, py, pz) { + return [ + nx * px + ny * py + nz * pz, // neue X-Komponente + yx * px + yy * py + yz * pz, // neue Y-Komponente + zx * px + zy * py + zz * pz, // neue Z-Komponente + ]; + } + + const robot = await readRobot(robotPath); + const links = robot.links ?? {}; + + // ── Ursprung: Schwerpunkt aller A0-Marker ──────────────────────────────── + const a0Pos = []; + for (const ld of Object.values(links)) { + for (const m of (ld.markers ?? [])) { + if (m.set === 'A0' && Array.isArray(m.position) && m.position.length >= 3) { + a0Pos.push(m.position.map(Number)); + } + } + } + let ox = 0, oy = 0, oz = 0; + if (a0Pos.length > 0) { + for (const [px, py, pz] of a0Pos) { ox += px; oy += py; oz += pz; } + ox /= a0Pos.length; oy /= a0Pos.length; oz /= a0Pos.length; + } + + // ── Alle Marker rotieren ────────────────────────────────────────────────── + let numChanged = 0; + for (const ld of Object.values(links)) { + for (const m of (ld.markers ?? [])) { + if (!Array.isArray(m.position) || m.position.length < 3) continue; + const [px, py, pz] = m.position.map(Number); + const [rx, ry, rz] = rotVec(px - ox, py - oy, pz - oz); + m.position = [ + Math.round((ox + rx) * 100) / 100, + Math.round((oy + ry) * 100) / 100, + Math.round((oz + rz) * 100) / 100, + ]; + numChanged++; + } + } + + robot.links = links; + await writeRobot(robotPath, robot); + + return { + numChanged, + origin: [ox, oy, oz].map(v => Math.round(v * 10) / 10), + newXAxis: [nx, ny, nz].map(v => Math.round(v * 10000) / 10000), + angleXYdeg: Math.round(Math.atan2(ny, nx) * 18000 / Math.PI) / 100, + angleXZdeg: Math.round(Math.atan2(nz, nx) * 18000 / Math.PI) / 100, + }; +} diff --git a/server/server.js b/server/server.js index 789000f..79d93e9 100755 --- a/server/server.js +++ b/server/server.js @@ -8,7 +8,7 @@ import { fileURLToPath } from 'url'; import process from 'process'; import { spawn } from 'child_process'; import { WebcamClient } from './webcamClient.js'; -import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId } from './editRobot.js'; +import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId, adoptXAxis } from './editRobot.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -864,6 +864,31 @@ app.post('/api/robot/assign-id', async (req, res) => { } }); +/** + * POST /api/robot/adopt-x-axis + * Dreht alle Marker-Positionen in robot.json so, dass die gemessene Richtung + * zur neuen X-Achse [1,0,0] wird. Rotation um den A0-Schwerpunkt. + * Body: { direction: [vx, vy, vz] } + */ +app.post('/api/robot/adopt-x-axis', async (req, res) => { + try { + const { direction } = req.body ?? {}; + if (!Array.isArray(direction) || direction.length < 3) { + return res.status(400).json({ error: '"direction" muss ein Array [vx,vy,vz] sein.' }); + } + const result = await adoptXAxis(ROBOT_JSON, { direction }); + console.log( + `robot/adopt-x-axis dir=[${direction.map(v => Number(v).toFixed(4)).join(', ')}]` + + ` → ${result.numChanged} Marker, Ursprung=[${result.origin.join(', ')}]` + + ` XY=${result.angleXYdeg}° XZ=${result.angleXZdeg}°`, + ); + return res.json(result); + } catch (err) { + console.error('robot/adopt-x-axis error:', err); + return res.status(500).json({ error: String(err) }); + } +}); + /** * POST /api/calibration/upload-npz * Liest {camera}_calibration.npz aus der aktuellen Session und