Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Object Remesher | 3D Mesh Optimization</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed #94a3b8; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #6366f1; | |
| background-color: #e0e7ff; | |
| } | |
| .mesh-preview { | |
| background: linear-gradient(45deg, #f3f4f6 25%, transparent 25%), | |
| linear-gradient(-45deg, #f3f4f6 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #f3f4f6 75%), | |
| linear-gradient(-45deg, transparent 75%, #f3f4f6 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
| } | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| } | |
| canvas { | |
| touch-action: none; | |
| } | |
| .tooltip { | |
| position: relative; | |
| } | |
| .tooltip-text { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: #1e293b; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltip-text { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-800"> | |
| <div class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-indigo-600 text-white shadow-md"> | |
| <div class="container mx-auto px-4 py-4 flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-cube text-2xl"></i> | |
| <h1 class="text-2xl font-bold">Object Remesher</h1> | |
| </div> | |
| <nav> | |
| <ul class="flex space-x-6"> | |
| <li><a href="#" class="hover:text-indigo-200 transition">Home</a></li> | |
| <li><a href="#" class="hover:text-indigo-200 transition">Features</a></li> | |
| <li><a href="#" class="hover:text-indigo-200 transition">Documentation</a></li> | |
| <li><a href="#" class="hover:text-indigo-200 transition">Contact</a></li> | |
| </ul> | |
| </nav> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-grow container mx-auto px-4 py-8"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Left Panel - Upload and Controls --> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-upload mr-2 text-indigo-500"></i> | |
| Upload 3D Model | |
| </h2> | |
| <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-4"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-indigo-400 mb-2"></i> | |
| <p class="text-gray-500">Drag & drop your 3D model file here</p> | |
| <p class="text-sm text-gray-400 mt-1">Supported formats: .obj, .stl, .fbx, .gltf</p> | |
| <input type="file" id="fileInput" class="hidden" accept=".obj,.stl,.fbx,.gltf"> | |
| <button id="browseBtn" class="mt-4 bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md transition"> | |
| Browse Files | |
| </button> | |
| </div> | |
| <div id="fileInfo" class="hidden bg-gray-100 p-3 rounded-md mb-4"> | |
| <div class="flex justify-between items-center"> | |
| <div> | |
| <p class="font-medium" id="fileName">model.obj</p> | |
| <p class="text-sm text-gray-500" id="fileSize">12.4 MB</p> | |
| </div> | |
| <button id="removeFile" class="text-red-500 hover:text-red-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-sliders-h mr-2 text-indigo-500"></i> | |
| Remeshing Settings | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Target Density</label> | |
| <div class="flex items-center space-x-2"> | |
| <input type="range" min="0.1" max="2" step="0.1" value="1" class="w-full" id="densitySlider"> | |
| <span class="text-sm w-10 text-center" id="densityValue">1.0</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Preserve Features</label> | |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| <option>Sharp Edges</option> | |
| <option selected>All Features</option> | |
| <option>None</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Remeshing Method</label> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button class="bg-indigo-100 text-indigo-700 border border-indigo-300 rounded-md px-3 py-2 text-sm font-medium">Isotropic</button> | |
| <button class="bg-white text-gray-700 border border-gray-300 rounded-md px-3 py-2 text-sm font-medium">Anisotropic</button> | |
| <button class="bg-white text-gray-700 border border-gray-300 rounded-md px-3 py-2 text-sm font-medium">Quad Dominant</button> | |
| <button class="bg-white text-gray-700 border border-gray-300 rounded-md px-3 py-2 text-sm font-medium">Adaptive</button> | |
| </div> | |
| </div> | |
| <div class="pt-2"> | |
| <button id="remeshBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-md font-medium transition flex items-center justify-center"> | |
| <i class="fas fa-magic mr-2"></i> | |
| Remesh Model | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-chart-bar mr-2 text-indigo-500"></i> | |
| Mesh Statistics | |
| </h2> | |
| <div class="space-y-3"> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Original Faces:</span> | |
| <span class="font-medium" id="originalFaces">0</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Remeshed Faces:</span> | |
| <span class="font-medium" id="remeshedFaces">0</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Reduction:</span> | |
| <span class="font-medium" id="reduction">0%</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">File Size:</span> | |
| <span class="font-medium" id="fileSizeStat">0 MB</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel - 3D Viewer and Results --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> | |
| <div class="flex justify-between items-center border-b border-gray-200 px-6 py-4"> | |
| <h2 class="text-xl font-semibold flex items-center"> | |
| <i class="fas fa-cube mr-2 text-indigo-500"></i> | |
| 3D Model Viewer | |
| </h2> | |
| <div class="flex space-x-2"> | |
| <button class="tooltip p-2 text-gray-500 hover:text-indigo-500 rounded-full hover:bg-gray-100 transition"> | |
| <i class="fas fa-rotate"></i> | |
| <span class="tooltip-text">Rotate Model</span> | |
| </button> | |
| <button class="tooltip p-2 text-gray-500 hover:text-indigo-500 rounded-full hover:bg-gray-100 transition"> | |
| <i class="fas fa-arrows-up-down-left-right"></i> | |
| <span class="tooltip-text">Pan Model</span> | |
| </button> | |
| <button class="tooltip p-2 text-gray-500 hover:text-indigo-500 rounded-full hover:bg-gray-100 transition"> | |
| <i class="fas fa-search-plus"></i> | |
| <span class="tooltip-text">Zoom In/Out</span> | |
| </button> | |
| <button class="tooltip p-2 text-gray-500 hover:text-indigo-500 rounded-full hover:bg-gray-100 transition"> | |
| <i class="fas fa-border-all"></i> | |
| <span class="tooltip-text">Toggle Wireframe</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mesh-preview h-96 relative"> | |
| <canvas id="modelCanvas" class="w-full h-full"></canvas> | |
| <div id="loadingOverlay" class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> | |
| <div class="text-center text-white"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mx-auto mb-4"></div> | |
| <p>Processing model...</p> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5 mt-4 mx-auto max-w-md"> | |
| <div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> | |
| <div class="border-b border-gray-200"> | |
| <nav class="flex -mb-px"> | |
| <button class="tab-button active py-4 px-6 text-center border-b-2 font-medium text-sm border-indigo-500 text-indigo-600"> | |
| <i class="fas fa-list mr-2"></i> | |
| Comparison | |
| </button> | |
| <button class="tab-button py-4 px-6 text-center border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"> | |
| <i class="fas fa-download mr-2"></i> | |
| Export | |
| </button> | |
| <button class="tab-button py-4 px-6 text-center border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"> | |
| <i class="fas fa-history mr-2"></i> | |
| History | |
| </button> | |
| </nav> | |
| </div> | |
| <div class="p-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <h3 class="font-medium text-gray-700 mb-3">Original Model</h3> | |
| <div class="bg-gray-100 rounded-lg p-4 h-48 flex items-center justify-center"> | |
| <p class="text-gray-500">Original mesh will appear here</p> | |
| </div> | |
| <div class="mt-3 text-sm text-gray-600"> | |
| <p><span class="font-medium">Vertices:</span> <span id="originalVertices">0</span></p> | |
| <p><span class="font-medium">Faces:</span> <span id="originalFaces2">0</span></p> | |
| </div> | |
| </div> | |
| <div> | |
| <h3 class="font-medium text-gray-700 mb-3">Remeshed Model</h3> | |
| <div class="bg-gray-100 rounded-lg p-4 h-48 flex items-center justify-center"> | |
| <p class="text-gray-500">Optimized mesh will appear here</p> | |
| </div> | |
| <div class="mt-3 text-sm text-gray-600"> | |
| <p><span class="font-medium">Vertices:</span> <span id="remeshedVertices">0</span></p> | |
| <p><span class="font-medium">Faces:</span> <span id="remeshedFaces2">0</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6"> | |
| <button class="bg-green-600 hover:bg-green-700 text-white py-2 px-6 rounded-md font-medium transition flex items-center"> | |
| <i class="fas fa-download mr-2"></i> | |
| Download Remeshed Model | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-800 text-white py-8"> | |
| <div class="container mx-auto px-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-8"> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-4">Object Remesher</h3> | |
| <p class="text-gray-400">Advanced 3D mesh optimization tool for professionals and enthusiasts.</p> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-4">Quick Links</h3> | |
| <ul class="space-y-2"> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Home</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Features</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Pricing</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Blog</a></li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-4">Support</h3> | |
| <ul class="space-y-2"> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Documentation</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Tutorials</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">API Reference</a></li> | |
| <li><a href="#" class="text-gray-400 hover:text-white transition">Contact Us</a></li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-4">Connect</h3> | |
| <div class="flex space-x-4"> | |
| <a href="#" class="text-gray-400 hover:text-white transition"><i class="fab fa-twitter"></i></a> | |
| <a href="#" class="text-gray-400 hover:text-white transition"><i class="fab fa-github"></i></a> | |
| <a href="#" class="text-gray-400 hover:text-white transition"><i class="fab fa-discord"></i></a> | |
| <a href="#" class="text-gray-400 hover:text-white transition"><i class="fab fa-youtube"></i></a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="border-t border-gray-700 mt-8 pt-6 text-center text-gray-400"> | |
| <p>© 2023 Object Remesher. All rights reserved.</p> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const browseBtn = document.getElementById('browseBtn'); | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const removeFile = document.getElementById('removeFile'); | |
| const remeshBtn = document.getElementById('remeshBtn'); | |
| const densitySlider = document.getElementById('densitySlider'); | |
| const densityValue = document.getElementById('densityValue'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const tabButtons = document.querySelectorAll('.tab-button'); | |
| // Event Listeners | |
| browseBtn.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| dropzone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.add('active'); | |
| }); | |
| dropzone.addEventListener('dragleave', () => { | |
| dropzone.classList.remove('active'); | |
| }); | |
| dropzone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('active'); | |
| if (e.dataTransfer.files.length) { | |
| fileInput.files = e.dataTransfer.files; | |
| handleFileSelect({ target: fileInput }); | |
| } | |
| }); | |
| removeFile.addEventListener('click', () => { | |
| fileInfo.classList.add('hidden'); | |
| fileInput.value = ''; | |
| }); | |
| densitySlider.addEventListener('input', () => { | |
| densityValue.textContent = densitySlider.value; | |
| }); | |
| remeshBtn.addEventListener('click', () => { | |
| if (!fileInput.files.length) { | |
| alert('Please upload a 3D model first'); | |
| return; | |
| } | |
| loadingOverlay.classList.remove('hidden'); | |
| simulateProcessing(); | |
| }); | |
| tabButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| tabButtons.forEach(btn => btn.classList.remove('active', 'text-indigo-600', 'border-indigo-500')); | |
| tabButtons.forEach(btn => btn.classList.add('text-gray-500', 'border-transparent')); | |
| button.classList.add('active', 'text-indigo-600', 'border-indigo-500'); | |
| button.classList.remove('text-gray-500', 'border-transparent'); | |
| }); | |
| }); | |
| // Functions | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| fileInfo.classList.remove('hidden'); | |
| // Update stats | |
| document.getElementById('originalFaces').textContent = Math.floor(Math.random() * 50000) + 10000; | |
| document.getElementById('originalFaces2').textContent = document.getElementById('originalFaces').textContent; | |
| document.getElementById('originalVertices').textContent = Math.floor(Math.random() * 25000) + 5000; | |
| document.getElementById('fileSizeStat').textContent = fileSize.textContent; | |
| // Simulate loading a model | |
| simulateModelLoad(); | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| function simulateModelLoad() { | |
| loadingOverlay.classList.remove('hidden'); | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 10; | |
| if (progress > 100) progress = 100; | |
| progressBar.style.width = `${progress}%`; | |
| if (progress === 100) { | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| loadingOverlay.classList.add('hidden'); | |
| progressBar.style.width = '0%'; | |
| // Initialize a simple 3D viewer (placeholder) | |
| init3DViewer(); | |
| }, 500); | |
| } | |
| }, 100); | |
| } | |
| function simulateProcessing() { | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 5; | |
| if (progress > 100) progress = 100; | |
| progressBar.style.width = `${progress}%`; | |
| if (progress === 100) { | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| loadingOverlay.classList.add('hidden'); | |
| progressBar.style.width = '0%'; | |
| // Update stats with "remeshed" values | |
| const originalFaces = parseInt(document.getElementById('originalFaces').textContent); | |
| const reduction = Math.floor(Math.random() * 70) + 10; // 10-80% reduction | |
| const remeshedFaces = Math.floor(originalFaces * (100 - reduction) / 100); | |
| document.getElementById('remeshedFaces').textContent = remeshedFaces; | |
| document.getElementById('remeshedFaces2').textContent = remeshedFaces; | |
| document.getElementById('remeshedVertices').textContent = Math.floor(remeshedFaces * 0.6); | |
| document.getElementById('reduction').textContent = `${reduction}%`; | |
| // Update the 3D viewer with "remeshed" model | |
| update3DViewer(); | |
| }, 500); | |
| } | |
| }, 200); | |
| } | |
| function init3DViewer() { | |
| const canvas = document.getElementById('modelCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set canvas dimensions | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = canvas.offsetHeight; | |
| // Draw a placeholder "3D" cube | |
| drawPlaceholderModel(ctx, canvas.width, canvas.height); | |
| // Add some basic interactivity | |
| let isDragging = false; | |
| let lastX = 0; | |
| let lastY = 0; | |
| let rotationX = 20; | |
| let rotationY = 30; | |
| canvas.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (!isDragging) return; | |
| const deltaX = e.clientX - lastX; | |
| const deltaY = e.clientY - lastY; | |
| rotationY += deltaX * 0.5; | |
| rotationX += deltaY * 0.5; | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| drawPlaceholderModel(ctx, canvas.width, canvas.height, rotationX, rotationY); | |
| }); | |
| canvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| canvas.addEventListener('mouseleave', () => { | |
| isDragging = false; | |
| }); | |
| } | |
| function update3DViewer() { | |
| const canvas = document.getElementById('modelCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Draw a different "remeshed" version | |
| drawPlaceholderModel(ctx, canvas.width, canvas.height, 20, 30, true); | |
| } | |
| function drawPlaceholderModel(ctx, width, height, rotX = 20, rotY = 30, isRemeshed = false) { | |
| ctx.clearRect(0, 0, width, height); | |
| // Simple 3D cube projection | |
| const size = Math.min(width, height) * 0.3; | |
| const centerX = width / 2; | |
| const centerY = height / 2; | |
| // Convert angles to radians | |
| const radX = rotX * Math.PI / 180; | |
| const radY = rotY * Math.PI / 180; | |
| // Cube vertices | |
| const vertices = [ | |
| { x: -1, y: -1, z: -1 }, | |
| { x: 1, y: -1, z: -1 }, | |
| { x: 1, y: 1, z: -1 }, | |
| { x: -1, y: 1, z: -1 }, | |
| { x: -1, y: -1, z: 1 }, | |
| { x: 1, y: -1, z: 1 }, | |
| { x: 1, y: 1, z: 1 }, | |
| { x: -1, y: 1, z: 1 } | |
| ]; | |
| // Project 3D to 2D | |
| const projected = vertices.map(v => { | |
| // Rotate around X and Y axes | |
| let x = v.x; | |
| let y = v.y * Math.cos(radX) - v.z * Math.sin(radX); | |
| let z = v.y * Math.sin(radX) + v.z * Math.cos(radX); | |
| let x2 = x * Math.cos(radY) - z * Math.sin(radY); | |
| let z2 = x * Math.sin(radY) + z * Math.cos(radY); | |
| // Perspective projection | |
| const distance = 4; | |
| const factor = distance / (distance + z2); | |
| return { | |
| x: centerX + x2 * factor * size, | |
| y: centerY + y * factor * size | |
| }; | |
| }); | |
| // Draw edges | |
| const edges = [ | |
| [0, 1], [1, 2], [2, 3], [3, 0], // back face | |
| [4, 5], [5, 6], [6, 7], [7, 4], // front face | |
| [0, 4], [1, 5], [2, 6], [3, 7] // connecting edges | |
| ]; | |
| ctx.strokeStyle = isRemeshed ? '#10b981' : '#6366f1'; | |
| ctx.lineWidth = isRemeshed ? 1 : 2; | |
| edges.forEach(edge => { | |
| const [a, b] = edge; | |
| ctx.beginPath(); | |
| ctx.moveTo(projected[a].x, projected[a].y); | |
| ctx.lineTo(projected[b].x, projected[b].y); | |
| ctx.stroke(); | |
| }); | |
| if (isRemeshed) { | |
| // Add some "remeshed" details | |
| const faceCenters = [ | |
| { x: 0, y: 0, z: -1.2 }, // back | |
| { x: 0, y: 0, z: 1.2 }, // front | |
| { x: -1.2, y: 0, z: 0 }, // left | |
| { x: 1.2, y: 0, z: 0 }, // right | |
| { x: 0, y: -1.2, z: 0 }, // bottom | |
| { x: 0, y: 1.2, z: 0 } // top | |
| ]; | |
| faceCenters.forEach(center => { | |
| // Project this point | |
| let x = center.x; | |
| let y = center.y * Math.cos(radX) - center.z * Math.sin(radX); | |
| let z = center.y * Math.sin(radX) + center.z * Math.cos(radX); | |
| let x2 = x * Math.cos(radY) - z * Math.sin(radY); | |
| let z2 = x * Math.sin(radY) + z * Math.cos(radY); | |
| const distance = 4; | |
| const factor = distance / (distance + z2); | |
| const px = centerX + x2 * factor * size; | |
| const py = centerY + y * factor * size; | |
| // Draw a small quad to represent remeshed faces | |
| ctx.fillStyle = '#a7f3d0'; | |
| ctx.fillRect(px - 5, py - 5, 10, 10); | |
| }); | |
| } | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=CuriousCreator/remesher" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |