Files
vibe3_physics/source/ui/ui_manager.cpp
Sergio Valor 9909d4c12d feat: Convertir BOIDS a sistema time-based (independiente de framerate)
- Conversión completa de físicas BOIDS de frame-based a time-based
- Velocidades: ×60 (px/frame → px/s)
- Aceleraciones (Separation, Cohesion): ×3600 (px/frame² → px/s²)
- Steering proporcional (Alignment): ×60
- Límites de velocidad: ×60

Constantes actualizadas en defines.h:
- BOID_SEPARATION_WEIGHT: 1.5 → 5400.0 (aceleración)
- BOID_COHESION_WEIGHT: 0.001 → 3.6 (aceleración)
- BOID_ALIGNMENT_WEIGHT: 1.0 → 60.0 (steering)
- BOID_MAX_SPEED: 2.5 → 150.0 px/s
- BOID_MIN_SPEED: 0.3 → 18.0 px/s
- BOID_MAX_FORCE: 0.05 → 3.0 px/s

Física ahora consistente en 60Hz, 144Hz, 240Hz screens.
Transiciones BOIDS↔PHYSICS preservan velocidad correctamente.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 20:05:49 +02:00

475 lines
20 KiB
C++

#include "ui_manager.h"
#include <SDL3/SDL.h>
#include <string>
#include "../ball.h" // for Ball
#include "../defines.h" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode
#include "../engine.h" // for Engine (info de sistema)
#include "../scene/scene_manager.h" // for SceneManager
#include "../shapes/shape.h" // for Shape
#include "../text/textrenderer.h" // for TextRenderer
#include "../theme_manager.h" // for ThemeManager
#include "notifier.h" // for Notifier
#include "help_overlay.h" // for HelpOverlay
// ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas)
// ============================================================================
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
// Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
// Deshabilitar presentación lógica temporalmente
SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
// Obtener viewport en coordenadas físicas (píxeles reales)
SDL_Rect physical_viewport;
SDL_GetRenderViewport(renderer, &physical_viewport);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer, logical_w, logical_h, presentation_mode);
return physical_viewport;
}
UIManager::UIManager()
: text_renderer_(nullptr)
, text_renderer_debug_(nullptr)
, text_renderer_notifier_(nullptr)
, notifier_(nullptr)
, help_overlay_(nullptr)
, show_debug_(false)
, show_text_(true)
, text_()
, text_pos_(0)
, text_init_time_(0)
, fps_last_time_(0)
, fps_frame_count_(0)
, fps_current_(0)
, fps_text_("FPS: 0")
, vsync_text_("VSYNC ON")
, renderer_(nullptr)
, theme_manager_(nullptr)
, physical_window_width_(0)
, physical_window_height_(0)
, current_font_size_(18) { // Tamaño por defecto (medium)
}
UIManager::~UIManager() {
// Limpieza: Los objetos creados con new deben ser eliminados
delete text_renderer_;
delete text_renderer_debug_;
delete text_renderer_notifier_;
delete notifier_;
delete help_overlay_;
}
void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
int physical_width, int physical_height) {
renderer_ = renderer;
theme_manager_ = theme_manager;
physical_window_width_ = physical_width;
physical_window_height_ = physical_height;
// Calcular tamaño de fuente apropiado según dimensiones físicas
current_font_size_ = calculateFontSize(physical_width, physical_height);
// Crear renderers de texto
text_renderer_ = new TextRenderer();
text_renderer_debug_ = new TextRenderer();
text_renderer_notifier_ = new TextRenderer();
// Inicializar renderers con tamaño dinámico
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
// Crear y configurar sistema de notificaciones
notifier_ = new Notifier();
notifier_->init(renderer, text_renderer_notifier_, theme_manager_,
physical_width, physical_height);
// Crear y configurar sistema de ayuda (overlay)
help_overlay_ = new HelpOverlay();
help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height, current_font_size_);
// Inicializar FPS counter
fps_last_time_ = SDL_GetTicks();
fps_frame_count_ = 0;
fps_current_ = 0;
}
void UIManager::update(Uint64 current_time, float delta_time) {
// Calcular FPS
fps_frame_count_++;
if (current_time - fps_last_time_ >= 1000) { // Actualizar cada segundo
fps_current_ = fps_frame_count_;
fps_frame_count_ = 0;
fps_last_time_ = current_time;
fps_text_ = "fps: " + std::to_string(fps_current_);
}
// Actualizar texto obsoleto (DEPRECATED)
if (show_text_) {
show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION);
}
// Actualizar sistema de notificaciones
notifier_->update(current_time);
}
void UIManager::render(SDL_Renderer* renderer,
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence,
int physical_width,
int physical_height,
int current_screen_width) {
// Actualizar dimensiones físicas (puede cambiar en fullscreen)
physical_window_width_ = physical_width;
physical_window_height_ = physical_height;
// Renderizar texto obsoleto centrado (DEPRECATED - mantener temporalmente)
if (show_text_) {
renderObsoleteText(current_screen_width);
}
// Renderizar debug HUD si está activo
if (show_debug_) {
renderDebugHUD(engine, scene_manager, current_mode, current_app_mode,
active_shape, shape_convergence);
}
// Renderizar notificaciones (siempre al final, sobre todo lo demás)
notifier_->render();
// Renderizar ayuda (siempre última, sobre todo incluso notificaciones)
if (help_overlay_) {
help_overlay_->render(renderer);
}
}
void UIManager::toggleDebug() {
show_debug_ = !show_debug_;
}
void UIManager::toggleHelp() {
if (help_overlay_) {
help_overlay_->toggle();
}
}
void UIManager::showNotification(const std::string& text, Uint64 duration) {
if (duration == 0) {
duration = NOTIFICATION_DURATION;
}
notifier_->show(text, duration);
}
void UIManager::updateVSyncText(bool enabled) {
vsync_text_ = enabled ? "V-Sync: On" : "V-Sync: Off";
}
void UIManager::updatePhysicalWindowSize(int width, int height) {
physical_window_width_ = width;
physical_window_height_ = height;
// Calcular nuevo tamaño de fuente apropiado
int new_font_size = calculateFontSize(width, height);
// Si el tamaño cambió, reinicializar todos los text renderers
if (new_font_size != current_font_size_) {
current_font_size_ = new_font_size;
// Reinicializar text renderers con nuevo tamaño
if (text_renderer_) {
text_renderer_->reinitialize(current_font_size_);
}
if (text_renderer_debug_) {
text_renderer_debug_->reinitialize(current_font_size_);
}
if (text_renderer_notifier_) {
text_renderer_notifier_->reinitialize(current_font_size_);
}
}
// Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente)
if (help_overlay_) {
help_overlay_->updateAll(current_font_size_, width, height);
}
// Actualizar otros componentes de UI con nuevas dimensiones
notifier_->updateWindowSize(width, height);
}
void UIManager::setTextObsolete(const std::string& text, int pos, int current_screen_width) {
text_ = text;
text_pos_ = pos;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
}
// === Métodos privados ===
void UIManager::renderDebugHUD(const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence) {
// Obtener altura de línea para espaciado dinámico
int line_height = text_renderer_debug_->getTextHeight();
int margin = 8; // Margen constante en píxeles físicos
// Obtener viewport FÍSICO (píxeles reales, no lógicos)
// CRÍTICO: En F3, SDL_GetRenderViewport() devuelve coordenadas LÓGICAS,
// pero printAbsolute() trabaja en píxeles FÍSICOS. Usar helper para obtener
// viewport en coordenadas físicas.
SDL_Rect physical_viewport = getPhysicalViewport(renderer_);
// ===========================
// COLUMNA LEFT (Sistema)
// ===========================
int left_y = margin;
// AppMode (antes estaba centrado, ahora va a la izquierda)
std::string appmode_text;
SDL_Color appmode_color = {255, 255, 255, 255}; // Blanco por defecto
if (current_app_mode == AppMode::LOGO) {
appmode_text = "AppMode: LOGO";
appmode_color = {255, 128, 0, 255}; // Naranja
} else if (current_app_mode == AppMode::DEMO) {
appmode_text = "AppMode: DEMO";
appmode_color = {255, 165, 0, 255}; // Naranja
} else if (current_app_mode == AppMode::DEMO_LITE) {
appmode_text = "AppMode: DEMO LITE";
appmode_color = {255, 200, 0, 255}; // Amarillo-naranja
} else {
appmode_text = "AppMode: SANDBOX";
appmode_color = {0, 255, 128, 255}; // Verde claro
}
text_renderer_debug_->printAbsolute(margin, left_y, appmode_text.c_str(), appmode_color);
left_y += line_height;
// SimulationMode
std::string simmode_text;
if (current_mode == SimulationMode::PHYSICS) {
simmode_text = "SimMode: PHYSICS";
} else if (current_mode == SimulationMode::SHAPE) {
if (active_shape) {
simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")";
} else {
simmode_text = "SimMode: SHAPE";
}
} else if (current_mode == SimulationMode::BOIDS) {
simmode_text = "SimMode: BOIDS";
}
text_renderer_debug_->printAbsolute(margin, left_y, simmode_text.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height;
// Número de pelotas (escenario actual)
size_t ball_count = scene_manager->getBallCount();
std::string balls_text;
if (ball_count >= 1000) {
// Formatear con separador de miles (ejemplo: 5,000 o 50,000)
std::string count_str = std::to_string(ball_count);
std::string formatted;
int digits = count_str.length();
for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i];
}
balls_text = "Balls: " + formatted;
} else {
balls_text = "Balls: " + std::to_string(ball_count);
}
text_renderer_debug_->printAbsolute(margin, left_y, balls_text.c_str(), {128, 255, 128, 255}); // Verde claro
left_y += line_height;
// V-Sync
text_renderer_debug_->printAbsolute(margin, left_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height;
// Modo de escalado (INTEGER/LETTERBOX/STRETCH o WINDOWED si no está en fullscreen)
std::string scaling_text;
if (engine->getFullscreenEnabled() || engine->getRealFullscreenEnabled()) {
ScalingMode scaling = engine->getCurrentScalingMode();
if (scaling == ScalingMode::INTEGER) {
scaling_text = "Scaling: INTEGER";
} else if (scaling == ScalingMode::LETTERBOX) {
scaling_text = "Scaling: LETTERBOX";
} else if (scaling == ScalingMode::STRETCH) {
scaling_text = "Scaling: STRETCH";
}
} else {
scaling_text = "Scaling: WINDOWED";
}
text_renderer_debug_->printAbsolute(margin, left_y, scaling_text.c_str(), {255, 255, 0, 255}); // Amarillo
left_y += line_height;
// Resolución física (píxeles reales de la ventana)
std::string phys_res_text = "Physical: " + std::to_string(physical_window_width_) + "x" + std::to_string(physical_window_height_);
text_renderer_debug_->printAbsolute(margin, left_y, phys_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro
left_y += line_height;
// Resolución lógica (resolución interna del renderizador)
std::string logic_res_text = "Logical: " + std::to_string(engine->getCurrentScreenWidth()) + "x" + std::to_string(engine->getCurrentScreenHeight());
text_renderer_debug_->printAbsolute(margin, left_y, logic_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro
left_y += line_height;
// Display refresh rate (obtener de SDL)
std::string refresh_text;
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm) {
refresh_text = "Refresh: " + std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
} else {
refresh_text = "Refresh: N/A";
}
SDL_free(displays);
} else {
refresh_text = "Refresh: N/A";
}
text_renderer_debug_->printAbsolute(margin, left_y, refresh_text.c_str(), {255, 255, 128, 255}); // Amarillo claro
left_y += line_height;
// Tema actual (delegado a ThemeManager)
std::string theme_text = std::string("Theme: ") + theme_manager_->getCurrentThemeNameEN();
text_renderer_debug_->printAbsolute(margin, left_y, theme_text.c_str(), {128, 255, 255, 255}); // Cian claro
left_y += line_height;
// ===========================
// COLUMNA RIGHT (Primera pelota)
// ===========================
int right_y = margin;
// FPS counter (esquina superior derecha)
int fps_text_width = text_renderer_debug_->getTextWidthPhysical(fps_text_.c_str());
int fps_x = physical_viewport.w - fps_text_width - margin;
text_renderer_debug_->printAbsolute(fps_x, right_y, fps_text_.c_str(), {255, 255, 0, 255}); // Amarillo
right_y += line_height;
// Info de la primera pelota (si existe)
const Ball* first_ball = scene_manager->getFirstBall();
if (first_ball != nullptr) {
// Posición X, Y
SDL_FRect pos = first_ball->getPosition();
std::string pos_text = "Pos: (" + std::to_string(static_cast<int>(pos.x)) + ", " + std::to_string(static_cast<int>(pos.y)) + ")";
int pos_width = text_renderer_debug_->getTextWidthPhysical(pos_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - pos_width - margin, right_y, pos_text.c_str(), {255, 128, 128, 255}); // Rojo claro
right_y += line_height;
// Velocidad X
int vx_int = static_cast<int>(first_ball->getVelocityX());
std::string vx_text = "VelX: " + std::to_string(vx_int);
int vx_width = text_renderer_debug_->getTextWidthPhysical(vx_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - vx_width - margin, right_y, vx_text.c_str(), {128, 255, 128, 255}); // Verde claro
right_y += line_height;
// Velocidad Y
int vy_int = static_cast<int>(first_ball->getVelocityY());
std::string vy_text = "VelY: " + std::to_string(vy_int);
int vy_width = text_renderer_debug_->getTextWidthPhysical(vy_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - vy_width - margin, right_y, vy_text.c_str(), {128, 255, 128, 255}); // Verde claro
right_y += line_height;
// Fuerza de gravedad
int grav_int = static_cast<int>(first_ball->getGravityForce());
std::string grav_text = "Gravity: " + std::to_string(grav_int);
int grav_width = text_renderer_debug_->getTextWidthPhysical(grav_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - grav_width - margin, right_y, grav_text.c_str(), {255, 255, 128, 255}); // Amarillo claro
right_y += line_height;
// Estado superficie
std::string surface_text = first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO";
int surface_width = text_renderer_debug_->getTextWidthPhysical(surface_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - surface_width - margin, right_y, surface_text.c_str(), {255, 200, 128, 255}); // Naranja claro
right_y += line_height;
// Coeficiente de rebote (loss)
float loss_val = first_ball->getLossCoefficient();
std::string loss_text = "Loss: " + std::to_string(loss_val).substr(0, 4);
int loss_width = text_renderer_debug_->getTextWidthPhysical(loss_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - loss_width - margin, right_y, loss_text.c_str(), {255, 128, 255, 255}); // Magenta
right_y += line_height;
// Dirección de gravedad
std::string gravity_dir_text = "Dir: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity()));
int dir_width = text_renderer_debug_->getTextWidthPhysical(gravity_dir_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - dir_width - margin, right_y, gravity_dir_text.c_str(), {128, 255, 255, 255}); // Cian claro
right_y += line_height;
}
// Convergencia en modo LOGO (solo cuando está activo) - Parte inferior derecha
if (current_app_mode == AppMode::LOGO && current_mode == SimulationMode::SHAPE) {
int convergence_percent = static_cast<int>(shape_convergence * 100.0f);
std::string convergence_text = "Convergence: " + std::to_string(convergence_percent) + "%";
int conv_width = text_renderer_debug_->getTextWidthPhysical(convergence_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - conv_width - margin, right_y, convergence_text.c_str(), {255, 128, 0, 255}); // Naranja
right_y += line_height;
}
}
void UIManager::renderObsoleteText(int current_screen_width) {
// DEPRECATED: Sistema antiguo de texto centrado
// Mantener por compatibilidad temporal hasta migrar todo a Notifier
// Calcular escala dinámica basada en resolución física
float text_scale_x = static_cast<float>(physical_window_width_) / 426.0f;
float text_scale_y = static_cast<float>(physical_window_height_) / 240.0f;
// Obtener color del tema actual (LERP interpolado)
int margin = 8;
Color text_color = theme_manager_->getInterpolatedColor(0);
int text_color_r = text_color.r;
int text_color_g = text_color.g;
int text_color_b = text_color.b;
// Renderizar texto centrado usando coordenadas físicas
text_renderer_->printPhysical(text_pos_, margin, text_.c_str(),
text_color_r, text_color_g, text_color_b,
text_scale_x, text_scale_y);
}
std::string UIManager::gravityDirectionToString(int direction) const {
switch (direction) {
case 0: return "Abajo"; // DOWN
case 1: return "Arriba"; // UP
case 2: return "Izquierda"; // LEFT
case 3: return "Derecha"; // RIGHT
default: return "Desconocida";
}
}
int UIManager::calculateFontSize(int physical_width, int physical_height) const {
// Calcular área física de la ventana
int area = physical_width * physical_height;
// Stepped scaling con 3 tamaños:
// - SMALL: < 800x600 (480,000 pixels) → 14px
// - MEDIUM: 800x600 a 1920x1080 (2,073,600 pixels) → 18px
// - LARGE: > 1920x1080 → 24px
if (area < 480000) {
return 14; // Ventanas pequeñas
} else if (area < 2073600) {
return 18; // Ventanas medianas (default)
} else {
return 24; // Ventanas grandes
}
}