23dfactory / frontend /index.html
LogicalTrue
feat: agregar soporte nativo de visualizacion FBX con THREE.FBXLoader para mostrar mallas cuadrilateras (Quads) en tiempo real
cb090b4
Raw
History Blame Contribute Delete
64.9 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>23DFactory - Transform 2D Images to 3D Models</title>
<!-- Immediate Mobile Redirect -->
<script>
if (window.innerWidth < 768 && sessionStorage.getItem('forceDesktop') !== 'true') {
window.location.href = '/viewer.html?v=123';
}
</script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700;800&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Import Model Viewer for 3D Rendering -->
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
<!-- Three.js for skeletal animations -->
<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/libs/fflate.min.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/loaders/FBXLoader.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/exporters/GLTFExporter.js"></script>
<!-- Cropper.js for Interactive Image Cropping -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
<!-- Supabase JS Client -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<!-- Custom Style -->
<link rel="stylesheet" href="styles.css?v=200">
</head>
<body>
<div class="app-container" id="appContainer" style="display: none;">
<!-- Header -->
<header class="app-header">
<div class="logo-container">
<h1 class="logo-text">23D<span>Factory</span></h1>
</div>
<div class="header-actions" id="userHeaderActions" style="display: none; align-items: center; gap: 0.75rem;">
<!-- Floating Glassmorphism Background Task Banner -->
<div id="bgTaskBanner" style="display: none; align-items: center; gap: 8px; background: rgba(124, 58, 237, 0.15); border: 1px solid rgba(167, 139, 250, 0.4); backdrop-filter: blur(10px); padding: 5px 14px; border-radius: 20px; box-shadow: 0 0 15px rgba(124, 58, 237, 0.25); font-size: 0.8rem; color: #ffffff;">
<div class="spinner-small" style="width: 13px; height: 13px; border-width: 2px; margin: 0;"></div>
<span id="bgTaskText" style="font-weight: 600;">Generando... 0%</span>
</div>
<div style="position: relative;">
<button id="userMenuBtn" class="user-menu-btn" style="background: linear-gradient(135deg, rgba(124, 58, 237, 0.4), rgba(167, 139, 250, 0.2)); border: 1.5px solid rgba(167, 139, 250, 0.6); color: #ffffff; padding: 0; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 0 12px rgba(124, 58, 237, 0.3); overflow: hidden; position: relative;" title="Menú de usuario">
<img id="userHeaderAvatarImg" src="" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover; display: none;">
<span id="userHeaderAvatarInitial" style="font-size: 1rem; font-weight: 800; color: #ffffff; text-transform: uppercase;">P</span>
</button>
<div id="userDropdownMenu" class="user-dropdown-menu">
<div class="dropdown-user-info">
<div class="dropdown-user-title">
<i data-lucide="user" style="width: 15px; height: 15px; color: #a78bfa; flex-shrink: 0;"></i>
<span id="dropdownUsernameText">Invitado</span>
</div>
</div>
<!-- Botón de Créditos dentro del Desplegable -->
<button id="headerCreditsBtn" class="dropdown-item credits-item">
<span style="display: flex; align-items: center; gap: 6px; white-space: nowrap;">
🪙 <span id="headerCreditsText">0</span> Créditos
</span>
<span class="credits-badge">RECARGAR</span>
</button>
<button id="profileMenuBtn" class="dropdown-item profile-item">
<i data-lucide="user-check" style="width: 15px; height: 15px; color: var(--primary-light);"></i>
<span>Mi perfil</span>
</button>
<button id="privacyMenuBtn" class="dropdown-item privacy-item">
<i data-lucide="shield-check" style="width: 15px; height: 15px; color: #4caf50;"></i>
<span>Privacidad y Seguridad</span>
</button>
<div class="dropdown-divider"></div>
<button id="logoutBtn" class="dropdown-item logout-item">
<i data-lucide="log-out" style="width: 15px; height: 15px;"></i>
<span>Salir</span>
</button>
</div>
</div>
</div>
</header>
<!-- Main Workspace -->
<main class="main-workspace">
<!-- Sidebar (Controls) -->
<aside class="sidebar">
<div class="sidebar-scroll">
<!-- Image Source Selection Tab -->
<nav class="tab-nav">
<button class="tab-btn active" data-tab="upload-tab">
<i data-lucide="box"></i>
<span>Generar 3D</span>
</button>
<button class="tab-btn" data-tab="generate-tab">
<i data-lucide="image"></i>
<span>Generar 2D</span>
</button>
<button class="tab-btn" data-tab="gallery-tab" id="galleryTabBtn">
<i data-lucide="library"></i>
<span>Biblioteca</span>
</button>
</nav>
<!-- Tab content: Upload / Select Image for 3D generation -->
<div class="tab-content active" id="upload-tab">
<div class="control-card">
<div class="card-title">
<i data-lucide="image"></i>
<span>Imagen 2D de Origen</span>
</div>
<!-- Source Selector -->
<div class="toggle-group" style="margin-bottom: 1.25rem;">
<button type="button" class="toggle-btn active" id="sourceUploadBtn">Subir Archivo</button>
<button type="button" class="toggle-btn" id="sourceLibraryBtn">Usar Biblioteca</button>
</div>
<!-- Upload Option -->
<div id="uploadSourceContainer">
<div class="dropzone" id="dropzone">
<i data-lucide="image-up" class="dropzone-icon"></i>
<p>Arrastra tu imagen aquí o haz clic para buscar</p>
<span style="font-size: 0.75rem; color: var(--text-dark);">Soporta PNG, JPG, WEBP (Fondo transparente preferible)</span>
<input type="file" id="fileInput" accept="image/*">
</div>
</div>
<!-- Library Selection Option -->
<div id="librarySourceContainer" style="display: none;">
<div class="library-thumbnail-grid" id="libraryThumbnailGrid">
<!-- Populated dynamically with 2D images -->
</div>
</div>
<div class="preview-container" id="previewContainer" style="margin-top: 1rem;">
<img src="" alt="Preview" class="preview-img" id="imagePreview">
<button class="remove-preview" id="removePreviewBtn" title="Quitar imagen">&times;</button>
<button type="button" id="btnOpenCropPC" style="margin-top: 8px; width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 0.82rem; padding: 8px 12px; background: rgba(124, 58, 237, 0.15); border: 1px solid rgba(124, 58, 237, 0.4); color: #a78bfa; border-radius: 8px; cursor: pointer; font-weight: 600;">
<i data-lucide="crop" style="width: 14px; height: 14px;"></i>
<span>Enmarcar / Recortar Sujeto</span>
</button>
</div>
</div>
<!-- Trellis 3D Parameters Card -->
<div class="control-card">
<div class="card-title">
<i data-lucide="sliders"></i>
<span>Ajustes Trellis 3D</span>
</div>
<!-- Resolution -->
<div class="input-group">
<label class="input-label" for="resolutionSelect">Resolución</label>
<select id="resolutionSelect" class="custom-select">
<option value="512">512 (Rápido)</option>
<option value="1024">1024 (Recomendado)</option>
<option value="1536" selected>1536 (Alta Calidad)</option>
</select>
</div>
<!-- Seed -->
<div class="input-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
<label class="input-label" for="seedInput" style="margin-bottom: 0;">Semilla (Seed)</label>
<div style="display: flex; align-items: center; gap: 0.25rem;">
<input type="checkbox" id="randomSeedCheck" checked style="width: 14px; height: 14px; cursor: pointer;">
<label for="randomSeedCheck" style="font-size: 0.75rem; color: var(--text-muted); cursor: pointer; user-select: none;">Aleatoria</label>
</div>
</div>
<input type="number" id="seedInput" class="text-input" value="0" min="0" max="2147483647">
</div>
<!-- Decimation Target -->
<div class="slider-container">
<div class="slider-header">
<span>Decimation Target (Malla original)</span>
<span id="decimateVal">300000</span>
</div>
<input type="range" id="decimateInput" class="range-slider" min="100000" max="500000" step="50000" value="300000">
</div>
<!-- Texture Size -->
<div class="input-group">
<label class="input-label" for="textureSizeSelect">Tamaño de Textura</label>
<select id="textureSizeSelect" class="custom-select">
<option value="1024">1024x1024</option>
<option value="2048">2048x2048</option>
<option value="4096" selected>4096x4096</option>
</select>
</div>
<!-- SS Guidance Strength -->
<div class="slider-container">
<div class="slider-header">
<span>SS Fuerza de Guía (SS Guidance)</span>
<span id="ssGuidanceVal">7.5</span>
</div>
<input type="range" id="ssGuidanceInput" class="range-slider" min="1.0" max="15.0" step="0.5" value="7.5">
</div>
<!-- SS Sampling Steps -->
<div class="slider-container">
<div class="slider-header">
<span>SS Pasos de Muestreo</span>
<span id="ssStepsVal">12</span>
</div>
<input type="range" id="ssStepsInput" class="range-slider" min="4" max="25" step="1" value="12">
</div>
<!-- SLAT Guidance Strength -->
<div class="slider-container">
<div class="slider-header">
<span>SLAT Fuerza de Guía</span>
<span id="slatGuidanceVal">7.5</span>
</div>
<input type="range" id="slatGuidanceInput" class="range-slider" min="1.0" max="10.0" step="0.5" value="7.5">
</div>
<!-- SLAT Steps -->
<div class="slider-container">
<div class="slider-header">
<span>SLAT Pasos de Muestreo</span>
<span id="slatStepsVal">12</span>
</div>
<input type="range" id="slatStepsInput" class="range-slider" min="4" max="25" step="1" value="12">
</div>
</div>
</div>
<!-- Tab content: Generate 2D AI -->
<div class="tab-content" id="generate-tab">
<div class="control-card">
<div class="card-title">
<i data-lucide="message-square"></i>
<span>Generador de Imágenes 2D</span>
</div>
<div class="input-group">
<label class="input-label" for="promptInput">Prompt de Imagen</label>
<textarea id="promptInput" class="text-input" rows="3" placeholder="Ej. Un juguete robot cyberpunk detallado, fondo blanco, modelo 3D, estilo Pixar..."></textarea>
</div>
<div class="input-group">
<label class="input-label" for="negativePromptInput">Prompt Negativo</label>
<input type="text" id="negativePromptInput" class="text-input" placeholder="Ej. blurry, low quality, detailed background" value="blurry, detailed background, shadows, realistic environment">
</div>
<div class="input-group">
<label class="input-label" for="generatorModelSelect">Modelo de IA</label>
<select id="generatorModelSelect" class="custom-select">
<option value="black-forest-labs/FLUX.1-schnell">FLUX.1 Schnell (Ultra Rápido)</option>
<option value="stabilityai/stable-diffusion-3-medium">Stable Diffusion 3 Medium</option>
<option value="black-forest-labs/FLUX.1-dev">FLUX.1 Dev (Alta Calidad)</option>
</select>
</div>
<button class="btn btn-accent" id="generate2dBtn">
<i data-lucide="wand-2"></i>
<span>Crear Imagen 2D (Cuesta 1 crédito)</span>
</button>
</div>
</div>
<!-- Tab content: Gallery / Library -->
<div class="tab-content" id="gallery-tab">
<div class="control-card">
<div class="card-title">
<i data-lucide="library"></i>
<span>Galería de Creaciones</span>
</div>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.75rem;">
Modelos 3D (.glb) e imágenes 2D generadas y almacenadas localmente.
</p>
<!-- Gallery Filter Selector -->
<div class="toggle-group filter-group" style="margin-bottom: 1.25rem;">
<button type="button" class="toggle-btn active" data-filter="all">Todos</button>
<button type="button" class="toggle-btn" data-filter="image">Imágenes 2D</button>
<button type="button" class="toggle-btn" data-filter="model">Modelos 3D</button>
</div>
<div class="gallery-list" id="galleryList">
<!-- Items will be dynamically loaded here -->
<div class="gallery-empty-state">
<i data-lucide="inbox" style="opacity: 0.5; margin-bottom: 0.5rem; display: block; margin: 0 auto; text-align: center;"></i>
<p style="font-size: 0.8rem; color: var(--text-dark); text-align: center;">Biblioteca vacía</p>
</div>
</div>
</div>
</div>
</div> <!-- End sidebar-scroll -->
<div class="sidebar-actions">
<!-- Main Build Trigger -->
<button class="btn btn-accent" id="generate3dBtn" style="width: 100%;" disabled>
<i data-lucide="box"></i>
<span>Construir Malla 3D (Cuesta 5 créditos)</span>
</button>
<!-- Footer -->
<footer class="sidebar-footer">
<span>23DFactory v1.0.0</span>
</footer>
</div>
</aside>
<!-- Viewer Container -->
<section class="viewer-container">
<!-- Loading Overlay -->
<div class="status-overlay" id="statusOverlay">
<div class="cyber-loader">
<div class="loader-ring"></div>
<div class="loader-ring"></div>
<div class="loader-ring"></div>
<div class="loader-text-pct" id="loaderPercentage">--</div>
</div>
<div class="status-info">
<h4 class="status-title" id="statusTitle">Inicializando...</h4>
<p class="status-desc" id="statusDesc">Por favor, espera unos instantes mientras el motor procesa.</p>
</div>
</div>
<!-- Interactive 3D Viewer Display -->
<div class="viewer-display" id="viewerDisplay">
<!-- Empty State -->
<div class="empty-state" id="emptyState">
<div class="empty-state-icon">
<i data-lucide="box"></i>
</div>
<h3>Visualizador 3D</h3>
<p>Carga o genera una imagen 2D en el panel izquierdo y haz clic en "Construir Malla 3D" para ver el resultado tridimensional aquí.</p>
</div>
<!-- Dynamic Model Viewer (will be injected here or toggled) -->
</div>
<!-- Floating Side Panels Container (Right Side) -->
<div id="rightPanelsContainer" style="position: absolute; top: 20px; right: 75px; width: 285px; z-index: 100; pointer-events: none; overflow: visible;">
<!-- Manual Bone Rig Controller Panel -->
<div class="manual-rig-panel" id="manualRigPanel" style="display: none; position: absolute; width: 285px; background: rgba(18, 18, 24, 0.9); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: inherit; pointer-events: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;">
<h4 style="margin: 0; font-family: var(--font-heading); font-size: 0.95rem; font-weight: 600; color: var(--primary-light); display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="sliders" style="width: 16px; height: 16px;"></i>
Ajuste de Esqueleto
</h4>
</div>
<!-- AI Classification Badge -->
<div id="aiClassificationBadge" style="font-size: 0.72rem; color: var(--primary-light); background: rgba(156, 39, 176, 0.08); border: 1px solid rgba(156, 39, 176, 0.2); padding: 0.4rem 0.6rem; border-radius: 6px; margin-bottom: 0.75rem; line-height: 1.35; display: flex; align-items: center; gap: 0.35rem;">
<i data-lucide="sparkles" style="width: 13px; height: 13px; flex-shrink: 0; color: var(--primary-light);"></i>
<span>Detección de IA: <strong id="aiClassificationText">Desconocido</strong></span>
</div>
<div class="input-group" style="margin-bottom: 0.75rem;">
<label class="input-label" style="font-size: 0.75rem; margin-bottom: 0.25rem;">Nombre del Hueso</label>
<div style="display: flex; gap: 0.5rem; width: 100%;">
<input type="text" id="manualBoneNameInput" class="text-input" style="padding: 0.4rem; font-size: 0.8rem; border-radius: 6px; background-color: rgba(30, 30, 40, 0.6); color: var(--text-main); border: 1px solid var(--border); width: 100%; font-family: inherit; height: 32px;" placeholder="(Selecciona un hueso)">
<button class="btn btn-primary" id="applyBoneRenameBtn" style="padding: 0 0.75rem; font-size: 0.75rem; height: 32px; flex-shrink: 0; background: var(--primary); color: white; display: none;">Aplicar</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.15rem;">
<span>Rotación X</span>
<span id="boneRotXVal"></span>
</div>
<input type="range" id="boneRotX" min="-180" max="180" value="0" class="slider" style="width: 100%; accent-color: var(--primary);">
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.15rem;">
<span>Rotación Y</span>
<span id="boneRotYVal"></span>
</div>
<input type="range" id="boneRotY" min="-180" max="180" value="0" class="slider" style="width: 100%; accent-color: var(--primary);">
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.15rem;">
<span>Rotación Z</span>
<span id="boneRotZVal"></span>
</div>
<input type="range" id="boneRotZ" min="-180" max="180" value="0" class="slider" style="width: 100%; accent-color: var(--primary);">
</div>
</div>
<button class="btn btn-secondary" id="resetBoneRotBtn" style="width: 100%; margin-top: 0.75rem; padding: 0.4rem; font-size: 0.8rem; height: auto;">
Restablecer Rotación
</button>
<button class="btn" id="toggleBonesHelperBtn" style="width: 100%; margin-top: 0.5rem; padding: 0.4rem; font-size: 0.8rem; height: auto; background: rgba(0, 150, 136, 0.15); color: #009688; border: 1px solid rgba(0, 150, 136, 0.3);">
<i data-lucide="eye" style="width: 12px; height: 12px;"></i>
<span>Ver Esqueleto (Rayos X)</span>
</button>
</div>
<!-- Manual Mesh Clean/Optimization Panel -->
<div class="manual-rig-panel" id="manualCleanPanel" style="display: none; position: absolute; width: 285px; background: rgba(18, 18, 24, 0.9); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: inherit; pointer-events: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;">
<h4 style="margin: 0; font-family: var(--font-heading); font-size: 0.95rem; font-weight: 600; color: #009688; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i>
Limpieza de Malla (Blender)
</h4>
</div>
<!-- Target Faces for Blender Remesh -->
<div class="slider-container" style="margin-bottom: 1.25rem;">
<div class="slider-header" style="margin-bottom: 0.5rem; display: flex; justify-content: space-between; font-size: 0.8rem;">
<span>Caras objetivo (Quads)</span>
<span id="manualQuadFacesVal">60000</span>
</div>
<input type="range" id="manualQuadFacesInput" class="range-slider" min="2000" max="200000" step="1000" value="60000" style="margin-bottom: 0.75rem; width: 100%; accent-color: #009688;">
<div class="input-group" style="margin-bottom: 1rem;">
<label class="input-label" for="manualRemeshMethodSelect" style="font-size: 0.75rem; opacity: 0.8; margin-bottom: 0.25rem; display: block;">Método de Quads</label>
<select id="manualRemeshMethodSelect" class="custom-select" style="padding: 0.35rem 0.5rem; font-size: 0.8rem; height: auto; width: 100%; border-radius: 6px; background-color: rgba(30, 30, 40, 0.6); color: var(--text-main); border: 1px solid var(--border); cursor: pointer; font-family: inherit;">
<option value="cleanup">Sólo Limpieza (Eliminar ruido y flotantes)</option>
<option value="tris_to_quads" selected>Conversión Rápida (Tris to Quads)</option>
<option value="quadriflow">Retopología Completa (QuadriFlow)</option>
</select>
</div>
</div>
<button class="btn" id="runManualCleanBtn" style="width: 100%; padding: 0.6rem; font-size: 0.85rem; background: #009688; font-weight: 500;">
Optimizar Malla con Blender
</button>
</div>
<!-- Manual Rig Generation Panel (Rigging trigger) -->
<div class="manual-rig-panel" id="rigTriggerPanel" style="display: none; position: absolute; width: 285px; background: rgba(18, 18, 24, 0.95); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: inherit; pointer-events: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;">
<h4 style="margin: 0; font-family: var(--font-heading); font-size: 0.95rem; font-weight: 600; color: #e91e63; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="accessibility" style="width: 16px; height: 16px;"></i>
Rigging Automático
</h4>
</div>
<!-- Rigging Info -->
<div style="font-size: 0.75rem; color: var(--text-main); margin-bottom: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border); padding: 0.6rem; border-radius: 6px;">
<div>
<span style="color: var(--text-muted); font-size: 0.7rem; display: block; margin-bottom: 0.1rem;">Estructura Detectada</span>
<strong id="rigDetectedCategoryText" style="color: var(--primary-light); font-size: 0.8rem;">Desconocido</strong>
</div>
<div style="border-top: 1px solid rgba(255, 255, 255, 0.05); padding-top: 0.4rem; margin-top: 0.2rem;">
<span style="color: var(--text-muted); font-size: 0.7rem; display: block; margin-bottom: 0.1rem;">Motor de Rigging</span>
<strong style="color: #e91e63; font-size: 0.8rem; display: flex; align-items: center; gap: 0.25rem;">
<i data-lucide="cloud" style="width: 13px; height: 13px;"></i>
UniRig (IA Nube)
</strong>
</div>
</div>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.45;">
Construye un esqueleto de huesos interactivo optimizado para esta criatura u objeto usando la IA en la nube.
</p>
<button class="btn" id="runTriggerRigBtn" style="width: 100%; padding: 0.6rem; font-size: 0.85rem; background: #e91e63; color: white; font-weight: 500; border: none; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<i data-lucide="accessibility" style="width: 16px; height: 16px;"></i>
Generar Rigging por IA
</button>
</div>
<!-- Corrección de Influencias (Weight Paint) Panel -->
<div class="manual-rig-panel" id="manualWeightPaintPanel" style="display: none; position: absolute; width: 285px; background: rgba(18, 18, 24, 0.9); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: inherit; pointer-events: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem;">
<h4 style="margin: 0; font-family: var(--font-heading); font-size: 0.95rem; font-weight: 600; color: #ff9800; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="paintbrush" style="width: 16px; height: 16px;"></i>
Weight Painting (Pintar Pesos)
</h4>
</div>
<!-- Toggle Switch Layout -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; background: rgba(255, 152, 0, 0.05); border: 1px solid rgba(255, 152, 0, 0.15); padding: 0.6rem 0.75rem; border-radius: 8px;">
<span style="font-size: 0.8rem; font-weight: 500; color: var(--text-light);">Activar Pincel de Pintado</span>
<label class="switch-container" style="position: relative; display: inline-block; width: 38px; height: 20px; margin: 0; flex-shrink: 0; cursor: pointer;">
<input type="checkbox" id="activeWeightPaintCheck" style="opacity: 0; width: 0; height: 0;">
<span class="switch-slider"></span>
</label>
</div>
<!-- Selected Bone Display Widget -->
<div style="margin-bottom: 0.75rem; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border); padding: 0.6rem 0.75rem; border-radius: 8px; font-size: 0.8rem; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;">
<span style="color: var(--text-muted);">Hueso a pintar:</span>
<span style="font-weight: 600; color: #ff9800; display: flex; align-items: center; gap: 0.35rem;">
<i data-lucide="bone" style="width: 13px; height: 13px; flex-shrink: 0; transform: rotate(-45deg);"></i>
<span id="weightPaintSelectedBoneText">Ninguno</span>
</span>
</div>
<!-- Keyboard Hotkey Instructions Card -->
<div id="paintInstructionCard" style="display: none; background: rgba(255, 152, 0, 0.08); border-left: 3px solid #ff9800; padding: 0.6rem 0.75rem; border-radius: 6px; margin-bottom: 0.75rem; font-size: 0.72rem; line-height: 1.4; color: rgba(255, 255, 255, 0.85); box-sizing: border-box;">
<div style="display: flex; gap: 0.4rem; align-items: flex-start;">
<i data-lucide="info" style="width: 13px; height: 13px; color: #ff9800; flex-shrink: 0; margin-top: 1px;"></i>
<span>
Mantén presionado <kbd style="background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.25); border-radius: 4px; padding: 1px 4px; font-family: inherit; font-size: 0.68rem; font-weight: 700; box-shadow: 0 1px 0 rgba(0,0,0,0.4); color: white; display: inline-block; line-height: 1;">Ctrl</kbd> y haz clic/arrastra sobre el modelo en el visor para pintar o borrar.
</span>
</div>
</div>
<div class="slider-container" style="margin-bottom: 0.75rem;">
<div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.15rem;">
<span>Radio del Pincel (Brush Size)</span>
<span id="weightPaintRadiusVal">0.10</span>
</div>
<input type="range" id="weightPaintRadiusInput" min="0.01" max="0.5" step="0.01" value="0.10" class="slider" style="width: 100%; accent-color: #ff9800;">
</div>
<div class="slider-container" style="margin-bottom: 0.75rem;">
<div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); margin-bottom: 0.15rem;">
<span>Fuerza de Pintado (Strength)</span>
<span id="weightPaintStrengthVal">0.50</span>
</div>
<input type="range" id="weightPaintStrengthInput" min="0.01" max="1.0" step="0.05" value="0.50" class="slider" style="width: 100%; accent-color: #ff9800;">
</div>
<div class="input-group" style="margin-bottom: 1rem;">
<label class="input-label" style="font-size: 0.75rem; margin-bottom: 0.25rem;">Modo de Pincel</label>
<select id="weightPaintModeSelect" class="text-input" style="padding: 0.4rem; font-size: 0.8rem; border-radius: 6px; background-color: rgba(30, 30, 40, 0.6); color: var(--text-main); border: 1px solid var(--border); cursor: pointer; width: 100%; font-family: inherit; height: 32px;">
<option value="add" selected>Pintar (Sumar influencia)</option>
<option value="sub">Borrar (Restar influencia)</option>
</select>
</div>
<button class="btn" id="saveWeightPaintBtn" style="width: 100%; padding: 0.6rem; font-size: 0.85rem; background: #ff9800; color: white; font-weight: 500; border: none; display: flex; align-items: center; justify-content: center; gap: 0.5rem;">
<i data-lucide="save" style="width: 16px; height: 16px;"></i>
Guardar Pesos de Hueso
</button>
</div>
</div>
<!-- Unified Vertical Toolbar on the Right Edge -->
<div class="right-vertical-toolbar" id="rightVerticalToolbar" style="display: none;">
<button class="toolbar-btn" id="btnRigPanel" title="Ajuste de Esqueleto">
<i data-lucide="bone" style="transform: rotate(-45deg);"></i>
</button>
<button class="toolbar-btn" id="btnCleanPanel" title="Limpieza de Malla">
<i data-lucide="sparkles"></i>
</button>
<button class="toolbar-btn" id="btnRigTriggerPanel" title="Rigging Automático (IA)">
<i data-lucide="accessibility"></i>
</button>
<button class="toolbar-btn" id="btnWeightPaintPanel" title="Weight Painting (Pintar Pesos)">
<i data-lucide="paintbrush"></i>
</button>
</div>
<!-- Visualizer Floating Overlay Toolbar -->
<div class="viewer-overlay" id="viewerOverlay">
<button class="overlay-btn" id="toggleWireframeBtn" title="Ver Estructura (Caras / Alambre)">
<i data-lucide="grid"></i>
</button>
<button class="overlay-btn active" id="toggleTextureBtn" title="Activar/Desactivar Textura">
<i data-lucide="palette"></i>
</button>
<button class="overlay-btn" id="toggleBonesBtn" title="Ver/Ocultar Huesos (Esqueleto)">
<i data-lucide="bone"></i>
</button>
<button class="btn btn-accent" id="downloadFbxBtn" style="padding: 0.4rem 1rem; border-radius: 20px; font-size: 0.85rem;" title="Descargar modelo .FBX (Quads)">
<i data-lucide="download"></i>
<span>Descargar FBX</span>
</button>
<!-- Animation Selection Control -->
<div id="animationControlGroup" style="display: none; align-items: center; gap: 0.5rem;">
<div style="width: 1px; background: var(--border); margin: 0.25rem 0; height: 20px;"></div>
<select id="animationSelect" class="text-input" style="padding: 0.3rem 1.5rem 0.3rem 0.75rem; font-size: 0.85rem; border-radius: 20px; background-color: rgba(30, 30, 40, 0.6); color: var(--text-main); border: 1px solid var(--border); cursor: pointer; width: auto; height: 32px; font-family: inherit;">
<option value="none">Pose Base (Detener)</option>
<option value="manual">Ajuste Manual ⚙️</option>
</select>
</div>
</div>
</section>
</main>
</div>
<!-- Custom Alert Modal -->
<div class="modal-overlay" id="alertModal">
<div class="modal-content" style="max-width: 400px; text-align: center;">
<div class="modal-header" style="justify-content: center; border-bottom: none; padding-bottom: 0;">
<div id="alertModalIconContainer" style="background: rgba(244, 67, 54, 0.1); color: #f44336; width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0.5rem auto;">
<i id="alertModalIcon" data-lucide="alert-triangle" style="width: 32px; height: 32px;"></i>
</div>
</div>
<h3 style="font-family: var(--font-heading); font-size: 1.25rem; font-weight: 700; color: var(--text-main); margin-top: 0.5rem;" id="alertModalTitle">Atención</h3>
<p style="font-size: 0.9rem; color: var(--text-muted); line-height: 1.5; margin: 0.5rem 0;" id="alertModalMessage">
Mensaje de error
</p>
<div style="display: flex; gap: 0.75rem; justify-content: center; margin-top: 1rem;">
<button class="btn btn-secondary" id="closeAlertBtn" style="padding: 0.6rem 1.5rem;">Cerrar</button>
<button class="btn" id="goConfigBtn" style="padding: 0.6rem 1.5rem; background: var(--primary);">Configurar API</button>
</div>
</div>
</div>
<!-- User Profile Modal -->
<div class="modal-overlay" id="profileModal">
<div class="modal-content" style="max-width: 440px;">
<div class="modal-header">
<h3 class="modal-title" style="display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="user-check" style="color: var(--primary-light);"></i>
<span>Mi Perfil</span>
</h3>
<button type="button" class="close-modal" id="closeProfileModalBtn"></button>
</div>
<form id="profileForm" style="display: flex; flex-direction: column; gap: 1rem;">
<div style="display: flex; align-items: center; gap: 1rem; background: rgba(255,255,255,0.02); border: 1px solid var(--border); padding: 1rem; border-radius: 12px;">
<div style="position: relative; width: 64px; height: 64px; border-radius: 50%; overflow: hidden; border: 2px solid var(--primary-light); flex-shrink: 0; background: #000;">
<img id="profileAvatarPreview" src="" alt="Avatar" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="flex: 1; display: flex; flex-direction: column; gap: 0.35rem;">
<label for="profileAvatarInput" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.4rem 0.75rem; display: inline-flex; align-items: center; gap: 0.35rem; width: fit-content; cursor: pointer;">
<i data-lucide="camera" style="width: 14px; height: 14px;"></i>
<span>Cambiar avatar</span>
</label>
<input type="file" id="profileAvatarInput" accept="image/*" style="display: none;">
<span style="font-size: 0.7rem; color: var(--text-muted);">Formato JPG o PNG transparente</span>
</div>
</div>
<div class="input-group">
<label class="input-label" for="profileNickInput">Apodo / Nickname</label>
<input type="text" id="profileNickInput" class="text-input" placeholder="Tu nombre público" required>
</div>
<div class="input-group">
<label class="input-label" for="profileFullNameInput">Nombre Completo (Opcional)</label>
<input type="text" id="profileFullNameInput" class="text-input" placeholder="Ej. Juan Pérez">
</div>
<div style="display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.5rem;">
<button type="button" class="btn btn-secondary" id="cancelProfileBtn" style="padding: 0.55rem 1.25rem;">Cancelar</button>
<button type="submit" class="btn btn-accent" id="saveProfileBtn" style="padding: 0.55rem 1.25rem; background: var(--primary);">Guardar cambios</button>
</div>
</form>
</div>
</div>
<!-- Privacy & Security Modal -->
<div class="modal-overlay" id="privacyModal">
<div class="modal-content" style="max-width: 440px;">
<div class="modal-header">
<h3 class="modal-title" style="display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="shield-check" style="color: #4caf50;"></i>
<span>Privacidad y Seguridad</span>
</h3>
<button type="button" class="close-modal" id="closePrivacyModalBtn"></button>
</div>
<form id="privacyForm" style="display: flex; flex-direction: column; gap: 1rem;">
<div class="input-group">
<label class="input-label" for="privacyEmailInput">Correo Electrónico de Recuperación</label>
<input type="email" id="privacyEmailInput" class="text-input" placeholder="tu@email.com">
</div>
<div style="border-top: 1px solid var(--border); padding-top: 1rem; margin-top: 0.25rem;">
<h4 style="font-size: 0.85rem; color: var(--text-main); margin-bottom: 0.75rem; font-weight: 600;">Cambiar Contraseña</h4>
<div class="input-group" style="margin-bottom: 0.75rem;">
<label class="input-label" for="privacyCurrentPasswordInput">Contraseña Actual</label>
<input type="password" id="privacyCurrentPasswordInput" class="text-input" placeholder="••••••••">
</div>
<div class="input-group" style="margin-bottom: 0.75rem;">
<label class="input-label" for="privacyNewPasswordInput">Nueva Contraseña</label>
<input type="password" id="privacyNewPasswordInput" class="text-input" placeholder="••••••••">
</div>
<div class="input-group">
<label class="input-label" for="privacyConfirmPasswordInput">Repetir Nueva Contraseña</label>
<input type="password" id="privacyConfirmPasswordInput" class="text-input" placeholder="••••••••">
</div>
</div>
<div style="display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.75rem;">
<button type="button" class="btn btn-secondary" id="cancelPrivacyBtn" style="padding: 0.55rem 1.25rem;">Cancelar</button>
<button type="submit" class="btn btn-accent" id="savePrivacyBtn" style="padding: 0.55rem 1.25rem; background: #4caf50; border-color: #4caf50;">Guardar seguridad</button>
</div>
</form>
</div>
</div>
<!-- Credits Store Modal -->
<div class="modal-overlay" id="creditsModal">
<div class="modal-content" style="max-width: 480px; text-align: center; border: 1px solid rgba(255, 235, 59, 0.25); box-shadow: 0 20px 45px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05); padding: 2rem;">
<div class="modal-header" style="justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; display: flex; width: 100%;">
<h3 style="font-family: var(--font-heading); font-size: 1.25rem; font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 0.5rem; margin: 0;">
<span>🪙 Tienda de Créditos</span>
</h3>
<button class="btn btn-secondary" id="closeCreditsModalBtn" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 8px; cursor: pointer;"></button>
</div>
<p style="font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; margin: 1rem 0 1.5rem 0; text-align: left;">
Recarga créditos para seguir utilizando las herramientas de generación de imágenes 2D y modelos 3D de alta definición.
</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; width: 100%;">
<!-- Pack 1 -->
<div style="background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; position: relative;">
<span style="font-size: 1.8rem; font-weight: 800; color: var(--text-main);">25</span>
<span style="font-size: 0.75rem; font-weight: 600; color: var(--primary-light); text-transform: uppercase; letter-spacing: 0.05em;">Créditos</span>
<div style="font-size: 1.15rem; font-weight: 700; color: #4caf50; margin-top: 0.5rem;">$2.50 USD</div>
<button class="btn btn-accent buy-pack-btn" data-amount="25" style="width: 100%; font-size: 0.8rem; padding: 0.5rem; border-radius: 8px; margin-top: 0.75rem; background: var(--primary-light); cursor: pointer;">Comprar</button>
</div>
<!-- Pack 2 -->
<div style="background: rgba(255, 235, 59, 0.03); border: 1px solid rgba(255, 235, 59, 0.2); border-radius: 12px; padding: 1.25rem; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; position: relative; overflow: hidden;">
<div style="position: absolute; top: 0; right: 0; background: #ffeb3b; color: #000; font-size: 0.55rem; font-weight: 800; padding: 0.25rem 0.65rem; border-bottom-left-radius: 8px; text-transform: uppercase;">Popular</div>
<span style="font-size: 1.8rem; font-weight: 800; color: var(--text-main);">100</span>
<span style="font-size: 0.75rem; font-weight: 600; color: #ffeb3b; text-transform: uppercase; letter-spacing: 0.05em;">Créditos</span>
<div style="font-size: 1.15rem; font-weight: 700; color: #4caf50; margin-top: 0.5rem;">$8.00 USD</div>
<button class="btn buy-pack-btn" data-amount="100" style="width: 100%; font-size: 0.8rem; padding: 0.5rem; border-radius: 8px; margin-top: 0.75rem; background: #ffeb3b; color: #000; border: none; font-weight: 700; cursor: pointer;">Comprar</button>
</div>
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); background: rgba(255, 255, 255, 0.03); padding: 0.75rem; border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 0.35rem; border: 1px dashed var(--border); width: 100%;">
<i data-lucide="shield-check" style="width: 16px; height: 16px; color: #4caf50;"></i>
<span>Simulador de checkout de Stripe activo (Entorno de pruebas)</span>
</div>
</div>
</div>
<!-- Mobile QR Viewer Modal -->
<div class="modal-overlay" id="mobileQrModal">
<div class="modal-content" style="max-width: 400px; text-align: center; border: 1px solid var(--border); box-shadow: 0 20px 45px rgba(0, 0, 0, 0.6); padding: 2rem;">
<div class="modal-header" style="justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; display: flex; width: 100%;">
<h3 style="font-family: var(--font-heading); font-size: 1.15rem; font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 0.5rem; margin: 0;">
<i data-lucide="smartphone"></i>
<span>Ver en tu Celular (AR)</span>
</h3>
<button class="btn btn-secondary" id="closeMobileQrModalBtn" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 8px; cursor: pointer;"></button>
</div>
<p style="font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; margin: 1rem 0;">
Escanea este código QR con tu celular para abrir el visor interactivo y proyectar el modelo en Realidad Aumentada (AR).
</p>
<div style="background: white; padding: 16px; border-radius: 12px; display: inline-block; margin: 0.5rem auto 1rem auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
<img id="mobileQrImage" src="" alt="Código QR" style="display: block; width: 200px; height: 200px;">
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); word-break: break-all; background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border); padding: 0.5rem; border-radius: 6px; margin-top: 0.5rem;">
<a id="mobileQrLink" href="#" target="_blank" style="color: var(--primary-light); text-decoration: none;">Abrir enlace directamente</a>
</div>
</div>
</div>
<div class="auth-overlay active" id="authOverlay">
<canvas id="authBgCanvas" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; opacity: 0.95;"></canvas>
<div class="auth-box" style="position: relative; z-index: 10;">
<h2 class="auth-title">23D<span>Factory</span></h2>
<p class="auth-subtitle">Transforma imágenes 2D en modelos 3D y esqueletos para Blender</p>
<div class="auth-tabs">
<button class="auth-tab active" id="tabLogin">Iniciar Sesión</button>
<button class="auth-tab" id="tabRegister">Registrarse</button>
</div>
<!-- Login Form -->
<form id="loginForm" class="auth-form active">
<div class="input-group">
<label class="input-label" for="loginUser">Usuario</label>
<input type="text" id="loginUser" class="text-input" placeholder="Ingresa tu usuario" required autocomplete="username">
</div>
<div class="input-group">
<label class="input-label" for="loginPass">Contraseña</label>
<input type="password" id="loginPass" class="text-input" placeholder="Ingresa tu contraseña" required autocomplete="current-password">
</div>
<div id="loginErrorMsg" class="auth-error-msg" style="color: #ff5252; font-size: 0.8rem; font-weight: 500; text-align: center; margin-top: 0.75rem; background: rgba(255, 82, 82, 0.1); border: 1px solid rgba(255, 82, 82, 0.2); padding: 0.5rem; border-radius: 6px; display: none;"></div>
<button type="submit" class="btn auth-btn" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 1rem;">
<i data-lucide="log-in" style="width: 16px; height: 16px;"></i>
<span>Iniciar Sesión</span>
</button>
</form>
<!-- Register Form -->
<form id="registerForm" class="auth-form">
<div class="input-group">
<label class="input-label" for="registerUser">Usuario o Email</label>
<input type="text" id="registerUser" class="text-input" placeholder="Ingresa tu usuario o email" required autocomplete="username">
</div>
<div class="input-group">
<label class="input-label" for="registerPass">Contraseña</label>
<input type="password" id="registerPass" class="text-input" placeholder="Crea tu contraseña" required autocomplete="new-password">
</div>
<div class="input-group">
<label class="input-label" for="registerPassConfirm">Confirmar Contraseña</label>
<input type="password" id="registerPassConfirm" class="text-input" placeholder="Repite tu contraseña" required autocomplete="new-password">
</div>
<div id="registerErrorMsg" class="auth-error-msg" style="color: #ff5252; font-size: 0.8rem; font-weight: 500; text-align: center; margin-top: 0.75rem; background: rgba(255, 82, 82, 0.1); border: 1px solid rgba(255, 82, 82, 0.2); padding: 0.5rem; border-radius: 6px; display: none;"></div>
<button type="submit" class="btn auth-btn" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 1rem; background: var(--primary-light);">
<i data-lucide="user-plus" style="width: 16px; height: 16px;"></i>
<span>Registrarse</span>
</button>
</form>
</div>
</div>
<!-- 2D Image Fullscreen Viewer Overlay for Desktop -->
<div id="image-viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 99999; background: #121214; flex-direction: column; justify-content: space-between;">
<!-- Image Viewer Header Control Overlay -->
<div id="imageViewerHeaderBar" style="position: absolute; top: 0; left: 0; right: 0; z-index: 100000; display: flex; justify-content: space-between; align-items: center; padding: 14px 24px; background: rgba(18, 18, 20, 0.9); border-bottom: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(12px);">
<div class="logo-container">
<h1 class="logo-text" style="font-size: 1.2rem; margin: 0;">23D<span>Factory</span></h1>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button id="btnCreate3DFrom2DViewerPC" style="background: linear-gradient(135deg, #10b981, #059669); border: none; color: #ffffff; padding: 8px 18px; border-radius: 20px; font-size: 0.85rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4); display: flex; align-items: center; gap: 6px;">
<i data-lucide="box" style="width: 15px; height: 15px;"></i>
<span>Crear Modelo 3D</span>
</button>
<button id="btnBackFrom2DViewerPC" style="background: linear-gradient(135deg, #ef4444, #dc2626); border: none; color: #ffffff; padding: 8px 18px; border-radius: 20px; font-size: 0.85rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); display: flex; align-items: center; gap: 6px;">
<span>Volver</span>
</button>
</div>
</div>
<!-- Centered 2D Image View -->
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 80px 24px 24px 24px; box-sizing: border-box;">
<img id="full2DImageViewPC" src="" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 12px; box-shadow: 0 15px 35px rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.1);">
</div>
</div>
<!-- Interactive Crop Modal Overlay -->
<div id="cropModalOverlay" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(10, 10, 15, 0.95); backdrop-filter: blur(12px); flex-direction: column; align-items: center; justify-content: space-between; padding: 16px; box-sizing: border-box;">
<!-- Modal Header -->
<div style="width: 100%; max-width: 650px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 1.1rem; color: #fff; display: flex; align-items: center; gap: 8px;">
<i data-lucide="crop" style="width: 18px; height: 18px; color: #a78bfa;"></i>
<span>Enmarcar / Recortar Sujeto</span>
</div>
<button id="btnCancelCrop" style="background: rgba(255,255,255,0.1); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1rem; display: flex; align-items: center; justify-content: center;"></button>
</div>
<!-- Crop Canvas Viewport Container -->
<div style="width: 100%; max-width: 650px; height: calc(100vh - 180px); max-height: 520px; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; margin: 12px 0; background: #000; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);">
<img id="cropImageTarget" src="" style="max-width: 100%; max-height: 100%;">
</div>
<!-- Modal Controls Footer -->
<div style="width: 100%; max-width: 650px; display: flex; flex-direction: column; gap: 10px;">
<div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;">
<button type="button" id="btnCropAspectFree" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Libre</button>
<button type="button" id="btnCropAspectSquare" style="background: rgba(124,58,237,0.3); border: 1px solid #7c3aed; color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">1:1 Cuadrado (Recomendado)</button>
<button type="button" id="btnCropReset" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 14px; border-radius: 16px; font-size: 0.78rem; cursor: pointer; font-weight: 600;">Reiniciar</button>
</div>
<button type="button" id="btnApplyCrop" style="width: 100%; background: linear-gradient(135deg, #10b981, #059669); color: #fff; border: none; padding: 12px; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; box-shadow: 0 4px 12px rgba(16,185,129,0.3); display: flex; align-items: center; justify-content: center; gap: 6px;">
<i data-lucide="check" style="width: 18px; height: 18px;"></i>
<span>Confirmar Recorte</span>
</button>
</div>
</div>
<script type="module" src="app.js?v=200"></script>
<script>
// Initialize Lucide icons on page load
lucide.createIcons();
</script>
</body>
</html>