rotation vorbereitung

This commit is contained in:
chk
2026-06-10 21:48:44 +02:00
parent 560a4823d4
commit e0e4212a90
9 changed files with 463 additions and 50 deletions

View File

@@ -159,7 +159,9 @@
<span><span class="dot circle" style="background:#dde3ec"></span>Erkannt (nur 2D)</span>
<span><span class="dot circle" style="background:#fbbf24"></span>Gemessen (3b)</span>
<span><span class="dot circle" style="background:#3b82f6"></span>Fremd (3b)</span>
<span><span class="dot circle" style="background:#f97316"></span>Vergleich</span>
<span><span class="dot circle" style="background:#f97316"></span>Pos B</span>
<span><span class="dot circle" style="background:#22d3ee"></span>Pos C</span>
<span><span class="dot circle" style="background:#fb7185"></span>Kreismittelpkt.</span>
<span><span class="dot" style="background:#9b7bff"></span>Kamera</span>
</div>
<span id="stats"></span>
@@ -172,12 +174,18 @@
<select id="sel-run-primary" class="btn" style="min-width:158px">
<option value="">⟳ aktuellster</option>
</select>
<span class="run-lbl" style="margin-left:10px">Vergleich
<span class="run-lbl" style="margin-left:10px">Pos B
<span style="text-transform:none;opacity:.7;font-size:9px">(nur fremd)</span>
</span>
<select id="sel-run-compare" class="btn" style="min-width:158px">
<option value=""> keiner </option>
</select>
<span class="run-lbl" style="margin-left:10px">Pos C
<span style="text-transform:none;opacity:.7;font-size:9px">(Y-Achse)</span>
</span>
<select id="sel-run-c" class="btn" style="min-width:158px">
<option value=""> keiner </option>
</select>
</div>
<div id="canvas-wrap">
@@ -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 = '<option value="">⟳ aktuellster</option>' +
runs5.map(r => `<option value="${r}"${r === cur ? ' selected' : ''}>${r}</option>`).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 = '<option value=""> keiner </option>' +
runs10.map(r => `<option value="${r}">${r}</option>`).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 = '<option value=""> keiner </option>' +
runs10.map(r => `<option value="${r}">${r}</option>`).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();
});

View File

@@ -22,8 +22,11 @@
<nav class="tab-sidebar" id="tabSidebar">
<button class="tab-btn active" data-tab="camera-npz" data-src="/calibration_camera.html">Camera NPZ</button>
<button class="tab-btn" data-tab="board" data-src="/calibration_board.html">Board</button>
<button class="tab-btn" data-tab="robot-x-axis" data-src="/calibration_xaxis.html">Robot X Axis</button>
<button class="tab-btn" data-tab="arm" data-src="/calibration_arm.html">Arm1 / Arm2</button>
<button class="tab-btn" data-tab="robot-x-axis" data-src="/calibration_xaxis.html">Robot X Axis</button>
<button class="tab-btn" data-tab="arm1" data-src="/calibration_arm.html">Arm1 Y</button>
<button class="tab-btn" data-tab="arm2" data-src="/calibration_arm2.html">Arm2 Z</button>
<button class="tab-btn" data-tab="elbow" data-src="/calibration_elbow.html">Elbow</button>
<button class="tab-btn" data-tab="hand" data-src="/calibration_hand.html">Hand</button>
</nav>
<!-- CONTENT (Panels werden lazy per fetch befüllt) -->
@@ -31,7 +34,10 @@
<div class="tab-panel active" id="tab-camera-npz"></div>
<div class="tab-panel" id="tab-board"></div>
<div class="tab-panel" id="tab-robot-x-axis"></div>
<div class="tab-panel" id="tab-arm"></div>
<div class="tab-panel" id="tab-arm1"></div>
<div class="tab-panel" id="tab-arm2"></div>
<div class="tab-panel" id="tab-elbow"></div>
<div class="tab-panel" id="tab-hand"></div>
</div>
</div><!-- /.calib-body -->

View File

@@ -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 */

View File

