mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-26 02:04:55 +00:00
* feat(ios): add RuView LiDAR frame protocol * feat(ios): capture ARKit scene depth for RuView * feat(ios): stream compact LiDAR frames over websocket * feat(ios): add native LiDAR capture UI * feat(ios): add RuView LiDAR app entrypoint * feat(web): add LiDAR bridge web package * feat(web): decode RuView LiDAR wire frames * feat(web): add local LiDAR websocket relay * feat(web): add LiDAR browser viewer * feat(web): render live LiDAR point cloud * fix(ios): use wall clock time for LiDAR provenance * feat(web): style LiDAR viewer * test(web): add LiDAR codec tests * docs: add iPhone LiDAR integration guide * docs(adr): define iPhone LiDAR sensor bridge * fix(ios): harden and validate LiDAR bridge * fix(ios): qualify LiDAR wire depth type
109 lines
3.3 KiB
JavaScript
109 lines
3.3 KiB
JavaScript
import { decodeLiDARPacket, depthToPointCloud } from './codec.mjs';
|
|
|
|
const canvas = document.querySelector('#view');
|
|
const ctx = canvas.getContext('2d');
|
|
const status = document.querySelector('#status');
|
|
const fpsEl = document.querySelector('#fps');
|
|
const pointsEl = document.querySelector('#points');
|
|
const seqEl = document.querySelector('#seq');
|
|
const latencyEl = document.querySelector('#latency');
|
|
const sensorEl = document.querySelector('#sensor');
|
|
|
|
let lastFrameAt = performance.now();
|
|
let yaw = 0.3;
|
|
let pitch = -0.15;
|
|
let scale = 120;
|
|
|
|
function connect() {
|
|
const token = new URLSearchParams(location.search).get('token');
|
|
if (!token) {
|
|
status.textContent = 'TOKEN REQUIRED';
|
|
status.dataset.state = 'warn';
|
|
return;
|
|
}
|
|
|
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const socket = new WebSocket(`${protocol}//${location.host}/ws/lidar?token=${encodeURIComponent(token)}`);
|
|
|
|
socket.addEventListener('open', () => {
|
|
status.textContent = 'LIVE';
|
|
status.dataset.state = 'live';
|
|
});
|
|
|
|
socket.addEventListener('close', () => {
|
|
status.textContent = 'RECONNECTING';
|
|
status.dataset.state = 'warn';
|
|
setTimeout(connect, 1000);
|
|
});
|
|
|
|
socket.addEventListener('message', (event) => {
|
|
try {
|
|
const raw = JSON.parse(event.data);
|
|
const frame = decodeLiDARPacket(raw);
|
|
const points = depthToPointCloud(frame, 1);
|
|
render(points);
|
|
|
|
const now = performance.now();
|
|
const delta = now - lastFrameAt;
|
|
lastFrameAt = now;
|
|
fpsEl.textContent = delta > 0 ? (1000 / delta).toFixed(1) : '0.0';
|
|
pointsEl.textContent = points.length.toLocaleString();
|
|
seqEl.textContent = frame.provenance.sequence;
|
|
latencyEl.textContent = Math.max(0, Date.now() - Number(frame.provenance.timestampNs / 1_000_000)).toFixed(0);
|
|
sensorEl.textContent = frame.provenance.sensor;
|
|
} catch (error) {
|
|
console.error(error);
|
|
status.textContent = 'FRAME ERROR';
|
|
status.dataset.state = 'warn';
|
|
}
|
|
});
|
|
}
|
|
|
|
function resize() {
|
|
const dpr = Math.min(devicePixelRatio || 1, 2);
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
|
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
|
|
function render(points) {
|
|
resize();
|
|
const w = canvas.clientWidth;
|
|
const h = canvas.clientHeight;
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.fillStyle = '#091018';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
const cy = Math.cos(yaw);
|
|
const sy = Math.sin(yaw);
|
|
const cp = Math.cos(pitch);
|
|
const sp = Math.sin(pitch);
|
|
|
|
ctx.fillStyle = '#58e0d2';
|
|
for (let i = 0; i < points.length; i += 1) {
|
|
let [x, y, z] = points[i];
|
|
const rx = x * cy - z * sy;
|
|
const rz = x * sy + z * cy;
|
|
const ry = y * cp - rz * sp;
|
|
const rz2 = y * sp + rz * cp;
|
|
const perspective = 1 / Math.max(0.35, 1.8 - rz2 * 0.12);
|
|
const px = w / 2 + rx * scale * perspective;
|
|
const py = h / 2 + ry * scale * perspective;
|
|
if (px >= 0 && px < w && py >= 0 && py < h) ctx.fillRect(px, py, 1.4, 1.4);
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener('pointermove', (event) => {
|
|
if (!event.buttons) return;
|
|
yaw += event.movementX * 0.006;
|
|
pitch += event.movementY * 0.006;
|
|
});
|
|
|
|
canvas.addEventListener('wheel', (event) => {
|
|
event.preventDefault();
|
|
scale = Math.max(40, Math.min(400, scale - event.deltaY * 0.2));
|
|
}, { passive: false });
|
|
|
|
connect();
|