style: aplicar fixes de clang-tidy (todo excepto uppercase-literal-suffix)

Corregidos ~2570 issues automáticamente con clang-tidy --fix-errors
más ajustes manuales posteriores:

- modernize: designated-initializers, trailing-return-type, use-auto,
  avoid-c-arrays (→ std::array<>), use-ranges, use-emplace,
  deprecated-headers, use-equals-default, pass-by-value,
  return-braced-init-list, use-default-member-init
- readability: math-missing-parentheses, implicit-bool-conversion,
  braces-around-statements, isolate-declaration, use-std-min-max,
  identifier-naming, else-after-return, redundant-casting,
  convert-member-functions-to-static, make-member-function-const,
  static-accessed-through-instance
- performance: avoid-endl, unnecessary-value-param, type-promotion,
  inefficient-vector-operation
- dead code: XOR_KEY (orphan tras eliminar encryptData/decryptData),
  dead stores en engine.cpp y png_shape.cpp
- NOLINT justificado en 10 funciones con alta complejidad cognitiva
  (initialize, render, main, processEvents, update×3, performDemoAction,
  randomizeOnDemoStart, renderDebugHUD, AppLogo::update)

Compilación: gcc -Wall sin warnings. clang-tidy: 0 issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 10:52:07 +01:00
parent 4801f287df
commit c9bcce6f9b
71 changed files with 3469 additions and 2838 deletions

View File

@@ -16,6 +16,7 @@ Checks: >
-performance-inefficient-string-concatenation, -performance-inefficient-string-concatenation,
-bugprone-integer-division, -bugprone-integer-division,
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-readability-uppercase-literal-suffix,
WarningsAsErrors: '*' WarningsAsErrors: '*'
# Solo incluir archivos de tu código fuente # Solo incluir archivos de tu código fuente

View File

