diff --git a/viz/static/js/scene.js b/viz/static/js/scene.js index 9275ec3..365b7e7 100644 --- a/viz/static/js/scene.js +++ b/viz/static/js/scene.js @@ -3,29 +3,33 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // ── Renderer ────────────────────────────────────────────────────────────────── const viewerEl = document.getElementById('viewer'); -const renderer = new THREE.WebGLRenderer({ antialias: true }); -renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); -renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight); -viewerEl.appendChild(renderer.domElement); +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 = '
WebGL indisponible — rechargez la page ou fermez les autres onglets.
'; +} // ── Scene ───────────────────────────────────────────────────────────────────── -const 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 + Controls ───────────────────────────────────────────────────────── -const camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000); -camera.position.set(0, 150, 250); -const controls = new OrbitControls(camera, renderer.domElement); -controls.enableDamping = true; -controls.dampingFactor = 0.08; -controls.minDistance = 1; -controls.maxDistance = 3000; - -// ── Layer registry ──────────────────────────────────────────────────────────── 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] || []; @@ -96,79 +100,88 @@ fetch('/api/trajectory') document.getElementById('status').textContent = `traj: ${d.traj_status || 'n/a'}`; - 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 (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])); - } + 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)); + 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)); // max 100 frustums - for (let i = 0; i < T_4x4.length; i += step) { - addLayer('frustums', makeFrustum(T_4x4[i])); + 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(); } } - // Center camera - if (x.length > 0) { - controls.target.set(x[0], z[0], -y[0]); - camera.position.set(x[0], z[0] + 50, -y[0] + 100); + 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(); } } - 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 only USV GPS, center on that - 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 depth/altitude chart + // 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; - const hasAlt = altitude_m && altitude_m.some(v => !isNaN(v) && v !== null); + // 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: 'Depth (m)', - data: t_s.map((t, i) => ({ x: t, y: -depth_m[i] })), + 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.08)', - borderWidth: 1.5, + backgroundColor: 'rgba(58,159,255,0.10)', + borderWidth: 2, pointRadius: 0, - fill: true, + 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: 'Altitude above seafloor (m)', - data: t_s.map((t, i) => ({ x: t, y: altitude_m[i] })), + 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: 'transparent', + backgroundColor: 'rgba(68,255,170,0.06)', borderWidth: 1.5, pointRadius: 0, + spanGaps: false, + fill: 'end', }); } Chart.defaults.color = '#556'; @@ -182,19 +195,20 @@ fetch('/api/trajectory') maintainAspectRatio: false, parsing: false, plugins: { - legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' } } }, + legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' }, boxWidth: 20 } }, tooltip: { enabled: false }, }, scales: { x: { type: 'linear', - title: { display: true, text: 'Time (s)', color: '#445' }, + title: { display: true, text: 'Temps (s)', color: '#445' }, ticks: { color: '#445', font: { size: 9 } }, grid: { color: '#111' }, }, y: { - suggestedMin: 0, - title: { display: true, text: 'm', color: '#445' }, + reverse: false, + suggestedMax: 0, + title: { display: true, text: 'profondeur (m)', color: '#445' }, ticks: { color: '#445', font: { size: 9 } }, grid: { color: '#111' }, }, @@ -216,15 +230,17 @@ document.querySelectorAll('[data-layer]').forEach(cb => { }); // ── Resize ──────────────────────────────────────────────────────────────────── -window.addEventListener('resize', () => { - camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight; - camera.updateProjectionMatrix(); - renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight); -}); +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); -})(); + // ── Render loop ───────────────────────────────────────────────────────────── + (function animate() { + requestAnimationFrame(animate); + controls.update(); + renderer.render(scene, camera); + })(); +}