Spaces:
Build error
Build error
| <html> | |
| <head> | |
| <title>Simple PLY Viewer</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| font-family: Arial, sans-serif; | |
| background: #222; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 80vh; | |
| } | |
| #upload-container { | |
| padding: 20px; | |
| background: #333; | |
| border-bottom: 1px solid #444; | |
| color: white; | |
| } | |
| #file-input { | |
| display: none; | |
| } | |
| .upload-btn { | |
| background: #4CAF50; | |
| color: white; | |
| padding: 10px 15px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| } | |
| .upload-btn:hover { | |
| background: #45a049; | |
| } | |
| #loading { | |
| display: none; | |
| margin-top: 10px; | |
| color: #aaa; | |
| } | |
| #controls { | |
| padding: 10px; | |
| background: #333; | |
| color: white; | |
| } | |
| .control-btn { | |
| padding: 8px 12px; | |
| margin-right: 5px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| background: #555; | |
| color: white; | |
| } | |
| .control-btn:hover { | |
| background: #666; | |
| } | |
| #instructions { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| color: white; | |
| background: rgba(0,0,0,0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="upload-container"> | |
| <label for="file-input" class="upload-btn">Select PLY/DRC Files</label> | |
| <input id="file-input" type="file" accept=".ply,.drc" multiple> | |
| <div id="loading">Loading...</div> | |
| </div> | |
| <div id="controls"> | |
| <button id="rotate-toggle" class="control-btn">Pause Rotation</button> | |
| <button id="reset-view" class="control-btn">Reset View</button> | |
| </div> | |
| <div id="instructions"> | |
| Controls: WASD to move, Mouse drag to look around | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/PLYLoader.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/DRACOLoader.js"></script> | |
| <script> | |
| // Initialize scene | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x222222); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); | |
| const renderer = new THREE.WebGLRenderer({ | |
| antialias: true, | |
| alpha: true // 允许透明背景 | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.body.appendChild(renderer.domElement); | |
| // Movement variables | |
| const moveSpeed = 0.01; | |
| const maxDistance = 0.3; // Maximum movement distance from origin | |
| const keys = { | |
| w: false, | |
| a: false, | |
| s: false, | |
| d: false | |
| }; | |
| let isMouseDown = false; | |
| let previousMousePosition = { | |
| x: 0, | |
| y: 0 | |
| }; | |
| let rotationSpeed = 0.0005; | |
| let isRotating = true; | |
| let sceneCenter = new THREE.Vector3(); | |
| let animationId = null; | |
| let isAtLimit = false; | |
| // Event listeners for keyboard controls | |
| document.addEventListener('keydown', (event) => { | |
| switch (event.key.toLowerCase()) { | |
| case 'w': keys.w = true; break; | |
| case 'a': keys.a = true; break; | |
| case 's': keys.s = true; break; | |
| case 'd': keys.d = true; break; | |
| } | |
| // Restart animation if it was stopped | |
| if (!animationId && (keys.w || keys.a || keys.s || keys.d)) { | |
| animate(); | |
| } | |
| }); | |
| document.addEventListener('keyup', (event) => { | |
| switch (event.key.toLowerCase()) { | |
| case 'w': keys.w = false; break; | |
| case 'a': keys.a = false; break; | |
| case 's': keys.s = false; break; | |
| case 'd': keys.d = false; break; | |
| } | |
| }); | |
| // Mouse drag for camera-relative rotation | |
| renderer.domElement.addEventListener('mousedown', (event) => { | |
| isMouseDown = true; | |
| previousMousePosition = { | |
| x: event.clientX, | |
| y: event.clientY | |
| }; | |
| // Prevent default to avoid text selection | |
| event.preventDefault(); | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| isMouseDown = false; | |
| }); | |
| document.addEventListener('mousemove', (event) => { | |
| if (isMouseDown) { | |
| const deltaMove = { | |
| x: event.clientX - previousMousePosition.x, | |
| y: event.clientY - previousMousePosition.y | |
| }; | |
| const quaternion = new THREE.Quaternion(); | |
| // Horizontal rotation (Y-axis) | |
| const yQuaternion = new THREE.Quaternion(); | |
| yQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -deltaMove.x * 0.002); | |
| quaternion.multiply(yQuaternion); | |
| // Vertical rotation (X-axis) | |
| const xQuaternion = new THREE.Quaternion(); | |
| xQuaternion.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -deltaMove.y * 0.002); | |
| quaternion.multiply(xQuaternion); | |
| // Apply rotation | |
| camera.quaternion.multiply(quaternion); | |
| previousMousePosition = { | |
| x: event.clientX, | |
| y: event.clientY | |
| }; | |
| } | |
| }); | |
| // Prevent context menu on canvas | |
| renderer.domElement.addEventListener('contextmenu', (event) => { | |
| event.preventDefault(); | |
| }); | |
| // initialize all loader | |
| const loader = new THREE.PLYLoader(); | |
| const dracoLoader = new THREE.DRACOLoader(); | |
| dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/libs/draco/'); | |
| let loadedCount = 0; | |
| let totalFiles = 0; | |
| // File upload handler | |
| document.getElementById('file-input').addEventListener('change', function(e) { | |
| const files = e.target.files; | |
| if (files.length === 0) return; | |
| document.getElementById('loading').style.display = 'block'; | |
| totalFiles = files.length; | |
| loadedCount = 0; | |
| // Clear existing models from scene | |
| scene.children.slice().forEach(child => { | |
| if (child instanceof THREE.Mesh) { | |
| if (child.geometry) child.geometry.dispose(); | |
| if (child.material) child.material.dispose(); | |
| scene.remove(child); | |
| } | |
| }); | |
| // Load each PLY file | |
| Array.from(files).forEach((file, index) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| try { | |
| if (file.name.endsWith('.ply')) { | |
| const geometry = loader.parse(event.target.result); | |
| const material = new THREE.MeshBasicMaterial({ | |
| side: THREE.DoubleSide, | |
| vertexColors: true, | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.rotateX(-Math.PI / 2); | |
| mesh.rotateZ(-Math.PI / 2); | |
| scene.add(mesh); | |
| } else if (file.name.endsWith('.drc')) { | |
| // Draco file handling | |
| dracoLoader.decodeDracoFile(event.target.result, function(geometry) { | |
| // Compute normals for Draco geometry if missing | |
| if (!geometry.attributes.normal) { | |
| geometry.computeVertexNormals(); | |
| } | |
| const material = new THREE.MeshBasicMaterial({ | |
| side: THREE.DoubleSide, | |
| vertexColors: true, | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.rotateX(-Math.PI / 2); | |
| mesh.rotateZ(-Math.PI / 2); | |
| scene.add(mesh); | |
| }); | |
| } | |
| loadedCount++; | |
| if(loadedCount === totalFiles) { | |
| document.getElementById('loading').style.display = 'none'; | |
| positionCamera(); | |
| isRotating = true; | |
| document.getElementById('rotate-toggle').textContent = 'Pause Rotation'; | |
| animate(); // Start animation after loading | |
| } | |
| } catch (error) { | |
| console.error('Error loading PLY file:', error); | |
| loadedCount++; | |
| if(loadedCount === totalFiles) { | |
| document.getElementById('loading').style.display = 'none'; | |
| if (scene.children.filter(c => c instanceof THREE.Mesh).length > 0) { | |
| positionCamera(); | |
| isRotating = true; | |
| document.getElementById('rotate-toggle').textContent = 'Pause Rotation'; | |
| animate(); // Start animation after loading | |
| } | |
| } | |
| } | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| }); | |
| // Position camera reset | |
| function positionCamera() { | |
| // Initial camera position | |
| scene.rotation.y = 0; | |
| camera.position.set(0, 0, 0); | |
| camera.lookAt(0, 0, -10); | |
| } | |
| // Control button events | |
| document.getElementById('rotate-toggle').addEventListener('click', function() { | |
| isRotating = !isRotating; | |
| this.textContent = isRotating ? 'Pause Rotation' : 'Start Rotation'; | |
| }); | |
| document.getElementById('reset-view').addEventListener('click', function() { | |
| positionCamera(); | |
| isAtLimit = false; | |
| isRotating = true; | |
| if (!animationId) { | |
| animate(); | |
| } | |
| }); | |
| // Function to limit movement distance | |
| function limitMovement(position) { | |
| const distance = Math.sqrt(position.x * position.x + position.z * position.z); | |
| if (distance > maxDistance) { | |
| const ratio = maxDistance / distance; | |
| position.x *= ratio; | |
| position.z *= ratio; | |
| return true; // Reached limit | |
| } | |
| return false; // Not at limit | |
| } | |
| // Animation loop | |
| function animate() { | |
| // Calculate movement direction based on camera orientation | |
| if (keys.w || keys.a || keys.s || keys.d) { | |
| // Get camera's forward vector (negative Z-axis) | |
| const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); | |
| // Get camera's right vector (positive X-axis) | |
| const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion); | |
| // Ignore Y component to keep movement horizontal | |
| forward.y = 0; | |
| right.y = 0; | |
| forward.normalize(); | |
| right.normalize(); | |
| // Calculate movement direction | |
| const movement = new THREE.Vector3(); | |
| if (keys.w) movement.add(forward); // Forward | |
| if (keys.s) movement.sub(forward); // Backward | |
| if (keys.a) movement.sub(right); // Left | |
| if (keys.d) movement.add(right); // Right | |
| // Only normalize if there's actual movement | |
| if (movement.length() > 0) { | |
| movement.normalize().multiplyScalar(moveSpeed); | |
| } | |
| // Store current Y position | |
| const currentY = camera.position.y; | |
| // Apply movement | |
| camera.position.add(movement); | |
| // Restore Y position to keep movement horizontal | |
| camera.position.y = currentY; | |
| // Check if reached movement limit | |
| isAtLimit = limitMovement(camera.position); | |
| } | |
| // Auto-rotation if enabled | |
| if (isRotating && scene.children.some(c => c instanceof THREE.Mesh)) { | |
| scene.rotation.y += rotationSpeed; | |
| } | |
| // Create a target position slightly in front of the camera | |
| const targetPosition = new THREE.Vector3(); | |
| targetPosition.copy(camera.position); | |
| targetPosition.add(new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion)); | |
| // Smoothly look at a point slightly in front of the camera | |
| camera.lookAt(targetPosition); | |
| renderer.render(scene, camera); | |
| // keep running | |
| animationId = requestAnimationFrame(animate); | |
| } | |
| // Initial animation start | |
| animate(); | |
| // Window resize handler | |
| window.addEventListener('resize', function() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| </script> | |
| </body> | |
| </html> |