@@ -1,30 +1,31 @@
#include "ball.hpp" #include "ball.hpp"
#include <stdlib.h> // for rand #include <algorithm>
#include <cmath> // for fabs #include <cmath> // for fabs
#include <cstdlib> // for rand
#include <utility>
#include "defines.hpp" // for Color, SCREEN_HEIGHT, GRAVITY_FORCE #include "defines.hpp" // for Color, SCREEN_HEIGHT, GRAVITY_FORCE
class Texture; class Texture;
// Función auxiliar para generar pérdida aleatoria en rebotes // Función auxiliar para generar pérdida aleatoria en rebotes
float generateBounceVariation() { auto generateBounceVariation() -> float {
// Genera un valor entre 0 y BOUNCE_RANDOM_LOSS_PERCENT (solo pérdida adicional) // Genera un valor entre 0 y BOUNCE_RANDOM_LOSS_PERCENT (solo pérdida adicional)
float loss = (rand() % 1000) / 1000.0f * BOUNCE_RANDOM_LOSS_PERCENT; float loss = (rand() % 1000) / 1000.0f * BOUNCE_RANDOM_LOSS_PERCENT;
return 1.0f - loss; // Retorna multiplicador (ej: 0.90 - 1.00 para 10% max pérdida) return 1.0f - loss; // Retorna multiplicador (ej: 0.90 - 1.00 para 10% max pérdida)
} }
// Función auxiliar para generar pérdida lateral aleatoria // Función auxiliar para generar pérdida lateral aleatoria
float generateLateralLoss() { auto generateLateralLoss() -> float {
// Genera un valor entre 0 y LATERAL_LOSS_PERCENT // Genera un valor entre 0 y LATERAL_LOSS_PERCENT
float loss = (rand() % 1000) / 1000.0f * LATERAL_LOSS_PERCENT; float loss = (rand() % 1000) / 1000.0f * LATERAL_LOSS_PERCENT;
return 1.0f - loss; // Retorna multiplicador (ej: 0.98 - 1.0 para 0-2% pérdida) return 1.0f - loss; // Retorna multiplicador (ej: 0.98 - 1.0 para 0-2% pérdida)
} }
// Constructor // Constructor
Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor) Ball::Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
: sprite_(std::make_unique<Sprite>(texture)), : sprite_(std::make_unique<Sprite>(texture)),
pos_({x, y, static_cast<float>(ball_size), static_cast<float>(ball_size)}) { pos_({.x = x, .y = y, .w = static_cast<float>(ball_size), .h = static_cast<float>(ball_size)}) {
// Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60) // Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60)
vx_ = vx * 60.0f; vx_ = vx * 60.0f;
vy_ = vy * 60.0f; vy_ = vy * 60.0f;
@@ -54,11 +55,11 @@ Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Te
} }
// Actualiza la lógica de la clase // Actualiza la lógica de la clase
void Ball::update(float deltaTime) { void Ball::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
// Aplica la gravedad según la dirección (píxeles/segundo²) // Aplica la gravedad según la dirección (píxeles/segundo²)
if (!on_surface_) { if (!on_surface_) {
// Aplicar gravedad multiplicada por factor de masa individual // Aplicar gravedad multiplicada por factor de masa individual
float effective_gravity = gravity_force_ * gravity_mass_factor_ * deltaTime; float effective_gravity = gravity_force_ * gravity_mass_factor_ * delta_time;
switch (gravity_direction_) { switch (gravity_direction_) {
case GravityDirection::DOWN: case GravityDirection::DOWN:
vy_ += effective_gravity; vy_ += effective_gravity;
@@ -77,26 +78,26 @@ void Ball::update(float deltaTime) {
// Actualiza la posición en función de la velocidad (píxeles/segundo) // Actualiza la posición en función de la velocidad (píxeles/segundo)
if (!on_surface_) { if (!on_surface_) {
pos_.x += vx_ * deltaTime; pos_.x += vx_ * delta_time;
pos_.y += vy_ * deltaTime; pos_.y += vy_ * delta_time;
} else { } else {
// Si está en superficie, mantener posición según dirección de gravedad // Si está en superficie, mantener posición según dirección de gravedad
switch (gravity_direction_) { switch (gravity_direction_) {
case GravityDirection::DOWN: case GravityDirection::DOWN:
pos_.y = screen_height_ - pos_.h; pos_.y = screen_height_ - pos_.h;
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X pos_.x += vx_ * delta_time; // Seguir moviéndose en X
break; break;
case GravityDirection::UP: case GravityDirection::UP:
pos_.y = 0; pos_.y = 0;
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X pos_.x += vx_ * delta_time; // Seguir moviéndose en X
break; break;
case GravityDirection::LEFT: case GravityDirection::LEFT:
pos_.x = 0; pos_.x = 0;
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
break; break;
case GravityDirection::RIGHT: case GravityDirection::RIGHT:
pos_.x = screen_width_ - pos_.w; pos_.x = screen_width_ - pos_.w;
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
break; break;
} }
} }
@@ -176,7 +177,7 @@ void Ball::update(float deltaTime) {
// Aplica rozamiento al estar en superficie // Aplica rozamiento al estar en superficie
if (on_surface_) { if (on_surface_) {
// Convertir rozamiento de frame-based a time-based // Convertir rozamiento de frame-based a time-based
float friction_factor = pow(0.97f, 60.0f * deltaTime); float friction_factor = std::pow(0.97f, 60.0f * delta_time);
switch (gravity_direction_) { switch (gravity_direction_) {
case GravityDirection::DOWN: case GravityDirection::DOWN:
@@ -246,7 +247,7 @@ void Ball::setGravityDirection(GravityDirection direction) {
// Aplica un pequeño empuje lateral aleatorio // Aplica un pequeño empuje lateral aleatorio
void Ball::applyRandomLateralPush() { void Ball::applyRandomLateralPush() {
// Generar velocidad lateral aleatoria (nunca 0) // Generar velocidad lateral aleatoria (nunca 0)
float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN); float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN));
// Signo aleatorio (+ o -) // Signo aleatorio (+ o -)
int sign = ((rand() % 2) * 2) - 1; int sign = ((rand() % 2) * 2) - 1;
@@ -304,18 +305,18 @@ void Ball::enableShapeAttraction(bool enable) {
} }
// Obtener distancia actual al punto objetivo (para calcular convergencia) // Obtener distancia actual al punto objetivo (para calcular convergencia)
float Ball::getDistanceToTarget() const { auto Ball::getDistanceToTarget() const -> float {
// Siempre calcular distancia (útil para convergencia en LOGO mode) // Siempre calcular distancia (útil para convergencia en LOGO mode)
float dx = target_x_ - pos_.x; float dx = target_x_ - pos_.x;
float dy = target_y_ - pos_.y; float dy = target_y_ - pos_.y;
return sqrtf(dx * dx + dy * dy); return sqrtf((dx * dx) + (dy * dy));
} }
// Aplicar fuerza de resorte hacia punto objetivo en figuras 3D // Aplicar fuerza de resorte hacia punto objetivo en figuras 3D
void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius, float deltaTime, void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base, float damping_base_base, float damping_near_base, float near_threshold_base, float max_force_base) {
float spring_k_base, float damping_base_base, float damping_near_base, if (!shape_attraction_active_) {
float near_threshold_base, float max_force_base) { return;
if (!shape_attraction_active_) return; }
// Calcular factor de escala basado en el radio (radio base = 80px) // Calcular factor de escala basado en el radio (radio base = 80px)
// Si radius=80 → scale=1.0, si radius=160 → scale=2.0, si radius=360 → scale=4.5 // Si radius=80 → scale=1.0, si radius=160 → scale=2.0, si radius=360 → scale=4.5
@@ -334,7 +335,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
float diff_y = target_y - pos_.y; float diff_y = target_y - pos_.y;
// Calcular distancia al punto objetivo // Calcular distancia al punto objetivo
float distance = sqrtf(diff_x * diff_x + diff_y * diff_y); float distance = sqrtf((diff_x * diff_x) + (diff_y * diff_y));
// Fuerza de resorte (Ley de Hooke: F = -k * x) // Fuerza de resorte (Ley de Hooke: F = -k * x)
float spring_force_x = spring_k * diff_x; float spring_force_x = spring_k * diff_x;
@@ -354,7 +355,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
float total_force_y = spring_force_y - damping_force_y; float total_force_y = spring_force_y - damping_force_y;
// Limitar magnitud de fuerza (evitar explosiones numéricas) // Limitar magnitud de fuerza (evitar explosiones numéricas)
float force_magnitude = sqrtf(total_force_x * total_force_x + total_force_y * total_force_y); float force_magnitude = sqrtf((total_force_x * total_force_x) + (total_force_y * total_force_y));
if (force_magnitude > max_force) { if (force_magnitude > max_force) {
float scale_limit = max_force / force_magnitude; float scale_limit = max_force / force_magnitude;
total_force_x *= scale_limit; total_force_x *= scale_limit;
@@ -363,18 +364,22 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
// Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar) // Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar)
// a = F/m, pero m=1, así que a = F // a = F/m, pero m=1, así que a = F
vx_ += total_force_x * deltaTime; vx_ += total_force_x * delta_time;
vy_ += total_force_y * deltaTime; vy_ += total_force_y * delta_time;
// Actualizar posición con física normal (velocidad integrada) // Actualizar posición con física normal (velocidad integrada)
pos_.x += vx_ * deltaTime; pos_.x += vx_ * delta_time;
pos_.y += vy_ * deltaTime; pos_.y += vy_ * delta_time;
// Mantener pelotas dentro de los límites de pantalla // Mantener pelotas dentro de los límites de pantalla
if (pos_.x < 0) pos_.x = 0; pos_.x = std::max<float>(pos_.x, 0);
if (pos_.x + pos_.w > screen_width_) pos_.x = screen_width_ - pos_.w; if (pos_.x + pos_.w > screen_width_) {
if (pos_.y < 0) pos_.y = 0; pos_.x = screen_width_ - pos_.w;
if (pos_.y + pos_.h > screen_height_) pos_.y = screen_height_ - pos_.h; }
pos_.y = std::max<float>(pos_.y, 0);
if (pos_.y + pos_.h > screen_height_) {
pos_.y = screen_height_ - pos_.h;
}
// Actualizar sprite para renderizado // Actualizar sprite para renderizado
sprite_->setPos({pos_.x, pos_.y}); sprite_->setPos({pos_.x, pos_.y});
@@ -393,5 +398,5 @@ void Ball::updateSize(int new_size) {
void Ball::setTexture(std::shared_ptr<Texture> texture) { void Ball::setTexture(std::shared_ptr<Texture> texture) {
// Actualizar textura del sprite // Actualizar textura del sprite
sprite_->setTexture(texture); sprite_->setTexture(std::move(texture));
} }

View File

@@ -31,13 +31,13 @@ class Ball {
public: public:
// Constructor // Constructor
Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f); Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
// Destructor // Destructor
~Ball() = default; ~Ball() = default;
// Actualiza la lógica de la clase // Actualiza la lógica de la clase
void update(float deltaTime); void update(float delta_time);
// Pinta la clase // Pinta la clase
void render(); void render();
@@ -72,11 +72,20 @@ class Ball {
bool isOnSurface() const { return on_surface_; } bool isOnSurface() const { return on_surface_; }
// Getters/Setters para velocidad (usado por BoidManager) // Getters/Setters para velocidad (usado por BoidManager)
void getVelocity(float& vx, float& vy) const { vx = vx_; vy = vy_; } void getVelocity(float& vx, float& vy) const {
void setVelocity(float vx, float vy) { vx_ = vx; vy_ = vy; } vx = vx_;
vy = vy_;
}
void setVelocity(float vx, float vy) {
vx_ = vx;
vy_ = vy;
}
// Setter para posición simple (usado por BoidManager) // Setter para posición simple (usado por BoidManager)
void setPosition(float x, float y) { pos_.x = x; pos_.y = y; } void setPosition(float x, float y) {
pos_.x = x;
pos_.y = y;
}
// Getters/Setters para batch rendering // Getters/Setters para batch rendering
SDL_FRect getPosition() const { return pos_; } SDL_FRect getPosition() const { return pos_; }
@@ -99,10 +108,5 @@ class Ball {
// Sistema de atracción física hacia figuras 3D // Sistema de atracción física hacia figuras 3D
void enableShapeAttraction(bool enable); void enableShapeAttraction(bool enable);
float getDistanceToTarget() const; // Distancia actual al punto objetivo float getDistanceToTarget() const; // Distancia actual al punto objetivo
void applyShapeForce(float target_x, float target_y, float sphere_radius, float deltaTime, void applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base = SHAPE_SPRING_K, float damping_base_base = SHAPE_DAMPING_BASE, float damping_near_base = SHAPE_DAMPING_NEAR, float near_threshold_base = SHAPE_NEAR_THRESHOLD, float max_force_base = SHAPE_MAX_FORCE);
float spring_k = SHAPE_SPRING_K,
float damping_base = SHAPE_DAMPING_BASE,
float damping_near = SHAPE_DAMPING_NEAR,
float near_threshold = SHAPE_NEAR_THRESHOLD,
float max_force = SHAPE_MAX_FORCE);
}; };

View File

@@ -10,32 +10,31 @@
#include "ui/ui_manager.hpp" // for UIManager #include "ui/ui_manager.hpp" // for UIManager
BoidManager::BoidManager() BoidManager::BoidManager()
: engine_(nullptr) : engine_(nullptr),
, scene_mgr_(nullptr) scene_mgr_(nullptr),
, ui_mgr_(nullptr) ui_mgr_(nullptr),
, state_mgr_(nullptr) state_mgr_(nullptr),
, screen_width_(0) screen_width_(0),
, screen_height_(0) screen_height_(0),
, boids_active_(false) boids_active_(false),
, spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize() spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize()
, separation_radius_(BOID_SEPARATION_RADIUS) ,
, alignment_radius_(BOID_ALIGNMENT_RADIUS) separation_radius_(BOID_SEPARATION_RADIUS),
, cohesion_radius_(BOID_COHESION_RADIUS) alignment_radius_(BOID_ALIGNMENT_RADIUS),
, separation_weight_(BOID_SEPARATION_WEIGHT) cohesion_radius_(BOID_COHESION_RADIUS),
, alignment_weight_(BOID_ALIGNMENT_WEIGHT) separation_weight_(BOID_SEPARATION_WEIGHT),
, cohesion_weight_(BOID_COHESION_WEIGHT) alignment_weight_(BOID_ALIGNMENT_WEIGHT),
, max_speed_(BOID_MAX_SPEED) cohesion_weight_(BOID_COHESION_WEIGHT),
, min_speed_(BOID_MIN_SPEED) max_speed_(BOID_MAX_SPEED),
, max_force_(BOID_MAX_FORCE) min_speed_(BOID_MIN_SPEED),
, boundary_margin_(BOID_BOUNDARY_MARGIN) max_force_(BOID_MAX_FORCE),
, boundary_weight_(BOID_BOUNDARY_WEIGHT) { boundary_margin_(BOID_BOUNDARY_MARGIN),
boundary_weight_(BOID_BOUNDARY_WEIGHT) {
} }
BoidManager::~BoidManager() { BoidManager::~BoidManager() = default;
}
void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
StateManager* state_mgr, int screen_width, int screen_height) {
engine_ = engine; engine_ = engine;
scene_mgr_ = scene_mgr; scene_mgr_ = scene_mgr;
ui_mgr_ = ui_mgr; ui_mgr_ = ui_mgr;
@@ -65,7 +64,8 @@ void BoidManager::activateBoids() {
auto& balls = scene_mgr_->getBallsMutable(); auto& balls = scene_mgr_->getBallsMutable();
for (auto& ball : balls) { for (auto& ball : balls) {
// Dar velocidad inicial aleatoria si está quieto // Dar velocidad inicial aleatoria si está quieto
float vx, vy; float vx;
float vy;
ball->getVelocity(vx, vy); ball->getVelocity(vx, vy);
if (vx == 0.0f && vy == 0.0f) { if (vx == 0.0f && vy == 0.0f) {
// Velocidad aleatoria entre -60 y +60 px/s (time-based) // Velocidad aleatoria entre -60 y +60 px/s (time-based)
@@ -76,13 +76,15 @@ void BoidManager::activateBoids() {
} }
// Mostrar notificación (solo si NO estamos en modo demo o logo) // Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo boids"); ui_mgr_->showNotification("Modo boids");
} }
} }
void BoidManager::deactivateBoids(bool force_gravity_on) { void BoidManager::deactivateBoids(bool force_gravity_on) {
if (!boids_active_) return; if (!boids_active_) {
return;
}
boids_active_ = false; boids_active_ = false;
@@ -92,7 +94,7 @@ void BoidManager::deactivateBoids(bool force_gravity_on) {
} }
// Mostrar notificación (solo si NO estamos en modo demo o logo) // Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo física"); ui_mgr_->showNotification("Modo física");
} }
} }
@@ -106,7 +108,9 @@ void BoidManager::toggleBoidsMode(bool force_gravity_on) {
} }
void BoidManager::update(float delta_time) { void BoidManager::update(float delta_time) {
if (!boids_active_) return; if (!boids_active_) {
return;
}
auto& balls = scene_mgr_->getBallsMutable(); auto& balls = scene_mgr_->getBallsMutable();
@@ -114,8 +118,8 @@ void BoidManager::update(float delta_time) {
spatial_grid_.clear(); spatial_grid_.clear();
for (auto& ball : balls) { for (auto& ball : balls) {
SDL_FRect pos = ball->getPosition(); SDL_FRect pos = ball->getPosition();
float center_x = pos.x + pos.w / 2.0f; float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + pos.h / 2.0f; float center_y = pos.y + (pos.h / 2.0f);
spatial_grid_.insert(ball.get(), center_x, center_y); spatial_grid_.insert(ball.get(), center_x, center_y);
} }
@@ -131,7 +135,8 @@ void BoidManager::update(float delta_time) {
// Actualizar posiciones con velocidades resultantes (time-based) // Actualizar posiciones con velocidades resultantes (time-based)
for (auto& ball : balls) { for (auto& ball : balls) {
float vx, vy; float vx;
float vy;
ball->getVelocity(vx, vy); ball->getVelocity(vx, vy);
SDL_FRect pos = ball->getPosition(); SDL_FRect pos = ball->getPosition();
@@ -153,22 +158,24 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
int count = 0; int count = 0;
SDL_FRect pos = boid->getPosition(); SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f; float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + pos.h / 2.0f; float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, separation_radius_); auto neighbors = spatial_grid_.queryRadius(center_x, center_y, separation_radius_);
for (Ball* other : neighbors) { for (Ball* other : neighbors) {
if (other == boid) continue; // Ignorar a sí mismo if (other == boid) {
continue; // Ignorar a sí mismo
}
SDL_FRect other_pos = other->getPosition(); SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f; float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + other_pos.h / 2.0f; float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x; float dx = center_x - other_x;
float dy = center_y - other_y; float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy); float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance > 0.0f && distance < separation_radius_) { if (distance > 0.0f && distance < separation_radius_) {
// FASE 1.3: Separación más fuerte cuando más cerca (inversamente proporcional a distancia) // FASE 1.3: Separación más fuerte cuando más cerca (inversamente proporcional a distancia)
@@ -186,7 +193,8 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
steer_y /= count; steer_y /= count;
// Aplicar fuerza de separación // Aplicar fuerza de separación
float vx, vy; float vx;
float vy;
boid->getVelocity(vx, vy); boid->getVelocity(vx, vy);
vx += steer_x * separation_weight_ * delta_time; vx += steer_x * separation_weight_ * delta_time;
vy += steer_y * separation_weight_ * delta_time; vy += steer_y * separation_weight_ * delta_time;
@@ -201,25 +209,28 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
int count = 0; int count = 0;
SDL_FRect pos = boid->getPosition(); SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f; float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + pos.h / 2.0f; float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, alignment_radius_); auto neighbors = spatial_grid_.queryRadius(center_x, center_y, alignment_radius_);
for (Ball* other : neighbors) { for (Ball* other : neighbors) {
if (other == boid) continue; if (other == boid) {
continue;
}
SDL_FRect other_pos = other->getPosition(); SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f; float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + other_pos.h / 2.0f; float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x; float dx = center_x - other_x;
float dy = center_y - other_y; float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy); float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance < alignment_radius_) { if (distance < alignment_radius_) {
float other_vx, other_vy; float other_vx;
float other_vy;
other->getVelocity(other_vx, other_vy); other->getVelocity(other_vx, other_vy);
avg_vx += other_vx; avg_vx += other_vx;
avg_vy += other_vy; avg_vy += other_vy;
@@ -233,13 +244,14 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
avg_vy /= count; avg_vy /= count;
// Steering hacia la velocidad promedio // Steering hacia la velocidad promedio
float vx, vy; float vx;
float vy;
boid->getVelocity(vx, vy); boid->getVelocity(vx, vy);
float steer_x = (avg_vx - vx) * alignment_weight_ * delta_time; float steer_x = (avg_vx - vx) * alignment_weight_ * delta_time;
float steer_y = (avg_vy - vy) * alignment_weight_ * delta_time; float steer_y = (avg_vy - vy) * alignment_weight_ * delta_time;
// Limitar fuerza máxima de steering // Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y); float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > max_force_) { if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_; steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_; steer_y = (steer_y / steer_mag) * max_force_;
@@ -258,22 +270,24 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
int count = 0; int count = 0;
SDL_FRect pos = boid->getPosition(); SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f; float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + pos.h / 2.0f; float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n)) // FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, cohesion_radius_); auto neighbors = spatial_grid_.queryRadius(center_x, center_y, cohesion_radius_);
for (Ball* other : neighbors) { for (Ball* other : neighbors) {
if (other == boid) continue; if (other == boid) {
continue;
}
SDL_FRect other_pos = other->getPosition(); SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f; float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + other_pos.h / 2.0f; float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x; float dx = center_x - other_x;
float dy = center_y - other_y; float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy); float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance < cohesion_radius_) { if (distance < cohesion_radius_) {
center_of_mass_x += other_x; center_of_mass_x += other_x;
@@ -290,7 +304,7 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
// FASE 1.4: Normalizar dirección hacia el centro (CRÍTICO - antes no estaba normalizado!) // FASE 1.4: Normalizar dirección hacia el centro (CRÍTICO - antes no estaba normalizado!)
float dx_to_center = center_of_mass_x - center_x; float dx_to_center = center_of_mass_x - center_x;
float dy_to_center = center_of_mass_y - center_y; float dy_to_center = center_of_mass_y - center_y;
float distance_to_center = std::sqrt(dx_to_center * dx_to_center + dy_to_center * dy_to_center); float distance_to_center = std::sqrt((dx_to_center * dx_to_center) + (dy_to_center * dy_to_center));
// Solo aplicar si hay distancia al centro (evitar división por cero) // Solo aplicar si hay distancia al centro (evitar división por cero)
if (distance_to_center > 0.1f) { if (distance_to_center > 0.1f) {
@@ -299,13 +313,14 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
float steer_y = (dy_to_center / distance_to_center) * cohesion_weight_ * delta_time; float steer_y = (dy_to_center / distance_to_center) * cohesion_weight_ * delta_time;
// Limitar fuerza máxima de steering // Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y); float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > max_force_) { if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_; steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_; steer_y = (steer_y / steer_mag) * max_force_;
} }
float vx, vy; float vx;
float vy;
boid->getVelocity(vx, vy); boid->getVelocity(vx, vy);
vx += steer_x; vx += steer_x;
vy += steer_y; vy += steer_y;
@@ -314,12 +329,12 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
} }
} }
void BoidManager::applyBoundaries(Ball* boid) { void BoidManager::applyBoundaries(Ball* boid) const {
// NUEVA IMPLEMENTACIÓN: Bordes como obstáculos (repulsión en lugar de wrapping) // NUEVA IMPLEMENTACIÓN: Bordes como obstáculos (repulsión en lugar de wrapping)
// Cuando un boid se acerca a un borde, se aplica una fuerza alejándolo // Cuando un boid se acerca a un borde, se aplica una fuerza alejándolo
SDL_FRect pos = boid->getPosition(); SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f; float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + pos.h / 2.0f; float center_y = pos.y + (pos.h / 2.0f);
float steer_x = 0.0f; float steer_x = 0.0f;
float steer_y = 0.0f; float steer_y = 0.0f;
@@ -363,11 +378,12 @@ void BoidManager::applyBoundaries(Ball* boid) {
// Aplicar fuerza de repulsión si hay alguna // Aplicar fuerza de repulsión si hay alguna
if (steer_x != 0.0f || steer_y != 0.0f) { if (steer_x != 0.0f || steer_y != 0.0f) {
float vx, vy; float vx;
float vy;
boid->getVelocity(vx, vy); boid->getVelocity(vx, vy);
// Normalizar fuerza de repulsión (para que todas las direcciones tengan la misma intensidad) // Normalizar fuerza de repulsión (para que todas las direcciones tengan la misma intensidad)
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y); float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > 0.0f) { if (steer_mag > 0.0f) {
steer_x /= steer_mag; steer_x /= steer_mag;
steer_y /= steer_mag; steer_y /= steer_mag;
@@ -381,12 +397,13 @@ void BoidManager::applyBoundaries(Ball* boid) {
} }
} }
void BoidManager::limitSpeed(Ball* boid) { void BoidManager::limitSpeed(Ball* boid) const {
// Limitar velocidad máxima del boid // Limitar velocidad máxima del boid
float vx, vy; float vx;
float vy;
boid->getVelocity(vx, vy); boid->getVelocity(vx, vy);
float speed = std::sqrt(vx * vx + vy * vy); float speed = std::sqrt((vx * vx) + (vy * vy));
// Limitar velocidad máxima // Limitar velocidad máxima
if (speed > max_speed_) { if (speed > max_speed_) {

View File

@@ -46,8 +46,7 @@ class BoidManager {
* @param screen_width Ancho de pantalla actual * @param screen_width Ancho de pantalla actual
* @param screen_height Alto de pantalla actual * @param screen_height Alto de pantalla actual
*/ */
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
StateManager* state_mgr, int screen_width, int screen_height);
/** /**
* @brief Actualiza el tamaño de pantalla (llamado en resize/fullscreen) * @brief Actualiza el tamaño de pantalla (llamado en resize/fullscreen)
@@ -121,6 +120,6 @@ class BoidManager {
void applySeparation(Ball* boid, float delta_time); void applySeparation(Ball* boid, float delta_time);
void applyAlignment(Ball* boid, float delta_time); void applyAlignment(Ball* boid, float delta_time);
void applyCohesion(Ball* boid, float delta_time); void applyCohesion(Ball* boid, float delta_time);
void applyBoundaries(Ball* boid); // Repulsión de bordes (ya no wrapping) void applyBoundaries(Ball* boid) const; // Repulsión de bordes (ya no wrapping)
void limitSpeed(Ball* boid); // Limitar velocidad máxima void limitSpeed(Ball* boid) const; // Limitar velocidad máxima
}; };

View File

@@ -6,9 +6,9 @@
#include "ball.hpp" // for Ball #include "ball.hpp" // for Ball
SpatialGrid::SpatialGrid(int world_width, int world_height, float cell_size) SpatialGrid::SpatialGrid(int world_width, int world_height, float cell_size)
: world_width_(world_width) : world_width_(world_width),
, world_height_(world_height) world_height_(world_height),
, cell_size_(cell_size) { cell_size_(cell_size) {
// Calcular número de celdas en cada dimensión // Calcular número de celdas en cada dimensión
grid_cols_ = static_cast<int>(std::ceil(world_width / cell_size)); grid_cols_ = static_cast<int>(std::ceil(world_width / cell_size));
grid_rows_ = static_cast<int>(std::ceil(world_height / cell_size)); grid_rows_ = static_cast<int>(std::ceil(world_height / cell_size));
@@ -21,7 +21,8 @@ void SpatialGrid::clear() {
void SpatialGrid::insert(Ball* ball, float x, float y) { void SpatialGrid::insert(Ball* ball, float x, float y) {
// Obtener coordenadas de celda // Obtener coordenadas de celda
int cell_x, cell_y; int cell_x;
int cell_y;
getCellCoords(x, y, cell_x, cell_y); getCellCoords(x, y, cell_x, cell_y);
// Generar hash key y añadir a la celda // Generar hash key y añadir a la celda
@@ -29,11 +30,14 @@ void SpatialGrid::insert(Ball* ball, float x, float y) {
cells_[key].push_back(ball); cells_[key].push_back(ball);
} }
std::vector<Ball*> SpatialGrid::queryRadius(float x, float y, float radius) { auto SpatialGrid::queryRadius(float x, float y, float radius) -> std::vector<Ball*> {
std::vector<Ball*> results; std::vector<Ball*> results;
// Calcular rango de celdas a revisar (AABB del círculo de búsqueda) // Calcular rango de celdas a revisar (AABB del círculo de búsqueda)
int min_cell_x, min_cell_y, max_cell_x, max_cell_y; int min_cell_x;
int min_cell_y;
int max_cell_x;
int max_cell_y;
getCellCoords(x - radius, y - radius, min_cell_x, min_cell_y); getCellCoords(x - radius, y - radius, min_cell_x, min_cell_y);
getCellCoords(x + radius, y + radius, max_cell_x, max_cell_y); getCellCoords(x + radius, y + radius, max_cell_x, max_cell_y);
@@ -82,8 +86,8 @@ void SpatialGrid::getCellCoords(float x, float y, int& cell_x, int& cell_y) cons
cell_y = static_cast<int>(std::floor(y / cell_size_)); cell_y = static_cast<int>(std::floor(y / cell_size_));
} }
int SpatialGrid::getCellKey(int cell_x, int cell_y) const { auto SpatialGrid::getCellKey(int cell_x, int cell_y) const -> int {
// Hash espacial 2D → 1D usando codificación por filas // Hash espacial 2D → 1D usando codificación por filas
// Formula: key = y * ancho + x (similar a array 2D aplanado) // Formula: key = y * ancho + x (similar a array 2D aplanado)
return cell_y * grid_cols_ + cell_x; return (cell_y * grid_cols_) + cell_x;
} }

View File

@@ -8,6 +8,7 @@
#include <SDL3/SDL_timer.h> // for SDL_GetTicks #include <SDL3/SDL_timer.h> // for SDL_GetTicks
#include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds #include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds
#include <array> // for std::array
#include <algorithm> // for std::min, std::max, std::sort #include <algorithm> // for std::min, std::max, std::sort
#include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas) #include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
#include <cstdlib> // for rand, srand #include <cstdlib> // for rand, srand
@@ -29,23 +30,25 @@
#include "shapes/png_shape.hpp" // for PNGShape (dynamic_cast en callbacks LOGO) #include "shapes/png_shape.hpp" // for PNGShape (dynamic_cast en callbacks LOGO)
// Implementación de métodos públicos // Implementación de métodos públicos
bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) { auto Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) -> bool { // NOLINT(readability-function-cognitive-complexity)
bool success = true; bool success = true;
// Obtener resolución de pantalla para validación // Obtener resolución de pantalla para validación
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() << '\n';
return false; return false;
} }
int num_displays = 0; int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr; const auto* dm = ((displays != nullptr) && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
int screen_w = dm ? dm->w : 1920; // Fallback si falla int screen_w = (dm != nullptr) ? dm->w : 1920; // Fallback si falla
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080; int screen_h = (dm != nullptr) ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
if (displays) SDL_free(displays); if (displays != nullptr) {
SDL_free(displays);
}
// Usar parámetros o valores por defecto // Usar parámetros o valores por defecto
int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH; int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
@@ -109,7 +112,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags); window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags);
if (window_ == nullptr) { if (window_ == nullptr) {
std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl; std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << '\n';
success = false; success = false;
} else { } else {
// Centrar ventana en pantalla si no está en fullscreen // Centrar ventana en pantalla si no está en fullscreen
@@ -120,7 +123,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal) // Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal)
gpu_ctx_ = std::make_unique<GpuContext>(); gpu_ctx_ = std::make_unique<GpuContext>();
if (!gpu_ctx_->init(window_)) { if (!gpu_ctx_->init(window_)) {
std::cout << "¡No se pudo inicializar SDL_GPU!" << std::endl; std::cout << "¡No se pudo inicializar SDL_GPU!" << '\n';
success = false; success = false;
} else { } else {
gpu_ctx_->setVSync(vsync_enabled_); gpu_ctx_->setVSync(vsync_enabled_);
@@ -128,11 +131,11 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU) // Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU)
// Renderiza a ui_surface_, que luego se sube como textura GPU overlay // Renderiza a ui_surface_, que luego se sube como textura GPU overlay
ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32); ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32);
if (ui_surface_) { if (ui_surface_ != nullptr) {
ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_); ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
} }
if (!ui_renderer_) { if (ui_renderer_ == nullptr) {
std::cout << "Advertencia: no se pudo crear el renderer de UI software" << std::endl; std::cout << "Advertencia: no se pudo crear el renderer de UI software" << '\n';
// No es crítico — el juego funciona sin texto // No es crítico — el juego funciona sin texto
} }
} }
@@ -188,7 +191,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
} }
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4) // Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) { std::ranges::sort(texture_files, [](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; return a.width > b.width;
}); });
@@ -200,14 +203,14 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Cargar textura GPU para renderizado de sprites // Cargar textura GPU para renderizado de sprites
auto gpu_tex = std::make_unique<GpuTexture>(); auto gpu_tex = std::make_unique<GpuTexture>();
if (gpu_ctx_ && !gpu_tex->fromFile(gpu_ctx_->device(), info.path)) { if (gpu_ctx_ && !gpu_tex->fromFile(gpu_ctx_->device(), info.path)) {
std::cerr << "Advertencia: no se pudo cargar textura GPU: " << info.name << std::endl; std::cerr << "Advertencia: no se pudo cargar textura GPU: " << info.name << '\n';
} }
gpu_textures_.push_back(std::move(gpu_tex)); gpu_textures_.push_back(std::move(gpu_tex));
} }
// Verificar que se cargaron texturas // Verificar que se cargaron texturas
if (textures_.empty()) { if (textures_.empty()) {
std::cerr << "ERROR: No se pudieron cargar texturas" << std::endl; std::cerr << "ERROR: No se pudieron cargar texturas" << '\n';
success = false; success = false;
} }
@@ -227,7 +230,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
gpu_pipeline_ = std::make_unique<GpuPipeline>(); gpu_pipeline_ = std::make_unique<GpuPipeline>();
if (!gpu_pipeline_->init(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(), offscreen_fmt)) { if (!gpu_pipeline_->init(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(), offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear el pipeline GPU" << std::endl; std::cerr << "ERROR: No se pudo crear el pipeline GPU" << '\n';
success = false; success = false;
} }
@@ -235,42 +238,45 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// addBackground() no usa el guard de pushQuad(), así que no consume slots aquí. // addBackground() no usa el guard de pushQuad(), así que no consume slots aquí.
// init() reserva internamente +1 quad extra garantizado para el overlay. // init() reserva internamente +1 quad extra garantizado para el overlay.
int sprite_capacity = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO]; int sprite_capacity = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > sprite_capacity) if (custom_scenario_enabled_ && custom_scenario_balls_ > sprite_capacity) {
sprite_capacity = custom_scenario_balls_; sprite_capacity = custom_scenario_balls_;
}
sprite_batch_ = std::make_unique<GpuSpriteBatch>(); sprite_batch_ = std::make_unique<GpuSpriteBatch>();
if (!sprite_batch_->init(gpu_ctx_->device(), sprite_capacity)) { if (!sprite_batch_->init(gpu_ctx_->device(), sprite_capacity)) {
std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << std::endl; std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << '\n';
success = false; success = false;
} }
gpu_ball_buffer_ = std::make_unique<GpuBallBuffer>(); gpu_ball_buffer_ = std::make_unique<GpuBallBuffer>();
if (!gpu_ball_buffer_->init(gpu_ctx_->device())) { if (!gpu_ball_buffer_->init(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear el ball buffer GPU" << std::endl; std::cerr << "ERROR: No se pudo crear el ball buffer GPU" << '\n';
success = false; success = false;
} }
ball_gpu_data_.reserve(GpuBallBuffer::MAX_BALLS); ball_gpu_data_.reserve(GpuBallBuffer::MAX_BALLS);
offscreen_tex_ = std::make_unique<GpuTexture>(); offscreen_tex_ = std::make_unique<GpuTexture>();
if (!offscreen_tex_->createRenderTarget(gpu_ctx_->device(), if (!offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_, current_screen_width_,
current_screen_height_,
offscreen_fmt)) { offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear render target offscreen" << std::endl; std::cerr << "ERROR: No se pudo crear render target offscreen" << '\n';
success = false; success = false;
} }
white_tex_ = std::make_unique<GpuTexture>(); white_tex_ = std::make_unique<GpuTexture>();
if (!white_tex_->createWhite(gpu_ctx_->device())) { if (!white_tex_->createWhite(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear textura blanca" << std::endl; std::cerr << "ERROR: No se pudo crear textura blanca" << '\n';
success = false; success = false;
} }
// Create UI overlay texture (render target usage so GPU can sample it) // Create UI overlay texture (render target usage so GPU can sample it)
ui_tex_ = std::make_unique<GpuTexture>(); ui_tex_ = std::make_unique<GpuTexture>();
if (!ui_tex_->createRenderTarget(gpu_ctx_->device(), if (!ui_tex_->createRenderTarget(gpu_ctx_->device(),
logical_width, logical_height, logical_width,
logical_height,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)) { SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)) {
std::cerr << "Advertencia: no se pudo crear textura UI GPU" << std::endl; std::cerr << "Advertencia: no se pudo crear textura UI GPU" << '\n';
} }
} }
@@ -284,8 +290,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
theme_manager_->initialize(); theme_manager_->initialize();
{ {
int max_balls = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO]; int max_balls = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > max_balls) if (custom_scenario_enabled_ && custom_scenario_balls_ > max_balls) {
max_balls = custom_scenario_balls_; max_balls = custom_scenario_balls_;
}
theme_manager_->setMaxBallCount(max_balls); theme_manager_->setMaxBallCount(max_balls);
} }
@@ -294,13 +301,15 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto
// Propagar configuración custom si fue establecida antes de initialize() // Propagar configuración custom si fue establecida antes de initialize()
if (custom_scenario_enabled_) if (custom_scenario_enabled_) {
scene_manager_->setCustomBallCount(custom_scenario_balls_); scene_manager_->setCustomBallCount(custom_scenario_balls_);
}
// Calcular tamaño físico de ventana ANTES de inicializar UIManager // Calcular tamaño físico de ventana ANTES de inicializar UIManager
// NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe // NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe
// Calcular manualmente para poder pasar valores al constructor de UIManager // Calcular manualmente para poder pasar valores al constructor de UIManager
int window_w = 0, window_h = 0; int window_w = 0;
int window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h); SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w; physical_window_width_ = window_w;
physical_window_height_ = window_h; physical_window_height_ = window_h;
@@ -308,14 +317,11 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar UIManager (HUD, FPS, notificaciones) // Inicializar UIManager (HUD, FPS, notificaciones)
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager // NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
ui_manager_ = std::make_unique<UIManager>(); ui_manager_ = std::make_unique<UIManager>();
ui_manager_->initialize(ui_renderer_, theme_manager_.get(), ui_manager_->initialize(ui_renderer_, theme_manager_.get(), physical_window_width_, physical_window_height_, current_screen_width_, current_screen_height_);
physical_window_width_, physical_window_height_,
current_screen_width_, current_screen_height_);
// Inicializar ShapeManager (gestión de figuras 3D) // Inicializar ShapeManager (gestión de figuras 3D)
shape_manager_ = std::make_unique<ShapeManager>(); shape_manager_ = std::make_unique<ShapeManager>();
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr, shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr, current_screen_width_, current_screen_height_);
current_screen_width_, current_screen_height_);
// Inicializar StateManager (gestión de estados DEMO/LOGO) // Inicializar StateManager (gestión de estados DEMO/LOGO)
state_manager_ = std::make_unique<StateManager>(); state_manager_ = std::make_unique<StateManager>();
@@ -326,50 +332,47 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
if (initial_mode == AppMode::DEMO) { if (initial_mode == AppMode::DEMO) {
state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_); state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
// Como estamos en SANDBOX (default), toggleDemoMode() cambiará a DEMO + randomizará // Como estamos en SANDBOX (default), toggleDemoMode() cambiará a DEMO + randomizará
} } else if (initial_mode == AppMode::DEMO_LITE) {
else if (initial_mode == AppMode::DEMO_LITE) {
state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_); state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
// Como estamos en SANDBOX (default), toggleDemoLiteMode() cambiará a DEMO_LITE + randomizará // Como estamos en SANDBOX (default), toggleDemoLiteMode() cambiará a DEMO_LITE + randomizará
} } else if (initial_mode == AppMode::LOGO) {
else if (initial_mode == AppMode::LOGO) {
size_t initial_ball_count = scene_manager_->getBallCount(); size_t initial_ball_count = scene_manager_->getBallCount();
state_manager_->enterLogoMode(false, current_screen_width_, current_screen_height_, initial_ball_count); state_manager_->enterLogoMode(false, current_screen_width_, current_screen_height_, initial_ball_count);
// enterLogoMode() hace: setState(LOGO) + configuración visual completa // enterLogoMode() hace: setState(LOGO) + configuración visual completa
} }
// Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero) // Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero)
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_);
current_screen_width_, current_screen_height_);
// Inicializar BoidManager (gestión de comportamiento de enjambre) // Inicializar BoidManager (gestión de comportamiento de enjambre)
boid_manager_ = std::make_unique<BoidManager>(); boid_manager_ = std::make_unique<BoidManager>();
boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(), current_screen_width_, current_screen_height_);
current_screen_width_, current_screen_height_);
// Inicializar AppLogo (logo periódico en pantalla) // Inicializar AppLogo (logo periódico en pantalla)
app_logo_ = std::make_unique<AppLogo>(); app_logo_ = std::make_unique<AppLogo>();
if (!app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_)) { if (!app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_)) {
std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl; std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << '\n';
// No es crítico, continuar sin logo // No es crítico, continuar sin logo
app_logo_.reset(); app_logo_.reset();
} }
// Benchmark de rendimiento (determina max_auto_scenario_ para modos automáticos) // Benchmark de rendimiento (determina max_auto_scenario_ para modos automáticos)
if (!skip_benchmark_) if (!skip_benchmark_) {
runPerformanceBenchmark(); runPerformanceBenchmark();
else if (custom_scenario_enabled_) } else if (custom_scenario_enabled_) {
custom_auto_available_ = true; // benchmark omitido: confiar en que el hardware lo soporta custom_auto_available_ = true; // benchmark omitido: confiar en que el hardware lo soporta
}
// Precalentar caché: shapes PNG (evitar I/O en primera activación de PNG_SHAPE) // Precalentar caché: shapes PNG (evitar I/O en primera activación de PNG_SHAPE)
{ {
unsigned char* tmp = nullptr; size_t tmp_size = 0; unsigned char* tmp = nullptr;
size_t tmp_size = 0;
ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size); ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size);
delete[] tmp; delete[] tmp;
} }
// Mostrar ventana ahora que el benchmark terminó // Mostrar ventana ahora que el benchmark terminó
SDL_ShowWindow(window_); SDL_ShowWindow(window_);
} }
return success; return success;
@@ -380,7 +383,7 @@ void Engine::run() {
calculateDeltaTime(); calculateDeltaTime();
// Procesar eventos de entrada (teclado, ratón, ventana) // Procesar eventos de entrada (teclado, ratón, ventana)
if (input_handler_->processEvents(*this)) { if (InputHandler::processEvents(*this)) {
should_exit_ = true; should_exit_ = true;
} }
@@ -391,29 +394,58 @@ void Engine::run() {
void Engine::shutdown() { void Engine::shutdown() {
// Wait for GPU idle before releasing GPU resources // Wait for GPU idle before releasing GPU resources
if (gpu_ctx_) SDL_WaitForGPUIdle(gpu_ctx_->device()); if (gpu_ctx_) {
SDL_WaitForGPUIdle(gpu_ctx_->device());
}
// Release GPU sprite textures // Release GPU sprite textures
gpu_textures_.clear(); gpu_textures_.clear();
// Release GPU render targets and utility textures // Release GPU render targets and utility textures
if (gpu_ctx_) { if (gpu_ctx_) {
if (ui_tex_) { ui_tex_->destroy(gpu_ctx_->device()); ui_tex_.reset(); } if (ui_tex_) {
if (white_tex_) { white_tex_->destroy(gpu_ctx_->device()); white_tex_.reset(); } ui_tex_->destroy(gpu_ctx_->device());
if (offscreen_tex_) { offscreen_tex_->destroy(gpu_ctx_->device()); offscreen_tex_.reset(); } ui_tex_.reset();
if (sprite_batch_) { sprite_batch_->destroy(gpu_ctx_->device()); sprite_batch_.reset(); } }
if (gpu_ball_buffer_) { gpu_ball_buffer_->destroy(gpu_ctx_->device()); gpu_ball_buffer_.reset(); } if (white_tex_) {
if (gpu_pipeline_) { gpu_pipeline_->destroy(gpu_ctx_->device()); gpu_pipeline_.reset(); } white_tex_->destroy(gpu_ctx_->device());
white_tex_.reset();
}
if (offscreen_tex_) {
offscreen_tex_->destroy(gpu_ctx_->device());
offscreen_tex_.reset();
}
if (sprite_batch_) {
sprite_batch_->destroy(gpu_ctx_->device());
sprite_batch_.reset();
}
if (gpu_ball_buffer_) {
gpu_ball_buffer_->destroy(gpu_ctx_->device());
gpu_ball_buffer_.reset();
}
if (gpu_pipeline_) {
gpu_pipeline_->destroy(gpu_ctx_->device());
gpu_pipeline_.reset();
}
} }
// Destroy software UI renderer and surface // Destroy software UI renderer and surface
if (ui_renderer_) { SDL_DestroyRenderer(ui_renderer_); ui_renderer_ = nullptr; } if (ui_renderer_ != nullptr) {
if (ui_surface_) { SDL_DestroySurface(ui_surface_); ui_surface_ = nullptr; } SDL_DestroyRenderer(ui_renderer_);
ui_renderer_ = nullptr;
}
if (ui_surface_ != nullptr) {
SDL_DestroySurface(ui_surface_);
ui_surface_ = nullptr;
}
// Destroy GPU context (releases device and window claim) // Destroy GPU context (releases device and window claim)
if (gpu_ctx_) { gpu_ctx_->destroy(); gpu_ctx_.reset(); } if (gpu_ctx_) {
gpu_ctx_->destroy();
gpu_ctx_.reset();
}
if (window_) { if (window_ != nullptr) {
SDL_DestroyWindow(window_); SDL_DestroyWindow(window_);
window_ = nullptr; window_ = nullptr;
} }
@@ -541,8 +573,8 @@ void Engine::toggleShapeMode() {
} else { } else {
// Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType) // Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType)
// Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE // Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE
const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"}; constexpr std::array<const char*, 10> SHAPE_NAMES = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
showNotificationForAction(shape_names[static_cast<int>(shape_manager_->getCurrentShapeType())]); showNotificationForAction(SHAPE_NAMES[static_cast<int>(shape_manager_->getCurrentShapeType())]);
} }
} }
@@ -567,7 +599,7 @@ void Engine::toggleDepthZoom() {
} }
// Boids (comportamiento de enjambre) // Boids (comportamiento de enjambre)
bool Engine::isScenarioAllowedForBoids(int scenario_id) const { auto Engine::isScenarioAllowedForBoids(int scenario_id) const -> bool {
int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX) int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX)
? custom_scenario_balls_ ? custom_scenario_balls_
: BALL_COUNT_SCENARIOS[scenario_id]; : BALL_COUNT_SCENARIOS[scenario_id];
@@ -654,8 +686,11 @@ void Engine::setMaxBallsOverride(int n) {
skip_benchmark_ = true; skip_benchmark_ = true;
int best = DEMO_AUTO_MIN_SCENARIO; int best = DEMO_AUTO_MIN_SCENARIO;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= DEMO_AUTO_MAX_SCENARIO; ++i) { for (int i = DEMO_AUTO_MIN_SCENARIO; i <= DEMO_AUTO_MAX_SCENARIO; ++i) {
if (BALL_COUNT_SCENARIOS[i] <= n) best = i; if (BALL_COUNT_SCENARIOS[i] <= n) {
else break; best = i;
} else {
break;
}
} }
max_auto_scenario_ = best; max_auto_scenario_ = best;
} }
@@ -665,9 +700,10 @@ void Engine::setCustomScenario(int balls) {
custom_scenario_balls_ = balls; custom_scenario_balls_ = balls;
custom_scenario_enabled_ = true; custom_scenario_enabled_ = true;
// scene_manager_ puede no existir aún (llamada pre-init); propagación en initialize() // scene_manager_ puede no existir aún (llamada pre-init); propagación en initialize()
if (scene_manager_) if (scene_manager_) {
scene_manager_->setCustomBallCount(balls); scene_manager_->setCustomBallCount(balls);
} }
}
// Escenarios (número de pelotas) // Escenarios (número de pelotas)
void Engine::changeScenario(int scenario_id, const char* notification_text) { void Engine::changeScenario(int scenario_id, const char* notification_text) {
@@ -747,15 +783,19 @@ void Engine::toggleLogoMode() {
} }
} }
void Engine::render() { void Engine::render() { // NOLINT(readability-function-cognitive-complexity)
if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) return; if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) {
return;
}
// === Render UI text to software surface === // === Render UI text to software surface ===
renderUIToSurface(); renderUIToSurface();
// === Acquire command buffer === // === Acquire command buffer ===
SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer(); SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
if (!cmd) return; if (cmd == nullptr) {
return;
}
// === Upload UI surface to GPU texture (inline copy pass) === // === Upload UI surface to GPU texture (inline copy pass) ===
uploadUISurface(cmd); uploadUISurface(cmd);
@@ -764,16 +804,27 @@ void Engine::render() {
sprite_batch_->beginFrame(); sprite_batch_->beginFrame();
// Background gradient // Background gradient
float top_r = 0, top_g = 0, top_b = 0, bot_r = 0, bot_g = 0, bot_b = 0; float top_r = 0;
float top_g = 0;
float top_b = 0;
float bot_r = 0;
float bot_g = 0;
float bot_b = 0;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b); theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b);
sprite_batch_->addBackground( sprite_batch_->addBackground(
static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_), static_cast<float>(current_screen_width_),
top_r, top_g, top_b, bot_r, bot_g, bot_b); static_cast<float>(current_screen_height_),
top_r,
top_g,
top_b,
bot_r,
bot_g,
bot_b);
// Sprites (balls) // Sprites (balls)
const auto& balls = scene_manager_->getBalls(); const auto& balls = scene_manager_->getBalls();
const float sw = static_cast<float>(current_screen_width_); const auto SW = static_cast<float>(current_screen_width_);
const float sh = static_cast<float>(current_screen_height_); const auto SH = static_cast<float>(current_screen_height_);
if (current_mode_ == SimulationMode::SHAPE) { if (current_mode_ == SimulationMode::SHAPE) {
// SHAPE mode: bucket sort by depth Z (Painter's Algorithm), with depth scale. // SHAPE mode: bucket sort by depth Z (Painter's Algorithm), with depth scale.
@@ -789,11 +840,7 @@ void Engine::render() {
float brightness = balls[idx]->getDepthBrightness(); float brightness = balls[idx]->getDepthBrightness();
float depth_scale = balls[idx]->getDepthScale(); float depth_scale = balls[idx]->getDepthScale();
float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f; float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h, sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h, color.r / 255.0f * bf, color.g / 255.0f * bf, color.b / 255.0f * bf, 1.0f, depth_scale, SW, SH);
color.r / 255.0f * bf,
color.g / 255.0f * bf,
color.b / 255.0f * bf,
1.0f, depth_scale, sw, sh);
} }
depth_buckets_[b].clear(); depth_buckets_[b].clear();
} }
@@ -805,13 +852,11 @@ void Engine::render() {
SDL_FRect pos = balls[idx]->getPosition(); SDL_FRect pos = balls[idx]->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx); Color color = theme_manager_->getInterpolatedColor(idx);
// Convert to NDC center + NDC half-size (both positive) // Convert to NDC center + NDC half-size (both positive)
float cx = ((pos.x + pos.w * 0.5f) / sw) * 2.0f - 1.0f; float cx = (((pos.x + pos.w * 0.5f) / SW) * 2.0f) - 1.0f;
float cy = 1.0f - ((pos.y + pos.h * 0.5f) / sh) * 2.0f; float cy = 1.0f - (((pos.y + pos.h * 0.5f) / SH) * 2.0f);
float hw = pos.w / sw; float hw = pos.w / SW;
float hh = pos.h / sh; float hh = pos.h / SH;
ball_gpu_data_.push_back({cx, cy, hw, hh, ball_gpu_data_.push_back({cx, cy, hw, hh, color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, 1.0f});
color.r / 255.0f, color.g / 255.0f,
color.b / 255.0f, 1.0f});
} }
} }
@@ -820,26 +865,26 @@ void Engine::render() {
// Upload sprite batch (background + SHAPE balls + UI overlay quad) // Upload sprite batch (background + SHAPE balls + UI overlay quad)
if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) { if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
gpu_ctx_->submit(cmd); GpuContext::submit(cmd);
return; return;
} }
// Upload instanced ball buffer (PHYSICS / CPU-BOIDS modes) // Upload instanced ball buffer (PHYSICS / CPU-BOIDS modes)
bool use_instanced_balls = (current_mode_ != SimulationMode::SHAPE) && !ball_gpu_data_.empty(); bool use_instanced_balls = (current_mode_ != SimulationMode::SHAPE) && !ball_gpu_data_.empty();
if (use_instanced_balls) { if (use_instanced_balls) {
gpu_ball_buffer_->upload(gpu_ctx_->device(), cmd, gpu_ball_buffer_->upload(gpu_ctx_->device(), cmd, ball_gpu_data_.data(), static_cast<int>(ball_gpu_data_.size()));
ball_gpu_data_.data(), static_cast<int>(ball_gpu_data_.size()));
} }
GpuTexture* sprite_tex = (!gpu_textures_.empty()) GpuTexture* sprite_tex = (!gpu_textures_.empty())
? gpu_textures_[current_texture_index_].get() : nullptr; ? gpu_textures_[current_texture_index_].get()
: nullptr;
// === Pass 1: Render background + balls to offscreen texture === // === Pass 1: Render background + balls to offscreen texture ===
if (offscreen_tex_ && offscreen_tex_->isValid() && sprite_tex && sprite_tex->isValid()) { if (offscreen_tex_ && offscreen_tex_->isValid() && (sprite_tex != nullptr) && sprite_tex->isValid()) {
SDL_GPUColorTargetInfo ct = {}; SDL_GPUColorTargetInfo ct = {};
ct.texture = offscreen_tex_->texture(); ct.texture = offscreen_tex_->texture();
ct.load_op = SDL_GPU_LOADOP_CLEAR; ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f}; ct.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE; ct.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr); SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
@@ -875,30 +920,35 @@ void Engine::render() {
SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT); SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()}; SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1); SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->spriteIndexCount(), 1, SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->spriteIndexCount(), 1, sprite_batch_->spriteIndexOffset(), 0, 0);
sprite_batch_->spriteIndexOffset(), 0, 0);
} }
SDL_EndGPURenderPass(pass1); SDL_EndGPURenderPass(pass1);
} }
// === Pass 2+: External multi-pass shader OR native PostFX → swapchain === // === Pass 2+: External multi-pass shader OR native PostFX → swapchain ===
Uint32 sw_w = 0, sw_h = 0; Uint32 sw_w = 0;
Uint32 sw_h = 0;
SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h); SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
if (swapchain && offscreen_tex_ && offscreen_tex_->isValid()) { if ((swapchain != nullptr) && offscreen_tex_ && offscreen_tex_->isValid()) {
// Helper lambda for viewport/scissor (used in the final pass) // Helper lambda for viewport/scissor (used in the final pass)
auto applyViewport = [&](SDL_GPURenderPass* rp) { auto apply_viewport = [&](SDL_GPURenderPass* rp) {
if (!fullscreen_enabled_) return; if (!fullscreen_enabled_) {
float vp_x, vp_y, vp_w, vp_h; return;
}
float vp_x;
float vp_y;
float vp_w;
float vp_h;
if (current_scaling_mode_ == ScalingMode::STRETCH) { if (current_scaling_mode_ == ScalingMode::STRETCH) {
vp_x = 0.0f; vp_y = 0.0f; vp_x = 0.0f;
vp_y = 0.0f;
vp_w = static_cast<float>(sw_w); vp_w = static_cast<float>(sw_w);
vp_h = static_cast<float>(sw_h); vp_h = static_cast<float>(sw_h);
} else if (current_scaling_mode_ == ScalingMode::INTEGER) { } else if (current_scaling_mode_ == ScalingMode::INTEGER) {
int scale = static_cast<int>(std::min(sw_w / static_cast<Uint32>(base_screen_width_), int scale = static_cast<int>(std::min(sw_w / static_cast<Uint32>(base_screen_width_),
sw_h / static_cast<Uint32>(base_screen_height_))); sw_h / static_cast<Uint32>(base_screen_height_)));
if (scale < 1) scale = 1; scale = std::max(scale, 1);
vp_w = static_cast<float>(base_screen_width_ * scale); vp_w = static_cast<float>(base_screen_width_ * scale);
vp_h = static_cast<float>(base_screen_height_ * scale); vp_h = static_cast<float>(base_screen_height_ * scale);
vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f; vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f;
@@ -913,8 +963,7 @@ void Engine::render() {
} }
SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f}; SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f};
SDL_SetGPUViewport(rp, &vp); SDL_SetGPUViewport(rp, &vp);
SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y), SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y), static_cast<int>(vp_w), static_cast<int>(vp_h)};
static_cast<int>(vp_w), static_cast<int>(vp_h)};
SDL_SetGPUScissor(rp, &scissor); SDL_SetGPUScissor(rp, &scissor);
}; };
@@ -923,11 +972,11 @@ void Engine::render() {
SDL_GPUColorTargetInfo ct = {}; SDL_GPUColorTargetInfo ct = {};
ct.texture = swapchain; ct.texture = swapchain;
ct.load_op = SDL_GPU_LOADOP_CLEAR; ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f}; ct.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE; ct.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr); SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
applyViewport(pass2); apply_viewport(pass2);
// PostFX: full-screen triangle via vertex_id (no vertex buffer needed) // PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline()); SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
@@ -945,15 +994,14 @@ void Engine::render() {
SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT); SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()}; SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1); SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1, SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1, sprite_batch_->overlayIndexOffset(), 0, 0);
sprite_batch_->overlayIndexOffset(), 0, 0);
} }
SDL_EndGPURenderPass(pass2); SDL_EndGPURenderPass(pass2);
} // end native PostFX } // end native PostFX
} // end if (swapchain && ...) } // end if (swapchain && ...)
gpu_ctx_->submit(cmd); GpuContext::submit(cmd);
} }
void Engine::showNotificationForAction(const std::string& text) { void Engine::showNotificationForAction(const std::string& text) {
@@ -975,7 +1023,9 @@ void Engine::toggleVSync() {
ui_manager_->updateVSyncText(vsync_enabled_); ui_manager_->updateVSyncText(vsync_enabled_);
// Aplicar el cambio de V-Sync al contexto GPU // Aplicar el cambio de V-Sync al contexto GPU
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_); if (gpu_ctx_) {
gpu_ctx_->setVSync(vsync_enabled_);
}
} }
void Engine::toggleFullscreen() { void Engine::toggleFullscreen() {
@@ -1092,18 +1142,22 @@ void Engine::toggleRealFullscreen() {
} }
void Engine::applyPostFXPreset(int mode) { void Engine::applyPostFXPreset(int mode) {
static constexpr float presets[4][3] = { static constexpr std::array<std::array<float, 3>, 4> PRESETS = {{
{0.8f, 0.0f, 0.0f}, // 0: Vinyeta {0.8f, 0.0f, 0.0f}, // 0: Vinyeta
{0.8f, 0.0f, 0.8f}, // 1: Scanlines {0.8f, 0.0f, 0.8f}, // 1: Scanlines
{0.8f, 0.2f, 0.0f}, // 2: Cromàtica {0.8f, 0.2f, 0.0f}, // 2: Cromàtica
{0.8f, 0.2f, 0.8f}, // 3: Complet {0.8f, 0.2f, 0.8f}, // 3: Complet
}; }};
postfx_uniforms_.vignette_strength = presets[mode][0]; postfx_uniforms_.vignette_strength = PRESETS[mode][0];
postfx_uniforms_.chroma_strength = presets[mode][1]; postfx_uniforms_.chroma_strength = PRESETS[mode][1];
postfx_uniforms_.scanline_strength = presets[mode][2]; postfx_uniforms_.scanline_strength = PRESETS[mode][2];
// Reaplicar overrides de CLI si están activos // Reaplicar overrides de CLI si están activos
if (postfx_override_vignette_ >= 0.f) postfx_uniforms_.vignette_strength = postfx_override_vignette_; if (postfx_override_vignette_ >= 0.f) {
if (postfx_override_chroma_ >= 0.f) postfx_uniforms_.chroma_strength = postfx_override_chroma_; postfx_uniforms_.vignette_strength = postfx_override_vignette_;
}
if (postfx_override_chroma_ >= 0.f) {
postfx_uniforms_.chroma_strength = postfx_override_chroma_;
}
} }
void Engine::handlePostFXCycle() { void Engine::handlePostFXCycle() {
@@ -1111,14 +1165,15 @@ void Engine::handlePostFXCycle() {
} }
void Engine::handlePostFXToggle() { void Engine::handlePostFXToggle() {
static constexpr const char* names[4] = { static constexpr std::array<const char*, 4> NAMES = {
"PostFX viñeta", "PostFX scanlines", "PostFX viñeta",
"PostFX cromática", "PostFX completo" "PostFX scanlines",
}; "PostFX cromática",
"PostFX completo"};
postfx_enabled_ = !postfx_enabled_; postfx_enabled_ = !postfx_enabled_;
if (postfx_enabled_) { if (postfx_enabled_) {
applyPostFXPreset(postfx_effect_mode_); applyPostFXPreset(postfx_effect_mode_);
showNotificationForAction(names[postfx_effect_mode_]); showNotificationForAction(NAMES[postfx_effect_mode_]);
} else { } else {
postfx_uniforms_.vignette_strength = 0.0f; postfx_uniforms_.vignette_strength = 0.0f;
postfx_uniforms_.chroma_strength = 0.0f; postfx_uniforms_.chroma_strength = 0.0f;
@@ -1138,23 +1193,30 @@ void Engine::setPostFXParamOverrides(float vignette, float chroma) {
postfx_override_chroma_ = chroma; postfx_override_chroma_ = chroma;
postfx_enabled_ = true; postfx_enabled_ = true;
// Aplicar inmediatamente sobre el preset activo // Aplicar inmediatamente sobre el preset activo
if (vignette >= 0.f) postfx_uniforms_.vignette_strength = vignette; if (vignette >= 0.f) {
if (chroma >= 0.f) postfx_uniforms_.chroma_strength = chroma; postfx_uniforms_.vignette_strength = vignette;
}
if (chroma >= 0.f) {
postfx_uniforms_.chroma_strength = chroma;
}
} }
void Engine::cycleShader() { void Engine::cycleShader() {
// X no hace nada si PostFX está desactivado // X no hace nada si PostFX está desactivado
if (!postfx_enabled_) return; if (!postfx_enabled_) {
return;
}
// Cicla solo entre los 4 modos (sin OFF) // Cicla solo entre los 4 modos (sin OFF)
postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 4; postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 4;
applyPostFXPreset(postfx_effect_mode_); applyPostFXPreset(postfx_effect_mode_);
static constexpr const char* names[4] = { static constexpr std::array<const char*, 4> NAMES = {
"PostFX viñeta", "PostFX scanlines", "PostFX viñeta",
"PostFX cromática", "PostFX completo" "PostFX scanlines",
}; "PostFX cromática",
showNotificationForAction(names[postfx_effect_mode_]); "PostFX completo"};
showNotificationForAction(NAMES[postfx_effect_mode_]);
} }
void Engine::toggleIntegerScaling() { void Engine::toggleIntegerScaling() {
@@ -1171,44 +1233,51 @@ void Engine::toggleIntegerScaling() {
break; break;
} }
const char* mode_name = "entero"; const char* mode_name = nullptr;
switch (current_scaling_mode_) { switch (current_scaling_mode_) {
case ScalingMode::INTEGER: mode_name = "entero"; break; case ScalingMode::INTEGER:
case ScalingMode::LETTERBOX: mode_name = "letterbox"; break; mode_name = "entero";
case ScalingMode::STRETCH: mode_name = "stretch"; break; break;
case ScalingMode::LETTERBOX:
mode_name = "letterbox";
break;
case ScalingMode::STRETCH:
mode_name = "stretch";
break;
} }
showNotificationForAction(std::string("Escalado ") + mode_name); showNotificationForAction(std::string("Escalado ") + mode_name);
} }
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) { void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
if (!sprite_batch_) return; if (!sprite_batch_) {
sprite_batch_->addSprite(x, y, w, h, return;
r / 255.0f, g / 255.0f, b / 255.0f, 1.0f, }
scale, sprite_batch_->addSprite(x, y, w, h, r / 255.0f, g / 255.0f, b / 255.0f, 1.0f, scale, static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_));
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
} }
// Sistema de escala de ventana (pasos del 10%) // Sistema de escala de ventana (pasos del 10%)
float Engine::calculateMaxWindowScale() const { auto Engine::calculateMaxWindowScale() const -> float {
SDL_Rect bounds; SDL_Rect bounds;
if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) { // bool: false = error if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) { // bool: false = error
return WINDOW_SCALE_MIN; // Fallback solo si falla de verdad return WINDOW_SCALE_MIN; // Fallback solo si falla de verdad
} }
float max_by_w = static_cast<float>(bounds.w - 2 * WINDOW_DESKTOP_MARGIN) / base_screen_width_; float max_by_w = static_cast<float>(bounds.w - (2 * WINDOW_DESKTOP_MARGIN)) / base_screen_width_;
float max_by_h = static_cast<float>(bounds.h - 2 * WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT) / base_screen_height_; float max_by_h = static_cast<float>(bounds.h - (2 * WINDOW_DESKTOP_MARGIN) - WINDOW_DECORATION_HEIGHT) / base_screen_height_;
float result = std::max(WINDOW_SCALE_MIN, std::min(max_by_w, max_by_h)); float result = std::max(WINDOW_SCALE_MIN, std::min(max_by_w, max_by_h));
return result; return result;
} }
// Redimensiona la ventana física manteniéndo su centro, con clamping a pantalla. // Redimensiona la ventana física manteniéndo su centro, con clamping a pantalla.
static void resizeWindowCentered(SDL_Window* window, int new_w, int new_h) { static void resizeWindowCentered(SDL_Window* window, int new_w, int new_h) {
int cur_x, cur_y, cur_w, cur_h; int cur_x;
int cur_y;
int cur_w;
int cur_h;
SDL_GetWindowPosition(window, &cur_x, &cur_y); SDL_GetWindowPosition(window, &cur_x, &cur_y);
SDL_GetWindowSize(window, &cur_w, &cur_h); SDL_GetWindowSize(window, &cur_w, &cur_h);
int new_x = cur_x + (cur_w - new_w) / 2; int new_x = cur_x + ((cur_w - new_w) / 2);
int new_y = cur_y + (cur_h - new_h) / 2; int new_y = cur_y + ((cur_h - new_h) / 2);
SDL_Rect bounds; SDL_Rect bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) { if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) {
@@ -1227,7 +1296,9 @@ void Engine::setWindowScale(float new_scale) {
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale)); new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f; new_scale = std::round(new_scale * 10.0f) / 10.0f;
if (new_scale == current_window_scale_) return; if (new_scale == current_window_scale_) {
return;
}
int new_width = static_cast<int>(std::round(current_screen_width_ * new_scale)); int new_width = static_cast<int>(std::round(current_screen_width_ * new_scale));
int new_height = static_cast<int>(std::round(current_screen_height_ * new_scale)); int new_height = static_cast<int>(std::round(current_screen_height_ * new_scale));
@@ -1242,9 +1313,9 @@ void Engine::zoomIn() {
float prev = current_window_scale_; float prev = current_window_scale_;
setWindowScale(current_window_scale_ + WINDOW_SCALE_STEP); setWindowScale(current_window_scale_ + WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) { if (current_window_scale_ != prev) {
char buf[32]; std::array<char, 32> buf{};
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f); std::snprintf(buf.data(), buf.size(), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf); showNotificationForAction(buf.data());
} }
} }
@@ -1252,9 +1323,9 @@ void Engine::zoomOut() {
float prev = current_window_scale_; float prev = current_window_scale_;
setWindowScale(current_window_scale_ - WINDOW_SCALE_STEP); setWindowScale(current_window_scale_ - WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) { if (current_window_scale_ != prev) {
char buf[32]; std::array<char, 32> buf{};
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f); std::snprintf(buf.data(), buf.size(), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf); showNotificationForAction(buf.data());
} }
} }
@@ -1262,7 +1333,9 @@ void Engine::setFieldScale(float new_scale) {
float max_scale = calculateMaxWindowScale(); float max_scale = calculateMaxWindowScale();
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale)); new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f; new_scale = std::round(new_scale * 10.0f) / 10.0f;
if (new_scale == current_field_scale_) return; if (new_scale == current_field_scale_) {
return;
}
current_field_scale_ = new_scale; current_field_scale_ = new_scale;
current_screen_width_ = static_cast<int>(std::round(base_screen_width_ * new_scale)); current_screen_width_ = static_cast<int>(std::round(base_screen_width_ * new_scale));
@@ -1282,7 +1355,9 @@ void Engine::setFieldScale(float new_scale) {
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_); scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_); boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_); shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
if (app_logo_) app_logo_->updateScreenSize(current_screen_width_, current_screen_height_); if (app_logo_) {
app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
}
if (current_mode_ == SimulationMode::SHAPE) { if (current_mode_ == SimulationMode::SHAPE) {
generateShape(); generateShape();
scene_manager_->enableShapeAttractionAll(true); scene_manager_->enableShapeAttractionAll(true);
@@ -1314,7 +1389,8 @@ void Engine::updatePhysicalWindowSize() {
} }
} else { } else {
// En modo ventana, obtener tamaño FÍSICO real del framebuffer // En modo ventana, obtener tamaño FÍSICO real del framebuffer
int window_w = 0, window_h = 0; int window_w = 0;
int window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h); SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w; physical_window_width_ = window_w;
physical_window_height_ = window_h; physical_window_height_ = window_h;
@@ -1323,9 +1399,7 @@ void Engine::updatePhysicalWindowSize() {
// Notificar a UIManager del cambio de tamaño (delegado) // Notificar a UIManager del cambio de tamaño (delegado)
// Pasar current_screen_height_ para que UIManager actualice la altura lógica // Pasar current_screen_height_ para que UIManager actualice la altura lógica
// (necesario en F4 donde la resolución lógica cambia a la del display) // (necesario en F4 donde la resolución lógica cambia a la del display)
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_, ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_, current_screen_height_);
current_screen_height_);
} }
// ============================================================================ // ============================================================================
@@ -1345,7 +1419,9 @@ void Engine::switchTextureSilent() {
} }
void Engine::setTextureByIndex(size_t index) { void Engine::setTextureByIndex(size_t index) {
if (index >= textures_.size()) return; if (index >= textures_.size()) {
return;
}
current_texture_index_ = index; current_texture_index_ = index;
texture_ = textures_[current_texture_index_]; texture_ = textures_[current_texture_index_];
int new_size = texture_->getWidth(); int new_size = texture_->getWidth();
@@ -1356,7 +1432,9 @@ void Engine::setTextureByIndex(size_t index) {
// Toggle manual del Modo Logo (tecla K) // Toggle manual del Modo Logo (tecla K)
// Sistema de cambio de sprites dinámico // Sistema de cambio de sprites dinámico
void Engine::switchTextureInternal(bool show_notification) { void Engine::switchTextureInternal(bool show_notification) {
if (textures_.empty()) return; if (textures_.empty()) {
return;
}
// Cambiar a siguiente textura (ciclar) // Cambiar a siguiente textura (ciclar)
current_texture_index_ = (current_texture_index_ + 1) % textures_.size(); current_texture_index_ = (current_texture_index_ + 1) % textures_.size();
@@ -1372,7 +1450,7 @@ void Engine::switchTextureInternal(bool show_notification) {
// Mostrar notificación con el nombre de la textura (solo si se solicita) // Mostrar notificación con el nombre de la textura (solo si se solicita)
if (show_notification) { if (show_notification) {
std::string texture_name = texture_names_[current_texture_index_]; std::string texture_name = texture_names_[current_texture_index_];
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::tolower); std::ranges::transform(texture_name, texture_name.begin(), ::tolower);
showNotificationForAction("Textura " + texture_name); showNotificationForAction("Textura " + texture_name);
} }
} }
@@ -1407,9 +1485,11 @@ void Engine::runPerformanceBenchmark() {
int num_displays = 0; int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
float monitor_hz = 60.0f; float monitor_hz = 60.0f;
if (displays && 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 && dm->refresh_rate > 0) monitor_hz = dm->refresh_rate; if ((dm != nullptr) && dm->refresh_rate > 0) {
monitor_hz = dm->refresh_rate;
}
SDL_free(displays); SDL_free(displays);
} }
@@ -1493,59 +1573,70 @@ void Engine::runPerformanceBenchmark() {
// GPU HELPERS // GPU HELPERS
// ============================================================================ // ============================================================================
bool Engine::loadGpuSpriteTexture(size_t index) { auto Engine::loadGpuSpriteTexture(size_t index) -> bool {
if (!gpu_ctx_ || index >= gpu_textures_.size()) return false; if (!gpu_ctx_ || index >= gpu_textures_.size()) {
return false;
}
return gpu_textures_[index] && gpu_textures_[index]->isValid(); return gpu_textures_[index] && gpu_textures_[index]->isValid();
} }
void Engine::recreateOffscreenTexture() { void Engine::recreateOffscreenTexture() {
if (!gpu_ctx_ || !offscreen_tex_) return; if (!gpu_ctx_ || !offscreen_tex_) {
return;
}
SDL_WaitForGPUIdle(gpu_ctx_->device()); SDL_WaitForGPUIdle(gpu_ctx_->device());
offscreen_tex_->destroy(gpu_ctx_->device()); offscreen_tex_->destroy(gpu_ctx_->device());
offscreen_tex_->createRenderTarget(gpu_ctx_->device(), offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_, current_screen_width_,
current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM); SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
// Recreate UI texture to match new screen size // Recreate UI texture to match new screen size
if (ui_tex_) { if (ui_tex_) {
ui_tex_->destroy(gpu_ctx_->device()); ui_tex_->destroy(gpu_ctx_->device());
ui_tex_->createRenderTarget(gpu_ctx_->device(), ui_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_, current_screen_width_,
current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM); SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
} }
// Recreate renderer de software (DESTRUIR renderer PRIMER, després surface) // Recreate renderer de software (DESTRUIR renderer PRIMER, després surface)
if (ui_renderer_) { SDL_DestroyRenderer(ui_renderer_); ui_renderer_ = nullptr; } if (ui_renderer_ != nullptr) {
if (ui_surface_) { SDL_DestroySurface(ui_surface_); ui_surface_ = nullptr; } SDL_DestroyRenderer(ui_renderer_);
ui_renderer_ = nullptr;
}
if (ui_surface_ != nullptr) {
SDL_DestroySurface(ui_surface_);
ui_surface_ = nullptr;
}
ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_, SDL_PIXELFORMAT_RGBA32); ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_, SDL_PIXELFORMAT_RGBA32);
if (ui_surface_) { if (ui_surface_ != nullptr) {
ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_); ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
} }
// Re-inicialitzar components UI amb nou renderer // Re-inicialitzar components UI amb nou renderer
if (ui_renderer_ && ui_manager_) { if ((ui_renderer_ != nullptr) && ui_manager_) {
ui_manager_->initialize(ui_renderer_, theme_manager_.get(), ui_manager_->initialize(ui_renderer_, theme_manager_.get(), current_screen_width_, current_screen_height_, // physical
current_screen_width_, current_screen_height_, // physical base_screen_width_,
base_screen_width_, base_screen_height_); // logical (font size based on base) base_screen_height_); // logical (font size based on base)
} }
if (ui_renderer_ && app_logo_) { if ((ui_renderer_ != nullptr) && app_logo_) {
app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_); app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_);
} }
} }
void Engine::renderUIToSurface() { void Engine::renderUIToSurface() {
if (!ui_renderer_ || !ui_surface_) return; if ((ui_renderer_ == nullptr) || (ui_surface_ == nullptr)) {
return;
}
// Clear surface (fully transparent) // Clear surface (fully transparent)
SDL_SetRenderDrawColor(ui_renderer_, 0, 0, 0, 0); SDL_SetRenderDrawColor(ui_renderer_, 0, 0, 0, 0);
SDL_RenderClear(ui_renderer_); SDL_RenderClear(ui_renderer_);
// Render UI (HUD, FPS counter, notifications) // Render UI (HUD, FPS counter, notifications)
ui_manager_->render(ui_renderer_, this, scene_manager_.get(), current_mode_, ui_manager_->render(ui_renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(), shape_manager_->getActiveShape(), shape_manager_->getConvergence(), physical_window_width_, physical_window_height_, current_screen_width_);
state_manager_->getCurrentMode(),
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
physical_window_width_, physical_window_height_, current_screen_width_);
// Render periodic logo overlay // Render periodic logo overlay
if (app_logo_) { if (app_logo_) {
@@ -1556,21 +1647,25 @@ void Engine::renderUIToSurface() {
} }
void Engine::uploadUISurface(SDL_GPUCommandBuffer* cmd_buf) { void Engine::uploadUISurface(SDL_GPUCommandBuffer* cmd_buf) {
if (!ui_tex_ || !ui_tex_->isValid() || !ui_surface_ || !gpu_ctx_) return; if (!ui_tex_ || !ui_tex_->isValid() || (ui_surface_ == nullptr) || !gpu_ctx_) {
return;
}
int w = ui_surface_->w; int w = ui_surface_->w;
int h = ui_surface_->h; int h = ui_surface_->h;
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel auto data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
SDL_GPUTransferBufferCreateInfo tb_info = {}; SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = data_size; tb_info.size = data_size;
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(gpu_ctx_->device(), &tb_info); SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(gpu_ctx_->device(), &tb_info);
if (!transfer) return; if (transfer == nullptr) {
return;
}
void* mapped = SDL_MapGPUTransferBuffer(gpu_ctx_->device(), transfer, true); void* mapped = SDL_MapGPUTransferBuffer(gpu_ctx_->device(), transfer, true);
if (!mapped) { if (mapped == nullptr) {
SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer); SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
return; return;
} }

View File

@@ -11,7 +11,6 @@
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "ui/app_logo.hpp" // for AppLogo
#include "ball.hpp" // for Ball #include "ball.hpp" // for Ball
#include "boids_mgr/boid_manager.hpp" // for BoidManager #include "boids_mgr/boid_manager.hpp" // for BoidManager
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType #include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
@@ -26,6 +25,7 @@
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager #include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "state/state_manager.hpp" // for StateManager #include "state/state_manager.hpp" // for StateManager
#include "theme_manager.hpp" // for ThemeManager #include "theme_manager.hpp" // for ThemeManager
#include "ui/app_logo.hpp" // for AppLogo
#include "ui/ui_manager.hpp" // for UIManager #include "ui/ui_manager.hpp" // for UIManager
class Engine { class Engine {
@@ -277,5 +277,4 @@ class Engine {
void recreateOffscreenTexture(); // Recreate when resolution changes void recreateOffscreenTexture(); // Recreate when resolution changes
void renderUIToSurface(); // Render text/UI to ui_surface_ void renderUIToSurface(); // Render text/UI to ui_surface_
void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_ void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_
}; };

View File

@@ -1,10 +1,11 @@
#include "gpu_ball_buffer.hpp" #include "gpu_ball_buffer.hpp"
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <algorithm> // std::min #include <algorithm> // std::min
#include <cstring> // memcpy #include <cstring> // memcpy
bool GpuBallBuffer::init(SDL_GPUDevice* device) { auto GpuBallBuffer::init(SDL_GPUDevice* device) -> bool {
Uint32 buf_size = static_cast<Uint32>(MAX_BALLS) * sizeof(BallGPUData); Uint32 buf_size = static_cast<Uint32>(MAX_BALLS) * sizeof(BallGPUData);
// GPU vertex buffer (instance-rate data read by the ball instanced shader) // GPU vertex buffer (instance-rate data read by the ball instanced shader)
@@ -12,7 +13,7 @@ bool GpuBallBuffer::init(SDL_GPUDevice* device) {
buf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; buf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
buf_info.size = buf_size; buf_info.size = buf_size;
gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info); gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info);
if (!gpu_buf_) { if (gpu_buf_ == nullptr) {
SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError()); SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -22,32 +23,43 @@ bool GpuBallBuffer::init(SDL_GPUDevice* device) {
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = buf_size; tb_info.size = buf_size;
transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info); transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!transfer_buf_) { if (transfer_buf_ == nullptr) {
SDL_Log("GpuBallBuffer: transfer buffer creation failed: %s", SDL_GetError()); SDL_Log("GpuBallBuffer: transfer buffer creation failed: %s", SDL_GetError());
return false; return false;
} }
SDL_Log("GpuBallBuffer: initialized (capacity %d balls, %.1f MB VRAM)", SDL_Log("GpuBallBuffer: initialized (capacity %d balls, %.1f MB VRAM)",
MAX_BALLS, buf_size / (1024.0f * 1024.0f)); MAX_BALLS,
buf_size / (1024.0f * 1024.0f));
return true; return true;
} }
void GpuBallBuffer::destroy(SDL_GPUDevice* device) { void GpuBallBuffer::destroy(SDL_GPUDevice* device) {
if (!device) return; if (device == nullptr) {
if (transfer_buf_) { SDL_ReleaseGPUTransferBuffer(device, transfer_buf_); transfer_buf_ = nullptr; } return;
if (gpu_buf_) { SDL_ReleaseGPUBuffer(device, gpu_buf_); gpu_buf_ = nullptr; } }
if (transfer_buf_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device, transfer_buf_);
transfer_buf_ = nullptr;
}
if (gpu_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, gpu_buf_);
gpu_buf_ = nullptr;
}
count_ = 0; count_ = 0;
} }
bool GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, auto GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count) -> bool {
const BallGPUData* data, int count) { if ((data == nullptr) || count <= 0) {
if (!data || count <= 0) { count_ = 0; return false; } count_ = 0;
return false;
}
count = std::min(count, MAX_BALLS); count = std::min(count, MAX_BALLS);
Uint32 upload_size = static_cast<Uint32>(count) * sizeof(BallGPUData); Uint32 upload_size = static_cast<Uint32>(count) * sizeof(BallGPUData);
void* ptr = SDL_MapGPUTransferBuffer(device, transfer_buf_, true /* cycle */); void* ptr = SDL_MapGPUTransferBuffer(device, transfer_buf_, true /* cycle */);
if (!ptr) { if (ptr == nullptr) {
SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError()); SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError());
return false; return false;
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL_gpu.h> #include <SDL3/SDL_gpu.h>
#include <cstdint> #include <cstdint>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -34,8 +35,7 @@ public:
// Upload ball array to GPU via an internal copy pass. // Upload ball array to GPU via an internal copy pass.
// count is clamped to MAX_BALLS. Returns false on error or empty input. // count is clamped to MAX_BALLS. Returns false on error or empty input.
bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count);
const BallGPUData* data, int count);
SDL_GPUBuffer* buffer() const { return gpu_buf_; } SDL_GPUBuffer* buffer() const { return gpu_buf_; }
int count() const { return count_; } int count() const { return count_; }

View File

@@ -1,9 +1,10 @@
#include "gpu_context.hpp" #include "gpu_context.hpp"
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <iostream> #include <iostream>
bool GpuContext::init(SDL_Window* window) { auto GpuContext::init(SDL_Window* window) -> bool {
window_ = window; window_ = window;
// Create GPU device: Metal on Apple, Vulkan elsewhere // Create GPU device: Metal on Apple, Vulkan elsewhere
@@ -13,15 +14,15 @@ bool GpuContext::init(SDL_Window* window) {
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV; SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV;
#endif #endif
device_ = SDL_CreateGPUDevice(preferred, false, nullptr); device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
if (!device_) { if (device_ == nullptr) {
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << std::endl; std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
return false; return false;
} }
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << std::endl; std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << '\n';
// Claim the window so the GPU device owns its swapchain // Claim the window so the GPU device owns its swapchain
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) { if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << std::endl; std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_); SDL_DestroyGPUDevice(device_);
device_ = nullptr; device_ = nullptr;
return false; return false;
@@ -29,17 +30,15 @@ bool GpuContext::init(SDL_Window* window) {
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM) // Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_); swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << std::endl; std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << '\n';
// Default: VSync ON // Default: VSync ON
SDL_SetGPUSwapchainParameters(device_, window_, SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, SDL_GPU_PRESENTMODE_VSYNC);
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
SDL_GPU_PRESENTMODE_VSYNC);
return true; return true;
} }
void GpuContext::destroy() { void GpuContext::destroy() {
if (device_) { if (device_ != nullptr) {
SDL_WaitForGPUIdle(device_); SDL_WaitForGPUIdle(device_);
SDL_ReleaseWindowFromGPUDevice(device_, window_); SDL_ReleaseWindowFromGPUDevice(device_, window_);
SDL_DestroyGPUDevice(device_); SDL_DestroyGPUDevice(device_);
@@ -48,16 +47,17 @@ void GpuContext::destroy() {
window_ = nullptr; window_ = nullptr;
} }
SDL_GPUCommandBuffer* GpuContext::acquireCommandBuffer() { auto GpuContext::acquireCommandBuffer() -> SDL_GPUCommandBuffer* {
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_); SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (!cmd) { if (cmd == nullptr) {
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError()); SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
} }
return cmd; return cmd;
} }
SDL_GPUTexture* GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf, auto GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h) { Uint32* out_w,
Uint32* out_h) -> SDL_GPUTexture* {
SDL_GPUTexture* tex = nullptr; SDL_GPUTexture* tex = nullptr;
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) { if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError()); SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
@@ -71,10 +71,8 @@ void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
SDL_SubmitGPUCommandBuffer(cmd_buf); SDL_SubmitGPUCommandBuffer(cmd_buf);
} }
bool GpuContext::setVSync(bool enabled) { auto GpuContext::setVSync(bool enabled) -> bool {
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
: SDL_GPU_PRESENTMODE_IMMEDIATE; : SDL_GPU_PRESENTMODE_IMMEDIATE;
return SDL_SetGPUSwapchainParameters(device_, window_, return SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode);
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
mode);
} }

View File

@@ -20,8 +20,9 @@ public:
SDL_GPUCommandBuffer* acquireCommandBuffer(); SDL_GPUCommandBuffer* acquireCommandBuffer();
// Returns nullptr if window is minimized (swapchain not available). // Returns nullptr if window is minimized (swapchain not available).
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf, SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h); Uint32* out_w,
void submit(SDL_GPUCommandBuffer* cmd_buf); Uint32* out_h);
static void submit(SDL_GPUCommandBuffer* cmd_buf);
// VSync control (call after init) // VSync control (call after init)
bool setVSync(bool enabled); bool setVSync(bool enabled);

View File

@@ -1,18 +1,21 @@
#include "gpu_pipeline.hpp" #include "gpu_pipeline.hpp"
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <array> // for std::array
#include <cstddef> // offsetof #include <cstddef> // offsetof
#include <cstring> // strlen #include <cstring> // strlen
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
#ifndef __APPLE__ #ifndef __APPLE__
// Generated at build time by CMake + glslc (see cmake/spv_to_header.cmake) // Generated at build time by CMake + glslc (see cmake/spv_to_header.cmake)
#include "sprite_vert_spv.h"
#include "sprite_frag_spv.h"
#include "postfx_vert_spv.h"
#include "postfx_frag_spv.h"
#include "ball_vert_spv.h" #include "ball_vert_spv.h"
#include "postfx_frag_spv.h"
#include "postfx_vert_spv.h"
#include "sprite_frag_spv.h"
#include "sprite_vert_spv.h"
#endif #endif
#ifdef __APPLE__ #ifdef __APPLE__
@@ -204,9 +207,9 @@ vertex BallVOut ball_instanced_vs(BallInstance inst [[stage_in]],
// GpuPipeline implementation // GpuPipeline implementation
// ============================================================================ // ============================================================================
bool GpuPipeline::init(SDL_GPUDevice* device, auto GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format, SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format) { SDL_GPUTextureFormat offscreen_format) -> bool {
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device); SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
#ifdef __APPLE__ #ifdef __APPLE__
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) { if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
@@ -214,7 +217,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
return false; return false;
} }
#else #else
if (!(supported & SDL_GPU_SHADERFORMAT_SPIRV)) { if ((supported & SDL_GPU_SHADERFORMAT_SPIRV) == 0u) {
SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported); SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported);
return false; return false;
} }
@@ -224,20 +227,20 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// Sprite pipeline // Sprite pipeline
// ---------------------------------------------------------------- // ----------------------------------------------------------------
#ifdef __APPLE__ #ifdef __APPLE__
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs", SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#else #else
SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size, SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* sprite_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* sprite_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#endif #endif
if (!sprite_vert || !sprite_frag) { if ((sprite_vert == nullptr) || (sprite_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create sprite shaders"); SDL_Log("GpuPipeline: failed to create sprite shaders");
if (sprite_vert) SDL_ReleaseGPUShader(device, sprite_vert); if (sprite_vert != nullptr) {
if (sprite_frag) SDL_ReleaseGPUShader(device, sprite_frag); SDL_ReleaseGPUShader(device, sprite_vert);
}
if (sprite_frag != nullptr) {
SDL_ReleaseGPUShader(device, sprite_frag);
}
return false; return false;
} }
@@ -248,7 +251,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
vb_desc.instance_step_rate = 0; vb_desc.instance_step_rate = 0;
SDL_GPUVertexAttribute attrs[3] = {}; std::array<SDL_GPUVertexAttribute, 3> attrs = {};
attrs[0].location = 0; attrs[0].location = 0;
attrs[0].buffer_slot = 0; attrs[0].buffer_slot = 0;
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
@@ -267,7 +270,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUVertexInputState vertex_input = {}; SDL_GPUVertexInputState vertex_input = {};
vertex_input.vertex_buffer_descriptions = &vb_desc; vertex_input.vertex_buffer_descriptions = &vb_desc;
vertex_input.num_vertex_buffers = 1; vertex_input.num_vertex_buffers = 1;
vertex_input.vertex_attributes = attrs; vertex_input.vertex_attributes = attrs.data();
vertex_input.num_vertex_attributes = 3; vertex_input.num_vertex_attributes = 3;
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) // Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
@@ -298,7 +301,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_ReleaseGPUShader(device, sprite_vert); SDL_ReleaseGPUShader(device, sprite_vert);
SDL_ReleaseGPUShader(device, sprite_frag); SDL_ReleaseGPUShader(device, sprite_frag);
if (!sprite_pipeline_) { if (sprite_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError()); SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -310,20 +313,20 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// Targets: offscreen (same as sprite pipeline) // Targets: offscreen (same as sprite pipeline)
// ---------------------------------------------------------------- // ----------------------------------------------------------------
#ifdef __APPLE__ #ifdef __APPLE__
SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs", SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#else #else
SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size, SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* ball_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* ball_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#endif #endif
if (!ball_vert || !ball_frag) { if ((ball_vert == nullptr) || (ball_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create ball instanced shaders"); SDL_Log("GpuPipeline: failed to create ball instanced shaders");
if (ball_vert) SDL_ReleaseGPUShader(device, ball_vert); if (ball_vert != nullptr) {
if (ball_frag) SDL_ReleaseGPUShader(device, ball_frag); SDL_ReleaseGPUShader(device, ball_vert);
}
if (ball_frag != nullptr) {
SDL_ReleaseGPUShader(device, ball_frag);
}
return false; return false;
} }
@@ -334,7 +337,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE; ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
ball_vb_desc.instance_step_rate = 1; ball_vb_desc.instance_step_rate = 1;
SDL_GPUVertexAttribute ball_attrs[3] = {}; std::array<SDL_GPUVertexAttribute, 3> ball_attrs = {};
// attr 0: center (float2) at offset 0 // attr 0: center (float2) at offset 0
ball_attrs[0].location = 0; ball_attrs[0].location = 0;
ball_attrs[0].buffer_slot = 0; ball_attrs[0].buffer_slot = 0;
@@ -354,7 +357,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUVertexInputState ball_vertex_input = {}; SDL_GPUVertexInputState ball_vertex_input = {};
ball_vertex_input.vertex_buffer_descriptions = &ball_vb_desc; ball_vertex_input.vertex_buffer_descriptions = &ball_vb_desc;
ball_vertex_input.num_vertex_buffers = 1; ball_vertex_input.num_vertex_buffers = 1;
ball_vertex_input.vertex_attributes = ball_attrs; ball_vertex_input.vertex_attributes = ball_attrs.data();
ball_vertex_input.num_vertex_attributes = 3; ball_vertex_input.num_vertex_attributes = 3;
SDL_GPUGraphicsPipelineCreateInfo ball_pipe_info = {}; SDL_GPUGraphicsPipelineCreateInfo ball_pipe_info = {};
@@ -370,7 +373,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_ReleaseGPUShader(device, ball_vert); SDL_ReleaseGPUShader(device, ball_vert);
SDL_ReleaseGPUShader(device, ball_frag); SDL_ReleaseGPUShader(device, ball_frag);
if (!ball_pipeline_) { if (ball_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError()); SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -389,20 +392,20 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// PostFX pipeline // PostFX pipeline
// ---------------------------------------------------------------- // ----------------------------------------------------------------
#ifdef __APPLE__ #ifdef __APPLE__
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs", SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else #else
SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size, SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* postfx_frag = createShaderSPIRV(device, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
SDL_GPUShader* postfx_frag = createShaderSPIRV(device, kpostfx_frag_spv, kpostfx_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif #endif
if (!postfx_vert || !postfx_frag) { if ((postfx_vert == nullptr) || (postfx_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create postfx shaders"); SDL_Log("GpuPipeline: failed to create postfx shaders");
if (postfx_vert) SDL_ReleaseGPUShader(device, postfx_vert); if (postfx_vert != nullptr) {
if (postfx_frag) SDL_ReleaseGPUShader(device, postfx_frag); SDL_ReleaseGPUShader(device, postfx_vert);
}
if (postfx_frag != nullptr) {
SDL_ReleaseGPUShader(device, postfx_frag);
}
return false; return false;
} }
@@ -430,7 +433,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_ReleaseGPUShader(device, postfx_vert); SDL_ReleaseGPUShader(device, postfx_vert);
SDL_ReleaseGPUShader(device, postfx_frag); SDL_ReleaseGPUShader(device, postfx_frag);
if (!postfx_pipeline_) { if (postfx_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError()); SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -440,19 +443,28 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
} }
void GpuPipeline::destroy(SDL_GPUDevice* device) { void GpuPipeline::destroy(SDL_GPUDevice* device) {
if (sprite_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_); sprite_pipeline_ = nullptr; } if (sprite_pipeline_ != nullptr) {
if (ball_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_); ball_pipeline_ = nullptr; } SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_);
if (postfx_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_); postfx_pipeline_ = nullptr; } sprite_pipeline_ = nullptr;
}
if (ball_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_);
ball_pipeline_ = nullptr;
}
if (postfx_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_);
postfx_pipeline_ = nullptr;
}
} }
SDL_GPUShader* GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device, auto GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code, const uint8_t* spv_code,
size_t spv_size, size_t spv_size,
const char* entrypoint, const char* entrypoint,
SDL_GPUShaderStage stage, SDL_GPUShaderStage stage,
Uint32 num_samplers, Uint32 num_samplers,
Uint32 num_uniform_buffers, Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) { Uint32 num_storage_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {}; SDL_GPUShaderCreateInfo info = {};
info.code = spv_code; info.code = spv_code;
info.code_size = spv_size; info.code_size = spv_size;
@@ -464,18 +476,19 @@ SDL_GPUShader* GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
info.num_storage_buffers = num_storage_buffers; info.num_storage_buffers = num_storage_buffers;
info.num_uniform_buffers = num_uniform_buffers; info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info); SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (!shader) if (shader == nullptr) {
SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError()); SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader; return shader;
} }
SDL_GPUShader* GpuPipeline::createShader(SDL_GPUDevice* device, auto GpuPipeline::createShader(SDL_GPUDevice* device,
const char* msl_source, const char* msl_source,
const char* entrypoint, const char* entrypoint,
SDL_GPUShaderStage stage, SDL_GPUShaderStage stage,
Uint32 num_samplers, Uint32 num_samplers,
Uint32 num_uniform_buffers, Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) { Uint32 num_storage_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {}; SDL_GPUShaderCreateInfo info = {};
info.code = reinterpret_cast<const Uint8*>(msl_source); info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = static_cast<size_t>(strlen(msl_source) + 1); info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
@@ -488,7 +501,7 @@ SDL_GPUShader* GpuPipeline::createShader(SDL_GPUDevice* device,
info.num_uniform_buffers = num_uniform_buffers; info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info); SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (!shader) { if (shader == nullptr) {
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError()); SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
} }
return shader; return shader;

View File

@@ -40,7 +40,7 @@ public:
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; } SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
private: private:
SDL_GPUShader* createShader(SDL_GPUDevice* device, static SDL_GPUShader* createShader(SDL_GPUDevice* device,
const char* msl_source, const char* msl_source,
const char* entrypoint, const char* entrypoint,
SDL_GPUShaderStage stage, SDL_GPUShaderStage stage,
@@ -48,7 +48,7 @@ private:
Uint32 num_uniform_buffers, Uint32 num_uniform_buffers,
Uint32 num_storage_buffers = 0); Uint32 num_storage_buffers = 0);
SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device, static SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code, const uint8_t* spv_code,
size_t spv_size, size_t spv_size,
const char* entrypoint, const char* entrypoint,

View File

@@ -1,13 +1,14 @@
#include "gpu_sprite_batch.hpp" #include "gpu_sprite_batch.hpp"
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <cstring> // memcpy #include <cstring> // memcpy
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Public interface // Public interface
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) { auto GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) -> bool {
max_sprites_ = max_sprites; max_sprites_ = max_sprites;
// Pre-allocate GPU buffers large enough for (max_sprites_ + 2) quads. // Pre-allocate GPU buffers large enough for (max_sprites_ + 2) quads.
// The +2 reserves one slot for the background quad and one for the fullscreen overlay. // The +2 reserves one slot for the background quad and one for the fullscreen overlay.
@@ -22,7 +23,7 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX; vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
vb_info.size = vb_size; vb_info.size = vb_size;
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info); vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
if (!vertex_buf_) { if (vertex_buf_ == nullptr) {
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError()); SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -32,7 +33,7 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX; ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
ib_info.size = ib_size; ib_info.size = ib_size;
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info); index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
if (!index_buf_) { if (index_buf_ == nullptr) {
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError()); SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
return false; return false;
} }
@@ -43,14 +44,14 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
tb_info.size = vb_size; tb_info.size = vb_size;
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info); vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!vertex_transfer_) { if (vertex_transfer_ == nullptr) {
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError()); SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
return false; return false;
} }
tb_info.size = ib_size; tb_info.size = ib_size;
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info); index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!index_transfer_) { if (index_transfer_ == nullptr) {
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError()); SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
return false; return false;
} }
@@ -61,11 +62,25 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
} }
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) { void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
if (!device) return; if (device == nullptr) {
if (vertex_transfer_) { SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_); vertex_transfer_ = nullptr; } return;
if (index_transfer_) { SDL_ReleaseGPUTransferBuffer(device, index_transfer_); index_transfer_ = nullptr; } }
if (vertex_buf_) { SDL_ReleaseGPUBuffer(device, vertex_buf_); vertex_buf_ = nullptr; } if (vertex_transfer_ != nullptr) {
if (index_buf_) { SDL_ReleaseGPUBuffer(device, index_buf_); index_buf_ = nullptr; } SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_);
vertex_transfer_ = nullptr;
}
if (index_transfer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device, index_transfer_);
index_transfer_ = nullptr;
}
if (vertex_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, vertex_buf_);
vertex_buf_ = nullptr;
}
if (index_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, index_buf_);
index_buf_ = nullptr;
}
} }
void GpuSpriteBatch::beginFrame() { void GpuSpriteBatch::beginFrame() {
@@ -78,14 +93,12 @@ void GpuSpriteBatch::beginFrame() {
overlay_index_count_ = 0; overlay_index_count_ = 0;
} }
void GpuSpriteBatch::addBackground(float screen_w, float screen_h, void GpuSpriteBatch::addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b) {
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b) {
// Background is the full screen quad, corners: // Background is the full screen quad, corners:
// TL(-1, 1) TR(1, 1) → top color // TL(-1, 1) TR(1, 1) → top color
// BL(-1,-1) BR(1,-1) → bottom color // BL(-1,-1) BR(1,-1) → bottom color
// We push it as 4 separate vertices (different colors per row). // We push it as 4 separate vertices (different colors per row).
uint32_t vi = static_cast<uint32_t>(vertices_.size()); auto vi = static_cast<uint32_t>(vertices_.size());
// Top-left // Top-left
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f}); vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f});
@@ -97,19 +110,21 @@ void GpuSpriteBatch::addBackground(float screen_w, float screen_h,
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f}); vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
// Two triangles: TL-TR-BR, BR-BL-TL // Two triangles: TL-TR-BR, BR-BL-TL
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2); indices_.push_back(vi + 0);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0); indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
bg_index_count_ = 6; bg_index_count_ = 6;
sprite_index_offset_ = 6; sprite_index_offset_ = 6;
(void)screen_w; (void)screen_h; // unused — bg always covers full NDC (void)screen_w;
(void)screen_h; // unused — bg always covers full NDC
} }
void GpuSpriteBatch::addSprite(float x, float y, float w, float h, void GpuSpriteBatch::addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h) {
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h) {
// Apply scale around the sprite centre // Apply scale around the sprite centre
float scaled_w = w * scale; float scaled_w = w * scale;
float scaled_h = h * scale; float scaled_h = h * scale;
@@ -121,7 +136,10 @@ void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
float px1 = px0 + scaled_w; float px1 = px0 + scaled_w;
float py1 = py0 + scaled_h; float py1 = py0 + scaled_h;
float ndx0, ndy0, ndx1, ndy1; float ndx0;
float ndy0;
float ndx1;
float ndy1;
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0); toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1); toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
@@ -133,30 +151,42 @@ void GpuSpriteBatch::addFullscreenOverlay() {
// El overlay es un slot reservado fuera del espacio de max_sprites_, igual que el background. // El overlay es un slot reservado fuera del espacio de max_sprites_, igual que el background.
// Escribe directamente sin pasar por el guard de pushQuad(). // Escribe directamente sin pasar por el guard de pushQuad().
overlay_index_offset_ = static_cast<int>(indices_.size()); overlay_index_offset_ = static_cast<int>(indices_.size());
uint32_t vi = static_cast<uint32_t>(vertices_.size()); auto vi = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}); vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}); vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}); vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}); vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2); indices_.push_back(vi + 0);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0); indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
overlay_index_count_ = 6; overlay_index_count_ = 6;
} }
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) { auto GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) -> bool {
if (vertices_.empty()) return false; if (vertices_.empty()) {
return false;
}
Uint32 vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex)); auto vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
Uint32 ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t)); auto ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
// Map → write → unmap transfer buffers // Map → write → unmap transfer buffers
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */); void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
if (!vp) { SDL_Log("GpuSpriteBatch: vertex map failed"); return false; } if (vp == nullptr) {
SDL_Log("GpuSpriteBatch: vertex map failed");
return false;
}
memcpy(vp, vertices_.data(), vb_size); memcpy(vp, vertices_.data(), vb_size);
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_); SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */); void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
if (!ip) { SDL_Log("GpuSpriteBatch: index map failed"); return false; } if (ip == nullptr) {
SDL_Log("GpuSpriteBatch: index map failed");
return false;
}
memcpy(ip, indices_.data(), ib_size); memcpy(ip, indices_.data(), ib_size);
SDL_UnmapGPUTransferBuffer(device, index_transfer_); SDL_UnmapGPUTransferBuffer(device, index_transfer_);
@@ -179,19 +209,17 @@ bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cm
// Private helpers // Private helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void GpuSpriteBatch::toNDC(float px, float py, void GpuSpriteBatch::toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy) {
float screen_w, float screen_h,
float& ndx, float& ndy) const {
ndx = (px / screen_w) * 2.0f - 1.0f; ndx = (px / screen_w) * 2.0f - 1.0f;
ndy = 1.0f - (py / screen_h) * 2.0f; ndy = 1.0f - (py / screen_h) * 2.0f;
} }
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a) {
float u0, float v0, float u1, float v1,
float r, float g, float b, float a) {
// +1 reserva el slot del background que ya entró sin pasar por este guard. // +1 reserva el slot del background que ya entró sin pasar por este guard.
if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) return; if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) {
uint32_t vi = static_cast<uint32_t>(vertices_.size()); return;
}
auto vi = static_cast<uint32_t>(vertices_.size());
// TL, TR, BR, BL // TL, TR, BR, BL
vertices_.push_back({ndx0, ndy0, u0, v0, r, g, b, a}); vertices_.push_back({ndx0, ndy0, u0, v0, r, g, b, a});
@@ -199,6 +227,10 @@ void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
vertices_.push_back({ndx1, ndy1, u1, v1, r, g, b, a}); vertices_.push_back({ndx1, ndy1, u1, v1, r, g, b, a});
vertices_.push_back({ndx0, ndy1, u0, v1, r, g, b, a}); vertices_.push_back({ndx0, ndy1, u0, v1, r, g, b, a});
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2); indices_.push_back(vi + 0);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0); indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
} }

