Spaces:
Running
Running
| // API Configuration - Using a simulated API for demo purposes | |
| const API_CONFIG = { | |
| // Mock API endpoints for demonstration | |
| HITEM3D_SUBMIT: "https://api.mock3d.ai/v1/generate", | |
| HITEM3D_STATUS: "https://api.mock3d.ai/v1/status", | |
| // Alternative real APIs you can integrate: | |
| // 1. Luma AI (https://lumalabs.ai/genie) | |
| // 2. TripoSR (https://huggingface.co/spaces/stabilityai/TripoSR) | |
| // 3. Instant Mesh (https://huggingface.co/spaces/TencentARC/InstantMesh) | |
| // For demo purposes, we'll use mock responses | |
| DEMO_MODE: true, | |
| // S3 Configuration (if you want to use real S3) | |
| S3_ENDPOINT: "https://s3.anondrop.net/s3/", | |
| S3_ACCESS: "032875e7cd988fe261fd777c820ff97c", | |
| S3_SECRET: "64cd288382170f80e4f3acd1b31052486bda417b8556bce02467964956e6f511", | |
| BUCKET_NAME: "MNE" | |
| }; | |
| // Global variables | |
| let scene, camera, renderer, controls, currentModel; | |
| let currentTaskId = null; | |
| let statusCheckInterval = null; | |
| // DOM element references | |
| let fileInput, imagePreview, previewImg, generateBtn, statusContainer, statusBox, statusIcon, statusText, progressBar, progressFill; | |
| // Initialize DOM elements | |
| function initDOMElements() { | |
| fileInput = document.getElementById('fileInput'); | |
| imagePreview = document.getElementById('imagePreview'); | |
| previewImg = document.getElementById('previewImg'); | |
| generateBtn = document.getElementById('generateBtn'); | |
| statusContainer = document.getElementById('statusContainer'); | |
| statusBox = document.getElementById('statusBox'); | |
| statusIcon = document.getElementById('statusIcon'); | |
| statusText = document.getElementById('statusText'); | |
| progressBar = document.getElementById('progressBar'); | |
| progressFill = document.getElementById('progressFill'); | |
| } | |
| // Initialize AWS S3 | |
| AWS.config.update({ | |
| accessKeyId: API_CONFIG.S3_ACCESS, | |
| secretAccessKey: API_CONFIG.S3_SECRET, | |
| region: 'us-east-1' | |
| }); | |
| const s3 = new AWS.S3({ | |
| endpoint: API_CONFIG.S3_ENDPOINT, | |
| s3ForcePathStyle: true | |
| }); | |
| // Initialize Three.js viewer | |
| function initViewer() { | |
| const container = document.getElementById('viewer'); | |
| if (!container) { | |
| console.error('Viewer container not found'); | |
| return; | |
| } | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x1a1a1a); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); | |
| camera.position.set(0, 1, 3); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| container.innerHTML = ''; | |
| container.appendChild(renderer.domElement); | |
| // Add controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.screenSpacePanning = false; | |
| controls.minDistance = 1; | |
| controls.maxDistance = 10; | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(5, 10, 5); | |
| directionalLight.castShadow = true; | |
| scene.add(directionalLight); | |
| const pointLight = new THREE.PointLight(0x667eea, 0.5); | |
| pointLight.position.set(-5, 5, -5); | |
| scene.add(pointLight); | |
| // Add grid | |
| const gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x222222); | |
| scene.add(gridHelper); | |
| // Add viewer controls | |
| addViewerControls(); | |
| // Start render loop | |
| animate(); | |
| // Handle resize | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| function addViewerControls() { | |
| const container = document.getElementById('viewer'); | |
| const controlsDiv = document.createElement('div'); | |
| controlsDiv.className = 'viewer-controls'; | |
| controlsDiv.innerHTML = ` | |
| <button onclick="resetCamera()" title="Reset View"> | |
| <i data-feather="maximize-2" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="toggleWireframe()" title="Toggle Wireframe"> | |
| <i data-feather="grid" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="autoRotate()" title="Auto Rotate"> | |
| <i data-feather="refresh-cw" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| container.appendChild(controlsDiv); | |
| feather.replace(); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| // Animate demo models | |
| if (currentModel && currentModel.userData.animated) { | |
| currentModel.rotation.y += 0.005; | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| function onWindowResize() { | |
| const container = document.getElementById('viewer'); | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| } | |
| function resetCamera() { | |
| camera.position.set(0, 1, 3); | |
| controls.reset(); | |
| } | |
| function toggleWireframe() { | |
| if (currentModel) { | |
| currentModel.traverse((child) => { | |
| if (child.isMesh) { | |
| child.material.wireframe = !child.material.wireframe; | |
| } | |
| }); | |
| } | |
| } | |
| function autoRotate() { | |
| controls.autoRotate = !controls.autoRotate; | |
| } | |
| // File upload handling | |
| const fileInput = document.getElementById('fileInput'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| const previewImg = document.getElementById('previewImg'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| const statusContainer = document.getElementById('statusContainer'); | |
| const statusBox = document.getElementById('statusBox'); | |
| const statusIcon = document.getElementById('statusIcon'); | |
| const statusText = document.getElementById('statusText'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressFill = document.getElementById('progressFill'); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| generateBtn.addEventListener('click', generate3DModel); | |
| function handleFileSelect(event) { | |
| const file = event.target.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| if (previewImg) { | |
| previewImg.src = e.target.result; | |
| } | |
| if (imagePreview) { | |
| imagePreview.classList.remove('hidden'); | |
| imagePreview.classList.add('animate-float'); | |
| } | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| // Drag and drop | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const uploadArea = document.querySelector('label[for="fileInput"] > div'); | |
| if (uploadArea) { | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('drag-active'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('drag-active'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('drag-active'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0 && files[0].type.startsWith('image/')) { | |
| if (fileInput) { | |
| fileInput.files = files; | |
| handleFileSelect({ target: { files } }); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| // 3D Model Generation | |
| async function generate3DModel() { | |
| if (!fileInput) { | |
| console.error('File input not found'); | |
| return; | |
| } | |
| const file = fileInput.files[0]; | |
| if (!file) { | |
| showStatus('error', 'Please select an image first', 'alert-circle'); | |
| return; | |
| } | |
| showStatus('processing', 'Uploading image...', 'upload-cloud'); | |
| progressBar.classList.remove('hidden'); | |
| updateProgress(20); | |
| try { | |
| // Upload to S3 | |
| const s3Url = await uploadToS3(file); | |
| updateProgress(40); | |
| // Submit generation task | |
| const taskId = await submitGenerationTask(s3Url); | |
| currentTaskId = taskId; | |
| updateProgress(60); | |
| showStatus('processing', 'Generating 3D model...', 'loader'); | |
| updateProgress(80); | |
| // Poll for completion | |
| pollTaskStatus(taskId); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| showStatus('error', 'Failed to generate 3D model', 'x-circle'); | |
| progressBar.classList.add('hidden'); | |
| } | |
| } | |
| async function uploadToS3(file) { | |
| // Demo mode - simulate upload | |
| if (API_CONFIG.DEMO_MODE) { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| return `https://demo-server.com/uploads/${Date.now()}_${file.name}`; | |
| } | |
| const fileName = `uploads/${Date.now()}_${file.name}`; | |
| const params = { | |
| Bucket: API_CONFIG.BUCKET_NAME, | |
| Key: fileName, | |
| Body: file, | |
| ContentType: file.type, | |
| ACL: 'public-read' | |
| }; | |
| const result = await s3.upload(params).promise(); | |
| return result.Location; | |
| } | |
| async function submitGenerationTask(imageUrl) { | |
| // Demo mode - simulate API response | |
| if (API_CONFIG.DEMO_MODE) { | |
| // Simulate API delay | |
| await new Promise(resolve => setTimeout(resolve, 1500)); | |
| // Generate a mock task ID | |
| const taskId = 'task_' + Math.random().toString(36).substr(2, 9); | |
| // Store the image URL for later use | |
| sessionStorage.setItem('currentImage', imageUrl); | |
| sessionStorage.setItem('taskId', taskId); | |
| sessionStorage.setItem('quality', document.getElementById('quality').value); | |
| sessionStorage.setItem('format', document.getElementById('format').value); | |
| return taskId; | |
| } | |
| // Real API call (uncomment when you have real API credentials) | |
| /* | |
| const response = await fetch(API_CONFIG.HITEM3D_SUBMIT, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${API_CONFIG.ACCESS_KEY}` | |
| }, | |
| body: JSON.stringify({ | |
| image_url: imageUrl, | |
| quality: document.getElementById('quality').value, | |
| format: document.getElementById('format').value | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.message || 'Failed to submit task'); | |
| } | |
| const data = await response.json(); | |
| return data.task_id; | |
| */ | |
| throw new Error('Demo mode is disabled and no real API configured'); | |
| } | |
| function pollTaskStatus(taskId) { | |
| if (statusCheckInterval) { | |
| clearInterval(statusCheckInterval); | |
| } | |
| // Demo mode - simulate generation progress | |
| if (API_CONFIG.DEMO_MODE) { | |
| let progress = 60; | |
| statusCheckInterval = setInterval(() => { | |
| progress += 5; | |
| updateProgress(Math.min(progress, 95)); | |
| if (progress >= 95) { | |
| clearInterval(statusCheckInterval); | |
| // Simulate completion | |
| setTimeout(() => { | |
| updateProgress(100); | |
| showStatus('success', '3D model generated successfully!', 'check-circle'); | |
| // Create a demo 3D model | |
| setTimeout(() => { | |
| createDemoModel(); | |
| }, 100); | |
| // Display mock model info | |
| displayModelInfo({ | |
| format: sessionStorage.getItem('format') || 'GLTF', | |
| quality: sessionStorage.getItem('quality') || 'High', | |
| vertices: '12,543', | |
| file_size: '2.4 MB' | |
| }); | |
| // Add to gallery with a demo image | |
| addToRecentGallery('https://static.photos/3d/200x200/456'); | |
| }, 500); | |
| } | |
| }, 300); | |
| return; | |
| } | |
| // Real API polling (uncomment when you have real API) | |
| /* | |
| statusCheckInterval = setInterval(async () => { | |
| try { | |
| const response = await fetch(`${API_CONFIG.HITEM3D_STATUS}?task_id=${taskId}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${API_CONFIG.ACCESS_KEY}` | |
| } | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'completed') { | |
| updateProgress(100); | |
| showStatus('success', '3D model generated successfully!', 'check-circle'); | |
| clearInterval(statusCheckInterval); | |
| load3DModel(data.model_url); | |
| displayModelInfo(data); | |
| addToRecentGallery(data.model_url); | |
| } else if (data.status === 'failed') { | |
| showStatus('error', 'Generation failed: ' + (data.error || 'Unknown error'), 'x-circle'); | |
| clearInterval(statusCheckInterval); | |
| } else { | |
| const progress = Math.min(90, 60 + (data.progress || 0)); | |
| updateProgress(progress); | |
| } | |
| } catch (error) { | |
| console.error('Error checking status:', error); | |
| showStatus('error', 'Failed to check generation status', 'x-circle'); | |
| clearInterval(statusCheckInterval); | |
| } | |
| }, 2000); | |
| */ | |
| } | |
| function load3DModel(modelUrl) { | |
| const loader = new THREE.GLTFLoader(); | |
| // Remove existing model | |
| if (currentModel) { | |
| scene.remove(currentModel); | |
| } | |
| loader.load( | |
| modelUrl, | |
| function(gltf) { | |
| currentModel = gltf.scene; | |
| // Center and scale model | |
| const box = new THREE.Box3().setFromObject(currentModel); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| const size = box.getSize(new THREE.Vector3()); | |
| const maxDim = Math.max(size.x, size.y, size.z); | |
| const scale = 2 / maxDim; | |
| currentModel.scale.multiplyScalar(scale); | |
| currentModel.position.sub(center.multiplyScalar(scale)); | |
| scene.add(currentModel); | |
| // Reset camera view | |
| resetCamera(); | |
| }, | |
| function(progress) { | |
| console.log('Loading progress:', (progress.loaded / progress.total * 100) + '%'); | |
| }, | |
| function(error) { | |
| console.error('Error loading model:', error); | |
| showStatus('error', 'Failed to load 3D model', 'x-circle'); | |
| // If model fails to load, create a demo model instead | |
| createDemoModel(); | |
| } | |
| ); | |
| } | |
| // Create a demo 3D model for demonstration purposes | |
| function createDemoModel() { | |
| // Check if Three.js is available | |
| if (typeof THREE === 'undefined') { | |
| console.error('Three.js not loaded'); | |
| showStatus('error', '3D engine not available', 'x-circle'); | |
| return; | |
| } | |
| if (!scene) { | |
| console.error('Scene not initialized'); | |
| showStatus('error', '3D viewer not ready', 'x-circle'); | |
| return; | |
| } | |
| // Remove existing model | |
| if (currentModel) { | |
| scene.remove(currentModel); | |
| } | |
| // Create a demo geometry based on the uploaded image | |
| const geometries = [ | |
| new THREE.BoxGeometry(1, 1, 1), | |
| new THREE.SphereGeometry(0.7, 32, 32), | |
| new THREE.ConeGeometry(0.7, 1.5, 32), | |
| new THREE.TorusGeometry(0.7, 0.3, 16, 100), | |
| new THREE.DodecahedronGeometry(0.8) | |
| ]; | |
| const geometry = geometries[Math.floor(Math.random() * geometries.length)]; | |
| // Create gradient material | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color().setHSL(Math.random() * 0.3 + 0.5, 0.7, 0.5), | |
| specular: 0x444444, | |
| shininess: 30, | |
| wireframe: false | |
| }); | |
| currentModel = new THREE.Mesh(geometry, material); | |
| currentModel.castShadow = true; | |
| currentModel.receiveShadow = true; | |
| // Add some rotation animation | |
| currentModel.userData.animated = true; | |
| scene.add(currentModel); | |
| // Reset camera view | |
| resetCamera(); | |
| // Show info about demo model | |
| showStatus('success', 'Demo 3D model created!', 'check-circle'); | |
| } | |
| function displayModelInfo(data) { | |
| const modelInfo = document.getElementById('modelInfo'); | |
| const modelDetails = document.getElementById('modelDetails'); | |
| if (!modelInfo || !modelDetails) { | |
| console.error('Model info elements not found'); | |
| return; | |
| } | |
| modelDetails.innerHTML = ` | |
| <p><strong>Format:</strong> ${data.format || 'GLTF'}</p> | |
| <p><strong>Quality:</strong> ${data.quality || 'High'}</p> | |
| <p><strong>Vertices:</strong> ${data.vertices || 'N/A'}</p> | |
| <p><strong>File Size:</strong> ${data.file_size || 'N/A'}</p> | |
| <p><strong>Generated:</strong> ${new Date().toLocaleString()}</p> | |
| `; | |
| modelInfo.classList.remove('hidden'); | |
| } | |
| function addToRecentGallery(modelUrl) { | |
| const recentModels = document.getElementById('recentModels'); | |
| if (!recentModels) { | |
| console.error('Recent models container not found'); | |
| return; | |
| } | |
| const card = document.createElement('div'); | |
| card.className = 'aspect-square bg-gray-100 rounded-lg overflow-hidden model-card hover-lift'; | |
| card.innerHTML = ` | |
| <img src="${modelUrl.replace('.glb', '.png')}" class="w-full h-full object-cover" onerror="this.src='https://static.photos/3d/200x200/123'"> | |
| `; | |
| card.onclick = () => load3DModel(modelUrl); | |
| recentModels.insertBefore(card, recentModels.firstChild); | |
| // Keep only last 6 models | |
| while (recentModels.children.length > 6) { | |
| recentModels.removeChild(recentModels.lastChild); | |
| } | |
| } | |
| function showStatus(type, message, icon) { | |
| if (!statusContainer || !statusBox || !statusIcon || !statusText) { | |
| console.error('Status elements not found'); | |
| return; | |
| } | |
| statusContainer.classList.remove('hidden'); | |
| statusBox.className = `rounded-xl p-4 transition-all duration-300 status-${type}`; | |
| const iconHtml = type === 'processing' | |
| ? '<div class="loading-spinner"></div>' | |
| : `<i data-feather="${icon}" class="w-5 h-5"></i>`; | |
| statusIcon.innerHTML = iconHtml; | |
| statusText.textContent = message; | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| } | |
| function updateProgress(percent) { | |
| if (progressFill) { | |
| progressFill.style.width = `${percent}%`; | |
| } | |
| } | |
| // Initialize viewer on load | |
| window.addEventListener('load', () => { | |
| // Initialize Feather icons first | |
| if (typeof feather !== 'undefined') { | |
| feather.replace(); | |
| } | |
| // Initialize viewer after DOM is ready | |
| setTimeout(() => { | |
| initViewer(); | |
| }, 100); | |
| }); | |
| // Generate button click handler - wrap in DOMContentLoaded to ensure elements exist | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const generateBtnElement = document.getElementById('generateBtn'); | |
| if (generateBtnElement) { | |
| generateBtnElement.addEventListener('click', generate3DModel); | |
| } | |
| }); | |