import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; const fileInput = document.getElementById("fileInput"); const presetSelect = document.getElementById("presetSelect"); const runButton = document.getElementById("runButton"); const statusEl = document.getElementById("status"); const healthEl = document.getElementById("health"); const metricsEl = document.getElementById("metrics"); const metricInput = document.getElementById("metricInput"); const metricAfter = document.getElementById("metricAfter"); const metricTriangles = document.getElementById("metricTriangles"); const metricReconstruction = document.getElementById("metricReconstruction"); const metricTime = document.getElementById("metricTime"); const viewport = document.getElementById("viewport"); let presets = []; let pointCloud = null; let meshObject = null; const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0b1118); const camera = new THREE.PerspectiveCamera(55, 1, 0.001, 100000); camera.position.set(2.5, 2.0, 2.5); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio || 1); viewport.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; scene.add(new THREE.AmbientLight(0xffffff, 0.65)); const keyLight = new THREE.DirectionalLight(0xffffff, 0.9); keyLight.position.set(4, 6, 3); scene.add(keyLight); const grid = new THREE.GridHelper(10, 20, 0x31465d, 0x1d2a38); grid.position.y = -0.001; scene.add(grid); function resize() { const width = viewport.clientWidth; const height = viewport.clientHeight; camera.aspect = width / Math.max(height, 1); camera.updateProjectionMatrix(); renderer.setSize(width, height, false); } function animate() { controls.update(); renderer.render(scene, camera); requestAnimationFrame(animate); } function clearSceneObjects() { if (pointCloud) { scene.remove(pointCloud); pointCloud.geometry.dispose(); pointCloud.material.dispose(); pointCloud = null; } if (meshObject) { scene.remove(meshObject); meshObject.geometry.dispose(); meshObject.material.dispose(); meshObject = null; } } function fitCameraToPoints(points) { if (!points || points.length === 0) { return; } const box = new THREE.Box3(); for (const point of points) { box.expandByPoint(new THREE.Vector3(point[0], point[1], point[2])); } const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const radius = Math.max(size.x, size.y, size.z) * 0.6 || 1.0; controls.target.copy(center); camera.position.copy(center.clone().add(new THREE.Vector3(radius * 1.8, radius * 1.2, radius * 1.8))); camera.near = Math.max(radius / 1000, 0.0001); camera.far = radius * 100; camera.updateProjectionMatrix(); controls.update(); } function renderGeometry(points, triangleIndices) { clearSceneObjects(); if (!points || points.length === 0) { return; } const positions = new Float32Array(points.length * 3); for (let i = 0; i < points.length; i += 1) { positions[i * 3] = points[i][0]; positions[i * 3 + 1] = points[i][1]; positions[i * 3 + 2] = points[i][2]; } const pointsGeometry = new THREE.BufferGeometry(); pointsGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); pointCloud = new THREE.Points( pointsGeometry, new THREE.PointsMaterial({ color: 0x7dd3fc, size: 0.01, sizeAttenuation: true }) ); scene.add(pointCloud); if (triangleIndices && triangleIndices.length > 0) { const indices = new Uint32Array(triangleIndices.length * 3); for (let i = 0; i < triangleIndices.length; i += 1) { indices[i * 3] = triangleIndices[i][0]; indices[i * 3 + 1] = triangleIndices[i][1]; indices[i * 3 + 2] = triangleIndices[i][2]; } const meshGeometry = new THREE.BufferGeometry(); meshGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); meshGeometry.setIndex(new THREE.BufferAttribute(indices, 1)); meshGeometry.computeVertexNormals(); meshObject = new THREE.Mesh( meshGeometry, new THREE.MeshStandardMaterial({ color: 0x60a5fa, transparent: true, opacity: 0.55, side: THREE.DoubleSide, flatShading: false, }) ); scene.add(meshObject); } fitCameraToPoints(points); } function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.style.color = isError ? "#ffb4c0" : "#9db0c3"; } function updateMetrics(result) { metricsEl.classList.remove("hidden"); metricInput.textContent = String(result.inputPoints ?? "-"); metricAfter.textContent = String(result.afterPreprocess ?? "-"); metricTriangles.textContent = String(result.triangles ?? "-"); metricReconstruction.textContent = String(result.reconstruction ?? "-"); metricTime.textContent = String(result.reconstructionMs ?? "-"); } async function loadHealth() { try { const response = await fetch("/api/health"); const data = await response.json(); if (!response.ok) { throw new Error("Health check failed"); } healthEl.textContent = data.binaryExists === "True" || data.binaryExists === true ? "API online, pipeline binary ready" : "API online, binary missing"; healthEl.className = "health ok"; } catch (error) { healthEl.textContent = "API unavailable"; healthEl.className = "health error"; } } async function loadPresets() { const response = await fetch("/api/presets"); presets = await response.json(); presetSelect.innerHTML = ""; const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.textContent = "Default pipeline"; presetSelect.appendChild(defaultOption); for (const preset of presets) { const option = document.createElement("option"); option.value = preset.id; option.textContent = preset.title || preset.id; presetSelect.appendChild(option); } } async function runPipeline() { const file = fileInput.files?.[0]; if (!file) { setStatus("Select a point cloud file first.", true); return; } runButton.disabled = true; setStatus("Running pipeline..."); const formData = new FormData(); formData.append("file", file); const presetId = presetSelect.value; if (presetId) { formData.append("preset_id", presetId); } try { const response = await fetch("/api/run", { method: "POST", body: formData, }); const payload = await response.json(); if (!response.ok) { throw new Error(payload.detail || "Pipeline failed"); } updateMetrics(payload); renderGeometry(payload.points || [], payload.triangleIndices || []); setStatus(payload.stdout || "Pipeline completed."); } catch (error) { setStatus(String(error.message || error), true); } finally { runButton.disabled = false; } } runButton.addEventListener("click", runPipeline); window.addEventListener("resize", resize); resize(); animate(); loadHealth(); loadPresets().catch((error) => setStatus(`Failed to load presets: ${error}`, true));