Compare commits
43 Commits
2d1f4195dc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 44509023dc | |||
| 2d2e338c7a | |||
| e5fdbd54ff | |||
| c9bcce6f9b | |||
| 4801f287df | |||
| 7af77fb3dd | |||
| 96dc964d6a | |||
| 2846987450 | |||
| e13905567d | |||
| 9ae851d5b6 | |||
| e1f6fd0f39 | |||
| 093b982e01 | |||
| 74d954df1e | |||
| 46b24bf075 | |||
| 33cb995872 | |||
| c40eb69fc1 | |||
| 1d2e9c5035 | |||
| f71f7cd5ed | |||
| dcea4ebbab | |||
| b9b5f0b29f | |||
| 200672756c | |||
| f3b029c5b6 | |||
| e46c3eb4ba | |||
| ea05e1eb2e | |||
| c052b45a60 | |||
| 8dde13409b | |||
| 5b4a970157 | |||
| f272bab296 | |||
| e3f29c864b | |||
| d76c7f75a2 | |||
|
|
0678a38a32 | ||
| 6ffe7594ab | |||
| 50926df97c | |||
| 5c0d0479ad | |||
| a51072db32 | |||
| d2e7f2ff86 | |||
| badf92420b | |||
| 310c6d244e | |||
| af0276255e | |||
| 00a5875c92 | |||
| 736db8cf41 | |||
| 821eba3483 | |||
| 6409b61bd5 |
@@ -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
|
||||||
|
|||||||
7
.vscode/c_cpp_properties.json
vendored
7
.vscode/c_cpp_properties.json
vendored
@@ -5,6 +5,7 @@
|
|||||||
"includePath": [
|
"includePath": [
|
||||||
"${workspaceFolder}/source",
|
"${workspaceFolder}/source",
|
||||||
"${workspaceFolder}/source/external",
|
"${workspaceFolder}/source/external",
|
||||||
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
"${env:HOMEBREW_PREFIX}/include",
|
"${env:HOMEBREW_PREFIX}/include",
|
||||||
"/opt/homebrew/include"
|
"/opt/homebrew/include"
|
||||||
],
|
],
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"includePath": [
|
"includePath": [
|
||||||
"${workspaceFolder}/source",
|
"${workspaceFolder}/source",
|
||||||
"${workspaceFolder}/source/external",
|
"${workspaceFolder}/source/external",
|
||||||
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
"/usr/include",
|
"/usr/include",
|
||||||
"/usr/local/include"
|
"/usr/local/include"
|
||||||
],
|
],
|
||||||
@@ -41,13 +43,14 @@
|
|||||||
"includePath": [
|
"includePath": [
|
||||||
"${workspaceFolder}/source",
|
"${workspaceFolder}/source",
|
||||||
"${workspaceFolder}/source/external",
|
"${workspaceFolder}/source/external",
|
||||||
"C:/mingw64/include"
|
"${workspaceFolder}/build/generated_shaders",
|
||||||
|
"C:/mingw/include"
|
||||||
],
|
],
|
||||||
"defines": [
|
"defines": [
|
||||||
"WINDOWS_BUILD",
|
"WINDOWS_BUILD",
|
||||||
"_WIN32"
|
"_WIN32"
|
||||||
],
|
],
|
||||||
"compilerPath": "C:/msys64/mingw64/bin/g++.exe",
|
"compilerPath": "C:/mingw/bin/g++.exe",
|
||||||
"cStandard": "c17",
|
"cStandard": "c17",
|
||||||
"cppStandard": "c++20",
|
"cppStandard": "c++20",
|
||||||
"intelliSenseMode": "windows-gcc-x64"
|
"intelliSenseMode": "windows-gcc-x64"
|
||||||
|
|||||||
@@ -11,23 +11,59 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Os -ffunction-sections -fdata-sec
|
|||||||
# Buscar SDL3 automáticamente
|
# Buscar SDL3 automáticamente
|
||||||
find_package(SDL3 REQUIRED)
|
find_package(SDL3 REQUIRED)
|
||||||
|
|
||||||
# Si no se encuentra SDL3, generar un error
|
|
||||||
if (NOT SDL3_FOUND)
|
|
||||||
message(FATAL_ERROR "SDL3 no encontrado. Por favor, verifica su instalación.")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Buscar SDL3_ttf
|
# Buscar SDL3_ttf
|
||||||
find_package(SDL3_ttf REQUIRED)
|
find_package(SDL3_ttf REQUIRED)
|
||||||
|
|
||||||
# Si no se encuentra SDL3_ttf, generar un error
|
# ---- Shader compilation (non-Apple only: Vulkan/SPIRV) ----
|
||||||
if (NOT SDL3_ttf_FOUND)
|
if(NOT APPLE)
|
||||||
message(FATAL_ERROR "SDL3_ttf no encontrado. Por favor, verifica su instalación.")
|
find_program(GLSLC glslc HINTS "$ENV{VULKAN_SDK}/bin" "$ENV{VULKAN_SDK}/Bin")
|
||||||
|
if(NOT GLSLC)
|
||||||
|
message(STATUS "glslc not found — using precompiled SPIR-V headers from shaders/precompiled/")
|
||||||
|
set(SHADER_GEN_DIR "${CMAKE_SOURCE_DIR}/shaders/precompiled")
|
||||||
|
else()
|
||||||
|
set(SHADER_SRC_DIR "${CMAKE_SOURCE_DIR}/shaders")
|
||||||
|
set(SHADER_GEN_DIR "${CMAKE_BINARY_DIR}/generated_shaders")
|
||||||
|
file(MAKE_DIRECTORY "${SHADER_GEN_DIR}")
|
||||||
|
|
||||||
|
set(SPIRV_HEADERS)
|
||||||
|
foreach(SHADER sprite_vert sprite_frag postfx_vert postfx_frag ball_vert)
|
||||||
|
if(SHADER MATCHES "_vert$")
|
||||||
|
set(STAGE_FLAG "-fshader-stage=vertex")
|
||||||
|
else()
|
||||||
|
set(STAGE_FLAG "-fshader-stage=fragment")
|
||||||
|
endif()
|
||||||
|
string(REGEX REPLACE "_vert$" ".vert" GLSL_NAME "${SHADER}")
|
||||||
|
string(REGEX REPLACE "_frag$" ".frag" GLSL_NAME "${GLSL_NAME}")
|
||||||
|
|
||||||
|
set(GLSL_FILE "${SHADER_SRC_DIR}/${GLSL_NAME}")
|
||||||
|
set(SPV_FILE "${SHADER_GEN_DIR}/${SHADER}.spv")
|
||||||
|
set(H_FILE "${SHADER_GEN_DIR}/${SHADER}_spv.h")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${H_FILE}"
|
||||||
|
COMMAND "${GLSLC}" ${STAGE_FLAG} -o "${SPV_FILE}" "${GLSL_FILE}"
|
||||||
|
COMMAND "${CMAKE_COMMAND}"
|
||||||
|
-DINPUT="${SPV_FILE}"
|
||||||
|
-DOUTPUT="${H_FILE}"
|
||||||
|
-DVAR_NAME="k${SHADER}_spv"
|
||||||
|
-P "${CMAKE_SOURCE_DIR}/cmake/spv_to_header.cmake"
|
||||||
|
DEPENDS "${GLSL_FILE}"
|
||||||
|
COMMENT "Compiling ${GLSL_NAME} to SPIRV"
|
||||||
|
)
|
||||||
|
list(APPEND SPIRV_HEADERS "${H_FILE}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_custom_target(shaders ALL DEPENDS ${SPIRV_HEADERS})
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Archivos fuente (excluir main_old.cpp)
|
# Archivos fuente (excluir main_old.cpp)
|
||||||
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/boids_mgr/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/shapes_mgr/*.cpp source/state/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp)
|
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/boids_mgr/*.cpp source/gpu/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/shapes_mgr/*.cpp source/state/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp)
|
||||||
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
|
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
|
||||||
|
|
||||||
|
# Suprimir falso positivo de GCC en stb_image.h (externo)
|
||||||
|
set_source_files_properties(source/external/texture.cpp PROPERTIES COMPILE_FLAGS "-Wno-stringop-overflow")
|
||||||
|
|
||||||
# Comprobar si se encontraron archivos fuente
|
# Comprobar si se encontraron archivos fuente
|
||||||
if(NOT SOURCE_FILES)
|
if(NOT SOURCE_FILES)
|
||||||
message(FATAL_ERROR "No se encontraron archivos fuente en el directorio 'source/'. Verifica la ruta.")
|
message(FATAL_ERROR "No se encontraron archivos fuente en el directorio 'source/'. Verifica la ruta.")
|
||||||
@@ -35,13 +71,10 @@ endif()
|
|||||||
|
|
||||||
# Detectar la plataforma y configuraciones específicas
|
# Detectar la plataforma y configuraciones específicas
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(PLATFORM windows)
|
|
||||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf mingw32 ws2_32)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf mingw32 ws2_32)
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
set(PLATFORM linux)
|
|
||||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||||
elseif(APPLE)
|
elseif(APPLE)
|
||||||
set(PLATFORM macos)
|
|
||||||
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
set(LINK_LIBS SDL3::SDL3 SDL3_ttf::SDL3_ttf)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -60,6 +93,13 @@ set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAK
|
|||||||
# Enlazar las bibliotecas necesarias
|
# Enlazar las bibliotecas necesarias
|
||||||
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
target_link_libraries(${PROJECT_NAME} ${LINK_LIBS})
|
||||||
|
|
||||||
|
if(NOT APPLE)
|
||||||
|
if(GLSLC)
|
||||||
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
|
endif()
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE "${SHADER_GEN_DIR}")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Tool: pack_resources
|
# Tool: pack_resources
|
||||||
add_executable(pack_resources tools/pack_resources.cpp source/resource_pack.cpp)
|
add_executable(pack_resources tools/pack_resources.cpp source/resource_pack.cpp)
|
||||||
target_include_directories(pack_resources PRIVATE ${CMAKE_SOURCE_DIR}/source)
|
target_include_directories(pack_resources PRIVATE ${CMAKE_SOURCE_DIR}/source)
|
||||||
|
|||||||
67
Makefile
67
Makefile
@@ -2,16 +2,15 @@
|
|||||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||||
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
|
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
|
||||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||||
DIR_BUILD := $(addsuffix /, $(DIR_ROOT)build)
|
|
||||||
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
|
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
|
||||||
|
|
||||||
# Variables
|
# Variables
|
||||||
TARGET_NAME := vibe3_physics
|
TARGET_NAME := vibe3_physics
|
||||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
||||||
APP_NAME := ViBe3 Physics
|
APP_NAME := ViBe3 Physics
|
||||||
RELEASE_FOLDER := vibe3_release
|
RELEASE_FOLDER := dist/_tmp
|
||||||
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||||
RESOURCE_FILE := release/windows/vibe3.res
|
RESOURCE_FILE := build/vibe3.res
|
||||||
DIST_DIR := dist
|
DIST_DIR := dist
|
||||||
|
|
||||||
# Variables para herramienta de empaquetado
|
# Variables para herramienta de empaquetado
|
||||||
@@ -51,6 +50,7 @@ RASPI_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-raspberry.t
|
|||||||
# Lista completa de archivos fuente (detección automática con wildcards, como CMakeLists.txt)
|
# Lista completa de archivos fuente (detección automática con wildcards, como CMakeLists.txt)
|
||||||
APP_SOURCES := $(wildcard source/*.cpp) \
|
APP_SOURCES := $(wildcard source/*.cpp) \
|
||||||
$(wildcard source/external/*.cpp) \
|
$(wildcard source/external/*.cpp) \
|
||||||
|
$(wildcard source/gpu/*.cpp) \
|
||||||
$(wildcard source/shapes/*.cpp) \
|
$(wildcard source/shapes/*.cpp) \
|
||||||
$(wildcard source/themes/*.cpp) \
|
$(wildcard source/themes/*.cpp) \
|
||||||
$(wildcard source/state/*.cpp) \
|
$(wildcard source/state/*.cpp) \
|
||||||
@@ -64,8 +64,18 @@ APP_SOURCES := $(wildcard source/*.cpp) \
|
|||||||
# Excluir archivos antiguos si existen
|
# Excluir archivos antiguos si existen
|
||||||
APP_SOURCES := $(filter-out source/main_old.cpp, $(APP_SOURCES))
|
APP_SOURCES := $(filter-out source/main_old.cpp, $(APP_SOURCES))
|
||||||
|
|
||||||
# Includes
|
# Includes: usar shaders pre-compilados si glslc no está disponible
|
||||||
INCLUDES := -Isource -Isource/external
|
ifeq ($(OS),Windows_NT)
|
||||||
|
GLSLC := $(shell where glslc 2>NUL)
|
||||||
|
else
|
||||||
|
GLSLC := $(shell command -v glslc 2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(GLSLC),)
|
||||||
|
SHADER_INCLUDE := -Ishaders/precompiled
|
||||||
|
else
|
||||||
|
SHADER_INCLUDE := -Ibuild/generated_shaders
|
||||||
|
endif
|
||||||
|
INCLUDES := -Isource -Isource/external $(SHADER_INCLUDE)
|
||||||
|
|
||||||
# Variables según el sistema operativo
|
# Variables según el sistema operativo
|
||||||
CXXFLAGS_BASE := -std=c++20 -Wall
|
CXXFLAGS_BASE := -std=c++20 -Wall
|
||||||
@@ -100,13 +110,17 @@ endif
|
|||||||
# Reglas para herramienta de empaquetado y resources.pack
|
# Reglas para herramienta de empaquetado y resources.pack
|
||||||
$(PACK_TOOL): $(PACK_SOURCES)
|
$(PACK_TOOL): $(PACK_SOURCES)
|
||||||
@echo "Compilando herramienta de empaquetado..."
|
@echo "Compilando herramienta de empaquetado..."
|
||||||
$(PACK_CXX) -std=c++17 -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
|
$(PACK_CXX) -std=c++20 -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
|
||||||
@echo "✓ Herramienta de empaquetado lista: $(PACK_TOOL)"
|
@echo "✓ Herramienta de empaquetado lista: $(PACK_TOOL)"
|
||||||
|
|
||||||
pack_tool: $(PACK_TOOL)
|
pack_tool: $(PACK_TOOL)
|
||||||
|
|
||||||
# Detectar todos los archivos en data/ como dependencias (regenera si cualquiera cambia)
|
# Detectar todos los archivos en data/ como dependencias (regenera si cualquiera cambia)
|
||||||
DATA_FILES := $(shell find data -type f 2>/dev/null)
|
ifeq ($(OS),Windows_NT)
|
||||||
|
DATA_FILES :=
|
||||||
|
else
|
||||||
|
DATA_FILES := $(shell find data -type f 2>/dev/null)
|
||||||
|
endif
|
||||||
|
|
||||||
resources.pack: $(PACK_TOOL) $(DATA_FILES)
|
resources.pack: $(PACK_TOOL) $(DATA_FILES)
|
||||||
@echo "Generando resources.pack desde directorio data/..."
|
@echo "Generando resources.pack desde directorio data/..."
|
||||||
@@ -122,8 +136,7 @@ force_resource_pack: $(PACK_TOOL)
|
|||||||
|
|
||||||
# Reglas para compilación
|
# Reglas para compilación
|
||||||
windows:
|
windows:
|
||||||
@echo off
|
@echo Compilando para Windows con nombre: $(APP_NAME).exe
|
||||||
@echo Compilando para Windows con nombre: "$(APP_NAME).exe"
|
|
||||||
windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
||||||
$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
|
$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
|
||||||
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
|
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
|
||||||
@@ -133,6 +146,7 @@ windows_release: force_resource_pack
|
|||||||
|
|
||||||
# Crea carpeta temporal 'RELEASE_FOLDER'
|
# Crea carpeta temporal 'RELEASE_FOLDER'
|
||||||
@if exist "$(RELEASE_FOLDER)" rmdir /S /Q "$(RELEASE_FOLDER)"
|
@if exist "$(RELEASE_FOLDER)" rmdir /S /Q "$(RELEASE_FOLDER)"
|
||||||
|
@if not exist "$(DIST_DIR)" mkdir "$(DIST_DIR)"
|
||||||
@mkdir "$(RELEASE_FOLDER)"
|
@mkdir "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
# Copia el archivo 'resources.pack'
|
# Copia el archivo 'resources.pack'
|
||||||
@@ -144,7 +158,6 @@ windows_release: force_resource_pack
|
|||||||
@copy /Y release\windows\dll\*.dll "$(RELEASE_FOLDER)\" >nul 2>&1 || echo DLLs copied successfully
|
@copy /Y release\windows\dll\*.dll "$(RELEASE_FOLDER)\" >nul 2>&1 || echo DLLs copied successfully
|
||||||
|
|
||||||
# Compila
|
# Compila
|
||||||
@if not exist "$(DIST_DIR)" mkdir "$(DIST_DIR)"
|
|
||||||
@windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
@windres release/windows/vibe3.rc -O coff -o $(RESOURCE_FILE)
|
||||||
@$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_RELEASE_FILE).exe"
|
@$(CXX) $(APP_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_RELEASE_FILE).exe"
|
||||||
@strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
@strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||||
@@ -197,40 +210,6 @@ macos_release: force_resource_pack
|
|||||||
cp LICENSE "$(RELEASE_FOLDER)"
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
# NOTA: create-dmg crea automáticamente el enlace a /Applications con --app-drop-link
|
|
||||||
# No es necesario crearlo manualmente aquí
|
|
||||||
|
|
||||||
# Compila la versión para procesadores Intel
|
|
||||||
ifdef ENABLE_MACOS_X86_64
|
|
||||||
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos12
|
|
||||||
|
|
||||||
# Firma la aplicación
|
|
||||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
|
||||||
|
|
||||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
|
||||||
@echo "Creando DMG Intel con iconos de 96x96..."
|
|
||||||
@create-dmg \
|
|
||||||
--volname "$(APP_NAME)" \
|
|
||||||
--window-pos 200 120 \
|
|
||||||
--window-size 720 300 \
|
|
||||||
--icon-size 96 \
|
|
||||||
--text-size 12 \
|
|
||||||
--icon "$(APP_NAME).app" 278 102 \
|
|
||||||
--icon "LICENSE" 441 102 \
|
|
||||||
--icon "README.md" 604 102 \
|
|
||||||
--app-drop-link 115 102 \
|
|
||||||
--hide-extension "$(APP_NAME).app" \
|
|
||||||
"$(MACOS_INTEL_RELEASE)" \
|
|
||||||
"$(RELEASE_FOLDER)"
|
|
||||||
@if [ -f "$(MACOS_INTEL_RELEASE)" ]; then \
|
|
||||||
echo "✓ Release Intel creado exitosamente: $(MACOS_INTEL_RELEASE)"; \
|
|
||||||
else \
|
|
||||||
echo "✗ Error: No se pudo crear el DMG Intel"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@rm -f rw.*.dmg 2>/dev/null || true
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Compila la versión para procesadores Apple Silicon
|
# Compila la versión para procesadores Apple Silicon
|
||||||
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DSDL_DISABLE_IMMINTRIN_H $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos12
|
$(CXX) $(APP_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DSDL_DISABLE_IMMINTRIN_H $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos12
|
||||||
|
|
||||||
|
|||||||
87
README.md
87
README.md
@@ -531,49 +531,92 @@ vibe3_physics/
|
|||||||
## 🔧 Requisitos del Sistema
|
## 🔧 Requisitos del Sistema
|
||||||
|
|
||||||
- **SDL3** (Simple DirectMedia Layer 3)
|
- **SDL3** (Simple DirectMedia Layer 3)
|
||||||
|
- **SDL3_ttf** (fuentes TTF para SDL3)
|
||||||
- **C++20** compatible compiler (GCC 10+, Clang 12+, MSVC 2019+)
|
- **C++20** compatible compiler (GCC 10+, Clang 12+, MSVC 2019+)
|
||||||
- **CMake 3.20+** o **Make**
|
- **CMake 3.20+**
|
||||||
- **Plataforma**: Windows, Linux, macOS
|
- **Plataforma**: Windows, Linux, macOS
|
||||||
|
|
||||||
### Instalación de SDL3
|
### Dependencias de shaders
|
||||||
|
|
||||||
|
El proyecto compila shaders GLSL a SPIR-V en todas las plataformas usando `glslc` (incluido en el Vulkan SDK). En macOS, adicionalmente transpila los `.spv` a MSL (Metal) con `spirv-cross` (shaders internos: sprite, ball, postfx).
|
||||||
|
|
||||||
|
| Plataforma | Backend GPU | Herramienta de shaders | Resultado |
|
||||||
|
|------------|-------------|------------------------|-----------|
|
||||||
|
| Linux | Vulkan | `glslc` (Vulkan SDK) | `.spv` |
|
||||||
|
| Windows | Vulkan | `glslc` (Vulkan SDK) | `.spv` |
|
||||||
|
| macOS | Metal | `glslc` + `spirv-cross`| `.spv.msl`|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Instalación de dependencias
|
||||||
|
|
||||||
|
#### macOS
|
||||||
|
|
||||||
#### Windows (MinGW)
|
|
||||||
```bash
|
```bash
|
||||||
# Usando vcpkg
|
# SDL3 + SDL3_ttf
|
||||||
vcpkg install sdl3
|
brew install sdl3 sdl3_ttf
|
||||||
|
|
||||||
# O compilar desde fuente
|
# Vulkan SDK (incluye glslc para compilar shaders)
|
||||||
git clone https://github.com/libsdl-org/SDL
|
# Descargar desde https://vulkan.lunarg.com/ o via Homebrew:
|
||||||
cd SDL
|
brew install --cask vulkan-sdk
|
||||||
mkdir build && cd build
|
|
||||||
cmake ..
|
# spirv-cross (transpilación SPIR-V → MSL para Metal)
|
||||||
make
|
brew install spirv-cross
|
||||||
sudo make install
|
```
|
||||||
|
|
||||||
|
Tras instalar el Vulkan SDK es posible que necesites añadir `glslc` al PATH:
|
||||||
|
```bash
|
||||||
|
# Añadir a ~/.zshrc o ~/.bash_profile (ajusta la versión):
|
||||||
|
export PATH="$HOME/VulkanSDK/<version>/macOS/bin:$PATH"
|
||||||
|
source ~/.zshrc
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Linux (Ubuntu/Debian)
|
#### Linux (Ubuntu/Debian)
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install libsdl3-dev
|
|
||||||
|
|
||||||
# Si no está disponible, compilar desde fuente
|
```bash
|
||||||
|
# SDL3 + SDL3_ttf
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libsdl3-dev libsdl3-ttf-dev
|
||||||
|
|
||||||
|
# Vulkan SDK (incluye glslc)
|
||||||
|
# Opción A: paquete del sistema
|
||||||
|
sudo apt install glslc
|
||||||
|
|
||||||
|
# Opción B: Vulkan SDK completo (recomendado para versión reciente)
|
||||||
|
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
|
||||||
|
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan.list \
|
||||||
|
https://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install vulkan-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Linux (Arch)
|
#### Linux (Arch)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S sdl3
|
sudo pacman -S sdl3 sdl3_ttf vulkan-devel glslang shaderc
|
||||||
```
|
```
|
||||||
|
|
||||||
#### macOS
|
#### Windows (MinGW / MSYS2)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install sdl3
|
# En MSYS2 shell:
|
||||||
|
pacman -S mingw-w64-x86_64-SDL3 mingw-w64-x86_64-SDL3_ttf
|
||||||
|
|
||||||
|
# Vulkan SDK: descargar el instalador desde https://vulkan.lunarg.com/
|
||||||
|
# El instalador añade glslc al PATH automáticamente.
|
||||||
|
```
|
||||||
|
|
||||||
|
Con Visual Studio + vcpkg:
|
||||||
|
```bash
|
||||||
|
vcpkg install sdl3 sdl3-ttf
|
||||||
|
# Instalar Vulkan SDK desde https://vulkan.lunarg.com/ (incluye glslc)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Compilación
|
## 🚀 Compilación
|
||||||
|
|
||||||
### Opción 1: CMake (Recomendado)
|
### CMake (todas las plataformas)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p build && cd build
|
mkdir -p build && cd build
|
||||||
@@ -581,7 +624,9 @@ cmake ..
|
|||||||
cmake --build .
|
cmake --build .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Opción 2: Make directo
|
En macOS, CMake detecta automáticamente `spirv-cross` y genera los `.spv.msl` necesarios para los shaders internos (sprite, ball, postfx).
|
||||||
|
|
||||||
|
### Make directo (Linux/macOS)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make
|
||||||
|
|||||||
20
cmake/spv_to_header.cmake
Normal file
20
cmake/spv_to_header.cmake
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Converts a SPIR-V binary to a C++ header with an embedded uint8_t array.
|
||||||
|
# cmake -DINPUT=<spv> -DOUTPUT=<h> -DVAR_NAME=<name> -P spv_to_header.cmake
|
||||||
|
|
||||||
|
if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED VAR_NAME)
|
||||||
|
message(FATAL_ERROR "Usage: -DINPUT=x.spv -DOUTPUT=x.h -DVAR_NAME=kname -P spv_to_header.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(READ "${INPUT}" raw_hex HEX)
|
||||||
|
string(REGEX REPLACE "([0-9a-fA-F][0-9a-fA-F])" "0x\\1," hex_bytes "${raw_hex}")
|
||||||
|
string(REGEX REPLACE ",$" "" hex_bytes "${hex_bytes}")
|
||||||
|
string(LENGTH "${raw_hex}" hex_len)
|
||||||
|
math(EXPR byte_count "${hex_len} / 2")
|
||||||
|
|
||||||
|
file(WRITE "${OUTPUT}"
|
||||||
|
"#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
static const uint8_t ${VAR_NAME}[] = { ${hex_bytes} };
|
||||||
|
static const size_t ${VAR_NAME}_size = ${byte_count};
|
||||||
|
")
|
||||||
Binary file not shown.
BIN
data/fonts/Exo2-Regular.ttf
Normal file
BIN
data/fonts/Exo2-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
// coffee.rc
|
// coffee.rc
|
||||||
IDI_ICON1 ICON "icon.ico"
|
IDI_ICON1 ICON "release/icons/icon.ico"
|
||||||
|
|||||||
23
shaders/ball.vert
Normal file
23
shaders/ball.vert
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#version 450
|
||||||
|
// Per-instance data (input_rate = INSTANCE in the pipeline)
|
||||||
|
layout(location=0) in vec2 center;
|
||||||
|
layout(location=1) in vec2 halfsize;
|
||||||
|
layout(location=2) in vec4 col;
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
layout(location=1) out vec4 v_col;
|
||||||
|
void main() {
|
||||||
|
// gl_VertexIndex cycles 0..5 per instance (6 vertices = 2 triangles)
|
||||||
|
// Vertex order: TL TR BL | TR BR BL (CCW winding)
|
||||||
|
const vec2 offsets[6] = vec2[6](
|
||||||
|
vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(-1.0,-1.0),
|
||||||
|
vec2( 1.0, 1.0), vec2(1.0,-1.0), vec2(-1.0,-1.0)
|
||||||
|
);
|
||||||
|
const vec2 uvs[6] = vec2[6](
|
||||||
|
vec2(0.0,0.0), vec2(1.0,0.0), vec2(0.0,1.0),
|
||||||
|
vec2(1.0,0.0), vec2(1.0,1.0), vec2(0.0,1.0)
|
||||||
|
);
|
||||||
|
int vid = gl_VertexIndex;
|
||||||
|
gl_Position = vec4(center + offsets[vid] * halfsize, 0.0, 1.0);
|
||||||
|
v_uv = uvs[vid];
|
||||||
|
v_col = col;
|
||||||
|
}
|
||||||
24
shaders/postfx.frag
Normal file
24
shaders/postfx.frag
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 v_uv;
|
||||||
|
layout(location=0) out vec4 out_color;
|
||||||
|
layout(set=2, binding=0) uniform sampler2D scene;
|
||||||
|
layout(set=3, binding=0) uniform PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_strength;
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
} u;
|
||||||
|
void main() {
|
||||||
|
float ca = u.chroma_strength * 0.005;
|
||||||
|
vec4 color;
|
||||||
|
color.r = texture(scene, v_uv + vec2( ca, 0.0)).r;
|
||||||
|
color.g = texture(scene, v_uv).g;
|
||||||
|
color.b = texture(scene, v_uv - vec2( ca, 0.0)).b;
|
||||||
|
color.a = texture(scene, v_uv).a;
|
||||||
|
float scan = 0.85 + 0.15 * sin(v_uv.y * 3.14159265 * u.screen_height);
|
||||||
|
color.rgb *= mix(1.0, scan, u.scanline_strength);
|
||||||
|
vec2 d = v_uv - vec2(0.5, 0.5);
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
color.rgb *= clamp(vignette, 0.0, 1.0);
|
||||||
|
out_color = color;
|
||||||
|
}
|
||||||
10
shaders/postfx.vert
Normal file
10
shaders/postfx.vert
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
void main() {
|
||||||
|
// Full-screen triangle from vertex index (no vertex buffer needed)
|
||||||
|
// NDC/UV mapping matches the MSL version (SDL3 GPU normalizes Y-up on all backends)
|
||||||
|
vec2 positions[3] = vec2[3](vec2(-1.0,-1.0), vec2(3.0,-1.0), vec2(-1.0,3.0));
|
||||||
|
vec2 uvs[3] = vec2[3](vec2(0.0, 1.0), vec2(2.0, 1.0), vec2(0.0,-1.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||||
|
v_uv = uvs[gl_VertexIndex];
|
||||||
|
}
|
||||||
5
shaders/precompiled/ball_vert_spv.h
Normal file
5
shaders/precompiled/ball_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/postfx_frag_spv.h
Normal file
5
shaders/precompiled/postfx_frag_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/postfx_vert_spv.h
Normal file
5
shaders/precompiled/postfx_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/sprite_frag_spv.h
Normal file
5
shaders/precompiled/sprite_frag_spv.h
Normal file
File diff suppressed because one or more lines are too long
5
shaders/precompiled/sprite_vert_spv.h
Normal file
5
shaders/precompiled/sprite_vert_spv.h
Normal file
File diff suppressed because one or more lines are too long
9
shaders/sprite.frag
Normal file
9
shaders/sprite.frag
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 v_uv;
|
||||||
|
layout(location=1) in vec4 v_col;
|
||||||
|
layout(location=0) out vec4 out_color;
|
||||||
|
layout(set=2, binding=0) uniform sampler2D tex;
|
||||||
|
void main() {
|
||||||
|
vec4 t = texture(tex, v_uv);
|
||||||
|
out_color = vec4(t.rgb * v_col.rgb, t.a * v_col.a);
|
||||||
|
}
|
||||||
11
shaders/sprite.vert
Normal file
11
shaders/sprite.vert
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#version 450
|
||||||
|
layout(location=0) in vec2 pos;
|
||||||
|
layout(location=1) in vec2 uv;
|
||||||
|
layout(location=2) in vec4 col;
|
||||||
|
layout(location=0) out vec2 v_uv;
|
||||||
|
layout(location=1) out vec4 v_col;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
v_uv = uv;
|
||||||
|
v_col = col;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
};
|
};
|
||||||
@@ -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,8 +94,8 @@ 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_) {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
71
source/boids_mgr/spatial_grid.hpp
Normal file
71
source/boids_mgr/spatial_grid.hpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Ball; // Forward declaration
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SPATIAL HASH GRID - Sistema genérico de particionamiento espacial
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// Divide el espacio 2D en celdas de tamaño fijo para acelerar búsquedas de vecinos.
|
||||||
|
// Reduce complejidad de O(n²) a O(n) para queries de proximidad.
|
||||||
|
//
|
||||||
|
// CASOS DE USO:
|
||||||
|
// - Boids: Buscar vecinos para reglas de Reynolds (separación/alineación/cohesión)
|
||||||
|
// - Física: Detección de colisiones ball-to-ball (futuro)
|
||||||
|
// - IA: Pathfinding con obstáculos dinámicos
|
||||||
|
//
|
||||||
|
// ALGORITMO:
|
||||||
|
// 1. Dividir pantalla en grid de celdas (ej: 100x100px cada una)
|
||||||
|
// 2. Insertar cada Ball en celda(s) correspondiente(s) según posición
|
||||||
|
// 3. Query: Solo revisar celdas adyacentes (9 celdas max) en lugar de TODOS los objetos
|
||||||
|
//
|
||||||
|
// MEJORA DE RENDIMIENTO:
|
||||||
|
// - Sin grid: 1000 boids = 1M comparaciones (1000²)
|
||||||
|
// - Con grid: 1000 boids ≈ 9K comparaciones (1000 * ~9 vecinos/celda promedio)
|
||||||
|
// - Speedup: ~100x en casos típicos
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class SpatialGrid {
|
||||||
|
public:
|
||||||
|
// Constructor: especificar dimensiones del mundo y tamaño de celda
|
||||||
|
SpatialGrid(int world_width, int world_height, float cell_size);
|
||||||
|
|
||||||
|
// Limpiar todas las celdas (llamar al inicio de cada frame)
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
// Insertar objeto en el grid según su posición (x, y)
|
||||||
|
void insert(Ball* ball, float x, float y);
|
||||||
|
|
||||||
|
// Buscar todos los objetos dentro del radio especificado desde (x, y)
|
||||||
|
// Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas)
|
||||||
|
std::vector<Ball*> queryRadius(float x, float y, float radius);
|
||||||
|
|
||||||
|
// Actualizar dimensiones del mundo (útil para cambios de resolución F4)
|
||||||
|
void updateWorldSize(int world_width, int world_height);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y)
|
||||||
|
void getCellCoords(float x, float y, int& cell_x, int& cell_y) const;
|
||||||
|
|
||||||
|
// Convertir (cell_x, cell_y) a hash key único para el mapa
|
||||||
|
int getCellKey(int cell_x, int cell_y) const;
|
||||||
|
|
||||||
|
// Dimensiones del mundo (ancho/alto en píxeles)
|
||||||
|
int world_width_;
|
||||||
|
int world_height_;
|
||||||
|
|
||||||
|
// Tamaño de cada celda (en píxeles)
|
||||||
|
float cell_size_;
|
||||||
|
|
||||||
|
// Número de celdas en cada dimensión
|
||||||
|
int grid_cols_;
|
||||||
|
int grid_rows_;
|
||||||
|
|
||||||
|
// Estructura de datos: hash map de cell_key → vector de Ball*
|
||||||
|
// Usamos unordered_map para O(1) lookup
|
||||||
|
std::unordered_map<int, std::vector<Ball*>> cells_;
|
||||||
|
};
|
||||||
@@ -5,22 +5,26 @@
|
|||||||
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
|
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
|
||||||
|
|
||||||
// Configuración de ventana y pantalla
|
// Configuración de ventana y pantalla
|
||||||
constexpr char WINDOW_CAPTION[] = "ViBe3 Physics (JailDesigner 2025)";
|
constexpr char WINDOW_CAPTION[] = "© 2025 ViBe3 Physics — JailDesigner";
|
||||||
|
|
||||||
// Resolución por defecto (usada si no se especifica en CLI)
|
// Resolución por defecto (usada si no se especifica en CLI)
|
||||||
constexpr int DEFAULT_SCREEN_WIDTH = 1280; // Ancho lógico por defecto (si no hay -w)
|
constexpr int DEFAULT_SCREEN_WIDTH = 1280; // Ancho lógico por defecto (si no hay -w)
|
||||||
constexpr int DEFAULT_SCREEN_HEIGHT = 720; // Alto lógico por defecto (si no hay -h)
|
constexpr int DEFAULT_SCREEN_HEIGHT = 720; // Alto lógico por defecto (si no hay -h)
|
||||||
constexpr int DEFAULT_WINDOW_ZOOM = 1; // Zoom inicial de ventana (1x = sin zoom)
|
constexpr int DEFAULT_WINDOW_ZOOM = 1; // Zoom inicial de ventana (1x = sin zoom)
|
||||||
|
|
||||||
// Configuración de zoom dinámico de ventana
|
|
||||||
constexpr int WINDOW_ZOOM_MIN = 1; // Zoom mínimo (320x240)
|
|
||||||
constexpr int WINDOW_ZOOM_MAX = 10; // Zoom máximo teórico (3200x2400)
|
|
||||||
constexpr int WINDOW_DESKTOP_MARGIN = 10; // Margen mínimo con bordes del escritorio
|
constexpr int WINDOW_DESKTOP_MARGIN = 10; // Margen mínimo con bordes del escritorio
|
||||||
constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones del SO
|
constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones del SO
|
||||||
|
|
||||||
|
// Configuración de escala de ventana por pasos (F1/F2)
|
||||||
|
constexpr float WINDOW_SCALE_STEP = 0.1f; // Incremento/decremento por pulsación (10%)
|
||||||
|
constexpr float WINDOW_SCALE_MIN = 0.5f; // Escala mínima (50% de la resolución base)
|
||||||
|
|
||||||
// Configuración de física
|
// Configuración de física
|
||||||
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
|
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
|
||||||
|
|
||||||
|
// Fuente de la interfaz
|
||||||
|
#define APP_FONT "data/fonts/Exo2-Regular.ttf"
|
||||||
|
|
||||||
// Configuración de interfaz
|
// Configuración de interfaz
|
||||||
constexpr float THEME_TRANSITION_DURATION = 0.5f; // Duración de transiciones LERP entre temas (segundos)
|
constexpr float THEME_TRANSITION_DURATION = 0.5f; // Duración de transiciones LERP entre temas (segundos)
|
||||||
|
|
||||||
@@ -32,7 +36,7 @@ constexpr Uint64 NOTIFICATION_FADE_TIME = 200; // Duración animación salida
|
|||||||
constexpr float NOTIFICATION_BG_ALPHA = 0.7f; // Opacidad fondo semitransparente (0.0-1.0)
|
constexpr float NOTIFICATION_BG_ALPHA = 0.7f; // Opacidad fondo semitransparente (0.0-1.0)
|
||||||
constexpr int NOTIFICATION_PADDING = 10; // Padding interno del fondo (píxeles físicos)
|
constexpr int NOTIFICATION_PADDING = 10; // Padding interno del fondo (píxeles físicos)
|
||||||
constexpr int NOTIFICATION_TOP_MARGIN = 20; // Margen superior desde borde pantalla (píxeles físicos)
|
constexpr int NOTIFICATION_TOP_MARGIN = 20; // Margen superior desde borde pantalla (píxeles físicos)
|
||||||
constexpr char KIOSK_NOTIFICATION_TEXT[] = "MODO KIOSKO";
|
constexpr char KIOSK_NOTIFICATION_TEXT[] = "Modo kiosko";
|
||||||
|
|
||||||
// Configuración de pérdida aleatoria en rebotes
|
// Configuración de pérdida aleatoria en rebotes
|
||||||
constexpr float BASE_BOUNCE_COEFFICIENT = 0.75f; // Coeficiente base IGUAL para todas las pelotas
|
constexpr float BASE_BOUNCE_COEFFICIENT = 0.75f; // Coeficiente base IGUAL para todas las pelotas
|
||||||
@@ -51,11 +55,27 @@ constexpr float GRAVITY_CHANGE_LATERAL_MAX = 0.08f; // Velocidad lateral máxim
|
|||||||
constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado)
|
constexpr float BALL_SPAWN_MARGIN = 0.15f; // Margen lateral para spawn (0.25 = 25% a cada lado)
|
||||||
|
|
||||||
// Escenarios de número de pelotas (teclas 1-8)
|
// Escenarios de número de pelotas (teclas 1-8)
|
||||||
constexpr int BALL_COUNT_SCENARIOS[8] = {10, 50, 100, 500, 1000, 5000, 10000, 50000};
|
constexpr int SCENE_BALLS_1 = 10;
|
||||||
|
constexpr int SCENE_BALLS_2 = 50;
|
||||||
|
constexpr int SCENE_BALLS_3 = 100;
|
||||||
|
constexpr int SCENE_BALLS_4 = 500;
|
||||||
|
constexpr int SCENE_BALLS_5 = 1000;
|
||||||
|
constexpr int SCENE_BALLS_6 = 5000;
|
||||||
|
constexpr int SCENE_BALLS_7 = 10000;
|
||||||
|
constexpr int SCENE_BALLS_8 = 50000; // Máximo escenario estándar (tecla 8)
|
||||||
|
|
||||||
// Límites de escenario para modos automáticos (índices en BALL_COUNT_SCENARIOS)
|
constexpr int SCENARIO_COUNT = 8;
|
||||||
// BALL_COUNT_SCENARIOS = {10, 50, 100, 500, 1000, 5000, 10000, 50000}
|
constexpr int BALL_COUNT_SCENARIOS[SCENARIO_COUNT] = {
|
||||||
// 0 1 2 3 4 5 6 7
|
SCENE_BALLS_1,
|
||||||
|
SCENE_BALLS_2,
|
||||||
|
SCENE_BALLS_3,
|
||||||
|
SCENE_BALLS_4,
|
||||||
|
SCENE_BALLS_5,
|
||||||
|
SCENE_BALLS_6,
|
||||||
|
SCENE_BALLS_7,
|
||||||
|
SCENE_BALLS_8};
|
||||||
|
|
||||||
|
constexpr int BOIDS_MAX_BALLS = SCENE_BALLS_5; // 1 000 bolas máximo en modo BOIDS
|
||||||
constexpr int DEMO_AUTO_MIN_SCENARIO = 2; // mínimo 100 bolas
|
constexpr int DEMO_AUTO_MIN_SCENARIO = 2; // mínimo 100 bolas
|
||||||
constexpr int DEMO_AUTO_MAX_SCENARIO = 7; // máximo sin restricción hardware (ajustado por benchmark)
|
constexpr int DEMO_AUTO_MAX_SCENARIO = 7; // máximo sin restricción hardware (ajustado por benchmark)
|
||||||
constexpr int LOGO_MIN_SCENARIO_IDX = 4; // mínimo 1000 bolas (sustituye LOGO_MODE_MIN_BALLS)
|
constexpr int LOGO_MIN_SCENARIO_IDX = 4; // mínimo 1000 bolas (sustituye LOGO_MODE_MIN_BALLS)
|
||||||
@@ -164,7 +184,6 @@ enum class ScalingMode {
|
|||||||
constexpr float ROTOBALL_RADIUS_FACTOR = 0.333f; // Radio como proporción de altura de pantalla (80/240 ≈ 0.333)
|
constexpr float ROTOBALL_RADIUS_FACTOR = 0.333f; // Radio como proporción de altura de pantalla (80/240 ≈ 0.333)
|
||||||
constexpr float ROTOBALL_ROTATION_SPEED_Y = 1.5f; // Velocidad rotación eje Y (rad/s)
|
constexpr float ROTOBALL_ROTATION_SPEED_Y = 1.5f; // Velocidad rotación eje Y (rad/s)
|
||||||
constexpr float ROTOBALL_ROTATION_SPEED_X = 0.8f; // Velocidad rotación eje X (rad/s)
|
constexpr float ROTOBALL_ROTATION_SPEED_X = 0.8f; // Velocidad rotación eje X (rad/s)
|
||||||
constexpr float ROTOBALL_TRANSITION_TIME = 1.5f; // Tiempo de transición (segundos)
|
|
||||||
constexpr int ROTOBALL_MIN_BRIGHTNESS = 50; // Brillo mínimo (fondo, 0-255)
|
constexpr int ROTOBALL_MIN_BRIGHTNESS = 50; // Brillo mínimo (fondo, 0-255)
|
||||||
constexpr int ROTOBALL_MAX_BRIGHTNESS = 255; // Brillo máximo (frente, 0-255)
|
constexpr int ROTOBALL_MAX_BRIGHTNESS = 255; // Brillo máximo (frente, 0-255)
|
||||||
|
|
||||||
@@ -273,7 +292,6 @@ constexpr int DEMO_LITE_WEIGHT_IMPULSE = 10; // Aplicar impulso (10%)
|
|||||||
// TOTAL: 100
|
// TOTAL: 100
|
||||||
|
|
||||||
// Configuración de Modo LOGO (easter egg - "marca de agua")
|
// Configuración de Modo LOGO (easter egg - "marca de agua")
|
||||||
constexpr int LOGO_MODE_MIN_BALLS = 500; // Mínimo de pelotas para activar modo logo
|
|
||||||
constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%)
|
constexpr float LOGO_MODE_SHAPE_SCALE = 1.2f; // Escala de figura en modo logo (120%)
|
||||||
constexpr float LOGO_ACTION_INTERVAL_MIN = 3.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
constexpr float LOGO_ACTION_INTERVAL_MIN = 3.0f; // Tiempo mínimo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
||||||
constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
constexpr float LOGO_ACTION_INTERVAL_MAX = 5.0f; // Tiempo máximo entre alternancia SHAPE/PHYSICS (escalado con resolución)
|
||||||
|
|||||||
2160
source/engine.cpp
2160
source/engine.cpp
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDL3/SDL_events.h> // for SDL_Event
|
#include <SDL3/SDL_events.h> // for SDL_Event
|
||||||
#include <SDL3/SDL_render.h> // for SDL_Renderer
|
#include <SDL3/SDL_render.h> // for SDL_Renderer (ui_renderer_ software renderer)
|
||||||
#include <SDL3/SDL_stdinc.h> // for Uint64
|
#include <SDL3/SDL_stdinc.h> // for Uint64
|
||||||
|
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
|
||||||
#include <SDL3/SDL_video.h> // for SDL_Window
|
#include <SDL3/SDL_video.h> // for SDL_Window
|
||||||
|
|
||||||
#include <array> // for array
|
#include <array> // for array
|
||||||
@@ -10,17 +11,21 @@
|
|||||||
#include <string> // for string
|
#include <string> // for string
|
||||||
#include <vector> // for vector
|
#include <vector> // for vector
|
||||||
|
|
||||||
#include "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
|
||||||
#include "external/texture.hpp" // for Texture
|
#include "external/texture.hpp" // for Texture
|
||||||
|
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
|
||||||
|
#include "gpu/gpu_context.hpp" // for GpuContext
|
||||||
|
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
|
||||||
|
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
|
||||||
|
#include "gpu/gpu_texture.hpp" // for GpuTexture
|
||||||
#include "input/input_handler.hpp" // for InputHandler
|
#include "input/input_handler.hpp" // for InputHandler
|
||||||
#include "scene/scene_manager.hpp" // for SceneManager
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
#include "shapes/shape.hpp" // for Shape (interfaz polimórfica)
|
|
||||||
#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 {
|
||||||
@@ -71,6 +76,20 @@ class Engine {
|
|||||||
void toggleRealFullscreen();
|
void toggleRealFullscreen();
|
||||||
void toggleIntegerScaling();
|
void toggleIntegerScaling();
|
||||||
|
|
||||||
|
// Campo de juego (tamaño lógico + físico)
|
||||||
|
void fieldSizeUp();
|
||||||
|
void fieldSizeDown();
|
||||||
|
void setFieldScale(float new_scale);
|
||||||
|
|
||||||
|
// PostFX presets
|
||||||
|
void handlePostFXCycle();
|
||||||
|
void handlePostFXToggle();
|
||||||
|
void setInitialPostFX(int mode);
|
||||||
|
void setPostFXParamOverrides(float vignette, float chroma);
|
||||||
|
|
||||||
|
// Cicle PostFX nadiu (OFF → Vinyeta → Scanlines → Cromàtica → Complet)
|
||||||
|
void cycleShader();
|
||||||
|
|
||||||
// Modo kiosko
|
// Modo kiosko
|
||||||
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
void setKioskMode(bool enabled) { kiosk_mode_ = enabled; }
|
||||||
bool isKioskMode() const { return kiosk_mode_; }
|
bool isKioskMode() const { return kiosk_mode_; }
|
||||||
@@ -93,17 +112,11 @@ class Engine {
|
|||||||
void toggleDemoLiteMode();
|
void toggleDemoLiteMode();
|
||||||
void toggleLogoMode();
|
void toggleLogoMode();
|
||||||
|
|
||||||
// === Métodos públicos para StateManager (callbacks) ===
|
// === Métodos públicos para StateManager (automatización DEMO/LOGO sin notificación) ===
|
||||||
// NOTA: StateManager coordina estados, Engine proporciona implementación
|
void enterShapeMode(ShapeType type); // Activar figura (sin notificación)
|
||||||
// Estos callbacks permiten que StateManager ejecute acciones complejas que
|
void exitShapeMode(bool force_gravity = true); // Volver a física (sin notificación)
|
||||||
// requieren acceso a múltiples componentes (SceneManager, ThemeManager, ShapeManager, etc.)
|
void switchTextureSilent(); // Cambiar textura (sin notificación)
|
||||||
// Este enfoque es pragmático y mantiene la separación de responsabilidades limpia
|
void setTextureByIndex(size_t index); // Restaurar textura específica
|
||||||
void performLogoAction(bool logo_waiting_for_flip);
|
|
||||||
void executeDemoAction(bool is_lite);
|
|
||||||
void executeRandomizeOnDemoStart(bool is_lite);
|
|
||||||
void executeToggleGravityOnOff();
|
|
||||||
void executeEnterLogoMode(size_t ball_count);
|
|
||||||
void executeExitLogoMode();
|
|
||||||
|
|
||||||
// === Getters públicos para UIManager (Debug HUD) ===
|
// === Getters públicos para UIManager (Debug HUD) ===
|
||||||
bool getVSyncEnabled() const { return vsync_enabled_; }
|
bool getVSyncEnabled() const { return vsync_enabled_; }
|
||||||
@@ -119,6 +132,12 @@ class Engine {
|
|||||||
int getBaseScreenWidth() const { return base_screen_width_; }
|
int getBaseScreenWidth() const { return base_screen_width_; }
|
||||||
int getBaseScreenHeight() const { return base_screen_height_; }
|
int getBaseScreenHeight() const { return base_screen_height_; }
|
||||||
int getMaxAutoScenario() const { return max_auto_scenario_; }
|
int getMaxAutoScenario() const { return max_auto_scenario_; }
|
||||||
|
size_t getCurrentTextureIndex() const { return current_texture_index_; }
|
||||||
|
bool isPostFXEnabled() const { return postfx_enabled_; }
|
||||||
|
int getPostFXMode() const { return postfx_effect_mode_; }
|
||||||
|
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
|
||||||
|
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
|
||||||
|
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// === Componentes del sistema (Composición) ===
|
// === Componentes del sistema (Composición) ===
|
||||||
@@ -130,14 +149,34 @@ class Engine {
|
|||||||
std::unique_ptr<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
|
std::unique_ptr<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
|
||||||
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
|
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
|
||||||
|
|
||||||
// Recursos SDL
|
// === SDL window ===
|
||||||
SDL_Window* window_ = nullptr;
|
SDL_Window* window_ = nullptr;
|
||||||
SDL_Renderer* renderer_ = nullptr;
|
|
||||||
|
// === SDL_GPU rendering pipeline ===
|
||||||
|
std::unique_ptr<GpuContext> gpu_ctx_; // Device + swapchain
|
||||||
|
std::unique_ptr<GpuPipeline> gpu_pipeline_; // Sprite + ball + postfx pipelines
|
||||||
|
std::unique_ptr<GpuSpriteBatch> sprite_batch_; // Per-frame vertex/index batch (bg + shape + UI)
|
||||||
|
std::unique_ptr<GpuBallBuffer> gpu_ball_buffer_; // Instanced ball instance data (PHYSICS/BOIDS)
|
||||||
|
std::vector<BallGPUData> ball_gpu_data_; // CPU-side staging vector (reused each frame)
|
||||||
|
std::unique_ptr<GpuTexture> offscreen_tex_; // Offscreen render target (Pass 1)
|
||||||
|
std::unique_ptr<GpuTexture> white_tex_; // 1×1 white (background gradient)
|
||||||
|
std::unique_ptr<GpuTexture> ui_tex_; // UI text overlay texture
|
||||||
|
|
||||||
|
// GPU sprite textures (one per ball skin, parallel to textures_/texture_names_)
|
||||||
|
std::unique_ptr<GpuTexture> gpu_texture_; // Active GPU sprite texture
|
||||||
|
std::vector<std::unique_ptr<GpuTexture>> gpu_textures_; // All GPU sprite textures
|
||||||
|
|
||||||
|
// === SDL_Renderer (software, for UI text via SDL3_ttf) ===
|
||||||
|
// Renders to ui_surface_, then uploaded as gpu texture overlay.
|
||||||
|
SDL_Renderer* ui_renderer_ = nullptr;
|
||||||
|
SDL_Surface* ui_surface_ = nullptr;
|
||||||
|
|
||||||
|
// Legacy Texture objects — kept for ball physics sizing and AppLogo
|
||||||
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
|
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
|
||||||
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
|
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
|
||||||
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
|
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
|
||||||
size_t current_texture_index_ = 0; // Índice de textura activa
|
size_t current_texture_index_ = 0; // Índice de textura activa
|
||||||
int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico, se actualiza desde texture)
|
int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico)
|
||||||
|
|
||||||
// Estado del simulador
|
// Estado del simulador
|
||||||
bool should_exit_ = false;
|
bool should_exit_ = false;
|
||||||
@@ -146,15 +185,25 @@ class Engine {
|
|||||||
Uint64 last_frame_time_ = 0;
|
Uint64 last_frame_time_ = 0;
|
||||||
float delta_time_ = 0.0f;
|
float delta_time_ = 0.0f;
|
||||||
|
|
||||||
// Sistema de zoom dinámico
|
// PostFX uniforms (passed to GPU each frame)
|
||||||
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
PostFXUniforms postfx_uniforms_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
int postfx_effect_mode_ = 3;
|
||||||
|
bool postfx_enabled_ = false;
|
||||||
|
float postfx_override_vignette_ = -1.f; // -1 = sin override
|
||||||
|
float postfx_override_chroma_ = -1.f;
|
||||||
|
|
||||||
// V-Sync
|
// Sistema de escala de ventana
|
||||||
|
float current_window_scale_ = 1.0f;
|
||||||
|
|
||||||
|
// Escala del campo de juego lógico (F7/F8)
|
||||||
|
float current_field_scale_ = 1.0f;
|
||||||
|
|
||||||
|
// V-Sync y fullscreen
|
||||||
bool vsync_enabled_ = true;
|
bool vsync_enabled_ = true;
|
||||||
bool fullscreen_enabled_ = false;
|
bool fullscreen_enabled_ = false;
|
||||||
bool real_fullscreen_enabled_ = false;
|
bool real_fullscreen_enabled_ = false;
|
||||||
bool kiosk_mode_ = false;
|
bool kiosk_mode_ = false;
|
||||||
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER; // Modo de escalado actual (F5)
|
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER;
|
||||||
|
|
||||||
// Resolución base (configurada por CLI o default)
|
// Resolución base (configurada por CLI o default)
|
||||||
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
||||||
@@ -170,24 +219,13 @@ class Engine {
|
|||||||
|
|
||||||
// Sistema de temas (delegado a ThemeManager)
|
// Sistema de temas (delegado a ThemeManager)
|
||||||
std::unique_ptr<ThemeManager> theme_manager_;
|
std::unique_ptr<ThemeManager> theme_manager_;
|
||||||
int theme_page_ = 0; // Página actual de temas (0 o 1) para acceso por Numpad
|
int theme_page_ = 0;
|
||||||
|
|
||||||
// Sistema de Figuras 3D (polimórfico)
|
// Modo de simulación actual (PHYSICS/SHAPE/BOIDS)
|
||||||
// NOTA: Engine mantiene implementación de figuras usada por callbacks DEMO/LOGO
|
|
||||||
// ShapeManager tiene implementación paralela para controles manuales del usuario
|
|
||||||
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
||||||
ShapeType current_shape_type_ = ShapeType::SPHERE; // Tipo de figura actual
|
|
||||||
ShapeType last_shape_type_ = ShapeType::SPHERE; // Última figura para toggle F
|
|
||||||
std::unique_ptr<Shape> active_shape_; // Puntero polimórfico a figura activa
|
|
||||||
float shape_scale_factor_ = 1.0f; // Factor de escala manual (Numpad +/-)
|
|
||||||
bool depth_zoom_enabled_ = true; // Zoom por profundidad Z activado
|
|
||||||
|
|
||||||
// Sistema de Modo DEMO (auto-play) y LOGO
|
// Sistema de Modo DEMO (auto-play) y LOGO
|
||||||
// NOTA: Engine mantiene estado de implementación para callbacks performLogoAction()
|
int max_auto_scenario_ = 5;
|
||||||
// StateManager coordina los triggers y timers, Engine ejecuta las acciones
|
|
||||||
float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción
|
|
||||||
float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos)
|
|
||||||
int max_auto_scenario_ = 5; // Índice máximo en modos auto (default conservador: 5000 bolas)
|
|
||||||
|
|
||||||
// Escenario custom (--custom-balls)
|
// Escenario custom (--custom-balls)
|
||||||
int custom_scenario_balls_ = 0;
|
int custom_scenario_balls_ = 0;
|
||||||
@@ -195,43 +233,10 @@ class Engine {
|
|||||||
bool custom_auto_available_ = false;
|
bool custom_auto_available_ = false;
|
||||||
bool skip_benchmark_ = false;
|
bool skip_benchmark_ = false;
|
||||||
|
|
||||||
// Sistema de convergencia para LOGO MODE (escala con resolución)
|
|
||||||
// Usado por performLogoAction() para detectar cuando las bolas forman el logo
|
|
||||||
float shape_convergence_ = 0.0f; // % de pelotas cerca del objetivo (0.0-1.0)
|
|
||||||
float logo_convergence_threshold_ = 0.90f; // Threshold aleatorio (75-100%)
|
|
||||||
float logo_min_time_ = 3.0f; // Tiempo mínimo escalado con resolución
|
|
||||||
float logo_max_time_ = 5.0f; // Tiempo máximo escalado (backup)
|
|
||||||
|
|
||||||
// Sistema de espera de flips en LOGO MODE (camino alternativo)
|
|
||||||
// Permite que LOGO espere a que ocurran rotaciones antes de cambiar estado
|
|
||||||
bool logo_waiting_for_flip_ = false; // true si eligió el camino "esperar flip"
|
|
||||||
int logo_target_flip_number_ = 0; // En qué flip actuar (1, 2 o 3)
|
|
||||||
float logo_target_flip_percentage_ = 0.0f; // % de flip a esperar (0.2-0.8)
|
|
||||||
int logo_current_flip_count_ = 0; // Flips observados hasta ahora
|
|
||||||
|
|
||||||
// NOTA: logo_entered_manually_ fue eliminado de Engine (duplicado)
|
|
||||||
// Ahora se obtiene de StateManager con state_manager_->getLogoEnteredManually()
|
|
||||||
// Esto evita desincronización entre Engine y StateManager
|
|
||||||
|
|
||||||
// Estado previo antes de entrar a Logo Mode (para restaurar al salir)
|
|
||||||
// Guardado por executeEnterLogoMode(), restaurado por executeExitLogoMode()
|
|
||||||
int logo_previous_theme_ = 0; // Índice de tema (0-9)
|
|
||||||
size_t logo_previous_texture_index_ = 0;
|
|
||||||
float logo_previous_shape_scale_ = 1.0f;
|
|
||||||
|
|
||||||
// Batch rendering
|
|
||||||
std::vector<SDL_Vertex> batch_vertices_;
|
|
||||||
std::vector<int> batch_indices_;
|
|
||||||
|
|
||||||
// Bucket sort per z-ordering (SHAPE mode)
|
// Bucket sort per z-ordering (SHAPE mode)
|
||||||
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
||||||
std::array<std::vector<size_t>, DEPTH_SORT_BUCKETS> depth_buckets_;
|
std::array<std::vector<size_t>, DEPTH_SORT_BUCKETS> depth_buckets_;
|
||||||
|
|
||||||
// Configuración del sistema de texto (constantes configurables)
|
|
||||||
static constexpr const char* TEXT_FONT_PATH = "data/fonts/determination.ttf";
|
|
||||||
static constexpr int TEXT_BASE_SIZE = 24; // Tamaño base para 240p
|
|
||||||
static constexpr bool TEXT_ANTIALIASING = true; // true = suavizado, false = píxeles nítidos
|
|
||||||
|
|
||||||
// Métodos principales del loop
|
// Métodos principales del loop
|
||||||
void calculateDeltaTime();
|
void calculateDeltaTime();
|
||||||
void update();
|
void update();
|
||||||
@@ -240,27 +245,36 @@ class Engine {
|
|||||||
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
|
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
|
||||||
void runPerformanceBenchmark();
|
void runPerformanceBenchmark();
|
||||||
|
|
||||||
// Métodos auxiliares privados (llamados por la interfaz pública)
|
// Métodos auxiliares privados
|
||||||
|
|
||||||
// Sistema de cambio de sprites dinámico - Métodos privados
|
// Sistema de cambio de sprites dinámico
|
||||||
void switchTextureInternal(bool show_notification); // Implementación interna del cambio de textura
|
void switchTextureInternal(bool show_notification);
|
||||||
|
|
||||||
// Sistema de zoom dinámico - Métodos privados
|
// Sistema de escala de ventana
|
||||||
int calculateMaxWindowZoom() const;
|
float calculateMaxWindowScale() const;
|
||||||
void setWindowZoom(int new_zoom);
|
void setWindowScale(float new_scale);
|
||||||
void zoomIn();
|
void zoomIn();
|
||||||
void zoomOut();
|
void zoomOut();
|
||||||
void updatePhysicalWindowSize(); // Actualizar tamaño físico real de ventana
|
void updatePhysicalWindowSize();
|
||||||
|
|
||||||
// Rendering
|
// Rendering (GPU path replaces addSpriteToBatch)
|
||||||
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f);
|
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f);
|
||||||
|
|
||||||
// Sistema de Figuras 3D - Métodos privados
|
// Sistema de Figuras 3D
|
||||||
// NOTA FASE 7: Métodos DUPLICADOS con ShapeManager (Engine mantiene implementación para DEMO/LOGO)
|
void toggleShapeModeInternal(bool force_gravity_on_exit = true);
|
||||||
// TODO FASE 8: Convertir en wrappers puros cuando migremos DEMO/LOGO
|
void activateShapeInternal(ShapeType type);
|
||||||
void toggleShapeModeInternal(bool force_gravity_on_exit = true); // Implementación interna del toggle
|
void updateShape();
|
||||||
void activateShapeInternal(ShapeType type); // Implementación interna de activación
|
void generateShape();
|
||||||
void updateShape(); // Actualizar figura activa
|
|
||||||
void generateShape(); // Generar puntos de figura activa
|
// PostFX helper
|
||||||
void clampShapeScale(); // Limitar escala para evitar clipping
|
void applyPostFXPreset(int mode);
|
||||||
|
|
||||||
|
// Boids: comprueba si un escenario tiene ≤ BOIDS_MAX_BALLS bolas
|
||||||
|
bool isScenarioAllowedForBoids(int scenario_id) const;
|
||||||
|
|
||||||
|
// GPU helpers
|
||||||
|
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
|
||||||
|
void recreateOffscreenTexture(); // Recreate when resolution changes
|
||||||
|
void renderUIToSurface(); // Render text/UI to ui_surface_
|
||||||
|
void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_
|
||||||
};
|
};
|
||||||
|
|||||||
6
source/external/texture.cpp
vendored
6
source/external/texture.cpp
vendored
@@ -50,11 +50,7 @@ bool Texture::loadFromFile(const std::string &file_path) {
|
|||||||
delete[] resourceData; // Liberar buffer temporal
|
delete[] resourceData; // Liberar buffer temporal
|
||||||
|
|
||||||
if (data != nullptr) {
|
if (data != nullptr) {
|
||||||
if (ResourceManager::isPackLoaded()) {
|
std::cout << "[Textura] " << filename << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
std::cout << "Imagen cargada desde pack: " << filename.c_str() << std::endl;
|
|
||||||
} else {
|
|
||||||
std::cout << "Imagen cargada desde disco: " << filename.c_str() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
77
source/gpu/gpu_ball_buffer.cpp
Normal file
77
source/gpu/gpu_ball_buffer.cpp
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#include "gpu_ball_buffer.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <algorithm> // std::min
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
|
||||||
|
auto GpuBallBuffer::init(SDL_GPUDevice* device) -> bool {
|
||||||
|
Uint32 buf_size = static_cast<Uint32>(MAX_BALLS) * sizeof(BallGPUData);
|
||||||
|
|
||||||
|
// GPU vertex buffer (instance-rate data read by the ball instanced shader)
|
||||||
|
SDL_GPUBufferCreateInfo buf_info = {};
|
||||||
|
buf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||||
|
buf_info.size = buf_size;
|
||||||
|
gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info);
|
||||||
|
if (gpu_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer buffer (upload staging, cycled every frame)
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = buf_size;
|
||||||
|
transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (transfer_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: transfer buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("GpuBallBuffer: initialized (capacity %d balls, %.1f MB VRAM)",
|
||||||
|
MAX_BALLS,
|
||||||
|
buf_size / (1024.0f * 1024.0f));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuBallBuffer::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count) -> bool {
|
||||||
|
if ((data == nullptr) || count <= 0) {
|
||||||
|
count_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
count = std::min(count, MAX_BALLS);
|
||||||
|
|
||||||
|
Uint32 upload_size = static_cast<Uint32>(count) * sizeof(BallGPUData);
|
||||||
|
|
||||||
|
void* ptr = SDL_MapGPUTransferBuffer(device, transfer_buf_, true /* cycle */);
|
||||||
|
if (ptr == nullptr) {
|
||||||
|
SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(ptr, data, upload_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, transfer_buf_);
|
||||||
|
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
SDL_GPUTransferBufferLocation src = {transfer_buf_, 0};
|
||||||
|
SDL_GPUBufferRegion dst = {gpu_buf_, 0, upload_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &src, &dst, true /* cycle */);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
|
||||||
|
count_ = count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
47
source/gpu/gpu_ball_buffer.hpp
Normal file
47
source/gpu/gpu_ball_buffer.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BallGPUData — 32-byte per-instance record stored in VRAM.
|
||||||
|
// Positions and sizes pre-converted to NDC space on CPU so the vertex shader
|
||||||
|
// needs no screen-dimension uniform.
|
||||||
|
// cx, cy : NDC center (cx = (x + w/2)/sw*2-1, cy = 1-(y+h/2)/sh*2)
|
||||||
|
// hw, hh : NDC half-size (hw = w/sw, hh = h/sh, both positive)
|
||||||
|
// r,g,b,a: RGBA in [0,1]
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct BallGPUData {
|
||||||
|
float cx, cy; // NDC center
|
||||||
|
float hw, hh; // NDC half-size (positive)
|
||||||
|
float r, g, b, a; // RGBA color [0,1]
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BallGPUData) == 32, "BallGPUData must be 32 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuBallBuffer — owns the GPU vertex buffer used for instanced ball rendering.
|
||||||
|
//
|
||||||
|
// Usage per frame:
|
||||||
|
// buffer.upload(device, cmd, data, count); // inside a copy pass
|
||||||
|
// // Then in render pass: bind buffer, SDL_DrawGPUPrimitives(pass, 6, count, 0, 0)
|
||||||
|
// ============================================================================
|
||||||
|
class GpuBallBuffer {
|
||||||
|
public:
|
||||||
|
static constexpr int MAX_BALLS = 500000;
|
||||||
|
|
||||||
|
bool init(SDL_GPUDevice* device);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
// Upload ball array to GPU via an internal copy pass.
|
||||||
|
// count is clamped to MAX_BALLS. Returns false on error or empty input.
|
||||||
|
bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count);
|
||||||
|
|
||||||
|
SDL_GPUBuffer* buffer() const { return gpu_buf_; }
|
||||||
|
int count() const { return count_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_GPUBuffer* gpu_buf_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* transfer_buf_ = nullptr;
|
||||||
|
int count_ = 0;
|
||||||
|
};
|
||||||
78
source/gpu/gpu_context.cpp
Normal file
78
source/gpu/gpu_context.cpp
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#include "gpu_context.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
auto GpuContext::init(SDL_Window* window) -> bool {
|
||||||
|
window_ = window;
|
||||||
|
|
||||||
|
// Create GPU device: Metal on Apple, Vulkan elsewhere
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
|
||||||
|
#else
|
||||||
|
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
#endif
|
||||||
|
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << '\n';
|
||||||
|
|
||||||
|
// Claim the window so the GPU device owns its swapchain
|
||||||
|
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||||
|
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
|
||||||
|
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << '\n';
|
||||||
|
|
||||||
|
// Default: VSync ON
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, SDL_GPU_PRESENTMODE_VSYNC);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuContext::destroy() {
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
}
|
||||||
|
window_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::acquireCommandBuffer() -> SDL_GPUCommandBuffer* {
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||||
|
if (cmd == nullptr) {
|
||||||
|
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||||
|
Uint32* out_w,
|
||||||
|
Uint32* out_h) -> SDL_GPUTexture* {
|
||||||
|
SDL_GPUTexture* tex = nullptr;
|
||||||
|
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
|
||||||
|
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// tex == nullptr when window is minimized — caller should skip rendering
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuContext::setVSync(bool enabled) -> bool {
|
||||||
|
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
|
||||||
|
: SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||||
|
return SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode);
|
||||||
|
}
|
||||||
34
source/gpu/gpu_context.hpp
Normal file
34
source/gpu/gpu_context.hpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <SDL3/SDL_video.h>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuContext — SDL_GPU device + swapchain wrapper
|
||||||
|
// Replaces SDL_Renderer as the main rendering backend.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuContext {
|
||||||
|
public:
|
||||||
|
bool init(SDL_Window* window);
|
||||||
|
void destroy();
|
||||||
|
|
||||||
|
SDL_GPUDevice* device() const { return device_; }
|
||||||
|
SDL_Window* window() const { return window_; }
|
||||||
|
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
|
||||||
|
|
||||||
|
// Per-frame helpers
|
||||||
|
SDL_GPUCommandBuffer* acquireCommandBuffer();
|
||||||
|
// Returns nullptr if window is minimized (swapchain not available).
|
||||||
|
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||||
|
Uint32* out_w,
|
||||||
|
Uint32* out_h);
|
||||||
|
static void submit(SDL_GPUCommandBuffer* cmd_buf);
|
||||||
|
|
||||||
|
// VSync control (call after init)
|
||||||
|
bool setVSync(bool enabled);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_GPUDevice* device_ = nullptr;
|
||||||
|
SDL_Window* window_ = nullptr;
|
||||||
|
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||||
|
};
|
||||||
508
source/gpu/gpu_pipeline.cpp
Normal file
508
source/gpu/gpu_pipeline.cpp
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
#include "gpu_pipeline.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <array> // for std::array
|
||||||
|
#include <cstddef> // offsetof
|
||||||
|
#include <cstring> // strlen
|
||||||
|
|
||||||
|
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
|
||||||
|
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
// Generated at build time by CMake + glslc (see cmake/spv_to_header.cmake)
|
||||||
|
#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
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// ============================================================================
|
||||||
|
// MSL Shaders (Metal Shading Language, macOS)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sprite vertex shader
|
||||||
|
// Input: GpuVertex (pos=NDC float2, uv float2, col float4)
|
||||||
|
// Output: position, uv, col forwarded to fragment stage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kSpriteVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct SpriteVIn {
|
||||||
|
float2 pos [[attribute(0)]];
|
||||||
|
float2 uv [[attribute(1)]];
|
||||||
|
float4 col [[attribute(2)]];
|
||||||
|
};
|
||||||
|
struct SpriteVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex SpriteVOut sprite_vs(SpriteVIn in [[stage_in]]) {
|
||||||
|
SpriteVOut out;
|
||||||
|
out.pos = float4(in.pos, 0.0, 1.0);
|
||||||
|
out.uv = in.uv;
|
||||||
|
out.col = in.col;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sprite fragment shader
|
||||||
|
// Samples a texture and multiplies by vertex color (for tinting + alpha).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kSpriteFragMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct SpriteVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
fragment float4 sprite_fs(SpriteVOut in [[stage_in]],
|
||||||
|
texture2d<float> tex [[texture(0)]],
|
||||||
|
sampler samp [[sampler(0)]]) {
|
||||||
|
float4 t = tex.sample(samp, in.uv);
|
||||||
|
return float4(t.rgb * in.col.rgb, t.a * in.col.a);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PostFX vertex shader
|
||||||
|
// Generates a full-screen triangle from vertex_id (no vertex buffer needed).
|
||||||
|
// UV mapping: NDC(-1,-1)→UV(0,1) NDC(-1,3)→UV(0,-1) NDC(3,-1)→UV(2,1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kPostFXVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||||
|
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||||
|
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||||
|
PostVOut out;
|
||||||
|
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||||
|
out.uv = uvs[vid];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PostFX fragment shader
|
||||||
|
// Effects driven by PostFXUniforms (uniform buffer slot 0):
|
||||||
|
// - Chromatic aberration: RGB channel UV offset
|
||||||
|
// - Scanlines: sin-wave intensity modulation
|
||||||
|
// - Vignette: radial edge darkening
|
||||||
|
// MSL binding for fragment uniform buffer 0 with 1 sampler, 0 storage:
|
||||||
|
// constant PostFXUniforms& u [[buffer(0)]]
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kPostFXFragMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct PostVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_strength;
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
};
|
||||||
|
|
||||||
|
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||||
|
texture2d<float> scene [[texture(0)]],
|
||||||
|
sampler samp [[sampler(0)]],
|
||||||
|
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||||
|
// Chromatic aberration: offset R and B channels horizontally
|
||||||
|
float ca = u.chroma_strength * 0.005;
|
||||||
|
float4 color;
|
||||||
|
color.r = scene.sample(samp, in.uv + float2( ca, 0.0)).r;
|
||||||
|
color.g = scene.sample(samp, in.uv ).g;
|
||||||
|
color.b = scene.sample(samp, in.uv - float2( ca, 0.0)).b;
|
||||||
|
color.a = scene.sample(samp, in.uv ).a;
|
||||||
|
|
||||||
|
// Scanlines: horizontal sine-wave at ~360 lines (one dark band per 2 px at 720p)
|
||||||
|
float scan = 0.85 + 0.15 * sin(in.uv.y * 3.14159265 * u.screen_height);
|
||||||
|
color.rgb *= mix(1.0, scan, u.scanline_strength);
|
||||||
|
|
||||||
|
// Vignette: radial edge darkening
|
||||||
|
float2 d = in.uv - float2(0.5, 0.5);
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
color.rgb *= clamp(vignette, 0.0, 1.0);
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ball instanced vertex shader
|
||||||
|
// Reads BallGPUData as per-instance attributes (input_rate = INSTANCE).
|
||||||
|
// Generates a 6-vertex quad (2 triangles) per instance using vertex_id.
|
||||||
|
//
|
||||||
|
// BallGPUData layout:
|
||||||
|
// float2 center [[attribute(0)]] — NDC center (cx, cy)
|
||||||
|
// float2 half [[attribute(1)]] — NDC half-size (hw, hh), both positive
|
||||||
|
// float4 col [[attribute(2)]] — RGBA [0,1]
|
||||||
|
//
|
||||||
|
// NDC convention (SDL / Metal): Y increases upward (+1=top, -1=bottom).
|
||||||
|
// half.x = w/screen_w, half.y = h/screen_h (positive; Y is not flipped)
|
||||||
|
// Vertex order: TL TR BL | TR BR BL (CCW winding, standard Metal)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static const char* kBallInstancedVertMSL = R"(
|
||||||
|
#include <metal_stdlib>
|
||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
struct BallInstance {
|
||||||
|
float2 center [[attribute(0)]]; // NDC center
|
||||||
|
float2 halfsize [[attribute(1)]]; // NDC half-size (both positive); 'half' is reserved in MSL
|
||||||
|
float4 col [[attribute(2)]];
|
||||||
|
};
|
||||||
|
struct BallVOut {
|
||||||
|
float4 pos [[position]];
|
||||||
|
float2 uv;
|
||||||
|
float4 col;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex BallVOut ball_instanced_vs(BallInstance inst [[stage_in]],
|
||||||
|
uint vid [[vertex_id]]) {
|
||||||
|
// Offset signs for each of the 6 vertices (TL TR BL | TR BR BL)
|
||||||
|
const float2 offsets[6] = {
|
||||||
|
{-1.0f, 1.0f}, // TL
|
||||||
|
{ 1.0f, 1.0f}, // TR
|
||||||
|
{-1.0f, -1.0f}, // BL
|
||||||
|
{ 1.0f, 1.0f}, // TR (shared)
|
||||||
|
{ 1.0f, -1.0f}, // BR
|
||||||
|
{-1.0f, -1.0f}, // BL (shared)
|
||||||
|
};
|
||||||
|
// UV: TL=(0,0) TR=(1,0) BL=(0,1) BR=(1,1)
|
||||||
|
const float2 uvs[6] = {
|
||||||
|
{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f},
|
||||||
|
{1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f},
|
||||||
|
};
|
||||||
|
float2 pos = inst.center + offsets[vid] * inst.halfsize;
|
||||||
|
BallVOut out;
|
||||||
|
out.pos = float4(pos.x, pos.y, 0.0f, 1.0f);
|
||||||
|
out.uv = uvs[vid];
|
||||||
|
out.col = inst.col;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuPipeline implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
auto GpuPipeline::init(SDL_GPUDevice* device,
|
||||||
|
SDL_GPUTextureFormat target_format,
|
||||||
|
SDL_GPUTextureFormat offscreen_format) -> bool {
|
||||||
|
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
|
||||||
|
SDL_Log("GpuPipeline: MSL not supported (format mask=%u)", supported);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if ((supported & SDL_GPU_SHADERFORMAT_SPIRV) == 0u) {
|
||||||
|
SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Sprite pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size, "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);
|
||||||
|
#endif
|
||||||
|
if ((sprite_vert == nullptr) || (sprite_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create sprite shaders");
|
||||||
|
if (sprite_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_vert);
|
||||||
|
}
|
||||||
|
if (sprite_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex input: GpuVertex layout
|
||||||
|
SDL_GPUVertexBufferDescription vb_desc = {};
|
||||||
|
vb_desc.slot = 0;
|
||||||
|
vb_desc.pitch = sizeof(GpuVertex);
|
||||||
|
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||||
|
vb_desc.instance_step_rate = 0;
|
||||||
|
|
||||||
|
std::array<SDL_GPUVertexAttribute, 3> attrs = {};
|
||||||
|
attrs[0].location = 0;
|
||||||
|
attrs[0].buffer_slot = 0;
|
||||||
|
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
|
||||||
|
|
||||||
|
attrs[1].location = 1;
|
||||||
|
attrs[1].buffer_slot = 0;
|
||||||
|
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
|
||||||
|
|
||||||
|
attrs[2].location = 2;
|
||||||
|
attrs[2].buffer_slot = 0;
|
||||||
|
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||||
|
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState vertex_input = {};
|
||||||
|
vertex_input.vertex_buffer_descriptions = &vb_desc;
|
||||||
|
vertex_input.num_vertex_buffers = 1;
|
||||||
|
vertex_input.vertex_attributes = attrs.data();
|
||||||
|
vertex_input.num_vertex_attributes = 3;
|
||||||
|
|
||||||
|
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
|
||||||
|
SDL_GPUColorTargetBlendState blend = {};
|
||||||
|
blend.enable_blend = true;
|
||||||
|
blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
||||||
|
blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||||
|
blend.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||||
|
blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
||||||
|
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||||
|
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||||
|
blend.enable_color_write_mask = false; // write all channels
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription color_target_desc = {};
|
||||||
|
color_target_desc.format = offscreen_format;
|
||||||
|
color_target_desc.blend_state = blend;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo sprite_pipe_info = {};
|
||||||
|
sprite_pipe_info.vertex_shader = sprite_vert;
|
||||||
|
sprite_pipe_info.fragment_shader = sprite_frag;
|
||||||
|
sprite_pipe_info.vertex_input_state = vertex_input;
|
||||||
|
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
sprite_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
|
||||||
|
|
||||||
|
sprite_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &sprite_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, sprite_frag);
|
||||||
|
|
||||||
|
if (sprite_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Ball instanced pipeline
|
||||||
|
// Vertex: ball_instanced_vs (BallGPUData per-instance, no index buffer)
|
||||||
|
// Fragment: sprite_fs (same texture+color blend as sprite pipeline)
|
||||||
|
// Targets: offscreen (same as sprite pipeline)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size, "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);
|
||||||
|
#endif
|
||||||
|
if ((ball_vert == nullptr) || (ball_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create ball instanced shaders");
|
||||||
|
if (ball_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, ball_vert);
|
||||||
|
}
|
||||||
|
if (ball_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, ball_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex input: BallGPUData as per-instance data (step rate = 1 instance)
|
||||||
|
SDL_GPUVertexBufferDescription ball_vb_desc = {};
|
||||||
|
ball_vb_desc.slot = 0;
|
||||||
|
ball_vb_desc.pitch = sizeof(BallGPUData);
|
||||||
|
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
|
||||||
|
ball_vb_desc.instance_step_rate = 1;
|
||||||
|
|
||||||
|
std::array<SDL_GPUVertexAttribute, 3> ball_attrs = {};
|
||||||
|
// attr 0: center (float2) at offset 0
|
||||||
|
ball_attrs[0].location = 0;
|
||||||
|
ball_attrs[0].buffer_slot = 0;
|
||||||
|
ball_attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
ball_attrs[0].offset = static_cast<Uint32>(offsetof(BallGPUData, cx));
|
||||||
|
// attr 1: half-size (float2) at offset 8
|
||||||
|
ball_attrs[1].location = 1;
|
||||||
|
ball_attrs[1].buffer_slot = 0;
|
||||||
|
ball_attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||||
|
ball_attrs[1].offset = static_cast<Uint32>(offsetof(BallGPUData, hw));
|
||||||
|
// attr 2: color (float4) at offset 16
|
||||||
|
ball_attrs[2].location = 2;
|
||||||
|
ball_attrs[2].buffer_slot = 0;
|
||||||
|
ball_attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||||
|
ball_attrs[2].offset = static_cast<Uint32>(offsetof(BallGPUData, r));
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState ball_vertex_input = {};
|
||||||
|
ball_vertex_input.vertex_buffer_descriptions = &ball_vb_desc;
|
||||||
|
ball_vertex_input.num_vertex_buffers = 1;
|
||||||
|
ball_vertex_input.vertex_attributes = ball_attrs.data();
|
||||||
|
ball_vertex_input.num_vertex_attributes = 3;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo ball_pipe_info = {};
|
||||||
|
ball_pipe_info.vertex_shader = ball_vert;
|
||||||
|
ball_pipe_info.fragment_shader = ball_frag;
|
||||||
|
ball_pipe_info.vertex_input_state = ball_vertex_input;
|
||||||
|
ball_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
ball_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
ball_pipe_info.target_info.color_target_descriptions = &color_target_desc;
|
||||||
|
|
||||||
|
ball_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &ball_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, ball_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, ball_frag);
|
||||||
|
|
||||||
|
if (ball_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// UI overlay pipeline (same as sprite but renders to swapchain format)
|
||||||
|
// Reuse sprite shaders with different target format.
|
||||||
|
// We create a second version of the sprite pipeline for swapchain.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// (postfx pipeline targets swapchain; UI overlay also targets swapchain
|
||||||
|
// but needs its own pipeline with swapchain format.)
|
||||||
|
// For simplicity, the sprite pipeline is used for the offscreen pass only.
|
||||||
|
// The UI overlay is composited via a separate postfx-like pass below.
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// PostFX pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size, "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);
|
||||||
|
#endif
|
||||||
|
if ((postfx_vert == nullptr) || (postfx_frag == nullptr)) {
|
||||||
|
SDL_Log("GpuPipeline: failed to create postfx shaders");
|
||||||
|
if (postfx_vert != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||||
|
}
|
||||||
|
if (postfx_frag != nullptr) {
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostFX: no vertex input (uses vertex_id), no blend (replace output)
|
||||||
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
|
no_blend.enable_blend = false;
|
||||||
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription postfx_target_desc = {};
|
||||||
|
postfx_target_desc.format = target_format;
|
||||||
|
postfx_target_desc.blend_state = no_blend;
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo postfx_pipe_info = {};
|
||||||
|
postfx_pipe_info.vertex_shader = postfx_vert;
|
||||||
|
postfx_pipe_info.fragment_shader = postfx_frag;
|
||||||
|
postfx_pipe_info.vertex_input_state = no_input;
|
||||||
|
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
postfx_pipe_info.target_info.num_color_targets = 1;
|
||||||
|
postfx_pipe_info.target_info.color_target_descriptions = &postfx_target_desc;
|
||||||
|
|
||||||
|
postfx_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &postfx_pipe_info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||||
|
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||||
|
|
||||||
|
if (postfx_pipeline_ == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("GpuPipeline: all pipelines created successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuPipeline::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (sprite_pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = spv_code;
|
||||||
|
info.code_size = spv_size;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_storage_textures = 0;
|
||||||
|
info.num_storage_buffers = num_storage_buffers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuPipeline::createShader(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||||
|
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_storage_textures = 0;
|
||||||
|
info.num_storage_buffers = num_storage_buffers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
63
source/gpu/gpu_pipeline.hpp
Normal file
63
source/gpu/gpu_pipeline.hpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PostFXUniforms — pushed to the fragment stage each frame via
|
||||||
|
// SDL_PushGPUFragmentUniformData(pass, 0, &uniforms, sizeof(PostFXUniforms))
|
||||||
|
// MSL binding: constant PostFXUniforms& u [[buffer(0)]]
|
||||||
|
// ============================================================================
|
||||||
|
struct PostFXUniforms {
|
||||||
|
float vignette_strength; // 0 = none, 0.8 = default subtle
|
||||||
|
float chroma_strength; // 0 = off, 0.2 = default chromatic aberration
|
||||||
|
float scanline_strength; // 0 = off, 1 = full scanlines
|
||||||
|
float screen_height; // logical render target height (px), for resolution-independent scanlines
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuPipeline — Creates and owns the graphics pipelines used by the engine.
|
||||||
|
//
|
||||||
|
// sprite_pipeline_ : textured quads, alpha blending.
|
||||||
|
// Vertex layout: GpuVertex (pos float2, uv float2, col float4).
|
||||||
|
// ball_pipeline_ : instanced ball rendering, alpha blending.
|
||||||
|
// Vertex layout: BallGPUData as per-instance data (input_rate=INSTANCE).
|
||||||
|
// 6 procedural vertices per instance (no index buffer).
|
||||||
|
// postfx_pipeline_ : full-screen triangle, no vertex buffer, no blend.
|
||||||
|
// Reads offscreen texture, writes to swapchain.
|
||||||
|
// Accepts PostFXUniforms via fragment uniform buffer slot 0.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuPipeline {
|
||||||
|
public:
|
||||||
|
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
|
||||||
|
// offscreen_format: format of the offscreen render target.
|
||||||
|
bool init(SDL_GPUDevice* device,
|
||||||
|
SDL_GPUTextureFormat target_format,
|
||||||
|
SDL_GPUTextureFormat offscreen_format);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
|
||||||
|
SDL_GPUGraphicsPipeline* ballPipeline() const { return ball_pipeline_; }
|
||||||
|
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static SDL_GPUShader* createShader(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers = 0);
|
||||||
|
|
||||||
|
static SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers,
|
||||||
|
Uint32 num_storage_buffers = 0);
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
|
||||||
|
SDL_GPUGraphicsPipeline* ball_pipeline_ = nullptr;
|
||||||
|
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
|
||||||
|
};
|
||||||
236
source/gpu/gpu_sprite_batch.cpp
Normal file
236
source/gpu/gpu_sprite_batch.cpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#include "gpu_sprite_batch.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) -> bool {
|
||||||
|
max_sprites_ = max_sprites;
|
||||||
|
// 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.
|
||||||
|
Uint32 max_verts = static_cast<Uint32>(max_sprites_ + 2) * 4;
|
||||||
|
Uint32 max_indices = static_cast<Uint32>(max_sprites_ + 2) * 6;
|
||||||
|
|
||||||
|
Uint32 vb_size = max_verts * sizeof(GpuVertex);
|
||||||
|
Uint32 ib_size = max_indices * sizeof(uint32_t);
|
||||||
|
|
||||||
|
// Vertex buffer
|
||||||
|
SDL_GPUBufferCreateInfo vb_info = {};
|
||||||
|
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||||
|
vb_info.size = vb_size;
|
||||||
|
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
|
||||||
|
if (vertex_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index buffer
|
||||||
|
SDL_GPUBufferCreateInfo ib_info = {};
|
||||||
|
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
|
||||||
|
ib_info.size = ib_size;
|
||||||
|
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
|
||||||
|
if (index_buf_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer buffers (reused every frame via cycle=true on upload)
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
|
||||||
|
tb_info.size = vb_size;
|
||||||
|
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (vertex_transfer_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tb_info.size = ib_size;
|
||||||
|
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (index_transfer_ == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vertices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 4);
|
||||||
|
indices_.reserve(static_cast<size_t>(max_sprites_ + 2) * 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vertex_transfer_ != 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() {
|
||||||
|
vertices_.clear();
|
||||||
|
indices_.clear();
|
||||||
|
bg_index_count_ = 0;
|
||||||
|
sprite_index_offset_ = 0;
|
||||||
|
sprite_index_count_ = 0;
|
||||||
|
overlay_index_offset_ = 0;
|
||||||
|
overlay_index_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Background is the full screen quad, corners:
|
||||||
|
// TL(-1, 1) TR(1, 1) → top color
|
||||||
|
// BL(-1,-1) BR(1,-1) → bottom color
|
||||||
|
// We push it as 4 separate vertices (different colors per row).
|
||||||
|
auto vi = static_cast<uint32_t>(vertices_.size());
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f});
|
||||||
|
// Top-right
|
||||||
|
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f});
|
||||||
|
// Bottom-right
|
||||||
|
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
|
||||||
|
// Bottom-left
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
sprite_index_offset_ = 6;
|
||||||
|
|
||||||
|
(void)screen_w;
|
||||||
|
(void)screen_h; // unused — bg always covers full NDC
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Apply scale around the sprite centre
|
||||||
|
float scaled_w = w * scale;
|
||||||
|
float scaled_h = h * scale;
|
||||||
|
float offset_x = (w - scaled_w) * 0.5f;
|
||||||
|
float offset_y = (h - scaled_h) * 0.5f;
|
||||||
|
|
||||||
|
float px0 = x + offset_x;
|
||||||
|
float py0 = y + offset_y;
|
||||||
|
float px1 = px0 + scaled_w;
|
||||||
|
float py1 = py0 + scaled_h;
|
||||||
|
|
||||||
|
float ndx0;
|
||||||
|
float ndy0;
|
||||||
|
float ndx1;
|
||||||
|
float ndy1;
|
||||||
|
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
|
||||||
|
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
|
||||||
|
|
||||||
|
pushQuad(ndx0, ndy0, ndx1, ndy1, 0.0f, 0.0f, 1.0f, 1.0f, r, g, b, a);
|
||||||
|
sprite_index_count_ += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuSpriteBatch::addFullscreenOverlay() {
|
||||||
|
// 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().
|
||||||
|
overlay_index_offset_ = static_cast<int>(indices_.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, 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, 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 + 2);
|
||||||
|
indices_.push_back(vi + 3);
|
||||||
|
indices_.push_back(vi + 0);
|
||||||
|
overlay_index_count_ = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) -> bool {
|
||||||
|
if (vertices_.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
|
||||||
|
auto ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
|
||||||
|
|
||||||
|
// Map → write → unmap transfer buffers
|
||||||
|
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
|
||||||
|
if (vp == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: vertex map failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(vp, vertices_.data(), vb_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
|
||||||
|
|
||||||
|
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
|
||||||
|
if (ip == nullptr) {
|
||||||
|
SDL_Log("GpuSpriteBatch: index map failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(ip, indices_.data(), ib_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, index_transfer_);
|
||||||
|
|
||||||
|
// Upload via copy pass
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferLocation v_src = {vertex_transfer_, 0};
|
||||||
|
SDL_GPUBufferRegion v_dst = {vertex_buf_, 0, vb_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &v_src, &v_dst, true /* cycle */);
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferLocation i_src = {index_transfer_, 0};
|
||||||
|
SDL_GPUBufferRegion i_dst = {index_buf_, 0, ib_size};
|
||||||
|
SDL_UploadToGPUBuffer(copy, &i_src, &i_dst, true /* cycle */);
|
||||||
|
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GpuSpriteBatch::toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy) {
|
||||||
|
ndx = (px / screen_w) * 2.0f - 1.0f;
|
||||||
|
ndy = 1.0f - (py / screen_h) * 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// +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;
|
||||||
|
}
|
||||||
|
auto vi = static_cast<uint32_t>(vertices_.size());
|
||||||
|
|
||||||
|
// TL, TR, BR, BL
|
||||||
|
vertices_.push_back({ndx0, ndy0, u0, v0, r, g, b, a});
|
||||||
|
vertices_.push_back({ndx1, ndy0, u1, v0, 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});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
81
source/gpu/gpu_sprite_batch.hpp
Normal file
81
source/gpu/gpu_sprite_batch.hpp
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GpuVertex — 8-float vertex layout sent to the GPU.
|
||||||
|
// Position is in NDC (pre-transformed on CPU), UV in [0,1], color in [0,1].
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct GpuVertex {
|
||||||
|
float x, y; // NDC position (−1..1)
|
||||||
|
float u, v; // Texture coords (0..1)
|
||||||
|
float r, g, b, a; // RGBA color (0..1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuSpriteBatch — Accumulates sprite quads, uploads them in one copy pass.
|
||||||
|
//
|
||||||
|
// Usage per frame:
|
||||||
|
// batch.beginFrame();
|
||||||
|
// batch.addBackground(...); // Must be first (bg indices = [0..5])
|
||||||
|
// batch.addSprite(...) × N;
|
||||||
|
// batch.uploadBatch(device, cmd); // Copy pass
|
||||||
|
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuSpriteBatch {
|
||||||
|
public:
|
||||||
|
// Default maximum sprites (background + UI overlay each count as one sprite)
|
||||||
|
static constexpr int DEFAULT_MAX_SPRITES = 200000;
|
||||||
|
|
||||||
|
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
void beginFrame();
|
||||||
|
|
||||||
|
// Add the full-screen background gradient quad.
|
||||||
|
// top_* and bot_* are RGB in [0,1].
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Add a sprite quad (pixel coordinates).
|
||||||
|
// scale: uniform scale around the quad centre.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Add a full-screen overlay quad (e.g. UI surface, NDC −1..1).
|
||||||
|
void addFullscreenOverlay();
|
||||||
|
|
||||||
|
// Upload CPU vectors to GPU buffers via a copy pass.
|
||||||
|
// Returns false if the batch is empty.
|
||||||
|
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
|
||||||
|
|
||||||
|
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
|
||||||
|
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
|
||||||
|
int bgIndexCount() const { return bg_index_count_; }
|
||||||
|
int overlayIndexOffset() const { return overlay_index_offset_; }
|
||||||
|
int overlayIndexCount() const { return overlay_index_count_; }
|
||||||
|
int spriteIndexOffset() const { return sprite_index_offset_; }
|
||||||
|
int spriteIndexCount() const { return sprite_index_count_; }
|
||||||
|
bool isEmpty() const { return vertices_.empty(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy);
|
||||||
|
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<uint32_t> indices_;
|
||||||
|
|
||||||
|
SDL_GPUBuffer* vertex_buf_ = nullptr;
|
||||||
|
SDL_GPUBuffer* index_buf_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
|
||||||
|
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
|
||||||
|
|
||||||
|
int bg_index_count_ = 0;
|
||||||
|
int sprite_index_offset_ = 0;
|
||||||
|
int sprite_index_count_ = 0;
|
||||||
|
int overlay_index_offset_ = 0;
|
||||||
|
int overlay_index_count_ = 0;
|
||||||
|
|
||||||
|
int max_sprites_ = DEFAULT_MAX_SPRITES;
|
||||||
|
};
|
||||||
228
source/gpu/gpu_texture.cpp
Normal file
228
source/gpu/gpu_texture.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#include "gpu_texture.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
#include <SDL3/SDL_pixels.h>
|
||||||
|
|
||||||
|
#include <array> // for std::array
|
||||||
|
#include <cstring> // memcpy
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// stb_image is compiled in texture.cpp (STB_IMAGE_IMPLEMENTATION defined there)
|
||||||
|
#include "external/stb_image.h"
|
||||||
|
#include "resource_manager.hpp"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) -> bool {
|
||||||
|
unsigned char* resource_data = nullptr;
|
||||||
|
size_t resource_size = 0;
|
||||||
|
|
||||||
|
if (!ResourceManager::loadResource(file_path, resource_data, resource_size)) {
|
||||||
|
SDL_Log("GpuTexture: can't load resource '%s'", file_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
int orig = 0;
|
||||||
|
unsigned char* pixels = stbi_load_from_memory(
|
||||||
|
resource_data,
|
||||||
|
static_cast<int>(resource_size),
|
||||||
|
&w,
|
||||||
|
&h,
|
||||||
|
&orig,
|
||||||
|
STBI_rgb_alpha);
|
||||||
|
delete[] resource_data;
|
||||||
|
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
|
||||||
|
file_path.c_str(),
|
||||||
|
stbi_failure_reason());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(device);
|
||||||
|
bool ok = uploadPixels(device, pixels, w, h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, true /*nearest = pixel-perfect sprites*/);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) -> bool {
|
||||||
|
if (surface == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure RGBA32 format
|
||||||
|
SDL_Surface* rgba = surface;
|
||||||
|
bool need_free = false;
|
||||||
|
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
|
||||||
|
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
if (rgba == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
need_free = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(device);
|
||||||
|
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, nearest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_free) {
|
||||||
|
SDL_DestroySurface(rgba);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format) -> bool {
|
||||||
|
destroy(device);
|
||||||
|
|
||||||
|
SDL_GPUTextureCreateInfo info = {};
|
||||||
|
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
info.format = format;
|
||||||
|
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
info.width = static_cast<Uint32>(w);
|
||||||
|
info.height = static_cast<Uint32>(h);
|
||||||
|
info.layer_count_or_depth = 1;
|
||||||
|
info.num_levels = 1;
|
||||||
|
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||||
|
|
||||||
|
texture_ = SDL_CreateGPUTexture(device, &info);
|
||||||
|
if (texture_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
width_ = w;
|
||||||
|
height_ = h;
|
||||||
|
|
||||||
|
// Render targets are sampled with linear filter (postfx reads them)
|
||||||
|
return createSampler(device, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createWhite(SDL_GPUDevice* device) -> bool {
|
||||||
|
destroy(device);
|
||||||
|
// 1×1 white RGBA pixel
|
||||||
|
constexpr std::array<Uint8, 4> WHITE = {255, 255, 255, 255};
|
||||||
|
bool ok = uploadPixels(device, WHITE.data(), 1, 1, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||||
|
if (ok) {
|
||||||
|
ok = createSampler(device, true);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuTexture::destroy(SDL_GPUDevice* device) {
|
||||||
|
if (device == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sampler_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUSampler(device, sampler_);
|
||||||
|
sampler_ = nullptr;
|
||||||
|
}
|
||||||
|
if (texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
}
|
||||||
|
width_ = height_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
auto GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format) -> bool {
|
||||||
|
// Create GPU texture
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = format;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(w);
|
||||||
|
tex_info.height = static_cast<Uint32>(h);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||||
|
|
||||||
|
texture_ = SDL_CreateGPUTexture(device, &tex_info);
|
||||||
|
if (texture_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transfer buffer and upload pixels
|
||||||
|
auto data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = data_size;
|
||||||
|
|
||||||
|
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||||
|
if (transfer == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||||
|
SDL_ReleaseGPUTexture(device, texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(mapped, pixels, data_size);
|
||||||
|
SDL_UnmapGPUTransferBuffer(device, transfer);
|
||||||
|
|
||||||
|
// Upload via command buffer
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
|
||||||
|
SDL_GPUTextureTransferInfo src = {};
|
||||||
|
src.transfer_buffer = transfer;
|
||||||
|
src.offset = 0;
|
||||||
|
src.pixels_per_row = static_cast<Uint32>(w);
|
||||||
|
src.rows_per_layer = static_cast<Uint32>(h);
|
||||||
|
|
||||||
|
SDL_GPUTextureRegion dst = {};
|
||||||
|
dst.texture = texture_;
|
||||||
|
dst.mip_level = 0;
|
||||||
|
dst.layer = 0;
|
||||||
|
dst.x = dst.y = dst.z = 0;
|
||||||
|
dst.w = static_cast<Uint32>(w);
|
||||||
|
dst.h = static_cast<Uint32>(h);
|
||||||
|
dst.d = 1;
|
||||||
|
|
||||||
|
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||||
|
width_ = w;
|
||||||
|
height_ = h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) -> bool {
|
||||||
|
SDL_GPUSamplerCreateInfo info = {};
|
||||||
|
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.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||||
|
info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
|
||||||
|
sampler_ = SDL_CreateGPUSampler(device, &info);
|
||||||
|
if (sampler_ == nullptr) {
|
||||||
|
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
47
source/gpu/gpu_texture.hpp
Normal file
47
source/gpu/gpu_texture.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
#include <SDL3/SDL_surface.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GpuTexture — SDL_GPU texture + sampler wrapper
|
||||||
|
// Handles sprite textures, render targets, and the 1×1 white utility texture.
|
||||||
|
// ============================================================================
|
||||||
|
class GpuTexture {
|
||||||
|
public:
|
||||||
|
GpuTexture() = default;
|
||||||
|
~GpuTexture() = default;
|
||||||
|
|
||||||
|
// Load from resource path (pack or disk) using stb_image.
|
||||||
|
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
|
||||||
|
|
||||||
|
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
|
||||||
|
// Uses nearest-neighbor filter for sprite pixel-perfect look.
|
||||||
|
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
|
||||||
|
|
||||||
|
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
|
||||||
|
bool createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format);
|
||||||
|
|
||||||
|
// Create a 1×1 opaque white texture (used for untextured geometry).
|
||||||
|
bool createWhite(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
// Release GPU resources.
|
||||||
|
void destroy(SDL_GPUDevice* device);
|
||||||
|
|
||||||
|
SDL_GPUTexture* texture() const { return texture_; }
|
||||||
|
SDL_GPUSampler* sampler() const { return sampler_; }
|
||||||
|
int width() const { return width_; }
|
||||||
|
int height() const { return height_; }
|
||||||
|
bool isValid() const { return texture_ != nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format);
|
||||||
|
bool createSampler(SDL_GPUDevice* device, bool nearest);
|
||||||
|
|
||||||
|
SDL_GPUTexture* texture_ = nullptr;
|
||||||
|
SDL_GPUSampler* sampler_ = nullptr;
|
||||||
|
int width_ = 0;
|
||||||
|
int height_ = 0;
|
||||||
|
};
|
||||||
@@ -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()) {
|
||||||
@@ -38,19 +39,19 @@ bool InputHandler::processEvents(Engine& engine) {
|
|||||||
|
|
||||||
// Controles de dirección de gravedad con teclas de cursor
|
// Controles de dirección de gravedad con teclas de cursor
|
||||||
case SDLK_UP:
|
case SDLK_UP:
|
||||||
engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad Arriba");
|
engine.handleGravityDirectionChange(GravityDirection::UP, "Gravedad arriba");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad Abajo");
|
engine.handleGravityDirectionChange(GravityDirection::DOWN, "Gravedad abajo");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad Izquierda");
|
engine.handleGravityDirectionChange(GravityDirection::LEFT, "Gravedad izquierda");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_RIGHT:
|
case SDLK_RIGHT:
|
||||||
engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad Derecha");
|
engine.handleGravityDirectionChange(GravityDirection::RIGHT, "Gravedad derecha");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_V:
|
case SDLK_V:
|
||||||
@@ -105,23 +106,21 @@ bool InputHandler::processEvents(Engine& engine) {
|
|||||||
|
|
||||||
// Toggle Modo Boids (comportamiento de enjambre)
|
// Toggle Modo Boids (comportamiento de enjambre)
|
||||||
case SDLK_B:
|
case SDLK_B:
|
||||||
// engine.toggleBoidsMode();
|
engine.toggleBoidsMode();
|
||||||
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:
|
||||||
@@ -193,76 +192,110 @@ bool InputHandler::processEvents(Engine& engine) {
|
|||||||
|
|
||||||
// Cambio de número de pelotas (escenarios 1-8)
|
// Cambio de número de pelotas (escenarios 1-8)
|
||||||
case SDLK_1:
|
case SDLK_1:
|
||||||
engine.changeScenario(0, "10 Pelotas");
|
engine.changeScenario(0, "10 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_2:
|
case SDLK_2:
|
||||||
engine.changeScenario(1, "50 Pelotas");
|
engine.changeScenario(1, "50 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_3:
|
case SDLK_3:
|
||||||
engine.changeScenario(2, "100 Pelotas");
|
engine.changeScenario(2, "100 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_4:
|
case SDLK_4:
|
||||||
engine.changeScenario(3, "500 Pelotas");
|
engine.changeScenario(3, "500 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_5:
|
case SDLK_5:
|
||||||
engine.changeScenario(4, "1,000 Pelotas");
|
engine.changeScenario(4, "1.000 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_6:
|
case SDLK_6:
|
||||||
engine.changeScenario(5, "5,000 Pelotas");
|
engine.changeScenario(5, "5.000 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_7:
|
case SDLK_7:
|
||||||
engine.changeScenario(6, "10,000 Pelotas");
|
engine.changeScenario(6, "10.000 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_8:
|
case SDLK_8:
|
||||||
engine.changeScenario(7, "50,000 Pelotas");
|
engine.changeScenario(7, "50.000 pelotas");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_9:
|
case SDLK_9:
|
||||||
if (engine.isCustomScenarioEnabled()) {
|
if (engine.isCustomScenarioEnabled()) {
|
||||||
std::string custom_notif = std::to_string(engine.getCustomScenarioBalls()) + " Pelotas";
|
std::string custom_notif = std::to_string(engine.getCustomScenarioBalls()) + " pelotas";
|
||||||
engine.changeScenario(CUSTOM_SCENARIO_IDX, custom_notif.c_str());
|
engine.changeScenario(CUSTOM_SCENARIO_IDX, custom_notif.c_str());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
|
// Toggle PostFX activo/inactivo
|
||||||
|
case SDLK_F5:
|
||||||
|
engine.handlePostFXToggle();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Toggle escalado entero/estirado (solo en fullscreen F3)
|
// Toggle escalado entero/estirado (solo en fullscreen F3)
|
||||||
case SDLK_F5:
|
case SDLK_F6:
|
||||||
engine.toggleIntegerScaling();
|
engine.toggleIntegerScaling();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Redimensionar campo de juego (tamaño lógico + físico)
|
||||||
|
case SDLK_F7:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.fieldSizeDown();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDLK_F8:
|
||||||
|
if (engine.isKioskMode()) {
|
||||||
|
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
|
||||||
|
} else {
|
||||||
|
engine.fieldSizeUp();
|
||||||
|
}
|
||||||
|
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
|
||||||
@@ -280,6 +313,11 @@ bool InputHandler::processEvents(Engine& engine) {
|
|||||||
engine.toggleLogoMode();
|
engine.toggleLogoMode();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Ciclar presets PostFX (vinyeta/scanlines/cromàtica/complet/desactivat)
|
||||||
|
case SDLK_X:
|
||||||
|
engine.handlePostFXCycle();
|
||||||
|
break;
|
||||||
|
|
||||||
// Toggle Debug Display (movido de H a F12)
|
// Toggle Debug Display (movido de H a F12)
|
||||||
case SDLK_F12:
|
case SDLK_F12:
|
||||||
engine.toggleDebug();
|
engine.toggleDebug();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL_render.h> // Para SDL_Renderer, SDL_Texture
|
|
||||||
#include <SDL3/SDL_video.h> // Para SDL_DisplayID, SDL_GetDisplays
|
|
||||||
|
|
||||||
#include <string> // Para std::string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Helper class para pre-escalar logos usando stb_image_resize2
|
|
||||||
*
|
|
||||||
* Proporciona funciones para:
|
|
||||||
* - Detectar resolución nativa del monitor
|
|
||||||
* - Cargar PNG y escalar a tamaño específico con algoritmos de alta calidad
|
|
||||||
* - Crear texturas SDL desde buffers escalados
|
|
||||||
*
|
|
||||||
* Usado por AppLogo para pre-generar versiones de logos al tamaño exacto
|
|
||||||
* de pantalla, eliminando el escalado dinámico de SDL y mejorando calidad visual.
|
|
||||||
*/
|
|
||||||
class LogoScaler {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Detecta la resolución nativa del monitor principal
|
|
||||||
*
|
|
||||||
* @param native_width [out] Ancho nativo del display en píxeles
|
|
||||||
* @param native_height [out] Alto nativo del display en píxeles
|
|
||||||
* @return true si se pudo detectar, false si hubo error
|
|
||||||
*/
|
|
||||||
static bool detectNativeResolution(int& native_width, int& native_height);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Carga un PNG y lo escala al tamaño especificado
|
|
||||||
*
|
|
||||||
* Usa stb_image para cargar y stb_image_resize2 para escalar con
|
|
||||||
* algoritmo Mitchell (balance calidad/velocidad) en espacio sRGB.
|
|
||||||
*
|
|
||||||
* @param path Ruta al archivo PNG (ej: "data/logo/logo.png")
|
|
||||||
* @param target_width Ancho destino en píxeles
|
|
||||||
* @param target_height Alto destino en píxeles
|
|
||||||
* @param out_width [out] Ancho real de la imagen escalada
|
|
||||||
* @param out_height [out] Alto real de la imagen escalada
|
|
||||||
* @return Buffer RGBA (4 bytes por píxel) o nullptr si falla
|
|
||||||
* IMPORTANTE: El caller debe liberar con free() cuando termine
|
|
||||||
*/
|
|
||||||
static unsigned char* loadAndScale(const std::string& path,
|
|
||||||
int target_width, int target_height,
|
|
||||||
int& out_width, int& out_height);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Crea una textura SDL desde un buffer RGBA
|
|
||||||
*
|
|
||||||
* @param renderer Renderizador SDL activo
|
|
||||||
* @param data Buffer RGBA (4 bytes por píxel)
|
|
||||||
* @param width Ancho del buffer en píxeles
|
|
||||||
* @param height Alto del buffer en píxeles
|
|
||||||
* @return Textura SDL creada o nullptr si falla
|
|
||||||
* IMPORTANTE: El caller debe destruir con SDL_DestroyTexture()
|
|
||||||
*/
|
|
||||||
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
|
|
||||||
unsigned char* data,
|
|
||||||
int width, int height);
|
|
||||||
};
|
|
||||||
@@ -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
|
||||||
@@ -11,9 +12,9 @@ void printHelp() {
|
|||||||
std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n";
|
std::cout << "ViBe3 Physics - Simulador de físicas avanzadas\n";
|
||||||
std::cout << "\nUso: vibe3_physics [opciones]\n\n";
|
std::cout << "\nUso: vibe3_physics [opciones]\n\n";
|
||||||
std::cout << "Opciones:\n";
|
std::cout << "Opciones:\n";
|
||||||
std::cout << " -w, --width <px> Ancho de resolución (default: 320)\n";
|
std::cout << " -w, --width <px> Ancho de resolución (default: " << DEFAULT_SCREEN_WIDTH << ")\n";
|
||||||
std::cout << " -h, --height <px> Alto de resolución (default: 240)\n";
|
std::cout << " -h, --height <px> Alto de resolución (default: " << DEFAULT_SCREEN_HEIGHT << ")\n";
|
||||||
std::cout << " -z, --zoom <n> Zoom de ventana (default: 3)\n";
|
std::cout << " -z, --zoom <n> Zoom de ventana (default: " << DEFAULT_WINDOW_ZOOM << ")\n";
|
||||||
std::cout << " -f, --fullscreen Modo pantalla completa (F3 - letterbox)\n";
|
std::cout << " -f, --fullscreen Modo pantalla completa (F3 - letterbox)\n";
|
||||||
std::cout << " -F, --real-fullscreen Modo pantalla completa real (F4 - nativo)\n";
|
std::cout << " -F, --real-fullscreen Modo pantalla completa real (F4 - nativo)\n";
|
||||||
std::cout << " -k, --kiosk Modo kiosko (F4 fijo, sin ESC, sin zoom)\n";
|
std::cout << " -k, --kiosk Modo kiosko (F4 fijo, sin ESC, sin zoom)\n";
|
||||||
@@ -21,9 +22,12 @@ void printHelp() {
|
|||||||
std::cout << " --custom-balls <n> Activa escenario custom (tecla 9) con N pelotas\n";
|
std::cout << " --custom-balls <n> Activa escenario custom (tecla 9) con N pelotas\n";
|
||||||
std::cout << " --skip-benchmark Salta el benchmark y usa el máximo de bolas (50000)\n";
|
std::cout << " --skip-benchmark Salta el benchmark y usa el máximo de bolas (50000)\n";
|
||||||
std::cout << " --max-balls <n> Limita el máximo de bolas en modos DEMO/DEMO_LITE\n";
|
std::cout << " --max-balls <n> Limita el máximo de bolas en modos DEMO/DEMO_LITE\n";
|
||||||
|
std::cout << " --postfx [efecto] Arrancar con PostFX activo (default: complet): vinyeta, scanlines, cromatica, complet\n";
|
||||||
|
std::cout << " --vignette <float> Sobreescribir vignette_strength (activa PostFX si no hay --postfx)\n";
|
||||||
|
std::cout << " --chroma <float> Sobreescribir chroma_strength (activa PostFX si no hay --postfx)\n";
|
||||||
std::cout << " --help Mostrar esta ayuda\n\n";
|
std::cout << " --help Mostrar esta ayuda\n\n";
|
||||||
std::cout << "Ejemplos:\n";
|
std::cout << "Ejemplos:\n";
|
||||||
std::cout << " vibe3_physics # 320x240 zoom 3 (ventana 960x720)\n";
|
std::cout << " vibe3_physics # " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << " zoom " << DEFAULT_WINDOW_ZOOM << " (default)\n";
|
||||||
std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 zoom 1 (auto)\n";
|
std::cout << " vibe3_physics -w 1920 -h 1080 # 1920x1080 zoom 1 (auto)\n";
|
||||||
std::cout << " vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (ventana 1280x960)\n";
|
std::cout << " vibe3_physics -w 640 -h 480 -z 2 # 640x480 zoom 2 (ventana 1280x960)\n";
|
||||||
std::cout << " vibe3_physics -f # Fullscreen letterbox (F3)\n";
|
std::cout << " vibe3_physics -f # Fullscreen letterbox (F3)\n";
|
||||||
@@ -35,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;
|
||||||
@@ -45,6 +49,9 @@ int main(int argc, char* argv[]) {
|
|||||||
bool kiosk_mode = false;
|
bool kiosk_mode = false;
|
||||||
bool skip_benchmark = false;
|
bool skip_benchmark = false;
|
||||||
int max_balls_override = 0;
|
int max_balls_override = 0;
|
||||||
|
int initial_postfx = -1;
|
||||||
|
float override_vignette = -1.f;
|
||||||
|
float override_chroma = -1.f;
|
||||||
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
AppMode initial_mode = AppMode::SANDBOX; // Modo inicial (default: SANDBOX)
|
||||||
|
|
||||||
// Parsear argumentos
|
// Parsear argumentos
|
||||||
@@ -52,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) {
|
||||||
@@ -124,6 +132,39 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
} else if (strcmp(argv[i], "--skip-benchmark") == 0) {
|
} else if (strcmp(argv[i], "--skip-benchmark") == 0) {
|
||||||
skip_benchmark = true;
|
skip_benchmark = true;
|
||||||
|
} else if (strcmp(argv[i], "--postfx") == 0) {
|
||||||
|
// Si no hay valor o el siguiente arg es otra opción, defaultear a complet
|
||||||
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||||
|
std::string fx = argv[++i];
|
||||||
|
if (fx == "vinyeta") {
|
||||||
|
initial_postfx = 0;
|
||||||
|
} else if (fx == "scanlines") {
|
||||||
|
initial_postfx = 1;
|
||||||
|
} else if (fx == "cromatica") {
|
||||||
|
initial_postfx = 2;
|
||||||
|
} else if (fx == "complet") {
|
||||||
|
initial_postfx = 3;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --postfx '" << fx << "' no válido. Usa: vinyeta, scanlines, cromatica, complet\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initial_postfx = 3; // default: complet
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--vignette") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
override_vignette = (float)atof(argv[++i]);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --vignette requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (strcmp(argv[i], "--chroma") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
override_chroma = (float)atof(argv[++i]);
|
||||||
|
} else {
|
||||||
|
std::cerr << "Error: --chroma requiere un valor\n";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
} else if (strcmp(argv[i], "--max-balls") == 0) {
|
} else if (strcmp(argv[i], "--max-balls") == 0) {
|
||||||
if (i + 1 < argc) {
|
if (i + 1 < argc) {
|
||||||
int n = atoi(argv[++i]);
|
int n = atoi(argv[++i]);
|
||||||
@@ -150,16 +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) {
|
||||||
|
engine.setInitialPostFX(initial_postfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (override_vignette >= 0.f || override_chroma >= 0.f) {
|
||||||
|
if (initial_postfx < 0) {
|
||||||
|
engine.setInitialPostFX(0);
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ResourcePack;
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
class ResourceManager {
|
class ResourceManager {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Inicializa el sistema de recursos empaquetados
|
* Inicializa el sistema de recursos empaquetados
|
||||||
* Debe llamarse una única vez al inicio del programa
|
* Debe llamarse una única vez al inicio del programa
|
||||||
@@ -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
|
||||||
@@ -69,7 +69,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
static size_t getResourceCount();
|
static size_t getResourceCount();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Constructor privado (singleton)
|
// Constructor privado (singleton)
|
||||||
ResourceManager() = default;
|
ResourceManager() = default;
|
||||||
~ResourceManager() = default;
|
~ResourceManager() = default;
|
||||||
|
|||||||
@@ -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,18 +230,7 @@ void ResourcePack::clear() {
|
|||||||
// FUNCIONES AUXILIARES
|
// FUNCIONES AUXILIARES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void ResourcePack::encryptData(unsigned char* data, size_t size) {
|
auto ResourcePack::calculateChecksum(const unsigned char* data, size_t size) -> uint32_t {
|
||||||
for (size_t i = 0; i < size; i++) {
|
|
||||||
data[i] ^= XOR_KEY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ResourcePack::decryptData(unsigned char* data, size_t size) {
|
|
||||||
// XOR es simétrico
|
|
||||||
encryptData(data, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size) {
|
|
||||||
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]);
|
||||||
@@ -251,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/");
|
||||||
|
|||||||
@@ -13,30 +13,30 @@
|
|||||||
* único y ofuscado. Proporciona fallback automático a carpeta data/ si no existe pack.
|
* único y ofuscado. Proporciona fallback automático a carpeta data/ si no existe pack.
|
||||||
*/
|
*/
|
||||||
class ResourcePack {
|
class ResourcePack {
|
||||||
public:
|
public:
|
||||||
ResourcePack();
|
ResourcePack();
|
||||||
~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;
|
||||||
size_t getResourceCount() const;
|
size_t getResourceCount() const;
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Header del pack (12 bytes)
|
// Header del pack (12 bytes)
|
||||||
struct PackHeader {
|
struct PackHeader {
|
||||||
char magic[4]; // "VBE3"
|
char magic[4]; // "VBE3"
|
||||||
@@ -58,8 +58,6 @@ private:
|
|||||||
bool isLoaded_;
|
bool isLoaded_;
|
||||||
|
|
||||||
// Funciones auxiliares
|
// Funciones auxiliares
|
||||||
void encryptData(unsigned char* data, size_t size);
|
static uint32_t calculateChecksum(const unsigned char* data, size_t size);
|
||||||
void decryptData(unsigned char* data, size_t size);
|
static std::string normalizePath(const std::string& path);
|
||||||
uint32_t calculateChecksum(const unsigned char* data, size_t size);
|
|
||||||
std::string normalizePath(const std::string& path);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -239,3 +253,15 @@ void SceneManager::updateBallSizes(int old_size, int new_size) {
|
|||||||
ball->updateSize(new_size);
|
ball->updateSize(new_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SceneManager::enableShapeAttractionAll(bool enabled) {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->enableShapeAttraction(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::resetDepthScalesAll() {
|
||||||
|
for (auto& ball : balls_) {
|
||||||
|
ball->setDepthScale(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,11 +109,22 @@ class SceneManager {
|
|||||||
const std::vector<std::unique_ptr<Ball>>& getBalls() const { return balls_; }
|
const std::vector<std::unique_ptr<Ball>>& getBalls() const { return balls_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Obtiene referencia mutable al vector de bolas (para ShapeManager)
|
* @brief Obtiene referencia mutable al vector de bolas (para ShapeManager/BoidManager)
|
||||||
* NOTA: Usar con cuidado, solo para sistemas que necesitan modificar estado de bolas
|
* NOTA: Usar con cuidado, solo para sistemas que necesitan modificar estado de bolas
|
||||||
*/
|
*/
|
||||||
std::vector<std::unique_ptr<Ball>>& getBallsMutable() { return balls_; }
|
std::vector<std::unique_ptr<Ball>>& getBallsMutable() { return balls_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa o desactiva la atracción de figura en todas las bolas
|
||||||
|
* @param enabled true para activar, false para desactivar
|
||||||
|
*/
|
||||||
|
void enableShapeAttractionAll(bool enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resetea la escala de profundidad a 1.0 en todas las bolas
|
||||||
|
*/
|
||||||
|
void resetDepthScalesAll();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Obtiene número total de bolas
|
* @brief Obtiene número total de bolas
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
// Comportamiento: Núcleo estático + electrones orbitando en planos inclinados
|
// Comportamiento: Núcleo estático + electrones orbitando en planos inclinados
|
||||||
// Efecto: Modelo atómico clásico Bohr
|
// Efecto: Modelo atómico clásico Bohr
|
||||||
class AtomShape : public Shape {
|
class AtomShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación global en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación global en eje Y (rad)
|
||||||
float orbit_phase_ = 0.0f; // Fase de rotación de electrones (rad)
|
float orbit_phase_ = 0.0f; // Fase de rotación de electrones (rad)
|
||||||
float nucleus_radius_ = 0.0f; // Radio del núcleo central (píxeles)
|
float nucleus_radius_ = 0.0f; // Radio del núcleo central (píxeles)
|
||||||
float orbit_radius_ = 0.0f; // Radio de las órbitas (píxeles)
|
float orbit_radius_ = 0.0f; // Radio de las órbitas (píxeles)
|
||||||
int num_points_ = 0; // Cantidad total de puntos
|
int num_points_ = 0; // Cantidad total de puntos
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
// - 27+ pelotas: Grid volumétrico 3D uniforme
|
// - 27+ pelotas: Grid volumétrico 3D uniforme
|
||||||
// Comportamiento: Rotación simultánea en ejes X, Y, Z (efecto Rubik)
|
// Comportamiento: Rotación simultánea en ejes X, Y, Z (efecto Rubik)
|
||||||
class CubeShape : public Shape {
|
class CubeShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
@@ -22,14 +23,14 @@ private:
|
|||||||
std::vector<float> base_y_;
|
std::vector<float> base_y_;
|
||||||
std::vector<float> base_z_;
|
std::vector<float> base_z_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
const char* getName() const override { return "CUBE"; }
|
const char* getName() const override { return "CUBE"; }
|
||||||
float getScaleFactor(float screen_height) const override;
|
float getScaleFactor(float screen_height) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Métodos auxiliares para distribución de puntos
|
// Métodos auxiliares para distribución de puntos
|
||||||
void generateVertices(); // 8 vértices
|
void generateVertices(); // 8 vértices
|
||||||
void generateVerticesAndCenters(); // 26 puntos (vértices + caras + aristas)
|
void generateVerticesAndCenters(); // 26 puntos (vértices + caras + aristas)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// Comportamiento: Superficie cilíndrica con rotación en eje Y + tumbling ocasional en X/Z
|
// Comportamiento: Superficie cilíndrica con rotación en eje Y + tumbling ocasional en X/Z
|
||||||
// Ecuaciones: x = r*cos(u), y = v, z = r*sin(u)
|
// Ecuaciones: x = r*cos(u), y = v, z = r*sin(u)
|
||||||
class CylinderShape : public Shape {
|
class CylinderShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (tumbling ocasional)
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (tumbling ocasional)
|
||||||
float radius_ = 0.0f; // Radio del cilindro (píxeles)
|
float radius_ = 0.0f; // Radio del cilindro (píxeles)
|
||||||
@@ -19,7 +19,7 @@ private:
|
|||||||
bool is_tumbling_ = false; // ¿Estamos en modo tumble?
|
bool is_tumbling_ = false; // ¿Estamos en modo tumble?
|
||||||
float tumble_target_ = 0.0f; // Ángulo objetivo del tumble
|
float tumble_target_ = 0.0f; // Ángulo objetivo del tumble
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// Comportamiento: Rotación en eje Y + animación de fase vertical
|
// Comportamiento: Rotación en eje Y + animación de fase vertical
|
||||||
// Ecuaciones: x = r*cos(t), y = pitch*t + phase, z = r*sin(t)
|
// Ecuaciones: x = r*cos(t), y = pitch*t + phase, z = r*sin(t)
|
||||||
class HelixShape : public Shape {
|
class HelixShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float phase_offset_ = 0.0f; // Offset de fase para animación vertical (rad)
|
float phase_offset_ = 0.0f; // Offset de fase para animación vertical (rad)
|
||||||
float radius_ = 0.0f; // Radio de la espiral (píxeles)
|
float radius_ = 0.0f; // Radio de la espiral (píxeles)
|
||||||
@@ -14,7 +14,7 @@ private:
|
|||||||
float total_height_ = 0.0f; // Altura total de la espiral (píxeles)
|
float total_height_ = 0.0f; // Altura total de la espiral (píxeles)
|
||||||
int num_points_ = 0; // Cantidad de puntos generados
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// Comportamiento: 12 vértices distribuidos uniformemente con rotación triple
|
// Comportamiento: 12 vértices distribuidos uniformemente con rotación triple
|
||||||
// Geometría: Basado en proporción áurea (golden ratio)
|
// Geometría: Basado en proporción áurea (golden ratio)
|
||||||
class IcosahedronShape : public Shape {
|
class IcosahedronShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
@@ -16,7 +16,7 @@ private:
|
|||||||
// Helper para aplicar rotaciones triple XYZ
|
// Helper para aplicar rotaciones triple XYZ
|
||||||
void applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const;
|
void applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// Comportamiento: Curva paramétrica 3D con rotación global y animación de fase
|
// Comportamiento: Curva paramétrica 3D con rotación global y animación de fase
|
||||||
// Ecuaciones: x(t) = A*sin(freq_x*t + phase_x), y(t) = A*sin(freq_y*t), z(t) = A*sin(freq_z*t + phase_z)
|
// Ecuaciones: x(t) = A*sin(freq_x*t + phase_x), y(t) = A*sin(freq_y*t), z(t) = A*sin(freq_z*t + phase_z)
|
||||||
class LissajousShape : public Shape {
|
class LissajousShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float freq_x_ = 0.0f; // Frecuencia en eje X
|
float freq_x_ = 0.0f; // Frecuencia en eje X
|
||||||
float freq_y_ = 0.0f; // Frecuencia en eje Y
|
float freq_y_ = 0.0f; // Frecuencia en eje Y
|
||||||
float freq_z_ = 0.0f; // Frecuencia en eje Z
|
float freq_z_ = 0.0f; // Frecuencia en eje Z
|
||||||
@@ -17,7 +17,7 @@ private:
|
|||||||
float amplitude_ = 0.0f; // Amplitud de la curva (píxeles)
|
float amplitude_ = 0.0f; // Amplitud de la curva (píxeles)
|
||||||
int num_points_ = 0; // Cantidad total de puntos
|
int num_points_ = 0; // Cantidad total de puntos
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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,20 +23,25 @@ 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::cout << "[PNGShape] Cargando recurso: " << resource_key << std::endl;
|
{
|
||||||
|
std::string fn = std::string(resource_key);
|
||||||
|
fn = fn.substr(fn.find_last_of("\\/") + 1);
|
||||||
|
std::cout << "[PNGShape] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -53,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;
|
||||||
@@ -86,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)});
|
||||||
}
|
}
|
||||||
@@ -119,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
|
||||||
@@ -192,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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,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()) {
|
||||||
@@ -239,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()) {
|
||||||
@@ -382,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;
|
||||||
@@ -404,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;
|
||||||
}
|
}
|
||||||
@@ -428,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
#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)
|
||||||
// Enfoque B: Voxelización 3D (preparado para futuro)
|
// Enfoque B: Voxelización 3D (preparado para futuro)
|
||||||
class PNGShape : public Shape {
|
class PNGShape : public Shape {
|
||||||
private:
|
private:
|
||||||
// Datos de la imagen cargada
|
// Datos de la imagen cargada
|
||||||
int image_width_ = 0;
|
int image_width_ = 0;
|
||||||
int image_height_ = 0;
|
int image_height_ = 0;
|
||||||
@@ -59,16 +60,16 @@ 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
|
||||||
PNGShape(const char* png_path = "data/shapes/jailgames.png");
|
PNGShape(const char* png_path = "data/shapes/jailgames.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) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// Interfaz abstracta para todas las figuras 3D
|
// Interfaz abstracta para todas las figuras 3D
|
||||||
class Shape {
|
class Shape {
|
||||||
public:
|
public:
|
||||||
virtual ~Shape() = default;
|
virtual ~Shape() = default;
|
||||||
|
|
||||||
// Generar distribución inicial de puntos en la figura
|
// Generar distribución inicial de puntos en la figura
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
// Comportamiento: Rotación dual en ejes X e Y
|
// Comportamiento: Rotación dual en ejes X e Y
|
||||||
// Uso anterior: RotoBall
|
// Uso anterior: RotoBall
|
||||||
class SphereShape : public Shape {
|
class SphereShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float radius_ = 0.0f; // Radio de la esfera (píxeles)
|
float radius_ = 0.0f; // Radio de la esfera (píxeles)
|
||||||
int num_points_ = 0; // Cantidad de puntos generados
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// Comportamiento: Superficie toroidal con rotación triple (X, Y, Z)
|
// Comportamiento: Superficie toroidal con rotación triple (X, Y, Z)
|
||||||
// Ecuaciones: x = (R + r*cos(v))*cos(u), y = (R + r*cos(v))*sin(u), z = r*sin(v)
|
// Ecuaciones: x = (R + r*cos(v))*cos(u), y = (R + r*cos(v))*sin(u), z = r*sin(v)
|
||||||
class TorusShape : public Shape {
|
class TorusShape : public Shape {
|
||||||
private:
|
private:
|
||||||
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
|
||||||
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
|
||||||
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
|
||||||
@@ -14,7 +14,7 @@ private:
|
|||||||
float minor_radius_ = 0.0f; // Radio menor r (grosor del tubo)
|
float minor_radius_ = 0.0f; // Radio menor r (grosor del tubo)
|
||||||
int num_points_ = 0; // Cantidad de puntos generados
|
int num_points_ = 0; // Cantidad de puntos generados
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
void generatePoints(int num_points, float screen_width, float screen_height) override;
|
||||||
void update(float delta_time, float screen_width, float screen_height) override;
|
void update(float delta_time, float screen_width, float screen_height) override;
|
||||||
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
void getPoint3D(int index, float& x, float& y, float& z) const override;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "shape_manager.hpp"
|
#include "shape_manager.hpp"
|
||||||
|
|
||||||
#include <algorithm> // for std::min, std::max
|
#include <algorithm> // for std::min, std::max, std::transform
|
||||||
|
#include <cctype> // for ::tolower
|
||||||
#include <cstdlib> // for rand
|
#include <cstdlib> // for rand
|
||||||
#include <string> // for std::string
|
#include <string> // for std::string
|
||||||
|
|
||||||
@@ -22,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;
|
||||||
@@ -65,33 +64,26 @@ 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, generar threshold aleatorio de convergencia (75-100%)
|
// 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;
|
||||||
float logo_convergence_threshold = LOGO_CONVERGENCE_MIN +
|
|
||||||
(rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
|
|
||||||
*/
|
|
||||||
shape_convergence_ = 0.0f; // Reset convergencia al entrar
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Volver a modo física normal
|
// Volver a modo física normal
|
||||||
current_mode_ = SimulationMode::PHYSICS;
|
current_mode_ = SimulationMode::PHYSICS;
|
||||||
|
|
||||||
// Desactivar atracción y resetear escala de profundidad
|
// Desactivar atracción y resetear escala de profundidad
|
||||||
auto& balls = scene_mgr_->getBallsMutable();
|
scene_mgr_->enableShapeAttractionAll(false);
|
||||||
for (auto& ball : balls) {
|
scene_mgr_->resetDepthScalesAll(); // Reset escala a 100% (evita "pop" visual)
|
||||||
ball->enableShapeAttraction(false);
|
|
||||||
ball->setDepthScale(1.0f); // Reset escala a 100% (evita "pop" visual)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activar gravedad al salir (solo si se especifica)
|
// Activar gravedad al salir (solo si se especifica)
|
||||||
if (force_gravity_on_exit) {
|
if (force_gravity_on_exit) {
|
||||||
@@ -99,8 +91,8 @@ 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,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%");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,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_));
|
||||||
@@ -167,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
|
||||||
@@ -185,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)
|
||||||
@@ -197,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)
|
||||||
|
|
||||||
@@ -221,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_));
|
||||||
@@ -280,18 +276,22 @@ void ShapeManager::activateShapeInternal(ShapeType type) {
|
|||||||
generateShape();
|
generateShape();
|
||||||
|
|
||||||
// Activar atracción física en todas las pelotas
|
// Activar atracción física en todas las pelotas
|
||||||
auto& balls = scene_mgr_->getBallsMutable();
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
for (auto& ball : balls) {
|
|
||||||
ball->enableShapeAttraction(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 notification = std::string("Modo ") + active_shape_->getName();
|
std::string shape_name = active_shape_->getName();
|
||||||
|
std::ranges::transform(shape_name, shape_name.begin(), ::tolower);
|
||||||
|
std::string notification = std::string("Modo ") + shape_name;
|
||||||
ui_mgr_->showNotification(notification);
|
ui_mgr_->showNotification(notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ShapeManager::setShapeScaleFactor(float scale) {
|
||||||
|
shape_scale_factor_ = scale;
|
||||||
|
clampShapeScale();
|
||||||
|
}
|
||||||
|
|
||||||
void ShapeManager::clampShapeScale() {
|
void ShapeManager::clampShapeScale() {
|
||||||
// Calcular tamaño máximo permitido según resolución actual
|
// Calcular tamaño máximo permitido según resolución actual
|
||||||
// La figura más grande (esfera/cubo) usa ~33% de altura por defecto
|
// La figura más grande (esfera/cubo) usa ~33% de altura por defecto
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -133,6 +132,18 @@ class ShapeManager {
|
|||||||
*/
|
*/
|
||||||
float getConvergence() const { return shape_convergence_; }
|
float getConvergence() const { return shape_convergence_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece la escala de figura y aplica clamping
|
||||||
|
* @param scale Nuevo factor de escala (se limitará a rango válido)
|
||||||
|
*/
|
||||||
|
void setShapeScaleFactor(float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el estado del zoom por profundidad Z
|
||||||
|
* @param enabled true para activar, false para desactivar
|
||||||
|
*/
|
||||||
|
void setDepthZoomEnabled(bool enabled) { depth_zoom_enabled_ = enabled; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// === Referencias a otros componentes ===
|
// === Referencias a otros componentes ===
|
||||||
Engine* engine_; // Callback al Engine (legacy - temporal)
|
Engine* engine_; // Callback al Engine (legacy - temporal)
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class Ball; // Forward declaration
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SPATIAL HASH GRID - Sistema genérico de particionamiento espacial
|
|
||||||
// ============================================================================
|
|
||||||
//
|
|
||||||
// Divide el espacio 2D en celdas de tamaño fijo para acelerar búsquedas de vecinos.
|
|
||||||
// Reduce complejidad de O(n²) a O(n) para queries de proximidad.
|
|
||||||
//
|
|
||||||
// CASOS DE USO:
|
|
||||||
// - Boids: Buscar vecinos para reglas de Reynolds (separación/alineación/cohesión)
|
|
||||||
// - Física: Detección de colisiones ball-to-ball (futuro)
|
|
||||||
// - IA: Pathfinding con obstáculos dinámicos
|
|
||||||
//
|
|
||||||
// ALGORITMO:
|
|
||||||
// 1. Dividir pantalla en grid de celdas (ej: 100x100px cada una)
|
|
||||||
// 2. Insertar cada Ball en celda(s) correspondiente(s) según posición
|
|
||||||
// 3. Query: Solo revisar celdas adyacentes (9 celdas max) en lugar de TODOS los objetos
|
|
||||||
//
|
|
||||||
// MEJORA DE RENDIMIENTO:
|
|
||||||
// - Sin grid: 1000 boids = 1M comparaciones (1000²)
|
|
||||||
// - Con grid: 1000 boids ≈ 9K comparaciones (1000 * ~9 vecinos/celda promedio)
|
|
||||||
// - Speedup: ~100x en casos típicos
|
|
||||||
//
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class SpatialGrid {
|
|
||||||
public:
|
|
||||||
// Constructor: especificar dimensiones del mundo y tamaño de celda
|
|
||||||
SpatialGrid(int world_width, int world_height, float cell_size);
|
|
||||||
|
|
||||||
// Limpiar todas las celdas (llamar al inicio de cada frame)
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
// Insertar objeto en el grid según su posición (x, y)
|
|
||||||
void insert(Ball* ball, float x, float y);
|
|
||||||
|
|
||||||
// Buscar todos los objetos dentro del radio especificado desde (x, y)
|
|
||||||
// Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas)
|
|
||||||
std::vector<Ball*> queryRadius(float x, float y, float radius);
|
|
||||||
|
|
||||||
// Actualizar dimensiones del mundo (útil para cambios de resolución F4)
|
|
||||||
void updateWorldSize(int world_width, int world_height);
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y)
|
|
||||||
void getCellCoords(float x, float y, int& cell_x, int& cell_y) const;
|
|
||||||
|
|
||||||
// Convertir (cell_x, cell_y) a hash key único para el mapa
|
|
||||||
int getCellKey(int cell_x, int cell_y) const;
|
|
||||||
|
|
||||||
// Dimensiones del mundo (ancho/alto en píxeles)
|
|
||||||
int world_width_;
|
|
||||||
int world_height_;
|
|
||||||
|
|
||||||
// Tamaño de cada celda (en píxeles)
|
|
||||||
float cell_size_;
|
|
||||||
|
|
||||||
// Número de celdas en cada dimensión
|
|
||||||
int grid_cols_;
|
|
||||||
int grid_rows_;
|
|
||||||
|
|
||||||
// Estructura de datos: hash map de cell_key → vector de Ball*
|
|
||||||
// Usamos unordered_map para O(1) lookup
|
|
||||||
std::unordered_map<int, std::vector<Ball*>> cells_;
|
|
||||||
};
|
|
||||||
@@ -1,35 +1,44 @@
|
|||||||
#include "state_manager.hpp"
|
#include "state_manager.hpp"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::min
|
||||||
|
#include <array> // for std::array
|
||||||
#include <cstdlib> // for rand
|
#include <cstdlib> // for rand
|
||||||
|
#include <vector> // for std::vector
|
||||||
|
|
||||||
#include "defines.hpp" // for constantes DEMO/LOGO
|
#include "defines.hpp" // for constantes DEMO/LOGO
|
||||||
#include "engine.hpp" // for Engine (callbacks)
|
#include "engine.hpp" // for Engine (enter/exitShapeMode, texture)
|
||||||
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
#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
|
||||||
|
|
||||||
StateManager::StateManager()
|
StateManager::StateManager()
|
||||||
: engine_(nullptr)
|
: engine_(nullptr),
|
||||||
, current_app_mode_(AppMode::SANDBOX)
|
scene_mgr_(nullptr),
|
||||||
, previous_app_mode_(AppMode::SANDBOX)
|
theme_mgr_(nullptr),
|
||||||
, demo_timer_(0.0f)
|
shape_mgr_(nullptr),
|
||||||
, demo_next_action_time_(0.0f)
|
current_app_mode_(AppMode::SANDBOX),
|
||||||
, logo_convergence_threshold_(0.90f)
|
previous_app_mode_(AppMode::SANDBOX),
|
||||||
, logo_min_time_(3.0f)
|
demo_timer_(0.0f),
|
||||||
, logo_max_time_(5.0f)
|
demo_next_action_time_(0.0f),
|
||||||
, logo_waiting_for_flip_(false)
|
logo_convergence_threshold_(0.90f),
|
||||||
, logo_target_flip_number_(0)
|
logo_min_time_(3.0f),
|
||||||
, logo_target_flip_percentage_(0.0f)
|
logo_max_time_(5.0f),
|
||||||
, logo_current_flip_count_(0)
|
logo_waiting_for_flip_(false),
|
||||||
, logo_entered_manually_(false)
|
logo_target_flip_number_(0),
|
||||||
, logo_previous_theme_(0)
|
logo_target_flip_percentage_(0.0f),
|
||||||
, logo_previous_texture_index_(0)
|
logo_current_flip_count_(0),
|
||||||
, logo_previous_shape_scale_(1.0f) {
|
logo_entered_manually_(false),
|
||||||
|
logo_previous_theme_(0),
|
||||||
|
logo_previous_texture_index_(0),
|
||||||
|
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) {
|
|
||||||
engine_ = engine;
|
engine_ = engine;
|
||||||
|
scene_mgr_ = scene_mgr;
|
||||||
|
theme_mgr_ = theme_mgr;
|
||||||
|
shape_mgr_ = shape_mgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::setLogoPreviousState(int theme, size_t texture_index, float shape_scale) {
|
void StateManager::setLogoPreviousState(int theme, size_t texture_index, float shape_scale) {
|
||||||
@@ -39,48 +48,39 @@ void StateManager::setLogoPreviousState(int theme, size_t texture_index, float s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// ACTUALIZACIÓN DE ESTADOS - Migrado desde Engine::updateDemoMode()
|
// 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)
|
||||||
// Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
|
if (current_app_mode_ == AppMode::SANDBOX) {
|
||||||
if (current_app_mode_ == AppMode::SANDBOX) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar timer
|
|
||||||
demo_timer_ += delta_time;
|
demo_timer_ += delta_time;
|
||||||
|
|
||||||
// Determinar si es hora de ejecutar acción (depende del modo)
|
|
||||||
bool should_trigger = false;
|
bool should_trigger = false;
|
||||||
|
|
||||||
if (current_app_mode_ == AppMode::LOGO) {
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
// LOGO MODE: Dos caminos posibles
|
|
||||||
if (logo_waiting_for_flip_) {
|
if (logo_waiting_for_flip_) {
|
||||||
// CAMINO B: Esperando a que ocurran flips
|
// CAMINO B: Esperando a que ocurran flips
|
||||||
// Obtener referencia a PNGShape si está activa
|
auto* png_shape = dynamic_cast<PNGShape*>(active_shape);
|
||||||
PNGShape* 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();
|
||||||
|
|
||||||
// Detectar nuevo flip completado
|
logo_current_flip_count_ = std::max(current_flip_count, logo_current_flip_count_);
|
||||||
if (current_flip_count > logo_current_flip_count_) {
|
|
||||||
logo_current_flip_count_ = current_flip_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si estamos EN o DESPUÉS del flip objetivo
|
|
||||||
// +1 porque queremos actuar DURANTE el flip N, no después de completarlo
|
|
||||||
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
|
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
|
||||||
// Monitorear progreso del flip actual
|
|
||||||
if (png_shape->isFlipping()) {
|
if (png_shape->isFlipping()) {
|
||||||
float flip_progress = png_shape->getFlipProgress();
|
float flip_progress = png_shape->getFlipProgress();
|
||||||
if (flip_progress >= logo_target_flip_percentage_) {
|
if (flip_progress >= logo_target_flip_percentage_) {
|
||||||
should_trigger = true; // ¡Trigger durante el flip!
|
should_trigger = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// CAMINO A: Esperar convergencia + tiempo (comportamiento original)
|
// CAMINO A: Esperar convergencia + tiempo
|
||||||
bool min_time_reached = demo_timer_ >= logo_min_time_;
|
bool min_time_reached = demo_timer_ >= logo_min_time_;
|
||||||
bool max_time_reached = demo_timer_ >= logo_max_time_;
|
bool max_time_reached = demo_timer_ >= logo_max_time_;
|
||||||
bool convergence_ok = shape_convergence >= logo_convergence_threshold_;
|
bool convergence_ok = shape_convergence >= logo_convergence_threshold_;
|
||||||
@@ -88,39 +88,107 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
|
|||||||
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
|
should_trigger = (min_time_reached && convergence_ok) || max_time_reached;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// DEMO/DEMO_LITE: Timer simple como antes
|
|
||||||
should_trigger = demo_timer_ >= demo_next_action_time_;
|
should_trigger = demo_timer_ >= demo_next_action_time_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si es hora de ejecutar acción
|
if (!should_trigger) {
|
||||||
if (should_trigger) {
|
return;
|
||||||
// MODO LOGO: Sistema de acciones variadas con gravedad dinámica
|
}
|
||||||
|
|
||||||
if (current_app_mode_ == AppMode::LOGO) {
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
// Llamar a Engine para ejecutar acciones de LOGO
|
// LOGO MODE: Sistema de acciones variadas con gravedad dinámica
|
||||||
// TODO FASE 9: Mover lógica de acciones LOGO desde Engine a StateManager
|
int action = rand() % 100;
|
||||||
if (engine_) {
|
|
||||||
engine_->performLogoAction(logo_waiting_for_flip_);
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
// Logo quieto (formado) → Decidir camino a seguir
|
||||||
|
if (logo_waiting_for_flip_) {
|
||||||
|
// Ya estábamos esperando flips → hacer el cambio SHAPE → PHYSICS
|
||||||
|
if (action < 50) {
|
||||||
|
engine_->exitShapeMode(true); // Con gravedad ON
|
||||||
|
} else {
|
||||||
|
engine_->exitShapeMode(false); // Con gravedad OFF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
} else if (rand() % 100 < LOGO_FLIP_WAIT_PROBABILITY) {
|
||||||
|
// CAMINO B (50%): Esperar flips
|
||||||
|
logo_waiting_for_flip_ = true;
|
||||||
|
logo_target_flip_number_ = LOGO_FLIP_WAIT_MIN + rand() % (LOGO_FLIP_WAIT_MAX - LOGO_FLIP_WAIT_MIN + 1);
|
||||||
|
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;
|
||||||
|
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->resetFlipCount();
|
||||||
}
|
}
|
||||||
// MODO DEMO/DEMO_LITE: Acciones normales
|
// No hacer nada más — esperar a que ocurran los flips
|
||||||
else {
|
} else {
|
||||||
|
// CAMINO A (50%): Cambio inmediato
|
||||||
|
if (action < 50) {
|
||||||
|
engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON
|
||||||
|
} else {
|
||||||
|
engine_->exitShapeMode(false); // SHAPE → PHYSICS con gravedad OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Logo animado (PHYSICS) → 4 opciones
|
||||||
|
if (action < 50) {
|
||||||
|
// 50%: PHYSICS → SHAPE (reconstruir logo)
|
||||||
|
engine_->exitShapeMode(false); // toggleShapeMode: PHYSICS → SHAPE con last_type
|
||||||
|
|
||||||
|
logo_waiting_for_flip_ = false;
|
||||||
|
logo_current_flip_count_ = 0;
|
||||||
|
} else if (action < 68) {
|
||||||
|
// 18%: Forzar gravedad ON
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
} else if (action < 84) {
|
||||||
|
// 16%: Forzar gravedad OFF
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
} else {
|
||||||
|
// 16%: Cambiar dirección de gravedad
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
demo_timer_ = 0.0f;
|
||||||
|
float interval_range = logo_max_time_ - logo_min_time_;
|
||||||
|
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salir automáticamente si la entrada fue automática (desde DEMO)
|
||||||
|
if (!logo_entered_manually_ && rand() % 100 < 60) {
|
||||||
|
exitLogoMode(true); // Volver a DEMO/DEMO_LITE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// DEMO/DEMO_LITE: Acciones normales
|
||||||
bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE);
|
bool is_lite = (current_app_mode_ == AppMode::DEMO_LITE);
|
||||||
performDemoAction(is_lite);
|
performDemoAction(is_lite);
|
||||||
|
|
||||||
// Resetear timer y calcular próximo intervalo aleatorio
|
|
||||||
demo_timer_ = 0.0f;
|
demo_timer_ = 0.0f;
|
||||||
|
|
||||||
// Usar intervalos diferentes según modo
|
|
||||||
float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
float interval_min = is_lite ? DEMO_LITE_ACTION_INTERVAL_MIN : DEMO_ACTION_INTERVAL_MIN;
|
||||||
float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
float interval_max = is_lite ? DEMO_LITE_ACTION_INTERVAL_MAX : DEMO_ACTION_INTERVAL_MAX;
|
||||||
float interval_range = interval_max - interval_min;
|
float interval_range = interval_max - interval_min;
|
||||||
demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
|
demo_next_action_time_ = interval_min + (rand() % 1000) / 1000.0f * interval_range;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -132,15 +200,13 @@ void StateManager::setState(AppMode new_mode, int current_screen_width, int curr
|
|||||||
|
|
||||||
current_app_mode_ = new_mode;
|
current_app_mode_ = new_mode;
|
||||||
|
|
||||||
// Resetear timer al cambiar modo
|
|
||||||
demo_timer_ = 0.0f;
|
demo_timer_ = 0.0f;
|
||||||
|
|
||||||
// Configurar timer de demo según el modo
|
|
||||||
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) {
|
||||||
// Escalar tiempos con resolución (720p como base)
|
|
||||||
float resolution_scale = current_screen_height / 720.0f;
|
float resolution_scale = current_screen_height / 720.0f;
|
||||||
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
|
logo_min_time_ = LOGO_ACTION_INTERVAL_MIN * resolution_scale;
|
||||||
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
|
logo_max_time_ = LOGO_ACTION_INTERVAL_MAX * resolution_scale;
|
||||||
@@ -162,7 +228,7 @@ void StateManager::toggleDemoMode(int current_screen_width, int current_screen_h
|
|||||||
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
||||||
} else {
|
} else {
|
||||||
setState(AppMode::DEMO, current_screen_width, current_screen_height);
|
setState(AppMode::DEMO, current_screen_width, current_screen_height);
|
||||||
randomizeOnDemoStart(false); // Randomizar estado al entrar
|
randomizeOnDemoStart(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,70 +237,329 @@ void StateManager::toggleDemoLiteMode(int current_screen_width, int current_scre
|
|||||||
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
setState(AppMode::SANDBOX, current_screen_width, current_screen_height);
|
||||||
} else {
|
} else {
|
||||||
setState(AppMode::DEMO_LITE, current_screen_width, current_screen_height);
|
setState(AppMode::DEMO_LITE, current_screen_width, current_screen_height);
|
||||||
randomizeOnDemoStart(true); // Randomizar estado al entrar
|
randomizeOnDemoStart(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StateManager::toggleLogoMode(int current_screen_width, int current_screen_height, size_t ball_count) {
|
void StateManager::toggleLogoMode(int current_screen_width, int current_screen_height, size_t ball_count) {
|
||||||
if (current_app_mode_ == AppMode::LOGO) {
|
if (current_app_mode_ == AppMode::LOGO) {
|
||||||
exitLogoMode(false); // Salir de LOGO manualmente
|
exitLogoMode(false);
|
||||||
} else {
|
} else {
|
||||||
enterLogoMode(false, current_screen_width, current_screen_height, ball_count); // Entrar manualmente
|
enterLogoMode(false, current_screen_width, current_screen_height, ball_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// ACCIONES DE DEMO - Migrado desde Engine::performDemoAction()
|
// ACCIONES DE DEMO
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
void StateManager::performDemoAction(bool is_lite) {
|
void StateManager::performDemoAction(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
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)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
// Obtener información necesaria desde Engine via callbacks
|
if (is_lite) {
|
||||||
// (En el futuro, se podría pasar como parámetros al método)
|
if (static_cast<int>(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX] &&
|
||||||
if (!engine_) return;
|
theme_mgr_->getCurrentThemeIndex() == 5) { // MONOCHROME
|
||||||
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO_LITE) {
|
||||||
|
enterLogoMode(true, 0, 0, scene_mgr_->getBallCount());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (static_cast<int>(scene_mgr_->getBallCount()) >= BALL_COUNT_SCENARIOS[LOGO_MIN_SCENARIO_IDX]) {
|
||||||
|
if (rand() % 100 < LOGO_JUMP_PROBABILITY_FROM_DEMO) {
|
||||||
|
enterLogoMode(true, 0, 0, scene_mgr_->getBallCount());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO FASE 9: Eliminar callbacks a Engine y pasar parámetros necesarios
|
// ============================================
|
||||||
|
// ACCIONES NORMALES DE DEMO/DEMO_LITE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
// Por ahora, delegar las acciones DEMO completas a Engine
|
int total_weight;
|
||||||
// ya que necesitan acceso a múltiples componentes (SceneManager, ThemeManager, etc.)
|
int random_value;
|
||||||
engine_->executeDemoAction(is_lite);
|
int accumulated_weight = 0;
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
if (is_lite) {
|
||||||
// RANDOMIZACIÓN AL INICIAR DEMO - Migrado desde Engine::randomizeOnDemoStart()
|
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;
|
||||||
|
|
||||||
void StateManager::randomizeOnDemoStart(bool is_lite) {
|
// Cambiar dirección gravedad (25%)
|
||||||
// Delegar a Engine para randomización completa
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
|
||||||
// TODO FASE 9: Implementar lógica completa aquí
|
if (random_value < accumulated_weight) {
|
||||||
if (engine_) {
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
engine_->executeRandomizeOnDemoStart(is_lite);
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle gravedad ON/OFF (20%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_TOGGLE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activar figura 3D (25%) - PNG_SHAPE excluido
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
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]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle física ↔ figura (20%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_TOGGLE_PHYSICS;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar impulso (10%)
|
||||||
|
accumulated_weight += DEMO_LITE_WEIGHT_IMPULSE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
scene_mgr_->pushBallsAwayFromGravity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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;
|
||||||
|
random_value = rand() % total_weight;
|
||||||
|
|
||||||
|
// Cambiar dirección gravedad (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle gravedad ON/OFF (8%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_GRAVITY_TOGGLE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activar figura 3D (20%) - PNG_SHAPE excluido
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
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]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle física ↔ figura (12%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_TOGGLE_PHYSICS;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-generar misma figura (8%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_REGENERATE_SHAPE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar tema (15%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_THEME;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
theme_mgr_->switchToTheme(rand() % 15);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar escenario (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SCENARIO;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
|
||||||
|
std::vector<int> candidates;
|
||||||
|
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||||
|
candidates.push_back(i);
|
||||||
|
}
|
||||||
|
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
|
||||||
|
candidates.push_back(CUSTOM_SCENARIO_IDX);
|
||||||
|
}
|
||||||
|
int new_scenario = candidates[rand() % candidates.size()];
|
||||||
|
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
|
||||||
|
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
|
||||||
|
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar impulso (10%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_IMPULSE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
scene_mgr_->pushBallsAwayFromGravity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle profundidad (3%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_DEPTH_ZOOM;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar escala de figura (2%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SHAPE_SCALE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
int scale_action = rand() % 3;
|
||||||
|
if (scale_action == 0) {
|
||||||
|
shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() + SHAPE_SCALE_STEP);
|
||||||
|
} else if (scale_action == 1) {
|
||||||
|
shape_mgr_->setShapeScaleFactor(shape_mgr_->getShapeScaleFactor() - SHAPE_SCALE_STEP);
|
||||||
|
} else {
|
||||||
|
shape_mgr_->setShapeScaleFactor(SHAPE_SCALE_DEFAULT);
|
||||||
|
}
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar sprite (2%)
|
||||||
|
accumulated_weight += DEMO_WEIGHT_SPRITE;
|
||||||
|
if (random_value < accumulated_weight) {
|
||||||
|
engine_->switchTextureSilent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// TOGGLE GRAVEDAD (para DEMO) - Migrado desde Engine::toggleGravityOnOff()
|
// RANDOMIZACIÓN AL INICIAR DEMO
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void StateManager::randomizeOnDemoStart(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
|
||||||
|
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_lite) {
|
||||||
|
// DEMO LITE: Solo randomizar física/figura + gravedad
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// DEMO COMPLETO: Randomizar TODO
|
||||||
|
|
||||||
|
// 1. Física o Figura
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
engine_->exitShapeMode(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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];
|
||||||
|
|
||||||
|
// Randomizar profundidad y escala ANTES de activar la figura
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
|
||||||
|
}
|
||||||
|
shape_mgr_->setShapeScaleFactor(0.5f + ((rand() % 1500) / 1000.0f));
|
||||||
|
|
||||||
|
engine_->enterShapeMode(selected_shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Escenario
|
||||||
|
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
|
||||||
|
std::vector<int> candidates;
|
||||||
|
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
|
||||||
|
candidates.push_back(i);
|
||||||
|
}
|
||||||
|
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
|
||||||
|
candidates.push_back(CUSTOM_SCENARIO_IDX);
|
||||||
|
}
|
||||||
|
int new_scenario = candidates[rand() % candidates.size()];
|
||||||
|
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
|
||||||
|
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
|
||||||
|
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tema
|
||||||
|
theme_mgr_->switchToTheme(rand() % 15);
|
||||||
|
|
||||||
|
// 4. Sprite
|
||||||
|
if (rand() % 2 == 0) {
|
||||||
|
engine_->switchTextureSilent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Gravedad
|
||||||
|
auto new_direction = static_cast<GravityDirection>(rand() % 4);
|
||||||
|
scene_mgr_->changeGravityDirection(new_direction);
|
||||||
|
if (rand() % 3 == 0) {
|
||||||
|
toggleGravityOnOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// TOGGLE GRAVEDAD (para DEMO)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
void StateManager::toggleGravityOnOff() {
|
void StateManager::toggleGravityOnOff() {
|
||||||
// Delegar a Engine temporalmente
|
if (scene_mgr_ == nullptr) {
|
||||||
if (engine_) {
|
return;
|
||||||
engine_->executeToggleGravityOnOff();
|
}
|
||||||
|
|
||||||
|
bool gravity_enabled = scene_mgr_->hasBalls() &&
|
||||||
|
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
|
||||||
|
|
||||||
|
if (gravity_enabled) {
|
||||||
|
scene_mgr_->forceBallsGravityOff();
|
||||||
|
} else {
|
||||||
|
scene_mgr_->forceBallsGravityOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// ENTRAR AL MODO LOGO - Migrado desde Engine::enterLogoMode()
|
// ENTRAR AL MODO LOGO
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
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) {
|
||||||
// Guardar si entrada fue manual (tecla K) o automática (desde DEMO)
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logo_entered_manually_ = !from_demo;
|
logo_entered_manually_ = !from_demo;
|
||||||
|
|
||||||
// Resetear variables de espera de flips
|
|
||||||
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;
|
||||||
@@ -243,34 +568,93 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
|
|||||||
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
|
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
|
||||||
setState(AppMode::LOGO, current_screen_width, current_screen_height);
|
setState(AppMode::LOGO, current_screen_width, current_screen_height);
|
||||||
|
|
||||||
// Delegar configuración visual a Engine
|
// Verificar mínimo de pelotas
|
||||||
// TODO FASE 9: Mover configuración completa aquí
|
if (scene_mgr_->getCurrentScenario() < LOGO_MIN_SCENARIO_IDX) {
|
||||||
if (engine_) {
|
scene_mgr_->changeScenario(LOGO_MIN_SCENARIO_IDX, shape_mgr_->getCurrentMode());
|
||||||
engine_->executeEnterLogoMode(ball_count);
|
}
|
||||||
|
|
||||||
|
// Guardar estado previo (para restaurar al salir)
|
||||||
|
logo_previous_theme_ = theme_mgr_->getCurrentThemeIndex();
|
||||||
|
logo_previous_texture_index_ = engine_->getCurrentTextureIndex();
|
||||||
|
logo_previous_shape_scale_ = shape_mgr_->getShapeScaleFactor();
|
||||||
|
|
||||||
|
// Aplicar textura "small" si existe — buscar por nombre iterando índices
|
||||||
|
if (engine_->getCurrentTextureName() != "small") {
|
||||||
|
size_t original_idx = logo_previous_texture_index_;
|
||||||
|
bool found = false;
|
||||||
|
for (size_t i = 0; i < 20; ++i) {
|
||||||
|
engine_->setTextureByIndex(i);
|
||||||
|
if (engine_->getCurrentTextureName() == "small") {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (engine_->getCurrentTextureName().empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
engine_->setTextureByIndex(original_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
|
||||||
|
constexpr std::array<int, 4> LOGO_THEMES = {5, 6, 7, 8};
|
||||||
|
theme_mgr_->switchToTheme(LOGO_THEMES[rand() % 4]);
|
||||||
|
|
||||||
|
// Establecer escala a 120%
|
||||||
|
shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE);
|
||||||
|
|
||||||
|
// Activar PNG_SHAPE (el logo)
|
||||||
|
engine_->enterShapeMode(ShapeType::PNG_SHAPE);
|
||||||
|
|
||||||
|
// Configurar PNG_SHAPE en modo LOGO
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->setLogoMode(true);
|
||||||
|
png_shape->resetFlipCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// SALIR DEL MODO LOGO - Migrado desde Engine::exitLogoMode()
|
// SALIR DEL MODO LOGO
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Resetear flag de entrada manual
|
|
||||||
logo_entered_manually_ = false;
|
logo_entered_manually_ = false;
|
||||||
|
|
||||||
// Delegar restauración visual a Engine
|
// Restaurar estado visual previo
|
||||||
// TODO FASE 9: Mover lógica completa aquí
|
theme_mgr_->switchToTheme(logo_previous_theme_);
|
||||||
if (engine_) {
|
engine_->setTextureByIndex(logo_previous_texture_index_);
|
||||||
engine_->executeExitLogoMode();
|
shape_mgr_->setShapeScaleFactor(logo_previous_shape_scale_);
|
||||||
|
shape_mgr_->generateShape();
|
||||||
|
|
||||||
|
// Activar atracción física si estamos en modo SHAPE
|
||||||
|
if (shape_mgr_->isShapeModeActive()) {
|
||||||
|
scene_mgr_->enableShapeAttractionAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desactivar modo LOGO en PNG_SHAPE
|
||||||
|
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
|
||||||
|
if (png_shape != nullptr) {
|
||||||
|
png_shape->setLogoMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
|
||||||
|
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!return_to_demo) {
|
if (!return_to_demo) {
|
||||||
// Salida manual (tecla K): volver a SANDBOX
|
|
||||||
setState(AppMode::SANDBOX, 0, 0);
|
setState(AppMode::SANDBOX, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
// Volver al modo previo (DEMO o DEMO_LITE)
|
|
||||||
current_app_mode_ = previous_app_mode_;
|
current_app_mode_ = previous_app_mode_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -9,6 +10,9 @@
|
|||||||
class Engine;
|
class Engine;
|
||||||
class Shape;
|
class Shape;
|
||||||
class PNGShape;
|
class PNGShape;
|
||||||
|
class SceneManager;
|
||||||
|
class ThemeManager;
|
||||||
|
class ShapeManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class StateManager
|
* @class StateManager
|
||||||
@@ -34,13 +38,16 @@ class StateManager {
|
|||||||
/**
|
/**
|
||||||
* @brief Destructor
|
* @brief Destructor
|
||||||
*/
|
*/
|
||||||
~StateManager();
|
~StateManager() = default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Inicializa el StateManager con referencia al Engine
|
* @brief Inicializa el StateManager con referencias a los subsistemas necesarios
|
||||||
* @param engine Puntero al Engine (para callbacks)
|
* @param engine Puntero al Engine (para cambios de modo y texturas)
|
||||||
|
* @param scene_mgr Puntero a SceneManager (para control de bolas/gravedad)
|
||||||
|
* @param theme_mgr Puntero a ThemeManager (para cambios de tema)
|
||||||
|
* @param shape_mgr Puntero a ShapeManager (para figuras)
|
||||||
*/
|
*/
|
||||||
void initialize(Engine* engine);
|
void initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Actualiza la máquina de estados (timers, triggers, acciones)
|
* @brief Actualiza la máquina de estados (timers, triggers, acciones)
|
||||||
@@ -145,8 +152,11 @@ class StateManager {
|
|||||||
void exitLogoMode(bool return_to_demo);
|
void exitLogoMode(bool return_to_demo);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// === Referencia al Engine (callback) ===
|
// === Referencias a subsistemas ===
|
||||||
Engine* engine_;
|
Engine* engine_;
|
||||||
|
SceneManager* scene_mgr_;
|
||||||
|
ThemeManager* theme_mgr_;
|
||||||
|
ShapeManager* shape_mgr_;
|
||||||
|
|
||||||
// === Estado de aplicación ===
|
// === Estado de aplicación ===
|
||||||
AppMode current_app_mode_;
|
AppMode current_app_mode_;
|
||||||
|
|||||||
@@ -1,23 +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 "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;
|
||||||
@@ -25,30 +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;
|
||||||
SDL_Log("Fuente cargada desde ResourceManager: %s (%lu bytes)", font_path, (unsigned long)fontDataSize);
|
{
|
||||||
return true;
|
std::string fn = std::string(font_path);
|
||||||
} else {
|
fn = fn.substr(fn.find_last_of("\\/") + 1);
|
||||||
delete[] fontData;
|
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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)
|
||||||
@@ -61,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()");
|
||||||
@@ -84,36 +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;
|
||||||
SDL_Log("Fuente recargada desde ResourceManager: %s (tamaño %d)", font_path_.c_str(), new_font_size);
|
{
|
||||||
return true;
|
std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1);
|
||||||
} else {
|
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
|
||||||
delete[] fontData;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,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);
|
||||||
|
|
||||||
@@ -293,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);
|
||||||
|
|
||||||
@@ -324,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;
|
||||||
}
|
}
|
||||||
@@ -337,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
|
||||||
@@ -354,10 +374,18 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TTF_GetFontHeight(font_);
|
return TTF_GetFontHeight(font_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::getGlyphHeight() -> int {
|
||||||
|
if (!isInitialized()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TTF_GetFontAscent(font_) - TTF_GetFontDescent(font_);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
#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 {
|
||||||
public:
|
public:
|
||||||
TextRenderer();
|
TextRenderer();
|
||||||
~TextRenderer();
|
~TextRenderer();
|
||||||
|
|
||||||
@@ -42,13 +43,16 @@ public:
|
|||||||
// Útil para notificaciones y elementos UI de tamaño fijo
|
// Útil para notificaciones y elementos UI de tamaño fijo
|
||||||
int getTextWidthPhysical(const char* text);
|
int getTextWidthPhysical(const char* text);
|
||||||
|
|
||||||
// Obtiene la altura de la fuente
|
// Obtiene la altura de la fuente (incluye line_gap)
|
||||||
int getTextHeight();
|
int getTextHeight();
|
||||||
|
|
||||||
|
// Obtiene la altura real del glifo (ascender + |descendente|, sin line_gap)
|
||||||
|
int getGlyphHeight();
|
||||||
|
|
||||||
// Verifica si está inicializado correctamente
|
// Verifica si está inicializado correctamente
|
||||||
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
|
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* renderer_;
|
SDL_Renderer* renderer_;
|
||||||
TTF_Font* font_;
|
TTF_Font* font_;
|
||||||
int font_size_;
|
int font_size_;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,7 @@ class ThemeManager {
|
|||||||
|
|
||||||
// Inicialización
|
// Inicialización
|
||||||
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
|
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
|
||||||
|
void setMaxBallCount(int n) { max_ball_count_ = n; } // Máximo real (escenario 8 o custom si mayor)
|
||||||
|
|
||||||
// Interfaz unificada (PHASE 2 + PHASE 3)
|
// Interfaz unificada (PHASE 2 + PHASE 3)
|
||||||
void switchToTheme(int theme_index); // Cambia a tema 0-14 con transición LERP suave (PHASE 3)
|
void switchToTheme(int theme_index); // Cambia a tema 0-14 con transición LERP suave (PHASE 3)
|
||||||
@@ -53,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_; }
|
||||||
@@ -99,6 +99,9 @@ class ThemeManager {
|
|||||||
// Snapshot del tema origen (capturado al iniciar transición)
|
// Snapshot del tema origen (capturado al iniciar transición)
|
||||||
std::unique_ptr<ThemeSnapshot> source_snapshot_; // nullptr si no hay transición
|
std::unique_ptr<ThemeSnapshot> source_snapshot_; // nullptr si no hay transición
|
||||||
|
|
||||||
|
// Máximo de bolas posible en esta sesión (max(SCENE_BALLS_8, custom_balls))
|
||||||
|
int max_ball_count_ = SCENE_BALLS_8;
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// MÉTODOS PRIVADOS
|
// MÉTODOS PRIVADOS
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -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_];
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -58,7 +63,7 @@ class Theme {
|
|||||||
* Actualiza progreso de animación interna (solo dinámicos)
|
* Actualiza progreso de animación interna (solo dinámicos)
|
||||||
* @param delta_time: Tiempo transcurrido desde último frame
|
* @param delta_time: Tiempo transcurrido desde último frame
|
||||||
*/
|
*/
|
||||||
virtual void update(float delta_time) { }
|
virtual void update(float delta_time) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ¿Este tema necesita update() cada frame?
|
* ¿Este tema necesita update() cada frame?
|
||||||
@@ -75,7 +80,7 @@ class Theme {
|
|||||||
/**
|
/**
|
||||||
* Reinicia progreso de animación a 0.0 (usado al activar tema)
|
* Reinicia progreso de animación a 0.0 (usado al activar tema)
|
||||||
*/
|
*/
|
||||||
virtual void resetProgress() { }
|
virtual void resetProgress() {}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// PAUSA (solo temas dinámicos)
|
// PAUSA (solo temas dinámicos)
|
||||||
@@ -90,5 +95,5 @@ class Theme {
|
|||||||
/**
|
/**
|
||||||
* Toggle pausa de animación (solo dinámicos, tecla Shift+D)
|
* Toggle pausa de animación (solo dinámicos, tecla Shift+D)
|
||||||
*/
|
*/
|
||||||
virtual void togglePause() { }
|
virtual void togglePause() {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "defines.hpp" // for Color
|
#include "defines.hpp" // for Color
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,7 +38,24 @@ 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_ != nullptr) {
|
||||||
|
SDL_DestroyTexture(logo1_base_texture_);
|
||||||
|
logo1_base_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;
|
||||||
base_screen_height_ = screen_height;
|
base_screen_height_ = screen_height;
|
||||||
@@ -48,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;
|
||||||
@@ -60,12 +80,6 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
|||||||
int logo_base_target_height = static_cast<int>(base_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
int logo_base_target_height = static_cast<int>(base_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
||||||
int logo_native_target_height = static_cast<int>(native_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
int logo_native_target_height = static_cast<int>(native_screen_height_ * APPLOGO_HEIGHT_PERCENT);
|
||||||
|
|
||||||
std::cout << "Pre-escalando logos:" << std::endl;
|
|
||||||
std::cout << " Base: " << base_screen_width_ << "x" << base_screen_height_
|
|
||||||
<< " (altura logo: " << logo_base_target_height << "px)" << std::endl;
|
|
||||||
std::cout << " Nativa: " << native_screen_width_ << "x" << native_screen_height_
|
|
||||||
<< " (altura logo: " << logo_native_target_height << "px)" << std::endl;
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// 3. Cargar y escalar LOGO1 (data/logo/logo.png) a 2 versiones
|
// 3. Cargar y escalar LOGO1 (data/logo/logo.png) a 2 versiones
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -77,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,11 +206,11 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
|
|||||||
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 << "Logos pre-escalados exitosamente (4 texturas creadas)" << std::endl;
|
std::cout << "[Logo] logo.png + logo2.png (base " << logo_base_target_height << "px, nativa " << logo_native_target_height << "px)\n";
|
||||||
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;
|
||||||
@@ -263,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);
|
||||||
@@ -280,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);
|
||||||
@@ -298,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;
|
||||||
@@ -318,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,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);
|
||||||
@@ -393,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);
|
||||||
@@ -411,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;
|
||||||
@@ -441,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,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_;
|
||||||
@@ -489,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
|
||||||
@@ -500,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;
|
|
||||||
}
|
|
||||||
|
|
||||||
float AppLogo::easeOutBack(float t) {
|
|
||||||
// Back easing out: overshoot suave al final
|
|
||||||
const float c1 = 1.70158f;
|
|
||||||
const float c3 = c1 + 1.0f;
|
|
||||||
|
|
||||||
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
float AppLogo::easeOutBounce(float t) {
|
|
||||||
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
|
|
||||||
const float n1 = 7.5625f;
|
|
||||||
const float d1 = 2.75f;
|
|
||||||
|
|
||||||
if (t < 1.0f / d1) {
|
|
||||||
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 == 1.0f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * C4)) + 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
float AppLogo::easeInOutQuad(float t) {
|
auto AppLogo::easeOutBack(float t) -> float {
|
||||||
|
// Back easing out: overshoot suave al final
|
||||||
|
const float C1 = 1.70158f;
|
||||||
|
const float C3 = C1 + 1.0f;
|
||||||
|
|
||||||
|
return 1.0f + (C3 * powf(t - 1.0f, 3.0f)) + (C1 * powf(t - 1.0f, 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AppLogo::easeOutBounce(float t) -> float {
|
||||||
|
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
|
||||||
|
const float N1 = 7.5625f;
|
||||||
|
const float D1 = 2.75f;
|
||||||
|
|
||||||
|
if (t < 1.0f / D1) {
|
||||||
|
return N1 * t * t;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -572,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_;
|
||||||
@@ -589,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_;
|
||||||
@@ -629,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;
|
||||||
@@ -639,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);
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#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 "text/textrenderer.hpp"
|
#include "text/textrenderer.hpp"
|
||||||
#include "theme_manager.hpp"
|
#include "theme_manager.hpp"
|
||||||
|
|
||||||
@@ -18,68 +20,71 @@ HelpOverlay::HelpOverlay()
|
|||||||
box_y_(0),
|
box_y_(0),
|
||||||
column1_width_(0),
|
column1_width_(0),
|
||||||
column2_width_(0),
|
column2_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 2 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", "Toggle Física - Última Figura"},
|
{.key = "F", .description = "Cambia entre figura y física"},
|
||||||
{"B", "Modo Boids (enjambre)"},
|
{.key = "B", .description = "Cambia entre boids y física"},
|
||||||
{"ESPACIO", "Impulso contra gravedad"},
|
{.key = "ESPACIO", .description = "Impulso contra la gravedad"},
|
||||||
{"G", "Toggle Gravedad ON/OFF"},
|
{.key = "G", .description = "Activar / Desactivar gravedad"},
|
||||||
{"CURSORES", "Dirección de 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"},
|
||||||
{"O", "Forma PNG"},
|
{.key = "Num+/-", .description = "Escalar figura"},
|
||||||
{"Num+/-", "Escalar figura"},
|
{.key = "Num*", .description = "Reset escala"},
|
||||||
{"Num*", "Reset escala"},
|
{.key = "Num/", .description = "Activar / Desactivar profundidad"},
|
||||||
{"Num/", "Toggle profundidad"},
|
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
|
||||||
{"", ""}, // Separador
|
|
||||||
|
|
||||||
// COLUMNA 1: VISUAL
|
|
||||||
{"VISUAL", ""},
|
|
||||||
{"C", "Tema siguiente"},
|
|
||||||
{"Shift+C", "Tema anterior"},
|
|
||||||
{"NumEnter", "Página de temas"},
|
|
||||||
{"N", "Cambiar sprite"},
|
|
||||||
{"[new_col]", ""}, // Separador -> CAMBIO DE COLUMNA
|
|
||||||
|
|
||||||
// COLUMNA 2: PANTALLA
|
|
||||||
{"PANTALLA", ""},
|
|
||||||
{"F1/F2", "Zoom out/in (ventana)"},
|
|
||||||
{"F3", "Fullscreen letterbox"},
|
|
||||||
{"F4", "Fullscreen real"},
|
|
||||||
{"F5", "Escalado (F3 activo)"},
|
|
||||||
{"V", "Toggle V-Sync"},
|
|
||||||
{"", ""}, // Separador
|
|
||||||
|
|
||||||
// COLUMNA 2: MODOS
|
// COLUMNA 2: MODOS
|
||||||
{"MODOS", ""},
|
{.key = "MODOS", .description = ""},
|
||||||
{"D", "Modo DEMO"},
|
{.key = "D", .description = "Activar / Desactivar modo demo"},
|
||||||
{"Shift+D", "Pausar tema dinámico"},
|
{.key = "L", .description = "Activar / Desactivar modo demo lite"},
|
||||||
{"L", "Modo DEMO LITE"},
|
{.key = "K", .description = "Activar / Desactivar modo logo"},
|
||||||
{"K", "Modo LOGO (easter egg)"},
|
{.key = "", .description = ""}, // Separador
|
||||||
{"", ""}, // Separador
|
|
||||||
|
|
||||||
// COLUMNA 2: DEBUG/AYUDA
|
// COLUMNA 2: VISUAL
|
||||||
{"DEBUG/AYUDA", ""},
|
{.key = "VISUAL", .description = ""},
|
||||||
{"F12", "Toggle info debug"},
|
{.key = "C", .description = "Tema siguiente"},
|
||||||
{"H", "Esta ayuda"},
|
{.key = "Shift+C", .description = "Tema anterior"},
|
||||||
{"ESC", "Salir"}};
|
{.key = "NumEnter", .description = "Página de temas"},
|
||||||
|
{.key = "Shift+D", .description = "Pausar tema dinámico"},
|
||||||
|
{.key = "N", .description = "Cambiar tamaño de pelota"},
|
||||||
|
{.key = "X", .description = "Ciclar presets PostFX"},
|
||||||
|
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
|
||||||
|
|
||||||
|
// COLUMNA 3: PANTALLA
|
||||||
|
{.key = "PANTALLA", .description = ""},
|
||||||
|
{.key = "F1", .description = "Disminuye ventana"},
|
||||||
|
{.key = "F2", .description = "Aumenta ventana"},
|
||||||
|
{.key = "F3", .description = "Pantalla completa"},
|
||||||
|
{.key = "F4", .description = "Pantalla completa real"},
|
||||||
|
{.key = "F5", .description = "Activar / Desactivar PostFX"},
|
||||||
|
{.key = "F6", .description = "Cambia el escalado de pantalla"},
|
||||||
|
{.key = "V", .description = "Activar / Desactivar V-Sync"},
|
||||||
|
{.key = "", .description = ""}, // Separador
|
||||||
|
|
||||||
|
// COLUMNA 3: DEBUG/AYUDA
|
||||||
|
{.key = "DEBUG / AYUDA", .description = ""},
|
||||||
|
{.key = "F12", .description = "Activar / Desactivar info debug"},
|
||||||
|
{.key = "H", .description = "Esta ayuda"},
|
||||||
|
{.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;
|
||||||
}
|
}
|
||||||
@@ -98,7 +103,7 @@ void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, in
|
|||||||
|
|
||||||
// Crear renderer de texto con tamaño dinámico
|
// Crear renderer de texto con tamaño dinámico
|
||||||
text_renderer_ = new TextRenderer();
|
text_renderer_ = new TextRenderer();
|
||||||
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", font_size, true);
|
text_renderer_->init(renderer, APP_FONT, font_size, true);
|
||||||
|
|
||||||
calculateBoxDimensions();
|
calculateBoxDimensions();
|
||||||
}
|
}
|
||||||
@@ -113,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);
|
||||||
@@ -132,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,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;
|
||||||
@@ -157,12 +164,13 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
|
|||||||
// Calcular ancho máximo por columna
|
// Calcular ancho máximo por columna
|
||||||
int max_col1_width = 0;
|
int max_col1_width = 0;
|
||||||
int max_col2_width = 0;
|
int max_col2_width = 0;
|
||||||
|
int max_col3_width = 0;
|
||||||
int current_column = 0;
|
int current_column = 0;
|
||||||
|
|
||||||
for (const auto& binding : key_bindings_) {
|
for (const auto& binding : key_bindings_) {
|
||||||
// Cambio de columna
|
// Cambio de columna
|
||||||
if (strcmp(binding.key, "[new_col]") == 0) {
|
if (strcmp(binding.key, "[new_col]") == 0) {
|
||||||
current_column = 1;
|
current_column++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,53 +194,57 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
|
|||||||
// Actualizar máximo de columna correspondiente
|
// Actualizar máximo de columna correspondiente
|
||||||
if (current_column == 0) {
|
if (current_column == 0) {
|
||||||
max_col1_width = std::max(max_col1_width, line_width);
|
max_col1_width = std::max(max_col1_width, line_width);
|
||||||
} else {
|
} else if (current_column == 1) {
|
||||||
max_col2_width = std::max(max_col2_width, line_width);
|
max_col2_width = std::max(max_col2_width, line_width);
|
||||||
|
} else {
|
||||||
|
max_col3_width = std::max(max_col3_width, line_width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Almacenar anchos de columnas en miembros para uso posterior
|
// Almacenar anchos de columnas en miembros para uso posterior
|
||||||
column1_width_ = max_col1_width;
|
column1_width_ = max_col1_width;
|
||||||
column2_width_ = max_col2_width;
|
column2_width_ = max_col2_width;
|
||||||
|
column3_width_ = max_col3_width;
|
||||||
|
|
||||||
// Ancho total: 2 columnas + 3 paddings (izq, medio, der)
|
// Gap entre columnas: doble del padding para dar más respiro
|
||||||
max_width = max_col1_width + max_col2_width + padding * 3;
|
int col_gap = padding * 2;
|
||||||
|
|
||||||
// Altura: contar líneas REALES en cada columna
|
// Ancho total: 3 columnas + padding izq/der + 2 gaps entre columnas
|
||||||
int col1_lines = 0;
|
max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2;
|
||||||
int col2_lines = 0;
|
|
||||||
|
// Calcular altura real simulando exactamente lo que hace el render
|
||||||
|
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_) {
|
||||||
// Cambio de columna
|
|
||||||
if (strcmp(binding.key, "[new_col]") == 0) {
|
if (strcmp(binding.key, "[new_col]") == 0) {
|
||||||
current_column = 1;
|
current_column++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separador vacío no cuenta como línea
|
|
||||||
if (binding.key[0] == '\0') {
|
if (binding.key[0] == '\0') {
|
||||||
continue;
|
col_heights[current_column] += line_height; // separador vacío
|
||||||
}
|
} else if (binding.description[0] == '\0') {
|
||||||
|
col_heights[current_column] += line_height; // encabezado
|
||||||
// Contar línea (ya sea encabezado o contenido)
|
|
||||||
if (current_column == 0) {
|
|
||||||
col1_lines++;
|
|
||||||
} else {
|
} else {
|
||||||
col2_lines++;
|
col_heights[current_column] += line_height; // línea normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usar la columna más larga para calcular altura
|
int content_height = std::max({col_heights[0], col_heights[1], col_heights[2]});
|
||||||
int max_column_lines = std::max(col1_lines, col2_lines);
|
|
||||||
|
|
||||||
// Altura: título (2 líneas) + contenido + padding superior e inferior
|
// Eliminar el line_gap de la última línea: ese gap es espacio entre líneas,
|
||||||
total_height = line_height * 2 + max_column_lines * line_height + padding * 2;
|
// pero la última línea no tiene siguiente, así que queda como padding muerto.
|
||||||
|
int glyph_height = text_renderer_->getGlyphHeight();
|
||||||
|
int visual_content_height = content_height - (line_height - glyph_height);
|
||||||
|
|
||||||
|
total_height = visual_content_height + padding * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -248,10 +260,12 @@ void HelpOverlay::calculateBoxDimensions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -263,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;
|
||||||
}
|
}
|
||||||
@@ -285,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};
|
||||||
|
|
||||||
@@ -330,60 +348,68 @@ 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();
|
||||||
// Padding dinámico basado en altura física: 25px para >= 600px, escalado proporcionalmente para menores
|
|
||||||
int padding = (physical_height_ >= 600) ? 25 : std::max(10, physical_height_ / 24);
|
int padding = (physical_height_ >= 600) ? 25 : std::max(10, physical_height_ / 24);
|
||||||
|
int col_gap = padding * 2;
|
||||||
|
|
||||||
int current_x = padding; // Coordenadas relativas a la textura (0,0)
|
// Posición X de inicio de cada columna
|
||||||
|
std::array<int, 3> col_start{};
|
||||||
|
col_start[0] = padding;
|
||||||
|
col_start[1] = padding + column1_width_ + col_gap;
|
||||||
|
col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap;
|
||||||
|
|
||||||
|
// Ancho de cada columna (para centrado interno)
|
||||||
|
std::array<int, 3> col_width = {column1_width_, column2_width_, column3_width_};
|
||||||
|
|
||||||
|
int glyph_height = text_renderer_->getGlyphHeight();
|
||||||
int current_y = padding;
|
int current_y = padding;
|
||||||
int current_column = 0;
|
int current_column = 0;
|
||||||
|
|
||||||
// Título principal
|
|
||||||
const char* title = "CONTROLES - ViBe3 Physics";
|
|
||||||
int title_width = text_renderer_->getTextWidthPhysical(title);
|
|
||||||
text_renderer_->printAbsolute(box_width_ / 2 - title_width / 2, current_y, title, category_color);
|
|
||||||
current_y += line_height * 2;
|
|
||||||
|
|
||||||
int content_start_y = current_y;
|
int content_start_y = current_y;
|
||||||
|
|
||||||
// Renderizar cada línea
|
// Renderizar cada línea
|
||||||
for (const auto& binding : key_bindings_) {
|
for (const auto& binding : key_bindings_) {
|
||||||
if (strcmp(binding.key, "[new_col]") == 0 && binding.description[0] == '\0') {
|
if (strcmp(binding.key, "[new_col]") == 0 && binding.description[0] == '\0') {
|
||||||
if (current_column == 0) {
|
if (current_column < 2) {
|
||||||
current_column = 1;
|
current_column++;
|
||||||
current_x = padding + column1_width_ + padding; // Usar ancho real de columna 1
|
|
||||||
current_y = content_start_y;
|
current_y = content_start_y;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK PADDING INFERIOR ANTES de escribir la línea (AMBAS COLUMNAS)
|
// CHECK PADDING INFERIOR ANTES de escribir la línea
|
||||||
// Verificar si la PRÓXIMA línea cabrá dentro del box con padding inferior
|
// Usamos glyph_height (no line_height) porque el gap después de la última línea no ocupa espacio visual
|
||||||
if (current_y + line_height >= box_height_ - padding) {
|
if (current_y + glyph_height > box_height_ - padding) {
|
||||||
if (current_column == 0) {
|
|
||||||
// Columna 0 llena: cambiar a columna 1
|
|
||||||
current_column = 1;
|
|
||||||
current_x = padding + column1_width_ + padding;
|
|
||||||
current_y = content_start_y;
|
|
||||||
} else {
|
|
||||||
// Columna 1 llena: omitir resto de texto (no cabe)
|
|
||||||
// Preferible omitir que sobresalir del overlay
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
int cx = col_start[current_column];
|
||||||
|
int cw = col_width[current_column];
|
||||||
|
|
||||||
if (binding.description[0] == '\0') {
|
if (binding.description[0] == '\0') {
|
||||||
text_renderer_->printAbsolute(current_x, current_y, binding.key, category_color);
|
if (binding.key[0] == '\0') {
|
||||||
current_y += line_height + 2;
|
// Separador vacío: avanzar una línea completa
|
||||||
|
current_y += line_height;
|
||||||
|
} else {
|
||||||
|
// Encabezado de sección — centrado en la columna
|
||||||
|
int w = text_renderer_->getTextWidthPhysical(binding.key);
|
||||||
|
text_renderer_->printAbsolute(cx + ((cw - w) / 2), current_y, binding.key, category_color);
|
||||||
|
current_y += line_height;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
text_renderer_->printAbsolute(current_x, current_y, binding.key, content_color);
|
// Par tecla+descripción — centrado como bloque en la columna
|
||||||
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
|
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
|
||||||
text_renderer_->printAbsolute(current_x + key_width + 10, current_y, binding.description, content_color);
|
int desc_width = text_renderer_->getTextWidthPhysical(binding.description);
|
||||||
|
int total_width = key_width + 10 + desc_width;
|
||||||
|
int line_x = cx + ((cw - total_width) / 2);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
current_y += line_height;
|
current_y += line_height;
|
||||||
}
|
}
|
||||||
@@ -396,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);
|
||||||
@@ -426,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);
|
||||||
@@ -443,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;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class HelpOverlay {
|
|||||||
// Anchos individuales de cada columna (para evitar solapamiento)
|
// Anchos individuales de cada columna (para evitar solapamiento)
|
||||||
int column1_width_;
|
int column1_width_;
|
||||||
int column2_width_;
|
int column2_width_;
|
||||||
|
int column3_width_;
|
||||||
|
|
||||||
// Sistema de caché para optimización de rendimiento
|
// Sistema de caché para optimización de rendimiento
|
||||||
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
|
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
|
|||||||
|
|
||||||
SDL_free(displays);
|
SDL_free(displays);
|
||||||
|
|
||||||
std::cout << "Resolución nativa detectada: " << native_width << "x" << native_height << std::endl;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,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
|
||||||
@@ -73,8 +75,6 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Imagen cargada: " << path << " (" << orig_width << "x" << orig_height << ")" << std::endl;
|
|
||||||
|
|
||||||
// 2. Calcular tamaño final manteniendo aspect ratio
|
// 2. Calcular tamaño final manteniendo aspect ratio
|
||||||
// El alto está fijado por target_height (APPLOGO_HEIGHT_PERCENT)
|
// El alto está fijado por target_height (APPLOGO_HEIGHT_PERCENT)
|
||||||
// Calcular ancho proporcional al aspect ratio original
|
// Calcular ancho proporcional al aspect ratio original
|
||||||
@@ -82,10 +82,8 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
|
|||||||
out_width = static_cast<int>(target_height * aspect_ratio);
|
out_width = static_cast<int>(target_height * aspect_ratio);
|
||||||
out_height = target_height;
|
out_height = target_height;
|
||||||
|
|
||||||
std::cout << " Escalando a: " << out_width << "x" << out_height << std::endl;
|
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -95,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
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,7 +113,6 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << " Escalado completado correctamente" << std::endl;
|
|
||||||
return scaled_data;
|
return scaled_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,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;
|
||||||
@@ -130,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());
|
||||||
64
source/ui/logo_scaler.hpp
Normal file
64
source/ui/logo_scaler.hpp
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL_render.h> // Para SDL_Renderer, SDL_Texture
|
||||||
|
#include <SDL3/SDL_video.h> // Para SDL_DisplayID, SDL_GetDisplays
|
||||||
|
|
||||||
|
#include <string> // Para std::string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper class para pre-escalar logos usando stb_image_resize2
|
||||||
|
*
|
||||||
|
* Proporciona funciones para:
|
||||||
|
* - Detectar resolución nativa del monitor
|
||||||
|
* - Cargar PNG y escalar a tamaño específico con algoritmos de alta calidad
|
||||||
|
* - Crear texturas SDL desde buffers escalados
|
||||||
|
*
|
||||||
|
* Usado por AppLogo para pre-generar versiones de logos al tamaño exacto
|
||||||
|
* de pantalla, eliminando el escalado dinámico de SDL y mejorando calidad visual.
|
||||||
|
*/
|
||||||
|
class LogoScaler {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Detecta la resolución nativa del monitor principal
|
||||||
|
*
|
||||||
|
* @param native_width [out] Ancho nativo del display en píxeles
|
||||||
|
* @param native_height [out] Alto nativo del display en píxeles
|
||||||
|
* @return true si se pudo detectar, false si hubo error
|
||||||
|
*/
|
||||||
|
static bool detectNativeResolution(int& native_width, int& native_height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Carga un PNG y lo escala al tamaño especificado
|
||||||
|
*
|
||||||
|
* Usa stb_image para cargar y stb_image_resize2 para escalar con
|
||||||
|
* algoritmo Mitchell (balance calidad/velocidad) en espacio sRGB.
|
||||||
|
*
|
||||||
|
* @param path Ruta al archivo PNG (ej: "data/logo/logo.png")
|
||||||
|
* @param target_width Ancho destino en píxeles
|
||||||
|
* @param target_height Alto destino en píxeles
|
||||||
|
* @param out_width [out] Ancho real de la imagen escalada
|
||||||
|
* @param out_height [out] Alto real de la imagen escalada
|
||||||
|
* @return Buffer RGBA (4 bytes por píxel) o nullptr si falla
|
||||||
|
* IMPORTANTE: El caller debe liberar con free() cuando termine
|
||||||
|
*/
|
||||||
|
static unsigned char* loadAndScale(const std::string& path,
|
||||||
|
int target_width,
|
||||||
|
int target_height,
|
||||||
|
int& out_width,
|
||||||
|
int& out_height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Crea una textura SDL desde un buffer RGBA
|
||||||
|
*
|
||||||
|
* @param renderer Renderizador SDL activo
|
||||||
|
* @param data Buffer RGBA (4 bytes por píxel)
|
||||||
|
* @param width Ancho del buffer en píxeles
|
||||||
|
* @param height Alto del buffer en píxeles
|
||||||
|
* @return Textura SDL creada o nullptr si falla
|
||||||
|
* IMPORTANTE: El caller debe destruir con SDL_DestroyTexture()
|
||||||
|
*/
|
||||||
|
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
|
||||||
|
unsigned char* data,
|
||||||
|
int width,
|
||||||
|
int height);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -20,7 +21,7 @@ class ThemeManager;
|
|||||||
* - Texto de tamaño fijo independiente de resolución
|
* - Texto de tamaño fijo independiente de resolución
|
||||||
*/
|
*/
|
||||||
class Notifier {
|
class Notifier {
|
||||||
public:
|
public:
|
||||||
enum class NotificationState {
|
enum class NotificationState {
|
||||||
SLIDING_IN, // Animación de entrada desde arriba
|
SLIDING_IN, // Animación de entrada desde arriba
|
||||||
VISIBLE, // Visible estático
|
VISIBLE, // Visible estático
|
||||||
@@ -89,7 +90,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* renderer_;
|
SDL_Renderer* renderer_;
|
||||||
TextRenderer* text_renderer_;
|
TextRenderer* text_renderer_;
|
||||||
ThemeManager* theme_manager_; // Gestor de temas para obtener colores dinámicos con LERP
|
ThemeManager* theme_manager_; // Gestor de temas para obtener colores dinámicos con LERP
|
||||||
|
|||||||
@@ -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,9 +70,16 @@ 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_notifier_;
|
||||||
|
text_renderer_notifier_ = nullptr;
|
||||||
|
delete notifier_;
|
||||||
|
notifier_ = nullptr;
|
||||||
|
delete help_overlay_;
|
||||||
|
help_overlay_ = nullptr;
|
||||||
|
|
||||||
renderer_ = renderer;
|
renderer_ = renderer;
|
||||||
theme_manager_ = theme_manager;
|
theme_manager_ = theme_manager;
|
||||||
physical_window_width_ = physical_width;
|
physical_window_width_ = physical_width;
|
||||||
@@ -85,17 +95,16 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
|
|||||||
text_renderer_notifier_ = new TextRenderer();
|
text_renderer_notifier_ = new TextRenderer();
|
||||||
|
|
||||||
// Inicializar renderers con tamaño dinámico
|
// Inicializar renderers con tamaño dinámico
|
||||||
text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", std::max(9, current_font_size_ - 2), true);
|
text_renderer_debug_->init(renderer, APP_FONT, std::max(9, current_font_size_ - 2), true);
|
||||||
text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
|
text_renderer_notifier_->init(renderer, APP_FONT, current_font_size_, true);
|
||||||
|
|
||||||
// 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();
|
||||||
help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height, current_font_size_);
|
help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height, std::max(9, current_font_size_ - 1));
|
||||||
|
|
||||||
// Inicializar FPS counter
|
// Inicializar FPS counter
|
||||||
fps_last_time_ = SDL_GetTicks();
|
fps_last_time_ = SDL_GetTicks();
|
||||||
@@ -133,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,10 +175,15 @@ void UIManager::updateVSyncText(bool enabled) {
|
|||||||
vsync_text_ = enabled ? "V-Sync: On" : "V-Sync: Off";
|
vsync_text_ = enabled ? "V-Sync: On" : "V-Sync: Off";
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIManager::updatePhysicalWindowSize(int width, int height) {
|
void UIManager::updatePhysicalWindowSize(int width, int height, int logical_height) {
|
||||||
physical_window_width_ = width;
|
physical_window_width_ = width;
|
||||||
physical_window_height_ = height;
|
physical_window_height_ = height;
|
||||||
|
|
||||||
|
// Actualizar altura lógica si se proporciona (ej. al entrar/salir de F4)
|
||||||
|
if (logical_height > 0) {
|
||||||
|
logical_window_height_ = logical_height;
|
||||||
|
}
|
||||||
|
|
||||||
// Calcular nuevo tamaño de fuente apropiado basado en altura LÓGICA
|
// Calcular nuevo tamaño de fuente apropiado basado en altura LÓGICA
|
||||||
// (las dimensiones lógicas no cambian con zoom, solo con cambios explícitos de resolución)
|
// (las dimensiones lógicas no cambian con zoom, solo con cambios explícitos de resolución)
|
||||||
int new_font_size = calculateFontSize(logical_window_height_);
|
int new_font_size = calculateFontSize(logical_window_height_);
|
||||||
@@ -180,17 +193,17 @@ void UIManager::updatePhysicalWindowSize(int width, int height) {
|
|||||||
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(current_font_size_, width, height);
|
help_overlay_->updateAll(std::max(9, current_font_size_ - 1), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar otros componentes de UI con nuevas dimensiones
|
// Actualizar otros componentes de UI con nuevas dimensiones
|
||||||
@@ -199,7 +212,7 @@ void UIManager::updatePhysicalWindowSize(int width, int height) {
|
|||||||
|
|
||||||
// === 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,
|
||||||
@@ -226,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";
|
||||||
@@ -236,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();
|
||||||
@@ -246,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;
|
||||||
@@ -265,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;
|
||||||
@@ -293,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";
|
||||||
@@ -312,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;
|
||||||
@@ -330,6 +347,21 @@ void UIManager::renderDebugHUD(const Engine* engine,
|
|||||||
lines.push_back(logic_res_text);
|
lines.push_back(logic_res_text);
|
||||||
lines.push_back(refresh_text);
|
lines.push_back(refresh_text);
|
||||||
lines.push_back(theme_text);
|
lines.push_back(theme_text);
|
||||||
|
std::string postfx_text;
|
||||||
|
if (!engine->isPostFXEnabled()) {
|
||||||
|
postfx_text = "PostFX: OFF";
|
||||||
|
} else {
|
||||||
|
static constexpr std::array<const char*, 4> PRESET_NAMES = {
|
||||||
|
"Vinyeta",
|
||||||
|
"Scanlines",
|
||||||
|
"Cromatica",
|
||||||
|
"Complet"};
|
||||||
|
int mode = engine->getPostFXMode();
|
||||||
|
std::array<char, 64> buf{};
|
||||||
|
SDL_snprintf(buf.data(), buf.size(), "PostFX: %s [V:%.2f C:%.2f S:%.2f]", PRESET_NAMES[mode], engine->getPostFXVignette(), engine->getPostFXChroma(), engine->getPostFXScanline());
|
||||||
|
postfx_text = buf.data();
|
||||||
|
}
|
||||||
|
lines.push_back(postfx_text);
|
||||||
lines.push_back(elapsed_text);
|
lines.push_back(elapsed_text);
|
||||||
|
|
||||||
const Ball* first_ball = scene_manager->getFirstBall();
|
const Ball* first_ball = scene_manager->getFirstBall();
|
||||||
@@ -339,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())));
|
||||||
}
|
}
|
||||||
@@ -351,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)
|
||||||
@@ -396,14 +433,20 @@ int UIManager::calculateFontSize(int logical_height) const {
|
|||||||
} else if (logical_height < 900) {
|
} else if (logical_height < 900) {
|
||||||
// Rango medio-alto (700-899px) → 18px
|
// Rango medio-alto (700-899px) → 18px
|
||||||
font_size = 18;
|
font_size = 18;
|
||||||
|
} else if (logical_height < 1200) {
|
||||||
|
// Rango alto (900-1199px): 900→22, 1080→27, 1199→29
|
||||||
|
font_size = logical_height / 40;
|
||||||
|
} else if (logical_height < 1600) {
|
||||||
|
// Rango muy alto (1200-1599px): 1200→25, 1440→30
|
||||||
|
font_size = logical_height / 48;
|
||||||
} else {
|
} else {
|
||||||
// Rango alto: proporcional (1080px→42, 1440px→55, 2160px→72)
|
// Rango ultra (>=1600px): 1600→26, 2000→33, 2160→36
|
||||||
font_size = logical_height / 26;
|
font_size = logical_height / 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -111,8 +110,9 @@ class UIManager {
|
|||||||
* @brief Actualiza tamaño físico de ventana (cambios de fullscreen)
|
* @brief Actualiza tamaño físico de ventana (cambios de fullscreen)
|
||||||
* @param width Nuevo ancho físico
|
* @param width Nuevo ancho físico
|
||||||
* @param height Nuevo alto físico
|
* @param height Nuevo alto físico
|
||||||
|
* @param logical_height Nuevo alto lógico (0 = sin cambio)
|
||||||
*/
|
*/
|
||||||
void updatePhysicalWindowSize(int width, int height);
|
void updatePhysicalWindowSize(int width, int height, int logical_height = 0);
|
||||||
|
|
||||||
// === Getters ===
|
// === Getters ===
|
||||||
|
|
||||||
@@ -148,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
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user