diff --git a/public/boardViewer.html b/public/boardViewer.html index d3d134c..c32d710 100644 --- a/public/boardViewer.html +++ b/public/boardViewer.html @@ -159,7 +159,9 @@ Erkannt (nur 2D) Gemessen (3b) Fremd (3b) - Vergleich + Pos B + Pos C + Kreismittelpkt. Kamera @@ -172,12 +174,18 @@ - Vergleich + Pos B (nur fremd) + Pos C + (Y-Achse) + +
@@ -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 (bary­zentrischer 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 = '' + runs5.map(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 = '' + runs10.map(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 = '' + + runs10.map(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 @@
-

Arm1 / Arm2 offen

-
- Ziel: Nullposition und Kinematikparameter von Arm1 und Arm2 einmessen. - Arm fährt in mechanische Nullposition, Kamera prüft die tatsächliche Pose, - Offset wird berechnet und gespeichert.

- Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto → - Winkel · Offset-Korrektur speichern.

- Aktionen werden ergänzt sobald das Konzept feststeht. +

Arm1 – Y-Achse offen

+
+ Ziel + + Y-Rotationsachse von Arm1 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen. + + Ablauf + + Board erkennen (Pos A) → Arm1 drehen → Board erkennen (Pos B) → Arm1 drehen → Board erkennen (Pos C) + → Viewer zeigt berechnete Rotationsachse (magenta) + + Letzter Run +
-
- - - - - - +
+

Ausgabe / Log

- + +
+ +
+

Board-Viewer

+

+ Pos A (Basis) · Pos B (orange) · Pos C (cyan) – + alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet. +

+
diff --git a/public/calibration_arm2.html b/public/calibration_arm2.html new file mode 100644 index 0000000..c8df57c --- /dev/null +++ b/public/calibration_arm2.html @@ -0,0 +1,42 @@ +
+ +
+

Arm2 – Z-Achse offen

+
+ Ziel + + Z-Rotationsachse von Arm2 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen. + + Ablauf + + Board erkennen (Pos A) → Arm2 drehen → Board erkennen (Pos B) → Arm2 drehen → Board erkennen (Pos C) + → Viewer zeigt berechnete Rotationsachse (magenta) + + Letzter Run + +
+
+ +
+
+ +
+

Ausgabe / Log

+ +
+ +
+

Board-Viewer

+

+ Pos A (Basis) · Pos B (orange) · Pos C (cyan) – + alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet. +

+ +
+ +
diff --git a/public/calibration_board.html b/public/calibration_board.html index c74e13d..913c485 100644 --- a/public/calibration_board.html +++ b/public/calibration_board.html @@ -140,7 +140,7 @@

diff --git a/public/calibration_elbow.html b/public/calibration_elbow.html new file mode 100644 index 0000000..ab7c9f9 --- /dev/null +++ b/public/calibration_elbow.html @@ -0,0 +1,42 @@ +
+ +
+

Elbow – Rotation offen

+
+ Ziel + + Rotationsachse des Elbow-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen. + + Ablauf + + Board erkennen (Pos A) → Elbow drehen → Board erkennen (Pos B) → Elbow drehen → Board erkennen (Pos C) + → Viewer zeigt berechnete Rotationsachse (magenta) + + Letzter Run + +
+
+ +
+
+ +
+

Ausgabe / Log

+ +
+ +
+

Board-Viewer

+

+ Pos A (Basis) · Pos B (orange) · Pos C (cyan) – + alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet. +

+ +
+ +
diff --git a/public/calibration_hand.html b/public/calibration_hand.html new file mode 100644 index 0000000..ffe7ccd --- /dev/null +++ b/public/calibration_hand.html @@ -0,0 +1,42 @@ +
+ +
+

Hand – Rotation offen

+
+ Ziel + + Rotationsachse des Hand-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen. + + Ablauf + + Board erkennen (Pos A) → Hand drehen → Board erkennen (Pos B) → Hand drehen → Board erkennen (Pos C) + → Viewer zeigt berechnete Rotationsachse (magenta) + + Letzter Run + +
+
+ +
+
+ +
+

Ausgabe / Log

+ +
+ +
+

Board-Viewer

+

+ Pos A (Basis) · Pos B (orange) · Pos C (cyan) – + alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet. +

+ +
+ +
diff --git a/public/calibration_xaxis.html b/public/calibration_xaxis.html index 8393eca..661c8dd 100644 --- a/public/calibration_xaxis.html +++ b/public/calibration_xaxis.html @@ -71,7 +71,7 @@