#include "png_shape.h" #include "../defines.h" #include "../external/stb_image.h" #include #include #include 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 } } 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(x), static_cast(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(x), static_cast(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 puntos según el enfoque configurado generateExtrudedPoints(); // Calcular cuántos puntos 2D se necesitan size_t num_2d_points = PNG_USE_EDGES_ONLY ? edge_points_.size() : filled_points_.size(); size_t total_3d_points = num_2d_points * num_layers_; // === ADAPTACIÓN AUTOMÁTICA MULTINIVEL === // Nivel 1: Si relleno completo y pocas pelotas → switch a bordes if (!PNG_USE_EDGES_ONLY && num_points < static_cast(total_3d_points) / 2) { std::cout << "[PNG_SHAPE] Paso 1: Cambiando de RELLENO a BORDES (pelotas insuficientes)\n"; detectEdges(); num_2d_points = edge_points_.size(); total_3d_points = num_2d_points * num_layers_; } // Nivel 2: Reducir capas de extrusión si aún hay pocas pelotas while (num_layers_ > 1 && num_points < static_cast(total_3d_points) / 2) { num_layers_ = std::max(1, num_layers_ / 2); // Dividir capas a la mitad total_3d_points = num_2d_points * num_layers_; std::cout << "[PNG_SHAPE] Paso 2: Reduciendo capas a " << num_layers_ << " (total puntos: " << total_3d_points << ")\n"; } // Nivel 3: Sampling de píxeles (tomar cada N píxeles) si aún insuficiente int sampling_step = 1; std::vector* points_to_sample = (!edge_points_.empty()) ? &edge_points_ : &filled_points_; while (sampling_step < 10 && num_points < static_cast(total_3d_points) / 2) { sampling_step++; // Aplicar sampling (tomar cada N puntos) std::vector sampled_points; for (size_t i = 0; i < points_to_sample->size(); i += sampling_step) { sampled_points.push_back((*points_to_sample)[i]); } if (!sampled_points.empty()) { *points_to_sample = sampled_points; num_2d_points = points_to_sample->size(); total_3d_points = num_2d_points * num_layers_; std::cout << "[PNG_SHAPE] Paso 3: Aplicando sampling 1/" << sampling_step << " (puntos: " << num_2d_points << ")\n"; } } // Debug: mostrar configuración final std::cout << "[PNG_SHAPE] === CONFIGURACIÓN FINAL ===\n"; std::string mode_str = (!edge_points_.empty()) ? "BORDES" : (PNG_USE_EDGES_ONLY ? "BORDES" : "RELLENO"); std::cout << "[PNG_SHAPE] Modo: " << mode_str; if (sampling_step > 1) { std::cout << " (optimizado)"; } std::cout << "\n"; std::cout << "[PNG_SHAPE] Píxeles 2D: " << num_2d_points; if (sampling_step > 1) { std::cout << " (sampling 1/" << sampling_step << ")"; } std::cout << "\n"; std::cout << "[PNG_SHAPE] Capas: " << num_layers_ << "\n"; std::cout << "[PNG_SHAPE] Total puntos 3D: " << total_3d_points << "\n"; std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n"; std::cout << "[PNG_SHAPE] Ratio: " << (float)num_points / (float)total_3d_points << " pelotas/punto\n"; // Calcular escala para centrar la imagen en pantalla float max_dimension = std::max(static_cast(image_width_), static_cast(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; } void PNGShape::update(float delta_time, float screen_width, float screen_height) { if (!is_flipping_) { // Estado IDLE: texto de frente idle_timer_ += delta_time; 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 next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_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 { // Seleccionar puntos según configuración (priorizar edges si fue auto-ajustado) const std::vector& points = (!edge_points_.empty()) ? edge_points_ : filled_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(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(num_layers_ - 1); z_base = -extrusion_depth_ * 0.5f + layer_index * layer_step; } // 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), forzar Z positivo (primer plano brillante) if (angle_x_ == 0.0f && angle_y_ == 0.0f) { // De frente: todo en primer plano (Z máximo) z = extrusion_depth_ * 0.5f; // Máximo Z = más brillante } else { z = z_rot; } } float PNGShape::getScaleFactor(float screen_height) const { // Escala dinámica según resolución return PNG_SIZE_FACTOR; }