Files
cosma-nav/viz/static/js/scene.js
2026-04-24 21:47:57 +02:00

247 lines
9.8 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.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ── Renderer ──────────────────────────────────────────────────────────────────
const viewerEl = document.getElementById('viewer');
let renderer, scene, camera, controls, webglOk = false;
try {
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
viewerEl.appendChild(renderer.domElement);
webglOk = true;
} catch(e) {
viewerEl.innerHTML = '<div style="color:#556;font-size:11px;padding:20px">WebGL indisponible — rechargez la page ou fermez les autres onglets.</div>';
}
// ── Scene ─────────────────────────────────────────────────────────────────────
const layers = {};
if (webglOk) {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x06060f);
scene.fog = new THREE.Fog(0x06060f, 500, 2000);
scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22));
scene.add(new THREE.AxesHelper(5));
camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000);
camera.position.set(0, 150, 250);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.minDistance = 1;
controls.maxDistance = 3000;
}
function addLayer(name, obj) {
layers[name] = layers[name] || [];
layers[name].push(obj);
scene.add(obj);
}
// ── Geometry helpers ──────────────────────────────────────────────────────────
function makeLine(xArr, yArr, zArr, color) {
const pts = xArr.map((x, i) => new THREE.Vector3(x, zArr[i], -yArr[i]));
const geo = new THREE.BufferGeometry().setFromPoints(pts);
return new THREE.Line(geo, new THREE.LineBasicMaterial({ color }));
}
function makePoints(xArr, yArr, zArr, colorFn) {
const pos = new Float32Array(xArr.length * 3);
const col = new Float32Array(xArr.length * 3);
for (let i = 0; i < xArr.length; i++) {
pos[i*3] = xArr[i];
pos[i*3+1] = zArr[i];
pos[i*3+2] = -yArr[i];
const [r,g,b] = colorFn(i);
col[i*3]=r; col[i*3+1]=g; col[i*3+2]=b;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
return new THREE.Points(geo, new THREE.PointsMaterial({ size: 4, vertexColors: true, sizeAttenuation: true }));
}
function makeFrustum(T_flat) {
// Wireframe camera pyramid pointing downward (-Y): apex at origin, base 1m×0.75m at y=-1.5m
const verts = new Float32Array([
// 4 edges from apex to base corners
0,0,0, 0.5,-1.5, 0.375,
0,0,0, -0.5,-1.5, 0.375,
0,0,0, 0.5,-1.5,-0.375,
0,0,0, -0.5,-1.5,-0.375,
// 4 edges of base rectangle
0.5,-1.5, 0.375, -0.5,-1.5, 0.375,
-0.5,-1.5, 0.375, -0.5,-1.5,-0.375,
-0.5,-1.5,-0.375, 0.5,-1.5,-0.375,
0.5,-1.5,-0.375, 0.5,-1.5, 0.375,
]);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
const mesh = new THREE.LineSegments(geo,
new THREE.LineBasicMaterial({ color: 0xffd700, transparent: true, opacity: 0.5 }));
const m = new THREE.Matrix4();
// T_flat is row-major 4x4 from Python (need to transpose for Three.js column-major)
m.set(
T_flat[0][0],T_flat[0][1],T_flat[0][2],T_flat[0][3],
T_flat[1][0],T_flat[1][1],T_flat[1][2],T_flat[1][3],
T_flat[2][0],T_flat[2][1],T_flat[2][2],T_flat[2][3],
T_flat[3][0],T_flat[3][1],T_flat[3][2],T_flat[3][3],
);
mesh.applyMatrix4(m);
return mesh;
}
// ── RTK color mapping ─────────────────────────────────────────────────────────
const RTK_COLORS = [[0.8,0.3,0.1],[0.9,0.7,0.1],[0.1,0.9,0.2]];
// ── Load data ─────────────────────────────────────────────────────────────────
fetch('/api/trajectory')
.then(r => r.json())
.then(d => {
document.getElementById('status').textContent =
`traj: ${d.traj_status || 'n/a'}`;
if (webglOk) {
if (d.usv_gps) {
const { x, y, z, rtk } = d.usv_gps;
addLayer('usv_gps', makeLine(x, y, z, 0x3a9fff));
addLayer('usv_gps', makePoints(x, y, z, i => RTK_COLORS[Math.min(rtk[i], 2)]));
}
if (d.auv_usbl) {
const { x, y, z } = d.auv_usbl;
addLayer('auv_usbl', makePoints(x, y, z, () => [0.27, 1.0, 0.67]));
}
const trajData = d.fused || d.lingbot_local;
const trajLayer = d.fused ? 'fused' : 'lingbot_local';
if (trajData) {
const { x, y, z, T_4x4 } = trajData;
addLayer(trajLayer, makeLine(x, y, z, d.fused ? 0xffffff : 0xffd700));
if (T_4x4 && T_4x4.length > 0) {
const step = Math.max(1, Math.floor(T_4x4.length / 100));
for (let i = 0; i < T_4x4.length; i += step) {
addLayer('frustums', makeFrustum(T_4x4[i]));
}
}
if (x.length > 0) {
controls.target.set(x[0], z[0], -y[0]);
camera.position.set(x[0], z[0] + 50, -y[0] + 100);
controls.update();
}
}
if (d.ply) {
const { x, y, z } = d.ply;
addLayer('ply', makePoints(x, y, z, (i) => {
const t = Math.min(1, Math.max(0, (-z[i]) / 20));
return [0.2+0.3*t, 0.35+0.2*t, 0.5-0.2*t];
}));
}
if (!trajData && d.usv_gps) {
const { x, y, z } = d.usv_gps;
const cx = x.reduce((a,b)=>a+b,0)/x.length;
const cy = y.reduce((a,b)=>a+b,0)/y.length;
controls.target.set(cx, 0, -cy);
camera.position.set(cx, 50, -cy + 80);
controls.update();
}
}
// 2D cross-section chart: surface=0, AUV below (negative), seafloor even lower
if (d.auv_profile) {
const { t_s, depth_m, altitude_m } = d.auv_profile;
// depth_m from HDF5 is already negative (below surface)
// altitude_m is positive (above seafloor), filter zeros (Kogger not yet reading)
const ALT_MIN = 0.3;
const datasets = [
{
label: 'AUV (m sous surface)',
data: t_s.map((t, i) => ({ x: t, y: depth_m[i] })),
borderColor: '#3a9fff',
backgroundColor: 'rgba(58,159,255,0.10)',
borderWidth: 2,
pointRadius: 0,
fill: { target: 'origin', above: 'rgba(58,159,255,0.05)' },
},
];
const hasAlt = altitude_m && altitude_m.some(v => v > ALT_MIN);
if (hasAlt) {
datasets.push({
label: 'Fond marin (m sous surface)',
// seafloor = AUV depth - altitude above seafloor; null where altitude unreliable
data: t_s.map((t, i) => ({
x: t,
y: (altitude_m[i] > ALT_MIN) ? depth_m[i] - altitude_m[i] : null,
})),
borderColor: '#44ffaa',
backgroundColor: 'rgba(68,255,170,0.06)',
borderWidth: 1.5,
pointRadius: 0,
spanGaps: false,
fill: 'end',
});
}
Chart.defaults.color = '#556';
Chart.defaults.backgroundColor = 'transparent';
new Chart(document.getElementById('depth-chart'), {
type: 'line',
data: { datasets },
options: {
animation: false,
responsive: true,
maintainAspectRatio: false,
parsing: false,
plugins: {
legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' }, boxWidth: 20 } },
tooltip: { enabled: false },
},
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Temps (s)', color: '#445' },
ticks: { color: '#445', font: { size: 9 } },
grid: { color: '#111' },
},
y: {
reverse: false,
suggestedMax: 0,
title: { display: true, text: 'profondeur (m)', color: '#445' },
ticks: { color: '#445', font: { size: 9 } },
grid: { color: '#111' },
},
},
},
});
}
})
.catch(err => {
document.getElementById('status').textContent = `Error: ${err}`;
});
// ── Layer toggles ─────────────────────────────────────────────────────────────
document.querySelectorAll('[data-layer]').forEach(cb => {
cb.addEventListener('change', e => {
const name = e.target.dataset.layer;
(layers[name] || []).forEach(obj => { obj.visible = e.target.checked; });
});
});
// ── Resize ────────────────────────────────────────────────────────────────────
if (webglOk) {
window.addEventListener('resize', () => {
camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
});
// ── Render loop ─────────────────────────────────────────────────────────────
(function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
})();
}