Spaces:
Running
Running

Quiero reemplazar la l贸gica de localStorage por una conexi贸n real a backend (por ejemplo, con Flask o Django REST). El objetivo es permitir que los modelos cargados (UUID, nombre, posici贸n, color, etc.) se almacenen en una API backend y se restauren autom谩ticamente al iniciar la aplicaci贸n. 馃幆 Objetivo: - Enviar los metadatos de cada modelo al backend cuando se carga. - Consultar todos los modelos guardados al iniciar para restaurar la sesi贸n. - Eliminar todos los modelos del backend al hacer reset. 馃З Backend: - Ruta POST `/api/models/` para guardar modelos. - Ruta GET `/api/models/` para listar modelos guardados. - Ruta DELETE `/api/models/` para eliminar todos. 馃З Cambios en el frontend: 1. Al final de `loadModel(file)`, justo despu茅s de `models.push(model);`, agrega: ```js fetch('/api/models/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: model.uuid, name: file.name, position: { x: model.position.x, y: model.position.y, z: model.position.z }, url: URL.createObjectURL(file) // opcional, si quieres usar ObjectURL temporal }) }); Al inicio de init(), justo antes de animate();, agrega: js Copiar Editar fetch('/api/models/') .then(res => res.json()) .then(modelsData => { modelsData.forEach(data => { const loader = new THREE.GLTFLoader(); loader.load(data.url, (gltf) => { const model = gltf.scene; model.uuid = data.uuid; model.traverse(child => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; originalMaterials.set(child.uuid, child.material.clone()); } }); const box = new THREE.Box3().setFromObject(model); 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 = 5 / maxDim; model.scale.multiplyScalar(scale); model.position.sub(center.multiplyScalar(scale)); model.position.x = data.position.x; model.position.y = data.position.y; model.position.z = data.position.z; scene.add(model); models.push(model); const selector = document.getElementById('model-selector'); const option = document.createElement('option'); option.value = model.uuid; option.textContent = data.name; selector.appendChild(option); document.getElementById('color-tools').classList.remove('opacity-50', 'pointer-events-none'); }); }); }); En setupResetButton(), reemplaza el contenido por: js Copiar Editar document.getElementById('reset-btn').addEventListener('click', () => { models.forEach(m => scene.remove(m)); models = []; originalMaterials.clear(); paintMode = false; document.getElementById('paint-mode').textContent = 'Activar pintura'; document.getElementById('paint-mode').classList.remove('bg-blue-600'); document.getElementById('paint-mode').classList.add('bg-slate-700'); document.getElementById('color-tools').classList.add('opacity-50', 'pointer-events-none'); document.body.style.cursor = 'default'; // Borrar del backend fetch('/api/models/', { method: 'DELETE' }); // Limpiar selector const selector = document.getElementById('model-selector'); selector.innerHTML = '<option value="">Selecciona un modelo</option>'; showToast('Modelos reseteados'); }); - Follow Up Deployment
81359da
verified
<html lang="es" class="h-full"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Visualizador 3D - Editor GLB</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/exporters/GLTFExporter.js"></script> | |
<style> | |
#canvas-container { | |
position: relative; | |
background: linear-gradient(135deg, #1e293b 0%, #334155 100%); | |
} | |
.glass-panel { | |
background: rgba(255, 255, 255, 0.05); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.drag-active { | |
border-color: #3b82f6; | |
background-color: rgba(59, 130, 246, 0.1); | |
} | |
.color-picker { | |
transition: all 0.2s ease; | |
} | |
.color-picker:hover { | |
transform: scale(1.1); | |
} | |
.color-picker.active { | |
transform: scale(1.2); | |
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5); | |
} | |
.fade-in { | |
animation: fadeIn 0.5s ease-in; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.loading-spinner { | |
border: 4px solid rgba(255, 255, 255, 0.1); | |
border-left-color: #3b82f6; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
</style> | |
</head> | |
<body class="bg-slate-900 text-white min-h-full flex flex-col"> | |
<!-- Header --> | |
<header class="glass-panel p-4 border-b border-slate-700"> | |
<div class="max-w-7xl mx-auto flex items-center justify-between"> | |
<h1 class="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> | |
Visualizador 3D GLB | |
</h1> | |
<button id="reset-btn" class="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200 flex items-center gap-2"> | |
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> | |
</svg> | |
Resetear Modelo | |
</button> | |
</div> | |
</header> | |
<div class="flex-1 flex flex-col lg:flex-row"> | |
<!-- Sidebar --> | |
<aside class="w-full lg:w-80 glass-panel p-6 space-y-6 overflow-y-auto"> | |
<!-- Upload Section --> | |
<div class="space-y-4"> | |
<h2 class="text-lg font-semibold text-slate-200">Cargar Modelo</h2> | |
<div id="drop-zone" class="border-2 border-dashed border-slate-600 rounded-lg p-8 text-center transition-all duration-200 cursor-pointer"> | |
<svg class="w-12 h-12 mx-auto mb-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
</svg> | |
<p class="text-slate-400 mb-2">Arrastra y suelta tu archivo .glb aqu铆</p> | |
<p class="text-sm text-slate-500">o</p> | |
<input type="file" id="file-input" accept=".glb" class="hidden"> | |
<button onclick="document.getElementById('file-input').click()" class="mt-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-200"> | |
Seleccionar archivo | |
</button> | |
</div> | |
</div> | |
<!-- Color Tools --> | |
<div id="color-tools" class="space-y-4 opacity-50 pointer-events-none"> | |
<h2 class="text-lg font-semibold text-slate-200">Herramientas de Color</h2> | |
<div> | |
<label for="model-selector" class="block text-sm text-slate-400 mb-2">Modelo activo para pintar</label> | |
<select id="model-selector" class="w-full px-3 py-2 rounded-lg bg-slate-800 text-white border border-slate-600"> | |
<option value="">Selecciona un modelo</option> | |
</select> | |
<button id="delete-model" class="w-full mt-2 px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200"> | |
Eliminar modelo | |
</button> | |
</div> | |
<div> | |
<label class="block text-sm text-slate-400 mb-2">Modo pintura</label> | |
<button id="paint-mode" class="w-full px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors duration-200"> | |
Activar pintura | |
</button> | |
</div> | |
<div> | |
<label class="block text-sm text-slate-400 mb-2">Color seleccionado</label> | |
<div class="grid grid-cols-6 gap-2"> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-red-500" data-color="#ef4444"></div> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-blue-500" data-color="#3b82f6"></div> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-green-500" data-color="#22c55e"></div> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-yellow-500" data-color="#eab308"></div> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-purple-500" data-color="#8b5cf6"></div> | |
<div class="color-picker w-10 h-10 rounded-lg cursor-pointer bg-pink-500" data-color="#ec4899"></div> | |
</div> | |
<input type="color" id="color-input" class="mt-2 w-full h-10 rounded-lg cursor-pointer" value="#3b82f6"> | |
</div> | |
<button id="download-btn" class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition-colors duration-200"> | |
Descargar modelo | |
</button> | |
<button id="restore-btn" class="w-full px-4 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg transition-colors duration-200"> | |
Restaurar colores | |
</button> | |
<button id="delete-btn" class="w-full px-4 py-2 bg-red-700 hover:bg-red-800 rounded-lg transition-colors duration-200"> | |
Eliminar modelo | |
</button> | |
<button id="rotate-btn" class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors duration-200"> | |
Detener rotaci贸n | |
</button> | |
</div> | |
<!-- Instructions --> | |
<div class="space-y-4"> | |
<h2 class="text-lg font-semibold text-slate-200">Instrucciones</h2> | |
<div class="text-sm text-slate-400 space-y-2"> | |
<p><strong>Rotar:</strong> Click izquierdo + arrastrar</p> | |
<p><strong>Zoom:</strong> Scroll del mouse</p> | |
<p><strong>Pan:</strong> Click derecho + arrastrar</p> | |
<p><strong>Pintar:</strong> Activar modo pintura y hacer clic en el modelo</p> | |
</div> | |
</div> | |
</aside> | |
<!-- Main Canvas --> | |
<main class="flex-1 relative" id="canvas-container"> | |
<canvas id="canvas" class="w-full h-full"></canvas> | |
<div id="loading" class="absolute inset-0 flex items-center justify-center bg-slate-900 bg-opacity-75 hidden"> | |
<div class="text-center"> | |
<div class="loading-spinner w-12 h-12 rounded-full mx-auto mb-4"></div> | |
<p class="text-slate-300">Cargando modelo...</p> | |
</div> | |
</div> | |
</main> | |
</div> | |
<!-- Toast --> | |
<div id="toast" class="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transform translate-y-full transition-transform duration-300"> | |
<span id="toast-message"></span> | |
</div> | |
<script> | |
// Variables globales | |
let scene, camera, renderer, controls, raycaster, mouse; | |
let models = []; | |
let paintMode = false; | |
let selectedColor = '#3b82f6'; | |
let originalMaterials = new Map(); | |
let autoRotate = true; | |
// Inicializaci贸n | |
function init() { | |
// Escena | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x0f172a); | |
// C谩maras | |
const container = document.getElementById('canvas-container'); | |
const aspect = container.clientWidth / container.clientHeight; | |
perspectiveCamera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000); | |
perspectiveCamera.position.set(5, 5, 5); | |
orthographicCamera = new THREE.OrthographicCamera( | |
-aspect * 5, aspect * 5, 5, -5, 0.1, 1000 | |
); | |
orthographicCamera.position.set(5, 5, 5); | |
orthographicCamera.zoom = 50; | |
orthographicCamera.updateProjectionMatrix(); | |
camera = perspectiveCamera; | |
// Renderer | |
renderer = new THREE.WebGLRenderer({ | |
canvas: document.getElementById('canvas'), | |
antialias: true | |
}); | |
renderer.setSize(container.clientWidth, container.clientHeight); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
// Controles | |
controls = new THREE.OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
// Bot贸n para cambiar c谩mara | |
const toggleBtn = document.createElement('button'); | |
toggleBtn.textContent = 'Vista ortogr谩fica'; | |
toggleBtn.className = 'absolute top-4 right-4 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-lg z-50'; | |
toggleBtn.style.position = 'absolute'; | |
toggleBtn.style.zIndex = 999; | |
toggleBtn.addEventListener('click', () => { | |
usingOrtho = !usingOrtho; | |
camera = usingOrtho ? orthographicCamera : perspectiveCamera; | |
controls.object = camera; | |
toggleBtn.textContent = usingOrtho ? 'Vista perspectiva' : 'Vista ortogr谩fica'; | |
camera.updateProjectionMatrix(); | |
}); | |
document.getElementById('canvas-container').appendChild(toggleBtn); | |
// Luces | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(10, 10, 5); | |
directionalLight.castShadow = true; | |
scene.add(directionalLight); | |
// Raycaster para interacciones | |
raycaster = new THREE.Raycaster(); | |
mouse = new THREE.Vector2(); | |
// Grid helper | |
const gridHelper = new THREE.GridHelper(10, 10, 0x334155, 0x334155); | |
scene.add(gridHelper); | |
// Event listeners | |
window.addEventListener('resize', onWindowResize); | |
renderer.domElement.addEventListener('click', onCanvasClick); | |
setupDragAndDrop(); | |
setupFileInput(); | |
setupColorTools(); | |
setupResetButton(); | |
setupDownloadButton(); | |
setupRestoreButton(); | |
setupDeleteButton(); | |
setupDeleteModelButton(); | |
setupToggleVisibilityButton(); | |
setupCameraToggle(); | |
// Restaurar modelos desde backend | |
fetch('/api/models/') | |
.then(res => res.json()) | |
.then(modelsData => { | |
modelsData.forEach(data => { | |
const loader = new THREE.GLTFLoader(); | |
loader.load(data.url, (gltf) => { | |
const model = gltf.scene; | |
model.uuid = data.uuid; | |
model.traverse(child => { | |
if (child.isMesh) { | |
child.castShadow = true; | |
child.receiveShadow = true; | |
originalMaterials.set(child.uuid, child.material.clone()); | |
} | |
}); | |
const box = new THREE.Box3().setFromObject(model); | |
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 = 5 / maxDim; | |
model.scale.multiplyScalar(scale); | |
model.position.sub(center.multiplyScalar(scale)); | |
model.position.x = data.position.x; | |
model.position.y = data.position.y; | |
model.position.z = data.position.z; | |
scene.add(model); | |
models.push(model); | |
const selector = document.getElementById('model-selector'); | |
const option = document.createElement('option'); | |
option.value = model.uuid; | |
option.textContent = data.name; | |
selector.appendChild(option); | |
document.getElementById('color-tools').classList.remove('opacity-50', 'pointer-events-none'); | |
}); | |
}); | |
}); | |
// Animaci贸n | |
animate(); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if (autoRotate) { | |
models.forEach(m => m.rotation.y += 0.003); | |
} | |
controls.update(); | |
renderer.render(scene, camera); | |
} | |
function onWindowResize() { | |
const container = document.getElementById('canvas-container'); | |
camera.aspect = container.clientWidth / container.clientHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(container.clientWidth, container.clientHeight); | |
} | |
// Carga de modelo | |
function loadModel(file) { | |
if (file.type !== 'model/gltf-binary') { | |
showToast('Archivo inv谩lido. Solo se permiten archivos .glb'); | |
return; | |
} | |
const loader = new THREE.GLTFLoader(); | |
const url = URL.createObjectURL(file); | |
document.getElementById('loading').classList.remove('hidden'); | |
loader.load(url, (gltf) => { | |
const model = gltf.scene; | |
model.traverse((child) => { | |
if (child.isMesh) { | |
child.castShadow = true; | |
child.receiveShadow = true; | |
// Guardar material original | |
originalMaterials.set(child.uuid, child.material.clone()); | |
} | |
}); | |
// Centrar y escalar modelo | |
const box = new THREE.Box3().setFromObject(model); | |
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 = 5 / maxDim; | |
// Forzar UUID persistente | |
model.uuid = crypto.randomUUID(); | |
model.scale.multiplyScalar(scale); | |
model.position.sub(center.multiplyScalar(scale)); | |
model.position.x = 5 * models.length; | |
scene.add(model); | |
models.push(model); | |
const boxHelper = new THREE.BoxHelper(model, 0xffff00); | |
scene.add(boxHelper); | |
const axesHelper = new THREE.AxesHelper(1); | |
model.add(axesHelper); | |
// Guardar en backend | |
fetch('/api/models/', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
uuid: model.uuid, | |
name: file.name, | |
position: { x: model.position.x, y: model.position.y, z: model.position.z }, | |
url: URL.createObjectURL(file) | |
}) | |
}); | |
const selector = document.getElementById('model-selector'); | |
const option = document.createElement('option'); | |
option.value = model.uuid; | |
option.textContent = file.name; | |
selector.appendChild(option); | |
URL.revokeObjectURL(url); | |
// Activar herramientas | |
document.getElementById('color-tools').classList.remove('opacity-50', 'pointer-events-none'); | |
document.getElementById('loading').classList.add('hidden'); | |
showToast('Modelo cargado exitosamente'); | |
}); | |
} | |
// Drag and Drop | |
function setupDragAndDrop() { | |
const dropZone = document.getElementById('drop-zone'); | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
dropZone.addEventListener(eventName, preventDefaults, false); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
['dragenter', 'dragover'].forEach(eventName => { | |
dropZone.addEventListener(eventName, () => { | |
dropZone.classList.add('drag-active'); | |
}); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropZone.addEventListener(eventName, () => { | |
dropZone.classList.remove('drag-active'); | |
}); | |
}); | |
dropZone.addEventListener('drop', (e) => { | |
const files = e.dataTransfer.files; | |
if (files.length > 0 && files[0].name.endsWith('.glb')) { | |
loadModel(files[0]); | |
} | |
}); | |
} | |
// File input | |
function setupFileInput() { | |
document.getElementById('file-input').addEventListener('change', (e) => { | |
const file = e.target.files[0]; | |
if (file && file.name.endsWith('.glb')) { | |
loadModel(file); | |
} | |
}); | |
} | |
// Herramientas de color | |
function setupColorTools() { | |
// Color picker | |
document.querySelectorAll('.color-picker').forEach(picker => { | |
picker.addEventListener('click', () => { | |
document.querySelectorAll('.color-picker').forEach(p => p.classList.remove('active')); | |
picker.classList.add('active'); | |
selectedColor = picker.dataset.color; | |
document.getElementById('color-input').value = selectedColor; | |
}); | |
}); | |
document.getElementById('color-input').addEventListener('change', (e) => { | |
selectedColor = e.target.value; | |
document.querySelectorAll('.color-picker').forEach(p => p.classList.remove('active')); | |
}); | |
// Paint mode | |
document.getElementById('paint-mode').addEventListener('click', (e) => { | |
paintMode = !paintMode; | |
e.target.textContent = paintMode ? 'Desactivar pintura' : 'Activar pintura'; | |
e.target.classList.toggle('bg-blue-600'); | |
e.target.classList.toggle('bg-slate-700'); | |
document.body.style.cursor = paintMode ? 'crosshair' : 'default'; | |
}); | |
// Control de rotaci贸n | |
document.getElementById('rotate-btn').addEventListener('click', (e) => { | |
autoRotate = !autoRotate; | |
e.target.textContent = autoRotate ? 'Detener rotaci贸n' : 'Reanudar rotaci贸n'; | |
}); | |
} | |
function onCanvasClick(event) { | |
if (!paintMode) return; | |
const selectedId = document.getElementById('model-selector').value; | |
const model = models.find(m => m.uuid === selectedId); | |
if (!model) return; | |
const rect = renderer.domElement.getBoundingClientRect(); | |
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; | |
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; | |
raycaster.setFromCamera(mouse, camera); | |
const intersects = raycaster.intersectObject(model, true); | |
if (intersects.length > 0) { | |
const point = intersects[0].point; | |
const radius = 0.3; // Radio del pincel | |
model.traverse(child => { | |
if (child.isMesh) { | |
const pos = child.getWorldPosition(new THREE.Vector3()); | |
const distance = pos.distanceTo(point); | |
if (distance <= radius) { | |
const newMat = child.material.clone(); | |
newMat.color.set(selectedColor); | |
child.material = newMat; | |
} | |
} | |
}); | |
} | |
} | |
// Reset | |
function setupResetButton() { | |
document.getElementById('reset-btn').addEventListener('click', () => { | |
models.forEach(m => scene.remove(m)); | |
models = []; | |
originalMaterials.clear(); | |
paintMode = false; | |
document.getElementById('paint-mode').textContent = 'Activar pintura'; | |
document.getElementById('paint-mode').classList.remove('bg-blue-600'); | |
document.getElementById('paint-mode').classList.add('bg-slate-700'); | |
document.getElementById('color-tools').classList.add('opacity-50', 'pointer-events-none'); | |
document.body.style.cursor = 'default'; | |
// Borrar del backend | |
fetch('/api/models/', { method: 'DELETE' }); | |
// Limpiar selector | |
const selector = document.getElementById('model-selector'); | |
selector.innerHTML = '<option value="">Selecciona un modelo</option>'; | |
showToast('Modelos reseteados'); | |
}); | |
} | |
function setupDownloadButton() { | |
document.getElementById('download-btn').addEventListener('click', () => { | |
const selectedId = document.getElementById('model-selector').value; | |
const model = models.find(m => m.uuid === selectedId); | |
if (!model) { | |
showToast('No hay modelo seleccionado para descargar'); | |
return; | |
} | |
const exporter = new THREE.GLTFExporter(); | |
exporter.parse( | |
model, | |
(gltf) => { | |
const blob = new Blob([gltf], { type: 'model/gltf-binary' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'modelo_modificado.glb'; | |
a.click(); | |
URL.revokeObjectURL(url); | |
showToast('Modelo descargado'); | |
}, | |
{ binary: true } | |
); | |
}); | |
} | |
function setupRestoreButton() { | |
const restoreBtn = document.getElementById('restore-btn'); | |
restoreBtn.addEventListener('click', () => { | |
const selectedId = document.getElementById('model-selector').value; | |
const model = models.find(m => m.uuid === selectedId); | |
if (!model) return; | |
model.traverse(child => { | |
if (child.isMesh && originalMaterials.has(child.uuid)) { | |
child.material = originalMaterials.get(child.uuid).clone(); | |
} | |
}); | |
showToast('Colores originales restaurados'); | |
}); | |
} | |
// Toast | |
function setupDeleteModelButton() { | |
const deleteBtn = document.getElementById('delete-model'); | |
deleteBtn.addEventListener('click', () => { | |
const selectedId = document.getElementById('model-selector').value; | |
const model = models.find(m => m.uuid === selectedId); | |
if (!model) return; | |
scene.remove(model); | |
models = models.filter(m => m.uuid !== selectedId); | |
document.getElementById('model-selector').querySelector(`option[value="${selectedId}"]`)?.remove(); | |
document.getElementById('model-selector').value = ""; | |
showToast("Modelo eliminado"); | |
}); | |
} | |
function setupCameraToggle() { | |
const camBtn = document.createElement('button'); | |
camBtn.id = 'toggle-camera'; | |
camBtn.textContent = 'Cambiar a c谩mara ortogr谩fica'; | |
camBtn.className = 'w-full px-4 py-2 bg-teal-600 hover:bg-teal-700 rounded-lg transition-colors duration-200 mt-2'; | |
document.getElementById('color-tools').appendChild(camBtn); | |
let isOrtho = false; | |
camBtn.addEventListener('click', () => { | |
const container = document.getElementById('canvas-container'); | |
const aspect = container.clientWidth / container.clientHeight; | |
if (!isOrtho) { | |
const frustumSize = 10; | |
const orthoCam = new THREE.OrthographicCamera( | |
(frustumSize * aspect) / -2, | |
(frustumSize * aspect) / 2, | |
frustumSize / 2, | |
frustumSize / -2, | |
0.1, | |
1000 | |
); | |
orthoCam.position.copy(camera.position); | |
orthoCam.lookAt(scene.position); | |
camera = orthoCam; | |
controls.object = camera; | |
camBtn.textContent = 'Cambiar a c谩mara perspectiva'; | |
} else { | |
const perspCam = new THREE.PerspectiveCamera( | |
75, | |
aspect, | |
0.1, | |
1000 | |
); | |
perspCam.position.copy(camera.position); | |
perspCam.lookAt(scene.position); | |
camera = perspCam; | |
controls.object = camera; | |
camBtn.textContent = 'Cambiar a c谩mara ortogr谩fica'; | |
} | |
isOrtho = !isOrtho; | |
camera.updateProjectionMatrix(); | |
}); | |
} | |
function setupToggleVisibilityButton() { | |
const toggleBtn = document.createElement('button'); | |
toggleBtn.id = 'toggle-visibility'; | |
toggleBtn.textContent = 'Ocultar modelo'; | |
toggleBtn.className = 'w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors duration-200 mt-2'; | |
document.getElementById('color-tools').appendChild(toggleBtn); | |
toggleBtn.addEventListener('click', () => { | |
const selectedId = document.getElementById('model-selector').value; | |
const model = models.find(m => m.uuid === selectedId); | |
if (!model) return; | |
model.visible = !model.visible; | |
toggleBtn.textContent = model.visible ? 'Ocultar modelo' : 'Mostrar modelo'; | |
}); | |
} | |
function showToast(message) { | |
const toast = document.getElementById('toast'); | |
const toastMessage = document.getElementById('toast-message'); | |
toastMessage.textContent = message; | |
toast.classList.remove('translate-y-full'); | |
setTimeout(() => { | |
toast.classList.add('translate-y-full'); | |
}, 3000); | |
} | |
// Inicializar cuando el DOM est茅 listo | |
document.addEventListener('DOMContentLoaded', init); | |
</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=thepipis/modelado-3d" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |