x-axis justierung: visualize

This commit is contained in:
chk
2026-06-10 18:30:16 +02:00
parent b1950ffa5a
commit 3084324f4a
4 changed files with 255 additions and 31 deletions

View File

@@ -51,6 +51,23 @@
font-size: 11px;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
#run-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
background: var(--panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
flex-wrap: wrap;
}
.run-lbl {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .04em;
white-space: nowrap;
}
#canvas-wrap { flex: 1; min-height: 360px; position: relative; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; }
#hint {
@@ -127,6 +144,7 @@
<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" style="background:#9b7bff"></span>Kamera</span>
</div>
<span id="stats"></span>
@@ -134,6 +152,19 @@
<button class="btn" id="btnReload"></button>
</div>
<div id="run-bar">
<span class="run-lbl">Basis</span>
<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 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>
</div>
<div id="canvas-wrap">
<canvas id="cv"></canvas>
<div id="hint">Orbit · Scroll · Rechte Taste = Pan</div>
@@ -182,7 +213,8 @@ 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
scene.add(gPaper, gMarkers, gMeasured, gCameras);
const gCompare = new THREE.Group(); // Vergleichs-Punkte (anderer Timestamp, nur fremd)
scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare);
function clearGroup(g) {
while (g.children.length) {
@@ -542,11 +574,17 @@ function buildTable(data) {
}
// ── Daten laden ───────────────────────────────────────────────────────────────
/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Run. */
async function loadData() {
const statusEl = document.getElementById('status');
const statusEl = document.getElementById('status');
statusEl.textContent = 'Laden …';
const selRun = document.getElementById('sel-run-primary')?.value ?? '';
const url = selRun
? `/api/board/latest?run=${encodeURIComponent(selRun)}`
: '/api/board/latest';
try {
const r = await fetch('/api/board/latest');
const r = await fetch(url);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
if (!data.runDir) {
@@ -564,10 +602,63 @@ async function loadData() {
}
}
loadData();
document.getElementById('btnReload').addEventListener('click', loadData);
/** Vergleichs-Run laden (Compare-Dropdown) zeigt nur fremd-triangulierte Marker als orange Kugeln. */
async function loadCompareData() {
clearGroup(gCompare);
const selRun = document.getElementById('sel-run-compare')?.value ?? '';
if (!selRun) return;
try {
const r = await fetch(`/api/board/latest?run=${encodeURIComponent(selRun)}`);
if (!r.ok) return;
const data = await r.json();
const markers = data.measuredMarkers?.markers ?? [];
if (!markers.length) return;
// Board-Marker-IDs aus Robot.json (für diesen Run)
const boardIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
for (const m of markers) {
if (!boardIds.has(m.marker_id)) {
// Nicht zugeordnet → orange Kugel (Vergleich)
gCompare.add(makeSphere(r2vArr(m.position_mm), 0.006, 0xf97316));
}
}
} catch { /* kein 3b-Output für diesen Run */ }
}
/** Run-Listen laden und beide Dropdowns befüllen. */
async function initRunSelectors() {
try {
const [r5, r10] = await Promise.all([
fetch('/api/board/runs?limit=5'),
fetch('/api/board/runs?limit=10'),
]);
const { runs: runs5 } = r5.ok ? await r5.json() : { runs: [] };
const { runs: runs10 } = r10.ok ? await r10.json() : { runs: [] };
const selP = document.getElementById('sel-run-primary');
const selC = document.getElementById('sel-run-compare');
const cur = selP?.value ?? ''; // aktuell gewählten Run behalten
if (selP) {
selP.innerHTML = '<option value="">⟳ aktuellster</option>' +
runs5.map(r => `<option value="${r}"${r === cur ? ' selected' : ''}>${r}</option>`).join('');
}
if (selC) {
selC.innerHTML = '<option value=""> keiner </option>' +
runs10.map(r => `<option value="${r}">${r}</option>`).join('');
}
} catch { /* offline oder noch keine Runs */ }
}
initRunSelectors().then(() => loadData());
document.getElementById('btnReload').addEventListener('click', () => {
initRunSelectors().then(() => { loadData(); loadCompareData(); });
});
document.getElementById('sel-run-primary')?.addEventListener('change', loadData);
document.getElementById('sel-run-compare')?.addEventListener('change', loadCompareData);
window.addEventListener('message', (e) => {
if (e.data?.type === 'reload') loadData();
if (e.data?.type === 'reload') {
initRunSelectors().then(() => { loadData(); loadCompareData(); });
}
});
// ── Resize & Render-Loop ──────────────────────────────────────────────────────

View File

@@ -18,8 +18,9 @@ async function loadPanel(tab, src) {
});
// Tab-spezifische Initialisierung
if (tab === 'camera-npz') initCameraNpz();
else if (tab === 'board') initBoard();
if (tab === 'camera-npz') initCameraNpz();
else if (tab === 'board') initBoard();
else if (tab === 'robot-x-axis') initXAxis();
} catch (err) {
document.getElementById('tab-' + tab).innerHTML =
@@ -354,6 +355,75 @@ async function loadBoardTable() {
// ── Board ─────────────────────────────────────────────────────────────────────
// ── Tab: Robot X Axis ─────────────────────────────────────────────────────────
async function populateXAxisSetDropdowns() {
let sets = [];
try {
const r = await fetch('/api/robot/board-sets');
if (r.ok) sets = (await r.json()).sets ?? [];
} catch {}
const sel = document.getElementById('xaxis-ref-set');
if (sel) {
sel.innerHTML = '<option value="">alle</option>' +
sets.map(s => `<option value="${s}">${s}</option>`).join('');
}
}
function initXAxis() {
const logEl = document.getElementById('log-xaxis');
function logX(msg) {
const ts = new Date().toLocaleTimeString('de-CH');
logEl.value += `[${ts}] ${msg}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
populateXAxisSetDropdowns();
document.getElementById('btn-xaxis-run').addEventListener('click', async () => {
const refSet = document.getElementById('xaxis-ref-set')?.value ?? '';
logX(`Board-Erkennung … Referenz: ${refSet || 'alle'}`);
const btn = document.getElementById('btn-xaxis-run');
btn.disabled = true;
try {
const response = await fetch('/api/board/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refSet: refSet || undefined }),
});
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}`; }
logX(`❌ HTTP ${response.status}: ${msg}`);
return;
}
await readSseStream(response, logX, (evt) => {
if (evt.exitCode === 0) {
logX('✅ Board-Run abgeschlossen.');
if (evt.runDir) {
document.getElementById('xaxis-last-run').textContent = evt.runDir;
const frame = document.getElementById('xaxis-viewer-frame');
if (frame?.contentWindow) {
frame.contentWindow.postMessage({ type: 'reload' }, '*');
}
}
} else {
logX(`❌ Beendet mit Exit-Code ${evt.exitCode}`);
}
});
} catch (err) {
logX(`❌ Fehler: ${err}`);
} finally {
btn.disabled = false;
}
});
}
// ── Tab: Board (shared helpers) ───────────────────────────────────────────────
/** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */
async function populateBoardSetDropdowns() {
let sets = [];

View File

@@ -1,28 +1,71 @@
<div class="sections">
<!-- ── Info ──────────────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Robot X Axis <span class="status-badge open">offen</span></h2>
<div class="placeholder-note">
Ziel: X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor und
Nullpunkt). Roboter fährt zwei bekannte Positionen an, Kamera beobachtet den
Endeffektor-Marker.<br><br>
Geplante Aktionen: Referenzposition 1 anfahren · Foto · Marker merken ·
Referenzposition 2 anfahren · Foto · Achsvektor berechnen · Speichern.<br><br>
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
<h2>Robot X Axis Board-Erkennung</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)">
X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor + Nullpunkt).
Der Roboter fährt entlang der X-Achse, die Kamera beobachtet das Board aus mehreren Positionen.
</span>
<span class="info-label">Ablauf</span>
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
Board erkennen → ⬅&thinsp;/&thinsp;➡ Roboter bewegen → Board erkennen
→ im Viewer die zwei Runs als Basis + Vergleich wählen → Achse berechnen
</span>
<span class="info-label">Letzter Run</span>
<span class="info-value" id="xaxis-last-run"></span>
</div>
<div class="controls" style="margin-top: 14px;">
<button disabled>Pos 1 anfahren</button>
<button disabled>Foto Pos 1</button>
<button disabled>Pos 2 anfahren</button>
<button disabled>Foto Pos 2</button>
<button disabled>Achse berechnen</button>
<button disabled>Speichern</button>
<div class="controls" style="margin-top:16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<button id="btn-xaxis-run">Board erkennen</button>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)">
Referenz:
<select id="xaxis-ref-set"
style="background:#1e293b;border:1px solid #334155;color:#c8cdd8;border-radius:3px;padding:3px 8px;font:inherit;font-size:12px;cursor:pointer">
<option value="">alle</option>
</select>
</label>
</div>
</div>
<!-- ── Aktionen ───────────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Aktionen</h2>
<div style="margin-top:14px;display:flex;align-items:center;gap:20px;flex-wrap:wrap">
<button id="btn-xaxis-left" disabled
style="font-size:15px;padding:6px 20px;opacity:.45;cursor:not-allowed"
title="Roboter X-Achse nach links folgt">
⬅ Links
</button>
<span style="color:var(--muted);font-size:11px">Roboter-X-Achse bewegen (Schrittweite folgt)</span>
<button id="btn-xaxis-right" disabled
style="font-size:15px;padding:6px 20px;opacity:.45;cursor:not-allowed"
title="Roboter X-Achse nach rechts folgt">
Rechts ➡
</button>
</div>
</div>
<!-- ── Ausgabe / Log ──────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Ausgabe / Log</h2>
<textarea id="log-xaxis" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
</div>
<!-- ── Board-Viewer ───────────────────────────────────────────────────────── -->
<div class="section full">
<h2>Board-Viewer</h2>
<p style="font-size:12px;color:var(--muted);margin-bottom:10px">
Basis-Dropdown: vollständige Anzeige eines Runs. &ensp;
Vergleich-Dropdown: zeigt nur fremd-triangulierte Punkte (orange) eines anderen Runs.
</p>
<iframe
id="xaxis-viewer-frame"
src="/boardViewer.html"
style="width:100%;height:740px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block"
title="Board-Viewer (X-Achse)"
></iframe>
</div>
</div>