From c23fbf75f2977ccc3d013a9242ec4b32c695de31 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:24:12 +0200 Subject: [PATCH] UI verbessern --- public/boardViewer.html | 116 +++++++++++++++++++++++++++++++++++++--- public/client.js | 11 ++-- public/index.html | 2 +- server/server.js | 20 +++++-- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/public/boardViewer.html b/public/boardViewer.html index 13e30f7..9e51b0a 100644 --- a/public/boardViewer.html +++ b/public/boardViewer.html @@ -204,6 +204,13 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const S = 1 / 1000; // mm → m +// ── Modus-Erkennung ────────────────────────────────────────────────────────── +const _urlParams = new URLSearchParams(window.location.search); +const IS_HOMING = _urlParams.get('mode') === 'homing'; +if (IS_HOMING) { + document.getElementById('run-bar').style.display = 'none'; +} + // robot (x=right, y=backward, z=up) → Three.js (x=right, y=up, z=toward viewer) function r2v(rx, ry, rz) { return new THREE.Vector3(rx * S, rz * S, -ry * S); } function r2vArr([rx, ry, rz]) { return r2v(rx, ry, rz); } @@ -243,13 +250,86 @@ const gCompare = new THREE.Group(); // Pos B Marker (nur fremd, orange) const gCompareLines = new THREE.Group(); // Verbindungslinien Pos A↔Pos B const gPositionC = new THREE.Group(); // Pos C Marker (nur fremd, cyan) const gYAxis = new THREE.Group(); // Y-Achse Visualisierung (Kreismittelpunkte, Achse) -scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines, gPositionC, gYAxis); +const gSkeleton = new THREE.Group(); // Roboter-Skeleton (FK, nur im Homing-Mode) +scene.add(gPaper, gMarkers, gMeasured, gCameras, gCompare, gCompareLines, gPositionC, gYAxis, gSkeleton); // ── Zustand für Positionen ──────────────────────────────────────────────────── let _primaryFremdMarkers = []; // Pos A – [{marker_id, position_mm, num_cameras}] let _compareFremdMarkers = []; // Pos B – [{marker_id, position_mm, num_cameras}] let _positionCFremdMarkers = []; // Pos C – [{marker_id, position_mm, num_cameras}] +// ── Homing-Mode Zustand ─────────────────────────────────────────────────────── +let _currentRobot = null; // robot.json nach loadData() +let _homingAngles = null; // { x, y, z, a, b, c, e } nach Homing-Run + +// ── Roboter-Skeleton (Forward Kinematics) ───────────────────────────────────── +/** + * Zeichnet das Roboter-Skeleton mit vorwärts-kinematischer Berechnung. + * Nutzt skeleton.from/to aus robot.json (in lokalem Link-Frame). + * angles: { x_mm→x, y_deg→y, z_deg→z, a_deg→a, b_deg→b, c_deg→c, e_mm→e } + */ +function buildSkeletonFK(robot, angles) { + clearGroup(gSkeleton); + if (!robot?.links) return; + + const links = robot.links; + const order = ['Base', 'Arm1', 'Ellbow', 'Arm2', 'Hand', 'Palm', 'FingerA', 'FingerB']; + // frames: Link-Name → Matrix4 (link-lokal → Welt) + const frames = { Board: new THREE.Matrix4() }; // Board = Welt-Ursprung + + for (const linkName of order) { + const link = links[linkName]; + if (!link?.jointToParent) continue; + + const parentName = link.parent ?? 'Board'; + const parentFrame = frames[parentName] ?? new THREE.Matrix4(); + const jtp = link.jointToParent; + + // 1. Translation zum Gelenk-Ursprung (im Parent-Frame) + const [ox, oy, oz] = jtp.origin ?? [0, 0, 0]; + const T_origin = new THREE.Matrix4().makeTranslation(ox * S, oz * S, -oy * S); + + // 2. Gelenk-Transformation (Rotation/Translation je nach Typ) + const varName = jtp.variable; + const q = angles?.[varName] ?? 0; + let T_joint = new THREE.Matrix4(); // Einheitsmatrix bei q=0 + + if (jtp.type === 'revolute') { + const [ax, ay, az] = jtp.axis ?? [0, 1, 0]; + // robot (x,y,z) → Three.js (x, z, -y) + const axisV = new THREE.Vector3(ax, az, -ay).normalize(); + T_joint.makeRotationAxis(axisV, q * Math.PI / 180); + } else if (jtp.type === 'linear') { + const [ax, ay, az] = jtp.axis ?? [1, 0, 0]; + T_joint.makeTranslation(ax * q * S, az * q * S, -ay * q * S); + } + + // Child-Frame = Parent-Frame × T_origin × T_joint + const childFrame = parentFrame.clone().multiply(T_origin).multiply(T_joint); + frames[linkName] = childFrame; + + // 3. Skeleton-Segment zeichnen + const skel = link.skeleton; + if (!skel?.from || !skel?.to) continue; + + const [fx, fy, fz] = skel.from; + const [tx, ty, tz] = skel.to; + const fromW = new THREE.Vector3(fx * S, fz * S, -fy * S).applyMatrix4(childFrame); + const toW = new THREE.Vector3(tx * S, tz * S, -ty * S).applyMatrix4(childFrame); + + const [cr, cg, cb] = skel.color ?? [0.8, 0.2, 0.2]; + const color = new THREE.Color(cr, cg, cb); + const rad = Math.max((skel.radius ?? 4) * S, 0.004); + + gSkeleton.add(makeLine(fromW, toW, color, 0.9)); + gSkeleton.add(makeSphere(fromW, rad, color)); + gSkeleton.add(makeSphere(toW, rad, color)); + // Gelenk-Mittelpunkt (Welt-Ursprung des Link-Frames) + const jointW = new THREE.Vector3().applyMatrix4(childFrame); + gSkeleton.add(makeSphere(jointW, 0.004, 0xc8cdd8)); + } +} + // ── Viewer-interner Logger ──────────────────────────────────────────────────── function vlog(msg, kind = '') { const el = document.getElementById('viewer-log'); @@ -810,14 +890,21 @@ function computeAndShowYAxis() { // ── Daten laden ─────────────────────────────────────────────────────────────── -/** Haupt-Run laden (Basis-Dropdown). Ohne Selektion → neuester Run. */ +/** Haupt-Run laden. Im Homing-Mode: immer neuester Homing-Run, kein Dropdown. */ async function loadData() { 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'; + + let url; + if (IS_HOMING) { + url = '/api/board/latest?from=homing'; + } else { + const selRun = document.getElementById('sel-run-primary')?.value ?? ''; + url = selRun + ? `/api/board/latest?run=${encodeURIComponent(selRun)}` + : '/api/board/latest'; + } + try { const r = await fetch(url); if (!r.ok) throw new Error(`HTTP ${r.status}`); @@ -826,6 +913,7 @@ async function loadData() { statusEl.textContent = 'Kein Board-Run vorhanden.'; document.getElementById('stats').textContent = ''; clearGroup(gPaper); clearGroup(gMarkers); clearGroup(gMeasured); clearGroup(gCameras); + if (IS_HOMING) clearGroup(gSkeleton); return; } buildScene(data); @@ -839,6 +927,13 @@ async function loadData() { buildCompareLines(); const robotLabel = data.robotFile ? ` • Robot: ${data.robotFile}` : ''; statusEl.textContent = `Run: ${data.runDir}${robotLabel} • ${new Date().toLocaleTimeString('de-CH')}`; + + // Skeleton im Homing-Mode + if (IS_HOMING && data.robot) { + _currentRobot = data.robot; + const angles = _homingAngles ?? data.robot.defaultPosition ?? {}; + buildSkeletonFK(data.robot, angles); + } } catch (err) { statusEl.textContent = `Fehler: ${err.message ?? err}`; } @@ -965,6 +1060,11 @@ async function initRunSelectors() { /** Vollständige Initialisierung: Selektoren → Pos A → Pos B → Pos C (sequenziell!) */ async function initAll() { + if (IS_HOMING) { + // Homing-Mode: nur neuester Run laden, kein Dropdown, kein Vergleich + await loadData(); + return; + } await initRunSelectors(); await loadData(); // setzt _primaryFremdMarkers await loadCompareData(); // setzt _compareFremdMarkers + baut Linien + Y-Achse (no-op) @@ -984,6 +1084,10 @@ document.getElementById('sel-run-compare')?.addEventListener('change', async () document.getElementById('sel-run-c')?.addEventListener('change', loadPositionC); window.addEventListener('message', async (e) => { if (e.data?.type === 'reload') await initAll(); + if (e.data?.type === 'homing-state' && IS_HOMING) { + _homingAngles = e.data.state; + if (_currentRobot) buildSkeletonFK(_currentRobot, _homingAngles); + } }); // ── Resize & Render-Loop ────────────────────────────────────────────────────── diff --git a/public/client.js b/public/client.js index bba1169..1f4b467 100755 --- a/public/client.js +++ b/public/client.js @@ -484,10 +484,13 @@ async function runHoming() { setHomingStatus('✗ Fehler', 'open'); setHomingProgress(6, 6, 'Fehler aufgetreten'); } - if (evt.runDir) { - await loadHomingImages(evt.runDir); - const frame = document.getElementById('board-viewer-frame'); - if (frame) { const s = frame.src; frame.src = ''; frame.src = s; } + if (evt.runDir) await loadHomingImages(evt.runDir); + const frame = document.getElementById('board-viewer-frame'); + if (frame?.contentWindow) { + frame.contentWindow.postMessage({ type: 'reload' }, '*'); + if (evt.state) { + frame.contentWindow.postMessage({ type: 'homing-state', state: evt.state }, '*'); + } } break; } diff --git a/public/index.html b/public/index.html index a9cf29d..5822f70 100755 --- a/public/index.html +++ b/public/index.html @@ -88,7 +88,7 @@

