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 ──────────────────────────────────────────────────────