@@ -1,28 +1,42 @@
<div class="sections">
<div class="section full">
<h2>Arm1 / Arm2 <span class="status-badge open">offen</span></h2>
<div class="placeholder-note">
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.<br><br>
Geplante Aktionen: Arm1 → Nullpos → Foto → Winkel · Arm2 → Nullpos → Foto →
Winkel · Offset-Korrektur speichern.<br><br>
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
<h2>Arm1 Y-Achse <span class="status-badge open">offen</span></h2>
<div class="info-grid" style="margin-top:14px">
<span class="info-label">Ziel</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Y-Rotationsachse von Arm1 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
</span>
<span class="info-label">Ablauf</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Board erkennen (Pos A) → Arm1 drehen → Board erkennen (Pos B) → Arm1 drehen → Board erkennen (Pos C)
→ Viewer zeigt berechnete Rotationsachse (magenta)
</span>
<span class="info-label">Letzter Run</span>
<span class="info-value" id="arm1-last-run"></span>
</div>
<div class="controls" style="margin-top: 14px;">
<button disabled>Arm1 → Nullpos</button>
<button disabled>Foto Arm1</button>
<button disabled>Arm2 → Nullpos</button>
<button disabled>Foto Arm2</button>
<button disabled>Offsets berechnen</button>
<button disabled>Speichern</button>
<div class="controls" style="margin-top:16px">
<button id="btn-arm1-run">Board erkennen</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-arm" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
<textarea id="log-arm1" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
<div class="section full">
<h2>Board-Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan)
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
</p>
<iframe
id="arm1-viewer-frame"
src="/boardViewer.html?defaults=abc"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (Arm1)"
></iframe>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<div class="sections">
<div class="section full">
<h2>Arm2 Z-Achse <span class="status-badge open">offen</span></h2>
<div class="info-grid" style="margin-top:14px">
<span class="info-label">Ziel</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Z-Rotationsachse von Arm2 bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
</span>
<span class="info-label">Ablauf</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Board erkennen (Pos A) → Arm2 drehen → Board erkennen (Pos B) → Arm2 drehen → Board erkennen (Pos C)
→ Viewer zeigt berechnete Rotationsachse (magenta)
</span>
<span class="info-label">Letzter Run</span>
<span class="info-value" id="arm2-last-run"></span>
</div>
<div class="controls" style="margin-top:16px">
<button id="btn-arm2-run">Board erkennen</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-arm2" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
<div class="section full">
<h2>Board-Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan)
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
</p>
<iframe
id="arm2-viewer-frame"
src="/boardViewer.html?defaults=abc"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (Arm2)"
></iframe>
</div>
</div>

View File

@@ -140,7 +140,7 @@
</p>
<iframe
id="board-viewer-frame"
src="/boardViewer.html?compare=none"
src="/boardViewer.html?defaults=a"
style="width: 100%; height: 740px; border: 1px solid #334155; border-radius: 6px; background: #0d0f13; display: block;"
title="Board-Viewer"
></iframe>

View File

@@ -0,0 +1,42 @@
<div class="sections">
<div class="section full">
<h2>Elbow Rotation <span class="status-badge open">offen</span></h2>
<div class="info-grid" style="margin-top:14px">
<span class="info-label">Ziel</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Rotationsachse des Elbow-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
</span>
<span class="info-label">Ablauf</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Board erkennen (Pos A) → Elbow drehen → Board erkennen (Pos B) → Elbow drehen → Board erkennen (Pos C)
→ Viewer zeigt berechnete Rotationsachse (magenta)
</span>
<span class="info-label">Letzter Run</span>
<span class="info-value" id="elbow-last-run"></span>
</div>
<div class="controls" style="margin-top:16px">
<button id="btn-elbow-run">Board erkennen</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-elbow" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
<div class="section full">
<h2>Board-Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan)
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
</p>
<iframe
id="elbow-viewer-frame"
src="/boardViewer.html?defaults=abc"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (Elbow)"
></iframe>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<div class="sections">
<div class="section full">
<h2>Hand Rotation <span class="status-badge open">offen</span></h2>
<div class="info-grid" style="margin-top:14px">
<span class="info-label">Ziel</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Rotationsachse des Hand-Gelenks bestimmen: drei Positionen aufnehmen, Umkreismittelpunkte berechnen.
</span>
<span class="info-label">Ablauf</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Board erkennen (Pos A) → Hand drehen → Board erkennen (Pos B) → Hand drehen → Board erkennen (Pos C)
→ Viewer zeigt berechnete Rotationsachse (magenta)
</span>
<span class="info-label">Letzter Run</span>
<span class="info-value" id="hand-last-run"></span>
</div>
<div class="controls" style="margin-top:16px">
<button id="btn-hand-run">Board erkennen</button>
</div>
</div>
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-hand" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
<div class="section full">
<h2>Board-Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
<strong>Pos A</strong> (Basis) · <strong>Pos B</strong> (orange) · <strong>Pos C</strong> (cyan)
alle drei Timestamps sind vorgewählt. Sobald Pos C gesetzt ist, wird die Rotationsachse berechnet.
</p>
<iframe
id="hand-viewer-frame"
src="/boardViewer.html?defaults=abc"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (Hand)"
></iframe>
</div>
</div>

View File

@@ -71,7 +71,7 @@
</p>
<iframe
id="xaxis-viewer-frame"
src="/boardViewer.html"
src="/boardViewer.html?defaults=ab"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (X-Achse)"
></iframe>