// 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 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'; }); })();