Preparando la interfaz
This commit is contained in:
commit
66bedbb5f1
17 files changed
+876
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(Tahgibu VERSION 2.0.0 LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
set(CMAKE_C_STANDARD 11)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "Tipo de compilación" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
option(TAHGIBU_BUILD_APP "Compilar el editor (requiere raylib)" ON)
|
||||||
|
option(TAHGIBU_BUILD_TESTS "Compilar las pruebas del núcleo" ON)
|
||||||
|
option(TAHGIBU_FETCH_RAYLIB "Descargar y compilar raylib si no se encuentra (requiere red y git)" OFF)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Núcleo: modelo de datos del proyecto. NO depende de raylib.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(tahgibu_core STATIC
|
||||||
|
source/core/Frame.cpp
|
||||||
|
source/core/Project.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(tahgibu_core PUBLIC source)
|
||||||
|
target_compile_definitions(tahgibu_core PUBLIC TAHGIBU_VERSION="${PROJECT_VERSION}")
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(tahgibu_core PUBLIC -Wall -Wextra)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua: solo se detecta. No se enlaza hasta la etapa 10.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
find_package(Lua QUIET)
|
||||||
|
if(LUA_FOUND)
|
||||||
|
message(STATUS "Lua ${LUA_VERSION_STRING} encontrado (se usará a partir de la etapa 10)")
|
||||||
|
else()
|
||||||
|
message(STATUS "Lua no encontrado. No hace falta todavía; será necesario en la etapa 10.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Editor (raylib)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if(TAHGIBU_BUILD_APP)
|
||||||
|
set(TAHGIBU_RAYLIB_TARGET "")
|
||||||
|
|
||||||
|
find_package(raylib QUIET)
|
||||||
|
if(TARGET raylib)
|
||||||
|
set(TAHGIBU_RAYLIB_TARGET raylib)
|
||||||
|
else()
|
||||||
|
find_package(PkgConfig QUIET)
|
||||||
|
if(PKG_CONFIG_FOUND)
|
||||||
|
pkg_check_modules(RAYLIB QUIET IMPORTED_TARGET raylib)
|
||||||
|
if(RAYLIB_FOUND)
|
||||||
|
set(TAHGIBU_RAYLIB_TARGET PkgConfig::RAYLIB)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT TAHGIBU_RAYLIB_TARGET AND TAHGIBU_FETCH_RAYLIB)
|
||||||
|
include(FetchContent)
|
||||||
|
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(raylib
|
||||||
|
GIT_REPOSITORY https://github.com/raysan5/raylib.git
|
||||||
|
GIT_TAG 5.0
|
||||||
|
GIT_SHALLOW ON)
|
||||||
|
FetchContent_MakeAvailable(raylib)
|
||||||
|
set(TAHGIBU_RAYLIB_TARGET raylib)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT TAHGIBU_RAYLIB_TARGET)
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"No se encontró raylib.\n"
|
||||||
|
"Opciones:\n"
|
||||||
|
" 1) Instalarla con el gestor de paquetes de tu distribución (ver README.md).\n"
|
||||||
|
" 2) Reconfigurar con -DTAHGIBU_FETCH_RAYLIB=ON (descarga y compila raylib).\n"
|
||||||
|
" 3) Compilar solo el núcleo y las pruebas con -DTAHGIBU_BUILD_APP=OFF.")
|
||||||
|
endif()
|
||||||
|
message(STATUS "raylib: ${TAHGIBU_RAYLIB_TARGET}")
|
||||||
|
|
||||||
|
add_executable(tahgibu
|
||||||
|
source/main.cpp
|
||||||
|
source/ui/Application.cpp
|
||||||
|
source/ui/Layout.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(tahgibu PRIVATE tahgibu_core ${TAHGIBU_RAYLIB_TARGET})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pruebas del núcleo (no necesitan raylib ni pantalla)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if(TAHGIBU_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_executable(tahgibu_core_tests tests/core_tests.cpp)
|
||||||
|
target_link_libraries(tahgibu_core_tests PRIVATE tahgibu_core)
|
||||||
|
add_test(NAME core COMMAND tahgibu_core_tests)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Tahgibu 2.0.0
|
||||||
|
|
||||||
|
Editor de animación 2D ligero y nativo para Linux.
|
||||||
|
C++ · C · Lua · raylib · CMake. **Sin Python.**
|
||||||
|
|
||||||
|
## Estado actual: Etapa 1 (base)
|
||||||
|
|
||||||
|
Hecho y verificado:
|
||||||
|
|
||||||
|
- Sistema de compilación con CMake (C, C++, raylib; Lua solo se detecta).
|
||||||
|
- Núcleo sin dependencias gráficas (`source/core`): `Frame`, `Project`, límite de 60 FPS,
|
||||||
|
duración real por frame. Con pruebas automáticas.
|
||||||
|
- Ventana de raylib con el reparto de zonas: menús, herramientas, canvas, propiedades, timeline.
|
||||||
|
- Registro de herramientas (`source/editor/Tool.hpp`): la barra sale de una tabla.
|
||||||
|
|
||||||
|
Todavía **no** hace nada más que eso:
|
||||||
|
|
||||||
|
- Los menús se dibujan pero no abren nada.
|
||||||
|
- Las herramientas se pueden seleccionar (queda resaltada), pero no editan nada.
|
||||||
|
- La timeline es un esqueleto vacío (etapa 3).
|
||||||
|
|
||||||
|
## Dependencias
|
||||||
|
|
||||||
|
Necesarias hoy: compilador C++17, CMake >= 3.16 y raylib >= 4.0.
|
||||||
|
|
||||||
|
| Distro | raylib |
|
||||||
|
|---|---|
|
||||||
|
| Arch | `sudo pacman -S raylib` |
|
||||||
|
| Fedora | `sudo dnf install raylib-devel` |
|
||||||
|
| Debian/Ubuntu | `libraylib-dev` si tu versión lo trae |
|
||||||
|
|
||||||
|
Si tu distro no la trae, dos opciones:
|
||||||
|
|
||||||
|
1. Compilarla desde https://github.com/raysan5/raylib (en Debian/Ubuntu necesita
|
||||||
|
`libasound2-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev
|
||||||
|
libxcursor-dev libxinerama-dev libwayland-dev libxkbcommon-dev`).
|
||||||
|
2. Dejar que CMake la descargue y compile: `cmake -S . -B build -DTAHGIBU_FETCH_RAYLIB=ON`
|
||||||
|
(requiere red y `git`).
|
||||||
|
|
||||||
|
Lua (`liblua5.4-dev` en Debian/Ubuntu, `lua` en Arch, `lua-devel` en Fedora) no hace falta
|
||||||
|
hasta la etapa 10.
|
||||||
|
|
||||||
|
## Compilar y ejecutar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build
|
||||||
|
./build/tahgibu
|
||||||
|
```
|
||||||
|
|
||||||
|
Solo núcleo + pruebas, sin raylib:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build -DTAHGIBU_BUILD_APP=OFF
|
||||||
|
cmake --build build
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura
|
||||||
|
|
||||||
|
```text
|
||||||
|
source/
|
||||||
|
├── main.cpp
|
||||||
|
├── core/ modelo de datos (sin raylib): Frame, Project, constantes
|
||||||
|
├── editor/ estado del editor y registro de herramientas
|
||||||
|
└── ui/ ventana, layout y dibujo con raylib
|
||||||
|
tests/ pruebas del núcleo
|
||||||
|
```
|
||||||
|
|
||||||
|
Los `.hpp` viven junto a su `.cpp`; `source/` es la raíz de includes
|
||||||
|
(`#include "core/Project.hpp"`). Las carpetas `canvas/`, `timeline/`, `animation/`,
|
||||||
|
`project/`, `media/`, `export/` y `lua/` se crean cuando llega su etapa.
|
||||||
|
|
||||||
|
## Decisiones de diseño
|
||||||
|
|
||||||
|
- **FPS por frame = override opcional.** Un frame usa el FPS base del proyecto salvo que
|
||||||
|
tenga FPS propios. Cambiar el FPS base afecta a los frames sin override.
|
||||||
|
- **Límite 60 FPS** aplicado en un único punto (`Frame::clampFps`) para el base y para cada frame.
|
||||||
|
- **Mínimo 0.1 FPS** (un frame puede durar hasta 10 s) para hacer pausas largas sin duplicar frames.
|
||||||
|
Se cambia en `core/Constants.hpp`.
|
||||||
|
- **El FPS de la interfaz (60) es independiente** del FPS de la animación.
|
||||||
|
|
||||||
|
## Limitaciones conocidas
|
||||||
|
|
||||||
|
- El texto usa la fuente por defecto de raylib. Si las tildes salen como `?`, hará falta
|
||||||
|
cargar una fuente propia.
|
||||||
|
- Esc no cierra la ventana (a propósito, para no perder trabajo); se cierra con el botón de la ventana.
|
||||||
|
|
||||||
|
## Hoja de ruta
|
||||||
|
|
||||||
|
1 Base · 2 Editor · 3 Frames · 4 Sprites · 5 Fondos · 6 FPS · 7 Audio · 8 Vértices ·
|
||||||
|
9 Proyecto · 10 Lua · 11 Exportación
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Constantes globales de Tahgibu. Sin dependencias (ni raylib).
|
||||||
|
|
||||||
|
// CMake define TAHGIBU_VERSION a partir de project(VERSION ...).
|
||||||
|
#ifndef TAHGIBU_VERSION
|
||||||
|
#define TAHGIBU_VERSION "2.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
inline constexpr const char* kAppName = "Tahgibu";
|
||||||
|
inline constexpr const char* kAppVersion = TAHGIBU_VERSION;
|
||||||
|
|
||||||
|
// Límite duro: ni el proyecto ni ningún frame individual pueden superarlo.
|
||||||
|
inline constexpr float kMaxFps = 60.0f;
|
||||||
|
|
||||||
|
// Mínimo permitido. 0.1 FPS = un frame visible hasta 10 s, para poder hacer
|
||||||
|
// pausas largas sin duplicar frames.
|
||||||
|
inline constexpr float kMinFps = 0.1f;
|
||||||
|
|
||||||
|
// FPS base de un proyecto nuevo.
|
||||||
|
inline constexpr float kDefaultFps = 24.0f;
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "core/Frame.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
float Frame::clampFps(float fps) noexcept {
|
||||||
|
if (!std::isfinite(fps)) {
|
||||||
|
return kDefaultFps;
|
||||||
|
}
|
||||||
|
return std::clamp(fps, kMinFps, kMaxFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Frame::setFpsOverride(float fps) noexcept {
|
||||||
|
fpsOverride_ = clampFps(fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
float Frame::effectiveFps(float baseFps) const noexcept {
|
||||||
|
return fpsOverride_ ? *fpsOverride_ : clampFps(baseFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
double Frame::durationSeconds(float baseFps) const noexcept {
|
||||||
|
// effectiveFps() >= kMinFps, así que nunca se divide por cero.
|
||||||
|
return 1.0 / static_cast<double>(effectiveFps(baseFps));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/Constants.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
// Un frame de la animación. Solo guarda cuánto dura: usa los FPS base del
|
||||||
|
// proyecto, salvo que tenga FPS propios (override).
|
||||||
|
class Frame {
|
||||||
|
public:
|
||||||
|
// Fuerza el valor al rango [kMinFps, kMaxFps]. Valores no finitos (NaN, inf)
|
||||||
|
// devuelven kDefaultFps.
|
||||||
|
static float clampFps(float fps) noexcept;
|
||||||
|
|
||||||
|
bool hasFpsOverride() const noexcept { return fpsOverride_.has_value(); }
|
||||||
|
void setFpsOverride(float fps) noexcept; // aplica clampFps
|
||||||
|
void clearFpsOverride() noexcept { fpsOverride_.reset(); }
|
||||||
|
|
||||||
|
float effectiveFps(float baseFps) const noexcept;
|
||||||
|
double durationSeconds(float baseFps) const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<float> fpsOverride_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#include "core/Project.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
Project::Project() : name_("Sin título") {
|
||||||
|
frames_.emplace_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Project::setBaseFps(float fps) noexcept {
|
||||||
|
baseFps_ = Frame::clampFps(fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Project::addFrame() {
|
||||||
|
frames_.emplace_back();
|
||||||
|
return frames_.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Project::frameDurationSeconds(std::size_t index) const {
|
||||||
|
return frames_.at(index).durationSeconds(baseFps_);
|
||||||
|
}
|
||||||
|
|
||||||
|
double Project::totalDurationSeconds() const {
|
||||||
|
double total = 0.0;
|
||||||
|
for (const Frame& frame : frames_) {
|
||||||
|
total += frame.durationSeconds(baseFps_);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/Frame.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
// Modelo de datos del proyecto. No conoce raylib ni la interfaz.
|
||||||
|
class Project {
|
||||||
|
public:
|
||||||
|
Project(); // 1280x720, FPS base kDefaultFps, un frame inicial
|
||||||
|
|
||||||
|
const std::string& name() const noexcept { return name_; }
|
||||||
|
int width() const noexcept { return width_; }
|
||||||
|
int height() const noexcept { return height_; }
|
||||||
|
|
||||||
|
float baseFps() const noexcept { return baseFps_; }
|
||||||
|
void setBaseFps(float fps) noexcept; // aplica Frame::clampFps
|
||||||
|
|
||||||
|
std::size_t frameCount() const noexcept { return frames_.size(); }
|
||||||
|
const Frame& frame(std::size_t index) const { return frames_.at(index); }
|
||||||
|
Frame& frame(std::size_t index) { return frames_.at(index); }
|
||||||
|
std::size_t addFrame(); // devuelve el índice del frame nuevo
|
||||||
|
|
||||||
|
double frameDurationSeconds(std::size_t index) const;
|
||||||
|
double totalDurationSeconds() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name_;
|
||||||
|
int width_ = 1280;
|
||||||
|
int height_ = 720;
|
||||||
|
float baseFps_ = kDefaultFps;
|
||||||
|
std::vector<Frame> frames_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "editor/Tool.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
// Estado del editor que NO forma parte del proyecto guardado.
|
||||||
|
struct EditorState {
|
||||||
|
Tool activeTool = Tool::Select;
|
||||||
|
std::size_t currentFrame = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
enum class Tool {
|
||||||
|
Select,
|
||||||
|
Move,
|
||||||
|
Scale,
|
||||||
|
Rotate,
|
||||||
|
Transform,
|
||||||
|
Vertices,
|
||||||
|
Draw,
|
||||||
|
Eraser,
|
||||||
|
Sprite,
|
||||||
|
ColorPicker,
|
||||||
|
Zoom,
|
||||||
|
Pan,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolInfo {
|
||||||
|
Tool id;
|
||||||
|
const char* label;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Registro de herramientas. La barra superior se genera a partir de esta
|
||||||
|
// tabla: agregar una herramienta nueva = agregar un valor al enum y una línea aquí.
|
||||||
|
inline constexpr ToolInfo kToolInfos[] = {
|
||||||
|
{Tool::Select, "Seleccionar"},
|
||||||
|
{Tool::Move, "Mover"},
|
||||||
|
{Tool::Scale, "Escalar"},
|
||||||
|
{Tool::Rotate, "Rotar"},
|
||||||
|
{Tool::Transform, "Transformar"},
|
||||||
|
{Tool::Vertices, "Vértices"},
|
||||||
|
{Tool::Draw, "Dibujar"},
|
||||||
|
{Tool::Eraser, "Borrador"},
|
||||||
|
{Tool::Sprite, "Sprite"},
|
||||||
|
{Tool::ColorPicker, "Color"},
|
||||||
|
{Tool::Zoom, "Zoom"},
|
||||||
|
{Tool::Pan, "Mano"},
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::size_t kToolCount = sizeof(kToolInfos) / sizeof(kToolInfos[0]);
|
||||||
|
|
||||||
|
inline const char* toolLabel(Tool tool) noexcept {
|
||||||
|
for (const ToolInfo& info : kToolInfos) {
|
||||||
|
if (info.id == tool) {
|
||||||
|
return info.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
#include "ui/Application.hpp"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
tahgibu::Application app;
|
||||||
|
return app.run();
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::fprintf(stderr, "Error fatal: %s\n", error.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
#include "ui/Application.hpp"
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/Constants.hpp"
|
||||||
|
#include "editor/Tool.hpp"
|
||||||
|
#include "ui/Theme.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kInitialWidth = 1280;
|
||||||
|
constexpr int kInitialHeight = 800;
|
||||||
|
constexpr int kMinWidth = 1000;
|
||||||
|
constexpr int kMinHeight = 640;
|
||||||
|
constexpr int kUiTargetFps = 60; // FPS de la interfaz; no tiene relación con los FPS de la animación
|
||||||
|
|
||||||
|
constexpr int kFontSize = 16;
|
||||||
|
constexpr int kSmallFontSize = 14;
|
||||||
|
constexpr int kButtonPadding = 10;
|
||||||
|
constexpr float kButtonGap = 4.0f;
|
||||||
|
constexpr float kPanelHeaderHeight = 24.0f;
|
||||||
|
|
||||||
|
constexpr std::array<const char*, 7> kMenus = {
|
||||||
|
"Archivo", "Editar", "Animación", "Sprite", "Audio", "Ver", "Lua"};
|
||||||
|
|
||||||
|
int px(float value) {
|
||||||
|
return static_cast<int>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawTextVCentered(const char* text, float x, const Rectangle& area, int size, Color color) {
|
||||||
|
DrawText(text, px(x), px(area.y + (area.height - static_cast<float>(size)) / 2.0f), size, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawPanelHeader(const Rectangle& panel, const char* title) {
|
||||||
|
DrawRectangle(px(panel.x), px(panel.y), px(panel.width), px(kPanelHeaderHeight), theme::kPanelHeader);
|
||||||
|
DrawText(title, px(panel.x + 12.0f), px(panel.y + 4.0f), kFontSize, theme::kText);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<Rectangle, kToolCount> computeToolButtons(const Rectangle& bar) {
|
||||||
|
std::array<Rectangle, kToolCount> rects{};
|
||||||
|
float x = bar.x + 8.0f;
|
||||||
|
for (std::size_t i = 0; i < kToolCount; ++i) {
|
||||||
|
const float w = static_cast<float>(MeasureText(kToolInfos[i].label, kFontSize) + 2 * kButtonPadding);
|
||||||
|
rects[i] = {x, bar.y + 6.0f, w, bar.height - 12.0f};
|
||||||
|
x += w + kButtonGap;
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<Rectangle, kMenus.size()> computeMenuRects(const Rectangle& bar) {
|
||||||
|
std::array<Rectangle, kMenus.size()> rects{};
|
||||||
|
float x = bar.x + 6.0f;
|
||||||
|
for (std::size_t i = 0; i < kMenus.size(); ++i) {
|
||||||
|
const float w = static_cast<float>(MeasureText(kMenus[i], kFontSize) + 2 * kButtonPadding);
|
||||||
|
rects[i] = {x, bar.y + 2.0f, w, bar.height - 4.0f};
|
||||||
|
x += w + 2.0f;
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Application::Application() {
|
||||||
|
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
|
||||||
|
const std::string title = std::string(kAppName) + " " + kAppVersion;
|
||||||
|
InitWindow(kInitialWidth, kInitialHeight, title.c_str());
|
||||||
|
if (!IsWindowReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SetWindowMinSize(kMinWidth, kMinHeight);
|
||||||
|
SetExitKey(KEY_NULL); // Esc no debe cerrar el editor
|
||||||
|
SetTargetFPS(kUiTargetFps);
|
||||||
|
layout_ = Layout::compute(GetScreenWidth(), GetScreenHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
Application::~Application() {
|
||||||
|
if (IsWindowReady()) {
|
||||||
|
CloseWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::run() {
|
||||||
|
if (!IsWindowReady()) {
|
||||||
|
std::fprintf(stderr, "No se pudo crear la ventana (¿hay entorno gráfico y OpenGL disponible?).\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
while (!WindowShouldClose()) {
|
||||||
|
update();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::update() {
|
||||||
|
layout_ = Layout::compute(GetScreenWidth(), GetScreenHeight());
|
||||||
|
|
||||||
|
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
|
||||||
|
const Vector2 mouse = GetMousePosition();
|
||||||
|
const auto rects = computeToolButtons(layout_.toolBar);
|
||||||
|
for (std::size_t i = 0; i < kToolCount; ++i) {
|
||||||
|
if (CheckCollisionPointRec(mouse, rects[i])) {
|
||||||
|
state_.activeTool = kToolInfos[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::draw() const {
|
||||||
|
BeginDrawing();
|
||||||
|
ClearBackground(theme::kWindowBg);
|
||||||
|
drawCanvas();
|
||||||
|
drawProperties();
|
||||||
|
drawTimeline();
|
||||||
|
drawToolBar();
|
||||||
|
drawMenuBar();
|
||||||
|
EndDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::drawMenuBar() const {
|
||||||
|
const Rectangle& bar = layout_.menuBar;
|
||||||
|
DrawRectangleRec(bar, theme::kPanelHeader);
|
||||||
|
|
||||||
|
const Vector2 mouse = GetMousePosition();
|
||||||
|
const auto rects = computeMenuRects(bar);
|
||||||
|
for (std::size_t i = 0; i < kMenus.size(); ++i) {
|
||||||
|
if (CheckCollisionPointRec(mouse, rects[i])) {
|
||||||
|
DrawRectangleRec(rects[i], theme::kHover);
|
||||||
|
}
|
||||||
|
drawTextVCentered(kMenus[i], rects[i].x + static_cast<float>(kButtonPadding), rects[i], kFontSize,
|
||||||
|
theme::kText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::drawToolBar() const {
|
||||||
|
const Rectangle& bar = layout_.toolBar;
|
||||||
|
DrawRectangleRec(bar, theme::kPanelBg);
|
||||||
|
DrawRectangle(px(bar.x), px(bar.y + bar.height - 1.0f), px(bar.width), 1, theme::kBorder);
|
||||||
|
|
||||||
|
const Vector2 mouse = GetMousePosition();
|
||||||
|
const auto rects = computeToolButtons(bar);
|
||||||
|
for (std::size_t i = 0; i < kToolCount; ++i) {
|
||||||
|
const bool active = state_.activeTool == kToolInfos[i].id;
|
||||||
|
const bool hover = CheckCollisionPointRec(mouse, rects[i]);
|
||||||
|
const Color background = active ? theme::kAccent : (hover ? theme::kHover : theme::kPanelHeader);
|
||||||
|
DrawRectangleRec(rects[i], background);
|
||||||
|
drawTextVCentered(kToolInfos[i].label, rects[i].x + static_cast<float>(kButtonPadding), rects[i],
|
||||||
|
kFontSize, active ? theme::kWindowBg : theme::kText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::drawCanvas() const {
|
||||||
|
const Rectangle& area = layout_.canvas;
|
||||||
|
DrawRectangleRec(area, theme::kCanvasBg);
|
||||||
|
|
||||||
|
// Escenario del tamaño del proyecto, ajustado al área disponible.
|
||||||
|
constexpr float kMargin = 32.0f;
|
||||||
|
const float projectW = static_cast<float>(project_.width());
|
||||||
|
const float projectH = static_cast<float>(project_.height());
|
||||||
|
const float availableW = std::max(1.0f, area.width - 2.0f * kMargin);
|
||||||
|
const float availableH = std::max(1.0f, area.height - 2.0f * kMargin);
|
||||||
|
const float scale = std::min(availableW / projectW, availableH / projectH);
|
||||||
|
const float w = projectW * scale;
|
||||||
|
const float h = projectH * scale;
|
||||||
|
const Rectangle stage{area.x + (area.width - w) / 2.0f, area.y + (area.height - h) / 2.0f, w, h};
|
||||||
|
|
||||||
|
DrawRectangleRec(stage, theme::kStage);
|
||||||
|
DrawRectangleLinesEx(stage, 1.0f, theme::kBorder);
|
||||||
|
|
||||||
|
char caption[96];
|
||||||
|
std::snprintf(caption, sizeof caption, "Escenario %d x %d", project_.width(), project_.height());
|
||||||
|
DrawText(caption, px(area.x + 10.0f), px(area.y + 8.0f), kSmallFontSize, theme::kTextDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::drawProperties() const {
|
||||||
|
const Rectangle& panel = layout_.properties;
|
||||||
|
DrawRectangleRec(panel, theme::kPanelBg);
|
||||||
|
drawPanelHeader(panel, "Propiedades");
|
||||||
|
DrawRectangle(px(panel.x), px(panel.y), 1, px(panel.height), theme::kBorder);
|
||||||
|
|
||||||
|
const std::size_t index = std::min(state_.currentFrame, project_.frameCount() - 1);
|
||||||
|
const Frame& frame = project_.frame(index);
|
||||||
|
|
||||||
|
float y = panel.y + kPanelHeaderHeight + 12.0f;
|
||||||
|
const float x = panel.x + 12.0f;
|
||||||
|
char line[128];
|
||||||
|
auto put = [&](Color color) {
|
||||||
|
DrawText(line, px(x), px(y), kFontSize, color);
|
||||||
|
y += 24.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::snprintf(line, sizeof line, "Proyecto: %s", project_.name().c_str());
|
||||||
|
put(theme::kText);
|
||||||
|
std::snprintf(line, sizeof line, "Resolución: %d x %d", project_.width(), project_.height());
|
||||||
|
put(theme::kText);
|
||||||
|
std::snprintf(line, sizeof line, "FPS base: %.1f (máx. %.0f)", static_cast<double>(project_.baseFps()),
|
||||||
|
static_cast<double>(kMaxFps));
|
||||||
|
put(theme::kText);
|
||||||
|
std::snprintf(line, sizeof line, "Frame: %zu / %zu", index + 1, project_.frameCount());
|
||||||
|
put(theme::kText);
|
||||||
|
std::snprintf(line, sizeof line, "FPS del frame: %.1f %s",
|
||||||
|
static_cast<double>(frame.effectiveFps(project_.baseFps())),
|
||||||
|
frame.hasFpsOverride() ? "(propio)" : "(base)");
|
||||||
|
put(theme::kText);
|
||||||
|
std::snprintf(line, sizeof line, "Herramienta: %s", toolLabel(state_.activeTool));
|
||||||
|
put(theme::kText);
|
||||||
|
|
||||||
|
y += 8.0f;
|
||||||
|
std::snprintf(line, sizeof line, "Sprites y vértices: etapas 4 y 8");
|
||||||
|
put(theme::kTextDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::drawTimeline() const {
|
||||||
|
const Rectangle& panel = layout_.timeline;
|
||||||
|
DrawRectangleRec(panel, theme::kPanelBg);
|
||||||
|
drawPanelHeader(panel, "Timeline");
|
||||||
|
DrawRectangle(px(panel.x), px(panel.y), px(panel.width), 1, theme::kBorder);
|
||||||
|
|
||||||
|
constexpr std::array<const char*, 3> kTracks = {"Fondo", "Sprite", "Audio"};
|
||||||
|
constexpr float kLaneHeight = 32.0f;
|
||||||
|
constexpr float kLabelWidth = 90.0f;
|
||||||
|
|
||||||
|
float y = panel.y + kPanelHeaderHeight + 12.0f;
|
||||||
|
for (const char* name : kTracks) {
|
||||||
|
DrawText(name, px(panel.x + 12.0f), px(y + (kLaneHeight - static_cast<float>(kFontSize)) / 2.0f),
|
||||||
|
kFontSize, theme::kText);
|
||||||
|
const Rectangle lane{panel.x + kLabelWidth, y, panel.width - kLabelWidth - 12.0f, kLaneHeight - 4.0f};
|
||||||
|
DrawRectangleRec(lane, theme::kPanelHeader);
|
||||||
|
y += kLaneHeight;
|
||||||
|
}
|
||||||
|
DrawText("La timeline se implementa en la etapa 3", px(panel.x + 12.0f), px(y + 8.0f), kSmallFontSize,
|
||||||
|
theme::kTextDim);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/Project.hpp"
|
||||||
|
#include "editor/EditorState.hpp"
|
||||||
|
#include "ui/Layout.hpp"
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
// Ventana principal: crea la ventana de raylib, ejecuta el bucle y dibuja las zonas.
|
||||||
|
// La lógica del programa vive en core/ y editor/, no aquí.
|
||||||
|
class Application {
|
||||||
|
public:
|
||||||
|
Application();
|
||||||
|
~Application();
|
||||||
|
Application(const Application&) = delete;
|
||||||
|
Application& operator=(const Application&) = delete;
|
||||||
|
|
||||||
|
// Devuelve el código de salida del proceso.
|
||||||
|
int run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void update();
|
||||||
|
void draw() const;
|
||||||
|
void drawMenuBar() const;
|
||||||
|
void drawToolBar() const;
|
||||||
|
void drawCanvas() const;
|
||||||
|
void drawProperties() const;
|
||||||
|
void drawTimeline() const;
|
||||||
|
|
||||||
|
Project project_;
|
||||||
|
EditorState state_;
|
||||||
|
Layout layout_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include "ui/Layout.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kMenuBarHeight = 26.0f;
|
||||||
|
constexpr float kToolBarHeight = 40.0f;
|
||||||
|
constexpr float kPropertiesWidth = 280.0f;
|
||||||
|
constexpr float kTimelineHeight = 200.0f;
|
||||||
|
constexpr float kMinCanvasWidth = 400.0f;
|
||||||
|
constexpr float kMinCanvasHeight = 200.0f;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Layout Layout::compute(int windowWidth, int windowHeight) {
|
||||||
|
const float w = static_cast<float>(windowWidth);
|
||||||
|
const float h = static_cast<float>(windowHeight);
|
||||||
|
const float top = kMenuBarHeight + kToolBarHeight;
|
||||||
|
|
||||||
|
const float timelineH = std::min(kTimelineHeight, std::max(0.0f, h - top - kMinCanvasHeight));
|
||||||
|
const float propertiesW = std::min(kPropertiesWidth, std::max(0.0f, w - kMinCanvasWidth));
|
||||||
|
const float middleH = std::max(0.0f, h - top - timelineH);
|
||||||
|
|
||||||
|
Layout layout;
|
||||||
|
layout.menuBar = {0.0f, 0.0f, w, kMenuBarHeight};
|
||||||
|
layout.toolBar = {0.0f, kMenuBarHeight, w, kToolBarHeight};
|
||||||
|
layout.canvas = {0.0f, top, w - propertiesW, middleH};
|
||||||
|
layout.properties = {w - propertiesW, top, propertiesW, middleH};
|
||||||
|
layout.timeline = {0.0f, top + middleH, w, timelineH};
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
namespace tahgibu {
|
||||||
|
|
||||||
|
// Reparto de la ventana en zonas. Función pura del tamaño de la ventana.
|
||||||
|
struct Layout {
|
||||||
|
Rectangle menuBar{};
|
||||||
|
Rectangle toolBar{};
|
||||||
|
Rectangle canvas{};
|
||||||
|
Rectangle properties{};
|
||||||
|
Rectangle timeline{};
|
||||||
|
|
||||||
|
static Layout compute(int windowWidth, int windowHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tahgibu
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
namespace tahgibu::theme {
|
||||||
|
|
||||||
|
inline constexpr Color kWindowBg{24, 24, 28, 255};
|
||||||
|
inline constexpr Color kPanelBg{34, 34, 40, 255};
|
||||||
|
inline constexpr Color kPanelHeader{44, 44, 52, 255};
|
||||||
|
inline constexpr Color kBorder{14, 14, 18, 255};
|
||||||
|
inline constexpr Color kHover{62, 62, 74, 255};
|
||||||
|
inline constexpr Color kText{225, 225, 230, 255};
|
||||||
|
inline constexpr Color kTextDim{140, 140, 152, 255};
|
||||||
|
inline constexpr Color kAccent{255, 140, 60, 255};
|
||||||
|
inline constexpr Color kCanvasBg{18, 18, 22, 255};
|
||||||
|
inline constexpr Color kStage{245, 245, 245, 255};
|
||||||
|
|
||||||
|
} // namespace tahgibu::theme
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Pruebas del núcleo. No usan raylib.
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "core/Constants.hpp"
|
||||||
|
#include "core/Frame.hpp"
|
||||||
|
#include "core/Project.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
void check(bool condition, const char* expression, int line) {
|
||||||
|
if (!condition) {
|
||||||
|
std::fprintf(stderr, "FALLO (línea %d): %s\n", line, expression);
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool near(double a, double b) {
|
||||||
|
return std::fabs(a - b) < 1e-9;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#define CHECK(expr) check((expr), #expr, __LINE__)
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace tahgibu;
|
||||||
|
|
||||||
|
// --- Límite de FPS -----------------------------------------------------
|
||||||
|
CHECK(Frame::clampFps(120.0f) == kMaxFps);
|
||||||
|
CHECK(Frame::clampFps(60.0f) == 60.0f);
|
||||||
|
CHECK(Frame::clampFps(1.0f) == 1.0f);
|
||||||
|
CHECK(Frame::clampFps(0.0f) == kMinFps);
|
||||||
|
CHECK(Frame::clampFps(-5.0f) == kMinFps);
|
||||||
|
CHECK(Frame::clampFps(std::numeric_limits<float>::quiet_NaN()) == kDefaultFps);
|
||||||
|
CHECK(Frame::clampFps(std::numeric_limits<float>::infinity()) == kDefaultFps);
|
||||||
|
|
||||||
|
// --- Frame ---------------------------------------------------------------
|
||||||
|
Frame frame;
|
||||||
|
CHECK(!frame.hasFpsOverride());
|
||||||
|
CHECK(near(frame.durationSeconds(24.0f), 1.0 / 24.0));
|
||||||
|
|
||||||
|
frame.setFpsOverride(1.0f);
|
||||||
|
CHECK(frame.hasFpsOverride());
|
||||||
|
CHECK(near(frame.durationSeconds(60.0f), 1.0));
|
||||||
|
|
||||||
|
frame.setFpsOverride(1000.0f); // nunca por encima de 60
|
||||||
|
CHECK(frame.effectiveFps(24.0f) == kMaxFps);
|
||||||
|
|
||||||
|
frame.clearFpsOverride();
|
||||||
|
CHECK(!frame.hasFpsOverride());
|
||||||
|
CHECK(frame.effectiveFps(30.0f) == 30.0f);
|
||||||
|
CHECK(frame.effectiveFps(500.0f) == kMaxFps); // la base también se limita
|
||||||
|
|
||||||
|
// --- Project -------------------------------------------------------------
|
||||||
|
Project project;
|
||||||
|
CHECK(project.frameCount() == 1);
|
||||||
|
CHECK(project.baseFps() == kDefaultFps);
|
||||||
|
|
||||||
|
project.setBaseFps(999.0f);
|
||||||
|
CHECK(project.baseFps() == kMaxFps);
|
||||||
|
project.setBaseFps(std::numeric_limits<float>::quiet_NaN());
|
||||||
|
CHECK(project.baseFps() == kDefaultFps);
|
||||||
|
|
||||||
|
// Ejemplo de la especificación: A(60) B(60) C(1) D(60)
|
||||||
|
Project example;
|
||||||
|
example.setBaseFps(60.0f);
|
||||||
|
example.addFrame(); // B
|
||||||
|
const std::size_t c = example.addFrame(); // C
|
||||||
|
example.addFrame(); // D
|
||||||
|
example.frame(c).setFpsOverride(1.0f);
|
||||||
|
|
||||||
|
CHECK(example.frameCount() == 4);
|
||||||
|
CHECK(near(example.frameDurationSeconds(0), 1.0 / 60.0));
|
||||||
|
CHECK(near(example.frameDurationSeconds(c), 1.0));
|
||||||
|
CHECK(near(example.totalDurationSeconds(), 3.0 / 60.0 + 1.0));
|
||||||
|
|
||||||
|
// Cambiar el FPS base afecta a los frames sin FPS propio, pero no al de C.
|
||||||
|
example.setBaseFps(30.0f);
|
||||||
|
CHECK(near(example.frameDurationSeconds(0), 1.0 / 30.0));
|
||||||
|
CHECK(near(example.frameDurationSeconds(c), 1.0));
|
||||||
|
|
||||||
|
bool threw = false;
|
||||||
|
try {
|
||||||
|
example.frame(99);
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
CHECK(threw);
|
||||||
|
|
||||||
|
if (g_failures == 0) {
|
||||||
|
std::puts("OK: todas las pruebas del núcleo pasaron.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "%d prueba(s) fallaron.\n", g_failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user