9 Commits

Author SHA1 Message Date
f0baa51415 WIP: Preparar infraestructura para Modo Logo (easter egg)
ROADMAP:
- Añadida tarea #4: Implementar Modo Logo (easter egg)
- Documentada integración con DEMO y DEMO LITE
- Añadida tarea #5: Mejorar sistema vértices PNG_SHAPE

INFRAESTRUCTURA AÑADIDA:
- engine.h: Variable logo_mode_enabled_ + estado previo
- engine.h: Métodos toggleLogoMode(), enterLogoMode(), exitLogoMode()
- defines.h: Constantes LOGO_MODE_* (min balls, scale, timings)
- defines.h: Probabilidades de salto desde DEMO/DEMO_LITE

PENDIENTE IMPLEMENTAR:
- Funciones enterLogoMode() y exitLogoMode()
- Integración con tecla K
- Lógica salto automático desde DEMO/DEMO_LITE
- Excluir PNG_SHAPE de arrays aleatorios
- Display visual "LOGO MODE"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 17:45:04 +02:00
db3d4d6630 Refactor: Mover BALL_COUNT_SCENARIOS a defines.h + priorizar 1 capa
REFACTORING:
- Movido array de escenarios desde engine.h a defines.h
- Nombre más descriptivo: test_ → BALL_COUNT_SCENARIOS
- Ahora es constexpr y accesible globalmente

MEJORA PNG_SHAPE:
- Priorizar calidad 2D sobre profundidad 3D
- Reducir capas AGRESIVAMENTE hasta 1 (antes se detenía en 3)
- Condiciones más estrictas: < total (antes < total * 0.8)
- Vértices activados hasta 150 pelotas (antes 100)

FILOSOFÍA NUEVA:
1. Reducir capas hasta 1 (llenar bien el texto en 2D)
2. Si no alcanza: filas alternas en relleno
3. Si no alcanza: cambiar a bordes
4. Si no alcanza: filas alternas en bordes
5. Último recurso: vértices

RESULTADO ESPERADO:
- 500 pelotas: RELLENO completo 1 capa (texto lleno, sin 3D)
- 100 pelotas: BORDES completos 1 capa (todo visible)
- 50 pelotas: VÉRTICES (esqueleto visible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 17:23:38 +02:00
d030d4270e Fix: PNG_SHAPE distribución adaptativa corregida completamente
PROBLEMAS RESUELTOS:
1. getPoint3D() ignoraba optimización → usaba edge_points_ siempre
2. extractAlternateRows() era destructiva → filtraba sobre filtrado
3. Con 10,000 pelotas mostraba bordes → ahora muestra RELLENO
4. Con 100 pelotas solo primera fila → ahora muestra todo el texto

CAMBIOS IMPLEMENTADOS:
- Añadido optimized_points_ (vector resultado final)
- extractAlternateRows() ahora es función pura (devuelve nuevo vector)
- extractCornerVertices() ahora es función pura
- Cada nivel recalcula desde original (no desde filtrado previo)
- getPoint3D() usa optimized_points_ exclusivamente

FLUJO CORRECTO:
- 10,000 pelotas: RELLENO completo (capas reducidas)
- 500 pelotas: RELLENO + filas alternas (texto completo visible)
- 100 pelotas: BORDES completos (todo el texto visible)
- 10 pelotas: VÉRTICES (esqueleto visible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 17:07:06 +02:00
fbd09b3201 Actualizar ROADMAP: Marcar mejoras de animaciones 3D como completadas
- CYLINDER: tumbling ocasional implementado
- WAVE_GRID: pivoteo sutil implementado
- Ambas mejoras ya estaban en commit 2ae5155

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:52:28 +02:00
a04c1cba13 PNG_SHAPE: Distribución adaptativa multinivel
Sistema de optimización en 3 niveles para cualquier cantidad de pelotas:

Nivel 1 - Cambio de modo:
- Si RELLENO y pocas pelotas → switch a BORDES
- Reduce de ~22K puntos a ~4.5K puntos

Nivel 2 - Reducción de capas:
- Si aún insuficiente → dividir capas a la mitad
- 15 capas → 7 capas → 3 capas → 1 capa
- Reduce profundidad pero mantiene forma visible

Nivel 3 - Sampling de píxeles:
- Si aún insuficiente → tomar cada N píxeles
- Sampling 1/2, 1/3, 1/4... hasta 1/10
- Funciona en BORDES o RELLENO
- Mantiene forma general con menos detalle

Resultado:
- Con 1 pelota: Funciona (1 píxel visible) 
- Con 10 pelotas: Forma reconocible 
- Con 100 pelotas: Forma clara 
- Con 1000+ pelotas: Relleno completo 

Output informativo:
  [PNG_SHAPE] Paso 1: Cambiando RELLENO → BORDES
  [PNG_SHAPE] Paso 2: Reduciendo capas a 3
  [PNG_SHAPE] Paso 3: Aplicando sampling 1/4
  [PNG_SHAPE] === CONFIGURACIÓN FINAL ===
  [PNG_SHAPE] Modo: BORDES (optimizado)
  [PNG_SHAPE] Píxeles 2D: 75 (sampling 1/4)
  [PNG_SHAPE] Capas: 3
  [PNG_SHAPE] Ratio: 1.33 pelotas/punto 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:46:59 +02:00
757bb9c525 PNG_SHAPE: Auto-switch a bordes cuando hay pocas pelotas
Problema:
- PNG_USE_EDGES_ONLY = false usa ~22,000 puntos 3D
- Con 1, 10 o 100 pelotas, no hay suficientes para formar el logo
- Resultado: logo invisible o mal formado

Solución:
- Detectar automáticamente si num_pelotas < total_puntos / 2
- Si hay pocas pelotas → cambiar automáticamente a BORDES
- Bordes usa ~300 puntos × 15 capas = ~4,500 puntos 3D
- Mucho mejor ratio para pocos sprites

Implementación:
- generatePoints() verifica ratio pelotas/puntos
- Si insuficiente: llama detectEdges() y regenera
- getPoint3D() usa edge_points_ si están disponibles
- Mensajes informativos en consola

Ahora funciona:
  Escenario 1 (1 pelota) → Auto-switch a bordes 
  Escenario 2 (10 pelotas) → Auto-switch a bordes 
  Escenario 5 (1000 pelotas) → Usa relleno completo 
  Escenario 6+ (10K+ pelotas) → Usa relleno completo 

Output de debug muestra:
  [PNG_SHAPE] Advertencia: Solo X pelotas para Y puntos
  [PNG_SHAPE] Cambiando automáticamente a BORDES...
  [PNG_SHAPE] Modo: BORDES/RELLENO

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:42:27 +02:00
723bb6d198 Añadir parámetro -z/--zoom con validación inteligente
Defaults correctos (sin CLI):
- Resolución: 320x240
- Zoom: 3
- Ventana resultante: 960x720

Nuevas funcionalidades:
- Parámetro -z/--zoom para especificar zoom de ventana
- Si se pasan -w/-h sin -z: zoom automático = 1
- Validación de resolución vs pantalla
- Validación de zoom vs max_zoom calculado

Lógica de validación:
1. Si resolución > pantalla → reset a 320x240 zoom 3
2. Calcular max_zoom = min(screen_w/width, screen_h/height)
3. Si zoom > max_zoom → ajustar a max_zoom
4. Si CLI con -w/-h pero sin -z → zoom = 1 (auto)

Ejemplos:
  ./vibe3_physics                   # 320x240 zoom 3 
  ./vibe3_physics -w 1920 -h 1080   # 1920x1080 zoom 1 
  ./vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (1280x960) 
  ./vibe3_physics -w 9999 -h 9999   # Reset a default (warning) 

Archivos:
- defines.h: Renombrar WINDOW_ZOOM → DEFAULT_WINDOW_ZOOM
- main.cpp: Añadir parsing -z/--zoom
- engine.h: initialize() acepta zoom
- engine.cpp: Validación + advertencias informativas

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 14:02:02 +02:00
1e5c9f8f9d Fix: F5 (toggle scaling) respeta resolución personalizada
Problema:
- Con -w 960 -h 800, al pulsar F5 en fullscreen (F3)
- La resolución cambiaba a 320x240 hardcoded
- Pantalla quedaba rota con viewport diminuto

Causa:
toggleIntegerScaling() usaba SCREEN_WIDTH/HEIGHT constantes
en SDL_SetRenderLogicalPresentation()

Solución:
Usar current_screen_width_/height_ dinámicos

Ahora funciona:
  ./vibe3_physics -w 960 -h 800
  F3 (fullscreen) → OK 
  F5 (cambiar scaling) → OK 
  ESC (salir fullscreen) → OK 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:43:59 +02:00
e24f06ed90 Fix: Resolución dinámica CLI respeta parámetros personalizados
Problema:
- Al usar -w/-h, la ventana se creaba correcta
- Pero el renderizado interno seguía usando SCREEN_WIDTH/HEIGHT (320x240)
- Resultado: ventana grande con área de juego pequeña en esquina

Solución:
- Añadidas variables base_screen_width/height_
- Guardan resolución configurada por CLI (o default)
- current_screen_* ahora se inicializa con valores base
- toggleRealFullscreen() restaura a resolución base, no constantes

Cambios:
- engine.h: Añadir base_screen_width/height_
- engine.cpp: Inicializar con valores CLI
- engine.cpp: Usar base_* al salir de fullscreen real

Ahora funciona:
  ./vibe3_physics -w 1920 -h 1080  # Renderiza en 1920x1080 
  ./vibe3_physics                  # Renderiza en 1280x720 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:34:00 +02:00
8 changed files with 740 additions and 415 deletions

View File

@@ -30,33 +30,24 @@
## Mejoras de Presentación 🎨 ## Mejoras de Presentación 🎨
### 1. Mejorar Animaciones de Figuras 3D ### 1. Mejorar Animaciones de Figuras 3D
**Descripción:** Añadir movimientos más dinámicos e interesantes a algunas figuras **Descripción:** Añadir movimientos más dinámicos e interesantes a algunas figuras
**Prioridad:** Media **Prioridad:** Media
**Estimación:** 1.5 horas **Estado:** ✅ COMPLETADO
**Detalles:** **Detalles:**
#### CYLINDER (Y): #### CYLINDER (Y):
- **Rotación actual:** Solo eje Y (spin horizontal continuo) - **Rotación principal en eje Y** (spin horizontal continuo)
- **Mejora propuesta:** Rotaciones multi-eje con cambios periódicos - **Tumbling ocasional en eje X** cada 3-5 segundos
- Rotación principal en eje Y (como ahora) - ✅ Transiciones suaves con ease-in-out (1.5s duración)
- Cada 3-5 segundos: tumbling en eje X o Z durante 1-2 segundos - ✅ Efecto visual: cilindro "se da una vuelta" ocasionalmente
- Efecto visual: "dar una vuelta" sobre otro eje ocasionalmente
- Transiciones suaves con aceleración/desaceleración
#### WAVE_GRID (W): #### WAVE_GRID (W):
- **Rotación actual:** XY girando como esfera (confuso) - **Vista frontal paralela a pantalla** (sin rotación confusa)
- **Mejora propuesta:** Vista frontal con pivoteo sutil - **Pivoteo sutil en ejes X e Y**
- **Vista principal:** Paralela a pantalla (mirando de frente) - ✅ Esquinas se mueven adelante/atrás según posición
- **Movimiento:** Pivoteo en centro con esquinas hacia adelante/atrás - Movimiento ondulatorio + pivoteo = efecto "océano"
- Ejemplo: esquina superior-derecha se aleja (Z-), inferior-izquierda se acerca (Z+) - ✅ Velocidades lentas (0.3-0.5 rad/s) para organicidad
- Movimiento ondulatorio sincronizado: olas + pivoteo crea efecto "océano"
- **Opcional:** Rotación completa ocasional (cada 10-15s) como transición
**Implementación técnica:**
- Añadir `rotation_mode_` y `mode_timer_` a cada shape
- Estados: NORMAL, TRANSITION, ALTERNATIVE
- Interpolación suave entre modos de rotación
### 2. ✅ Modo DEMO (Auto-play) ### 2. ✅ Modo DEMO (Auto-play)
**Descripción:** Modo demostración automática con acciones aleatorias **Descripción:** Modo demostración automática con acciones aleatorias
@@ -83,7 +74,57 @@
- ✅ Help text con `--help` - ✅ Help text con `--help`
- Ejemplo: `./vibe3_physics -w 1920 -h 1080 -f` - Ejemplo: `./vibe3_physics -w 1920 -h 1080 -f`
### 4. 🐛 Corregir Escalado de Pelotas en Reposo ### 4. 🎯 Implementar Modo Logo (Easter Egg)
**Descripción:** Modo especial que muestra el logo JAILGAMES como "marca de agua"
**Prioridad:** Alta (característica distintiva)
**Estimación:** 2 horas
**Detalles:**
#### Configuración Modo Logo:
- **Figura:** Solo PNG_SHAPE (logo JAILGAMES)
- **Textura:** Siempre "tiny" (pelota más pequeña)
- **Tema:** Siempre MONOCHROME (blanco puro)
- **Escala:** 120% (figuras más grandes que normal)
- **Pelotas mínimas:** 500
- **Tecla manual:** K (activa/desactiva modo logo)
#### Comportamiento en Modo Logo:
- Alterna entre modo SHAPE y modo PHYSICS (como DEMO)
- Mantiene configuración fija (no cambia tema/textura/escala)
- Es como un "DEMO específico del logo"
#### Integración con DEMO LITE:
- **Requisitos para salto automático:**
- Mínimo 500 pelotas
- Tema MONOCHROME activo
- Si se cumplen → cambia automáticamente textura a "tiny" y escala a 120%
- **Duración:** Menos tiempo que DEMO normal (es un "recordatorio")
- **Después:** Vuelve a DEMO LITE normal
#### Integración con DEMO:
- **Requisitos:** Mínimo 500 pelotas
- **Acción:** Cambia automáticamente a: MONOCHROME + tiny + escala 120%
- **Duración:** Menos tiempo que acciones normales
- **Después:** Vuelve a DEMO normal
#### Proporción temporal sugerida:
- DEMO/DEMO_LITE normal: 80-90% del tiempo
- Modo Logo: 10-20% del tiempo (aparición ocasional como "easter egg")
### 5. ⏳ Mejorar Sistema de Vértices PNG_SHAPE
**Descripción:** Con 50 pelotas no activa modo vértices correctamente
**Prioridad:** Baja (mejora visual)
**Estimación:** 1 hora
**Detalles:**
- **Comportamiento actual:** Con 50 pelotas usa filas alternas en bordes
- **Comportamiento deseado:** Activar modo VÉRTICES (extremos izq/der de cada fila)
- **Problema:** Condición `num_points < 150` no es suficientemente agresiva
- **Solución propuesta:**
- Ajustar umbrales de activación de vértices
- Mejorar algoritmo extractCornerVertices() para detectar puntos clave
- Considerar densidad de píxeles en decisión (no solo cantidad absoluta)
### 5. 🐛 Corregir Escalado de Pelotas en Reposo
**Descripción:** Las pelotas cambian de tamaño cuando están quietas (bug visual) **Descripción:** Las pelotas cambian de tamaño cuando están quietas (bug visual)
**Prioridad:** Alta (bug visible) **Prioridad:** Alta (bug visible)
**Estimación:** 30 minutos **Estimación:** 30 minutos
@@ -96,7 +137,7 @@
- **Investigar:** Ball::render(), scale calculations, depth brightness - **Investigar:** Ball::render(), scale calculations, depth brightness
- **Solución esperada:** Tamaño constante independiente de velocidad - **Solución esperada:** Tamaño constante independiente de velocidad
### 5. ⏳ Sistema de Release ### 6. ⏳ Sistema de Release
**Descripción:** Empaquetado para distribución standalone **Descripción:** Empaquetado para distribución standalone
**Prioridad:** Baja **Prioridad:** Baja
**Estimación:** 30 minutos **Estimación:** 30 minutos
@@ -165,13 +206,15 @@
- ✅ Resolución configurable (mínimo 640x480) - ✅ Resolución configurable (mínimo 640x480)
- 📝 Preparado para voxelización 3D (Enfoque B) en futuro - 📝 Preparado para voxelización 3D (Enfoque B) en futuro
### 2025-10-04 (Sesión 4) - Modo DEMO ### 2025-10-04 (Sesión 4) - Modo DEMO + Mejoras Animaciones
-**Implementado Modo DEMO (auto-play)** - Tecla D para toggle -**Implementado Modo DEMO (auto-play)** - Tecla D para toggle
- ✅ Sistema de acciones aleatorias cada 3-8 segundos (configurable) - ✅ Sistema de acciones aleatorias cada 3-8 segundos (configurable)
- ✅ 8 tipos de acciones con pesos de probabilidad ajustables - ✅ 8 tipos de acciones con pesos de probabilidad ajustables
- ✅ Totalmente interactivo - usuario puede seguir controlando - ✅ Totalmente interactivo - usuario puede seguir controlando
- ✅ Display visual "DEMO MODE" centrado en naranja - ✅ Display visual "DEMO MODE" centrado en naranja
- ✅ Mejoras animaciones 3D: tumbling en cilindro + pivoteo en wave grid -**Mejoras animaciones 3D**: tumbling en cilindro + pivoteo en wave grid
- ✅ CYLINDER: tumbling ocasional en eje X cada 3-5s con ease-in-out
- ✅ WAVE_GRID: pivoteo sutil paralelo a pantalla (efecto océano)
- ❌ Eliminado sistema auto-restart antiguo (ya no necesario) - ❌ Eliminado sistema auto-restart antiguo (ya no necesario)
### 2025-10-04 (Sesión 3) ### 2025-10-04 (Sesión 3)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -5,10 +5,10 @@
// Configuración de ventana y pantalla // Configuración de ventana y pantalla
constexpr char WINDOW_CAPTION[] = "vibe3_physics"; constexpr char WINDOW_CAPTION[] = "vibe3_physics";
constexpr int SCREEN_WIDTH = 320; // Ancho de la pantalla lógica (píxeles) // Resolución por defecto (usada si no se especifica en CLI)
constexpr int SCREEN_HEIGHT = 240; // Alto de la pantalla lógica (píxeles) constexpr int DEFAULT_SCREEN_WIDTH = 320; // Ancho lógico por defecto (si no hay -w)
constexpr int WINDOW_ZOOM = 3; // Zoom inicial de la ventana constexpr int DEFAULT_SCREEN_HEIGHT = 240; // Alto lógico por defecto (si no hay -h)
// BALL_SIZE eliminado: ahora se obtiene dinámicamente desde texture_->getWidth() constexpr int DEFAULT_WINDOW_ZOOM = 3; // Zoom inicial de ventana (1x = sin zoom)
// Configuración de zoom dinámico de ventana // Configuración de zoom dinámico de ventana
constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (320x240) constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (320x240)
@@ -38,6 +38,9 @@ constexpr float GRAVITY_CHANGE_LATERAL_MAX = 0.08f; // Velocidad lateral máxim
// Configuración de spawn inicial de pelotas // Configuración de spawn inicial de pelotas
constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado) constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado)
// Escenarios de número de pelotas (teclas 1-8)
constexpr int BALL_COUNT_SCENARIOS[8] = {10, 50, 100, 500, 1000, 5000, 10000, 50000};
// Estructura para representar colores RGB // Estructura para representar colores RGB
struct Color { struct Color {
int r, g, b; // Componentes rojo, verde, azul (0-255) int r, g, b; // Componentes rojo, verde, azul (0-255)
@@ -197,10 +200,21 @@ constexpr float DEMO_LITE_ACTION_INTERVAL_MAX = 4.0f;
// Pesos de probabilidad DEMO LITE (solo física/figuras, sin cambios de escenario/tema) // Pesos de probabilidad DEMO LITE (solo física/figuras, sin cambios de escenario/tema)
constexpr int DEMO_LITE_WEIGHT_GRAVITY_DIR = 25; // Cambiar dirección gravedad (25%) constexpr int DEMO_LITE_WEIGHT_GRAVITY_DIR = 25; // Cambiar dirección gravedad (25%)
constexpr int DEMO_LITE_WEIGHT_GRAVITY_TOGGLE = 20;// Toggle gravedad ON/OFF (20%) constexpr int DEMO_LITE_WEIGHT_GRAVITY_TOGGLE = 20; // Toggle gravedad ON/OFF (20%)
constexpr int DEMO_LITE_WEIGHT_SHAPE = 25; // Activar figura 3D (25%) constexpr int DEMO_LITE_WEIGHT_SHAPE = 25; // Activar figura 3D (25%)
constexpr int DEMO_LITE_WEIGHT_TOGGLE_PHYSICS = 20;// Toggle física ↔ figura (20%) constexpr int DEMO_LITE_WEIGHT_TOGGLE_PHYSICS = 20; // Toggle física ↔ figura (20%)
constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%) constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%)
// TOTAL: 100 // TOTAL: 100
// Configuración de Modo LOGO (easter egg - "marca de agua")
constexpr int LOGO_MODE_MIN_BALLS = 500; // Mínimo de pelotas para activar modo logo
constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%)
constexpr float LOGO_ACTION_INTERVAL_MIN = 2.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS
constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS
constexpr int LOGO_WEIGHT_TOGGLE_PHYSICS = 100; // Único peso: alternar SHAPE ↔ PHYSICS (100%)
// Probabilidad de salto a Logo Mode desde DEMO/DEMO_LITE (%)
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO = 15; // 15% probabilidad en DEMO normal
constexpr int LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE = 10; // 10% probabilidad en DEMO LITE
constexpr float PI = 3.14159265358979323846f; // Constante PI constexpr float PI = 3.14159265358979323846f; // Constante PI

