x-axis justierung: visualize
This commit is contained in:
@@ -51,6 +51,23 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
.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-wrap { flex: 1; min-height: 360px; position: relative; overflow: hidden; }
|
||||||
canvas { display: block; width: 100%; height: 100%; }
|
canvas { display: block; width: 100%; height: 100%; }
|
||||||
#hint {
|
#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:#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:#fbbf24"></span>Gemessen (3b)</span>
|
||||||
<span><span class="dot circle" style="background:#3b82f6"></span>Fremd (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>
|
<span><span class="dot" style="background:#9b7bff"></span>Kamera</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="stats"></span>
|
<span id="stats"></span>
|
||||||
@@ -134,6 +152,19 @@
|
|||||||
<button class="btn" id="btnReload">↺</button>
|
<button class="btn" id="btnReload">↺</button>
|
||||||
</div>
|
</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">
|
<div id="canvas-wrap">
|
||||||
<canvas id="cv"></canvas>
|
<canvas id="cv"></canvas>
|
||||||
<div id="hint">Orbit · Scroll · Rechte Taste = Pan</div>
|
<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 gMarkers = new THREE.Group(); // Modell-Rechtecke
|
||||||
const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
|
const gMeasured = new THREE.Group(); // gemessene Positionen (3b)
|
||||||
const gCameras = new THREE.Group(); // Kamera-Frusta
|
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) {
|
function clearGroup(g) {
|
||||||
while (g.children.length) {
|
while (g.children.length) {
|
||||||
@@ -542,11 +574,17 @@ function buildTable(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Daten laden ───────────────────────────────────────────────────────────────
|
// ── Daten laden ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Run. */
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
statusEl.textContent = 'Laden …';
|
statusEl.textContent = 'Laden …';
|
||||||
|
const selRun = document.getElementById('sel-run-primary')?.value ?? '';
|
||||||
|
const url = selRun
|
||||||
|
? `/api/board/latest?run=${encodeURIComponent(selRun)}`
|
||||||
|
: '/api/board/latest';
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/board/latest');
|
const r = await fetch(url);
|
||||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (!data.runDir) {
|
if (!data.runDir) {
|
||||||
@@ -564,10 +602,63 @@ async function loadData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData();
|
/** Vergleichs-Run laden (Compare-Dropdown) – zeigt nur fremd-triangulierte Marker als orange Kugeln. */
|
||||||
document.getElementById('btnReload').addEventListener('click', loadData);
|
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) => {
|
window.addEventListener('message', (e) => {
|
||||||
if (e.data?.type === 'reload') loadData();
|
if (e.data?.type === 'reload') {
|
||||||
|
initRunSelectors().then(() => { loadData(); loadCompareData(); });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Resize & Render-Loop ──────────────────────────────────────────────────────
|
// ── Resize & Render-Loop ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ async function loadPanel(tab, src) {
|
|||||||
// Tab-spezifische Initialisierung
|
// Tab-spezifische Initialisierung
|
||||||
if (tab === 'camera-npz') initCameraNpz();
|
if (tab === 'camera-npz') initCameraNpz();
|
||||||
else if (tab === 'board') initBoard();
|
else if (tab === 'board') initBoard();
|
||||||
|
else if (tab === 'robot-x-axis') initXAxis();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById('tab-' + tab).innerHTML =
|
document.getElementById('tab-' + tab).innerHTML =
|
||||||
@@ -354,6 +355,75 @@ async function loadBoardTable() {
|
|||||||
|
|
||||||
// ── Board ─────────────────────────────────────────────────────────────────────
|
// ── 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 */
|
/** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */
|
||||||
async function populateBoardSetDropdowns() {
|
async function populateBoardSetDropdowns() {
|
||||||
let sets = [];
|
let sets = [];
|
||||||
|
|||||||
@@ -1,28 +1,71 @@
|
|||||||
<div class="sections">
|
<div class="sections">
|
||||||
|
|
||||||
|
<!-- ── Info ──────────────────────────────────────────────────────────────── -->
|
||||||
<div class="section full">
|
<div class="section full">
|
||||||
<h2>Robot X Axis <span class="status-badge open">offen</span></h2>
|
<h2>Robot X Axis – Board-Erkennung</h2>
|
||||||
<div class="placeholder-note">
|
<div class="info-grid" style="margin-top:14px">
|
||||||
Ziel: X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor und
|
<span class="info-label">Ziel</span>
|
||||||
Nullpunkt). Roboter fährt zwei bekannte Positionen an, Kamera beobachtet den
|
<span class="info-value" style="font-family:inherit;font-size:13px;color:var(--muted)">
|
||||||
Endeffektor-Marker.<br><br>
|
X-Achse des Roboters im Weltkoordinatensystem verorten (Richtungsvektor + Nullpunkt).
|
||||||
Geplante Aktionen: Referenzposition 1 anfahren · Foto · Marker merken ·
|
Der Roboter fährt entlang der X-Achse, die Kamera beobachtet das Board aus mehreren Positionen.
|
||||||
Referenzposition 2 anfahren · Foto · Achsvektor berechnen · Speichern.<br><br>
|
</span>
|
||||||
<em>Aktionen werden ergänzt sobald das Konzept feststeht.</em>
|
<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>
|
||||||
<div class="controls" style="margin-top: 14px;">
|
<div class="controls" style="margin-top:16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||||
<button disabled>Pos 1 anfahren</button>
|
<button id="btn-xaxis-run">Board erkennen</button>
|
||||||
<button disabled>Foto Pos 1</button>
|
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)">
|
||||||
<button disabled>Pos 2 anfahren</button>
|
Referenz:
|
||||||
<button disabled>Foto Pos 2</button>
|
<select id="xaxis-ref-set"
|
||||||
<button disabled>Achse berechnen</button>
|
style="background:#1e293b;border:1px solid #334155;color:#c8cdd8;border-radius:3px;padding:3px 8px;font:inherit;font-size:12px;cursor:pointer">
|
||||||
<button disabled>Speichern</button>
|
<option value="">alle</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="section full">
|
||||||
<h2>Ausgabe / Log</h2>
|
<h2>Ausgabe / Log</h2>
|
||||||
<textarea id="log-xaxis" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
<textarea id="log-xaxis" readonly placeholder="(Ausgabe erscheint hier)"></textarea>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -620,26 +620,46 @@ app.post('/api/board/run', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Neuestes Board-Run-Verzeichnis (Timestamp-Name) oder null */
|
/** Alle Board-Run-Verzeichnisse, neueste zuerst */
|
||||||
async function findLatestBoardRun() {
|
async function listBoardRuns() {
|
||||||
try {
|
try {
|
||||||
await fsPromises.access(boardDataDir);
|
await fsPromises.access(boardDataDir);
|
||||||
const entries = await fsPromises.readdir(boardDataDir, { withFileTypes: true });
|
const entries = await fsPromises.readdir(boardDataDir, { withFileTypes: true });
|
||||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
|
return entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse();
|
||||||
return dirs[0] ?? null;
|
|
||||||
} catch {
|
} 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
|
* GET /api/board/runs?limit=N
|
||||||
* Gibt Daten des letzten Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
|
* 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.
|
* Wird vom Board-Viewer (boardViewer.html) abgefragt.
|
||||||
*/
|
*/
|
||||||
app.get('/api/board/latest', async (req, res) => {
|
app.get('/api/board/latest', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const runName = await findLatestBoardRun();
|
const runName = req.query.run || await findLatestBoardRun();
|
||||||
if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] });
|
if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] });
|
||||||
|
|
||||||
const runDir = path.join(boardDataDir, runName);
|
const runDir = path.join(boardDataDir, runName);
|
||||||
|
|||||||
Reference in New Issue
Block a user