View File

@@ -1,8 +1,9 @@
#pragma once #pragma once
#include <SDL3/SDL_gpu.h> #include <SDL3/SDL_gpu.h>
#include <vector>
#include <cstdint> #include <cstdint>
#include <vector>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// GpuVertex — 8-float vertex layout sent to the GPU. // GpuVertex — 8-float vertex layout sent to the GPU.
@@ -36,16 +37,11 @@ public:
// Add the full-screen background gradient quad. // Add the full-screen background gradient quad.
// top_* and bot_* are RGB in [0,1]. // top_* and bot_* are RGB in [0,1].
void addBackground(float screen_w, float screen_h, void addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b);
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b);
// Add a sprite quad (pixel coordinates). // Add a sprite quad (pixel coordinates).
// scale: uniform scale around the quad centre. // scale: uniform scale around the quad centre.
void addSprite(float x, float y, float w, float h, void addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h);
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h);
// Add a full-screen overlay quad (e.g. UI surface, NDC 1..1). // Add a full-screen overlay quad (e.g. UI surface, NDC 1..1).
void addFullscreenOverlay(); void addFullscreenOverlay();
@@ -64,11 +60,8 @@ public:
bool isEmpty() const { return vertices_.empty(); } bool isEmpty() const { return vertices_.empty(); }
private: private:
void toNDC(float px, float py, float screen_w, float screen_h, static void toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy);
float& ndx, float& ndy) const; void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a);
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a);
std::vector<GpuVertex> vertices_; std::vector<GpuVertex> vertices_;
std::vector<uint32_t> indices_; std::vector<uint32_t> indices_;

View File

