| import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d'; |
| import * as THREE from 'three'; |
|
|
| |
| window.THREE = THREE; |
| window.GaussianSplats3D = GaussianSplats3D; |
|
|
| |
| let viewer = null; |
| let initialCameraPosition = null; |
| let initialCameraTarget = null; |
|
|
| |
| 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'); |
|
|
| |
| window.showViewer = async function (result) { |
| |
| viewerFilename.textContent = result.ply_filename; |
|
|
| |
| card.classList.add('hidden'); |
| header.classList.add('minimized'); |
| viewerContainer.classList.add('active'); |
|
|
| |
| 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); |
|
|
| |
| if (viewer) { |
| viewer.dispose(); |
| viewer = null; |
| |
| while (viewerCanvasContainer.firstChild) { |
| if (viewerCanvasContainer.firstChild.id !== 'controlsHint') { |
| viewerCanvasContainer.removeChild(viewerCanvasContainer.firstChild); |
| } else { |
| break; |
| } |
| } |
| } |
|
|
| |
| await new Promise(resolve => setTimeout(resolve, 100)); |
|
|
| try { |
| |
| 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, |
| }); |
|
|
| |
| await viewer.addSplatScene(plyUrl, { |
| splatAlphaRemovalThreshold: 5, |
| showLoadingUI: false, |
| progressiveLoad: false, |
| format: GaussianSplats3D.SceneFormat.Ply, |
| }); |
|
|
| viewer.start(); |
|
|
| |
| if (viewer.camera) { |
| initialCameraPosition = viewer.camera.position.clone(); |
| initialCameraTarget = new THREE.Vector3(0, 0, 0); |
| } |
|
|
| |
| setTimeout(() => { |
| controlsHint.style.opacity = '0'; |
| }, 5000); |
|
|
| |
| URL.revokeObjectURL(plyUrl); |
|
|
| } catch (error) { |
| console.error('Error loading Gaussian Splat:', error); |
| viewerCanvasContainer.innerHTML = ` |
| <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #ef4444; text-align: center; padding: 2rem;"> |
| <div> |
| <p style="font-size: 1.1rem; margin-bottom: 0.5rem;">Failed to load 3D viewer</p> |
| <p style="font-size: 0.875rem; opacity: 0.7;">${error.message}</p> |
| </div> |
| </div> |
| `; |
| } |
| }; |
|
|
| |
| backBtn.addEventListener('click', () => { |
| |
| if (viewer) { |
| viewer.dispose(); |
| viewer = null; |
| } |
|
|
| |
| const hint = document.getElementById('controlsHint'); |
| viewerCanvasContainer.innerHTML = ''; |
| if (hint) { |
| hint.style.opacity = '1'; |
| viewerCanvasContainer.appendChild(hint); |
| } |
|
|
| |
| const dropZone = document.getElementById('dropZone'); |
| const fileList = document.getElementById('fileList'); |
| const submitBtn = document.getElementById('submitBtn'); |
| dropZone.style.display = ''; |
| fileList.style.display = ''; |
| submitBtn.style.display = ''; |
|
|
| |
| card.classList.remove('hidden'); |
| header.classList.remove('minimized'); |
| viewerContainer.classList.remove('active'); |
| }); |
|
|
| |
| 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(); |
| } |
| } |
| }); |
|
|
| |
| 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); |
| } |
| }); |
|
|
| |
| window.addEventListener('resize', () => { |
| if (viewer && viewerContainer.classList.contains('active')) { |
| |
| } |
| }); |
|
|