Files
appRobotRoom/public/app.js
2026-06-16 10:06:05 +02:00

338 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Importe gemäß Import-Map
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
(function safeMain(){
// Robust gegen Fehlstarts
window.addEventListener('error', (e) => console.error('[APP] Window error:', e.message, e.error));
window.addEventListener('unhandledrejection', (e) => console.error('[APP] Unhandled promise rejection:', e.reason));
(async function(){
const statusEl = document.getElementById('status');
const fpsEl = document.getElementById('fps');
const frameInfoEl = document.getElementById('frameInfo');
const frames10sEl = document.getElementById('frames10s');
//const cfgEl = document.getElementById('cfg');
const canvas = document.getElementById('three');
function setStatus(txt) { statusEl.textContent = txt; console.log('[APP]', txt); }
// --- Config laden ---
setStatus('Lade /config …');
let sensor, pose;
try {
const res = await fetch('/config', { cache: 'no-store' });
if (!res.ok) throw new Error(`/config HTTP ${res.status}`);
const json = await res.json();
sensor = json.sensor; pose = json.pose;
//cfgEl.textContent = JSON.stringify({ sensor, pose }, null, 2);
setStatus('Config geladen');
} catch (err) {
console.error('[APP] /config Fehler:', err);
setStatus('Fehler: /config nicht erreichbar');
return;
}
// --- Three.js Setup ---
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff); // weißer Hintergrund
const camera = new THREE.PerspectiveCamera(60, 1, 0.01, 200);
camera.position.set(0.5, 0.5, 1.2);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0.4);
function resize() {
const w = window.innerWidth ; //- 320; // Seitenpanel
const h = window.innerHeight - 48; // Header
renderer.setSize(w, h, false);
camera.aspect = Math.max(0.1, w / h);
camera.updateProjectionMatrix();
}
// Licht & Helfer
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
const dir = new THREE.DirectionalLight(0xffffff, 0.5); dir.position.set(1,2,3); scene.add(dir);
const grid = new THREE.GridHelper(8, 80, 0xbbc4ce, 0xe0e6eb); grid.position.y = -0.001; scene.add(grid);
const axesWorld = new THREE.AxesHelper(1.0); scene.add(axesWorld); // R/G/B
const axesSensor = new THREE.AxesHelper(0.5); scene.add(axesSensor); // Sensorpose
// Instanced spheres
const W = sensor.gridW, H = sensor.gridH;
const instances = W * H;
const radius = (sensor.sphereDiameterM || 0.05) / 2;
const geom = new THREE.SphereGeometry(radius, 16, 12);
// 1) Basis-Vertexfarben = Weiß (1,1,1) an die Geometrie hängen
const vcount = geom.attributes.position.count;
const white = new Float32Array(vcount * 3);
white.fill(1.0);
geom.setAttribute('color', new THREE.BufferAttribute(white, 3));
// 2) Material: Vertexfarben aktivieren, neutraler Materialfarbton
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, vertexColors: true });
// 3) InstancedMesh erzeugen
const spheres = new THREE.InstancedMesh(geom, mat, instances);
spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
if (!spheres.instanceColor) {
const colors = new Float32Array(instances * 3);
for (let i = 0; i < instances; i++) {
colors[i * 3 + 0] = 1.0; // R
colors[i * 3 + 1] = 1.0; // G
colors[i * 3 + 2] = 1.0; // B
}
spheres.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
}
spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(spheres);
const dummy = new THREE.Object3D();
// Pose
let sensorPose = {
position: new THREE.Vector3(...pose.position),
rotationEulerDeg: pose.rotationEulerDeg,
eulerOrder: pose.eulerOrder || 'ZYX'
};
function applyPose(vec) {
const euler = new THREE.Euler(
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[0] || 0),
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[1] || 0),
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[2] || 0),
sensorPose.eulerOrder || 'ZYX'
);
const q = new THREE.Quaternion().setFromEuler(euler);
vec.applyQuaternion(q).add(sensorPose.position);
return vec;
}
function updateSensorAxes() {
axesSensor.position.copy(sensorPose.position);
const euler = new THREE.Euler(
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[0] || 0),
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[1] || 0),
THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[2] || 0),
sensorPose.eulerOrder || 'ZYX'
);
axesSensor.setRotationFromEuler(euler);
}
updateSensorAxes();
// Distanz-Farbskala: 0 m (nah) = rot, 4 m (weit) = blau
function colorForDist(m) {
// Normierung auf 0..4 m
let t = THREE.MathUtils.clamp(m/4 , 0, 1); // << const -> let
t = Math.min(Math.max(t, 0.01), 0.99); // sanftes Clamping, vermeidet reinste Endfarben
const col = new THREE.Color();
col.setHSL( (1 - t) * 0.0 + t * (240/360), 1.0, 0.5 );
return col;
}
// Richtungsvektor je Zelle (FoV Konfiguration aus sensor.*)
const hTan = Math.tan(THREE.MathUtils.degToRad(sensor.hFovDeg) / 2);
const vTan = Math.tan(THREE.MathUtils.degToRad(sensor.vFovDeg) / 2);
function cellDir(xIdx, yIdx) {
const u = ((xIdx + 0.5) / W) * 2 - 1; // -1..+1
const v = ((yIdx + 0.5) / H) * 2 - 1; // -1..+1
const dx = u * hTan;
const dy = -v * vTan; // screen-y -> world up
return new THREE.Vector3(dx, dy, 1).normalize();
}
// --- WebSockets ---
function explainCloseCode(code) {
switch (code) {
case 1000: return 'Normal Closure';
case 1001: return 'Going Away';
case 1002: return 'Protocol Error';
case 1003: return 'Unsupported Data';
case 1005: return 'No Status (internal)';
case 1006: return 'Abnormal Closure häufig: Handshake blockiert (Pfad/Port), Reverse-Proxy, CORS/Mixed-Content, Firewall';
case 1007: return 'Invalid Payload';
case 1008: return 'Policy Violation';
case 1009: return 'Message Too Big';
case 1011: return 'Server Error';
default: return 'Unbekannt';
}
}
function makeWS(path, label) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${location.host}${path}`;
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
ws.addEventListener('open', () => {
setStatus(`${label}: Verbunden (${url})`);
});
ws.addEventListener('close', (ev) => {
console.warn(`[WS ${label}] close`, { code: ev.code, reason: ev.reason, wasClean: ev.wasClean });
const note = explainCloseCode(ev.code);
setStatus(`${label}: Getrennt (code ${ev.code}${note ? ` · ${note}` : ''})`);
console.info(`[WS ${label}] Diagnose: Prüfe Pfad ${path}, Server-Port, Mixed-Content (http+ws vs https+wss), evtl. Proxy/Firewall.`);
});
ws.addEventListener('error', (err) => {
console.error(`[WS ${label}] error`, err);
setStatus(`${label}: Fehler (siehe Konsole)`);
});
ws.addEventListener('message', (ev) => {
// nur Sizing loggen, um Konsole nicht zu fluten
// console.debug(`[WS ${label}] message`, typeof ev.data, ev.data?.byteLength || ev.data?.length);
});
return ws;
}
const wsFrames = makeWS('/ws/frames', 'Frames'); // Datenstrom
const wsState = makeWS('/ws/state', 'State'); // Pose/Status
// Client-Statistik: empfangene Frames pro 10s
let framesReceived10s = 0;
setInterval(() => {
frames10sEl.textContent = String(framesReceived10s);
console.info(`[STAT 10s Client] framesReceived:${framesReceived10s}`);
framesReceived10s = 0;
}, 10_000);
wsState.onmessage = (ev) => {
try {
const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data));
if (msg.type === 'pose' && msg.pose) {
sensorPose.position = new THREE.Vector3(...msg.pose.position);
sensorPose.rotationEulerDeg = msg.pose.rotationEulerDeg;
sensorPose.eulerOrder = msg.pose.eulerOrder || 'ZYX';
updateSensorAxes();
}
} catch (e) {
console.error('[WS State] parse error', e);
}
};
// CSV: ts,w,h,d0..dN
function handleCSV(text) {
try {
const s = text.trim();
const p = s.split(',');
if (p.length < 3) return;
const expected = 3 + W * H;
if (p.length < expected) {
console.warn('[CSV] zu kurze Zeile', { got: p.length, expected });
return; // oder fehlende Werte als NaN behandeln
}
const w = parseInt(p[1], 10), h = parseInt(p[2], 10);
if (w !== W || h !== H) {
console.warn('[CSV] Unerwartete Dimension:', w, 'x', h, 'erwartet:', W, 'x', H);
return;
}
const vals = new Array(W * H);
for (let i = 0; i < W * H; i++) {
const raw = p[3 + i];
// Leere Felder oder fehlende Indizes abfangen
if (raw === undefined || raw === null || raw === '') {
vals[i] = NaN;
continue;
}
const v = Number(raw); // robust gegen floats und "nan"
vals[i] = Number.isFinite(v) ? v : NaN;
}
const col = new THREE.Color();
for (let y=0; y<H; y++) {
for (let x=0; x<W; x++) {
const idx = y*W + x;
const mm = vals[idx];
const m = mm * 0.001; // mm -> m
const dir = cellDir(x,y);
const posLocal = dir.clone().multiplyScalar(m);
const posWorld = applyPose(posLocal);
dummy.position.copy(posWorld);
dummy.rotation.set(0,0,0);
dummy.updateMatrix();
spheres.setMatrixAt(idx, dummy.matrix);
col.copy(colorForDist(m));
spheres.setColorAt(idx, col);
//const testColor = new THREE.Color(0x00ff00);
//spheres.setColorAt(idx, testColor);
}
}
spheres.instanceMatrix.needsUpdate = true;
if (spheres.instanceColor) spheres.instanceColor.needsUpdate = true;
frameInfoEl.textContent = `${new Date().toLocaleTimeString()} · ${W}x${H}`;
framesReceived10s++;
} catch (e) {
console.error('[CSV] handle error:', e);
}
// nachdem alle setMatrixAt(...) und setColorAt(...) fertig sind:
spheres.instanceMatrix.needsUpdate = true;
if (spheres.instanceColor) spheres.instanceColor.needsUpdate = true;
if (!window.__primed__) {
spheres.material.needsUpdate = true; // <<< WICHTIG
window.__primed__ = true;
}
}
wsFrames.onmessage = (ev) => {
if (typeof ev.data === 'string') handleCSV(ev.data);
else if (ev.data instanceof Blob) ev.data.text().then(handleCSV, (e)=>console.error('[CSV] blob->text error', e));
else if (ev.data instanceof ArrayBuffer) {
const txt = new TextDecoder().decode(ev.data);
handleCSV(txt);
} else {
console.warn('[WS Frames] unbekannter Datentyp', ev.data);
}
};
// Renderloop + FPS
let last = performance.now(), accum = 0, frames = 0;
function loop(t){
const dt = t - last; last = t; accum += dt; frames++;
if (accum >= 1000) { fpsEl.textContent = Math.round(frames * 1000 / accum); frames = 0; accum = 0; }
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
window.addEventListener('resize', resize); resize();
/*
document.getElementById('btnReset').addEventListener('click', () => {
camera.position.set(0.5, 0.5, 1.2);
controls.target.set(0, 0, 0.4);
controls.update();
});
*/
setStatus('Viewer bereit warte auf Frames …');
})().catch(err => {
console.error('[APP] fatal:', err);
const statusEl = document.getElementById('status');
if (statusEl) statusEl.textContent = 'Fehler: siehe Konsole';
});
})();