@@ -2,6 +2,8 @@
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
#include <SDL3/SDL_pixels.h> #include <SDL3/SDL_pixels.h>
#include <array> // for std::array
#include <cstring> // memcpy #include <cstring> // memcpy
#include <string> #include <string>
@@ -13,7 +15,7 @@
// Public interface // Public interface
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) { auto GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) -> bool {
unsigned char* resource_data = nullptr; unsigned char* resource_data = nullptr;
size_t resource_size = 0; size_t resource_size = 0;
@@ -22,15 +24,22 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
return false; return false;
} }
int w = 0, h = 0, orig = 0; int w = 0;
int h = 0;
int orig = 0;
unsigned char* pixels = stbi_load_from_memory( unsigned char* pixels = stbi_load_from_memory(
resource_data, static_cast<int>(resource_size), resource_data,
&w, &h, &orig, STBI_rgb_alpha); static_cast<int>(resource_size),
&w,
&h,
&orig,
STBI_rgb_alpha);
delete[] resource_data; delete[] resource_data;
if (!pixels) { if (pixels == nullptr) {
SDL_Log("GpuTexture: stbi decode failed for '%s': %s", SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
file_path.c_str(), stbi_failure_reason()); file_path.c_str(),
stbi_failure_reason());
return false; return false;
} }
@@ -44,15 +53,17 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
return ok; return ok;
} }
bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) { auto GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) -> bool {
if (!surface) return false; if (surface == nullptr) {
return false;
}
// Ensure RGBA32 format // Ensure RGBA32 format
SDL_Surface* rgba = surface; SDL_Surface* rgba = surface;
bool need_free = false; bool need_free = false;
if (surface->format != SDL_PIXELFORMAT_RGBA32) { if (surface->format != SDL_PIXELFORMAT_RGBA32) {
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32); rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
if (!rgba) { if (rgba == nullptr) {
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError()); SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
return false; return false;
} }
@@ -60,23 +71,24 @@ bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool n
} }
destroy(device); destroy(device);
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h, bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM); if (ok) {
if (ok) ok = createSampler(device, nearest); ok = createSampler(device, nearest);
}
if (need_free) SDL_DestroySurface(rgba); if (need_free) {
SDL_DestroySurface(rgba);
}
return ok; return ok;
} }
bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h, auto GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format) -> bool {
SDL_GPUTextureFormat format) {
destroy(device); destroy(device);
SDL_GPUTextureCreateInfo info = {}; SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D; info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = format; info.format = format;
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
| SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = static_cast<Uint32>(w); info.width = static_cast<Uint32>(w);
info.height = static_cast<Uint32>(h); info.height = static_cast<Uint32>(h);
info.layer_count_or_depth = 1; info.layer_count_or_depth = 1;
@@ -84,7 +96,7 @@ bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
info.sample_count = SDL_GPU_SAMPLECOUNT_1; info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &info); texture_ = SDL_CreateGPUTexture(device, &info);
if (!texture_) { if (texture_ == nullptr) {
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError()); SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
return false; return false;
} }
@@ -95,20 +107,29 @@ bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
return createSampler(device, false); return createSampler(device, false);
} }
bool GpuTexture::createWhite(SDL_GPUDevice* device) { auto GpuTexture::createWhite(SDL_GPUDevice* device) -> bool {
destroy(device); destroy(device);
// 1×1 white RGBA pixel // 1×1 white RGBA pixel
const Uint8 white[4] = {255, 255, 255, 255}; constexpr std::array<Uint8, 4> WHITE = {255, 255, 255, 255};
bool ok = uploadPixels(device, white, 1, 1, bool ok = uploadPixels(device, WHITE.data(), 1, 1, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM); if (ok) {
if (ok) ok = createSampler(device, true); ok = createSampler(device, true);
}
return ok; return ok;
} }
void GpuTexture::destroy(SDL_GPUDevice* device) { void GpuTexture::destroy(SDL_GPUDevice* device) {
if (!device) return; if (device == nullptr) {
if (sampler_) { SDL_ReleaseGPUSampler(device, sampler_); sampler_ = nullptr; } return;
if (texture_) { SDL_ReleaseGPUTexture(device, texture_); texture_ = nullptr; } }
if (sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device, sampler_);
sampler_ = nullptr;
}
if (texture_ != nullptr) {
SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr;
}
width_ = height_ = 0; width_ = height_ = 0;
} }
@@ -116,8 +137,7 @@ void GpuTexture::destroy(SDL_GPUDevice* device) {
// Private helpers // Private helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels, auto GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format) -> bool {
int w, int h, SDL_GPUTextureFormat format) {
// Create GPU texture // Create GPU texture
SDL_GPUTextureCreateInfo tex_info = {}; SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D; tex_info.type = SDL_GPU_TEXTURETYPE_2D;
@@ -130,20 +150,20 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1; tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &tex_info); texture_ = SDL_CreateGPUTexture(device, &tex_info);
if (!texture_) { if (texture_ == nullptr) {
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError()); SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
return false; return false;
} }
// Create transfer buffer and upload pixels // Create transfer buffer and upload pixels
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel auto data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
SDL_GPUTransferBufferCreateInfo tb_info = {}; SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = data_size; tb_info.size = data_size;
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info); SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!transfer) { if (transfer == nullptr) {
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError()); SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device, texture_); SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr; texture_ = nullptr;
@@ -151,7 +171,7 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
} }
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false); void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
if (!mapped) { if (mapped == nullptr) {
SDL_Log("GpuTexture: map failed: %s", SDL_GetError()); SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device, transfer); SDL_ReleaseGPUTransferBuffer(device, transfer);
SDL_ReleaseGPUTexture(device, texture_); SDL_ReleaseGPUTexture(device, texture_);
@@ -190,7 +210,7 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
return true; return true;
} }
bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) { auto GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) -> bool {
SDL_GPUSamplerCreateInfo info = {}; SDL_GPUSamplerCreateInfo info = {};
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR; info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR; info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
@@ -200,7 +220,7 @@ bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) {
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device, &info); sampler_ = SDL_CreateGPUSampler(device, &info);
if (!sampler_) { if (sampler_ == nullptr) {
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError()); SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
return false; return false;
} }

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL_gpu.h> #include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_surface.h> #include <SDL3/SDL_surface.h>
#include <string> #include <string>
// ============================================================================ // ============================================================================
@@ -21,8 +22,7 @@ public:
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true); bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage). // Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
bool createRenderTarget(SDL_GPUDevice* device, int w, int h, bool createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format);
SDL_GPUTextureFormat format);
// Create a 1×1 opaque white texture (used for untextured geometry). // Create a 1×1 opaque white texture (used for untextured geometry).
bool createWhite(SDL_GPUDevice* device); bool createWhite(SDL_GPUDevice* device);
@@ -37,8 +37,7 @@ public:
bool isValid() const { return texture_ != nullptr; } bool isValid() const { return texture_ != nullptr; }
private: private:
bool uploadPixels(SDL_GPUDevice* device, const void* pixels, bool uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format);
int w, int h, SDL_GPUTextureFormat format);
bool createSampler(SDL_GPUDevice* device, bool nearest); bool createSampler(SDL_GPUDevice* device, bool nearest);
SDL_GPUTexture* texture_ = nullptr; SDL_GPUTexture* texture_ = nullptr;

View File

