@@ -229,13 +237,16 @@ const gPaper = new THREE.Group(); // weißes A0-Papier
const gMarkers = new THREE.Group(); // Modell-Rechtecke
const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
const gCameras = new THREE.Group(); // Kamera-Frusta
-const gCompare = new THREE.Group(); // Vergleichs-Punkte (anderer Timestamp, nur fremd)
-const gCompareLines = new THREE.Group(); // Verbindungslinien Basis↔Vergleich
-scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines);
+const gCompare = new THREE.Group(); // Pos B Marker (nur fremd, orange)
+const gCompareLines = new THREE.Group(); // Verbindungslinien Pos A↔Pos B
+const gPositionC = new THREE.Group(); // Pos C Marker (nur fremd, cyan)
+const gYAxis = new THREE.Group(); // Y-Achse Visualisierung (Kreismittelpunkte, Achse)
+scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines, gPositionC, gYAxis);
-// ── Zustand für Vergleichs-Linien ─────────────────────────────────────────────
-let _primaryFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
-let _compareFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
+// ── Zustand für Positionen ────────────────────────────────────────────────────
+let _primaryFremdMarkers = []; // Pos A – [{marker_id, position_mm, num_cameras}]
+let _compareFremdMarkers = []; // Pos B – [{marker_id, position_mm, num_cameras}]
+let _positionCFremdMarkers = []; // Pos C – [{marker_id, position_mm, num_cameras}]
// ── Viewer-interner Logger ────────────────────────────────────────────────────
function vlog(msg, kind = '') {
@@ -663,13 +674,19 @@ function buildCompareLines() {
const noMatch = onlyPrimary.filter(id => !matchedIds.includes(id));
const parts = [`${matchedIds.length} Linien`];
if (matchedIds.length) parts.push(`IDs: ${matchedIds.join(' ')}`);
- if (onlyCompare.length) parts.push(`nur Vergleich: ${onlyCompare.join(' ')}`);
- if (noMatch.length) parts.push(`nur Basis: ${noMatch.join(' ')}`);
+ if (onlyCompare.length) parts.push(`nur Pos B: ${onlyCompare.join(' ')}`);
+ if (noMatch.length) parts.push(`nur Pos A: ${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)
+ // ── Bewegungsanalyse ──────────────────────────────────────────────────────────
+ // Pos C aktiv → Bogen-Modus (Y-Achse), keine lineare X-Achsen-Analyse
+ const posC_active = !!document.getElementById('sel-run-c')?.value;
+
+ if (posC_active) {
+ vlog('Pos C aktiv → Bogen-Modus, X-Achsen-Analyse übersprungen');
+ window.parent.postMessage({ type: 'xaxis-measurement', direction: null }, '*');
+ } else if (matchedIds.length > 0) {
+ // ── Lineare X-Achsen-Analyse (mittlerer Verschiebungsvektor) ──────────────
let sx = 0, sy = 0, sz = 0;
for (const cm of _compareFremdMarkers) {
const pm = primaryMap.get(cm.marker_id);
@@ -699,7 +716,6 @@ function buildCompareLines() {
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],
@@ -710,10 +726,129 @@ function buildCompareLines() {
}, '*');
} 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 }, '*');
}
}
+
+ // Y-Achse neu berechnen (nutzt _positionCFremdMarkers – kein-op wenn leer)
+ computeAndShowYAxis();
+}
+
+// ── Y-Achsen-Berechnung aus drei Positionen ───────────────────────────────────
+
+function computeAndShowYAxis() {
+ clearGroup(gYAxis);
+
+ // Nur aktiv wenn alle drei Positionen vorliegen
+ if (_positionCFremdMarkers.length === 0) return;
+ if (_primaryFremdMarkers.length === 0) return;
+ if (_compareFremdMarkers.length === 0) {
+ vlog('Y-Achse: Pos B nicht gewählt – kein Vergleich aktiv', 'warn');
+ return;
+ }
+
+ const mapA = new Map(_primaryFremdMarkers .map(m => [m.marker_id, m]));
+ const mapB = new Map(_compareFremdMarkers .map(m => [m.marker_id, m]));
+ const mapC = new Map(_positionCFremdMarkers.map(m => [m.marker_id, m]));
+
+ function dist2(P, Q) { return (P[0]-Q[0])**2 + (P[1]-Q[1])**2 + (P[2]-Q[2])**2; }
+
+ const circumcenters = []; // [{id, C:[x,y,z]}] in mm
+ const normals = []; // [[nx,ny,nz]] – Achsenrichtung je Marker
+
+ for (const [id, ma] of mapA) {
+ const mb = mapB.get(id);
+ const mc = mapC.get(id);
+ if (!mb || !mc) continue;
+
+ const P1 = ma.position_mm.map(Number);
+ const P2 = mb.position_mm.map(Number);
+ const P3 = mc.position_mm.map(Number);
+
+ // Normalenvektor der Kreisebene = Achsenrichtung
+ const v1 = [P2[0]-P1[0], P2[1]-P1[1], P2[2]-P1[2]];
+ const v2 = [P3[0]-P1[0], P3[1]-P1[1], P3[2]-P1[2]];
+ const cross = [
+ v1[1]*v2[2] - v1[2]*v2[1],
+ v1[2]*v2[0] - v1[0]*v2[2],
+ v1[0]*v2[1] - v1[1]*v2[0],
+ ];
+ const crossLen = Math.sqrt(cross[0]**2 + cross[1]**2 + cross[2]**2);
+ if (crossLen < 1e-3) {
+ vlog(`Y-Achse: Marker ${id} degenerat (Punkte zu nahe / kollinear)`, 'warn');
+ continue;
+ }
+ const n = cross.map(c => c / crossLen);
+ normals.push(n);
+
+ // Umkreismittelpunkt (baryzentrischer Ansatz)
+ const a2 = dist2(P2, P3), b2 = dist2(P1, P3), c2 = dist2(P1, P2);
+ const w1 = a2*(b2+c2-a2), w2 = b2*(a2+c2-b2), w3 = c2*(a2+b2-c2);
+ const wSum = w1 + w2 + w3;
+ if (Math.abs(wSum) < 1e-6) {
+ vlog(`Y-Achse: Marker ${id} – Umkreis undefiniert`, 'warn');
+ continue;
+ }
+ const C = [
+ (w1*P1[0] + w2*P2[0] + w3*P3[0]) / wSum,
+ (w1*P1[1] + w2*P2[1] + w3*P3[1]) / wSum,
+ (w1*P1[2] + w2*P2[2] + w3*P3[2]) / wSum,
+ ];
+ circumcenters.push({ id, C });
+
+ // Kreismittelpunkt (rose)
+ gYAxis.add(makeSphere(r2vArr(C), 0.007, 0xfb7185));
+ // Bogen-Linie B→C (cyan)
+ gYAxis.add(makeLine(r2vArr(P2), r2vArr(P3), 0x22d3ee, 0.6));
+ }
+
+ if (circumcenters.length === 0) {
+ vlog('Y-Achse: Keine gemeinsamen fremd-Marker in Pos A+B+C gefunden', 'warn');
+ window.parent.postMessage({ type: 'yaxis-measurement', axisDir: null }, '*');
+ return;
+ }
+
+ // Achsenrichtung: Mittlere Normale (Vorzeichen angleichen)
+ const ref = normals[0];
+ const aligned = normals.map(n => {
+ const dot = n[0]*ref[0] + n[1]*ref[1] + n[2]*ref[2];
+ return dot >= 0 ? n : n.map(c => -c);
+ });
+ const meanN = [0, 1, 2].map(i => aligned.reduce((s, n) => s + n[i], 0) / aligned.length);
+ const meanNLen = Math.sqrt(meanN[0]**2 + meanN[1]**2 + meanN[2]**2);
+ const axisDir = meanN.map(c => c / meanNLen); // Roboter-Koordinaten
+
+ // Referenzpunkt: Schwerpunkt der Umkreismittelpunkte
+ const axisPoint = [0, 1, 2].map(i =>
+ circumcenters.reduce((s, c) => s + c.C[i], 0) / circumcenters.length
+ );
+
+ // Achse als Linie ±500 mm visualisieren (magenta)
+ const L = 500;
+ const p1mm = axisPoint.map((v, i) => v - L * axisDir[i]);
+ const p2mm = axisPoint.map((v, i) => v + L * axisDir[i]);
+ gYAxis.add(makeLine(r2vArr(p1mm), r2vArr(p2mm), 0xe879f9, 0.9));
+ gYAxis.add(makeSphere(r2vArr(axisPoint), 0.011, 0xe879f9));
+
+ // Abweichung von der idealen Y-Achse [0,1,0] in Roboter-Koordinaten
+ const [ax, ay, az] = axisDir;
+ const tiltXY = Math.atan2(ax, ay) * 180 / Math.PI; // Kippung in XY-Ebene
+ const tiltYZ = Math.atan2(az, ay) * 180 / Math.PI; // Kippung in YZ-Ebene
+ const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
+ const good = Math.abs(tiltXY) < 0.5 && Math.abs(tiltYZ) < 0.5;
+
+ vlog(`Y-Achse (${circumcenters.length} Marker): dir=[${axisDir.map(v => v.toFixed(4)).join(', ')}]`);
+ vlog(` Referenzpunkt: [${axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
+ vlog(` Abw. von Y-Achse: XY ${fmt(tiltXY)} YZ ${fmt(tiltYZ)}`, good ? 'ok' : 'warn');
+
+ window.parent.postMessage({
+ type: 'yaxis-measurement',
+ axisDir,
+ axisPoint,
+ tiltXY,
+ tiltYZ,
+ numMarkers: circumcenters.length,
+ }, '*');
}
// ── Daten laden ───────────────────────────────────────────────────────────────
@@ -778,11 +913,44 @@ async function loadCompareData() {
}
vlog(`Vergleich: ${markers.length} gesamt fremd=${_compareFremdMarkers.length} boardIDs=${boardIds.size}` +
(_compareFremdMarkers.length ? ` (${_compareFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
- } catch (err) { vlog(`Vergleich Fehler: ${err}`, 'err'); }
- buildCompareLines(); // Linien + Transparenz aktualisieren
+ } catch (err) { vlog(`Pos B Fehler: ${err}`, 'err'); }
+ buildCompareLines(); // Linien + Transparenz + Y-Achse aktualisieren
}
-/** Run-Listen laden und beide Dropdowns befüllen. */
+/**
+ * Pos C laden.
+ * Zeigt nur fremd-triangulierte Marker als cyan Kugeln.
+ * Danach wird Y-Achse neu berechnet (computeAndShowYAxis).
+ */
+async function loadPositionC() {
+ clearGroup(gPositionC);
+ _positionCFremdMarkers = [];
+ const selRun = document.getElementById('sel-run-c')?.value ?? '';
+ if (!selRun) {
+ vlog('Pos C: nicht gewählt');
+ computeAndShowYAxis(); // räumt gYAxis auf
+ return;
+ }
+ vlog(`Pos C: lade ${selRun} …`);
+ try {
+ const r = await fetch(`/api/board/latest?run=${encodeURIComponent(selRun)}`);
+ if (!r.ok) { vlog(`Pos C: HTTP ${r.status}`, 'err'); computeAndShowYAxis(); return; }
+ const data = await r.json();
+ const markers = data.measuredMarkers?.markers ?? [];
+ const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
+ for (const m of markers) {
+ if (!boardIds.has(m.marker_id)) {
+ _positionCFremdMarkers.push(m);
+ gPositionC.add(makeSphere(r2vArr(m.position_mm), 0.006, 0x22d3ee)); // cyan
+ }
+ }
+ vlog(`Pos C: ${markers.length} gesamt fremd=${_positionCFremdMarkers.length}` +
+ (_positionCFremdMarkers.length ? ` (${_positionCFremdMarkers.map(m => m.marker_id).join(' ')})` : ''));
+ } catch (err) { vlog(`Pos C Fehler: ${err}`, 'err'); }
+ computeAndShowYAxis();
+}
+
+/** Run-Listen laden und alle Dropdowns befüllen. */
async function initRunSelectors() {
try {
const [r5, r10] = await Promise.all([
@@ -800,28 +968,50 @@ async function initRunSelectors() {
selP.innerHTML = '
⟳ aktuellster ' +
runs5.map(r => `
${r} `).join('');
}
+ // ── Default-Verhalten per URL-Param steuern ──────────────────────────────
+ // ?defaults=a → nur Pos A bekommt Default (Board-Tab, Y-Achse-Tab)
+ // ?defaults=ab → Pos A + Pos B (X-Achsen-Tab)
+ // ?defaults=abc → alle drei (Arm-Tab)
+ // kein Param → wie "ab" (Rückwärtskompatibilität)
+ // ?compare=none → wie "a" (Rückwärtskompatibilität)
+ const params = new URLSearchParams(window.location.search);
+ const rawDef = params.get('defaults')
+ ?? (params.get('compare') === 'none' ? 'a' : 'ab');
+ const defB = rawDef.includes('b');
+ const defC = rawDef.includes('c');
+
if (selC) {
- // URL-Param ?compare=none → kein Default-Vergleich (Board-Tab)
- // Ohne den Param → zweiten neuesten Run vorwählen (X-Achsen-Tab)
- const noCompare = new URLSearchParams(window.location.search).get('compare') === 'none';
const prevCompare = selC.value;
selC.innerHTML = '
– keiner – ' +
runs10.map(r => `
${r} `).join('');
if (prevCompare) {
- selC.value = prevCompare; // bisher gewählten behalten
- } else if (!noCompare && runs10.length >= 2) {
- selC.value = runs10[1]; // zweiter neuester als Default (X-Achse)
+ selC.value = prevCompare; // bisher gewählten behalten
+ } else if (defB && runs10.length >= 2) {
+ selC.value = runs10[1]; // zweiter neuester als Default
+ }
+ }
+
+ // ── Pos C ──
+ const selRunC = document.getElementById('sel-run-c');
+ if (selRunC) {
+ const prevC = selRunC.value;
+ selRunC.innerHTML = '
– keiner – ' +
+ runs10.map(r => `
${r} `).join('');
+ if (prevC) {
+ selRunC.value = prevC; // bisher gewählten behalten
+ } else if (defC && runs10.length >= 3) {
+ selRunC.value = runs10[2]; // dritter neuester als Default
}
- // Andernfalls bleibt "– keiner –" aktiv (Board-Tab mit ?compare=none)
}
} catch { /* offline oder noch keine Runs */ }
}
-/** Vollständige Initialisierung: Selektoren → Basis → Vergleich (sequenziell!) */
+/** Vollständige Initialisierung: Selektoren → Pos A → Pos B → Pos C (sequenziell!) */
async function initAll() {
await initRunSelectors();
- await loadData(); // setzt _primaryFremdMarkers
- await loadCompareData(); // setzt _compareFremdMarkers + baut Linien
+ await loadData(); // setzt _primaryFremdMarkers
+ await loadCompareData(); // setzt _compareFremdMarkers + baut Linien + Y-Achse (no-op)
+ await loadPositionC(); // setzt _positionCFremdMarkers + berechnet Y-Achse
}
initAll();
@@ -829,8 +1019,12 @@ document.getElementById('btnReload').addEventListener('click', initAll);
document.getElementById('sel-run-primary')?.addEventListener('change', async () => {
await loadData();
await loadCompareData();
+ await loadPositionC();
});
-document.getElementById('sel-run-compare')?.addEventListener('change', loadCompareData);
+document.getElementById('sel-run-compare')?.addEventListener('change', async () => {
+ await loadCompareData(); // baut Linien + ruft computeAndShowYAxis auf
+});
+document.getElementById('sel-run-c')?.addEventListener('change', loadPositionC);
window.addEventListener('message', async (e) => {
if (e.data?.type === 'reload') await initAll();
});
diff --git a/public/calibration.html b/public/calibration.html
index 055902b..61909ff 100644
--- a/public/calibration.html
+++ b/public/calibration.html
@@ -22,8 +22,11 @@
@@ -31,7 +34,10 @@
-
+
+
+
+
diff --git a/public/calibration.js b/public/calibration.js
index dfda969..9df55c0 100644
--- a/public/calibration.js
+++ b/public/calibration.js
@@ -21,6 +21,10 @@ async function loadPanel(tab, src) {
if (tab === 'camera-npz') initCameraNpz();
else if (tab === 'board') initBoard();
else if (tab === 'robot-x-axis') initXAxis();
+ else if (tab === 'arm1') initArm('arm1');
+ else if (tab === 'arm2') initArm('arm2');
+ else if (tab === 'elbow') initArm('elbow');
+ else if (tab === 'hand') initArm('hand');
} catch (err) {
document.getElementById('tab-' + tab).innerHTML =
@@ -496,6 +500,75 @@ function initXAxis() {
}
}
+// ── Tabs: Arm1 / Arm2 / Elbow / Hand (generisch) ────────────────────────────
+// Alle Gelenk-Tabs teilen dieselbe Init-Logik – der Tab-Name (arm1, arm2, …)
+// wird als Prefix für Element-IDs und Viewer-Frame-ID genutzt.
+
+function initArm(tab) {
+ const logEl = document.getElementById(`log-${tab}`);
+ const frameEl = document.getElementById(`${tab}-viewer-frame`);
+ const runBtn = document.getElementById(`btn-${tab}-run`);
+ const lastRunEl = document.getElementById(`${tab}-last-run`);
+
+ if (!logEl) return; // Panel noch nicht geladen
+
+ function log(msg) {
+ const ts = new Date().toLocaleTimeString('de-CH');
+ logEl.value += `[${ts}] ${msg}\n`;
+ logEl.scrollTop = logEl.scrollHeight;
+ }
+
+ // "Board erkennen"-Button
+ if (runBtn) {
+ runBtn.addEventListener('click', async () => {
+ log('Board-Erkennung …');
+ runBtn.disabled = true;
+ try {
+ const response = await fetch('/api/board/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({}),
+ });
+ if (!response.ok) {
+ const raw = await response.text().catch(() => '');
+ let msg;
+ try { msg = JSON.parse(raw).error || raw; }
+ catch { msg = raw.slice(0, 300) || `HTTP ${response.status}`; }
+ log(`❌ HTTP ${response.status}: ${msg}`);
+ return;
+ }
+ await readSseStream(response, log, (evt) => {
+ if (evt.exitCode === 0) {
+ log('✅ Board-Run abgeschlossen.');
+ if (evt.runDir) {
+ if (lastRunEl) lastRunEl.textContent = evt.runDir;
+ if (frameEl?.contentWindow) frameEl.contentWindow.postMessage({ type: 'reload' }, '*');
+ }
+ } else {
+ log(`❌ Beendet mit Exit-Code ${evt.exitCode}`);
+ }
+ });
+ } catch (err) {
+ log(`❌ Fehler: ${err}`);
+ } finally {
+ runBtn.disabled = false;
+ }
+ });
+ }
+
+ // Achsen-Messung vom Viewer empfangen
+ window.addEventListener('message', (e) => {
+ if (!frameEl || e.source !== frameEl.contentWindow) return;
+ const msg = e.data;
+ if (msg?.type === 'yaxis-measurement' && Array.isArray(msg.axisDir)) {
+ const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°';
+ log(`📐 Achse (${msg.numMarkers} Marker): dir=[${msg.axisDir.map(v => v.toFixed(4)).join(', ')}]` +
+ ` XY=${fmt(msg.tiltXY)} YZ=${fmt(msg.tiltYZ)}`);
+ log(` Referenzpunkt: [${msg.axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`);
+ }
+ });
+}
+
// ── Tab: Board (shared helpers) ───────────────────────────────────────────────
/** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */
diff --git a/public/calibration_arm.html b/public/calibration_arm.html
index 91e599d..fb97732 100644
--- a/public/calibration_arm.html
+++ b/public/calibration_arm.html
@@ -1,28 +1,42 @@