View File

@@ -13,9 +13,9 @@
#include <cstdlib> // for rand, srand #include <cstdlib> // for rand, srand
#include <cstring> // for strlen #include <cstring> // for strlen
#include <ctime> // for time #include <ctime> // for time
#include <filesystem> // for path operations
#include <iostream> // for cout #include <iostream> // for cout
#include <string> // for string #include <string> // for string
#include <filesystem> // for path operations
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> // for GetModuleFileName #include <windows.h> // for GetModuleFileName
@@ -24,15 +24,15 @@
#include "ball.h" // for Ball #include "ball.h" // for Ball
#include "external/dbgtxt.h" // for dbg_init, dbg_print #include "external/dbgtxt.h" // for dbg_init, dbg_print
#include "external/texture.h" // for Texture #include "external/texture.h" // for Texture
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/cube_shape.h" // for CubeShape
#include "shapes/helix_shape.h" // for HelixShape
#include "shapes/wave_grid_shape.h" // for WaveGridShape
#include "shapes/torus_shape.h" // for TorusShape
#include "shapes/cylinder_shape.h" // for CylinderShape
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
#include "shapes/atom_shape.h" // for AtomShape #include "shapes/atom_shape.h" // for AtomShape
#include "shapes/cube_shape.h" // for CubeShape
#include "shapes/cylinder_shape.h" // for CylinderShape
#include "shapes/helix_shape.h" // for HelixShape
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
#include "shapes/png_shape.h" // for PNGShape #include "shapes/png_shape.h" // for PNGShape
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/torus_shape.h" // for TorusShape
#include "shapes/wave_grid_shape.h" // for WaveGridShape
// Función auxiliar para obtener la ruta del directorio del ejecutable // Función auxiliar para obtener la ruta del directorio del ejecutable
std::string getExecutableDirectory() { std::string getExecutableDirectory() {
@@ -48,19 +48,64 @@ std::string getExecutableDirectory() {
} }
// Implementación de métodos públicos // Implementación de métodos públicos
bool Engine::initialize(int width, int height, bool fullscreen) { bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
bool success = true; bool success = true;
// Usar parámetros o valores por defecto // Obtener resolución de pantalla para validación
int window_width = (width > 0) ? width : SCREEN_WIDTH * WINDOW_ZOOM;
int window_height = (height > 0) ? height : SCREEN_HEIGHT * WINDOW_ZOOM;
int logical_width = (width > 0) ? width : SCREEN_WIDTH;
int logical_height = (height > 0) ? height : SCREEN_HEIGHT;
if (!SDL_Init(SDL_INIT_VIDEO)) { if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl; std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl;
success = false; return false;
} else { }
int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
const auto *dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
int screen_w = dm ? dm->w : 1920; // Fallback si falla
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
if (displays) SDL_free(displays);
// Usar parámetros o valores por defecto
int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT;
int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM;
// VALIDACIÓN 1: Si resolución > pantalla → reset a default
if (logical_width > screen_w || logical_height > screen_h) {
std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height
<< " excede pantalla " << screen_w << "x" << screen_h
<< ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n";
logical_width = DEFAULT_SCREEN_WIDTH;
logical_height = DEFAULT_SCREEN_HEIGHT;
window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también
}
// VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario
int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height);
if (window_zoom > max_zoom) {
std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom
<< " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n";
window_zoom = max_zoom;
}
// Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1
if ((width > 0 || height > 0) && zoom == 0) {
window_zoom = 1;
}
// Calcular tamaño de ventana
int window_width = logical_width * window_zoom;
int window_height = logical_height * window_zoom;
// Guardar resolución base (configurada por CLI o default)
base_screen_width_ = logical_width;
base_screen_height_ = logical_height;
current_screen_width_ = logical_width;
current_screen_height_ = logical_height;
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica) // Crear ventana principal (fullscreen si se especifica)
Uint32 window_flags = SDL_WINDOW_OPENGL; Uint32 window_flags = SDL_WINDOW_OPENGL;
if (fullscreen) { if (fullscreen) {
@@ -211,7 +256,7 @@ void Engine::update() {
// Bifurcar actualización según modo activo // Bifurcar actualización según modo activo
if (current_mode_ == SimulationMode::PHYSICS) { if (current_mode_ == SimulationMode::PHYSICS) {
// Modo física normal: actualizar física de cada pelota // Modo física normal: actualizar física de cada pelota
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->update(delta_time_); // Pasar delta time a cada pelota ball->update(delta_time_); // Pasar delta time a cada pelota
} }
} else if (current_mode_ == SimulationMode::SHAPE) { } else if (current_mode_ == SimulationMode::SHAPE) {
@@ -607,8 +652,7 @@ void Engine::render() {
} }
// Ordenar índices por profundidad Z (menor primero = fondo primero) // Ordenar índices por profundidad Z (menor primero = fondo primero)
std::sort(render_order.begin(), render_order.end(), std::sort(render_order.begin(), render_order.end(), [this](size_t a, size_t b) {
[this](size_t a, size_t b) {
return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness(); return balls_[a]->getDepthBrightness() < balls_[b]->getDepthBrightness();
}); });
@@ -620,8 +664,7 @@ void Engine::render() {
float depth_scale = balls_[idx]->getDepthScale(); float depth_scale = balls_[idx]->getDepthScale();
// Mapear brightness de 0-1 a rango MIN-MAX // Mapear brightness de 0-1 a rango MIN-MAX
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
(ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
// Aplicar factor de brillo al color // Aplicar factor de brillo al color
int r_mod = static_cast<int>(color.r * brightness_factor); int r_mod = static_cast<int>(color.r * brightness_factor);
@@ -633,7 +676,7 @@ void Engine::render() {
} else { } else {
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad) // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
size_t idx = 0; size_t idx = 0;
for (auto &ball : balls_) { for (auto& ball : balls_) {
SDL_FRect pos = ball->getPosition(); SDL_FRect pos = ball->getPosition();
Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP) Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP)
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f); addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
@@ -735,7 +778,7 @@ void Engine::initBalls(int value) {
changeGravityDirection(GravityDirection::DOWN); changeGravityDirection(GravityDirection::DOWN);
// Crear las bolas según el escenario // Crear las bolas según el escenario
for (int i = 0; i < test_.at(value); ++i) { for (int i = 0; i < BALL_COUNT_SCENARIOS[value]; ++i) {
const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -) const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -)
// Calcular spawn zone: margen a cada lado, zona central para spawn // Calcular spawn zone: margen a cada lado, zona central para spawn
const int margin = static_cast<int>(current_screen_width_ * BALL_SPAWN_MARGIN); const int margin = static_cast<int>(current_screen_width_ * BALL_SPAWN_MARGIN);
@@ -744,7 +787,7 @@ void Engine::initBalls(int value) {
const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X
const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y
// Seleccionar color de la paleta del tema actual // Seleccionar color de la paleta del tema actual
ThemeColors &theme = themes_[static_cast<int>(current_theme_)]; ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
int color_index = rand() % theme.ball_colors.size(); // Cantidad variable de colores por tema int color_index = rand() % theme.ball_colors.size(); // Cantidad variable de colores por tema
const Color COLOR = theme.ball_colors[color_index]; const Color COLOR = theme.ball_colors[color_index];
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada) // Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
@@ -758,7 +801,7 @@ void Engine::setText() {
// Suprimir textos durante modos demo // Suprimir textos durante modos demo
if (demo_mode_enabled_ || demo_lite_enabled_) return; if (demo_mode_enabled_ || demo_lite_enabled_) return;
int num_balls = test_.at(scenario_); int num_balls = BALL_COUNT_SCENARIOS[scenario_];
if (num_balls == 1) { if (num_balls == 1) {
text_ = "1 PELOTA"; text_ = "1 PELOTA";
} else { } else {
@@ -770,7 +813,7 @@ void Engine::setText() {
} }
void Engine::pushBallsAwayFromGravity() { void Engine::pushBallsAwayFromGravity() {
for (auto &ball : balls_) { for (auto& ball : balls_) {
const int SIGNO = ((rand() % 2) * 2) - 1; const int SIGNO = ((rand() % 2) * 2) - 1;
const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO; const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
const float MAIN = ((rand() % 40) * 0.1f) + 5; const float MAIN = ((rand() % 40) * 0.1f) + 5;
@@ -799,32 +842,32 @@ void Engine::pushBallsAwayFromGravity() {
} }
void Engine::switchBallsGravity() { void Engine::switchBallsGravity() {
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->switchGravity(); ball->switchGravity();
} }
} }
void Engine::enableBallsGravityIfDisabled() { void Engine::enableBallsGravityIfDisabled() {
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->enableGravityIfDisabled(); ball->enableGravityIfDisabled();
} }
} }
void Engine::forceBallsGravityOn() { void Engine::forceBallsGravityOn() {
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->forceGravityOn(); ball->forceGravityOn();
} }
} }
void Engine::forceBallsGravityOff() { void Engine::forceBallsGravityOff() {
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->forceGravityOff(); ball->forceGravityOff();
} }
} }
void Engine::changeGravityDirection(GravityDirection direction) { void Engine::changeGravityDirection(GravityDirection direction) {
current_gravity_ = direction; current_gravity_ = direction;
for (auto &ball : balls_) { for (auto& ball : balls_) {
ball->setGravityDirection(direction); ball->setGravityDirection(direction);
ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio ball->applyRandomLateralPush(); // Aplicar empuje lateral aleatorio
} }
@@ -867,9 +910,9 @@ void Engine::toggleRealFullscreen() {
if (real_fullscreen_enabled_) { if (real_fullscreen_enabled_) {
// Obtener resolución del escritorio // Obtener resolución del escritorio
int num_displays = 0; int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr && num_displays > 0) { if (displays != nullptr && num_displays > 0) {
const auto *dm = SDL_GetCurrentDisplayMode(displays[0]); const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) { if (dm != nullptr) {
// Cambiar a resolución nativa del escritorio // Cambiar a resolución nativa del escritorio
current_screen_width_ = dm->w; current_screen_width_ = dm->w;
@@ -891,16 +934,16 @@ void Engine::toggleRealFullscreen() {
// Ocultar cursor en real fullscreen // Ocultar cursor en real fullscreen
SDL_HideCursor(); SDL_HideCursor();
} else { } else {
// Volver a resolución original // Volver a resolución base (configurada por CLI o default)
current_screen_width_ = SCREEN_WIDTH; current_screen_width_ = base_screen_width_;
current_screen_height_ = SCREEN_HEIGHT; current_screen_height_ = base_screen_height_;
// Restaurar ventana normal // Restaurar ventana normal
SDL_SetWindowFullscreen(window_, false); SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, SCREEN_WIDTH * WINDOW_ZOOM, SCREEN_HEIGHT * WINDOW_ZOOM); SDL_SetWindowSize(window_, base_screen_width_ * DEFAULT_WINDOW_ZOOM, base_screen_height_ * DEFAULT_WINDOW_ZOOM);
// Restaurar presentación lógica original // Restaurar presentación lógica base
SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Reinicar la escena con resolución original // Reinicar la escena con resolución original
initBalls(scenario_); initBalls(scenario_);
@@ -948,7 +991,7 @@ void Engine::toggleIntegerScaling() {
break; break;
} }
SDL_SetRenderLogicalPresentation(renderer_, SCREEN_WIDTH, SCREEN_HEIGHT, presentation); SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
// Mostrar texto informativo // Mostrar texto informativo
text_ = "SCALING: "; text_ = "SCALING: ";
@@ -960,11 +1003,16 @@ void Engine::toggleIntegerScaling() {
std::string Engine::gravityDirectionToString(GravityDirection direction) const { std::string Engine::gravityDirectionToString(GravityDirection direction) const {
switch (direction) { switch (direction) {
case GravityDirection::DOWN: return "DOWN"; case GravityDirection::DOWN:
case GravityDirection::UP: return "UP"; return "DOWN";
case GravityDirection::LEFT: return "LEFT"; case GravityDirection::UP:
case GravityDirection::RIGHT: return "RIGHT"; return "UP";
default: return "UNKNOWN"; case GravityDirection::LEFT:
return "LEFT";
case GravityDirection::RIGHT:
return "RIGHT";
default:
return "UNKNOWN";
} }
} }
@@ -977,8 +1025,8 @@ void Engine::renderGradientBackground() {
if (transitioning_) { if (transitioning_) {
// Interpolar entre tema actual y tema destino // Interpolar entre tema actual y tema destino
ThemeColors &current = themes_[static_cast<int>(current_theme_)]; ThemeColors& current = themes_[static_cast<int>(current_theme_)];
ThemeColors &target = themes_[static_cast<int>(target_theme_)]; ThemeColors& target = themes_[static_cast<int>(target_theme_)];
top_r = lerp(current.bg_top_r, target.bg_top_r, transition_progress_); top_r = lerp(current.bg_top_r, target.bg_top_r, transition_progress_);
top_g = lerp(current.bg_top_g, target.bg_top_g, transition_progress_); top_g = lerp(current.bg_top_g, target.bg_top_g, transition_progress_);
@@ -989,7 +1037,7 @@ void Engine::renderGradientBackground() {
bottom_b = lerp(current.bg_bottom_b, target.bg_bottom_b, transition_progress_); bottom_b = lerp(current.bg_bottom_b, target.bg_bottom_b, transition_progress_);
} else { } else {
// Sin transición: usar tema actual directamente // Sin transición: usar tema actual directamente
ThemeColors &theme = themes_[static_cast<int>(current_theme_)]; ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
top_r = theme.bg_top_r; top_r = theme.bg_top_r;
top_g = theme.bg_top_g; top_g = theme.bg_top_g;
top_b = theme.bg_top_b; top_b = theme.bg_top_b;
@@ -1081,20 +1129,20 @@ void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g,
int Engine::calculateMaxWindowZoom() const { int Engine::calculateMaxWindowZoom() const {
// Obtener información del display usando el método de Coffee Crisis // Obtener información del display usando el método de Coffee Crisis
int num_displays = 0; int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) { if (displays == nullptr || num_displays == 0) {
return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener
} }
// Obtener el modo de display actual // Obtener el modo de display actual
const auto *dm = SDL_GetCurrentDisplayMode(displays[0]); const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm == nullptr) { if (dm == nullptr) {
SDL_free(displays); SDL_free(displays);
return WINDOW_ZOOM_MIN; return WINDOW_ZOOM_MIN;
} }
// Calcular zoom máximo usando la fórmula de Coffee Crisis // Calcular zoom máximo usando la fórmula de Coffee Crisis
const int MAX_ZOOM = std::min(dm->w / SCREEN_WIDTH, (dm->h - WINDOW_DECORATION_HEIGHT) / SCREEN_HEIGHT); const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);
SDL_free(displays); SDL_free(displays);
@@ -1114,12 +1162,12 @@ void Engine::setWindowZoom(int new_zoom) {
// Obtener posición actual del centro de la ventana // Obtener posición actual del centro de la ventana
int current_x, current_y; int current_x, current_y;
SDL_GetWindowPosition(window_, &current_x, &current_y); SDL_GetWindowPosition(window_, &current_x, &current_y);
int current_center_x = current_x + (SCREEN_WIDTH * current_window_zoom_) / 2; int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
int current_center_y = current_y + (SCREEN_HEIGHT * current_window_zoom_) / 2; int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;
// Calcular nuevo tamaño // Calcular nuevo tamaño
int new_width = SCREEN_WIDTH * new_zoom; int new_width = base_screen_width_ * new_zoom;
int new_height = SCREEN_HEIGHT * new_zoom; int new_height = base_screen_height_ * new_zoom;
// Calcular nueva posición (centrada en el punto actual) // Calcular nueva posición (centrada en el punto actual)
int new_x = current_center_x - new_width / 2; int new_x = current_center_x - new_width / 2;
@@ -1156,46 +1204,77 @@ void Engine::zoomOut() {
void Engine::initializeThemes() { void Engine::initializeThemes() {
// SUNSET: Naranjas, rojos, amarillos, rosas (8 colores) // SUNSET: Naranjas, rojos, amarillos, rosas (8 colores)
themes_[0] = { themes_[0] = {
"SUNSET", "ATARDECER", // Nombres (inglés, español) "SUNSET",
255, 140, 60, // Color texto: naranja cálido "ATARDECER", // Nombres (inglés, español)
180.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo superior (naranja suave) 255,
40.0f / 255.0f, 20.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior (púrpura oscuro) 140,
{{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}} 60, // Color texto: naranja cálido
}; 180.0f / 255.0f,
140.0f / 255.0f,
100.0f / 255.0f, // Fondo superior (naranja suave)
40.0f / 255.0f,
20.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior (púrpura oscuro)
{{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}}};
// OCEAN: Azules, turquesas, blancos (8 colores) // OCEAN: Azules, turquesas, blancos (8 colores)
themes_[1] = { themes_[1] = {
"OCEAN", "OCEANO", // Nombres (inglés, español) "OCEAN",
80, 200, 255, // Color texto: azul océano "OCEANO", // Nombres (inglés, español)
100.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo superior (azul cielo) 80,
20.0f / 255.0f, 40.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior (azul marino) 200,
{{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}} 255, // Color texto: azul océano
}; 100.0f / 255.0f,
150.0f / 255.0f,
200.0f / 255.0f, // Fondo superior (azul cielo)
20.0f / 255.0f,
40.0f / 255.0f,
80.0f / 255.0f, // Fondo inferior (azul marino)
{{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}}};
// NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores) // NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores)
themes_[2] = { themes_[2] = {
"NEON", "NEON", // Nombres (inglés, español) "NEON",
255, 60, 255, // Color texto: magenta brillante "NEON", // Nombres (inglés, español)
20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior (negro azulado) 255,
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro) 60,
{{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}} 255, // Color texto: magenta brillante
}; 20.0f / 255.0f,
20.0f / 255.0f,
40.0f / 255.0f, // Fondo superior (negro azulado)
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior (negro)
{{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}}};
// FOREST: Verdes, marrones, amarillos otoño (8 colores) // FOREST: Verdes, marrones, amarillos otoño (8 colores)
themes_[3] = { themes_[3] = {
"FOREST", "BOSQUE", // Nombres (inglés, español) "FOREST",
100, 255, 100, // Color texto: verde natural "BOSQUE", // Nombres (inglés, español)
144.0f / 255.0f, 238.0f / 255.0f, 144.0f / 255.0f, // Fondo superior (verde claro) 100,
101.0f / 255.0f, 67.0f / 255.0f, 33.0f / 255.0f, // Fondo inferior (marrón tierra) 255,
{{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}} 100, // Color texto: verde natural
}; 144.0f / 255.0f,
238.0f / 255.0f,
144.0f / 255.0f, // Fondo superior (verde claro)
101.0f / 255.0f,
67.0f / 255.0f,
33.0f / 255.0f, // Fondo inferior (marrón tierra)
{{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}}};
// RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática // RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática
themes_[4] = { themes_[4] = {
"RGB", "RGB", // Nombres (inglés, español) "RGB",
100, 100, 100, // Color texto: gris oscuro (contraste con fondo blanco) "RGB", // Nombres (inglés, español)
1.0f, 1.0f, 1.0f, // Fondo superior (blanco puro) 100,
1.0f, 1.0f, 1.0f, // Fondo inferior (blanco puro) - sin degradado 100,
100, // Color texto: gris oscuro (contraste con fondo blanco)
1.0f,
1.0f,
1.0f, // Fondo superior (blanco puro)
1.0f,
1.0f,
1.0f, // Fondo inferior (blanco puro) - sin degradado
{ {
{255, 0, 0}, // 0° - Rojo puro {255, 0, 0}, // 0° - Rojo puro
{255, 64, 0}, // 15° - Rojo-Naranja {255, 64, 0}, // 15° - Rojo-Naranja
@@ -1221,15 +1300,21 @@ void Engine::initializeThemes() {
{255, 0, 191}, // 315° - Magenta-Rojo claro {255, 0, 191}, // 315° - Magenta-Rojo claro
{255, 0, 128}, // 330° - Magenta-Rojo {255, 0, 128}, // 330° - Magenta-Rojo
{255, 0, 64} // 345° - Magenta claro-Rojo {255, 0, 64} // 345° - Magenta claro-Rojo
} }};
};
// MONOCHROME: Fondo negro degradado, sprites blancos monocromáticos (8 tonos grises) // MONOCHROME: Fondo negro degradado, sprites blancos monocromáticos (8 tonos grises)
themes_[5] = { themes_[5] = {
"MONOCHROME", "MONOCROMO", // Nombres (inglés, español) "MONOCHROME",
200, 200, 200, // Color texto: gris claro "MONOCROMO", // Nombres (inglés, español)
20.0f / 255.0f, 20.0f / 255.0f, 20.0f / 255.0f, // Fondo superior (gris muy oscuro) 200,
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro) 200,
200, // Color texto: gris claro
20.0f / 255.0f,
20.0f / 255.0f,
20.0f / 255.0f, // Fondo superior (gris muy oscuro)
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior (negro)
{ {
{255, 255, 255}, // Blanco puro - todas las pelotas del mismo color {255, 255, 255}, // Blanco puro - todas las pelotas del mismo color
{255, 255, 255}, {255, 255, 255},
@@ -1238,9 +1323,7 @@ void Engine::initializeThemes() {
{255, 255, 255}, {255, 255, 255},
{255, 255, 255}, {255, 255, 255},
{255, 255, 255}, {255, 255, 255},
{255, 255, 255} {255, 255, 255}}};
}
};
} }
void Engine::startThemeTransition(ColorTheme new_theme) { void Engine::startThemeTransition(ColorTheme new_theme) {
@@ -1278,8 +1361,7 @@ Color Engine::getInterpolatedColor(size_t ball_index) const {
return { return {
static_cast<Uint8>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)), static_cast<Uint8>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)), static_cast<Uint8>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_)) static_cast<Uint8>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))};
};
} }
// Sistema de Modo DEMO (auto-play) // Sistema de Modo DEMO (auto-play)
@@ -1315,9 +1397,7 @@ void Engine::performDemoAction(bool is_lite) {
if (is_lite) { if (is_lite) {
// DEMO LITE: Solo física/figuras // DEMO LITE: Solo física/figuras
TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
+ DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS
+ DEMO_LITE_WEIGHT_IMPULSE;
random_value = rand() % TOTAL_WEIGHT; random_value = rand() % TOTAL_WEIGHT;
// Cambiar dirección gravedad (25%) // Cambiar dirección gravedad (25%)
@@ -1338,9 +1418,7 @@ void Engine::performDemoAction(bool is_lite) {
// Activar figura 3D (25%) // Activar figura 3D (25%)
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE; accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
if (random_value < accumulated_weight) { if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE};
int shape_index = rand() % 9; int shape_index = rand() % 9;
activateShape(shapes[shape_index]); activateShape(shapes[shape_index]);
return; return;
@@ -1362,10 +1440,7 @@ void Engine::performDemoAction(bool is_lite) {
} else { } else {
// DEMO COMPLETO: Todas las acciones // DEMO COMPLETO: Todas las acciones
TOTAL_WEIGHT = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE TOTAL_WEIGHT = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
+ DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME
+ DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM
+ DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
random_value = rand() % TOTAL_WEIGHT; random_value = rand() % TOTAL_WEIGHT;
// Cambiar dirección gravedad (10%) // Cambiar dirección gravedad (10%)
@@ -1386,9 +1461,7 @@ void Engine::performDemoAction(bool is_lite) {
// Activar figura 3D (20%) // Activar figura 3D (20%)
accumulated_weight += DEMO_WEIGHT_SHAPE; accumulated_weight += DEMO_WEIGHT_SHAPE;
if (random_value < accumulated_weight) { if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE};
int shape_index = rand() % 9; int shape_index = rand() % 9;
activateShape(shapes[shape_index]); activateShape(shapes[shape_index]);
return; return;
@@ -1483,9 +1556,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
} }
} else { } else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial) // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]); activateShape(shapes[rand() % 8]);
} }
@@ -1521,9 +1592,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
} }
} else { } else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial) // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]); activateShape(shapes[rand() % 8]);
// 5. Profundidad (solo si estamos en figura) // 5. Profundidad (solo si estamos en figura)
@@ -1774,9 +1843,7 @@ void Engine::updateShape() {
// Aplicar fuerza de atracción física hacia el punto rotado // Aplicar fuerza de atracción física hacia el punto rotado
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL) // Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
float shape_size = scale_factor * 80.0f; // 80px = radio base float shape_size = scale_factor * 80.0f; // 80px = radio base
balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_, balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_, SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR,
SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
// Calcular brillo según profundidad Z para renderizado // Calcular brillo según profundidad Z para renderizado
// Normalizar Z al rango de la figura (asumiendo simetría ±shape_size) // Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)

View File

@@ -10,19 +10,19 @@
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "defines.h" // for GravityDirection, ColorTheme, ShapeType
#include "ball.h" // for Ball #include "ball.h" // for Ball
#include "defines.h" // for GravityDirection, ColorTheme, ShapeType
#include "external/texture.h" // for Texture #include "external/texture.h" // for Texture
#include "shapes/shape.h" // for Shape (interfaz polimórfica) #include "shapes/shape.h" // for Shape (interfaz polimórfica)
class Engine { class Engine {
public: public:
// Interfaz pública // Interfaz pública
bool initialize(int width = 0, int height = 0, bool fullscreen = false); bool initialize(int width = 0, int height = 0, int zoom = 0, bool fullscreen = false);
void run(); void run();
void shutdown(); void shutdown();
private: private:
// Recursos SDL // Recursos SDL
SDL_Window* window_ = nullptr; SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr; SDL_Renderer* renderer_ = nullptr;
@@ -34,7 +34,6 @@ private:
// Estado del simulador // Estado del simulador
std::vector<std::unique_ptr<Ball>> balls_; std::vector<std::unique_ptr<Ball>> balls_;
std::array<int, 8> test_ = {1, 10, 100, 500, 1000, 10000, 50000, 100000};
GravityDirection current_gravity_ = GravityDirection::DOWN; GravityDirection current_gravity_ = GravityDirection::DOWN;
int scenario_ = 0; int scenario_ = 0;
bool should_exit_ = false; bool should_exit_ = false;
@@ -48,7 +47,7 @@ private:
bool show_text_ = true; bool show_text_ = true;
// Sistema de zoom dinámico // Sistema de zoom dinámico
int current_window_zoom_ = WINDOW_ZOOM; int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
std::string text_; std::string text_;
int text_pos_ = 0; int text_pos_ = 0;
Uint64 text_init_time_ = 0; Uint64 text_init_time_ = 0;
@@ -64,9 +63,13 @@ private:
bool real_fullscreen_enabled_ = false; bool real_fullscreen_enabled_ = false;
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER; // Modo de escalado actual (F5) ScalingMode current_scaling_mode_ = ScalingMode::INTEGER; // Modo de escalado actual (F5)
// Resolución dinámica para modo real fullscreen // Resolución base (configurada por CLI o default)
int current_screen_width_ = SCREEN_WIDTH; int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
int current_screen_height_ = SCREEN_HEIGHT; int base_screen_height_ = DEFAULT_SCREEN_HEIGHT;
// Resolución dinámica actual (cambia en fullscreen real)
int current_screen_width_ = DEFAULT_SCREEN_WIDTH;
int current_screen_height_ = DEFAULT_SCREEN_HEIGHT;
// Sistema de temas // Sistema de temas
ColorTheme current_theme_ = ColorTheme::SUNSET; ColorTheme current_theme_ = ColorTheme::SUNSET;
@@ -99,9 +102,15 @@ private:
// Sistema de Modo DEMO (auto-play) // Sistema de Modo DEMO (auto-play)
bool demo_mode_enabled_ = false; // ¿Está activo el modo demo completo? bool demo_mode_enabled_ = false; // ¿Está activo el modo demo completo?
bool demo_lite_enabled_ = false; // ¿Está activo el modo demo lite? bool demo_lite_enabled_ = false; // ¿Está activo el modo demo lite?
bool logo_mode_enabled_ = false; // ¿Está activo el modo logo (easter egg)?
float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción
float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos) float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos)
// Estado previo antes de entrar a Logo Mode (para restaurar al salir)
ColorTheme logo_previous_theme_ = ColorTheme::SUNSET;
size_t logo_previous_texture_index_ = 0;
float logo_previous_shape_scale_ = 1.0f;
// Batch rendering // Batch rendering
std::vector<SDL_Vertex> batch_vertices_; std::vector<SDL_Vertex> batch_vertices_;
std::vector<int> batch_indices_; std::vector<int> batch_indices_;
@@ -134,6 +143,11 @@ private:
void randomizeOnDemoStart(bool is_lite); void randomizeOnDemoStart(bool is_lite);
void toggleGravityOnOff(); void toggleGravityOnOff();
// Sistema de Modo Logo (easter egg)
void toggleLogoMode(); // Activar/desactivar modo logo manual (tecla K)
void enterLogoMode(bool from_demo = false); // Entrar al modo logo (manual o automático)
void exitLogoMode(bool return_to_demo = false); // Salir del modo logo
// Sistema de transiciones LERP // Sistema de transiciones LERP
float lerp(float a, float b, float t) const { return a + (b - a) * t; } float lerp(float a, float b, float t) const { return a + (b - a) * t; }
Color getInterpolatedColor(size_t ball_index) const; // Obtener color interpolado durante transición Color getInterpolatedColor(size_t ball_index) const; // Obtener color interpolado durante transición

View File

@@ -6,19 +6,23 @@ void printHelp() {
std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n"; std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n";
std::cout << "\nUso: vibe3_physics [opciones]\n\n"; std::cout << "\nUso: vibe3_physics [opciones]\n\n";
std::cout << "Opciones:\n"; std::cout << "Opciones:\n";
std::cout << " -w, --width <px> Ancho de resolución (default: 1280)\n"; std::cout << " -w, --width <px> Ancho de resolución (default: 320)\n";
std::cout << " -h, --height <px> Alto de resolución (default: 720)\n"; std::cout << " -h, --height <px> Alto de resolución (default: 240)\n";
std::cout << " -z, --zoom <n> Zoom de ventana (default: 3)\n";
std::cout << " -f, --fullscreen Modo pantalla completa\n"; std::cout << " -f, --fullscreen Modo pantalla completa\n";
std::cout << " --help Mostrar esta ayuda\n\n"; std::cout << " --help Mostrar esta ayuda\n\n";
std::cout << "Ejemplos:\n"; std::cout << "Ejemplos:\n";
std::cout << " vibe3_physics # 1280x720 ventana\n"; std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\n";
std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 ventana\n"; std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 zoom 1 (auto)\n";
std::cout << " vibe3_physics -w 1920 -h 1080 -f # 1920x1080 fullscreen\n"; std::cout << " vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (ventana 1280x960)\n";
std::cout << " vibe3_physics -w 1920 -h 1080 -f # 1920x1080 fullscreen\n\n";
std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n";
} }
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
int width = 0; int width = 0;
int height = 0; int height = 0;
int zoom = 0;
bool fullscreen = false; bool fullscreen = false;
// Parsear argumentos // Parsear argumentos
@@ -48,6 +52,17 @@ int main(int argc, char* argv[]) {
std::cerr << "Error: -h/--height requiere un valor\n"; std::cerr << "Error: -h/--height requiere un valor\n";
return -1; return -1;
} }
} else if (strcmp(argv[i], "-z") == 0 || strcmp(argv[i], "--zoom") == 0) {
if (i + 1 < argc) {
zoom = atoi(argv[++i]);
if (zoom < 1) {
std::cerr << "Error: Zoom mínimo es 1\n";
return -1;
}
} else {
std::cerr << "Error: -z/--zoom requiere un valor\n";
return -1;
}
} else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--fullscreen") == 0) { } else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--fullscreen") == 0) {
fullscreen = true; fullscreen = true;
} else { } else {
@@ -59,7 +74,7 @@ int main(int argc, char* argv[]) {
Engine engine; Engine engine;
if (!engine.initialize(width, height, fullscreen)) { if (!engine.initialize(width, height, zoom, fullscreen)) {
std::cout << "¡Error al inicializar el engine!" << std::endl; std::cout << "¡Error al inicializar el engine!" << std::endl;
return -1; return -1;
} }

View File

@@ -4,6 +4,7 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include <map>
PNGShape::PNGShape(const char* png_path) { PNGShape::PNGShape(const char* png_path) {
// Cargar PNG desde path // Cargar PNG desde path
@@ -98,14 +99,118 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR; extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR;
num_layers_ = PNG_NUM_EXTRUSION_LAYERS; num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
// Generar puntos según el enfoque // Generar AMBOS conjuntos de puntos (relleno Y bordes)
generateExtrudedPoints(); floodFill(); // Generar filled_points_
detectEdges(); // Generar edge_points_
// Debug: mostrar cantidad de puntos 2D detectados // Guardar copias originales (las funciones de filtrado modifican los vectores)
size_t num_2d_points = PNG_USE_EDGES_ONLY ? edge_points_.size() : filled_points_.size(); std::vector<Point2D> filled_points_original = filled_points_;
size_t total_3d_points = num_2d_points * num_layers_; std::vector<Point2D> edge_points_original = edge_points_;
std::cout << "[PNG_SHAPE] Detectados " << num_2d_points << " puntos 2D × "
<< num_layers_ << " capas = " << total_3d_points << " puntos 3D totales\n"; // Conjunto de puntos ACTIVO (será modificado por filtros)
std::vector<Point2D> active_points_data;
std::string mode_name = "";
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
// Estrategia: Optimizar según número de pelotas disponibles
// Objetivo: SIEMPRE intentar usar relleno primero, solo bordes si es necesario
size_t num_2d_points = 0;
size_t total_3d_points = 0;
// NIVEL 1: Decidir punto de partida (relleno o bordes por configuración)
if (PNG_USE_EDGES_ONLY) {
active_points_data = edge_points_original;
mode_name = "BORDES (config)";
} else {
active_points_data = filled_points_original;
mode_name = "RELLENO";
}
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 1: Modo inicial " << mode_name
<< " (puntos 2D: " << num_2d_points << ", capas: " << num_layers_
<< ", total 3D: " << total_3d_points << ")\n";
std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n";
// NIVEL 2: Reducir capas AGRESIVAMENTE hasta 1 (priorizar calidad 2D sobre profundidad 3D)
// Objetivo: Llenar bien el texto en 2D antes de reducir píxeles
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points)) {
num_layers_ = std::max(1, num_layers_ / 2);
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 2: Reduciendo capas a " << num_layers_
<< " (total 3D: " << total_3d_points << ")\n";
}
// NIVEL 3: Filas alternas en RELLENO (solo si 1 capa no alcanza)
// Esto permite usar relleno incluso con pocas pelotas
int row_skip = 1;
if (!PNG_USE_EDGES_ONLY) { // Solo si empezamos con relleno
while (row_skip < 5 && num_points < static_cast<int>(total_3d_points)) {
row_skip++;
// ✅ CLAVE: Recalcular desde el ORIGINAL cada vez (no desde el filtrado previo)
active_points_data = extractAlternateRows(filled_points_original, row_skip);
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 3: Filas alternas RELLENO (cada " << row_skip
<< " filas, puntos 2D: " << num_2d_points << ", total 3D: " << total_3d_points << ")\n";
mode_name = "RELLENO + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 4: Cambiar a BORDES (solo si relleno con filas alternas no alcanza)
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points)) {
active_points_data = edge_points_original;
mode_name = "BORDES (auto)";
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
row_skip = 1; // Reset row_skip para bordes
std::cout << "[PNG_SHAPE] Nivel 4: Cambiando a BORDES (pelotas: " << num_points
<< ", necesarias: " << total_3d_points << ")\n";
}
// NIVEL 5: Filas alternas en BORDES (si aún no alcanza)
while (row_skip < 8 && num_points < static_cast<int>(total_3d_points)) {
row_skip++;
// ✅ CLAVE: Recalcular desde edge_points_original cada vez
active_points_data = extractAlternateRows(edge_points_original, row_skip);
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
std::cout << "[PNG_SHAPE] Nivel 5: Filas alternas BORDES (cada " << row_skip
<< " filas, puntos 2D: " << num_2d_points << ", total 3D: " << total_3d_points << ")\n";
if (mode_name.find("FILAS") == std::string::npos) {
mode_name += " + FILAS/" + std::to_string(row_skip);
}
}
// NIVEL 6: Vértices/esquinas (último recurso, muy pocas pelotas)
if (num_points < static_cast<int>(total_3d_points) && num_points < 150) {
// Determinar desde qué conjunto extraer vértices (el que esté activo actualmente)
const std::vector<Point2D>& source_for_vertices = (mode_name.find("BORDES") != std::string::npos)
? edge_points_original
: filled_points_original;
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
if (!vertices.empty() && vertices.size() < active_points_data.size()) {
active_points_data = vertices;
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
mode_name = "VÉRTICES";
std::cout << "[PNG_SHAPE] Nivel 6: Solo vértices (puntos 2D: " << num_2d_points << ")\n";
}
}
// ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D)
optimized_points_ = active_points_data;
// Debug: mostrar configuración final
std::cout << "[PNG_SHAPE] === CONFIGURACIÓN FINAL ===\n";
std::cout << "[PNG_SHAPE] Modo: " << mode_name << "\n";
std::cout << "[PNG_SHAPE] Píxeles 2D: " << num_2d_points << "\n";
std::cout << "[PNG_SHAPE] Capas extrusión: " << num_layers_ << "\n";
std::cout << "[PNG_SHAPE] Total puntos 3D: " << total_3d_points << "\n";
std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n"; std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n";
std::cout << "[PNG_SHAPE] Ratio: " << (float)num_points / (float)total_3d_points << " pelotas/punto\n"; std::cout << "[PNG_SHAPE] Ratio: " << (float)num_points / (float)total_3d_points << " pelotas/punto\n";
@@ -118,6 +223,68 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
center_offset_y_ = image_height_ * 0.5f; center_offset_y_ = image_height_ * 0.5f;
} }
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
// Recibe vector original y devuelve nuevo vector filtrado
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) {
std::vector<Point2D> result;
if (row_skip <= 1 || source.empty()) {
return source; // Sin filtrado, devolver copia del original
}
// Organizar puntos por fila (Y)
std::map<int, std::vector<Point2D>> rows;
for (const auto& p : source) {
int row = static_cast<int>(p.y);
rows[row].push_back(p);
}
// Tomar solo cada N filas
int row_counter = 0;
for (const auto& [row_y, row_points] : rows) {
if (row_counter % row_skip == 0) {
result.insert(result.end(), row_points.begin(), row_points.end());
}
row_counter++;
}
return result;
}
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) {
std::vector<Point2D> result;
if (source.empty()) {
return result;
}
// Estrategia simple: tomar bordes extremos de cada fila
// Esto da el "esqueleto" mínimo de las letras
std::map<int, std::pair<float, float>> row_extremes; // Y -> (min_x, max_x)
for (const auto& p : source) {
int row = static_cast<int>(p.y);
if (row_extremes.find(row) == row_extremes.end()) {
row_extremes[row] = {p.x, p.x};
} else {
row_extremes[row].first = std::min(row_extremes[row].first, p.x);
row_extremes[row].second = std::max(row_extremes[row].second, p.x);
}
}
// Generar puntos en extremos de cada fila
for (const auto& [row_y, extremes] : row_extremes) {
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
}
}
return result;
}
void PNGShape::update(float delta_time, float screen_width, float screen_height) { void PNGShape::update(float delta_time, float screen_width, float screen_height) {
if (!is_flipping_) { if (!is_flipping_) {
// Estado IDLE: texto de frente // Estado IDLE: texto de frente
@@ -159,8 +326,8 @@ void PNGShape::update(float delta_time, float screen_width, float screen_height)
} }
void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const { void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Seleccionar puntos según configuración // Usar SIEMPRE el vector optimizado (resultado final de generatePoints)
const std::vector<Point2D>& points = PNG_USE_EDGES_ONLY ? edge_points_ : filled_points_; const std::vector<Point2D>& points = optimized_points_;
if (points.empty()) { if (points.empty()) {
x = y = z = 0.0f; x = y = z = 0.0f;

View File

@@ -17,8 +17,9 @@ private:
struct Point2D { struct Point2D {
float x, y; float x, y;
}; };
std::vector<Point2D> edge_points_; // Contorno (solo bordes) std::vector<Point2D> edge_points_; // Contorno (solo bordes) - ORIGINAL sin optimizar
std::vector<Point2D> filled_points_; // Relleno completo (para Enfoque B) std::vector<Point2D> filled_points_; // Relleno completo - ORIGINAL sin optimizar
std::vector<Point2D> optimized_points_; // Puntos finales optimizados (usado por getPoint3D)
// Parámetros de extrusión // Parámetros de extrusión
float extrusion_depth_ = 0.0f; // Profundidad de extrusión en Z float extrusion_depth_ = 0.0f; // Profundidad de extrusión en Z
@@ -46,6 +47,10 @@ private:
void floodFill(); // Rellenar interior (Enfoque B - futuro) void floodFill(); // Rellenar interior (Enfoque B - futuro)
void generateExtrudedPoints(); // Generar puntos con extrusión 2D void generateExtrudedPoints(); // Generar puntos con extrusión 2D
// Métodos de distribución adaptativa (funciones puras, no modifican parámetros)
std::vector<Point2D> extractAlternateRows(const std::vector<Point2D>& source, int row_skip); // Extraer filas alternas
std::vector<Point2D> extractCornerVertices(const std::vector<Point2D>& source); // Extraer vértices/esquinas
public: public:
// Constructor: recibe path relativo al PNG // Constructor: recibe path relativo al PNG
PNGShape(const char* png_path = "data/shapes/jailgames.png"); PNGShape(const char* png_path = "data/shapes/jailgames.png");