@@ -1,13 +1,14 @@
#include "input_handler.hpp" #include "input_handler.hpp"
#include <SDL3/SDL_keycode.h> // for SDL_Keycode #include <SDL3/SDL_keycode.h> // for SDL_Keycode
#include <string> // for std::string, std::to_string #include <string> // for std::string, std::to_string
#include "defines.hpp" // for KIOSK_NOTIFICATION_TEXT #include "defines.hpp" // for KIOSK_NOTIFICATION_TEXT
#include "engine.hpp" // for Engine #include "engine.hpp" // for Engine
#include "external/mouse.hpp" // for Mouse namespace #include "external/mouse.hpp" // for Mouse namespace
bool InputHandler::processEvents(Engine& engine) { auto InputHandler::processEvents(Engine& engine) -> bool { // NOLINT(readability-function-cognitive-complexity)
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
// Procesar eventos de ratón (auto-ocultar cursor) // Procesar eventos de ratón (auto-ocultar cursor)
@@ -19,7 +20,7 @@ bool InputHandler::processEvents(Engine& engine) {
} }
// Procesar eventos de teclado // Procesar eventos de teclado
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) { if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
switch (event.key.key) { switch (event.key.key) {
case SDLK_ESCAPE: case SDLK_ESCAPE:
if (engine.isKioskMode()) { if (engine.isKioskMode()) {
@@ -109,19 +110,17 @@ bool InputHandler::processEvents(Engine& engine) {
break; break;
// Ciclar temas de color (movido de B a C) // Ciclar temas de color (movido de B a C)
case SDLK_C: case SDLK_C: {
{
// Detectar si Shift está presionado // Detectar si Shift está presionado
SDL_Keymod modstate = SDL_GetModState(); SDL_Keymod modstate = SDL_GetModState();
if (modstate & SDL_KMOD_SHIFT) { if ((modstate & SDL_KMOD_SHIFT) != 0u) {
// Shift+C: Ciclar hacia atrás (tema anterior) // Shift+C: Ciclar hacia atrás (tema anterior)
engine.cycleTheme(false); engine.cycleTheme(false);
} else { } else {
// C solo: Ciclar hacia adelante (tema siguiente) // C solo: Ciclar hacia adelante (tema siguiente)
engine.cycleTheme(true); engine.cycleTheme(true);
} }
} } break;
break;
// Temas de colores con teclado numérico (con transición suave) // Temas de colores con teclado numérico (con transición suave)
case SDLK_KP_1: case SDLK_KP_1:
@@ -233,25 +232,37 @@ bool InputHandler::processEvents(Engine& engine) {
// Controles de zoom dinámico (solo si no estamos en fullscreen) // Controles de zoom dinámico (solo si no estamos en fullscreen)
case SDLK_F1: case SDLK_F1:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.handleZoomOut(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.handleZoomOut();
}
break; break;
case SDLK_F2: case SDLK_F2:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.handleZoomIn(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.handleZoomIn();
}
break; break;
// Control de pantalla completa // Control de pantalla completa
case SDLK_F3: case SDLK_F3:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.toggleFullscreen(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.toggleFullscreen();
}
break; break;
// Modo real fullscreen (cambia resolución interna) // Modo real fullscreen (cambia resolución interna)
case SDLK_F4: case SDLK_F4:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.toggleRealFullscreen(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.toggleRealFullscreen();
}
break; break;
// Toggle PostFX activo/inactivo // Toggle PostFX activo/inactivo
@@ -266,19 +277,25 @@ bool InputHandler::processEvents(Engine& engine) {
// Redimensionar campo de juego (tamaño lógico + físico) // Redimensionar campo de juego (tamaño lógico + físico)
case SDLK_F7: case SDLK_F7:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.fieldSizeDown(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.fieldSizeDown();
}
break; break;
case SDLK_F8: case SDLK_F8:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT); if (engine.isKioskMode()) {
else engine.fieldSizeUp(); engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.fieldSizeUp();
}
break; break;
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D) // Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
case SDLK_D: case SDLK_D:
// Shift+D = Pausar tema dinámico // Shift+D = Pausar tema dinámico
if (event.key.mod & SDL_KMOD_SHIFT) { if ((event.key.mod & SDL_KMOD_SHIFT) != 0u) {
engine.pauseDynamicTheme(); engine.pauseDynamicTheme();
} else { } else {
// D sin Shift = Toggle DEMO ↔ SANDBOX // D sin Shift = Toggle DEMO ↔ SANDBOX

View File

@@ -24,7 +24,7 @@ class InputHandler {
* @param engine Referencia al engine para ejecutar acciones * @param engine Referencia al engine para ejecutar acciones
* @return true si se debe salir de la aplicación (ESC o cerrar ventana), false en caso contrario * @return true si se debe salir de la aplicación (ESC o cerrar ventana), false en caso contrario
*/ */
bool processEvents(Engine& engine); static bool processEvents(Engine& engine);
private: private:
// Sin estado interno por ahora - el InputHandler es stateless // Sin estado interno por ahora - el InputHandler es stateless

View File

@@ -1,8 +1,9 @@
#include <iostream>
#include <cstring> #include <cstring>
#include <iostream>
#include <string> #include <string>
#include "engine.hpp"
#include "defines.hpp" #include "defines.hpp"
#include "engine.hpp"
#include "resource_manager.hpp" #include "resource_manager.hpp"
// getExecutableDirectory() ya está definido en defines.h como inline // getExecutableDirectory() ya está definido en defines.h como inline
@@ -38,7 +39,7 @@ void printHelp() {
std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n"; std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n";
} }
int main(int argc, char* argv[]) { auto main(int argc, char* argv[]) -> int { // NOLINT(readability-function-cognitive-complexity)
int width = 0; int width = 0;
int height = 0; int height = 0;
int zoom = 0; int zoom = 0;
@@ -58,7 +59,8 @@ int main(int argc, char* argv[]) {
if (strcmp(argv[i], "--help") == 0) { if (strcmp(argv[i], "--help") == 0) {
printHelp(); printHelp();
return 0; return 0;
} else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) { }
if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
if (i + 1 < argc) { if (i + 1 < argc) {
width = atoi(argv[++i]); width = atoi(argv[++i]);
if (width < 320) { if (width < 320) {
@@ -189,25 +191,29 @@ int main(int argc, char* argv[]) {
Engine engine; Engine engine;
if (custom_balls > 0) if (custom_balls > 0) {
engine.setCustomScenario(custom_balls); // pre-init: asigna campos antes del benchmark engine.setCustomScenario(custom_balls); // pre-init: asigna campos antes del benchmark
}
if (max_balls_override > 0) if (max_balls_override > 0) {
engine.setMaxBallsOverride(max_balls_override); engine.setMaxBallsOverride(max_balls_override);
else if (skip_benchmark) } else if (skip_benchmark) {
engine.setSkipBenchmark(); engine.setSkipBenchmark();
}
if (initial_postfx >= 0) if (initial_postfx >= 0) {
engine.setInitialPostFX(initial_postfx); engine.setInitialPostFX(initial_postfx);
}
if (override_vignette >= 0.f || override_chroma >= 0.f) { if (override_vignette >= 0.f || override_chroma >= 0.f) {
if (initial_postfx < 0) if (initial_postfx < 0) {
engine.setInitialPostFX(0); engine.setInitialPostFX(0);
}
engine.setPostFXParamOverrides(override_vignette, override_chroma); engine.setPostFXParamOverrides(override_vignette, override_chroma);
} }
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) { if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
std::cout << "¡Error al inicializar el engine!" << std::endl; std::cout << "¡Error al inicializar el engine!" << '\n';
return -1; return -1;
} }

View File

@@ -1,15 +1,16 @@
#include "resource_manager.hpp" #include "resource_manager.hpp"
#include "resource_pack.hpp"
#include <iostream>
#include <fstream>
#include <cstring> #include <cstring>
#include <fstream>
#include <iostream>
#include "resource_pack.hpp"
// Inicializar estáticos // Inicializar estáticos
ResourcePack* ResourceManager::resourcePack_ = nullptr; ResourcePack* ResourceManager::resourcePack_ = nullptr;
std::map<std::string, std::vector<unsigned char>> ResourceManager::cache_; std::map<std::string, std::vector<unsigned char>> ResourceManager::cache_;
bool ResourceManager::init(const std::string& packFilePath) { auto ResourceManager::init(const std::string& pack_file_path) -> bool {
// Si ya estaba inicializado, liberar primero // Si ya estaba inicializado, liberar primero
if (resourcePack_ != nullptr) { if (resourcePack_ != nullptr) {
delete resourcePack_; delete resourcePack_;
@@ -18,15 +19,15 @@ bool ResourceManager::init(const std::string& packFilePath) {
// Intentar cargar el pack // Intentar cargar el pack
resourcePack_ = new ResourcePack(); resourcePack_ = new ResourcePack();
if (!resourcePack_->loadPack(packFilePath)) { if (!resourcePack_->loadPack(pack_file_path)) {
// Si falla, borrar instancia (usará fallback a disco) // Si falla, borrar instancia (usará fallback a disco)
delete resourcePack_; delete resourcePack_;
resourcePack_ = nullptr; resourcePack_ = nullptr;
std::cout << "resources.pack no encontrado - usando carpeta data/" << std::endl; std::cout << "resources.pack no encontrado - usando carpeta data/" << '\n';
return false; return false;
} }
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << std::endl; std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << '\n';
return true; return true;
} }
@@ -38,12 +39,12 @@ void ResourceManager::shutdown() {
} }
} }
bool ResourceManager::loadResource(const std::string& resourcePath, unsigned char*& data, size_t& size) { auto ResourceManager::loadResource(const std::string& resource_path, unsigned char*& data, size_t& size) -> bool {
data = nullptr; data = nullptr;
size = 0; size = 0;
// 1. Consultar caché en RAM (sin I/O) // 1. Consultar caché en RAM (sin I/O)
auto it = cache_.find(resourcePath); auto it = cache_.find(resource_path);
if (it != cache_.end()) { if (it != cache_.end()) {
size = it->second.size(); size = it->second.size();
data = new unsigned char[size]; data = new unsigned char[size];
@@ -53,20 +54,20 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
// 2. Intentar cargar desde pack (si está disponible) // 2. Intentar cargar desde pack (si está disponible)
if (resourcePack_ != nullptr) { if (resourcePack_ != nullptr) {
ResourcePack::ResourceData packData = resourcePack_->loadResource(resourcePath); ResourcePack::ResourceData pack_data = resourcePack_->loadResource(resource_path);
if (packData.data != nullptr) { if (pack_data.data != nullptr) {
cache_[resourcePath] = std::vector<unsigned char>(packData.data, packData.data + packData.size); cache_[resource_path] = std::vector<unsigned char>(pack_data.data, pack_data.data + pack_data.size);
data = packData.data; data = pack_data.data;
size = packData.size; size = pack_data.size;
return true; return true;
} }
} }
// 3. Fallback: cargar desde disco // 3. Fallback: cargar desde disco
std::ifstream file(resourcePath, std::ios::binary | std::ios::ate); std::ifstream file(resource_path, std::ios::binary | std::ios::ate);
if (!file) { if (!file) {
std::string dataPath = "data/" + resourcePath; std::string data_path = "data/" + resource_path;
file.open(dataPath, std::ios::binary | std::ios::ate); file.open(data_path, std::ios::binary | std::ios::ate);
if (!file) { return false; } if (!file) { return false; }
} }
size = static_cast<size_t>(file.tellg()); size = static_cast<size_t>(file.tellg());
@@ -76,22 +77,22 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
file.close(); file.close();
// Guardar en caché // Guardar en caché
cache_[resourcePath] = std::vector<unsigned char>(data, data + size); cache_[resource_path] = std::vector<unsigned char>(data, data + size);
return true; return true;
} }
bool ResourceManager::isPackLoaded() { auto ResourceManager::isPackLoaded() -> bool {
return resourcePack_ != nullptr; return resourcePack_ != nullptr;
} }
std::vector<std::string> ResourceManager::getResourceList() { auto ResourceManager::getResourceList() -> std::vector<std::string> {
if (resourcePack_ != nullptr) { if (resourcePack_ != nullptr) {
return resourcePack_->getResourceList(); return resourcePack_->getResourceList();
} }
return std::vector<std::string>(); // Vacío si no hay pack return {}; // Vacío si no hay pack
} }
size_t ResourceManager::getResourceCount() { auto ResourceManager::getResourceCount() -> size_t {
if (resourcePack_ != nullptr) { if (resourcePack_ != nullptr) {
return resourcePack_->getResourceCount(); return resourcePack_->getResourceCount();
} }

View File

@@ -33,7 +33,7 @@ public:
* @param packFilePath Ruta al archivo .pack (ej: "resources.pack") * @param packFilePath Ruta al archivo .pack (ej: "resources.pack")
* @return true si el pack se cargó correctamente, false si no existe (fallback a disco) * @return true si el pack se cargó correctamente, false si no existe (fallback a disco)
*/ */
static bool init(const std::string& packFilePath); static bool init(const std::string& pack_file_path);
/** /**
* Libera el sistema de recursos * Libera el sistema de recursos
@@ -49,7 +49,7 @@ public:
* @param size [out] Tamaño del buffer en bytes * @param size [out] Tamaño del buffer en bytes
* @return true si se cargó correctamente, false si falla * @return true si se cargó correctamente, false si falla
*/ */
static bool loadResource(const std::string& resourcePath, unsigned char*& data, size_t& size); static bool loadResource(const std::string& resource_path, unsigned char*& data, size_t& size);
/** /**
* Verifica si el pack está cargado * Verifica si el pack está cargado

View File

@@ -7,10 +7,8 @@
namespace fs = std::filesystem; namespace fs = std::filesystem;
// Clave XOR para ofuscación simple (puede cambiarse) ResourcePack::ResourcePack()
constexpr uint8_t XOR_KEY = 0x5A; : isLoaded_(false) {}
ResourcePack::ResourcePack() : isLoaded_(false) {}
ResourcePack::~ResourcePack() { ResourcePack::~ResourcePack() {
clear(); clear();
@@ -20,54 +18,54 @@ ResourcePack::~ResourcePack() {
// EMPAQUETADO (herramienta pack_resources) // EMPAQUETADO (herramienta pack_resources)
// ============================================================================ // ============================================================================
bool ResourcePack::addDirectory(const std::string& dirPath, const std::string& prefix) { auto ResourcePack::addDirectory(const std::string& dir_path, const std::string& prefix) -> bool {
if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) { if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "Error: Directorio no existe: " << dirPath << std::endl; std::cerr << "Error: Directorio no existe: " << dir_path << '\n';
return false; return false;
} }
for (const auto& entry : fs::recursive_directory_iterator(dirPath)) { for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (entry.is_regular_file()) { if (entry.is_regular_file()) {
// Construir ruta relativa desde data/ (ej: "data/ball.png" → "ball.png") // Construir ruta relativa desde data/ (ej: "data/ball.png" → "ball.png")
std::string relativePath = fs::relative(entry.path(), dirPath).string(); std::string relative_path = fs::relative(entry.path(), dir_path).string();
std::string fullPath = prefix.empty() ? relativePath : prefix + "/" + relativePath; std::string full_path = prefix.empty() ? relative_path : prefix + "/" + relative_path;
fullPath = normalizePath(fullPath); full_path = normalizePath(full_path);
// Leer archivo completo // Leer archivo completo
std::ifstream file(entry.path(), std::ios::binary); std::ifstream file(entry.path(), std::ios::binary);
if (!file) { if (!file) {
std::cerr << "Error: No se pudo abrir: " << entry.path() << std::endl; std::cerr << "Error: No se pudo abrir: " << entry.path() << '\n';
continue; continue;
} }
file.seekg(0, std::ios::end); file.seekg(0, std::ios::end);
size_t fileSize = file.tellg(); size_t file_size = file.tellg();
file.seekg(0, std::ios::beg); file.seekg(0, std::ios::beg);
std::vector<unsigned char> buffer(fileSize); std::vector<unsigned char> buffer(file_size);
file.read(reinterpret_cast<char*>(buffer.data()), fileSize); file.read(reinterpret_cast<char*>(buffer.data()), file_size);
file.close(); file.close();
// Crear entrada de recurso // Crear entrada de recurso
ResourceEntry resource; ResourceEntry resource;
resource.path = fullPath; resource.path = full_path;
resource.offset = 0; // Se calculará al guardar resource.offset = 0; // Se calculará al guardar
resource.size = static_cast<uint32_t>(fileSize); resource.size = static_cast<uint32_t>(file_size);
resource.checksum = calculateChecksum(buffer.data(), fileSize); resource.checksum = calculateChecksum(buffer.data(), file_size);
resources_[fullPath] = resource; resources_[full_path] = resource;
std::cout << " Añadido: " << fullPath << " (" << fileSize << " bytes)" << std::endl; std::cout << " Añadido: " << full_path << " (" << file_size << " bytes)" << '\n';
} }
} }
return !resources_.empty(); return !resources_.empty();
} }
bool ResourcePack::savePack(const std::string& packFilePath) { auto ResourcePack::savePack(const std::string& pack_file_path) -> bool {
std::ofstream packFile(packFilePath, std::ios::binary); std::ofstream pack_file(pack_file_path, std::ios::binary);
if (!packFile) { if (!pack_file) {
std::cerr << "Error: No se pudo crear pack: " << packFilePath << std::endl; std::cerr << "Error: No se pudo crear pack: " << pack_file_path << '\n';
return false; return false;
} }
@@ -76,39 +74,39 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
std::memcpy(header.magic, "VBE3", 4); std::memcpy(header.magic, "VBE3", 4);
header.version = 1; header.version = 1;
header.fileCount = static_cast<uint32_t>(resources_.size()); header.fileCount = static_cast<uint32_t>(resources_.size());
packFile.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader)); pack_file.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
// 2. Calcular offsets (después del header + índice) // 2. Calcular offsets (después del header + índice)
uint32_t currentOffset = sizeof(PackHeader); uint32_t current_offset = sizeof(PackHeader);
// Calcular tamaño del índice (cada entrada: uint32_t pathLen + path + 3*uint32_t) // Calcular tamaño del índice (cada entrada: uint32_t pathLen + path + 3*uint32_t)
for (const auto& [path, entry] : resources_) { for (const auto& [path, entry] : resources_) {
currentOffset += sizeof(uint32_t); // pathLen current_offset += sizeof(uint32_t); // pathLen
currentOffset += static_cast<uint32_t>(path.size()); // path current_offset += static_cast<uint32_t>(path.size()); // path
currentOffset += sizeof(uint32_t) * 3; // offset, size, checksum current_offset += sizeof(uint32_t) * 3; // offset, size, checksum
} }
// 3. Escribir índice // 3. Escribir índice
for (auto& [path, entry] : resources_) { for (auto& [path, entry] : resources_) {
entry.offset = currentOffset; entry.offset = current_offset;
uint32_t pathLen = static_cast<uint32_t>(path.size()); auto path_len = static_cast<uint32_t>(path.size());
packFile.write(reinterpret_cast<const char*>(&pathLen), sizeof(uint32_t)); pack_file.write(reinterpret_cast<const char*>(&path_len), sizeof(uint32_t));
packFile.write(path.c_str(), pathLen); pack_file.write(path.c_str(), path_len);
packFile.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t)); pack_file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t));
packFile.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t)); pack_file.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t));
packFile.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t)); pack_file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t));
currentOffset += entry.size; current_offset += entry.size;
} }
// 4. Escribir datos de archivos (sin encriptar en pack, se encripta al cargar) // 4. Escribir datos de archivos (sin encriptar en pack, se encripta al cargar)
for (const auto& [path, entry] : resources_) { for (const auto& [path, entry] : resources_) {
// Encontrar archivo original en disco // Encontrar archivo original en disco
fs::path originalPath = fs::current_path() / "data" / path; fs::path original_path = fs::current_path() / "data" / path;
std::ifstream file(originalPath, std::ios::binary); std::ifstream file(original_path, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "Error: No se pudo re-leer: " << originalPath << std::endl; std::cerr << "Error: No se pudo re-leer: " << original_path << '\n';
continue; continue;
} }
@@ -116,10 +114,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
file.read(reinterpret_cast<char*>(buffer.data()), entry.size); file.read(reinterpret_cast<char*>(buffer.data()), entry.size);
file.close(); file.close();
packFile.write(reinterpret_cast<const char*>(buffer.data()), entry.size); pack_file.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
} }
packFile.close(); pack_file.close();
return true; return true;
} }
@@ -127,10 +125,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
// DESEMPAQUETADO (juego) // DESEMPAQUETADO (juego)
// ============================================================================ // ============================================================================
bool ResourcePack::loadPack(const std::string& packFilePath) { auto ResourcePack::loadPack(const std::string& pack_file_path) -> bool {
clear(); clear();
packFile_.open(packFilePath, std::ios::binary); packFile_.open(pack_file_path, std::ios::binary);
if (!packFile_) { if (!packFile_) {
return false; return false;
} }
@@ -140,13 +138,13 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
packFile_.read(reinterpret_cast<char*>(&header), sizeof(PackHeader)); packFile_.read(reinterpret_cast<char*>(&header), sizeof(PackHeader));
if (std::memcmp(header.magic, "VBE3", 4) != 0) { if (std::memcmp(header.magic, "VBE3", 4) != 0) {
std::cerr << "Error: Pack inválido (magic incorrecto)" << std::endl; std::cerr << "Error: Pack inválido (magic incorrecto)" << '\n';
packFile_.close(); packFile_.close();
return false; return false;
} }
if (header.version != 1) { if (header.version != 1) {
std::cerr << "Error: Versión de pack no soportada: " << header.version << std::endl; std::cerr << "Error: Versión de pack no soportada: " << header.version << '\n';
packFile_.close(); packFile_.close();
return false; return false;
} }
@@ -155,12 +153,12 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
for (uint32_t i = 0; i < header.fileCount; i++) { for (uint32_t i = 0; i < header.fileCount; i++) {
ResourceEntry entry; ResourceEntry entry;
uint32_t pathLen; uint32_t path_len;
packFile_.read(reinterpret_cast<char*>(&pathLen), sizeof(uint32_t)); packFile_.read(reinterpret_cast<char*>(&path_len), sizeof(uint32_t));
std::vector<char> pathBuffer(pathLen + 1, '\0'); std::vector<char> path_buffer(path_len + 1, '\0');
packFile_.read(pathBuffer.data(), pathLen); packFile_.read(path_buffer.data(), path_len);
entry.path = std::string(pathBuffer.data()); entry.path = std::string(path_buffer.data());
packFile_.read(reinterpret_cast<char*>(&entry.offset), sizeof(uint32_t)); packFile_.read(reinterpret_cast<char*>(&entry.offset), sizeof(uint32_t));
packFile_.read(reinterpret_cast<char*>(&entry.size), sizeof(uint32_t)); packFile_.read(reinterpret_cast<char*>(&entry.size), sizeof(uint32_t));
@@ -173,15 +171,15 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
return true; return true;
} }
ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourcePath) { auto ResourcePack::loadResource(const std::string& resource_path) -> ResourcePack::ResourceData {
ResourceData result = {nullptr, 0}; ResourceData result = {.data = nullptr, .size = 0};
if (!isLoaded_) { if (!isLoaded_) {
return result; return result;
} }
std::string normalizedPath = normalizePath(resourcePath); std::string normalized_path = normalizePath(resource_path);
auto it = resources_.find(normalizedPath); auto it = resources_.find(normalized_path);
if (it == resources_.end()) { if (it == resources_.end()) {
return result; return result;
} }
@@ -197,7 +195,7 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
// Verificar checksum // Verificar checksum
uint32_t checksum = calculateChecksum(result.data, entry.size); uint32_t checksum = calculateChecksum(result.data, entry.size);
if (checksum != entry.checksum) { if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum incorrecto para: " << resourcePath << std::endl; std::cerr << "Warning: Checksum incorrecto para: " << resource_path << '\n';
} }
return result; return result;
@@ -207,15 +205,16 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
// UTILIDADES // UTILIDADES
// ============================================================================ // ============================================================================
std::vector<std::string> ResourcePack::getResourceList() const { auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list; std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [path, entry] : resources_) { for (const auto& [path, entry] : resources_) {
list.push_back(path); list.push_back(path);
} }
return list; return list;
} }
size_t ResourcePack::getResourceCount() const { auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size(); return resources_.size();
} }
@@ -231,7 +230,7 @@ void ResourcePack::clear() {
// FUNCIONES AUXILIARES // FUNCIONES AUXILIARES
// ============================================================================ // ============================================================================
uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size) { auto ResourcePack::calculateChecksum(const unsigned char* data, size_t size) -> uint32_t {
uint32_t checksum = 0; uint32_t checksum = 0;
for (size_t i = 0; i < size; i++) { for (size_t i = 0; i < size; i++) {
checksum ^= static_cast<uint32_t>(data[i]); checksum ^= static_cast<uint32_t>(data[i]);
@@ -240,11 +239,11 @@ uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size)
return checksum; return checksum;
} }
std::string ResourcePack::normalizePath(const std::string& path) { auto ResourcePack::normalizePath(const std::string& path) -> std::string {
std::string normalized = path; std::string normalized = path;
// Reemplazar \ por / // Reemplazar \ por /
std::replace(normalized.begin(), normalized.end(), '\\', '/'); std::ranges::replace(normalized, '\\', '/');
// Buscar "data/" en cualquier parte del path y extraer lo que viene después // Buscar "data/" en cualquier parte del path y extraer lo que viene después
size_t data_pos = normalized.find("data/"); size_t data_pos = normalized.find("data/");

View File

@@ -18,18 +18,18 @@ public:
~ResourcePack(); ~ResourcePack();
// Empaquetado (usado por herramienta pack_resources) // Empaquetado (usado por herramienta pack_resources)
bool addDirectory(const std::string& dirPath, const std::string& prefix = ""); bool addDirectory(const std::string& dir_path, const std::string& prefix = "");
bool savePack(const std::string& packFilePath); bool savePack(const std::string& pack_file_path);
// Desempaquetado (usado por el juego) // Desempaquetado (usado por el juego)
bool loadPack(const std::string& packFilePath); bool loadPack(const std::string& pack_file_path);
// Carga de recursos individuales // Carga de recursos individuales
struct ResourceData { struct ResourceData {
unsigned char* data; unsigned char* data;
size_t size; size_t size;
}; };
ResourceData loadResource(const std::string& resourcePath); ResourceData loadResource(const std::string& resource_path);
// Utilidades // Utilidades
std::vector<std::string> getResourceList() const; std::vector<std::string> getResourceList() const;
@@ -58,6 +58,6 @@ private:
bool isLoaded_; bool isLoaded_;
// Funciones auxiliares // Funciones auxiliares
uint32_t calculateChecksum(const unsigned char* data, size_t size); static uint32_t calculateChecksum(const unsigned char* data, size_t size);
std::string normalizePath(const std::string& path); static std::string normalizePath(const std::string& path);
}; };

View File

@@ -1,24 +1,25 @@
#include "scene_manager.hpp" #include "scene_manager.hpp"
#include <cstdlib> // for rand #include <cstdlib> // for rand
#include <utility>
#include "defines.hpp" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc #include "defines.hpp" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc
#include "external/texture.hpp" // for Texture #include "external/texture.hpp" // for Texture
#include "theme_manager.hpp" // for ThemeManager #include "theme_manager.hpp" // for ThemeManager
SceneManager::SceneManager(int screen_width, int screen_height) SceneManager::SceneManager(int screen_width, int screen_height)
: current_gravity_(GravityDirection::DOWN) : current_gravity_(GravityDirection::DOWN),
, scenario_(0) scenario_(0),
, screen_width_(screen_width) screen_width_(screen_width),
, screen_height_(screen_height) screen_height_(screen_height),
, current_ball_size_(10) current_ball_size_(10),
, texture_(nullptr) texture_(nullptr),
, theme_manager_(nullptr) { theme_manager_(nullptr) {
} }
void SceneManager::initialize(int scenario, std::shared_ptr<Texture> texture, ThemeManager* theme_manager) { void SceneManager::initialize(int scenario, std::shared_ptr<Texture> texture, ThemeManager* theme_manager) {
scenario_ = scenario; scenario_ = scenario;
texture_ = texture; texture_ = std::move(texture);
theme_manager_ = theme_manager; theme_manager_ = theme_manager;
current_ball_size_ = texture_->getWidth(); current_ball_size_ = texture_->getWidth();
@@ -48,28 +49,31 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
? custom_ball_count_ ? custom_ball_count_
: BALL_COUNT_SCENARIOS[scenario_id]; : BALL_COUNT_SCENARIOS[scenario_id];
for (int i = 0; i < ball_count; ++i) { for (int i = 0; i < ball_count; ++i) {
float X, Y, VX, VY; float x;
float y;
float vx;
float vy;
// Inicialización según SimulationMode (RULES.md líneas 23-26) // Inicialización según SimulationMode (RULES.md líneas 23-26)
switch (mode) { switch (mode) {
case SimulationMode::PHYSICS: { case SimulationMode::PHYSICS: {
// PHYSICS: Parte superior, 75% distribución central en X // PHYSICS: Parte superior, 75% distribución central en X
const int SIGN = ((rand() % 2) * 2) - 1; const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN); const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin); const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
X = (rand() % spawn_zone_width) + margin; x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
Y = 0.0f; // Parte superior y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN; vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f; vy = ((rand() % 60) - 30) * 0.1f;
break; break;
} }
case SimulationMode::SHAPE: { case SimulationMode::SHAPE: {
// SHAPE: Centro de pantalla, sin velocidad inicial // SHAPE: Centro de pantalla, sin velocidad inicial
X = screen_width_ / 2.0f; x = screen_width_ / 2.0f;
Y = screen_height_ / 2.0f; // Centro vertical y = screen_height_ / 2.0f; // Centro vertical
VX = 0.0f; vx = 0.0f;
VY = 0.0f; vy = 0.0f;
break; break;
} }
@@ -77,48 +81,57 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
// BOIDS: Posiciones aleatorias, velocidades aleatorias // BOIDS: Posiciones aleatorias, velocidades aleatorias
const int SIGN_X = ((rand() % 2) * 2) - 1; const int SIGN_X = ((rand() % 2) * 2) - 1;
const int SIGN_Y = ((rand() % 2) * 2) - 1; const int SIGN_Y = ((rand() % 2) * 2) - 1;
X = static_cast<float>(rand() % screen_width_); x = static_cast<float>(rand() % screen_width_);
Y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria
VX = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame vx = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame
VY = (((rand() % 40) + 10) * 0.1f) * SIGN_Y; vy = (((rand() % 40) + 10) * 0.1f) * SIGN_Y;
break; break;
} }
default: default:
// Fallback a PHYSICS por seguridad // Fallback a PHYSICS por seguridad
const int SIGN = ((rand() % 2) * 2) - 1; const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN); const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin); const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
X = (rand() % spawn_zone_width) + margin; x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
Y = 0.0f; // Parte superior y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN; vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f; vy = ((rand() % 60) - 30) * 0.1f;
break; break;
} }
// Seleccionar color de la paleta del tema actual (delegado a ThemeManager) // Seleccionar color de la paleta del tema actual (delegado a ThemeManager)
int random_index = rand(); int random_index = rand();
Color COLOR = theme_manager_->getInitialBallColor(random_index); Color color = theme_manager_->getInitialBallColor(random_index);
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada) // Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN); float mass_factor = GRAVITY_MASS_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN));
balls_.emplace_back(std::make_unique<Ball>( balls_.emplace_back(std::make_unique<Ball>(
X, Y, VX, VY, COLOR, texture_, x,
screen_width_, screen_height_, current_ball_size_, y,
current_gravity_, mass_factor vx,
)); vy,
color,
texture_,
screen_width_,
screen_height_,
current_ball_size_,
current_gravity_,
mass_factor));
} }
} }
void SceneManager::updateBallTexture(std::shared_ptr<Texture> new_texture, int new_ball_size) { void SceneManager::updateBallTexture(std::shared_ptr<Texture> new_texture, int new_ball_size) {
if (balls_.empty()) return; if (balls_.empty()) {
return;
}
// Guardar tamaño antiguo // Guardar tamaño antiguo
int old_size = current_ball_size_; int old_size = current_ball_size_;
// Actualizar textura y tamaño // Actualizar textura y tamaño
texture_ = new_texture; texture_ = std::move(new_texture);
current_ball_size_ = new_ball_size; current_ball_size_ = new_ball_size;
// Actualizar texturas de todas las pelotas // Actualizar texturas de todas las pelotas
@@ -136,7 +149,8 @@ void SceneManager::pushBallsAwayFromGravity() {
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;
float vx = 0, vy = 0; float vx = 0;
float vy = 0;
switch (current_gravity_) { switch (current_gravity_) {
case GravityDirection::DOWN: // Impulsar ARRIBA case GravityDirection::DOWN: // Impulsar ARRIBA
vx = LATERAL; vx = LATERAL;

View File

@@ -1,7 +1,10 @@
#include "atom_shape.hpp" #include "atom_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "defines.hpp"
void AtomShape::generatePoints(int num_points, float screen_width, float screen_height) { void AtomShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
nucleus_radius_ = screen_height * ATOM_NUCLEUS_RADIUS_FACTOR; nucleus_radius_ = screen_height * ATOM_NUCLEUS_RADIUS_FACTOR;
@@ -26,13 +29,13 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Calcular cuántos puntos para núcleo vs órbitas // Calcular cuántos puntos para núcleo vs órbitas
int nucleus_points = (num_points_ < 10) ? 1 : (num_points_ / 10); // 10% para núcleo int nucleus_points = (num_points_ < 10) ? 1 : (num_points_ / 10); // 10% para núcleo
if (nucleus_points < 1) nucleus_points = 1; nucleus_points = std::max(nucleus_points, 1);
// Si estamos en el núcleo // Si estamos en el núcleo
if (index < nucleus_points) { if (index < nucleus_points) {
// Distribuir puntos en esfera pequeña (núcleo) // Distribuir puntos en esfera pequeña (núcleo)
float t = static_cast<float>(index) / static_cast<float>(nucleus_points); float t = static_cast<float>(index) / static_cast<float>(nucleus_points);
float phi = acosf(1.0f - 2.0f * t); float phi = acosf(1.0f - (2.0f * t));
float theta = PI * 2.0f * t * 3.61803398875f; // Golden ratio float theta = PI * 2.0f * t * 3.61803398875f; // Golden ratio
float x_nuc = nucleus_radius_ * cosf(theta) * sinf(phi); float x_nuc = nucleus_radius_ * cosf(theta) * sinf(phi);
@@ -51,10 +54,12 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Puntos restantes: distribuir en órbitas // Puntos restantes: distribuir en órbitas
int orbit_points = num_points_ - nucleus_points; int orbit_points = num_points_ - nucleus_points;
int points_per_orbit = orbit_points / num_orbits; int points_per_orbit = orbit_points / num_orbits;
if (points_per_orbit < 1) points_per_orbit = 1; points_per_orbit = std::max(points_per_orbit, 1);
int orbit_index = (index - nucleus_points) / points_per_orbit; int orbit_index = (index - nucleus_points) / points_per_orbit;
if (orbit_index >= num_orbits) orbit_index = num_orbits - 1; if (orbit_index >= num_orbits) {
orbit_index = num_orbits - 1;
}
int point_in_orbit = (index - nucleus_points) % points_per_orbit; int point_in_orbit = (index - nucleus_points) % points_per_orbit;
@@ -73,21 +78,21 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Inclinar el plano orbital (rotación en eje X local) // Inclinar el plano orbital (rotación en eje X local)
float cos_tilt = cosf(orbit_tilt); float cos_tilt = cosf(orbit_tilt);
float sin_tilt = sinf(orbit_tilt); float sin_tilt = sinf(orbit_tilt);
float y_tilted = y_local * cos_tilt - z_local * sin_tilt; float y_tilted = (y_local * cos_tilt) - (z_local * sin_tilt);
float z_tilted = y_local * sin_tilt + z_local * cos_tilt; float z_tilted = (y_local * sin_tilt) + (z_local * cos_tilt);
// Aplicar rotación global del átomo (eje Y) // Aplicar rotación global del átomo (eje Y)
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot = x_local * cos_y - z_tilted * sin_y; float x_rot = (x_local * cos_y) - (z_tilted * sin_y);
float z_rot = x_local * sin_y + z_tilted * cos_y; float z_rot = (x_local * sin_y) + (z_tilted * cos_y);
x = x_rot; x = x_rot;
y = y_tilted; y = y_tilted;
z = z_rot; z = z_rot;
} }
float AtomShape::getScaleFactor(float screen_height) const { auto AtomShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio de órbita // Factor de escala para física: proporcional al radio de órbita
// Radio órbita base = 72px (0.30 * 240px en resolución 320x240) // Radio órbita base = 72px (0.30 * 240px en resolución 320x240)
const float BASE_RADIUS = 72.0f; const float BASE_RADIUS = 72.0f;

View File

@@ -1,7 +1,10 @@
#include "cube_shape.hpp" #include "cube_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "defines.hpp"
void CubeShape::generatePoints(int num_points, float screen_width, float screen_height) { void CubeShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
size_ = screen_height * CUBE_SIZE_FACTOR; size_ = screen_height * CUBE_SIZE_FACTOR;
@@ -52,23 +55,23 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Z // Aplicar rotación en eje Z
float cos_z = cosf(angle_z_); float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_); float sin_z = sinf(angle_z_);
float x_rot_z = x_base * cos_z - y_base * sin_z; float x_rot_z = (x_base * cos_z) - (y_base * sin_z);
float y_rot_z = x_base * sin_z + y_base * cos_z; float y_rot_z = (x_base * sin_z) + (y_base * cos_z);
float z_rot_z = z_base; float z_rot_z = z_base;
// Aplicar rotación en eje Y // Aplicar rotación en eje Y
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot_y = x_rot_z * cos_y + z_rot_z * sin_y; float x_rot_y = (x_rot_z * cos_y) + (z_rot_z * sin_y);
float y_rot_y = y_rot_z; float y_rot_y = y_rot_z;
float z_rot_y = -x_rot_z * sin_y + z_rot_z * cos_y; float z_rot_y = (-x_rot_z * sin_y) + (z_rot_z * cos_y);
// Aplicar rotación en eje X // Aplicar rotación en eje X
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float x_final = x_rot_y; float x_final = x_rot_y;
float y_final = y_rot_y * cos_x - z_rot_y * sin_x; float y_final = (y_rot_y * cos_x) - (z_rot_y * sin_x);
float z_final = y_rot_y * sin_x + z_rot_y * cos_x; float z_final = (y_rot_y * sin_x) + (z_rot_y * cos_x);
// Retornar coordenadas finales rotadas // Retornar coordenadas finales rotadas
x = x_final; x = x_final;
@@ -76,7 +79,7 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final; z = z_final;
} }
float CubeShape::getScaleFactor(float screen_height) const { auto CubeShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al tamaño del cubo // Factor de escala para física: proporcional al tamaño del cubo
// Tamaño base = 60px (resolución 320x240, factor 0.25) // Tamaño base = 60px (resolución 320x240, factor 0.25)
const float BASE_SIZE = 60.0f; const float BASE_SIZE = 60.0f;
@@ -105,12 +108,24 @@ void CubeShape::generateVerticesAndCenters() {
// 2. Añadir 6 centros de caras // 2. Añadir 6 centros de caras
// Caras: X=±size (Y,Z varían), Y=±size (X,Z varían), Z=±size (X,Y varían) // Caras: X=±size (Y,Z varían), Y=±size (X,Z varían), Z=±size (X,Y varían)
base_x_.push_back(size_); base_y_.push_back(0); base_z_.push_back(0); // +X base_x_.push_back(size_);
base_x_.push_back(-size_); base_y_.push_back(0); base_z_.push_back(0); // -X base_y_.push_back(0);
base_x_.push_back(0); base_y_.push_back(size_); base_z_.push_back(0); // +Y base_z_.push_back(0); // +X
base_x_.push_back(0); base_y_.push_back(-size_);base_z_.push_back(0); // -Y base_x_.push_back(-size_);
base_x_.push_back(0); base_y_.push_back(0); base_z_.push_back(size_); // +Z base_y_.push_back(0);
base_x_.push_back(0); base_y_.push_back(0); base_z_.push_back(-size_); // -Z base_z_.push_back(0); // -X
base_x_.push_back(0);
base_y_.push_back(size_);
base_z_.push_back(0); // +Y
base_x_.push_back(0);
base_y_.push_back(-size_);
base_z_.push_back(0); // -Y
base_x_.push_back(0);
base_y_.push_back(0);
base_z_.push_back(size_); // +Z
base_x_.push_back(0);
base_y_.push_back(0);
base_z_.push_back(-size_); // -Z
// 3. Añadir 12 centros de aristas // 3. Añadir 12 centros de aristas
// Aristas paralelas a X (4), Y (4), Z (4) // Aristas paralelas a X (4), Y (4), Z (4)
@@ -143,16 +158,16 @@ void CubeShape::generateVerticesAndCenters() {
void CubeShape::generateVolumetricGrid() { void CubeShape::generateVolumetricGrid() {
// Calcular dimensión del grid cúbico: N³ ≈ num_points // Calcular dimensión del grid cúbico: N³ ≈ num_points
int grid_dim = static_cast<int>(ceilf(cbrtf(static_cast<float>(num_points_)))); int grid_dim = static_cast<int>(ceilf(cbrtf(static_cast<float>(num_points_))));
if (grid_dim < 3) grid_dim = 3; // Mínimo grid 3x3x3 grid_dim = std::max(grid_dim, 3); // Mínimo grid 3x3x3
float step = (2.0f * size_) / (grid_dim - 1); // Espacio entre puntos float step = (2.0f * size_) / (grid_dim - 1); // Espacio entre puntos
for (int ix = 0; ix < grid_dim; ix++) { for (int ix = 0; ix < grid_dim; ix++) {
for (int iy = 0; iy < grid_dim; iy++) { for (int iy = 0; iy < grid_dim; iy++) {
for (int iz = 0; iz < grid_dim; iz++) { for (int iz = 0; iz < grid_dim; iz++) {
float x = -size_ + ix * step; float x = -size_ + (ix * step);
float y = -size_ + iy * step; float y = -size_ + (iy * step);
float z = -size_ + iz * step; float z = -size_ + (iz * step);
base_x_.push_back(x); base_x_.push_back(x);
base_y_.push_back(y); base_y_.push_back(y);

View File

@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "shape.hpp"
#include <vector> #include <vector>
#include "shape.hpp"
// Figura: Cubo 3D rotante // Figura: Cubo 3D rotante
// Distribución: // Distribución:
// - 1-8 pelotas: Solo vértices (8 puntos) // - 1-8 pelotas: Solo vértices (8 puntos)

View File

@@ -1,8 +1,11 @@
#include "cylinder_shape.hpp" #include "cylinder_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdlib> // Para rand() #include <cstdlib> // Para rand()
#include "defines.hpp"
void CylinderShape::generatePoints(int num_points, float screen_width, float screen_height) { void CylinderShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
radius_ = screen_height * CYLINDER_RADIUS_FACTOR; radius_ = screen_height * CYLINDER_RADIUS_FACTOR;
@@ -37,7 +40,7 @@ void CylinderShape::update(float delta_time, float screen_width, float screen_he
float t = tumble_progress; float t = tumble_progress;
float ease = t < 0.5f float ease = t < 0.5f
? 2.0f * t * t ? 2.0f * t * t
: 1.0f - (-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f; : 1.0f - ((-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f);
angle_x_ = ease * tumble_target_; angle_x_ = ease * tumble_target_;
} }
} else { } else {
@@ -58,10 +61,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Calcular número de anillos (altura) y puntos por anillo (circunferencia) // Calcular número de anillos (altura) y puntos por anillo (circunferencia)
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f)); int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
if (num_rings < 2) num_rings = 2; num_rings = std::max(num_rings, 2);
int points_per_ring = num_points_ / num_rings; int points_per_ring = num_points_ / num_rings;
if (points_per_ring < 3) points_per_ring = 3; points_per_ring = std::max(points_per_ring, 3);
// Obtener parámetros u (ángulo) y v (altura) del índice // Obtener parámetros u (ángulo) y v (altura) del índice
int ring = index / points_per_ring; int ring = index / points_per_ring;
@@ -80,8 +83,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
float u = (static_cast<float>(point_in_ring) / static_cast<float>(points_per_ring)) * 2.0f * PI; float u = (static_cast<float>(point_in_ring) / static_cast<float>(points_per_ring)) * 2.0f * PI;
// Parámetro v (altura normalizada): [-1, 1] // Parámetro v (altura normalizada): [-1, 1]
float v = (static_cast<float>(ring) / static_cast<float>(num_rings - 1)) * 2.0f - 1.0f; float v = ((static_cast<float>(ring) / static_cast<float>(num_rings - 1)) * 2.0f) - 1.0f;
if (num_rings == 1) v = 0.0f; if (num_rings == 1) {
v = 0.0f;
}
// Ecuaciones paramétricas del cilindro // Ecuaciones paramétricas del cilindro
// x = radius * cos(u) // x = radius * cos(u)
@@ -94,14 +99,14 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y (principal, siempre activa) // Aplicar rotación en eje Y (principal, siempre activa)
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot_y = x_base * cos_y - z_base * sin_y; float x_rot_y = (x_base * cos_y) - (z_base * sin_y);
float z_rot_y = x_base * sin_y + z_base * cos_y; float z_rot_y = (x_base * sin_y) + (z_base * cos_y);
// Aplicar rotación en eje X (tumbling ocasional) // Aplicar rotación en eje X (tumbling ocasional)
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float y_rot = y_base * cos_x - z_rot_y * sin_x; float y_rot = (y_base * cos_x) - (z_rot_y * sin_x);
float z_rot = y_base * sin_x + z_rot_y * cos_x; float z_rot = (y_base * sin_x) + (z_rot_y * cos_x);
// Retornar coordenadas finales con ambas rotaciones // Retornar coordenadas finales con ambas rotaciones
x = x_rot_y; x = x_rot_y;
@@ -109,7 +114,7 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot; z = z_rot;
} }
float CylinderShape::getScaleFactor(float screen_height) const { auto CylinderShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la dimensión mayor (altura) // Factor de escala para física: proporcional a la dimensión mayor (altura)
// Altura base = 120px (0.5 * 240px en resolución 320x240) // Altura base = 120px (0.5 * 240px en resolución 320x240)
const float BASE_HEIGHT = 120.0f; const float BASE_HEIGHT = 120.0f;

View File

@@ -1,7 +1,9 @@
#include "helix_shape.hpp" #include "helix_shape.hpp"
#include "defines.hpp"
#include <cmath> #include <cmath>
#include "defines.hpp"
void HelixShape::generatePoints(int num_points, float screen_width, float screen_height) { void HelixShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
radius_ = screen_height * HELIX_RADIUS_FACTOR; radius_ = screen_height * HELIX_RADIUS_FACTOR;
@@ -41,8 +43,8 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y (horizontal) // Aplicar rotación en eje Y (horizontal)
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot = x_base * cos_y - z_base * sin_y; float x_rot = (x_base * cos_y) - (z_base * sin_y);
float z_rot = x_base * sin_y + z_base * cos_y; float z_rot = (x_base * sin_y) + (z_base * cos_y);
// Retornar coordenadas finales // Retornar coordenadas finales
x = x_rot; x = x_rot;
@@ -50,7 +52,7 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot; z = z_rot;
} }
float HelixShape::getScaleFactor(float screen_height) const { auto HelixShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la dimensión mayor (altura total) // Factor de escala para física: proporcional a la dimensión mayor (altura total)
// Altura base = 180px para 3 vueltas con pitch=0.25 en 240px de altura (180 = 240 * 0.25 * 3) // Altura base = 180px para 3 vueltas con pitch=0.25 en 240px de altura (180 = 240 * 0.25 * 3)
const float BASE_HEIGHT = 180.0f; const float BASE_HEIGHT = 180.0f;

View File

@@ -1,8 +1,12 @@
#include "icosahedron_shape.hpp" #include "icosahedron_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <vector> #include <vector>
#include "defines.hpp"
void IcosahedronShape::generatePoints(int num_points, float screen_width, float screen_height) { void IcosahedronShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
radius_ = screen_height * ICOSAHEDRON_RADIUS_FACTOR; radius_ = screen_height * ICOSAHEDRON_RADIUS_FACTOR;
@@ -21,37 +25,36 @@ void IcosahedronShape::update(float delta_time, float screen_width, float screen
void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const { void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Proporción áurea (golden ratio) // Proporción áurea (golden ratio)
const float phi = (1.0f + sqrtf(5.0f)) / 2.0f; const float PHI = (1.0f + sqrtf(5.0f)) / 2.0f;
// 12 vértices del icosaedro regular normalizado // 12 vértices del icosaedro regular normalizado
// Basados en 3 rectángulos áureos ortogonales // Basados en 3 rectángulos áureos ortogonales
static const float vertices[12][3] = { const std::array<std::array<float, 3>, 12> VERTICES = {{
// Rectángulo XY // Rectángulo XY
{-1.0f, phi, 0.0f}, {-1.0f, PHI, 0.0f},
{ 1.0f, phi, 0.0f}, {1.0f, PHI, 0.0f},
{-1.0f, -phi, 0.0f}, {-1.0f, -PHI, 0.0f},
{ 1.0f, -phi, 0.0f}, {1.0f, -PHI, 0.0f},
// Rectángulo YZ // Rectángulo YZ
{ 0.0f, -1.0f, phi}, {0.0f, -1.0f, PHI},
{ 0.0f, 1.0f, phi}, {0.0f, 1.0f, PHI},
{ 0.0f, -1.0f, -phi}, {0.0f, -1.0f, -PHI},
{ 0.0f, 1.0f, -phi}, {0.0f, 1.0f, -PHI},
// Rectángulo ZX // Rectángulo ZX
{ phi, 0.0f, -1.0f}, {PHI, 0.0f, -1.0f},
{ phi, 0.0f, 1.0f}, {PHI, 0.0f, 1.0f},
{-phi, 0.0f, -1.0f}, {-PHI, 0.0f, -1.0f},
{-phi, 0.0f, 1.0f} {-PHI, 0.0f, 1.0f}}};
};
// Normalizar para esfera circunscrita // Normalizar para esfera circunscrita
const float normalization = sqrtf(1.0f + phi * phi); const float NORMALIZATION = sqrtf(1.0f + (PHI * PHI));
// Si tenemos 12 o menos puntos, usar solo vértices // Si tenemos 12 o menos puntos, usar solo vértices
if (num_points_ <= 12) { if (num_points_ <= 12) {
int vertex_index = index % 12; int vertex_index = index % 12;
float x_base = vertices[vertex_index][0] / normalization * radius_; float x_base = VERTICES[vertex_index][0] / NORMALIZATION * radius_;
float y_base = vertices[vertex_index][1] / normalization * radius_; float y_base = VERTICES[vertex_index][1] / NORMALIZATION * radius_;
float z_base = vertices[vertex_index][2] / normalization * radius_; float z_base = VERTICES[vertex_index][2] / NORMALIZATION * radius_;
// Aplicar rotaciones // Aplicar rotaciones
applyRotations(x_base, y_base, z_base, x, y, z); applyRotations(x_base, y_base, z_base, x, y, z);
@@ -62,9 +65,9 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
// Distribuir puntos entre vértices (primero) y caras (después) // Distribuir puntos entre vértices (primero) y caras (después)
if (index < 12) { if (index < 12) {
// Primeros 12 puntos: vértices del icosaedro // Primeros 12 puntos: vértices del icosaedro
float x_base = vertices[index][0] / normalization * radius_; float x_base = VERTICES[index][0] / NORMALIZATION * radius_;
float y_base = vertices[index][1] / normalization * radius_; float y_base = VERTICES[index][1] / NORMALIZATION * radius_;
float z_base = vertices[index][2] / normalization * radius_; float z_base = VERTICES[index][2] / NORMALIZATION * radius_;
applyRotations(x_base, y_base, z_base, x, y, z); applyRotations(x_base, y_base, z_base, x, y, z);
return; return;
} }
@@ -73,38 +76,55 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
// El icosaedro tiene 20 caras triangulares // El icosaedro tiene 20 caras triangulares
int remaining_points = index - 12; int remaining_points = index - 12;
int points_per_face = (num_points_ - 12) / 20; int points_per_face = (num_points_ - 12) / 20;
if (points_per_face < 1) points_per_face = 1; points_per_face = std::max(points_per_face, 1);
int face_index = remaining_points / points_per_face; int face_index = remaining_points / points_per_face;
if (face_index >= 20) face_index = 19; if (face_index >= 20) {
face_index = 19;
}
int point_in_face = remaining_points % points_per_face; int point_in_face = remaining_points % points_per_face;
// Definir algunas caras del icosaedro (usando índices de vértices) // Definir algunas caras del icosaedro (usando índices de vértices)
// Solo necesitamos generar puntos, no renderizar caras completas // Solo necesitamos generar puntos, no renderizar caras completas
static const int faces[20][3] = { static constexpr std::array<std::array<int, 3>, 20> FACES = {{
{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11}, {0, 11, 5},
{1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8}, {0, 5, 1},
{3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9}, {0, 1, 7},
{4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1} {0, 7, 10},
}; {0, 10, 11},
{1, 5, 9},
{5, 11, 4},
{11, 10, 2},
{10, 7, 6},
{7, 1, 8},
{3, 9, 4},
{3, 4, 2},
{3, 2, 6},
{3, 6, 8},
{3, 8, 9},
{4, 9, 5},
{2, 4, 11},
{6, 2, 10},
{8, 6, 7},
{9, 8, 1}}};
// Obtener vértices de la cara // Obtener vértices de la cara
int v0 = faces[face_index][0]; int v0 = FACES[face_index][0];
int v1 = faces[face_index][1]; int v1 = FACES[face_index][1];
int v2 = faces[face_index][2]; int v2 = FACES[face_index][2];
// Interpolar dentro del triángulo usando coordenadas baricéntricas simples // Interpolar dentro del triángulo usando coordenadas baricéntricas simples
float t = static_cast<float>(point_in_face) / static_cast<float>(points_per_face + 1); float t = static_cast<float>(point_in_face) / static_cast<float>(points_per_face + 1);
float u = sqrtf(t); float u = sqrtf(t);
float v = t - u; float v = t - u;
float x_interp = vertices[v0][0] * (1.0f - u - v) + vertices[v1][0] * u + vertices[v2][0] * v; float x_interp = (VERTICES[v0][0] * (1.0f - u - v)) + (VERTICES[v1][0] * u) + (VERTICES[v2][0] * v);
float y_interp = vertices[v0][1] * (1.0f - u - v) + vertices[v1][1] * u + vertices[v2][1] * v; float y_interp = (VERTICES[v0][1] * (1.0f - u - v)) + (VERTICES[v1][1] * u) + (VERTICES[v2][1] * v);
float z_interp = vertices[v0][2] * (1.0f - u - v) + vertices[v1][2] * u + vertices[v2][2] * v; float z_interp = (VERTICES[v0][2] * (1.0f - u - v)) + (VERTICES[v1][2] * u) + (VERTICES[v2][2] * v);
// Proyectar a la esfera // Proyectar a la esfera
float len = sqrtf(x_interp * x_interp + y_interp * y_interp + z_interp * z_interp); float len = sqrtf((x_interp * x_interp) + (y_interp * y_interp) + (z_interp * z_interp));
if (len > 0.0001f) { if (len > 0.0001f) {
x_interp /= len; x_interp /= len;
y_interp /= len; y_interp /= len;
@@ -122,27 +142,27 @@ void IcosahedronShape::applyRotations(float x_in, float y_in, float z_in, float&
// Aplicar rotación en eje X // Aplicar rotación en eje X
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float y_rot_x = y_in * cos_x - z_in * sin_x; float y_rot_x = (y_in * cos_x) - (z_in * sin_x);
float z_rot_x = y_in * sin_x + z_in * cos_x; float z_rot_x = (y_in * sin_x) + (z_in * cos_x);
// Aplicar rotación en eje Y // Aplicar rotación en eje Y
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot_y = x_in * cos_y - z_rot_x * sin_y; float x_rot_y = (x_in * cos_y) - (z_rot_x * sin_y);
float z_rot_y = x_in * sin_y + z_rot_x * cos_y; float z_rot_y = (x_in * sin_y) + (z_rot_x * cos_y);
// Aplicar rotación en eje Z // Aplicar rotación en eje Z
float cos_z = cosf(angle_z_); float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_); float sin_z = sinf(angle_z_);
float x_final = x_rot_y * cos_z - y_rot_x * sin_z; float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
float y_final = x_rot_y * sin_z + y_rot_x * cos_z; float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
x_out = x_final; x_out = x_final;
y_out = y_final; y_out = y_final;
z_out = z_rot_y; z_out = z_rot_y;
} }
float IcosahedronShape::getScaleFactor(float screen_height) const { auto IcosahedronShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio // Factor de escala para física: proporcional al radio
// Radio base = 72px (0.30 * 240px en resolución 320x240) // Radio base = 72px (0.30 * 240px en resolución 320x240)
const float BASE_RADIUS = 72.0f; const float BASE_RADIUS = 72.0f;

View File

@@ -1,7 +1,9 @@
#include "lissajous_shape.hpp" #include "lissajous_shape.hpp"
#include "defines.hpp"
#include <cmath> #include <cmath>
#include "defines.hpp"
void LissajousShape::generatePoints(int num_points, float screen_width, float screen_height) { void LissajousShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
amplitude_ = screen_height * LISSAJOUS_SIZE_FACTOR; amplitude_ = screen_height * LISSAJOUS_SIZE_FACTOR;
@@ -33,21 +35,21 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
// x(t) = A * sin(freq_x * t + phase_x) // x(t) = A * sin(freq_x * t + phase_x)
// y(t) = A * sin(freq_y * t) // y(t) = A * sin(freq_y * t)
// z(t) = A * sin(freq_z * t + phase_z) // z(t) = A * sin(freq_z * t + phase_z)
float x_local = amplitude_ * sinf(freq_x_ * t + phase_x_); float x_local = amplitude_ * sinf((freq_x_ * t) + phase_x_);
float y_local = amplitude_ * sinf(freq_y_ * t); float y_local = amplitude_ * sinf(freq_y_ * t);
float z_local = amplitude_ * sinf(freq_z_ * t + phase_z_); float z_local = amplitude_ * sinf((freq_z_ * t) + phase_z_);
// Aplicar rotación global en eje X // Aplicar rotación global en eje X
float cos_x = cosf(rotation_x_); float cos_x = cosf(rotation_x_);
float sin_x = sinf(rotation_x_); float sin_x = sinf(rotation_x_);
float y_rot = y_local * cos_x - z_local * sin_x; float y_rot = (y_local * cos_x) - (z_local * sin_x);
float z_rot = y_local * sin_x + z_local * cos_x; float z_rot = (y_local * sin_x) + (z_local * cos_x);
// Aplicar rotación global en eje Y // Aplicar rotación global en eje Y
float cos_y = cosf(rotation_y_); float cos_y = cosf(rotation_y_);
float sin_y = sinf(rotation_y_); float sin_y = sinf(rotation_y_);
float x_final = x_local * cos_y - z_rot * sin_y; float x_final = (x_local * cos_y) - (z_rot * sin_y);
float z_final = x_local * sin_y + z_rot * cos_y; float z_final = (x_local * sin_y) + (z_rot * cos_y);
// Retornar coordenadas rotadas // Retornar coordenadas rotadas
x = x_final; x = x_final;
@@ -55,7 +57,7 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final; z = z_final;
} }
float LissajousShape::getScaleFactor(float screen_height) const { auto LissajousShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la amplitud de la curva // Factor de escala para física: proporcional a la amplitud de la curva
// Amplitud base = 84px (0.35 * 240px en resolución 320x240) // Amplitud base = 84px (0.35 * 240px en resolución 320x240)
const float BASE_SIZE = 84.0f; const float BASE_SIZE = 84.0f;

View File

@@ -1,16 +1,18 @@
#include "png_shape.hpp" #include "png_shape.hpp"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <map>
#include "defines.hpp" #include "defines.hpp"
#include "external/stb_image.h" #include "external/stb_image.h"
#include "resource_manager.hpp" #include "resource_manager.hpp"
#include <cmath>
#include <algorithm>
#include <iostream>
#include <map>
PNGShape::PNGShape(const char* png_path) { PNGShape::PNGShape(const char* png_path) {
// Cargar PNG desde path // Cargar PNG desde path
if (!loadPNG(png_path)) { if (!loadPNG(png_path)) {
std::cerr << "[PNGShape] Usando fallback 10x10" << std::endl; std::cerr << "[PNGShape] Usando fallback 10x10" << '\n';
// Fallback: generar un cuadrado simple si falla la carga // Fallback: generar un cuadrado simple si falla la carga
image_width_ = 10; image_width_ = 10;
image_height_ = 10; image_height_ = 10;
@@ -21,7 +23,7 @@ PNGShape::PNGShape(const char* png_path) {
next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN); next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
} }
bool PNGShape::loadPNG(const char* resource_key) { auto PNGShape::loadPNG(const char* resource_key) -> bool {
{ {
std::string fn = std::string(resource_key); std::string fn = std::string(resource_key);
fn = fn.substr(fn.find_last_of("\\/") + 1); fn = fn.substr(fn.find_last_of("\\/") + 1);
@@ -30,15 +32,16 @@ bool PNGShape::loadPNG(const char* resource_key) {
unsigned char* file_data = nullptr; unsigned char* file_data = nullptr;
size_t file_size = 0; size_t file_size = 0;
if (!ResourceManager::loadResource(resource_key, file_data, file_size)) { if (!ResourceManager::loadResource(resource_key, file_data, file_size)) {
std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << std::endl; std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << '\n';
return false; return false;
} }
int width, height, channels; int width;
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size), int height;
&width, &height, &channels, 1); int channels;
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size), &width, &height, &channels, 1);
delete[] file_data; delete[] file_data;
if (!pixels) { if (pixels == nullptr) {
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << std::endl; std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << '\n';
return false; return false;
} }
image_width_ = width; image_width_ = width;
@@ -57,9 +60,11 @@ void PNGShape::detectEdges() {
// Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro) // Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro)
for (int y = 0; y < image_height_; y++) { for (int y = 0; y < image_height_; y++) {
for (int x = 0; x < image_width_; x++) { for (int x = 0; x < image_width_; x++) {
int idx = y * image_width_ + x; int idx = (y * image_width_) + x;
if (!pixel_data_[idx]) continue; // Solo píxeles blancos if (!pixel_data_[idx]) {
continue; // Solo píxeles blancos
}
// Verificar vecinos (arriba, abajo, izq, der) // Verificar vecinos (arriba, abajo, izq, der)
bool is_edge = false; bool is_edge = false;
@@ -90,7 +95,7 @@ void PNGShape::floodFill() {
for (int y = 0; y < image_height_; y++) { for (int y = 0; y < image_height_; y++) {
for (int x = 0; x < image_width_; x++) { for (int x = 0; x < image_width_; x++) {
int idx = y * image_width_ + x; int idx = (y * image_width_) + x;
if (pixel_data_[idx]) { if (pixel_data_[idx]) {
filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)}); filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
} }
@@ -123,7 +128,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
// Conjunto de puntos ACTIVO (será modificado por filtros) // Conjunto de puntos ACTIVO (será modificado por filtros)
std::vector<Point2D> active_points_data; std::vector<Point2D> active_points_data;
std::string mode_name = ""; std::string mode_name;
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA === // === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
// Estrategia: Optimizar según número de pelotas disponibles // Estrategia: Optimizar según número de pelotas disponibles
@@ -196,8 +201,6 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices); std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
if (!vertices.empty() && vertices.size() < active_points_data.size()) { if (!vertices.empty() && vertices.size() < active_points_data.size()) {
active_points_data = vertices; active_points_data = vertices;
num_2d_points = active_points_data.size();
total_3d_points = num_2d_points * num_layers_;
mode_name = "VÉRTICES"; mode_name = "VÉRTICES";
} }
} }
@@ -216,7 +219,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros) // Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
// Recibe vector original y devuelve nuevo vector filtrado // Recibe vector original y devuelve nuevo vector filtrado
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) { auto PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) -> std::vector<PNGShape::Point2D> {
std::vector<Point2D> result; std::vector<Point2D> result;
if (row_skip <= 1 || source.empty()) { if (row_skip <= 1 || source.empty()) {
@@ -243,7 +246,7 @@ std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<
} }
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector) // Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) { auto PNGShape::extractCornerVertices(const std::vector<Point2D>& source) -> std::vector<PNGShape::Point2D> {
std::vector<Point2D> result; std::vector<Point2D> result;
if (source.empty()) { if (source.empty()) {
@@ -386,14 +389,14 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y (horizontal) // Aplicar rotación en eje Y (horizontal)
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot_y = x_base * cos_y - z_base * sin_y; float x_rot_y = (x_base * cos_y) - (z_base * sin_y);
float z_rot_y = x_base * sin_y + z_base * cos_y; float z_rot_y = (x_base * sin_y) + (z_base * cos_y);
// Aplicar rotación en eje X (vertical) // Aplicar rotación en eje X (vertical)
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float y_rot = y_base * cos_x - z_rot_y * sin_x; float y_rot = (y_base * cos_x) - (z_rot_y * sin_x);
float z_rot = y_base * sin_x + z_rot_y * cos_x; float z_rot = (y_base * sin_x) + (z_rot_y * cos_x);
// Retornar coordenadas finales // Retornar coordenadas finales
x = x_rot_y; x = x_rot_y;
@@ -408,7 +411,7 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
} }
} }
float PNGShape::getScaleFactor(float screen_height) const { auto PNGShape::getScaleFactor(float screen_height) const -> float {
// Escala dinámica según resolución // Escala dinámica según resolución
return PNG_SIZE_FACTOR; return PNG_SIZE_FACTOR;
} }
@@ -432,7 +435,7 @@ void PNGShape::setConvergence(float convergence) {
} }
// Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip) // Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip)
float PNGShape::getFlipProgress() const { auto PNGShape::getFlipProgress() const -> float {
if (!is_flipping_) { if (!is_flipping_) {
return 0.0f; // No está flipping, progreso = 0 return 0.0f; // No está flipping, progreso = 0
} }

View File

@@ -1,9 +1,10 @@
#pragma once #pragma once
#include "shape.hpp"
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
#include <vector>
#include <cstdlib> // Para rand() #include <cstdlib> // Para rand()
#include <vector>
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
#include "shape.hpp"
// Figura: Shape generada desde PNG 1-bit (blanco sobre negro) // Figura: Shape generada desde PNG 1-bit (blanco sobre negro)
// Enfoque A: Extrusión 2D (implementado) // Enfoque A: Extrusión 2D (implementado)
@@ -59,14 +60,14 @@ private:
int num_points_ = 0; // Total de puntos generados (para indexación) int num_points_ = 0; // Total de puntos generados (para indexación)
// Métodos internos // Métodos internos
bool loadPNG(const char* path); // Cargar PNG con stb_image bool loadPNG(const char* resource_key); // Cargar PNG con stb_image
void detectEdges(); // Detectar contorno (Enfoque A) void detectEdges(); // Detectar contorno (Enfoque A)
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) // 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 static 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 static 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
@@ -88,7 +89,10 @@ public:
int getFlipCount() const { return flip_count_; } int getFlipCount() const { return flip_count_; }
// Resetear contador de flips (llamar al entrar a LOGO MODE) // Resetear contador de flips (llamar al entrar a LOGO MODE)
void resetFlipCount() { flip_count_ = 0; was_flipping_last_frame_ = false; } void resetFlipCount() {
flip_count_ = 0;
was_flipping_last_frame_ = false;
}
// Control de modo LOGO (flip intervals más largos) // Control de modo LOGO (flip intervals más largos)
void setLogoMode(bool enable) { void setLogoMode(bool enable) {

View File

@@ -1,7 +1,9 @@
#include "sphere_shape.hpp" #include "sphere_shape.hpp"
#include "defines.hpp"
#include <cmath> #include <cmath>
#include "defines.hpp"
void SphereShape::generatePoints(int num_points, float screen_width, float screen_height) { void SphereShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
radius_ = screen_height * ROTOBALL_RADIUS_FACTOR; radius_ = screen_height * ROTOBALL_RADIUS_FACTOR;
@@ -19,12 +21,12 @@ void SphereShape::update(float delta_time, float screen_width, float screen_heig
void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const { void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Algoritmo Fibonacci Sphere para distribución uniforme // Algoritmo Fibonacci Sphere para distribución uniforme
const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f; const float GOLDEN_RATIO = (1.0f + sqrtf(5.0f)) / 2.0f;
const float angle_increment = PI * 2.0f * golden_ratio; const float ANGLE_INCREMENT = PI * 2.0f * GOLDEN_RATIO;
float t = static_cast<float>(index) / static_cast<float>(num_points_); float t = static_cast<float>(index) / static_cast<float>(num_points_);
float phi = acosf(1.0f - 2.0f * t); // Latitud float phi = acosf(1.0f - (2.0f * t)); // Latitud
float theta = angle_increment * static_cast<float>(index); // Longitud float theta = ANGLE_INCREMENT * static_cast<float>(index); // Longitud
// Convertir coordenadas esféricas a cartesianas // Convertir coordenadas esféricas a cartesianas
float x_base = cosf(theta) * sinf(phi) * radius_; float x_base = cosf(theta) * sinf(phi) * radius_;
@@ -34,14 +36,14 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y // Aplicar rotación en eje Y
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot = x_base * cos_y - z_base * sin_y; float x_rot = (x_base * cos_y) - (z_base * sin_y);
float z_rot = x_base * sin_y + z_base * cos_y; float z_rot = (x_base * sin_y) + (z_base * cos_y);
// Aplicar rotación en eje X // Aplicar rotación en eje X
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float y_rot = y_base * cos_x - z_rot * sin_x; float y_rot = (y_base * cos_x) - (z_rot * sin_x);
float z_final = y_base * sin_x + z_rot * cos_x; float z_final = (y_base * sin_x) + (z_rot * cos_x);
// Retornar coordenadas finales rotadas // Retornar coordenadas finales rotadas
x = x_rot; x = x_rot;
@@ -49,7 +51,7 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final; z = z_final;
} }
float SphereShape::getScaleFactor(float screen_height) const { auto SphereShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio // Factor de escala para física: proporcional al radio
// Radio base = 80px (resolución 320x240) // Radio base = 80px (resolución 320x240)
const float BASE_RADIUS = 80.0f; const float BASE_RADIUS = 80.0f;

View File

@@ -1,7 +1,10 @@
#include "torus_shape.hpp" #include "torus_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "defines.hpp"
void TorusShape::generatePoints(int num_points, float screen_width, float screen_height) { void TorusShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points; num_points_ = num_points;
major_radius_ = screen_height * TORUS_MAJOR_RADIUS_FACTOR; major_radius_ = screen_height * TORUS_MAJOR_RADIUS_FACTOR;
@@ -26,10 +29,10 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Calcular número aproximado de anillos y puntos por anillo // Calcular número aproximado de anillos y puntos por anillo
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f)); int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
if (num_rings < 2) num_rings = 2; num_rings = std::max(num_rings, 2);
int points_per_ring = num_points_ / num_rings; int points_per_ring = num_points_ / num_rings;
if (points_per_ring < 3) points_per_ring = 3; points_per_ring = std::max(points_per_ring, 3);
// Obtener parámetros u y v del índice // Obtener parámetros u y v del índice
int ring = index / points_per_ring; int ring = index / points_per_ring;
@@ -57,7 +60,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
float cos_u = cosf(u); float cos_u = cosf(u);
float sin_u = sinf(u); float sin_u = sinf(u);
float radius_at_v = major_radius_ + minor_radius_ * cos_v; float radius_at_v = major_radius_ + (minor_radius_ * cos_v);
float x_base = radius_at_v * cos_u; float x_base = radius_at_v * cos_u;
float y_base = radius_at_v * sin_u; float y_base = radius_at_v * sin_u;
@@ -66,20 +69,20 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje X // Aplicar rotación en eje X
float cos_x = cosf(angle_x_); float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_); float sin_x = sinf(angle_x_);
float y_rot_x = y_base * cos_x - z_base * sin_x; float y_rot_x = (y_base * cos_x) - (z_base * sin_x);
float z_rot_x = y_base * sin_x + z_base * cos_x; float z_rot_x = (y_base * sin_x) + (z_base * cos_x);
// Aplicar rotación en eje Y // Aplicar rotación en eje Y
float cos_y = cosf(angle_y_); float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_); float sin_y = sinf(angle_y_);
float x_rot_y = x_base * cos_y - z_rot_x * sin_y; float x_rot_y = (x_base * cos_y) - (z_rot_x * sin_y);
float z_rot_y = x_base * sin_y + z_rot_x * cos_y; float z_rot_y = (x_base * sin_y) + (z_rot_x * cos_y);
// Aplicar rotación en eje Z // Aplicar rotación en eje Z
float cos_z = cosf(angle_z_); float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_); float sin_z = sinf(angle_z_);
float x_final = x_rot_y * cos_z - y_rot_x * sin_z; float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
float y_final = x_rot_y * sin_z + y_rot_x * cos_z; float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
// Retornar coordenadas finales rotadas // Retornar coordenadas finales rotadas
x = x_final; x = x_final;
@@ -87,7 +90,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot_y; z = z_rot_y;
} }
float TorusShape::getScaleFactor(float screen_height) const { auto TorusShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio mayor // Factor de escala para física: proporcional al radio mayor
// Radio mayor base = 60px (0.25 * 240px en resolución 320x240) // Radio mayor base = 60px (0.25 * 240px en resolución 320x240)
const float BASE_RADIUS = 60.0f; const float BASE_RADIUS = 60.0f;

View File

@@ -23,26 +23,24 @@
#include "shapes/torus_shape.hpp" #include "shapes/torus_shape.hpp"
ShapeManager::ShapeManager() ShapeManager::ShapeManager()
: engine_(nullptr) : engine_(nullptr),
, scene_mgr_(nullptr) scene_mgr_(nullptr),
, ui_mgr_(nullptr) ui_mgr_(nullptr),
, state_mgr_(nullptr) state_mgr_(nullptr),
, current_mode_(SimulationMode::PHYSICS) current_mode_(SimulationMode::PHYSICS),
, current_shape_type_(ShapeType::SPHERE) current_shape_type_(ShapeType::SPHERE),
, last_shape_type_(ShapeType::SPHERE) last_shape_type_(ShapeType::SPHERE),
, active_shape_(nullptr) active_shape_(nullptr),
, shape_scale_factor_(1.0f) shape_scale_factor_(1.0f),
, depth_zoom_enabled_(true) depth_zoom_enabled_(true),
, screen_width_(0) screen_width_(0),
, screen_height_(0) screen_height_(0),
, shape_convergence_(0.0f) { shape_convergence_(0.0f) {
} }
ShapeManager::~ShapeManager() { ShapeManager::~ShapeManager() = default;
}
void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
StateManager* state_mgr, int screen_width, int screen_height) {
engine_ = engine; engine_ = engine;
scene_mgr_ = scene_mgr; scene_mgr_ = scene_mgr;
ui_mgr_ = ui_mgr; ui_mgr_ = ui_mgr;
@@ -66,17 +64,17 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
activateShapeInternal(last_shape_type_); activateShapeInternal(last_shape_type_);
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO // Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) { if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
if (active_shape_) { if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get()); auto* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) { if (png_shape != nullptr) {
png_shape->setLogoMode(true); png_shape->setLogoMode(true);
} }
} }
} }
// Si estamos en LOGO MODE, resetear convergencia al entrar // Si estamos en LOGO MODE, resetear convergencia al entrar
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO) { if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO) {
shape_convergence_ = 0.0f; shape_convergence_ = 0.0f;
} }
} else { } else {
@@ -93,7 +91,7 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
} }
// Mostrar notificación (solo si NO estamos en modo demo o logo) // Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo física"); ui_mgr_->showNotification("Modo física");
} }
} }
@@ -113,8 +111,8 @@ void ShapeManager::handleShapeScaleChange(bool increase) {
clampShapeScale(); clampShapeScale();
// Mostrar notificación si está en modo SANDBOX // Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
std::string notification = "Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%"; std::string notification = "Escala " + std::to_string(static_cast<int>((shape_scale_factor_ * 100.0f) + 0.5f)) + "%";
ui_mgr_->showNotification(notification); ui_mgr_->showNotification(notification);
} }
} }
@@ -125,7 +123,7 @@ void ShapeManager::resetShapeScale() {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT; shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
// Mostrar notificación si está en modo SANDBOX // Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Escala 100%"); ui_mgr_->showNotification("Escala 100%");
} }
} }
@@ -136,14 +134,16 @@ void ShapeManager::toggleDepthZoom() {
depth_zoom_enabled_ = !depth_zoom_enabled_; depth_zoom_enabled_ = !depth_zoom_enabled_;
// Mostrar notificación si está en modo SANDBOX // Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification(depth_zoom_enabled_ ? "Profundidad on" : "Profundidad off"); ui_mgr_->showNotification(depth_zoom_enabled_ ? "Profundidad on" : "Profundidad off");
} }
} }
} }
void ShapeManager::update(float delta_time) { void ShapeManager::update(float delta_time) {
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return; if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) {
return;
}
// Actualizar animación de la figura // Actualizar animación de la figura
active_shape_->update(delta_time, static_cast<float>(screen_width_), static_cast<float>(screen_height_)); active_shape_->update(delta_time, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
@@ -161,7 +161,9 @@ void ShapeManager::update(float delta_time) {
// Actualizar cada pelota con física de atracción // Actualizar cada pelota con física de atracción
for (size_t i = 0; i < balls.size(); i++) { for (size_t i = 0; i < balls.size(); i++) {
// Obtener posición 3D rotada del punto i // Obtener posición 3D rotada del punto i
float x_3d, y_3d, z_3d; float x_3d;
float y_3d;
float z_3d;
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d); active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
// Aplicar escala manual a las coordenadas 3D // Aplicar escala manual a las coordenadas 3D
@@ -179,9 +181,7 @@ void ShapeManager::update(float delta_time) {
// 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]->applyShapeForce(target_x, target_y, shape_size, delta_time, balls[i]->applyShapeForce(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)
@@ -191,12 +191,12 @@ void ShapeManager::update(float delta_time) {
// Calcular escala según profundidad Z (perspectiva) - solo si está activado // Calcular escala según profundidad Z (perspectiva) - solo si está activado
// 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x // 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f; float depth_scale = depth_zoom_enabled_ ? (0.5f + (z_normalized * 1.0f)) : 1.0f;
balls[i]->setDepthScale(depth_scale); balls[i]->setDepthScale(depth_scale);
} }
// Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo) // Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) { if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int balls_near = 0; int balls_near = 0;
float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo) float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo)
@@ -215,7 +215,9 @@ void ShapeManager::update(float delta_time) {
} }
void ShapeManager::generateShape() { void ShapeManager::generateShape() {
if (!active_shape_) return; if (!active_shape_) {
return;
}
int num_points = static_cast<int>(scene_mgr_->getBallCount()); int num_points = static_cast<int>(scene_mgr_->getBallCount());
active_shape_->generatePoints(num_points, static_cast<float>(screen_width_), static_cast<float>(screen_height_)); active_shape_->generatePoints(num_points, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
@@ -277,9 +279,9 @@ void ShapeManager::activateShapeInternal(ShapeType type) {
scene_mgr_->enableShapeAttractionAll(true); scene_mgr_->enableShapeAttractionAll(true);
// Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo) // Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo)
if (active_shape_ && state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) { if (active_shape_ && (state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
std::string shape_name = active_shape_->getName(); std::string shape_name = active_shape_->getName();
std::transform(shape_name.begin(), shape_name.end(), shape_name.begin(), ::tolower); std::ranges::transform(shape_name, shape_name.begin(), ::tolower);
std::string notification = std::string("Modo ") + shape_name; std::string notification = std::string("Modo ") + shape_name;
ui_mgr_->showNotification(notification); ui_mgr_->showNotification(notification);
} }

View File

@@ -46,8 +46,7 @@ class ShapeManager {
* @param screen_width Ancho lógico de pantalla * @param screen_width Ancho lógico de pantalla
* @param screen_height Alto lógico de pantalla * @param screen_height Alto lógico de pantalla
*/ */
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
StateManager* state_mgr, int screen_width, int screen_height);
/** /**
* @brief Toggle entre modo PHYSICS y SHAPE * @brief Toggle entre modo PHYSICS y SHAPE

View File

@@ -1,39 +1,37 @@
#include "state_manager.hpp" #include "state_manager.hpp"
#include <algorithm> // for std::min #include <algorithm> // for std::min
#include <array> // for std::array
#include <cstdlib> // for rand #include <cstdlib> // for rand
#include <vector> // for std::vector #include <vector> // for std::vector
#include "defines.hpp" // for constantes DEMO/LOGO #include "defines.hpp" // for constantes DEMO/LOGO
#include "engine.hpp" // for Engine (enter/exitShapeMode, texture) #include "engine.hpp" // for Engine (enter/exitShapeMode, texture)
#include "scene/scene_manager.hpp" // for SceneManager #include "scene/scene_manager.hpp" // for SceneManager
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "shapes/png_shape.hpp" // for PNGShape flip detection #include "shapes/png_shape.hpp" // for PNGShape flip detection
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "theme_manager.hpp" // for ThemeManager #include "theme_manager.hpp" // for ThemeManager
StateManager::StateManager() StateManager::StateManager()
: engine_(nullptr) : engine_(nullptr),
, scene_mgr_(nullptr) scene_mgr_(nullptr),
, theme_mgr_(nullptr) theme_mgr_(nullptr),
, shape_mgr_(nullptr) shape_mgr_(nullptr),
, current_app_mode_(AppMode::SANDBOX) current_app_mode_(AppMode::SANDBOX),
, previous_app_mode_(AppMode::SANDBOX) previous_app_mode_(AppMode::SANDBOX),
, demo_timer_(0.0f) demo_timer_(0.0f),
, demo_next_action_time_(0.0f) demo_next_action_time_(0.0f),
, logo_convergence_threshold_(0.90f) logo_convergence_threshold_(0.90f),
, logo_min_time_(3.0f) logo_min_time_(3.0f),
, logo_max_time_(5.0f) logo_max_time_(5.0f),
, logo_waiting_for_flip_(false) logo_waiting_for_flip_(false),
, logo_target_flip_number_(0) logo_target_flip_number_(0),
, logo_target_flip_percentage_(0.0f) logo_target_flip_percentage_(0.0f),
, logo_current_flip_count_(0) logo_current_flip_count_(0),
, logo_entered_manually_(false) logo_entered_manually_(false),
, logo_previous_theme_(0) logo_previous_theme_(0),
, logo_previous_texture_index_(0) logo_previous_texture_index_(0),
, logo_previous_shape_scale_(1.0f) { logo_previous_shape_scale_(1.0f) {
}
StateManager::~StateManager() {
} }
void StateManager::initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr) { void StateManager::initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr) {
@@ -53,8 +51,10 @@ void StateManager::setLogoPreviousState(int theme, size_t texture_index, float s
// ACTUALIZACIÓN DE ESTADOS // ACTUALIZACIÓN DE ESTADOS
// =========================================================================== // ===========================================================================
void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) { void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) { // NOLINT(readability-function-cognitive-complexity)
if (current_app_mode_ == AppMode::SANDBOX) return; if (current_app_mode_ == AppMode::SANDBOX) {
return;
}
demo_timer_ += delta_time; demo_timer_ += delta_time;
@@ -63,14 +63,12 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
if (current_app_mode_ == AppMode::LOGO) { if (current_app_mode_ == AppMode::LOGO) {
if (logo_waiting_for_flip_) { if (logo_waiting_for_flip_) {
// CAMINO B: Esperando a que ocurran flips // CAMINO B: Esperando a que ocurran flips
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape); auto* png_shape = dynamic_cast<PNGShape*>(active_shape);
if (png_shape) { if (png_shape != nullptr) {
int current_flip_count = png_shape->getFlipCount(); int current_flip_count = png_shape->getFlipCount();
if (current_flip_count > logo_current_flip_count_) { logo_current_flip_count_ = std::max(current_flip_count, logo_current_flip_count_);
logo_current_flip_count_ = current_flip_count;
}
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) { if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
if (png_shape->isFlipping()) { if (png_shape->isFlipping()) {
@@ -93,7 +91,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
should_trigger = demo_timer_ >= demo_next_action_time_; should_trigger = demo_timer_ >= demo_next_action_time_;
} }
if (!should_trigger) return; if (!should_trigger) {
return;
}
if (current_app_mode_ == AppMode::LOGO) { if (current_app_mode_ == AppMode::LOGO) {
// LOGO MODE: Sistema de acciones variadas con gravedad dinámica // LOGO MODE: Sistema de acciones variadas con gravedad dinámica
@@ -122,8 +122,8 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN); logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
logo_current_flip_count_ = 0; logo_current_flip_count_ = 0;
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape()); auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) { if (png_shape != nullptr) {
png_shape->resetFlipCount(); png_shape->resetFlipCount();
} }
// No hacer nada más — esperar a que ocurran los flips // No hacer nada más — esperar a que ocurran los flips
@@ -158,7 +158,7 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
scene_mgr_->forceBallsGravityOff(); scene_mgr_->forceBallsGravityOff();
} else { } else {
// 16%: Cambiar dirección de gravedad // 16%: Cambiar dirección de gravedad
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4); auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction); scene_mgr_->changeGravityDirection(new_direction);
scene_mgr_->forceBallsGravityOn(); scene_mgr_->forceBallsGravityOn();
} }
@@ -186,7 +186,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
} }
void StateManager::setState(AppMode new_mode, int current_screen_width, int current_screen_height) { void StateManager::setState(AppMode new_mode, int current_screen_width, int current_screen_height) {
if (current_app_mode_ == new_mode) return; if (current_app_mode_ == new_mode) {
return;
}
if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) { if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
previous_app_mode_ = new_mode; previous_app_mode_ = new_mode;
@@ -201,7 +203,8 @@ void StateManager::setState(AppMode new_mode, int current_screen_width, int curr
demo_timer_ = 0.0f; demo_timer_ = 0.0f;
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) { if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
float min_interval, max_interval; float min_interval;
float max_interval;
if (new_mode == AppMode::LOGO) { if (new_mode == AppMode::LOGO) {
float resolution_scale = current_screen_height / 720.0f; float resolution_scale = current_screen_height / 720.0f;
@@ -250,8 +253,10 @@ void StateManager::toggleLogoMode(int current_screen_width, int current_screen_h
// ACCIONES DE DEMO // ACCIONES DE DEMO
// =========================================================================== // ===========================================================================
void StateManager::performDemoAction(bool is_lite) { void StateManager::performDemoAction(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
// ============================================ // ============================================
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg) // SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
@@ -278,18 +283,18 @@ void StateManager::performDemoAction(bool is_lite) {
// ACCIONES NORMALES DE DEMO/DEMO_LITE // ACCIONES NORMALES DE DEMO/DEMO_LITE
// ============================================ // ============================================
int TOTAL_WEIGHT; int total_weight;
int random_value; int random_value;
int accumulated_weight = 0; int accumulated_weight = 0;
if (is_lite) { if (is_lite) {
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; 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;
random_value = rand() % TOTAL_WEIGHT; random_value = rand() % total_weight;
// Cambiar dirección gravedad (25%) // Cambiar dirección gravedad (25%)
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR; accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) { if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4); auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction); scene_mgr_->changeGravityDirection(new_direction);
return; return;
} }
@@ -304,8 +309,8 @@ void StateManager::performDemoAction(bool is_lite) {
// Activar figura 3D (25%) - PNG_SHAPE excluido // Activar figura 3D (25%) - PNG_SHAPE excluido
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::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]); engine_->enterShapeMode(SHAPES[rand() % 8]);
return; return;
} }
@@ -324,13 +329,13 @@ void StateManager::performDemoAction(bool is_lite) {
} }
} else { } else {
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; 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;
random_value = rand() % TOTAL_WEIGHT; random_value = rand() % total_weight;
// Cambiar dirección gravedad (10%) // Cambiar dirección gravedad (10%)
accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR; accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) { if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4); auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction); scene_mgr_->changeGravityDirection(new_direction);
return; return;
} }
@@ -345,8 +350,8 @@ void StateManager::performDemoAction(bool is_lite) {
// Activar figura 3D (20%) - PNG_SHAPE excluido // Activar figura 3D (20%) - PNG_SHAPE excluido
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::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]); engine_->enterShapeMode(SHAPES[rand() % 8]);
return; return;
} }
@@ -378,10 +383,12 @@ void StateManager::performDemoAction(bool is_lite) {
if (random_value < accumulated_weight) { if (random_value < accumulated_weight) {
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO); int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
std::vector<int> candidates; std::vector<int> candidates;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
candidates.push_back(i); candidates.push_back(i);
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) }
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
candidates.push_back(CUSTOM_SCENARIO_IDX); candidates.push_back(CUSTOM_SCENARIO_IDX);
}
int new_scenario = candidates[rand() % candidates.size()]; int new_scenario = candidates[rand() % candidates.size()];
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode(); SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
scene_mgr_->changeScenario(new_scenario, current_sim_mode); scene_mgr_->changeScenario(new_scenario, current_sim_mode);
@@ -439,15 +446,15 @@ void StateManager::performDemoAction(bool is_lite) {
// RANDOMIZACIÓN AL INICIAR DEMO // RANDOMIZACIÓN AL INICIAR DEMO
// =========================================================================== // ===========================================================================
void StateManager::randomizeOnDemoStart(bool is_lite) { void StateManager::randomizeOnDemoStart(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente // Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) { if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, engine_->enterShapeMode(SHAPES[rand() % 8]);
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
} }
if (is_lite) { if (is_lite) {
@@ -457,11 +464,11 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
engine_->exitShapeMode(false); engine_->exitShapeMode(false);
} }
} else { } else {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]); engine_->enterShapeMode(SHAPES[rand() % 8]);
} }
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4); auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction); scene_mgr_->changeGravityDirection(new_direction);
if (rand() % 2 == 0) { if (rand() % 2 == 0) {
toggleGravityOnOff(); toggleGravityOnOff();
@@ -476,14 +483,14 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
engine_->exitShapeMode(false); engine_->exitShapeMode(false);
} }
} else { } else {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType selected_shape = shapes[rand() % 8]; ShapeType selected_shape = SHAPES[rand() % 8];
// Randomizar profundidad y escala ANTES de activar la figura // Randomizar profundidad y escala ANTES de activar la figura
if (rand() % 2 == 0) { if (rand() % 2 == 0) {
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled()); shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
} }
shape_mgr_->setShapeScaleFactor(0.5f + (rand() % 1500) / 1000.0f); shape_mgr_->setShapeScaleFactor(0.5f + ((rand() % 1500) / 1000.0f));
engine_->enterShapeMode(selected_shape); engine_->enterShapeMode(selected_shape);
} }
@@ -491,10 +498,12 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
// 2. Escenario // 2. Escenario
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO); int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
std::vector<int> candidates; std::vector<int> candidates;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
candidates.push_back(i); candidates.push_back(i);
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) }
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
candidates.push_back(CUSTOM_SCENARIO_IDX); candidates.push_back(CUSTOM_SCENARIO_IDX);
}
int new_scenario = candidates[rand() % candidates.size()]; int new_scenario = candidates[rand() % candidates.size()];
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode(); SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
scene_mgr_->changeScenario(new_scenario, current_sim_mode); scene_mgr_->changeScenario(new_scenario, current_sim_mode);
@@ -513,7 +522,7 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
} }
// 5. Gravedad // 5. Gravedad
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4); auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction); scene_mgr_->changeGravityDirection(new_direction);
if (rand() % 3 == 0) { if (rand() % 3 == 0) {
toggleGravityOnOff(); toggleGravityOnOff();
@@ -526,7 +535,9 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
// =========================================================================== // ===========================================================================
void StateManager::toggleGravityOnOff() { void StateManager::toggleGravityOnOff() {
if (!scene_mgr_) return; if (scene_mgr_ == nullptr) {
return;
}
bool gravity_enabled = scene_mgr_->hasBalls() && bool gravity_enabled = scene_mgr_->hasBalls() &&
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f); (scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
@@ -543,7 +554,9 @@ void StateManager::toggleGravityOnOff() {
// =========================================================================== // ===========================================================================
void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) { void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) {
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
logo_entered_manually_ = !from_demo; logo_entered_manually_ = !from_demo;
@@ -585,8 +598,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
} }
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA // Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
int logo_themes[] = {5, 6, 7, 8}; constexpr std::array<int, 4> LOGO_THEMES = {5, 6, 7, 8};
theme_mgr_->switchToTheme(logo_themes[rand() % 4]); theme_mgr_->switchToTheme(LOGO_THEMES[rand() % 4]);
// Establecer escala a 120% // Establecer escala a 120%
shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE); shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE);
@@ -595,8 +608,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
engine_->enterShapeMode(ShapeType::PNG_SHAPE); engine_->enterShapeMode(ShapeType::PNG_SHAPE);
// Configurar PNG_SHAPE en modo LOGO // Configurar PNG_SHAPE en modo LOGO
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape()); auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) { if (png_shape != nullptr) {
png_shape->setLogoMode(true); png_shape->setLogoMode(true);
png_shape->resetFlipCount(); png_shape->resetFlipCount();
} }
@@ -607,8 +620,12 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
// =========================================================================== // ===========================================================================
void StateManager::exitLogoMode(bool return_to_demo) { void StateManager::exitLogoMode(bool return_to_demo) {
if (current_app_mode_ != AppMode::LOGO) return; if (current_app_mode_ != AppMode::LOGO) {
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return; return;
}
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
logo_entered_manually_ = false; logo_entered_manually_ = false;
@@ -624,17 +641,15 @@ void StateManager::exitLogoMode(bool return_to_demo) {
} }
// Desactivar modo LOGO en PNG_SHAPE // Desactivar modo LOGO en PNG_SHAPE
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape()); auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) { if (png_shape != nullptr) {
png_shape->setLogoMode(false); png_shape->setLogoMode(false);
} }
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria // Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) { if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, engine_->enterShapeMode(SHAPES[rand() % 8]);
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
} }
if (!return_to_demo) { if (!return_to_demo) {

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL_stdinc.h> // for Uint64 #include <SDL3/SDL_stdinc.h> // for Uint64
#include <cstddef> // for size_t #include <cstddef> // for size_t
#include "defines.hpp" // for AppMode, ShapeType, GravityDirection #include "defines.hpp" // for AppMode, ShapeType, GravityDirection
@@ -37,7 +38,7 @@ class StateManager {
/** /**
* @brief Destructor * @brief Destructor
*/ */
~StateManager(); ~StateManager() = default;
/** /**
* @brief Inicializa el StateManager con referencias a los subsistemas necesarios * @brief Inicializa el StateManager con referencias a los subsistemas necesarios

View File

@@ -1,24 +1,32 @@
#include "textrenderer.hpp" #include "textrenderer.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
#include <iostream> #include <iostream>
#include "resource_manager.hpp" #include "resource_manager.hpp"
TextRenderer::TextRenderer() : renderer_(nullptr), font_(nullptr), font_size_(0), use_antialiasing_(true), font_data_buffer_(nullptr) { TextRenderer::TextRenderer()
: renderer_(nullptr),
font_(nullptr),
font_size_(0),
use_antialiasing_(true),
font_data_buffer_(nullptr) {
} }
TextRenderer::~TextRenderer() { TextRenderer::~TextRenderer() {
cleanup(); cleanup();
} }
bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) { auto TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) -> bool {
renderer_ = renderer; renderer_ = renderer;
font_size_ = font_size; font_size_ = font_size;
use_antialiasing_ = use_antialiasing; use_antialiasing_ = use_antialiasing;
font_path_ = font_path; // Guardar ruta para reinitialize() font_path_ = font_path; // Guardar ruta para reinitialize()
// Inicializar SDL_ttf si no está inicializado // Inicializar SDL_ttf si no está inicializado
if (!TTF_WasInit()) { if (TTF_WasInit() == 0) {
if (!TTF_Init()) { if (!TTF_Init()) {
SDL_Log("Error al inicializar SDL_ttf: %s", SDL_GetError()); SDL_Log("Error al inicializar SDL_ttf: %s", SDL_GetError());
return false; return false;
@@ -26,34 +34,33 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
} }
// Intentar cargar la fuente desde ResourceManager (pack o disco) // Intentar cargar la fuente desde ResourceManager (pack o disco)
unsigned char* fontData = nullptr; unsigned char* font_data = nullptr;
size_t fontDataSize = 0; size_t font_data_size = 0;
if (ResourceManager::loadResource(font_path, fontData, fontDataSize)) { if (ResourceManager::loadResource(font_path, font_data, font_data_size)) {
// Crear SDL_IOStream desde memoria // Crear SDL_IOStream desde memoria
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize)); SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
if (fontIO != nullptr) { if (font_io != nullptr) {
// Cargar fuente desde IOStream // Cargar fuente desde IOStream
font_ = TTF_OpenFontIO(fontIO, true, font_size); // true = cerrar stream automáticamente font_ = TTF_OpenFontIO(font_io, true, font_size); // true = cerrar stream automáticamente
if (font_ == nullptr) { if (font_ == nullptr) {
SDL_Log("Error al cargar fuente desde memoria '%s': %s", font_path, SDL_GetError()); SDL_Log("Error al cargar fuente desde memoria '%s': %s", font_path, SDL_GetError());
delete[] fontData; // Liberar solo si falla la carga delete[] font_data; // Liberar solo si falla la carga
return false; return false;
} }
// CRÍTICO: NO eliminar fontData aquí - SDL_ttf necesita estos datos en memoria // CRÍTICO: NO eliminar fontData aquí - SDL_ttf necesita estos datos en memoria
// mientras la fuente esté abierta. Se liberará en cleanup() // mientras la fuente esté abierta. Se liberará en cleanup()
font_data_buffer_ = fontData; font_data_buffer_ = font_data;
{ {
std::string fn = std::string(font_path); std::string fn = std::string(font_path);
fn = fn.substr(fn.find_last_of("\\/") + 1); fn = fn.substr(fn.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n"; std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
} }
return true; return true;
} else {
delete[] fontData;
} }
delete[] font_data;
} }
// Fallback final: intentar cargar directamente desde disco (por si falla ResourceManager) // Fallback final: intentar cargar directamente desde disco (por si falla ResourceManager)
@@ -66,7 +73,7 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
return true; return true;
} }
bool TextRenderer::reinitialize(int new_font_size) { auto TextRenderer::reinitialize(int new_font_size) -> bool {
// Verificar que tenemos todo lo necesario // Verificar que tenemos todo lo necesario
if (renderer_ == nullptr || font_path_.empty()) { if (renderer_ == nullptr || font_path_.empty()) {
SDL_Log("Error: TextRenderer no inicializado correctamente para reinitialize()"); SDL_Log("Error: TextRenderer no inicializado correctamente para reinitialize()");
@@ -89,39 +96,42 @@ bool TextRenderer::reinitialize(int new_font_size) {
} }
// Intentar cargar la fuente desde ResourceManager con el nuevo tamaño // Intentar cargar la fuente desde ResourceManager con el nuevo tamaño
unsigned char* fontData = nullptr; unsigned char* font_data = nullptr;
size_t fontDataSize = 0; size_t font_data_size = 0;
if (ResourceManager::loadResource(font_path_, fontData, fontDataSize)) { if (ResourceManager::loadResource(font_path_, font_data, font_data_size)) {
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize)); SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
if (fontIO != nullptr) { if (font_io != nullptr) {
font_ = TTF_OpenFontIO(fontIO, true, new_font_size); font_ = TTF_OpenFontIO(font_io, true, new_font_size);
if (font_ == nullptr) { if (font_ == nullptr) {
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s", SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
font_path_.c_str(), new_font_size, SDL_GetError()); font_path_.c_str(),
delete[] fontData; // Liberar solo si falla new_font_size,
SDL_GetError());
delete[] font_data; // Liberar solo si falla
return false; return false;
} }
// Mantener buffer en memoria (NO eliminar) // Mantener buffer en memoria (NO eliminar)
font_data_buffer_ = fontData; font_data_buffer_ = font_data;
font_size_ = new_font_size; font_size_ = new_font_size;
{ {
std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1); std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n"; std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
} }
return true; return true;
} else {
delete[] fontData;
} }
delete[] font_data;
} }
// Fallback: cargar directamente desde disco // Fallback: cargar directamente desde disco
font_ = TTF_OpenFont(font_path_.c_str(), new_font_size); font_ = TTF_OpenFont(font_path_.c_str(), new_font_size);
if (font_ == nullptr) { if (font_ == nullptr) {
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s", SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
font_path_.c_str(), new_font_size, SDL_GetError()); font_path_.c_str(),
new_font_size,
SDL_GetError());
return false; return false;
} }
@@ -233,7 +243,8 @@ void TextRenderer::printPhysical(int logical_x, int logical_y, const char* text,
dest_rect.h = static_cast<float>(text_surface->h); dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos // Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0; int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode; SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode); SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -301,7 +312,8 @@ void TextRenderer::printAbsolute(int physical_x, int physical_y, const char* tex
dest_rect.h = static_cast<float>(text_surface->h); dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos // Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0; int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode; SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode); SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -332,7 +344,7 @@ void TextRenderer::printAbsoluteShadowed(int physical_x, int physical_y, const s
printAbsoluteShadowed(physical_x, physical_y, text.c_str()); printAbsoluteShadowed(physical_x, physical_y, text.c_str());
} }
int TextRenderer::getTextWidth(const char* text) { auto TextRenderer::getTextWidth(const char* text) -> int {
if (!isInitialized() || text == nullptr) { if (!isInitialized() || text == nullptr) {
return 0; return 0;
} }
@@ -345,7 +357,7 @@ int TextRenderer::getTextWidth(const char* text) {
return width; return width;
} }
int TextRenderer::getTextWidthPhysical(const char* text) { auto TextRenderer::getTextWidthPhysical(const char* text) -> int {
// Retorna el ancho REAL en píxeles físicos (sin escalado lógico) // Retorna el ancho REAL en píxeles físicos (sin escalado lógico)
// Idéntico a getTextWidth() pero semánticamente diferente: // Idéntico a getTextWidth() pero semánticamente diferente:
// - Este método se usa cuando se necesita el ancho REAL de la fuente // - Este método se usa cuando se necesita el ancho REAL de la fuente
@@ -362,7 +374,7 @@ int TextRenderer::getTextWidthPhysical(const char* text) {
return width; // Ancho real de la textura generada por TTF return width; // Ancho real de la textura generada por TTF
} }
int TextRenderer::getTextHeight() { auto TextRenderer::getTextHeight() -> int {
if (!isInitialized()) { if (!isInitialized()) {
return 0; return 0;
} }
@@ -370,7 +382,7 @@ int TextRenderer::getTextHeight() {
return TTF_GetFontHeight(font_); return TTF_GetFontHeight(font_);
} }
int TextRenderer::getGlyphHeight() { auto TextRenderer::getGlyphHeight() -> int {
if (!isInitialized()) { if (!isInitialized()) {
return 0; return 0;
} }

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h> #include <SDL3_ttf/SDL_ttf.h>
#include <string> #include <string>
class TextRenderer { class TextRenderer {

File diff suppressed because it is too large Load Diff

View File

@@ -54,8 +54,7 @@ class ThemeManager {
// Queries de colores (usado en rendering) // Queries de colores (usado en rendering)
Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota
void getBackgroundColors(float& top_r, float& top_g, float& top_b, void getBackgroundColors(float& top_r, float& top_g, float& top_b, float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
// Queries de estado (para debug display y lógica) // Queries de estado (para debug display y lógica)
int getCurrentThemeIndex() const { return current_theme_index_; } int getCurrentThemeIndex() const { return current_theme_index_; }

View File

@@ -1,19 +1,15 @@
#include "dynamic_theme.hpp" #include "dynamic_theme.hpp"
#include <algorithm> // for std::min #include <algorithm> // for std::min
DynamicTheme::DynamicTheme(const char* name_en, const char* name_es, DynamicTheme::DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop)
int text_r, int text_g, int text_b,
std::vector<DynamicThemeKeyframe> keyframes,
bool loop)
: name_en_(name_en), : name_en_(name_en),
name_es_(name_es), name_es_(name_es),
text_r_(text_r), text_g_(text_g), text_b_(text_b), text_r_(text_r),
text_g_(text_g),
text_b_(text_b),
keyframes_(std::move(keyframes)), keyframes_(std::move(keyframes)),
loop_(loop), loop_(loop) {
current_keyframe_index_(0),
target_keyframe_index_(1),
transition_progress_(0.0f),
paused_(false) {
// Validación: mínimo 2 keyframes // Validación: mínimo 2 keyframes
if (keyframes_.size() < 2) { if (keyframes_.size() < 2) {
// Fallback: duplicar primer keyframe si solo hay 1 // Fallback: duplicar primer keyframe si solo hay 1
@@ -29,7 +25,9 @@ DynamicTheme::DynamicTheme(const char* name_en, const char* name_es,
} }
void DynamicTheme::update(float delta_time) { void DynamicTheme::update(float delta_time) {
if (paused_) return; // No actualizar si está pausado if (paused_) {
return; // No actualizar si está pausado
}
// Obtener duración del keyframe objetivo // Obtener duración del keyframe objetivo
float duration = keyframes_[target_keyframe_index_].duration; float duration = keyframes_[target_keyframe_index_].duration;
@@ -74,14 +72,14 @@ void DynamicTheme::advanceToNextKeyframe() {
transition_progress_ = 0.0f; transition_progress_ = 0.0f;
} }
Color DynamicTheme::getBallColor(size_t ball_index, float progress) const { auto DynamicTheme::getBallColor(size_t ball_index, float progress) const -> Color {
// Obtener keyframes actual y objetivo // Obtener keyframes actual y objetivo
const auto& current_kf = keyframes_[current_keyframe_index_]; const auto& current_kf = keyframes_[current_keyframe_index_];
const auto& target_kf = keyframes_[target_keyframe_index_]; const auto& target_kf = keyframes_[target_keyframe_index_];
// Si paletas vacías, retornar blanco // Si paletas vacías, retornar blanco
if (current_kf.ball_colors.empty() || target_kf.ball_colors.empty()) { if (current_kf.ball_colors.empty() || target_kf.ball_colors.empty()) {
return {255, 255, 255}; return {.r = 255, .g = 255, .b = 255};
} }
// Obtener colores de ambos keyframes (con wrap) // Obtener colores de ambos keyframes (con wrap)
@@ -95,15 +93,18 @@ Color DynamicTheme::getBallColor(size_t ball_index, float progress) const {
// (progress parámetro será usado en PHASE 3 para LERP externo) // (progress parámetro será usado en PHASE 3 para LERP externo)
float t = transition_progress_; float t = transition_progress_;
return { return {
static_cast<int>(lerp(c1.r, c2.r, t)), .r = static_cast<int>(lerp(c1.r, c2.r, t)),
static_cast<int>(lerp(c1.g, c2.g, t)), .g = static_cast<int>(lerp(c1.g, c2.g, t)),
static_cast<int>(lerp(c1.b, c2.b, t)) .b = static_cast<int>(lerp(c1.b, c2.b, t))};
};
} }
void DynamicTheme::getBackgroundColors(float progress, void DynamicTheme::getBackgroundColors(float progress,
float& tr, float& tg, float& tb, float& tr,
float& br, float& bg, float& bb) const { float& tg,
float& tb,
float& br,
float& bg,
float& bb) const {
// Obtener keyframes actual y objetivo // Obtener keyframes actual y objetivo
const auto& current_kf = keyframes_[current_keyframe_index_]; const auto& current_kf = keyframes_[current_keyframe_index_];
const auto& target_kf = keyframes_[target_keyframe_index_]; const auto& target_kf = keyframes_[target_keyframe_index_];

View File

@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "theme.hpp"
#include <string> #include <string>
#include "theme.hpp"
// Forward declaration (estructura definida en defines.h) // Forward declaration (estructura definida en defines.h)
struct DynamicThemeKeyframe; struct DynamicThemeKeyframe;
@@ -30,10 +31,7 @@ class DynamicTheme : public Theme {
* @param keyframes: Vector de keyframes (mínimo 2) * @param keyframes: Vector de keyframes (mínimo 2)
* @param loop: ¿Volver al inicio al terminar? (siempre true en esta app) * @param loop: ¿Volver al inicio al terminar? (siempre true en esta app)
*/ */
DynamicTheme(const char* name_en, const char* name_es, DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop = true);
int text_r, int text_g, int text_b,
std::vector<DynamicThemeKeyframe> keyframes,
bool loop = true);
~DynamicTheme() override = default; ~DynamicTheme() override = default;
@@ -56,8 +54,12 @@ class DynamicTheme : public Theme {
Color getBallColor(size_t ball_index, float progress) const override; Color getBallColor(size_t ball_index, float progress) const override;
void getBackgroundColors(float progress, void getBackgroundColors(float progress,
float& tr, float& tg, float& tb, float& tr,
float& br, float& bg, float& bb) const override; float& tg,
float& tb,
float& br,
float& bg,
float& bb) const override;
// ======================================== // ========================================
// ANIMACIÓN (soporte completo) // ANIMACIÓN (soporte completo)

View File

@@ -1,32 +1,39 @@
#include "static_theme.hpp" #include "static_theme.hpp"
StaticTheme::StaticTheme(const char* name_en, const char* name_es, StaticTheme::StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors)
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors)
: name_en_(name_en), : name_en_(name_en),
name_es_(name_es), name_es_(name_es),
text_r_(text_r), text_g_(text_g), text_b_(text_b), text_r_(text_r),
notif_bg_r_(notif_bg_r), notif_bg_g_(notif_bg_g), notif_bg_b_(notif_bg_b), text_g_(text_g),
bg_top_r_(bg_top_r), bg_top_g_(bg_top_g), bg_top_b_(bg_top_b), text_b_(text_b),
bg_bottom_r_(bg_bottom_r), bg_bottom_g_(bg_bottom_g), bg_bottom_b_(bg_bottom_b), notif_bg_r_(notif_bg_r),
notif_bg_g_(notif_bg_g),
notif_bg_b_(notif_bg_b),
bg_top_r_(bg_top_r),
bg_top_g_(bg_top_g),
bg_top_b_(bg_top_b),
bg_bottom_r_(bg_bottom_r),
bg_bottom_g_(bg_bottom_g),
bg_bottom_b_(bg_bottom_b),
ball_colors_(std::move(ball_colors)) { ball_colors_(std::move(ball_colors)) {
} }
Color StaticTheme::getBallColor(size_t ball_index, float progress) const { auto StaticTheme::getBallColor(size_t ball_index, float progress) const -> Color {
// Tema estático: siempre retorna color de paleta según índice // Tema estático: siempre retorna color de paleta según índice
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo) // (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
if (ball_colors_.empty()) { if (ball_colors_.empty()) {
return {255, 255, 255}; // Blanco por defecto si paleta vacía return {.r = 255, .g = 255, .b = 255}; // Blanco por defecto si paleta vacía
} }
return ball_colors_[ball_index % ball_colors_.size()]; return ball_colors_[ball_index % ball_colors_.size()];
} }
void StaticTheme::getBackgroundColors(float progress, void StaticTheme::getBackgroundColors(float progress,
float& tr, float& tg, float& tb, float& tr,
float& br, float& bg, float& bb) const { float& tg,
float& tb,
float& br,
float& bg,
float& bb) const {
// Tema estático: siempre retorna colores de fondo fijos // Tema estático: siempre retorna colores de fondo fijos
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo) // (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
tr = bg_top_r_; tr = bg_top_r_;

View File

@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "theme.hpp"
#include <string> #include <string>
#include "theme.hpp"
/** /**
* StaticTheme: Tema estático con 1 keyframe (sin animación) * StaticTheme: Tema estático con 1 keyframe (sin animación)
* *
@@ -28,12 +29,7 @@ class StaticTheme : public Theme {
* @param bg_bottom_r, bg_bottom_g, bg_bottom_b: Color inferior de fondo * @param bg_bottom_r, bg_bottom_g, bg_bottom_b: Color inferior de fondo
* @param ball_colors: Paleta de colores para pelotas * @param ball_colors: Paleta de colores para pelotas
*/ */
StaticTheme(const char* name_en, const char* name_es, StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors);
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors);
~StaticTheme() override = default; ~StaticTheme() override = default;
@@ -60,8 +56,12 @@ class StaticTheme : public Theme {
Color getBallColor(size_t ball_index, float progress) const override; Color getBallColor(size_t ball_index, float progress) const override;
void getBackgroundColors(float progress, void getBackgroundColors(float progress,
float& tr, float& tg, float& tb, float& tr,
float& br, float& bg, float& bb) const override; float& tg,
float& tb,
float& br,
float& bg,
float& bb) const override;
// ======================================== // ========================================
// ANIMACIÓN (sin soporte - tema estático) // ANIMACIÓN (sin soporte - tema estático)

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include "defines.hpp" // for Color, ThemeKeyframe #include "defines.hpp" // for Color, ThemeKeyframe
/** /**
@@ -47,8 +48,12 @@ class Theme {
* @param br, bg, bb: Color inferior (out) * @param br, bg, bb: Color inferior (out)
*/ */
virtual void getBackgroundColors(float progress, virtual void getBackgroundColors(float progress,
float& tr, float& tg, float& tb, float& tr,
float& br, float& bg, float& bb) const = 0; float& tg,
float& tb,
float& br,
float& bg,
float& bb) const = 0;
// ======================================== // ========================================
// ANIMACIÓN (solo temas dinámicos) // ANIMACIÓN (solo temas dinámicos)

View File

@@ -2,6 +2,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "defines.hpp" // for Color #include "defines.hpp" // for Color
/** /**

View File

@@ -1,31 +1,34 @@
#include "app_logo.hpp" #include "app_logo.hpp"
#include <SDL3/SDL_render.h> // for SDL_DestroyTexture, SDL_RenderGeometry, SDL_SetTextureAlphaMod #include <SDL3/SDL_render.h> // for SDL_DestroyTexture, SDL_RenderGeometry, SDL_SetTextureAlphaMod
#include <array> // for std::array
#include <cmath> // for powf, sinf, cosf #include <cmath> // for powf, sinf, cosf
#include <cstdlib> // for free() #include <cstdlib> // for free()
#include <iostream> // for std::cout #include <iostream> // for std::cout
#include <numbers>
#include "logo_scaler.hpp" // for LogoScaler
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory #include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
#include "logo_scaler.hpp" // for LogoScaler
// ============================================================================ // ============================================================================
// Destructor - Liberar las 4 texturas SDL // Destructor - Liberar las 4 texturas SDL
// ============================================================================ // ============================================================================
AppLogo::~AppLogo() { AppLogo::~AppLogo() {
if (logo1_base_texture_) { if (logo1_base_texture_ != nullptr) {
SDL_DestroyTexture(logo1_base_texture_); SDL_DestroyTexture(logo1_base_texture_);
logo1_base_texture_ = nullptr; logo1_base_texture_ = nullptr;
} }
if (logo1_native_texture_) { if (logo1_native_texture_ != nullptr) {
SDL_DestroyTexture(logo1_native_texture_); SDL_DestroyTexture(logo1_native_texture_);
logo1_native_texture_ = nullptr; logo1_native_texture_ = nullptr;
} }
if (logo2_base_texture_) { if (logo2_base_texture_ != nullptr) {
SDL_DestroyTexture(logo2_base_texture_); SDL_DestroyTexture(logo2_base_texture_);
logo2_base_texture_ = nullptr; logo2_base_texture_ = nullptr;
} }
if (logo2_native_texture_) { if (logo2_native_texture_ != nullptr) {
SDL_DestroyTexture(logo2_native_texture_); SDL_DestroyTexture(logo2_native_texture_);
logo2_native_texture_ = nullptr; logo2_native_texture_ = nullptr;
} }
@@ -35,11 +38,23 @@ AppLogo::~AppLogo() {
// Inicialización - Pre-escalar logos a 2 resoluciones (base y nativa) // Inicialización - Pre-escalar logos a 2 resoluciones (base y nativa)
// ============================================================================ // ============================================================================
bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) { auto AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) -> bool {
if (logo1_base_texture_) { SDL_DestroyTexture(logo1_base_texture_); logo1_base_texture_ = nullptr; } if (logo1_base_texture_ != nullptr) {
if (logo1_native_texture_) { SDL_DestroyTexture(logo1_native_texture_); logo1_native_texture_ = nullptr; } SDL_DestroyTexture(logo1_base_texture_);
if (logo2_base_texture_) { SDL_DestroyTexture(logo2_base_texture_); logo2_base_texture_ = nullptr; } logo1_base_texture_ = nullptr;
if (logo2_native_texture_) { SDL_DestroyTexture(logo2_native_texture_); logo2_native_texture_ = nullptr; } }
if (logo1_native_texture_ != nullptr) {
SDL_DestroyTexture(logo1_native_texture_);
logo1_native_texture_ = nullptr;
}
if (logo2_base_texture_ != nullptr) {
SDL_DestroyTexture(logo2_base_texture_);
logo2_base_texture_ = nullptr;
}
if (logo2_native_texture_ != nullptr) {
SDL_DestroyTexture(logo2_native_texture_);
logo2_native_texture_ = nullptr;
}
renderer_ = renderer; renderer_ = renderer;
base_screen_width_ = screen_width; base_screen_width_ = screen_width;
@@ -53,7 +68,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
// 1. Detectar resolución nativa del monitor // 1. Detectar resolución nativa del monitor
// ======================================================================== // ========================================================================
if (!LogoScaler::detectNativeResolution(native_screen_width_, native_screen_height_)) { if (!LogoScaler::detectNativeResolution(native_screen_width_, native_screen_height_)) {
std::cout << "No se pudo detectar resolución nativa, usando solo base" << std::endl; std::cout << "No se pudo detectar resolución nativa, usando solo base" << '\n';
// Fallback: usar resolución base como nativa // Fallback: usar resolución base como nativa
native_screen_width_ = screen_width; native_screen_width_ = screen_width;
native_screen_height_ = screen_height; native_screen_height_ = screen_height;
@@ -76,20 +91,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, // width calculado automáticamente por aspect ratio 0, // width calculado automáticamente por aspect ratio
logo_base_target_height, logo_base_target_height,
logo1_base_width_, logo1_base_width_,
logo1_base_height_ logo1_base_height_);
);
if (logo1_base_data == nullptr) { if (logo1_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (base)" << std::endl; std::cout << "Error: No se pudo escalar logo1 (base)" << '\n';
return false; return false;
} }
logo1_base_texture_ = LogoScaler::createTextureFromBuffer( logo1_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_base_data, logo1_base_width_, logo1_base_height_ renderer,
); logo1_base_data,
logo1_base_width_,
logo1_base_height_);
free(logo1_base_data); // Liberar buffer temporal free(logo1_base_data); // Liberar buffer temporal
if (logo1_base_texture_ == nullptr) { if (logo1_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (base)" << std::endl; std::cout << "Error: No se pudo crear textura logo1 (base)" << '\n';
return false; return false;
} }
@@ -102,20 +118,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, // width calculado automáticamente 0, // width calculado automáticamente
logo_native_target_height, logo_native_target_height,
logo1_native_width_, logo1_native_width_,
logo1_native_height_ logo1_native_height_);
);
if (logo1_native_data == nullptr) { if (logo1_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (nativa)" << std::endl; std::cout << "Error: No se pudo escalar logo1 (nativa)" << '\n';
return false; return false;
} }
logo1_native_texture_ = LogoScaler::createTextureFromBuffer( logo1_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_native_data, logo1_native_width_, logo1_native_height_ renderer,
); logo1_native_data,
logo1_native_width_,
logo1_native_height_);
free(logo1_native_data); free(logo1_native_data);
if (logo1_native_texture_ == nullptr) { if (logo1_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << std::endl; std::cout << "Error: No se pudo crear textura logo1 (nativa)" << '\n';
return false; return false;
} }
@@ -132,20 +149,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, 0,
logo_base_target_height, logo_base_target_height,
logo2_base_width_, logo2_base_width_,
logo2_base_height_ logo2_base_height_);
);
if (logo2_base_data == nullptr) { if (logo2_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (base)" << std::endl; std::cout << "Error: No se pudo escalar logo2 (base)" << '\n';
return false; return false;
} }
logo2_base_texture_ = LogoScaler::createTextureFromBuffer( logo2_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_base_data, logo2_base_width_, logo2_base_height_ renderer,
); logo2_base_data,
logo2_base_width_,
logo2_base_height_);
free(logo2_base_data); free(logo2_base_data);
if (logo2_base_texture_ == nullptr) { if (logo2_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (base)" << std::endl; std::cout << "Error: No se pudo crear textura logo2 (base)" << '\n';
return false; return false;
} }
@@ -157,20 +175,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, 0,
logo_native_target_height, logo_native_target_height,
logo2_native_width_, logo2_native_width_,
logo2_native_height_ logo2_native_height_);
);
if (logo2_native_data == nullptr) { if (logo2_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (nativa)" << std::endl; std::cout << "Error: No se pudo escalar logo2 (nativa)" << '\n';
return false; return false;
} }
logo2_native_texture_ = LogoScaler::createTextureFromBuffer( logo2_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_native_data, logo2_native_width_, logo2_native_height_ renderer,
); logo2_native_data,
logo2_native_width_,
logo2_native_height_);
free(logo2_native_data); free(logo2_native_data);
if (logo2_native_texture_ == nullptr) { if (logo2_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << std::endl; std::cout << "Error: No se pudo crear textura logo2 (nativa)" << '\n';
return false; return false;
} }
@@ -191,7 +210,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
return true; return true;
} }
void AppLogo::update(float delta_time, AppMode current_mode) { void AppLogo::update(float delta_time, AppMode current_mode) { // NOLINT(readability-function-cognitive-complexity)
// Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado) // Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado)
if (current_mode == AppMode::SANDBOX) { if (current_mode == AppMode::SANDBOX) {
state_ = AppLogoState::HIDDEN; state_ = AppLogoState::HIDDEN;
@@ -262,8 +281,7 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = 0.0f; logo2_rotation_ = 0.0f;
break; break;
case AppLogoAnimationType::ELASTIC_STICK: case AppLogoAnimationType::ELASTIC_STICK: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
float elastic_t1 = easeOutElastic(prog1); float elastic_t1 = easeOutElastic(prog1);
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f); logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
@@ -279,11 +297,9 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f); logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f; logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
logo2_rotation_ = 0.0f; logo2_rotation_ = 0.0f;
} } break;
break;
case AppLogoAnimationType::ROTATE_SPIRAL: case AppLogoAnimationType::ROTATE_SPIRAL: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1); float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 0.3f + (ease_t1 * 0.7f); logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
@@ -297,11 +313,9 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = (1.0f - prog2) * 6.28f; logo2_rotation_ = (1.0f - prog2) * 6.28f;
logo2_squash_y_ = 1.0f; logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f; logo2_stretch_x_ = 1.0f;
} } break;
break;
case AppLogoAnimationType::BOUNCE_SQUASH: case AppLogoAnimationType::BOUNCE_SQUASH: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
float bounce_t1 = easeOutBounce(prog1); float bounce_t1 = easeOutBounce(prog1);
logo1_scale_ = 1.0f; logo1_scale_ = 1.0f;
@@ -317,8 +331,7 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_squash_y_ = 1.0f - squash_amount2; logo2_squash_y_ = 1.0f - squash_amount2;
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f; logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
logo2_rotation_ = 0.0f; logo2_rotation_ = 0.0f;
} } break;
break;
} }
} }
} }
@@ -379,8 +392,7 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = 0.0f; logo2_rotation_ = 0.0f;
break; break;
case AppLogoAnimationType::ELASTIC_STICK: case AppLogoAnimationType::ELASTIC_STICK: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f); logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
logo1_squash_y_ = 1.0f + (prog1 * 0.3f); logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
@@ -392,11 +404,9 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_squash_y_ = 1.0f + (prog2 * 0.3f); logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f); logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
logo2_rotation_ = prog2 * 0.1f; logo2_rotation_ = prog2 * 0.1f;
} } break;
break;
case AppLogoAnimationType::ROTATE_SPIRAL: case AppLogoAnimationType::ROTATE_SPIRAL: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1); float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 1.0f - (ease_t1 * 0.7f); logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
@@ -410,11 +420,9 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = prog2 * 6.28f; logo2_rotation_ = prog2 * 6.28f;
logo2_squash_y_ = 1.0f; logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f; logo2_stretch_x_ = 1.0f;
} } break;
break;
case AppLogoAnimationType::BOUNCE_SQUASH: case AppLogoAnimationType::BOUNCE_SQUASH: {
{
float prog1 = std::min(1.0f, fade_progress_logo1); float prog1 = std::min(1.0f, fade_progress_logo1);
if (prog1 < 0.2f) { if (prog1 < 0.2f) {
float squash_t = prog1 / 0.2f; float squash_t = prog1 / 0.2f;
@@ -440,8 +448,7 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
} }
logo2_scale_ = 1.0f + (prog2 * 0.3f); logo2_scale_ = 1.0f + (prog2 * 0.3f);
logo2_rotation_ = 0.0f; logo2_rotation_ = 0.0f;
} } break;
break;
} }
} }
} }
@@ -477,7 +484,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
logo2_current_width_ = logo2_native_width_; logo2_current_width_ = logo2_native_width_;
logo2_current_height_ = logo2_native_height_; logo2_current_height_ = logo2_native_height_;
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << std::endl; std::cout << "AppLogo: Cambiado a texturas NATIVAS" << '\n';
} else { } else {
// Cambiar a texturas base (ventana redimensionable) // Cambiar a texturas base (ventana redimensionable)
logo1_current_texture_ = logo1_base_texture_; logo1_current_texture_ = logo1_base_texture_;
@@ -488,7 +495,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
logo2_current_width_ = logo2_base_width_; logo2_current_width_ = logo2_base_width_;
logo2_current_height_ = logo2_base_height_; logo2_current_height_ = logo2_base_height_;
std::cout << "AppLogo: Cambiado a texturas BASE" << std::endl; std::cout << "AppLogo: Cambiado a texturas BASE" << '\n';
} }
// Nota: No es necesario recalcular escalas porque las texturas están pre-escaladas // Nota: No es necesario recalcular escalas porque las texturas están pre-escaladas
@@ -499,57 +506,61 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
// Funciones de easing para animaciones // Funciones de easing para animaciones
// ============================================================================ // ============================================================================
float AppLogo::easeOutElastic(float t) { auto AppLogo::easeOutElastic(float t) -> float {
// Elastic easing out: bounce elástico al final // Elastic easing out: bounce elástico al final
const float c4 = (2.0f * 3.14159f) / 3.0f; const float C4 = (2.0f * std::numbers::pi_v<float>) / 3.0f;
if (t == 0.0f) return 0.0f; if (t == 0.0f) {
if (t == 1.0f) return 1.0f; return 0.0f;
}
return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f; if (t == 1.0f) {
return 1.0f;
} }
float AppLogo::easeOutBack(float t) { return (powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * C4)) + 1.0f;
}
auto AppLogo::easeOutBack(float t) -> float {
// Back easing out: overshoot suave al final // Back easing out: overshoot suave al final
const float c1 = 1.70158f; const float C1 = 1.70158f;
const float c3 = c1 + 1.0f; const float C3 = C1 + 1.0f;
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f); return 1.0f + (C3 * powf(t - 1.0f, 3.0f)) + (C1 * powf(t - 1.0f, 2.0f));
} }
float AppLogo::easeOutBounce(float t) { auto AppLogo::easeOutBounce(float t) -> float {
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH) // Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
const float n1 = 7.5625f; const float N1 = 7.5625f;
const float d1 = 2.75f; const float D1 = 2.75f;
if (t < 1.0f / d1) { if (t < 1.0f / D1) {
return n1 * t * t; return N1 * t * t;
} else if (t < 2.0f / d1) {
t -= 1.5f / d1;
return n1 * t * t + 0.75f;
} else if (t < 2.5f / d1) {
t -= 2.25f / d1;
return n1 * t * t + 0.9375f;
} else {
t -= 2.625f / d1;
return n1 * t * t + 0.984375f;
} }
if (t < 2.0f / D1) {
t -= 1.5f / D1;
return (N1 * t * t) + 0.75f;
}
if (t < 2.5f / D1) {
t -= 2.25f / D1;
return (N1 * t * t) + 0.9375f;
}
t -= 2.625f / D1;
return (N1 * t * t) + 0.984375f;
} }
float AppLogo::easeInOutQuad(float t) { auto AppLogo::easeInOutQuad(float t) -> float {
// Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL) // Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL)
if (t < 0.5f) { if (t < 0.5f) {
return 2.0f * t * t; return 2.0f * t * t;
} else {
return 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f;
} }
return 1.0f - (powf((-2.0f * t) + 2.0f, 2.0f) / 2.0f);
} }
// ============================================================================ // ============================================================================
// Función auxiliar para aleatorización // Función auxiliar para aleatorización
// ============================================================================ // ============================================================================
AppLogoAnimationType AppLogo::getRandomAnimation() { auto AppLogo::getRandomAnimation() -> AppLogoAnimationType {
// Generar número aleatorio entre 0 y 3 (4 tipos de animación) // Generar número aleatorio entre 0 y 3 (4 tipos de animación)
int random_value = rand() % 4; int random_value = rand() % 4;
@@ -571,15 +582,23 @@ AppLogoAnimationType AppLogo::getRandomAnimation() {
// ============================================================================ // ============================================================================
void AppLogo::renderWithGeometry(int logo_index) { void AppLogo::renderWithGeometry(int logo_index) {
if (!renderer_) return; if (renderer_ == nullptr) {
return;
}
// Seleccionar variables según el logo_index (1 = logo1, 2 = logo2) // Seleccionar variables según el logo_index (1 = logo1, 2 = logo2)
SDL_Texture* texture; SDL_Texture* texture;
int base_width, base_height; int base_width;
float scale, squash_y, stretch_x, rotation; int base_height;
float scale;
float squash_y;
float stretch_x;
float rotation;
if (logo_index == 1) { if (logo_index == 1) {
if (!logo1_current_texture_) return; if (logo1_current_texture_ == nullptr) {
return;
}
texture = logo1_current_texture_; texture = logo1_current_texture_;
base_width = logo1_current_width_; base_width = logo1_current_width_;
base_height = logo1_current_height_; base_height = logo1_current_height_;
@@ -588,7 +607,9 @@ void AppLogo::renderWithGeometry(int logo_index) {
stretch_x = logo1_stretch_x_; stretch_x = logo1_stretch_x_;
rotation = logo1_rotation_; rotation = logo1_rotation_;
} else if (logo_index == 2) { } else if (logo_index == 2) {
if (!logo2_current_texture_) return; if (logo2_current_texture_ == nullptr) {
return;
}
texture = logo2_current_texture_; texture = logo2_current_texture_;
base_width = logo2_current_width_; base_width = logo2_current_width_;
base_height = logo2_current_height_; base_height = logo2_current_height_;
@@ -628,7 +649,7 @@ void AppLogo::renderWithGeometry(int logo_index) {
float sin_rot = sinf(rotation); float sin_rot = sinf(rotation);
// Crear 4 vértices del quad (centrado en center_x, center_y) // Crear 4 vértices del quad (centrado en center_x, center_y)
SDL_Vertex vertices[4]; std::array<SDL_Vertex, 4> vertices{};
// Offset desde el centro // Offset desde el centro
float half_w = width / 2.0f; float half_w = width / 2.0f;
@@ -638,49 +659,49 @@ void AppLogo::renderWithGeometry(int logo_index) {
{ {
float local_x = -half_w; float local_x = -half_w;
float local_y = -half_h; float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot; float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = local_x * sin_rot + local_y * cos_rot; float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[0].position = {center_x + rotated_x, center_y + rotated_y}; vertices[0].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[0].tex_coord = {0.0f, 0.0f}; vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
vertices[0].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice vertices[0].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
} }
// Vértice superior derecho (rotado) // Vértice superior derecho (rotado)
{ {
float local_x = half_w; float local_x = half_w;
float local_y = -half_h; float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot; float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = local_x * sin_rot + local_y * cos_rot; float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[1].position = {center_x + rotated_x, center_y + rotated_y}; vertices[1].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[1].tex_coord = {1.0f, 0.0f}; vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
vertices[1].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice vertices[1].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
} }
// Vértice inferior derecho (rotado) // Vértice inferior derecho (rotado)
{ {
float local_x = half_w; float local_x = half_w;
float local_y = half_h; float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot; float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = local_x * sin_rot + local_y * cos_rot; float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[2].position = {center_x + rotated_x, center_y + rotated_y}; vertices[2].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[2].tex_coord = {1.0f, 1.0f}; vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
vertices[2].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice vertices[2].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
} }
// Vértice inferior izquierdo (rotado) // Vértice inferior izquierdo (rotado)
{ {
float local_x = -half_w; float local_x = -half_w;
float local_y = half_h; float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot; float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = local_x * sin_rot + local_y * cos_rot; float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[3].position = {center_x + rotated_x, center_y + rotated_y}; vertices[3].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[3].tex_coord = {0.0f, 1.0f}; vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
vertices[3].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice vertices[3].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
} }
// Índices para 2 triángulos // Índices para 2 triángulos
int indices[6] = {0, 1, 2, 2, 3, 0}; std::array<int, 6> indices = {0, 1, 2, 2, 3, 0};
// Renderizar con la textura del logo correspondiente // Renderizar con la textura del logo correspondiente
SDL_RenderGeometry(renderer_, texture, vertices, 4, indices, 6); SDL_RenderGeometry(renderer_, texture, vertices.data(), 4, indices.data(), 6);
} }

View File

@@ -107,11 +107,11 @@ class AppLogo {
void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2) void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2)
// Funciones de easing // Funciones de easing
float easeOutElastic(float t); // Elastic bounce out static float easeOutElastic(float t); // Elastic bounce out
float easeOutBack(float t); // Overshoot out static float easeOutBack(float t); // Overshoot out
float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH) static float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL) static float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
// Función auxiliar para elegir animación aleatoria // Función auxiliar para elegir animación aleatoria
AppLogoAnimationType getRandomAnimation(); static AppLogoAnimationType getRandomAnimation();
}; };

View File

@@ -1,6 +1,7 @@
#include "help_overlay.hpp" #include "help_overlay.hpp"
#include <algorithm> // for std::min #include <algorithm> // for std::min
#include <array> // for std::array
#include "defines.hpp" #include "defines.hpp"
#include "text/textrenderer.hpp" #include "text/textrenderer.hpp"
@@ -21,69 +22,69 @@ HelpOverlay::HelpOverlay()
column2_width_(0), column2_width_(0),
column3_width_(0), column3_width_(0),
cached_texture_(nullptr), cached_texture_(nullptr),
last_category_color_({0, 0, 0, 255}), last_category_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
last_content_color_({0, 0, 0, 255}), last_content_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
last_bg_color_({0, 0, 0, 255}), last_bg_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
texture_needs_rebuild_(true) { texture_needs_rebuild_(true) {
// Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas) // Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas)
key_bindings_ = { key_bindings_ = {
// COLUMNA 1: SIMULACIÓN // COLUMNA 1: SIMULACIÓN
{"SIMULACIÓN", ""}, {.key = "SIMULACIÓN", .description = ""},
{"1-8", "Escenarios (10 a 50.000 pelotas)"}, {.key = "1-8", .description = "Escenarios (10 a 50.000 pelotas)"},
{"F", "Cambia entre figura y física"}, {.key = "F", .description = "Cambia entre figura y física"},
{"B", "Cambia entre boids y física"}, {.key = "B", .description = "Cambia entre boids y física"},
{"ESPACIO", "Impulso contra la gravedad"}, {.key = "ESPACIO", .description = "Impulso contra la gravedad"},
{"G", "Activar / Desactivar gravedad"}, {.key = "G", .description = "Activar / Desactivar gravedad"},
{"CURSORES", "Dirección de la gravedad"}, {.key = "CURSORES", .description = "Dirección de la gravedad"},
{"", ""}, // Separador {.key = "", .description = ""}, // Separador
// COLUMNA 1: FIGURAS 3D // COLUMNA 1: FIGURAS 3D
{"FIGURAS 3D", ""}, {.key = "FIGURAS 3D", .description = ""},
{"Q/W/E/R", "Esfera / Lissajous / Hélice / Toroide"}, {.key = "Q/W/E/R", .description = "Esfera / Lissajous / Hélice / Toroide"},
{"T/Y/U/I", "Cubo / Cilindro / Icosaedro / Átomo"}, {.key = "T/Y/U/I", .description = "Cubo / Cilindro / Icosaedro / Átomo"},
{"Num+/-", "Escalar figura"}, {.key = "Num+/-", .description = "Escalar figura"},
{"Num*", "Reset escala"}, {.key = "Num*", .description = "Reset escala"},
{"Num/", "Activar / Desactivar profundidad"}, {.key = "Num/", .description = "Activar / Desactivar profundidad"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 2 {.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
// COLUMNA 2: MODOS // COLUMNA 2: MODOS
{"MODOS", ""}, {.key = "MODOS", .description = ""},
{"D", "Activar / Desactivar modo demo"}, {.key = "D", .description = "Activar / Desactivar modo demo"},
{"L", "Activar / Desactivar modo demo lite"}, {.key = "L", .description = "Activar / Desactivar modo demo lite"},
{"K", "Activar / Desactivar modo logo"}, {.key = "K", .description = "Activar / Desactivar modo logo"},
{"", ""}, // Separador {.key = "", .description = ""}, // Separador
// COLUMNA 2: VISUAL // COLUMNA 2: VISUAL
{"VISUAL", ""}, {.key = "VISUAL", .description = ""},
{"C", "Tema siguiente"}, {.key = "C", .description = "Tema siguiente"},
{"Shift+C", "Tema anterior"}, {.key = "Shift+C", .description = "Tema anterior"},
{"NumEnter", "Página de temas"}, {.key = "NumEnter", .description = "Página de temas"},
{"Shift+D", "Pausar tema dinámico"}, {.key = "Shift+D", .description = "Pausar tema dinámico"},
{"N", "Cambiar tamaño de pelota"}, {.key = "N", .description = "Cambiar tamaño de pelota"},
{"X", "Ciclar presets PostFX"}, {.key = "X", .description = "Ciclar presets PostFX"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 3 {.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
// COLUMNA 3: PANTALLA // COLUMNA 3: PANTALLA
{"PANTALLA", ""}, {.key = "PANTALLA", .description = ""},
{"F1", "Disminuye ventana"}, {.key = "F1", .description = "Disminuye ventana"},
{"F2", "Aumenta ventana"}, {.key = "F2", .description = "Aumenta ventana"},
{"F3", "Pantalla completa"}, {.key = "F3", .description = "Pantalla completa"},
{"F4", "Pantalla completa real"}, {.key = "F4", .description = "Pantalla completa real"},
{"F5", "Activar / Desactivar PostFX"}, {.key = "F5", .description = "Activar / Desactivar PostFX"},
{"F6", "Cambia el escalado de pantalla"}, {.key = "F6", .description = "Cambia el escalado de pantalla"},
{"V", "Activar / Desactivar V-Sync"}, {.key = "V", .description = "Activar / Desactivar V-Sync"},
{"", ""}, // Separador {.key = "", .description = ""}, // Separador
// COLUMNA 3: DEBUG/AYUDA // COLUMNA 3: DEBUG/AYUDA
{"DEBUG / AYUDA", ""}, {.key = "DEBUG / AYUDA", .description = ""},
{"F12", "Activar / Desactivar info debug"}, {.key = "F12", .description = "Activar / Desactivar info debug"},
{"H", "Esta ayuda"}, {.key = "H", .description = "Esta ayuda"},
{"ESC", "Salir"}}; {.key = "ESC", .description = "Salir"}};
} }
HelpOverlay::~HelpOverlay() { HelpOverlay::~HelpOverlay() {
// Destruir textura cacheada si existe // Destruir textura cacheada si existe
if (cached_texture_) { if (cached_texture_ != nullptr) {
SDL_DestroyTexture(cached_texture_); SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr; cached_texture_ = nullptr;
} }
@@ -117,7 +118,9 @@ void HelpOverlay::updatePhysicalWindowSize(int physical_width, int physical_heig
} }
void HelpOverlay::reinitializeFontSize(int new_font_size) { void HelpOverlay::reinitializeFontSize(int new_font_size) {
if (!text_renderer_) return; if (text_renderer_ == nullptr) {
return;
}
// Reinicializar text renderer con nuevo tamaño // Reinicializar text renderer con nuevo tamaño
text_renderer_->reinitialize(new_font_size); text_renderer_->reinitialize(new_font_size);
@@ -136,7 +139,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
physical_height_ = physical_height; physical_height_ = physical_height;
// Reinicializar text renderer con nuevo tamaño (si cambió) // Reinicializar text renderer con nuevo tamaño (si cambió)
if (text_renderer_) { if (text_renderer_ != nullptr) {
text_renderer_->reinitialize(font_size); text_renderer_->reinitialize(font_size);
} }
@@ -148,7 +151,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
} }
void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) { void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
if (!text_renderer_) { if (text_renderer_ == nullptr) {
max_width = 0; max_width = 0;
total_height = 0; total_height = 0;
return; return;
@@ -210,7 +213,7 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2; max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2;
// Calcular altura real simulando exactamente lo que hace el render // Calcular altura real simulando exactamente lo que hace el render
int col_heights[3] = {0, 0, 0}; std::array<int, 3> col_heights = {0, 0, 0};
current_column = 0; current_column = 0;
for (const auto& binding : key_bindings_) { for (const auto& binding : key_bindings_) {
@@ -240,7 +243,8 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
void HelpOverlay::calculateBoxDimensions() { void HelpOverlay::calculateBoxDimensions() {
// Calcular dimensiones necesarias según el texto // Calcular dimensiones necesarias según el texto
int text_width, text_height; int text_width;
int text_height;
calculateTextDimensions(text_width, text_height); calculateTextDimensions(text_width, text_height);
// Aplicar límites máximos: 95% ancho, 90% altura // Aplicar límites máximos: 95% ancho, 90% altura
@@ -253,14 +257,15 @@ void HelpOverlay::calculateBoxDimensions() {
// Centrar en pantalla // Centrar en pantalla
box_x_ = (physical_width_ - box_width_) / 2; box_x_ = (physical_width_ - box_width_) / 2;
box_y_ = (physical_height_ - box_height_) / 2; box_y_ = (physical_height_ - box_height_) / 2;
} }
void HelpOverlay::rebuildCachedTexture() { void HelpOverlay::rebuildCachedTexture() {
if (!renderer_ || !theme_mgr_ || !text_renderer_) return; if ((renderer_ == nullptr) || (theme_mgr_ == nullptr) || (text_renderer_ == nullptr)) {
return;
}
// Destruir textura anterior si existe // Destruir textura anterior si existe
if (cached_texture_) { if (cached_texture_ != nullptr) {
SDL_DestroyTexture(cached_texture_); SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr; cached_texture_ = nullptr;
} }
@@ -272,7 +277,7 @@ void HelpOverlay::rebuildCachedTexture() {
box_width_, box_width_,
box_height_); box_height_);
if (!cached_texture_) { if (cached_texture_ == nullptr) {
SDL_Log("Error al crear textura cacheada: %s", SDL_GetError()); SDL_Log("Error al crear textura cacheada: %s", SDL_GetError());
return; return;
} }
@@ -294,42 +299,46 @@ void HelpOverlay::rebuildCachedTexture() {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
// Obtener colores actuales del tema // Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b; int notif_bg_r;
int notif_bg_g;
int notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b); theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
// Renderizar fondo del overlay a la textura // Renderizar fondo del overlay a la textura
float alpha = 0.85f; float alpha = 0.85f;
SDL_Vertex bg_vertices[4]; std::array<SDL_Vertex, 4> bg_vertices{};
float r = notif_bg_r / 255.0f; float r = notif_bg_r / 255.0f;
float g = notif_bg_g / 255.0f; float g = notif_bg_g / 255.0f;
float b = notif_bg_b / 255.0f; float b = notif_bg_b / 255.0f;
// Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura) // Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura)
bg_vertices[0].position = {0, 0}; bg_vertices[0].position = {.x = 0, .y = 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f}; bg_vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
bg_vertices[0].color = {r, g, b, alpha}; bg_vertices[0].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[1].position = {static_cast<float>(box_width_), 0}; bg_vertices[1].position = {.x = static_cast<float>(box_width_), .y = 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f}; bg_vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
bg_vertices[1].color = {r, g, b, alpha}; bg_vertices[1].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[2].position = {static_cast<float>(box_width_), static_cast<float>(box_height_)}; bg_vertices[2].position = {.x = static_cast<float>(box_width_), .y = static_cast<float>(box_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f}; bg_vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
bg_vertices[2].color = {r, g, b, alpha}; bg_vertices[2].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[3].position = {0, static_cast<float>(box_height_)}; bg_vertices[3].position = {.x = 0, .y = static_cast<float>(box_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f}; bg_vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
bg_vertices[3].color = {r, g, b, alpha}; bg_vertices[3].color = {.r = r, .g = g, .b = b, .a = alpha};
int bg_indices[6] = {0, 1, 2, 2, 3, 0}; std::array<int, 6> bg_indices = {0, 1, 2, 2, 3, 0};
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6); SDL_RenderGeometry(renderer_, nullptr, bg_vertices.data(), 4, bg_indices.data(), 6);
// Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0) // Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0)
// Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas // Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas
// Obtener colores para el texto // Obtener colores para el texto
int text_r, text_g, text_b; int text_r;
int text_g;
int text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b); theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255}; SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
@@ -339,7 +348,7 @@ void HelpOverlay::rebuildCachedTexture() {
// Guardar colores actuales para comparación futura // Guardar colores actuales para comparación futura
last_category_color_ = category_color; last_category_color_ = category_color;
last_content_color_ = content_color; last_content_color_ = content_color;
last_bg_color_ = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255}; last_bg_color_ = {.r = static_cast<Uint8>(notif_bg_r), .g = static_cast<Uint8>(notif_bg_g), .b = static_cast<Uint8>(notif_bg_b), .a = 255};
// Configuración de espaciado // Configuración de espaciado
int line_height = text_renderer_->getTextHeight(); int line_height = text_renderer_->getTextHeight();
@@ -347,13 +356,13 @@ void HelpOverlay::rebuildCachedTexture() {
int col_gap = padding * 2; int col_gap = padding * 2;
// Posición X de inicio de cada columna // Posición X de inicio de cada columna
int col_start[3]; std::array<int, 3> col_start{};
col_start[0] = padding; col_start[0] = padding;
col_start[1] = padding + column1_width_ + col_gap; col_start[1] = padding + column1_width_ + col_gap;
col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap; col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap;
// Ancho de cada columna (para centrado interno) // Ancho de cada columna (para centrado interno)
int col_width[3] = {column1_width_, column2_width_, column3_width_}; std::array<int, 3> col_width = {column1_width_, column2_width_, column3_width_};
int glyph_height = text_renderer_->getGlyphHeight(); int glyph_height = text_renderer_->getGlyphHeight();
int current_y = padding; int current_y = padding;
@@ -387,7 +396,7 @@ void HelpOverlay::rebuildCachedTexture() {
} else { } else {
// Encabezado de sección — centrado en la columna // Encabezado de sección — centrado en la columna
int w = text_renderer_->getTextWidthPhysical(binding.key); int w = text_renderer_->getTextWidthPhysical(binding.key);
text_renderer_->printAbsolute(cx + (cw - w) / 2, current_y, binding.key, category_color); text_renderer_->printAbsolute(cx + ((cw - w) / 2), current_y, binding.key, category_color);
current_y += line_height; current_y += line_height;
} }
continue; continue;
@@ -397,7 +406,7 @@ void HelpOverlay::rebuildCachedTexture() {
int key_width = text_renderer_->getTextWidthPhysical(binding.key); int key_width = text_renderer_->getTextWidthPhysical(binding.key);
int desc_width = text_renderer_->getTextWidthPhysical(binding.description); int desc_width = text_renderer_->getTextWidthPhysical(binding.description);
int total_width = key_width + 10 + desc_width; int total_width = key_width + 10 + desc_width;
int line_x = cx + (cw - total_width) / 2; int line_x = cx + ((cw - total_width) / 2);
text_renderer_->printAbsolute(line_x, current_y, binding.key, category_color); text_renderer_->printAbsolute(line_x, current_y, binding.key, category_color);
text_renderer_->printAbsolute(line_x + key_width + 10, current_y, binding.description, content_color); text_renderer_->printAbsolute(line_x + key_width + 10, current_y, binding.description, content_color);
@@ -413,13 +422,19 @@ void HelpOverlay::rebuildCachedTexture() {
} }
void HelpOverlay::render(SDL_Renderer* renderer) { void HelpOverlay::render(SDL_Renderer* renderer) {
if (!visible_) return; if (!visible_) {
return;
}
// Obtener colores actuales del tema // Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b; int notif_bg_r;
int notif_bg_g;
int notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b); theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
int text_r, text_g, text_b; int text_r;
int text_g;
int text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b); theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
Color ball_color = theme_mgr_->getInterpolatedColor(0); Color ball_color = theme_mgr_->getInterpolatedColor(0);
@@ -443,12 +458,14 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD); abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
// Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo) // Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo)
if (texture_needs_rebuild_ || colors_changed || !cached_texture_) { if (texture_needs_rebuild_ || colors_changed || (cached_texture_ == nullptr)) {
rebuildCachedTexture(); rebuildCachedTexture();
} }
// Si no hay textura cacheada (error), salir // Si no hay textura cacheada (error), salir
if (!cached_texture_) return; if (cached_texture_ == nullptr) {
return;
}
// CRÍTICO: Habilitar alpha blending para que la transparencia funcione // CRÍTICO: Habilitar alpha blending para que la transparencia funcione
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
@@ -460,8 +477,8 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
// Calcular posición centrada dentro del VIEWPORT, no de la pantalla física // Calcular posición centrada dentro del VIEWPORT, no de la pantalla física
// viewport.w y viewport.h son las dimensiones del área visible // viewport.w y viewport.h son las dimensiones del área visible
// viewport.x y viewport.y son el offset de las barras negras // viewport.x y viewport.y son el offset de las barras negras
int centered_x = viewport.x + (viewport.w - box_width_) / 2; int centered_x = viewport.x + ((viewport.w - box_width_) / 2);
int centered_y = viewport.y + (viewport.h - box_height_) / 2; int centered_y = viewport.y + ((viewport.h - box_height_) / 2);
// Renderizar la textura cacheada centrada en el viewport // Renderizar la textura cacheada centrada en el viewport
SDL_FRect dest_rect; SDL_FRect dest_rect;

View File

@@ -19,7 +19,7 @@
// Detectar resolución nativa del monitor principal // Detectar resolución nativa del monitor principal
// ============================================================================ // ============================================================================
bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) { auto LogoScaler::detectNativeResolution(int& native_width, int& native_height) -> bool {
int num_displays = 0; int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
@@ -48,22 +48,25 @@ bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
// Cargar PNG y escalar al tamaño especificado // Cargar PNG y escalar al tamaño especificado
// ============================================================================ // ============================================================================
unsigned char* LogoScaler::loadAndScale(const std::string& path, auto LogoScaler::loadAndScale(const std::string& path,
int target_width, int target_height, int target_width,
int& out_width, int& out_height) { int target_height,
int& out_width,
int& out_height) -> unsigned char* {
// 1. Intentar cargar imagen desde ResourceManager (pack o disco) // 1. Intentar cargar imagen desde ResourceManager (pack o disco)
int orig_width, orig_height, orig_channels; int orig_width;
int orig_height;
int orig_channels;
unsigned char* orig_data = nullptr; unsigned char* orig_data = nullptr;
// 1a. Cargar desde ResourceManager // 1a. Cargar desde ResourceManager
unsigned char* resourceData = nullptr; unsigned char* resource_data = nullptr;
size_t resourceSize = 0; size_t resource_size = 0;
if (ResourceManager::loadResource(path, resourceData, resourceSize)) { if (ResourceManager::loadResource(path, resource_data, resource_size)) {
// Descodificar imagen desde memoria usando stb_image // Descodificar imagen desde memoria usando stb_image
orig_data = stbi_load_from_memory(resourceData, static_cast<int>(resourceSize), orig_data = stbi_load_from_memory(resource_data, static_cast<int>(resource_size), &orig_width, &orig_height, &orig_channels, STBI_rgb_alpha);
&orig_width, &orig_height, &orig_channels, STBI_rgb_alpha); delete[] resource_data; // Liberar buffer temporal
delete[] resourceData; // Liberar buffer temporal
} }
// 1b. Si falla todo, error // 1b. Si falla todo, error
@@ -80,7 +83,7 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
out_height = target_height; out_height = target_height;
// 3. Alocar buffer para imagen escalada (RGBA = 4 bytes por píxel) // 3. Alocar buffer para imagen escalada (RGBA = 4 bytes por píxel)
unsigned char* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4)); auto* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4));
if (scaled_data == nullptr) { if (scaled_data == nullptr) {
SDL_Log("Error al alocar memoria para imagen escalada"); SDL_Log("Error al alocar memoria para imagen escalada");
stbi_image_free(orig_data); stbi_image_free(orig_data);
@@ -90,8 +93,14 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
// 4. Escalar con stb_image_resize2 (algoritmo Mitchell, espacio sRGB) // 4. Escalar con stb_image_resize2 (algoritmo Mitchell, espacio sRGB)
// La función devuelve el puntero de salida, o nullptr si falla // La función devuelve el puntero de salida, o nullptr si falla
unsigned char* result = stbir_resize_uint8_srgb( unsigned char* result = stbir_resize_uint8_srgb(
orig_data, orig_width, orig_height, 0, // Input orig_data,
scaled_data, out_width, out_height, 0, // Output orig_width,
orig_height,
0, // Input
scaled_data,
out_width,
out_height,
0, // Output
STBIR_RGBA // Formato píxel STBIR_RGBA // Formato píxel
); );
@@ -111,9 +120,10 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
// Crear textura SDL desde buffer RGBA // Crear textura SDL desde buffer RGBA
// ============================================================================ // ============================================================================
SDL_Texture* LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer, auto LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data, unsigned char* data,
int width, int height) { int width,
int height) -> SDL_Texture* {
if (renderer == nullptr || data == nullptr || width <= 0 || height <= 0) { if (renderer == nullptr || data == nullptr || width <= 0 || height <= 0) {
SDL_Log("Parámetros inválidos para createTextureFromBuffer"); SDL_Log("Parámetros inválidos para createTextureFromBuffer");
return nullptr; return nullptr;
@@ -124,11 +134,11 @@ SDL_Texture* LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
SDL_PixelFormat pixel_format = SDL_PIXELFORMAT_RGBA32; SDL_PixelFormat pixel_format = SDL_PIXELFORMAT_RGBA32;
SDL_Surface* surface = SDL_CreateSurfaceFrom( SDL_Surface* surface = SDL_CreateSurfaceFrom(
width, height, width,
height,
pixel_format, pixel_format,
data, data,
pitch pitch);
);
if (surface == nullptr) { if (surface == nullptr) {
SDL_Log("Error al crear surface: %s", SDL_GetError()); SDL_Log("Error al crear surface: %s", SDL_GetError());

View File

@@ -42,8 +42,10 @@ public:
* IMPORTANTE: El caller debe liberar con free() cuando termine * IMPORTANTE: El caller debe liberar con free() cuando termine
*/ */
static unsigned char* loadAndScale(const std::string& path, static unsigned char* loadAndScale(const std::string& path,
int target_width, int target_height, int target_width,
int& out_width, int& out_height); int target_height,
int& out_width,
int& out_height);
/** /**
* @brief Crea una textura SDL desde un buffer RGBA * @brief Crea una textura SDL desde un buffer RGBA
@@ -57,5 +59,6 @@ public:
*/ */
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer, static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data, unsigned char* data,
int width, int height); int width,
int height);
}; };

View File

@@ -1,9 +1,11 @@
#include "notifier.hpp" #include "notifier.hpp"
#include <SDL3/SDL.h>
#include "defines.hpp"
#include "text/textrenderer.hpp" #include "text/textrenderer.hpp"
#include "theme_manager.hpp" #include "theme_manager.hpp"
#include "defines.hpp"
#include "utils/easing_functions.hpp" #include "utils/easing_functions.hpp"
#include <SDL3/SDL.h>
// ============================================================================ // ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas) // HELPER: Obtener viewport en coordenadas físicas (no lógicas)
@@ -11,9 +13,10 @@
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación // SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar // lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica. // temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) { static auto getPhysicalViewport(SDL_Renderer* renderer) -> SDL_Rect {
// Guardar estado actual de presentación lógica // Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0; int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode; SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode); SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
@@ -31,19 +34,19 @@ static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
} }
Notifier::Notifier() Notifier::Notifier()
: renderer_(nullptr) : renderer_(nullptr),
, text_renderer_(nullptr) text_renderer_(nullptr),
, theme_manager_(nullptr) theme_manager_(nullptr),
, window_width_(0) window_width_(0),
, window_height_(0) window_height_(0),
, current_notification_(nullptr) { current_notification_(nullptr) {
} }
Notifier::~Notifier() { Notifier::~Notifier() {
clear(); clear();
} }
bool Notifier::init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height) { auto Notifier::init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height) -> bool {
renderer_ = renderer; renderer_ = renderer;
text_renderer_ = text_renderer; text_renderer_ = text_renderer;
theme_manager_ = theme_manager; theme_manager_ = theme_manager;
@@ -151,28 +154,30 @@ void Notifier::update(Uint64 current_time) {
} }
void Notifier::render() { void Notifier::render() {
if (!current_notification_ || !text_renderer_ || !renderer_ || !theme_manager_) { if (!current_notification_ || (text_renderer_ == nullptr) || (renderer_ == nullptr) || (theme_manager_ == nullptr)) {
return; return;
} }
// Obtener colores DINÁMICOS desde ThemeManager (incluye LERP automático) // Obtener colores DINÁMICOS desde ThemeManager (incluye LERP automático)
int text_r, text_g, text_b; int text_r;
int text_g;
int text_b;
theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b); theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color text_color = { SDL_Color text_color = {
static_cast<Uint8>(text_r), static_cast<Uint8>(text_r),
static_cast<Uint8>(text_g), static_cast<Uint8>(text_g),
static_cast<Uint8>(text_b), static_cast<Uint8>(text_b),
static_cast<Uint8>(current_notification_->alpha * 255.0f) static_cast<Uint8>(current_notification_->alpha * 255.0f)};
};
int bg_r, bg_g, bg_b; int bg_r;
int bg_g;
int bg_b;
theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b); theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b);
SDL_Color bg_color = { SDL_Color bg_color = {
static_cast<Uint8>(bg_r), static_cast<Uint8>(bg_r),
static_cast<Uint8>(bg_g), static_cast<Uint8>(bg_g),
static_cast<Uint8>(bg_b), static_cast<Uint8>(bg_b),
255 255};
};
// Calcular dimensiones del texto en píxeles FÍSICOS // Calcular dimensiones del texto en píxeles FÍSICOS
// IMPORTANTE: Usar getTextWidthPhysical() en lugar de getTextWidth() // IMPORTANTE: Usar getTextWidthPhysical() en lugar de getTextWidth()
@@ -207,7 +212,7 @@ void Notifier::render() {
} }
void Notifier::renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color) { void Notifier::renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color) {
if (!renderer_) { if (renderer_ == nullptr) {
return; return;
} }
@@ -225,7 +230,7 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
bg_rect.h = static_cast<float>(height); bg_rect.h = static_cast<float>(height);
// Color del tema con alpha // Color del tema con alpha
Uint8 bg_alpha = static_cast<Uint8>(alpha * 255.0f); auto bg_alpha = static_cast<Uint8>(alpha * 255.0f);
SDL_SetRenderDrawColor(renderer_, bg_color.r, bg_color.g, bg_color.b, bg_alpha); SDL_SetRenderDrawColor(renderer_, bg_color.r, bg_color.g, bg_color.b, bg_alpha);
// Habilitar blending para transparencia // Habilitar blending para transparencia
@@ -233,7 +238,8 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
// CRÍTICO: Deshabilitar presentación lógica para renderizar en píxeles físicos absolutos // CRÍTICO: Deshabilitar presentación lógica para renderizar en píxeles físicos absolutos
// (igual que printAbsolute() en TextRenderer) // (igual que printAbsolute() en TextRenderer)
int logical_w = 0, logical_h = 0; int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode; SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode); SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -248,7 +254,7 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE); SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
} }
bool Notifier::isActive() const { auto Notifier::isActive() const -> bool {
return (current_notification_ != nullptr); return (current_notification_ != nullptr);
} }

View File

@@ -1,9 +1,10 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string>
#include <queue>
#include <memory> #include <memory>
#include <queue>
#include <string>
// Forward declarations // Forward declarations
class TextRenderer; class TextRenderer;

View File

@@ -1,18 +1,20 @@
#include "ui_manager.hpp" #include "ui_manager.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <array>
#include <string> #include <string>
#include "ball.hpp" // for Ball #include "ball.hpp" // for Ball
#include "defines.hpp" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode #include "defines.hpp" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode
#include "engine.hpp" // for Engine (info de sistema) #include "engine.hpp" // for Engine (info de sistema)
#include "help_overlay.hpp" // for HelpOverlay
#include "notifier.hpp" // for Notifier
#include "scene/scene_manager.hpp" // for SceneManager #include "scene/scene_manager.hpp" // for SceneManager
#include "shapes/shape.hpp" // for Shape #include "shapes/shape.hpp" // for Shape
#include "text/textrenderer.hpp" // for TextRenderer #include "text/textrenderer.hpp" // for TextRenderer
#include "theme_manager.hpp" // for ThemeManager #include "theme_manager.hpp" // for ThemeManager
#include "notifier.hpp" // for Notifier
#include "help_overlay.hpp" // for HelpOverlay
// ============================================================================ // ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas) // HELPER: Obtener viewport en coordenadas físicas (no lógicas)
@@ -20,9 +22,10 @@
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación // SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar // lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica. // temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) { static auto getPhysicalViewport(SDL_Renderer* renderer) -> SDL_Rect {
// Guardar estado actual de presentación lógica // Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0; int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode; SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode); SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
@@ -40,23 +43,23 @@ static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
} }
UIManager::UIManager() UIManager::UIManager()
: text_renderer_debug_(nullptr) : text_renderer_debug_(nullptr),
, text_renderer_notifier_(nullptr) text_renderer_notifier_(nullptr),
, notifier_(nullptr) notifier_(nullptr),
, help_overlay_(nullptr) help_overlay_(nullptr),
, show_debug_(false) show_debug_(false),
, fps_last_time_(0) fps_last_time_(0),
, fps_frame_count_(0) fps_frame_count_(0),
, fps_current_(0) fps_current_(0),
, fps_text_("FPS: 0") fps_text_("FPS: 0"),
, vsync_text_("VSYNC ON") vsync_text_("VSYNC ON"),
, renderer_(nullptr) renderer_(nullptr),
, theme_manager_(nullptr) theme_manager_(nullptr),
, physical_window_width_(0) physical_window_width_(0),
, physical_window_height_(0) physical_window_height_(0),
, logical_window_width_(0) logical_window_width_(0),
, logical_window_height_(0) logical_window_height_(0),
, current_font_size_(18) { // Tamaño por defecto (medium) current_font_size_(18) { // Tamaño por defecto (medium)
} }
UIManager::~UIManager() { UIManager::~UIManager() {
@@ -67,13 +70,15 @@ UIManager::~UIManager() {
delete help_overlay_; delete help_overlay_;
} }
void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, int physical_width, int physical_height, int logical_width, int logical_height) {
int physical_width, int physical_height, delete text_renderer_debug_;
int logical_width, int logical_height) { text_renderer_debug_ = nullptr;
delete text_renderer_debug_; text_renderer_debug_ = nullptr; delete text_renderer_notifier_;
delete text_renderer_notifier_; text_renderer_notifier_ = nullptr; text_renderer_notifier_ = nullptr;
delete notifier_; notifier_ = nullptr; delete notifier_;
delete help_overlay_; help_overlay_ = nullptr; notifier_ = nullptr;
delete help_overlay_;
help_overlay_ = nullptr;
renderer_ = renderer; renderer_ = renderer;
theme_manager_ = theme_manager; theme_manager_ = theme_manager;
@@ -95,8 +100,7 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
// Crear y configurar sistema de notificaciones // Crear y configurar sistema de notificaciones
notifier_ = new Notifier(); notifier_ = new Notifier();
notifier_->init(renderer, text_renderer_notifier_, theme_manager_, notifier_->init(renderer, text_renderer_notifier_, theme_manager_, physical_width, physical_height);
physical_width, physical_height);
// Crear y configurar sistema de ayuda (overlay) // Crear y configurar sistema de ayuda (overlay)
help_overlay_ = new HelpOverlay(); help_overlay_ = new HelpOverlay();
@@ -138,15 +142,14 @@ void UIManager::render(SDL_Renderer* renderer,
// Renderizar debug HUD si está activo // Renderizar debug HUD si está activo
if (show_debug_) { if (show_debug_) {
renderDebugHUD(engine, scene_manager, current_mode, current_app_mode, renderDebugHUD(engine, scene_manager, current_mode, current_app_mode, active_shape, shape_convergence);
active_shape, shape_convergence);
} }
// Renderizar notificaciones (siempre al final, sobre todo lo demás) // Renderizar notificaciones (siempre al final, sobre todo lo demás)
notifier_->render(); notifier_->render();
// Renderizar ayuda (siempre última, sobre todo incluso notificaciones) // Renderizar ayuda (siempre última, sobre todo incluso notificaciones)
if (help_overlay_) { if (help_overlay_ != nullptr) {
help_overlay_->render(renderer); help_overlay_->render(renderer);
} }
} }
@@ -156,7 +159,7 @@ void UIManager::toggleDebug() {
} }
void UIManager::toggleHelp() { void UIManager::toggleHelp() {
if (help_overlay_) { if (help_overlay_ != nullptr) {
help_overlay_->toggle(); help_overlay_->toggle();
} }
} }
@@ -190,16 +193,16 @@ void UIManager::updatePhysicalWindowSize(int width, int height, int logical_heig
current_font_size_ = new_font_size; current_font_size_ = new_font_size;
// Reinicializar text renderers con nuevo tamaño // Reinicializar text renderers con nuevo tamaño
if (text_renderer_debug_) { if (text_renderer_debug_ != nullptr) {
text_renderer_debug_->reinitialize(std::max(9, current_font_size_ - 2)); text_renderer_debug_->reinitialize(std::max(9, current_font_size_ - 2));
} }
if (text_renderer_notifier_) { if (text_renderer_notifier_ != nullptr) {
text_renderer_notifier_->reinitialize(current_font_size_); text_renderer_notifier_->reinitialize(current_font_size_);
} }
} }
// Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente) // Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente)
if (help_overlay_) { if (help_overlay_ != nullptr) {
help_overlay_->updateAll(std::max(9, current_font_size_ - 1), width, height); help_overlay_->updateAll(std::max(9, current_font_size_ - 1), width, height);
} }
@@ -209,7 +212,7 @@ void UIManager::updatePhysicalWindowSize(int width, int height, int logical_heig
// === Métodos privados === // === Métodos privados ===
void UIManager::renderDebugHUD(const Engine* engine, void UIManager::renderDebugHUD(const Engine* engine, // NOLINT(readability-function-cognitive-complexity)
const SceneManager* scene_manager, const SceneManager* scene_manager,
SimulationMode current_mode, SimulationMode current_mode,
AppMode current_app_mode, AppMode current_app_mode,
@@ -236,7 +239,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
if (current_mode == SimulationMode::PHYSICS) { if (current_mode == SimulationMode::PHYSICS) {
simmode_text = "SimMode: PHYSICS"; simmode_text = "SimMode: PHYSICS";
} else if (current_mode == SimulationMode::SHAPE) { } else if (current_mode == SimulationMode::SHAPE) {
if (active_shape) { if (active_shape != nullptr) {
simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")"; simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")";
} else { } else {
simmode_text = "SimMode: SHAPE"; simmode_text = "SimMode: SHAPE";
@@ -246,7 +249,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
} }
std::string sprite_name = engine->getCurrentTextureName(); std::string sprite_name = engine->getCurrentTextureName();
std::transform(sprite_name.begin(), sprite_name.end(), sprite_name.begin(), ::toupper); std::ranges::transform(sprite_name, sprite_name.begin(), ::toupper);
std::string sprite_text = "Sprite: " + sprite_name; std::string sprite_text = "Sprite: " + sprite_name;
size_t ball_count = scene_manager->getBallCount(); size_t ball_count = scene_manager->getBallCount();
@@ -256,7 +259,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string formatted; std::string formatted;
int digits = static_cast<int>(count_str.length()); int digits = static_cast<int>(count_str.length());
for (int i = 0; i < digits; i++) { for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) formatted += ','; if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i]; formatted += count_str[i];
} }
balls_text = "Balls: " + formatted; balls_text = "Balls: " + formatted;
@@ -275,7 +280,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string formatted; std::string formatted;
int digits = static_cast<int>(count_str.length()); int digits = static_cast<int>(count_str.length());
for (int i = 0; i < digits; i++) { for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) formatted += ','; if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i]; formatted += count_str[i];
} }
max_auto_text = "Auto max: " + formatted; max_auto_text = "Auto max: " + formatted;
@@ -303,9 +310,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string refresh_text; std::string refresh_text;
int num_displays = 0; int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays && 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) { if (dm != nullptr) {
refresh_text = "Refresh: " + std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz"; refresh_text = "Refresh: " + std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
} else { } else {
refresh_text = "Refresh: N/A"; refresh_text = "Refresh: N/A";
@@ -322,9 +329,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
int hh = static_cast<int>(total_secs / 3600); int hh = static_cast<int>(total_secs / 3600);
int mm = static_cast<int>((total_secs % 3600) / 60); int mm = static_cast<int>((total_secs % 3600) / 60);
int ss = static_cast<int>(total_secs % 60); int ss = static_cast<int>(total_secs % 60);
char elapsed_buf[32]; std::array<char, 32> elapsed_buf{};
SDL_snprintf(elapsed_buf, sizeof(elapsed_buf), "Elapsed: %02d:%02d:%02d", hh, mm, ss); SDL_snprintf(elapsed_buf.data(), elapsed_buf.size(), "Elapsed: %02d:%02d:%02d", hh, mm, ss);
std::string elapsed_text(elapsed_buf); std::string elapsed_text(elapsed_buf.data());
// --- Construir vector de líneas en orden --- // --- Construir vector de líneas en orden ---
std::vector<std::string> lines; std::vector<std::string> lines;
@@ -344,17 +351,15 @@ void UIManager::renderDebugHUD(const Engine* engine,
if (!engine->isPostFXEnabled()) { if (!engine->isPostFXEnabled()) {
postfx_text = "PostFX: OFF"; postfx_text = "PostFX: OFF";
} else { } else {
static constexpr const char* preset_names[4] = { static constexpr std::array<const char*, 4> PRESET_NAMES = {
"Vinyeta", "Scanlines", "Cromatica", "Complet" "Vinyeta",
}; "Scanlines",
"Cromatica",
"Complet"};
int mode = engine->getPostFXMode(); int mode = engine->getPostFXMode();
char buf[64]; std::array<char, 64> buf{};
SDL_snprintf(buf, sizeof(buf), "PostFX: %s [V:%.2f C:%.2f S:%.2f]", SDL_snprintf(buf.data(), buf.size(), "PostFX: %s [V:%.2f C:%.2f S:%.2f]", PRESET_NAMES[mode], engine->getPostFXVignette(), engine->getPostFXChroma(), engine->getPostFXScanline());
preset_names[mode], postfx_text = buf.data();
engine->getPostFXVignette(),
engine->getPostFXChroma(),
engine->getPostFXScanline());
postfx_text = buf;
} }
lines.push_back(postfx_text); lines.push_back(postfx_text);
lines.push_back(elapsed_text); lines.push_back(elapsed_text);
@@ -366,7 +371,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
SDL_FRect pos = first_ball->getPosition(); SDL_FRect pos = first_ball->getPosition();
lines.push_back("Pos: (" + std::to_string(static_cast<int>(pos.x)) + ", " + std::to_string(static_cast<int>(pos.y)) + ")"); lines.push_back("Pos: (" + std::to_string(static_cast<int>(pos.x)) + ", " + std::to_string(static_cast<int>(pos.y)) + ")");
lines.push_back("Gravity: " + std::to_string(static_cast<int>(first_ball->getGravityForce()))); lines.push_back("Gravity: " + std::to_string(static_cast<int>(first_ball->getGravityForce())));
lines.push_back(first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO"); lines.emplace_back(first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO");
lines.push_back("Loss: " + std::to_string(first_ball->getLossCoefficient()).substr(0, 4)); lines.push_back("Loss: " + std::to_string(first_ball->getLossCoefficient()).substr(0, 4));
lines.push_back("Dir: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity()))); lines.push_back("Dir: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity())));
} }
@@ -378,29 +383,34 @@ void UIManager::renderDebugHUD(const Engine* engine,
// --- Render con desbordamiento a segunda columna --- // --- Render con desbordamiento a segunda columna ---
int max_lines = (physical_viewport.h - 2 * margin) / line_height; int max_lines = (physical_viewport.h - 2 * margin) / line_height;
if (max_lines < 1) max_lines = 1; max_lines = std::max(max_lines, 1);
int col_width = physical_viewport.w / 2; int col_width = physical_viewport.w / 2;
for (int i = 0; i < static_cast<int>(lines.size()); i++) { for (int i = 0; i < static_cast<int>(lines.size()); i++) {
int col = i / max_lines; int col = i / max_lines;
int row = i % max_lines; int row = i % max_lines;
int x = margin + col * col_width; int x = margin + (col * col_width);
int y = margin + row * line_height; int y = margin + (row * line_height);
text_renderer_debug_->printAbsoluteShadowed(x, y, lines[i].c_str()); text_renderer_debug_->printAbsoluteShadowed(x, y, lines[i].c_str());
} }
} }
std::string UIManager::gravityDirectionToString(int direction) const { auto UIManager::gravityDirectionToString(int direction) -> std::string {
switch (direction) { switch (direction) {
case 0: return "Abajo"; // DOWN case 0:
case 1: return "Arriba"; // UP return "Abajo"; // DOWN
case 2: return "Izquierda"; // LEFT case 1:
case 3: return "Derecha"; // RIGHT return "Arriba"; // UP
default: return "Desconocida"; case 2:
return "Izquierda"; // LEFT
case 3:
return "Derecha"; // RIGHT
default:
return "Desconocida";
} }
} }
int UIManager::calculateFontSize(int logical_height) const { auto UIManager::calculateFontSize(int logical_height) -> int {
// Escalado híbrido basado en ALTURA LÓGICA (resolución interna, sin zoom) // Escalado híbrido basado en ALTURA LÓGICA (resolución interna, sin zoom)
// Esto asegura que el tamaño de fuente sea consistente independientemente del zoom de ventana // Esto asegura que el tamaño de fuente sea consistente independientemente del zoom de ventana
// - Proporcional en extremos (muy bajo/alto) // - Proporcional en extremos (muy bajo/alto)
@@ -435,8 +445,8 @@ int UIManager::calculateFontSize(int logical_height) const {
} }
// Aplicar límites: mínimo 9px, máximo 72px // Aplicar límites: mínimo 9px, máximo 72px
if (font_size < 9) font_size = 9; font_size = std::max(font_size, 9);
if (font_size > 72) font_size = 72; font_size = std::min(font_size, 72);
return font_size; return font_size;
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL_stdinc.h> // for Uint64 #include <SDL3/SDL_stdinc.h> // for Uint64
#include <string> // for std::string #include <string> // for std::string
// Forward declarations // Forward declarations
@@ -49,9 +50,7 @@ class UIManager {
* @param logical_width Ancho lógico (resolución interna) * @param logical_width Ancho lógico (resolución interna)
* @param logical_height Alto lógico (resolución interna) * @param logical_height Alto lógico (resolución interna)
*/ */
void initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, void initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, int physical_width, int physical_height, int logical_width, int logical_height);
int physical_width, int physical_height,
int logical_width, int logical_height);
/** /**
* @brief Actualiza UI (FPS counter, notificaciones, texto obsoleto) * @brief Actualiza UI (FPS counter, notificaciones, texto obsoleto)
@@ -149,14 +148,14 @@ class UIManager {
* @param direction Dirección como int (cast de GravityDirection) * @param direction Dirección como int (cast de GravityDirection)
* @return String en español ("Abajo", "Arriba", etc.) * @return String en español ("Abajo", "Arriba", etc.)
*/ */
std::string gravityDirectionToString(int direction) const; static std::string gravityDirectionToString(int direction);
/** /**
* @brief Calcula tamaño de fuente apropiado según dimensiones lógicas * @brief Calcula tamaño de fuente apropiado según dimensiones lógicas
* @param logical_height Alto lógico (resolución interna, sin zoom) * @param logical_height Alto lógico (resolución interna, sin zoom)
* @return Tamaño de fuente (9-72px) * @return Tamaño de fuente (9-72px)
*/ */
int calculateFontSize(int logical_height) const; static int calculateFontSize(int logical_height);
// === Recursos de renderizado === // === Recursos de renderizado ===
TextRenderer* text_renderer_debug_; // HUD de debug TextRenderer* text_renderer_debug_; // HUD de debug