## 1. Sistema de Estados AppMode (MANUAL/DEMO/DEMO_LITE/LOGO) **engine.h:** - Creado enum AppMode con 4 estados mutuamente excluyentes - Reemplazadas 4 flags booleanas por 2 variables de estado: * current_app_mode_: Modo actual * previous_app_mode_: Para restaurar al salir de LOGO - Añadido método setState() para gestión centralizada **engine.cpp:** - Implementado setState() con configuración automática de timers - Actualizado updateDemoMode() para usar current_app_mode_ - Actualizado handleEvents() para teclas D/L/K con setState() - Actualizadas todas las referencias a flags antiguas (8 ubicaciones) - enterLogoMode/exitLogoMode usan setState() **Comportamiento:** - Teclas D/L/K ahora desactivan otros modos automáticamente - Al salir de LOGO vuelve al modo previo (DEMO/DEMO_LITE/MANUAL) ## 2. Ajuste Ratio DEMO:LOGO = 6:1 **defines.h:** - Probabilidad DEMO→LOGO: 15% → 5% (más raro) - Probabilidad DEMO_LITE→LOGO: 10% → 3% - Probabilidad salir de LOGO: 25% → 60% (sale rápido) - Intervalos LOGO: 4-8s → 3-5s (más corto que DEMO) **Resultado:** DEMO pasa 6x más tiempo activo que LOGO ## 3. Fix PNG_SHAPE no hace flip en modo LOGO **Bugs encontrados:** 1. next_idle_time_ inicializado a 5.0s (hardcoded) > intervalos LOGO (3-5s) 2. toggleShapeMode() recrea PNG_SHAPE → pierde is_logo_mode_=true **Soluciones:** **png_shape.cpp (constructor):** - Inicializa next_idle_time_ con PNG_IDLE_TIME_MIN/MAX (no hardcoded) **png_shape.h:** - Añadidos includes: defines.h, <cstdlib> - Flag is_logo_mode_ para distinguir MANUAL vs LOGO - Expandido setLogoMode() para recalcular next_idle_time_ con rangos apropiados - PNG_IDLE_TIME_MIN_LOGO/MAX_LOGO: 2.5-4.5s (ajustable en defines.h) **engine.cpp (toggleShapeMode):** - Detecta si vuelve a SHAPE en modo LOGO con PNG_SHAPE - Restaura setLogoMode(true) después de recrear instancia **defines.h:** - PNG_IDLE_TIME_MIN/MAX = 0.5-2.0s (modo MANUAL) - PNG_IDLE_TIME_MIN_LOGO/MAX_LOGO = 2.5-4.5s (modo LOGO) **Resultado:** PNG_SHAPE ahora hace flip cada 2.5-4.5s en modo LOGO (visible antes de toggles) ## 4. Nuevas Texturas **data/balls/big.png:** 16x16px (añadida) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
390 lines
14 KiB
C++
390 lines
14 KiB
C++
#include "png_shape.h"
|
|
#include "../defines.h"
|
|
#include "../external/stb_image.h"
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
#include <map>
|
|
|
|
PNGShape::PNGShape(const char* png_path) {
|
|
// Cargar PNG desde path
|
|
if (!loadPNG(png_path)) {
|
|
// Fallback: generar un cuadrado simple si falla la carga
|
|
image_width_ = 10;
|
|
image_height_ = 10;
|
|
pixel_data_.resize(100, true); // Cuadrado 10x10 blanco
|
|
}
|
|
|
|
// Inicializar next_idle_time_ con valores apropiados (no hardcoded 5.0)
|
|
next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
|
|
}
|
|
|
|
bool PNGShape::loadPNG(const char* path) {
|
|
int width, height, channels;
|
|
unsigned char* data = stbi_load(path, &width, &height, &channels, 1); // Forzar 1 canal (grayscale)
|
|
|
|
if (!data) {
|
|
return false;
|
|
}
|
|
|
|
image_width_ = width;
|
|
image_height_ = height;
|
|
pixel_data_.resize(width * height);
|
|
|
|
// Convertir a mapa booleano (true = píxel blanco/visible, false = negro/transparente)
|
|
for (int i = 0; i < width * height; i++) {
|
|
pixel_data_[i] = (data[i] > 128); // Umbral: >128 = blanco
|
|
}
|
|
|
|
stbi_image_free(data);
|
|
return true;
|
|
}
|
|
|
|
void PNGShape::detectEdges() {
|
|
edge_points_.clear();
|
|
|
|
// Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro)
|
|
for (int y = 0; y < image_height_; y++) {
|
|
for (int x = 0; x < image_width_; x++) {
|
|
int idx = y * image_width_ + x;
|
|
|
|
if (!pixel_data_[idx]) continue; // Solo píxeles blancos
|
|
|
|
// Verificar vecinos (arriba, abajo, izq, der)
|
|
bool is_edge = false;
|
|
|
|
if (x == 0 || x == image_width_ - 1 || y == 0 || y == image_height_ - 1) {
|
|
is_edge = true; // Bordes de la imagen
|
|
} else {
|
|
// Verificar 4 vecinos
|
|
if (!pixel_data_[idx - 1] || // Izquierda
|
|
!pixel_data_[idx + 1] || // Derecha
|
|
!pixel_data_[idx - image_width_] || // Arriba
|
|
!pixel_data_[idx + image_width_]) { // Abajo
|
|
is_edge = true;
|
|
}
|
|
}
|
|
|
|
if (is_edge) {
|
|
edge_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PNGShape::floodFill() {
|
|
// TODO: Implementar flood-fill para Enfoque B (voxelización)
|
|
// Por ahora, rellenar con todos los píxeles blancos
|
|
filled_points_.clear();
|
|
|
|
for (int y = 0; y < image_height_; y++) {
|
|
for (int x = 0; x < image_width_; x++) {
|
|
int idx = y * image_width_ + x;
|
|
if (pixel_data_[idx]) {
|
|
filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PNGShape::generateExtrudedPoints() {
|
|
if (PNG_USE_EDGES_ONLY) {
|
|
// Usar solo bordes (contorno) de las letras
|
|
detectEdges();
|
|
} else {
|
|
// Usar relleno completo (todos los píxeles blancos)
|
|
floodFill();
|
|
}
|
|
}
|
|
|
|
void PNGShape::generatePoints(int num_points, float screen_width, float screen_height) {
|
|
num_points_ = num_points;
|
|
extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR;
|
|
num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
|
|
|
|
// Generar AMBOS conjuntos de puntos (relleno Y bordes)
|
|
floodFill(); // Generar filled_points_
|
|
detectEdges(); // Generar edge_points_
|
|
|
|
// Guardar copias originales (las funciones de filtrado modifican los vectores)
|
|
std::vector<Point2D> filled_points_original = filled_points_;
|
|
std::vector<Point2D> edge_points_original = edge_points_;
|
|
|
|
// Conjunto de puntos ACTIVO (será modificado por filtros)
|
|
std::vector<Point2D> active_points_data;
|
|
std::string mode_name = "";
|
|
|
|
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
|
|
// Estrategia: Optimizar según número de pelotas disponibles
|
|
// Objetivo: SIEMPRE intentar usar relleno primero, solo bordes si es necesario
|
|
|
|
size_t num_2d_points = 0;
|
|
size_t total_3d_points = 0;
|
|
|
|
// NIVEL 1: Decidir punto de partida (relleno o bordes por configuración)
|
|
if (PNG_USE_EDGES_ONLY) {
|
|
active_points_data = edge_points_original;
|
|
mode_name = "BORDES (config)";
|
|
} else {
|
|
active_points_data = filled_points_original;
|
|
mode_name = "RELLENO";
|
|
}
|
|
|
|
num_2d_points = active_points_data.size();
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
|
|
// NIVEL 2: Reducir capas AGRESIVAMENTE hasta 1 (priorizar calidad 2D sobre profundidad 3D)
|
|
// Objetivo: Llenar bien el texto en 2D antes de reducir píxeles
|
|
while (num_layers_ > 1 && num_points < static_cast<int>(total_3d_points)) {
|
|
num_layers_ = std::max(1, num_layers_ / 2);
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
}
|
|
|
|
// NIVEL 3: Filas alternas en RELLENO (solo si 1 capa no alcanza)
|
|
// Esto permite usar relleno incluso con pocas pelotas
|
|
int row_skip = 1;
|
|
if (!PNG_USE_EDGES_ONLY) { // Solo si empezamos con relleno
|
|
while (row_skip < 5 && num_points < static_cast<int>(total_3d_points)) {
|
|
row_skip++;
|
|
// ✅ CLAVE: Recalcular desde el ORIGINAL cada vez (no desde el filtrado previo)
|
|
active_points_data = extractAlternateRows(filled_points_original, row_skip);
|
|
num_2d_points = active_points_data.size();
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
mode_name = "RELLENO + FILAS/" + std::to_string(row_skip);
|
|
}
|
|
}
|
|
|
|
// NIVEL 4: Cambiar a BORDES (solo si relleno con filas alternas no alcanza)
|
|
if (!PNG_USE_EDGES_ONLY && num_points < static_cast<int>(total_3d_points)) {
|
|
active_points_data = edge_points_original;
|
|
mode_name = "BORDES (auto)";
|
|
num_2d_points = active_points_data.size();
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
row_skip = 1; // Reset row_skip para bordes
|
|
}
|
|
|
|
// NIVEL 5: Filas alternas en BORDES (si aún no alcanza)
|
|
while (row_skip < 8 && num_points < static_cast<int>(total_3d_points)) {
|
|
row_skip++;
|
|
// ✅ CLAVE: Recalcular desde edge_points_original cada vez
|
|
active_points_data = extractAlternateRows(edge_points_original, row_skip);
|
|
num_2d_points = active_points_data.size();
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
if (mode_name.find("FILAS") == std::string::npos) {
|
|
mode_name += " + FILAS/" + std::to_string(row_skip);
|
|
}
|
|
}
|
|
|
|
// NIVEL 6: Vértices/esquinas (último recurso, muy pocas pelotas)
|
|
if (num_points < static_cast<int>(total_3d_points) && num_points < 150) {
|
|
// Determinar desde qué conjunto extraer vértices (el que esté activo actualmente)
|
|
const std::vector<Point2D>& source_for_vertices = (mode_name.find("BORDES") != std::string::npos)
|
|
? edge_points_original
|
|
: filled_points_original;
|
|
|
|
std::vector<Point2D> vertices = extractCornerVertices(source_for_vertices);
|
|
if (!vertices.empty() && vertices.size() < active_points_data.size()) {
|
|
active_points_data = vertices;
|
|
num_2d_points = active_points_data.size();
|
|
total_3d_points = num_2d_points * num_layers_;
|
|
mode_name = "VÉRTICES";
|
|
}
|
|
}
|
|
|
|
// ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D)
|
|
optimized_points_ = active_points_data;
|
|
|
|
// Calcular escala para centrar la imagen en pantalla
|
|
float max_dimension = std::max(static_cast<float>(image_width_), static_cast<float>(image_height_));
|
|
scale_factor_ = (screen_height * PNG_SIZE_FACTOR) / max_dimension;
|
|
|
|
// Calcular offset para centrar
|
|
center_offset_x_ = image_width_ * 0.5f;
|
|
center_offset_y_ = image_height_ * 0.5f;
|
|
}
|
|
|
|
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
|
|
// Recibe vector original y devuelve nuevo vector filtrado
|
|
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) {
|
|
std::vector<Point2D> result;
|
|
|
|
if (row_skip <= 1 || source.empty()) {
|
|
return source; // Sin filtrado, devolver copia del original
|
|
}
|
|
|
|
// Organizar puntos por fila (Y)
|
|
std::map<int, std::vector<Point2D>> rows;
|
|
for (const auto& p : source) {
|
|
int row = static_cast<int>(p.y);
|
|
rows[row].push_back(p);
|
|
}
|
|
|
|
// Tomar solo cada N filas
|
|
int row_counter = 0;
|
|
for (const auto& [row_y, row_points] : rows) {
|
|
if (row_counter % row_skip == 0) {
|
|
result.insert(result.end(), row_points.begin(), row_points.end());
|
|
}
|
|
row_counter++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
|
|
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) {
|
|
std::vector<Point2D> result;
|
|
|
|
if (source.empty()) {
|
|
return result;
|
|
}
|
|
|
|
// Estrategia simple: tomar bordes extremos de cada fila
|
|
// Esto da el "esqueleto" mínimo de las letras
|
|
|
|
std::map<int, std::pair<float, float>> row_extremes; // Y -> (min_x, max_x)
|
|
|
|
for (const auto& p : source) {
|
|
int row = static_cast<int>(p.y);
|
|
if (row_extremes.find(row) == row_extremes.end()) {
|
|
row_extremes[row] = {p.x, p.x};
|
|
} else {
|
|
row_extremes[row].first = std::min(row_extremes[row].first, p.x);
|
|
row_extremes[row].second = std::max(row_extremes[row].second, p.x);
|
|
}
|
|
}
|
|
|
|
// Generar puntos en extremos de cada fila
|
|
for (const auto& [row_y, extremes] : row_extremes) {
|
|
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
|
|
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
|
|
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void PNGShape::update(float delta_time, float screen_width, float screen_height) {
|
|
if (!is_flipping_) {
|
|
// Estado IDLE: texto de frente con pivoteo sutil (como WAVE_GRID)
|
|
idle_timer_ += delta_time;
|
|
|
|
// Pivoteo sutil constante (movimiento orgánico)
|
|
tilt_x_ += 0.4f * delta_time; // Velocidad sutil en X
|
|
tilt_y_ += 0.6f * delta_time; // Velocidad sutil en Y
|
|
|
|
if (idle_timer_ >= next_idle_time_) {
|
|
// Iniciar voltereta
|
|
is_flipping_ = true;
|
|
flip_timer_ = 0.0f;
|
|
idle_timer_ = 0.0f;
|
|
|
|
// Elegir eje aleatorio (0=X, 1=Y, 2=ambos)
|
|
flip_axis_ = rand() % 3;
|
|
|
|
// Próximo tiempo idle aleatorio (según modo LOGO o MANUAL)
|
|
float idle_min = is_logo_mode_ ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN;
|
|
float idle_max = is_logo_mode_ ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
|
|
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
|
|
}
|
|
} else {
|
|
// Estado FLIP: voltereta en curso
|
|
flip_timer_ += delta_time;
|
|
|
|
// Rotar según eje elegido
|
|
if (flip_axis_ == 0 || flip_axis_ == 2) {
|
|
angle_x_ += PNG_FLIP_SPEED * delta_time;
|
|
}
|
|
if (flip_axis_ == 1 || flip_axis_ == 2) {
|
|
angle_y_ += PNG_FLIP_SPEED * delta_time;
|
|
}
|
|
|
|
// Terminar voltereta
|
|
if (flip_timer_ >= PNG_FLIP_DURATION) {
|
|
is_flipping_ = false;
|
|
// Resetear ángulos a 0 (volver de frente)
|
|
angle_x_ = 0.0f;
|
|
angle_y_ = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
|
|
// Usar SIEMPRE el vector optimizado (resultado final de generatePoints)
|
|
const std::vector<Point2D>& points = optimized_points_;
|
|
|
|
if (points.empty()) {
|
|
x = y = z = 0.0f;
|
|
return;
|
|
}
|
|
|
|
// ENFOQUE A: Extrusión 2D
|
|
// Cada punto 2D se replica en múltiples capas Z
|
|
|
|
int num_2d_points = static_cast<int>(points.size());
|
|
int point_2d_index = index % num_2d_points;
|
|
int layer_index = (index / num_2d_points) % num_layers_;
|
|
|
|
// Obtener coordenadas 2D del píxel
|
|
Point2D p = points[point_2d_index];
|
|
|
|
// Centrar y escalar
|
|
float x_base = (p.x - center_offset_x_) * scale_factor_;
|
|
float y_base = (p.y - center_offset_y_) * scale_factor_;
|
|
|
|
// Calcular Z según capa (distribuir uniformemente en profundidad)
|
|
float z_base = 0.0f;
|
|
if (num_layers_ > 1) {
|
|
float layer_step = extrusion_depth_ / static_cast<float>(num_layers_ - 1);
|
|
z_base = -extrusion_depth_ * 0.5f + layer_index * layer_step;
|
|
}
|
|
|
|
// Añadir pivoteo sutil en estado IDLE (similar a WAVE_GRID)
|
|
// Calcular tamaño del logo en pantalla para normalizar correctamente
|
|
float logo_width = image_width_ * scale_factor_;
|
|
float logo_height = image_height_ * scale_factor_;
|
|
float logo_size = std::max(logo_width, logo_height);
|
|
|
|
// Normalizar coordenadas a rango [-1, 1]
|
|
float u = x_base / (logo_size * 0.5f);
|
|
float v = y_base / (logo_size * 0.5f);
|
|
|
|
// Calcular pivoteo (amplitudes más grandes, similar a WAVE_GRID)
|
|
float tilt_amount_x = sinf(tilt_x_) * 0.15f; // 15% como WAVE_GRID
|
|
float tilt_amount_y = sinf(tilt_y_) * 0.1f; // 10% como WAVE_GRID
|
|
|
|
// Aplicar pivoteo proporcional al tamaño del logo
|
|
float z_tilt = (u * tilt_amount_y + v * tilt_amount_x) * logo_size;
|
|
z_base += z_tilt; // Añadir pivoteo sutil a la profundidad
|
|
|
|
// Aplicar rotación en eje Y (horizontal)
|
|
float cos_y = cosf(angle_y_);
|
|
float sin_y = sinf(angle_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;
|
|
|
|
// Aplicar rotación en eje X (vertical)
|
|
float cos_x = cosf(angle_x_);
|
|
float sin_x = sinf(angle_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;
|
|
|
|
// Retornar coordenadas finales
|
|
x = x_rot_y;
|
|
y = y_rot;
|
|
|
|
// Cuando está de frente (sin rotación), usar Z con pivoteo sutil
|
|
if (angle_x_ == 0.0f && angle_y_ == 0.0f) {
|
|
// De frente: usar z_base que incluye el pivoteo sutil
|
|
z = z_base;
|
|
} else {
|
|
z = z_rot;
|
|
}
|
|
}
|
|
|
|
float PNGShape::getScaleFactor(float screen_height) const {
|
|
// Escala dinámica según resolución
|
|
return PNG_SIZE_FACTOR;
|
|
}
|