| | 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')) { |
| | |
| | } |
| | }); |
| |
|