Spaces:
Build error
Build error
LogicalTrue
feat: agregar soporte nativo de visualizacion FBX con THREE.FBXLoader para mostrar mallas cuadrilateras (Quads) en tiempo real
cb090b4 | <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">×</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">0°</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">0°</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">0°</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> | |