UI verbessern

This commit is contained in:
chk
2026-06-14 18:24:12 +02:00
parent 7d76caa00b
commit c23fbf75f2
4 changed files with 135 additions and 14 deletions

View File

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

View File

@@ -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;
}

View File

@@ -88,7 +88,7 @@
<h2>Board-Viewer</h2>
<iframe
id="board-viewer-frame"
src="/boardViewer.html?defaults=a"
src="/boardViewer.html?mode=homing"
style="width:100%;height:600px;border:1px solid #334155;border-radius:6px;background:#0d0f13;display:block;margin-top:12px"
title="Board-Viewer"
></iframe>

View File

@@ -667,17 +667,31 @@ app.get('/api/board/runs', async (req, res) => {
});
/**
* GET /api/board/latest?run=<timestamp>
* GET /api/board/latest?run=<timestamp>&from=homing
* Gibt Daten eines Board-Runs zurück: robot.json + Detection-Ergebnisse + Kamera-Posen.
* Ohne ?run → neuester Run. Mit ?run=<timestamp> → 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 {}