diff --git a/public/boardViewer.html b/public/boardViewer.html
index 2996e0c..3483277 100644
--- a/public/boardViewer.html
+++ b/public/boardViewer.html
@@ -213,8 +213,13 @@ 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)
-scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare);
+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);
+
+// ── Zustand für Vergleichs-Linien ─────────────────────────────────────────────
+let _primaryFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
+let _compareFremdMarkers = []; // [{marker_id, position_mm, num_cameras}]
function clearGroup(g) {
while (g.children.length) {
@@ -573,6 +578,48 @@ function buildTable(data) {
wrap.innerHTML = html;
}
+// ── Vergleichs-Overlay: Transparenz + Linien ─────────────────────────────────
+
+/**
+ * Setzt Board-Marker und Papier-Ebene auf geringere Deckkraft, wenn der
+ * Vergleichs-Modus aktiv ist (compareActive=true → 10 % Deckkraft).
+ * Ursprüngliche Deckkraft wird pro Material einmalig gespeichert.
+ */
+function setSceneOpacity(compareActive) {
+ for (const obj of [...gPaper.children, ...gMarkers.children]) {
+ const mats = obj.material
+ ? (Array.isArray(obj.material) ? obj.material : [obj.material])
+ : [];
+ for (const mat of mats) {
+ if (!mat) continue;
+ if (mat._origOpacity === undefined) mat._origOpacity = mat.opacity ?? 1.0;
+ if (mat._origTransparent === undefined) mat._origTransparent = mat.transparent ?? false;
+ mat.transparent = compareActive || mat._origTransparent;
+ mat.opacity = compareActive ? mat._origOpacity * 0.10 : mat._origOpacity;
+ mat.needsUpdate = true;
+ }
+ }
+}
+
+/**
+ * Zeichnet Verbindungslinien von Basis-fremd-Markern zu gleich-ID-Vergleichs-Markern.
+ * Aktualisiert gleichzeitig die Board-Transparenz.
+ */
+function buildCompareLines() {
+ clearGroup(gCompareLines);
+ const compareActive = _compareFremdMarkers.length > 0;
+ setSceneOpacity(compareActive);
+ if (!compareActive || !_primaryFremdMarkers.length) return;
+
+ const primaryMap = new Map(_primaryFremdMarkers.map(m => [m.marker_id, m]));
+ for (const cm of _compareFremdMarkers) {
+ const pm = primaryMap.get(cm.marker_id);
+ if (!pm) continue;
+ // Linie: Basis-Position (blau) → Vergleichs-Position (orange)
+ gCompareLines.add(makeLine(r2vArr(pm.position_mm), r2vArr(cm.position_mm), 0xfb923c, 0.85));
+ }
+}
+
// ── Daten laden ───────────────────────────────────────────────────────────────
/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Run. */
@@ -595,6 +642,10 @@ async function loadData() {
}
buildScene(data);
buildTable(data);
+ // Fremd-Marker für Verbindungslinien merken (Marker, die nicht in Board-Link stehen)
+ const bIds = new Set((data.robot?.links?.Board?.markers ?? []).map(m => m.id));
+ _primaryFremdMarkers = (data.measuredMarkers?.markers ?? []).filter(m => !bIds.has(m.marker_id));
+ buildCompareLines();
const robotLabel = data.robotFile ? ` • Robot: ${data.robotFile}` : '';
statusEl.textContent = `Run: ${data.runDir}${robotLabel} • ${new Date().toLocaleTimeString('de-CH')}`;
} catch (err) {
@@ -602,26 +653,31 @@ async function loadData() {
}
}
-/** Vergleichs-Run laden (Compare-Dropdown) – zeigt nur fremd-triangulierte Marker als orange Kugeln. */
+/**
+ * Vergleichs-Run laden (Compare-Dropdown).
+ * Zeigt nur fremd-triangulierte Marker (nicht im Board-Link) als orange Kugeln.
+ * Zieht außerdem Verbindungslinien zu gleich-ID-Markern im Basis-Run.
+ */
async function loadCompareData() {
clearGroup(gCompare);
+ _compareFremdMarkers = [];
const selRun = document.getElementById('sel-run-compare')?.value ?? '';
- if (!selRun) return;
+ if (!selRun) { buildCompareLines(); return; }
try {
const r = await fetch(`/api/board/latest?run=${encodeURIComponent(selRun)}`);
- if (!r.ok) return;
- const data = await r.json();
+ if (!r.ok) { buildCompareLines(); 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)
+ // Board-Marker-IDs aus Robot.json dieses Runs
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));
+ _compareFremdMarkers.push(m); // für Linien
+ gCompare.add(makeSphere(r2vArr(m.position_mm), 0.006, 0xf97316)); // orange Kugel
}
}
} catch { /* kein 3b-Output für diesen Run */ }
+ buildCompareLines(); // Linien + Transparenz aktualisieren
}
/** Run-Listen laden und beide Dropdowns befüllen. */