Board-Viewer

diff --git a/server/server.js b/server/server.js index 3a7e38f..8b8118f 100755 --- a/server/server.js +++ b/server/server.js @@ -667,17 +667,31 @@ app.get('/api/board/runs', async (req, res) => { }); /** - * GET /api/board/latest?run= + * GET /api/board/latest?run=&from=homing * Gibt Daten eines Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen. * Ohne ?run → neuester Run. Mit ?run= → genau dieser Run. + * ?from=homing → liest aus data/homing/ statt data/board/ (für boardViewer im Homing-Mode). * Wird vom Board-Viewer (boardViewer.html) abgefragt. */ app.get('/api/board/latest', async (req, res) => { try { - const runName = req.query.run || await findLatestBoardRun(); + const fromHoming = req.query.from === 'homing'; + const dataDir = fromHoming ? homingDataDir : boardDataDir; + + let runName = req.query.run; + if (!runName) { + if (fromHoming) { + try { + const entries = await fsPromises.readdir(dataDir, { withFileTypes: true }); + runName = entries.filter(e => e.isDirectory()).map(e => e.name).sort().reverse()[0] ?? null; + } catch { runName = null; } + } else { + runName = await findLatestBoardRun(); + } + } if (!runName) return res.json({ runDir: null, robot: null, detections: [], cameraPoses: [] }); - const runDir = path.join(boardDataDir, runName); + const runDir = path.join(dataDir, runName); let robot = null; try { robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {}