import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d'; import * as THREE from 'three'; // Make THREE available globally for the viewer window.THREE = THREE; window.GaussianSplats3D = GaussianSplats3D; // Viewer state let viewer = null; let initialCameraPosition = null; let initialCameraTarget = null; // DOM elements const card = document.querySelector('.card'); const header = document.querySelector('.header'); const viewerContainer = document.getElementById('viewerContainer'); const viewerCanvasContainer = document.getElementById('viewerCanvasContainer'); const viewerFilename = document.getElementById('viewerFilename'); const backBtn = document.getElementById('backBtn'); const resetViewBtn = document.getElementById('resetViewBtn'); const downloadBtn = document.getElementById('downloadBtn'); const controlsHint = document.getElementById('controlsHint'); // Expose showViewer to global scope window.showViewer = async function (result) { // Update filename badge viewerFilename.textContent = result.ply_filename; // Hide card and show viewer card.classList.add('hidden'); header.classList.add('minimized'); viewerContainer.classList.add('active'); // Decode base64 PLY data const binaryString = atob(result.ply_data); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const plyBlob = new Blob([bytes], { type: 'application/octet-stream' }); const plyUrl = URL.createObjectURL(plyBlob); // Clean up previous viewer if exists if (viewer) { viewer.dispose(); viewer = null; // Clear the container while (viewerCanvasContainer.firstChild) { if (viewerCanvasContainer.firstChild.id !== 'controlsHint') { viewerCanvasContainer.removeChild(viewerCanvasContainer.firstChild); } else { break; } } } // Wait for container to be visible and sized await new Promise(resolve => setTimeout(resolve, 100)); try { // Create the Gaussian Splat viewer viewer = new GaussianSplats3D.Viewer({ cameraUp: [0, -1, 0], initialCameraPosition: [0, 0, -3], initialCameraLookAt: [0, 0, 0], rootElement: viewerCanvasContainer, sharedMemoryForWorkers: false, dynamicScene: false, sceneRevealMode: GaussianSplats3D.SceneRevealMode.Instant, antialiased: true, }); // Load the PLY file - specify format since blob URLs don't have extensions await viewer.addSplatScene(plyUrl, { splatAlphaRemovalThreshold: 5, showLoadingUI: false, progressiveLoad: false, format: GaussianSplats3D.SceneFormat.Ply, }); viewer.start(); // Store initial camera state for reset if (viewer.camera) { initialCameraPosition = viewer.camera.position.clone(); initialCameraTarget = new THREE.Vector3(0, 0, 0); } // Hide controls hint after a few seconds setTimeout(() => { controlsHint.style.opacity = '0'; }, 5000); // Cleanup blob URL after loading URL.revokeObjectURL(plyUrl); } catch (error) { console.error('Error loading Gaussian Splat:', error); viewerCanvasContainer.innerHTML = `

Failed to load 3D viewer

${error.message}

`; } }; // Back button handler backBtn.addEventListener('click', () => { // Clean up viewer if (viewer) { viewer.dispose(); viewer = null; } // Clear the canvas container except for the hint const hint = document.getElementById('controlsHint'); viewerCanvasContainer.innerHTML = ''; if (hint) { hint.style.opacity = '1'; viewerCanvasContainer.appendChild(hint); } // Restore upload UI elements const dropZone = document.getElementById('dropZone'); const fileList = document.getElementById('fileList'); const submitBtn = document.getElementById('submitBtn'); dropZone.style.display = ''; fileList.style.display = ''; submitBtn.style.display = ''; // Show card and hide viewer card.classList.remove('hidden'); header.classList.remove('minimized'); viewerContainer.classList.remove('active'); }); // Reset view button handler resetViewBtn.addEventListener('click', () => { if (viewer && viewer.camera && initialCameraPosition) { viewer.camera.position.copy(initialCameraPosition); viewer.camera.lookAt(initialCameraTarget); if (viewer.controls) { viewer.controls.target.copy(initialCameraTarget); viewer.controls.update(); } } }); // Download button handler downloadBtn.addEventListener('click', () => { if (window.currentPlyData && window.currentPlyFilename) { const binaryString = atob(window.currentPlyData); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const blob = new Blob([bytes], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = window.currentPlyFilename; a.click(); URL.revokeObjectURL(url); } }); // Handle window resize for the viewer window.addEventListener('resize', () => { if (viewer && viewerContainer.classList.contains('active')) { // The viewer should handle resize automatically } });