fix: guard all 3D calls behind webglOk — chart fonctionne sans WebGL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,29 +3,33 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|||||||
|
|
||||||
// ── Renderer ──────────────────────────────────────────────────────────────────
|
// ── Renderer ──────────────────────────────────────────────────────────────────
|
||||||
const viewerEl = document.getElementById('viewer');
|
const viewerEl = document.getElementById('viewer');
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
let renderer, scene, camera, controls, webglOk = false;
|
||||||
|
try {
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||||
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
|
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
|
||||||
viewerEl.appendChild(renderer.domElement);
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Scene ─────────────────────────────────────────────────────────────────────
|
||||||
const scene = new THREE.Scene();
|
const layers = {};
|
||||||
|
if (webglOk) {
|
||||||
|
scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0x06060f);
|
scene.background = new THREE.Color(0x06060f);
|
||||||
scene.fog = new THREE.Fog(0x06060f, 500, 2000);
|
scene.fog = new THREE.Fog(0x06060f, 500, 2000);
|
||||||
scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22));
|
scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22));
|
||||||
scene.add(new THREE.AxesHelper(5));
|
scene.add(new THREE.AxesHelper(5));
|
||||||
|
camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000);
|
||||||
// ── Camera + Controls ─────────────────────────────────────────────────────────
|
|
||||||
const camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000);
|
|
||||||
camera.position.set(0, 150, 250);
|
camera.position.set(0, 150, 250);
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
controls.dampingFactor = 0.08;
|
controls.dampingFactor = 0.08;
|
||||||
controls.minDistance = 1;
|
controls.minDistance = 1;
|
||||||
controls.maxDistance = 3000;
|
controls.maxDistance = 3000;
|
||||||
|
}
|
||||||
// ── Layer registry ────────────────────────────────────────────────────────────
|
|
||||||
const layers = {};
|
|
||||||
|
|
||||||
function addLayer(name, obj) {
|
function addLayer(name, obj) {
|
||||||
layers[name] = layers[name] || [];
|
layers[name] = layers[name] || [];
|
||||||
@@ -96,6 +100,7 @@ fetch('/api/trajectory')
|
|||||||
document.getElementById('status').textContent =
|
document.getElementById('status').textContent =
|
||||||
`traj: ${d.traj_status || 'n/a'}`;
|
`traj: ${d.traj_status || 'n/a'}`;
|
||||||
|
|
||||||
|
if (webglOk) {
|
||||||
if (d.usv_gps) {
|
if (d.usv_gps) {
|
||||||
const { x, y, z, rtk } = d.usv_gps;
|
const { x, y, z, rtk } = d.usv_gps;
|
||||||
addLayer('usv_gps', makeLine(x, y, z, 0x3a9fff));
|
addLayer('usv_gps', makeLine(x, y, z, 0x3a9fff));
|
||||||
@@ -114,13 +119,12 @@ fetch('/api/trajectory')
|
|||||||
addLayer(trajLayer, makeLine(x, y, z, d.fused ? 0xffffff : 0xffd700));
|
addLayer(trajLayer, makeLine(x, y, z, d.fused ? 0xffffff : 0xffd700));
|
||||||
|
|
||||||
if (T_4x4 && T_4x4.length > 0) {
|
if (T_4x4 && T_4x4.length > 0) {
|
||||||
const step = Math.max(1, Math.floor(T_4x4.length / 100)); // max 100 frustums
|
const step = Math.max(1, Math.floor(T_4x4.length / 100));
|
||||||
for (let i = 0; i < T_4x4.length; i += step) {
|
for (let i = 0; i < T_4x4.length; i += step) {
|
||||||
addLayer('frustums', makeFrustum(T_4x4[i]));
|
addLayer('frustums', makeFrustum(T_4x4[i]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center camera
|
|
||||||
if (x.length > 0) {
|
if (x.length > 0) {
|
||||||
controls.target.set(x[0], z[0], -y[0]);
|
controls.target.set(x[0], z[0], -y[0]);
|
||||||
camera.position.set(x[0], z[0] + 50, -y[0] + 100);
|
camera.position.set(x[0], z[0] + 50, -y[0] + 100);
|
||||||
@@ -136,7 +140,6 @@ fetch('/api/trajectory')
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only USV GPS, center on that
|
|
||||||
if (!trajData && d.usv_gps) {
|
if (!trajData && d.usv_gps) {
|
||||||
const { x, y, z } = d.usv_gps;
|
const { x, y, z } = d.usv_gps;
|
||||||
const cx = x.reduce((a,b)=>a+b,0)/x.length;
|
const cx = x.reduce((a,b)=>a+b,0)/x.length;
|
||||||
@@ -145,30 +148,40 @@ fetch('/api/trajectory')
|
|||||||
camera.position.set(cx, 50, -cy + 80);
|
camera.position.set(cx, 50, -cy + 80);
|
||||||
controls.update();
|
controls.update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2D depth/altitude chart
|
// 2D cross-section chart: surface=0, AUV below (negative), seafloor even lower
|
||||||
if (d.auv_profile) {
|
if (d.auv_profile) {
|
||||||
const { t_s, depth_m, altitude_m } = 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 = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: 'Depth (m)',
|
label: 'AUV (m sous surface)',
|
||||||
data: t_s.map((t, i) => ({ x: t, y: -depth_m[i] })),
|
data: t_s.map((t, i) => ({ x: t, y: depth_m[i] })),
|
||||||
borderColor: '#3a9fff',
|
borderColor: '#3a9fff',
|
||||||
backgroundColor: 'rgba(58,159,255,0.08)',
|
backgroundColor: 'rgba(58,159,255,0.10)',
|
||||||
borderWidth: 1.5,
|
borderWidth: 2,
|
||||||
pointRadius: 0,
|
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) {
|
if (hasAlt) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: 'Altitude above seafloor (m)',
|
label: 'Fond marin (m sous surface)',
|
||||||
data: t_s.map((t, i) => ({ x: t, y: altitude_m[i] })),
|
// 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',
|
borderColor: '#44ffaa',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'rgba(68,255,170,0.06)',
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
|
spanGaps: false,
|
||||||
|
fill: 'end',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Chart.defaults.color = '#556';
|
Chart.defaults.color = '#556';
|
||||||
@@ -182,19 +195,20 @@ fetch('/api/trajectory')
|
|||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
parsing: false,
|
parsing: false,
|
||||||
plugins: {
|
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 },
|
tooltip: { enabled: false },
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
title: { display: true, text: 'Time (s)', color: '#445' },
|
title: { display: true, text: 'Temps (s)', color: '#445' },
|
||||||
ticks: { color: '#445', font: { size: 9 } },
|
ticks: { color: '#445', font: { size: 9 } },
|
||||||
grid: { color: '#111' },
|
grid: { color: '#111' },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
suggestedMin: 0,
|
reverse: false,
|
||||||
title: { display: true, text: 'm', color: '#445' },
|
suggestedMax: 0,
|
||||||
|
title: { display: true, text: 'profondeur (m)', color: '#445' },
|
||||||
ticks: { color: '#445', font: { size: 9 } },
|
ticks: { color: '#445', font: { size: 9 } },
|
||||||
grid: { color: '#111' },
|
grid: { color: '#111' },
|
||||||
},
|
},
|
||||||
@@ -216,15 +230,17 @@ document.querySelectorAll('[data-layer]').forEach(cb => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Resize ────────────────────────────────────────────────────────────────────
|
// ── Resize ────────────────────────────────────────────────────────────────────
|
||||||
|
if (webglOk) {
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
|
camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
|
renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Render loop ───────────────────────────────────────────────────────────────
|
// ── Render loop ─────────────────────────────────────────────────────────────
|
||||||
(function animate() {
|
(function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
controls.update();
|
controls.update();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
})();
|
})();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user