File size: 6,080 Bytes
6fe70f4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
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 = `
<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>
`;
}
};
// 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
}
});
|