commit 66bedbb5f1d4e1a89c33b7a0a07995aaf32a15c0 Author: TEC-GO Date: Tue Sep 29 23:35:03 2026 -0300 Preparando la interfaz diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..902f3c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.cache/ +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..253ab07 --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e3012d --- /dev/null +++ b/README.md @@ -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 diff --git a/source/core/Constants.hpp b/source/core/Constants.hpp new file mode 100644 index 0000000..6b92d71 --- /dev/null +++ b/source/core/Constants.hpp @@ -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 diff --git a/source/core/Frame.cpp b/source/core/Frame.cpp new file mode 100644 index 0000000..ef70d69 --- /dev/null +++ b/source/core/Frame.cpp @@ -0,0 +1,28 @@ +#include "core/Frame.hpp" + +#include +#include + +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(effectiveFps(baseFps)); +} + +} // namespace tahgibu diff --git a/source/core/Frame.hpp b/source/core/Frame.hpp new file mode 100644 index 0000000..e17db9a --- /dev/null +++ b/source/core/Frame.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#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 fpsOverride_; +}; + +} // namespace tahgibu diff --git a/source/core/Project.cpp b/source/core/Project.cpp new file mode 100644 index 0000000..cf15ec8 --- /dev/null +++ b/source/core/Project.cpp @@ -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 diff --git a/source/core/Project.hpp b/source/core/Project.hpp new file mode 100644 index 0000000..75fce26 --- /dev/null +++ b/source/core/Project.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#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 frames_; +}; + +} // namespace tahgibu diff --git a/source/editor/EditorState.hpp b/source/editor/EditorState.hpp new file mode 100644 index 0000000..ded11de --- /dev/null +++ b/source/editor/EditorState.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#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 diff --git a/source/editor/Tool.hpp b/source/editor/Tool.hpp new file mode 100644 index 0000000..20e3779 --- /dev/null +++ b/source/editor/Tool.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +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 diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..3b4bbd9 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,14 @@ +#include +#include + +#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; + } +} diff --git a/source/ui/Application.cpp b/source/ui/Application.cpp new file mode 100644 index 0000000..ac0ccff --- /dev/null +++ b/source/ui/Application.cpp @@ -0,0 +1,242 @@ +#include "ui/Application.hpp" + +#include + +#include +#include +#include +#include +#include + +#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 kMenus = { + "Archivo", "Editar", "Animación", "Sprite", "Audio", "Ver", "Lua"}; + +int px(float value) { + return static_cast(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(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 computeToolButtons(const Rectangle& bar) { + std::array rects{}; + float x = bar.x + 8.0f; + for (std::size_t i = 0; i < kToolCount; ++i) { + const float w = static_cast(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 computeMenuRects(const Rectangle& bar) { + std::array rects{}; + float x = bar.x + 6.0f; + for (std::size_t i = 0; i < kMenus.size(); ++i) { + const float w = static_cast(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(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(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(project_.width()); + const float projectH = static_cast(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(project_.baseFps()), + static_cast(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(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 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(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 diff --git a/source/ui/Application.hpp b/source/ui/Application.hpp new file mode 100644 index 0000000..c29de5d --- /dev/null +++ b/source/ui/Application.hpp @@ -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 diff --git a/source/ui/Layout.cpp b/source/ui/Layout.cpp new file mode 100644 index 0000000..e674768 --- /dev/null +++ b/source/ui/Layout.cpp @@ -0,0 +1,35 @@ +#include "ui/Layout.hpp" + +#include + +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(windowWidth); + const float h = static_cast(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 diff --git a/source/ui/Layout.hpp b/source/ui/Layout.hpp new file mode 100644 index 0000000..fc62dc2 --- /dev/null +++ b/source/ui/Layout.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +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 diff --git a/source/ui/Theme.hpp b/source/ui/Theme.hpp new file mode 100644 index 0000000..8fe6b91 --- /dev/null +++ b/source/ui/Theme.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +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 diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp new file mode 100644 index 0000000..8f1fe07 --- /dev/null +++ b/tests/core_tests.cpp @@ -0,0 +1,101 @@ +// Pruebas del núcleo. No usan raylib. +#include +#include +#include +#include + +#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::quiet_NaN()) == kDefaultFps); + CHECK(Frame::clampFps(std::numeric_limits::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::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; +}