x-axis justierung: visualize
This commit is contained in:
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 → ⬅ / ➡ 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.  
|
||||
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>
|
||||
|
||||
@@ -620,26 +620,46 @@ app.post('/api/board/run', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */
|
||||
async function findLatestBoardRun() {
|
||||
/** Alle Board-Run-Verzeichnisse, neueste zuerst */
|
||||
async function listBoardRuns() {
|
||||
try {
|
||||
await fsPromises.access(boardDataDir);
|
||||
const entries = await fsPromises.readdir(boardDataDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
|
||||
return dirs[0] ?? null;
|
||||
return entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
|
||||
} catch {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */
|
||||
async function findLatestBoardRun() {
|
||||
const dirs = await listBoardRuns();
|
||||
return dirs[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/board/latest
|
||||
* Gibt Daten des letzten Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
|
||||
* GET /api/board/runs?limit=N
|
||||
* Gibt eine Liste der vorhandenen Board-Run-Verzeichnisse zurück (neueste zuerst).
|
||||
*/
|
||||
app.get('/api/board/runs', async (req, res) => {
|
||||
try {
|
||||
const limit = Math.max(1, Math.min(50, parseInt(req.query.limit ?? '10', 10)));
|
||||
const runs = await listBoardRuns();
|
||||
return res.json({ runs: runs.slice(0, limit) });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/board/latest?run=<timestamp>
|
||||
* Gibt Daten eines Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
|
||||
* Ohne ?run → neuester Run. Mit ?run=<timestamp> → genau dieser Run.
|
||||
* Wird vom Board-Viewer (boardViewer.html) abgefragt.
|
||||
*/
|
||||
app.get('/api/board/latest', async (req, res) => {
|
||||
try {
|
||||
const runName = await findLatestBoardRun();
|
||||
const runName = req.query.run || await findLatestBoardRun();
|
||||
if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] });
|
||||
|
||||
const runDir = path.join(boardDataDir, runName);
|
||||
|
||||
Reference in New Issue
Block a user