Implementar sistema polimórfico de figuras 3D (Sphere + Cube)

- Crear interfaz abstracta Shape con métodos virtuales
- Refactorizar RotoBall → SphereShape (clase polimórfica)
- Implementar CubeShape con triple rotación (X/Y/Z)
- Distribución inteligente en cubo: vértices/centros/grid 3D
- Cambiar controles: F=toggle, Q/W/E/R/T/Y/U/I=figuras, B=temas
- Actualizar SimulationMode: ROTOBALL → SHAPE
- Añadir enum ShapeType (8 figuras: Sphere/Cube/Helix/Torus/etc.)
- Incluir source/shapes/*.cpp en CMakeLists.txt
- Física compartida escalable entre todas las figuras
- Roadmap: 6 figuras pendientes (Helix/Torus/Wave/Cylinder/Icosahedron/Atom)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 20:20:10 +02:00
parent b196683e4a
commit a7ec764ebc
9 changed files with 484 additions and 137 deletions

View File

@@ -23,6 +23,8 @@
#include "ball.h" // for Ball
#include "external/dbgtxt.h" // for dbg_init, dbg_print
#include "external/texture.h" // for Texture
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/cube_shape.h" // for CubeShape
// Función auxiliar para obtener la ruta del directorio del ejecutable
std::string getExecutableDirectory() {
@@ -148,9 +150,9 @@ void Engine::update() {
// Verificar auto-reinicio cuando todas las pelotas están quietas (solo en modo física)
checkAutoRestart();
} else if (current_mode_ == SimulationMode::ROTOBALL) {
// Modo RotoBall: actualizar esfera 3D rotante
updateRotoBall();
} else if (current_mode_ == SimulationMode::SHAPE) {
// Modo Figura 3D: actualizar figura polimórfica
updateShape();
}
// Actualizar texto (sin cambios en la lógica)
@@ -180,9 +182,9 @@ void Engine::handleEvents() {
break;
case SDLK_G:
// Si estamos en RotoBall, salir a modo física SIN GRAVEDAD
if (current_mode_ == SimulationMode::ROTOBALL) {
toggleRotoBallMode(false); // Desactivar RotoBall sin forzar gravedad ON
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(false); // Desactivar figura sin forzar gravedad ON
} else {
switchBallsGravity(); // Toggle normal en modo física
}
@@ -190,9 +192,9 @@ void Engine::handleEvents() {
// Controles de dirección de gravedad con teclas de cursor
case SDLK_UP:
// Si estamos en RotoBall, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::ROTOBALL) {
toggleRotoBallMode(); // Desactivar RotoBall (activa gravedad automáticamente)
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
@@ -200,9 +202,9 @@ void Engine::handleEvents() {
break;
case SDLK_DOWN:
// Si estamos en RotoBall, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::ROTOBALL) {
toggleRotoBallMode(); // Desactivar RotoBall (activa gravedad automáticamente)
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
@@ -210,9 +212,9 @@ void Engine::handleEvents() {
break;
case SDLK_LEFT:
// Si estamos en RotoBall, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::ROTOBALL) {
toggleRotoBallMode(); // Desactivar RotoBall (activa gravedad automáticamente)
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
@@ -220,9 +222,9 @@ void Engine::handleEvents() {
break;
case SDLK_RIGHT:
// Si estamos en RotoBall, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::ROTOBALL) {
toggleRotoBallMode(); // Desactivar RotoBall (activa gravedad automáticamente)
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(); // Desactivar figura (activa gravedad automáticamente)
} else {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
@@ -237,11 +239,46 @@ void Engine::handleEvents() {
show_debug_ = !show_debug_;
break;
case SDLK_C:
toggleRotoBallMode();
// Toggle Física ↔ Última Figura (antes era C)
case SDLK_F:
toggleShapeMode();
break;
// Selección directa de figuras 3D
case SDLK_Q:
activateShape(ShapeType::SPHERE);
break;
case SDLK_W:
activateShape(ShapeType::WAVE_GRID);
break;
case SDLK_E:
activateShape(ShapeType::HELIX);
break;
case SDLK_R:
activateShape(ShapeType::TORUS);
break;
case SDLK_T:
activateShape(ShapeType::CUBE);
break;
case SDLK_Y:
activateShape(ShapeType::CYLINDER);
break;
case SDLK_U:
activateShape(ShapeType::ICOSAHEDRON);
break;
case SDLK_I:
activateShape(ShapeType::ATOM);
break;
// Ciclar temas de color (movido de T a B)
case SDLK_B:
// Ciclar al siguiente tema
current_theme_ = static_cast<ColorTheme>((static_cast<int>(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0])));
initBalls(scenario_); // Regenerar bolas con nueva paleta
@@ -348,8 +385,8 @@ void Engine::render() {
batch_vertices_.clear();
batch_indices_.clear();
if (current_mode_ == SimulationMode::ROTOBALL) {
// MODO ROTOBALL: Ordenar por profundidad Z (Painter's Algorithm)
if (current_mode_ == SimulationMode::SHAPE) {
// MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
// Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero
// Crear vector de índices para ordenamiento
@@ -462,7 +499,14 @@ void Engine::render() {
dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema
// Debug: Mostrar modo de simulación actual
std::string mode_text = current_mode_ == SimulationMode::PHYSICS ? "MODE PHYSICS" : "MODE ROTOBALL";
std::string mode_text;
if (current_mode_ == SimulationMode::PHYSICS) {
mode_text = "MODE PHYSICS";
} else if (active_shape_) {
mode_text = std::string("MODE ") + active_shape_->getName();
} else {
mode_text = "MODE SHAPE";
}
dbg_print(8, 72, mode_text.c_str(), 0, 255, 128); // Verde claro para modo
}
@@ -470,11 +514,10 @@ void Engine::render() {
}
void Engine::initBalls(int value) {
// Si estamos en modo RotoBall, desactivarlo antes de regenerar pelotas
if (current_mode_ == SimulationMode::ROTOBALL) {
// Si estamos en modo figura 3D, desactivarlo antes de regenerar pelotas
if (current_mode_ == SimulationMode::SHAPE) {
current_mode_ = SimulationMode::PHYSICS;
rotoball_.transitioning = false;
rotoball_.transition_progress = 0.0f;
active_shape_.reset(); // Liberar figura actual
}
// Limpiar las bolas actuales
@@ -922,39 +965,14 @@ void Engine::performRandomRestart() {
all_balls_stopped_start_time_ = 0;
}
// Sistema RotoBall - Alternar entre modo física y esfera 3D
void Engine::toggleRotoBallMode(bool force_gravity_on_exit) {
// Sistema de Figuras 3D - Alternar entre modo física y última figura (Toggle con tecla F)
void Engine::toggleShapeMode(bool force_gravity_on_exit) {
if (current_mode_ == SimulationMode::PHYSICS) {
// Cambiar a modo RotoBall
current_mode_ = SimulationMode::ROTOBALL;
rotoball_.transitioning = true;
rotoball_.transition_progress = 0.0f;
rotoball_.angle_y = 0.0f;
rotoball_.angle_x = 0.0f;
// Desactivar gravedad al entrar en modo figura
forceBallsGravityOff();
// Generar esfera 3D
generateRotoBallSphere();
// Activar atracción física en todas las pelotas
// Las pelotas mantienen su velocidad actual y son atraídas hacia la esfera
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(true);
}
// Mostrar texto informativo
text_ = "MODO ROTOBALL";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
// Cambiar a modo figura (usar última figura seleccionada)
activateShape(last_shape_type_);
} else {
// Volver a modo física normal
current_mode_ = SimulationMode::PHYSICS;
rotoball_.transitioning = false;
rotoball_.transition_progress = 0.0f;
// Desactivar atracción - las pelotas conservan su velocidad tangencial actual
for (auto& ball : balls_) {
@@ -975,53 +993,67 @@ void Engine::toggleRotoBallMode(bool force_gravity_on_exit) {
}
}
// Generar esfera 3D usando algoritmo Fibonacci Sphere
void Engine::generateRotoBallSphere() {
int num_points = static_cast<int>(balls_.size());
if (num_points == 0) return;
// Activar figura específica (llamado por teclas Q/W/E/R/Y/U/I o por toggleShapeMode)
void Engine::activateShape(ShapeType type) {
// Guardar como última figura seleccionada
last_shape_type_ = type;
current_shape_type_ = type;
// Calcular radio dinámico proporcional a la altura de pantalla
float radius = current_screen_height_ * ROTOBALL_RADIUS_FACTOR;
// Cambiar a modo figura
current_mode_ = SimulationMode::SHAPE;
// Constante Golden Ratio para Fibonacci sphere
const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f;
const float angle_increment = PI * 2.0f * golden_ratio;
// Desactivar gravedad al entrar en modo figura
forceBallsGravityOff();
for (int i = 0; i < num_points; i++) {
// Distribución uniforme usando Fibonacci sphere
float t = static_cast<float>(i) / static_cast<float>(num_points);
float phi = acosf(1.0f - 2.0f * t); // Latitud
float theta = angle_increment * static_cast<float>(i); // Longitud
// Crear instancia polimórfica de la figura correspondiente
switch (type) {
case ShapeType::SPHERE:
active_shape_ = std::make_unique<SphereShape>();
break;
case ShapeType::CUBE:
active_shape_ = std::make_unique<CubeShape>();
break;
// Futuras figuras se añadirán aquí
default:
active_shape_ = std::make_unique<SphereShape>(); // Fallback
break;
}
// Convertir coordenadas esféricas a cartesianas
float x = cosf(theta) * sinf(phi) * radius;
float y = sinf(theta) * sinf(phi) * radius;
float z = cosf(phi) * radius;
// Generar puntos de la figura
generateShape();
// Guardar posición 3D en la pelota
balls_[i]->setRotoBallPosition3D(x, y, z);
// Activar atracción física en todas las pelotas
for (auto& ball : balls_) {
ball->enableRotoBallAttraction(true);
}
// Calcular posición 2D inicial (centro de pantalla)
float center_x = current_screen_width_ / 2.0f;
float center_y = current_screen_height_ / 2.0f;
balls_[i]->setRotoBallTarget2D(center_x + x, center_y + y);
// Calcular brillo inicial según profundidad Z
float z_normalized = (z + radius) / (2.0f * radius);
balls_[i]->setDepthBrightness(z_normalized);
// Mostrar texto informativo con nombre de figura
if (active_shape_) {
text_ = std::string("MODO ") + active_shape_->getName();
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
}
// Actualizar esfera RotoBall con física de atracción
void Engine::updateRotoBall() {
if (current_mode_ != SimulationMode::ROTOBALL) return;
// Generar puntos de la figura activa
void Engine::generateShape() {
if (!active_shape_) return;
// Calcular radio dinámico proporcional a la altura de pantalla
float radius = current_screen_height_ * ROTOBALL_RADIUS_FACTOR;
int num_points = static_cast<int>(balls_.size());
active_shape_->generatePoints(num_points, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
}
// Actualizar ángulos de rotación de la esfera
rotoball_.angle_y += ROTOBALL_ROTATION_SPEED_Y * delta_time_;
rotoball_.angle_x += ROTOBALL_ROTATION_SPEED_X * delta_time_;
// Actualizar figura activa (rotación, animación, etc.)
void Engine::updateShape() {
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return;
// Actualizar animación de la figura
active_shape_->update(delta_time_, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
// Obtener factor de escala para física
float scale_factor = active_shape_->getScaleFactor(static_cast<float>(current_screen_height_));
// Centro de la pantalla
float center_x = current_screen_width_ / 2.0f;
@@ -1029,40 +1061,22 @@ void Engine::updateRotoBall() {
// Actualizar cada pelota con física de atracción
for (size_t i = 0; i < balls_.size(); i++) {
// Recalcular posición 3D original usando Fibonacci sphere
int num_points = static_cast<int>(balls_.size());
const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f;
const float angle_increment = PI * 2.0f * golden_ratio;
float t = static_cast<float>(i) / static_cast<float>(num_points);
float phi = acosf(1.0f - 2.0f * t);
float theta = angle_increment * static_cast<float>(i);
float x = cosf(theta) * sinf(phi) * radius;
float y = sinf(theta) * sinf(phi) * radius;
float z = cosf(phi) * radius;
// Aplicar rotación en eje Y
float cos_y = cosf(rotoball_.angle_y);
float sin_y = sinf(rotoball_.angle_y);
float x_rot = x * cos_y - z * sin_y;
float z_rot = x * sin_y + z * cos_y;
// Aplicar rotación en eje X
float cos_x = cosf(rotoball_.angle_x);
float sin_x = sinf(rotoball_.angle_x);
float y_rot = y * cos_x - z_rot * sin_x;
float z_final = y * sin_x + z_rot * cos_x;
// Obtener posición 3D rotada del punto i
float x_3d, y_3d, z_3d;
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
// Proyección 2D ortográfica (punto objetivo móvil)
float target_x = center_x + x_rot;
float target_y = center_y + y_rot;
float target_x = center_x + x_3d;
float target_y = center_y + y_3d;
// Aplicar fuerza de atracción física hacia el punto rotado
balls_[i]->applyRotoBallForce(target_x, target_y, radius, delta_time_);
// Pasar el tamaño de la figura para escalar fuerzas
float shape_size = scale_factor * 80.0f; // 80px = radio base
balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_);
// Calcular brillo según profundidad Z para renderizado
float z_normalized = (z_final + radius) / (2.0f * radius);
// Normalizar Z al rango de la figura (asumiendo simetría ±shape_size)
float z_normalized = (z_3d + shape_size) / (2.0f * shape_size);
z_normalized = std::max(0.0f, std::min(1.0f, z_normalized));
balls_[i]->setDepthBrightness(z_normalized);
}