Anlegen vom Raum-Scanner

This commit is contained in:
chk
2026-06-16 10:06:05 +02:00
commit 792022d1fb
1836 changed files with 773869 additions and 0 deletions

338
public/app.js Normal file
View File

@@ -0,0 +1,338 @@
// 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';
});
})();

63
public/index.html Normal file
View File

@@ -0,0 +1,63 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AppRobot_Room 3D Viewer</title>
data:,
<style>
html, body { height: 100%; margin: 0; background: #ffffff; color: #111; font-family: system-ui, Segoe UI, Arial, sans-serif; }
#app { display: grid; grid-template-columns: 1fr 320px; grid-template-rows: auto 1fr; height: 100%; }
header { grid-column: 1 / span 2; padding: 8px 12px; background: #f4f6f8; border-bottom: 1px solid #d9dee3; }
#canvas-wrap { position: relative; }
#hud { position: absolute; left: 8px; top: 8px; background: rgba(0,0,0,0.5); color:#fff; padding: 6px 8px; border-radius: 6px; font-size: 12px; }
aside { border-left: 1px solid #d9dee3; padding: 10px; background: #f9fbfc; }
.row { margin-bottom: 8px; }
label { font-size: 12px; color: #4b5563; }
input[type="number"], input[type="text"] { width: 100%; padding: 6px; background: #fff; border: 1px solid #cbd5e1; color: #111; border-radius: 4px; }
button { padding: 6px 10px; background: #2563eb; border: 0; color: #fff; border-radius: 4px; cursor: pointer; }
a { color: #1d4ed8; }
canvas { display:block; }
</style>
<!-- Import-Map: three & addons von /vendor/three (server.js mappt node_modules) -->
<script type="importmap">
{
"imports": {
"three": "/vendor/three/build/three.module.js",
"three/addons/": "/vendor/three/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="app">
<header>
<strong>AppRobot_Room</strong> · <span id="status">Verbinde …</span>
</header>
<div id="canvas-wrap">
<div id="hud">
<div>FPS: <span id="fps"></span></div>
<div>Frame: <span id="frameInfo"></span></div>
<div>Frames (10s): <span id="frames10s">0</span></div>
</div>
<canvas id="three"></canvas>
</div>
<!--
<aside>
<div class="row"><label>Server-Config:</label>
<pre id="cfg" style="white-space: pre-wrap; word-break: break-word; font-size: 12px; background: #fff; padding: 8px; border-radius: 6px; max-height: 40vh; overflow:auto;"></pre>
</div>
<div class="row"><button id="btnReset">Ansicht zurücksetzen</button></div>
<p style="font-size:12px; color:#4b5563;">WASD/Mouse: Kamera · Spheres: ~5 cm Ø · Farben: Distanz</p>
</aside>
-->
</div>
<!-- App als ES-Modul -->
<script type="module" src="./app.js"></script>
</body>
</html>