diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..b50f575c7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,79 @@ +--- +Language: Cpp +BasedOnStyle: Google +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: false +AlignOperands: true +AlignTrailingComments: true +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: None +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: BeforeColon +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 120 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ContinuationIndentWidth: 4 +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^<.*' + Priority: 1 + - Regex: '^".*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentAccessModifiers: True +IndentCaseBlocks: false +IndentCaseLabels: true +IndentGotoLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertNewlineAtEOF: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +... diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f327509b..8e22e7e9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,16 +1,28 @@ name: Build, test and Package -on: [push] +on: + push: + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Enable remote SSH connection. Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false jobs: build: name: Build, test and Package # env and permissions are setup to add dependencies from vcpkg to the repo's dependency graph env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VCPKG_FEATURE_FLAGS: dependencygraph + VCPKG_EXE: ${{ github.workspace }}/vcpkg/vcpkg + FEED_URL: https://nuget.pkg.github.com/NexoEngine/index.json + VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/NexoEngine/index.json,readwrite" + DOTNET_INSTALL_DIR: "./.dotnet" permissions: contents: write + packages: write strategy: fail-fast: false matrix: @@ -37,11 +49,19 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 # Fetch all history for all tags and branches (for SonarCloud) + # DEBUGGING ONLY, to run this trigger with even + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + with: + limit-access-to-actor: true # Only the person who triggered the workflow can access + detached: true # Run in the background and wait for connection + - name: Add Ubuntu toolchain repository if: ${{ matrix.os == 'ubuntu-latest' && matrix.compiler == 'gcc'}} run: | @@ -90,10 +110,15 @@ jobs: libxext-dev libxi-dev libgl1-mesa-dev libxinerama-dev \ libxcursor-dev '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev \ libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev \ - libegl1-mesa-dev + libegl1-mesa-dev mono-complete dotnet-sdk-9.0 # Pre-install .NET SDK 9.0 for caching version: 1.0 execute_install_scripts: true + - name: Install .NET SDK 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + - name: Init submodules run: | git submodule update --init --recursive @@ -110,6 +135,40 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + - name: Add NuGet sources + if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh + env: + USERNAME: NexoEngine + run: | + .$(${{ env.VCPKG_EXE }} fetch nuget) ` + sources add ` + -Source "${{ env.FEED_URL }}" ` + -StorePasswordInClearText ` + -Name GitHubPackages ` + -UserName "${{ env.USERNAME }}" ` + -Password "${{ secrets.GITHUB_TOKEN }}" + .$(${{ env.VCPKG_EXE }} fetch nuget) ` + setapikey "${{ secrets.GITHUB_TOKEN }}" ` + -Source "${{ env.FEED_URL }}" + + - name: Add NuGet sources + if: ${{ matrix.os != 'windows-latest' }} + shell: bash + env: + USERNAME: NexoEngine + run: | + mono `${{ env.VCPKG_EXE }} fetch nuget | tail -n 1` \ + sources add \ + -Source "${{ env.FEED_URL }}" \ + -StorePasswordInClearText \ + -Name GitHubPackages \ + -UserName "${{ env.USERNAME }}" \ + -Password "${{ secrets.GITHUB_TOKEN }}" + mono `${{ env.VCPKG_EXE }} fetch nuget | tail -n 1` \ + setapikey "${{ secrets.GITHUB_TOKEN }}" \ + -Source "${{ env.FEED_URL }}" + - name: CMake Workflow with preset 'build-coverage' for tests uses: lukka/run-cmake@v10 with: @@ -120,6 +179,7 @@ jobs: CXX: ${{ matrix.compiler == 'clang' && steps.set-up-clang.outputs.clangxx || matrix.compiler == 'gcc' && steps.set-up-gcc.outputs.gxx || '' }} CMAKE_C_COMPILER: ${{ matrix.compiler == 'clang' && steps.set-up-clang.outputs.clang || matrix.compiler == 'gcc' && steps.set-up-gcc.outputs.gcc || '' }} CMAKE_CXX_COMPILER: ${{ matrix.compiler == 'clang' && steps.set-up-clang.outputs.clangxx || matrix.compiler == 'gcc' && steps.set-up-gcc.outputs.gxx || '' }} + USERNAME: NexoEngine - name: Install Mesa for Windows shell: cmd @@ -204,6 +264,8 @@ jobs: test-nsis-installer: name: Test NSIS installer runs-on: windows-latest + env: + DOTNET_INSTALL_DIR: "./.dotnet" needs: build steps: - name: Download NSIS installer @@ -212,6 +274,11 @@ jobs: with: pattern: 'nexo-engine-installer-msvc14-windows-latest' + - name: Install .NET SDK 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + - name: Run NSIS installer shell: pwsh run: | @@ -269,6 +336,8 @@ jobs: test-deb-installer: name: Test DEB installer runs-on: ubuntu-latest + env: + DOTNET_INSTALL_DIR: "./.dotnet" needs: build steps: - name: Download DEB installer @@ -277,6 +346,11 @@ jobs: with: pattern: 'nexo-engine-installer-gcc13-ubuntu-latest' + - name: Install .NET SDK 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + - name: Install DEB package shell: bash run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9c838d8b6..ac37b38d1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,6 +5,11 @@ on: [push] jobs: analyze: name: Analyze (${{ matrix.language }}/${{ matrix.compiler }}) on ${{ matrix.os }} + env: + VCPKG_EXE: ${{ github.workspace }}/vcpkg/vcpkg + FEED_URL: https://nuget.pkg.github.com/NexoEngine/index.json + VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/NexoEngine/index.json,readwrite" + DOTNET_INSTALL_DIR: "./.dotnet" runs-on: ${{ matrix.os }} permissions: # required for all workflows @@ -57,10 +62,15 @@ jobs: libxext-dev libxi-dev libgl1-mesa-dev libxinerama-dev \ libxcursor-dev '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev \ libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev \ - libegl1-mesa-dev + libegl1-mesa-dev mono-complete dotnet-sdk-9.0 # Pre-install .NET SDK 9.0 for caching version: 1.0 execute_install_scripts: true + - name: Install .NET SDK 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + - name: Set up GCC if: ${{ matrix.compiler == 'gcc' }} id: set-up-gcc @@ -82,9 +92,27 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + - name: Add NuGet sources + shell: bash + env: + USERNAME: NexoEngine + run: | + mono `${{ env.VCPKG_EXE }} fetch nuget | tail -n 1` \ + sources add \ + -Source "${{ env.FEED_URL }}" \ + -StorePasswordInClearText \ + -Name GitHubPackages \ + -UserName "${{ env.USERNAME }}" \ + -Password "${{ secrets.GITHUB_TOKEN }}" + mono `${{ env.VCPKG_EXE }} fetch nuget | tail -n 1` \ + setapikey "${{ secrets.GITHUB_TOKEN }}" \ + -Source "${{ env.FEED_URL }}" + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 + env: + USERNAME: NexoEngine with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index c420a3352..b14ee3f30 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -12,6 +12,6 @@ jobs: steps: - uses: actions/checkout@v2 - run: git submodule add https://github.com/jothepro/doxygen-awesome-css.git && cd doxygen-awesome-css && git checkout v2.2.0 - - uses: langroodi/doxygenize@v1.6.1 + - uses: langroodi/doxygenize@v1.7.0 with: htmloutput: './html/' diff --git a/.gitignore b/.gitignore index 400e3d902..f8b9d9755 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Prevent pushing window layouts ./config/default-layout.ini +# Do not push auto generated COPYRIGHT +COPYRIGHT_generated + # Prerequisites *.d diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml deleted file mode 100644 index 8ab94a851..000000000 --- a/.idea/sonarlint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e95a8f99..e808cec2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,12 +5,14 @@ project(client CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(NEXO_COVERAGE OFF CACHE BOOL "Enable coverage for binaries") set(NEXO_GIT_SUBMODULE OFF CACHE BOOL "Enable git submodules init and update") set(NEXO_BOOTSTRAP_VCPKG OFF CACHE BOOL "Enable vcpkg bootstrap") set(NEXO_BUILD_TESTS ON CACHE BOOL "Enable tests") set(NEXO_BUILD_EXAMPLES OFF CACHE BOOL "Enable examples") +set(NEXO_BUILD_SCRIPTING ON CACHE BOOL "Enable C# scripting support") set(NEXO_GRAPHICS_API "OpenGL" CACHE STRING "Graphics API to use") if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") @@ -19,7 +21,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set(NEXO_COMPILER_FLAGS_RELEASE -O3) set(NEXO_COVERAGE_FLAGS -O0 --coverage) elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - set(NEXO_COMPILER_FLAGS_ALL /std:c++${CMAKE_CXX_STANDARD}) + set(NEXO_COMPILER_FLAGS_ALL /std:c++${CMAKE_CXX_STANDARD} /Zc:preprocessor) set(NEXO_COMPILER_FLAGS_DEBUG /Zi /Od /Zc:preprocessor) set(NEXO_COMPILER_FLAGS_RELEASE /O2 /Zc:preprocessor) set(NEXO_COVERAGE_FLAGS "") # MSVC doesn't support coverage in the same way @@ -95,6 +97,11 @@ message(STATUS "VCPKG done.") include("${CMAKE_CURRENT_SOURCE_DIR}/editor/CMakeLists.txt") # SETUP ENGINE include("${CMAKE_CURRENT_SOURCE_DIR}/engine/CMakeLists.txt") +# SETUP MANAGED CSHARP LIB +if(NEXO_BUILD_SCRIPTING) + include("${CMAKE_CURRENT_SOURCE_DIR}/engine/src/scripting/managed/CMakeLists.txt") + add_dependencies(nexoEditor nexoManaged) +endif() # SETUP EXAMPLE include("${CMAKE_CURRENT_SOURCE_DIR}/examples/CMakeLists.txt") # SETUP TESTS diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 000000000..56b92264a --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,217 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Comment: + NEXO Engine Exhaustive list of all licenses used in the project + --------------------------------------------------------------- + This file is a template that is processed by CMake to generate the + final copyright file. See scripts/copyright.cmake for more information. + When generated some information might be missing and should be + verified and updated manually. + . + This file lists copyright holders and licenses for the NEXO project, its + source code, dependencies and libraries. + . + The format is based on the Debian copyright format 1.0, which is a + human and machine readable format for copyright information. + For more information, see: + https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Upstream-Name: NEXO Engine +Upstream-Contact: NEXO Engine Team +Source: https://github.com/NexoEngine/game-engine + +Files: * +Comment: NEXO Engine +Copyright: 2025 NEXO Engine contributors +License: MIT + +Files: + resources/nexo.ico + resources/nexo.png + resources/nexo.rc + resources/nexo_header.bmp +Comment: NEXO Engine Logo +Copyright: 2025 NEXO Engine contributors +License: CC-BY-SA-4.0 + +Files: * +Copyright: 2006-2021, assimp team +License: BSD-3-clause +Comment: + assimp full license in /external/licenses/assimp + +Files: + engine/* + editor/* + common/* +Copyright: Boost Software License - Version 1.0 - August 17th, 2003 +License: BSL-1.0 +Comment: + boost full license in /external/licenses/boost + +Files: * +Copyright: Google Inc. and other contributors +License: Apache-2.0 +Comment: + draco full license in /external/licenses/draco + +Files: * +Copyright: 2008-2018 The Khronos Group Inc. +License: Apache-2.0 +Comment: + egl-registry full license in /external/licenses/egl-registry + +Files: + engine/* +Copyright: 2013-2021 David Herberth +License: MIT +Comment: + glad full license in /external/licenses/glad + +Files: + engine/* +Copyright: 2002-2006 Marcus Geelnard + 2006-2019 Camilla Löwy +License: zlib/libpng +Comment: + glfw3 full license in /external/licenses/glfw3 + +Files: + engine/* + editor/* + common/* +Copyright: 2005 - G-Truc Creation +License: MIT +Comment: + glm full license in /external/licenses/glm + +Files: + tests/* +Copyright: 2008, Google Inc. +License: BSD-3-clause +Comment: + gtest full license in /external/licenses/gtest + +Files: + editor/* +Copyright: 2014-2025 Omar Cornut +License: MIT +Comment: + imgui full license in /external/licenses/imgui + +Files: + editor/* +Copyright: 2016 Cedric Guillemet +License: MIT +Comment: + imguizmo full license in /external/licenses/imguizmo + +Files: * +Copyright: 2009-2018, Poly2Tri Contributors +License: BSD-3-clause +Comment: + jhasse-poly2tri full license in /external/licenses/jhasse-poly2tri + +Files: + engine/* +Copyright: 2021 Jorrit Rouwe +License: MIT +Comment: + joltphysics full license in /external/licenses/joltphysics + +Files: * +Copyright: Kuba Podgórski +License: public-domain +Comment: + kubazip full license in /external/licenses/kubazip + +Files: + engine/* + editor/* + common/* +Copyright: Emil Ernerfeldt +License: public-domain +Comment: + loguru full license in /external/licenses/loguru + +Files: * +Copyright: 1998-2010 - by Gilles Vollant - version 1.1 64 bits from Mathias Svensson +License: zlib +Comment: + minizip full license in /external/licenses/minizip + +Files: + engine/* + editor/* + common/* +Copyright: 2013-2022 Niels Lohmann +License: MIT +Comment: + nlohmann-json full license in /external/licenses/nlohmann-json + +Files: * +Copyright: 2008-2018 The Khronos Group Inc. +License: MIT +Comment: + opengl-registry full license in /external/licenses/opengl-registry + +Files: * +Copyright: Angus Johnson and other contributors +License: BSL-1.0 +Comment: + polyclipping full license in /external/licenses/polyclipping + +Files: * +Copyright: 2006-2023 Arseny Kapoulkine +License: MIT +Comment: + pugixml full license in /external/licenses/pugixml + +Files: * +Copyright: 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. +License: MIT +Comment: + rapidjson full license in /external/licenses/rapidjson + +Files: + engine/* +Copyright: 2017 Sean Barrett +License: MIT or public-domain +Comment: + stb full license in /external/licenses/stb + +Files: + editor/* +Copyright: Pascal Costanza +License: zlib/libpng +Comment: + tinyfiledialogs full license in /external/licenses/tinyfiledialogs + +Files: * +Copyright: Nemanja Trifunovic +License: BSL-1.0 +Comment: + utfcpp full license in /external/licenses/utfcpp + +Files: vcpkg +Copyright: Microsoft Corporation +License: MIT +Comment: + vcpkg-cmake full license in /external/licenses/vcpkg-cmake + +Files: vcpkg +Copyright: Microsoft Corporation +License: MIT +Comment: + vcpkg-cmake-config full license in /external/licenses/vcpkg-cmake-config + +Files: vcpkg +Copyright: Microsoft Corporation +License: MIT +Comment: + vcpkg-cmake-get-vars full license in /external/licenses/vcpkg-cmake-get-vars + +Files: * +Copyright: 1995-2022 Jean-loup Gailly and Mark Adler +License: zlib +Comment: + zlib full license in /external/licenses/zlib diff --git a/README.md b/README.md index 7ce283086..1dc07dded 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ Welcome to the NEXO Engine repository! This project is a collaborative effort to - [Create an installer for Linux (DEB)](#create-an-installer-for-linux-deb) - [Run the tests](#run-the-tests) - [The Team](#the-team) + - [Acknowledgements](#acknowledgements) + - [License](#license) + - [How to extract the third-party licenses file](#how-to-extract-the-third-party-licenses-file) + - [How to generate the COPYRIGHT file](#how-to-generate-the-copyright-file) > [!NOTE] > Find the whole documentation on our [website](https://nexoengine.github.io/game-engine/). @@ -53,6 +57,7 @@ https://github.com/user-attachments/assets/f675cdc0-3a53-4fb8-8544-a22dc7a332f4 To run this project, ensure you have the following: - **CMake**: Necessary for building the project from source. - **C++ Compiler**: We recommend using GCC or Clang for Linux and MacOS, and MSVC for Windows. +- **.NET SDK 9.0**: Required for the C# scripting support. ## Build the project @@ -175,3 +180,36 @@ NEXO Engine is brought to life by a dedicated team of fourth-year students from This project is part of our curriculum and end of studies project, showcasing our collective skills in advanced software development with modern C++. We thank Epitech for the opportunity to work on such an engaging project and for the support throughout our educational journey. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+For more information about the copyright of the project, please refer to the [COPYRIGHT](COPYRIGHT) file.
+You can also find the license of the third-party libraries used in the project in the [external/licenses](external/licenses) directory. + +> [!TIP] +> For any license inquiry, please contact us at [nexo.engine@gmail.com](mailto:nexo.engine@gmail.com?subject=[NEXO%20Engine]%20License) + +### How to extract the third-party licenses file + +You can use the cmake install command: +```bash +cmake --install build --prefix /path/to/install --component generate-licenses +``` + +This will extract all licenses per third-party library in the `/path/to/install/external/licenses` directory. + +> [!NOTE] +> These licenses are automatically extracted from vcpkg, there might be missing third-party libraries. + +### How to generate the COPYRIGHT file + +You can use the cmake install command: +```bash +cmake --install build --prefix /path/to/install --component generate-copyright +``` + +This will generate the COPYRIGHT file in the `/path/to/install` directory. + +> [!WARNING] +> By default the COPYRIGHT file is generated with some `TODO:`, the generator cannot always determine exact licenses for some files. Please check each entry for errors. diff --git a/common/Logger.hpp b/common/Logger.hpp index 48f414d0f..a8afd4825 100644 --- a/common/Logger.hpp +++ b/common/Logger.hpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include namespace nexo { @@ -36,13 +38,14 @@ namespace nexo { } } - enum class LogLevel { + enum class LogLevel : uint32_t { FATAL, - ERROR, + ERR, WARN, INFO, DEBUG, - DEV + DEV, + USER }; inline std::string toString(const LogLevel level) @@ -50,9 +53,10 @@ namespace nexo { switch (level) { case LogLevel::FATAL: return "FATAL"; - case LogLevel::ERROR: return "ERROR"; + case LogLevel::ERR: return "ERROR"; case LogLevel::WARN: return "WARN"; case LogLevel::INFO: return "INFO"; + case LogLevel::USER: return "USER"; case LogLevel::DEBUG: return "DEBUG"; case LogLevel::DEV: return "DEV"; } @@ -67,17 +71,73 @@ namespace nexo { return path; } - inline void defaultCallback(const LogLevel level, const std::string &message) + inline void defaultCallback(const LogLevel level, const std::source_location& loc, const std::string &message) { - if (level == LogLevel::FATAL || level == LogLevel::ERROR) - std::cerr << "[" << toString(level) << "] " << message << std::endl; - else - std::cout << "[" << toString(level) << "] " << message << std::endl; + std::ostream& outputStream = level == LogLevel::FATAL || level == LogLevel::ERR + ? std::cerr + : std::cout; + + outputStream << "[" << toString(level) << "] " << getFileName(loc.file_name()) << ":" << loc.line() << " - " << message << std::endl; } + /** + * @brief Registry to track which log messages have been emitted + * + * This class is used to ensure certain messages are only logged once + * until they are explicitly reset. + */ + class OnceRegistry { + public: + /** + * @brief Get the singleton instance of the registry + * + * @return OnceRegistry& Reference to the registry + */ + static OnceRegistry& instance() { + static OnceRegistry registry; + return registry; + } + + /** + * @brief Check if a message has been logged and mark it as logged + * + * @param key The unique identifier for the message + * @return true If this is the first time seeing this message + * @return false If this message has been logged before + */ + bool shouldLog(const std::string& key) { + std::scoped_lock lock(m_mutex); + return m_loggedKeys.insert(key).second; + } + + /** + * @brief Reset a specific message so it can be logged again + * + * @param key The unique identifier for the message to reset + */ + void reset(const std::string& key) { + std::scoped_lock lock(m_mutex); + m_loggedKeys.erase(key); + } + + /** + * @brief Reset all messages so they can be logged again + */ + void resetAll() { + std::scoped_lock lock(m_mutex); + m_loggedKeys.clear(); + } + + private: + OnceRegistry() = default; + + std::unordered_set m_loggedKeys; + std::mutex m_mutex; + }; + class Logger { public: - static void setCallback(std::function callback) + static void setCallback(std::function callback) { logCallback = std::move(callback); } @@ -93,24 +153,69 @@ namespace nexo { }, transformed); - if (level == LogLevel::INFO) - logString(level, message); - else - { - std::stringstream ss; - ss << getFileName(loc.file_name()) << ":" << loc.line() << " - " << message; - logString(level, ss.str()); - } + logCallback(level, loc, message); } - private: - static void logString(const LogLevel level, const std::string &message) + + /** + * @brief Generate a key incorporating format string and parameter values + * + * @tparam Args Variadic template for format arguments + * @param fmt Format string + * @param args Format arguments + * @return std::string Key incorporating the parameters + */ + template + static std::string generateKey(const std::string_view fmt, const std::string& location, Args&&... args) + { + std::stringstream ss; + ss << fmt << "@" << location << "|"; + + // Add parameter values to the key + ((ss << toFormatFriendly(args) << "|"), ...); + + return ss.str(); + } + + /** + * @brief Log a message only once until reset + * + * This method logs a message only the first time it is called with a given key. + * The key incorporates both the format string and the parameter values. + * + * @tparam Args Variadic template for format arguments + * @param level Log level + * @param loc Source location of the log call + * @param fmt Format string + * @param key Key including format string and parameter values + * @param args Format arguments + */ + template + static void logOnce(const LogLevel level, const std::source_location loc, + const std::string_view fmt, const std::string& key, Args &&... args) { - if (logCallback) - logCallback(level, message); - else - defaultCallback(level, message); + if (OnceRegistry::instance().shouldLog(key)) { + logWithFormat(level, loc, fmt, std::forward(args)...); + } } - static inline std::function logCallback = nullptr; + + /** + * @brief Reset a specific log message so it can be logged again with logOnce + * + * @param key The unique identifier for the log message to reset + */ + static void resetOnce(const std::string& key) { + OnceRegistry::instance().reset(key); + } + + /** + * @brief Reset all log messages so they can be logged again with logOnce + */ + static void resetAllOnce() { + OnceRegistry::instance().resetAll(); + } + + private: + static inline std::function logCallback = defaultCallback; }; } @@ -121,8 +226,28 @@ namespace nexo { #define LOG_EXCEPTION(exception) \ LOG(NEXO_ERROR, "{}:{} - Exception: {}", exception.getFile(), exception.getLine(), exception.getMessage()) +/** + * @brief Generate a unique key for a log message incorporating format and parameters + * + * This creates a key that includes both the format string and the parameter values, + * allowing specific message instances to be reset later. + */ +#define NEXO_LOG_ONCE_KEY(fmt, ...) \ + nexo::Logger::generateKey(fmt, std::string(__FILE__) + ":" + std::to_string(__LINE__), ##__VA_ARGS__) + +/** + * @brief Log a message only once until it's reset + * + * This ensures the message is only logged the first time this line is executed + * with these specific parameters. Subsequent calls with the same parameters + * will be ignored until the message is reset. + */ +#define LOG_ONCE(level, fmt, ...) \ + nexo::Logger::logOnce(level, std::source_location::current(), fmt, \ + NEXO_LOG_ONCE_KEY(fmt, ##__VA_ARGS__), ##__VA_ARGS__) + #define NEXO_FATAL nexo::LogLevel::FATAL -#define NEXO_ERROR nexo::LogLevel::ERROR +#define NEXO_ERROR nexo::LogLevel::ERR #define NEXO_WARN nexo::LogLevel::WARN #define NEXO_INFO nexo::LogLevel::INFO #define NEXO_DEBUG nexo::LogLevel::DEBUG diff --git a/common/String.hpp b/common/String.hpp new file mode 100644 index 000000000..810010dab --- /dev/null +++ b/common/String.hpp @@ -0,0 +1,38 @@ +//// String.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 23/11/2024 +// Description: Utils for strings +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace nexo { + + /** + * @brief Compare two strings case-insensitively. + * + * @param a The first string. + * @param b The second string. + * @return true if the strings are equal (case-insensitive), false otherwise. + */ + [[nodiscard]] constexpr bool iequals(const std::string_view& a, const std::string_view& b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](const char _a, const char _b) { + return std::tolower(static_cast(_a)) == + std::tolower(static_cast(_b)); + }); + } + + +} // namespace nexo diff --git a/common/Timestep.hpp b/common/Timestep.hpp index f1f61eb03..4349d1e46 100644 --- a/common/Timestep.hpp +++ b/common/Timestep.hpp @@ -16,14 +16,15 @@ namespace nexo { class Timestep { public: - Timestep(const float time = 0.0f) : m_time(time) {}; + explicit(false) Timestep(const double time = 0.0f) : m_time(time) {} - operator float() const {return m_time; }; + explicit operator float() const { return m_time; } + explicit operator double() const { return m_time; } - [[nodiscard]] float getSeconds() const {return m_time; }; - [[nodiscard]] float getMilliseconds() const { return m_time * 1000.0f; }; + [[nodiscard]] double getSeconds() const {return m_time; } + [[nodiscard]] double getMilliseconds() const { return m_time * 1000.0f; } private: - float m_time = 0.0f; + double m_time = 0.0f; }; } diff --git a/common/math/Projection.cpp b/common/math/Projection.cpp new file mode 100644 index 000000000..0f4cbf4f0 --- /dev/null +++ b/common/math/Projection.cpp @@ -0,0 +1,39 @@ +//// Projection.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 20/04/2025 +// Description: Source file for the math projection utils +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Projection.hpp" + +namespace nexo::math { + + glm::vec3 projectRayToWorld(const float x, const float y, + const glm::mat4 &viewProjectionMatrix, + const glm::vec3 &cameraPosition, + const unsigned int width, const unsigned int height + ) { + // Convert to NDC + const float ndcX = (2.0f * x) / static_cast(width) - 1.0f; + const float ndcY = 1.0f - (2.0f * y) / static_cast(height); + + const glm::mat4 inverseViewProj = glm::inverse(viewProjectionMatrix); + + // Points in NDC space at near and far planes + glm::vec4 nearPoint = inverseViewProj * glm::vec4(ndcX, ndcY, -1.0f, 1.0f); + + nearPoint /= nearPoint.w; + + const glm::vec3 rayDir = glm::normalize(glm::vec3(nearPoint) - cameraPosition); + + return rayDir; + } +} diff --git a/common/math/Projection.hpp b/common/math/Projection.hpp new file mode 100644 index 000000000..ec4b0056d --- /dev/null +++ b/common/math/Projection.hpp @@ -0,0 +1,24 @@ +//// Projection.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 20/04/2025 +// Description: Header file for the math projection utils +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include + +namespace nexo::math { + glm::vec3 projectRayToWorld(float x, float y, + const glm::mat4 &viewProjectionMatrix, + const glm::vec3 &cameraPosition, + unsigned int width, unsigned int height + ); +} diff --git a/common/math/Vector.cpp b/common/math/Vector.cpp index c191a0d63..999a2e374 100644 --- a/common/math/Vector.cpp +++ b/common/math/Vector.cpp @@ -46,4 +46,9 @@ namespace nexo::math { return glm::degrees(euler); } + + bool isPosInBounds(const glm::vec2 pos, const glm::vec2 &min, const glm::vec2 &max) + { + return pos.x >= min.x && pos.x <= max.x && pos.y >= min.y && pos.y <= max.y; + } } diff --git a/common/math/Vector.hpp b/common/math/Vector.hpp index d856d377b..64bd05f02 100644 --- a/common/math/Vector.hpp +++ b/common/math/Vector.hpp @@ -44,4 +44,6 @@ namespace nexo::math { * when sinp approaches ±1. */ glm::vec3 customQuatToEuler(const glm::quat &q); + + bool isPosInBounds(const glm::vec2 pos, const glm::vec2 &min, const glm::vec2 &max); } diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index d7ebae4a5..a614bb773 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -15,21 +15,66 @@ set(SRCS editor/src/backends/ImGuiBackend.cpp editor/src/backends/opengl/openglImGuiBackend.cpp editor/src/context/Selector.cpp - editor/src/Components/Components.cpp - editor/src/Components/EntityPropertiesComponents.cpp - editor/src/Components/Widgets.cpp + editor/src/context/ActionManager.cpp + editor/src/context/ActionHistory.cpp + editor/src/context/ActionGroup.cpp + editor/src/context/actions/EntityActions.cpp + editor/src/context/actions/ComponentRestoreFactory.cpp + editor/src/ImNexo/EntityProperties.cpp + editor/src/ImNexo/Components.cpp + editor/src/ImNexo/Elements.cpp + editor/src/ImNexo/Panels.cpp + editor/src/ImNexo/Utils.cpp + editor/src/ImNexo/Widgets.cpp + editor/src/ImNexo/ImNexo.cpp editor/src/utils/ScenePreview.cpp editor/src/utils/Config.cpp editor/src/utils/String.cpp + editor/src/utils/FileSystem.cpp + editor/src/utils/EditorProps.cpp + editor/src/inputs/Command.cpp + editor/src/inputs/InputManager.cpp + editor/src/inputs/WindowState.cpp editor/src/Editor.cpp editor/src/WindowRegistry.cpp editor/src/DockingRegistry.cpp - editor/src/DocumentWindows/ConsoleWindow.cpp - editor/src/DocumentWindows/EditorScene.cpp - editor/src/DocumentWindows/SceneTreeWindow.cpp + editor/src/ADocumentWindow.cpp + editor/src/DocumentWindows/EditorScene/Gizmo.cpp + editor/src/DocumentWindows/EditorScene/Init.cpp + editor/src/DocumentWindows/EditorScene/Shortcuts.cpp + editor/src/DocumentWindows/EditorScene/Show.cpp + editor/src/DocumentWindows/EditorScene/Shutdown.cpp + editor/src/DocumentWindows/EditorScene/Toolbar.cpp + editor/src/DocumentWindows/EditorScene/Update.cpp + editor/src/DocumentWindows/AssetManager/Init.cpp + editor/src/DocumentWindows/AssetManager/Show.cpp + editor/src/DocumentWindows/AssetManager/Shutdown.cpp + editor/src/DocumentWindows/AssetManager/Update.cpp + editor/src/DocumentWindows/ConsoleWindow/Init.cpp + editor/src/DocumentWindows/ConsoleWindow/Log.cpp + editor/src/DocumentWindows/ConsoleWindow/Show.cpp + editor/src/DocumentWindows/ConsoleWindow/Shutdown.cpp + editor/src/DocumentWindows/ConsoleWindow/Update.cpp + editor/src/DocumentWindows/ConsoleWindow/Utils.cpp + editor/src/DocumentWindows/InspectorWindow/Init.cpp + editor/src/DocumentWindows/InspectorWindow/Show.cpp + editor/src/DocumentWindows/InspectorWindow/Shutdown.cpp + editor/src/DocumentWindows/InspectorWindow/Update.cpp + editor/src/DocumentWindows/MaterialInspector/Init.cpp + editor/src/DocumentWindows/MaterialInspector/Show.cpp + editor/src/DocumentWindows/MaterialInspector/Shutdown.cpp + editor/src/DocumentWindows/MaterialInspector/Update.cpp + editor/src/DocumentWindows/SceneTreeWindow/Hovering.cpp + editor/src/DocumentWindows/SceneTreeWindow/Init.cpp + editor/src/DocumentWindows/SceneTreeWindow/NodeHandling.cpp + editor/src/DocumentWindows/SceneTreeWindow/Rename.cpp + editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp + editor/src/DocumentWindows/SceneTreeWindow/Selection.cpp + editor/src/DocumentWindows/SceneTreeWindow/Show.cpp + editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp + editor/src/DocumentWindows/SceneTreeWindow/Update.cpp + editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp editor/src/DocumentWindows/PopupManager.cpp - editor/src/DocumentWindows/InspectorWindow.cpp - editor/src/DocumentWindows/MaterialInspector.cpp editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.cpp @@ -38,7 +83,13 @@ set(SRCS editor/src/DocumentWindows/EntityProperties/SpotLightProperty.cpp editor/src/DocumentWindows/EntityProperties/CameraProperty.cpp editor/src/DocumentWindows/EntityProperties/CameraController.cpp - editor/src/DocumentWindows/AssetManagerWindow.cpp + editor/src/DocumentWindows/EntityProperties/CameraTarget.cpp + editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.cpp + editor/src/DocumentWindows/TestWindow/Init.cpp + editor/src/DocumentWindows/TestWindow/Parser.cpp + editor/src/DocumentWindows/TestWindow/Show.cpp + editor/src/DocumentWindows/TestWindow/Shutdown.cpp + editor/src/DocumentWindows/TestWindow/Update.cpp ) # Windows App Icon @@ -105,7 +156,7 @@ target_link_libraries(nexoEditor PRIVATE Boost::uuid) include_directories(include) if(NEXO_GRAPHICS_API STREQUAL "OpenGL") - target_compile_definitions(nexoEditor PRIVATE GRAPHICS_API_OPENGL) + target_compile_definitions(nexoEditor PRIVATE NX_GRAPHICS_API_OPENGL) endif() # Set the output directory for the executable (prevents generator from creating Debug/Release folders) diff --git a/editor/main.cpp b/editor/main.cpp index f4dfe00c0..dd5f6db66 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -13,49 +13,64 @@ /////////////////////////////////////////////////////////////////////////////// #include "src/Editor.hpp" -#include "src/DocumentWindows/ConsoleWindow.hpp" -#include "src/DocumentWindows/EditorScene.hpp" -#include "src/DocumentWindows/SceneTreeWindow.hpp" -#include "src/DocumentWindows/InspectorWindow.hpp" -#include "src/DocumentWindows/AssetManagerWindow.hpp" -#include "src/DocumentWindows/MaterialInspector.hpp" +#include "src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp" +#include "src/DocumentWindows/EditorScene/EditorScene.hpp" +#include "src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp" +#include "src/DocumentWindows/InspectorWindow/InspectorWindow.hpp" +#include "src/DocumentWindows/AssetManager/AssetManagerWindow.hpp" +#include "src/DocumentWindows/MaterialInspector/MaterialInspector.hpp" #include +#include #include +#include "Path.hpp" +#ifdef NEXO_SCRIPTING_ENABLED +#include "scripting/native/ManagedTypedef.hpp" +#include "scripting/native/Scripting.hpp" +#endif + int main(int argc, char **argv) -{ - try { - loguru::init(argc, argv); - loguru::g_stderr_verbosity = loguru::Verbosity_3; - nexo::editor::Editor &editor = nexo::editor::Editor::getInstance(); - - editor.registerWindow(NEXO_WND_USTRID_DEFAULT_SCENE); - editor.registerWindow(NEXO_WND_USTRID_SCENE_TREE); - editor.registerWindow(NEXO_WND_USTRID_INSPECTOR); - editor.registerWindow(NEXO_WND_USTRID_CONSOLE); - editor.registerWindow(NEXO_WND_USTRID_MATERIAL_INSPECTOR); - editor.registerWindow(NEXO_WND_USTRID_ASSET_MANAGER); - - if (auto defaultScene = editor.getWindow(NEXO_WND_USTRID_DEFAULT_SCENE).lock()) - defaultScene->setDefault(); - - editor.init(); - - while (editor.isOpen()) - { - auto start = std::chrono::high_resolution_clock::now(); - editor.render(); - editor.update(); - - auto end = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end - start; - std::this_thread::sleep_for(std::chrono::milliseconds(16) - elapsed); - } - editor.shutdown(); - return 0; - } catch (const nexo::Exception &e) { - LOG_EXCEPTION(e); - return 1; +try { + loguru::init(argc, argv); + loguru::g_stderr_verbosity = loguru::Verbosity_3; + nexo::editor::Editor &editor = nexo::editor::Editor::getInstance(); + + editor.registerWindow( + std::format("Default Scene{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, 0) + ); + editor.registerWindow(NEXO_WND_USTRID_SCENE_TREE); + editor.registerWindow(NEXO_WND_USTRID_INSPECTOR); + editor.registerWindow(NEXO_WND_USTRID_CONSOLE); + editor.registerWindow(NEXO_WND_USTRID_MATERIAL_INSPECTOR); + editor.registerWindow(NEXO_WND_USTRID_ASSET_MANAGER); + + if (const auto defaultScene = editor.getWindow(std::format("Default Scene{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, 0)).lock()) + defaultScene->setDefault(); + + editor.init(); + + while (editor.isOpen()) + { + auto start = std::chrono::high_resolution_clock::now(); + editor.render(); + editor.update(); + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end - start; + + std::this_thread::sleep_for(std::chrono::milliseconds(16) - elapsed); } + + editor.shutdown(); + return 0; +} catch (const nexo::Exception &e) { + LOG_EXCEPTION(e); + return 1; +} catch (const std::exception &e) { + LOG(NEXO_ERROR, "Unhandled exception: {}", e.what()); + return 1; +} catch (...) { + LOG(NEXO_ERROR, "Unhandled unknown exception"); + return 1; } diff --git a/editor/src/ADocumentWindow.cpp b/editor/src/ADocumentWindow.cpp new file mode 100644 index 000000000..009f07c66 --- /dev/null +++ b/editor/src/ADocumentWindow.cpp @@ -0,0 +1,80 @@ +//// ADocumentWindow.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 29/04/2025 +// Description: Source file for the abstract document window class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ADocumentWindow.hpp" +#include + +namespace nexo::editor { + + void ADocumentWindow::beginRender(const std::string &windowName) + { + dockingUpdate(windowName); + visibilityUpdate(); + sizeUpdate(); + } + + void ADocumentWindow::dockingUpdate(const std::string &windowName) + { + if (const ImGuiWindow* currentWindow = ImGui::GetCurrentWindow(); currentWindow) + { + const bool isDocked = currentWindow->DockIsActive; + const ImGuiID currentDockID = currentWindow->DockId; + auto dockId = m_windowRegistry.getDockId(windowName); + + // If it's the first time opening the window and we have a dock id saved in the registry, then we force set it + if (m_firstOpened && (dockId && currentDockID != *dockId)) + ImGui::DockBuilderDockWindow(windowName.c_str(), *dockId); + // If the docks ids differ, it means the window got rearranged in the global layout + // If we are docked but we dont have a dock id saved in the registry, it means the user moved the window + // In both cases, we update our docking registry with the new dock id + else if ((dockId && currentDockID != *dockId) || (isDocked && !dockId)) + m_windowRegistry.setDockId(windowName, currentDockID); + + + // If it is not docked anymore, we have a floating window without docking node, + // So we erase it from the docking registry + if (!m_firstOpened && !isDocked) + m_windowRegistry.resetDockId(windowName); + m_firstOpened = false; + } + } + + void ADocumentWindow::visibilityUpdate() + { + m_focused = ImGui::IsWindowFocused(); + const bool isDocked = ImGui::IsWindowDocked(); + const ImGuiWindow* window = ImGui::GetCurrentWindow(); + + if (isDocked) { + // If the window is currently being rendered with normal content, + // and not hidden or set to skip items, then it is visible + m_isVisibleInDock = !window->Hidden && !window->SkipItems && window->Active; + } + else { + // Not docked windows are visible if we've reached this point + m_isVisibleInDock = true; + } + m_hovered = ImGui::IsWindowHovered(); + } + + void ADocumentWindow::sizeUpdate() + { + const ImGuiWindow* window = ImGui::GetCurrentWindow(); + m_windowPos = window->Pos; + m_windowSize = window->Size; + m_contentSizeMin = ImGui::GetWindowContentRegionMin(); + m_contentSizeMax = ImGui::GetWindowContentRegionMax(); + m_contentSize = ImGui::GetContentRegionAvail(); + } +} diff --git a/editor/src/ADocumentWindow.hpp b/editor/src/ADocumentWindow.hpp index 648f37d12..798c6070b 100644 --- a/editor/src/ADocumentWindow.hpp +++ b/editor/src/ADocumentWindow.hpp @@ -14,20 +14,23 @@ #pragma once +#include + #include "IDocumentWindow.hpp" #include "Nexo.hpp" #include "WindowRegistry.hpp" - -#include +#include "inputs/WindowState.hpp" namespace nexo::editor { - #define NEXO_WND_USTRID_INSPECTOR "Inspector" - #define NEXO_WND_USTRID_SCENE_TREE "Scene Tree" - #define NEXO_WND_USTRID_ASSET_MANAGER "Asset Manager" - #define NEXO_WND_USTRID_CONSOLE "Console" - #define NEXO_WND_USTRID_MATERIAL_INSPECTOR "Material Inspector" - #define NEXO_WND_USTRID_DEFAULT_SCENE "Default Scene" + #define NEXO_WND_USTRID_INSPECTOR "###Inspector" + #define NEXO_WND_USTRID_SCENE_TREE "###Scene Tree" + #define NEXO_WND_USTRID_ASSET_MANAGER "###Asset Manager" + #define NEXO_WND_USTRID_CONSOLE "###Console" + #define NEXO_WND_USTRID_MATERIAL_INSPECTOR "###Material Inspector" + #define NEXO_WND_USTRID_DEFAULT_SCENE "###Default Scene" + #define NEXO_WND_USTRID_BOTTOM_BAR "###CommandsBar" + #define NEXO_WND_USTRID_TEST "###TestWindow" class ADocumentWindow : public IDocumentWindow { public: @@ -37,7 +40,7 @@ namespace nexo::editor { * Initializes the document window by storing a reference to the provided WindowRegistry and assigning a unique window * identifier. This setup is essential for integrating the window with the docking management system. */ - explicit ADocumentWindow(const std::string &windowName, WindowRegistry &windowRegistry) : m_windowName(windowName), m_windowRegistry(windowRegistry) + explicit ADocumentWindow(std::string windowName, WindowRegistry &windowRegistry) : m_windowName(std::move(windowName)), m_windowRegistry(windowRegistry) { windowId = nextWindowId++; }; @@ -45,8 +48,11 @@ namespace nexo::editor { [[nodiscard]] bool isFocused() const override { return m_focused; } [[nodiscard]] bool isOpened() const override { return m_opened; } + void setOpened(bool opened) override { m_opened = opened; } [[nodiscard]] bool isHovered() const override { return m_hovered; } + [[nodiscard]] const ImVec2 &getContentSize() const override { return m_contentSize; } + /** * @brief Retrieves the open state of the document window. * @@ -60,43 +66,46 @@ namespace nexo::editor { [[nodiscard]] const std::string &getWindowName() const override { return m_windowName; } - /** - * @brief Initializes the docking configuration for the document window on its first display. - * - * This function retrieves the current ImGui window and checks its docking state to ensure it aligns with the expected - * configuration from the WindowRegistry. On the first open (when m_firstOpened is true), if the window is not actively - * docked or its current dock ID does not match the expected ID obtained via the provided window name, the function assigns - * the expected dock ID to the window. If the window is already docked but the dock IDs still differ, the current dock ID is - * saved to the WindowRegistry. The m_firstOpened flag is then set to false so that the docking configuration is applied only once. - * - * @param windowName The name used to look up the expected dock identifier in the WindowRegistry. - */ - void firstDockSetup(const std::string &windowName) - { - if (ImGuiWindow* currentWindow = ImGui::GetCurrentWindow(); currentWindow) - { - const bool isDocked = currentWindow->DockIsActive; - const ImGuiID currentDockID = currentWindow->DockId; - auto dockId = m_windowRegistry.getDockId(windowName); - - if (m_firstOpened && (!isDocked || (dockId && currentDockID != *dockId))) - currentWindow->DockId = *dockId; - else if (dockId && currentDockID != *dockId) - m_windowRegistry.setDockId(windowName, currentDockID); - m_firstOpened = false; - } - } + [[nodiscard]] const WindowState &getWindowState() const override { return m_windowState; }; + + WindowId windowId; protected: bool m_opened = true; bool m_focused = false; bool m_hovered = false; // TODO: make these update without user intervention + bool m_wasVisibleLastFrame = false; + bool m_isVisibleInDock = true; + + ImVec2 m_windowPos; + ImVec2 m_windowSize; + ImVec2 m_contentSizeMin; + ImVec2 m_contentSizeMax; + ImVec2 m_contentSize; bool m_firstOpened = true; std::string m_windowName; + WindowState m_windowState; WindowRegistry &m_windowRegistry; + + void beginRender(const std::string &windowName); + private: + /** + * @brief Initializes the docking configuration for the document window on its first display. + * + * This function retrieves the current ImGui window and checks its docking state to ensure it aligns with the expected + * configuration from the WindowRegistry. On the first open (when m_firstOpened is true), if the window is not actively + * docked or its current dock ID does not match the expected ID obtained via the provided window name, the function assigns + * the expected dock ID to the window. If the window is already docked but the dock IDs still differ, the current dock ID is + * saved to the WindowRegistry. The m_firstOpened flag is then set to false so that the docking configuration is applied only once. + * + * @param windowName The name used to look up the expected dock identifier in the WindowRegistry. + */ + void dockingUpdate(const std::string &windowName); + void visibilityUpdate(); + void sizeUpdate(); }; } diff --git a/editor/src/Components/Components.cpp b/editor/src/Components/Components.cpp deleted file mode 100644 index f38fef1f3..000000000 --- a/editor/src/Components/Components.cpp +++ /dev/null @@ -1,323 +0,0 @@ -//// Components.cpp /////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 17/02/2025 -// Description: Source file for the utilitary ImGui functions -// -/////////////////////////////////////////////////////////////////////////////// - -#include "Components.hpp" - -#include -#include -#include -#include - -namespace nexo::editor { - - bool Components::drawButton( - const std::string &label, - const ImVec2 &size, - const ImU32 bg, - const ImU32 bgHovered, - const ImU32 bgActive, const ImU32 txtColor - ) { - if (bg != 0) ImGui::PushStyleColor(ImGuiCol_Button, bg); - if (bgHovered != 0) ImGui::PushStyleColor(ImGuiCol_ButtonHovered, bgHovered); - if (bgActive != 0) ImGui::PushStyleColor(ImGuiCol_ButtonActive, bgActive); - if (txtColor != 0) ImGui::PushStyleColor(ImGuiCol_Text, txtColor); - - const bool clicked = ImGui::Button(label.c_str(), size); - - const int popCount = (bg != 0) + (bgHovered != 0) + (bgActive != 0) + (txtColor != 0); - ImGui::PopStyleColor(popCount); - return clicked; - } - - void Components::drawButtonBorder( - const ImU32 borderColor, - const ImU32 borderColorHovered, - const ImU32 borderColorActive, - const float rounding, - const ImDrawFlags flags, - const float thickness - ) { - const ImVec2 p_min = ImGui::GetItemRectMin(); - const ImVec2 p_max = ImGui::GetItemRectMax(); - ImU32 color = borderColor ? borderColor : ImGui::GetColorU32(ImGuiCol_Button); - if (ImGui::IsItemHovered()) - color = borderColorHovered ? borderColorHovered : ImGui::GetColorU32(ImGuiCol_ButtonHovered); - if (ImGui::IsItemActive()) - color = borderColorActive ? borderColorActive : ImGui::GetColorU32(ImGuiCol_ButtonActive); - - ImGui::GetWindowDrawList()->AddRect(p_min, p_max, color, rounding, flags, thickness); - } - - void Components::drawButtonInnerBorder( - const ImU32 borderColor, - const ImU32 borderColorHovered, - const ImU32 borderColorActive, - const float rounding, - const ImDrawFlags flags, - const float thickness - ) { - ImVec2 p_min = ImGui::GetItemRectMin(); - ImVec2 p_max = ImGui::GetItemRectMax(); - ImU32 color = borderColor ? borderColor : ImGui::GetColorU32(ImGuiCol_Button); - if (ImGui::IsItemHovered()) - color = borderColorHovered ? borderColorHovered : ImGui::GetColorU32(ImGuiCol_ButtonHovered); - if (ImGui::IsItemActive()) - color = borderColorActive ? borderColorActive : ImGui::GetColorU32(ImGuiCol_ButtonActive); - - ImGui::GetWindowDrawList()->AddRect( - ImVec2(p_min.x + thickness, p_min.y + thickness), - ImVec2(p_max.x - thickness, p_max.y - thickness), - color, rounding, flags, thickness); - } - - bool Components::drawDragFloat( - const std::string &label, - float *values, const float speed, - const float min, const float max, - const std::string &format, - const ImU32 bg, const ImU32 bgHovered, const ImU32 bgActive, const ImU32 textColor - ) { - if (bg) ImGui::PushStyleColor(ImGuiCol_FrameBg, bg); - if (bgHovered) ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, bgHovered); - if (bgActive) ImGui::PushStyleColor(ImGuiCol_FrameBgActive, bgActive); - if (textColor) ImGui::PushStyleColor(ImGuiCol_Text, textColor); - const bool clicked = ImGui::DragFloat(label.c_str(), values, speed, min, max, format.c_str()); - - const int popCount = (bg != 0) + (bgHovered != 0) + (bgActive != 0) + (textColor != 0); - ImGui::PopStyleColor(popCount); - return clicked; - } - - void Components::drawColorButton(const std::string &label, const ImVec2 size, const ImVec4 color, bool *clicked, ImGuiColorEditFlags flags) - { - flags |= ImGuiColorEditFlags_NoTooltip; - constexpr float borderThickness = 3.0f; - const float defaultSize = ImGui::GetFrameHeight() + borderThickness; - const auto calculatedSize = ImVec2(size.x == 0 ? defaultSize : size.x - borderThickness * 2, size.y == 0 ? defaultSize : size.y - borderThickness * 2); - if (ImGui::ColorButton(label.c_str(), - color, - flags, - calculatedSize) && clicked) - { - *clicked = !*clicked; - } - Components::drawButtonBorder(ImGui::GetColorU32(ImGuiCol_Button), ImGui::GetColorU32(ImGuiCol_ButtonHovered), ImGui::GetColorU32(ImGuiCol_ButtonActive), borderThickness); - } - - void Components::drawCustomSeparatorText(const std::string &text, const float textPadding, const float leftSpacing, const float thickness, ImU32 lineColor, ImU32 textColor) - { - const ImVec2 pos = ImGui::GetCursorScreenPos(); - const float availWidth = ImGui::GetContentRegionAvail().x; - const float textWidth = ImGui::CalcTextSize(text.c_str()).x; - - // Compute the length of each line. Clamp to zero if the region is too small. - float lineWidth = (availWidth - textWidth - 2 * textPadding) * leftSpacing; - if (lineWidth < 0.0f) - lineWidth = 0.0f; - - // Compute Y coordinate to draw lines so they align with the text center. - const float lineY = pos.y + ImGui::GetTextLineHeight() * 0.5f; - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - - const ImVec2 lineStart(pos.x, lineY); - const ImVec2 lineEnd(pos.x + lineWidth, lineY); - draw_list->AddLine(lineStart, lineEnd, lineColor, thickness); - - const ImVec2 textPos(pos.x + lineWidth + textPadding, pos.y); - draw_list->AddText(textPos, textColor, text.c_str()); - - const ImVec2 rightLineStart(pos.x + lineWidth + textPadding + textWidth + textPadding, lineY); - const ImVec2 rightLineEnd(pos.x + availWidth, lineY); - draw_list->AddLine(rightLineStart, rightLineEnd, lineColor, thickness); - - ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight())); - } - - /** - * @brief Linearly interpolates between two colors (ImU32, ImGui 32-bits ARGB format). - * @param[in] colA The first color (ARGB format). - * @param[in] colB The second color (ARGB format). - * @param[in] t The interpolation factor (0.0 to 1.0). - * @return The interpolated color (ARGB format). - */ - static ImU32 imLerpColor(const ImU32 colA, const ImU32 colB, const float t) - { - const unsigned char a0 = (colA >> 24) & 0xFF, r0 = (colA >> 16) & 0xFF, g0 = (colA >> 8) & 0xFF, b0 = colA & 0xFF; - const unsigned char a1 = (colB >> 24) & 0xFF, r1 = (colB >> 16) & 0xFF, g1 = (colB >> 8) & 0xFF, b1 = colB & 0xFF; - const auto a = static_cast(static_cast(a0) + t * static_cast(a1 - a0)); - const auto r = static_cast(static_cast(r0) + t * static_cast(r1 - r0)); - const auto g = static_cast(static_cast(g0) + t * static_cast(g1 - g0)); - const auto b = static_cast(static_cast(b0) + t * static_cast(b1 - b0)); - return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); - } - - /** - * @brief Clip a convex polygon against a half-plane defined by: (dot(normal, v) >= offset) - * - * This function uses the Sutherland-Hodgman algorithm to clip a polygon against a line defined by a normal vector and an offset. - * @param[in] poly Vector of vertices representing the polygon to be clipped. - * @param[in] normal The normal vector of the line used for clipping. - * @param[in] offset The offset from the origin of the line. - * @param[out] outPoly Output vector to store the clipped polygon vertices. - */ - static void clipPolygonWithLine(const std::vector& poly, const ImVec2& normal, float offset, std::vector& outPoly) - { - outPoly.clear(); - const auto count = poly.size(); - outPoly.reserve(count * 2); // Preallocate space for the output polygon (prepare worst case) - for (size_t i = 0; i < count; i++) { - const ImVec2& a = poly[i]; - const ImVec2& b = poly[(i + 1) % count]; - const float da = ImDot(a, normal) - offset; - const float db = ImDot(b, normal) - offset; - if (da >= 0) - outPoly.push_back(a); - // if the edge spans the boundary, compute intersection - if ((da >= 0 && db < 0) || (da < 0 && db >= 0)) { - const float t = da / (da - db); - ImVec2 inter; - inter.x = a.x + t * (b.x - a.x); - inter.y = a.y + t * (b.y - a.y); - outPoly.push_back(inter); - } - } - } - - /** - * @brief Fill a convex polygon with triangles using a triangle fan. - * @param[in] drawList The ImDrawList to which the triangles will be added. - * @param[in] poly Vector of vertices representing the polygon to be filled. - * @param[in] polyColors Vector of colors for each vertex in the polygon. - */ - static void fillConvexPolygon(ImDrawList* drawList, const std::vector& poly, const std::vector& polyColors) - { - if (poly.size() < 3) - return; - const auto count = static_cast(poly.size()); - drawList->PrimReserve((count - 2) * 3, count); - // Use the first vertex as pivot. - for (int i = 1; i < count - 1; i++) { - const auto currentIdx = drawList->_VtxCurrentIdx; - drawList->PrimWriteIdx(static_cast(currentIdx)); - drawList->PrimWriteIdx(static_cast(currentIdx + i)); - drawList->PrimWriteIdx(static_cast(currentIdx + i + 1)); - } - // Write vertices with their computed colors. - for (int i = 0; i < count; i++) { - // For a vertex, we determine its position t between the segment boundaries later. - // Here we assume the provided poly_colors already correspond vertex-by-vertex. - drawList->PrimWriteVtx(poly[i], drawList->_Data->TexUvWhitePixel, polyColors[i]); - } - } - - - void Components::drawRectFilledLinearGradient(const ImVec2& pMin, const ImVec2& pMax, float angle, - std::vector stops, ImDrawList* drawList) - { - if (!drawList) - drawList = ImGui::GetWindowDrawList(); - - // Check if we have at least two stops. - // If not, we can't create a gradient. - if (stops.size() < 2) - return; - - angle -= 90.0f; // rotate 90 degrees to match the CSS gradients rotations - - // Convert angle from degrees to radians. Also keep it in range of radians - // [0, 2*PI) for consistency. - angle = fmodf(angle, 360.0f); - if (angle < 0.0f) - angle += 360.0f; - angle = angle * std::numbers::pi_v / 180.0f; - - const auto gradDir = ImVec2(cosf(angle), sinf(angle)); - - // Define rectangle polygon (clockwise order). - const std::vector rectPoly = { pMin, ImVec2(pMax.x, pMin.y), pMax, ImVec2(pMin.x, pMax.y) }; - - // Compute projection range (d_min, d_max) for the rectangle. - float d_min = std::numeric_limits::max(); - float d_max = -std::numeric_limits::max(); - for (auto const& v : rectPoly) { - const float d = ImDot(v, gradDir); - if (d < d_min) d_min = d; - if (d > d_max) d_max = d; - } - - // sanitize stops - float stop_max = 0.0f; - for (auto& [pos, color] : stops) { - (void)color; // ignore color for now - // Clamp stop position to [0.0f, 1.0f] - if (pos < 0.0f) pos = 0.0f; - if (pos > 1.0f) pos = 1.0f; - - // Clamp stop position to [stop_max, 1.0f] - if (pos < stop_max) { - pos = stop_max; - } else { - stop_max = pos; - } - } - - // if first stop does not start at 0.0f, we need to add a stop at 0.0f - if (stops[0].pos > 0.0f) { - stops.insert(stops.begin(), { 0.0f, stops[0].color }); - } - // if last stop does not end at 1.0f, we need to add a stop at 1.0f - if (stops[stops.size() - 1].pos < 1.0f) { - stops.push_back({ 1.0f, stops[stops.size() - 1].color }); - } - - // For each segment defined by consecutive stops: - for (long i = static_cast(stops.size()) - 1; i > 0; i--) { - const long posStart = i - 1; - const long posEnd = i; - // Compute threshold projections for the current segment. - const float segStart = d_min + stops[posStart].pos * (d_max - d_min); - const float segEnd = d_min + stops[posEnd].pos * (d_max - d_min); - - // Start with the whole rectangle. - std::vector segPoly = rectPoly; - std::vector tempPoly; - // Clip against lower boundary: d >= seg_start - clipPolygonWithLine(segPoly, gradDir, segStart, tempPoly); - segPoly = tempPoly; // copy result - // Clip against upper boundary: d <= seg_end - // To clip with an upper-bound, invert the normal. - clipPolygonWithLine(segPoly, ImVec2(-gradDir.x, -gradDir.y), -segEnd, tempPoly); - segPoly = tempPoly; - - if (segPoly.empty()) - continue; - - // Now, compute per-vertex colors for the segment polygon. - std::vector polyColors; - polyColors.reserve(segPoly.size()); - for (const ImVec2& v : segPoly) { - // Compute projection for the vertex. - const float d = ImDot(v, gradDir); - // Map projection to [0,1] relative to current segment boundaries. - const float t = (d - segStart) / (segEnd - segStart); - // Interpolate the color between the two stops. - polyColors.push_back(imLerpColor(stops[posStart].color, stops[posEnd].color, t)); - } - - // Draw the filled and colored polygon. - fillConvexPolygon(drawList, segPoly, polyColors); - } - } -} diff --git a/editor/src/Components/Components.hpp b/editor/src/Components/Components.hpp deleted file mode 100644 index b0b29d955..000000000 --- a/editor/src/Components/Components.hpp +++ /dev/null @@ -1,153 +0,0 @@ -//// Components.hpp /////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 17/02/2025 -// Description: Header file for the utilitary ImGui functions -// -/////////////////////////////////////////////////////////////////////////////// -#pragma once - -#include -#include -#include - -namespace nexo::editor { - - /** - * @brief A collection of utility functions for custom ImGui components. - * - * This class provides helper functions to draw custom buttons, drag floats, - * color buttons, and separators with text. - */ - class Components { - public: - /** - * @brief Draws a button with custom style colors. - * - * Pushes custom style colors for the button and its states, draws the button, - * and then pops the style colors. - * - * @param label The button label. - * @param size The size of the button. - * @param bg The background color. - * @param bgHovered The background color when hovered. - * @param bgActive The background color when active. - * @param txtColor The text color. - * @return true if the button was clicked; false otherwise. - */ - static bool drawButton(const std::string &label, const ImVec2& size = ImVec2(0, 0), ImU32 bg = 0, ImU32 bgHovered = 0, ImU32 bgActive = 0, ImU32 txtColor = 0); - - /** - * @brief Draws a border around the last item. - * - * Uses the current item's rectangle and draws a border with specified colors - * for normal, hovered, and active states. - * - * @param borderColor The border color for normal state. - * @param borderColorHovered The border color when hovered. - * @param borderColorActive The border color when active. - * @param rounding The rounding of the border corners. - * @param flags Additional draw flags. - * @param thickness The thickness of the border. - */ - static void drawButtonBorder(ImU32 borderColor, ImU32 borderColorHovered, ImU32 borderColorActive, float rounding = 2.0f, ImDrawFlags flags = 0, float thickness = 3.0f); - - /** - * @brief Draws a border inside the last item. - * - * Similar to drawButtonBorder, but draws a border inside the item rectangle instead of outside. - * - * @param borderColor The border color for normal state. - * @param borderColorHovered The border color when hovered. - * @param borderColorActive The border color when active. - * @param rounding The rounding of the border corners. - * @param flags Additional draw flags. - * @param thickness The thickness of the border. - */ - static void drawButtonInnerBorder(ImU32 borderColor, ImU32 borderColorHovered, ImU32 borderColorActive, float rounding = 2.0f, ImDrawFlags flags = 0, float thickness = 3.0f); - - - /** - * @brief Draws a draggable float widget with custom styling. - * - * Pushes custom style colors for the drag float widget, draws it, and then pops the styles. - * - * @param label The label for the drag float. - * @param values Pointer to the float value. - * @param speed The speed of value change. - * @param min The minimum allowable value. - * @param max The maximum allowable value. - * @param format The display format. - * @param bg The background color. - * @param bgHovered The background color when hovered. - * @param bgActive The background color when active. - * @param textColor The text color. - * @return true if the value was changed; false otherwise. - */ - static bool drawDragFloat(const std::string &label, float *values, float speed, float min, float max, const std::string &format, ImU32 bg = 0, ImU32 bgHovered = 0, ImU32 bgActive = 0, ImU32 textColor = 0); - - /** - * @brief Draws an icon button with custom style colors. - * - * Similar to drawButton, but intended for icon-only buttons. - * - * @param label The label for the button. - * @param size The size of the button. - * @param bg The background color. - * @param bgHovered The background color when hovered. - * @param bgActive The background color when active. - * @param txtColor The text (icon) color. - * @return true if the button was clicked; false otherwise. - */ - static bool drawIconButton(const std::string &label, ImVec2 size = ImVec2(0, 0), ImU32 bg = 0, ImU32 bgHovered = 0, ImU32 bgActive = 0, ImU32 txtColor = 0); - - /** - * @brief Draws a color button with a border. - * - * Displays a color button with the provided label and size. Optionally toggles a clicked state. - * - * @param label The label for the color button. - * @param size The size of the button. - * @param color The color to display. - * @param clicked Optional pointer to a boolean that is toggled when the button is clicked. - * @param flags Additional color edit flags. - */ - static void drawColorButton(const std::string &label, ImVec2 size, ImVec4 color, bool *clicked = nullptr, ImGuiColorEditFlags flags = ImGuiColorEditFlags_None); - - /** - * @brief Draws a custom separator with centered text. - * - * Renders a separator line with text in the middle, with customizable padding, spacing, - * thickness, and colors. - * - * @param text The text to display at the separator. - * @param textPadding Padding around the text. - * @param leftSpacing The spacing multiplier for the left separator line. - * @param thickness The thickness of the separator lines. - * @param lineColor The color of the separator lines. - * @param textColor The color of the text. - */ - static void drawCustomSeparatorText(const std::string &text, float textPadding, float leftSpacing, float thickness, ImU32 lineColor, ImU32 textColor); - - struct GradientStop - { - float pos; // percentage position along the gradient [0.0f, 1.0f] - ImU32 color; // color at this stop - }; - - /** - * @brief Draw filled rectangle with a linear gradient defined by an arbitrary angle and gradient stops. - * @param pMin Upper left corner position of the rectangle - * @param pMax Lower right corner position of the rectangle - * @param angle Angle of the gradient in degrees (0.0f = down, 90.0f = right, 180.0f = up, 270.0f = left) - * @param stops Vector of gradient stops, each defined by a position (0.0f to 1.0f) and a color - */ - static void drawRectFilledLinearGradient(const ImVec2& pMin, const ImVec2& pMax, float angle, std::vector stops, ImDrawList* drawList = nullptr); - }; -} diff --git a/editor/src/Components/EntityPropertiesComponents.cpp b/editor/src/Components/EntityPropertiesComponents.cpp deleted file mode 100644 index d822fa488..000000000 --- a/editor/src/Components/EntityPropertiesComponents.cpp +++ /dev/null @@ -1,286 +0,0 @@ -//// EntityPropertiesComponents.cpp /////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 22/02/2025 -// Description: Source file for the entity properties components -// -/////////////////////////////////////////////////////////////////////////////// - -#include "EntityPropertiesComponents.hpp" -#include "Components.hpp" - -namespace nexo::editor { - - bool EntityPropertiesComponents::drawHeader(const std::string &label, std::string_view headerText) - { - float increasedPadding = 2.0f; - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, - ImVec2(ImGui::GetStyle().FramePadding.x, increasedPadding)); - - bool open = ImGui::TreeNodeEx(label.c_str(), - ImGuiTreeNodeFlags_DefaultOpen | - ImGuiTreeNodeFlags_Framed | - ImGuiTreeNodeFlags_AllowItemOverlap); - ImGui::PopStyleVar(); - - // Horizontal centering: - const float arrowPosX = ImGui::GetCursorPosX(); - ImGui::SameLine(0.0f, 0.0f); - const float totalWidth = ImGui::GetContentRegionAvail().x + arrowPosX; - const ImVec2 textSize = ImGui::CalcTextSize(headerText.data()); - const float textPosX = (totalWidth - textSize.x) * 0.5f; - ImGui::SetCursorPosX(textPosX); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 2.5f); // This stuff seems strange, should check in the long run if there is a better way - - ImGui::TextUnformatted(headerText.data()); - - return open; - } - - void EntityPropertiesComponents::drawRowLabel(const ChannelLabel &rowLabel) - { - ImGui::TableNextColumn(); - if (rowLabel.fixedWidth != -1.0f) - { - //TODO: Implement now fixed width row label - //float fixedCellWidth = rowLabel.fixedWidth; - //ImVec2 textSize = ImGui::CalcTextSize(rowLabel.label.c_str()); - //float offsetX = (fixedCellWidth - textSize.x) * 0.5f; - //float rowHeight = ImGui::GetTextLineHeightWithSpacing(); - //float offsetY = (rowHeight - textSize.y) * 0.5f; - //ImVec2 cellPos = ImGui::GetCursorPos(); - } - //ImGui::SetWindowFontScale(1.11f); - - ImGui::TextUnformatted(rowLabel.label.c_str()); - //ImGui::SetWindowFontScale(1.0f); - } - - bool EntityPropertiesComponents::drawRowDragFloat(const Channels &channels) - { - bool clicked = false; - for (unsigned int i = 0; i < channels.count; ++i) - { - ImGui::TableNextColumn(); - if (!channels.badges[i].label.empty()) - { - const auto &[label, size, bg, bgHovered, bgActive, txtColor] = channels.badges[i]; - Components::drawButton(label, size, bg, bgHovered, bgActive, txtColor); - } - ImGui::SameLine(0, 2); - const auto &[label, value, speed, min, max, bg, bgHovered, bgActive, textColor, format] = channels.sliders[i]; - clicked = Components::drawDragFloat( - label, - value, - speed, - min, - max, - format, - bg, - bgHovered, - bgActive) || clicked; - } - return clicked; - } - - bool EntityPropertiesComponents::drawRowDragFloat1(const char *uniqueLabel, const std::string &badgeLabel, float *value, float minValue, float maxValue, float speed) - { - const std::string labelStr = uniqueLabel; - std::string labelX = std::string("##X") + labelStr; - - std::string badgeLabelX = (badgeLabel.empty()) ? "" : badgeLabel + std::string("##") + labelStr; - - ImGui::TableNextRow(); - - ChannelLabel chanLabel; - chanLabel.label = std::string(uniqueLabel); - chanLabel.fixedWidth = -1.0f; - - std::vector badges; - badges.reserve(1); - badges.emplace_back(Badge{badgeLabelX, {0, 0}, IM_COL32(80, 0, 0, 255), IM_COL32(80, 0, 0, 255), IM_COL32(80, 0, 0, 255), IM_COL32(255, 180, 180, 255)}); - - std::vector sliders; - sliders.reserve(1); - sliders.emplace_back(labelX, value, speed, minValue, maxValue, 0, 0, 0, 0, "%.2f"); - - Channels channels; - channels.count = 1; - channels.badges = badges; - channels.sliders = sliders; - - EntityPropertiesComponents::drawRowLabel(chanLabel); - return EntityPropertiesComponents::drawRowDragFloat(channels); - } - - bool EntityPropertiesComponents::drawRowDragFloat2( - const char *uniqueLabel, - const std::string &badLabelX, - const std::string &badLabelY, - float *values, - float minValue, - float maxValue, - float speed, - std::vector badgeColor, - std::vector textBadgeColor, - const bool disabled - ) - { - const std::string labelStr = uniqueLabel; - std::string labelX = std::string("##X") + labelStr; - std::string labelY = std::string("##Y") + labelStr; - - const std::string badgeLabelX = badLabelX + std::string("##") + labelStr; - const std::string badgeLabelY = badLabelY + std::string("##") + labelStr; - - ImGui::TableNextRow(); - - ChannelLabel chanLabel; - chanLabel.label = std::string(uniqueLabel); - chanLabel.fixedWidth = -1.0f; - - if (badgeColor.empty()) - badgeColor = {IM_COL32(102, 28, 28, 255), IM_COL32(0, 80, 0, 255)}; - if (textBadgeColor.empty()) - textBadgeColor = {IM_COL32(255, 180, 180, 255), IM_COL32(180, 255, 180, 255)}; - std::vector badges; - badges.reserve(2); - badges.emplace_back(Badge{badgeLabelX, {0, 0}, badgeColor[0], badgeColor[0], badgeColor[0], textBadgeColor[0]}); - badges.emplace_back(Badge{badgeLabelY, {0, 0}, badgeColor[1], badgeColor[1], badgeColor[1], textBadgeColor[1]}); - - std::vector sliders; - sliders.reserve(2); - std::vector sliderColors = {ImGui::GetColorU32(ImGuiCol_FrameBg), ImGui::GetColorU32(ImGuiCol_FrameBgHovered), ImGui::GetColorU32(ImGuiCol_FrameBgActive), ImGui::GetColorU32(ImGuiCol_Text)}; - if (disabled) - sliderColors = {ImGui::GetColorU32(ImGuiCol_FrameBg), ImGui::GetColorU32(ImGuiCol_FrameBgHovered), ImGui::GetColorU32(ImGuiCol_FrameBgActive), ImGui::GetColorU32(ImGuiCol_TextDisabled)}; - sliders.emplace_back(labelX, &values[0], speed, minValue, maxValue, sliderColors[0], sliderColors[1], - sliderColors[2], sliderColors[3], "%.2f"); - sliders.emplace_back(labelY, &values[1], speed, minValue, maxValue, sliderColors[0], sliderColors[1], - sliderColors[2], sliderColors[3], "%.2f"); - - Channels channels; - channels.count = 2; - channels.badges = badges; - channels.sliders = sliders; - - EntityPropertiesComponents::drawRowLabel(chanLabel); - return EntityPropertiesComponents::drawRowDragFloat(channels); - } - - bool EntityPropertiesComponents::drawRowDragFloat3( - const char *uniqueLabel, - const std::string &badLabelX, - const std::string &badLabelY, - const std::string &badLabelZ, - float *values, - float minValue, - float maxValue, - float speed, - std::vector badgeColors, - std::vector textBadgeColor - ) - { - std::string labelStr = uniqueLabel; - std::string labelX = std::string("##X") + labelStr; - std::string labelY = std::string("##Y") + labelStr; - std::string labelZ = std::string("##Z") + labelStr; - - std::string badgeLabelX = badLabelX + std::string("##") + labelStr; - std::string badgeLabelY = badLabelY + std::string("##") + labelStr; - std::string badgeLabelZ = badLabelZ + std::string("##") + labelStr; - - ImGui::TableNextRow(); - - ChannelLabel chanLabel; - chanLabel.label = std::string(uniqueLabel); - chanLabel.fixedWidth = -1.0f; - - float badgeSize = ImGui::GetFrameHeight(); - if (badgeColors.empty()) - badgeColors = {IM_COL32(102, 28, 28, 255), IM_COL32(0, 80, 0, 255), IM_COL32(38, 49, 121, 255)}; - if (textBadgeColor.empty()) - textBadgeColor = {IM_COL32(255, 180, 180, 255), IM_COL32(180, 255, 180, 255), IM_COL32(180, 180, 255, 255)}; - std::vector badges; - badges.reserve(3); - badges.emplace_back(Badge{badgeLabelX, {badgeSize, badgeSize}, badgeColors[0], badgeColors[0], badgeColors[0], textBadgeColor[0]}); - badges.emplace_back(Badge{badgeLabelY, {badgeSize, badgeSize}, badgeColors[1], badgeColors[1], badgeColors[1], textBadgeColor[1]}); - badges.emplace_back(Badge{badgeLabelZ, {badgeSize, badgeSize}, badgeColors[2], badgeColors[2], badgeColors[2], textBadgeColor[2]}); - - std::vector sliders; - sliders.reserve(3); - sliders.emplace_back(labelX, &values[0], speed, minValue, maxValue, 0, - 0, 0, ImGui::GetColorU32(ImGuiCol_Text), - "%.2f"); - sliders.emplace_back(labelY, &values[1], speed, minValue, maxValue, 0, - 0, 0, ImGui::GetColorU32(ImGuiCol_Text), - "%.2f"); - sliders.emplace_back(labelZ, &values[2], speed, minValue, maxValue, 0, - 0, 0, ImGui::GetColorU32(ImGuiCol_Text), - "%.2f"); - - Channels channels; - channels.count = 3; - channels.badges = badges; - channels.sliders = sliders; - - if (!chanLabel.label.empty()) - EntityPropertiesComponents::drawRowLabel(chanLabel); - return EntityPropertiesComponents::drawRowDragFloat(channels); - } - - bool EntityPropertiesComponents::drawToggleButtonWithSeparator(const std::string &label, bool* toggled) - { - bool clicked = false; - ImGui::PushID(label.c_str()); - - constexpr ImVec2 buttonSize(24, 24); - if (const std::string arrowLabel = "##arrow" + label; ImGui::InvisibleButton(arrowLabel.c_str(), buttonSize)) - clicked = true; - if (clicked) - *toggled = !(*toggled); - - const ImVec2 btnPos = ImGui::GetItemRectMin(); - const ImVec2 btnSize = ImGui::GetItemRectSize(); - const ImVec2 center(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - constexpr float arrowSize = 5.0f; - const ImU32 arrowColor = ImGui::GetColorU32(ImGuiCol_TextTab); - if (*toggled) - { - // Draw a downward pointing arrow - draw_list->AddTriangleFilled( - ImVec2(center.x - arrowSize, center.y - arrowSize), - ImVec2(center.x + arrowSize, center.y - arrowSize), - ImVec2(center.x, center.y + arrowSize), - arrowColor); - } - else - { - // Draw a rightward pointing arrow - draw_list->AddTriangleFilled( - ImVec2(center.x - arrowSize, center.y - arrowSize), - ImVec2(center.x - arrowSize, center.y + arrowSize), - ImVec2(center.x + arrowSize, center.y), - arrowColor); - } - - ImGui::SameLine(); - const ImVec2 separatorPos = ImGui::GetCursorScreenPos(); - constexpr float separatorHeight = buttonSize.y; // match button height - draw_list->AddLine(separatorPos, ImVec2(separatorPos.x, separatorPos.y + separatorHeight), - ImGui::GetColorU32(ImGuiCol_Separator), 1.0f); - ImGui::Dummy(ImVec2(4, buttonSize.y)); - - ImGui::SameLine(); - Components::drawCustomSeparatorText(label, 10.0f, 0.1f, 0.5f, IM_COL32(255, 255, 255, 255), IM_COL32(255, 255, 255, 255)); - ImGui::PopID(); - return clicked; - } -} diff --git a/editor/src/Components/EntityPropertiesComponents.hpp b/editor/src/Components/EntityPropertiesComponents.hpp deleted file mode 100644 index cf42cbe86..000000000 --- a/editor/src/Components/EntityPropertiesComponents.hpp +++ /dev/null @@ -1,202 +0,0 @@ -//// EntityPropertiesComponents.hpp /////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 22/05/2025 -// Description: Header file for the entity properties components -// -/////////////////////////////////////////////////////////////////////////////// -#pragma once - -#include -#include -#include - -namespace nexo::editor { - - /** - * @struct ChannelLabel - * @brief Represents a label for a channel in the entity properties editor - * - * Labels can have optional fixed width for precise layout control. - */ - struct ChannelLabel { - std::string label; - float fixedWidth = -1.0f; - }; - - /** - * @struct Badge - * @brief A styled badge component with customizable appearance - * - * Used as visual indicators or labels in the UI, typically alongside sliders. - */ - struct Badge { - std::string label; ///< The displayed text - ImVec2 size; ///< Size of the badge in pixels - ImU32 bg; ///< Background color - ImU32 bgHovered; ///< Background color when hovered - ImU32 bgActive; ///< Background color when active - ImU32 txtColor; ///< Text color - }; - - /** - * @struct DragFloat - * @brief A drag float slider component with customizable appearance - * - * Used for editing float values with adjustable range and visual styling. - */ - struct DragFloat { - std::string label; ///< Unique label/ID for the component - float *value; ///< Pointer to the value being edited - float speed; ///< Speed of value change during dragging - float min; ///< Minimum value - float max; ///< Maximum value - ImU32 bg; ///< Background color - ImU32 bgHovered; ///< Background color when hovered - ImU32 bgActive; ///< Background color when active - ImU32 textColor; ///< Text color - std::string format; ///< Format string for displaying the value - }; - - /** - * @struct Channels - * @brief A collection of badges and sliders forming a multi-channel editing row - * - * Used to create rows with multiple editable values (like X, Y, Z components). - */ - struct Channels { - unsigned int count; ///< Number of channels - std::vector badges; ///< Badge component for each channel - std::vector sliders; ///< Slider component for each channel - }; - - /** - * @class EntityPropertiesComponents - * @brief Static class providing UI components for entity property editing - * - * This class offers methods to draw various ImGui-based UI components - * specifically designed for editing entity properties in a consistent - * and visually appealing way. - */ - class EntityPropertiesComponents { - public: - /** - * @brief Draws a collapsible header with centered text - * - * @param[in] label Unique label/ID for the header - * @param[in] headerText Text to display in the header - * @return true if the header is open/expanded, false otherwise - */ - static bool drawHeader(const std::string &label, std::string_view headerText); - - /** - * @brief Draws a row label in the current table column - * - * @param[in] rowLabel The label configuration to draw - */ - static void drawRowLabel(const ChannelLabel &rowLabel); - - /** - * @brief Draws a row with a single float value slider - * - * @param[in] uniqueLabel Unique label/ID for the component - * @param[in] badgeLabel Text for the badge (empty for no badge) - * @param[in,out] value Pointer to the float value to edit - * @param[in] minValue Minimum allowed value (default: -FLT_MAX) - * @param[in] maxValue Maximum allowed value (default: FLT_MAX) - * @param[in] speed Speed of value change during dragging (default: 0.3f) - * @return true if the value was changed, false otherwise - */ - static bool drawRowDragFloat1( - const char *uniqueLabel, - const std::string &badgeLabel, - float *value, - float minValue = -FLT_MAX, - float maxValue = FLT_MAX, - float speed = 0.3f); - - /** - * @brief Draws a row with two float value sliders (X and Y components) - * - * @param[in] uniqueLabel Unique label/ID for the component - * @param[in] badLabelX Text for the X component badge - * @param[in] badLabelY Text for the Y component badge - * @param[in,out] values Pointer to array of two float values to edit - * @param[in] minValue Minimum allowed value (default: -FLT_MAX) - * @param[in] maxValue Maximum allowed value (default: FLT_MAX) - * @param[in] speed Speed of value change during dragging (default: 0.3f) - * @param[in] badgeColor Optional custom colors for badges - * @param[in] textBadgeColor Optional custom text colors for badges - * @param[in] disabled If true, renders in an inactive/disabled state (default: false) - * @return true if any value was changed, false otherwise - */ - static bool drawRowDragFloat2( - const char *uniqueLabel, - const std::string &badLabelX, - const std::string &badLabelY, - float *values, - float minValue = -FLT_MAX, - float maxValue = FLT_MAX, - float speed = 0.3f, - std::vector badgeColor = {}, - std::vector textBadgeColor = {}, - bool disabled = false - ); - - /** - * @brief Draws a row with three float value sliders (X, Y, and Z components) - * - * @param[in] uniqueLabel Unique label/ID for the component - * @param[in] badLabelX Text for the X component badge - * @param[in] badLabelY Text for the Y component badge - * @param[in] badLabelZ Text for the Z component badge - * @param[in,out] values Pointer to array of three float values to edit - * @param[in] minValue Minimum allowed value (default: -FLT_MAX) - * @param[in] maxValue Maximum allowed value (default: FLT_MAX) - * @param[in] speed Speed of value change during dragging (default: 0.3f) - * @param[in] badgeColors Optional custom colors for badges - * @param[in] textBadgeColors Optional custom text colors for badges - * @return true if any value was changed, false otherwise - */ - static bool drawRowDragFloat3( - const char *uniqueLabel, - const std::string &badLabelX, - const std::string &badLabelY, - const std::string &badLabelZ, - float *values, - float minValue = -FLT_MAX, - float maxValue = FLT_MAX, - float speed = 0.3f, - std::vector badgeColors = {}, - std::vector textBadgeColors = {} - ); - - /** - * @brief Draws a row with multiple channels (badge + slider pairs) - * - * This is a lower-level function used by the other drawRowDragFloatX functions. - * - * @param[in] channels The channel configuration to draw - * @return true if any value was changed, false otherwise - */ - static bool drawRowDragFloat(const Channels &channels); - - /** - * @brief Draws a toggle button with a separator and label - * - * Creates a collapsible section control with an arrow that toggles - * between expanded and collapsed states. - * - * @param[in] label The label to display - * @param[in,out] toggled Pointer to bool that tracks the toggle state - * @return true if the toggle state changed, false otherwise - */ - static bool drawToggleButtonWithSeparator(const std::string &label, bool* toggled); - }; -} diff --git a/editor/src/Components/Widgets.cpp b/editor/src/Components/Widgets.cpp deleted file mode 100644 index 62fdc5385..000000000 --- a/editor/src/Components/Widgets.cpp +++ /dev/null @@ -1,169 +0,0 @@ -//// Widgets.cpp ////////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 22/02/2025 -// Description: Source file for the widgets components -// -/////////////////////////////////////////////////////////////////////////////// - -#include "Widgets.hpp" - -#include -#include - -#include "Components.hpp" -#include "IconsFontAwesome.h" -#include "tinyfiledialogs.h" - -namespace nexo::editor { - bool Widgets::drawColorEditor( - const std::string &label, - glm::vec4 *selectedEntityColor, - ImGuiColorEditFlags *colorPickerMode, - bool *showPicker, - const ImGuiColorEditFlags colorButtonFlags - ) { - const ImGuiStyle &style = ImGui::GetStyle(); - const ImVec2 contentAvailable = ImGui::GetContentRegionAvail(); - bool colorModified = false; - - const std::string colorButton = std::string("##ColorButton") + label; - - const ImVec2 cogIconSize = ImGui::CalcTextSize(ICON_FA_COG); - const ImVec2 cogIconPadding = style.FramePadding; - const ImVec2 itemSpacing = style.ItemSpacing; - - // Color button - Components::drawColorButton( - colorButton, - ImVec2(contentAvailable.x - cogIconSize.x - cogIconPadding.x * 2 - itemSpacing.x, 0), // Make room for the cog button - ImVec4(selectedEntityColor->x, selectedEntityColor->y, selectedEntityColor->z, selectedEntityColor->w), - showPicker, - colorButtonFlags - ); - - ImGui::SameLine(); - - const std::string pickerSettings = std::string("##PickerSettings") + label; - const std::string colorPickerPopup = std::string("##ColorPickerPopup") + label; - - // Cog button - if (Components::drawButton(std::string(ICON_FA_COG) + pickerSettings)) { - ImGui::OpenPopup(colorPickerPopup.c_str()); - } - - if (ImGui::BeginPopup(colorPickerPopup.c_str())) - { - ImGui::Text("Picker Mode:"); - if (ImGui::RadioButton("Hue Wheel", *colorPickerMode == ImGuiColorEditFlags_PickerHueWheel)) - *colorPickerMode = ImGuiColorEditFlags_PickerHueWheel; - if (ImGui::RadioButton("Hue bar", *colorPickerMode == ImGuiColorEditFlags_PickerHueBar)) - *colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - ImGui::EndPopup(); - } - - const std::string colorPickerInline = std::string("##ColorPickerInline") + label; - if (*showPicker) - { - ImGui::Spacing(); - colorModified = ImGui::ColorPicker4(colorPickerInline.c_str(), - reinterpret_cast(selectedEntityColor), *colorPickerMode); - } - return colorModified; - } - - bool Widgets::drawTextureButton(const std::string &label, std::shared_ptr &texture) - { - bool textureModified = false; - constexpr ImVec2 previewSize(32, 32); - ImGui::PushID(label.c_str()); - - const ImTextureID textureId = texture ? static_cast(static_cast(texture->getId())) : 0; - const std::string textureButton = std::string("##TextureButton") + label; - - if (ImGui::ImageButton(textureButton.c_str(), textureId, previewSize)) - { - const char* filePath = tinyfd_openFileDialog( - "Open Texture", - "", - 0, - nullptr, - nullptr, - 0 - ); - - if (filePath) - { - const std::string path(filePath); - std::shared_ptr newTexture = renderer::Texture2D::create(path); - if (newTexture) - { - texture = newTexture; - textureModified = true; - } - } - } - Components::drawButtonBorder(IM_COL32(255,255,255,0), IM_COL32(255,255,255,255), IM_COL32(255,255,255,0), 0.0f, 0, 2.0f); - ImGui::PopID(); - ImGui::SameLine(); - ImGui::Text("%s", label.c_str()); - return textureModified; - } - - bool Widgets::drawMaterialInspector(components::Material *material) - { - bool modified = false; - // --- Shader Selection --- - ImGui::BeginGroup(); - { - ImGui::Text("Shader:"); - ImGui::SameLine(); - - static int currentShaderIndex = 0; - const char* shaderOptions[] = { "Standard", "Unlit", "CustomPBR" }; - const float availableWidth = ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(availableWidth); - - if (ImGui::Combo("##ShaderCombo", ¤tShaderIndex, shaderOptions, IM_ARRAYSIZE(shaderOptions))) - { - //TODO: implement shader selection - } - } - ImGui::EndGroup(); - ImGui::Spacing(); - - // --- Rendering mode selection --- - ImGui::Text("Rendering mode:"); - ImGui::SameLine(); - static int currentRenderingModeIndex = 0; - const char* renderingModeOptions[] = { "Opaque", "Transparent", "Refraction" }; - float availableWidth = ImGui::GetContentRegionAvail().x; - - ImGui::SetNextItemWidth(availableWidth); - if (ImGui::Combo("##RenderingModeCombo", ¤tRenderingModeIndex, renderingModeOptions, IM_ARRAYSIZE(renderingModeOptions))) - { - //TODO: implement rendering mode - } - - // --- Albedo texture --- - static ImGuiColorEditFlags colorPickerModeAlbedo = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPickerAlbedo = false; - modified = Widgets::drawTextureButton("Albedo texture", material->albedoTexture) || modified; - ImGui::SameLine(); - modified = Widgets::drawColorEditor("##ColorEditor Albedo texture", &material->albedoColor, &colorPickerModeAlbedo, &showColorPickerAlbedo) || modified; - - // --- Specular texture --- - static ImGuiColorEditFlags colorPickerModeSpecular = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPickerSpecular = false; - modified = Widgets::drawTextureButton("Specular texture", material->metallicMap) || modified; - ImGui::SameLine(); - modified = Widgets::drawColorEditor("##ColorEditor Specular texture", &material->specularColor, &colorPickerModeSpecular, &showColorPickerSpecular) || modified; - return modified; - } -} diff --git a/editor/src/Components/Widgets.hpp b/editor/src/Components/Widgets.hpp deleted file mode 100644 index 551b35148..000000000 --- a/editor/src/Components/Widgets.hpp +++ /dev/null @@ -1,77 +0,0 @@ -//// Widgets.hpp ////////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 22/02/2025 -// Description: Header file for the widgets components -// -/////////////////////////////////////////////////////////////////////////////// -#pragma once - -#include -#include -#include - -#include "components/Render3D.hpp" -#include "renderer/Texture.hpp" - -namespace nexo::editor { - - /** - * @brief A collection of custom ImGui widget drawing functions. - * - * Provides utility functions for drawing color editors, texture buttons, and a material inspector, - * which can be used to simplify UI code for rendering material properties. - */ - class Widgets { - public: - - /** - * @brief Draws a color editor with a button and an optional inline color picker. - * - * Displays a custom color button (with a cog icon for picker settings) and, if enabled, - * an inline color picker. The function returns true if the color was modified. - * - * @param label A unique label identifier for the widget. - * @param selectedEntityColor Pointer to the glm::vec4 representing the current color. - * @param colorPickerMode Pointer to the ImGuiColorEditFlags for the picker mode. - * @param showPicker Pointer to a boolean that determines if the inline color picker is visible. - * @param colorButtonFlags Optional flags for the color button (default is none). - * @return true if the color was modified; false otherwise. - */ - static bool drawColorEditor( - const std::string &label, - glm::vec4 *selectedEntityColor, - ImGuiColorEditFlags *colorPickerMode, - bool *showPicker, - ImGuiColorEditFlags colorButtonFlags = ImGuiColorEditFlags_None); - - /** - * @brief Draws a texture button that displays a texture preview. - * - * When clicked, opens a file dialog to select a new texture. If a new texture is loaded, - * the passed texture pointer is updated and the function returns true. - * - * @param label A unique label identifier for the button. - * @param texture A shared pointer to the renderer::Texture2D that holds the texture. - * @return true if the texture was modified; false otherwise. - */ - static bool drawTextureButton(const std::string &label, std::shared_ptr &texture); - - /** - * @brief Draws a material inspector widget for editing material properties. - * - * This function displays controls for shader selection, rendering mode, and textures/colors - * for material properties such as albedo and specular components. - * - * @param material Pointer to the components::Material to be inspected and modified. - * @return true if any material property was modified; false otherwise. - */ - static bool drawMaterialInspector(components::Material *material); - }; -} diff --git a/editor/src/DockingRegistry.cpp b/editor/src/DockingRegistry.cpp index eacbc17c2..7cb210dd6 100644 --- a/editor/src/DockingRegistry.cpp +++ b/editor/src/DockingRegistry.cpp @@ -14,10 +14,11 @@ #include "DockingRegistry.hpp" #include +#include namespace nexo::editor { - void DockingRegistry::setDockId(const std::string& name, ImGuiID id) + void DockingRegistry::setDockId(const std::string& name, const ImGuiID id) { dockIds[name] = id; } @@ -30,4 +31,12 @@ namespace nexo::editor { } return std::nullopt; } + + void DockingRegistry::resetDockId(const std::string &name) + { + auto it = dockIds.find(name); + if (it == dockIds.end()) + return; + dockIds.erase(it); + } } diff --git a/editor/src/DockingRegistry.hpp b/editor/src/DockingRegistry.hpp index 395e6e289..094081d14 100644 --- a/editor/src/DockingRegistry.hpp +++ b/editor/src/DockingRegistry.hpp @@ -22,6 +22,17 @@ namespace nexo::editor { + /** + * @brief Manages associations between window names and their docking identifiers. + * + * The DockingRegistry maintains a mapping between window names and ImGui dock IDs, + * allowing the editor to track and restore docking configurations across sessions. + * This class is central to the editor's window layout management system, enabling + * persistent window arrangements and proper docking behavior. + * + * It uses a TransparentStringHash for efficient string lookups and provides methods + * for setting, retrieving, and removing dock ID associations. + */ class DockingRegistry { public: @@ -47,6 +58,20 @@ namespace nexo::editor { */ std::optional getDockId(const std::string& name) const; + /** + * @brief Removes a dock ID association for the specified name. + * + * This method removes the association between the given name and its dock ID + * from the registry. If the name does not exist in the registry, this method + * has no effect. + * + * Removing a dock ID is useful when a window is closed or when its docking + * configuration needs to be reset to default. + * + * @param name The name identifier of the dock association to remove. + */ + void resetDockId(const std::string &name); + private: std::unordered_map> dockIds; }; diff --git a/editor/src/DocumentWindows/AssetManagerWindow.hpp b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp similarity index 94% rename from editor/src/DocumentWindows/AssetManagerWindow.hpp rename to editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp index d665eb5cb..ad6a51d5e 100644 --- a/editor/src/DocumentWindows/AssetManagerWindow.hpp +++ b/editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp @@ -14,11 +14,8 @@ #pragma once #include -#include #include -#include #include -#include #include namespace nexo::editor { @@ -73,7 +70,6 @@ namespace nexo::editor { void drawAssetsGrid(); void drawAsset(const assets::GenericAssetRef& asset, int index, const ImVec2& itemPos, const ImVec2& itemSize); void handleSelection(int index, bool isSelected); - ImU32 getAssetTypeOverlayColor(assets::AssetType type) const; }; } // namespace nexo::editor diff --git a/editor/src/DocumentWindows/AssetManager/Init.cpp b/editor/src/DocumentWindows/AssetManager/Init.cpp new file mode 100644 index 000000000..349c8a235 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/Init.cpp @@ -0,0 +1,41 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the setup of the asset manager window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" +#include "assets/AssetCatalog.hpp" +#include "assets/Assets/Model/ModelImporter.hpp" +#include "assets/Assets/Texture/TextureImporter.hpp" +#include "Path.hpp" + +namespace nexo::editor { + void AssetManagerWindow::setup() + { + auto& catalog = assets::AssetCatalog::getInstance(); + auto asset = std::make_unique(); + catalog.registerAsset(assets::AssetLocation("my_package::My_Model@foo/bar/"), std::move(asset)); + + /*{ + assets::AssetImporter importer; + std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/models/9mn/scene.gltf"); + assets::ImporterFileInput fileInput{path}; + auto assetRef9mn = importer.importAsset(assets::AssetLocation("my_package::9mn@foo/bar/"), fileInput); + }*/ + { + assets::AssetImporter importer; + std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/textures/logo_nexo.png"); + assets::ImporterFileInput fileInput{path}; + auto textureRef = importer.importAsset(assets::AssetLocation("nexo_logo@foo/bar/"), fileInput); + } + } +} diff --git a/editor/src/DocumentWindows/AssetManagerWindow.cpp b/editor/src/DocumentWindows/AssetManager/Show.cpp similarity index 63% rename from editor/src/DocumentWindows/AssetManagerWindow.cpp rename to editor/src/DocumentWindows/AssetManager/Show.cpp index 064b6df7c..3aa3b2e8d 100644 --- a/editor/src/DocumentWindows/AssetManagerWindow.cpp +++ b/editor/src/DocumentWindows/AssetManager/Show.cpp @@ -1,4 +1,4 @@ -//// AssetManagerWindow.cpp /////////////////////////////////////////////////// +//// Show.cpp /////////////////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -6,74 +6,35 @@ // zzz zzz zzz z zzzz zzzz zzzz zzzz // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // -// Author: Guillaume HEIN -// Date: 18/11/2024 -// Description: Source file for the AssetManagerWindow class +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the rendering of the asset manager window // /////////////////////////////////////////////////////////////////////////////// #include "AssetManagerWindow.hpp" -#include -#include -#include -#include -#include -#include -#include -#include +#include "assets/AssetCatalog.hpp" +#include "IconsFontAwesome.h" namespace nexo::editor { - - void AssetManagerWindow::setup() { - auto& catalog = assets::AssetCatalog::getInstance(); - - const auto asset = new assets::Model(); - - catalog.registerAsset(assets::AssetLocation("my_package::My_Model@foo/bar/"), asset); - - - - { - assets::AssetImporter importer; - std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/models/9mn/scene.gltf"); - assets::ImporterFileInput fileInput{path}; - auto assetRef9mn = importer.importAssetAuto(assets::AssetLocation("my_package::9mn@foo/bar/"), fileInput); - } - { - assets::AssetImporter importer; - std::filesystem::path path = Path::resolvePathRelativeToExe("../resources/textures/logo_nexo.png"); - assets::ImporterFileInput fileInput{path}; - auto textureRef = importer.importAsset(assets::AssetLocation("nexo_logo@foo/bar/"), fileInput); + void AssetManagerWindow::drawMenuBar() + { + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("Options")) { + ImGui::SliderFloat("Icon Size", &m_layout.size.iconSize, 32.0f, 128.0f, "%.0f"); + ImGui::SliderInt("Icon Spacing", &m_layout.size.iconSpacing, 0, 32); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); } } - void AssetManagerWindow::shutdown() { - } - - void AssetManagerWindow::show() { - ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); - ImGui::Begin(ICON_FA_FOLDER_OPEN " Asset Manager" "###" NEXO_WND_USTRID_ASSET_MANAGER, &m_opened, ImGuiWindowFlags_MenuBar); - firstDockSetup(NEXO_WND_USTRID_ASSET_MANAGER); - - drawMenuBar(); - - float availWidth = ImGui::GetContentRegionAvail().x; - calculateLayout(availWidth); - - drawAssetsGrid(); - - ImGui::End(); - } - - void AssetManagerWindow::update() { - // Update logic if necessary - } - - void AssetManagerWindow::calculateLayout(float availWidth) { + void AssetManagerWindow::calculateLayout(const float availWidth) + { // Sizes - m_layout.size.columnCount = std::max(static_cast(availWidth / (m_layout.size.iconSize + m_layout.size.iconSpacing)), 1); + m_layout.size.columnCount = std::max(static_cast(availWidth / (m_layout.size.iconSize + static_cast(m_layout.size.iconSpacing))), 1); m_layout.size.itemSize = ImVec2(m_layout.size.iconSize + ImGui::GetFontSize() * 1.5f, m_layout.size.iconSize + ImGui::GetFontSize() * 1.7f); - m_layout.size.itemStep = ImVec2(m_layout.size.itemSize.x + m_layout.size.iconSpacing, m_layout.size.itemSize.y + m_layout.size.iconSpacing); + m_layout.size.itemStep = ImVec2(m_layout.size.itemSize.x + static_cast(m_layout.size.iconSpacing), m_layout.size.itemSize.y + static_cast(m_layout.size.iconSpacing)); // Colors m_layout.color.thumbnailBg = ImGui::GetColorU32(ImGuiCol_Button); @@ -91,51 +52,58 @@ namespace nexo::editor { m_layout.color.titleText = ImGui::GetColorU32(ImGuiCol_Text); } - void AssetManagerWindow::drawMenuBar() { - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("Options")) { - ImGui::SliderFloat("Icon Size", &m_layout.size.iconSize, 32.0f, 128.0f, "%.0f"); - ImGui::SliderInt("Icon Spacing", &m_layout.size.iconSpacing, 0, 32); - ImGui::EndMenu(); + void AssetManagerWindow::handleSelection(int index, const bool isSelected) + { + LOG(NEXO_INFO, "Asset {} {}", index, isSelected ? "deselected" : "selected"); + if (ImGui::GetIO().KeyCtrl) { + if (isSelected) + m_selectedAssets.erase(index); + else + m_selectedAssets.insert(index); + } else if (ImGui::GetIO().KeyShift) { + const int latestSelected = m_selectedAssets.empty() ? 0 : *m_selectedAssets.rbegin(); + if (latestSelected <= index) { + for (int i = latestSelected ; i <= index; ++i) { + m_selectedAssets.insert(i); + } + } else { + for (int i = index; i <= latestSelected; ++i) { + m_selectedAssets.insert(i); + } } - ImGui::EndMenuBar(); + } else { + m_selectedAssets.clear(); + m_selectedAssets.insert(index); } } - void AssetManagerWindow::drawAssetsGrid() { - ImVec2 startPos = ImGui::GetCursorScreenPos(); - - ImGuiListClipper clipper; - const auto assets = assets::AssetCatalog::getInstance().getAssets(); - clipper.Begin(assets.size(), m_layout.size.itemStep.y); - while (clipper.Step()) { - for (int lineIdx = clipper.DisplayStart; lineIdx < clipper.DisplayEnd; ++lineIdx) { - int startIdx = lineIdx * m_layout.size.columnCount; - int endIdx = std::min(startIdx + m_layout.size.columnCount, static_cast(assets.size())); - - for (int i = startIdx; i < endIdx; ++i) { - ImVec2 itemPos = ImVec2(startPos.x + (i % m_layout.size.columnCount) * m_layout.size.itemStep.x, - startPos.y + (i / m_layout.size.columnCount) * m_layout.size.itemStep.y); - drawAsset(assets[i], i, itemPos, m_layout.size.itemSize); - } - } + static ImU32 getAssetTypeOverlayColor(const assets::AssetType type) + { + switch (type) { + case assets::AssetType::TEXTURE: return IM_COL32(200, 70, 70, 255); + case assets::AssetType::MODEL: return IM_COL32(70, 170, 70, 255); + default: return IM_COL32(0, 0, 0, 0); } - clipper.End(); } - void AssetManagerWindow::drawAsset(const assets::GenericAssetRef& asset, int index, const ImVec2& itemPos, const ImVec2& itemSize) { + void AssetManagerWindow::drawAsset( + const assets::GenericAssetRef& asset, + const int index, + const ImVec2& itemPos, + const ImVec2& itemSize + ) { auto assetData = asset.lock(); if (!assetData) return; ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); + const auto itemEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y); ImGui::PushID(index); // Highlight selection - bool isSelected = std::find(m_selectedAssets.begin(), m_selectedAssets.end(), index) != m_selectedAssets.end(); - ImU32 bgColor = isSelected ? m_layout.color.thumbnailBgSelected : m_layout.color.thumbnailBg; + const bool isSelected = std::ranges::find(m_selectedAssets, index) != m_selectedAssets.end(); + const ImU32 bgColor = isSelected ? m_layout.color.thumbnailBgSelected : m_layout.color.thumbnailBg; drawList->AddRectFilled(itemPos, itemEnd, bgColor, m_layout.size.cornerRadius); // Add selection border @@ -152,17 +120,17 @@ namespace nexo::editor { } // Draw thumbnail - ImVec2 thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * m_layout.size.thumbnailHeightRatio); + const auto thumbnailEnd = ImVec2(itemPos.x + itemSize.x, itemPos.y + itemSize.y * m_layout.size.thumbnailHeightRatio); drawList->AddRectFilled(itemPos, thumbnailEnd, m_layout.color.thumbnailBg); // Draw type overlay - ImVec2 overlayPos = ImVec2(thumbnailEnd.x - m_layout.size.overlayPadding, itemPos.y + m_layout.size.overlayPadding); - ImU32 overlayColor = getAssetTypeOverlayColor(assetData->getType()); + const auto overlayPos = ImVec2(thumbnailEnd.x - m_layout.size.overlayPadding, itemPos.y + m_layout.size.overlayPadding); + const ImU32 overlayColor = getAssetTypeOverlayColor(assetData->getType()); drawList->AddRectFilled(overlayPos, ImVec2(overlayPos.x + m_layout.size.overlaySize, overlayPos.y + m_layout.size.overlaySize), overlayColor); // Draw title const char *assetName = assetData->getMetadata().location.getName().c_str(); - ImVec2 textPos = ImVec2(itemPos.x + (itemSize.x - ImGui::CalcTextSize(assetName).x) * 0.5f, + const auto textPos = ImVec2(itemPos.x + (itemSize.x - ImGui::CalcTextSize(assetName).x) * 0.5f, thumbnailEnd.y + m_layout.size.titlePadding); // Background rectangle for text drawList->AddRectFilled(ImVec2(itemPos.x, thumbnailEnd.y), ImVec2(itemEnd.x, itemEnd.y), m_layout.color.titleBg); @@ -182,41 +150,52 @@ namespace nexo::editor { } ImGui::PopID(); - - } - void AssetManagerWindow::handleSelection(int index, bool isSelected) + void AssetManagerWindow::drawAssetsGrid() { - LOG(NEXO_INFO, "Asset {} {}", index, isSelected ? "deselected" : "selected"); - if (ImGui::GetIO().KeyCtrl) { - if (isSelected) - m_selectedAssets.erase(index); - else - m_selectedAssets.insert(index); - } else if (ImGui::GetIO().KeyShift) { - const int latestSelected = m_selectedAssets.empty() ? 0 : *m_selectedAssets.rbegin(); - if (latestSelected <= index) { - for (int i = latestSelected ; i <= index; ++i) { - m_selectedAssets.insert(i); - } - } else { - for (int i = index; i <= latestSelected; ++i) { - m_selectedAssets.insert(i); + ImVec2 startPos = ImGui::GetCursorScreenPos(); + + ImGuiListClipper clipper; + const auto assets = assets::AssetCatalog::getInstance().getAssets(); + clipper.Begin(static_cast(assets.size()), m_layout.size.itemStep.y); + while (clipper.Step()) { + for (int lineIdx = clipper.DisplayStart; lineIdx < clipper.DisplayEnd; ++lineIdx) { + int startIdx = lineIdx * m_layout.size.columnCount; + int endIdx = std::min(startIdx + m_layout.size.columnCount, static_cast(assets.size())); + + int columns = m_layout.size.columnCount; + float stepX = m_layout.size.itemStep.x; + float stepY = m_layout.size.itemStep.y; + + for (int i = startIdx; i < endIdx; ++i) { + auto idx = static_cast(i); + float col = std::fmod(idx, static_cast(columns)); + float row = std::floor(idx / static_cast(columns)); + ImVec2 itemPos{ + startPos.x + col * stepX, + startPos.y + row * stepY + }; + drawAsset(assets[i], i, itemPos, m_layout.size.itemSize); } } - } else { - m_selectedAssets.clear(); - m_selectedAssets.insert(index); } + clipper.End(); } - ImU32 AssetManagerWindow::getAssetTypeOverlayColor(assets::AssetType type) const { - switch (type) { - case assets::AssetType::TEXTURE: return IM_COL32(200, 70, 70, 255); - case assets::AssetType::MODEL: return IM_COL32(70, 170, 70, 255); - default: return IM_COL32(0, 0, 0, 0); - } - } + void AssetManagerWindow::show() + { + ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); + ImGui::Begin(ICON_FA_FOLDER_OPEN " Asset Manager" NEXO_WND_USTRID_ASSET_MANAGER, &m_opened, ImGuiWindowFlags_MenuBar); + beginRender(NEXO_WND_USTRID_ASSET_MANAGER); + + drawMenuBar(); -} // namespace nexo::editor + float availWidth = ImGui::GetContentRegionAvail().x; + calculateLayout(availWidth); + + drawAssetsGrid(); + + ImGui::End(); + } +} diff --git a/editor/src/DocumentWindows/AssetManager/Shutdown.cpp b/editor/src/DocumentWindows/AssetManager/Shutdown.cpp new file mode 100644 index 000000000..0bd9b07e8 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/Shutdown.cpp @@ -0,0 +1,24 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the shutdown of the asset manager window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" + +namespace nexo::editor { + + void AssetManagerWindow::shutdown() + { + //Nothing to do in the shutdown for now + } + +} diff --git a/editor/src/DocumentWindows/AssetManager/Update.cpp b/editor/src/DocumentWindows/AssetManager/Update.cpp new file mode 100644 index 000000000..475078433 --- /dev/null +++ b/editor/src/DocumentWindows/AssetManager/Update.cpp @@ -0,0 +1,24 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the update function of the asset manager window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "AssetManagerWindow.hpp" + +namespace nexo::editor { + + void AssetManagerWindow::update() + { + // Nothing to do for now + } + +} diff --git a/editor/src/DocumentWindows/ConsoleWindow.cpp b/editor/src/DocumentWindows/ConsoleWindow.cpp deleted file mode 100644 index bba45007f..000000000 --- a/editor/src/DocumentWindows/ConsoleWindow.cpp +++ /dev/null @@ -1,296 +0,0 @@ -//// ConsoleWindow.cpp //////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Guillaume HEIN -// Date: 10/11/2024 -// Description: Source file for the console window class -// -/////////////////////////////////////////////////////////////////////////////// - -#include -#include -#include "ConsoleWindow.hpp" - -#include - -namespace nexo::editor { - /** - * @brief Converts a loguru verbosity level to its corresponding string label. - * - * This function maps a given loguru verbosity level to a predefined string representation, - * such as "[FATAL]", "[ERROR]", "[WARNING]", "[INFO]", "[INVALID]", "[DEBUG]", or "[DEV]". - * If the provided level does not match any known values, it returns "[UNKNOWN]". - * - * @param level The loguru verbosity level to convert. - * @return std::string The string label corresponding to the provided verbosity level. - */ - static inline std::string verbosityToString(const loguru::Verbosity level) - { - switch (level) - { - case loguru::Verbosity_FATAL: return "[FATAL]"; - case loguru::Verbosity_ERROR: return "[ERROR]"; - case loguru::Verbosity_WARNING: return "[WARNING]"; - case loguru::Verbosity_INFO: return "[INFO]"; - case loguru::Verbosity_INVALID: return "[INVALID]"; - case loguru::Verbosity_1: return "[DEBUG]"; - case loguru::Verbosity_2: return "[DEV]"; - default: return "[UNKNOWN]"; - } - } - - /** - * @brief Converts a custom LogLevel to its corresponding loguru::Verbosity level. - * - * Maps each supported LogLevel to a specific loguru verbosity constant. If the provided - * level does not match any known value, the function returns loguru::Verbosity_INVALID. - * - * @param level The custom logging level to convert. - * @return The equivalent loguru verbosity level. - */ - loguru::Verbosity nexoLevelToLoguruLevel(const LogLevel level) - { - switch (level) - { - case LogLevel::FATAL: return loguru::Verbosity_FATAL; - case LogLevel::ERROR: return loguru::Verbosity_ERROR; - case LogLevel::WARN: return loguru::Verbosity_WARNING; - case LogLevel::INFO: return loguru::Verbosity_INFO; - case LogLevel::DEBUG: return loguru::Verbosity_1; - case LogLevel::DEV: return loguru::Verbosity_2; - default: return loguru::Verbosity_INVALID; - } - return loguru::Verbosity_INVALID; - } - - /** - * @brief Returns the color corresponding to a log verbosity level. - * - * Maps the given loguru::Verbosity level to a specific ImVec4 color used for rendering log messages in the console. - * - Fatal and error messages are shown in red. - * - Warnings use yellow. - * - Informational messages appear in blue. - * - Debug levels display distinct pink and purple hues. - * The default color is white for any unrecognized verbosity levels. - * - * @param level The verbosity level for which the corresponding color is computed. - * @return ImVec4 The color associated with the specified verbosity level. - */ - static constexpr ImVec4 getVerbosityColor(loguru::Verbosity level) - { - ImVec4 color; - - switch (level) - { - case loguru::Verbosity_FATAL: // Red - case loguru::Verbosity_ERROR: color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); - break; // Red - case loguru::Verbosity_WARNING: color = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); - break; // Yellow - case loguru::Verbosity_INFO: color = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); - break; // Blue - case loguru::Verbosity_1: color = ImVec4(0.898f, 0.0f, 1.0f, 1.0f); // Debug - break; // Pink - case loguru::Verbosity_2: color = ImVec4(0.388f, 0.055f, 0.851f, 1.0f); // Debug - break; // Purple - default: color = ImVec4(1, 1, 1, 1); // White - } - return color; - } - - - void ConsoleWindow::loguruCallback([[maybe_unused]] void *userData, - const loguru::Message &message) - { - const auto console = static_cast(userData); - console->addLog({ - .verbosity = message.verbosity, - .message = message.message, - .prefix = message.prefix - }); - } - - - ConsoleWindow::ConsoleWindow(const std::string &windowName, WindowRegistry ®istry) : ADocumentWindow(windowName, registry) - { - loguru::add_callback(LOGURU_CALLBACK_NAME, &ConsoleWindow::loguruCallback, - this, loguru::Verbosity_MAX); - - auto engineLogCallback = [](const LogLevel level, const std::string &message) { - const auto loguruLevel = nexoLevelToLoguruLevel(level); - VLOG_F(loguruLevel, "%s", message.c_str()); - }; - Logger::setCallback(engineLogCallback); - }; - - void ConsoleWindow::setup() - { - //All the setup is made in the constructor because the rest of the editor needs the log setup before setting up the windows - } - - void ConsoleWindow::shutdown() - { - clearLog(); - } - - void ConsoleWindow::addLog(const LogMessage &message) - { - m_logs.push_back(message); - } - - void ConsoleWindow::clearLog() - { - m_logs.clear(); - items.clear(); - } - - template - void ConsoleWindow::addLog(const char *fmt, Args &&... args) - { - try - { - std::string formattedMessage = std::vformat(fmt, std::make_format_args(std::forward(args)...)); - items.emplace_back(formattedMessage); - } catch (const std::format_error &e) - { - items.emplace_back(std::format("[Error formatting log message]: {}", e.what())); - } - - scrollToBottom = true; - } - - void ConsoleWindow::executeCommand(const char *command_line) - { - commands.emplace_back(command_line); - addLog("# {}\n", command_line); - } - - void ConsoleWindow::calcLogPadding() - { - m_logPadding = 0.0f; - for (const auto &[verbosity, message, prefix]: m_logs) - { - if (!selectedVerbosityLevels.contains(verbosity)) - continue; - - const std::string tag = verbosityToString(verbosity); - const ImVec2 textSize = ImGui::CalcTextSize(tag.c_str()); - if (textSize.x > m_logPadding) - { - m_logPadding = textSize.x; - } - } - m_logPadding += ImGui::GetStyle().ItemSpacing.x; - } - - - void ConsoleWindow::displayLog(loguru::Verbosity verbosity, const std::string &msg) const - { - ImVec4 color = getVerbosityColor(verbosity); - ImGui::PushStyleColor(ImGuiCol_Text, color); - - const std::string tag = verbosityToString(verbosity); - ImGui::TextUnformatted(tag.c_str()); - ImGui::PopStyleColor(); - - ImGui::SameLine(); - ImGui::SetCursorPosX(m_logPadding); - - ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x); - ImGui::TextWrapped("%s", msg.c_str()); - ImGui::PopTextWrapPos(); - } - - void ConsoleWindow::showVerbositySettingsPopup() - { - ImGui::Text("Select Verbosity Levels"); - ImGui::Separator(); - - const struct { - loguru::Verbosity level; - const char *name; - } levels[] = { - {loguru::Verbosity_FATAL, "FATAL"}, - {loguru::Verbosity_ERROR, "ERROR"}, - {loguru::Verbosity_WARNING, "WARNING"}, - {loguru::Verbosity_INFO, "INFO"}, - {loguru::Verbosity_1, "DEBUG"}, - {loguru::Verbosity_2, "DEV"}, - }; - - for (const auto &[level, name]: levels) - { - bool selected = (selectedVerbosityLevels.contains(level)); - if (ImGui::Checkbox(name, &selected)) - { - if (selected) - { - selectedVerbosityLevels.insert(level); - calcLogPadding(); - } else - { - selectedVerbosityLevels.erase(level); - calcLogPadding(); - } - } - } - - ImGui::EndPopup(); - } - - void ConsoleWindow::show() - { - ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); - ImGui::Begin(ICON_FA_FILE_TEXT " Console" "###" NEXO_WND_USTRID_CONSOLE, &m_opened, ImGuiWindowFlags_NoCollapse); - firstDockSetup(NEXO_WND_USTRID_CONSOLE); - - const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); - ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeight), false, ImGuiWindowFlags_HorizontalScrollbar); - - if (m_logPadding == 0.0f) - calcLogPadding(); - - auto id = 0; - for (const auto &[verbosity, message, prefix]: m_logs) - { - if (!selectedVerbosityLevels.contains(verbosity)) - continue; - - ImGui::PushID(id++); - displayLog(verbosity, message); - ImGui::PopID(); - } - - if (scrollToBottom) - ImGui::SetScrollHereY(1.0f); - scrollToBottom = false; - - ImGui::EndChild(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f); - - if (ImGui::InputText("Input", inputBuf, IM_ARRAYSIZE(inputBuf), ImGuiInputTextFlags_EnterReturnsTrue)) - { - executeCommand(inputBuf); - std::memset(inputBuf, '\0', sizeof(inputBuf)); - } - - ImGui::SameLine(); - if (ImGui::Button("...")) - ImGui::OpenPopup("VerbositySettings"); - - if (ImGui::BeginPopup("VerbositySettings")) - showVerbositySettingsPopup(); - - ImGui::End(); - } - - void ConsoleWindow::update() - { - //No need to update anything - } -} diff --git a/editor/src/DocumentWindows/ConsoleWindow.hpp b/editor/src/DocumentWindows/ConsoleWindow.hpp deleted file mode 100644 index c8c0531d5..000000000 --- a/editor/src/DocumentWindows/ConsoleWindow.hpp +++ /dev/null @@ -1,139 +0,0 @@ -//// ConsoleWindow.hpp //////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Guillaume HEIN -// Date: 10/11/2024 -// Description: Header file for the console window class -// -/////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "ADocumentWindow.hpp" -#include "Editor.hpp" - -namespace nexo::editor { - - constexpr auto LOGURU_CALLBACK_NAME = "GEE"; - - struct LogMessage { - loguru::Verbosity verbosity; - std::string message; - std::string prefix; - }; - - class ConsoleWindow final : public ADocumentWindow { - public: - /** - * @brief Constructs and initializes a ConsoleWindow. - * - * This constructor sets up the console's logging functionality by registering a loguru callback via - * loguru::add_callback to route log messages to the console (using the static loguruCallback) and by - * establishing an engine log callback that maps custom LogLevel messages to loguru verbosity levels - * using nexoLevelToLoguruLevel before logging them with VLOG_F. - * - * @param registry The window registry used to register this console window. - */ - explicit ConsoleWindow(const std::string &windowName, WindowRegistry ®istry); - - /** - * @brief Destructor that cleans up the ConsoleWindow. - * - * Removes the registered loguru callback identified by LOGURU_CALLBACK_NAME to prevent further logging after the window is destroyed. - */ - ~ConsoleWindow() override - { - loguru::remove_callback(LOGURU_CALLBACK_NAME); - }; - - void setup() override; - - /** - * @brief Clears all stored log entries during shutdown. - * - * This method resets the console's log by invoking clearLog(), ensuring that all previous - * log entries are removed as part of the shutdown process. - */ - void shutdown() override; - - /** - * @brief Renders the console window interface. - * - * This method initializes and displays the console window using ImGui. It sets a predefined window size and - * creates a scrolling region to display log messages filtered by selected verbosity levels. When the console is - * opened for the first time, it performs an initial docking setup. The function also adjusts log padding for proper - * alignment and automatically scrolls to the bottom if new messages have been added. - * - * An input field is provided for entering commands, which are executed upon pressing Enter, with the input buffer - * cleared afterward. Additionally, a popup for adjusting verbosity settings is available, accessible via a button. - */ - void show() override; - void update() override; - - template - void addLog(const char* fmt, Args&&... args); - void executeCommand(const char* command_line); - - private: - float m_logPadding = 0.0f; - char inputBuf[512] = {}; - std::deque items; - bool scrollToBottom = true; - std::vector commands; // History of executed commands. - - std::set selectedVerbosityLevels = { - loguru::Verbosity_FATAL, - loguru::Verbosity_ERROR, - loguru::Verbosity_WARNING, - loguru::Verbosity_INFO, - }; - - std::vector m_logs; - - /** - * @brief Clears all log entries and display items. - * - * Removes all log messages from the internal storage and resets the list of display items. - */ - void clearLog(); - - /** - * @brief Appends a log message to the console's log collection. - * - * This method adds the provided log message to the internal container, ensuring it is available - * for display in the console window. - * - * @param message The log message to append. - */ - void addLog(const LogMessage& message); - void displayLog(loguru::Verbosity verbosity, const std::string &msg) const; - void showVerbositySettingsPopup(); - - /** - * @brief Updates the horizontal padding for log entries. - * - * Iterates over the stored log messages to compute the maximum width of the verbosity tag text for - * messages that are currently visible (filtered by selected verbosity levels). The computed maximum - * width is then increased by the spacing defined in the ImGui style to ensure proper alignment in the UI. - */ - void calcLogPadding(); - - /** - * @brief Processes a loguru message and adds it to the console log. - * - * Converts a loguru message to the internal log format and appends it to the ConsoleWindow's log list. - * The userData pointer is cast to a ConsoleWindow instance, which is then used to record the message details, - * including verbosity, message content, and prefix. - * - * @param userData Pointer to the ConsoleWindow instance. - * @param message The loguru message carrying the log details. - */ - static void loguruCallback(void *userData, const loguru::Message& message); - }; - -} diff --git a/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp b/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp new file mode 100644 index 000000000..3d25cb82b --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/ConsoleWindow.hpp @@ -0,0 +1,247 @@ +//// ConsoleWindow.hpp //////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 10/11/2024 +// Description: Header file for the console window class +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "ADocumentWindow.hpp" +#include "Editor.hpp" +#include + +namespace nexo::editor { + + std::string verbosityToString(loguru::Verbosity level); + loguru::Verbosity nexoLevelToLoguruLevel(LogLevel level); + const ImVec4 getVerbosityColor(loguru::Verbosity level); + std::string generateLogFilePath(); + + constexpr auto LOGURU_CALLBACK_NAME = "GEE"; + + /** + * @brief Structure representing a formatted log message. + * + * Contains all necessary information for displaying a log message in the console, + * including its verbosity level, the message text, and an optional prefix. + */ + struct LogMessage { + loguru::Verbosity verbosity; ///< The verbosity level of the log message + std::string message; ///< The content of the log message + std::string prefix; ///< Optional prefix for the log message + }; + + + /** + * @brief Console window for displaying and managing application logs. + * + * This class provides a visual interface for viewing log messages with different + * verbosity levels, executing commands, and managing log settings. It integrates + * with the loguru logging system to display real-time log messages. + */ + class ConsoleWindow final : public ADocumentWindow { + public: + /** + * @brief Constructs and initializes a ConsoleWindow. + * + * This constructor sets up the console's logging functionality by registering a loguru callback via + * loguru::add_callback to route log messages to the console (using the static loguruCallback) and by + * establishing an engine log callback that maps custom LogLevel messages to loguru verbosity levels + * using nexoLevelToLoguruLevel before logging them with VLOG_F. + * + * @param registry The window registry used to register this console window. + */ + explicit ConsoleWindow(const std::string &windowName, WindowRegistry ®istry); + + /** + * @brief Destructor that cleans up the ConsoleWindow. + * + * Removes the registered loguru callback identified by LOGURU_CALLBACK_NAME to prevent further logging after the window is destroyed. + */ + ~ConsoleWindow() override + { + loguru::remove_callback(LOGURU_CALLBACK_NAME); + }; + + // No-op method in this class + void setup() override; + + /** + * @brief Clears all stored log entries during shutdown. + * + * This method resets the console's log by invoking clearLog(), ensuring that all previous + * log entries are removed as part of the shutdown process. + */ + void shutdown() override; + + /** + * @brief Renders the console window interface. + * + * This method initializes and displays the console window using ImGui. It sets a predefined window size and + * creates a scrolling region to display log messages filtered by selected verbosity levels. When the console is + * opened for the first time, it performs an initial docking setup. The function also adjusts log padding for proper + * alignment and automatically scrolls to the bottom if new messages have been added. + * + * An input field is provided for entering commands, which are executed upon pressing Enter, with the input buffer + * cleared afterward. Additionally, a popup for adjusting verbosity settings is available, accessible via a button. + */ + void show() override; + + // No-op method in this class + void update() override; + + /** + * @brief Executes a command entered in the console. + * + * Processes the given command line, adds it to the command history, + * and displays it in the log. + * + * @param commandLine The command text to execute. + * @note not implemented yet + */ + void executeCommand(const char* commandLine); + + private: + char m_inputBuf[512] = {}; + std::vector m_commands; // History of executed commands. + + std::string m_logFilePath; + bool m_exportLog = true; + + bool m_scrollToBottom = true; + + + std::set m_selectedVerbosityLevels = { + loguru::Verbosity_FATAL, + loguru::Verbosity_ERROR, + loguru::Verbosity_WARNING, + loguru::Verbosity_INFO, + loguru::Verbosity_1, + }; + + float m_logPadding = 0.0f; + std::vector m_logs; + size_t m_maxLogCapacity = 200; + std::vector m_bufferLogsToExport; + size_t m_maxBufferLogToExportCapacity = 20; + + + /** + * @brief Clears all log entries and display items. + * + * Removes all log messages from the internal storage and resets the list of display items. + */ + void clearLog(); + + /** + * @brief Appends a log message to the console's log collection. + * + * This method adds the provided log message to the internal container, ensuring it is available + * for display in the console window. + * + * @param message The log message to append. + */ + void addLog(const LogMessage& message); + + /** + * @brief Adds a formatted log message to the console. + * + * Creates a log message using printf-style formatting and adds it to the log collection. + * + * @tparam Args Variadic template for format arguments + * @param fmt Format string similar to printf + * @param args Arguments for the format string + */ + template + void addLog(std::format_string fmt, Args&&... args) + { + try { + std::string formattedString = std::format(fmt, std::forward(args)...); + + LogMessage newMessage; + newMessage.verbosity = loguru::Verbosity_1; + newMessage.message = formattedString; + newMessage.prefix = ""; + m_logs.push_back(newMessage); + } catch (const std::exception &e) { + LogMessage newMessage; + newMessage.verbosity = loguru::Verbosity_ERROR; + + char errorBuffer[1024]; + + // format up to sizeof(errorBuffer)-1 characters + auto result = std::format_to_n( + std::begin(errorBuffer), + std::size(errorBuffer) - 1, + "Error formatting log message: {}", + e.what() + ); + + // null-terminate + *result.out = '\0'; + + newMessage.message = std::string(errorBuffer); + newMessage.prefix = ""; + m_logs.push_back(newMessage); + } + + m_scrollToBottom = true; + } + + /** + * @brief Displays a single log entry in the console UI. + * + * Renders a log message with appropriate styling based on its verbosity level. + * + * @param verbosity The verbosity level that determines the message's color + * @param msg The message text to display + */ + void displayLog(loguru::Verbosity verbosity, const std::string &msg) const; + + /** + * @brief Exports buffered logs to the log file. + * + * Writes any logs in the export buffer to the configured log file, + * helping to prevent memory buildup from excessive logging. + */ + void exportLogsBuffered() const; + + /** + * @brief Displays the popup for configuring verbosity settings. + * + * Shows a popup menu that allows the user to select which verbosity levels + * to display in the console and configure other log-related settings. + */ + void showVerbositySettingsPopup(); + + /** + * @brief Updates the horizontal padding for log entries. + * + * Iterates over the stored log messages to compute the maximum width of the verbosity tag text for + * messages that are currently visible (filtered by selected verbosity levels). The computed maximum + * width is then increased by the spacing defined in the ImGui style to ensure proper alignment in the UI. + */ + void calcLogPadding(); + + /** + * @brief Processes a loguru message and adds it to the console log. + * + * Converts a loguru message to the internal log format and appends it to the ConsoleWindow's log list. + * The userData pointer is cast to a ConsoleWindow instance, which is then used to record the message details, + * including verbosity, message content, and prefix. + * + * @param userData Pointer to the ConsoleWindow instance. + * @param message The loguru message carrying the log details. + */ + static void loguruCallback(void *userData, const loguru::Message& message); + }; + +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Init.cpp b/editor/src/DocumentWindows/ConsoleWindow/Init.cpp new file mode 100644 index 000000000..f1aec186e --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Init.cpp @@ -0,0 +1,52 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the setup function of the console window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" +#include "Path.hpp" + +namespace nexo::editor { + void ConsoleWindow::loguruCallback([[maybe_unused]] void *userData, + const loguru::Message &message) + { + const auto console = static_cast(userData); + LogMessage newMessage; + newMessage.verbosity = message.verbosity; + newMessage.message = message.message; + newMessage.prefix = message.prefix; + console->addLog(newMessage); + } + + + ConsoleWindow::ConsoleWindow(const std::string &windowName, WindowRegistry ®istry) : ADocumentWindow(windowName, registry) + { + loguru::add_callback(LOGURU_CALLBACK_NAME, &ConsoleWindow::loguruCallback, + this, loguru::Verbosity_MAX); + + auto engineLogCallback = [](const LogLevel level, const std::source_location& loc, const std::string &message) { + const auto loguruLevel = nexoLevelToLoguruLevel(level); + if (loguruLevel > loguru::current_verbosity_cutoff()) + return; + loguru::log(loguruLevel, loc.file_name(), loc.line(), "%s", message.c_str()); + }; + Logger::setCallback(engineLogCallback); + m_logFilePath = Path::resolvePathRelativeToExe(generateLogFilePath()).string(); + m_logs.reserve(m_maxLogCapacity); + m_bufferLogsToExport.reserve(m_maxBufferLogToExportCapacity); + }; + + void ConsoleWindow::setup() + { + //All the setup is made in the constructor because the rest of the editor needs the log setup before setting up the windows + } +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Log.cpp b/editor/src/DocumentWindows/ConsoleWindow/Log.cpp new file mode 100644 index 000000000..e2af09fce --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Log.cpp @@ -0,0 +1,59 @@ +//// Log.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the log function of the console window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" +#include + +namespace nexo::editor { + + void ConsoleWindow::addLog(const LogMessage &message) + { + if (m_logs.size() >= m_maxLogCapacity) { + m_bufferLogsToExport.push_back(message); + m_logs.erase(m_logs.begin()); + } + + if (m_bufferLogsToExport.size() > m_maxBufferLogToExportCapacity) { + exportLogsBuffered(); + m_bufferLogsToExport.clear(); + } + + m_logs.push_back(message); + } + + void ConsoleWindow::clearLog() + { + exportLogsBuffered(); + if (m_exportLog) { + std::ofstream logFile(m_logFilePath, std::ios::app); + for (const auto& log : m_logs) { + logFile << verbosityToString(log.verbosity) << " " + << log.message << std::endl; + } + } + + m_logs.clear(); + } + + void ConsoleWindow::exportLogsBuffered() const + { + if (!m_exportLog) + return; + std::ofstream logFile(m_logFilePath, std::ios::app); + for (const auto& log : m_bufferLogsToExport) { + logFile << verbosityToString(log.verbosity) << " " + << log.message << std::endl; + } + } +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Show.cpp b/editor/src/DocumentWindows/ConsoleWindow/Show.cpp new file mode 100644 index 000000000..bec5ba2dc --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Show.cpp @@ -0,0 +1,147 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the rendering of the console window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" +#include "IconsFontAwesome.h" +#include "ImNexo/Elements.hpp" +#include "utils/FileSystem.hpp" +#include "Path.hpp" + +namespace nexo::editor { + + void ConsoleWindow::showVerbositySettingsPopup() + { + ImGui::Text("Select Verbosity Levels"); + ImGui::Separator(); + + const struct { + loguru::Verbosity level; + const char *name; + } levels[] = { + {loguru::Verbosity_FATAL, "FATAL"}, + {loguru::Verbosity_ERROR, "ERROR"}, + {loguru::Verbosity_WARNING, "WARNING"}, + {loguru::Verbosity_INFO, "INFO"}, + {loguru::Verbosity_1, "USER"}, + {loguru::Verbosity_2, "DEBUG"}, + {loguru::Verbosity_3, "DEV"} + }; + + for (const auto &[level, name]: levels) + { + bool selected = (m_selectedVerbosityLevels.contains(level)); + if (ImGui::Checkbox(name, &selected)) + { + if (selected) + { + m_selectedVerbosityLevels.insert(level); + calcLogPadding(); + } else + { + m_selectedVerbosityLevels.erase(level); + calcLogPadding(); + } + } + } + + ImGui::Separator(); + ImGui::Checkbox("File logging", &m_exportLog); + if (ImNexo::Button("Open log folder")) + utils::openFolder(Path::resolvePathRelativeToExe("../logs").string()); + + ImGui::EndPopup(); + } + + void ConsoleWindow::executeCommand(const char *commandLine) + { + m_commands.emplace_back(commandLine); + addLog("{}", commandLine); + } + + void ConsoleWindow::calcLogPadding() + { + m_logPadding = 0.0f; + + for (const auto &selectedLevel : m_selectedVerbosityLevels) { + const std::string tag = verbosityToString(selectedLevel); + const ImVec2 textSize = ImGui::CalcTextSize(tag.c_str()); + if (textSize.x > m_logPadding) + m_logPadding = textSize.x; + } + m_logPadding += ImGui::GetStyle().ItemSpacing.x; + } + + void ConsoleWindow::displayLog(loguru::Verbosity verbosity, const std::string &msg) const + { + ImVec4 color = getVerbosityColor(verbosity); + ImGui::PushStyleColor(ImGuiCol_Text, color); + + const std::string tag = verbosityToString(verbosity); + ImGui::TextUnformatted(tag.c_str()); + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::SetCursorPosX(m_logPadding); + + ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x); + ImGui::TextWrapped("%s", msg.c_str()); + ImGui::PopTextWrapPos(); + } + + void ConsoleWindow::show() + { + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); + ImGui::Begin(ICON_FA_FILE_TEXT " Console" NEXO_WND_USTRID_CONSOLE, &m_opened, ImGuiWindowFlags_NoCollapse); + beginRender(NEXO_WND_USTRID_CONSOLE); + + const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeight), false, ImGuiWindowFlags_HorizontalScrollbar); + + if (m_logPadding == 0.0f) + calcLogPadding(); + + auto id = 0; + for (const auto &[verbosity, message, prefix]: m_logs) + { + if (!m_selectedVerbosityLevels.contains(verbosity)) + continue; + + ImGui::PushID(id++); + displayLog(verbosity, message); + ImGui::PopID(); + } + + if (m_scrollToBottom) + ImGui::SetScrollHereY(1.0f); + m_scrollToBottom = false; + + ImGui::EndChild(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f); + + if (ImGui::InputText("Input", m_inputBuf, IM_ARRAYSIZE(m_inputBuf), ImGuiInputTextFlags_EnterReturnsTrue)) + { + executeCommand(m_inputBuf); + std::memset(m_inputBuf, '\0', sizeof(m_inputBuf)); + } + + ImGui::SameLine(); + if (ImNexo::Button("...")) + ImGui::OpenPopup("VerbositySettings"); + + if (ImGui::BeginPopup("VerbositySettings")) + showVerbositySettingsPopup(); + + ImGui::End(); + } +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Shutdown.cpp b/editor/src/DocumentWindows/ConsoleWindow/Shutdown.cpp new file mode 100644 index 000000000..a2bb7ec7f --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Shutdown.cpp @@ -0,0 +1,24 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the shutdown of the console window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" + +namespace nexo::editor { + + void ConsoleWindow::shutdown() + { + clearLog(); + } + +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Update.cpp b/editor/src/DocumentWindows/ConsoleWindow/Update.cpp new file mode 100644 index 000000000..25c0711db --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Update.cpp @@ -0,0 +1,22 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the update of the console window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" + +namespace nexo::editor { + void ConsoleWindow::update() + { + //No need to update anything + } +} diff --git a/editor/src/DocumentWindows/ConsoleWindow/Utils.cpp b/editor/src/DocumentWindows/ConsoleWindow/Utils.cpp new file mode 100644 index 000000000..9a517ce9f --- /dev/null +++ b/editor/src/DocumentWindows/ConsoleWindow/Utils.cpp @@ -0,0 +1,117 @@ +//// Utils.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the utils methods +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ConsoleWindow.hpp" +#include + +namespace nexo::editor { + /** + * @brief Converts a loguru verbosity level to its corresponding string label. + * + * This function maps a given loguru verbosity level to a predefined string representation, + * such as "[FATAL]", "[ERROR]", "[WARNING]", "[INFO]", "[INVALID]", "[DEBUG]", or "[DEV]". + * If the provided level does not match any known values, it returns "[UNKNOWN]". + * + * @param level The loguru verbosity level to convert. + * @return std::string The string label corresponding to the provided verbosity level. + */ + std::string verbosityToString(const loguru::Verbosity level) + { + switch (level) + { + case loguru::Verbosity_FATAL: return "[FATAL]"; + case loguru::Verbosity_ERROR: return "[ERROR]"; + case loguru::Verbosity_WARNING: return "[WARNING]"; + case loguru::Verbosity_INFO: return "[INFO]"; + case loguru::Verbosity_INVALID: return "[INVALID]"; + case loguru::Verbosity_1: return "[USER]"; + case loguru::Verbosity_2: return "[DEBUG]"; + case loguru::Verbosity_3: return "[DEV]"; + default: return "[UNKNOWN]"; + } + } + + /** + * @brief Converts a custom LogLevel to its corresponding loguru::Verbosity level. + * + * Maps each supported LogLevel to a specific loguru verbosity constant. If the provided + * level does not match any known value, the function returns loguru::Verbosity_INVALID. + * + * @param level The custom logging level to convert. + * @return The equivalent loguru verbosity level. + */ + loguru::Verbosity nexoLevelToLoguruLevel(const LogLevel level) + { + switch (level) + { + case LogLevel::FATAL: return loguru::Verbosity_FATAL; + case LogLevel::ERR: return loguru::Verbosity_ERROR; + case LogLevel::WARN: return loguru::Verbosity_WARNING; + case LogLevel::INFO: return loguru::Verbosity_INFO; + case LogLevel::USER: return loguru::Verbosity_1; + case LogLevel::DEBUG: return loguru::Verbosity_2; + case LogLevel::DEV: return loguru::Verbosity_3; + default: return loguru::Verbosity_INVALID; + } + } + + /** + * @brief Returns the color corresponding to a log verbosity level. + * + * Maps the given loguru::Verbosity level to a specific ImVec4 color used for rendering log messages in the console. + * - Fatal and error messages are shown in red. + * - Warnings use yellow. + * - Informational messages appear in blue. + * - Debug levels display distinct pink and purple hues. + * The default color is white for any unrecognized verbosity levels. + * + * @param level The verbosity level for which the corresponding color is computed. + * @return ImVec4 The color associated with the specified verbosity level. + */ + const ImVec4 getVerbosityColor(const loguru::Verbosity level) + { + ImVec4 color; + + switch (level) + { + case loguru::Verbosity_FATAL: // Red + case loguru::Verbosity_ERROR: color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); + break; // Red + case loguru::Verbosity_WARNING: color = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); + break; // Yellow + case loguru::Verbosity_INFO: color = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); + break; // Blue + case loguru::Verbosity_1: color = ImVec4(0.09f, 0.67f, 0.14f, 1.0f); // User + break; // Green + case loguru::Verbosity_2: color = ImVec4(0.898f, 0.0f, 1.0f, 1.0f); // Debug + break; // Pink + case loguru::Verbosity_3: color = ImVec4(0.388f, 0.055f, 0.851f, 1.0f); // Dev + break; // Purple + default: color = ImVec4(1, 1, 1, 1); // White + } + return color; + } + + std::string generateLogFilePath() + { + using namespace std::chrono; + + // Truncate to seconds precision + auto now = floor(system_clock::now()); + zoned_time local_zoned{ current_zone(), now }; + + std::string ts = std::format("{:%Y%m%d_%H%M%S}", local_zoned); + return std::format("../logs/NEXO-{}.log", ts); + } +} diff --git a/editor/src/DocumentWindows/EditorScene.cpp b/editor/src/DocumentWindows/EditorScene.cpp deleted file mode 100644 index 143b29ab8..000000000 --- a/editor/src/DocumentWindows/EditorScene.cpp +++ /dev/null @@ -1,276 +0,0 @@ -//// EditorScene.cpp ////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Guillaume HEIN -// Date: 10/11/2024 -// Description: Source for the editor scene document window -// -/////////////////////////////////////////////////////////////////////////////// - -#include "EditorScene.hpp" - -#include - -#include "EntityFactory3D.hpp" -#include "LightFactory.hpp" -#include "CameraFactory.hpp" -#include "Nexo.hpp" -#include "WindowRegistry.hpp" -#include "components/Camera.hpp" -#include "components/Uuid.hpp" -#include "math/Matrix.hpp" -#include "context/Selector.hpp" -#include "utils/String.hpp" - -#include -#include -#include -#include - -namespace nexo::editor { - - void EditorScene::setup() - { - setupImguizmo(); - setupWindow(); - setupScene(); - } - - void EditorScene::setupScene() - { - auto &app = getApp(); - - // New handling - m_sceneId = static_cast(app.getSceneManager().createScene(m_windowName)); - renderer::FramebufferSpecs framebufferSpecs; - framebufferSpecs.attachments = { - renderer::FrameBufferTextureFormats::RGBA8, renderer::FrameBufferTextureFormats::RED_INTEGER, renderer::FrameBufferTextureFormats::Depth - }; - framebufferSpecs.width = static_cast(m_viewSize.x); - framebufferSpecs.height = static_cast(m_viewSize.y); - const auto renderTarget = renderer::Framebuffer::create(framebufferSpecs); - m_activeCamera = CameraFactory::createPerspectiveCamera({0.0f, 0.0f, 0.0f}, static_cast(m_viewSize.x), static_cast(m_viewSize.y), renderTarget); - m_cameras.insert(static_cast(m_activeCamera)); - app.getSceneManager().getScene(m_sceneId).addEntity(static_cast(m_activeCamera)); - components::PerspectiveCameraController controller; - Application::m_coordinator->addComponent(static_cast(m_activeCamera), controller); - - m_sceneUuid = app.getSceneManager().getScene(m_sceneId).getUuid(); - if (m_defaultScene) - loadDefaultEntities(); - } - - void EditorScene::setupImguizmo() const - { - ImGuizmo::SetOrthographic(true); - } - - void EditorScene::loadDefaultEntities() const - { - auto &app = getApp(); - scene::Scene &scene = app.getSceneManager().getScene(m_sceneId); - const ecs::Entity ambientLight = LightFactory::createAmbientLight({0.5f, 0.5f, 0.5f}); - scene.addEntity(ambientLight); - const ecs::Entity pointLight = LightFactory::createPointLight({1.2f, 5.0f, 0.1f}); - scene.addEntity(pointLight); - const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.2f, -1.0f, -0.3f}); - scene.addEntity(directionalLight); - const ecs::Entity spotLight = LightFactory::createSpotLight({0.0f, 0.5f, -2.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}); - scene.addEntity(spotLight); - const ecs::Entity basicCube = EntityFactory3D::createCube({0.0f, -5.0f, -5.0f}, {20.0f, 1.0f, 20.0f}, - {0.0f, 0.0f, 0.0f}, {1.0f, 0.5f, 0.31f, 1.0f}); - app.getSceneManager().getScene(m_sceneId).addEntity(basicCube); - } - - void EditorScene::setupWindow() - { - constexpr auto size = ImVec2(1280, 720); - m_viewSize = size; - } - - void EditorScene::shutdown() - { - // Should probably check if it is necessary to delete the scene here ? (const for now) - } - - void EditorScene::handleKeyEvents() const - { - // Will be implemeneted later - } - - void EditorScene::deleteCamera(const ecs::Entity cameraId) - { - if (cameraId == m_activeCamera) - m_activeCamera = -1; - m_cameras.erase(cameraId); - if (!m_cameras.empty()) - m_activeCamera = *m_cameras.begin(); - } - - void EditorScene::renderToolbar() const - { - // Empty for now, will add it later - } - - void EditorScene::renderGizmo() const - { - const auto &coord = nexo::Application::m_coordinator; - auto const &selector = Selector::get(); - if (selector.getSelectionType() != SelectionType::ENTITY || - selector.getSelectedScene() != m_sceneId) - return; - const ecs::Entity entity = selector.getSelectedEntity(); - const auto &transformCameraComponent = coord->getComponent(m_activeCamera); - const auto &cameraComponent = coord->getComponent(m_activeCamera); - ImGuizmo::SetOrthographic(cameraComponent.type == components::CameraType::ORTHOGRAPHIC); - ImGuizmo::SetDrawlist(); - ImGuizmo::SetID(static_cast(entity)); - ImGuizmo::SetRect(m_viewPosition.x, m_viewPosition.y, m_viewSize.x, m_viewSize.y); - glm::mat4 viewMatrix = cameraComponent.getViewMatrix(transformCameraComponent); - glm::mat4 projectionMatrix = cameraComponent.getProjectionMatrix(); - const auto transf = coord->tryGetComponent(entity); - if (!transf) - return; - const glm::mat4 rotationMat = glm::toMat4(transf->get().quat); - glm::mat4 transformMatrix = glm::translate(glm::mat4(1.0f), transf->get().pos) * - rotationMat * - glm::scale(glm::mat4(1.0f), {transf->get().size.x, transf->get().size.y, transf->get().size.z}); - ImGuizmo::Enable(true); - ImGuizmo::Manipulate(glm::value_ptr(viewMatrix), glm::value_ptr(projectionMatrix), - m_currentGizmoOperation, - ImGuizmo::MODE::WORLD, - glm::value_ptr(transformMatrix)); - - glm::vec3 translation(0); - glm::vec3 scale(0); - glm::quat quaternion; - - math::decomposeTransformQuat(transformMatrix, translation, quaternion, scale); - - if (ImGuizmo::IsUsing()) - { - transf->get().pos = translation; - transf->get().quat = quaternion; - transf->get().size = scale; - } - } - - void EditorScene::renderView() - { - const auto viewPortOffset = ImGui::GetCursorPos(); - auto &cameraComponent = Application::m_coordinator->getComponent(m_activeCamera); - - // Resize handling - if (const ImVec2 viewportPanelSize = ImGui::GetContentRegionAvail(); - m_viewSize.x != viewportPanelSize.x || m_viewSize.y != viewportPanelSize.y) - { - cameraComponent.resize(static_cast(viewportPanelSize.x), - static_cast(viewportPanelSize.y)); - - m_viewSize.x = viewportPanelSize.x; - m_viewSize.y = viewportPanelSize.y; - } - - // Render framebuffer - const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); - ImGui::Image(static_cast(static_cast(textureId)), m_viewSize, ImVec2(0, 1), ImVec2(1, 0)); - - const auto windowSize = ImGui::GetWindowSize(); - auto minBounds = ImGui::GetWindowPos(); - - minBounds.x += viewPortOffset.x; - minBounds.y += viewPortOffset.y; - - const ImVec2 maxBounds = {minBounds.x + windowSize.x, minBounds.y + windowSize.y}; - m_viewportBounds[0] = minBounds; - m_viewportBounds[1] = maxBounds; - } - - void EditorScene::show() - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); - ImGui::SetNextWindowSizeConstraints(ImVec2(480, 270), ImVec2(1920, 1080)); - auto &selector = Selector::get(); - - if (ImGui::Begin(m_windowName.c_str(), &m_opened, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse)) - { - firstDockSetup(m_windowName); - auto &app = getApp(); - m_viewPosition = ImGui::GetCursorScreenPos(); - - m_focused = ImGui::IsWindowFocused(); - m_hovered = ImGui::IsWindowHovered(); - app.getSceneManager().getScene(m_sceneId).setActiveStatus(m_focused); - if (m_focused && selector.getSelectedScene() != m_sceneId) - { - selector.setSelectedScene(m_sceneId); - selector.unselectEntity(); - } - - if (m_activeCamera == -1) - { - // No active camera, render the text at the center of the screen - ImVec2 textSize = ImGui::CalcTextSize("No active camera"); - auto textPos = ImVec2((m_viewSize.x - textSize.x) / 2, (m_viewSize.y - textSize.y) / 2); - - ImGui::SetCursorScreenPos(textPos); - ImGui::Text("No active camera"); - } - else - { - renderView(); - renderGizmo(); - } - } - ImGui::End(); - ImGui::PopStyleVar(); - } - - void EditorScene::update() - { - auto &selector = Selector::get(); - //m_windowName = selector.getUiHandle(m_sceneUuid, m_windowName); - if (!m_opened || m_activeCamera == -1) - return; - if (m_focused && m_hovered) - handleKeyEvents(); - - auto const &cameraComponent = Application::m_coordinator->getComponent(static_cast(m_activeCamera)); - runEngine(m_sceneId, RenderingType::FRAMEBUFFER); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGuizmo::IsUsing() && m_focused) - { - auto [mx, my] = ImGui::GetMousePos(); - mx -= m_viewportBounds[0].x; - my -= m_viewportBounds[0].y; - - // Flip the y-coordinate to match opengl texture format (maybe make it modular in some way) - my = m_viewSize.y - my; - - // Mouse is not inside the viewport - if (!(mx >= 0 && my >= 0 && mx < m_viewSize.x && my < m_viewSize.y)) - return; - - cameraComponent.m_renderTarget->bind(); - int data = cameraComponent.m_renderTarget->getPixel(1, static_cast(mx), static_cast(my)); - cameraComponent.m_renderTarget->unbind(); - if (data == -1) - { - selector.unselectEntity(); - return; - } - const auto uuid = Application::m_coordinator->tryGetComponent(data); - if (uuid) - { - selector.setSelectedEntity(uuid->get().uuid, data); - selector.setSelectionType(SelectionType::ENTITY); - } - selector.setSelectedScene(m_sceneId); - } - } - -} diff --git a/editor/src/DocumentWindows/EditorScene.hpp b/editor/src/DocumentWindows/EditorScene.hpp deleted file mode 100644 index 5833dd668..000000000 --- a/editor/src/DocumentWindows/EditorScene.hpp +++ /dev/null @@ -1,130 +0,0 @@ -//// MainScene.hpp //////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 10/11/2024 -// Description: Header file for the main document window -// -/////////////////////////////////////////////////////////////////////////////// -#pragma once - -#include "ADocumentWindow.hpp" -#include "IDocumentWindow.hpp" -#include "core/scene/SceneManager.hpp" -#include -#include - -namespace nexo::editor { - class EditorScene : public ADocumentWindow { - public: - using ADocumentWindow::ADocumentWindow; - - /** - * @brief Initializes the main scene. - * - * Configures essential components of the main scene by sequentially: - * - Setting up ImGuizmo parameters, - * - Initializing the window settings, and - * - Creating and configuring the scene. - */ - void setup() override; - void shutdown() override; - - /** - * @brief Displays the main scene window and updates the active scene selection. - * - * This method creates an ImGui window with specific size constraints and zero padding, - * then determines the window's focus status to update the scene's active state. When focused, - * it sets the current scene as selected in the scene view manager and clears any entity selection. - * Finally, it renders both the scene view and transformation gizmos within the window. - */ - void show() override; - - /** - * @brief Updates the scene by processing input events and rendering the current frame. - * - * This function handles key events and updates the scene by executing the rendering engine in framebuffer mode. - * It processes left mouse clicks when the scene view is focused and ImGuizmo is not active. The mouse position is - * adjusted relative to the viewport and its y-coordinate is flipped to match OpenGL's texture format. If the click - * falls within valid bounds and corresponds to a valid entity (pixel value not equal to -1), the entity is selected - * and the SceneViewManager is notified of the active scene; otherwise, any existing selection is cleared. - * - * The update is skipped entirely if the scene window is not open. - */ - void update() override; - - /** - * @brief Retrieves the unique identifier of the scene. - * - * @return scene::SceneId The identifier of this scene. - */ - [[nodiscard]] scene::SceneId getSceneId() const {return m_sceneId;}; - - - /** - * @brief Removes a camera from the scene and updates the active camera. - * - * Removes the specified camera entity from the collection. If the removed camera was the active one, - * it resets the active camera to an invalid state (-1) and, if any cameras remain, sets the active camera - * to the first available camera in the collection. - * - * @param cameraId The identifier of the camera entity to delete. - */ - void deleteCamera(ecs::Entity cameraId); - - void setDefault() { m_defaultScene = true; }; - - private: - bool m_defaultScene = false; - ImVec2 m_viewSize = {0, 0}; - ImVec2 m_viewPosition = {0, 0}; - ImVec2 m_viewportBounds[2]; - ImGuizmo::OPERATION m_currentGizmoOperation = ImGuizmo::UNIVERSAL; - ImGuizmo::MODE m_currentGizmoMode = ImGuizmo::WORLD; - - int m_sceneId = -1; - std::string m_sceneUuid; - std::set m_cameras; - int m_activeCamera = -1; - - /** - * @brief Sets the main scene window's view size. - * - * Configures the view to a default size of 1280x720 pixels. - */ - void setupWindow(); - void setupImguizmo() const; - void setupScene(); - void loadDefaultEntities() const; - - void handleKeyEvents() const; - - /** - * @brief Renders the toolbar overlay within the main scene view. - * - * This method uses ImGui to display a toolbar that includes buttons for switching between orthographic and perspective camera modes, - * a popup placeholder for adding primitive entities, and a draggable input for adjusting the target frames per second (FPS). - * The toolbar is positioned relative to the current view to align with the scene layout. - */ - void renderToolbar() const; - - /** - * @brief Renders the transformation gizmo for the selected entity. - * - * This method displays an interactive ImGuizmo tool to manipulate the translation, rotation, and scale - * of the currently selected entity. It first verifies that the selection is an entity and that the active - * scene corresponds to the one managed by this instance. The method then retrieves the view and projection - * matrices from the active camera, configures ImGuizmo to match the view's dimensions, and constructs the - * entity's transformation matrix from its current translation, rotation, and scale. - * - * If the gizmo is actively manipulated, the entity's transform component is updated with the new values. - */ - void renderGizmo() const; - void renderView(); - }; -} diff --git a/editor/src/DocumentWindows/EditorScene/EditorScene.hpp b/editor/src/DocumentWindows/EditorScene/EditorScene.hpp new file mode 100644 index 000000000..cf2a4390e --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/EditorScene.hpp @@ -0,0 +1,310 @@ +//// MainScene.hpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 10/11/2024 +// Description: Header file for the main document window +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "ADocumentWindow.hpp" +#include "inputs/WindowState.hpp" +#include "core/scene/SceneManager.hpp" +#include "../PopupManager.hpp" +#include +#include +#include "ImNexo/Widgets.hpp" + +namespace nexo::editor { + + class EditorScene final : public ADocumentWindow { + public: + using ADocumentWindow::ADocumentWindow; + + /** + * @brief Initializes the main scene. + * + * Configures essential components of the main scene by sequentially: + * - Setting up ImGuizmo parameters, + * - Initializing the window settings, and + * - Creating and configuring the scene. + */ + void setup() override; + + // No-op method in this class + void shutdown() override; + + /** + * @brief Displays the main scene window and updates the active scene selection. + * + * This method creates an ImGui window with specific size constraints and zero padding, + * then determines the window's focus status to update the scene's active state. When focused, + * it sets the current scene as selected in the scene view manager and clears any entity selection. + * Finally, it renders both the scene view and transformation gizmos within the window. + */ + void show() override; + + /** + * @brief Updates the scene by processing input events and rendering the current frame. + * + * This function handles key events and updates the scene by executing the rendering engine in framebuffer mode. + * It processes left mouse clicks when the scene view is focused and ImGuizmo is not active. The mouse position is + * adjusted relative to the viewport and its y-coordinate is flipped to match OpenGL's texture format. If the click + * falls within valid bounds and corresponds to a valid entity (pixel value not equal to -1), the entity is selected + * and the SceneViewManager is notified of the active scene; otherwise, any existing selection is cleared. + * + * The update is skipped entirely if the scene window is not open. + */ + void update() override; + + /** + * @brief Retrieves the unique identifier of the scene. + * + * @return scene::SceneId The identifier of this scene. + */ + [[nodiscard]] scene::SceneId getSceneId() const {return m_sceneId;}; + + /** + * @brief Sets the active camera for this scene. + * + * Deactivates the current camera and switches to the specified camera entity. + * The previously active camera will have its render and active flags set to false. + * + * @param cameraId Entity ID of the camera to set as active. + */ + void setCamera(ecs::Entity cameraId); + + /** + * @brief Marks this scene as the default scene. + * + * When a scene is set as the default, it will be populated with + * default entities (lights, basic geometry) during setup. + */ + void setDefault() { m_defaultScene = true; }; + + private: + bool m_defaultScene = false; + ImVec2 m_viewportBounds[2]; + ImGuizmo::OPERATION m_currentGizmoOperation = ImGuizmo::UNIVERSAL; + ImGuizmo::MODE m_currentGizmoMode = ImGuizmo::WORLD; + bool m_snapTranslateOn = false; + glm::vec3 m_snapTranslate = {10.0f, 10.0f, 10.0f}; + bool m_snapRotateOn = false; + float m_angleSnap = 90.0f; + bool m_snapToGrid = false; + bool m_wireframeEnabled = false; + + int m_sceneId = -1; + std::string m_sceneUuid; + int m_activeCamera = -1; + int m_editorCamera = -1; + + PopupManager m_popupManager; + + const std::vector m_buttonGradient = { + {0.0f, IM_COL32(50, 50, 70, 230)}, + {1.0f, IM_COL32(30, 30, 45, 230)} + }; + + // Selected button gradient - lighter blue gradient + const std::vector m_selectedGradient = { + {0.0f, IM_COL32(70, 70, 120, 230)}, + {1.0f, IM_COL32(50, 50, 100, 230)} + }; + + /** + * @brief Sets the main scene window's view size. + * + * Configures the view to a default size of 1280x720 pixels. + */ + void setupWindow(); + + /** + * @brief Creates and initializes a scene with basic components. + * + * Sets up the scene with a framebuffer, editor camera, and loads default + * entities if this is the default scene. + */ + void setupScene(); + + void hideAllButSelectionCallback() const; + void selectAllCallback(); + void unhideAllCallback() const; + void deleteCallback(); + + void setupGlobalState(); + void setupGizmoState(); + void setupGizmoTranslateState(); + void setupGizmoRotateState(); + void setupGizmoScaleState(); + void setupShortcuts(); + + /** + * @brief Populates the scene with default entities. + * + * Creates standard light sources (ambient, directional, point, spot) + * and a simple ground plane in the scene. + */ + void loadDefaultEntities() const; + + /** + * @brief Renders the toolbar overlay within the main scene view. + * + * This method uses ImGui to display a toolbar that includes buttons for switching between orthographic and perspective camera modes, + * a popup placeholder for adding primitive entities, and a draggable input for adjusting the target frames per second (FPS). + * The toolbar is positioned relative to the current view to align with the scene layout. + */ + void renderToolbar(); + + /** + * @brief Sets up the initial layout and style for the toolbar. + * + * Creates a child window with specific size and style settings for the toolbar, + * positions it at the top of the viewport, and configures spacing. + * + * @param buttonWidth Standard width for toolbar buttons + */ + void initialToolbarSetup(float buttonWidth) const; + + /** + * @brief Renders the editor camera button in the toolbar. + * + * Shows either a camera settings button (when editor camera is active) or a + * "switch back to editor camera" button (when a different camera is active). + */ + void renderEditorCameraToolbarButton(); + + /** + * @brief Renders the button to toggle between world/local coordinate modes. + * + * Updates the button props based on the current mode and renders the appropriate + * button with correct styling. + * + * @param showGizmoModeMenu Flag indicating if the mode dropdown menu is visible + * @param activeGizmoMode Reference to store the active mode button properties + * @param inactiveGizmoMode Reference to store the inactive mode button properties + * @return true if the button was clicked + */ + bool renderGizmoModeToolbarButton( + bool showGizmoModeMenu, + ImNexo::ButtonProps &activeGizmoMode, + ImNexo::ButtonProps &inactiveGizmoMode + ); + + /** + * @brief Renders the primitive creation dropdown menu. + * + * Creates a dropdown with buttons for adding primitive shapes like cubes, + * spheres, etc. to the scene. + * + * @param primitiveButtonPos Position of the parent button that opened this menu + * @param buttonSize Size of buttons in the dropdown + * @param showPrimitiveMenu Reference to the flag controlling menu visibility + */ + void renderPrimitiveSubMenu(const ImVec2 &primitiveButtonPos, const ImVec2 &buttonSize, bool &showPrimitiveMenu) const; + + /** + * @brief Renders the snap settings dropdown menu. + * + * Creates a dropdown with toggles for different snapping modes (translate, rotate) + * and their settings. + * + * @param snapButtonPos Position of the parent button that opened this menu + * @param buttonSize Size of buttons in the dropdown + * @param showSnapMenu Reference to the flag controlling menu visibility + */ + void renderSnapSubMenu(const ImVec2 &snapButtonPos, const ImVec2 &buttonSize, bool &showSnapMenu); + + /** + * @brief Handles the snap settings popup dialog. + * + * Creates a modal popup allowing users to configure fine-grained snap settings + * like translate values and rotation angles. + */ + void snapSettingsPopup(); + + void gridSettingsPopup(); + + /** + * @brief Renders a standard toolbar button with optional tooltip and styling. + * + * Creates a gradient button with the specified icon and shows a tooltip on hover. + * + * @param uniqueId Unique identifier for the ImGui control + * @param icon Font icon to display on the button + * @param tooltip Text to show when hovering over the button + * @param gradientStop Color gradient for the button background + * @return true if the button was clicked + */ + static bool renderToolbarButton( + const std::string &uniqueId, + const std::string &icon, + const std::string &tooltip, + const std::vector & gradientStop, + bool *rightClicked = nullptr + ); + + ImGuizmo::OPERATION getLastGuizmoOperation(); + + /** + * @brief Renders the transformation gizmo for the selected entity. + * + * This method displays an interactive ImGuizmo tool to manipulate the translation, rotation, and scale + * of the currently selected entity. It first verifies that the selection is an entity and that the active + * scene corresponds to the one managed by this instance. The method then retrieves the view and projection + * matrices from the active camera, configures ImGuizmo to match the view's dimensions, and constructs the + * entity's transformation matrix from its current translation, rotation, and scale. + * + * If the gizmo is actively manipulated, the entity's transform component is updated with the new values. + */ + void renderGizmo(); + void setupGizmoContext(const components::CameraComponent& camera) const; + float* getSnapSettingsForOperation(ImGuizmo::OPERATION operation); + static void captureInitialTransformStates(const std::vector& entities); + void applyTransformToEntities( + ecs::Entity sourceEntity, + const components::TransformComponent& sourceTransform, + const components::TransformComponent& newTransform, + const std::vector& targetEntities) const; + static void createTransformUndoActions(const std::vector& entities); + static bool s_wasUsingGizmo; + static ImGuizmo::OPERATION s_lastOperation; + static std::unordered_map s_initialTransformStates; + + /** + * @brief Renders the main viewport showing the 3D scene. + * + * Handles resizing of the viewport, draws the framebuffer texture containing the + * rendered scene, and updates viewport bounds for input handling. + */ + void renderView(); + void renderNoActiveCamera() const; + void renderNewEntityPopup(); + + void handleSelection(); + int sampleEntityTexture(float mx, float my) const; + void updateSelection(int entityId, bool isShiftPressed, bool isCtrlPressed); + void updateWindowState(); + + enum class EditorState { + GLOBAL, + GIZMO, + GIZMO_TRANSLATE, + GIZMO_ROTATE, + GIZMO_SCALE, + NB_STATE + }; + + WindowState m_globalState; + WindowState m_gizmoState; + WindowState m_gizmoTranslateState; + WindowState m_gizmoRotateState; + WindowState m_gizmoScaleState; + }; +} diff --git a/editor/src/DocumentWindows/EditorScene/Gizmo.cpp b/editor/src/DocumentWindows/EditorScene/Gizmo.cpp new file mode 100644 index 000000000..19a2ae304 --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Gizmo.cpp @@ -0,0 +1,292 @@ +//// Gizmo.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the gizmo rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "context/Selector.hpp" +#include "context/ActionManager.hpp" +#include "math/Matrix.hpp" + +#include + +namespace nexo::editor { + // Class-level variables for tracking gizmo state between frames + bool EditorScene::s_wasUsingGizmo = false; + ImGuizmo::OPERATION EditorScene::s_lastOperation = ImGuizmo::OPERATION::UNIVERSAL; + std::unordered_map EditorScene::s_initialTransformStates; + + static ImGuizmo::OPERATION getActiveGuizmoOperation() + { + for (int bitPos = 0; bitPos <= 13; bitPos++) + { + auto op = static_cast(1u << bitPos); + if (ImGuizmo::IsOver(op)) + return op; + } + return ImGuizmo::OPERATION::UNIVERSAL; + } + + static std::optional findEntityWithTransform(const std::vector& entities) + { + const auto& coord = nexo::Application::m_coordinator; + + for (const auto& entity : entities) { + if (coord->tryGetComponent(entity)) { + return entity; + } + } + + return std::nullopt; + } + + void EditorScene::setupGizmoContext(const components::CameraComponent& camera) const + { + ImGuizmo::SetOrthographic(camera.type == components::CameraType::ORTHOGRAPHIC); + ImGuizmo::SetDrawlist(); + ImGuizmo::SetRect(m_viewportBounds[0].x, m_viewportBounds[0].y, m_contentSize.x, m_contentSize.y); + ImGuizmo::Enable(true); + } + + static glm::mat4 buildEntityTransformMatrix(const components::TransformComponent& transform) + { + const glm::mat4 rotationMat = glm::toMat4(transform.quat); + return glm::translate(glm::mat4(1.0f), transform.pos) * + rotationMat * + glm::scale(glm::mat4(1.0f), transform.size); + } + + float* EditorScene::getSnapSettingsForOperation(const ImGuizmo::OPERATION operation) + { + if (m_snapTranslateOn && operation & ImGuizmo::OPERATION::TRANSLATE) + return &m_snapTranslate.x; + if (m_snapRotateOn && operation & ImGuizmo::OPERATION::ROTATE) { + return &m_angleSnap; + } + return nullptr; + } + + void EditorScene::captureInitialTransformStates(const std::vector& entities) + { + const auto& coord = nexo::Application::m_coordinator; + s_initialTransformStates.clear(); + + for (const auto& entity : entities) { + auto transform = coord->tryGetComponent(entity); + if (transform) { + s_initialTransformStates[entity] = transform->get().save(); + } + } + } + + void EditorScene::applyTransformToEntities( + const ecs::Entity sourceEntity, + const components::TransformComponent& sourceTransform, + const components::TransformComponent& newTransform, + const std::vector& targetEntities) const + { + const auto& coord = nexo::Application::m_coordinator; + + // Calculate transformation deltas + const glm::vec3 positionDelta = newTransform.pos - sourceTransform.pos; + const glm::vec3 scaleFactor = newTransform.size / sourceTransform.size; + const glm::quat rotationDelta = newTransform.quat * glm::inverse(sourceTransform.quat); + + // Apply transforms to all selected entities except the source + for (const auto& entity : targetEntities) { + if (entity == static_cast(sourceEntity)) continue; + + auto entityTransform = coord->tryGetComponent(entity); + if (!entityTransform) continue; + + // Apply relevant transformations based on current operation + if (m_currentGizmoOperation & ImGuizmo::OPERATION::TRANSLATE) { + entityTransform->get().pos += positionDelta; + } + + if (m_currentGizmoOperation & ImGuizmo::OPERATION::ROTATE) { + entityTransform->get().quat = rotationDelta * entityTransform->get().quat; + } + + if (m_currentGizmoOperation & ImGuizmo::OPERATION::SCALE) { + entityTransform->get().size.x *= scaleFactor.x; + entityTransform->get().size.y *= scaleFactor.y; + entityTransform->get().size.z *= scaleFactor.z; + } + } + } + + + static bool hasTransformChanged(const components::TransformComponent::Memento& before, + const components::TransformComponent::Memento& after) + { + return before.position != after.position || + before.rotation != after.rotation || + before.scale != after.scale; + } + + void EditorScene::createTransformUndoActions(const std::vector& entities) + { + const auto& coord = nexo::Application::m_coordinator; + auto& actionManager = ActionManager::get(); + + // If multiple entities selected, create a group action + if (entities.size() > 1) { + auto groupAction = ActionManager::createActionGroup(); + bool anyChanges = false; + + for (const auto& entity : entities) { + auto transform = coord->tryGetComponent(entity); + if (!transform) continue; + + auto it = s_initialTransformStates.find(entity); + if (it == s_initialTransformStates.end()) continue; + + auto beforeState = it->second; + auto afterState = transform->get().save(); + + // Check if anything actually changed + if (hasTransformChanged(beforeState, afterState)) { + auto action = std::make_unique>( + entity, beforeState, afterState); + groupAction->addAction(std::move(action)); + anyChanges = true; + } + } + + if (anyChanges) { + actionManager.recordAction(std::move(groupAction)); + } + } + // Single entity selected - simpler action + else if (entities.size() == 1) { + auto entity = entities[0]; + auto transform = coord->tryGetComponent(entity); + + if (s_initialTransformStates.contains(entity)) { + auto beforeState = s_initialTransformStates[entity]; + auto afterState = transform->get().save(); + + if (hasTransformChanged(beforeState, afterState)) { + actionManager.recordComponentChange( + entity, beforeState, afterState); + } + } + } + + // Reset stored states + s_initialTransformStates.clear(); + } + + void EditorScene::renderGizmo() + { + const auto& coord = nexo::Application::m_coordinator; + auto const& selector = Selector::get(); + + // Skip if no valid selection + if (selector.getPrimarySelectionType() == SelectionType::SCENE || + selector.getSelectedScene() != m_sceneId || + !selector.hasSelection()) { + return; + } + + // Find entity with transform component + const auto& selectedEntities = selector.getSelectedEntities(); + ecs::Entity primaryEntity = selector.getPrimaryEntity(); + + auto primaryTransform = coord->tryGetComponent(primaryEntity); + if (!primaryTransform) { + const auto entityWithTransform = findEntityWithTransform(selectedEntities); + if (!entityWithTransform) return; // No entity with transform found + + primaryEntity = *entityWithTransform; + primaryTransform = coord->tryGetComponent(primaryEntity); + } + + // Camera setup + const auto& cameraTransform = coord->getComponent(m_activeCamera); + auto& camera = coord->getComponent(m_activeCamera); + + // Configure ImGuizmo + setupGizmoContext(camera); + ImGuizmo::SetID(static_cast(primaryEntity)); + + // Prepare matrices + glm::mat4 viewMatrix = camera.getViewMatrix(cameraTransform); + glm::mat4 projectionMatrix = camera.getProjectionMatrix(); + glm::mat4 transformMatrix = buildEntityTransformMatrix(primaryTransform->get()); + + // Track which operation is active + if (!ImGuizmo::IsUsing()) { + s_lastOperation = getActiveGuizmoOperation(); + } + + // Get snap settings if applicable + const float* snap = getSnapSettingsForOperation(s_lastOperation); + + // Capture initial state when starting to use gizmo + if (!s_wasUsingGizmo && ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGuizmo::IsOver()) { + captureInitialTransformStates(selectedEntities); + } + + // Perform the actual manipulation + ImGuizmo::Manipulate( + glm::value_ptr(viewMatrix), + glm::value_ptr(projectionMatrix), + m_currentGizmoOperation, + m_currentGizmoMode, + glm::value_ptr(transformMatrix), + nullptr, + snap + ); + + // Update isUsingGizmo after manipulation + bool isUsingGizmo = ImGuizmo::IsUsing(); + + if (isUsingGizmo) { + // Disable camera movement during manipulation + camera.active = false; + + // Extract the original transform values + const components::TransformComponent originalTransform = primaryTransform->get(); + + // Extract the new transform values from the matrix + glm::vec3 newPos; + glm::vec3 newScale; + glm::quat newRot; + math::decomposeTransformQuat(transformMatrix, newPos, newRot, newScale); + + // Update the primary entity's transform + primaryTransform->get().pos = newPos; + primaryTransform->get().quat = newRot; + primaryTransform->get().size = newScale; + + // Apply changes to other selected entities + applyTransformToEntities( + primaryEntity, + originalTransform, + primaryTransform->get(), + selectedEntities + ); + } + else if (s_wasUsingGizmo) { + // Re-enable camera when done + camera.active = true; + + // Create undo/redo actions + createTransformUndoActions(selectedEntities); + } + + // Update state for next frame + s_wasUsingGizmo = isUsingGizmo; + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Init.cpp b/editor/src/DocumentWindows/EditorScene/Init.cpp new file mode 100644 index 000000000..2db883bec --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Init.cpp @@ -0,0 +1,90 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the init functions of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "CameraFactory.hpp" +#include "LightFactory.hpp" +#include "EntityFactory3D.hpp" +#include "utils/EditorProps.hpp" + +namespace nexo::editor { + + void EditorScene::setup() + { + setupWindow(); + setupScene(); + setupShortcuts(); + } + + void EditorScene::setupScene() + { + auto &app = getApp(); + + m_sceneId = static_cast(app.getSceneManager().createScene(m_windowName)); + renderer::NxFramebufferSpecs framebufferSpecs; + framebufferSpecs.attachments = { + renderer::NxFrameBufferTextureFormats::RGBA8, renderer::NxFrameBufferTextureFormats::RED_INTEGER, renderer::NxFrameBufferTextureFormats::Depth + }; + framebufferSpecs.width = static_cast(m_contentSize.x); + framebufferSpecs.height = static_cast(m_contentSize.y); + const auto renderTarget = renderer::NxFramebuffer::create(framebufferSpecs); + m_editorCamera = static_cast(CameraFactory::createPerspectiveCamera({0.0f, 3.0f, -2.0f}, static_cast(m_contentSize.x), static_cast(m_contentSize.y), renderTarget)); + auto &cameraComponent = Application::m_coordinator->getComponent(m_editorCamera); + cameraComponent.render = true; + app.getSceneManager().getScene(m_sceneId).addEntity(static_cast(m_editorCamera)); + const components::PerspectiveCameraController controller; + Application::m_coordinator->addComponent(static_cast(m_editorCamera), controller); + constexpr components::EditorCameraTag editorCameraTag; + Application::m_coordinator->addComponent(m_editorCamera, editorCameraTag); + m_activeCamera = m_editorCamera; + + m_sceneUuid = app.getSceneManager().getScene(m_sceneId).getUuid(); + if (m_defaultScene) + loadDefaultEntities(); + } + + void EditorScene::loadDefaultEntities() const + { + auto &app = getApp(); + scene::Scene &scene = app.getSceneManager().getScene(m_sceneId); + const ecs::Entity ambientLight = LightFactory::createAmbientLight({0.5f, 0.5f, 0.5f}); + scene.addEntity(ambientLight); + const ecs::Entity pointLight = LightFactory::createPointLight({2.0f, 5.0f, 0.0f}); + utils::addPropsTo(pointLight, utils::PropsType::POINT_LIGHT); + scene.addEntity(pointLight); + const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.2f, -1.0f, -0.3f}); + scene.addEntity(directionalLight); + const ecs::Entity spotLight = LightFactory::createSpotLight({-2.0f, 5.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}); + utils::addPropsTo(spotLight, utils::PropsType::SPOT_LIGHT); + scene.addEntity(spotLight); + const ecs::Entity basicCube = EntityFactory3D::createCube({0.0f, 0.25f, 0.0f}, {20.0f, 0.5f, 20.0f}, + {0.0f, 0.0f, 0.0f}, {0.05f * 1.7, 0.09f * 1.35, 0.13f * 1.45, 1.0f}); + app.getSceneManager().getScene(m_sceneId).addEntity(basicCube); + } + + void EditorScene::setupWindow() + { + m_contentSize = ImVec2(1280, 720); + } + + void EditorScene::setCamera(const ecs::Entity cameraId) + { + auto &oldCameraComponent = Application::m_coordinator->getComponent(m_activeCamera); + oldCameraComponent.active = false; + oldCameraComponent.render = false; + m_activeCamera = static_cast(cameraId); + auto &newCameraComponent = Application::m_coordinator->getComponent(cameraId); + newCameraComponent.resize(static_cast(m_contentSize.x), static_cast(m_contentSize.y)); + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Shortcuts.cpp b/editor/src/DocumentWindows/EditorScene/Shortcuts.cpp new file mode 100644 index 000000000..836b199d3 --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Shortcuts.cpp @@ -0,0 +1,753 @@ +//// Shortcuts.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the shortcuts init of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "context/Selector.hpp" +#include "context/ActionManager.hpp" +#include "components/Uuid.hpp" + +namespace nexo::editor { + + static void hideCallback() + { + auto &selector = Selector::get(); + const auto &selectedEntities = selector.getSelectedEntities(); + auto& actionManager = ActionManager::get(); + auto actionGroup = ActionManager::createActionGroup(); + for (const auto entity : selectedEntities) { + auto &renderComponent = Application::m_coordinator->getComponent(entity); + auto beforeState = renderComponent.save(); + renderComponent.isRendered = !renderComponent.isRendered; + auto afterState = renderComponent.save(); + actionGroup->addAction(std::make_unique>( + entity, beforeState, afterState)); + } + actionManager.recordAction(std::move(actionGroup)); + selector.clearSelection(); + } + + void EditorScene::selectAllCallback() + { + auto &selector = Selector::get(); + auto &app = nexo::getApp(); + const auto &scene = app.getSceneManager().getScene(m_sceneId); + + selector.clearSelection(); + + for (const auto entity : scene.getEntities()) { + if (static_cast(entity) == m_editorCamera) continue; // Skip editor camera + + const auto uuidComponent = Application::m_coordinator->tryGetComponent(entity); + if (uuidComponent) + selector.addToSelection(uuidComponent->get().uuid, static_cast(entity)); + } + m_windowState = m_gizmoState; + } + + void EditorScene::hideAllButSelectionCallback() const + { + auto &app = getApp(); + const auto &entities = app.getSceneManager().getScene(m_sceneId).getEntities(); + const auto &selector = Selector::get(); + auto &actionManager = ActionManager::get(); + auto actionGroup = ActionManager::createActionGroup(); + for (const auto entity : entities) { + if (Application::m_coordinator->entityHasComponent(entity) && !selector.isEntitySelected(static_cast(entity))) { + auto &renderComponent = Application::m_coordinator->getComponent(entity); + if (renderComponent.isRendered) { + auto beforeState = renderComponent.save(); + renderComponent.isRendered = false; + auto afterState = renderComponent.save(); + actionGroup->addAction(std::make_unique>( + entity, beforeState, afterState)); + } + } + } + actionManager.recordAction(std::move(actionGroup)); + } + + void EditorScene::deleteCallback() + { + auto &selector = Selector::get(); + const auto &selectedEntities = selector.getSelectedEntities(); + auto &app = nexo::getApp(); + auto& actionManager = ActionManager::get(); + if (selectedEntities.size() > 1) { + auto actionGroup = ActionManager::createActionGroup(); + for (const auto entity : selectedEntities) { + actionGroup->addAction(ActionManager::prepareEntityDeletion(entity)); + app.deleteEntity(entity); + } + actionManager.recordAction(std::move(actionGroup)); + } else { + auto deleteAction = ActionManager::prepareEntityDeletion(selectedEntities[0]); + app.deleteEntity(selectedEntities[0]); + actionManager.recordAction(std::move(deleteAction)); + } + selector.clearSelection(); + this->m_windowState = m_globalState; + } + + void EditorScene::unhideAllCallback() const + { + auto &app = getApp(); + const auto &entities = app.getSceneManager().getScene(m_sceneId).getEntities(); + auto &actionManager = ActionManager::get(); + auto actionGroup = ActionManager::createActionGroup(); + for (const auto entity : entities) { + if (Application::m_coordinator->entityHasComponent(entity)) { + auto &renderComponent = Application::m_coordinator->getComponent(entity); + if (!renderComponent.isRendered) { + auto beforeState = renderComponent.save(); + renderComponent.isRendered = true; + auto afterState = renderComponent.save(); + actionGroup->addAction(std::make_unique>( + entity, beforeState, afterState)); + } + } + } + actionManager.recordAction(std::move(actionGroup)); + } + + void EditorScene::setupGlobalState() + { + // ================= GLOBAL STATE ============================= + m_globalState = {static_cast(EditorState::GLOBAL)}; + + // Shift context + m_globalState.registerCommand( + Command::create() + .description("Shift context") + .key("Shift") + .modifier(true) + .addChild( + Command::create() + .description("Add entity") + .key("A") + .onPressed([this]{ this->m_popupManager.openPopup("Add new entity popup"); }) + .build() + ) + .build() + ); + + // Control context + m_globalState.registerCommand( + Command::create() + .description("Control context") + .key("Ctrl") + .modifier(true) + .addChild( + Command::create() + .description("Unhide all") + .key("H") + .onPressed([this]() { this->unhideAllCallback(); }) + .build() + ) + .build() + ); + + // Select all + m_globalState.registerCommand( + Command::create() + .description("Select all") + .key("A") + .onPressed([this]{ this->selectAllCallback(); }) + .build() + ); + } + + void EditorScene::setupGizmoState() + { + // ================= GIZMO STATE ============================= + m_gizmoState = {static_cast(EditorState::GIZMO)}; + + // Delete + m_gizmoState.registerCommand( + Command::create() + .description("Delete") + .key("Delete") + .onPressed([this]() { this->deleteCallback(); }) + .build() + ); + + // Hide + m_gizmoState.registerCommand( + Command::create() + .description("Hide") + .key("H") + .onPressed(&hideCallback) + .build() + ); + + // Translate + m_gizmoState.registerCommand( + Command::create() + .description("Translate") + .key("G") + .onPressed([this]{ + this->m_windowState = m_gizmoTranslateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + + // Rotate + m_gizmoState.registerCommand( + Command::create() + .description("Rotate") + .key("R") + .onPressed([this]{ + this->m_windowState = m_gizmoRotateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + + // Scale + m_gizmoState.registerCommand( + Command::create() + .description("Scale") + .key("S") + .onPressed([this]{ + this->m_windowState = m_gizmoScaleState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + + // Shift context + m_gizmoState.registerCommand( + Command::create() + .description("Shift context") + .key("Shift") + .modifier(true) + .addChild( + Command::create() + .description("Toggle snapping") + .key("S") + .onPressed([this]{ + m_snapTranslateOn = true; + m_snapRotateOn = true; + }) + .onReleased([this]{ + m_snapTranslateOn = false; + m_snapRotateOn = false; + }) + .build() + ) + .addChild( + Command::create() + .description("Hide all but selection") + .key("H") + .onPressed([this]{ this->hideAllButSelectionCallback(); }) + .build() + ) + .build() + ); + } + + void EditorScene::setupGizmoTranslateState() + { + // ================= TRANSLATE STATE ============================= + m_gizmoTranslateState = {static_cast(EditorState::GIZMO_TRANSLATE)}; + + // Universal + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Universal") + .key("U") + .onPressed([this]{ + this->m_windowState = m_gizmoState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::UNIVERSAL; + }) + .build() + ); + + // Translate + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Translate") + .key("G") + .onPressed([this]{ + this->m_windowState = m_gizmoTranslateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .onRepeat([this]{ + if (this->m_currentGizmoMode == ImGuizmo::MODE::LOCAL) + this->m_currentGizmoMode = ImGuizmo::MODE::WORLD; + else + this->m_currentGizmoMode = ImGuizmo::MODE::LOCAL; + }) + .build() + ); + + // Rotate + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Rotate") + .key("R") + .onPressed([this]{ + this->m_windowState = m_gizmoRotateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + + // Scale + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Scale") + .key("S") + .onPressed([this]{ + this->m_windowState = m_gizmoScaleState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + + // Shift context + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Shift context") + .key("Shift") + .modifier(true) + .addChild( + Command::create() + .description("Exclude X") + .key("X") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::TRANSLATE_X); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::TRANSLATE_X); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Y") + .key("Y") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::TRANSLATE_Y); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::TRANSLATE_Y); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Z") + .key("Z") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::TRANSLATE_Z); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::TRANSLATE_Z); + }) + .build() + ) + .addChild( + Command::create() + .description("Toggle snapping") + .key("S") + .onPressed([this]{ + m_snapTranslateOn = true; + }) + .onReleased([this]{ + m_snapTranslateOn = false; + }) + .build() + ) + .build() + ); + + // Lock X + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Lock X") + .key("X") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE_X; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + + // Lock Y + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Lock Y") + .key("Y") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE_Y; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + + // Lock Z + m_gizmoTranslateState.registerCommand( + Command::create() + .description("Lock Z") + .key("Z") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE_Z; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + } + + void EditorScene::setupGizmoRotateState() + { + // ================= ROTATE STATE ============================= + m_gizmoRotateState = {static_cast(EditorState::GIZMO_ROTATE)}; + + // Universal + m_gizmoRotateState.registerCommand( + Command::create() + .description("Universal") + .key("U") + .onPressed([this]{ + this->m_windowState = m_gizmoState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::UNIVERSAL; + }) + .build() + ); + + // Rotate + m_gizmoRotateState.registerCommand( + Command::create() + .description("Rotate") + .key("R") + .onPressed([this]{ + this->m_windowState = m_gizmoRotateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .onRepeat([this]{ + if (this->m_currentGizmoMode == ImGuizmo::MODE::LOCAL) + this->m_currentGizmoMode = ImGuizmo::MODE::WORLD; + else + this->m_currentGizmoMode = ImGuizmo::MODE::LOCAL; + }) + .build() + ); + + // Translate + m_gizmoRotateState.registerCommand( + Command::create() + .description("Translate") + .key("G") + .onPressed([this]{ + this->m_windowState = m_gizmoTranslateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + + // Scale + m_gizmoRotateState.registerCommand( + Command::create() + .description("Scale") + .key("S") + .onPressed([this]{ + this->m_windowState = m_gizmoScaleState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + + // Shift context + m_gizmoRotateState.registerCommand( + Command::create() + .description("Shift context") + .key("Shift") + .modifier(true) + .addChild( + Command::create() + .description("Exclude X") + .key("X") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::ROTATE_X); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::ROTATE_X); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Y") + .key("Y") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::ROTATE_Y); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::ROTATE_Y); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Z") + .key("Z") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::ROTATE_Z); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation | ImGuizmo::OPERATION::ROTATE_Z); + }) + .build() + ) + .addChild( + Command::create() + .description("Toggle snapping") + .key("S") + .onPressed([this]{ + m_snapRotateOn = true; + }) + .onReleased([this]{ + m_snapRotateOn = false; + }) + .build() + ) + .build() + ); + + // Lock X + m_gizmoRotateState.registerCommand( + Command::create() + .description("Lock X") + .key("X") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE_X; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + + // Lock Y + m_gizmoRotateState.registerCommand( + Command::create() + .description("Lock Y") + .key("Y") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE_Y; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + + // Lock Z + m_gizmoRotateState.registerCommand( + Command::create() + .description("Lock Z") + .key("Z") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE_Z; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + } + + void EditorScene::setupGizmoScaleState() + { + // ================= SCALE STATE ============================= + m_gizmoScaleState = {static_cast(EditorState::GIZMO_SCALE)}; + + // Universal + m_gizmoScaleState.registerCommand( + Command::create() + .description("Universal") + .key("U") + .onPressed([this]{ + this->m_windowState = m_gizmoState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::UNIVERSAL; + }) + .build() + ); + + // Scale + m_gizmoScaleState.registerCommand( + Command::create() + .description("Scale") + .key("S") + .onPressed([this]{ + this->m_windowState = m_gizmoScaleState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .onRepeat([this]{ + if (this->m_currentGizmoMode == ImGuizmo::MODE::LOCAL) + this->m_currentGizmoMode = ImGuizmo::MODE::WORLD; + else + this->m_currentGizmoMode = ImGuizmo::MODE::LOCAL; + }) + .build() + ); + + // Translate + m_gizmoScaleState.registerCommand( + Command::create() + .description("Translate") + .key("G") + .onPressed([this]{ + this->m_windowState = m_gizmoTranslateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE; + }) + .build() + ); + + // Rotate + m_gizmoScaleState.registerCommand( + Command::create() + .description("Rotate") + .key("R") + .onPressed([this]{ + this->m_windowState = m_gizmoRotateState; + this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE; + }) + .build() + ); + + // Shift context + m_gizmoScaleState.registerCommand( + Command::create() + .description("Shift context") + .key("Shift") + .modifier(true) + .addChild( + Command::create() + .description("Exclude X") + .key("X") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::SCALE_X); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::SCALE_X); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Y") + .key("Y") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::SCALE_Y); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::SCALE_Y); + }) + .build() + ) + .addChild( + Command::create() + .description("Exclude Z") + .key("Z") + .onPressed([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ~ImGuizmo::OPERATION::SCALE_Z); + }) + .onReleased([this]{ + m_currentGizmoOperation = + static_cast(m_currentGizmoOperation & ImGuizmo::OPERATION::SCALE_Z); + }) + .build() + ) + .build() + ); + + // Lock X + m_gizmoScaleState.registerCommand( + Command::create() + .description("Lock X") + .key("X") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE_X; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + + // Lock Y + m_gizmoScaleState.registerCommand( + Command::create() + .description("Lock Y") + .key("Y") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE_Y; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + + // Lock Z + m_gizmoScaleState.registerCommand( + Command::create() + .description("Lock Z") + .key("Z") + .onPressed([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE_Z; + }) + .onReleased([this]{ + this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE; + }) + .build() + ); + } + + void EditorScene::setupShortcuts() + { + setupGlobalState(); + m_windowState = m_globalState; + + setupGizmoState(); + setupGizmoTranslateState(); + setupGizmoRotateState(); + setupGizmoScaleState(); + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Show.cpp b/editor/src/DocumentWindows/EditorScene/Show.cpp new file mode 100644 index 000000000..e183370cf --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Show.cpp @@ -0,0 +1,162 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the editor scene rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "context/Selector.hpp" +#include "EntityFactory3D.hpp" +#include "LightFactory.hpp" +#include "IconsFontAwesome.h" +#include "ImNexo/Panels.hpp" +#include "utils/EditorProps.hpp" +#include "context/actions/EntityActions.hpp" +#include "context/ActionManager.hpp" + +namespace nexo::editor { + + void EditorScene::renderNoActiveCamera() const + { + // No active camera, render the text at the center of the screen + const ImVec2 textSize = ImGui::CalcTextSize("No active camera"); + const auto textPos = ImVec2((m_contentSize.x - textSize.x) / 2, (m_contentSize.y - textSize.y) / 2); + + ImGui::SetCursorScreenPos(textPos); + ImGui::Text("No active camera"); + } + + void EditorScene::renderNewEntityPopup() + { + auto &app = Application::getInstance(); + auto &sceneManager = app.getSceneManager(); + const auto sceneId = m_sceneId; + + // --- Primitives submenu --- + if (ImGui::BeginMenu("Primitives")) { + if (ImGui::MenuItem("Cube")) { + const ecs::Entity newCube = EntityFactory3D::createCube({0.0f, 0.0f, -5.0f}, {1.0f, 1.0f, 1.0f}, + {0.0f, 0.0f, 0.0f}, {0.05f * 1.5, 0.09f * 1.15, 0.13f * 1.25, 1.0f}); + sceneManager.getScene(sceneId).addEntity(newCube); + auto createAction = std::make_unique(newCube); + ActionManager::get().recordAction(std::move(createAction)); + } + ImGui::EndMenu(); + } + + // --- Model item (with file‑dialog) --- + if (ImGui::MenuItem("Model")) { + //TODO: import model + } + + // --- Lights submenu --- + if (ImGui::BeginMenu("Lights")) { + if (ImGui::MenuItem("Directional")) { + const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.0f, -1.0f, 0.0f}); + sceneManager.getScene(sceneId).addEntity(directionalLight); + auto createAction = std::make_unique(directionalLight); + ActionManager::get().recordAction(std::move(createAction)); + } + if (ImGui::MenuItem("Point")) { + const ecs::Entity pointLight = LightFactory::createPointLight({0.0f, 0.5f, 0.0f}); + utils::addPropsTo(pointLight, utils::PropsType::POINT_LIGHT); + sceneManager.getScene(sceneId).addEntity(pointLight); + auto createAction = std::make_unique(pointLight); + ActionManager::get().recordAction(std::move(createAction)); + } + if (ImGui::MenuItem("Spot")) { + const ecs::Entity spotLight = LightFactory::createSpotLight({0.0f, 0.5f, 0.0f}, {0.0f, -1.0f, 0.0f}); + utils::addPropsTo(spotLight, utils::PropsType::SPOT_LIGHT); + sceneManager.getScene(sceneId).addEntity(spotLight); + auto createAction = std::make_unique(spotLight); + ActionManager::get().recordAction(std::move(createAction)); + } + ImGui::EndMenu(); + } + + // --- Camera item --- + if (ImGui::MenuItem("Camera")) { + m_popupManager.openPopupWithCallback("Popup camera inspector", [this]() { + ImNexo::CameraInspector(this->m_sceneId); + }, ImVec2(1440,900)); + } + PopupManager::closePopup(); + } + + void EditorScene::renderView() + { + auto &cameraComponent = Application::m_coordinator->getComponent(m_activeCamera); + if (!cameraComponent.m_renderTarget) + return; + const glm::vec2 renderTargetSize = cameraComponent.m_renderTarget->getSize(); + + // Resize handling + if (!cameraComponent.viewportLocked && (m_contentSize.x > 0 && m_contentSize.y > 0) + && (m_contentSize.x != renderTargetSize.x || m_contentSize.y != renderTargetSize.y)) + { + cameraComponent.resize(static_cast(m_contentSize.x), + static_cast(m_contentSize.y)); + } + + // Render framebuffer + const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); + ImNexo::Image(static_cast(static_cast(textureId)), m_contentSize); + + const ImVec2 viewportMin = ImGui::GetItemRectMin(); + const ImVec2 viewportMax = ImGui::GetItemRectMax(); + m_viewportBounds[0] = viewportMin; + m_viewportBounds[1] = viewportMax; + } + + void EditorScene::show() + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::SetNextWindowSizeConstraints(ImVec2(480, 270), ImVec2(1920, 1080)); + auto &selector = Selector::get(); + m_windowName = selector.getUiHandle(m_sceneUuid, std::string(ICON_FA_GLOBE) + " " + m_windowName); + const std::string &sceneWindowName = std::format("{}{}{}", + m_windowName, + NEXO_WND_USTRID_DEFAULT_SCENE, + m_sceneId + ); + m_wasVisibleLastFrame = m_isVisibleInDock; + m_isVisibleInDock = false; + if (ImGui::Begin(sceneWindowName.c_str(), &m_opened, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse)) + { + std::string renderName = std::format("{}{}", + NEXO_WND_USTRID_DEFAULT_SCENE, + m_sceneId + ); + beginRender(renderName); + auto &app = getApp(); + + app.getSceneManager().getScene(m_sceneId).setActiveStatus(m_focused); + + if (m_focused && selector.getSelectedScene() != m_sceneId) { + selector.setSelectedScene(m_sceneId); + selector.clearSelection(); + } + + if (m_activeCamera == -1) + renderNoActiveCamera(); + else { + renderView(); + renderGizmo(); + renderToolbar(); + } + + if (m_popupManager.showPopup("Add new entity popup")) + renderNewEntityPopup(); + } + ImGui::End(); + ImGui::PopStyleVar(); + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Shutdown.cpp b/editor/src/DocumentWindows/EditorScene/Shutdown.cpp new file mode 100644 index 000000000..14c412d8d --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Shutdown.cpp @@ -0,0 +1,22 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the shutdown of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" + +namespace nexo::editor { + void EditorScene::shutdown() + { + // Should probably check if it is necessary to delete the scene here ? + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Toolbar.cpp b/editor/src/DocumentWindows/EditorScene/Toolbar.cpp new file mode 100644 index 000000000..5b9685bcf --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Toolbar.cpp @@ -0,0 +1,421 @@ +//// Toolbar.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the toobal rendering of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "EntityFactory3D.hpp" +#include "IconsFontAwesome.h" +#include "context/Selector.hpp" +#include "components/Uuid.hpp" +#include "context/actions/EntityActions.hpp" +#include "context/ActionManager.hpp" + +namespace nexo::editor { + + void EditorScene::initialToolbarSetup(const float buttonWidth) const + { + ImVec2 toolbarPos = m_windowPos; + toolbarPos.x += 10.0f; + toolbarPos.y += 20.0f; + + ImGui::SetCursorScreenPos(toolbarPos); + + const auto toolbarSize = ImVec2(m_contentSize.x - buttonWidth, 50.0f); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.0f)); + ImGui::BeginChild("##ToolbarOverlay", toolbarSize, 0, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoSavedSettings); + + ImGui::SetCursorPosY((ImGui::GetWindowHeight() - ImGui::GetFrameHeight()) * 0.5f); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 0)); + } + + bool EditorScene::renderToolbarButton(const std::string &uniqueId, const std::string &icon, const std::string &tooltip, const std::vector & gradientStop, bool *rightClicked) + { + constexpr float buttonWidth = 35.0f; + constexpr float buttonHeight = 35.0f; + const bool clicked = ImNexo::IconGradientButton(uniqueId, icon, ImVec2(buttonWidth, buttonHeight), gradientStop); + if (!tooltip.empty() && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tooltip.c_str()); + if (rightClicked != nullptr) + *rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); + return clicked; + } + + void EditorScene::renderPrimitiveSubMenu(const ImVec2 &primitiveButtonPos, const ImVec2 &buttonSize, bool &showPrimitiveMenu) const + { + auto &app = getApp(); + static const std::vector buttonProps = + { + { + .uniqueId = "cube_primitive", + .icon = ICON_FA_CUBE, + .onClick = [this, &app]() + { + const ecs::Entity newCube = EntityFactory3D::createCube({0.0f, 0.0f, -5.0f}, {1.0f, 1.0f, 1.0f}, + {0.0f, 0.0f, 0.0f}, {0.05f * 1.5, 0.09f * 1.15, 0.13f * 1.25, 1.0f}); + app.getSceneManager().getScene(this->m_sceneId).addEntity(newCube); + auto createAction = std::make_unique(newCube); + ActionManager::get().recordAction(std::move(createAction)); + }, + .tooltip = "Create Cube" + } + }; + ImNexo::ButtonDropDown(primitiveButtonPos, buttonSize, buttonProps, showPrimitiveMenu); + } + + void EditorScene::renderSnapSubMenu(const ImVec2 &snapButtonPos, const ImVec2 &buttonSize, bool &showSnapMenu) + { + const std::vector buttonProps = + { + { + .uniqueId = "toggle_translate_snap", + .icon = ICON_FA_TH, + .onClick = [this]() + { + this->m_snapTranslateOn = !this->m_snapTranslateOn; + }, + .onRightClick = [this]() + { + this->m_popupManager.openPopup("Snap settings popup", ImVec2(400, 140)); + }, + .tooltip = "Toggle Translate Snap", + .buttonGradient = m_snapTranslateOn ? m_selectedGradient : m_buttonGradient + }, + { + .uniqueId = "toggle_rotate_snap", + .icon = ICON_FA_BULLSEYE, + .onClick = [this]() + { + this->m_snapRotateOn = !m_snapRotateOn; + }, + .onRightClick = [this]() + { + this->m_popupManager.openPopup("Snap settings popup", ImVec2(400, 140)); + }, + .tooltip = "Toggle Rotate Snap", + .buttonGradient = m_snapRotateOn ? m_selectedGradient : m_buttonGradient + } + // Snap on scale is kinda strange, the IsOver is not able to detect it, so for now we disable it + // { + // .uniqueId = "toggle_scale_snap", + // .icon = ICON_FA_EXPAND, + // .onClick = [this]() + // { + // this->m_snapScaleOn = !m_snapScaleOn; + // }, + // .onRightClick = [this]() + // { + // this->m_popupManager.openPopup("Snap settings popup", ImVec2(400, 180)); + // }, + // .tooltip = "Toggle Scale Snap", + // .buttonGradient = (m_snapScaleOn) ? m_selectedGradient : buttonGradient + // } + }; + ImNexo::ButtonDropDown(snapButtonPos, buttonSize, buttonProps, showSnapMenu); + } + + void EditorScene::snapSettingsPopup() + { + if (m_popupManager.showPopupModal("Snap settings popup")) + { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + ImGui::Indent(10.0f); + + if (ImGui::BeginTable("TranslateSnap", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImNexo::RowDragFloat3("Translate Snap", "X", "Y", "Z", &this->m_snapTranslate.x); + ImGui::EndTable(); + } + + if (ImGui::BeginTable("ScaleAndRotateSnap", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Value", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + // Empty columns to match the first table's structure + ImGui::TableSetupColumn("##Empty1", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Empty2", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + ImNexo::RowDragFloat1("Rotate Snap", "", &this->m_angleSnap); + ImGui::EndTable(); + } + ImGui::Spacing(); + ImGui::Spacing(); + + constexpr float buttonWidth = 120.0f; + const float windowWidth = ImGui::GetWindowSize().x; + ImGui::SetCursorPosX((windowWidth - buttonWidth) * 0.5f); + + if (ImNexo::Button("OK", ImVec2(buttonWidth, 0.0f))) + { + PopupManager::closePopupInContext(); + } + ImGui::Unindent(10.0f); + ImGui::PopStyleVar(); + PopupManager::closePopup(); + } + } + + void EditorScene::gridSettingsPopup() + { + if (m_popupManager.showPopupModal("Grid settings")) + { + components::RenderContext::GridParams &gridSettings = + Application::m_coordinator->getSingletonComponent().gridParams; + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + ImGui::Indent(10.0f); + + if (ImGui::BeginTable("GridSettings", 2, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImNexo::RowDragFloat1("Grid size", "", &gridSettings.gridSize, 50.0f, 150.0f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("The total size of the grid"); + ImNexo::RowDragFloat1("Pixel cell spacing", "", &gridSettings.minPixelsBetweenCells, 0.0f, 100.0f, 0.1f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Level of detail of internal cells"); + ImNexo::RowDragFloat1("Cell size", "", &gridSettings.cellSize, 0.1f, 20.0f, 0.02f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("The size of the internal cells"); + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + + constexpr float buttonWidth = 120.0f; + const float windowWidth = ImGui::GetWindowSize().x; + ImGui::SetCursorPosX((windowWidth - buttonWidth) * 0.5f); + + if (ImNexo::Button("OK", ImVec2(buttonWidth, 0.0f))) + { + PopupManager::closePopupInContext(); + } + ImGui::Unindent(10.0f); + ImGui::PopStyleVar(); + PopupManager::closePopup(); + } + } + + void EditorScene::renderEditorCameraToolbarButton() + { + auto &selector = Selector::get(); + if (m_activeCamera == m_editorCamera) { + if (renderToolbarButton("editor_camera", ICON_FA_CAMERA, "Edit Editor Camera Setting", m_buttonGradient)) { + const auto &uuidComponent = Application::m_coordinator->getComponent(m_editorCamera); + selector.addToSelection(uuidComponent.uuid, m_editorCamera); + } + } else { + if (renderToolbarButton("switch_back", ICON_FA_EXCHANGE, "Switch back to editor camera", m_buttonGradient)) { + auto &oldCameraComponent = Application::m_coordinator->getComponent(m_activeCamera); + oldCameraComponent.active = false; + oldCameraComponent.render = false; + m_activeCamera = m_editorCamera; + auto &editorCameraComponent = Application::m_coordinator->getComponent(m_activeCamera); + editorCameraComponent.render = true; + editorCameraComponent.active = true; + } + } + } + + bool EditorScene::renderGizmoModeToolbarButton(const bool showGizmoModeMenu, ImNexo::ButtonProps &activeGizmoMode, ImNexo::ButtonProps &inactiveGizmoMode) + { + static const ImNexo::ButtonProps gizmoLocalModeButtonProps = {"local_coords", ICON_FA_CROSSHAIRS, [this]() {this->m_currentGizmoMode = ImGuizmo::MODE::LOCAL;}, nullptr, "Local coordinates"}; + static const ImNexo::ButtonProps gizmoWorldModeButtonProps = {"world_coords", ICON_FA_GLOBE, [this]() {this->m_currentGizmoMode = ImGuizmo::MODE::WORLD;}, nullptr, "World coordinates"}; + if (m_currentGizmoMode == ImGuizmo::MODE::LOCAL) { + activeGizmoMode = gizmoLocalModeButtonProps; + inactiveGizmoMode = gizmoWorldModeButtonProps; + } else { + activeGizmoMode = gizmoWorldModeButtonProps; + inactiveGizmoMode = gizmoLocalModeButtonProps; + } + return renderToolbarButton(activeGizmoMode.uniqueId, activeGizmoMode.icon, + activeGizmoMode.tooltip, showGizmoModeMenu ? m_selectedGradient : m_buttonGradient); + } + + void EditorScene::renderToolbar() + { + constexpr float buttonWidth = 35.0f; + constexpr float buttonHeight = 35.0f; + constexpr ImVec2 buttonSize{buttonWidth, buttonHeight}; + ImVec2 originalCursorPos = ImGui::GetCursorPos(); + + initialToolbarSetup(buttonWidth); + + // -------------------------------- BUTTONS ------------------------------- + // -------- Add primitve button -------- + // This can open a submenu, see at the end + ImVec2 addPrimButtonPos = ImGui::GetCursorScreenPos(); + static bool showPrimitiveMenu = false; + bool addPrimitiveClicked = renderToolbarButton( + "add_primitive", ICON_FA_PLUS_SQUARE, + "Add primitive", showPrimitiveMenu ? m_selectedGradient : m_buttonGradient); + if (addPrimitiveClicked) + showPrimitiveMenu = !showPrimitiveMenu; + + ImGui::SameLine(); + + // -------- Editor camera settings / Switch back to editor camera button -------- + renderEditorCameraToolbarButton(); + + ImGui::SameLine(); + + // -------- Gizmo operation button -------- + static const auto gizmoTranslateButtonProps = ImNexo::ButtonProps{"translate", ICON_FA_ARROWS, [this]() {this->m_currentGizmoOperation = ImGuizmo::OPERATION::TRANSLATE;}, nullptr, "Translate"}; + static const auto gizmoRotateButtonProps = ImNexo::ButtonProps{"rotate", ICON_FA_REFRESH, [this]() {this->m_currentGizmoOperation = ImGuizmo::OPERATION::ROTATE;}, nullptr, "Rotate"}; + static const auto gizmoScaleButtonProps = ImNexo::ButtonProps{"scale", ICON_FA_EXPAND, [this]() {this->m_currentGizmoOperation = ImGuizmo::OPERATION::SCALE;}, nullptr, "Scale"}; + static const auto gizmoUniversalButtonProps = ImNexo::ButtonProps{"universal", ICON_FA_ARROWS_ALT, [this]() {this->m_currentGizmoOperation = ImGuizmo::OPERATION::UNIVERSAL;}, nullptr, "Universal"}; + std::vector gizmoButtons = { + gizmoTranslateButtonProps, + gizmoRotateButtonProps, + gizmoScaleButtonProps, + gizmoUniversalButtonProps + }; + + ImNexo::ButtonProps activeOp; + switch (m_currentGizmoOperation) { + case ImGuizmo::OPERATION::TRANSLATE: + activeOp = gizmoTranslateButtonProps; + std::erase_if(gizmoButtons, [](const auto& prop) { return prop.uniqueId == "translate"; }); + break; + case ImGuizmo::OPERATION::ROTATE: + activeOp = gizmoRotateButtonProps; + std::erase_if(gizmoButtons, [](const auto& prop) { return prop.uniqueId == "rotate"; }); + break; + case ImGuizmo::OPERATION::SCALE: + activeOp = gizmoScaleButtonProps; + std::erase_if(gizmoButtons, [](const auto& prop) { return prop.uniqueId == "scale"; }); + break; + case ImGuizmo::OPERATION::UNIVERSAL: + activeOp = gizmoUniversalButtonProps; + std::erase_if(gizmoButtons, [](const auto& prop) { return prop.uniqueId == "universal"; }); + break; + default: + break; + } + + ImVec2 changeGizmoOpPos = ImGui::GetCursorScreenPos(); + static bool showGizmoOpMenu = false; + bool changeGizmoOpClicked = renderToolbarButton( + activeOp.uniqueId, activeOp.icon, + activeOp.tooltip, showGizmoOpMenu ? m_selectedGradient : m_buttonGradient); + if (changeGizmoOpClicked) + showGizmoOpMenu = !showGizmoOpMenu; + + ImGui::SameLine(); + + // -------- Gizmo operation button -------- + ImNexo::ButtonProps activeGizmoMode; + ImNexo::ButtonProps inactiveGizmoMode; + ImVec2 changeGizmoModePos = ImGui::GetCursorScreenPos(); + static bool showGizmoModeMenu = false; + bool changeGizmoModeClicked = renderGizmoModeToolbarButton(showGizmoModeMenu, activeGizmoMode, inactiveGizmoMode); + if (changeGizmoModeClicked) + showGizmoModeMenu = !showGizmoModeMenu; + + ImGui::SameLine(); + + // -------- Toggle snap button -------- + // This can open a submenu, see at the end + ImVec2 toggleSnapPos = ImGui::GetCursorScreenPos(); + static bool showSnapToggleMenu = false; + bool snapOn = m_snapRotateOn || m_snapTranslateOn; + bool toggleSnapClicked = renderToolbarButton("toggle_snap", ICON_FA_MAGNET, "Toggle gizmo snap", (showSnapToggleMenu || snapOn) ? m_selectedGradient : m_buttonGradient); + if (toggleSnapClicked) + showSnapToggleMenu = !showSnapToggleMenu; + + ImGui::SameLine(); + + // -------- Grid enabled button -------- + bool rightClicked = false; + components::RenderContext::GridParams &gridParams = Application::m_coordinator->getSingletonComponent().gridParams; + if (renderToolbarButton("grid_enabled", ICON_FA_TH_LARGE, "Enable / Disable grid", gridParams.enabled ? m_selectedGradient : m_buttonGradient, &rightClicked)) + { + gridParams.enabled = !gridParams.enabled; + } + if (rightClicked) + m_popupManager.openPopup("Grid settings", ImVec2(300, 180)); + + ImGui::SameLine(); + + // -------- Snap to gridbutton -------- + // NOTE: This seems complicated to implement using ImGuizmo, we leave it for now but i dont know if it will be implemented + if (renderToolbarButton("snap_to_grid", ICON_FA_TH, "Enable snapping to grid\n(only horizontal translation and scaling)", m_snapToGrid ? m_selectedGradient : m_buttonGradient)) + { + m_snapToGrid = !m_snapToGrid; + } + + ImGui::SameLine(); + + // -------- Enable wireframe button -------- + if (renderToolbarButton("wireframe", ICON_FA_CUBE, "Enable / Disable wireframe", m_wireframeEnabled ? m_selectedGradient : m_buttonGradient)) + { + m_wireframeEnabled = !m_wireframeEnabled; + } + + ImGui::SameLine(); + + // -------- Play button button -------- + renderToolbarButton("play", ICON_FA_PLAY, "Play scene", m_buttonGradient); + + ImGui::PopStyleVar(); + ImGui::EndChild(); + ImGui::PopStyleColor(); + + // -------------------------------- SUB-MENUS ------------------------------- + // -------- Primitives sub-menus -------- + if (showPrimitiveMenu) + { + renderPrimitiveSubMenu(addPrimButtonPos, buttonSize, showPrimitiveMenu); + } + + // -------- Gizmo operation sub-menu -------- + if (showGizmoOpMenu) + { + ImNexo::ButtonDropDown(changeGizmoOpPos, buttonSize, gizmoButtons, showGizmoOpMenu); + } + + // -------- Gizmo mode sub-menu -------- + if (showGizmoModeMenu) + { + ImNexo::ButtonDropDown(changeGizmoModePos, buttonSize, {inactiveGizmoMode}, showGizmoModeMenu); + } + + // -------- Snap sub-menu -------- + if (showSnapToggleMenu) + { + renderSnapSubMenu(toggleSnapPos, buttonSize, showSnapToggleMenu); + } + + // -------- Snap settings popup -------- + snapSettingsPopup(); + + // -------- Grid settings popup -------- + gridSettingsPopup(); + + // IMPORTANT: Restore original cursor position so we don't affect layout + ImGui::SetCursorPos(originalCursorPos); + } +} diff --git a/editor/src/DocumentWindows/EditorScene/Update.cpp b/editor/src/DocumentWindows/EditorScene/Update.cpp new file mode 100644 index 000000000..e0670deec --- /dev/null +++ b/editor/src/DocumentWindows/EditorScene/Update.cpp @@ -0,0 +1,137 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the update of the editor scene +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorScene.hpp" +#include "Types.hpp" +#include "context/Selector.hpp" +#include "components/Uuid.hpp" + +namespace nexo::editor { + int EditorScene::sampleEntityTexture(const float mx, const float my) const + { + const auto &coord = Application::m_coordinator; + const auto &cameraComponent = coord->getComponent(static_cast(m_activeCamera)); + + cameraComponent.m_renderTarget->bind(); + const int entityId = cameraComponent.m_renderTarget->getPixel(1, static_cast(mx), static_cast(my)); + cameraComponent.m_renderTarget->unbind(); + return entityId; + } + + static SelectionType getSelectionType(const int entityId) + { + const auto &coord = Application::m_coordinator; + auto selType = SelectionType::ENTITY; + if (coord->entityHasComponent(entityId)) { + selType = SelectionType::CAMERA; + } else if (coord->entityHasComponent(entityId)) { + selType = SelectionType::DIR_LIGHT; + } else if (coord->entityHasComponent(entityId)) { + selType = SelectionType::POINT_LIGHT; + } else if (coord->entityHasComponent(entityId)) { + selType = SelectionType::SPOT_LIGHT; + } else if (coord->entityHasComponent(entityId)) { + selType = SelectionType::AMBIENT_LIGHT; + } + return selType; + } + + void EditorScene::updateWindowState() + { + const auto &selector = Selector::get(); + if (selector.hasSelection()) { + if (m_currentGizmoOperation == ImGuizmo::OPERATION::TRANSLATE) + m_windowState = m_gizmoTranslateState; + else if (m_currentGizmoOperation == ImGuizmo::OPERATION::ROTATE) + m_windowState = m_gizmoRotateState; + else if (m_currentGizmoOperation == ImGuizmo::OPERATION::SCALE) + m_windowState = m_gizmoScaleState; + else + m_windowState = m_gizmoState; + } + } + + void EditorScene::updateSelection(const int entityId, const bool isShiftPressed, const bool isCtrlPressed) + { + const auto &coord = Application::m_coordinator; + const auto uuid = coord->tryGetComponent(entityId); + if (!uuid) + return; + + // Determine selection type + const SelectionType selType = getSelectionType(entityId); + auto &selector = Selector::get(); + + // Handle different selection modes + if (isCtrlPressed) + selector.toggleSelection(uuid->get().uuid, entityId, selType); + else if (isShiftPressed) + selector.addToSelection(uuid->get().uuid, entityId, selType); + else + selector.selectEntity(uuid->get().uuid, entityId, selType); + + updateWindowState(); + selector.setSelectedScene(m_sceneId); + } + + void EditorScene::handleSelection() + { + auto [mx, my] = ImGui::GetMousePos(); + mx -= m_viewportBounds[0].x; + my -= m_viewportBounds[0].y; + + // Flip the y-coordinate to match opengl texture format + my = m_contentSize.y - my; + + // Check if mouse is inside viewport + if (!(mx >= 0 && my >= 0 && mx < m_contentSize.x && my < m_contentSize.y)) + return; + const int entityId = sampleEntityTexture(mx, my); + + // Check for multi-selection key modifiers + const bool isShiftPressed = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift); + const bool isCtrlPressed = ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl); + auto &selector = Selector::get(); + + if (entityId == -1) { + // Clicked on empty space - clear selection unless shift/ctrl is held + if (!isShiftPressed && !isCtrlPressed) { + selector.clearSelection(); + m_windowState = m_globalState; + } + return; + } + + updateSelection(entityId, isShiftPressed, isCtrlPressed); + } + + void EditorScene::update() + { + const bool isCurrentlyVisible = m_isVisibleInDock || m_wasVisibleLastFrame; + + if (!m_opened || m_activeCamera == -1 || !isCurrentlyVisible) + return; + const SceneType sceneType = m_activeCamera == m_editorCamera ? SceneType::EDITOR : SceneType::GAME; + Application::SceneInfo sceneInfo{static_cast(m_sceneId), RenderingType::FRAMEBUFFER, sceneType}; + sceneInfo.isChildWindow = true; + sceneInfo.viewportBounds[0] = glm::vec2{m_viewportBounds[0].x, m_viewportBounds[0].y}; + sceneInfo.viewportBounds[1] = glm::vec2{m_viewportBounds[1].x, m_viewportBounds[1].y}; + runEngine(sceneInfo); + + + // Handle mouse clicks for selection + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGuizmo::IsUsing() && m_focused) + handleSelection(); + } +} diff --git a/editor/src/DocumentWindows/EntityProperties/AEntityProperty.hpp b/editor/src/DocumentWindows/EntityProperties/AEntityProperty.hpp index 570af6509..762c37189 100644 --- a/editor/src/DocumentWindows/EntityProperties/AEntityProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/AEntityProperty.hpp @@ -27,17 +27,17 @@ namespace nexo::editor { }; class AEntityProperty : public IEntityProperty { - public: - /** - * @brief Constructs an AEntityProperty instance. - * - * Initializes the entity property by storing a reference to the provided InspectorWindow, - * which is used for displaying and managing entity properties in the editor. - * - * @param inspector Reference to the InspectorWindow associated with this property. - */ - explicit AEntityProperty(InspectorWindow &inspector) : m_inspector(inspector) {}; - protected: - InspectorWindow &m_inspector; + public: + /** + * @brief Constructs an AEntityProperty instance. + * + * Initializes the entity property by storing a reference to the provided InspectorWindow, + * which is used for displaying and managing entity properties in the editor. + * + * @param inspector Reference to the InspectorWindow associated with this property. + */ + explicit AEntityProperty(InspectorWindow &inspector) : m_inspector(inspector) {}; + protected: + InspectorWindow &m_inspector; }; }; diff --git a/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.cpp b/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.cpp index 5ce11724c..b53fc5f5e 100644 --- a/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.cpp @@ -14,26 +14,35 @@ #include "AmbientLightProperty.hpp" #include "components/Light.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "ImNexo/Widgets.hpp" +#include "context/actions/EntityActions.hpp" +#include "context/ActionManager.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "ImNexo/ImNexo.hpp" +#include namespace nexo::editor { - void AmbientLightProperty::show(const ecs::Entity entity) - { + void AmbientLightProperty::show(const ecs::Entity entity) + { auto& ambientComponent = Application::getEntityComponent(entity); + static components::AmbientLightComponent::Memento beforeState; - if (EntityPropertiesComponents::drawHeader("##AmbientNode", "Ambient light")) + if (ImNexo::Header("##AmbientNode", "Ambient light")) { - ImGui::Spacing(); - static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPicker = false; - ImGui::Text("Color"); - ImGui::SameLine(); - glm::vec4 color = {ambientComponent.color, 1.0f}; - Widgets::drawColorEditor("##ColorEditor Ambient light", &color, &colorPickerMode, &showColorPicker); - ambientComponent.color = color; - ImGui::TreePop(); + auto ambientComponentCopy = ambientComponent; + ImNexo::resetItemStates(); + ImNexo::Ambient(ambientComponent); + if (ImNexo::isItemActivated()) { + beforeState = ambientComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = ambientComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + beforeState = components::AmbientLightComponent::Memento{}; + } + + ImGui::TreePop(); } - } + } } diff --git a/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.hpp b/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.hpp index 45f279d89..f59cac886 100644 --- a/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/AmbientLightProperty.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class AmbientLightProperty : public AEntityProperty { + class AmbientLightProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/CameraController.cpp b/editor/src/DocumentWindows/EntityProperties/CameraController.cpp index 6f1045aa4..d869a6d95 100644 --- a/editor/src/DocumentWindows/EntityProperties/CameraController.cpp +++ b/editor/src/DocumentWindows/EntityProperties/CameraController.cpp @@ -13,32 +13,35 @@ /////////////////////////////////////////////////////////////////////////////// #include "CameraController.hpp" +#include "ImNexo/ImNexo.hpp" #include "components/Camera.hpp" -#include "Components/EntityPropertiesComponents.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "context/ActionManager.hpp" +#include "context/actions/EntityActions.hpp" namespace nexo::editor { void CameraController::show(const ecs::Entity entity) { auto& controllerComponent = Application::getEntityComponent(entity); + static components::PerspectiveCameraController::Memento beforeState; - if (EntityPropertiesComponents::drawHeader("##ControllerNode", "Camera Controller")) + if (ImNexo::Header("##ControllerNode", "Camera Controller")) { - ImGui::Spacing(); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - if (ImGui::BeginTable("InspectorControllerTable", 2, - ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat1("Mouse sensitivity", "", &controllerComponent.mouseSensitivity); - - ImGui::EndTable(); + ImGui::Spacing(); + const auto controllerComponentCopy = controllerComponent; + ImNexo::resetItemStates(); + ImNexo::CameraController(controllerComponent); + if (ImNexo::isItemActivated()) { + beforeState = controllerComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = controllerComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + beforeState = components::PerspectiveCameraController::Memento{}; } - ImGui::PopStyleVar(); - ImGui::TreePop(); + ImGui::TreePop(); } } } diff --git a/editor/src/DocumentWindows/EntityProperties/CameraController.hpp b/editor/src/DocumentWindows/EntityProperties/CameraController.hpp index b263c1ece..18a620016 100644 --- a/editor/src/DocumentWindows/EntityProperties/CameraController.hpp +++ b/editor/src/DocumentWindows/EntityProperties/CameraController.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class CameraController : public AEntityProperty { + class CameraController final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/CameraProperty.cpp b/editor/src/DocumentWindows/EntityProperties/CameraProperty.cpp index 3fe1abbd8..f58efc729 100644 --- a/editor/src/DocumentWindows/EntityProperties/CameraProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/CameraProperty.cpp @@ -13,78 +13,34 @@ /////////////////////////////////////////////////////////////////////////////// #include "CameraProperty.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "ImNexo/ImNexo.hpp" #include "components/Camera.hpp" -#include "IconsFontAwesome.h" -#include "Components/Components.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "context/ActionManager.hpp" +#include "context/actions/EntityActions.hpp" namespace nexo::editor { void CameraProperty::show(const ecs::Entity entity) { auto& cameraComponent = Application::getEntityComponent(entity); - - if (EntityPropertiesComponents::drawHeader("##CameraNode", "Camera")) - { - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - if (ImGui::BeginTable("InspectorViewPortParams", 4, - ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Lock", ImGuiTableColumnFlags_WidthStretch); - glm::vec2 viewPort = {cameraComponent.width, cameraComponent.height}; - std::vector badgeColors; - std::vector textBadgeColors; - - const bool disabled = cameraComponent.viewportLocked; - if (disabled) - ImGui::BeginDisabled(); - if (EntityPropertiesComponents::drawRowDragFloat2("Viewport size", "W", "H", &viewPort.x, -FLT_MAX, FLT_MAX, 1.0f, badgeColors, textBadgeColors, disabled)) - { - if (!cameraComponent.viewportLocked) - cameraComponent.resize(static_cast(viewPort.x), static_cast(viewPort.y)); - } - if (disabled) - ImGui::EndDisabled(); - - ImGui::TableSetColumnIndex(3); - - // Lock button - const std::string lockBtnLabel = cameraComponent.viewportLocked ? ICON_FA_LOCK "##ViewPortSettings" : ICON_FA_UNLOCK "##ViewPortSettings"; - if (Components::drawButton(lockBtnLabel)) { - cameraComponent.viewportLocked = !cameraComponent.viewportLocked; - } - - - ImGui::EndTable(); - } - - if (ImGui::BeginTable("InspectorCameraVariables", 2, ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat1("FOV", "", &cameraComponent.fov, 30.0f, 120.0f, 0.3f); - EntityPropertiesComponents::drawRowDragFloat1("Near plane", "", &cameraComponent.nearPlane, 0.01f, 1.0f, 0.001f); - EntityPropertiesComponents::drawRowDragFloat1("Far plane", "", &cameraComponent.farPlane, 100.0f, 10000.0f, 1.0f); - - ImGui::EndTable(); + static components::CameraComponent::Memento beforeState; + + if (ImNexo::Header("##CameraNode", "Camera")) + { + const auto cameraComponentCopy = cameraComponent; + ImNexo::resetItemStates(); + ImNexo::Camera(cameraComponent); + if (ImNexo::isItemActivated()) { + beforeState = cameraComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = cameraComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + beforeState = components::CameraComponent::Memento{}; } - - - ImGui::PopStyleVar(); - - ImGui::Spacing(); - static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPicker = false; - ImGui::Text("Clear Color"); - ImGui::SameLine(); - Widgets::drawColorEditor("##ColorEditor Spot light", &cameraComponent.clearColor, &colorPickerMode, &showColorPicker); - - ImGui::TreePop(); + ImGui::TreePop(); } } } diff --git a/editor/src/DocumentWindows/EntityProperties/CameraProperty.hpp b/editor/src/DocumentWindows/EntityProperties/CameraProperty.hpp index d80c49fdb..aa771fc6c 100644 --- a/editor/src/DocumentWindows/EntityProperties/CameraProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/CameraProperty.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class CameraProperty : public AEntityProperty { + class CameraProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/CameraTarget.cpp b/editor/src/DocumentWindows/EntityProperties/CameraTarget.cpp new file mode 100644 index 000000000..0ec702f32 --- /dev/null +++ b/editor/src/DocumentWindows/EntityProperties/CameraTarget.cpp @@ -0,0 +1,47 @@ +//// CameraTarget.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Source file for the camera target property +// +/////////////////////////////////////////////////////////////////////////////// + +#include "CameraTarget.hpp" +#include "Definitions.hpp" +#include "ImNexo/ImNexo.hpp" +#include "components/Camera.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "context/ActionManager.hpp" + +namespace nexo::editor { + + void CameraTarget::show(const ecs::Entity entity) + { + auto& targetComponent = Application::getEntityComponent(entity); + static components::PerspectiveCameraTarget::Memento beforeState; + + if (ImNexo::Header("##TargetNode", "Camera Target")) + { + const auto targetComponentCopy = targetComponent; + ImGui::Spacing(); + ImNexo::resetItemStates(); + ImNexo::CameraTarget(targetComponent); + if (ImNexo::isItemActivated()) { + beforeState = targetComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = targetComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + beforeState = components::PerspectiveCameraTarget::Memento{}; + } + ImGui::TreePop(); + } + } +} diff --git a/editor/src/DocumentWindows/EntityProperties/CameraTarget.hpp b/editor/src/DocumentWindows/EntityProperties/CameraTarget.hpp new file mode 100644 index 000000000..30598254c --- /dev/null +++ b/editor/src/DocumentWindows/EntityProperties/CameraTarget.hpp @@ -0,0 +1,25 @@ +//// CameraTarget.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header file for the camera target component +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "AEntityProperty.hpp" + +namespace nexo::editor { + class CameraTarget final : public AEntityProperty { + public: + using AEntityProperty::AEntityProperty; + + void show(ecs::Entity entity) override; + }; +} diff --git a/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.cpp b/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.cpp index c1a3315e5..f972cba18 100644 --- a/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.cpp @@ -13,43 +13,33 @@ /////////////////////////////////////////////////////////////////////////////// #include "DirectionalLightProperty.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "ImNexo/ImNexo.hpp" #include "components/Light.hpp" +#include "ImNexo/Widgets.hpp" +#include "context/ActionManager.hpp" namespace nexo::editor { - void DirectionalLightProperty::show(const ecs::Entity entity) - { + void DirectionalLightProperty::show(const ecs::Entity entity) + { auto& directionalComponent = Application::getEntityComponent(entity); + static components::DirectionalLightComponent::Memento beforeState; - - if (EntityPropertiesComponents::drawHeader("##DirectionalNode", "Directional light")) + if (ImNexo::Header("##DirectionalNode", "Directional light")) { - ImGui::Spacing(); - static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPicker = false; - ImGui::Text("Color"); - ImGui::SameLine(); - glm::vec4 color = {directionalComponent.color, 1.0f}; - Widgets::drawColorEditor("##ColorEditor Directional light", &color, &colorPickerMode, &showColorPicker); - directionalComponent.color = color; - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - if (ImGui::BeginTable("InspectorDirectionTable", 4, - ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat3("Direction", "X", "Y", "Z", &directionalComponent.direction.x); - - ImGui::EndTable(); + const auto directionalComponentCopy = directionalComponent; + ImNexo::resetItemStates(); + ImNexo::DirectionalLight(directionalComponent); + if (ImNexo::isItemActivated()) { + beforeState = directionalComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = directionalComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + beforeState = components::DirectionalLightComponent::Memento{}; } - ImGui::PopStyleVar(); - ImGui::TreePop(); + ImGui::TreePop(); } - } + } } diff --git a/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.hpp b/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.hpp index 143319525..9aa41f16c 100644 --- a/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/DirectionalLightProperty.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class DirectionalLightProperty : public AEntityProperty { + class DirectionalLightProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/PointLightProperty.cpp b/editor/src/DocumentWindows/EntityProperties/PointLightProperty.cpp index 8bc02f816..0f9c4549c 100644 --- a/editor/src/DocumentWindows/EntityProperties/PointLightProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/PointLightProperty.cpp @@ -13,54 +13,43 @@ /////////////////////////////////////////////////////////////////////////////// #include "PointLightProperty.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "ImNexo/ImNexo.hpp" #include "components/Light.hpp" -#include "math/Light.hpp" +#include "components/Transform.hpp" +#include "context/actions/EntityActions.hpp" +#include "ImNexo/Widgets.hpp" +#include "context/ActionManager.hpp" namespace nexo::editor { void PointLightProperty::show(const ecs::Entity entity) { - auto& pointComponent = nexo::Application::getEntityComponent(entity); + auto& pointComponent = Application::getEntityComponent(entity); + auto &transform = Application::getEntityComponent(entity); - if (EntityPropertiesComponents::drawHeader("##PointNode", "Point light")) - { - ImGui::Spacing(); - static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPicker = false; - ImGui::Text("Color"); - ImGui::SameLine(); - glm::vec4 color = {pointComponent.color, 1.0f}; - Widgets::drawColorEditor("##ColorEditor Point light", &color, &colorPickerMode, &showColorPicker); - pointComponent.color = color; - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - if (ImGui::BeginTable("InspectorPointTable", 4, - ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat3("Position", "X", "Y", "Z", &pointComponent.pos.x); + static components::PointLightComponent::Memento beforeStatePoint; + static components::TransformComponent::Memento beforeStateTransform; - ImGui::EndTable(); - } - - ImGui::Spacing(); - ImGui::Text("Distance"); - ImGui::SameLine(); - if (ImGui::DragFloat("##DistanceSlider", &pointComponent.maxDistance, 1.0f, 1.0f, 3250.0f)) - { - // Recompute the attenuation from the distance - auto [lin, quad] = math::computeAttenuationFromDistance(pointComponent.maxDistance); - pointComponent.constant = 1.0f; - pointComponent.linear = lin; - pointComponent.quadratic = quad; + if (ImNexo::Header("##PointNode", "Point light")) + { + auto pointComponentCopy = pointComponent; + auto transformComponentCopy = transform; + ImNexo::resetItemStates(); + ImNexo::PointLight(pointComponent, transform); + if (ImNexo::isItemActivated()) { + beforeStatePoint = pointComponentCopy.save(); + beforeStateTransform = transformComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterStatePoint = pointComponent.save(); + auto afterStateTransform = transform.save(); + auto actionGroup = ActionManager::createActionGroup(); + auto pointAction = std::make_unique>(entity, beforeStatePoint, afterStatePoint); + auto transformAction = std::make_unique>(entity, beforeStateTransform, afterStateTransform); + actionGroup->addAction(std::move(pointAction)); + actionGroup->addAction(std::move(transformAction)); + ActionManager::get().recordAction(std::move(actionGroup)); } - ImGui::PopStyleVar(); ImGui::TreePop(); } } diff --git a/editor/src/DocumentWindows/EntityProperties/PointLightProperty.hpp b/editor/src/DocumentWindows/EntityProperties/PointLightProperty.hpp index decacab67..07baa7547 100644 --- a/editor/src/DocumentWindows/EntityProperties/PointLightProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/PointLightProperty.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class PointLightProperty : public AEntityProperty { + class PointLightProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp b/editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp index 623e4bee3..2ddd4151a 100644 --- a/editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp @@ -17,18 +17,23 @@ #include "RenderProperty.hpp" #include "AEntityProperty.hpp" #include "Application.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "DocumentWindows/PopupManager.hpp" #include "Framebuffer.hpp" +#include "components/Light.hpp" +#include "context/actions/EntityActions.hpp" #include "utils/ScenePreview.hpp" #include "components/Camera.hpp" #include "components/Render.hpp" -#include "DocumentWindows/InspectorWindow.hpp" -#include "DocumentWindows/MaterialInspector.hpp" +#include "DocumentWindows/InspectorWindow/InspectorWindow.hpp" +#include "DocumentWindows/MaterialInspector/MaterialInspector.hpp" +#include "context/ActionManager.hpp" +#include "ImNexo/Panels.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/Components.hpp" namespace nexo::editor { - void RenderProperty::createMaterialPopup(ecs::Entity entity) const + void RenderProperty::createMaterialPopup(const ecs::Entity entity) { ImGui::Text("Create New Material"); ImGui::Separator(); @@ -37,7 +42,7 @@ namespace nexo::editor { const float totalWidth = availSize.x; float totalHeight = availSize.y - 40; // Reserve space for bottom buttons - // Define layout: 60% for inspector, 40% for preview + // Define layout: 40% for inspector, 60% for preview const float inspectorWidth = totalWidth * 0.4f; const float previewWidth = totalWidth - inspectorWidth - 8; // Subtract spacing between panels @@ -47,6 +52,7 @@ namespace nexo::editor { utils::genScenePreview("New Material Preview", {previewWidth - 8, totalHeight}, entity, scenePreviewInfo); auto &cameraComponent = Application::m_coordinator->getComponent(scenePreviewInfo.cameraId); cameraComponent.clearColor = {67.0f/255.0f, 65.0f/255.0f, 80.0f/255.0f, 111.0f/255.0f}; + cameraComponent.render = true; } auto renderable3D = std::dynamic_pointer_cast(nexo::Application::m_coordinator->getComponent(scenePreviewInfo.entityCopy).renderable); @@ -60,7 +66,7 @@ namespace nexo::editor { ImGui::InputText("Name", materialName, IM_ARRAYSIZE(materialName)); ImGui::Spacing(); - Widgets::drawMaterialInspector(&renderable3D->material); + ImNexo::MaterialInspector(&renderable3D->material); ImGui::EndChild(); } ImGui::NextColumn(); @@ -69,7 +75,8 @@ namespace nexo::editor { ImGui::BeginChild("MaterialPreview", ImVec2(previewWidth - 4, totalHeight), true); auto &app = getApp(); - app.run(scenePreviewInfo.sceneId, RenderingType::FRAMEBUFFER); + const Application::SceneInfo sceneInfo{scenePreviewInfo.sceneId, nexo::RenderingType::FRAMEBUFFER}; + app.run(sceneInfo); auto const &cameraComponent = Application::m_coordinator->getComponent(scenePreviewInfo.cameraId); const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); @@ -80,8 +87,8 @@ namespace nexo::editor { const float displayWidth = displayHeight * aspectRatio; ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + 4, ImGui::GetCursorPosY() + 4)); - ImGui::Image(static_cast(static_cast(textureId)), - ImVec2(displayWidth, displayHeight), ImVec2(0, 1), ImVec2(1, 0)); + ImNexo::Image(static_cast(static_cast(textureId)), + ImVec2(displayWidth, displayHeight)); ImGui::EndChild(); } @@ -92,7 +99,7 @@ namespace nexo::editor { // Bottom buttons - centered constexpr float buttonWidth = 120.0f; - if (ImGui::Button("OK", ImVec2(buttonWidth, 0))) + if (ImNexo::Button("OK", ImVec2(buttonWidth, 0))) { // TODO: Insert logic to create the new material @@ -109,7 +116,7 @@ namespace nexo::editor { ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) + if (ImNexo::Button("Cancel", ImVec2(buttonWidth, 0))) { if (scenePreviewInfo.sceneGenerated) { @@ -124,6 +131,10 @@ namespace nexo::editor { void RenderProperty::show(ecs::Entity entity) { + if (Application::m_coordinator->entityHasComponent(entity) || + Application::m_coordinator->entityHasComponent(entity) || + Application::m_coordinator->entityHasComponent(entity)) + return; auto& renderComponent = Application::getEntityComponent(entity); if (renderComponent.type == components::RenderType::RENDER_3D) @@ -140,39 +151,42 @@ namespace nexo::editor { } static bool sectionOpen = true; - if (EntityPropertiesComponents::drawHeader("##RenderNode", "Render Component")) + if (ImNexo::Header("##RenderNode", "Render Component")) { - //ImGui::SetWindowFontScale(1.15f); ImGui::Text("Hide"); ImGui::SameLine(0, 12); bool hidden = !renderComponent.isRendered; - ImGui::Checkbox("##HideCheckBox", &hidden); - renderComponent.isRendered = !hidden; + if (ImGui::Checkbox("##HideCheckBox", &hidden)) { + auto beforeState = renderComponent.save(); + renderComponent.isRendered = !hidden; + auto afterState = renderComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); + } - EntityPropertiesComponents::drawToggleButtonWithSeparator("Material", §ionOpen); - static std::shared_ptr framebuffer = nullptr; + ImNexo::ToggleButtonWithSeparator("Material", §ionOpen); + static std::shared_ptr framebuffer = nullptr; static int entityBase = -1; if (sectionOpen) { - if (entityBase != static_cast(entity)) - { - //TODO: I guess all of this should be centralized in the assets - utils::ScenePreviewOut previewParams; - utils::genScenePreview("Modify material inspector", {64, 64}, entity, previewParams); - auto &app = nexo::getApp(); - app.getSceneManager().getScene(previewParams.sceneId).setActiveStatus(false); - auto &cameraComponent = Application::m_coordinator->getComponent(previewParams.cameraId); - cameraComponent.clearColor = {0.05f, 0.05f, 0.05f, 0.0f}; - app.run(previewParams.sceneId, RenderingType::FRAMEBUFFER); - framebuffer = cameraComponent.m_renderTarget; - app.getSceneManager().deleteScene(previewParams.sceneId); - entityBase = static_cast(entity); - } + if (entityBase != static_cast(entity)) + { + //TODO: I guess all of this should be centralized in the assets + utils::ScenePreviewOut previewParams; + utils::genScenePreview("Modify material inspector", {64, 64}, entity, previewParams); + auto &app = nexo::getApp(); + app.getSceneManager().getScene(previewParams.sceneId).setActiveStatus(false); + const Application::SceneInfo sceneInfo{previewParams.sceneId, nexo::RenderingType::FRAMEBUFFER}; + app.run(sceneInfo); + const auto &cameraComponent = Application::m_coordinator->getComponent(previewParams.cameraId); + framebuffer = cameraComponent.m_renderTarget; + app.getSceneManager().deleteScene(previewParams.sceneId); + entityBase = static_cast(entity); + } // --- Material Preview --- - if (framebuffer->getColorAttachmentId(0) != 0) - ImGui::Image(static_cast(static_cast(framebuffer->getColorAttachmentId(0))), ImVec2(64, 64), ImVec2(0, 1), ImVec2(1, 0)); - + if (framebuffer && framebuffer->getColorAttachmentId(0) != 0) + ImNexo::Image(static_cast(static_cast(framebuffer->getColorAttachmentId(0))), ImVec2(64, 64)); ImGui::SameLine(); ImGui::BeginGroup(); @@ -183,12 +197,12 @@ namespace nexo::editor { ImGui::Combo("##MaterialType", &selectedMaterialIndex, materialTypes, IM_ARRAYSIZE(materialTypes)); // --- Material Action Buttons --- - if (ImGui::Button("Create new material")) + if (ImNexo::Button("Create new material")) { - m_popupManager.openPopup("Create new material"); + m_popupManager.openPopup("Create new material", ImVec2(1440,900)); } ImGui::SameLine(); - if (ImGui::Button("Modify Material")) + if (ImNexo::Button("Modify Material")) { m_inspector.setSubInspectorVisibility(true); } @@ -201,11 +215,10 @@ namespace nexo::editor { ImGui::TreePop(); } - ImGui::SetNextWindowSize(ImVec2(1440,900)); if (m_popupManager.showPopupModal("Create new material")) { createMaterialPopup(entity); - m_popupManager.closePopup(); + PopupManager::closePopup(); } } } diff --git a/editor/src/DocumentWindows/EntityProperties/RenderProperty.hpp b/editor/src/DocumentWindows/EntityProperties/RenderProperty.hpp index a31bca12f..b83ea1ebb 100644 --- a/editor/src/DocumentWindows/EntityProperties/RenderProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/RenderProperty.hpp @@ -18,7 +18,7 @@ #include "DocumentWindows/PopupManager.hpp" namespace nexo::editor { - class RenderProperty : public nexo::editor::AEntityProperty { + class RenderProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; @@ -46,7 +46,7 @@ namespace nexo::editor { * * @param entity The entity associated with the material being created. */ - void createMaterialPopup(ecs::Entity entity) const; + static void createMaterialPopup(ecs::Entity entity); private: PopupManager m_popupManager; }; diff --git a/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.cpp b/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.cpp index 0375d54d6..7630696fb 100644 --- a/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.cpp @@ -13,66 +13,43 @@ /////////////////////////////////////////////////////////////////////////////// #include "SpotLightProperty.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "Components/Widgets.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "ImNexo/ImNexo.hpp" #include "components/Light.hpp" -#include "math/Light.hpp" +#include "components/Transform.hpp" +#include "context/actions/EntityActions.hpp" +#include "ImNexo/Widgets.hpp" +#include "context/ActionManager.hpp" namespace nexo::editor { void SpotLightProperty::show(ecs::Entity entity) { auto& spotComponent = Application::getEntityComponent(entity); + auto &transformComponent = Application::getEntityComponent(entity); - if (EntityPropertiesComponents::drawHeader("##SpotNode", "Spot light")) - { - ImGui::Spacing(); - static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; - static bool showColorPicker = false; - ImGui::Text("Color"); - ImGui::SameLine(); - glm::vec4 color = {spotComponent.color, 1.0f}; - Widgets::drawColorEditor("##ColorEditor Spot light", &color, &colorPickerMode, &showColorPicker); - spotComponent.color = color; - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - if (ImGui::BeginTable("InspectorSpotTable", 4, - ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat3("Direction", "X", "Y", "Z", &spotComponent.direction.x, -FLT_MAX, FLT_MAX, 0.1f); - EntityPropertiesComponents::drawRowDragFloat3("Position", "X", "Y", "Z", &spotComponent.pos.x, -FLT_MAX, FLT_MAX, 0.1f); - - - ImGui::EndTable(); - } - - if (ImGui::BeginTable("InspectorCutOffSpotTable", 2, ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - if (EntityPropertiesComponents::drawRowDragFloat1("Distance", "", &spotComponent.maxDistance, 1.0f, 3250.0f, 1.0f)) - { - auto [lin, quad] = math::computeAttenuationFromDistance(spotComponent.maxDistance); - spotComponent.linear = lin; - spotComponent.quadratic = quad; - } - float innerCutOffDegrees = glm::degrees(glm::acos(spotComponent.cutOff)); - float outerCutOffDegrees = glm::degrees(glm::acos(spotComponent.outerCutoff)); - if (EntityPropertiesComponents::drawRowDragFloat1("Inner cut off", "", &innerCutOffDegrees, 0.0f, outerCutOffDegrees, 0.5f)) - spotComponent.cutOff = glm::cos(glm::radians(innerCutOffDegrees)); - if (EntityPropertiesComponents::drawRowDragFloat1("Outer cut off", "", &outerCutOffDegrees, innerCutOffDegrees, 90.0f, 0.5f)) - spotComponent.outerCutoff = glm::cos(glm::radians(outerCutOffDegrees)); + static components::SpotLightComponent::Memento beforeStateSpot; + static components::TransformComponent::Memento beforeStateTransform; - ImGui::EndTable(); + if (ImNexo::Header("##SpotNode", "Spot light")) + { + ImNexo::resetItemStates(); + auto spotComponentCopy = spotComponent; + auto transformComponentCopy = transformComponent; + ImNexo::SpotLight(spotComponent, transformComponent); + if (ImNexo::isItemActivated()) { + beforeStateSpot = spotComponentCopy.save(); + beforeStateTransform = transformComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterStateSpot = spotComponent.save(); + auto afterStateTransform = transformComponent.save(); + auto actionGroup = ActionManager::createActionGroup(); + auto spotAction = std::make_unique>(entity, beforeStateSpot, afterStateSpot); + auto transformAction = std::make_unique>(entity, beforeStateTransform, afterStateTransform); + actionGroup->addAction(std::move(spotAction)); + actionGroup->addAction(std::move(transformAction)); + ActionManager::get().recordAction(std::move(actionGroup)); } - - ImGui::PopStyleVar(); ImGui::TreePop(); } } diff --git a/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.hpp b/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.hpp index 99667a40b..cf451c3b1 100644 --- a/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/SpotLightProperty.hpp @@ -16,7 +16,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class SpotLightProperty : public AEntityProperty { + class SpotLightProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp b/editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp index 1ed39ab49..daefd6a58 100644 --- a/editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp +++ b/editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp @@ -15,53 +15,36 @@ #include #include "TransformProperty.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "math/Vector.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/EntityProperties.hpp" +#include "ImNexo/ImNexo.hpp" +#include "components/Light.hpp" +#include "components/Transform.hpp" +#include "context/ActionManager.hpp" namespace nexo::editor { void TransformProperty::show(ecs::Entity entity) { - auto& [pos, size, quat] = Application::getEntityComponent(entity); - + if (Application::m_coordinator->entityHasComponent(entity) || + Application::m_coordinator->entityHasComponent(entity)) + return; + auto& transformComponent = Application::getEntityComponent(entity); static glm::vec3 lastDisplayedEuler(0.0f); + static components::TransformComponent::Memento beforeState; - if (EntityPropertiesComponents::drawHeader("##TransformNode", "Transform Component")) + if (ImNexo::Header("##TransformNode", "Transform Component")) { - // Increase cell padding so rows have more space: - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); - - if (ImGui::BeginTable("InspectorTransformTable", 4, - ImGuiTableFlags_SizingStretchProp)) - { - // Only the first column has a fixed width - ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); - - EntityPropertiesComponents::drawRowDragFloat3("Position", "X", "Y", "Z", &pos.x); - - const glm::vec3 computedEuler = math::customQuatToEuler(quat); - - lastDisplayedEuler = computedEuler; - glm::vec3 rotation = lastDisplayedEuler; - - // Draw the Rotation row. - // When the user edits the rotation, we compute the delta from the last displayed Euler, - // convert that delta into an incremental quaternion, and update the master quaternion. - if (EntityPropertiesComponents::drawRowDragFloat3("Rotation", "X", "Y", "Z", &rotation.x)) { - const glm::vec3 deltaEuler = rotation - lastDisplayedEuler; - const glm::quat deltaQuat = glm::radians(deltaEuler); - quat = glm::normalize(deltaQuat * quat); - lastDisplayedEuler = math::customQuatToEuler(quat); - rotation = lastDisplayedEuler; - } - EntityPropertiesComponents::drawRowDragFloat3("Scale", "X", "Y", "Z", &size.x); - - ImGui::EndTable(); + const auto transformComponentCopy = transformComponent; + ImNexo::resetItemStates(); + ImNexo::Transform(transformComponent, lastDisplayedEuler); + if (ImNexo::isItemActivated()) { + beforeState = transformComponentCopy.save(); + } else if (ImNexo::isItemDeactivated()) { + auto afterState = transformComponent.save(); + auto action = std::make_unique>(entity, beforeState, afterState); + ActionManager::get().recordAction(std::move(action)); } - ImGui::PopStyleVar(); ImGui::TreePop(); } } diff --git a/editor/src/DocumentWindows/EntityProperties/TransformProperty.hpp b/editor/src/DocumentWindows/EntityProperties/TransformProperty.hpp index a4a3f6076..9f844dded 100644 --- a/editor/src/DocumentWindows/EntityProperties/TransformProperty.hpp +++ b/editor/src/DocumentWindows/EntityProperties/TransformProperty.hpp @@ -17,7 +17,7 @@ #include "AEntityProperty.hpp" namespace nexo::editor { - class TransformProperty : public nexo::editor::AEntityProperty { + class TransformProperty final : public AEntityProperty { public: using AEntityProperty::AEntityProperty; diff --git a/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.cpp b/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.cpp new file mode 100644 index 000000000..d893efd30 --- /dev/null +++ b/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.cpp @@ -0,0 +1,111 @@ +//// TypeErasedProperty.cpp /////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Implementation file for the type erased property class +// used to display and edit entity properties +// for C# defined components +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "TypeErasedProperty.hpp" + +#include "ImNexo/Components.hpp" +#include "ImNexo/Elements.hpp" + +namespace nexo::editor { + + void showField(const ecs::Field& field, void *data) + { + switch (field.type) { + case ecs::FieldType::Bool: + static_assert(sizeof(bool) == 1 && "Size of bool must be 1 byte"); + ImGui::Checkbox(field.name.c_str(), static_cast(data)); + break; + case ecs::FieldType::Int8: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_S8, data); + break; + case ecs::FieldType::Int16: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_S16, data); + break; + case ecs::FieldType::Int32: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_S32, data); + break; + case ecs::FieldType::Int64: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_S64, data); + break; + case ecs::FieldType::UInt8: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_U8, data); + case ecs::FieldType::UInt16: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_U16, data); + case ecs::FieldType::UInt32: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_U32, data); + case ecs::FieldType::UInt64: + ImGui::InputScalar(field.name.c_str(), ImGuiDataType_U64, data); + break; + case ecs::FieldType::Float: + ImGui::InputFloat(field.name.c_str(), static_cast(data)); + break; + case ecs::FieldType::Double: + ImGui::InputDouble(field.name.c_str(), static_cast(data)); + break; + + // Widgets + case ecs::FieldType::Vector3: + if (ImGui::BeginTable("InspectorTransformTable", 4, + ImGuiTableFlags_SizingStretchProp)) + { + // Only the first column has a fixed width + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + ImNexo::RowDragFloat3(field.name.c_str(), "X", "Y", "Z", static_cast(data)); + + ImGui::EndTable(); + } + break; + case ecs::FieldType::Vector4: + ImGui::Text("Cannot edit Vector4 for now"); // TODO: Implement Vector4 editing + default: return; + } + } + + void TypeErasedProperty::show(ecs::Entity entity) + { + const auto& coordinator = Application::m_coordinator; + const auto& componentDescriptions = coordinator->getComponentDescriptions(); + + // Check if the entity has any type erased components + if (componentDescriptions.empty()) { + ImGui::Text("No type erased components available for this entity."); + return; + } + + auto componentData = static_cast(coordinator->tryGetComponentById(m_componentType, entity)); + if (ImNexo::Header(std::format("##{}", m_description->name), m_description->name + " Component")) + { + for (const auto& field : m_description->fields) { + // Move to pointer to next field data + auto currentComponentData = componentData + field.offset; + // Show the field in the UI + showField(field, currentComponentData); + } + + ImGui::TreePop(); + } + + + } + + +} diff --git a/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.hpp b/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.hpp new file mode 100644 index 000000000..ff6cd5314 --- /dev/null +++ b/editor/src/DocumentWindows/EntityProperties/TypeErasedProperty.hpp @@ -0,0 +1,47 @@ +//// TypeErasedProperty.hpp /////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the type erased property class +// used to display and edit entity properties +// for C# defined components +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "AEntityProperty.hpp" + +namespace nexo::editor { + class TypeErasedProperty final : public AEntityProperty { + public: + TypeErasedProperty(InspectorWindow &inspector, const ecs::ComponentType componentType, const std::shared_ptr& description) + : AEntityProperty(inspector) + , m_componentType(componentType), m_description(description) + { + } + + /** + * @brief Displays and edits the transform properties of an entity using an ImGui interface. + * + * Retrieves the transform component (position, scale, and rotation quaternion) of the given entity, + * displaying the values in an ImGui table. The rotation is converted from a quaternion to Euler angles + * to allow intuitive editing; any changes in Euler angles are applied incrementally back to the quaternion, + * ensuring it remains normalized. + * + * @param entity The entity whose transform properties are rendered. + */ + void show(ecs::Entity entity) override; + + private: + const ecs::ComponentType m_componentType; // Type of the component being displayed + const std::shared_ptr m_description; // Description of the component being displayed + }; + +} diff --git a/editor/src/DocumentWindows/InspectorWindow.cpp b/editor/src/DocumentWindows/InspectorWindow.cpp deleted file mode 100644 index 1c628683c..000000000 --- a/editor/src/DocumentWindows/InspectorWindow.cpp +++ /dev/null @@ -1,141 +0,0 @@ -//// InspectorWindow.cpp ////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Marie GIACOMEL -// Date: 23/11/2024 -// Description: Inspector window source file -// -/////////////////////////////////////////////////////////////////////////////// - -#include "InspectorWindow.hpp" - -#include -#include -#include -#include - -#include "Application.hpp" -#include "EntityProperties/RenderProperty.hpp" -#include "EntityProperties/TransformProperty.hpp" -#include "EntityProperties/AmbientLightProperty.hpp" -#include "EntityProperties/DirectionalLightProperty.hpp" -#include "EntityProperties/PointLightProperty.hpp" -#include "EntityProperties/SpotLightProperty.hpp" -#include "EntityProperties/CameraProperty.hpp" -#include "EntityProperties/CameraController.hpp" -#include "components/Transform.hpp" -#include "utils/ScenePreview.hpp" -#include "Components/EntityPropertiesComponents.hpp" -#include "components/Camera.hpp" -#include "components/Light.hpp" -#include "context/Selector.hpp" -#include "core/scene/SceneManager.hpp" - -#include "Components/Widgets.hpp" - -extern ImGuiID g_materialInspectorDockID; - -namespace nexo::editor -{ - - InspectorWindow::~InspectorWindow() = default; - - void InspectorWindow::setup() - { - registerProperty(); - registerProperty(); - registerProperty(); - registerProperty(); - registerProperty(); - registerProperty(); - registerProperty(); - registerProperty(); - } - - void InspectorWindow::shutdown() - { - // Nothing to clear for now - } - - void InspectorWindow::show() - { - ImGui::Begin(ICON_FA_SLIDERS " Inspector" "###" NEXO_WND_USTRID_INSPECTOR, &m_opened, ImGuiWindowFlags_NoCollapse); - firstDockSetup(NEXO_WND_USTRID_INSPECTOR); - auto const &selector = Selector::get(); - const int selectedEntity = selector.getSelectedEntity(); - - if (selectedEntity != -1) - { - if (selector.getSelectionType() == SelectionType::SCENE) - { - showSceneProperties(selectedEntity); - } - else - { - showEntityProperties(selectedEntity); - } - } - - ImGui::End(); - } - - void InspectorWindow::showSceneProperties(const scene::SceneId sceneId) const - { - auto &app = getApp(); - auto &selector = Selector::get(); - scene::SceneManager &manager = app.getSceneManager(); - scene::Scene &scene = manager.getScene(sceneId); - std::string uiHandle = selector.getUiHandle(scene.getUuid(), ""); - - // Remove the icon prefix - if (size_t spacePos = uiHandle.find(' '); spacePos != std::string::npos) - uiHandle = uiHandle.substr(spacePos + 1); - - if (EntityPropertiesComponents::drawHeader("##SceneNode", uiHandle)) - { - ImGui::Spacing(); - //ImGui::SetWindowFontScale(1.15f); - ImGui::Columns(2, "sceneProps"); - ImGui::SetColumnWidth(0, 80); - - ImGui::Text("Hide"); - ImGui::NextColumn(); - bool hidden = !scene.isRendered(); - ImGui::Checkbox("##HideCheckBox", &hidden); - scene.setRenderStatus(!hidden); - ImGui::NextColumn(); - - ImGui::Text("Pause"); - ImGui::NextColumn(); - bool paused = !scene.isActive(); - ImGui::Checkbox("##PauseCheckBox", &paused); - scene.setActiveStatus(!paused); - ImGui::NextColumn(); - - ImGui::Columns(1); - ImGui::TreePop(); - } - } - - void InspectorWindow::showEntityProperties(const ecs::Entity entity) - { - const std::vector componentsType = nexo::Application::getAllEntityComponentTypes(entity); - for (auto& type : componentsType) - { - if (m_entityProperties.contains(type)) - { - m_entityProperties[type]->show(entity); - } - } - } - - void InspectorWindow::update() - { - // Nothing to update here - } -} diff --git a/editor/src/DocumentWindows/InspectorWindow/Init.cpp b/editor/src/DocumentWindows/InspectorWindow/Init.cpp new file mode 100644 index 000000000..4ac5a91c7 --- /dev/null +++ b/editor/src/DocumentWindows/InspectorWindow/Init.cpp @@ -0,0 +1,56 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the inspector window setup +// +/////////////////////////////////////////////////////////////////////////////// + +#include "InspectorWindow.hpp" +#include "../EntityProperties/RenderProperty.hpp" +#include "../EntityProperties/TransformProperty.hpp" +#include "../EntityProperties/AmbientLightProperty.hpp" +#include "../EntityProperties/DirectionalLightProperty.hpp" +#include "../EntityProperties/PointLightProperty.hpp" +#include "../EntityProperties/SpotLightProperty.hpp" +#include "../EntityProperties/CameraProperty.hpp" +#include "../EntityProperties/CameraController.hpp" +#include "../EntityProperties/CameraTarget.hpp" +#include "DocumentWindows/EntityProperties/TypeErasedProperty.hpp" +#include "components/Camera.hpp" + +namespace nexo::editor { + + void InspectorWindow::setup() + { + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + registerProperty(); + + registerTypeErasedProperties(); + } + + void InspectorWindow::registerTypeErasedProperties() + { + // Register TypeErased components + const auto& coordinator = Application::m_coordinator; + const auto& componentDescriptions = coordinator->getComponentDescriptions(); + + for (const auto& [componentType, description] : componentDescriptions) { + registerProperty(componentType, std::make_shared(*this, componentType, description)); + } + } + +} diff --git a/editor/src/DocumentWindows/InspectorWindow.hpp b/editor/src/DocumentWindows/InspectorWindow/InspectorWindow.hpp similarity index 79% rename from editor/src/DocumentWindows/InspectorWindow.hpp rename to editor/src/DocumentWindows/InspectorWindow/InspectorWindow.hpp index 662bd205d..8c44e8760 100644 --- a/editor/src/DocumentWindows/InspectorWindow.hpp +++ b/editor/src/DocumentWindows/InspectorWindow/InspectorWindow.hpp @@ -17,7 +17,6 @@ #include "DocumentWindows/EntityProperties/AEntityProperty.hpp" #include "core/scene/SceneManager.hpp" -#include #include namespace nexo::editor { @@ -25,7 +24,7 @@ namespace nexo::editor { class InspectorWindow final : public ADocumentWindow { public: using ADocumentWindow::ADocumentWindow; - ~InspectorWindow() override; + ~InspectorWindow() override = default; /** * @brief Initializes the property handlers for various entity component types. @@ -37,6 +36,10 @@ namespace nexo::editor { * properties in the inspector UI. */ void setup() override; + + void registerTypeErasedProperties(); + + // No-op method in this class void shutdown() override; /** @@ -47,6 +50,8 @@ namespace nexo::editor { * if a valid selection exists, displays either scene or entity properties depending on the selection type. */ void show() override; + + // No-op method in this class void update() override; /** @@ -59,7 +64,7 @@ namespace nexo::editor { * @param visible The desired visibility state (true for visible, false for hidden). */ template - void setSubInspectorVisibility(bool visible) + void setSubInspectorVisibility(const bool visible) { m_subInspectorVisibility[std::type_index(typeid(T))] = visible; } @@ -112,27 +117,34 @@ namespace nexo::editor { } /** - * @brief Retrieves the material data associated with the specified sub-inspector type. + * @brief Retrieves the material data associated with the specified sub-inspector window type. * - * This templated function searches for data in the sub-inspector data map using the type index of T. - * If an entry for T exists, it returns the associated pointer to a material; otherwise, it returns a variant - * containing std::monostate to indicate that no data is set. + * This templated function searches for data in the sub-inspector data map using the type index of WindowType. + * If an entry for WindowType exists, it returns the associated pointer to a Data type; otherwise, it returns nullptr * - * @tparam T The sub-inspector type used to look up the associated data. - * @return std::variant A variant holding a pointer to components::Material if set, - * or std::monostate if no data is available. + * @tparam WindowType The sub-inspector type used to look up the associated data. + * @tparam Data The type of data to retrieve. + * @return A pointer to the Data type if found, or nullptr if not found. */ - template - std::variant getSubInspectorData() const + template + Data *getSubInspectorData() const { - auto it = m_subInspectorData.find(std::type_index(typeid(T))); - return (it != m_subInspectorData.end()) ? it->second : std::variant{std::monostate{}}; + auto it = m_subInspectorData.find(std::type_index(typeid(WindowType))); + if (it != m_subInspectorData.end()) { + try { + return std::any_cast(it->second); + } + catch (const std::bad_any_cast& e) { + return nullptr; + } + } + return nullptr; } private: - std::unordered_map> m_entityProperties; + std::unordered_map> m_entityProperties; std::unordered_map m_subInspectorVisibility; - std::unordered_map> m_subInspectorData; + std::unordered_map m_subInspectorData; /** * @brief Displays the scene's properties in the inspector UI. @@ -144,7 +156,7 @@ namespace nexo::editor { * * @param sceneId The identifier of the scene whose properties are to be displayed. */ - void showSceneProperties(scene::SceneId sceneId) const; + static void showSceneProperties(scene::SceneId sceneId); /** * @brief Renders the UI for the properties of an entity's components. @@ -171,7 +183,14 @@ namespace nexo::editor { requires std::derived_from void registerProperty() { - m_entityProperties[std::type_index(typeid(Component))] = std::make_shared(*this); + const auto type = Application::m_coordinator->getComponentType(); + m_entityProperties[type] = std::make_shared(*this); } + + void registerProperty(const ecs::ComponentType type, std::shared_ptr property) + { + m_entityProperties[type] = std::move(property); + } + }; }; diff --git a/editor/src/DocumentWindows/InspectorWindow/Show.cpp b/editor/src/DocumentWindows/InspectorWindow/Show.cpp new file mode 100644 index 000000000..853d5ca99 --- /dev/null +++ b/editor/src/DocumentWindows/InspectorWindow/Show.cpp @@ -0,0 +1,99 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the inspector window rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "InspectorWindow.hpp" +#include "context/Selector.hpp" +#include "ImNexo/Components.hpp" +#include "IconsFontAwesome.h" + +namespace nexo::editor { + + void InspectorWindow::showSceneProperties(const scene::SceneId sceneId) + { + auto &app = getApp(); + auto &selector = Selector::get(); + scene::SceneManager &manager = app.getSceneManager(); + scene::Scene &scene = manager.getScene(sceneId); + std::string uiHandle = selector.getUiHandle(scene.getUuid(), ""); + + // Remove the icon prefix + if (size_t spacePos = uiHandle.find(' '); spacePos != std::string::npos) + uiHandle = uiHandle.substr(spacePos + 1); + + if (ImNexo::Header("##SceneNode", uiHandle)) + { + ImGui::Spacing(); + ImGui::Columns(2, "sceneProps"); + ImGui::SetColumnWidth(0, 80); + + ImGui::Text("Hide"); + ImGui::NextColumn(); + bool hidden = !scene.isRendered(); + ImGui::Checkbox("##HideCheckBox", &hidden); + scene.setRenderStatus(!hidden); + ImGui::NextColumn(); + + ImGui::Text("Pause"); + ImGui::NextColumn(); + bool paused = !scene.isActive(); + ImGui::Checkbox("##PauseCheckBox", &paused); + scene.setActiveStatus(!paused); + ImGui::NextColumn(); + + ImGui::Columns(1); + ImGui::TreePop(); + } + } + + void InspectorWindow::showEntityProperties(const ecs::Entity entity) + { + const std::vector& componentsType = Application::getAllEntityComponentTypes(entity); + for (auto& type : componentsType) + { + if (m_entityProperties.contains(type)) + { + m_entityProperties[type]->show(entity); + } + } + } + + void InspectorWindow::show() + { + ImGui::Begin(ICON_FA_SLIDERS " Inspector" NEXO_WND_USTRID_INSPECTOR, &m_opened, ImGuiWindowFlags_NoCollapse); + beginRender(NEXO_WND_USTRID_INSPECTOR); + auto const &selector = Selector::get(); + + if (selector.getPrimarySelectionType() == SelectionType::SCENE) { + // Scene selection stays the same - only show the selected scene + showSceneProperties(selector.getSelectedScene()); + } + else if (selector.hasSelection()) { + const ecs::Entity primaryEntity = selector.getPrimaryEntity(); + + const auto& selectedEntities = selector.getSelectedEntities(); + if (selectedEntities.size() > 1) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); + ImGui::TextWrapped("%zu entities selected. Displaying properties for the primary entity.", + selectedEntities.size()); + ImGui::PopStyleColor(); + ImGui::Separator(); + } + + showEntityProperties(primaryEntity); + } + + ImGui::End(); + } + +} diff --git a/editor/src/DocumentWindows/InspectorWindow/Shutdown.cpp b/editor/src/DocumentWindows/InspectorWindow/Shutdown.cpp new file mode 100644 index 000000000..cadc04517 --- /dev/null +++ b/editor/src/DocumentWindows/InspectorWindow/Shutdown.cpp @@ -0,0 +1,24 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file the inspector window shutdown +// +/////////////////////////////////////////////////////////////////////////////// + +#include "InspectorWindow.hpp" + +namespace nexo::editor { + + void InspectorWindow::shutdown() + { + // Nothing to clear for now + } + +} diff --git a/editor/src/DocumentWindows/InspectorWindow/Update.cpp b/editor/src/DocumentWindows/InspectorWindow/Update.cpp new file mode 100644 index 000000000..bea86f4f6 --- /dev/null +++ b/editor/src/DocumentWindows/InspectorWindow/Update.cpp @@ -0,0 +1,24 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file the inspector window update +// +/////////////////////////////////////////////////////////////////////////////// + +#include "InspectorWindow.hpp" + +namespace nexo::editor { + + void InspectorWindow::update() + { + // Nothing to update here + } + +} diff --git a/editor/src/DocumentWindows/MaterialInspector.cpp b/editor/src/DocumentWindows/MaterialInspector.cpp deleted file mode 100644 index 3a21311e5..000000000 --- a/editor/src/DocumentWindows/MaterialInspector.cpp +++ /dev/null @@ -1,113 +0,0 @@ -//// MaterialInspector.cpp /////////////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 25/03/2025 -// Description: Source file for the material inspector window -// -/////////////////////////////////////////////////////////////////////////////// - -#include "MaterialInspector.hpp" -#include "DocumentWindows/InspectorWindow.hpp" -#include "Exception.hpp" -#include "components/Render.hpp" -#include "utils/ScenePreview.hpp" -#include "components/Camera.hpp" -#include "Components/Widgets.hpp" -#include "context/Selector.hpp" -#include "exceptions/Exceptions.hpp" - -namespace nexo::editor { - - void MaterialInspector::setup() - { - // No need to setup anything - } - - void MaterialInspector::shutdown() - { - // No need to delete anything since the destructor of the framebuffer will handle it - } - - void MaterialInspector::renderMaterialInspector(int selectedEntity) - { - bool &materialModified = m_materialModified; - static utils::ScenePreviewOut previewParams; - - if (selectedEntity != -1 && m_ecsEntity != selectedEntity) - { - auto renderComp = Application::m_coordinator->tryGetComponent(selectedEntity); - if (renderComp) - { - m_ecsEntity = selectedEntity; - materialModified = true; - } - else - { - m_ecsEntity = -1; - } - } - - if (m_ecsEntity == -1) - return; - - if (materialModified) - { - utils::genScenePreview("Modify material inspector", {64, 64}, m_ecsEntity, previewParams); - auto &app = nexo::getApp(); - auto &cameraComponent = nexo::Application::m_coordinator->getComponent(previewParams.cameraId); - cameraComponent.clearColor = {0.05f, 0.05f, 0.05f, 0.0f}; - app.run(previewParams.sceneId, RenderingType::FRAMEBUFFER); - m_framebuffer = cameraComponent.m_renderTarget; - materialModified = false; - app.getSceneManager().deleteScene(previewParams.sceneId); - } - if (!m_framebuffer) - THROW_EXCEPTION(BackendRendererApiFatalFailure, "OPENGL", "Failed to initialize framebuffer in Material Inspector window"); - // --- Material preview --- - if (m_framebuffer->getColorAttachmentId(0) != 0) - ImGui::Image(static_cast(static_cast(m_framebuffer->getColorAttachmentId(0))), {64, 64}, ImVec2(0, 1), ImVec2(1, 0)); - ImGui::SameLine(); - - auto inspectorWindow = m_windowRegistry.getWindow(NEXO_WND_USTRID_INSPECTOR).lock(); - if (!inspectorWindow) - return; - auto materialVariant = inspectorWindow->getSubInspectorData(); - if (std::holds_alternative(materialVariant)) { - auto materialPtr = std::get(materialVariant); - materialModified = Widgets::drawMaterialInspector(materialPtr); - } - } - - void MaterialInspector::show() - { - auto const &selector = Selector::get(); - const int selectedEntity = selector.getSelectedEntity(); - auto inspectorWindow = m_windowRegistry.getWindow(NEXO_WND_USTRID_INSPECTOR).lock(); - if (!inspectorWindow) - return; - if (inspectorWindow->getSubInspectorVisibility()) - { - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse; - if (m_firstOpened) - window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; - - if (ImGui::Begin("Material Inspector" "###" NEXO_WND_USTRID_MATERIAL_INSPECTOR, &inspectorWindow->getSubInspectorVisibility(), window_flags)) - { - firstDockSetup(NEXO_WND_USTRID_MATERIAL_INSPECTOR); - renderMaterialInspector(selectedEntity); - } - ImGui::End(); - } - } - - void MaterialInspector::update() - { - // No need to update anything - } -} diff --git a/editor/src/DocumentWindows/MaterialInspector/Init.cpp b/editor/src/DocumentWindows/MaterialInspector/Init.cpp new file mode 100644 index 000000000..0a8792880 --- /dev/null +++ b/editor/src/DocumentWindows/MaterialInspector/Init.cpp @@ -0,0 +1,24 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the material inspector setup +// +/////////////////////////////////////////////////////////////////////////////// + +#include "MaterialInspector.hpp" + +namespace nexo::editor { + + void MaterialInspector::setup() + { + // No need to setup anything + } + +} diff --git a/editor/src/DocumentWindows/MaterialInspector.hpp b/editor/src/DocumentWindows/MaterialInspector/MaterialInspector.hpp similarity index 91% rename from editor/src/DocumentWindows/MaterialInspector.hpp rename to editor/src/DocumentWindows/MaterialInspector/MaterialInspector.hpp index 91f1f055e..5357301fa 100644 --- a/editor/src/DocumentWindows/MaterialInspector.hpp +++ b/editor/src/DocumentWindows/MaterialInspector/MaterialInspector.hpp @@ -17,7 +17,7 @@ namespace nexo::editor { - class MaterialInspector : public ADocumentWindow { + class MaterialInspector final : public ADocumentWindow { public: using ADocumentWindow::ADocumentWindow; void setup() override; @@ -30,7 +30,11 @@ namespace nexo::editor { * rendering to renderMaterialInspector() if the Material Inspector is visible. */ void show() override; + + // No-op method in this class void shutdown() override; + + // No-op method in this class void update() override; private: @@ -49,7 +53,7 @@ namespace nexo::editor { */ void renderMaterialInspector(int selectedEntity); - std::shared_ptr m_framebuffer = nullptr; + std::shared_ptr m_framebuffer = nullptr; int m_ecsEntity = -1; bool m_materialModified = true; }; diff --git a/editor/src/DocumentWindows/MaterialInspector/Show.cpp b/editor/src/DocumentWindows/MaterialInspector/Show.cpp new file mode 100644 index 000000000..2c3998c4d --- /dev/null +++ b/editor/src/DocumentWindows/MaterialInspector/Show.cpp @@ -0,0 +1,94 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the material inspector rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "MaterialInspector.hpp" +#include "utils/ScenePreview.hpp" +#include "DocumentWindows/InspectorWindow/InspectorWindow.hpp" +#include "ImNexo/Elements.hpp" +#include "ImNexo/Panels.hpp" +#include "context/Selector.hpp" + +namespace nexo::editor { + + void MaterialInspector::renderMaterialInspector(const int selectedEntity) + { + bool &materialModified = m_materialModified; + static utils::ScenePreviewOut previewParams; + + if (selectedEntity != -1 && m_ecsEntity != selectedEntity) + { + auto renderComp = Application::m_coordinator->tryGetComponent(selectedEntity); + if (renderComp) + { + m_ecsEntity = selectedEntity; + materialModified = true; + } + else + { + m_ecsEntity = -1; + } + } + + if (m_ecsEntity == -1) + return; + + if (materialModified) + { + utils::genScenePreview("Modify material inspector", {64, 64}, m_ecsEntity, previewParams); + auto &app = getApp(); + const Application::SceneInfo sceneInfo{previewParams.sceneId, RenderingType::FRAMEBUFFER}; + app.run(sceneInfo); + const auto &cameraComponent = Application::m_coordinator->getComponent(previewParams.cameraId); + m_framebuffer = cameraComponent.m_renderTarget; + materialModified = false; + app.getSceneManager().deleteScene(previewParams.sceneId); + } + + if (!m_framebuffer) + THROW_EXCEPTION(BackendRendererApiFatalFailure, "OPENGL", "Failed to initialize framebuffer in Material Inspector window"); + // --- Material preview --- + if (m_framebuffer->getColorAttachmentId(0) != 0) + ImNexo::Image(static_cast(static_cast(m_framebuffer->getColorAttachmentId(0))), {64, 64}); + ImGui::SameLine(); + + const auto inspectorWindow = m_windowRegistry.getWindow(NEXO_WND_USTRID_INSPECTOR).lock(); + if (!inspectorWindow) + return; + auto materialVariant = inspectorWindow->getSubInspectorData(); + if (materialVariant) + materialModified = ImNexo::MaterialInspector(materialVariant); + } + + void MaterialInspector::show() + { + auto const &selector = Selector::get(); + const int selectedEntity = selector.getPrimaryEntity(); + const auto inspectorWindow = m_windowRegistry.getWindow(NEXO_WND_USTRID_INSPECTOR).lock(); + if (!inspectorWindow) + return; + if (inspectorWindow->getSubInspectorVisibility()) + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse; + if (m_firstOpened) + window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; + + if (ImGui::Begin("Material Inspector" NEXO_WND_USTRID_MATERIAL_INSPECTOR, &inspectorWindow->getSubInspectorVisibility(), window_flags)) + { + beginRender(NEXO_WND_USTRID_MATERIAL_INSPECTOR); + renderMaterialInspector(selectedEntity); + } + ImGui::End(); + } + } +} diff --git a/editor/src/DocumentWindows/MaterialInspector/Shutdown.cpp b/editor/src/DocumentWindows/MaterialInspector/Shutdown.cpp new file mode 100644 index 000000000..f5f26157a --- /dev/null +++ b/editor/src/DocumentWindows/MaterialInspector/Shutdown.cpp @@ -0,0 +1,24 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the material inspector shutdown +// +/////////////////////////////////////////////////////////////////////////////// + +#include "MaterialInspector.hpp" + +namespace nexo::editor { + + void MaterialInspector::shutdown() + { + // No need to delete anything since the destructor of the framebuffer will handle it + } + +} diff --git a/editor/src/DocumentWindows/MaterialInspector/Update.cpp b/editor/src/DocumentWindows/MaterialInspector/Update.cpp new file mode 100644 index 000000000..ad8af8908 --- /dev/null +++ b/editor/src/DocumentWindows/MaterialInspector/Update.cpp @@ -0,0 +1,24 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the material inspector update +// +/////////////////////////////////////////////////////////////////////////////// + +#include "MaterialInspector.hpp" + +namespace nexo::editor { + + void MaterialInspector::update() + { + // No need to update anything + } + +} diff --git a/editor/src/DocumentWindows/PopupManager.cpp b/editor/src/DocumentWindows/PopupManager.cpp index 406fba922..ef856f6f9 100644 --- a/editor/src/DocumentWindows/PopupManager.cpp +++ b/editor/src/DocumentWindows/PopupManager.cpp @@ -13,22 +13,40 @@ /////////////////////////////////////////////////////////////////////////////// #include "PopupManager.hpp" +#include "ImNexo/Elements.hpp" #include "Logger.hpp" #include +#include + namespace nexo::editor { - void PopupManager::openPopup(const std::string &popupName) + void PopupManager::openPopup(const std::string &popupName, const ImVec2 &popupSize) + { + const PopupProps props{ + .open = true, + .callback = nullptr, + .size = popupSize + }; + m_popups[popupName] = props; + } + + void PopupManager::openPopupWithCallback(const std::string &popupName, PopupCallback callback, const ImVec2 &popupSize) { - m_popups[popupName] = true; + const PopupProps props{ + .open = true, + .callback = std::move(callback), + .size = popupSize + }; + m_popups[popupName] = props; } - void PopupManager::closePopup() const + void PopupManager::closePopup() { ImGui::EndPopup(); } - void PopupManager::closePopupInContext() const + void PopupManager::closePopupInContext() { ImGui::CloseCurrentPopup(); } @@ -37,10 +55,15 @@ namespace nexo::editor { { if (!m_popups.contains(popupName)) return false; - if (m_popups.at(popupName)) + PopupProps &props = m_popups.at(popupName); + if (props.open) { ImGui::OpenPopup(popupName.c_str()); - m_popups.at(popupName) = false; + props.open = false; + } + if (props.size.x != 0 && props.size.y != 0) + { + ImGui::SetNextWindowSize(props.size); } return ImGui::BeginPopup(popupName.c_str()); } @@ -49,13 +72,47 @@ namespace nexo::editor { { if (!m_popups.contains(popupModalName)) return false; - if (m_popups.at(popupModalName)) + PopupProps &props = m_popups.at(popupModalName); + if (m_popups.at(popupModalName).open) { - LOG(NEXO_INFO, "Opened {} popup", popupModalName); - ImGui::OpenPopup(popupModalName.c_str()); - m_popups.at(popupModalName) = false; + props.open = false; + } + + if (props.size.x != 0 && props.size.y != 0) + { + ImGui::SetNextWindowSize(props.size); + } + + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize + | ImGuiWindowFlags_NoBackground + | ImGuiWindowFlags_NoTitleBar; + if (!ImGui::BeginPopupModal(popupModalName.c_str(), nullptr, flags)) + return false; + const ImVec2 pMin = ImGui::GetWindowPos(); + const ImVec2 size = ImGui::GetWindowSize(); + const auto pMax = ImVec2(pMin.x + size.x, pMin.y + size.y); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const std::vector stops = { + { 0.06f, IM_COL32(58 / 3, 124 / 3, 161 / 3, 255) }, + { 0.26f, IM_COL32(88 / 3, 87 / 3, 154 / 3, 255) }, + { 0.50f, IM_COL32(88 / 3, 87 / 3, 154 / 3, 255) }, + { 0.73f, IM_COL32(58 / 3, 124 / 3, 161 / 3, 255) }, + }; + constexpr float angle = 148.0f; + + ImNexo::RectFilledLinearGradient(pMin, pMax, angle, stops, drawList); + + return true; + } + + void PopupManager::runPopupCallback(const std::string &popupName) const + { + if (m_popups.contains(popupName) && m_popups.at(popupName).callback != nullptr) + { + m_popups.at(popupName).callback(); } - return ImGui::BeginPopupModal(popupModalName.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize); } + } diff --git a/editor/src/DocumentWindows/PopupManager.hpp b/editor/src/DocumentWindows/PopupManager.hpp index c85404517..cbd4e848e 100644 --- a/editor/src/DocumentWindows/PopupManager.hpp +++ b/editor/src/DocumentWindows/PopupManager.hpp @@ -15,6 +15,8 @@ #include #include +#include +#include namespace nexo::editor { @@ -26,15 +28,41 @@ namespace nexo::editor { */ class PopupManager { public: + using PopupCallback = std::function; - /** + /** + * @brief Properties of a popup. + * + * Contains state information for a popup, including whether it should + * be opened, its associated callback function, and its size. + */ + struct PopupProps { + bool open = false; ///< Whether the popup is marked to open + PopupCallback callback = nullptr; ///< Optional callback function + ImVec2 size = ImVec2(0, 0); ///< Size of the popup (0,0 means auto-size) + }; + + /** * @brief Opens a popup by name. * * Marks the popup as active so that it will be opened in the next frame. * * @param popupName The unique name of the popup. + * @param popupSize Optional size for the popup window (0,0 for auto-size). */ - void openPopup(const std::string &popupName); + void openPopup(const std::string &popupName, const ImVec2 &popupSize = ImVec2(0, 0)); + + /** + * @brief Opens a popup with an associated callback function. + * + * Marks the popup as active and associates a callback function with it. + * The callback can later be executed via runPopupCallback(). + * + * @param popupName The unique name of the popup. + * @param callback Function to be called when the popup is processed. + * @param popupSize Optional size for the popup window (0,0 for auto-size). + */ + void openPopupWithCallback(const std::string &popupName, PopupCallback callback, const ImVec2 &popupSize = ImVec2(0, 0)); /** * @brief Displays a modal popup. @@ -63,14 +91,24 @@ namespace nexo::editor { * * Ends the current ImGui popup. */ - void closePopup() const; + static void closePopup() ; /** * @brief Closes the current popup in its context. * * Requests ImGui to close the current popup. */ - void closePopupInContext() const; + static void closePopupInContext() ; + + /** + * @brief Executes the callback associated with a popup. + * + * If a callback function was registered with the specified popup, + * this method will execute it. + * + * @param popupName The name of the popup whose callback should be executed. + */ + void runPopupCallback(const std::string &popupName) const; private: struct TransparentHasher { @@ -80,6 +118,6 @@ namespace nexo::editor { } }; - std::unordered_map> m_popups; + std::unordered_map> m_popups; }; } diff --git a/editor/src/DocumentWindows/SceneTreeWindow.cpp b/editor/src/DocumentWindows/SceneTreeWindow.cpp deleted file mode 100644 index 71e3c57c6..000000000 --- a/editor/src/DocumentWindows/SceneTreeWindow.cpp +++ /dev/null @@ -1,413 +0,0 @@ -//// SceneTreeWindow.cpp ////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Guillaume HEIN -// Date: 13/11/2024 -// Description: Source for the scene tree document window -// -/////////////////////////////////////////////////////////////////////////////// - -#include "SceneTreeWindow.hpp" -#include "DocumentWindows/InspectorWindow.hpp" -#include "Primitive.hpp" - -#include -#include - -#include "EditorScene.hpp" -#include "components/Camera.hpp" -#include "components/Light.hpp" -#include "components/Render.hpp" -#include "components/SceneComponents.hpp" -#include "components/Transform.hpp" -#include "components/Uuid.hpp" -#include "context/Selector.hpp" - -namespace nexo::editor { - - SceneTreeWindow::~SceneTreeWindow() = default; - - void SceneTreeWindow::setup() - {} - - void SceneTreeWindow::shutdown() - {} - - void SceneTreeWindow::handleRename(SceneObject &obj) - { - ImGui::BeginGroup(); - ImGui::TextUnformatted(ObjectTypeToIcon.at(obj.type).c_str()); - ImGui::SameLine(); - - char buffer[256]; - const std::string editableName = obj.uiName.substr(ObjectTypeToIcon.at(obj.type).size()); - buffer[sizeof(buffer) - 1] = '\0'; - strncpy(buffer, editableName.c_str(), sizeof(buffer)); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); // Remove border - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); // No rounding - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - - if (ImGui::InputText("##Rename", buffer, sizeof(buffer), - ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) - { - obj.uiName = ObjectTypeToIcon.at(obj.type) + std::string(buffer); - auto &selector = Selector::get(); - selector.setUiHandle(obj.uuid, obj.uiName); - if (obj.type == SelectionType::SCENE) - { - auto &app = getApp(); - app.getSceneManager().getScene(obj.data.sceneProperties.sceneId).setName(obj.uiName); - } - m_renameTarget.reset(); - } - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) - m_renameTarget.reset(); - ImGui::PopStyleVar(3); - ImGui::EndGroup(); - } - - bool SceneTreeWindow::handleSelection(const SceneObject &obj, const std::string &uniqueLabel, - const ImGuiTreeNodeFlags baseFlags) const - { - const bool nodeOpen = ImGui::TreeNodeEx(uniqueLabel.c_str(), baseFlags); - if (!nodeOpen) - return nodeOpen; - if (ImGui::IsItemClicked()) - { - auto &selector = Selector::get(); - selector.setSelectedEntity(obj.uuid, obj.data.entity); - selector.setSelectionType(obj.type); - selector.setSelectedScene(obj.data.sceneProperties.sceneId); - } - return nodeOpen; - } - - void SceneTreeWindow::sceneSelected([[maybe_unused]] const SceneObject &obj) const - { - //TODO: Delete scene - } - - void SceneTreeWindow::lightSelected(const SceneObject &obj) const - { - auto &app = Application::getInstance(); - auto &selector = Selector::get(); - if (ImGui::MenuItem("Delete Light")) - { - selector.unselectEntity(); - app.deleteEntity(obj.data.entity); - } - } - - void SceneTreeWindow::cameraSelected(const SceneObject &obj) const - { - auto &app = Application::getInstance(); - auto &selector = Selector::get(); - if (ImGui::MenuItem("Delete Camera")) - { - const auto &scenes = m_windowRegistry.getWindows(); - for (const auto &scene : scenes) - scene->deleteCamera(obj.data.entity); - selector.unselectEntity(); - app.deleteEntity(obj.data.entity); - } - } - - void SceneTreeWindow::entitySelected(const SceneObject &obj) const - { - if (ImGui::MenuItem("Delete Entity")) - { - auto &selector = Selector::get(); - selector.unselectEntity(); - auto &app = nexo::getApp(); - app.deleteEntity(obj.data.entity); - } - } - - void SceneTreeWindow::showNode(SceneObject &object) - { - ImGuiTreeNodeFlags baseFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | - ImGuiTreeNodeFlags_SpanAvailWidth; - // Checks if the object is at the end of a tree - const bool leaf = object.children.empty(); - if (leaf) - baseFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; - - // Checks if the object is selected - if (auto const &selector = Selector::get(); selector.isEntitySelected() && object.uuid == selector.getSelectedUuid()) - baseFlags |= ImGuiTreeNodeFlags_Selected; - - bool nodeOpen = false; - const std::string uniqueLabel = object.uiName; - - // If the user wishes to rename handle the rename, else handle the selection - if (m_renameTarget && m_renameTarget->first == object.type && m_renameTarget->second == object.uuid) - handleRename(object); - else - nodeOpen = handleSelection(object, uniqueLabel, baseFlags); - - // Handles the right click on each different type of object - if (object.type != SelectionType::NONE && ImGui::BeginPopupContextItem(uniqueLabel.c_str())) - { - // Renaming works on every object excepts entities and cameras - if (ImGui::MenuItem("Rename")) - { - m_renameTarget = {object.type, object.uuid}; - m_renameBuffer = object.uiName; - } - if (object.type == SelectionType::SCENE) - sceneSelected(object); - else if (object.type == SelectionType::DIR_LIGHT || object.type == SelectionType::POINT_LIGHT || object.type == SelectionType::SPOT_LIGHT) - lightSelected(object); - else if (object.type == SelectionType::CAMERA) - cameraSelected(object); - else if (object.type == SelectionType::ENTITY) - entitySelected(object); - ImGui::EndPopup(); - } - - // Go further into the tree - if (nodeOpen && !leaf) - { - for (auto &child: object.children) - { - showNode(child); - } - ImGui::TreePop(); - } - } - - void SceneTreeWindow::sceneContextMenu() - { - if (m_popupManager.showPopup("Scene Tree Context Menu")) - { - if (ImGui::MenuItem("Create Scene")) - m_popupManager.openPopup("Create New Scene"); - m_popupManager.closePopup(); - } - } - - void SceneTreeWindow::sceneCreationMenu() - { - if (m_popupManager.showPopupModal("Create New Scene")) - { - static char sceneNameBuffer[256] = ""; - - ImGui::Text("Enter Scene Name:"); - ImGui::InputText("##SceneName", sceneNameBuffer, sizeof(sceneNameBuffer)); - - if (ImGui::Button("Create")) - { - if (const std::string newSceneName(sceneNameBuffer); !newSceneName.empty()) - { - //TOOD: create scene - auto newScene = std::make_shared(sceneNameBuffer, m_windowRegistry); - newScene->setDefault(); - newScene->setup(); - m_windowRegistry.registerWindow(newScene); - memset(sceneNameBuffer, 0, sizeof(sceneNameBuffer)); - - m_popupManager.closePopupInContext(); - } else - LOG(NEXO_WARN, "Scene name is empty !"); - } - - ImGui::SameLine(); - if (ImGui::Button("Cancel")) - m_popupManager.closePopupInContext(); - - m_popupManager.closePopup(); - } - } - - void SceneTreeWindow::show() - { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 300, 20), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(300, ImGui::GetIO().DisplaySize.y - 40), ImGuiCond_FirstUseEver); - - if (ImGui::Begin(ICON_FA_SITEMAP " Scene Tree" "###" NEXO_WND_USTRID_SCENE_TREE, &m_opened, ImGuiWindowFlags_NoCollapse)) - { - firstDockSetup(NEXO_WND_USTRID_SCENE_TREE); - // Opens the right click popup when no items are hovered - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered( - ImGuiHoveredFlags_AllowWhenBlockedByPopup) && !ImGui::IsAnyItemHovered()) - m_popupManager.openPopup("Scene Tree Context Menu"); - if (!root_.children.empty()) { - for (auto &node : root_.children) - showNode(node); - } - sceneContextMenu(); - sceneCreationMenu(); - - - } - ImGui::End(); - } - - SceneObject SceneTreeWindow::newSceneNode(const std::string &sceneName, const scene::SceneId sceneId, const WindowId uiId) const - { - SceneObject sceneNode; - const std::string uiName = ObjectTypeToIcon.at(SelectionType::SCENE) + sceneName; - sceneNode.data.sceneProperties = SceneProperties{sceneId, uiId}; - sceneNode.data.entity = sceneId; - sceneNode.type = SelectionType::SCENE; - auto &app = Application::getInstance(); - auto &selector = Selector::get(); - sceneNode.uuid = app.getSceneManager().getScene(sceneId).getUuid(); - sceneNode.uiName = selector.getUiHandle(sceneNode.uuid, uiName); - return sceneNode; - } - - void SceneTreeWindow::newLightNode(SceneObject &lightNode, const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity, const std::string &uiName) const - { - const SceneProperties sceneProperties{sceneId, uiId}; - lightNode.data.sceneProperties = sceneProperties; - lightNode.data.entity = lightEntity; - auto &selector = Selector::get(); - const auto entityUuid = Application::m_coordinator->tryGetComponent(lightEntity); - if (entityUuid) - { - lightNode.uuid = entityUuid->get().uuid; - lightNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); - } else - lightNode.uiName = uiName; - } - - SceneObject SceneTreeWindow::newAmbientLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) const - { - SceneObject lightNode; - lightNode.type = SelectionType::AMBIENT_LIGHT; - const std::string uiName = std::format("{}Ambient light ", ObjectTypeToIcon.at(lightNode.type)); - newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); - return lightNode; - } - - SceneObject SceneTreeWindow::newDirectionalLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) - { - SceneObject lightNode; - lightNode.type = SelectionType::DIR_LIGHT; - const std::string uiName = std::format("{}Directional light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbDirLights); - newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); - return lightNode; - } - - SceneObject SceneTreeWindow::newSpotLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) - { - SceneObject lightNode; - lightNode.type = SelectionType::SPOT_LIGHT; - const std::string uiName = std::format("{}Spot light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbSpotLights); - newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); - return lightNode; - } - - SceneObject SceneTreeWindow::newPointLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) - { - SceneObject lightNode; - lightNode.type = SelectionType::POINT_LIGHT; - const std::string uiName = std::format("{}Point light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbPointLights); - newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); - return lightNode; - } - - SceneObject SceneTreeWindow::newCameraNode(const scene::SceneId sceneId, const WindowId uiId, - const ecs::Entity cameraEntity) const - { - SceneObject cameraNode; - const std::string uiName = ObjectTypeToIcon.at(SelectionType::CAMERA) + std::string("Camera"); - cameraNode.type = SelectionType::CAMERA; - const SceneProperties sceneProperties{sceneId, uiId}; - cameraNode.data.sceneProperties = sceneProperties; - cameraNode.data.entity = cameraEntity; - auto &selector = Selector::get(); - const auto entityUuid = nexo::Application::m_coordinator->tryGetComponent(cameraEntity); - if (entityUuid) - { - cameraNode.uuid = entityUuid->get().uuid; - cameraNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); - } else - cameraNode.uiName = uiName; - return cameraNode; - } - - SceneObject SceneTreeWindow::newEntityNode(const scene::SceneId sceneId, const WindowId uiId, - const ecs::Entity entity) const - { - auto &selector = Selector::get(); - SceneObject entityNode; - const std::string uiName = std::format("{}{}", ObjectTypeToIcon.at(SelectionType::ENTITY), entity); - entityNode.type = SelectionType::ENTITY; - entityNode.data.sceneProperties = SceneProperties{sceneId, uiId}; - entityNode.data.entity = entity; - const auto entityUuid = nexo::Application::m_coordinator->tryGetComponent(entity); - if (entityUuid) - { - entityNode.uuid = entityUuid->get().uuid; - entityNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); - } - else - entityNode.uiName = uiName; - return entityNode; - } - - void SceneTreeWindow::update() - { - root_.uiName = "Scene Tree"; - root_.data.entity = -1; - root_.type = SelectionType::NONE; - root_.children.clear(); - m_nbPointLights = 0; - m_nbDirLights = 0; - m_nbSpotLights = 0; - - // Retrieves the scenes that are displayed on the GUI - const auto &scenes = m_windowRegistry.getWindows(); - std::map sceneNodes; - for (const auto &scene : scenes) - { - sceneNodes[scene->getSceneId()] = newSceneNode(scene->getWindowName(), scene->getSceneId(), windowId); - } - - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newAmbientLightNode(sceneId, uiId, entity); - }); - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newDirectionalLightNode(sceneId, uiId, entity); - }); - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newPointLightNode(sceneId, uiId, entity); - }); - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newSpotLightNode(sceneId, uiId, entity); - }); - - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newCameraNode(sceneId, uiId, entity); - }); - - generateNodes( - sceneNodes, - [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { - return this->newEntityNode(sceneId, uiId, entity); - }); - - for (const auto &[_, sceneNode] : sceneNodes) - { - root_.children.push_back(sceneNode); - } - } -} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Hovering.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Hovering.cpp new file mode 100644 index 000000000..218f6ffcd --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Hovering.cpp @@ -0,0 +1,53 @@ +//// Hovering.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the hovering handling of the scene tree window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" + +namespace nexo::editor { + + void SceneTreeWindow::cameraHovered(const SceneObject &obj) + { + auto &cameraComponent = Application::m_coordinator->getComponent(obj.data.entity); + + if (cameraComponent.m_renderTarget) + { + ImGui::BeginTooltip(); + constexpr float previewSize = 200.0f; + cameraComponent.render = true; + const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); + + ImNexo::Image( + static_cast(static_cast(textureId)), + ImVec2(previewSize, previewSize) + ); + + ImGui::EndTooltip(); + } + } + + void SceneTreeWindow::handleHovering(const SceneObject &obj) + { + if (obj.type == SelectionType::CAMERA) { + static bool cameraHoveredLastFrame = false; + if (ImGui::IsItemHovered()) { + cameraHovered(obj); + cameraHoveredLastFrame = true; + } else if (cameraHoveredLastFrame) { + cameraHoveredLastFrame = false; + auto &cameraComponent = Application::m_coordinator->getComponent(obj.data.entity); + cameraComponent.render = false; + } + } + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Init.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Init.cpp new file mode 100644 index 000000000..c61efed26 --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Init.cpp @@ -0,0 +1,24 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the scene tree window setup +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" + +namespace nexo::editor { + + void SceneTreeWindow::setup() + { + setupShortcuts(); + } + +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/NodeHandling.cpp b/editor/src/DocumentWindows/SceneTreeWindow/NodeHandling.cpp new file mode 100644 index 000000000..dc256f3bd --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/NodeHandling.cpp @@ -0,0 +1,125 @@ +//// NodeHandling.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the node handling in the scene tree window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "components/Uuid.hpp" + +namespace nexo::editor { + + // Node creation methods + SceneObject SceneTreeWindow::newSceneNode(const std::string &sceneName, const scene::SceneId sceneId, const WindowId uiId) + { + SceneObject sceneNode; + const std::string uiName = ObjectTypeToIcon.at(SelectionType::SCENE) + sceneName; + sceneNode.data.sceneProperties = SceneProperties{sceneId, uiId}; + sceneNode.data.entity = sceneId; + sceneNode.type = SelectionType::SCENE; + auto &app = Application::getInstance(); + auto &selector = Selector::get(); + sceneNode.uuid = app.getSceneManager().getScene(sceneId).getUuid(); + sceneNode.uiName = selector.getUiHandle(sceneNode.uuid, uiName); + return sceneNode; + } + + void SceneTreeWindow::newLightNode(SceneObject &lightNode, const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity, const std::string &uiName) + { + const SceneProperties sceneProperties{sceneId, uiId}; + lightNode.data.sceneProperties = sceneProperties; + lightNode.data.entity = lightEntity; + auto &selector = Selector::get(); + const auto entityUuid = Application::m_coordinator->tryGetComponent(lightEntity); + if (entityUuid) + { + lightNode.uuid = entityUuid->get().uuid; + lightNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); + } else + lightNode.uiName = uiName; + } + + SceneObject SceneTreeWindow::newAmbientLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) + { + SceneObject lightNode; + lightNode.type = SelectionType::AMBIENT_LIGHT; + const std::string uiName = std::format("{}Ambient light ", ObjectTypeToIcon.at(lightNode.type)); + newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); + return lightNode; + } + + SceneObject SceneTreeWindow::newDirectionalLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) + { + SceneObject lightNode; + lightNode.type = SelectionType::DIR_LIGHT; + const std::string uiName = std::format("{}Directional light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbDirLights); + newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); + return lightNode; + } + + SceneObject SceneTreeWindow::newSpotLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) + { + SceneObject lightNode; + lightNode.type = SelectionType::SPOT_LIGHT; + const std::string uiName = std::format("{}Spot light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbSpotLights); + newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); + return lightNode; + } + + SceneObject SceneTreeWindow::newPointLightNode(const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity lightEntity) + { + SceneObject lightNode; + lightNode.type = SelectionType::POINT_LIGHT; + const std::string uiName = std::format("{}Point light {}", ObjectTypeToIcon.at(lightNode.type), ++m_nbPointLights); + newLightNode(lightNode, sceneId, uiId, lightEntity, uiName); + return lightNode; + } + + SceneObject SceneTreeWindow::newCameraNode(const scene::SceneId sceneId, const WindowId uiId, + const ecs::Entity cameraEntity) + { + SceneObject cameraNode; + const std::string uiName = ObjectTypeToIcon.at(SelectionType::CAMERA) + std::string("Camera"); + cameraNode.type = SelectionType::CAMERA; + const SceneProperties sceneProperties{sceneId, uiId}; + cameraNode.data.sceneProperties = sceneProperties; + cameraNode.data.entity = cameraEntity; + auto &selector = Selector::get(); + const auto entityUuid = nexo::Application::m_coordinator->tryGetComponent(cameraEntity); + if (entityUuid) + { + cameraNode.uuid = entityUuid->get().uuid; + cameraNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); + } else + cameraNode.uiName = uiName; + return cameraNode; + } + + SceneObject SceneTreeWindow::newEntityNode(const scene::SceneId sceneId, const WindowId uiId, + const ecs::Entity entity) + { + auto &selector = Selector::get(); + SceneObject entityNode; + const std::string uiName = std::format("{}{}", ObjectTypeToIcon.at(SelectionType::ENTITY), entity); + entityNode.type = SelectionType::ENTITY; + entityNode.data.sceneProperties = SceneProperties{sceneId, uiId}; + entityNode.data.entity = entity; + const auto entityUuid = nexo::Application::m_coordinator->tryGetComponent(entity); + if (entityUuid) + { + entityNode.uuid = entityUuid->get().uuid; + entityNode.uiName = selector.getUiHandle(entityUuid->get().uuid, uiName); + } + else + entityNode.uiName = uiName; + return entityNode; + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Rename.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Rename.cpp new file mode 100644 index 000000000..105c18e3e --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Rename.cpp @@ -0,0 +1,54 @@ +//// Rename.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the rename handling of the scene tree window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" + +namespace nexo::editor { + + void SceneTreeWindow::handleRename(SceneObject &obj) + { + ImGui::BeginGroup(); + ImGui::TextUnformatted(ObjectTypeToIcon.at(obj.type).c_str()); + ImGui::SameLine(); + + const std::string editableName = obj.uiName.substr(ObjectTypeToIcon.at(obj.type).size()); + char buffer[256]; + auto result = std::format_to_n(buffer, sizeof(buffer) - 1, "{}", editableName); + // Null-terminate at the position where format_to_n stopped writing + *(result.out) = '\0'; + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); // Remove border + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); // No rounding + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + + if (ImGui::InputText("##Rename", buffer, sizeof(buffer), + ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) + { + obj.uiName = ObjectTypeToIcon.at(obj.type) + std::string(buffer); + auto &selector = Selector::get(); + selector.setUiHandle(obj.uuid, obj.uiName); + if (obj.type == SelectionType::SCENE) + { + auto &app = getApp(); + app.getSceneManager().getScene(obj.data.sceneProperties.sceneId).setName(obj.uiName); + } + m_renameTarget.reset(); + } + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + m_renameTarget.reset(); + ImGui::PopStyleVar(3); + ImGui::EndGroup(); + } + +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp b/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp new file mode 100644 index 000000000..d406b0b4a --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/SceneCreation.cpp @@ -0,0 +1,87 @@ +//// SceneCreation.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the scene creation of the scene tree window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "utils/Config.hpp" + +namespace nexo::editor { + + bool SceneTreeWindow::setupNewDockSpaceNode(const std::string &floatingWindowName, const std::string &newSceneName) const + { + const ImGuiWindow* floatingWindow = ImGui::FindWindowByName(floatingWindowName.c_str()); + if (!floatingWindow) + return false; + + // Create a new docking node + const auto newDockId = ImGui::GetID("##DockNode"); + + // Configure the docking node + ImGui::DockBuilderRemoveNode(newDockId); + ImGui::DockBuilderAddNode(newDockId, ImGuiDockNodeFlags_None); + + // Set node size and position based on the floating window + const ImVec2 windowPos = floatingWindow->Pos; + const ImVec2 windowSize = floatingWindow->Size; + ImGui::DockBuilderSetNodeSize(newDockId, windowSize); + ImGui::DockBuilderSetNodePos(newDockId, windowPos); + + // Dock the windows to this node + ImGui::DockBuilderDockWindow(floatingWindowName.c_str(), newDockId); + ImGui::DockBuilderFinish(newDockId); + + // Update the registry with the new dock IDs + m_windowRegistry.setDockId(floatingWindowName, newDockId); + m_windowRegistry.setDockId(newSceneName, newDockId); + return true; + } + + bool SceneTreeWindow::handleSceneCreation(const std::string &newSceneName) const + { + if (newSceneName.empty()) { + LOG(NEXO_WARN, "Scene name is empty !"); + return false; + } + + const auto newScene = std::make_shared(newSceneName, m_windowRegistry); + newScene->setDefault(); + newScene->setup(); + m_windowRegistry.registerWindow(newScene); + + auto currentEditorSceneWindow = m_windowRegistry.getWindows(); + // If no editor scene is open, check the config file + if (currentEditorSceneWindow.empty()) { + const std::vector &editorSceneInConfig = findAllEditorScenes(); + if (!editorSceneInConfig.empty()) { + const auto dockId = m_windowRegistry.getDockId(editorSceneInConfig[0]); + if (!dockId) + return false; + m_windowRegistry.setDockId(std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, newScene->getSceneId()), *dockId); + return true; + } + // If nothing is present in config file, simply let it float + return false; + } + + // Else we retrieve the first active editor scene + const std::string windowName = std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, currentEditorSceneWindow[0]->getSceneId()); + const auto dockId = m_windowRegistry.getDockId(windowName); + // If we dont find the dockId, it means the scene is floating, so we create a new dock space node + if (!dockId) { + setupNewDockSpaceNode(windowName, std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, newScene->getSceneId())); + return true; + } + m_windowRegistry.setDockId(std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, newScene->getSceneId()), *dockId); + return true; + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow.hpp b/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp similarity index 66% rename from editor/src/DocumentWindows/SceneTreeWindow.hpp rename to editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp index 06000ef3d..0d06fd971 100644 --- a/editor/src/DocumentWindows/SceneTreeWindow.hpp +++ b/editor/src/DocumentWindows/SceneTreeWindow/SceneTreeWindow.hpp @@ -14,12 +14,13 @@ #pragma once #include "ADocumentWindow.hpp" +#include "DocumentWindows/EditorScene/EditorScene.hpp" #include "IconsFontAwesome.h" #include "Nexo.hpp" #include "core/scene/SceneManager.hpp" #include "context/Selector.hpp" -#include "PopupManager.hpp" +#include "../PopupManager.hpp" #include #include @@ -28,14 +29,26 @@ namespace nexo::editor { + /** + * @brief Stores scene identification information. + * + * Contains the scene's unique identifier and its associated window ID + * to facilitate operations that require both scene and UI context. + */ struct SceneProperties { - scene::SceneId sceneId; - WindowId windowId; + scene::SceneId sceneId; ///< The unique identifier for the scene + WindowId windowId; ///< The associated window identifier in the UI }; + /** + * @brief Links an entity with its parent scene information. + * + * Combines entity ID with scene properties to maintain the hierarchical + * relationship between entities and their containing scenes. + */ struct EntityProperties { - SceneProperties sceneProperties; - ecs::Entity entity; + SceneProperties sceneProperties; ///< Properties of the scene containing this entity + ecs::Entity entity; ///< The entity identifier }; /** @@ -65,7 +78,7 @@ namespace nexo::editor { explicit SceneObject(std::string name = "", std::vector children = {}, SelectionType type = SelectionType::NONE, EntityProperties data = {}) - : uiName(std::move(name)), type(type), data(std::move(data)), children(std::move(children)) {} + : uiName(std::move(name)), type(type), data(data), children(std::move(children)) {} }; /** @@ -74,12 +87,15 @@ namespace nexo::editor { * The SceneTreeWindow class is responsible for drawing the scene tree, handling selection, * renaming, context menus, and scene/node creation. */ - class SceneTreeWindow : public ADocumentWindow { + class SceneTreeWindow final : public ADocumentWindow { public: using ADocumentWindow::ADocumentWindow; - ~SceneTreeWindow() override; + ~SceneTreeWindow() override = default; + // No-op method in this class void setup() override; + + // No-op method in this class void shutdown() override; /** @@ -125,12 +141,12 @@ namespace nexo::editor { * @param nodeCreator Function that creates a SceneObject given a scene ID, UI window ID, and entity. */ template - void generateNodes(std::map &scenes, NodeCreator nodeCreator) + static void generateNodes(std::map &scenes, NodeCreator nodeCreator) { - const std::set entities = nexo::Application::m_coordinator->getAllEntitiesWith(); + const std::vector entities = Application::m_coordinator->getAllEntitiesWith(); for (const ecs::Entity entity : entities) { - const auto& sceneTag = nexo::Application::m_coordinator->getComponent(entity); + const auto& sceneTag = Application::m_coordinator->getComponent(entity); if (auto it = scenes.find(sceneTag.id); it != scenes.end()) { SceneObject newNode = nodeCreator(it->second.data.sceneProperties.sceneId, it->second.data.sceneProperties.windowId, entity); @@ -152,7 +168,7 @@ namespace nexo::editor { * @param uiId The identifier for the scene's UI element. * @return SceneObject The newly created scene node with initialized properties. */ - SceneObject newSceneNode(const std::string &sceneName, const scene::SceneId sceneId, const WindowId uiId) const; + static SceneObject newSceneNode(const std::string &sceneName, scene::SceneId sceneId, WindowId uiId); /** * @brief Creates a new light node and adds properties to it. @@ -163,7 +179,7 @@ namespace nexo::editor { * @param lightEntity The light entity. * @param uiName The UI display name. */ - void newLightNode(SceneObject &lightNode, scene::SceneId sceneId, WindowId uiId, ecs::Entity lightEntity, const std::string &uiName) const; + static void newLightNode(SceneObject &lightNode, scene::SceneId sceneId, WindowId uiId, ecs::Entity lightEntity, const std::string &uiName); /** * @brief Creates a new ambient light node. @@ -173,7 +189,7 @@ namespace nexo::editor { * @param lightEntity The ambient light entity. * @return A SceneObject representing the ambient light. */ - SceneObject newAmbientLightNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity lightEntity) const; + static SceneObject newAmbientLightNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity lightEntity); /** * @brief Creates a new directional light node. * @@ -212,7 +228,7 @@ namespace nexo::editor { * @param cameraEntity The camera entity. * @return A SceneObject representing the camera. */ - SceneObject newCameraNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity cameraEntity) const; + static SceneObject newCameraNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity cameraEntity) ; /** * @brief Creates a new entity node. @@ -222,7 +238,7 @@ namespace nexo::editor { * @param entity The entity. * @return A SceneObject representing the entity. */ - SceneObject newEntityNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity entity) const; + static SceneObject newEntityNode(scene::SceneId sceneId, WindowId uiId, ecs::Entity entity) ; /** * @brief Handles the renaming of a scene object. @@ -245,7 +261,17 @@ namespace nexo::editor { * @param baseFlags ImGui tree node flags to customize the node's appearance. * @return true if the tree node is open (expanded); false otherwise. */ - bool handleSelection(const SceneObject &obj, const std::string &uniqueLabel, ImGuiTreeNodeFlags baseFlags) const; + static bool handleSelection(const SceneObject &obj, const std::string &uniqueLabel, ImGuiTreeNodeFlags baseFlags) ; + + /** + * @brief Handles camera preview tooltips when hovering over camera nodes. + * + * When hovering over a camera node, renders a preview tooltip showing the camera's + * view and enables rendering for that camera temporarily. + * + * @param obj The scene object (camera) being hovered over. + */ + static void handleHovering(const SceneObject &obj) ; /** * @brief Displays a context menu option to delete a scene. @@ -255,7 +281,7 @@ namespace nexo::editor { * * @param obj The scene object representing the scene to be deleted. */ - void sceneSelected(const SceneObject &obj) const; + void sceneSelected(const SceneObject &obj); /** * @brief Displays a context menu option to delete a light node. @@ -265,7 +291,7 @@ namespace nexo::editor { * * @param obj The scene object representing the light node to delete. */ - void lightSelected(const SceneObject &obj) const; + static void lightSelected(const SceneObject &obj) ; /** * @brief Displays a context menu option for deleting a camera. @@ -277,10 +303,47 @@ namespace nexo::editor { * @param obj The scene object representing the camera to be deleted. */ void cameraSelected(const SceneObject &obj) const; - void entitySelected(const SceneObject &obj) const; + + /** + * @brief Handles camera preview when hovering over a camera node. + * + * Renders a small preview of what the camera sees when the cursor + * hovers over the camera node in the scene tree. + * + * @param obj The scene object representing the camera being hovered. + */ + static void cameraHovered(const SceneObject &obj); + + /** + * @brief Displays context menu options for an entity. + * + * Shows options like "Delete Entity" when right-clicking on an entity + * in the scene tree. Handles entity deletion when selected. + * + * @param obj The scene object representing the entity. + */ + static void entitySelected(const SceneObject &obj) ; + + /** + * @brief Renders a node and its children in the scene tree. + * + * Handles the recursive display of scene tree nodes, including selection, + * renaming, and context menus for each node type. + * + * @param object The scene object to render. + */ void showNode(SceneObject &object); + + /** + * @brief Shows the context menu for the scene tree background. + * + * Displays options like "Create Scene" when right-clicking on an empty + * area of the scene tree. + */ void sceneContextMenu(); + void showSceneSelectionContextMenu(scene::SceneId sceneId, const std::string &uuid = "", const std::string &uiName = ""); + /** * @brief Displays a modal popup for creating a new scene. * @@ -290,5 +353,53 @@ namespace nexo::editor { * The popup is closed either upon successful scene creation or when the "Cancel" button is clicked. */ void sceneCreationMenu(); + + /** + * @brief Creates a new scene with the provided name. + * + * Validates the scene name, creates a new EditorScene object, and configures + * appropriate docking settings for the new scene window. + * + * @param newSceneName The name for the new scene. + * @return true if the scene was created successfully, false otherwise. + */ + bool handleSceneCreation(const std::string &newSceneName) const; + + /** + * @brief Sets up docking for a new scene window. + * + * Creates a new docking node and configures it to contain the specified windows, + * positioning it at the location of the existing floating window. + * + * @param floatingWindowName The name of the existing floating window to use as a reference. + * @param newSceneName The name of the new scene window to dock. + * @return true if the docking setup was successful, false otherwise. + */ + bool setupNewDockSpaceNode(const std::string &floatingWindowName, const std::string &newSceneName) const; + + enum class SceneTreeState { + GLOBAL, + NB_STATE + }; + WindowState m_defaultState; + bool m_forceExpandAll = false; + bool m_forceCollapseAll = false; + bool m_resetExpandState = false; + + // Add these method declarations + void setupShortcuts(); + void setupDefaultState(); + + // Add these callback methods + void deleteSelectedCallback(); + void addEntityCallback(); + void expandAllCallback(); + void collapseAllCallback(); + void renameSelectedCallback(); + static void duplicateSelectedCallback(); + + static void selectAllCallback(); + static void hideSelectedCallback(); + static void showAllCallback(); }; } diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Selection.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Selection.cpp new file mode 100644 index 000000000..6f09f3d9c --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Selection.cpp @@ -0,0 +1,177 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the selection of the scene tree +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "EntityFactory3D.hpp" +#include "utils/EditorProps.hpp" +#include "context/ActionManager.hpp" +#include "context/actions/EntityActions.hpp" + +#include +#include + +namespace nexo::editor { + + void SceneTreeWindow::entitySelected(const SceneObject &obj) + { + auto &selector = Selector::get(); + auto &app = nexo::getApp(); + + // Check if we're operating on a single item or multiple items + const auto& selectedEntities = selector.getSelectedEntities(); + const bool multipleSelected = selectedEntities.size() > 1; + + const std::string menuText = multipleSelected ? + std::format("Delete Selected Entities ({})", selectedEntities.size()) : + "Delete Entity"; + + if (ImGui::MenuItem(menuText.c_str())) + { + if (multipleSelected) { + // Delete all selected entities + auto actionGroup = ActionManager::createActionGroup(); + for (const auto& entityId : selectedEntities) { + auto deleteAction = std::make_unique(entityId); + actionGroup->addAction(std::move(deleteAction)); + app.deleteEntity(entityId); + } + ActionManager::get().recordAction(std::move(actionGroup)); + selector.clearSelection(); + } else { + // Delete just this entity + selector.clearSelection(); + auto action = std::make_unique(obj.data.entity); + ActionManager::get().recordAction(std::move(action)); + app.deleteEntity(obj.data.entity); + } + } + } + + void SceneTreeWindow::cameraSelected(const SceneObject &obj) const + { + auto &app = Application::getInstance(); + auto &selector = Selector::get(); + + // Check if we're operating on a single item or multiple items + const auto& selectedEntities = selector.getSelectedEntities(); + const bool multipleSelected = selectedEntities.size() > 1; + + const std::string deleteMenuText = multipleSelected ? + std::format("Delete Selected Entities ({})", selectedEntities.size()) : + "Delete Camera"; + + if (ImGui::MenuItem(deleteMenuText.c_str())) + { + if (multipleSelected) { + // Delete all selected entities + auto actionGroup = ActionManager::createActionGroup(); + for (const auto& entityId : selectedEntities) { + auto deleteAction = std::make_unique(entityId); + actionGroup->addAction(std::move(deleteAction)); + app.deleteEntity(entityId); + } + ActionManager::get().recordAction(std::move(actionGroup)); + selector.clearSelection(); + } else { + // Delete just this entity + selector.clearSelection(); + auto action = std::make_unique(obj.data.entity); + ActionManager::get().recordAction(std::move(action)); + app.deleteEntity(obj.data.entity); + } + } + + // Switch to camera only makes sense for a single camera + if (!multipleSelected && ImGui::MenuItem("Switch to")) + { + auto &cameraComponent = Application::m_coordinator->getComponent(obj.data.entity); + cameraComponent.render = true; + cameraComponent.active = true; + const auto &scenes = m_windowRegistry.getWindows(); + for (const auto &scene : scenes) { + if (scene->getSceneId() == obj.data.sceneProperties.sceneId) { + scene->setCamera(obj.data.entity); + break; + } + } + } + } + + void SceneTreeWindow::lightSelected(const SceneObject &obj) + { + auto &app = Application::getInstance(); + auto &selector = Selector::get(); + + // Check if we're operating on a single item or multiple items + const auto& selectedEntities = selector.getSelectedEntities(); + const bool multipleSelected = selectedEntities.size() > 1; + + const std::string menuText = multipleSelected ? + std::format("Delete Selected Entities ({})", selectedEntities.size()) : + "Delete Light"; + + if (ImGui::MenuItem(menuText.c_str())) + { + if (multipleSelected) { + // Delete all selected entities + auto actionGroup = ActionManager::createActionGroup(); + for (const auto& entityId : selectedEntities) { + auto deleteAction = std::make_unique(entityId); + actionGroup->addAction(std::move(deleteAction)); + app.deleteEntity(entityId); + } + ActionManager::get().recordAction(std::move(actionGroup)); + selector.clearSelection(); + } else { + // Delete just this entity + selector.clearSelection(); + auto action = std::make_unique(obj.data.entity); + ActionManager::get().recordAction(std::move(action)); + app.deleteEntity(obj.data.entity); + } + } + } + + void SceneTreeWindow::sceneSelected([[maybe_unused]] const SceneObject &obj) + { + m_popupManager.openPopupWithCallback( + "Scene selection context menu", + [this, obj]() {this->showSceneSelectionContextMenu(obj.data.sceneProperties.sceneId, obj.uuid, obj.uiName);} + ); + } + + bool SceneTreeWindow::handleSelection(const SceneObject &obj, const std::string &uniqueLabel, + const ImGuiTreeNodeFlags baseFlags) + { + const bool nodeOpen = ImGui::TreeNodeEx(uniqueLabel.c_str(), baseFlags); + if (!nodeOpen) + return nodeOpen; + + if (ImGui::IsItemClicked()) + { + auto &selector = Selector::get(); + const bool isShiftPressed = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift); + const bool isCtrlPressed = ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl); + + if (isCtrlPressed) + selector.toggleSelection(obj.uuid, static_cast(obj.data.entity), obj.type); + else if (isShiftPressed) + selector.addToSelection(obj.uuid, static_cast(obj.data.entity), obj.type); + else + selector.selectEntity(obj.uuid, static_cast(obj.data.entity), obj.type); + selector.setSelectedScene(static_cast(obj.data.sceneProperties.sceneId)); + } + return nodeOpen; + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp new file mode 100644 index 000000000..26a12014c --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp @@ -0,0 +1,295 @@ +//// Shortcuts.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 29/04/2025 +// Description: Source file for the scene tree window shortcuts +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "components/SceneComponents.hpp" +#include "context/ActionManager.hpp" +#include "components/Uuid.hpp" +#include "context/actions/EntityActions.hpp" + +namespace nexo::editor { + void SceneTreeWindow::setupShortcuts() + { + setupDefaultState(); + m_windowState = m_defaultState; + } + + void SceneTreeWindow::setupDefaultState() + { + m_defaultState = {static_cast(SceneTreeState::GLOBAL)}; + + // CTRL context + m_defaultState.registerCommand( + Command::create() + .description("Control context") + .key("Ctrl") + .modifier(true) + .addChild( + Command::create() + .description("Select all") + .key("A") + .onPressed([](){ + selectAllCallback(); }) + .build() + ) + .addChild( + Command::create() + .description("Duplicate") + .key("D") + .onPressed([](){ duplicateSelectedCallback(); }) + .build() + ) + .addChild( + Command::create() + .description("Unhide all") + .key("H") + .onPressed([](){ showAllCallback(); }) + .build() + ) + .addChild( + Command::create() + .description("Create Scene") + .key("N") + .onPressed([this](){ this->m_popupManager.openPopup("Create New Scene"); }) + .build() + ) + .build() + ); + + // Delete selected + m_defaultState.registerCommand( + Command::create() + .description("Add Entity") + .key("A") + .onPressed([this](){ addEntityCallback(); }) + .build() + ); + + // Delete selected + m_defaultState.registerCommand( + Command::create() + .description("Delete") + .key("Delete") + .onPressed([this](){ deleteSelectedCallback(); }) + .build() + ); + + // Rename selected + m_defaultState.registerCommand( + Command::create() + .description("Rename") + .key("F2") + .onPressed([this](){ renameSelectedCallback(); }) + .build() + ); + + // Expand all nodes + m_defaultState.registerCommand( + Command::create() + .description("Expand all") + .key("Down") + .onPressed([this](){ expandAllCallback(); }) + .build() + ); + + // Collapse all nodes + m_defaultState.registerCommand( + Command::create() + .description("Collapse all") + .key("Up") + .onPressed([this](){ collapseAllCallback(); }) + .build() + ); + + // Hide selected + m_defaultState.registerCommand( + Command::create() + .description("Hide") + .key("H") + .onPressed([](){ hideSelectedCallback(); }) + .build() + ); + } + + void SceneTreeWindow::addEntityCallback() + { + const auto &selector = Selector::get(); + int currentSceneId = selector.getSelectedScene(); + if (currentSceneId == -1) + return; + + m_popupManager.openPopupWithCallback( + "Scene selection context menu", + [this, currentSceneId]() {showSceneSelectionContextMenu(currentSceneId);} + ); + } + + void SceneTreeWindow::selectAllCallback() + { + auto& selector = Selector::get(); + int currentSceneId = selector.getSelectedScene(); + + if (currentSceneId != -1) { + auto& app = nexo::getApp(); + const auto& scene = app.getSceneManager().getScene(currentSceneId); + + selector.clearSelection(); + + for (const auto entity : scene.getEntities()) { + const auto uuidComponent = Application::m_coordinator->tryGetComponent(entity); + if (uuidComponent) { + selector.addToSelection(uuidComponent->get().uuid, static_cast(entity)); + } + } + } + } + + void SceneTreeWindow::deleteSelectedCallback() + { + auto& selector = Selector::get(); + const auto& selectedEntities = selector.getSelectedEntities(); + + if (selectedEntities.empty()) return; + + auto& app = getApp(); + auto& actionManager = ActionManager::get(); + + if (selectedEntities.size() > 1) { + auto actionGroup = ActionManager::createActionGroup(); + for (const auto entity : selectedEntities) { + actionGroup->addAction(ActionManager::prepareEntityDeletion(entity)); + app.deleteEntity(entity); + } + actionManager.recordAction(std::move(actionGroup)); + } else { + auto deleteAction = ActionManager::prepareEntityDeletion(selectedEntities[0]); + app.deleteEntity(selectedEntities[0]); + actionManager.recordAction(std::move(deleteAction)); + } + + selector.clearSelection(); + m_windowState = m_defaultState; + } + + void SceneTreeWindow::expandAllCallback() + { + m_forceCollapseAll = false; + m_forceExpandAll = true; + } + + void SceneTreeWindow::collapseAllCallback() + { + m_forceCollapseAll = true; + m_forceExpandAll = false; + } + + void SceneTreeWindow::renameSelectedCallback() + { + //TODO: Implement rename callback + } + + void SceneTreeWindow::duplicateSelectedCallback() + { + auto& selector = Selector::get(); + const auto& selectedEntities = selector.getSelectedEntities(); + + if (selectedEntities.empty()) return; + + auto& app = nexo::getApp(); + auto& actionManager = ActionManager::get(); + int currentSceneId = selector.getSelectedScene(); + + if (currentSceneId == -1) return; + + std::vector newEntities; + newEntities.reserve(selectedEntities.size()); + auto actionGroup = ActionManager::createActionGroup(); + selector.clearSelection(); + + for (const auto entity : selectedEntities) { + ecs::Entity newEntity = Application::m_coordinator->duplicateEntity(entity); + const components::UuidComponent uuidComponent; + Application::m_coordinator->getComponent(newEntity) = uuidComponent; + Application::m_coordinator->removeComponent(newEntity); + app.getSceneManager().getScene(currentSceneId).addEntity(newEntity); + auto action = std::make_unique(newEntity); + actionGroup->addAction(std::move(action)); + newEntities.push_back(newEntity); + } + + actionManager.recordAction(std::move(actionGroup)); + + // Select all the newly created entities + for (const auto newEntity : newEntities) { + const auto uuidComponent = Application::m_coordinator->tryGetComponent(newEntity); + if (uuidComponent) { + selector.addToSelection(uuidComponent->get().uuid, static_cast(newEntity)); + } + } + } + + void SceneTreeWindow::hideSelectedCallback() + { + const auto& selector = Selector::get(); + const auto& selectedEntities = selector.getSelectedEntities(); + + if (selectedEntities.empty()) return; + + auto& actionManager = ActionManager::get(); + auto actionGroup = ActionManager::createActionGroup(); + + for (const auto entity : selectedEntities) { + if (Application::m_coordinator->entityHasComponent(entity)) { + auto& renderComponent = Application::m_coordinator->getComponent(entity); + if (renderComponent.isRendered) { + auto beforeState = renderComponent.save(); + renderComponent.isRendered = !renderComponent.isRendered; + auto afterState = renderComponent.save(); + actionGroup->addAction(std::make_unique>( + entity, beforeState, afterState)); + } + } + } + + actionManager.recordAction(std::move(actionGroup)); + } + + void SceneTreeWindow::showAllCallback() + { + const auto& selector = Selector::get(); + int currentSceneId = selector.getSelectedScene(); + + if (currentSceneId == -1) return; + + auto& app = getApp(); + const auto& scene = app.getSceneManager().getScene(currentSceneId); + auto& actionManager = ActionManager::get(); + auto actionGroup = ActionManager::createActionGroup(); + + for (const auto entity : scene.getEntities()) { + if (Application::m_coordinator->entityHasComponent(entity)) { + auto& renderComponent = Application::m_coordinator->getComponent(entity); + if (!renderComponent.isRendered) { + auto beforeState = renderComponent.save(); + renderComponent.isRendered = true; + auto afterState = renderComponent.save(); + actionGroup->addAction(std::make_unique>( + entity, beforeState, afterState)); + } + } + } + + actionManager.recordAction(std::move(actionGroup)); + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp new file mode 100644 index 000000000..f6eb37037 --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Show.cpp @@ -0,0 +1,249 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the scene tree window rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" +#include "components/Uuid.hpp" +#include "EntityFactory3D.hpp" +#include "LightFactory.hpp" +#include "context/actions/EntityActions.hpp" +#include "utils/EditorProps.hpp" +#include "ImNexo/Panels.hpp" +#include "context/ActionManager.hpp" + +namespace nexo::editor { + + void SceneTreeWindow::showSceneSelectionContextMenu(scene::SceneId sceneId, const std::string &uuid, const std::string &uiName) + { + if (!uuid.empty() && !uiName.empty() &&ImGui::MenuItem("Delete Scene")) { + auto &app = Application::getInstance(); + auto &selector = Selector::get(); + selector.clearSelection(); + const std::string &sceneName = selector.getUiHandle(uuid, uiName); + m_windowRegistry.unregisterWindow(sceneName); + app.getSceneManager().deleteScene(sceneId); + } + + // ---- Add Entity submenu ---- + if (ImGui::BeginMenu("Add Entity")) { + auto &app = Application::getInstance(); + auto &sceneManager = app.getSceneManager(); + + // --- Primitives submenu --- + if (ImGui::BeginMenu("Primitives")) { + if (ImGui::MenuItem("Cube")) { + const ecs::Entity newCube = EntityFactory3D::createCube({0.0f, 0.0f, -5.0f}, {1.0f, 1.0f, 1.0f}, + {0.0f, 0.0f, 0.0f}, {0.05f * 1.5, 0.09f * 1.15, 0.13f * 1.25, 1.0f}); + sceneManager.getScene(sceneId).addEntity(newCube); + auto action = std::make_unique(newCube); + ActionManager::get().recordAction(std::move(action)); + } + ImGui::EndMenu(); + } + + // --- Model item (with file‑dialog) --- + if (ImGui::MenuItem("Model")) { + //TODO: import model + } + + // --- Lights submenu --- + if (ImGui::BeginMenu("Lights")) { + if (ImGui::MenuItem("Directional")) { + const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.0f, -1.0f, 0.0f}); + sceneManager.getScene(sceneId).addEntity(directionalLight); + auto action = std::make_unique(directionalLight); + ActionManager::get().recordAction(std::move(action)); + } + if (ImGui::MenuItem("Point")) { + const ecs::Entity pointLight = LightFactory::createPointLight({0.0f, 0.5f, 0.0f}); + utils::addPropsTo(pointLight, utils::PropsType::POINT_LIGHT); + sceneManager.getScene(sceneId).addEntity(pointLight); + auto action = std::make_unique(pointLight); + ActionManager::get().recordAction(std::move(action)); + } + if (ImGui::MenuItem("Spot")) { + const ecs::Entity spotLight = LightFactory::createSpotLight({0.0f, 0.5f, 0.0f}, {0.0f, -1.0f, 0.0f}); + utils::addPropsTo(spotLight, utils::PropsType::SPOT_LIGHT); + sceneManager.getScene(sceneId).addEntity(spotLight); + auto action = std::make_unique(spotLight); + ActionManager::get().recordAction(std::move(action)); + } + ImGui::EndMenu(); + } + + // --- Camera item --- + if (ImGui::MenuItem("Camera")) { + m_popupManager.openPopupWithCallback("Popup camera inspector", [this, sceneId]() { + const auto &editorScenes = m_windowRegistry.getWindows(); + for (const auto &scene : editorScenes) { + if (scene->getSceneId() == sceneId) { + ImNexo::CameraInspector(sceneId); + break; + } + } + }, ImVec2(1440,900)); + } + + ImGui::EndMenu(); + } + } + + void SceneTreeWindow::sceneContextMenu() + { + if (m_popupManager.showPopup("Scene Tree Context Menu")) + { + if (ImGui::MenuItem("Create Scene")) + m_popupManager.openPopup("Create New Scene"); + PopupManager::closePopup(); + } + + if (m_popupManager.showPopup("Scene selection context menu")) + { + m_popupManager.runPopupCallback("Scene selection context menu"); + PopupManager::closePopup(); + } + + if (m_popupManager.showPopupModal("Popup camera inspector")) { + m_popupManager.runPopupCallback("Popup camera inspector"); + PopupManager::closePopup(); + } + } + + void SceneTreeWindow::sceneCreationMenu() + { + if (!m_popupManager.showPopupModal("Create New Scene")) + return; + + static char sceneNameBuffer[256] = ""; + + ImGui::Text("Enter Scene Name:"); + ImGui::InputText("##SceneName", sceneNameBuffer, sizeof(sceneNameBuffer)); + + if (ImNexo::Button("Create") && handleSceneCreation(sceneNameBuffer)) { + memset(sceneNameBuffer, 0, sizeof(sceneNameBuffer)); + PopupManager::closePopupInContext(); + } + + ImGui::SameLine(); + if (ImNexo::Button("Cancel")) + PopupManager::closePopupInContext(); + + PopupManager::closePopup(); + } + + void SceneTreeWindow::showNode(SceneObject &object) + { + ImGuiTreeNodeFlags baseFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | + ImGuiTreeNodeFlags_SpanAvailWidth; + // Checks if the object is at the end of a tree + const bool leaf = object.children.empty(); + if (leaf) + baseFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + + // Check if this object is selected + auto const &selector = Selector::get(); + const bool isSelected = selector.isEntitySelected(static_cast(object.data.entity)); + + if (isSelected) + baseFlags |= ImGuiTreeNodeFlags_Selected; + + bool nodeOpen = false; + const std::string uniqueLabel = object.uiName; + + if (m_forceExpandAll && !leaf) { + ImGui::SetNextItemOpen(true); + m_resetExpandState = true; + } else if (m_forceCollapseAll) { + ImGui::SetNextItemOpen(false); + m_resetExpandState = true; + } + + // If the user wishes to rename handle the rename, else handle the selection + if (m_renameTarget && m_renameTarget->first == object.type && m_renameTarget->second == object.uuid) + handleRename(object); + else + nodeOpen = handleSelection(object, uniqueLabel, baseFlags); + + handleHovering(object); + + // Handles the right click on each different type of object + if (object.type != SelectionType::NONE && ImGui::BeginPopupContextItem(uniqueLabel.c_str())) + { + // Only show rename option for the primary selected entity or for non-selected entities + if ((!isSelected || selector.getPrimaryEntity() == static_cast(object.data.entity)) && + ImGui::MenuItem("Rename")) + { + m_renameTarget = {object.type, object.uuid}; + m_renameBuffer = object.uiName; + } + + if (object.type == SelectionType::SCENE) + sceneSelected(object); + else if (object.type == SelectionType::DIR_LIGHT || object.type == SelectionType::POINT_LIGHT || object.type == SelectionType::SPOT_LIGHT) + lightSelected(object); + else if (object.type == SelectionType::CAMERA) + cameraSelected(object); + else if (object.type == SelectionType::ENTITY) + entitySelected(object); + ImGui::EndPopup(); + } + + // Go further into the tree + if (nodeOpen && !leaf) + { + for (auto &child: object.children) + { + showNode(child); + } + ImGui::TreePop(); + } + } + + void SceneTreeWindow::show() + { + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 300, 20), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(300, ImGui::GetIO().DisplaySize.y - 40), ImGuiCond_FirstUseEver); + + if (ImGui::Begin(ICON_FA_SITEMAP " Scene Tree" NEXO_WND_USTRID_SCENE_TREE, &m_opened, ImGuiWindowFlags_NoCollapse)) + { + beginRender(NEXO_WND_USTRID_SCENE_TREE); + m_focused = ImGui::IsWindowFocused(); + m_hovered = ImGui::IsWindowHovered(); + + const auto &selector = Selector::get(); + + // Opens the right click popup when no items are hovered + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered( + ImGuiHoveredFlags_AllowWhenBlockedByPopup) && !ImGui::IsAnyItemHovered()) { + m_popupManager.openPopup("Scene Tree Context Menu"); + } + + // Display multi-selection count at top of window if applicable + const auto& selectedEntities = selector.getSelectedEntities(); + if (selectedEntities.size() > 1) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); + ImGui::Text("%zu entities selected", selectedEntities.size()); + ImGui::PopStyleColor(); + ImGui::Separator(); + } + + if (!root_.children.empty()) { + for (auto &node : root_.children) + showNode(node); + } + sceneContextMenu(); + sceneCreationMenu(); + } + ImGui::End(); + } +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp new file mode 100644 index 000000000..0387ef769 --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp @@ -0,0 +1,24 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the scene tree window shutdown +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" + +namespace nexo::editor { + + void SceneTreeWindow::shutdown() + { + // Nothing to shutdown + } + +} diff --git a/editor/src/DocumentWindows/SceneTreeWindow/Update.cpp b/editor/src/DocumentWindows/SceneTreeWindow/Update.cpp new file mode 100644 index 000000000..e3a752697 --- /dev/null +++ b/editor/src/DocumentWindows/SceneTreeWindow/Update.cpp @@ -0,0 +1,86 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the update of the scene tree window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "SceneTreeWindow.hpp" + +namespace nexo::editor { + void SceneTreeWindow::update() + { + root_.uiName = "Scene Tree"; + root_.data.entity = -1; + root_.type = SelectionType::NONE; + root_.children.clear(); + m_nbPointLights = 0; + m_nbDirLights = 0; + m_nbSpotLights = 0; + + if (m_resetExpandState) { + m_forceExpandAll = false; + m_forceCollapseAll = false; + m_resetExpandState = false; + } + + // Retrieves the scenes that are displayed on the GUI + const auto &scenes = m_windowRegistry.getWindows(); + std::map sceneNodes; + for (const auto &scene : scenes) + { + sceneNodes[scene->getSceneId()] = newSceneNode(scene->getWindowName(), scene->getSceneId(), windowId); + } + + generateNodes( + sceneNodes, + [](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newAmbientLightNode(sceneId, uiId, entity); + }); + generateNodes( + sceneNodes, + [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newDirectionalLightNode(sceneId, uiId, entity); + }); + generateNodes( + sceneNodes, + [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newPointLightNode(sceneId, uiId, entity); + }); + generateNodes( + sceneNodes, + [this](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newSpotLightNode(sceneId, uiId, entity); + }); + + generateNodes>( + sceneNodes, + [](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newCameraNode(sceneId, uiId, entity); + }); + + generateNodes< + components::RenderComponent, + components::TransformComponent, + components::SceneTag, + ecs::Exclude, + ecs::Exclude, + ecs::Exclude>( + sceneNodes, + [](const scene::SceneId sceneId, const WindowId uiId, const ecs::Entity entity) { + return newEntityNode(sceneId, uiId, entity); + }); + + for (const auto &sceneNode: sceneNodes | std::views::values) + { + root_.children.push_back(sceneNode); + } + } +} diff --git a/editor/src/DocumentWindows/TestWindow/Init.cpp b/editor/src/DocumentWindows/TestWindow/Init.cpp new file mode 100644 index 000000000..a6e0f0a17 --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/Init.cpp @@ -0,0 +1,24 @@ +//// Init.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Source file for the test window initialization +// +/////////////////////////////////////////////////////////////////////////////// + +#include "TestWindow.hpp" + +namespace nexo::editor { + + void TestWindow::setup() + { + parseTestFolder(); + } + +} diff --git a/editor/src/DocumentWindows/TestWindow/Parser.cpp b/editor/src/DocumentWindows/TestWindow/Parser.cpp new file mode 100644 index 000000000..95a4142b6 --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/Parser.cpp @@ -0,0 +1,99 @@ +//// Parser.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Source file for the parsing logic for the test window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Exception.hpp" +#include "TestWindow.hpp" +#include "utils/String.hpp" +#include "Path.hpp" +#include "Logger.hpp" +#include "exceptions/Exceptions.hpp" + +#include +#include +#include + +namespace nexo::editor { + + static std::string parseBullet(std::string line) + { + if (line.size() >= 2 && line[0] == '-' && std::isspace(line[1])) + return line.substr(2); + return {}; + } + + void TestWindow::parseFile(const std::filesystem::directory_entry &entry) + { + std::ifstream in(entry.path()); + if (!in) + THROW_EXCEPTION(FileReadException, entry.path().string(), std::strerror(errno)); + + TestSection* currentSection = nullptr; + TestSection* currentSubSection = nullptr; + std::string line; + + while (std::getline(in, line)) { + utils::trim(line); + // Top-level section + if (line.rfind("# ", 0) == 0) { + m_testSections.emplace_back(); + currentSection = &m_testSections.back(); + currentSection->name = line.substr(2); + currentSubSection = nullptr; + // Sub-section + } else if (line.rfind("## ", 0) == 0) { + if (!currentSection) + THROW_EXCEPTION(InvalidTestFileFormat, entry.path().string(), "Subsection found without main section"); + currentSection->subSections.emplace_back(); + currentSubSection = ¤tSection->subSections.back(); + currentSubSection->name = line.substr(3); + // Test case + } else if (line.rfind("-", 0) == 0) { + std::string testName = parseBullet(line); + if (testName.empty()) + THROW_EXCEPTION(InvalidTestFileFormat, entry.path().string(), "Test case format is invalid : \"- Test case name \""); + + TestCase testCase; + testCase.name = std::move(testName); + + if (currentSubSection) + currentSubSection->testCases.push_back(std::move(testCase)); + else if (currentSection) + currentSection->testCases.push_back(std::move(testCase)); + } + } + } + + void TestWindow::parseTestFolder() + { + std::filesystem::path testDir = Path::resolvePathRelativeToExe( + "../tests/editor"); + + for (const auto &entry : std::filesystem::directory_iterator(testDir)) { + if (!entry.is_regular_file()) { + LOG(NEXO_WARN, "{} is a directory, skipping...", entry.path().string()); + continue; + } + if (entry.path().extension() != ".test") { + LOG(NEXO_WARN, "{} is not a test file, skipping...", entry.path().string()); + continue; + } + try { + parseFile(entry); + } catch (const nexo::Exception &e) { + LOG_EXCEPTION(e); + } + } + } + +} diff --git a/editor/src/DocumentWindows/TestWindow/Show.cpp b/editor/src/DocumentWindows/TestWindow/Show.cpp new file mode 100644 index 000000000..82a1eddef --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/Show.cpp @@ -0,0 +1,123 @@ +//// Show.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Source file for the test window rendering +// +/////////////////////////////////////////////////////////////////////////////// + +#include "TestWindow.hpp" +#include "ImNexo/Components.hpp" + +namespace nexo::editor { + + static void renderRadioButtons(TestResult &result) + { + if (ImGui::RadioButton("Passed", result == TestResult::PASSED)) + result = TestResult::PASSED; + ImGui::SameLine(); + if (ImGui::RadioButton("Failed", result == TestResult::FAILED)) + result = TestResult::FAILED; + ImGui::SameLine(); + if (ImGui::RadioButton("Skipped", result == TestResult::SKIPPED)) + result = TestResult::SKIPPED; + } + + static void renderTestCases(TestSection §ion) + { + if (ImGui::BeginTable("TestCasesTable", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) + { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Result", ImGuiTableColumnFlags_WidthStretch); + + for (unsigned int i = 0; i < section.testCases.size(); ++i) { + auto &tc = section.testCases[i]; + ImGui::PushID(static_cast(i)); + + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextWrapped("%s", tc.name.c_str()); + + ImGui::TableSetColumnIndex(1); + renderRadioButtons(tc.result); + + // Skip reason (below the table row) + if (tc.result == TestResult::SKIPPED) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Indent(20.0f); + ImGui::InputTextWithHint( + "##skip_reason", + "Reason for skip...", + tc.skippedMessage, + sizeof(tc.skippedMessage) + ); + ImGui::Unindent(20.0f); + + ImGui::TableSetColumnIndex(1); + } + + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + + static void renderSubSections(std::vector &subSections) + { + for (unsigned int i = 0; i < subSections.size(); ++i) { + auto &sub = subSections[i]; + ImGui::PushID(static_cast(i)); + ImNexo::ToggleButtonWithSeparator(sub.name, &sub.sectionOpen); + if (sub.sectionOpen) + renderTestCases(sub); + ImGui::PopID(); + } + } + + void TestWindow::show() + { + if (!ImGui::Begin(NEXO_WND_USTRID_TEST, &m_opened, ImGuiWindowFlags_None)) { + ImGui::End(); + return; + } + beginRender(NEXO_WND_USTRID_TEST); + + for (unsigned int i = 0; i < m_testSections.size(); ++i) { + auto §ion = m_testSections[i]; + ImGui::PushID(static_cast(i)); + if (ImNexo::Header(std::format("##MainSection{}", i), section.name)) { + // Any test cases directly in this section + renderTestCases(section); + + // Now sub-sections + renderSubSections(section.subSections); + ImGui::TreePop(); + } + ImGui::PopID(); + } + + // Action buttons + ImGui::Separator(); + if (ImGui::Button("Cancel")) { + resetTestCases(); + m_opened = false; + } + ImGui::SameLine(); + if (ImGui::Button("Confirm")) { + writeTestReport(); + resetTestCases(); + m_opened = false; + } + + ImGui::End(); + } +} diff --git a/editor/src/DocumentWindows/TestWindow/Shutdown.cpp b/editor/src/DocumentWindows/TestWindow/Shutdown.cpp new file mode 100644 index 000000000..aa0ae1587 --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/Shutdown.cpp @@ -0,0 +1,190 @@ +//// Shutdown.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Source file for the shutdown logic of the test window +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Exception.hpp" +#include "TestWindow.hpp" +#include "Path.hpp" +#include "exceptions/Exceptions.hpp" + +#include + +#ifdef __linux__ + #include + #include +#endif + +#ifdef NX_GRAPHICS_API_OPENGL + #include +#endif + +namespace nexo::editor { + + void TestWindow::resetTestCases() + { + for (auto §ion : m_testSections) { + for (auto &tc : section.testCases) { + tc.result = TestResult::NOT_TESTED; + memset(&tc.skippedMessage, 0, sizeof(tc.skippedMessage)); + } + for (auto &sub : section.subSections) { + for (auto &tc : sub.testCases) { + tc.result = TestResult::NOT_TESTED; + memset(&tc.skippedMessage, 0, sizeof(tc.skippedMessage)); + } + } + } + } + + // Helper to get OS name + static std::string getOSName() + { +#if defined(_WIN32) + return "Windows"; +#elif defined(__APPLE__) + return "macOS"; +#elif defined(__linux__) + struct utsname info; + if (uname(&info) == 0) + return std::string(info.sysname) + " " + info.release; + else + return "Linux"; +#else + return "Unknown OS"; +#endif + } + + // Helper to get CPU info (model name) + static std::string getCPUInfo() + { +#if defined(__linux__) + std::ifstream cpuinfoFile("/proc/cpuinfo"); + std::string line; + while (std::getline(cpuinfoFile, line)) { + if (line.rfind("model name", 0) == 0) { + auto pos = line.find(':'); + if (pos != std::string::npos) { + std::string model = line.substr(pos + 1); + // trim leading spaces + model.erase( + model.begin(), + std::ranges::find_if(model, [](unsigned char ch) { return !std::isspace(ch); }) + ); + return model; + } + } + } + return "Unknown CPU"; +#elif defined(_WIN32) && defined(_M_X64) + // Using __cpuid to get CPU brand string + int cpuInfo[4] = {0}; + char brand[0x40] = { 0 }; + __cpuid(cpuInfo, 0x80000000); + unsigned int maxId = cpuInfo[0]; + if (maxId >= 0x80000004) { + __cpuid((int*)cpuInfo, 0x80000002); + memcpy(brand, cpuInfo, sizeof(cpuInfo)); + __cpuid((int*)cpuInfo, 0x80000003); + memcpy(brand + 16, cpuInfo, sizeof(cpuInfo)); + __cpuid((int*)cpuInfo, 0x80000004); + memcpy(brand + 32, cpuInfo, sizeof(cpuInfo)); + return std::string(brand); + } + return "Unknown CPU"; +#else + return std::to_string(std::thread::hardware_concurrency()) + " cores"; +#endif + } + + // Helper to get GPU / graphics backend info (OpenGL example) + static std::string getGraphicsInfo() + { +#ifdef NX_GRAPHICS_API_OPENGL + auto vendor = reinterpret_cast(glGetString(GL_VENDOR)); + auto renderer = reinterpret_cast(glGetString(GL_RENDERER)); + auto version = reinterpret_cast(glGetString(GL_VERSION)); + return std::string("OpenGL: ") + vendor + " - " + renderer + " (" + version + ")"; +#else + return "Graphics info not available"; +#endif + } + + // Write environment section at top of report + static void writeEnvironmentReport(std::ofstream &out) + { + out << std::format("# Environment\n"); + out << std::format("OS: {}\n", getOSName()); + out << std::format("CPU: {}\n", getCPUInfo()); + out << std::format("Graphics: {}\n", getGraphicsInfo()); + out << std::format("Timestamp: {:%Y-%m-%d %H:%M:%S}\n\n", std::chrono::system_clock::now()); + } + + static constexpr std::string testResultToString(const TestResult r) + { + switch (r) + { + case TestResult::PASSED: return "PASSED"; + case TestResult::FAILED: return "FAILED"; + case TestResult::SKIPPED: return "SKIPPED"; + default: return "NOT_TESTED"; + } + } + + static void writeTestCaseReport(std::ofstream& out, const TestCase& tc) + { + out << std::format("- {} : {}\n", tc.name, testResultToString(tc.result)); + if (tc.result == TestResult::SKIPPED) + out << std::format(" Reason: {}\n", tc.skippedMessage); + } + + static std::filesystem::path getTestReportFilePath() + { + auto now_tp = floor(std::chrono::system_clock::now()); + std::chrono::zoned_time local_zoned{std::chrono::current_zone(), now_tp}; + std::string ts = std::format("{:%Y%m%d}", local_zoned); + std::string filename = std::format("EditorTestResults_{}.report", ts); + + auto testDir = std::filesystem::path(Path::resolvePathRelativeToExe("../tests/editor")); + std::filesystem::create_directories(testDir); + auto filePath = testDir / filename; + return filePath; + } + + void TestWindow::writeTestReport() + { + const auto filePath = getTestReportFilePath(); + + std::ofstream out(filePath.string()); + if (!out) + THROW_EXCEPTION(FileWriteException, filePath.string(), std::strerror(errno)); + + writeEnvironmentReport(out); + + for (auto §ion : m_testSections) { + out << std::format("# {}\n", section.name); + for (const auto &tc : section.testCases) + writeTestCaseReport(out, tc); + for (const auto &sub : section.subSections) { + out << std::format("## {}\n", sub.name); + for (auto &tc : sub.testCases) + writeTestCaseReport(out, tc); + } + } + } + + void TestWindow::shutdown() + { + writeTestReport(); + } + +} diff --git a/editor/src/DocumentWindows/TestWindow/TestWindow.hpp b/editor/src/DocumentWindows/TestWindow/TestWindow.hpp new file mode 100644 index 000000000..2299a54c5 --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/TestWindow.hpp @@ -0,0 +1,59 @@ +//// TestWindow.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Header file for the test window +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "ADocumentWindow.hpp" + +#include +#include + +namespace nexo::editor { + + enum class TestResult { + NOT_TESTED, + PASSED, + FAILED, + SKIPPED + }; + + struct TestCase { + std::string name; + TestResult result = TestResult::NOT_TESTED; + char skippedMessage[512]; + }; + + struct TestSection { + std::string name; + std::vector testCases; + bool sectionOpen = false; + std::vector subSections; + }; + + class TestWindow final : public ADocumentWindow { + public: + using ADocumentWindow::ADocumentWindow; + + void setup() override; + void shutdown() override; + void show() override; + void update() override; + private: + std::vector m_testSections; + + void parseTestFolder(); + void parseFile(const std::filesystem::directory_entry &entry); + void resetTestCases(); + void writeTestReport(); + }; +} diff --git a/editor/src/DocumentWindows/TestWindow/Update.cpp b/editor/src/DocumentWindows/TestWindow/Update.cpp new file mode 100644 index 000000000..276bed1b7 --- /dev/null +++ b/editor/src/DocumentWindows/TestWindow/Update.cpp @@ -0,0 +1,24 @@ +//// Update.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 05/05/2025 +// Description: Source file for the test window update +// +/////////////////////////////////////////////////////////////////////////////// + +#include "TestWindow.hpp" + +namespace nexo::editor { + + void TestWindow::update() + { + // Nothing to do in the update + } + +} diff --git a/editor/src/Editor.cpp b/editor/src/Editor.cpp index 8062079fa..2cf25c800 100644 --- a/editor/src/Editor.cpp +++ b/editor/src/Editor.cpp @@ -12,7 +12,7 @@ // ////////////////////////////////////////////////////////////////////////////// -#define IMGUI_DEFINE_MATH_OPERATORS +#include "ADocumentWindow.hpp" #include "utils/Config.hpp" #include "Nexo.hpp" @@ -21,19 +21,26 @@ #include "Path.hpp" #include "backends/ImGuiBackend.hpp" #include "IconsFontAwesome.h" +#include "ImNexo/Elements.hpp" +#include "context/ActionManager.hpp" +#include "DocumentWindows/TestWindow/TestWindow.hpp" +#define IMGUI_DEFINE_MATH_OPERATORS #include "imgui.h" #include #include #include -#include -ImGuiID g_materialInspectorDockID = 0; +#include "DocumentWindows/EditorScene/EditorScene.hpp" +#include "DocumentWindows/InspectorWindow/InspectorWindow.hpp" namespace nexo::editor { void Editor::shutdown() const { + Application& app = Application::getInstance(); + + app.shutdownScripting(); LOG(NEXO_INFO, "Closing editor"); LOG(NEXO_INFO, "All windows destroyed"); m_windowRegistry.shutdown(); @@ -80,8 +87,8 @@ namespace nexo::editor { float scaleFactorX = 0.0f; float scaleFactorY = 0.0f; - nexo::getApp().getWindow()->getDpiScale(&scaleFactorX, &scaleFactorY); - nexo::getApp().getWindow()->setWindowIcon(Path::resolvePathRelativeToExe( + getApp().getWindow()->getDpiScale(&scaleFactorX, &scaleFactorY); + getApp().getWindow()->setWindowIcon(Path::resolvePathRelativeToExe( "../resources/nexo.png")); if (scaleFactorX > 1.0f || scaleFactorY > 1.0f) { @@ -93,8 +100,8 @@ namespace nexo::editor { LOG(NEXO_INFO, "ImGui version: {}", IMGUI_VERSION); ImGuiIO &io = ImGui::GetIO(); - io.DisplaySize = ImVec2(static_cast(nexo::getApp().getWindow()->getWidth()), - static_cast(nexo::getApp().getWindow()->getHeight())); + io.DisplaySize = ImVec2(static_cast(getApp().getWindow()->getWidth()), + static_cast(getApp().getWindow()->getHeight())); io.DisplayFramebufferScale = ImVec2(scaleFactorX, scaleFactorY); // Apply the DPI scale to ImGui rendering io.ConfigWindowsMoveFromTitleBarOnly = true; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; @@ -110,19 +117,19 @@ namespace nexo::editor { // Setup NEXO Color Scheme ImVec4* colors = style->Colors; - const ImVec4 colWindowBg = ImVec4(0.02f, 0.02f, 0.04f, 0.59f); // Every color above it will depend on it because of the alpha - const ImVec4 colTitleBg = ImVec4(0.00f, 0.00f, 0.00f, 0.28f); - const ImVec4 colTitleBgActive = ImVec4(0.00f, 0.00f, 0.00f, 0.31f); - const ImVec4 colTabSelectedOverline = ImVec4(0.30f, 0.12f, 0.45f, 0.85f); - const ImVec4 colTabDimmedSelectedOverline = ImVec4(0.29f, 0.12f, 0.43f, 0.15f); + constexpr auto colWindowBg = ImVec4(0.02f, 0.02f, 0.04f, 0.59f); // Every color above it will depend on it because of the alpha + constexpr auto colTitleBg = ImVec4(0.00f, 0.00f, 0.00f, 0.28f); + constexpr auto colTitleBgActive = ImVec4(0.00f, 0.00f, 0.00f, 0.31f); + constexpr auto colTabSelectedOverline = ImVec4(0.30f, 0.12f, 0.45f, 0.85f); + constexpr auto colTabDimmedSelectedOverline = ImVec4(0.29f, 0.12f, 0.43f, 0.15f); // Dependent colors // We want the tabs to have the same color as colWindowBg, but titleBg is under tabs, so we subtract titleBg - const ImVec4 colTab = ImVec4(0, 0, 0, (colWindowBg.w - colTitleBgActive.w) * 0.60f); - const ImVec4 colTabDimmed = ImVec4(0, 0, 0, colTab.w * 0.90f); - const ImVec4 colTabSelected = ImVec4(0, 0, 0, colWindowBg.w - colTitleBg.w); - const ImVec4 colTabDimmedSelected = ImVec4(0, 0, 0, colTabSelected.w); - const ImVec4 colTabHovered = ImVec4(0.33f, 0.25f, 0.40f, colWindowBg.w - colTitleBg.w); + constexpr auto colTab = ImVec4(0, 0, 0, (colWindowBg.w - colTitleBgActive.w) * 0.60f); + constexpr auto colTabDimmed = ImVec4(0, 0, 0, colTab.w * 0.90f); + constexpr auto colTabSelected = ImVec4(0, 0, 0, colWindowBg.w - colTitleBg.w); + constexpr auto colTabDimmedSelected = ImVec4(0, 0, 0, colTabSelected.w); + constexpr auto colTabHovered = ImVec4(0.33f, 0.25f, 0.40f, colWindowBg.w - colTitleBg.w); // Depending definitions colors[ImGuiCol_WindowBg] = colWindowBg; @@ -138,7 +145,7 @@ namespace nexo::editor { colors[ImGuiCol_TabHovered] = colTabHovered; // Static definitions - ImVec4 whiteText = colors[ImGuiCol_Text]; + const ImVec4 whiteText = colors[ImGuiCol_Text]; colors[ImGuiCol_Border] = ImVec4(0.08f, 0.08f, 0.25f, 0.19f); colors[ImGuiCol_TableRowBg] = ImVec4(0.49f, 0.63f, 0.71f, 0.15f); @@ -158,6 +165,7 @@ namespace nexo::editor { colors[ImGuiCol_Button] = ImVec4(0.49f, 0.63f, 0.71f, 0.15f); colors[ImGuiCol_ButtonHovered] = ImVec4(0.49f, 0.63f, 0.71f, 0.30f); colors[ImGuiCol_ButtonActive] = ImVec4(0.49f, 0.63f, 0.71f, 0.45f); + colors[ImGuiCol_PopupBg] = ImVec4(0.05f * 1.5f, 0.09f * 1.15f, 0.13f * 1.25, 1.0f); // Optionally, you might want to adjust the text color if needed: setupFonts(scaleFactorX, scaleFactorY); @@ -218,11 +226,18 @@ namespace nexo::editor { setupEngine(); setupStyle(); m_windowRegistry.setup(); + + const Application& app = Application::getInstance(); + app.initScripting(); // TODO: scripting is init here since it requires a scene, later scenes shouldn't be created in the editor window +#ifdef NEXO_SCRIPTING_ENABLED + for (const auto inspectorWindow : m_windowRegistry.getWindows()) + inspectorWindow->registerTypeErasedProperties(); // TODO: this should be done in the InspectorWindow constructor, but we need the scripting to init +#endif } bool Editor::isOpen() const { - return !m_quit && nexo::getApp().isWindowOpen() && nexo::getApp().isRunning(); + return !m_quit && getApp().isWindowOpen() && getApp().isRunning(); } void Editor::drawMenuBar() @@ -247,8 +262,7 @@ namespace nexo::editor { static bool dockingRegistryFilled = false; // If the dockspace node doesn't exist yet, create it - if (!ImGui::DockBuilderGetNode(dockspaceID)) - { + if (!ImGui::DockBuilderGetNode(dockspaceID)) { ImGui::DockBuilderRemoveNode(dockspaceID); ImGui::DockSpaceOverViewport(viewport->ID); ImGui::DockBuilderAddNode(dockspaceID, ImGuiDockNodeFlags_None); @@ -286,14 +300,15 @@ namespace nexo::editor { // ───────────────────────────────────────────── // Dock the windows into their corresponding nodes. - ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_DEFAULT_SCENE, mainSceneTop); + const std::string defaultSceneUniqueStrId = std::format("{}{}", NEXO_WND_USTRID_DEFAULT_SCENE, 0); // for the default scene + ImGui::DockBuilderDockWindow(defaultSceneUniqueStrId.c_str(), mainSceneTop); ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_CONSOLE, consoleNode); ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_SCENE_TREE, sceneTreeNode); ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_INSPECTOR, inspectorNode); ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_MATERIAL_INSPECTOR, materialInspectorNode); ImGui::DockBuilderDockWindow(NEXO_WND_USTRID_ASSET_MANAGER, consoleNode); - m_windowRegistry.setDockId(NEXO_WND_USTRID_DEFAULT_SCENE, mainSceneTop); + m_windowRegistry.setDockId(defaultSceneUniqueStrId, mainSceneTop); m_windowRegistry.setDockId(NEXO_WND_USTRID_CONSOLE, consoleNode); m_windowRegistry.setDockId(NEXO_WND_USTRID_SCENE_TREE, sceneTreeNode); m_windowRegistry.setDockId(NEXO_WND_USTRID_INSPECTOR, inspectorNode); @@ -301,19 +316,11 @@ namespace nexo::editor { m_windowRegistry.setDockId(NEXO_WND_USTRID_ASSET_MANAGER, consoleNode); dockingRegistryFilled = true; - g_materialInspectorDockID = materialInspectorNode; - // Finish building the dock layout. ImGui::DockBuilderFinish(dockspaceID); } - else if (!dockingRegistryFilled) - { - m_windowRegistry.setDockId(NEXO_WND_USTRID_DEFAULT_SCENE, findWindowDockIDFromConfig(NEXO_WND_USTRID_DEFAULT_SCENE)); - m_windowRegistry.setDockId(NEXO_WND_USTRID_CONSOLE, findWindowDockIDFromConfig(NEXO_WND_USTRID_CONSOLE)); - m_windowRegistry.setDockId(NEXO_WND_USTRID_SCENE_TREE, findWindowDockIDFromConfig(NEXO_WND_USTRID_SCENE_TREE)); - m_windowRegistry.setDockId(NEXO_WND_USTRID_INSPECTOR, findWindowDockIDFromConfig(NEXO_WND_USTRID_INSPECTOR)); - m_windowRegistry.setDockId(NEXO_WND_USTRID_MATERIAL_INSPECTOR, findWindowDockIDFromConfig(NEXO_WND_USTRID_MATERIAL_INSPECTOR)); - m_windowRegistry.setDockId(NEXO_WND_USTRID_ASSET_MANAGER, findWindowDockIDFromConfig(NEXO_WND_USTRID_ASSET_MANAGER)); + else if (!dockingRegistryFilled) { + setAllWindowDockIDsFromConfig(m_windowRegistry); dockingRegistryFilled = true; } @@ -321,24 +328,176 @@ namespace nexo::editor { ImGui::DockSpaceOverViewport(viewport->ID); } - void Editor::render() + void Editor::handleGlobalCommands() { - getApp().beginFrame(); + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) + { + if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) + { + ActionManager::get().redo(); + } + else + { + ActionManager::get().undo(); + } + } + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyDown(ImGuiKey_LeftShift) && ImGui::IsKeyPressed(ImGuiKey_T)) + { + if (auto testWindow = getWindow(NEXO_WND_USTRID_TEST).lock()) { + testWindow->setOpened(true); + } else { + registerWindow(NEXO_WND_USTRID_TEST); + getWindow(NEXO_WND_USTRID_TEST).lock()->setup(); + } + } + } - ImGuiBackend::begin(); + std::vector Editor::handleFocusedWindowCommands() + { + std::vector possibleCommands; + static std::vector lastValidCommands; // Store the last valid set of commands + static float commandDisplayTimer = 0.0f; // Track how long to show last commands - ImGuizmo::SetImGuiContext(ImGui::GetCurrentContext()); - ImGuizmo::BeginFrame(); - buildDockspace(); + auto focusedWindow = m_windowRegistry.getFocusedWindow(); + if (focusedWindow) + { + const WindowState currentState = m_windowRegistry.getFocusedWindow()->getWindowState(); + m_inputManager.processInputs(currentState); + possibleCommands = m_inputManager.getPossibleCommands(currentState); - drawMenuBar(); - ImGui::ShowDemoWindow(); + // Update the last valid commands if we have any + if (!possibleCommands.empty()) + { + constexpr float commandPersistTime = 2.0f; + lastValidCommands = possibleCommands; + commandDisplayTimer = commandPersistTime; // Reset timer + } + else if (commandDisplayTimer > 0.0f) + { + // Use the last valid commands if timer is still active + possibleCommands = lastValidCommands; + commandDisplayTimer -= ImGui::GetIO().DeltaTime; + } + else if (lastValidCommands.empty()) + { + // Fallback: If we've never had commands, grab all possible commands from the window + // This is a more complex operation but ensures we always have something to show + possibleCommands = m_inputManager.getAllPossibleCommands(currentState); + lastValidCommands = possibleCommands; + } + else + { + // Use the last valid set of commands + possibleCommands = lastValidCommands; + } + } + return possibleCommands; + } + void Editor::drawShortcutBar(const std::vector &possibleCommands) const + { + constexpr float bottomBarHeight = 38.0f; + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.06f, 0.12f, 0.85f)); // Matches your dark blue theme + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.0f)); + const auto viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + viewport->Size.y - bottomBarHeight)); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, bottomBarHeight)); + ImGui::SetNextWindowViewport(viewport->ID); - m_windowRegistry.render(); + ImGuiWindowFlags bottomBarFlags = + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBackground; + + if (ImGui::Begin(NEXO_WND_USTRID_BOTTOM_BAR, nullptr, bottomBarFlags)) + { + constexpr float textScaleFactor = 0.90f; // 15% larger text + ImGui::SetWindowFontScale(textScaleFactor); + // Vertically center the content + const float windowHeight = ImGui::GetWindowHeight(); + const float textHeight = ImGui::GetTextLineHeight(); + const float paddingY = (windowHeight - textHeight) * 0.5f; + + // Apply the vertical padding + ImGui::SetCursorPosY(paddingY); - auto viewport = ImGui::GetMainViewport(); + // Start with a small horizontal padding + ImGui::SetCursorPosX(10.0f); + + if (!possibleCommands.empty()) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + // Use horizontal layout for commands, left-aligned + for (const auto& cmd : possibleCommands) + { + // Calculate text sizes for proper positioning and border sizing + ImVec2 keySize = ImGui::CalcTextSize(cmd.key.c_str()); + ImVec2 colonSize = ImGui::CalcTextSize(":"); + ImVec2 descSize = ImGui::CalcTextSize(cmd.description.c_str()); + + // Position of the start of this command + const ImVec2 commandStart = ImGui::GetCursorScreenPos(); + + // Total size of command group with padding + const float commandWidth = keySize.x + colonSize.x + 5.0f + descSize.x; + const float commandHeight = std::max(keySize.y, std::max(colonSize.y, descSize.y)); + + // Add padding around the entire command + constexpr float borderPadding = 6.0f; + constexpr float borderCornerRadius = 3.0f; + + // Draw the gradient border rectangle + const auto rectMin = ImVec2(commandStart.x - borderPadding, commandStart.y - borderPadding); + const auto rectMax = ImVec2(commandStart.x + commandWidth + borderPadding, + commandStart.y + commandHeight + borderPadding); + + // Draw gradient border rectangle + drawList->AddRect( + rectMin, + rectMax, + IM_COL32(58, 124, 161, 200), // Gradient start color + borderCornerRadius, + 0, + 1.5f // Border thickness + ); + + // Dark inner background + drawList->AddRectFilled( + ImVec2(rectMin.x + 1, rectMin.y + 1), + ImVec2(rectMax.x - 1, rectMax.y - 1), + IM_COL32(10, 11, 25, 200), // Dark inner background + borderCornerRadius - 0.5f + ); + + // Draw the command components + const std::string &key = cmd.key + ":"; + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%s", key.c_str()); + ImGui::SameLine(0.0f, 5.0f); + ImGui::Text("%s", cmd.description.c_str()); + + // Add space between commands + ImGui::SameLine(0.0f, 20.0f); + + // Update cursor position to account for the border we added + ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, commandStart.y)); + } + } + } + ImGui::End(); + ImGui::PopStyleColor(2); // Pop both text and bg colors + } + + void Editor::drawBackground() const + { + const auto viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->Pos); ImGui::SetNextWindowSize(viewport->Size); ImGui::SetNextWindowViewport(viewport->ID); @@ -353,19 +512,40 @@ namespace nexo::editor { ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground); - const std::vector stops = { + const std::vector stops = { { 0.06f, IM_COL32(58, 124, 161, 255) }, {0.26f, IM_COL32(88, 87, 154, 255) }, { 0.50f, IM_COL32(88, 87, 154, 255) }, {0.73f, IM_COL32(58, 124, 161, 255) }, }; - float angle = 148; + constexpr float angle = 148; - Components::drawRectFilledLinearGradient(viewport->Pos, + ImNexo::RectFilledLinearGradient(viewport->Pos, ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y), angle, stops); ImGui::End(); + } + + void Editor::render() + { + getApp().beginFrame(); + + ImGuiBackend::begin(); + + ImGuizmo::SetImGuiContext(ImGui::GetCurrentContext()); + ImGuizmo::BeginFrame(); + buildDockspace(); + + drawMenuBar(); + m_windowRegistry.render(); + + handleGlobalCommands(); + + // Get the commands to display in the bottom bar + const std::vector possibleCommands = handleFocusedWindowCommands(); + drawShortcutBar(possibleCommands); + drawBackground(); ImGui::Render(); @@ -375,6 +555,7 @@ namespace nexo::editor { void Editor::update() const { m_windowRegistry.update(); + Application& app = Application::getInstance(); getApp().endFrame(); } } diff --git a/editor/src/Editor.hpp b/editor/src/Editor.hpp index 69e7901f4..a56be2a04 100644 --- a/editor/src/Editor.hpp +++ b/editor/src/Editor.hpp @@ -14,25 +14,16 @@ #pragma once -#include "ADocumentWindow.hpp" -#include "Exception.hpp" #include "IDocumentWindow.hpp" -#include "exceptions/Exceptions.hpp" #define L_DEBUG 1 -#include #include -#include #include "WindowRegistry.hpp" +#include "inputs/InputManager.hpp" namespace nexo::editor { class Editor { - private: - // Singleton: private constructor and destructor - Editor() = default; - ~Editor() = default; - public: // Singleton: Meyers' Singleton Pattern static Editor& getInstance() @@ -119,6 +110,9 @@ namespace nexo::editor { return m_windowRegistry.getWindow(windowName); } private: + // Singleton: private constructor and destructor + Editor() = default; + ~Editor() = default; /** * @brief Initializes the core engine and configures ImGui components. @@ -163,9 +157,15 @@ namespace nexo::editor { * sets a flag to signal that the editor should quit. */ void drawMenuBar(); + void drawShortcutBar(const std::vector &possibleCommands) const; + void drawBackground() const; + + void handleGlobalCommands(); + std::vector handleFocusedWindowCommands(); bool m_quit = false; bool m_showDemoWindow = false; WindowRegistry m_windowRegistry; + InputManager m_inputManager; }; } diff --git a/editor/src/IDocumentWindow.hpp b/editor/src/IDocumentWindow.hpp index 86cee11cd..e88dcec35 100644 --- a/editor/src/IDocumentWindow.hpp +++ b/editor/src/IDocumentWindow.hpp @@ -16,6 +16,8 @@ #include +#include "inputs/WindowState.hpp" + namespace nexo::editor { using WindowId = unsigned int; @@ -31,8 +33,11 @@ namespace nexo::editor { [[nodiscard]] virtual bool isFocused() const = 0; [[nodiscard]] virtual bool isOpened() const = 0; + virtual void setOpened(bool opened) = 0; [[nodiscard]] virtual bool isHovered() const = 0; + [[nodiscard]] virtual const ImVec2 &getContentSize() const = 0; [[nodiscard]] virtual bool &getOpened() = 0; [[nodiscard]] virtual const std::string &getWindowName() const = 0; + [[nodiscard]] virtual const WindowState &getWindowState() const = 0; }; } diff --git a/editor/src/ImNexo/Components.cpp b/editor/src/ImNexo/Components.cpp new file mode 100644 index 000000000..76e59597c --- /dev/null +++ b/editor/src/ImNexo/Components.cpp @@ -0,0 +1,522 @@ +//// Components.cpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 17/02/2025 +// Description: Source file for the utilitary ImGui functions +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Components.hpp" +#include "Elements.hpp" +#include "Guard.hpp" +#include "Utils.hpp" +#include "tinyfiledialogs.h" +#include "ImNexo.hpp" + +#include +#include + +namespace ImNexo { + + bool ButtonWithIconAndText( + const std::string &uniqueId, + const std::string &icon, + const std::string &label, + const ImVec2 &itemSize + ) + { + IdGuard idGuard(uniqueId); + std::string invisButtonLabel = "##" + uniqueId; + + if (ImGui::InvisibleButton(invisButtonLabel.c_str(), itemSize)) + return true; + + // Draw the background + auto [p0, p1] = utils::getItemRect(); + ImGui::GetWindowDrawList()->AddRectFilled( + p0, p1, + ImGui::GetColorU32(ImGui::IsItemHovered() ? ImGuiCol_ButtonHovered : ImGuiCol_Button), + ImGui::GetStyle().FrameRounding + ); + + // Draw the icon at 25% from top + CenteredIcon( + icon, + p0, p1, + ImGui::GetColorU32(ImGuiCol_Text), + 1.5f, + 0.25f, + 0.5f + ); + + // Draw the label with wrapping if needed + WrappedCenteredText( + label, + p0, p1, + ImGui::GetColorU32(ImGuiCol_Text), + 0.6f // Position at 60% from top + ); + + // Use drawButtonBorder instead of direct drawing + ButtonBorder( + 0, // Use default color + ImGui::GetColorU32(ImGuiCol_ButtonHovered), + ImGui::GetColorU32(ImGuiCol_ButtonActive), + ImGui::GetStyle().FrameRounding + ); + + return false; + } + + void ColorButton(const std::string &label, const ImVec2 size, const ImVec4 color, bool *clicked, ImGuiColorEditFlags flags) + { + flags |= ImGuiColorEditFlags_NoTooltip; + constexpr float borderThickness = 3.0f; + const float defaultSize = ImGui::GetFrameHeight() + borderThickness; + const auto calculatedSize = ImVec2( + size.x == 0 ? defaultSize : size.x - borderThickness * 2, + size.y == 0 ? defaultSize : size.y - borderThickness * 2 + ); + + if (ImGui::ColorButton(label.c_str(), color, flags, calculatedSize) && clicked) + { + *clicked = !*clicked; + } + + ButtonBorder( + ImGui::GetColorU32(ImGuiCol_Button), + ImGui::GetColorU32(ImGuiCol_ButtonHovered), + ImGui::GetColorU32(ImGuiCol_ButtonActive), + borderThickness + ); + } + + + bool TextureButton(const std::string &label, const std::shared_ptr& texture, std::filesystem::path& outPath) + { + bool modified = false; + constexpr ImVec2 previewSize(32, 32); + ImGui::PushID(label.c_str()); + + const ImTextureID textureId = texture ? static_cast(static_cast(texture->getId())) : 0; + const std::string textureButton = std::string("##TextureButton") + label; + + if (ImageButton(textureButton.c_str(), textureId, previewSize)) { + const char* filePath = tinyfd_openFileDialog( + "Open Texture", + "", + 0, + nullptr, + nullptr, + 0 + ); + + if (filePath) { + outPath = filePath; + modified = true; + } + } + ButtonBorder(IM_COL32(255,255,255,0), IM_COL32(255,255,255,255), IM_COL32(255,255,255,0), 0.0f, 0, 2.0f); + ImGui::PopID(); + ImGui::SameLine(); + ImGui::Text("%s", label.c_str()); + return modified; + } + + bool IconGradientButton( + const std::string& uniqueId, + const std::string& icon, + const ImVec2& size, + const std::vector& gradientStops, + const float gradientAngle, + const ImU32 borderColor, + const ImU32 borderColorHovered, + const ImU32 borderColorActive, + const ImU32 iconColor + ) + { + IdGuard idGuard(uniqueId); + + // Create invisible button for interaction + const bool clicked = ImGui::InvisibleButton(("##" + uniqueId).c_str(), size); + + // Get button rectangle coordinates + auto [p_min, p_max] = utils::getItemRect(); + + // Draw the gradient background + ImDrawList* drawList = ImGui::GetWindowDrawList(); + RectFilledLinearGradient(p_min, p_max, gradientAngle, gradientStops, drawList); + + // Draw the icon centered using our helper function + CenteredIcon(icon, p_min, p_max, iconColor); + + // Use our drawButtonBorder helper instead of direct drawing + ButtonBorder( + borderColor, + borderColorHovered, + borderColorActive, + 3.0f, // rounding + 0, // no flags + 1.5f // thickness + ); + + return clicked; + } + + bool RowEntityDropdown( + const std::string &label, + nexo::ecs::Entity &targetEntity, + const std::vector& entities, + const std::function& getNameFunc + ) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label.c_str()); + + ImGui::TableNextColumn(); + IdGuard idGuard(label); + + bool changed = false; + + // Build entity-name mapping + static std::vector> entityNamePairs; + static nexo::ecs::Entity lastTargetEntity = 0; + static std::vector lastEntities; + + // Only rebuild the mapping if entities list changed or target entity changed + bool needRebuild = lastTargetEntity != targetEntity || lastEntities.size() != entities.size(); + + if (!needRebuild) { + for (size_t i = 0; i < entities.size() && !needRebuild; i++) { + needRebuild = lastEntities[i] != entities[i]; + } + } + + if (needRebuild) { + entityNamePairs.clear(); + entityNamePairs.reserve(entities.size()); + lastEntities = entities; + lastTargetEntity = targetEntity; + + for (nexo::ecs::Entity entity : entities) { + std::string name = getNameFunc(entity); + entityNamePairs.emplace_back(entity, name); + } + } + + // Find current index + int currentIndex = -1; + for (size_t i = 0; i < entityNamePairs.size(); i++) { + if (entityNamePairs[i].first == targetEntity) { + currentIndex = static_cast(i); + break; + } + } + + // Add a "None" option if we want to allow null selection + const std::string currentItemName = currentIndex >= 0 ? entityNamePairs[currentIndex].second : "None"; + + // Draw the combo box + ImGui::SetNextItemWidth(-FLT_MIN); // Use all available width + if (ImGui::BeginCombo("##entity_dropdown", currentItemName.c_str())) + { + // Optional: Add a "None" option for clearing the target + if (ImGui::Selectable("None", targetEntity == nexo::ecs::MAX_ENTITIES)) { + targetEntity = nexo::ecs::MAX_ENTITIES; + changed = true; + } + + for (size_t i = 0; i < entityNamePairs.size(); i++) + { + const bool isSelected = (currentIndex == static_cast(i)); + if (ImGui::Selectable(entityNamePairs[i].second.c_str(), isSelected)) + { + targetEntity = entityNamePairs[i].first; + changed = true; + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemActive()) + itemIsActive(); + if (ImGui::IsItemActivated()) + itemIsActivated(); + if (ImGui::IsItemDeactivated()) + itemIsDeactivated(); + + return changed; + } + + bool RowDragFloat(const Channels &channels) + { + bool modified = false; + + for (unsigned int i = 0; i < channels.count; ++i) + { + ImGui::TableNextColumn(); + + // Draw the badge (if provided) + if (!channels.badges[i].label.empty()) + { + const auto &badge = channels.badges[i]; + Button(badge.label, badge.size, badge.bg, badge.bgHovered, + badge.bgActive, badge.txtColor); + ImGui::SameLine(0, 2); + } + + // Draw the drag float control + const auto &slider = channels.sliders[i]; + const bool changed = DragFloat( + slider.label, + slider.value, + slider.speed, + slider.min, + slider.max, + slider.format, + slider.bg, + slider.bgHovered, + slider.bgActive, + slider.textColor); + + modified |= changed; + if (ImGui::IsItemActive()) + itemIsActive(); + if (ImGui::IsItemActivated()) + itemIsActivated(); + if (ImGui::IsItemDeactivated()) + itemIsDeactivated(); + } + + return modified; + } + + bool RowDragFloat1( + const char *uniqueLabel, + const std::string &badgeLabel, + float *value, + float minValue, + float maxValue, + float speed + ) + { + ImGui::TableNextRow(); + + ChannelLabel chanLabel; + chanLabel.label = uniqueLabel; + RowLabel(chanLabel); + + // Create channels structure for a single value + const std::string labelId = "##X" + std::string(uniqueLabel); + const std::string badgeId = badgeLabel.empty() ? "" : badgeLabel + "##" + uniqueLabel; + + // Setup single badge and control + Channels channels; + channels.count = 1; + channels.badges.push_back({ + badgeId, + {0, 0}, + IM_COL32(80, 0, 0, 255), + IM_COL32(80, 0, 0, 255), + IM_COL32(80, 0, 0, 255), + IM_COL32(255, 180, 180, 255) + }); + + channels.sliders.emplace_back(labelId, + value, + speed, + minValue, + maxValue, + 0, 0, 0, 0, + "%.2f"); + + return RowDragFloat(channels); + } + + bool RowDragFloat2( + const char *uniqueLabel, + const std::string &badLabelX, + const std::string &badLabelY, + float *values, + float minValue, + float maxValue, + float speed, + std::vector badgeColor, + std::vector textBadgeColor, + const bool disabled + ) + { + ImGui::TableNextRow(); + + ChannelLabel chanLabel; + chanLabel.label = uniqueLabel; + RowLabel(chanLabel); + + // Setup badge colors with defaults if not provided + if (badgeColor.size() < 2) { + badgeColor = {IM_COL32(102, 28, 28, 255), IM_COL32(0, 80, 0, 255)}; + } + + if (textBadgeColor.size() < 2) { + textBadgeColor = {IM_COL32(255, 180, 180, 255), IM_COL32(180, 255, 180, 255)}; + } + + // Base ID for controls + const std::string baseId = uniqueLabel; + + // Set up channels structure + Channels channels; + channels.count = 2; + + // Badge labels + channels.badges = { + {badLabelX + "##" + baseId, {0, 0}, badgeColor[0], badgeColor[0], badgeColor[0], textBadgeColor[0]}, + {badLabelY + "##" + baseId, {0, 0}, badgeColor[1], badgeColor[1], badgeColor[1], textBadgeColor[1]} + }; + + // Slider colors + ImU32 textColor = disabled ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : ImGui::GetColorU32(ImGuiCol_Text); + ImU32 bgColor = ImGui::GetColorU32(ImGuiCol_FrameBg); + ImU32 bgHoveredColor = ImGui::GetColorU32(ImGuiCol_FrameBgHovered); + ImU32 bgActiveColor = ImGui::GetColorU32(ImGuiCol_FrameBgActive); + + // Slider controls + channels.sliders = { + {"##X" + baseId, &values[0], speed, minValue, maxValue, bgColor, bgHoveredColor, bgActiveColor, textColor, "%.2f"}, + {"##Y" + baseId, &values[1], speed, minValue, maxValue, bgColor, bgHoveredColor, bgActiveColor, textColor, "%.2f"} + }; + + return RowDragFloat(channels); + } + + // Creates standard badge colors for X/Y/Z axes if not provided + static void setupAxisBadgeColors(std::vector& badgeColors, std::vector& textBadgeColors) + { + if (badgeColors.empty()) { + badgeColors = { + IM_COL32(102, 28, 28, 255), // X - Red + IM_COL32(0, 80, 0, 255), // Y - Green + IM_COL32(38, 49, 121, 255) // Z - Blue + }; + } + + if (textBadgeColors.empty()) { + textBadgeColors = { + IM_COL32(255, 180, 180, 255), // X - Light Red + IM_COL32(180, 255, 180, 255), // Y - Light Green + IM_COL32(180, 180, 255, 255) // Z - Light Blue + }; + } + } + + bool RowDragFloat3( + const char *uniqueLabel, + const std::string &badLabelX, + const std::string &badLabelY, + const std::string &badLabelZ, + float *values, + float minValue, + float maxValue, + float speed, + std::vector badgeColors, + std::vector textBadgeColors + ) + { + ImGui::TableNextRow(); + + // Setup the label for the row + ChannelLabel chanLabel; + chanLabel.label = uniqueLabel; + + // Setup standard axis colors if not provided + setupAxisBadgeColors(badgeColors, textBadgeColors); + + // Base ID for controls + std::string baseId = uniqueLabel; + float badgeSize = ImGui::GetFrameHeight(); + + // Set up channels structure + Channels channels; + channels.count = 3; + + // Badge labels + channels.badges = { + {badLabelX + "##" + baseId, {badgeSize, badgeSize}, badgeColors[0], badgeColors[0], badgeColors[0], textBadgeColors[0]}, + {badLabelY + "##" + baseId, {badgeSize, badgeSize}, badgeColors[1], badgeColors[1], badgeColors[1], textBadgeColors[1]}, + {badLabelZ + "##" + baseId, {badgeSize, badgeSize}, badgeColors[2], badgeColors[2], badgeColors[2], textBadgeColors[2]} + }; + + ImU32 textColor = ImGui::GetColorU32(ImGuiCol_Text); + + // Slider controls + channels.sliders = { + {"##X" + baseId, &values[0], speed, minValue, maxValue, 0, 0, 0, textColor, "%.2f"}, + {"##Y" + baseId, &values[1], speed, minValue, maxValue, 0, 0, 0, textColor, "%.2f"}, + {"##Z" + baseId, &values[2], speed, minValue, maxValue, 0, 0, 0, textColor, "%.2f"} + }; + + if (!chanLabel.label.empty()) + RowLabel(chanLabel); + + return RowDragFloat(channels); + } + + bool ToggleButtonWithSeparator(const std::string &label, bool* toggled) + { + IdGuard idGuard(label); + bool clicked = false; + + // Create the toggle button + constexpr ImVec2 buttonSize(24, 24); + if (ImGui::InvisibleButton("##arrow", buttonSize)) + { + clicked = true; + *toggled = !(*toggled); + } + + // Get button bounds and draw the arrow + auto [p_min, p_max] = utils::getItemRect(); + const ImVec2 center((p_min.x + p_max.x) * 0.5f, (p_min.y + p_max.y) * 0.5f); + + constexpr float arrowSize = 5.0f; + const ImU32 arrowColor = ImGui::GetColorU32(ImGuiCol_TextTab); + Arrow(center, *toggled, arrowColor, arrowSize); + + ImGui::SameLine(); + + // Draw separator line + const ImVec2 separatorPos = ImGui::GetCursorScreenPos(); + constexpr float separatorHeight = 24.0f; // match button height + ImGui::GetWindowDrawList()->AddLine( + separatorPos, + ImVec2(separatorPos.x, separatorPos.y + separatorHeight), + ImGui::GetColorU32(ImGuiCol_Separator), + 1.0f + ); + + ImGui::Dummy(ImVec2(4, buttonSize.y)); + ImGui::SameLine(); + + // Use the existing custom separator text component + CustomSeparatorText( + label, + 10.0f, + 0.1f, + 0.5f, + IM_COL32(255, 255, 255, 255), + IM_COL32(255, 255, 255, 255) + ); + + return clicked; + } +} diff --git a/editor/src/ImNexo/Components.hpp b/editor/src/ImNexo/Components.hpp new file mode 100644 index 000000000..7a1037d04 --- /dev/null +++ b/editor/src/ImNexo/Components.hpp @@ -0,0 +1,212 @@ +//// Components.hpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 17/02/2025 +// Description: Header file for the utilitary ImGui functions +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include +#include +#include + +#include "ecs/Coordinator.hpp" +#include "renderer/Texture.hpp" +#include "Elements.hpp" + +namespace ImNexo { + + /** + * @brief Creates a button with both an icon and text label. + * + * Renders a custom button with an icon positioned at the top portion of the button + * and a text label below it. The text automatically wraps if it's too wide for the button. + * + * @param uniqueId A unique identifier string for ImGui to track this widget + * @param icon The icon string to display (typically a FontAwesome character) + * @param label The text label to display below the icon + * @param itemSize The size dimensions of the button + * @return true if the button was clicked, false otherwise + */ + bool ButtonWithIconAndText(const std::string &uniqueId, const std::string &icon, const std::string &label, const ImVec2 &itemSize); + + /** + * @brief Draws a color button with a border. + * + * Displays a color button with the provided label and size. Optionally toggles a clicked state. + * + * @param label The label for the color button. + * @param size The size of the button. + * @param color The color to display. + * @param clicked Optional pointer to a boolean that is toggled when the button is clicked. + * @param flags Additional color edit flags. + */ + void ColorButton(const std::string &label, ImVec2 size, ImVec4 color, bool *clicked = nullptr, ImGuiColorEditFlags flags = ImGuiColorEditFlags_None); + + /** + * @brief Draws a texture button that displays a texture preview. + * + * When pressed, opens a file dialog to select a new texture. If a path was + * selected, it is set in `outPath` and the function returns true. + * + * @param[in] label A unique label identifier for the button. + * @param[in] texture A shared pointer to the renderer::NxTexture2D that holds the texture. + * @param[out] outPath The path to the selected texture. + * @return true if a texture path was set; false otherwise. + */ + bool TextureButton(const std::string &label, const std::shared_ptr& texture, std::filesystem::path& outPath); + + /** + * @brief Creates a customizable gradient button with a centered icon. + * + * Renders a button with a linear gradient background, customizable border colors + * for different states (normal, hovered, active), and a centered icon. + * + * @param uniqueId A unique identifier string for ImGui to track this widget + * @param icon The icon string to display (typically a FontAwesome character) + * @param size Button dimensions (width, height) + * @param gradientStops Array of gradient color stops defining the background gradient + * @param gradientAngle Angle of the linear gradient in degrees + * @param borderColor Color of the button border in normal state + * @param borderColorHovered Color of the button border when hovered + * @param borderColorActive Color of the button border when active/pressed + * @param iconColor Color of the icon + * @return true if the button was clicked, false otherwise + */ + bool IconGradientButton(const std::string& uniqueId, const std::string& icon, + const ImVec2& size = ImVec2(40, 40), + const std::vector& gradientStops = { + {0.0f, IM_COL32(60, 60, 80, 255)}, + {1.0f, IM_COL32(30, 30, 40, 255)} + }, + float gradientAngle = 45.0f, + ImU32 borderColor = IM_COL32(100, 100, 120, 255), + ImU32 borderColorHovered = IM_COL32(150, 150, 200, 255), + ImU32 borderColorActive = IM_COL32(200, 200, 255, 255), + ImU32 iconColor = IM_COL32(255, 255, 255, 255) + ); + + /** + * @brief Displays a dropdown to select an entity from a list. + * + * Creates a row in a table with a label and dropdown menu showing + * available entities. Updates the target entity when a selection is made. + * + * @param label Text label displayed next to the dropdown + * @param targetEntity Reference to the entity variable that will be updated with the selection + * @param entities Vector of available entities to choose from + * @param getNameFunc Function that converts an entity ID to a displayable name string + * @return true if an entity was selected (value changed), false otherwise + */ + bool RowEntityDropdown(const std::string &label, nexo::ecs::Entity &targetEntity, + const std::vector& entities, + const std::function& getNameFunc); + + /** + * @brief Draws a row with multiple channels (badge + slider pairs) + * + * This is a lower-level function used by the other drawRowDragFloatX functions. + * + * @param[in] channels The channel configuration to draw + * @return true if any value was changed, false otherwise + */ + bool RowDragFloat(const Channels &channels); + + /** + * @brief Draws a row with a single float value slider + * + * @param[in] uniqueLabel Unique label/ID for the component + * @param[in] badgeLabel Text for the badge (empty for no badge) + * @param[in,out] value Pointer to the float value to edit + * @param[in] minValue Minimum allowed value (default: -FLT_MAX) + * @param[in] maxValue Maximum allowed value (default: FLT_MAX) + * @param[in] speed Speed of value change during dragging (default: 0.3f) + * @return true if the value was changed, false otherwise + */ + bool RowDragFloat1( + const char *uniqueLabel, + const std::string &badgeLabel, + float *value, + float minValue = -FLT_MAX, + float maxValue = FLT_MAX, + float speed = 0.3f + ); + + + /** + * @brief Draws a row with two float value sliders (X and Y components) + * + * @param[in] uniqueLabel Unique label/ID for the component + * @param[in] badLabelX Text for the X component badge + * @param[in] badLabelY Text for the Y component badge + * @param[in,out] values Pointer to array of two float values to edit + * @param[in] minValue Minimum allowed value (default: -FLT_MAX) + * @param[in] maxValue Maximum allowed value (default: FLT_MAX) + * @param[in] speed Speed of value change during dragging (default: 0.3f) + * @param[in] badgeColor Optional custom colors for badges + * @param[in] textBadgeColor Optional custom text colors for badges + * @param[in] disabled If true, renders in an inactive/disabled state (default: false) + * @return true if any value was changed, false otherwise + */ + bool RowDragFloat2( + const char *uniqueLabel, + const std::string &badLabelX, + const std::string &badLabelY, + float *values, + float minValue = -FLT_MAX, + float maxValue = FLT_MAX, + float speed = 0.3f, + std::vector badgeColor = {}, + std::vector textBadgeColor = {}, + bool disabled = false + ); + + /** + * @brief Draws a row with three float value sliders (X, Y, and Z components) + * + * @param[in] uniqueLabel Unique label/ID for the component + * @param[in] badLabelX Text for the X component badge + * @param[in] badLabelY Text for the Y component badge + * @param[in] badLabelZ Text for the Z component badge + * @param[in,out] values Pointer to array of three float values to edit + * @param[in] minValue Minimum allowed value (default: -FLT_MAX) + * @param[in] maxValue Maximum allowed value (default: FLT_MAX) + * @param[in] speed Speed of value change during dragging (default: 0.3f) + * @param[in] badgeColors Optional custom colors for badges + * @param[in] textBadgeColors Optional custom text colors for badges + * @return true if any value was changed, false otherwise + */ + bool RowDragFloat3( + const char *uniqueLabel, + const std::string &badLabelX, + const std::string &badLabelY, + const std::string &badLabelZ, + float *values, + float minValue = -FLT_MAX, + float maxValue = FLT_MAX, + float speed = 0.3f, + std::vector badgeColors = {}, + std::vector textBadgeColors = {} + ); + + /** + * @brief Draws a toggle button with a separator and label + * + * Creates a collapsible section control with an arrow that toggles + * between expanded and collapsed states. + * + * @param[in] label The label to display + * @param[in,out] toggled Pointer to bool that tracks the toggle state + * @return true if the toggle state changed, false otherwise + */ + bool ToggleButtonWithSeparator(const std::string &label, bool* toggled); +} diff --git a/editor/src/ImNexo/Elements.cpp b/editor/src/ImNexo/Elements.cpp new file mode 100644 index 000000000..a334907fb --- /dev/null +++ b/editor/src/ImNexo/Elements.cpp @@ -0,0 +1,413 @@ +//// Elements.cpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Source file for the ui elements +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Elements.hpp" +#include "Guard.hpp" +#include "Utils.hpp" + +#include +#include + +namespace ImNexo { + + /** + * @brief Draw an icon centered within a rectangle with optional vertical positioning + * @param[in] icon Text of the icon to draw + * @param[in] p_min Minimum bounds of the rectangle + * @param[in] p_max Maximum bounds of the rectangle + * @param[in] color Color of the icon + * @param[in] scale Scale factor for the icon font + * @param[in] verticalPosition Vertical position factor (0-1), 0.5 for centered + * @param[in] horizontalPosition Horizontal position factor (0-1), 0.5 for centered + * @param[in] font Font to use (nullptr for current font) + */ + void CenteredIcon( + const std::string& icon, + const ImVec2& p_min, + const ImVec2& p_max, + const ImU32 color, + const float scale, + const float verticalPosition, + const float horizontalPosition, + ImFont* font + ) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + // Use specified font or current font + if (font) { + ImGui::PushFont(font); + } + + // Calculate icon size with scale + ImGui::SetWindowFontScale(scale); + const ImVec2 iconSize = ImGui::CalcTextSize(icon.c_str()); + ImGui::SetWindowFontScale(1.0f); + + // Calculate position + const auto iconPos = ImVec2( + p_min.x + (p_max.x - p_min.x - iconSize.x) * horizontalPosition, + p_min.y + (p_max.y - p_min.y) * verticalPosition - iconSize.y * 0.5f + ); + + // Draw the icon + drawList->AddText( + font ? font : ImGui::GetFont(), + ImGui::GetFontSize() * scale, + iconPos, color, + icon.c_str() + ); + + if (font) { + ImGui::PopFont(); + } + } + + /** + * @brief Draw wrapped text within bounds, attempts to split on spaces for better appearance + * @param[in] text Text to draw + * @param[in] p_min Minimum bounds + * @param[in] p_max Maximum bounds + * @param[in] color Text color + * @param[in] verticalPosition Vertical position (0-1), 0.5 for centered + */ + void WrappedCenteredText( + const std::string& text, + const ImVec2& p_min, + const ImVec2& p_max, + const ImU32 color, + const float verticalPosition + ) { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const float textHeight = ImGui::GetFontSize(); + const float wrapWidth = p_max.x - p_min.x - 10.0f; // 5px padding on each side + const float textY = p_min.y + (p_max.y - p_min.y) * verticalPosition; + + // Calculate text size to determine if wrapping is needed + const ImVec2 textSize = ImGui::CalcTextSize(text.c_str()); + + if (textSize.x > wrapWidth) { + // Try to find a space to split the text + size_t splitPos = text.find(' '); + + if (splitPos != std::string::npos) { + // Split text into two lines + const std::string line1 = text.substr(0, splitPos); + const std::string line2 = text.substr(splitPos + 1); + + // Calculate positions for both lines + const auto line1Pos = ImVec2( + p_min.x + (p_max.x - p_min.x - ImGui::CalcTextSize(line1.c_str()).x) * 0.5f, + textY - textHeight * 0.5f + ); + + const auto line2Pos = ImVec2( + p_min.x + (p_max.x - p_min.x - ImGui::CalcTextSize(line2.c_str()).x) * 0.5f, + textY + textHeight * 0.5f + ); + + // Draw both lines + drawList->AddText(line1Pos, color, line1.c_str()); + drawList->AddText(line2Pos, color, line2.c_str()); + } else { + // No space to split, draw single line (might be clipped) + const auto textPos = ImVec2( + p_min.x + (p_max.x - p_min.x - textSize.x) * 0.5f, + textY - textHeight * 0.5f + ); + drawList->AddText(textPos, color, text.c_str()); + } + } else { + // No wrapping needed, draw centered + const auto textPos = ImVec2( + p_min.x + (p_max.x - p_min.x - textSize.x) * 0.5f, + textY - textHeight * 0.5f + ); + drawList->AddText(textPos, color, text.c_str()); + } + } + + bool Button( + const std::string &label, + const ImVec2 &size, + const ImU32 bg, + const ImU32 bgHovered, + const ImU32 bgActive, + const ImU32 txtColor + ) + { + StyleGuard colorGuard(ImGuiCol_Button, bg); + colorGuard + .push(ImGuiCol_ButtonHovered, bgHovered) + .push(ImGuiCol_ButtonActive, bgActive) + .push(ImGuiCol_Text, txtColor); + + return ImGui::Button(label.c_str(), size); + } + + void ButtonBorder( + const ImU32 borderColor, + const ImU32 borderColorHovered, + const ImU32 borderColorActive, + const float rounding, + const ImDrawFlags flags, + const float thickness + ) + { + auto [p_min, p_max] = utils::getItemRect(); + ImU32 color = borderColor ? borderColor : ImGui::GetColorU32(ImGuiCol_Button); + + if (ImGui::IsItemHovered()) + color = borderColorHovered ? borderColorHovered : ImGui::GetColorU32(ImGuiCol_ButtonHovered); + if (ImGui::IsItemActive()) + color = borderColorActive ? borderColorActive : ImGui::GetColorU32(ImGuiCol_ButtonActive); + + ImGui::GetWindowDrawList()->AddRect(p_min, p_max, color, rounding, flags, thickness); + } + + bool DragFloat( + const std::string &label, + float *values, const float speed, + const float min, const float max, + const std::string &format, + const ImU32 bg, const ImU32 bgHovered, const ImU32 bgActive, const ImU32 textColor + ) + { + StyleGuard colorGuard(ImGuiCol_FrameBg, bg); + colorGuard + .push(ImGuiCol_FrameBgHovered, bgHovered) + .push(ImGuiCol_FrameBgActive, bgActive) + .push(ImGuiCol_Text, textColor); + + return ImGui::DragFloat(label.c_str(), values, speed, min, max, format.c_str()); + } + + /** + * @brief Sanitizes gradient stops to ensure proper ordering and range + */ + static void sanitizeGradientStops(std::vector& stops) + { + if (stops.size() < 2) + return; + + // Sort stops by position if needed + std::ranges::sort(stops, + [](const GradientStop& a, const GradientStop& b) { + return a.pos < b.pos; + }); + + // Clamp positions to valid range + float stop_max = 0.0f; + for (auto& [pos, color] : stops) { + // Clamp stop position to [0.0f, 1.0f] + pos = std::clamp(pos, 0.0f, 1.0f); + + // Ensure stops are monotonically increasing + pos = std::max(pos, stop_max); + stop_max = pos; + } + + // if first stop does not start at 0.0f, we need to add a stop at 0.0f + if (stops[0].pos > 0.0f) { + stops.insert(stops.begin(), { 0.0f, stops[0].color }); + } + // if last stop does not end at 1.0f, we need to add a stop at 1.0f + if (stops.back().pos < 1.0f) { + stops.push_back({ 1.0f, stops.back().color }); + } + } + + void RectFilledLinearGradient( + const ImVec2& pMin, + const ImVec2& pMax, + float angle, + std::vector stops, + ImDrawList* drawList + ) + { + if (!drawList) + drawList = ImGui::GetWindowDrawList(); + + // Check if we have at least two stops. + if (stops.size() < 2) + return; + + angle -= 90.0f; // rotate 90 degrees to match the CSS gradients rotations + + // Convert angle from degrees to radians and normalize + angle = fmodf(angle, 360.0f); + if (angle < 0.0f) + angle += 360.0f; + angle = angle * std::numbers::pi_v / 180.0f; + + const auto gradDir = ImVec2(cosf(angle), sinf(angle)); + + // Define rectangle polygon (clockwise order). + const std::vector rectPoly = { pMin, ImVec2(pMax.x, pMin.y), pMax, ImVec2(pMin.x, pMax.y) }; + + // Compute projection range (d_min, d_max) for the rectangle. + float d_min = std::numeric_limits::max(); + float d_max = -std::numeric_limits::max(); + for (auto const& v : rectPoly) { + const float d = ImDot(v, gradDir); + d_min = std::min(d_min, d); + d_max = std::max(d_max, d); + } + + // Sanitize gradient stops + sanitizeGradientStops(stops); + + // For each segment defined by consecutive stops: + for (long i = static_cast(stops.size()) - 1; i > 0; i--) { + const long posStart = i - 1; + const long posEnd = i; + // Compute threshold projections for the current segment. + const float segStart = d_min + stops[posStart].pos * (d_max - d_min); + const float segEnd = d_min + stops[posEnd].pos * (d_max - d_min); + + // Start with the whole rectangle. + std::vector segPoly = rectPoly; + std::vector tempPoly; + // Clip against lower boundary: d >= seg_start + utils::clipPolygonWithLine(segPoly, gradDir, segStart, tempPoly); + segPoly = tempPoly; // copy result + // Clip against upper boundary: d <= seg_end + // To clip with an upper-bound, invert the normal. + utils::clipPolygonWithLine(segPoly, ImVec2(-gradDir.x, -gradDir.y), -segEnd, tempPoly); + segPoly = tempPoly; + + if (segPoly.empty()) + continue; + + // Now, compute per-vertex colors for the segment polygon. + std::vector polyColors; + polyColors.reserve(segPoly.size()); + for (const ImVec2& v : segPoly) { + // Compute projection for the vertex. + const float d = ImDot(v, gradDir); + // Map projection to [0,1] relative to current segment boundaries. + const float t = (d - segStart) / (segEnd - segStart); + // Interpolate the color between the two stops. + polyColors.push_back(utils::imLerpColor(stops[posStart].color, stops[posEnd].color, t)); + } + + // Draw the filled and colored polygon. + utils::fillConvexPolygon(drawList, segPoly, polyColors); + } + } + + bool Header(const std::string &label, const std::string_view headerText) + { + StyleVarGuard styleGuard(ImGuiStyleVar_FramePadding, + ImVec2(ImGui::GetStyle().FramePadding.x, 3.0f)); + + bool open = ImGui::TreeNodeEx( + label.c_str(), + ImGuiTreeNodeFlags_DefaultOpen | + ImGuiTreeNodeFlags_Framed | + ImGuiTreeNodeFlags_AllowItemOverlap | + ImGuiTreeNodeFlags_SpanAvailWidth + ); + + // Get the bounding box and draw centered text + auto [p_min, p_max] = utils::getItemRect(); + const ImVec2 textPos = utils::calculateCenteredTextPosition(headerText.data(), p_min, p_max); + + ImGui::GetWindowDrawList()->AddText( + ImGui::GetFont(), + ImGui::GetFontSize(), + textPos, + ImGui::GetColorU32(ImGuiCol_Text), + headerText.data() + ); + + return open; + } + + void RowLabel(const ChannelLabel &rowLabel) + { + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(rowLabel.label.c_str()); + } + + // Helper method to draw arrow indicators + void Arrow(const ImVec2& center, const bool isExpanded, const ImU32 color, const float size) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + if (isExpanded) { + // Downward arrow (expanded) + draw_list->AddTriangleFilled( + ImVec2(center.x - size, center.y - size), + ImVec2(center.x + size, center.y - size), + ImVec2(center.x, center.y + size), + color); + } else { + // Rightward arrow (collapsed) + draw_list->AddTriangleFilled( + ImVec2(center.x - size, center.y - size), + ImVec2(center.x - size, center.y + size), + ImVec2(center.x + size, center.y), + color); + } + } + + void CustomSeparatorText( + const std::string &text, + const float textPadding, + const float leftSpacing, + const float thickness, + const ImU32 lineColor, + const ImU32 textColor + ) + { + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const float availWidth = ImGui::GetContentRegionAvail().x; + const float textWidth = ImGui::CalcTextSize(text.c_str()).x; + + // Compute the length of each line. Clamp to zero if the region is too small. + float lineWidth = (availWidth - textWidth - 2 * textPadding) * leftSpacing; + lineWidth = std::max(lineWidth, 0.0f); + + // Compute Y coordinate to draw lines so they align with the text center. + const float lineY = pos.y + ImGui::GetTextLineHeight() * 0.5f; + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const ImVec2 lineStart(pos.x, lineY); + const ImVec2 lineEnd(pos.x + lineWidth, lineY); + draw_list->AddLine(lineStart, lineEnd, lineColor, thickness); + + const ImVec2 textPos(pos.x + lineWidth + textPadding, pos.y); + draw_list->AddText(textPos, textColor, text.c_str()); + + const ImVec2 rightLineStart(pos.x + lineWidth + textPadding + textWidth + textPadding, lineY); + const ImVec2 rightLineEnd(pos.x + availWidth, lineY); + draw_list->AddLine(rightLineStart, rightLineEnd, lineColor, thickness); + + ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight())); + } + + void Image(const ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, + const ImVec4& tint_col, const ImVec4& border_col) + { + ImGui::Image(user_texture_id, image_size, uv0, uv1, tint_col, border_col); + } + + bool ImageButton(const char *str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, + const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) + { + return ImGui::ImageButton(str_id, user_texture_id, image_size, uv0, uv1, bg_col, tint_col); + } +} diff --git a/editor/src/ImNexo/Elements.hpp b/editor/src/ImNexo/Elements.hpp new file mode 100644 index 000000000..901165182 --- /dev/null +++ b/editor/src/ImNexo/Elements.hpp @@ -0,0 +1,336 @@ +//// Elements.hpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header file for the ui elements +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include +#include + +namespace ImNexo { + + /** + * @struct ChannelLabel + * @brief Represents a label for a channel in the entity properties editor + * + * Labels can have optional fixed width for precise layout control. + */ + struct ChannelLabel { + std::string label; + float fixedWidth = -1.0f; + }; + + /** + * @struct Badge + * @brief A styled badge component with customizable appearance + * + * Used as visual indicators or labels in the UI, typically alongside sliders. + */ + struct Badge { + std::string label; ///< The displayed text + ImVec2 size; ///< Size of the badge in pixels + ImU32 bg; ///< Background color + ImU32 bgHovered; ///< Background color when hovered + ImU32 bgActive; ///< Background color when active + ImU32 txtColor; ///< Text color + }; + + /** + * @struct DragFloat + * @brief A drag float slider component with customizable appearance + * + * Used for editing float values with adjustable range and visual styling. + */ + struct DragFloat { + std::string label; ///< Unique label/ID for the component + float *value; ///< Pointer to the value being edited + float speed; ///< Speed of value change during dragging + float min; ///< Minimum value + float max; ///< Maximum value + ImU32 bg; ///< Background color + ImU32 bgHovered; ///< Background color when hovered + ImU32 bgActive; ///< Background color when active + ImU32 textColor; ///< Text color + std::string format; ///< Format string for displaying the value + }; + + /** + * @struct Channels + * @brief A collection of badges and sliders forming a multi-channel editing row + * + * Used to create rows with multiple editable values (like X, Y, Z components). + */ + struct Channels { + unsigned int count; ///< Number of channels + std::vector badges; ///< Badge component for each channel + std::vector sliders; ///< Slider component for each channel + }; + + /** + * @brief Draw an icon centered within a rectangle with optional vertical positioning + * @param[in] icon Text of the icon to draw + * @param[in] p_min Minimum bounds of the rectangle + * @param[in] p_max Maximum bounds of the rectangle + * @param[in] color Color of the icon + * @param[in] scale Scale factor for the icon font + * @param[in] verticalPosition Vertical position factor (0-1), 0.5 for centered + * @param[in] horizontalPosition Horizontal position factor (0-1), 0.5 for centered + * @param[in] font Font to use (nullptr for current font) + */ + void CenteredIcon( + const std::string& icon, + const ImVec2& p_min, + const ImVec2& p_max, + ImU32 color, + float scale = 1.0f, + float verticalPosition = 0.5f, + float horizontalPosition = 0.5f, + ImFont* font = nullptr + ); + + + /** + * @brief Draw wrapped text within bounds, attempts to split on spaces for better appearance + * @param[in] text Text to draw + * @param[in] p_min Minimum bounds + * @param[in] p_max Maximum bounds + * @param[in] color Text color + * @param[in] verticalPosition Vertical position (0-1), 0.5 for centered + */ + void WrappedCenteredText( + const std::string& text, + const ImVec2& p_min, + const ImVec2& p_max, + ImU32 color, + float verticalPosition = 0.5f + ); + + /** + * @brief Draws a button with custom style colors. + * + * Pushes custom style colors for the button and its states, draws the button, + * and then pops the style colors. + * + * @param label The button label. + * @param size The size of the button. + * @param bg The background color. + * @param bgHovered The background color when hovered. + * @param bgActive The background color when active. + * @param txtColor The text color. + * @return true if the button was clicked; false otherwise. + */ + bool Button( + const std::string &label, + const ImVec2& size = ImVec2(0, 0), + ImU32 bg = 0, + ImU32 bgHovered = 0, + ImU32 bgActive = 0, + ImU32 txtColor = 0 + ); + + /** + * @brief Draws a border around the last item. + * + * Uses the current item's rectangle and draws a border with specified colors + * for normal, hovered, and active states. + * + * @param borderColor The border color for normal state. + * @param borderColorHovered The border color when hovered. + * @param borderColorActive The border color when active. + * @param rounding The rounding of the border corners. + * @param flags Additional draw flags. + * @param thickness The thickness of the border. + */ + void ButtonBorder( + ImU32 borderColor, + ImU32 borderColorHovered, + ImU32 borderColorActive, + float rounding = 2.0f, + ImDrawFlags flags = 0, + float thickness = 3.0f + ); + + /** + * @brief Draws a border inside the last item. + * + * Similar to drawButtonBorder, but draws a border inside the item rectangle instead of outside. + * + * @param borderColor The border color for normal state. + * @param borderColorHovered The border color when hovered. + * @param borderColorActive The border color when active. + * @param rounding The rounding of the border corners. + * @param flags Additional draw flags. + * @param thickness The thickness of the border. + */ + void ButtonInnerBorder( + ImU32 borderColor, + ImU32 borderColorHovered, + ImU32 borderColorActive, + float rounding = 2.0f, + ImDrawFlags flags = 0, + float thickness = 3.0f + ); + + /** + * @brief Draws a draggable float widget with custom styling. + * + * Pushes custom style colors for the drag float widget, draws it, and then pops the styles. + * + * @param label The label for the drag float. + * @param values Pointer to the float value. + * @param speed The speed of value change. + * @param min The minimum allowable value. + * @param max The maximum allowable value. + * @param format The display format. + * @param bg The background color. + * @param bgHovered The background color when hovered. + * @param bgActive The background color when active. + * @param textColor The text color. + * @return true if the value was changed; false otherwise. + */ + bool DragFloat( + const std::string &label, + float *values, + float speed, + float min, + float max, + const std::string &format, + ImU32 bg = 0, + ImU32 bgHovered = 0, + ImU32 bgActive = 0, + ImU32 textColor = 0 + ); + + /** + * @struct GradientStop + * @brief Defines a color position in a gradient + * + * Each gradient stop has a position (from 0.0 to 1.0) that represents + * where along the gradient the color appears, and a color value in ImGui's + * 32-bit color format. + */ + struct GradientStop + { + float pos; // percentage position along the gradient [0.0f, 1.0f] + ImU32 color; // color at this stop + }; + + /** + * @brief Draw filled rectangle with a linear gradient defined by an arbitrary angle and gradient stops. + * @param pMin Upper left corner position of the rectangle + * @param pMax Lower right corner position of the rectangle + * @param angle Angle of the gradient in degrees (0.0f = down, 90.0f = right, 180.0f = up, 270.0f = left) + * @param stops Vector of gradient stops, each defined by a position (0.0f to 1.0f) and a color + */ + void RectFilledLinearGradient( + const ImVec2& pMin, + const ImVec2& pMax, + float angle, + std::vector stops, + ImDrawList* drawList = nullptr + ); + + /** + * @brief Draws a collapsible header with centered text + * + * @param[in] label Unique label/ID for the header + * @param[in] headerText Text to display in the header + * @return true if the header is open/expanded, false otherwise + */ + bool Header(const std::string &label, std::string_view headerText); + + /** + * @brief Draws a row label in the current table column + * + * @param[in] rowLabel The label configuration to draw + */ + void RowLabel(const ChannelLabel &rowLabel); + + /** + * @brief Draws an arrow shape indicating expanded/collapsed state + * + * Creates a filled triangle pointing downward (expanded) or rightward (collapsed) + * that is commonly used to indicate a toggleable/expandable UI element. + * + * @param center Center point of the arrow + * @param isExpanded Whether the arrow should show expanded state (down arrow) or collapsed state (right arrow) + * @param color Color of the arrow + * @param size Size of the arrow from center to tip + */ + void Arrow(const ImVec2& center, bool isExpanded, ImU32 color, float size); + + /** + * @brief Draws a custom separator with centered text. + * + * Renders a separator line with text in the middle, with customizable padding, spacing, + * thickness, and colors. + * + * @param text The text to display at the separator. + * @param textPadding Padding around the text. + * @param leftSpacing The spacing multiplier for the left separator line. + * @param thickness The thickness of the separator lines. + * @param lineColor The color of the separator lines. + * @param textColor The color of the text. + */ + void CustomSeparatorText(const std::string &text, float textPadding, float leftSpacing, float thickness, ImU32 lineColor, ImU32 textColor); + + /** + * @brief ImGui::Image wrapper with different default UV coordinates (to flip the Y-axis). + * + * This function behaves exactly like ImGui::Image, except that the default UV coordinates are + * flipped to invert the image vertically. If you provide custom UV coordinates, the flipping + * behavior is effectively disabled. + * + * @param[in] user_texture_id The texture identifier for ImGui to render. + * @param[in] image_size The size of the image on the screen (in pixels). + * @param[in] uv0 The normalized UV coordinate for the top-left corner of the texture. + * @param[in] uv1 The normalized UV coordinate for the bottom-right corner of the texture. + * @param[in] tint_col The tint color applied to the image. + * @param[in] border_col The border color drawn around the image, if any. + */ + void Image( + ImTextureID user_texture_id, + const ImVec2& image_size, + const ImVec2& uv0 = ImVec2(0, 1), // Flipped Y + const ImVec2& uv1 = ImVec2(1, 0), // Flipped Y + const ImVec4& tint_col = ImVec4(1, 1, 1, 1), + const ImVec4& border_col = ImVec4(0, 0, 0, 0) + ); + + /** + * @brief ImGui::ImageButton wrapper with different default UV coordinates (to flip the Y-axis). + * + * This function behaves exactly like ImGui::ImageButton, except that the default UV coordinates are + * flipped to invert the image vertically. If you provide custom UV coordinates, the flipping + * behavior is effectively disabled. + * + * @param[in] str_id The unique label for the image button. + * @param[in] user_texture_id The texture identifier for ImGui to render. + * @param[in] image_size The size of the image on the screen (in pixels). + * @param[in] uv0 The normalized UV coordinate for the top-left corner of the texture. + * @param[in] uv1 The normalized UV coordinate for the bottom-right corner of the texture. + * @param[in] bg_col The background color of the button. + * @param[in] tint_col The tint color applied to the image. + */ + bool ImageButton( + const char* str_id, + ImTextureID user_texture_id, + const ImVec2& image_size, + const ImVec2& uv0 = ImVec2(0, 1), // Flipped Y + const ImVec2& uv1 = ImVec2(1, 0), // Flipped Y + const ImVec4& bg_col = ImVec4(0, 0, 0, 0), + const ImVec4& tint_col = ImVec4(1, 1, 1, 1) + ); + +} diff --git a/editor/src/ImNexo/EntityProperties.cpp b/editor/src/ImNexo/EntityProperties.cpp new file mode 100644 index 000000000..cfc058943 --- /dev/null +++ b/editor/src/ImNexo/EntityProperties.cpp @@ -0,0 +1,315 @@ +//// EntityProperties.cpp ///////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 22/02/2025 +// Description: Source file for the entity properties components +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ImNexo/ImNexo.hpp" +#include "Widgets.hpp" +#include "Guard.hpp" +#include "EntityProperties.hpp" +#include "IconsFontAwesome.h" +#include "Nexo.hpp" +#include "components/Camera.hpp" +#include "context/Selector.hpp" +#include "components/Uuid.hpp" +#include "components/Light.hpp" +#include "components/Transform.hpp" +#include "math/Vector.hpp" +#include "math/Light.hpp" + +namespace ImNexo { + + void Ambient(nexo::components::AmbientLightComponent &ambientComponent) + { + ImGui::Spacing(); + static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPicker = false; + + ImGui::Text("Color"); + ImGui::SameLine(); + glm::vec4 color = {ambientComponent.color, 1.0f}; + ColorEditor("##ColorEditor Ambient light", &color, &colorPickerMode, &showColorPicker); + ambientComponent.color = color; + } + + void DirectionalLight(nexo::components::DirectionalLightComponent &directionalComponent) + { + ImGui::Spacing(); + static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPicker = false; + ImGui::Text("Color"); + ImGui::SameLine(); + glm::vec4 color = {directionalComponent.color, 1.0f}; + ColorEditor("##ColorEditor Directional light", &color, &colorPickerMode, &showColorPicker); + directionalComponent.color = color; + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("InspectorDirectionTable", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + RowDragFloat3("Direction", "X", "Y", "Z", &directionalComponent.direction.x); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + void PointLight( + nexo::components::PointLightComponent &pointComponent, + nexo::components::TransformComponent &pointTransform + ) { + ImGui::Spacing(); + static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPicker = false; + ImGui::Text("Color"); + ImGui::SameLine(); + glm::vec4 color = {pointComponent.color, 1.0f}; + ColorEditor("##ColorEditor Point light", &color, &colorPickerMode, &showColorPicker); + pointComponent.color = color; + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("InspectorPointTable", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + RowDragFloat3("Position", "X", "Y", "Z", &pointTransform.pos.x); + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::Text("Distance"); + ImGui::SameLine(); + if (ImGui::DragFloat("##DistanceSlider", &pointComponent.maxDistance, 1.0f, 1.0f, 3250.0f)) { + // Recompute the attenuation from the distance + auto [lin, quad] = nexo::math::computeAttenuationFromDistance(pointComponent.maxDistance); + pointComponent.constant = 1.0f; + pointComponent.linear = lin; + pointComponent.quadratic = quad; + } + if (ImGui::IsItemActive()) + itemIsActive(); + if (ImGui::IsItemActivated()) + itemIsActivated(); + if (ImGui::IsItemDeactivated()) + itemIsDeactivated(); + ImGui::PopStyleVar(); + } + + void SpotLight(nexo::components::SpotLightComponent &spotComponent, nexo::components::TransformComponent &spotTransform) + { + ImGui::Spacing(); + static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPicker = false; + ImGui::Text("Color"); + ImGui::SameLine(); + glm::vec4 color = {spotComponent.color, 1.0f}; + ColorEditor("##ColorEditor Spot light", &color, &colorPickerMode, &showColorPicker); + spotComponent.color = color; + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("InspectorSpotTable", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + RowDragFloat3("Direction", "X", "Y", "Z", &spotComponent.direction.x, -FLT_MAX, FLT_MAX, 0.1f); + RowDragFloat3("Position", "X", "Y", "Z", &spotTransform.pos.x, -FLT_MAX, FLT_MAX, 0.1f); + + + ImGui::EndTable(); + } + + if (ImGui::BeginTable("InspectorCutOffSpotTable", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + if (RowDragFloat1("Distance", "", &spotComponent.maxDistance, 1.0f, 3250.0f, 1.0f)) + { + auto [lin, quad] = nexo::math::computeAttenuationFromDistance(spotComponent.maxDistance); + spotComponent.linear = lin; + spotComponent.quadratic = quad; + } + float innerCutOffDegrees = glm::degrees(glm::acos(spotComponent.cutOff)); + float outerCutOffDegrees = glm::degrees(glm::acos(spotComponent.outerCutoff)); + if (RowDragFloat1("Inner cut off", "", &innerCutOffDegrees, 0.0f, outerCutOffDegrees, 0.5f)) + spotComponent.cutOff = glm::cos(glm::radians(innerCutOffDegrees)); + if (RowDragFloat1("Outer cut off", "", &outerCutOffDegrees, innerCutOffDegrees, 90.0f, 0.5f)) + spotComponent.outerCutoff = glm::cos(glm::radians(outerCutOffDegrees)); + + ImGui::EndTable(); + } + + ImGui::PopStyleVar(); + } + + void Transform(nexo::components::TransformComponent &transformComponent, glm::vec3 &lastDisplayedEuler) + { + // Increase cell padding so rows have more space: + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + auto& [pos, size, quat] = transformComponent; + + if (ImGui::BeginTable("InspectorTransformTable", 4, + ImGuiTableFlags_SizingStretchProp)) + { + // Only the first column has a fixed width + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Z", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + RowDragFloat3("Position", "X", "Y", "Z", &pos.x); + + const glm::vec3 computedEuler = nexo::math::customQuatToEuler(quat); + + lastDisplayedEuler = computedEuler; + glm::vec3 rotation = lastDisplayedEuler; + + // Draw the Rotation row. + // When the user edits the rotation, we compute the delta from the last displayed Euler, + // convert that delta into an incremental quaternion, and update the master quaternion. + if (RowDragFloat3("Rotation", "X", "Y", "Z", &rotation.x)) { + const glm::vec3 deltaEuler = rotation - lastDisplayedEuler; + const glm::quat deltaQuat = glm::radians(deltaEuler); + quat = glm::normalize(deltaQuat * quat); + lastDisplayedEuler = nexo::math::customQuatToEuler(quat); + } + RowDragFloat3("Scale", "X", "Y", "Z", &size.x); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + void Camera(nexo::components::CameraComponent &cameraComponent) + { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("CameraInspectorViewPortParams", 4, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Y", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##Lock", ImGuiTableColumnFlags_WidthStretch); + glm::vec2 viewPort = {cameraComponent.width, cameraComponent.height}; + std::vector badgeColors; + std::vector textBadgeColors; + + const bool disabled = !cameraComponent.viewportLocked; + if (disabled) + ImGui::BeginDisabled(); + bool toResize = RowDragFloat2("Viewport size", "W", "H", &viewPort.x, -FLT_MAX, FLT_MAX, 1.0f, badgeColors, textBadgeColors, disabled); + if (toResize && cameraComponent.viewportLocked) + cameraComponent.resize(static_cast(viewPort.x), static_cast(viewPort.y)); + if (disabled) + ImGui::EndDisabled(); + + ImGui::TableSetColumnIndex(3); + + // Lock button + const std::string lockBtnLabel = !cameraComponent.viewportLocked ? ICON_FA_LOCK "##ViewPortSettings" : ICON_FA_UNLOCK "##ViewPortSettings"; + if (Button(lockBtnLabel)) { + cameraComponent.viewportLocked = !cameraComponent.viewportLocked; + } + ImGui::EndTable(); + } + + if (ImGui::BeginTable("InspectorCameraVariables", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + RowDragFloat1("FOV", "", &cameraComponent.fov, 30.0f, 120.0f, 0.3f); + RowDragFloat1("Near plane", "", &cameraComponent.nearPlane, 0.01f, 1.0f, 0.001f); + RowDragFloat1("Far plane", "", &cameraComponent.farPlane, 100.0f, 10000.0f, 1.0f); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + ImGui::Spacing(); + static ImGuiColorEditFlags colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPicker = false; + ImGui::AlignTextToFramePadding(); + ImGui::Text("Clear Color"); + ImGui::SameLine(); + ColorEditor("##ColorEditor Spot light", &cameraComponent.clearColor, &colorPickerMode, &showColorPicker); + } + + void CameraTarget(nexo::components::PerspectiveCameraTarget &cameraTargetComponent) + { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("InspectorControllerTable", 2, + ImGuiTableFlags_SizingStretchProp)) + { + auto &selector = nexo::editor::Selector::get(); + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + const std::vector &entities = nexo::Application::m_coordinator->getAllEntitiesWith< + nexo::components::TransformComponent, + nexo::ecs::Exclude, + nexo::ecs::Exclude, + nexo::ecs::Exclude, + nexo::ecs::Exclude, + nexo::ecs::Exclude>(); + + RowDragFloat1("Mouse sensitivity", "", &cameraTargetComponent.mouseSensitivity, 0.1f); + RowDragFloat1("Distance", "", &cameraTargetComponent.distance, 0.1f); + RowEntityDropdown( + "Target Entity", + cameraTargetComponent.targetEntity, entities, + [&selector](const nexo::ecs::Entity e) { + return selector.getUiHandle( + nexo::Application::m_coordinator->getComponent(e).uuid, + std::to_string(e) + ); + } + ); + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + void CameraController(nexo::components::PerspectiveCameraController &cameraControllerComponent) + { + ImGui::Spacing(); + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(5.0f, 10.0f)); + if (ImGui::BeginTable("InspectorControllerTable", 2, + ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("##Label", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::TableSetupColumn("##X", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel); + + float mouseSensitivity = cameraControllerComponent.mouseSensitivity; + RowDragFloat1("Mouse sensitivity", "", &mouseSensitivity); + cameraControllerComponent.mouseSensitivity = mouseSensitivity; + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + +} diff --git a/editor/src/ImNexo/EntityProperties.hpp b/editor/src/ImNexo/EntityProperties.hpp new file mode 100644 index 000000000..3296b65d2 --- /dev/null +++ b/editor/src/ImNexo/EntityProperties.hpp @@ -0,0 +1,87 @@ +//// EntityPropertiesComponents.hpp /////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 22/05/2025 +// Description: Header file for the entity properties components +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "components/Light.hpp" +#include "components/Transform.hpp" +#include "components/Camera.hpp" + +namespace ImNexo { + + void Ambient(nexo::components::AmbientLightComponent &ambientComponent); + + void DirectionalLight(nexo::components::DirectionalLightComponent &directionalComponent); + + void PointLight(nexo::components::PointLightComponent &pointComponent, nexo::components::TransformComponent &pointTransform); + + void SpotLight(nexo::components::SpotLightComponent &spotComponent, nexo::components::TransformComponent &spotTransform); + + + /** + * @brief Renders and handles the transform component editor UI. + * + * Creates a table-based editor for position, rotation, and scale values of a transform component. + * Rotation is handled specially to convert between quaternion (internal) and euler angles (UI display). + * When the user modifies euler angles, the function calculates the delta from the last displayed euler + * angles and applies a corresponding rotation to the master quaternion. + * + * @param transformComponent Reference to the transform component being edited + * @param lastDisplayedEuler Reference to vector storing the last displayed euler angles for computing deltas + */ + void Transform(nexo::components::TransformComponent &transformComponent, glm::vec3 &lastDisplayedEuler); + + + /** + * @brief Renders and handles the camera component editor UI. + * + * Creates a table-based editor for camera parameters, including: + * - Viewport size (width/height) with optional locking + * - Field of view (FOV) adjustment + * - Near and far clipping planes + * - Camera clear color with color picker + * + * The viewport size can be locked to prevent accidental changes, which is useful + * when the camera is being used in a specific context that requires fixed dimensions. + * + * @param cameraComponent Reference to the camera component being edited + */ + void Camera(nexo::components::CameraComponent &cameraComponent); + + /** + * @brief Renders and handles the camera target component editor UI. + * + * Creates a table-based editor for a camera target component, which controls + * a camera that orbits around a target entity. The editor includes: + * - Mouse sensitivity for orbit control + * - Distance from camera to target + * - Target entity selection dropdown showing available entities + * + * The entity dropdown filters out cameras and lights to show only valid targets. + * + * @param cameraTargetComponent Reference to the camera target component being edited + */ + void CameraTarget(nexo::components::PerspectiveCameraTarget &cameraTargetComponent); + + /** + * @brief Renders and handles the camera controller component editor UI. + * + * Creates a table-based editor for a free-moving camera controller component. + * Currently includes only mouse sensitivity adjustment, which controls how + * quickly the camera rotates in response to mouse movement. + * + * @param cameraControllerComponent Reference to the camera controller component being edited + */ + void CameraController(nexo::components::PerspectiveCameraController &cameraControllerComponent); + +} diff --git a/editor/src/ImNexo/Guard.hpp b/editor/src/ImNexo/Guard.hpp new file mode 100644 index 000000000..d8c031ec2 --- /dev/null +++ b/editor/src/ImNexo/Guard.hpp @@ -0,0 +1,202 @@ +//// Guard.hpp //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header for the utils guard class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include +#include + +namespace ImNexo { + /** + * @brief Guard class for managing ImGui style colors. + * + * Automatically pushes ImGui style colors on construction and pops them on destruction, + * ensuring the style state is properly restored even when exceptions occur. Supports + * chaining multiple color changes with the push() method. + */ + class StyleGuard { + public: + /** + * @brief Constructs a StyleGuard and pushes the initial style color. + * + * @param col The ImGui color enumeration to modify + * @param color The new color value (0 to skip pushing) + */ + explicit StyleGuard(const ImGuiCol_ col, const ImU32 color) + { + if (color != 0) { + m_colorIndices.push_back(col); + ImGui::PushStyleColor(col, color); + } + } + + /** + * @brief Pushes an additional style color to the guard. + * + * @param col The ImGui color enumeration to modify + * @param color The new color value (0 to skip pushing) + * @return Reference to this guard for method chaining + */ + StyleGuard& push(const ImGuiCol_ col, const ImU32 color) + { + if (color != 0) { + m_colorIndices.push_back(col); + ImGui::PushStyleColor(col, color); + } + return *this; + } + + /** + * @brief Destructor that automatically pops all pushed style colors. + */ + ~StyleGuard() + { + if (!m_colorIndices.empty()) + ImGui::PopStyleColor(static_cast(m_colorIndices.size())); + } + + private: + std::vector m_colorIndices; ///< Tracks which color indices were pushed + }; + + /** + * @brief Guard class for managing ImGui style variables. + * + * Automatically pushes ImGui style variables on construction and pops them on destruction, + * ensuring the style state is properly restored even when exceptions occur. Supports + * chaining multiple variable changes with the push() method. + */ + class StyleVarGuard { + public: + /** + * @brief Constructs a StyleVarGuard and pushes an initial vector style variable. + * + * @param var The ImGui style variable enumeration to modify + * @param value The new vector value + */ + explicit StyleVarGuard(const ImGuiStyleVar_ var, const ImVec2 value) + { + m_varCount++; + ImGui::PushStyleVar(var, value); + } + + /** + * @brief Constructs a StyleVarGuard and pushes an initial scalar style variable. + * + * @param var The ImGui style variable enumeration to modify + * @param value The new scalar value + */ + explicit StyleVarGuard(const ImGuiStyleVar_ var, const float value) + { + m_varCount++; + ImGui::PushStyleVar(var, value); + } + + /** + * @brief Pushes an additional vector style variable to the guard. + * + * @param var The ImGui style variable enumeration to modify + * @param value The new vector value + * @return Reference to this guard for method chaining + */ + StyleVarGuard& push(const ImGuiStyleVar_ var, const ImVec2 value) + { + m_varCount++; + ImGui::PushStyleVar(var, value); + return *this; + } + + /** + * @brief Pushes an additional scalar style variable to the guard. + * + * @param var The ImGui style variable enumeration to modify + * @param value The new scalar value + * @return Reference to this guard for method chaining + */ + StyleVarGuard& push(const ImGuiStyleVar_ var, const float value) + { + m_varCount++; + ImGui::PushStyleVar(var, value); + return *this; + } + + /** + * @brief Destructor that automatically pops all pushed style variables. + */ + ~StyleVarGuard() + { + if (m_varCount > 0) + ImGui::PopStyleVar(m_varCount); + } + + private: + int m_varCount = 0; ///< Counts how many style variables were pushed + }; + + /** + * @brief Guard class for managing ImGui ID stack. + * + * Automatically pushes an ID to the ImGui ID stack on construction and pops + * it on destruction, ensuring proper nesting and scoping of unique identifiers + * even when exceptions occur. + */ + class IdGuard { + public: + /** + * @brief Constructs an IdGuard and pushes the specified ID. + * + * @param id The string identifier to push onto the ImGui ID stack + */ + explicit IdGuard(const std::string& id) + { + ImGui::PushID(id.c_str()); + } + + /** + * @brief Destructor that automatically pops the pushed ID. + */ + ~IdGuard() + { + ImGui::PopID(); + } + }; + + /** + * @brief Guard class for managing ImGui font scaling. + * + * Temporarily changes the window font scale factor and restores it + * to the default scale (1.0) when the guard goes out of scope. + */ + class FontScaleGuard { + public: + /** + * @brief Constructs a FontScaleGuard and sets the font scale. + * + * @param scale The scaling factor to apply to the current window's font + */ + explicit FontScaleGuard(const float scale) + { + ImGui::SetWindowFontScale(scale); + } + + /** + * @brief Destructor that automatically resets the font scale to default. + */ + ~FontScaleGuard() + { + ImGui::SetWindowFontScale(1.0f); + } + }; +} diff --git a/editor/src/ImNexo/ImNexo.cpp b/editor/src/ImNexo/ImNexo.cpp new file mode 100644 index 000000000..bb029fa71 --- /dev/null +++ b/editor/src/ImNexo/ImNexo.cpp @@ -0,0 +1,70 @@ +//// ImNexo.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 01/05/2025 +// Description: Source file for the ImNexo functions +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ImNexo.hpp" + +namespace ImNexo { + bool isItemActive() + { + return g_isItemActive; + } + + static void resetItemActiveState() + { + g_isItemActive = false; + } + + void itemIsActive() + { + g_isItemActive = true; + } + + bool isItemActivated() + { + return g_isItemActivated; + } + + static void resetItemActivatedState() + { + g_isItemActivated = false; + } + + void itemIsActivated() + { + g_isItemActivated = true; + } + + bool isItemDeactivated() + { + return g_isItemDeactivated; + } + + static void resetItemDeactivatedState() + { + g_isItemDeactivated = false; + } + + void itemIsDeactivated() + { + g_isItemDeactivated = true; + } + + void resetItemStates() + { + resetItemActivatedState(); + resetItemActiveState(); + resetItemDeactivatedState(); + } + +} diff --git a/editor/src/ImNexo/ImNexo.hpp b/editor/src/ImNexo/ImNexo.hpp new file mode 100644 index 000000000..a7df78188 --- /dev/null +++ b/editor/src/ImNexo/ImNexo.hpp @@ -0,0 +1,33 @@ +//// ImNexo.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 01/05/2025 +// Description: Header for the ImNexo functions +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +namespace ImNexo { + + inline bool g_isItemActive = false; + inline bool g_isItemActivated = false; + inline bool g_isItemDeactivated = false; + + bool isItemActive(); + void itemIsActive(); + + bool isItemActivated(); + void itemIsActivated(); + + bool isItemDeactivated(); + void itemIsDeactivated(); + + void resetItemStates(); + +} diff --git a/editor/src/ImNexo/Panels.cpp b/editor/src/ImNexo/Panels.cpp new file mode 100644 index 000000000..fd220d96a --- /dev/null +++ b/editor/src/ImNexo/Panels.cpp @@ -0,0 +1,433 @@ +//// Panels.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Source file for the ui panels +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ImNexo/ImNexo.hpp" +#include "Nexo.hpp" +#include "Panels.hpp" +#include "Elements.hpp" +#include "Widgets.hpp" +#include "EntityProperties.hpp" +#include "CameraFactory.hpp" +#include "Path.hpp" +#include "IconsFontAwesome.h" +#include "assets/AssetCatalog.hpp" +#include "components/Camera.hpp" +#include "components/Transform.hpp" +#include "components/Uuid.hpp" +#include "context/Selector.hpp" +#include "context/actions/EntityActions.hpp" +#include "utils/EditorProps.hpp" +#include "context/ActionManager.hpp" + +namespace ImNexo { + bool MaterialInspector(nexo::components::Material *material) + { + bool modified = false; + // --- Shader Selection --- + ImGui::BeginGroup(); + { + ImGui::Text("Shader:"); + ImGui::SameLine(); + + static int currentShaderIndex = 0; + const char* shaderOptions[] = { "Standard", "Unlit", "CustomPBR" }; + const float availableWidth = ImGui::GetContentRegionAvail().x; + ImGui::SetNextItemWidth(availableWidth); + + if (ImGui::Combo("##ShaderCombo", ¤tShaderIndex, shaderOptions, IM_ARRAYSIZE(shaderOptions))) + { + //TODO: implement shader selection + } + } + ImGui::EndGroup(); + ImGui::Spacing(); + + // --- Rendering mode selection --- + ImGui::Text("Rendering mode:"); + ImGui::SameLine(); + static int currentRenderingModeIndex = 0; + const char* renderingModeOptions[] = { "Opaque", "Transparent", "Refraction" }; + const float availableWidth = ImGui::GetContentRegionAvail().x; + + ImGui::SetNextItemWidth(availableWidth); + if (ImGui::Combo("##RenderingModeCombo", ¤tRenderingModeIndex, renderingModeOptions, IM_ARRAYSIZE(renderingModeOptions))) + { + //TODO: implement rendering mode + } + + auto& catalog = nexo::assets::AssetCatalog::getInstance(); + // --- Albedo texture --- + { + static ImGuiColorEditFlags colorPickerModeAlbedo = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPickerAlbedo = false; + const auto asset = material->albedoTexture.lock(); + const auto albedoTexture = asset && asset->isLoaded() ? asset->getData()->texture : nullptr; + + std::filesystem::path newTexturePath; + if (TextureButton("Albedo texture", albedoTexture, newTexturePath) + && !newTexturePath.empty()) { + // TODO: /!\ This is not futureproof, this would modify the texture for every asset that use this material + const auto newTexture = catalog.createAsset( + nexo::assets::AssetLocation(newTexturePath.filename().string()), + newTexturePath + ); + if (newTexture) + material->albedoTexture = newTexture; + } + ImGui::SameLine(); + modified = ColorEditor("##ColorEditor Albedo texture", &material->albedoColor, &colorPickerModeAlbedo, &showColorPickerAlbedo) || modified; + } + + // --- Specular texture --- + { + static ImGuiColorEditFlags colorPickerModeSpecular = ImGuiColorEditFlags_PickerHueBar; + static bool showColorPickerSpecular = false; + const auto asset = material->metallicMap.lock(); + const auto metallicTexture = asset && asset->isLoaded() ? asset->getData()->texture : nullptr; + + std::filesystem::path newTexturePath; + if (TextureButton("Specular texture", metallicTexture, newTexturePath) + && !newTexturePath.empty()) { + // TODO: /!\ This is not futureproof, this would modify the texture for every asset that use this material + const auto newTexture = catalog.createAsset( + nexo::assets::AssetLocation(newTexturePath.filename().string()), + newTexturePath + ); + if (newTexture) + material->metallicMap = newTexture; + } + ImGui::SameLine(); + modified = ColorEditor("##ColorEditor Specular texture", &material->specularColor, &colorPickerModeSpecular, &showColorPickerSpecular) || modified; + } + return modified; + } + + /** + * @brief Creates a default perspective camera for the camera inspector preview. + * + * Sets up a perspective camera with a framebuffer for rendering the preview view. + * Also adds a billboard with a camera icon for visualization in the scene. + * + * @param sceneId The ID of the scene where the camera should be created + * @param sceneViewportSize The dimensions to use for the camera's framebuffer + * @return The entity ID of the created camera + */ + static nexo::ecs::Entity createDefaultPerspectiveCamera(const nexo::scene::SceneId sceneId, const ImVec2 sceneViewportSize) + { + auto &app = nexo::getApp(); + nexo::renderer::NxFramebufferSpecs framebufferSpecs; + framebufferSpecs.attachments = { + nexo::renderer::NxFrameBufferTextureFormats::RGBA8, nexo::renderer::NxFrameBufferTextureFormats::RED_INTEGER, nexo::renderer::NxFrameBufferTextureFormats::Depth + }; + + // Define layout: 60% for inspector, 40% for preview + framebufferSpecs.width = static_cast(sceneViewportSize.x); + framebufferSpecs.height = static_cast(sceneViewportSize.y); + const auto renderTarget = nexo::renderer::NxFramebuffer::create(framebufferSpecs); + const nexo::ecs::Entity defaultCamera = nexo::CameraFactory::createPerspectiveCamera({0.0f, 0.0f, -5.0f}, static_cast(sceneViewportSize.x), static_cast(sceneViewportSize.y), renderTarget); + app.getSceneManager().getScene(sceneId).addEntity(defaultCamera); + nexo::editor::utils::addPropsTo(defaultCamera, nexo::editor::utils::PropsType::CAMERA); + return defaultCamera; + } + + bool CameraInspector(const nexo::scene::SceneId sceneId) + { + auto &app = nexo::getApp(); + static int undoStackSize = -1; + if (undoStackSize == -1) + undoStackSize = static_cast(nexo::editor::ActionManager::get().getUndoStackSize()); + + const ImVec2 availSize = ImGui::GetContentRegionAvail(); + const float totalWidth = availSize.x; + float totalHeight = availSize.y - 40; // Reserve space for bottom buttons + + // Define layout: 60% for inspector, 40% for preview + const float inspectorWidth = totalWidth * 0.4f; + const float previewWidth = totalWidth - inspectorWidth - 8; // Subtract spacing between panels + static nexo::ecs::Entity camera = nexo::ecs::MAX_ENTITIES; + if (camera == nexo::ecs::MAX_ENTITIES) + { + camera = createDefaultPerspectiveCamera(sceneId, ImVec2(previewWidth, totalHeight)); + } + + static char cameraName[128] = ""; + static bool nameIsEmpty = false; + static bool closingPopup = false; + + // We do this since imgui seems to render once more the popup, so we need to wait one frame before deleting + // the render target + if (closingPopup) { + // Now it's safe to delete the entity + app.deleteEntity(camera); + + camera = nexo::ecs::MAX_ENTITIES; + cameraName[0] = '\0'; + nameIsEmpty = false; + undoStackSize = -1; + closingPopup = false; + ImGui::CloseCurrentPopup(); + return true; + } + ImGui::Columns(2, "CameraCreatorColumns", false); + + ImGui::SetColumnWidth(0, inspectorWidth); + // --- Left Side: Camera Inspector --- + { + ImGui::BeginChild("CameraInspector", ImVec2(inspectorWidth - 4, totalHeight), true); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Name"); + ImGui::SameLine(); + if (nameIsEmpty) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.9f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + } + ImGui::InputText("##CameraName", cameraName, IM_ARRAYSIZE(cameraName)); + if (nameIsEmpty) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.2f, 0.2f, 1.0f)); + ImGui::TextWrapped("Name is empty"); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } else { + ImGui::Spacing(); + } + if (nameIsEmpty && cameraName[0] != '\0') + nameIsEmpty = false; + ImGui::Spacing(); + + if (Header("##CameraNode", "Camera")) + { + auto &cameraComponent = nexo::Application::m_coordinator->getComponent(camera); + cameraComponent.render = true; + static nexo::components::CameraComponent::Memento beforeState; + auto cameraComponentCopy = cameraComponent; + resetItemStates(); + Camera(cameraComponent); + if (isItemActivated()) { + beforeState = cameraComponentCopy.save(); + } else if (isItemDeactivated()) { + auto afterState = cameraComponent.save(); + auto action = std::make_unique>(camera, beforeState, afterState); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + } + ImGui::TreePop(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + + if (Header("##TransformNode", "Transform Component")) + { + static glm::vec3 lastDisplayedEuler(0.0f); + auto &transformComponent = nexo::Application::m_coordinator->getComponent(camera); + static nexo::components::TransformComponent::Memento beforeState; + resetItemStates(); + auto transformComponentCopy = transformComponent; + Transform(transformComponent, lastDisplayedEuler); + if (isItemActivated()) { + beforeState = transformComponentCopy.save(); + } else if (isItemDeactivated()) { + auto afterState = transformComponent.save(); + auto action = std::make_unique>(camera, beforeState, afterState); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + } + ImGui::TreePop(); + } + + if (nexo::Application::m_coordinator->entityHasComponent(camera) && + Header("##PerspectiveCameraTarget", "Camera Target Component")) + { + auto &cameraTargetComponent = nexo::Application::m_coordinator->getComponent(camera); + nexo::components::PerspectiveCameraTarget::Memento beforeState{}; + resetItemStates(); + auto cameraTargetComponentCopy = cameraTargetComponent; + CameraTarget(cameraTargetComponent); + if (isItemActivated()) { + beforeState = cameraTargetComponentCopy.save(); + } else if (isItemDeactivated()) { + auto afterState = cameraTargetComponent.save(); + auto action = std::make_unique>(camera, beforeState, afterState); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + } + ImGui::TreePop(); + } + + if (nexo::Application::m_coordinator->entityHasComponent(camera) && + Header("##PerspectiveCameraController", "Camera Controller Component")) + { + auto &cameraControllerComponent = nexo::Application::m_coordinator->getComponent(camera); + nexo::components::PerspectiveCameraController::Memento beforeState{}; + auto cameraControllerComponentCopy = cameraControllerComponent; + resetItemStates(); + CameraController(cameraControllerComponent); + if (isItemActivated()) { + beforeState = cameraControllerComponentCopy.save(); + } else if (isItemDeactivated()) { + auto afterState = cameraControllerComponent.save(); + auto action = std::make_unique>(camera, beforeState, afterState); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + } + ImGui::TreePop(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + // Add Component button + const float buttonWidth = inspectorWidth - 16; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4)); + float centeredX = (inspectorWidth - buttonWidth) * 0.5f; + ImGui::SetCursorPosX(centeredX); + + // Static variables for state tracking + static bool showComponentSelector = false; + static float animProgress = 0.0f; + static double lastClickTime = 0.0f; + + // Button with arrow indicating state + std::string buttonText = "Add Component " + std::string(showComponentSelector ? ICON_FA_CHEVRON_UP : ICON_FA_CHEVRON_DOWN); + + if (Button(buttonText, ImVec2(buttonWidth, 0))) + { + showComponentSelector = !showComponentSelector; + if (showComponentSelector) { + lastClickTime = ImGui::GetTime(); + animProgress = 0.0f; + } + } + ImGui::PopStyleVar(); + + // Component selector with just two options + if (showComponentSelector) + { + // Animation calculation + constexpr float animDuration = 0.25f; + auto timeSinceClick = static_cast(ImGui::GetTime() - lastClickTime); + animProgress = std::min(timeSinceClick / animDuration, 1.0f); + + // Simplified component grid with compact layout + constexpr float maxGridHeight = 90.0f; + const float currentHeight = maxGridHeight * animProgress; + + // Create child window for components with animated height + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4)); // Reduce spacing between items + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 8)); // Better padding inside items + + ImGui::BeginChild("ComponentSelector", ImVec2(buttonWidth, currentHeight), 0, ImGuiWindowFlags_NoScrollbar); + + if (animProgress > 0.5f) + { + + // Draw component buttons side-by-side with controlled spacing + ImGui::BeginGroup(); + + if (!nexo::Application::m_coordinator->entityHasComponent(camera) && + !nexo::Application::m_coordinator->entityHasComponent(camera) && + ButtonWithIconAndText("camera_target", ICON_FA_CAMERA, "Camera target", ImVec2(75.0f, 75.0f))) + { + auto action = std::make_unique>(camera); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + nexo::components::PerspectiveCameraTarget cameraTarget{}; + nexo::Application::m_coordinator->addComponent(camera, cameraTarget); + showComponentSelector = false; + } + ImGui::SameLine(); + if (!nexo::Application::m_coordinator->entityHasComponent(camera) && + !nexo::Application::m_coordinator->entityHasComponent(camera) && + ButtonWithIconAndText("camera_controller", ICON_FA_GAMEPAD, "Camera Controller", ImVec2(75.0f, 75.0f))) + { + auto action = std::make_unique>(camera); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + nexo::components::PerspectiveCameraController cameraController{}; + nexo::Application::m_coordinator->addComponent(camera, cameraController); + showComponentSelector = false; + } + ImGui::EndGroup(); + } + + ImGui::EndChild(); + ImGui::PopStyleVar(3); + + // Reset animation if needed + if (!showComponentSelector && animProgress >= 1.0f) { + animProgress = 0.0f; + } + } + + ImGui::EndChild(); // End CameraInspector + } + ImGui::NextColumn(); + // --- Right Side: Camera Preview --- + { + ImGui::BeginChild("CameraPreview", ImVec2(previewWidth - 4, totalHeight), true); + + nexo::Application::SceneInfo sceneInfo{sceneId, nexo::RenderingType::FRAMEBUFFER}; + app.run(sceneInfo); + auto const &cameraComponent = nexo::Application::m_coordinator->getComponent(camera); + const unsigned int textureId = cameraComponent.m_renderTarget->getColorAttachmentId(0); + + const float displayHeight = totalHeight - 20; + const float displayWidth = displayHeight; + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + 4, ImGui::GetCursorPosY() + 4)); + Image(static_cast(static_cast(textureId)), + ImVec2(displayWidth, displayHeight)); + + ImGui::EndChild(); + } + + ImGui::Columns(1); + ImGui::Spacing(); + + // Bottom buttons - centered + constexpr float buttonWidth = 120.0f; + + if (ImGui::Button("OK", ImVec2(buttonWidth, 0))) + { + if (cameraName[0] == '\0') { + nameIsEmpty = true; + return false; + } + nameIsEmpty = false; + auto &selector = nexo::editor::Selector::get(); + const auto &uuid = nexo::Application::m_coordinator->getComponent(camera); + auto &cameraComponent = nexo::Application::m_coordinator->getComponent(camera); + cameraComponent.active = false; + selector.setUiHandle(uuid.uuid, std::string(ICON_FA_CAMERA " ") + cameraName); + unsigned int stackSize = nexo::editor::ActionManager::get().getUndoStackSize() - undoStackSize; + nexo::editor::ActionManager::get().clearHistory(stackSize); + auto action = std::make_unique(camera); + nexo::editor::ActionManager::get().recordAction(std::move(action)); + camera = nexo::ecs::MAX_ENTITIES; + cameraName[0] = '\0'; + undoStackSize = -1; + ImGui::CloseCurrentPopup(); + return true; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) + { + unsigned int stackSize = nexo::editor::ActionManager::get().getUndoStackSize() - undoStackSize; + nexo::editor::ActionManager::get().clearHistory(stackSize); + closingPopup = true; + return false; + } + return false; + } +} diff --git a/editor/src/ImNexo/Panels.hpp b/editor/src/ImNexo/Panels.hpp new file mode 100644 index 000000000..8df0115d9 --- /dev/null +++ b/editor/src/ImNexo/Panels.hpp @@ -0,0 +1,54 @@ +//// NxUiPanels.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header file for ui panels +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "components/Render.hpp" +#include "core/scene/SceneManager.hpp" + +#include + +namespace ImNexo { + /** + * @brief Draws a material inspector widget for editing material properties. + * + * This function displays controls for shader selection, rendering mode, and textures/colors + * for material properties such as albedo and specular components. + * + * @param material Pointer to the components::Material to be inspected and modified. + * @return true if any material property was modified; false otherwise. + */ + bool MaterialInspector(nexo::components::Material *material); + + /** + * @brief Displays a camera creation and configuration dialog. + * + * Creates a modal window with a split layout: + * - Left panel: Camera property inspector with fields for name, camera parameters, + * transform values, and optional components + * - Right panel: Real-time preview of the camera's view + * + * The dialog includes an animated "Add Component" dropdown that allows adding + * optional camera components (Camera Target or Camera Controller). At the bottom, + * OK and Cancel buttons allow confirming or aborting camera creation. + * + * When OK is clicked, the camera name is validated. If valid, the camera is added + * to the specified scene with the configured parameters. If Cancel is clicked or + * the dialog is otherwise closed, any temporary camera is deleted. + * + * @param sceneId The ID of the scene where the camera will be created + * @param sceneViewportSize The size of the scene viewport for proper camera aspect ratio + * @return true if the dialog was closed (either by confirming or canceling), false if still open + */ + bool CameraInspector(nexo::scene::SceneId sceneId); +} diff --git a/editor/src/ImNexo/Utils.cpp b/editor/src/ImNexo/Utils.cpp new file mode 100644 index 000000000..1ddb207cb --- /dev/null +++ b/editor/src/ImNexo/Utils.cpp @@ -0,0 +1,98 @@ +//// Utils.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Source file for the ui utils functions +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Utils.hpp" + +namespace ImNexo::utils { + + ImU32 imLerpColor(const ImU32 colA, const ImU32 colB, const float t) + { + const unsigned char a0 = (colA >> 24) & 0xFF, r0 = (colA >> 16) & 0xFF, g0 = (colA >> 8) & 0xFF, b0 = colA & 0xFF; + const unsigned char a1 = (colB >> 24) & 0xFF, r1 = (colB >> 16) & 0xFF, g1 = (colB >> 8) & 0xFF, b1 = colB & 0xFF; + const auto a = static_cast(static_cast(a0) + t * static_cast(a1 - a0)); + const auto r = static_cast(static_cast(r0) + t * static_cast(r1 - r0)); + const auto g = static_cast(static_cast(g0) + t * static_cast(g1 - g0)); + const auto b = static_cast(static_cast(b0) + t * static_cast(b1 - b0)); + return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + + void clipPolygonWithLine(const std::vector& poly, const ImVec2& normal, const float offset, std::vector& outPoly) + { + outPoly.clear(); + const auto count = poly.size(); + outPoly.reserve(count * 2); // Preallocate space for the output polygon (prepare worst case) + for (size_t i = 0; i < count; i++) { + const ImVec2& a = poly[i]; + const ImVec2& b = poly[(i + 1) % count]; + const float da = ImDot(a, normal) - offset; + const float db = ImDot(b, normal) - offset; + if (da >= 0) + outPoly.push_back(a); + // if the edge spans the boundary, compute intersection + if ((da >= 0 && db < 0) || (da < 0 && db >= 0)) { + const float t = da / (da - db); + ImVec2 inter; + inter.x = a.x + t * (b.x - a.x); + inter.y = a.y + t * (b.y - a.y); + outPoly.push_back(inter); + } + } + } + + void fillConvexPolygon(ImDrawList* drawList, const std::vector& poly, const std::vector& polyColors) + { + if (poly.size() < 3) + return; + const auto count = static_cast(poly.size()); + drawList->PrimReserve((count - 2) * 3, count); + // Use the first vertex as pivot. + for (int i = 1; i < count - 1; i++) { + const auto currentIdx = drawList->_VtxCurrentIdx; + drawList->PrimWriteIdx(static_cast(currentIdx)); + drawList->PrimWriteIdx(static_cast(currentIdx + i)); + drawList->PrimWriteIdx(static_cast(currentIdx + i + 1)); + } + // Write vertices with their computed colors. + for (int i = 0; i < count; i++) { + // For a vertex, we determine its position t between the segment boundaries later. + // Here we assume the provided poly_colors already correspond vertex-by-vertex. + drawList->PrimWriteVtx(poly[i], drawList->_Data->TexUvWhitePixel, polyColors[i]); + } + } + + std::pair getItemRect(const ImVec2& padding) + { + ImVec2 p_min = ImGui::GetItemRectMin(); + ImVec2 p_max = ImGui::GetItemRectMax(); + + p_min.x += padding.x; + p_min.y += padding.y; + p_max.x -= padding.x; + p_max.y -= padding.y; + + return {p_min, p_max}; + } + + /** + * @brief Positions text centered within a rectangle + */ + ImVec2 calculateCenteredTextPosition(const std::string& text, const ImVec2& p_min, const ImVec2& p_max) + { + const ImVec2 textSize = ImGui::CalcTextSize(text.c_str()); + return { + p_min.x + (p_max.x - p_min.x - textSize.x) * 0.5f, + p_min.y + (p_max.y - p_min.y - textSize.y) * 0.5f + }; + } +} diff --git a/editor/src/ImNexo/Utils.hpp b/editor/src/ImNexo/Utils.hpp new file mode 100644 index 000000000..75e3e26cd --- /dev/null +++ b/editor/src/ImNexo/Utils.hpp @@ -0,0 +1,60 @@ +//// Utils.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header file for the ui utils functions +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include +#include + +namespace ImNexo::utils { + + /** + * @brief Linearly interpolates between two colors (ImU32, ImGui 32-bits ARGB format). + * @param[in] colA The first color (ARGB format). + * @param[in] colB The second color (ARGB format). + * @param[in] t The interpolation factor (0.0 to 1.0). + * @return The interpolated color (ARGB format). + */ + ImU32 imLerpColor(ImU32 colA, ImU32 colB, float t); + + /** + * @brief Clip a convex polygon against a half-plane defined by: (dot(normal, v) >= offset) + * + * This function uses the Sutherland-Hodgman algorithm to clip a polygon against a line defined by a normal vector and an offset. + * @param[in] poly Vector of vertices representing the polygon to be clipped. + * @param[in] normal The normal vector of the line used for clipping. + * @param[in] offset The offset from the origin of the line. + * @param[out] outPoly Output vector to store the clipped polygon vertices. + */ + void clipPolygonWithLine(const std::vector& poly, const ImVec2& normal, float offset, std::vector& outPoly); + + /** + * @brief Fill a convex polygon with triangles using a triangle fan. + * @param[in] drawList The ImDrawList to which the triangles will be added. + * @param[in] poly Vector of vertices representing the polygon to be filled. + * @param[in] polyColors Vector of colors for each vertex in the polygon. + */ + void fillConvexPolygon(ImDrawList* drawList, const std::vector& poly, const std::vector& polyColors); + + /** + * @brief Helper to get the current item's rect and optionally apply padding + */ + std::pair getItemRect(const ImVec2& padding = ImVec2(0, 0)); + + /** + * @brief Positions text centered within a rectangle + */ + ImVec2 calculateCenteredTextPosition(const std::string& text, const ImVec2& p_min, const ImVec2& p_max); +} diff --git a/editor/src/ImNexo/Widgets.cpp b/editor/src/ImNexo/Widgets.cpp new file mode 100644 index 000000000..86d71fbae --- /dev/null +++ b/editor/src/ImNexo/Widgets.cpp @@ -0,0 +1,167 @@ +//// Widgets.cpp ////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 22/02/2025 +// Description: Source file for the widgets components +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Widgets.hpp" + +#include + +#include "IconsFontAwesome.h" +#include "Nexo.hpp" +#include "Texture.hpp" +#include "components/Camera.hpp" +#include "components/Uuid.hpp" +#include "ImNexo.hpp" + +namespace ImNexo { + + bool ColorEditor( + const std::string &label, + glm::vec4 *selectedEntityColor, + ImGuiColorEditFlags *colorPickerMode, + bool *showPicker, + const ImGuiColorEditFlags colorButtonFlags + ) { + const ImGuiStyle &style = ImGui::GetStyle(); + const ImVec2 contentAvailable = ImGui::GetContentRegionAvail(); + bool colorModified = false; + + const std::string colorButton = std::string("##ColorButton") + label; + + const ImVec2 cogIconSize = ImGui::CalcTextSize(ICON_FA_COG); + const ImVec2 cogIconPadding = style.FramePadding; + const ImVec2 itemSpacing = style.ItemSpacing; + + // Color button + ColorButton( + colorButton, + ImVec2(contentAvailable.x - cogIconSize.x - cogIconPadding.x * 2 - itemSpacing.x, 0), // Make room for the cog button + ImVec4(selectedEntityColor->x, selectedEntityColor->y, selectedEntityColor->z, selectedEntityColor->w), + showPicker, + colorButtonFlags + ); + + ImGui::SameLine(); + + const std::string pickerSettings = std::string("##PickerSettings") + label; + const std::string colorPickerPopup = std::string("##ColorPickerPopup") + label; + + // Cog button + if (Button(std::string(ICON_FA_COG) + pickerSettings)) { + ImGui::OpenPopup(colorPickerPopup.c_str()); + } + + if (ImGui::BeginPopup(colorPickerPopup.c_str())) + { + ImGui::Text("Picker Mode:"); + if (ImGui::RadioButton("Hue Wheel", *colorPickerMode == ImGuiColorEditFlags_PickerHueWheel)) + *colorPickerMode = ImGuiColorEditFlags_PickerHueWheel; + if (ImGui::RadioButton("Hue bar", *colorPickerMode == ImGuiColorEditFlags_PickerHueBar)) + *colorPickerMode = ImGuiColorEditFlags_PickerHueBar; + ImGui::EndPopup(); + } + + const std::string colorPickerInline = std::string("##ColorPickerInline") + label; + if (*showPicker) + { + ImGui::Spacing(); + colorModified = ImGui::ColorPicker4(colorPickerInline.c_str(), + reinterpret_cast(selectedEntityColor), *colorPickerMode); + if (ImGui::IsItemActive()) + itemIsActive(); + if (ImGui::IsItemActivated()) + itemIsActivated(); + if (ImGui::IsItemDeactivated()) + itemIsDeactivated(); + } + return colorModified; + } + + void ButtonDropDown(const ImVec2& buttonPos, const ImVec2 buttonSize, const std::vector &buttonProps, bool &closure, DropdownOrientation orientation) + { + constexpr float buttonSpacing = 5.0f; + constexpr float padding = 10.0f; + + // Calculate menu dimensions + const float menuWidth = buttonSize.x + padding; // Add padding + const float menuHeight =static_cast(buttonProps.size()) * buttonSize.y + + (static_cast(buttonProps.size()) - 1.0f) * buttonSpacing + 2 * buttonSpacing; + + // Calculate menu position based on orientation + ImVec2 menuPos; + switch (orientation) { + case DropdownOrientation::DOWN: + menuPos = ImVec2(buttonPos.x - padding / 2.0f, buttonPos.y + buttonSize.y); + break; + case DropdownOrientation::UP: + menuPos = ImVec2(buttonPos.x - padding / 2.0f, buttonPos.y - menuHeight); + break; + case DropdownOrientation::RIGHT: + menuPos = ImVec2(buttonPos.x + buttonSize.x, buttonPos.y - padding / 2.0f); + break; + case DropdownOrientation::LEFT: + menuPos = ImVec2(buttonPos.x - menuWidth, buttonPos.y - padding / 2.0f); + break; + } + + // Adjust layout for horizontal orientations + bool isHorizontal = (orientation == DropdownOrientation::LEFT || + orientation == DropdownOrientation::RIGHT); + + // For horizontal layouts, swap width and height + ImVec2 menuSize = isHorizontal ? + ImVec2(menuHeight, buttonSize.y + 10.0f) : + ImVec2(menuWidth, menuHeight); + + ImGui::SetNextWindowPos(menuPos); + ImGui::SetNextWindowSize(menuSize); + ImGui::SetNextWindowBgAlpha(0.2f); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(5.0f, buttonSpacing)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, + isHorizontal ? ImVec2(buttonSpacing, 0) : ImVec2(0, buttonSpacing)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + + if (ImGui::Begin("##PrimitiveMenuOverlay", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize)) + { + for (const auto &button : buttonProps) + { + // Strangely here the clicked inside here does not seem to work + IconGradientButton(button.uniqueId, button.icon, ImVec2(buttonSize.x, buttonSize.y), button.buttonGradient); + // So we rely on IsItemClicked from imgui + if (button.onClick && ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + button.onClick(); + closure = false; + } + if (button.onRightClick && ImGui::IsItemClicked(ImGuiMouseButton_Right)) + { + button.onRightClick(); + } + if (!button.tooltip.empty() && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", button.tooltip.c_str()); + } + } + // Check for clicks outside to close menu + if (ImGui::IsMouseClicked(0) && !ImGui::IsWindowHovered()) + { + closure = false; + } + ImGui::End(); + + ImGui::PopStyleVar(3); + } +} diff --git a/editor/src/ImNexo/Widgets.hpp b/editor/src/ImNexo/Widgets.hpp new file mode 100644 index 000000000..a7afac28e --- /dev/null +++ b/editor/src/ImNexo/Widgets.hpp @@ -0,0 +1,100 @@ +//// Widgets.hpp ////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 22/02/2025 +// Description: Header file for the widgets components +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include + +#include "Components.hpp" +#include "components/Render3D.hpp" +#include "renderer/Texture.hpp" + +namespace ImNexo { + + /** + * @brief Draws a color editor with a button and an optional inline color picker. + * + * Displays a custom color button (with a cog icon for picker settings) and, if enabled, + * an inline color picker. The function returns true if the color was modified. + * + * @param label A unique label identifier for the widget. + * @param selectedEntityColor Pointer to the glm::vec4 representing the current color. + * @param colorPickerMode Pointer to the ImGuiColorEditFlags for the picker mode. + * @param showPicker Pointer to a boolean that determines if the inline color picker is visible. + * @param colorButtonFlags Optional flags for the color button (default is none). + * @return true if the color was modified; false otherwise. + */ + bool ColorEditor( + const std::string &label, + glm::vec4 *selectedEntityColor, + ImGuiColorEditFlags *colorPickerMode, + bool *showPicker, + ImGuiColorEditFlags colorButtonFlags = ImGuiColorEditFlags_None + ); + + /** + * @brief Configuration properties for a button in a dropdown menu. + * + * This structure defines the appearance and behavior of buttons in a + * dropdown menu created with ButtonDropDown. It allows for specifying + * icons, callbacks for different mouse actions, tooltips, and custom styling. + */ + struct ButtonProps { + std::string uniqueId; ///< Unique identifier for ImGui tracking + std::string icon; ///< Icon to display on the button (typically FontAwesome) + std::function onClick = nullptr; ///< Callback executed when button is left-clicked + std::function onRightClick = nullptr; ///< Callback executed when button is right-clicked + std::string tooltip; ///< Tooltip text displayed when hovering + + /** + * @brief Gradient colors for button styling + * + * Default gradient uses a dark blue theme that matches the editor's style. + * Override this with custom colors to create visually distinct buttons. + */ + std::vector buttonGradient = { + {0.0f, IM_COL32(50, 50, 70, 230)}, + {1.0f, IM_COL32(30, 30, 45, 230)} + }; + }; + + enum class DropdownOrientation { + DOWN, // Dropdown appears below the button + UP, // Dropdown appears above the button + RIGHT, // Dropdown appears to the right of the button + LEFT // Dropdown appears to the left of the button + }; + + /** + * @brief Creates a dropdown menu of buttons at a specified position. + * + * Displays a configurable dropdown menu containing multiple buttons defined by ButtonProps. + * The dropdown automatically closes when a button is clicked or when clicking outside + * the dropdown area. Button layout adapts based on the specified orientation. + * + * @param buttonPos Position where the dropdown should appear, typically the position of the trigger button + * @param buttonSize Size dimensions for each button in the dropdown + * @param buttonProps Vector of button configurations (icons, callbacks, tooltips, etc.) + * @param closure Reference to a boolean flag controlling dropdown visibility; set to false to close + * @param orientation Direction the dropdown should expand (DOWN, UP, LEFT, RIGHT) + */ + void ButtonDropDown( + const ImVec2& buttonPos, + ImVec2 buttonSize, + const std::vector &buttonProps, + bool &closure, + DropdownOrientation orientation = DropdownOrientation::DOWN + ); +} diff --git a/editor/src/WindowRegistry.cpp b/editor/src/WindowRegistry.cpp index 7de7dd8be..c7e88de33 100644 --- a/editor/src/WindowRegistry.cpp +++ b/editor/src/WindowRegistry.cpp @@ -48,6 +48,24 @@ namespace nexo::editor { return m_dockingRegistry.getDockId(name); } + std::shared_ptr WindowRegistry::getFocusedWindow() const + { + for (const auto &[_, windows]: m_windows) + { + for (const auto &window : windows) + { + if (window->isFocused()) + return window; + } + } + return nullptr; + } + + void WindowRegistry::resetDockId(const std::string &name) + { + m_dockingRegistry.resetDockId(name); + } + void WindowRegistry::update() const { for (const auto &[_, windows]: m_windows) diff --git a/editor/src/WindowRegistry.hpp b/editor/src/WindowRegistry.hpp index 5bf2de2e8..7c0760322 100644 --- a/editor/src/WindowRegistry.hpp +++ b/editor/src/WindowRegistry.hpp @@ -27,8 +27,35 @@ namespace nexo::editor { + /** + * @brief Helper function to cast a window from IDocumentWindow to a specific type. + * + * Used by the transform_view in getWindows() to perform the static cast from + * the base IDocumentWindow type to the requested derived type. + * + * @tparam T The derived window type to cast to + * @param ptr The shared pointer to an IDocumentWindow instance + * @return std::shared_ptr The same pointer cast to the derived type + */ template - std::shared_ptr castWindow(const std::shared_ptr& ptr) { + std::shared_ptr castWindow(const std::shared_ptr& ptr) + { + return std::static_pointer_cast(ptr); + } + + /** + * @brief Non-const version of the window casting helper function. + * + * Used by the non-const getWindows() method to cast windows from the base + * IDocumentWindow type to the requested derived type. + * + * @tparam T The derived window type to cast to + * @param ptr The shared pointer to an IDocumentWindow instance + * @return std::shared_ptr The same pointer cast to the derived type + */ + template + std::shared_ptr castWindow(std::shared_ptr& ptr) + { return std::static_pointer_cast(ptr); } @@ -60,6 +87,39 @@ namespace nexo::editor { windowsOfType.push_back(window); } + /** + * @brief Removes a window from the registry. + * + * This function searches for a window of type T with the specified name and + * removes it from the registry if found. If no window matches the criteria, + * a warning message is logged but no exception is thrown. + * + * @tparam T The concrete window type derived from IDocumentWindow. + * @param windowName The name of the window to unregister. + */ + template + requires std::derived_from + void unregisterWindow(const std::string &windowName) + { + auto it = m_windows.find(typeid(T)); + if (it == m_windows.end()) { + LOG(NEXO_WARN, "Window of type {} not found", typeid(T).name()); + return; + } + + auto &windowsOfType = it->second; + auto found = std::ranges::find_if(windowsOfType, [&windowName](const auto &w) { + return w->getWindowName() == windowName; + }); + + if (found == windowsOfType.end()) { + LOG(NEXO_WARN, "Window of type {} with name {} not found", typeid(T).name(), windowName); + return; + } + + windowsOfType.erase(found); + } + /** * @brief Retrieves a registered window of the specified type and name. * @@ -127,6 +187,37 @@ namespace nexo::editor { return std::ranges::transform_view(std::ranges::ref_view(it->second), caster); } + /** + * @brief Retrieves a mutable range view of document windows cast to a specified derived type. + * + * Similar to the const version, but returns a transform_view that allows modifying the + * underlying windows. The transformation is performed using the non-const castWindow + * helper function, which casts each std::shared_ptr to a std::shared_ptr. + * + * If no windows of the requested type T are found in m_windows, an empty range is returned. + * + * @tparam T The derived type of IDocumentWindow to retrieve. + * @return A transform_view range over the mutable vector of document windows cast to std::shared_ptr. + */ + template + requires std::derived_from + std::ranges::transform_view< + std::ranges::ref_view>>, + std::shared_ptr(*)(std::shared_ptr&) + > + getWindows() + { + // Helper: non-capturing function for casting: + std::shared_ptr(*caster)(std::shared_ptr&) = &castWindow; + + auto it = m_windows.find(typeid(T)); + if (it == m_windows.end()) { + static std::vector> empty; + return std::ranges::transform_view(std::ranges::ref_view(empty), caster); + } + return std::ranges::transform_view(std::ranges::ref_view(it->second), caster); + } + /** * @brief Assigns a docking identifier to a window. * @@ -148,6 +239,19 @@ namespace nexo::editor { */ std::optional getDockId(const std::string& name) const; + std::shared_ptr getFocusedWindow() const; + + /** + * @brief Removes a window's docking identifier. + * + * This function removes any docking identifier association for the specified window, + * allowing it to be positioned freely or receive a new docking assignment. + * If no docking ID exists for the window, this operation has no effect. + * + * @param name The name of the window whose docking ID should be removed. + */ + void resetDockId(const std::string &name); + /** * @brief Initializes all managed windows. * diff --git a/editor/src/backends/ImGuiBackend.cpp b/editor/src/backends/ImGuiBackend.cpp index 31ac39280..82543d58d 100644 --- a/editor/src/backends/ImGuiBackend.cpp +++ b/editor/src/backends/ImGuiBackend.cpp @@ -14,15 +14,15 @@ #include "ImGuiBackend.hpp" #include "exceptions/Exceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/openglImGuiBackend.hpp" #endif namespace nexo::editor { - void ImGuiBackend::init([[maybe_unused]] const std::shared_ptr& window) + void ImGuiBackend::init([[maybe_unused]] const std::shared_ptr& window) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL OpenGLImGuiBackend::init(static_cast(window->window())); return; #endif @@ -31,7 +31,7 @@ namespace nexo::editor { void ImGuiBackend::shutdown() { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL OpenGLImGuiBackend::shutdown(); return; #endif @@ -40,7 +40,7 @@ namespace nexo::editor { void ImGuiBackend::initFontAtlas() { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL OpenGLImGuiBackend::initFontAtlas(); return; #endif @@ -49,25 +49,25 @@ namespace nexo::editor { void ImGuiBackend::begin() { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL OpenGLImGuiBackend::begin(); return; #endif THROW_EXCEPTION(BackendRendererApiNotSupported, "UNKNOWN"); } - void ImGuiBackend::end([[maybe_unused]] const std::shared_ptr& window) + void ImGuiBackend::end([[maybe_unused]] const std::shared_ptr& window) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL OpenGLImGuiBackend::end(static_cast(window->window())); return; #endif THROW_EXCEPTION(BackendRendererApiNotSupported, "UNKNOWN"); } - void ImGuiBackend::setErrorCallback([[maybe_unused]] const std::shared_ptr &window) + void ImGuiBackend::setErrorCallback([[maybe_unused]] const std::shared_ptr &window) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL const auto callback = OpenGLImGuiBackend::getErrorCallback(); window->setErrorCallback(callback); return; diff --git a/editor/src/backends/ImGuiBackend.hpp b/editor/src/backends/ImGuiBackend.hpp index bb3f1d80d..b1d1cbce9 100644 --- a/editor/src/backends/ImGuiBackend.hpp +++ b/editor/src/backends/ImGuiBackend.hpp @@ -33,7 +33,7 @@ namespace nexo::editor { * @param[in] window The application window to initialize ImGui with * @throws BackendRendererApiNotSupported If the current graphics API is not supported */ - static void init(const std::shared_ptr& window); + static void init(const std::shared_ptr& window); /** * @brief Shuts down and cleans up the ImGui backend @@ -68,7 +68,7 @@ namespace nexo::editor { * @param[in] window The application window to render ImGui to * @throws BackendRendererApiNotSupported If the current graphics API is not supported */ - static void end(const std::shared_ptr& window); + static void end(const std::shared_ptr& window); /** * @brief Sets up the error callback for ImGui on the window @@ -76,6 +76,6 @@ namespace nexo::editor { * @param[in] window The application window to set the error callback for * @throws BackendRendererApiNotSupported If the current graphics API is not supported */ - static void setErrorCallback(const std::shared_ptr& window); + static void setErrorCallback(const std::shared_ptr& window); }; } diff --git a/editor/src/context/ActionGroup.cpp b/editor/src/context/ActionGroup.cpp new file mode 100644 index 000000000..31dfa00fa --- /dev/null +++ b/editor/src/context/ActionGroup.cpp @@ -0,0 +1,42 @@ +//// ActionGroup.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Source file for the action group class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ActionGroup.hpp" + +#include + +namespace nexo::editor { + + void ActionGroup::addAction(std::unique_ptr action) + { + actions.push_back(std::move(action)); + } + + bool ActionGroup::hasActions() const + { + return !actions.empty(); + } + + void ActionGroup::redo() + { + for (const auto &action : actions) + action->redo(); + } + + void ActionGroup::undo() + { + for (const auto &action : std::ranges::reverse_view(actions)) + action->undo(); + } +} diff --git a/editor/src/context/ActionGroup.hpp b/editor/src/context/ActionGroup.hpp new file mode 100644 index 000000000..a09e93243 --- /dev/null +++ b/editor/src/context/ActionGroup.hpp @@ -0,0 +1,38 @@ +//// ActionGroup.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Header file for the action group class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "actions/Action.hpp" +#include +#include + +namespace nexo::editor { + + /** + * Groups multiple actions into a single undoable action + */ + class ActionGroup final : public Action { + public: + ActionGroup() = default; + + void addAction(std::unique_ptr action); + [[nodiscard]] bool hasActions() const; + void redo() override; + void undo() override; + + private: + std::vector> actions; + }; + +} diff --git a/editor/src/context/ActionHistory.cpp b/editor/src/context/ActionHistory.cpp new file mode 100644 index 000000000..61a1b3728 --- /dev/null +++ b/editor/src/context/ActionHistory.cpp @@ -0,0 +1,80 @@ +//// ActionHistory.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Source file for the action history class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ActionHistory.hpp" + +namespace nexo::editor { + void ActionHistory::addAction(std::unique_ptr action) + { + undoStack.push_back(std::move(action)); + redoStack.clear(); + + while (undoStack.size() > maxUndoLevels) + undoStack.pop_front(); + } + + bool ActionHistory::canUndo() const + { + return !undoStack.empty(); + } + + bool ActionHistory::canRedo() const + { + return !redoStack.empty(); + } + + void ActionHistory::undo() + { + if (!canUndo()) + return; + auto action = std::move(undoStack.back()); + undoStack.pop_back(); + action->undo(); + redoStack.push_back(std::move(action)); + } + + void ActionHistory::redo() + { + if (!canRedo()) + return; + auto action = std::move(redoStack.back()); + redoStack.pop_back(); + action->redo(); + undoStack.push_back(std::move(action)); + } + + void ActionHistory::setMaxUndoLevels(size_t levels) + { + maxUndoLevels = levels; + while (undoStack.size() > maxUndoLevels) + undoStack.pop_front(); + } + + void ActionHistory::clear(unsigned int count) + { + if (!count) { + undoStack.clear(); + redoStack.clear(); + return; + } + const unsigned int elementsToRemove = std::min(static_cast(undoStack.size()), count); + for (unsigned int i = 0; i < elementsToRemove; ++i) + undoStack.pop_back(); + } + + unsigned int ActionHistory::getUndoStackSize() const + { + return static_cast(undoStack.size()); + } +} diff --git a/editor/src/context/ActionHistory.hpp b/editor/src/context/ActionHistory.hpp new file mode 100644 index 000000000..3cfcbc65b --- /dev/null +++ b/editor/src/context/ActionHistory.hpp @@ -0,0 +1,46 @@ +//// ActionHistory.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Header file for the action history class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "actions/Action.hpp" +#include +#include + +namespace nexo::editor { + + /** + * Maintains the undo and redo stacks + */ + class ActionHistory { + public: + // Add a action to history after it was already executed + void addAction(std::unique_ptr action); + + [[nodiscard]] bool canUndo() const; + [[nodiscard]] bool canRedo() const; + void undo(); + void redo(); + + void setMaxUndoLevels(size_t levels); + + void clear(unsigned int count = 0); + [[nodiscard]] unsigned int getUndoStackSize() const; + + private: + std::deque> undoStack; + std::deque> redoStack; + size_t maxUndoLevels = 50; + }; + +} diff --git a/editor/src/context/ActionManager.cpp b/editor/src/context/ActionManager.cpp new file mode 100644 index 000000000..54416b6b8 --- /dev/null +++ b/editor/src/context/ActionManager.cpp @@ -0,0 +1,68 @@ +//// ActionManager.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Source file for the action manager class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ActionManager.hpp" +#include "context/actions/EntityActions.hpp" + +namespace nexo::editor { + void ActionManager::recordAction(std::unique_ptr action) + { + history.addAction(std::move(action)); + } + + void ActionManager::recordEntityCreation(ecs::Entity entityId) + { + recordAction(std::make_unique(entityId)); + } + + std::unique_ptr ActionManager::prepareEntityDeletion(ecs::Entity entityId) + { + return std::make_unique(entityId); + } + + std::unique_ptr ActionManager::createActionGroup() + { + return std::make_unique(); + } + + void ActionManager::undo() + { + history.undo(); + } + + void ActionManager::redo() + { + history.redo(); + } + + bool ActionManager::canUndo() const + { + return history.canUndo(); + } + + bool ActionManager::canRedo() const + { + return history.canRedo(); + } + + void ActionManager::clearHistory(const unsigned int count) + { + history.clear(count); + } + + unsigned int ActionManager::getUndoStackSize() const + { + return history.getUndoStackSize(); + } +} diff --git a/editor/src/context/ActionManager.hpp b/editor/src/context/ActionManager.hpp new file mode 100644 index 000000000..35fccdb88 --- /dev/null +++ b/editor/src/context/ActionManager.hpp @@ -0,0 +1,61 @@ +//// ActionManager.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Header file for the action manager class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "ActionHistory.hpp" +#include "ActionGroup.hpp" +#include "context/actions/EntityActions.hpp" +#include + +namespace nexo::editor { + + class ActionManager { + public: + // Record a command after an operation is done + void recordAction(std::unique_ptr action); + // Record entity creation + void recordEntityCreation(ecs::Entity entityId); + // Record entity deletion (call before actually deleting) + static std::unique_ptr prepareEntityDeletion(ecs::Entity entityId); + + // For component changes using memento pattern + template + void recordComponentChange(ecs::Entity entityId, + const typename MementoComponent::Memento& beforeState, + const typename MementoComponent::Memento& afterState) + { + auto action = std::make_unique>(entityId, beforeState, afterState); + recordAction(std::move(action)); + } + + // Action group for multiple operations + static std::unique_ptr createActionGroup(); + + // Basic undo/redo operations + void undo(); + void redo(); + [[nodiscard]] bool canUndo() const; + [[nodiscard]] bool canRedo() const; + void clearHistory(unsigned int count = 0); + [[nodiscard]] unsigned int getUndoStackSize() const; + + static ActionManager& get() { + static ActionManager instance; + return instance; + } + private: + ActionHistory history; + }; + +} diff --git a/editor/src/context/Selector.cpp b/editor/src/context/Selector.cpp index c48891bb3..d35d5d018 100644 --- a/editor/src/context/Selector.cpp +++ b/editor/src/context/Selector.cpp @@ -13,70 +13,167 @@ /////////////////////////////////////////////////////////////////////////////// #include "Selector.hpp" +#include "Application.hpp" +#include "components/Editor.hpp" namespace nexo::editor { - int Selector::getSelectedEntity() const - { - return m_selectedEntity; - } - - const std::string &Selector::getSelectedUuid() const - { - return m_selectedUuid; - } - - void Selector::setSelectedEntity(std::string_view uuid, const int entity) - { - m_selectedUuid = uuid; - m_selectedEntity = entity; - } - - void Selector::setSelectedScene(int scene) - { - m_selectedScene = scene; - } - - int Selector::getSelectedScene() const - { - return m_selectedScene; - } - - void Selector::unselectEntity() - { - m_selectionType = SelectionType::NONE; - m_selectedEntity = -1; - m_selectedUuid = ""; - } - - SelectionType Selector::getSelectionType() const - { - return m_selectionType; - } - - void Selector::setSelectionType(SelectionType type) - { - m_selectionType = type; - } - - bool Selector::isEntitySelected() const - { - return (m_selectedEntity != -1); - } - - const std::string &Selector::getUiHandle(const std::string &uuid, const std::string &defaultHandle) - { - const auto it = m_uiHandles.find(uuid); - if (it == m_uiHandles.end()) - { - m_uiHandles[uuid] = defaultHandle; - return defaultHandle; - } - return it->second; - } - - void Selector::setUiHandle(const std::string &uuid, std::string_view handle) - { - m_uiHandles[uuid] = handle; - } + int Selector::getPrimaryEntity() const + { + if (m_selectedEntities.empty()) { + return -1; + } + return m_selectedEntities.front().entityId; // First entity is the primary + } + + const std::vector& Selector::getSelectedEntities() const + { + static std::vector entityIds; + entityIds.clear(); + + for (const auto& data : m_selectedEntities) { + entityIds.push_back(data.entityId); + } + + return entityIds; + } + + const std::string& Selector::getPrimaryUuid() const + { + static std::string emptyString; + if (m_selectedEntities.empty()) { + return emptyString; + } + return m_selectedEntities.front().uuid; + } + + std::vector Selector::getSelectedUuids() const + { + std::vector uuids; + uuids.reserve(m_selectedEntities.size()); + + for (const auto& data : m_selectedEntities) { + uuids.push_back(data.uuid); + } + + return uuids; + } + + void Selector::selectEntity(const std::string_view uuid, const int entity, const SelectionType type) + { + clearSelection(); + addToSelection(uuid, entity, type); + } + + bool Selector::addToSelection(const std::string_view uuid, const int entity, const SelectionType type) + { + if (m_selectedEntityIds.contains(entity)) + return false; + + SelectionData data = { + .entityId = entity, + .uuid = std::string(uuid), + .type = type + }; + m_selectedEntities.push_back(std::move(data)); + m_selectedEntityIds.insert(entity); + + addSelectedTag(entity); + return true; + } + + bool Selector::toggleSelection(const std::string_view uuid, const int entity, const SelectionType type) + { + if (isEntitySelected(entity)) { + removeFromSelection(entity); + return false; + } + addToSelection(uuid, entity, type); + return true; + } + + bool Selector::removeFromSelection(const int entity) { + if (!m_selectedEntityIds.contains(entity)) + return false; + + m_selectedEntityIds.erase(entity); + for (auto it = m_selectedEntities.begin(); it != m_selectedEntities.end(); ++it) { + if (it->entityId == entity) { + m_selectedEntities.erase(it); + break; + } + } + + removeSelectedTag(entity); + return true; + } + + void Selector::setSelectedScene(const int scene) + { + m_selectedScene = scene; + } + + int Selector::getSelectedScene() const + { + return m_selectedScene; + } + + void Selector::clearSelection() + { + for (const auto& data : m_selectedEntities) + removeSelectedTag(data.entityId); + + m_selectedEntities.clear(); + m_selectedEntityIds.clear(); + } + + bool Selector::isEntitySelected(const int entity) const + { + return m_selectedEntityIds.contains(entity); + } + + bool Selector::hasSelection() const + { + return !m_selectedEntities.empty(); + } + + SelectionType Selector::getPrimarySelectionType() const + { + if (m_selectedEntities.empty()) { + return SelectionType::NONE; + } + return m_selectedEntities.front().type; + } + + void Selector::setSelectionType(const SelectionType type) + { + m_defaultSelectionType = type; + } + + const std::string& Selector::getUiHandle(const std::string& uuid, const std::string& defaultHandle) + { + const auto it = m_uiHandles.find(uuid); + if (it == m_uiHandles.end()) { + m_uiHandles[uuid] = defaultHandle; + return defaultHandle; + } + return it->second; + } + + void Selector::setUiHandle(const std::string& uuid, const std::string_view handle) + { + m_uiHandles[uuid] = handle; + } + + void Selector::addSelectedTag(const int entity) + { + constexpr components::SelectedTag selectTag{}; + Application::m_coordinator->addComponent(entity, selectTag); + } + + void Selector::removeSelectedTag(const int entity) + { + if (Application::m_coordinator->entityHasComponent(entity)) + Application::m_coordinator->removeComponent(entity); + } } diff --git a/editor/src/context/Selector.hpp b/editor/src/context/Selector.hpp index 4929defa9..2a1b76d7a 100644 --- a/editor/src/context/Selector.hpp +++ b/editor/src/context/Selector.hpp @@ -16,99 +16,208 @@ #include "ecs/Coordinator.hpp" #include +#include +#include +#include namespace nexo::editor { - enum class SelectionType { - NONE, - SCENE, - CAMERA, - DIR_LIGHT, - AMBIENT_LIGHT, - SPOT_LIGHT, - POINT_LIGHT, - ENTITY - }; - - /** + enum class SelectionType { + NONE, + SCENE, + CAMERA, + DIR_LIGHT, + AMBIENT_LIGHT, + SPOT_LIGHT, + POINT_LIGHT, + ENTITY + }; + + /** * @class Selector * @brief Singleton class managing entity selection state in the editor * - * The Selector class tracks the currently selected entity, its type, and + * The Selector class tracks the currently selected entities, their types, and * provides methods to manipulate the selection state. It also maintains * entity UUID to UI handle mappings for consistent labeling in the interface. */ - class Selector { - public: - int getSelectedEntity() const; - const std::string &getSelectedUuid() const; - void setSelectedEntity(std::string_view uuid, int entity); - - /** - * @brief Sets the currently selected scene - * - * @param[in] scene The scene entity ID to select - */ - void setSelectedScene(int scene); - - /** - * @brief Gets the currently selected scene - * - * @return int The entity ID of the selected scene, or -1 if no scene is selected - */ - int getSelectedScene() const; - - void unselectEntity(); - - SelectionType getSelectionType() const; - void setSelectionType(SelectionType type); - - bool isEntitySelected() const; - - /** - * @brief Gets the UI handle associated with a UUID - * - * If the UUID doesn't have an associated handle yet, the default - * handle is stored and returned. - * - * @param[in] uuid The UUID to look up - * @param[in] defaultHandle The default handle to use if none exists - * @return const std::string& Reference to the UI handle for the UUID - */ - const std::string &getUiHandle(const std::string &uuid, const std::string &defaultHandle); - - /** - * @brief Sets the UI handle associated with a UUID - * - * @param[in] uuid The UUID to set the handle for - * @param[in] handle The handle to set - */ - void setUiHandle(const std::string &uuid, std::string_view handle); - - static Selector &get() - { - static Selector instance; - return instance; - } - - private: - std::string m_selectedUuid; - int m_selectedEntity = -1; - int m_selectedScene = -1; - SelectionType m_selectionType = SelectionType::NONE; - - struct TransparentHasher { - using is_transparent = void; // Marks this hasher as transparent for heterogeneous lookup - - size_t operator()(std::string_view key) const noexcept { - return std::hash{}(key); - } - - size_t operator()(const std::string &key) const noexcept { - return std::hash{}(key); - } - }; - - std::unordered_map> m_uiHandles; - }; + class Selector { + public: + /** + * @brief Gets the primary selected entity + * + * The primary entity is the entity that gizmos and other operations + * will primarily act on when multiple entities are selected. + * This returns the last entity selected if there are multiple selections. + * + * @return int The entity ID of the primary selection, or -1 if no entity is selected + */ + int getPrimaryEntity() const; + + /** + * @brief Gets all selected entities + * + * @return const std::vector& A reference to the vector of all selected entity IDs + */ + const std::vector& getSelectedEntities() const; + + /** + * @brief Gets the UUID of the primary entity + * + * @return const std::string& The UUID of the primary entity + */ + const std::string& getPrimaryUuid() const; + + /** + * @brief Gets all selected entity UUIDs + * + * @return std::vector Vector containing UUIDs of all selected entities + */ + std::vector getSelectedUuids() const; + + /** + * @brief Selects a single entity, replacing the current selection + * + * @param[in] uuid The UUID of the entity to select + * @param[in] entity The entity ID to select + * @param[in] type The type of entity being selected + */ + void selectEntity(std::string_view uuid, int entity, SelectionType type = SelectionType::ENTITY); + + /** + * @brief Adds an entity to the current selection + * + * @param[in] uuid The UUID of the entity to add + * @param[in] entity The entity ID to add + * @param[in] type The type of entity being added + * @return true If the entity was successfully added to the selection + * @return false If the entity was already selected + */ + bool addToSelection(std::string_view uuid, int entity, SelectionType type = SelectionType::ENTITY); + + /** + * @brief Toggle selection state of an entity + * + * @param[in] uuid The UUID of the entity to toggle + * @param[in] entity The entity ID to toggle + * @param[in] type The type of entity being toggled + * @return true If the entity is now selected + * @return false If the entity is now deselected + */ + bool toggleSelection(std::string_view uuid, int entity, SelectionType type = SelectionType::ENTITY); + + /** + * @brief Removes an entity from the selection + * + * @param[in] entity The entity ID to remove + * @return true If the entity was successfully removed from the selection + * @return false If the entity wasn't selected + */ + bool removeFromSelection(int entity); + + /** + * @brief Sets the currently selected scene + * + * @param[in] scene The scene entity ID to select + */ + void setSelectedScene(int scene); + + /** + * @brief Gets the currently selected scene + * + * @return int The entity ID of the selected scene, or -1 if no scene is selected + */ + int getSelectedScene() const; + + /** + * @brief Clears the current entity selection + */ + void clearSelection(); + + /** + * @brief Checks if a specific entity is currently selected + * + * @param entity The entity ID to check + * @return true If the entity is selected + * @return false If the entity is not selected + */ + bool isEntitySelected(int entity) const; + + /** + * @brief Checks if any entity is currently selected + * + * @return true If at least one entity is selected + * @return false If no entities are selected + */ + bool hasSelection() const; + + /** + * @brief Gets the primary selection type + * + * @return SelectionType The type of the primary selected entity + */ + SelectionType getPrimarySelectionType() const; + + /** + * @brief Sets the selection type (only applied to subsequent selections) + * + * @param type The selection type to set + */ + void setSelectionType(SelectionType type); + + /** + * @brief Gets the UI handle associated with a UUID + * + * If the UUID doesn't have an associated handle yet, the default + * handle is stored and returned. + * + * @param[in] uuid The UUID to look up + * @param[in] defaultHandle The default handle to use if none exists + * @return const std::string& Reference to the UI handle for the UUID + */ + const std::string& getUiHandle(const std::string& uuid, const std::string& defaultHandle); + + /** + * @brief Sets the UI handle associated with a UUID + * + * @param[in] uuid The UUID to set the handle for + * @param[in] handle The handle to set + */ + void setUiHandle(const std::string& uuid, std::string_view handle); + + static Selector& get() { + static Selector instance; + return instance; + } + + private: + // Selection data + struct SelectionData { + int entityId; + std::string uuid; + SelectionType type; + }; + std::vector m_selectedEntities; // Ordered list of selected entities + std::unordered_set m_selectedEntityIds; // Set for quick lookups + + int m_selectedScene = -1; + SelectionType m_defaultSelectionType = SelectionType::ENTITY; + + struct TransparentHasher { + using is_transparent = void; // Marks this hasher as transparent for heterogeneous lookup + + size_t operator()(std::string_view key) const noexcept { + return std::hash{}(key); + } + + size_t operator()(const std::string& key) const noexcept { + return std::hash{}(key); + } + }; + + std::unordered_map> m_uiHandles; + + static void addSelectedTag(int entity); + static void removeSelectedTag(int entity); + }; } diff --git a/editor/src/context/actions/Action.hpp b/editor/src/context/actions/Action.hpp new file mode 100644 index 000000000..256481172 --- /dev/null +++ b/editor/src/context/actions/Action.hpp @@ -0,0 +1,30 @@ +//// Action.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Header file for the action interface +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +namespace nexo::editor { + + /** + * Base Action interface for all undoable operations + */ + class Action { + public: + virtual ~Action() = default; + + virtual void redo() = 0; + + virtual void undo() = 0; + }; + +} diff --git a/editor/src/context/actions/ComponentRestoreFactory.cpp b/editor/src/context/actions/ComponentRestoreFactory.cpp new file mode 100644 index 000000000..237f0e28d --- /dev/null +++ b/editor/src/context/actions/ComponentRestoreFactory.cpp @@ -0,0 +1,51 @@ +//// ComponentRestoreFactory.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 30/04/2025 +// Description: Source file for the component restore action factory +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ComponentRestoreFactory.hpp" +#include "EntityActions.hpp" +#include "components/Camera.hpp" +#include "components/Light.hpp" +#include "components/Render.hpp" +#include "components/Transform.hpp" +#include "components/Uuid.hpp" + +namespace nexo::editor { + + std::unique_ptr ComponentRestoreFactory::createRestoreComponent(ecs::Entity entity, const std::type_index typeIndex) + { + if (typeIndex == typeid(components::TransformComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::RenderComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::SceneTag)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::CameraComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::AmbientLightComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::DirectionalLightComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::PointLightComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::SpotLightComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::UuidComponent)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::PerspectiveCameraController)) + return std::make_unique>(entity); + if (typeIndex == typeid(components::PerspectiveCameraTarget)) + return std::make_unique>(entity); + return nullptr; + } +} diff --git a/editor/src/context/actions/ComponentRestoreFactory.hpp b/editor/src/context/actions/ComponentRestoreFactory.hpp new file mode 100644 index 000000000..412256163 --- /dev/null +++ b/editor/src/context/actions/ComponentRestoreFactory.hpp @@ -0,0 +1,26 @@ +//// ComponentRestoreFactory.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 30/04/2025 +// Description: Header file for the restore component action factory +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Action.hpp" +#include "ecs/Definitions.hpp" +#include +#include + +namespace nexo::editor { + class ComponentRestoreFactory { + public: + static std::unique_ptr createRestoreComponent(ecs::Entity entity, std::type_index typeIndex); + }; +} diff --git a/editor/src/context/actions/EntityActions.cpp b/editor/src/context/actions/EntityActions.cpp new file mode 100644 index 000000000..455ec79a4 --- /dev/null +++ b/editor/src/context/actions/EntityActions.cpp @@ -0,0 +1,67 @@ +//// EntityActions.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/04/2025 +// Description: Source file for the entity actions +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EntityActions.hpp" +#include "ComponentRestoreFactory.hpp" + +namespace nexo::editor { + + void EntityCreationAction::redo() + { + const auto &coordinator = Application::m_coordinator; + m_entityId = coordinator->createEntity(); + + for (const auto &action : m_componentRestoreActions) + action->undo(); + } + + void EntityCreationAction::undo() + { + const auto &coordinator = Application::m_coordinator; + const std::vector& componentsTypeIndex = coordinator->getAllComponentTypeIndices(m_entityId); + for (const auto typeIndex : componentsTypeIndex) { + if (!coordinator->supportsMementoPattern(typeIndex)) + continue; + m_componentRestoreActions.push_back(ComponentRestoreFactory::createRestoreComponent(m_entityId, typeIndex)); + } + coordinator->destroyEntity(m_entityId); + } + + EntityDeletionAction::EntityDeletionAction(const ecs::Entity entityId) : m_entityId(entityId) + { + const auto &coordinator = Application::m_coordinator; + std::vector componentsTypeIndex = coordinator->getAllComponentTypeIndices(entityId); + for (const auto typeIndex : componentsTypeIndex) { + if (!coordinator->supportsMementoPattern(typeIndex)) + continue; + m_componentRestoreActions.push_back(ComponentRestoreFactory::createRestoreComponent(entityId, typeIndex)); + } + } + + void EntityDeletionAction::redo() + { + // Simply destroy the entity + const auto& coordinator = Application::m_coordinator; + coordinator->destroyEntity(m_entityId); + } + + void EntityDeletionAction::undo() + { + const auto& coordinator = Application::m_coordinator; + // This can cause problem is the entity is not the same, maybe in the future we would need another method + m_entityId = coordinator->createEntity(); + for (const auto &action : m_componentRestoreActions) + action->undo(); + } +} diff --git a/editor/src/context/actions/EntityActions.hpp b/editor/src/context/actions/EntityActions.hpp new file mode 100644 index 000000000..e23bd09e0 --- /dev/null +++ b/editor/src/context/actions/EntityActions.hpp @@ -0,0 +1,143 @@ +// EntityActions.hpp +#pragma once + +#include "Action.hpp" +#include "Nexo.hpp" + +namespace nexo::editor { + + template + class ComponentRestoreAction final : public Action { + public: + explicit ComponentRestoreAction(const ecs::Entity entity) : m_entity(entity) + { + ComponentType &target = Application::m_coordinator->getComponent(m_entity); + m_memento = target.save(); + }; + + void undo() override + { + ComponentType target; + target.restore(m_memento); + Application::m_coordinator->addComponent(m_entity, target); + } + + void redo() override + { + //We have nothing to do here since we are simply redeleting the entity and its components + } + + private: + ecs::Entity m_entity; + typename ComponentType::Memento m_memento; + }; + + template + class ComponentAddAction final : public Action { + public: + explicit ComponentAddAction(const ecs::Entity entity) + : m_entity(entity) {} + + void undo() override + { + m_memento = Application::m_coordinator->getComponent(m_entity).save(); + Application::m_coordinator->removeComponent(m_entity); + } + + void redo() override + { + ComponentType target; + target.restore(m_memento); + Application::m_coordinator->addComponent(m_entity, target); + } + + private: + ecs::Entity m_entity; + typename ComponentType::Memento m_memento; + }; + + template + class ComponentRemoveAction final : public Action { + public: + explicit ComponentRemoveAction(const ecs::Entity entity) : m_entity(entity) + { + m_memento = Application::m_coordinator->getComponent(m_entity).save(); + } + + void undo() override + { + ComponentType target; + target.restore(m_memento); + Application::m_coordinator->addComponent(m_entity, target); + } + + void redo() override + { + Application::m_coordinator->removeComponent(m_entity); + } + + private: + ecs::Entity m_entity; + typename ComponentType::Memento m_memento; + }; + + template + class ComponentChangeAction final : public Action { + public: + explicit ComponentChangeAction( + const ecs::Entity entity, + const typename ComponentType::Memento& before, + const typename ComponentType::Memento& after + ) : m_entity(entity), m_beforeState(before), m_afterState(after){} + + void redo() override + { + ComponentType &target = Application::m_coordinator->getComponent(m_entity); + target.restore(m_afterState); + } + + void undo() override + { + ComponentType &target = Application::m_coordinator->getComponent(m_entity); + target.restore(m_beforeState); + } + + private: + ecs::Entity m_entity; + typename ComponentType::Memento m_beforeState; + typename ComponentType::Memento m_afterState; + }; + + /** + * Stores information needed to undo/redo entity creation + * Relies on engine systems for actual creation/deletion logic + */ + class EntityCreationAction final : public Action { + public: + explicit EntityCreationAction(const ecs::Entity entityId) + : m_entityId(entityId) {} + + void redo() override; + void undo() override; + + private: + ecs::Entity m_entityId; + std::vector> m_componentRestoreActions; + }; + + /** + * Stores information needed to undo/redo entity deletion + * Relies on engine systems for actual deletion logic + */ + class EntityDeletionAction final : public Action { + public: + explicit EntityDeletionAction(ecs::Entity entityId); + + void redo() override; + void undo() override; + private: + ecs::Entity m_entityId; + std::vector> m_componentRestoreActions; + }; + +} diff --git a/editor/src/context/actions/StateAction.hpp b/editor/src/context/actions/StateAction.hpp new file mode 100644 index 000000000..fbac24d4c --- /dev/null +++ b/editor/src/context/actions/StateAction.hpp @@ -0,0 +1,52 @@ +//// StateAction.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 26/04/2025 +// Description: Header file for the state action class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Action.hpp" + +#include +#include + +namespace nexo::editor { + + /** + * Generic action for objects that can save and restore their state + * Object must implement save() and restore() methods + */ + template + class StateAction : public Action { + public: + StateAction( + T& target, + const typename T::Memento& before, + const typename T::Memento& after + ) : m_target(target), m_beforeState(before), m_afterState(after){} + + void redo() override + { + m_target = m_afterState.restore(); + } + + void undo() override + { + m_target = m_beforeState.restore(); + } + + private: + T& m_target; + typename T::Memento m_beforeState; + typename T::Memento m_afterState; + }; + +} diff --git a/editor/src/exceptions/Exceptions.hpp b/editor/src/exceptions/Exceptions.hpp index fe3ab5361..fb95d9b17 100644 --- a/editor/src/exceptions/Exceptions.hpp +++ b/editor/src/exceptions/Exceptions.hpp @@ -38,6 +38,20 @@ namespace nexo::editor { : Exception("File not found: " + filePath, loc) {} }; + class FileReadException final : public Exception { + public: + explicit FileReadException(const std::string &filePath, const std::string &message, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Error reading file {}: {}", filePath, message), loc) {} + }; + + class FileWriteException final : public Exception { + public: + explicit FileWriteException(const std::string &filePath, const std::string &message, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Error writing to file {}: {}", filePath, message), loc) {} + }; + class WindowNotRegistered final : public Exception { public: /** @@ -104,4 +118,11 @@ namespace nexo::editor { : Exception("[" + backendApiName + " FATAL ERROR]" + message, loc) {} }; + class InvalidTestFileFormat final : public Exception { + public: + explicit InvalidTestFileFormat(const std::string &filePath, const std::string &message, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Invalid test file protocol format {}: {}", filePath, message), loc) {} + }; + } diff --git a/editor/src/inputs/Command.cpp b/editor/src/inputs/Command.cpp new file mode 100644 index 000000000..e2722b1cb --- /dev/null +++ b/editor/src/inputs/Command.cpp @@ -0,0 +1,202 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 23/05/2025 +// Description: Source file for the input command +// +/////////////////////////////////////////////////////////////////////////////// + +#include "Command.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace nexo::editor { + + struct StringHash { + using is_transparent = void; // enable heterogeneous lookup + size_t operator()(std::string_view sv) const noexcept { + return std::hash{}(sv); + } + size_t operator()(const std::string &s) const noexcept { + return operator()(std::string_view(s)); + } + }; + + // 2) Transparent equal + struct StringEqual { + using is_transparent = void; // enable heterogeneous lookup + bool operator()(std::string_view a, std::string_view b) const noexcept { + return a == b; + } + bool operator()(const std::string &a, const std::string &b) const noexcept { + return a == b; + } + }; + + Command::Command( + std::string description, + const std::string &key, + const std::function &pressedCallback, + const std::function &releaseCallback, + const std::function &repeatCallback, + bool isModifier, + const std::vector &childrens) + : m_description(std::move(description)), m_key(key), m_pressedCallback(pressedCallback), m_releaseCallback(releaseCallback), m_repeatCallback(repeatCallback), m_isModifier(isModifier), m_childrens(childrens) + { + // Create a mapping of key names to ImGuiKey values + static const std::unordered_map keyMap = { + // Common modifiers + {"ctrl", ImGuiKey_LeftCtrl}, + {"control", ImGuiKey_LeftCtrl}, + {"shift", ImGuiKey_LeftShift}, + {"alt", ImGuiKey_LeftAlt}, + {"super", ImGuiKey_LeftSuper}, + {"cmd", ImGuiKey_LeftSuper}, + {"win", ImGuiKey_LeftSuper}, + + // Alphanumeric keys + {"a", ImGuiKey_A}, {"b", ImGuiKey_B}, {"c", ImGuiKey_C}, {"d", ImGuiKey_D}, + {"e", ImGuiKey_E}, {"f", ImGuiKey_F}, {"g", ImGuiKey_G}, {"h", ImGuiKey_H}, + {"i", ImGuiKey_I}, {"j", ImGuiKey_J}, {"k", ImGuiKey_K}, {"l", ImGuiKey_L}, + {"m", ImGuiKey_M}, {"n", ImGuiKey_N}, {"o", ImGuiKey_O}, {"p", ImGuiKey_P}, + {"q", ImGuiKey_Q}, {"r", ImGuiKey_R}, {"s", ImGuiKey_S}, {"t", ImGuiKey_T}, + {"u", ImGuiKey_U}, {"v", ImGuiKey_V}, {"w", ImGuiKey_W}, {"x", ImGuiKey_X}, + {"y", ImGuiKey_Y}, {"z", ImGuiKey_Z}, + + // Numbers + {"0", ImGuiKey_0}, {"1", ImGuiKey_1}, {"2", ImGuiKey_2}, {"3", ImGuiKey_3}, + {"4", ImGuiKey_4}, {"5", ImGuiKey_5}, {"6", ImGuiKey_6}, {"7", ImGuiKey_7}, + {"8", ImGuiKey_8}, {"9", ImGuiKey_9}, + + // Function keys + {"f1", ImGuiKey_F1}, {"f2", ImGuiKey_F2}, {"f3", ImGuiKey_F3}, {"f4", ImGuiKey_F4}, + {"f5", ImGuiKey_F5}, {"f6", ImGuiKey_F6}, {"f7", ImGuiKey_F7}, {"f8", ImGuiKey_F8}, + {"f9", ImGuiKey_F9}, {"f10", ImGuiKey_F10}, {"f11", ImGuiKey_F11}, {"f12", ImGuiKey_F12}, + + // Special keys + {"space", ImGuiKey_Space}, + {"enter", ImGuiKey_Enter}, + {"return", ImGuiKey_Enter}, + {"escape", ImGuiKey_Escape}, + {"esc", ImGuiKey_Escape}, + {"tab", ImGuiKey_Tab}, + {"backspace", ImGuiKey_Backspace}, + {"delete", ImGuiKey_Delete}, + {"insert", ImGuiKey_Insert}, + {"home", ImGuiKey_Home}, + {"end", ImGuiKey_End}, + {"pageup", ImGuiKey_PageUp}, + {"pagedown", ImGuiKey_PageDown}, + {"up", ImGuiKey_UpArrow}, + {"down", ImGuiKey_DownArrow}, + {"left", ImGuiKey_LeftArrow}, + {"right", ImGuiKey_RightArrow}, + {"capslock", ImGuiKey_CapsLock}, + {"numlock", ImGuiKey_NumLock}, + {"printscreen", ImGuiKey_PrintScreen}, + {"pause", ImGuiKey_Pause}, + + // Keypad + {"keypad0", ImGuiKey_Keypad0}, + {"keypad1", ImGuiKey_Keypad1}, + {"keypad2", ImGuiKey_Keypad2}, + {"keypad3", ImGuiKey_Keypad3}, + {"keypad4", ImGuiKey_Keypad4}, + {"keypad5", ImGuiKey_Keypad5}, + {"keypad6", ImGuiKey_Keypad6}, + {"keypad7", ImGuiKey_Keypad7}, + {"keypad8", ImGuiKey_Keypad8}, + {"keypad9", ImGuiKey_Keypad9}, + {"keypad.", ImGuiKey_KeypadDecimal}, + {"keypad+", ImGuiKey_KeypadAdd}, + {"keypad-", ImGuiKey_KeypadSubtract}, + {"keypad*", ImGuiKey_KeypadMultiply}, + {"keypad/", ImGuiKey_KeypadDivide} + }; + + // Split the key string by '+' (e.g., "Ctrl+Shift+S") + std::istringstream keyStream(key); + std::string segment; + + while (std::getline(keyStream, segment, '+')) { + // Trim whitespace + segment.erase(0, segment.find_first_not_of(" \t")); + segment.erase(segment.find_last_not_of(" \t") + 1); + + // Convert to lowercase for case-insensitive comparison + std::ranges::transform(segment, segment.begin(), + [](const unsigned char c){ return std::tolower(c); }); + + // Look up in the map and set the bit in the signature + auto it = keyMap.find(segment); + if (it != keyMap.end()) { + m_signature.set(static_cast(it->second - ImGuiKey_NamedKey_BEGIN)); + } + } + } + + bool Command::exactMatch(const std::bitset &inputSignature) const + { + return m_signature == inputSignature; + } + + bool Command::partialMatch(const std::bitset &inputSignature) const + { + return (m_signature & inputSignature) == m_signature; + } + + void Command::executePressedCallback() const + { + if (m_pressedCallback) + m_pressedCallback(); + } + + void Command::executeReleasedCallback() const + { + if (m_releaseCallback) + m_releaseCallback(); + } + + void Command::executeRepeatCallback() const + { + if (m_repeatCallback) + m_repeatCallback(); + } + + std::span Command::getChildren() const + { + return std::span(m_childrens); + } + + const std::bitset &Command::getSignature() const + { + return m_signature; + } + + const std::string &Command::getKey() const + { + return m_key; + } + + bool Command::isModifier() const + { + return m_isModifier; + } + + const std::string &Command::getDescription() const + { + return m_description; + } + +} diff --git a/editor/src/inputs/Command.hpp b/editor/src/inputs/Command.hpp new file mode 100644 index 000000000..06c341988 --- /dev/null +++ b/editor/src/inputs/Command.hpp @@ -0,0 +1,83 @@ +//// Command.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 23/05/2025 +// Description: Header file for the input commands +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include +#include +#include + +namespace nexo::editor { + + class Command { + public: + Command( + std::string description, + const std::string &key, + const std::function &pressedCallback, + const std::function &releaseCallback, + const std::function &repeatCallback, + bool isModifier = false, + const std::vector &childrens = {} + ); + + [[nodiscard]] bool exactMatch(const std::bitset &inputSignature) const; + [[nodiscard]] bool partialMatch(const std::bitset &inputSignature) const; + void executePressedCallback() const; + void executeReleasedCallback() const; + void executeRepeatCallback() const; + [[nodiscard]] std::span getChildren() const; + [[nodiscard]] const std::bitset &getSignature() const; + [[nodiscard]] const std::string &getKey() const; + [[nodiscard]] const std::string &getDescription() const; + [[nodiscard]] bool isModifier() const; + + class Builder { + public: + Builder& description(std::string val) { desc = std::move(val); return *this; } + Builder& key(std::string val) { k = std::move(val); return *this; } + Builder& onPressed(const std::function &cb) { pressed = cb; return *this; } + Builder& onReleased(const std::function &cb) { released = cb; return *this; } + Builder& onRepeat(const std::function &cb) { repeat = cb; return *this; } + Builder& modifier(const bool val) { mod = val; return *this; } + Builder& addChild(Command child) { children.push_back(std::move(child)); return *this; } + + [[nodiscard]] Command build() const { + return {desc, k, pressed, released, repeat, mod, children}; + } + + private: + std::string desc; + std::string k; + std::function pressed = nullptr; + std::function released = nullptr; + std::function repeat = nullptr; + bool mod = false; + std::vector children; + }; + + static Builder create() { return {}; } + + private: + std::bitset m_signature; + std::string m_description; + std::string m_key; + std::function m_pressedCallback; + std::function m_releaseCallback; + std::function m_repeatCallback; + bool m_isModifier; + std::vector m_childrens; + }; +} diff --git a/editor/src/inputs/InputManager.cpp b/editor/src/inputs/InputManager.cpp new file mode 100644 index 000000000..f1f0c6c96 --- /dev/null +++ b/editor/src/inputs/InputManager.cpp @@ -0,0 +1,317 @@ +//// InputManager.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 23/05/2025 +// Description: Source file for the input manager +// +/////////////////////////////////////////////////////////////////////////////// + +#include "InputManager.hpp" +#include +#include +#include + +namespace nexo::editor { + + void InputManager::processInputs(const WindowState& windowState) + { + std::bitset pressedSignature; + std::bitset releasedSignature; + std::bitset repeatSignature; + std::bitset currentlyHeldKeys; + + static const std::set excludedKeys = { + ImGuiKey_MouseLeft, + ImGuiKey_MouseRight, + ImGuiKey_MouseMiddle, + ImGuiKey_MouseX1, + ImGuiKey_MouseX2, + ImGuiKey_MouseWheelX, + ImGuiKey_MouseWheelY + }; + + // Track keys that were held down last frame + static std::bitset lastFrameHeldKeys; + + // Track multiple-press detection + static std::vector keyLastPressTime(ImGuiKey_NamedKey_COUNT, -1.0f); + static std::vector keyPressCount(ImGuiKey_NamedKey_COUNT, 0); + + const double currentTime = ImGui::GetTime(); + + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_COUNT + ImGuiKey_NamedKey_BEGIN - 5; key++) { + constexpr float multiPressThreshold = 0.3f; + if (excludedKeys.contains(static_cast(key))) + continue; + + const auto imKey = static_cast(key); + const auto idx = static_cast(key - ImGuiKey_NamedKey_BEGIN); + + const bool keyDown = ImGui::IsKeyDown(imKey); + + // Update currently held keys + if (keyDown) { + currentlyHeldKeys.set(idx); + + if (!lastFrameHeldKeys[idx]) { // Key was just pressed this frame + pressedSignature.set(idx); + + // Handle multiple press detection + if (currentTime - keyLastPressTime[idx] < multiPressThreshold) { + keyPressCount[idx]++; + if (keyPressCount[idx] > 1) { + repeatSignature.set(idx); + } + } else { + keyPressCount[idx] = 1; + } + keyLastPressTime[idx] = static_cast(currentTime); + } + } else { + if (lastFrameHeldKeys[idx]) { + // Key was just released + releasedSignature.set(idx); + } else if (keyLastPressTime[idx] > 0 && currentTime - keyLastPressTime[idx] > multiPressThreshold) { + // Too much time has passed since last press, reset the counter + keyPressCount[idx] = 0; + } + } + } + + // Get all commands to process + const auto& commands = windowState.getCommands(); + + // STEP 1: Find and process modifier/key combinations first + bool modifierCombinationProcessed = false; + + for (const auto& command : commands) { + if (command.isModifier()) { + std::bitset modifierSignature = command.getSignature(); + if ((modifierSignature & currentlyHeldKeys) == modifierSignature) { + // This modifier is held down, now check its children + for (const auto& childCmd : command.getChildren()) { + std::bitset childSignature = childCmd.getSignature(); + if ((childSignature & pressedSignature).any()) { + // We found a modifier+key combination! Execute it + childCmd.executePressedCallback(); + modifierCombinationProcessed = true; + break; // Process only one modifier combination at a time + } + + // Check for key releases while modifier is held + if ((childSignature & releasedSignature).any()) { + childCmd.executeReleasedCallback(); + } + } + + if (modifierCombinationProcessed) { + break; + } + } + } + } + + // STEP 2: Only process regular commands if no modifier combination was processed + if (!modifierCombinationProcessed) { + for (const auto& command : commands) { + // Skip modifiers, we already handled them + if (command.isModifier()) continue; + + if (command.exactMatch(pressedSignature)) { + command.executePressedCallback(); + } + + if (command.exactMatch(releasedSignature)) { + command.executeReleasedCallback(); + } + } + } + + if (repeatSignature.any()) { + processRepeatCommands(windowState.getCommands(), repeatSignature, currentlyHeldKeys); + } + + // Store current key state for next frame + lastFrameHeldKeys = currentlyHeldKeys; + } + + void InputManager::processRepeatCommands( + const std::span& commands, + const std::bitset& repeatSignature, + const std::bitset& currentlyHeldKeys + ) { + for (const auto& command : commands) { + // If this is a non-modifier command that has a repeat key + if (command.exactMatch(repeatSignature)) { + command.executeRepeatCallback(); + } + + // Handle cases where a modifier is held and another key is repeating + if (!command.getChildren().empty()) { + // Special case for modifiers: if the modifier key is held and a child key is repeating + if (command.isModifier() && (command.getSignature() & currentlyHeldKeys) == command.getSignature()) { + // Check if any child key is in the repeat signature + for (const auto& child : command.getChildren()) { + // If this child is directly repeating + if ((child.getSignature() & repeatSignature) == child.getSignature() && + child.getSignature() != command.getSignature()) { + child.executeRepeatCallback(); + } + } + + // Also check deeper in the hierarchy + const auto &remainingBits = repeatSignature; + processRepeatCommands(command.getChildren(), remainingBits, currentlyHeldKeys); + } + // Standard partial match handling + else if (command.partialMatch(repeatSignature)) { + auto remainingBits = repeatSignature ^ command.getSignature(); + if (remainingBits.any()) { + processRepeatCommands(command.getChildren(), remainingBits, currentlyHeldKeys); + } + } + } + } + } + + // Add this method implementation + std::vector InputManager::getAllPossibleCommands(const WindowState& windowState) const + { + std::vector allCommands; + // Use an empty signature to get all commands + const std::bitset emptySignature; + collectPossibleCommands(windowState.getCommands(), emptySignature, allCommands); + return allCommands; + } + + std::vector InputManager::getPossibleCommands(const WindowState& windowState) const + { + std::bitset pressedSignature; + + static const std::set excludedKeys = { + ImGuiKey_MouseLeft, + ImGuiKey_MouseRight, + ImGuiKey_MouseMiddle, + ImGuiKey_MouseX1, + ImGuiKey_MouseX2, + ImGuiKey_MouseWheelX, + ImGuiKey_MouseWheelY + }; + + // Get currently pressed keys + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_COUNT + ImGuiKey_NamedKey_BEGIN - 5; key++) { + if (excludedKeys.contains(static_cast(key))) + continue; + if (ImGui::IsKeyDown(static_cast(key))) + { + pressedSignature.set(static_cast(key - ImGuiKey_NamedKey_BEGIN)); + } + } + + std::vector possibleCommands; + collectPossibleCommands(windowState.getCommands(), pressedSignature, possibleCommands); + return possibleCommands; + } + + void InputManager::collectPossibleCommands( + const std::span& commands, + const std::bitset& pressedSignature, + std::vector& possibleCommands) const + { + for (const auto& command : commands) { + // If no keys are pressed, show all possible command chains + if (pressedSignature.none()) { + if (command.getChildren().empty() || !command.isModifier()) { + possibleCommands.emplace_back(command.getKey(), command.getDescription()); + } else { + // For modifiers with children, build combinations recursively + std::vector childCombinations; + buildCommandCombinations(command, "", childCombinations); + possibleCommands.insert(possibleCommands.end(), childCombinations.begin(), childCombinations.end()); + } + continue; + } + + // If this command matches the pressed signature exactly or partially + if (command.partialMatch(pressedSignature)) { + if (command.isModifier() && (command.getSignature() & pressedSignature) == command.getSignature()) { + bool hasActivatedChildModifier = false; + + for (const auto& child : command.getChildren()) { + if (child.isModifier() && (child.getSignature() & pressedSignature) == child.getSignature()) { + hasActivatedChildModifier = true; + break; + } + } + + // For each child command, add the appropriate representation + for (const auto& child : command.getChildren()) { + if (hasActivatedChildModifier) { + // Skip non-modifier children or modifiers that aren't pressed + if (!child.isModifier() || (child.getSignature() & pressedSignature) != child.getSignature()) { + continue; + } + + // Child modifier is pressed, show only its children's keys + for (const auto& grandchild : child.getChildren()) { + possibleCommands.emplace_back(grandchild.getKey(), grandchild.getDescription()); + } + } else { + // No child modifiers are pressed, show all children + if (child.isModifier() && !child.getChildren().empty()) { + // Build combinations for this child modifier + for (const auto& grandchild : child.getChildren()) { + possibleCommands.emplace_back( + child.getKey() + "+" + grandchild.getKey(), + grandchild.getDescription() + ); + } + } else { + // Child is not a modifier + possibleCommands.emplace_back(child.getKey(), child.getDescription()); + } + } + } + + // Skip recursive processing if we've handled modifiers + continue; + } + + // Recursively check child commands if this is not an exact match + if (!command.getChildren().empty() && !command.exactMatch(pressedSignature)) { + auto remainingBits = pressedSignature ^ command.getSignature(); + if (remainingBits.any()) { // Only recurse if there are remaining bits + collectPossibleCommands(command.getChildren(), remainingBits, possibleCommands); + } + } + } + } + } + + // Helper method to recursively build all possible command combinations + void InputManager::buildCommandCombinations( + const Command& command, + const std::string& prefix, + std::vector& combinations) const + { + + std::string currentPrefix = prefix.empty() ? command.getKey() : prefix + "+" + command.getKey(); + + // If this is a leaf command or not a modifier, add the combination + if (command.getChildren().empty() || !command.isModifier()) { + combinations.emplace_back(currentPrefix, command.getDescription()); + return; + } + + // For modifiers with children, recursively build combinations + for (const auto& child : command.getChildren()) { + buildCommandCombinations(child, currentPrefix, combinations); + } + } +} diff --git a/editor/src/inputs/InputManager.hpp b/editor/src/inputs/InputManager.hpp new file mode 100644 index 000000000..565fa2216 --- /dev/null +++ b/editor/src/inputs/InputManager.hpp @@ -0,0 +1,62 @@ +//// Input.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: +// Description: +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "WindowState.hpp" +#include "Command.hpp" +#include +#include + +namespace nexo::editor { + + struct CommandInfo { + std::string key; + std::string description; + + CommandInfo(std::string k, std::string d) : key(std::move(k)), description(std::move(d)) {} + }; + + class InputManager { + public: + InputManager() = default; + ~InputManager() = default; + + // Process inputs based on current window state + void processInputs(const WindowState& windowState); + + // Update these method signatures: + [[nodiscard]] std::vector getPossibleCommands(const WindowState& windowState) const; + [[nodiscard]] std::vector getAllPossibleCommands(const WindowState& windowState) const; + + private: + // Current and previous key states for detecting changes + std::bitset m_currentKeyState; + std::bitset m_keyWasDownLastFrame; + + void processRepeatCommands( + const std::span& commands, + const std::bitset& repeatSignature, + const std::bitset& currentlyHeldKeys + ); + void collectPossibleCommands( + const std::span& commands, + const std::bitset& pressedSignature, + std::vector& possibleCommands) const; + + void buildCommandCombinations( + const Command& command, + const std::string& prefix, + std::vector& combinations) const; + }; +} diff --git a/editor/src/inputs/WindowState.cpp b/editor/src/inputs/WindowState.cpp new file mode 100644 index 000000000..69dfe21bd --- /dev/null +++ b/editor/src/inputs/WindowState.cpp @@ -0,0 +1,33 @@ +//// WindowState.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 23/05/2025 +// Description: Source file for the window state class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "WindowState.hpp" + +namespace nexo::editor { + + unsigned int WindowState::getId() const + { + return m_id; + } + + void WindowState::registerCommand(const Command &command) + { + m_commands.push_back(command); + } + + std::span WindowState::getCommands() const + { + return std::span(m_commands.data(), m_commands.size()); + } +} diff --git a/editor/src/inputs/WindowState.hpp b/editor/src/inputs/WindowState.hpp new file mode 100644 index 000000000..8fcec7b15 --- /dev/null +++ b/editor/src/inputs/WindowState.hpp @@ -0,0 +1,35 @@ +//// WindowState.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 23/05/2025 +// Description: Header file for the window state, a class wrapping a set of input commands +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Command.hpp" +#include + +namespace nexo::editor { + + class WindowState { + public: + WindowState() = default; + + WindowState(const unsigned int id) : m_id(id) {} + ~WindowState() = default; + + [[nodiscard]] unsigned int getId() const; + void registerCommand(const Command &command); + [[nodiscard]] std::span getCommands() const; + private: + unsigned int m_id{}; + std::vector m_commands; + }; +} diff --git a/editor/src/utils/Config.cpp b/editor/src/utils/Config.cpp index a04e5c854..46a191e6d 100644 --- a/editor/src/utils/Config.cpp +++ b/editor/src/utils/Config.cpp @@ -65,4 +65,98 @@ namespace nexo::editor { configFile.close(); return dockId; } + + std::vector findAllEditorScenes() + { + std::string configPath = Path::resolvePathRelativeToExe( + "../config/default-layout.ini").string(); + std::ifstream configFile(configPath); + + std::vector sceneWindows; + + if (!configFile.is_open()) { + std::cout << "Could not open config file: " << configPath << std::endl; + return sceneWindows; + } + + std::string line; + std::regex windowRegex(R"(\[Window\]\[(###Default Scene\d+)\])"); + + while (std::getline(configFile, line)) { + std::smatch match; + if (std::regex_search(line, match, windowRegex) && match.size() > 1) { + // match[1] contains the window name (e.g., "Default Scene0") + sceneWindows.push_back(match[1].str()); + } + } + + configFile.close(); + return sceneWindows; + } + + void setAllWindowDockIDsFromConfig(WindowRegistry& registry) + { + std::string configPath = Path::resolvePathRelativeToExe( + "../config/default-layout.ini").string(); + std::ifstream configFile(configPath); + + if (!configFile.is_open()) { + std::cout << "Could not open config file: " << configPath << std::endl; + return; + } + + std::string line; + std::string currentWindowName; + bool inWindowSection = false; + bool isHashedWindow = false; + + std::regex windowHeaderRegex(R"(\[Window\]\[(.+)\])"); + std::regex dockIdRegex("DockId=(0x[0-9a-fA-F]+)"); + + while (std::getline(configFile, line)) { + // Check if this line is a window header + std::smatch windowMatch; + if (std::regex_search(line, windowMatch, windowHeaderRegex) && windowMatch.size() > 1) { + currentWindowName = windowMatch[1].str(); + inWindowSection = true; + + // Check if the window name starts with ### + isHashedWindow = currentWindowName.starts_with("###"); + + continue; + } + + // If we're in a window section and it's a hashed window, look for DockId + if (inWindowSection && isHashedWindow) { + // If we hit a new section, reset state + if (!line.empty() && line[0] == '[') { + inWindowSection = false; + isHashedWindow = false; + continue; + } + + std::smatch dockMatch; + if (std::regex_search(line, dockMatch, dockIdRegex) && dockMatch.size() > 1) { + std::string hexDockId = dockMatch[1]; + ImGuiID dockId = 0; + std::stringstream ss; + ss << std::hex << hexDockId; + ss >> dockId; + + // Set the dock ID for this window in the registry + if (dockId != 0) { + std::cout << "Setting dock id " << dockId << " for hashed window " + << currentWindowName << std::endl; + registry.setDockId(currentWindowName, dockId); + } + } + } else if (inWindowSection && !isHashedWindow && !line.empty() && line[0] == '[') { + // Reset state when we hit a new section + inWindowSection = false; + isHashedWindow = false; + } + } + + configFile.close(); + } } diff --git a/editor/src/utils/Config.hpp b/editor/src/utils/Config.hpp index db827ce6d..fdb77661f 100644 --- a/editor/src/utils/Config.hpp +++ b/editor/src/utils/Config.hpp @@ -13,8 +13,11 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once +#include "WindowRegistry.hpp" + #include #include +#include namespace nexo::editor { /** @@ -28,4 +31,31 @@ namespace nexo::editor { * @return ImGuiID The dock ID corresponding to the window. Returns 0 if not found. */ ImGuiID findWindowDockIDFromConfig(const std::string& windowName); + + /** + * @brief Finds all editor scene windows defined in the configuration file. + * + * Scans the default layout configuration file and extracts all window names that match + * the pattern for editor scene windows (those with names matching "###Default Scene" followed by digits). + * This allows the editor to reconstruct scene windows from a saved layout configuration. + * + * @return A vector of strings containing the window names of all editor scenes found in the config file. + * Returns an empty vector if no scene windows are found or if the config file cannot be opened. + */ + std::vector findAllEditorScenes(); + + /** + * @brief Sets dock IDs for all windows in the registry based on the configuration file. + * + * Reads the default layout configuration file and extracts dock IDs for all windows with + * names starting with "###" (hashed windows). For each matching window found in the config file, + * the function updates the corresponding window in the registry with the appropriate dock ID. + * This allows the editor to restore a previously saved docking layout. + * + * The function specifically targets hashed window names (starting with "###") as these are + * the identifier format used for persistent window references in ImGui. + * + * @param registry Reference to the WindowRegistry where dock IDs will be set. + */ + void setAllWindowDockIDsFromConfig(WindowRegistry& registry); } diff --git a/editor/src/utils/EditorProps.cpp b/editor/src/utils/EditorProps.cpp new file mode 100644 index 000000000..6d86c98eb --- /dev/null +++ b/editor/src/utils/EditorProps.cpp @@ -0,0 +1,94 @@ +//// EditorProps.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 19/04/2025 +// Description: Source file for the editor props utils +// +/////////////////////////////////////////////////////////////////////////////// + +#include "EditorProps.hpp" +#include "renderer/Texture.hpp" +#include "components/Render3D.hpp" +#include "components/Shapes3D.hpp" +#include "Path.hpp" +#include "Nexo.hpp" +#include "assets/AssetCatalog.hpp" + +namespace nexo::editor::utils { + + static void addCameraProps(const ecs::Entity entity) + { + auto& catalog = assets::AssetCatalog::getInstance(); + components::Material billboardMat{}; + billboardMat.isOpaque = false; + static const assets::AssetRef cameraIconTexture = catalog.createAsset( + assets::AssetLocation("_internal::cameraIcon@_internal"), + Path::resolvePathRelativeToExe("../resources/textures/cameraIcon.png") + ); + billboardMat.albedoTexture = cameraIconTexture; + billboardMat.shader = "Albedo unshaded transparent"; + auto billboard = std::make_shared(); + auto renderable = std::make_shared(billboardMat, billboard); + const components::RenderComponent renderComponent(renderable, components::RenderType::RENDER_3D); + Application::m_coordinator->addComponent(entity, renderComponent); + } + + static void addPointLightProps(const ecs::Entity entity) + { + auto& catalog = assets::AssetCatalog::getInstance(); + components::Material billboardMat{}; + billboardMat.isOpaque = false; + static const assets::AssetRef pointLightIconTexture = catalog.createAsset( + assets::AssetLocation("_internal::pointLightIcon@_internal"), + Path::resolvePathRelativeToExe("../resources/textures/pointLightIcon.png") + ); + billboardMat.albedoTexture = pointLightIconTexture; + billboardMat.shader = "Albedo unshaded transparent"; + auto billboard = std::make_shared(); + auto renderable = std::make_shared(billboardMat, billboard); + const components::RenderComponent renderComponent(renderable, components::RenderType::RENDER_3D); + Application::m_coordinator->addComponent(entity, renderComponent); + } + + static void addSpotLightProps(const ecs::Entity entity) + { + auto& catalog = assets::AssetCatalog::getInstance(); + components::Material billboardMat{}; + billboardMat.isOpaque = false; + static const assets::AssetRef spotLightIconTexture = catalog.createAsset( + assets::AssetLocation("_internal::spotLightIcon@_internal"), + Path::resolvePathRelativeToExe("../resources/textures/spotLightIcon.png") + ); + billboardMat.albedoTexture = spotLightIconTexture; + billboardMat.shader = "Albedo unshaded transparent"; + auto billboard = std::make_shared(); + auto renderable = std::make_shared(billboardMat, billboard); + const components::RenderComponent renderComponent(renderable, components::RenderType::RENDER_3D); + Application::m_coordinator->addComponent(entity, renderComponent); + } + + void addPropsTo(const ecs::Entity entity, const PropsType type) + { + switch (type) + { + case PropsType::CAMERA: + addCameraProps(entity); + break; + case PropsType::POINT_LIGHT: + addPointLightProps(entity); + break; + case PropsType::SPOT_LIGHT: + addSpotLightProps(entity); + break; + default: + break; + } + } + +} diff --git a/editor/src/utils/EditorProps.hpp b/editor/src/utils/EditorProps.hpp new file mode 100644 index 000000000..1e8edc334 --- /dev/null +++ b/editor/src/utils/EditorProps.hpp @@ -0,0 +1,27 @@ +//// EditorProps.hpp ////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 19/04/2025 +// Description: Header file for the utils function to create editor props +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "ecs/Coordinator.hpp" + +namespace nexo::editor::utils { + + enum class PropsType { + CAMERA, + POINT_LIGHT, + SPOT_LIGHT + }; + + void addPropsTo(ecs::Entity entity, PropsType type); +} diff --git a/editor/src/utils/FileSystem.cpp b/editor/src/utils/FileSystem.cpp new file mode 100644 index 000000000..c681265d8 --- /dev/null +++ b/editor/src/utils/FileSystem.cpp @@ -0,0 +1,34 @@ +//// FileSystem.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 15/04/2025 +// Description: Source file for the file system utils functions +// +/////////////////////////////////////////////////////////////////////////////// + +#ifdef WIN32 + #define NOMINMAX + #include +#endif + +#include "FileSystem.hpp" + + + +namespace nexo::editor::utils { + void openFolder(const std::string &folderPath) + { + #ifdef _WIN32 + ShellExecuteA(nullptr, "open", folderPath.c_str(), nullptr, nullptr, SW_SHOWDEFAULT); + #else + const std::string command = "xdg-open " + folderPath; + std::system(command.c_str()); // TODO: replace this system with safer commands, this is vulnerable to user injections + #endif + } +} diff --git a/editor/src/utils/FileSystem.hpp b/editor/src/utils/FileSystem.hpp new file mode 100644 index 000000000..6adf8e17a --- /dev/null +++ b/editor/src/utils/FileSystem.hpp @@ -0,0 +1,42 @@ +//// FileSystem.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 15/04/2025 +// Description: Header file for utils function to handle files +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include + +namespace nexo::editor::utils { + /** + * @brief Opens a file explorer window showing a specified folder. + * + * Uses platform-specific mechanisms to open the operating system's file explorer + * at the specified folder location: + * - On Windows: Uses ShellExecuteA to open Windows Explorer + * - On Linux/Unix: Uses xdg-open via system command + * + * This function is intended for user interaction purposes, such as revealing + * exported files, log directories, or other locations that the user may need + * to access directly. + * + * @param folderPath The absolute path to the folder to open + * + * @note This function does not check if the path exists or is accessible. + * It simply passes the request to the operating system which will handle + * any error conditions according to its standard behavior. + * + * @note On non-Windows platforms, this executes a system command, which may have + * security implications if folderPath contains untrusted input. + */ + void openFolder(const std::string &folderPath); +} diff --git a/editor/src/utils/ScenePreview.cpp b/editor/src/utils/ScenePreview.cpp index 869d8e687..517fd947d 100644 --- a/editor/src/utils/ScenePreview.cpp +++ b/editor/src/utils/ScenePreview.cpp @@ -19,6 +19,7 @@ #include "components/Camera.hpp" namespace nexo::editor::utils { + float computeBoundingSphereRadius(const components::TransformComponent &objectTransform) { const float halfX = objectTransform.size.x * 0.5f; @@ -37,112 +38,98 @@ namespace nexo::editor::utils { return atanf(radius / distance); } - static ecs::Entity copyEntity(ecs::Entity entity) + static ecs::Entity copyEntity(const ecs::Entity entity) { - const ecs::Entity entityCopy = nexo::Application::m_coordinator->createEntity(); - const auto renderComponentCopy = nexo::Application::m_coordinator->getComponent(entity).clone(); - const auto &transformComponentBase = nexo::Application::m_coordinator->getComponent(entity); + const ecs::Entity entityCopy = Application::m_coordinator->createEntity(); + const auto renderComponentCopy = Application::m_coordinator->getComponent(entity).clone(); + const auto &transformComponentBase = Application::m_coordinator->getComponent(entity); components::TransformComponent transformComponent; transformComponent.pos = {0.0f, 0.0f, -transformComponentBase.size.z * 2.0f}; transformComponent.quat = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); transformComponent.size = transformComponentBase.size; - nexo::Application::m_coordinator->addComponent(entityCopy, renderComponentCopy); - nexo::Application::m_coordinator->addComponent(entityCopy, transformComponent); + Application::m_coordinator->addComponent(entityCopy, renderComponentCopy); + Application::m_coordinator->addComponent(entityCopy, transformComponent); return entityCopy; } - static ecs::Entity createPreviewCamera(scene::SceneId sceneId, ecs::Entity entity, ecs::Entity entityCopy, const glm::vec2 &previewSize) + static ecs::Entity createPreviewCamera(scene::SceneId sceneId, ecs::Entity entity, ecs::Entity entityCopy, const glm::vec2 &previewSize, const glm::vec4 &clearColor) { auto &app = getApp(); - renderer::FramebufferSpecs framebufferSpecs; + renderer::NxFramebufferSpecs framebufferSpecs; framebufferSpecs.attachments = { - renderer::FrameBufferTextureFormats::RGBA8, - renderer::FrameBufferTextureFormats::RED_INTEGER, - renderer::FrameBufferTextureFormats::Depth + renderer::NxFrameBufferTextureFormats::RGBA8, + renderer::NxFrameBufferTextureFormats::RED_INTEGER, + renderer::NxFrameBufferTextureFormats::Depth }; framebufferSpecs.width = static_cast(previewSize.x); framebufferSpecs.height = static_cast(previewSize.y); - const auto &transformComponentBase = nexo::Application::m_coordinator->getComponent(entity); - const auto &transformComponent = nexo::Application::m_coordinator->getComponent(entityCopy); + const auto &transformComponentBase = Application::m_coordinator->getComponent(entity); + const auto &transformComponent = Application::m_coordinator->getComponent(entityCopy); - // Create the render target for the preview scene. - auto framebuffer = renderer::Framebuffer::create(framebufferSpecs); + auto framebuffer = renderer::NxFramebuffer::create(framebufferSpecs); float distance = transformComponentBase.size.z * 2.0f; - // Define default angular offsets (in degrees) for yaw and pitch float defaultYawDeg = 30.0f; // horizontal offset float defaultPitchDeg = -20.0f; // vertical offset - // Convert the angles to radians float defaultYaw = glm::radians(defaultYawDeg); float defaultPitch = glm::radians(defaultPitchDeg); - // Set the target position for the camera. - // In this preview, the target is the copied entity, whose transform we set above. glm::vec3 targetPos = transformComponent.pos; - // Start with an initial offset vector. - // Here we assume the camera initially lies along the positive Z axis (relative to the target). glm::vec3 initialOffset = {0.0f, 0.0f, distance}; - // Create an incremental quaternion for horizontal rotation (yaw) about the world up. glm::quat qYaw = glm::angleAxis(defaultYaw, glm::vec3(0, 1, 0)); - // For the pitch (vertical rotation), compute the right axis. glm::vec3 rightAxis = glm::normalize(glm::cross(glm::vec3(0, 1, 0), initialOffset)); if (glm::length(rightAxis) < 0.001f) // Fallback if the vector is degenerate. rightAxis = glm::vec3(1, 0, 0); glm::quat qPitch = glm::angleAxis(defaultPitch, rightAxis); - // Combine the yaw and pitch rotations, similar to the event handler logic. glm::quat incrementalRotation = qYaw * qPitch; - // Apply the incremental rotation to the initial offset to get the final offset. glm::vec3 newOffset = incrementalRotation * initialOffset; - // Normalize and apply the desired distance (optional if you need to clamp or adjust) newOffset = glm::normalize(newOffset) * distance; - // Compute the camera's starting position. glm::vec3 cameraPos = targetPos + newOffset; - // Create the perspective camera using the computed position. ecs::Entity cameraId = CameraFactory::createPerspectiveCamera(cameraPos, - framebufferSpecs.width, framebufferSpecs.height, framebuffer); + framebufferSpecs.width, framebufferSpecs.height, framebuffer, clearColor); - // Update the camera's transform. - auto &cameraTransform = nexo::Application::m_coordinator->getComponent(cameraId); + auto &cameraTransform = Application::m_coordinator->getComponent(cameraId); cameraTransform.pos = cameraPos; + auto &cameraComponent = Application::m_coordinator->getComponent(cameraId); + cameraComponent.render = true; - // Compute the camera's orientation so that it looks at the target. glm::vec3 newFront = glm::normalize(targetPos - cameraPos); cameraTransform.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0.0f, 1.0f, 0.0f))); components::PerspectiveCameraTarget cameraTarget; cameraTarget.targetEntity = entityCopy; cameraTarget.distance = transformComponentBase.size.z * 2.0f; - nexo::Application::m_coordinator->addComponent(cameraId, cameraTarget); + Application::m_coordinator->addComponent(cameraId, cameraTarget); app.getSceneManager().getScene(sceneId).addEntity(cameraId); return cameraId; } - static void setupPreviewLights(scene::SceneId sceneId, ecs::Entity entityCopy) + static void setupPreviewLights(const scene::SceneId sceneId, const ecs::Entity entityCopy) { auto &app = getApp(); const auto &transformComponent = Application::m_coordinator->getComponent(entityCopy); app.getSceneManager().getScene(sceneId).addEntity(entityCopy); - ecs::Entity ambientLight = LightFactory::createAmbientLight({0.5f, 0.5f, 0.5f}); + const ecs::Entity ambientLight = LightFactory::createAmbientLight({0.5f, 0.5f, 0.5f}); app.getSceneManager().getScene(sceneId).addEntity(ambientLight); - ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.2f, -1.0f, -0.3f}); + const ecs::Entity directionalLight = LightFactory::createDirectionalLight({0.2f, -1.0f, -0.3f}); app.getSceneManager().getScene(sceneId).addEntity(directionalLight); - float spotLightHalfAngle = utils::computeSpotlightHalfAngle(transformComponent, {0.0, 2.0f, -5.0f}); - float margin = glm::radians(2.5f); - ecs::Entity spotLight = LightFactory::createSpotLight({0.0f, 2.0f, -5.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 1.0f}, 0.0900000035F, 0.0320000015F, glm::cos(spotLightHalfAngle), glm::cos(spotLightHalfAngle + margin)); + const float spotLightHalfAngle = utils::computeSpotlightHalfAngle(transformComponent, {0.0, 2.0f, -5.0f}); + constexpr float margin = glm::radians(2.5f); + const ecs::Entity spotLight = LightFactory::createSpotLight({0.0f, 2.0f, -5.0f}, {0.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 1.0f}, 0.0900000035F, 0.0320000015F, glm::cos(spotLightHalfAngle), glm::cos(spotLightHalfAngle + margin)); app.getSceneManager().getScene(sceneId).addEntity(spotLight); } - void genScenePreview(const std::string &uniqueSceneName, const glm::vec2 &previewSize, ecs::Entity entity, ScenePreviewOut &out) + void genScenePreview(const std::string &uniqueSceneName, const glm::vec2 &previewSize, const ecs::Entity entity, ScenePreviewOut &out, const glm::vec4 &clearColor) { auto &app = getApp(); @@ -150,7 +137,7 @@ namespace nexo::editor::utils { out.entityCopy = copyEntity(entity); - out.cameraId = createPreviewCamera(out.sceneId, entity, out.entityCopy, previewSize); + out.cameraId = createPreviewCamera(out.sceneId, entity, out.entityCopy, previewSize, clearColor); setupPreviewLights(out.sceneId, out.entityCopy); out.sceneGenerated = true; diff --git a/editor/src/utils/ScenePreview.hpp b/editor/src/utils/ScenePreview.hpp index a9417c52c..e6c09df36 100644 --- a/editor/src/utils/ScenePreview.hpp +++ b/editor/src/utils/ScenePreview.hpp @@ -27,9 +27,9 @@ namespace nexo::editor::utils { * was successfully generated. */ struct ScenePreviewOut { - scene::SceneId sceneId; ///< The ID of the generated preview scene. - ecs::Entity cameraId; ///< The entity ID of the preview camera. - ecs::Entity entityCopy; ///< A copy of the original entity for preview purposes. + scene::SceneId sceneId{}; ///< The ID of the generated preview scene. + ecs::Entity cameraId{}; ///< The entity ID of the preview camera. + ecs::Entity entityCopy{}; ///< A copy of the original entity for preview purposes. bool sceneGenerated = false; ///< Flag indicating whether the scene preview was generated. }; @@ -67,6 +67,6 @@ namespace nexo::editor::utils { * @param entity The entity to generate the preview from. * @param out Output structure containing preview scene details. */ - void genScenePreview(const std::string &uniqueSceneName, const glm::vec2 &previewSize, ecs::Entity entity, ScenePreviewOut &out); + void genScenePreview(const std::string &uniqueSceneName, const glm::vec2 &previewSize, ecs::Entity entity, ScenePreviewOut &out, const glm::vec4 &clearColor = {0.05f, 0.05f, 0.05f, 0.0f}); } diff --git a/editor/src/utils/String.cpp b/editor/src/utils/String.cpp index 4cf33fae2..743b79992 100644 --- a/editor/src/utils/String.cpp +++ b/editor/src/utils/String.cpp @@ -14,11 +14,28 @@ #include "String.hpp" +#include +#include +#include +#include + namespace nexo::editor::utils { std::string removeIconPrefix(const std::string &str) { - if (size_t pos = str.find(" "); pos != std::string::npos) + if (const size_t pos = str.find(' '); pos != std::string::npos) return str.substr(pos + 1); return str; } + + void trim(std::string &s) + { + auto not_space = [](char c){ return !std::isspace(static_cast(c)); }; + + s.erase(s.begin(), std::ranges::find_if(s, not_space)); + auto rit = std::ranges::find_if( + s | std::views::reverse, + not_space + ); + s.erase(rit.base(),s.end()); + } } diff --git a/editor/src/utils/String.hpp b/editor/src/utils/String.hpp index b25790f48..417f3c918 100644 --- a/editor/src/utils/String.hpp +++ b/editor/src/utils/String.hpp @@ -27,4 +27,6 @@ namespace nexo::editor::utils { * @return std::string The string with the icon prefix removed. */ std::string removeIconPrefix(const std::string &str); + + void trim(std::string &s); } diff --git a/editor/src/utils/TransparentStringHash.hpp b/editor/src/utils/TransparentStringHash.hpp index 2b33b01ad..2cc6d0e6f 100644 --- a/editor/src/utils/TransparentStringHash.hpp +++ b/editor/src/utils/TransparentStringHash.hpp @@ -11,6 +11,7 @@ // Description: Header file containing the transparent string hash used for maps // /////////////////////////////////////////////////////////////////////////////// +#pragma once #include diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 5c7513d50..422343dbb 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -11,6 +11,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) set(COMMON_SOURCES common/Exception.cpp common/math/Vector.cpp + common/math/Projection.cpp engine/src/Nexo.cpp engine/src/EntityFactory2D.cpp engine/src/EntityFactory3D.cpp @@ -25,6 +26,7 @@ set(COMMON_SOURCES engine/src/core/event/WindowEvent.cpp engine/src/renderer/Buffer.cpp engine/src/renderer/Shader.cpp + engine/src/renderer/ShaderLibrary.cpp engine/src/renderer/ShaderStorageBuffer.cpp engine/src/renderer/VertexArray.cpp engine/src/renderer/RendererAPI.cpp @@ -37,10 +39,12 @@ set(COMMON_SOURCES engine/src/renderer/Framebuffer.cpp engine/src/renderer/primitives/Mesh.cpp engine/src/renderer/primitives/Cube.cpp + engine/src/renderer/primitives/Billboard.cpp engine/src/core/scene/Scene.cpp engine/src/core/scene/SceneManager.cpp engine/src/ecs/Entity.cpp engine/src/ecs/Components.cpp + engine/src/ecs/ComponentArray.cpp engine/src/ecs/Coordinator.cpp engine/src/ecs/System.cpp engine/src/systems/CameraSystem.cpp @@ -56,8 +60,20 @@ set(COMMON_SOURCES engine/src/assets/AssetCatalog.cpp engine/src/assets/AssetImporter.cpp engine/src/assets/AssetImporterContext.cpp + engine/src/assets/Assets/Model/ModelImporter.cpp + engine/src/assets/Assets/Texture/TextureImporter.cpp ) +# Add scripting sources if enabled +if(NEXO_BUILD_SCRIPTING) + list(APPEND COMMON_SOURCES + engine/src/systems/ScriptingSystem.cpp + engine/src/scripting/native/Scripting.cpp + engine/src/scripting/native/HostString.cpp + engine/src/scripting/native/NativeApi.cpp + ) +endif() + # Add API-specific sources if(NEXO_GRAPHICS_API STREQUAL "OpenGL") list(APPEND COMMON_SOURCES @@ -93,7 +109,7 @@ target_include_directories(nexoRenderer PUBLIC # loguru find_package(loguru CONFIG REQUIRED) -target_link_libraries(nexoEditor PRIVATE loguru::loguru) +target_link_libraries(nexoRenderer PRIVATE loguru::loguru) # Stb find_package(Stb REQUIRED) @@ -108,10 +124,24 @@ target_link_libraries(nexoRenderer PRIVATE assimp::assimp) find_package(Boost CONFIG REQUIRED COMPONENTS dll) target_link_libraries(nexoRenderer PRIVATE Boost::dll) +########################################### +# Scripting +########################################### + +if(NEXO_BUILD_SCRIPTING) + # nethost + find_package(unofficial-nethost CONFIG REQUIRED) + target_link_libraries(nexoRenderer PRIVATE unofficial::nethost::nethost) + target_compile_definitions(nexoRenderer PUBLIC NEXO_SCRIPTING_ENABLED) +endif() + if(NEXO_GRAPHICS_API STREQUAL "OpenGL") - target_compile_definitions(nexoRenderer PRIVATE GRAPHICS_API_OPENGL) + target_compile_definitions(nexoRenderer PRIVATE NX_GRAPHICS_API_OPENGL) find_package(OpenGL REQUIRED) find_package(glfw3 3.4 REQUIRED) find_package(glad CONFIG REQUIRED) target_link_libraries(nexoRenderer PRIVATE OpenGL::GL glfw glad::glad) endif() + +target_compile_definitions(nexoRenderer PRIVATE NEXO_EXPORT) +set_target_properties(nexoRenderer PROPERTIES ENABLE_EXPORTS ON) diff --git a/engine/src/Application.cpp b/engine/src/Application.cpp index 0ceb803d3..151abd3ba 100644 --- a/engine/src/Application.cpp +++ b/engine/src/Application.cpp @@ -24,11 +24,19 @@ #include "components/RenderContext.hpp" #include "components/SceneComponents.hpp" #include "components/Transform.hpp" +#include "components/Editor.hpp" #include "components/Uuid.hpp" #include "core/event/Input.hpp" #include "Timestep.hpp" +#include "exceptions/Exceptions.hpp" #include "renderer/RendererExceptions.hpp" +#include "renderer/Renderer.hpp" +#ifdef NEXO_SCRIPTING_ENABLED +#include "scripting/native/Scripting.hpp" +#include "systems/ScriptingSystem.hpp" +#endif #include "systems/CameraSystem.hpp" +#include "systems/RenderSystem.hpp" #include "systems/lights/DirectionalLightsSystem.hpp" #include "systems/lights/PointLightsSystem.hpp" @@ -58,20 +66,37 @@ namespace nexo { void Application::registerEcsComponents() const { + m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); + m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); + m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); m_coordinator->registerSingletonComponent(); m_coordinator->registerComponent(); + m_coordinator->setRestoreComponent(); } void Application::registerWindowCallbacks() const @@ -162,49 +187,44 @@ namespace nexo { void Application::registerSystems() { - m_cameraContextSystem = registerSystem(); - - m_perspectiveCameraControllerSystem = registerSystem(); - - m_perspectiveCameraTargetSystem = registerSystem(); - - m_renderSystem = registerSystem(); - - auto pointLightSystem = registerSystem(); - - auto directionalLightSystem = registerSystem(); - - auto spotLightSystem = registerSystem(); - - auto ambientLightSystem = registerSystem(); + m_cameraContextSystem = m_coordinator->registerGroupSystem(); + m_perspectiveCameraControllerSystem = m_coordinator->registerQuerySystem(); + m_perspectiveCameraTargetSystem = m_coordinator->registerQuerySystem(); + m_renderSystem = m_coordinator->registerGroupSystem(); + + auto pointLightSystem = m_coordinator->registerGroupSystem(); + auto directionalLightSystem = m_coordinator->registerGroupSystem(); + auto spotLightSystem = m_coordinator->registerGroupSystem(); + auto ambientLightSystem = m_coordinator->registerGroupSystem(); m_lightSystem = std::make_shared(ambientLightSystem, directionalLightSystem, pointLightSystem, spotLightSystem); + +#ifdef NEXO_SCRIPTING_ENABLED + m_scriptingSystem = std::make_shared(); +#endif + } + + int Application::initScripting() const + { +#ifdef NEXO_SCRIPTING_ENABLED + return m_scriptingSystem->init(); +#else + return 0; +#endif + } + + int Application::shutdownScripting() const + { +#ifdef NEXO_SCRIPTING_ENABLED + return m_scriptingSystem->shutdown(); +#else + return 0; +#endif } Application::Application() { - m_window = renderer::Window::create(); + m_window = renderer::NxWindow::create(); m_eventManager = std::make_shared(); registerAllDebugListeners(); registerSignalListeners(); @@ -240,20 +260,21 @@ namespace nexo { registerWindowCallbacks(); m_window->setVsync(false); -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) { - THROW_EXCEPTION(renderer::GraphicsApiInitFailure, "Failed to initialize OpenGL context with glad"); + THROW_EXCEPTION(renderer::NxGraphicsApiInitFailure, "Failed to initialize OpenGL context with glad"); } LOG(NEXO_INFO, "OpenGL context initialized with glad"); glViewport(0, 0, static_cast(m_window->getWidth()), static_cast(m_window->getHeight())); #endif - renderer::Renderer::init(); + renderer::NxRenderer::init(); m_coordinator->init(); registerEcsComponents(); registerSystems(); + // std::cout << "Application m_coordinator: " << m_coordinator.get() << std::endl; m_SceneManager.setCoordinator(m_coordinator); LOG(NEXO_DEV, "Application initialized"); @@ -261,32 +282,43 @@ namespace nexo { void Application::beginFrame() { - const auto time = static_cast(glfwGetTime()); - m_currentTimestep = time - m_lastFrameTime; - m_lastFrameTime = time; + const auto time = glfwGetTime(); + m_worldState.time.deltaTime = time - m_worldState.time.totalTime; + m_worldState.time.totalTime = time; + m_worldState.stats.frameCount += 1; } - void Application::run(const scene::SceneId sceneId, const RenderingType renderingType) + void Application::run(const SceneInfo &sceneInfo) { auto &renderContext = m_coordinator->getSingletonComponent(); +#ifdef NEXO_SCRIPTING_ENABLED + m_scriptingSystem->update(); +#endif + if (!m_isMinimized) { - renderContext.sceneRendered = sceneId; - if (m_SceneManager.getScene(sceneId).isRendered()) + renderContext.sceneRendered = static_cast(sceneInfo.id); + renderContext.sceneType = sceneInfo.sceneType; + if (sceneInfo.isChildWindow) { + renderContext.isChildWindow = true; + renderContext.viewportBounds[0] = sceneInfo.viewportBounds[0]; + renderContext.viewportBounds[1] = sceneInfo.viewportBounds[1]; + } + if (m_SceneManager.getScene(sceneInfo.id).isRendered()) { m_cameraContextSystem->update(); m_lightSystem->update(); m_renderSystem->update(); } - if (m_SceneManager.getScene(sceneId).isActive()) + if (m_SceneManager.getScene(sceneInfo.id).isActive()) { - m_perspectiveCameraControllerSystem->update(m_currentTimestep); + m_perspectiveCameraControllerSystem->update(m_worldState.time.deltaTime); } } // Update (swap buffers and poll events) - if (renderingType == RenderingType::WINDOW) + if (sceneInfo.renderingType == RenderingType::WINDOW) m_window->onUpdate(); m_eventManager->dispatchEvents(); renderContext.reset(); @@ -304,12 +336,12 @@ namespace nexo { return m_coordinator->createEntity(); } - void Application::deleteEntity(ecs::Entity entity) + void Application::deleteEntity(const ecs::Entity entity) { const auto tag = m_coordinator->tryGetComponent(entity); if (tag) { - unsigned int sceneId = tag->get().id; + const unsigned int sceneId = tag->get().id; m_SceneManager.getScene(sceneId).removeEntity(entity); } m_coordinator->destroyEntity(entity); diff --git a/engine/src/Application.hpp b/engine/src/Application.hpp index d6d196114..17e88d400 100644 --- a/engine/src/Application.hpp +++ b/engine/src/Application.hpp @@ -18,15 +18,16 @@ #include #include +#include "Types.hpp" #include "renderer/Window.hpp" #include "core/event/WindowEvent.hpp" #include "core/event/SignalEvent.hpp" #include "renderer/Buffer.hpp" -#include "renderer/Renderer.hpp" #include "ecs/Coordinator.hpp" #include "core/scene/SceneManager.hpp" #include "Logger.hpp" #include "Timer.hpp" +#include "WorldState.hpp" #include "components/Light.hpp" #include "systems/CameraSystem.hpp" @@ -37,10 +38,11 @@ namespace nexo { - enum class RenderingType { - WINDOW, - FRAMEBUFFER - }; +#ifdef NEXO_SCRIPTING_ENABLED + namespace system { + class ScriptingSystem; + } +#endif enum EventDebugFlags { DEBUG_LOG_RESIZE_EVENT = 1 << 0, @@ -78,6 +80,14 @@ namespace nexo { */ void beginFrame(); + struct SceneInfo { + scene::SceneId id; + RenderingType renderingType = RenderingType::WINDOW; + SceneType sceneType = SceneType::GAME; + bool isChildWindow = false; //<< Is the current scene embedded in a sub window ? + glm::vec2 viewportBounds[2]{}; //<< Viewport bounds in absolute coordinates (if the window viewport is embedded in the window), this is used for mouse coordinates + }; + /** * @brief Runs the application for the specified scene and rendering type. * @@ -93,8 +103,9 @@ namespace nexo { * * @param sceneId The ID of the scene to render. * @param renderingType The rendering mode (e.g., WINDOW or other types). + * @param sceneType The type of scene to render. */ - void run(scene::SceneId sceneId, RenderingType renderingType); + void run(const SceneInfo &sceneInfo); /** * @brief Ends the current frame by clearing processed events. @@ -206,20 +217,19 @@ namespace nexo { return m_coordinator->getComponent(entity); } - static std::vector getAllEntityComponentTypes(const ecs::Entity entity) + static std::vector getAllEntityComponentTypes(const ecs::Entity entity) { return m_coordinator->getAllComponentTypes(entity); } - static std::vector> getAllEntityComponents(const ecs::Entity entity) - { - return m_coordinator->getAllComponents(entity); - } - scene::SceneManager &getSceneManager() { return m_SceneManager; } - [[nodiscard]] const std::shared_ptr &getWindow() const { return m_window; }; - [[nodiscard]] bool isWindowOpen() const { return m_window->isOpen(); }; + [[nodiscard]] const std::shared_ptr &getWindow() const { return m_window; } + [[nodiscard]] bool isWindowOpen() const { return m_window->isOpen(); } + [[nodiscard]] WorldState &getWorldState() { return m_worldState; } + + int initScripting() const; + int shutdownScripting() const; static std::shared_ptr m_coordinator; protected: @@ -231,18 +241,6 @@ namespace nexo { void registerSignalListeners(); void registerEcsComponents() const; void registerWindowCallbacks() const; - template - std::shared_ptr registerSystem() - { - auto system = m_coordinator->registerSystem(); - - ecs::Signature signature; - (signature.set(m_coordinator->getComponentType()), ...); - - m_coordinator->setSystemSignature(signature); - - return system; - } void registerSystems(); void displayProfileResults() const; @@ -254,10 +252,9 @@ namespace nexo { bool m_isRunning = true; bool m_isMinimized = false; bool m_displayProfileResult = true; - std::shared_ptr m_window; + std::shared_ptr m_window; - float m_lastFrameTime = 0.0f; - Timestep m_currentTimestep; + WorldState m_worldState; int m_eventDebugFlags{}; @@ -266,6 +263,9 @@ namespace nexo { std::shared_ptr m_lightSystem; std::shared_ptr m_perspectiveCameraControllerSystem; std::shared_ptr m_perspectiveCameraTargetSystem; +#ifdef NEXO_SCRIPTING_ENABLED + std::shared_ptr m_scriptingSystem; +#endif std::vector m_profilesResults; }; diff --git a/engine/src/CameraFactory.cpp b/engine/src/CameraFactory.cpp index 6dddcf44e..ee3365a9f 100644 --- a/engine/src/CameraFactory.cpp +++ b/engine/src/CameraFactory.cpp @@ -13,6 +13,8 @@ /////////////////////////////////////////////////////////////////////////////// #include "CameraFactory.hpp" + +#include #include "Application.hpp" #include "components/Transform.hpp" #include "components/Camera.hpp" @@ -20,8 +22,8 @@ namespace nexo { ecs::Entity CameraFactory::createPerspectiveCamera(glm::vec3 pos, unsigned int width, - unsigned int height, std::shared_ptr renderTarget, - float fov, float nearPlane, float farPlane) + unsigned int height, std::shared_ptr renderTarget, + const glm::vec4 &clearColor, float fov, float nearPlane, float farPlane) { components::TransformComponent transform{}; transform.pos = pos; @@ -33,7 +35,9 @@ namespace nexo { camera.nearPlane = nearPlane; camera.farPlane = farPlane; camera.type = components::CameraType::PERSPECTIVE; - camera.m_renderTarget = renderTarget; + if (renderTarget) + camera.m_renderTarget = std::move(renderTarget); + camera.clearColor = clearColor; ecs::Entity newCamera = Application::m_coordinator->createEntity(); Application::m_coordinator->addComponent(newCamera, transform); diff --git a/engine/src/CameraFactory.hpp b/engine/src/CameraFactory.hpp index 2cf0bbdd8..f591302aa 100644 --- a/engine/src/CameraFactory.hpp +++ b/engine/src/CameraFactory.hpp @@ -22,7 +22,7 @@ namespace nexo { class CameraFactory { public: static ecs::Entity createPerspectiveCamera(glm::vec3 pos, unsigned int width, - unsigned int height, std::shared_ptr renderTarget = nullptr, - float fov = 45.0f, float nearPlane = 0.1f, float farPlane = 1000.0f); + unsigned int height, std::shared_ptr renderTarget = nullptr, + const glm::vec4 &clearColor = {37.0f/255.0f, 35.0f/255.0f, 50.0f/255.0f, 111.0f/255.0f}, float fov = 45.0f, float nearPlane = 0.1f, float farPlane = 1000.0f); }; } diff --git a/engine/src/EntityFactory3D.cpp b/engine/src/EntityFactory3D.cpp index d3fb37580..86efcda56 100644 --- a/engine/src/EntityFactory3D.cpp +++ b/engine/src/EntityFactory3D.cpp @@ -14,10 +14,10 @@ #include "EntityFactory3D.hpp" #include "components/Light.hpp" +#include "components/Shapes3D.hpp" #include "components/Transform.hpp" #include "components/Uuid.hpp" #include "components/Camera.hpp" -#include "core/exceptions/Exceptions.hpp" #include "Application.hpp" #define GLM_ENABLE_EXPERIMENTAL @@ -66,166 +66,43 @@ namespace nexo { return newCube; } - ecs::Entity EntityFactory3D::createModel(const std::string &path, glm::vec3 pos, glm::vec3 size, glm::vec3 rotation) + ecs::Entity EntityFactory3D::createBillboard(const glm::vec3 &pos, const glm::vec3 &size, const glm::vec4 &color) { components::TransformComponent transform{}; + transform.pos = pos; transform.size = size; - transform.quat = glm::quat(rotation); components::Material material{}; - std::shared_ptr rootNode = utils::loadModel(path); - auto model = std::make_shared(rootNode); - auto renderable = std::make_shared(material, model); + material.albedoColor = color; + auto billboard = std::make_shared(); + auto renderable = std::make_shared(material, billboard); components::RenderComponent renderComponent(renderable, components::RenderType::RENDER_3D); - ecs::Entity newModel = Application::m_coordinator->createEntity(); - Application::m_coordinator->addComponent(newModel, transform); - Application::m_coordinator->addComponent(newModel, renderComponent); + ecs::Entity newBillboard = Application::m_coordinator->createEntity(); + Application::m_coordinator->addComponent(newBillboard, transform); + Application::m_coordinator->addComponent(newBillboard, renderComponent); components::UuidComponent uuid; - Application::m_coordinator->addComponent(newModel, uuid); - return newModel; - } -} + Application::m_coordinator->addComponent(newBillboard, uuid); -namespace nexo::utils { - glm::mat4 convertAssimpMatrixToGLM(const aiMatrix4x4 &matrix) - { - return glm::mat4( - matrix.a1, matrix.b1, matrix.c1, matrix.d1, - matrix.a2, matrix.b2, matrix.c2, matrix.d2, - matrix.a3, matrix.b3, matrix.c3, matrix.d3, - matrix.a4, matrix.b4, matrix.c4, matrix.d4 - ); + return newBillboard; } - components::Mesh processMesh(const std::string &path, aiMesh *mesh, const aiScene *scene) + ecs::Entity EntityFactory3D::createBillboard(const glm::vec3 &pos, const glm::vec3 &size, const components::Material &material) { - std::vector vertices; - std::vector indices; - vertices.reserve(mesh->mNumVertices); - - for (unsigned int i = 0; i < mesh->mNumVertices; i++) - { - renderer::Vertex vertex{}; - vertex.position = {mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z}; - - if (mesh->HasNormals()) { - vertex.normal = { mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z }; - } - - if (mesh->mTextureCoords[0]) - vertex.texCoord = {mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y}; - else - vertex.texCoord = {0.0f, 0.0f}; - - vertices.push_back(vertex); - } - - for (unsigned int i = 0; i < mesh->mNumFaces; i++) - { - aiFace face = mesh->mFaces[i]; - indices.insert(indices.end(), face.mIndices, face.mIndices + face.mNumIndices); - } - - aiMaterial const *material = scene->mMaterials[mesh->mMaterialIndex]; - - components::Material materialComponent; - - aiColor4D color; - if (material->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) { - materialComponent.albedoColor = { color.r, color.g, color.b, color.a }; - } - - if (material->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) { - materialComponent.specularColor = { color.r, color.g, color.b, color.a }; - } - - if (material->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) { - materialComponent.emissiveColor = { color.r, color.g, color.b }; - } - - float roughness = 0.0f; - if (material->Get(AI_MATKEY_SHININESS, roughness) == AI_SUCCESS) { - materialComponent.roughness = 1.0f - (roughness / 100.0f); // Convert glossiness to roughness - } - - // Load Metallic - float metallic = 0.0f; - if (material->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS) { - materialComponent.metallic = metallic; - } - - float opacity = 1.0f; - if (material->Get(AI_MATKEY_OPACITY, opacity) == AI_SUCCESS) { - materialComponent.opacity = opacity; - } - - // Load Textures - std::filesystem::path modelPath(path); - std::filesystem::path modelDirectory = modelPath.parent_path(); - - auto loadTexture = [&](aiTextureType type) -> std::shared_ptr { - aiString str; - if (material->GetTexture(type, 0, &str) == AI_SUCCESS) { - std::filesystem::path texturePath = modelDirectory / std::string(str.C_Str()); - return renderer::Texture2D::create(texturePath.string()); - } - return nullptr; - }; - - materialComponent.albedoTexture = loadTexture(aiTextureType_DIFFUSE); - materialComponent.normalMap = loadTexture(aiTextureType_NORMALS); - materialComponent.metallicMap = loadTexture(aiTextureType_SPECULAR); // Specular can store metallic in some cases - materialComponent.roughnessMap = loadTexture(aiTextureType_SHININESS); - materialComponent.emissiveMap = loadTexture(aiTextureType_EMISSIVE); - - LOG(NEXO_INFO, "Loaded material: Diffuse = {}, Normal = {}, Metallic = {}, Roughness = {}", - materialComponent.albedoTexture ? "Yes" : "No", - materialComponent.normalMap ? "Yes" : "No", - materialComponent.metallicMap ? "Yes" : "No", - materialComponent.roughnessMap ? "Yes" : "No"); - - LOG(NEXO_INFO, "Loaded mesh {}", mesh->mName.data); - - return {mesh->mName.data, vertices, indices, materialComponent}; - } - - - std::shared_ptr processNode(const std::string &path, aiNode const *node, const aiScene *scene) - { - static int nbNode = 0; - nbNode++; - auto meshNode = std::make_shared(); - - glm::mat4 nodeTransform = convertAssimpMatrixToGLM(node->mTransformation); - - meshNode->transform = nodeTransform; - - for (unsigned int i = 0; i < node->mNumMeshes; i++) - { - aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; - meshNode->meshes.push_back(processMesh(path, mesh, scene)); - } - - for (unsigned int i = 0; i < node->mNumChildren; i++) - { - auto newNode = processNode(path, node->mChildren[i], scene); - if (newNode) - meshNode->children.push_back(newNode); - } - - return meshNode; - } + components::TransformComponent transform{}; - std::shared_ptr loadModel(const std::string &path) - { - Assimp::Importer importer; - const aiScene *scene = importer.ReadFile( - path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); + transform.pos = pos; + transform.size = size; + auto billboard = std::make_shared(); + auto renderable = std::make_shared(material, billboard); + components::RenderComponent renderComponent(renderable, components::RenderType::RENDER_3D); - if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) - THROW_EXCEPTION(core::LoadModelException, path, importer.GetErrorString()); + ecs::Entity newBillboard = Application::m_coordinator->createEntity(); + Application::m_coordinator->addComponent(newBillboard, transform); + Application::m_coordinator->addComponent(newBillboard, renderComponent); + components::UuidComponent uuid; + Application::m_coordinator->addComponent(newBillboard, uuid); - return processNode(path, scene->mRootNode, scene); + return newBillboard; } } diff --git a/engine/src/EntityFactory3D.hpp b/engine/src/EntityFactory3D.hpp index 12b10ef65..f88c9bf2d 100644 --- a/engine/src/EntityFactory3D.hpp +++ b/engine/src/EntityFactory3D.hpp @@ -14,13 +14,9 @@ #pragma once #include -#include -#include -#include -#include "ecs/ECSExceptions.hpp" +#include "assets/Assets/Model/Model.hpp" #include "components/Components.hpp" -#include "renderer/Framebuffer.hpp" namespace nexo { @@ -59,14 +55,10 @@ namespace nexo * @return ecs::Entity The newly created cube entity. */ static ecs::Entity createCube(glm::vec3 pos, glm::vec3 size, glm::vec3 rotation, const components::Material &material); - static ecs::Entity createModel(const std::string& path, glm::vec3 pos, glm::vec3 size, glm::vec3 rotation); - }; -} -namespace nexo::utils -{ - std::shared_ptr loadModel(const std::string& path); - std::shared_ptr processNode(const std::string &path, aiNode const *node, const aiScene* scene); - components::Mesh processMesh(const std::string &path, aiMesh* mesh, const aiScene* scene); - glm::mat4 convertAssimpMatrixToGLM(const aiMatrix4x4& matrix); + static ecs::Entity createModel(assets::AssetRef modelAsset, glm::vec3 pos, glm::vec3 size, glm::vec3 rotation); + + static ecs::Entity createBillboard(const glm::vec3 &pos, const glm::vec3 &size, const glm::vec4 &color); + static ecs::Entity createBillboard(const glm::vec3 &pos, const glm::vec3 &size, const components::Material &material); + }; } diff --git a/engine/src/LightFactory.cpp b/engine/src/LightFactory.cpp index a32f8c736..d554b9c4b 100644 --- a/engine/src/LightFactory.cpp +++ b/engine/src/LightFactory.cpp @@ -16,35 +16,38 @@ #include "Application.hpp" #include "components/Light.hpp" +#include "components/Transform.hpp" #include "components/Uuid.hpp" namespace nexo { - ecs::Entity LightFactory::createAmbientLight(glm::vec3 color) + ecs::Entity LightFactory::createAmbientLight(const glm::vec3 color) { - ecs::Entity newAmbientLight = Application::m_coordinator->createEntity(); - components::AmbientLightComponent newAmbientLightComponent{color}; + const ecs::Entity newAmbientLight = Application::m_coordinator->createEntity(); + const components::AmbientLightComponent newAmbientLightComponent{color}; Application::m_coordinator->addComponent(newAmbientLight, newAmbientLightComponent); - components::UuidComponent uuid; + const components::UuidComponent uuid; Application::m_coordinator->addComponent(newAmbientLight, uuid); return newAmbientLight; } - ecs::Entity LightFactory::createDirectionalLight(glm::vec3 lightDir, glm::vec3 color) + ecs::Entity LightFactory::createDirectionalLight(const glm::vec3 lightDir, const glm::vec3 color) { - ecs::Entity newDirectionalLight = Application::m_coordinator->createEntity(); - components::DirectionalLightComponent newDirectionalLightComponent(lightDir, color); + const ecs::Entity newDirectionalLight = Application::m_coordinator->createEntity(); + const components::DirectionalLightComponent newDirectionalLightComponent(lightDir, color); Application::m_coordinator->addComponent(newDirectionalLight, newDirectionalLightComponent); - components::UuidComponent uuid; + const components::UuidComponent uuid; Application::m_coordinator->addComponent(newDirectionalLight, uuid); return newDirectionalLight; } - ecs::Entity LightFactory::createPointLight(glm::vec3 position, glm::vec3 color, float linear, float quadratic) + ecs::Entity LightFactory::createPointLight(const glm::vec3 position, const glm::vec3 color, const float linear, const float quadratic) { - ecs::Entity newPointLight = Application::m_coordinator->createEntity(); - components::PointLightComponent newPointLightComponent(position, color, linear, quadratic); + const ecs::Entity newPointLight = Application::m_coordinator->createEntity(); + const components::TransformComponent transformComponent{position}; + Application::m_coordinator->addComponent(newPointLight, transformComponent); + const components::PointLightComponent newPointLightComponent{color, linear, quadratic}; Application::m_coordinator->addComponent(newPointLight, newPointLightComponent); - components::UuidComponent uuid; + const components::UuidComponent uuid; Application::m_coordinator->addComponent(newPointLight, uuid); return newPointLight; } @@ -55,7 +58,9 @@ namespace nexo { float outerCutOff) { ecs::Entity newSpotLight = Application::m_coordinator->createEntity(); - components::SpotLightComponent newSpotLightComponent(position, direction, color, cutOff, outerCutOff, linear, quadratic); + components::TransformComponent transformComponent{position}; + Application::m_coordinator->addComponent(newSpotLight, transformComponent); + components::SpotLightComponent newSpotLightComponent{direction, color, cutOff, outerCutOff, linear, quadratic}; Application::m_coordinator->addComponent(newSpotLight, newSpotLightComponent); components::UuidComponent uuid; Application::m_coordinator->addComponent(newSpotLight, uuid); diff --git a/engine/src/Nexo.cpp b/engine/src/Nexo.cpp index 1496ef2b3..ef18a5b6c 100644 --- a/engine/src/Nexo.cpp +++ b/engine/src/Nexo.cpp @@ -28,10 +28,10 @@ namespace nexo { return Application::getInstance(); } - void runEngine(const scene::SceneId id, const RenderingType renderingType) + void runEngine(const Application::SceneInfo &sceneInfo) { Application &app = Application::getInstance(); - app.run(id, renderingType); + app.run(sceneInfo); } } diff --git a/engine/src/Nexo.hpp b/engine/src/Nexo.hpp index de468cee9..292c95317 100644 --- a/engine/src/Nexo.hpp +++ b/engine/src/Nexo.hpp @@ -45,5 +45,5 @@ namespace nexo { Application &getApp(); - void runEngine(scene::SceneId id, RenderingType renderingType = RenderingType::WINDOW); + void runEngine(const Application::SceneInfo &sceneInfo); } diff --git a/engine/src/Types.hpp b/engine/src/Types.hpp new file mode 100644 index 000000000..c0e41fccd --- /dev/null +++ b/engine/src/Types.hpp @@ -0,0 +1,26 @@ +//// Types.hpp //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 18/04/2025 +// Description: Header file for the common engine types +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +namespace nexo { + enum class RenderingType { + WINDOW, + FRAMEBUFFER + }; + + enum class SceneType { + EDITOR, + GAME + }; +} diff --git a/engine/src/WorldState.hpp b/engine/src/WorldState.hpp new file mode 100644 index 000000000..a2a5f4b4d --- /dev/null +++ b/engine/src/WorldState.hpp @@ -0,0 +1,33 @@ +//// WorldState.hpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 21/06/2025 +// Description: Header file for the WorldState class, +// which manages the state of the world in the game engine +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "Application.hpp" + +namespace nexo { + + struct WorldState { + struct WorldTime { + double deltaTime = 0.0; // Time since last update + double totalTime = 0.0; // Total time since the start of the world + } time; + + struct WorldStats { + int frameCount = 0; // Number of frames rendered + } stats; + }; + +} // namespace nexo::scripting \ No newline at end of file diff --git a/engine/src/assets/Asset.cpp b/engine/src/assets/Asset.cpp index f1e426c9f..29e78e9d8 100644 --- a/engine/src/assets/Asset.cpp +++ b/engine/src/assets/Asset.cpp @@ -13,7 +13,6 @@ /////////////////////////////////////////////////////////////////////////////// #include "Asset.hpp" -#include "Assets/Model/ModelImporter.hpp" namespace nexo::assets { diff --git a/engine/src/assets/Asset.hpp b/engine/src/assets/Asset.hpp index d9d4f35e0..aaf96826f 100644 --- a/engine/src/assets/Asset.hpp +++ b/engine/src/assets/Asset.hpp @@ -20,12 +20,13 @@ #include #include #include -#include #include "AssetLocation.hpp" #include "AssetRef.hpp" #include "json.hpp" +#undef ERROR // Avoid conflict with Windows.h + namespace nexo::assets { constexpr unsigned short ASSET_MAX_DEPENDENCIES = 10000; @@ -38,6 +39,7 @@ namespace nexo::assets { enum class AssetType { UNKNOWN, TEXTURE, + MATERIAL, MODEL, SOUND, MUSIC, @@ -56,6 +58,7 @@ namespace nexo::assets { constexpr const char *AssetTypeNames[] = { "UNKNOWN", "TEXTURE", + "MATERIAL", "MODEL", "SOUND", "MUSIC", @@ -84,7 +87,7 @@ namespace nexo::assets { /** * @brief Serializes an AssetType value to JSON. * - * Converts the provided AssetType enum into its string representation using getAssetTypeName + * Converts the provided AssetType enum into its string representation using getAssetTypeName * and assigns this string to the JSON object. * * @param j JSON object to receive the serialized asset type. @@ -137,33 +140,23 @@ namespace nexo::assets { AssetLocation location; //< Location of the asset }; + /** + * @brief Pure virtual interface for all assets + */ class IAsset { friend class AssetCatalog; friend class AssetImporter; public: - //IAsset() = delete; virtual ~IAsset() = default; - [[nodiscard]] virtual const AssetMetadata& getMetadata() const { return m_metadata; } - [[nodiscard]] virtual AssetType getType() const { return getMetadata().type; } - [[nodiscard]] virtual AssetID getID() const { return getMetadata().id; } - [[nodiscard]] virtual AssetStatus getStatus() const { return getMetadata().status; } + [[nodiscard]] virtual const AssetMetadata& getMetadata() const = 0; + [[nodiscard]] virtual AssetType getType() const = 0; + [[nodiscard]] virtual AssetID getID() const = 0; + [[nodiscard]] virtual AssetStatus getStatus() const = 0; - [[nodiscard]] virtual bool isLoaded() const { return getStatus() == AssetStatus::LOADED; } - [[nodiscard]] virtual bool isErrored() const { return getStatus() == AssetStatus::ERROR; } + [[nodiscard]] virtual bool isLoaded() const = 0; + [[nodiscard]] virtual bool isErrored() const = 0; - /** - * @brief Get the asset data pointer - * @return Raw pointer to the asset data - */ - [[nodiscard]] virtual void* getRawData() const = 0; - - /** - * @brief Set the asset data pointer - * @param rawData Raw pointer to the asset data - * @note This transfers ownership of the data to the asset, which will delete it in its destructor - */ - virtual IAsset& setRawData(void* rawData) = 0; protected: explicit IAsset() : m_metadata({ @@ -183,63 +176,30 @@ namespace nexo::assets { * @brief Get the metadata of the asset (for modification) */ //[[nodiscard]] AssetMetadata& getMetadata() { return m_metadata; } - - /*virtual AssetStatus load() = 0; - virtual AssetStatus unload() = 0;*/ - }; template class Asset : public IAsset { friend class AssetCatalog; - friend class AssetRef; + public: + // SFINAE definitions + using AssetDataType = TAssetData; static constexpr AssetType TYPE = TAssetType; - /** - * @brief Destructor that releases the allocated asset data. - * - * Deletes the dynamically allocated asset data to ensure proper memory cleanup when the asset is destroyed. - */ - virtual ~Asset() override - { - delete data; - } - - TAssetData *data; + ~Asset() override = default; - // Implementation of IAsset virtual methods - [[nodiscard]] void* getRawData() const override { - return data; - } + [[nodiscard]] const AssetMetadata& getMetadata() const override { return m_metadata; } + [[nodiscard]] AssetType getType() const override { return getMetadata().type; } + [[nodiscard]] AssetID getID() const override { return getMetadata().id; } + [[nodiscard]] AssetStatus getStatus() const override { return getMetadata().status; } - IAsset& setRawData(void* rawData) override { - delete data; // Clean up existing data - if (rawData == nullptr) { - m_metadata.status = AssetStatus::UNLOADED; - } else { + [[nodiscard]] bool isLoaded() const override { return getStatus() == AssetStatus::LOADED; } + [[nodiscard]] bool isErrored() const override { return getStatus() == AssetStatus::ERROR; } - m_metadata.status = AssetStatus::LOADED; - } - data = static_cast(rawData); - return *this; - } - - [[nodiscard]] TAssetData* getData() const { - return data; - } - - Asset& setData(TAssetData* newData) { - delete data; - if (newData == nullptr) { - m_metadata.status = AssetStatus::UNLOADED; - } else { - m_metadata.status = AssetStatus::LOADED; - } - data = newData; - return *this; - } + [[nodiscard]] const std::unique_ptr& getData() const { return data; } + Asset& setData(std::unique_ptr newData); protected: explicit Asset() : data(nullptr) @@ -253,11 +213,34 @@ namespace nexo::assets { m_metadata.status = AssetStatus::LOADED; } - private: + std::unique_ptr data; + private: /*virtual AssetStatus load() = 0; virtual AssetStatus unload() = 0;*/ - }; + + template + concept IsAsset = + requires { + typename T::AssetDataType; + { T::TYPE } -> std::convertible_to; + } && std::derived_from> + && std::is_base_of_v, T> + && std::is_base_of_v; + + + template + Asset& Asset::setData(std::unique_ptr newData) + { + data.reset(); // Clean up existing data + if (newData == nullptr) { + m_metadata.status = AssetStatus::UNLOADED; + } else { + m_metadata.status = AssetStatus::LOADED; + } + data = std::move(newData); + return *this; + } } // namespace nexo::editor diff --git a/engine/src/assets/AssetCatalog.cpp b/engine/src/assets/AssetCatalog.cpp index 62b861e6d..b2736d0ac 100644 --- a/engine/src/assets/AssetCatalog.cpp +++ b/engine/src/assets/AssetCatalog.cpp @@ -16,6 +16,8 @@ #include +#include + namespace nexo::assets { void AssetCatalog::deleteAsset(AssetID id) @@ -58,12 +60,12 @@ namespace nexo::assets { return assets; } - GenericAssetRef AssetCatalog::registerAsset(const AssetLocation& location, IAsset* asset) + GenericAssetRef AssetCatalog::registerAsset(const AssetLocation& location, std::unique_ptr asset) { if (!asset) return GenericAssetRef::null(); // TODO: implement error handling if already exists (once we have the folder tree) - auto shared_ptr = std::shared_ptr(asset); + std::shared_ptr shared_ptr = std::move(asset); shared_ptr->m_metadata.location = location; if (shared_ptr->m_metadata.id.is_nil()) shared_ptr->m_metadata.id = boost::uuids::random_generator()(); diff --git a/engine/src/assets/AssetCatalog.hpp b/engine/src/assets/AssetCatalog.hpp index 1cb7424e7..600652f25 100644 --- a/engine/src/assets/AssetCatalog.hpp +++ b/engine/src/assets/AssetCatalog.hpp @@ -14,12 +14,14 @@ #pragma once +#include +#include + +#include "Asset.hpp" #include "AssetImporter.hpp" #include "AssetLocation.hpp" -#include "Asset.hpp" -#include -#include +#include "Assets/Texture/Texture.hpp" namespace nexo::assets { @@ -122,8 +124,7 @@ namespace nexo::assets { * @tparam AssetType The type of asset to get. (e.g. Model, Texture) * @return A vector of all assets of the specified type in the catalog. */ - template - requires std::derived_from + template [[nodiscard]] std::vector> getAssetsOfType() const; /** @@ -131,9 +132,8 @@ namespace nexo::assets { * @tparam AssetType The type of asset to get. (e.g. Model, Texture) * @return A view of all assets of the specified type in the catalog. */ - template - requires std::derived_from - [[nodiscard]] std::ranges::view auto getAssetsOfTypeView() const; + template + [[nodiscard]] decltype(auto) getAssetsOfTypeView() const; /** * @brief Registers an asset in the catalog. @@ -149,13 +149,40 @@ namespace nexo::assets { * @param asset Pointer to the asset to be registered. * @return GenericAssetRef A reference to the registered asset, or a null reference if the asset pointer was null. */ - GenericAssetRef registerAsset(const AssetLocation& location, IAsset *asset); + GenericAssetRef registerAsset(const AssetLocation& location, std::unique_ptr asset); + + /** + * @brief Creates and registers a new asset in the catalog. + * + * This method creates a new asset of the specified type, registers it in the catalog, and returns a reference to it. + * + * @tparam AssetType The type of asset to create. Must be derived from IAsset. + * @param location The asset's location metadata. + * @param args Constructor arguments for the asset. + * @return AssetRef A reference to the created and registered asset. + */ + template + AssetRef createAsset(const AssetLocation& location, Args&& ...args) + { + auto asset = std::make_unique(std::forward(args)...); + auto assetRef = registerAsset(location, std::move(asset)); + return assetRef.template as(); + } + + template + AssetRef createAsset(const AssetLocation& location, std::unique_ptr assetData) + { + auto asset = std::make_unique(); + asset->setData(std::move(assetData)); + auto assetRef = registerAsset(location, std::move(asset)); + return assetRef.template as(); + } + private: std::unordered_map> m_assets; }; - template - requires std::derived_from + template std::vector> AssetCatalog::getAssetsOfType() const { // TODO: AssetType::TYPE is not a thing, need to find a way to get the type of the asset @@ -168,8 +195,8 @@ namespace nexo::assets { return assets; } - template requires std::derived_from - std::ranges::view auto AssetCatalog::getAssetsOfTypeView() const + template + decltype(auto) AssetCatalog::getAssetsOfTypeView() const { // TODO: AssetType::TYPE is not a thing, need to find a way to get the type of the asset static_assert(true, "Filtering not implemented yet"); diff --git a/engine/src/assets/AssetImporter.cpp b/engine/src/assets/AssetImporter.cpp index 9c067d3bb..f651bc456 100644 --- a/engine/src/assets/AssetImporter.cpp +++ b/engine/src/assets/AssetImporter.cpp @@ -16,9 +16,13 @@ #include "AssetImporterBase.hpp" #include "AssetCatalog.hpp" +#include "Assets/Model/Model.hpp" #include "Assets/Model/ModelImporter.hpp" +#include "Assets/Texture/Texture.hpp" #include "Assets/Texture/TextureImporter.hpp" +#include + namespace nexo::assets { AssetImporter::AssetImporter() { @@ -59,7 +63,7 @@ namespace nexo::assets { importer->import(*ctx); - const auto asset = ctx->getMainAsset(); + auto asset = ctx->releaseMainAsset(); if (!asset) return GenericAssetRef::null(); if (asset->getID().is_nil()) @@ -67,7 +71,7 @@ namespace nexo::assets { if (asset->m_metadata.location == AssetLocation("default")) asset->m_metadata.location = location; - return AssetCatalog::getInstance().registerAsset(location, asset); + return AssetCatalog::getInstance().registerAsset(location, std::move(asset)); } GenericAssetRef AssetImporter::importAssetTryImporters(const AssetLocation& location, @@ -96,7 +100,7 @@ namespace nexo::assets { getImportersForType(const std::type_index& typeIdx) const { if (const auto it = m_importers.find(typeIdx) ; it == m_importers.end()) { - static const std::vector empty; + static std::vector empty; return empty; } return m_importers.at(typeIdx); diff --git a/engine/src/assets/AssetImporter.hpp b/engine/src/assets/AssetImporter.hpp index 7d57b264c..fa53de4cb 100644 --- a/engine/src/assets/AssetImporter.hpp +++ b/engine/src/assets/AssetImporter.hpp @@ -89,7 +89,7 @@ namespace nexo::assets { * @param importers A list of asset importers to attempt the import operation with. * @return GenericAssetRef A reference to the successfully imported asset, or a null reference if the import fails. */ - GenericAssetRef importAssetTryImporters(const AssetLocation& location, const ImporterInputVariant& inputVariant, const std::vector& + [[nodiscard]] GenericAssetRef importAssetTryImporters(const AssetLocation& location, const ImporterInputVariant& inputVariant, const std::vector& importers) const; /** diff --git a/engine/src/assets/AssetImporterBase.hpp b/engine/src/assets/AssetImporterBase.hpp index 4177de4b7..1a7305c10 100644 --- a/engine/src/assets/AssetImporterBase.hpp +++ b/engine/src/assets/AssetImporterBase.hpp @@ -14,11 +14,11 @@ #pragma once -#include "Asset.hpp" #include "AssetImporterContext.hpp" #include "AssetImporterInput.hpp" #include +#include namespace nexo::assets { @@ -69,7 +69,19 @@ namespace nexo::assets { } } catch (const std::exception& e) { // Log the error - LOG(NEXO_ERROR, "Failed to import asset from file '{}': {}", ctx.location.getPath(), e.what()); + if (std::holds_alternative(ctx.input)) + LOG(NEXO_ERROR, "Failed to import asset {} from file {}: {}", + std::quoted(ctx.location.getFullLocation()), + std::quoted(std::get(ctx.input).filePath.generic_string()), + e.what()); + else if (std::holds_alternative(ctx.input)) + LOG(NEXO_ERROR, "Failed to import asset {} from memory: {}", + std::quoted(ctx.location.getFullLocation()), + e.what()); + else + LOG(NEXO_ERROR, "Failed to import asset {}: {}", + std::quoted(ctx.location.getFullLocation()), + e.what()); } } }; diff --git a/engine/src/assets/AssetImporterContext.cpp b/engine/src/assets/AssetImporterContext.cpp index 7cfce359e..aa1c6581a 100644 --- a/engine/src/assets/AssetImporterContext.cpp +++ b/engine/src/assets/AssetImporterContext.cpp @@ -16,16 +16,22 @@ namespace nexo::assets { - void AssetImporterContext::setMainAsset(IAsset* asset) + void AssetImporterContext::setMainAsset(std::unique_ptr asset) { - m_mainAsset = asset; + m_mainAsset = std::move(asset); } - IAsset* AssetImporterContext::getMainAsset() const + const std::unique_ptr& AssetImporterContext::getMainAsset() const { return m_mainAsset; } + std::unique_ptr AssetImporterContext::releaseMainAsset() + { + return std::move(m_mainAsset); + } + + void AssetImporterContext::addDependency(const GenericAssetRef& dependency) { m_dependencies.push_back(dependency); diff --git a/engine/src/assets/AssetImporterContext.hpp b/engine/src/assets/AssetImporterContext.hpp index a2ce7683f..29fb35ff8 100644 --- a/engine/src/assets/AssetImporterContext.hpp +++ b/engine/src/assets/AssetImporterContext.hpp @@ -48,13 +48,21 @@ namespace nexo::assets { * @param asset The main asset * @note This method must be called by the importer to set the main asset data */ - void setMainAsset(IAsset* asset); + void setMainAsset(std::unique_ptr asset); /** * @brief Get the main asset data for this context * @return The main asset data */ - [[nodiscard]] IAsset* getMainAsset() const; + [[nodiscard]] const std::unique_ptr& getMainAsset() const; + + /** + * @brief Release the main asset data for this context + * @warning This function will take ownership of the mainAsset ptr + * The mainAsset ptr will become NULL in the context + * @return The main asset data + */ + [[nodiscard]] std::unique_ptr releaseMainAsset(); /** * @brief Add dependency to main asset. @@ -113,11 +121,11 @@ namespace nexo::assets { */ static AssetName formatUniqueName(const std::string& name, const AssetType type, unsigned int id) { - return AssetName(std::format("{}_{}{}", name, getAssetTypeName(type), id)); + return {std::format("{}_{}{}", name, getAssetTypeName(type), id)}; } private: - IAsset *m_mainAsset = nullptr; //< Main asset being imported, resulting asset (MUST be set by importer) + std::unique_ptr m_mainAsset = nullptr; //< Main asset being imported, resulting asset (MUST be set by importer) std::vector m_dependencies; //< Dependencies to import json m_jsonParameters; //< JSON parameters for the importer unsigned int m_depUniqueId = 0; //< Unique ID for the dependency name diff --git a/engine/src/assets/AssetImporterInput.hpp b/engine/src/assets/AssetImporterInput.hpp index 36b21bf83..a3e641cce 100644 --- a/engine/src/assets/AssetImporterInput.hpp +++ b/engine/src/assets/AssetImporterInput.hpp @@ -28,7 +28,7 @@ namespace nexo::assets { // Import from memory buffer, importer should read from the buffer struct ImporterMemoryInput { std::vector memoryData; //< Memory buffer - std::optional fileExtension; //< For format detection with memory sources (MUST start with a dot, e.g.: .png) + std::string formatHint; //< For format detection with memory sources (can be ARGB8888, .obj, etc.) }; using ImporterInputVariant = std::variant; diff --git a/engine/src/assets/AssetRef.hpp b/engine/src/assets/AssetRef.hpp index 5ed27c32e..3ed52b8ce 100644 --- a/engine/src/assets/AssetRef.hpp +++ b/engine/src/assets/AssetRef.hpp @@ -56,6 +56,22 @@ namespace nexo::assets { return !m_weakPtr.expired(); } + [[nodiscard]] bool operator==(const GenericAssetRef& other) const noexcept { + return m_weakPtr.lock() == other.m_weakPtr.lock(); + } + + [[nodiscard]] bool operator!=(const GenericAssetRef& other) const noexcept { + return !(*this == other); + } + + [[nodiscard]] bool operator==(const std::nullptr_t) const noexcept { + return !isValid(); + } + + [[nodiscard]] bool operator!=(const std::nullptr_t) const noexcept { + return isValid(); + } + /** * @brief Get a shared_ptr to the referenced asset * @return A shared_ptr to the asset, or nullptr if expired @@ -146,6 +162,8 @@ namespace nexo::assets { explicit AssetRef(const std::shared_ptr& assetPtr) : GenericAssetRef(assetPtr) {} + explicit(false) AssetRef(std::nullptr_t) : GenericAssetRef(nullptr) {} + /** * @brief Locks the asset reference, providing safe access * @return A shared_ptr to the asset, or empty shared_ptr if expired diff --git a/engine/src/assets/Assets/Material/Material.hpp b/engine/src/assets/Assets/Material/Material.hpp new file mode 100644 index 000000000..04b0a3702 --- /dev/null +++ b/engine/src/assets/Assets/Material/Material.hpp @@ -0,0 +1,33 @@ +//// Material.hpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/02/2025 +// Description: Header file for the Material class +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "assets/Asset.hpp" + +namespace nexo::assets { + + /** + * @class Material + * + * @brief Represents a material asset. + */ + class Material final : public Asset { + public: + Material() = default; + + ~Material() override = default; + }; + +} diff --git a/engine/src/assets/Assets/Model/Model.hpp b/engine/src/assets/Assets/Model/Model.hpp index 4a8db1b69..2f14a0397 100644 --- a/engine/src/assets/Assets/Model/Model.hpp +++ b/engine/src/assets/Assets/Model/Model.hpp @@ -16,23 +16,16 @@ #include "assets/Asset.hpp" -#include -#include +#include "components/Shapes3D.hpp" namespace nexo::assets { - struct ModelData { - // Model data - // TODO: Implement model data - const aiScene *scene; - }; - /** * @class Model * * @brief Represents a 3D model asset. */ - class Model final : public Asset { + class Model final : public Asset { public: Model() = default; diff --git a/engine/src/assets/Assets/Model/ModelImporter.cpp b/engine/src/assets/Assets/Model/ModelImporter.cpp new file mode 100644 index 000000000..8d2aea761 --- /dev/null +++ b/engine/src/assets/Assets/Model/ModelImporter.cpp @@ -0,0 +1,382 @@ +//// ModelImporter.cpp //////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 10/04/2025 +// Description: Implementation file for the ModelImporter class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ModelImporter.hpp" + +#include +#include + +#include +#include +#include + +#include "components/Shapes3D.hpp" +#include "Path.hpp" + +#include "assets/AssetImporterBase.hpp" +#include "assets/Assets/Model/Model.hpp" +#include "ModelParameters.hpp" + +#include "core/exceptions/Exceptions.hpp" + +namespace nexo::assets { + + bool ModelImporter::canRead(const ImporterInputVariant& inputVariant) + { + std::string extension; + if (std::holds_alternative(inputVariant)) + extension = std::get(inputVariant).filePath.extension().string(); + if (std::holds_alternative(inputVariant)) { + const auto& mem = std::get(inputVariant); + extension = mem.formatHint; + } + const Assimp::Importer importer; + return importer.IsExtensionSupported(extension); + } + + void ModelImporter::importImpl(AssetImporterContext& ctx) + { + std::unique_ptr model = loadModel(ctx); + ctx.setMainAsset(std::move(model)); + } + + std::unique_ptr ModelImporter::loadModel(AssetImporterContext& ctx) + { + auto model = std::make_unique(); + + const auto param = ctx.getParameters(); + constexpr int flags = aiProcess_Triangulate + | aiProcess_GenNormals; + const aiScene* scene = nullptr; + if (std::holds_alternative(ctx.input)) + scene = m_importer.ReadFile(std::get(ctx.input).filePath.string(), flags); + if (std::holds_alternative(ctx.input)) { + auto& [memoryData, formatHint] = std::get(ctx.input); + scene = m_importer.ReadFileFromMemory(memoryData.data(), memoryData.size(), flags, formatHint.c_str()); + } + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + //log error TODO: improve error handling in importers + if (scene) + m_importer.FreeScene(); + throw core::LoadModelException(ctx.location.getFullLocation(), m_importer.GetErrorString()); + } + + loadSceneEmbeddedTextures(ctx, scene); + loadSceneMaterials(ctx, scene); + + auto meshNode = processNode(ctx, scene->mRootNode, scene); + if (!meshNode) { + throw core::LoadModelException(ctx.location.getFullLocation(), "Failed to process model node"); + } + model->setData(std::make_unique(meshNode)); + return model; + } + + void ModelImporter::loadSceneEmbeddedTextures(AssetImporterContext& ctx, const aiScene* scene) + { + m_textures.reserve(scene->mNumTextures); + // Load embedded textures + for (int i = 0; scene->mNumTextures; ++i) { + aiTexture *texture = scene->mTextures[i]; + auto loadedTexture = loadEmbeddedTexture(ctx, texture); + m_textures.try_emplace(texture->mFilename.C_Str(), loadedTexture); + } + + } + + AssetRef ModelImporter::loadEmbeddedTexture(AssetImporterContext& ctx, aiTexture* texture) + { + if (texture->mHeight == 0) { // Compressed texture + AssetImporter assetImporter; + const ImporterInputVariant inputVariant = ImporterMemoryInput{ + // Reinterpret cast to uint8_t* because this is raw memory data, not aiTexels, see assimp docs + .memoryData = std::vector(reinterpret_cast(texture->pcData), reinterpret_cast(texture->pcData) + texture->mWidth), + .formatHint = std::string(texture->achFormatHint) + }; + + return assetImporter.importAsset( + ctx.genUniqueDependencyLocation(), + inputVariant); + } + // Uncompressed texture + auto& catalog = AssetCatalog::getInstance(); + + renderer::NxTextureFormat format; + if (texture->achFormatHint[0] == '\0') { // if empty, then ARGB888 + renderer::NxTextureFormatConvertArgb8ToRgba8( + reinterpret_cast(texture->pcData), + static_cast(texture->mWidth) * static_cast(texture->mHeight) * sizeof(aiTexel) + ); + format = renderer::NxTextureFormat::RGBA8; + } else { + format = convertAssimpHintToNxTextureFormat(texture->achFormatHint); + } + + if (format == renderer::NxTextureFormat::INVALID) { + LOG(NEXO_WARN, "ModelImporter: Model {}: Texture {} has an invalid format hint: {}", std::quoted(ctx.location.getFullLocation()), texture->mFilename.C_Str(), texture->achFormatHint); + return nullptr; + } + + return catalog.createAsset(ctx.genUniqueDependencyLocation(), + reinterpret_cast(texture->pcData), texture->mWidth, texture->mHeight, format); + } + + renderer::NxTextureFormat ModelImporter::convertAssimpHintToNxTextureFormat(const char achFormatHint[9]) + { + if (std::memchr(achFormatHint, '\0', 9) == nullptr + || std::strlen(achFormatHint) != 8) { + return renderer::NxTextureFormat::INVALID; + } + + // Split into channels (first 4 chars) and bit depths (next 4 chars) + const std::string_view channels(achFormatHint, 4); + const std::string_view bits_str(achFormatHint + 4, 4); + + // Parse active channels and their bit depths + struct ChannelInfo { char code; int bits; }; + std::vector active_channels; + + for (int i = 0; i < 4; ++i) { + const auto ch = static_cast(std::tolower(channels[i])); + if (!(ch == 'r' || ch == 'g' || ch == 'b' || ch == 'a')) { + return renderer::NxTextureFormat::INVALID; + } + if (!std::isdigit(bits_str[i])) { + return renderer::NxTextureFormat::INVALID; + } + const int bits = bits_str[i] - '0'; + + if (ch != '\0' && bits > 0) { + active_channels.push_back({ch, bits}); + } + } + + // Check all active channels have exactly 8 bits + for (const auto& ci : active_channels) { + if (ci.bits != 8) return renderer::NxTextureFormat::INVALID; + } + + // Match channel patterns + switch (active_channels.size()) { + case 1: + if (active_channels[0].code == 'r') + return renderer::NxTextureFormat::R8; + break; + + case 2: + if (active_channels[0].code == 'r' && + active_channels[1].code == 'g') + return renderer::NxTextureFormat::RG8; + break; + + case 3: + if (active_channels[0].code == 'r' && + active_channels[1].code == 'g' && + active_channels[2].code == 'b') + return renderer::NxTextureFormat::RGB8; + break; + + case 4: + if (active_channels[0].code == 'r' && + active_channels[1].code == 'g' && + active_channels[2].code == 'b' && + active_channels[3].code == 'a') + return renderer::NxTextureFormat::RGBA8; + break; + default: + break; + } + + return renderer::NxTextureFormat::INVALID; + } + + void ModelImporter::loadSceneMaterials(AssetImporterContext& ctx, const aiScene* scene) + { + m_materials.assign(scene->mNumMaterials, nullptr); + + std::filesystem::path modelPath; + if (std::holds_alternative(ctx.input)) + modelPath = std::get(ctx.input).filePath; + else { + modelPath = Path::getExecutablePath(); + LOG(NEXO_WARN, "ModelImporter: Model {}: Model path not given (imported from memory), using executable path for texture lookup.", std::quoted(ctx.location.getFullLocation())); + } + std::filesystem::path modelDirectory = modelPath.parent_path(); + + for (unsigned int matIdx = 0; matIdx < scene->mNumMaterials; ++matIdx) { + aiMaterial const *material = scene->mMaterials[matIdx]; + + auto materialComponent = std::make_unique(); + + aiColor4D color; + if (material->Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) { + materialComponent->albedoColor = { color.r, color.g, color.b, color.a }; + } + + if (material->Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) { + materialComponent->specularColor = { color.r, color.g, color.b, color.a }; + } + + if (material->Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) { + materialComponent->emissiveColor = { color.r, color.g, color.b }; + } + + if (float roughness = 0.0f; material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS) { + materialComponent->roughness = roughness; + } + + // Load Metallic + if (float metallic = 0.0f; material->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS) { + materialComponent->metallic = metallic; + } + + if (float opacity = 1.0f; material->Get(AI_MATKEY_OPACITY, opacity) == AI_SUCCESS) { + materialComponent->opacity = opacity; + } + + // Load Textures + + + auto loadTexture = [&](aiTextureType type) -> AssetRef { + if (material->GetTextureCount(type) > 1) { + LOG(NEXO_WARN, "ModelImporter: Model {}: Material {} has more than one texture of type {}, only the first one will be used.", std::quoted(ctx.location.getFullLocation()), matIdx, type); + } + + aiString aiStr; + if (material->GetTexture(type, 0, &aiStr) == AI_SUCCESS) { + const char* cStr = aiStr.C_Str(); + if (cStr[0] == '*' || scene->GetEmbeddedTexture(cStr)) { + // Embedded texture + if (const auto it = m_textures.find(cStr) ; it != m_textures.end()) { + return it->second; + } + } + const std::filesystem::path texturePath = (modelDirectory / cStr).lexically_normal(); + const auto texturePathStr = texturePath.string(); + if (const auto it = m_textures.find(texturePathStr.c_str()) ; it != m_textures.end()) { + return it->second; + } + AssetImporter assetImporter; + const ImporterInputVariant inputVariant = ImporterFileInput{ + .filePath = texturePath + }; + auto assetTexture = assetImporter.importAsset( + ctx.genUniqueDependencyLocation(), + inputVariant); + m_textures.try_emplace(texturePathStr.c_str(), assetTexture); + return assetTexture; + } + return nullptr; + }; + + materialComponent->albedoTexture = loadTexture(aiTextureType_DIFFUSE); + materialComponent->normalMap = loadTexture(aiTextureType_NORMALS); + materialComponent->metallicMap = loadTexture(aiTextureType_SPECULAR); // Specular can store metallic in some cases + materialComponent->roughnessMap = loadTexture(aiTextureType_SHININESS); + materialComponent->emissiveMap = loadTexture(aiTextureType_EMISSIVE); + + LOG(NEXO_INFO, "Loaded material: Diffuse = {}, Normal = {}, Metallic = {}, Roughness = {}", + materialComponent->albedoTexture ? "Yes" : "No", + materialComponent->normalMap ? "Yes" : "No", + materialComponent->metallicMap ? "Yes" : "No", + materialComponent->roughnessMap ? "Yes" : "No"); + + const auto materialRef = AssetCatalog::getInstance().createAsset( + ctx.genUniqueDependencyLocation(), + std::move(materialComponent) + ); + m_materials[matIdx] = materialRef; + } // end for (int matIdx = 0; matIdx < scene->mNumMaterials; ++matIdx) + } + + std::shared_ptr ModelImporter::processNode(AssetImporterContext& ctx, aiNode const* node, + const aiScene* scene) + { + auto meshNode = std::make_shared(); + + const glm::mat4 nodeTransform = convertAssimpMatrixToGLM(node->mTransformation); + + meshNode->transform = nodeTransform; + + for (unsigned int i = 0; i < node->mNumMeshes; i++) + { + aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; + meshNode->meshes.push_back(processMesh(ctx, mesh, scene)); + } + + for (unsigned int i = 0; i < node->mNumChildren; i++) + { + auto newNode = processNode(ctx, node->mChildren[i], scene); + if (newNode) + meshNode->children.push_back(newNode); + } + + return meshNode; + } + + components::Mesh ModelImporter::processMesh(AssetImporterContext& ctx, aiMesh* mesh, [[maybe_unused]] const aiScene* scene) + { + std::vector vertices; + std::vector indices; + vertices.reserve(mesh->mNumVertices); + + for (unsigned int i = 0; i < mesh->mNumVertices; i++) + { + renderer::NxVertex vertex{}; + vertex.position = {mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z}; + + if (mesh->HasNormals()) { + vertex.normal = { mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z }; + } + + if (mesh->mTextureCoords[0]) + vertex.texCoord = {mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y}; + else + vertex.texCoord = {0.0f, 0.0f}; + + vertices.push_back(vertex); + } + + for (unsigned int i = 0; i < mesh->mNumFaces; i++) + { + const aiFace face = mesh->mFaces[i]; + indices.insert(indices.end(), face.mIndices, face.mIndices + face.mNumIndices); + } + + + AssetRef materialComponent = nullptr; + if (mesh->mMaterialIndex < m_materials.size()) { + materialComponent = m_materials[mesh->mMaterialIndex]; + } else { + LOG(NEXO_ERROR, "ModelImporter: Model {}: Mesh {} has invalid material index {}.", std::quoted(ctx.location.getFullLocation()), std::quoted(mesh->mName.C_Str()), mesh->mMaterialIndex); + } + if (!materialComponent) { + LOG(NEXO_WARN, "ModelImporter: Model {}: Mesh {} has no material.", std::quoted(ctx.location.getFullLocation()), std::quoted(mesh->mName.C_Str())); + } + + LOG(NEXO_INFO, "Loaded mesh {}", mesh->mName.data); + return {mesh->mName.data, vertices, indices, materialComponent}; + } + + glm::mat4 ModelImporter::convertAssimpMatrixToGLM(const aiMatrix4x4& matrix) + { + return { + matrix.a1, matrix.b1, matrix.c1, matrix.d1, + matrix.a2, matrix.b2, matrix.c2, matrix.d2, + matrix.a3, matrix.b3, matrix.c3, matrix.d3, + matrix.a4, matrix.b4, matrix.c4, matrix.d4 + }; + } + +} // namespace nexo::assets diff --git a/engine/src/assets/Assets/Model/ModelImporter.hpp b/engine/src/assets/Assets/Model/ModelImporter.hpp index b4b26f4ed..f42c839f5 100644 --- a/engine/src/assets/Assets/Model/ModelImporter.hpp +++ b/engine/src/assets/Assets/Model/ModelImporter.hpp @@ -14,63 +14,40 @@ #pragma once -#include #include #include -#include +#include #include "assets/AssetImporterBase.hpp" #include "assets/Assets/Model/Model.hpp" -#include "ModelParameters.hpp" +#include "assets/AssetRef.hpp" namespace nexo::assets { - class ModelImporter final : public AssetImporterBase { + class ModelImporter : public AssetImporterBase { public: ModelImporter() = default; ~ModelImporter() override = default; - bool canRead(const ImporterInputVariant& inputVariant) override - { - std::string extension; - if (std::holds_alternative(inputVariant)) - extension = std::get(inputVariant).filePath.extension().string(); - if (std::holds_alternative(inputVariant)) { - const auto& mem = std::get(inputVariant); - if (mem.fileExtension) - extension = mem.fileExtension.value(); - } - const Assimp::Importer importer; - return importer.IsExtensionSupported(extension); - } + bool canRead(const ImporterInputVariant& inputVariant) override; - void importImpl(AssetImporterContext& ctx) override - { - m_model = new Model(); - m_model->setData(new ModelData()); - const auto param = ctx.getParameters(); - int flags = aiProcess_Triangulate - | aiProcess_FlipUVs - | aiProcess_GenNormals; - const aiScene* scene = nullptr; - if (std::holds_alternative(ctx.input)) - scene = m_importer.ReadFile(std::get(ctx.input).filePath.string(), flags); - if (std::holds_alternative(ctx.input)) { - auto memInput = std::get(ctx.input); - scene = m_importer.ReadFileFromMemory(memInput.memoryData.data(), memInput.memoryData.size(), flags, memInput.fileExtension ? memInput.fileExtension->c_str() : nullptr); - } - if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { - //log error TODO: improve error handling in importers - auto error = m_importer.GetErrorString(); - LOG(NEXO_ERROR, "Error while importing model: {}: {}", ctx.location.getPath(), error); - } - m_model->data->scene = scene; - ctx.setMainAsset(m_model); - } + void importImpl(AssetImporterContext& ctx) override; protected: - Model *m_model = nullptr; //< Model being imported + std::unique_ptr loadModel(AssetImporterContext& ctx); + void loadSceneEmbeddedTextures(AssetImporterContext& ctx, const aiScene* scene); + AssetRef loadEmbeddedTexture(AssetImporterContext& ctx, aiTexture *texture); + void loadSceneMaterials(AssetImporterContext& ctx, const aiScene* scene); + + std::shared_ptr processNode(AssetImporterContext& ctx, aiNode const *node, const aiScene* scene); + components::Mesh processMesh(AssetImporterContext& ctx, aiMesh* mesh, const aiScene* scene); + + static renderer::NxTextureFormat convertAssimpHintToNxTextureFormat(const char achFormatHint[9]); + static glm::mat4 convertAssimpMatrixToGLM(const aiMatrix4x4& matrix); + Assimp::Importer m_importer; //< Assimp importer instance + std::unordered_map> m_textures; //< Map of textures used in the model, std::string is the texture name (path, or *0, *1, etc. for embedded textures, see assimp) + std::vector> m_materials; //< Map of materials used in the model, index is the assimp material index }; } // namespace nexo::assets diff --git a/engine/src/assets/Assets/Model/ModelParameters.hpp b/engine/src/assets/Assets/Model/ModelParameters.hpp index 1d42f8b06..94f7ef1bd 100644 --- a/engine/src/assets/Assets/Model/ModelParameters.hpp +++ b/engine/src/assets/Assets/Model/ModelParameters.hpp @@ -30,7 +30,7 @@ namespace nexo::assets { NLOHMANN_DEFINE_TYPE_INTRUSIVE(ModelImportParameters, textureParameters - ); + ) }; /** @@ -71,7 +71,7 @@ namespace nexo::assets { globalScale, textureQuality, convertToUncompressed - ); + ) }; NLOHMANN_JSON_SERIALIZE_ENUM(ModelImportPostProcessParameters::TextureQuality, diff --git a/engine/src/assets/Assets/Texture/Texture.hpp b/engine/src/assets/Assets/Texture/Texture.hpp index 731b0f7e3..fd8e67245 100644 --- a/engine/src/assets/Assets/Texture/Texture.hpp +++ b/engine/src/assets/Assets/Texture/Texture.hpp @@ -19,9 +19,7 @@ namespace nexo::assets { struct TextureData { - // Texture data - // TODO: Implement texture data - std::shared_ptr texture; + std::shared_ptr texture; }; /** @@ -31,8 +29,111 @@ namespace nexo::assets { */ class Texture final : public Asset { public: + + /** + * @brief Default constructor. + * + * Creates an empty Texture asset without an underlying texture. + * Use one of the provided static factory methods to create a fully initialized texture. + */ Texture() = default; + /** + * @brief Constructs a Texture object from a file path. + * + * Creates a texture asset by loading image data from the specified file. + * Supported formats include PNG, JPEG, BMP, GIF, TGA, and more + * (any format supported by stb_image). + * + * @param path The path to the texture file. + * + * @throws NxFileNotFoundException If the file cannot be found or read. + * @throws NxTextureUnsupportedFormat If the image format is not supported. + * @throws NxTextureInvalidSize If the image dimensions exceed the maximum texture size. + */ + explicit Texture(const std::filesystem::path &path) + : Asset() + { + const auto texture = renderer::NxTexture2D::create(path.string()); + auto textureData = std::make_unique(); + textureData->texture = texture; + setData(std::move(textureData)); + } + + /** + * @brief Constructs a blank Texture with the specified dimensions. + * + * Creates a new empty texture with the given width and height. + * The texture is initialized with transparent black pixels (all zeros). + * This is useful for creating render targets or textures that will be + * filled with data later. + * + * @param width The width of the texture in pixels. + * @param height The height of the texture in pixels. + * + * @throws NxTextureInvalidSize If the dimensions exceed the maximum texture size. + */ + Texture(unsigned int width, unsigned int height) + : Asset() + { + const auto texture = renderer::NxTexture2D::create(width, height); + auto textureData = std::make_unique(); + textureData->texture = texture; + setData(std::move(textureData)); + } + + /** + * @brief Constructs a Texture from raw pixel data in memory. + * + * Creates a texture from a raw pixel buffer with the specified dimensions and format. + * This is useful for creating textures from procedurally generated data or when working + * with raw pixel data from other sources. + * + * @param buffer Pointer to the raw pixel data. The buffer should contain pixel data + * in a format that matches the specified NxTextureFormat. The data consists + * of height scanlines of width pixels, with each pixel consisting of N components + * (where N depends on the format). The first pixel pointed to is bottom-left-most + * in the image. There is no padding between image scanlines or between pixels. + * Each component is an 8-bit unsigned value (uint8_t). + * @param width The width of the texture in pixels. + * @param height The height of the texture in pixels. + * @param format The format of the pixel data (R8, RG8, RGB8, or RGBA8). + * + * @throws NxInvalidValue If the buffer is null. + * @throws NxTextureUnsupportedFormat If the specified format is not supported. + * @throws NxTextureInvalidSize If the dimensions exceed the maximum texture size. + */ + Texture(const uint8_t *buffer, const unsigned int width, const unsigned int height, const renderer::NxTextureFormat format) + : Asset() + { + const auto texture = renderer::NxTexture2D::create(buffer, width, height, format); + auto textureData = std::make_unique(); + textureData->texture = texture; + setData(std::move(textureData)); + } + + /** + * @brief Constructs a Texture from image data in memory. + * + * Creates a texture by loading image data from a memory buffer. + * The buffer should contain a complete image file (e.g., PNG, JPEG) + * that will be decoded using stb_image. + * + * @param buffer Pointer to the image file data in memory. + * @param size Size of the buffer in bytes. + * + * @throws NxTextureUnsupportedFormat If the image format is not supported. + * @throws NxTextureInvalidSize If the image dimensions exceed the maximum texture size. + */ + Texture(const uint8_t* buffer, const unsigned int size) + : Asset() + { + const auto texture = renderer::NxTexture2D::create(buffer, size); + auto textureData = std::unique_ptr(); + textureData->texture = texture; + setData(std::move(textureData)); + } + ~Texture() override = default; }; diff --git a/engine/src/assets/Assets/Texture/TextureImporter.cpp b/engine/src/assets/Assets/Texture/TextureImporter.cpp new file mode 100644 index 000000000..0dacf5b4d --- /dev/null +++ b/engine/src/assets/Assets/Texture/TextureImporter.cpp @@ -0,0 +1,70 @@ +//// TextureImporter.cpp ////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 13/04/2025 +// Description: Implementation file for the TextureImporter class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "TextureImporter.hpp" + +#include +#include +#include "assets/AssetImporterBase.hpp" +#include "assets/Assets/Texture/Texture.hpp" +#include + +namespace nexo::assets { + + bool TextureImporter::canRead(const ImporterInputVariant& inputVariant) + { + if (std::holds_alternative(inputVariant)) + return canReadFile(std::get(inputVariant)); + if (std::holds_alternative(inputVariant)) { + return canReadMemory(std::get(inputVariant)); + } + return false; + } + + void TextureImporter::importImpl(AssetImporterContext& ctx) + { + // TODO: we need to import textures independently from graphics API back end renderer::NxTexture2D::create implementation + auto asset = std::make_unique(); + std::shared_ptr rendererTexture; + if (std::holds_alternative(ctx.input)) + rendererTexture = renderer::NxTexture2D::create(std::get(ctx.input).filePath.string()); + else { + auto data = std::get(ctx.input).memoryData; + rendererTexture = renderer::NxTexture2D::create(data.data(), data.size()); + } + auto assetData = std::make_unique(); + assetData->texture = rendererTexture; + + asset->m_metadata.id = boost::uuids::random_generator()(); + + asset->setData(std::move(assetData)); + ctx.setMainAsset(std::move(asset)); + } + + bool TextureImporter::canReadMemory(const ImporterMemoryInput& input) + { + if (input.formatHint == "ARGB8888") { // Special case for ARGB8888 format (from assimp model textures) + return true; + } + const int ok = stbi_info_from_memory(input.memoryData.data(), static_cast(input.memoryData.size()), nullptr, nullptr, nullptr); + return ok; + } + + bool TextureImporter::canReadFile(const ImporterFileInput& input) + { + const int ok = stbi_info(input.filePath.string().c_str(), nullptr, nullptr, nullptr); + return ok; + } + +} // namespace nexo::assets diff --git a/engine/src/assets/Assets/Texture/TextureImporter.hpp b/engine/src/assets/Assets/Texture/TextureImporter.hpp index 4c94b5d4b..9b1b4aadb 100644 --- a/engine/src/assets/Assets/Texture/TextureImporter.hpp +++ b/engine/src/assets/Assets/Texture/TextureImporter.hpp @@ -14,11 +14,7 @@ #pragma once -#include -#include #include "assets/AssetImporterBase.hpp" -#include "assets/Assets/Texture/Texture.hpp" -#include "TextureParameters.hpp" namespace nexo::assets { @@ -27,48 +23,11 @@ namespace nexo::assets { TextureImporter() = default; ~TextureImporter() override = default; - bool canRead(const ImporterInputVariant& inputVariant) override - { - if (std::holds_alternative(inputVariant)) - return canReadFile(std::get(inputVariant)); - if (std::holds_alternative(inputVariant)) { - return canReadMemory(std::get(inputVariant)); - } - return false; - } - - void importImpl(AssetImporterContext& ctx) override - { - auto asset = new Texture(); - std::shared_ptr rendererTexture; - if (std::holds_alternative(ctx.input)) - rendererTexture = renderer::Texture2D::create(std::get(ctx.input).filePath.string()); - else { - auto data = std::get(ctx.input).memoryData; - rendererTexture = renderer::Texture2D::create(data.data(), data.size()); - } - auto assetData = new TextureData(); - assetData->texture = rendererTexture; - asset->setData(assetData); - asset->m_metadata.id = boost::uuids::random_generator()(); - - ctx.setMainAsset(asset); - } - + bool canRead(const ImporterInputVariant& inputVariant) override; + void importImpl(AssetImporterContext& ctx) override; protected: - - bool canReadMemory(const ImporterMemoryInput& input) - { - const int ok = stbi_info_from_memory(input.memoryData.data(), input.memoryData.size(), nullptr, nullptr, nullptr); - return ok; - } - - bool canReadFile(const ImporterFileInput& input) - { - const int ok = stbi_info(input.filePath.string().c_str(), nullptr, nullptr, nullptr); - return ok; - } - + static bool canReadMemory(const ImporterMemoryInput& input); + static bool canReadFile(const ImporterFileInput& input); }; } // namespace nexo::assets diff --git a/engine/src/assets/Assets/Texture/TextureParameters.hpp b/engine/src/assets/Assets/Texture/TextureParameters.hpp index 90a792dac..e2845e743 100644 --- a/engine/src/assets/Assets/Texture/TextureParameters.hpp +++ b/engine/src/assets/Assets/Texture/TextureParameters.hpp @@ -46,7 +46,7 @@ namespace nexo::assets { format, maxSize, compressionQuality - ); + ) }; /** diff --git a/engine/src/components/Camera.hpp b/engine/src/components/Camera.hpp index 5d4efabea..8bac373de 100644 --- a/engine/src/components/Camera.hpp +++ b/engine/src/components/Camera.hpp @@ -17,11 +17,10 @@ #include "math/Vector.hpp" #include "core/event/Input.hpp" #include "renderer/Framebuffer.hpp" -#include "ecs/Entity.hpp" +#include "ecs/Definitions.hpp" #include #include - namespace nexo::components { enum class CameraType { @@ -40,7 +39,7 @@ namespace nexo::components { unsigned int width; ///< Width of the camera's viewport. unsigned int height; ///< Height of the camera's viewport. bool viewportLocked = false; ///< If true, the viewport dimensions are locked. - float fov = 45.0f; ///< Field of view (in degrees) for perspective cameras. + float fov = 45.0f; ///< Field of view (in degrees) for perspective cameras.- float nearPlane = 0.1f; ///< Near clipping plane distance. float farPlane = 1000.0f; ///< Far clipping plane distance. CameraType type = CameraType::PERSPECTIVE; ///< The type of the camera (perspective or orthographic). @@ -48,10 +47,11 @@ namespace nexo::components { glm::vec4 clearColor = {37.0f/255.0f, 35.0f/255.0f, 50.0f/255.0f, 111.0f/255.0f}; ///< Background clear color. bool active = true; ///< Indicates if the camera is active. + bool render = false; ///< Indicates if the camera has to be rendered. bool main = true; ///< Indicates if the camera is the main camera. bool resizing = false; ///< Internal flag indicating if the camera is resizing. - std::shared_ptr m_renderTarget = nullptr; ///< The render target framebuffer. + std::shared_ptr m_renderTarget = nullptr; ///< The render target framebuffer. /** * @brief Retrieves the projection matrix for this camera. @@ -102,8 +102,56 @@ namespace nexo::components { m_renderTarget->resize(newWidth, newHeight); } } + + struct Memento { + unsigned int width; + unsigned int height; + bool viewportLocked; + float fov; + float nearPlane; + float farPlane; + CameraType type; + glm::vec4 clearColor; + bool main; + std::shared_ptr renderTarget; + }; + + void restore(const Memento& memento) + { + if (width != memento.width || height != memento.height) { + width = memento.width; + height = memento.height; + resize(width, height); + } + viewportLocked = memento.viewportLocked; + fov = memento.fov; + nearPlane = memento.nearPlane; + farPlane = memento.farPlane; + type = memento.type; + clearColor = memento.clearColor; + main = memento.main; + m_renderTarget = memento.renderTarget; + } + + [[nodiscard]] Memento save() const + { + return { + width, + height, + viewportLocked, + fov, + nearPlane, + farPlane, + type, + clearColor, + main, + m_renderTarget + }; + } }; + struct EditorCameraTag {}; + /** * @brief Component used to control a perspective camera using mouse input. * @@ -115,8 +163,28 @@ namespace nexo::components { glm::vec2 lastMousePosition{}; ///< Last recorded mouse position. float mouseSensitivity = 0.1f;///< Sensitivity factor for mouse movement. - float yaw = 0.0f; ///< Yaw angle in degrees. - float pitch = 0.0f; ///< Pitch angle in degrees. + float translationSpeed = 5.0f; ///< Camera speed + bool wasMouseReleased = true; + bool wasActiveLastFrame = true; + + struct Memento { + float mouseSensitivity; + float translationSpeed; + }; + + void restore(const Memento& memento) + { + mouseSensitivity = memento.mouseSensitivity; + translationSpeed = memento.translationSpeed; + } + + [[nodiscard]] Memento save() const + { + return { + mouseSensitivity, + translationSpeed + }; + } }; /** @@ -129,6 +197,28 @@ namespace nexo::components { float mouseSensitivity = 0.1f; ///< Sensitivity factor for mouse movement. float distance = 5.0f; ///< Distance from the camera to the target entity. ecs::Entity targetEntity; ///< The target entity the camera is focusing on. + + struct Memento { + float mouseSensitivity; + float distance; + ecs::Entity targetEntity; + }; + + void restore(const Memento& memento) + { + mouseSensitivity = memento.mouseSensitivity; + distance = memento.distance; + targetEntity = memento.targetEntity; + } + + [[nodiscard]] Memento save() const + { + return { + mouseSensitivity, + distance, + targetEntity + }; + } }; /** @@ -141,6 +231,6 @@ namespace nexo::components { glm::mat4 viewProjectionMatrix; ///< Combined view and projection matrix. glm::vec3 cameraPosition; ///< The position of the camera. glm::vec4 clearColor; ///< Clear color used for rendering. - std::shared_ptr renderTarget; ///< The render target framebuffer. + std::shared_ptr renderTarget; ///< The render target framebuffer. }; } diff --git a/engine/src/ecs/Signature.hpp b/engine/src/components/Editor.hpp similarity index 66% rename from engine/src/ecs/Signature.hpp rename to engine/src/components/Editor.hpp index 1972fc3be..2414f2649 100644 --- a/engine/src/ecs/Signature.hpp +++ b/engine/src/components/Editor.hpp @@ -1,4 +1,4 @@ -//// Signature.hpp //////////////////////////////////////////////////////////// +//// Editor.hpp /////////////////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -7,15 +7,12 @@ // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // // Author: Mehdy MORVAN -// Date: 08/11/2024 -// Description: Header file for the signatures +// Date: 19/04/2025 +// Description: Header file for the editor related components // /////////////////////////////////////////////////////////////////////////////// #pragma once -#include -#include "Components.hpp" - -namespace nexo::ecs { - using Signature = std::bitset; +namespace nexo::components { + struct SelectedTag {}; } diff --git a/engine/src/components/Light.hpp b/engine/src/components/Light.hpp index fcbcd6974..192733d36 100644 --- a/engine/src/components/Light.hpp +++ b/engine/src/components/Light.hpp @@ -17,7 +17,8 @@ #include #include -constexpr unsigned int MAX_DIRECTIONAL_LIGHTS = 8; +#include "ecs/Definitions.hpp" + constexpr unsigned int MAX_POINT_LIGHTS = 8; constexpr unsigned int MAX_SPOT_LIGHTS = 8; @@ -25,6 +26,22 @@ namespace nexo::components { struct AmbientLightComponent { glm::vec3 color{}; + + struct Memento { + glm::vec3 color; + }; + + void restore(const Memento& memento) + { + color = memento.color; + } + + Memento save() const + { + return {color}; + } + + }; struct DirectionalLightComponent { @@ -35,58 +52,99 @@ namespace nexo::components { glm::vec3 direction{}; glm::vec3 color{}; + + struct Memento { + glm::vec3 direction; + glm::vec3 color; + }; + + void restore(const Memento& memento) + { + direction = memento.direction; + color = memento.color; + } + + Memento save() const + { + return {direction, color}; + } }; struct PointLightComponent { - PointLightComponent() = default; - explicit PointLightComponent(const glm::vec3 lightPos, - const glm::vec3 &lightColor = {1.0f, 1.0f, 1.0f}, - const float linear = 0.09f, - const float quadratic = 0.032f) : - pos(lightPos), color(lightColor), - linear(linear), quadratic(quadratic) {}; - - glm::vec3 pos{}; glm::vec3 color{}; - float maxDistance = 50.0f; - float constant = 1.0f; float linear{}; float quadratic{}; + float maxDistance = 50.0f; + float constant = 1.0f; + + struct Memento { + glm::vec3 color{}; + float linear{}; + float quadratic{}; + float maxDistance; + float constant; + }; + + void restore(const Memento& memento) + { + color = memento.color; + linear = memento.linear; + quadratic = memento.quadratic; + maxDistance = memento.maxDistance; + constant = memento.constant; + } + + Memento save() const + { + return {color, linear, quadratic, maxDistance, constant}; + } }; struct SpotLightComponent { - SpotLightComponent() = default; - explicit SpotLightComponent(glm::vec3 lightPos, - glm::vec3 lightDir, - glm::vec3 lightColor, - float cutOff, - float outerCutoff, - float linear = 0.0014f, - float quadractic = 0.0007f) : - pos(lightPos), color(lightColor), - direction(lightDir), cutOff(cutOff), - outerCutoff(outerCutoff), linear(linear), - quadratic(quadractic) {} - - glm::vec3 pos{}; - glm::vec3 color{}; - glm::vec3 direction{}; - float maxDistance = 325.0f; - float cutOff{}; - float outerCutoff{}; - - float constant = 1.0f; + glm::vec3 direction{}; + glm::vec3 color{}; + float cutOff{}; + float outerCutoff{}; float linear{}; float quadratic{}; + float maxDistance = 325.0f; + float constant = 1.0f; + + struct Memento { + glm::vec3 direction{}; + glm::vec3 color{}; + float cutOff{}; + float outerCutoff{}; + float linear{}; + float quadratic{}; + float maxDistance; + float constant; + }; + + void restore(const Memento& memento) + { + direction = memento.direction; + color = memento.color; + cutOff = memento.cutOff; + outerCutoff = memento.outerCutoff; + linear = memento.linear; + quadratic = memento.quadratic; + maxDistance = memento.maxDistance; + constant = memento.constant; + } + + Memento save() const + { + return {direction, color, cutOff, outerCutoff, linear, quadratic, maxDistance, constant}; + } }; struct LightContext { - glm::vec3 ambientLight; - std::array pointLights; - unsigned int pointLightCount = 0; - std::array spotLights; - unsigned int spotLightCount = 0; - std::array directionalLights; - unsigned int directionalLightCount = 0; + glm::vec3 ambientLight; + std::array pointLights; + unsigned int pointLightCount = 0; + std::array spotLights; + unsigned int spotLightCount = 0; + DirectionalLightComponent dirLight; }; } diff --git a/engine/src/components/Render.hpp b/engine/src/components/Render.hpp index 36386fecd..a50dcbcdd 100644 --- a/engine/src/components/Render.hpp +++ b/engine/src/components/Render.hpp @@ -36,7 +36,7 @@ namespace nexo::components bool isRendered = true; - virtual void draw(std::shared_ptr &context, + virtual void draw(std::shared_ptr &context, const TransformComponent &transf, int entityID) const = 0; [[nodiscard]] virtual std::shared_ptr clone() const = 0; }; @@ -51,7 +51,7 @@ namespace nexo::components { }; - void draw(std::shared_ptr &context, + void draw(std::shared_ptr &context, const TransformComponent &transform, int entityID) const override { shape->draw(context, transform, sprite, entityID); @@ -66,13 +66,13 @@ namespace nexo::components }; struct Renderable3D final : Renderable { - Material material; + Material material; // TODO: replace with AssetRef std::shared_ptr shape; explicit Renderable3D(Material material, const std::shared_ptr &shape) : material(std::move(material)), shape(shape) {}; - void draw(std::shared_ptr &context, const TransformComponent &transf, int entityID) const override + void draw(std::shared_ptr &context, const TransformComponent &transf, int entityID) const override { shape->draw(context, transf, material, entityID); } @@ -97,13 +97,37 @@ namespace nexo::components { } - void draw(std::shared_ptr &context, const TransformComponent &transform, const int entityID = -1) const + void draw(std::shared_ptr &context, const TransformComponent &transform, const int entityID = -1) const { if (isRendered && renderable) renderable->draw(context, transform, entityID); } - [[nodiscard]] RenderComponent clone() const { + struct Memento { + bool isRendered = true; + RenderType type = RenderType::RENDER_2D; + + std::shared_ptr renderable; + }; + + void restore(const Memento &memento) + { + isRendered = memento.isRendered; + type = memento.type; + renderable = memento.renderable; + } + + [[nodiscard]] Memento save() const + { + return { + isRendered, + type, + renderable ? renderable->clone() : nullptr + }; + } + + [[nodiscard]] RenderComponent clone() const + { RenderComponent copy; copy.isRendered = isRendered; copy.type = type; diff --git a/engine/src/components/Render2D.hpp b/engine/src/components/Render2D.hpp index 5efa4b5d0..8dc48138d 100644 --- a/engine/src/components/Render2D.hpp +++ b/engine/src/components/Render2D.hpp @@ -20,8 +20,8 @@ namespace nexo::components { struct SpriteComponent { glm::vec4 color; - std::shared_ptr texture = nullptr; - std::shared_ptr sprite = nullptr; + std::shared_ptr texture = nullptr; + std::shared_ptr sprite = nullptr; }; } diff --git a/engine/src/components/Render3D.hpp b/engine/src/components/Render3D.hpp index 7f91d6605..33ba4f397 100644 --- a/engine/src/components/Render3D.hpp +++ b/engine/src/components/Render3D.hpp @@ -13,10 +13,10 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "renderer/Texture.hpp" -#include "renderer/Shader.hpp" #include -#include + +#include "assets/AssetRef.hpp" +#include "assets/Assets/Texture/Texture.hpp" namespace nexo::components { @@ -25,16 +25,18 @@ namespace nexo::components { glm::vec4 specularColor = glm::vec4(1.0f); glm::vec3 emissiveColor = glm::vec3(0.0f); + bool isOpaque = true; + float roughness = 0.0f; // 0 = smooth, 1 = rough float metallic = 0.0f; // 0 = non-metal, 1 = fully metallic float opacity = 1.0f; // 1 = opaque, 0 = fully transparent - std::shared_ptr albedoTexture = nullptr; - std::shared_ptr normalMap = nullptr; - std::shared_ptr metallicMap = nullptr; - std::shared_ptr roughnessMap = nullptr; - std::shared_ptr emissiveMap = nullptr; + assets::AssetRef albedoTexture = nullptr; + assets::AssetRef normalMap = nullptr; + assets::AssetRef metallicMap = nullptr; + assets::AssetRef roughnessMap = nullptr; + assets::AssetRef emissiveMap = nullptr; - std::optional> shader = std::nullopt; + std::string shader = ""; }; } diff --git a/engine/src/components/RenderContext.hpp b/engine/src/components/RenderContext.hpp index bcedd1e08..ab6402601 100644 --- a/engine/src/components/RenderContext.hpp +++ b/engine/src/components/RenderContext.hpp @@ -15,45 +15,63 @@ #include #include "Camera.hpp" +#include "Types.hpp" #include "renderer/Renderer3D.hpp" #include "Light.hpp" namespace nexo::components { - struct RenderContext { - int sceneRendered = -1; - renderer::Renderer3D renderer3D; - std::queue cameras; - LightContext sceneLights; - - RenderContext() - { - renderer3D.init(); - } - - RenderContext(RenderContext&& other) noexcept - : sceneRendered(other.sceneRendered), - renderer3D(std::move(other.renderer3D)), - cameras(std::move(other.cameras)), - sceneLights(std::move(other.sceneLights)) - { - } + struct RenderContext { + int sceneRendered = -1; + SceneType sceneType = SceneType::GAME; + bool isChildWindow = false; //<< Is the current scene embedded in a sub window ? + glm::vec2 viewportBounds[2]{}; //<< Viewport bounds in absolute coordinates (if the window viewport is embedded in the window), this is used for mouse coordinates + struct GridParams { + bool enabled = true; + float gridSize = 100.0f; + float minPixelsBetweenCells = 2.0f; + float cellSize = 0.025f; + }; + GridParams gridParams; + renderer::NxRenderer3D renderer3D; + std::queue cameras; + LightContext sceneLights{}; + + + RenderContext() + { + renderer3D.init(); + } + + // Delete copy constructor to enforce singleton semantics + RenderContext(const RenderContext&) = delete; + RenderContext& operator=(const RenderContext&) = delete; + + RenderContext(RenderContext&& other) noexcept + : sceneRendered(other.sceneRendered), sceneType{}, + renderer3D(std::move(other.renderer3D)), + cameras(std::move(other.cameras)), + sceneLights(other.sceneLights) + {} ~RenderContext() { - renderer3D.shutdown(); + renderer3D.shutdown(); - reset(); + reset(); } - void reset() - { - sceneRendered = -1; - std::queue empty; - std::swap(cameras, empty); - sceneLights.ambientLight = glm::vec3(0.0f); - sceneLights.pointLightCount = 0; - sceneLights.spotLightCount = 0; - sceneLights.directionalLightCount = 0; - } - }; + void reset() + { + sceneRendered = -1; + isChildWindow = false; + viewportBounds[0] = glm::vec2{}; + viewportBounds[1] = glm::vec2{}; + std::queue empty; + std::swap(cameras, empty); + sceneLights.ambientLight = glm::vec3(0.0f); + sceneLights.pointLightCount = 0; + sceneLights.spotLightCount = 0; + sceneLights.dirLight = DirectionalLightComponent{}; + } + }; } diff --git a/engine/src/components/SceneComponents.hpp b/engine/src/components/SceneComponents.hpp index 0dcdadd87..71e168077 100644 --- a/engine/src/components/SceneComponents.hpp +++ b/engine/src/components/SceneComponents.hpp @@ -23,9 +23,27 @@ namespace nexo::components { }; struct SceneTag { - unsigned int id; - bool isActive = true; - bool isRendered = true; + unsigned int id{}; + bool isActive = true; + bool isRendered = true; + + struct Memento { + unsigned int id; + bool isActive; + bool isRendered; + }; + + void restore(const Memento &memento) + { + id = memento.id; + isActive = memento.isActive; + isRendered = memento.isRendered; + } + + [[nodiscard]] Memento save() const + { + return {id, isActive, isRendered}; + } }; } diff --git a/engine/src/components/Shapes2D.hpp b/engine/src/components/Shapes2D.hpp index 3088f3687..c4fe75528 100644 --- a/engine/src/components/Shapes2D.hpp +++ b/engine/src/components/Shapes2D.hpp @@ -24,11 +24,11 @@ namespace nexo::components { struct Shape2D { virtual ~Shape2D() = default; - virtual void draw(std::shared_ptr &context, const TransformComponent &transf, const SpriteComponent &sprite, int entityID) const = 0; + virtual void draw(std::shared_ptr &context, const TransformComponent &transf, const SpriteComponent &sprite, int entityID) const = 0; }; struct Quad final : Shape2D { - void draw([[maybe_unused]] std::shared_ptr &context, [[maybe_unused]] const TransformComponent &transf, [[maybe_unused]] const SpriteComponent &sprite, [[maybe_unused]] int entityID) const override + void draw([[maybe_unused]] std::shared_ptr &context, [[maybe_unused]] const TransformComponent &transf, [[maybe_unused]] const SpriteComponent &sprite, [[maybe_unused]] int entityID) const override { //if (sprite.sprite != nullptr) // renderer2D.drawQuad(transf.pos, {transf.size.x, transf.size.y}, transf.rotation.z, diff --git a/engine/src/components/Shapes3D.hpp b/engine/src/components/Shapes3D.hpp index 3069ea6f2..fd185f597 100644 --- a/engine/src/components/Shapes3D.hpp +++ b/engine/src/components/Shapes3D.hpp @@ -13,29 +13,54 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "Render3D.hpp" -#include "Transform.hpp" -#include "renderer/RendererContext.hpp" #define GLM_ENABLE_EXPERIMENTAL #include #include #include -#include + +#include "Render3D.hpp" +#include "Transform.hpp" +#include "renderer/RendererContext.hpp" +#include "assets/Assets/Material/Material.hpp" namespace nexo::components { struct Shape3D { virtual ~Shape3D() = default; - virtual void draw(std::shared_ptr &context, const TransformComponent &transf, const Material &material, int entityID) = 0; + virtual void draw(std::shared_ptr &context, const TransformComponent &transf, const Material &material, int entityID) = 0; [[nodiscard]] virtual std::shared_ptr clone() const = 0; }; struct Cube final : Shape3D { - void draw(std::shared_ptr &context, const TransformComponent &transf, const Material &material, const int entityID) override + void draw(std::shared_ptr &context, const TransformComponent &transf, const Material &material, const int entityID) override { const auto renderer3D = context->renderer3D; - renderer3D.drawCube(transf.pos, transf.size, transf.quat, material, entityID); + + // lock all textures + auto albedoTextureAsset = material.albedoTexture.lock(); + auto normalMapAsset = material.normalMap.lock(); + auto metallicMapAsset = material.metallicMap.lock(); + auto roughnessMapAsset = material.roughnessMap.lock(); + auto emissiveMapAsset = material.emissiveMap.lock(); + + + renderer::NxMaterial inputMaterial = { + .albedoColor = material.albedoColor, + .specularColor = material.specularColor, + .emissiveColor = material.emissiveColor, + .roughness = material.roughness, + .metallic = material.metallic, + .opacity = material.opacity, + .albedoTexture = albedoTextureAsset && albedoTextureAsset->isLoaded() ? albedoTextureAsset->getData()->texture : nullptr, + .normalMap = normalMapAsset && normalMapAsset->isLoaded() ? normalMapAsset->getData()->texture : nullptr, + .metallicMap = metallicMapAsset && metallicMapAsset->isLoaded() ? metallicMapAsset->getData()->texture : nullptr, + .roughnessMap = roughnessMapAsset && roughnessMapAsset->isLoaded() ? roughnessMapAsset->getData()->texture : nullptr, + .emissiveMap = emissiveMapAsset && emissiveMapAsset->isLoaded() ? emissiveMapAsset->getData()->texture : nullptr, + .shader = material.shader + }; + + renderer3D.drawCube(transf.pos, transf.size, transf.quat, inputMaterial, entityID); } [[nodiscard]] std::shared_ptr clone() const override { @@ -45,9 +70,9 @@ namespace nexo::components { struct Mesh { std::string name; - std::vector vertices; + std::vector vertices; std::vector indices; - std::optional material; + assets::AssetRef material; }; struct MeshNode { @@ -55,16 +80,23 @@ namespace nexo::components { std::vector meshes; std::vector> children; - void draw(renderer::Renderer3D &renderer3D, const glm::mat4 &parentTransform, const int entityID) const + void draw(renderer::NxRenderer3D &renderer3D, const glm::mat4 &parentTransform, const int entityID) const { const glm::mat4 localTransform = parentTransform * transform; for (const auto &mesh: meshes) { //TODO: Implement a way to pass the transform directly to the shader - std::vector transformedVertices = mesh.vertices; + std::vector transformedVertices = mesh.vertices; for (auto &vertex: transformedVertices) vertex.position = glm::vec3(localTransform * glm::vec4(vertex.position, 1.0f)); - renderer3D.drawMesh(transformedVertices, mesh.indices, mesh.material->albedoTexture, entityID); + + { + const auto meshMaterialAsset = mesh.material.lock(); + const auto albedoTextureAsset = meshMaterialAsset && meshMaterialAsset->isLoaded() ? meshMaterialAsset->getData()->albedoTexture.lock() : nullptr; + + const auto albedoTexture = albedoTextureAsset && albedoTextureAsset->isLoaded() ? albedoTextureAsset->getData()->texture : nullptr; + renderer3D.drawMesh(transformedVertices, mesh.indices, albedoTexture, entityID); + } } for (const auto &child: children) @@ -88,7 +120,7 @@ namespace nexo::components { explicit Model(const std::shared_ptr &rootNode) : root(rootNode) {}; // NOT WORKING ANYMORE - void draw(std::shared_ptr &context, const TransformComponent &transf, [[maybe_unused]] const Material &material, const int entityID) override + void draw(std::shared_ptr &context, const TransformComponent &transf, [[maybe_unused]] const Material &material, const int entityID) override { auto renderer3D = context->renderer3D; //TODO: Pass the material to the draw mesh function @@ -112,4 +144,40 @@ namespace nexo::components { } }; + struct BillBoard final : Shape3D { + void draw(std::shared_ptr &context, const TransformComponent &transf, const Material &material, const int entityID) override + { + const auto renderer3D = context->renderer3D; + + // lock all textures + auto albedoTextureAsset = material.albedoTexture.lock(); + auto normalMapAsset = material.normalMap.lock(); + auto metallicMapAsset = material.metallicMap.lock(); + auto roughnessMapAsset = material.roughnessMap.lock(); + auto emissiveMapAsset = material.emissiveMap.lock(); + + + renderer::NxMaterial inputMaterial = { + .albedoColor = material.albedoColor, + .specularColor = material.specularColor, + .emissiveColor = material.emissiveColor, + .roughness = material.roughness, + .metallic = material.metallic, + .opacity = material.opacity, + .albedoTexture = albedoTextureAsset && albedoTextureAsset->isLoaded() ? albedoTextureAsset->getData()->texture : nullptr, + .normalMap = normalMapAsset && normalMapAsset->isLoaded() ? normalMapAsset->getData()->texture : nullptr, + .metallicMap = metallicMapAsset && metallicMapAsset->isLoaded() ? metallicMapAsset->getData()->texture : nullptr, + .roughnessMap = roughnessMapAsset && roughnessMapAsset->isLoaded() ? roughnessMapAsset->getData()->texture : nullptr, + .emissiveMap = emissiveMapAsset && emissiveMapAsset->isLoaded() ? emissiveMapAsset->getData()->texture : nullptr, + .shader = material.shader + }; + + renderer3D.drawBillboard(transf.pos, transf.size, inputMaterial, entityID); + } + + [[nodiscard]] std::shared_ptr clone() const override + { + return std::make_shared(*this); + } + }; } diff --git a/engine/src/components/Transform.hpp b/engine/src/components/Transform.hpp index ff23c4008..fa7d5b060 100644 --- a/engine/src/components/Transform.hpp +++ b/engine/src/components/Transform.hpp @@ -16,12 +16,29 @@ #include #include - namespace nexo::components { struct TransformComponent final { + struct Memento { + glm::vec3 position; + glm::quat rotation; + glm::vec3 scale; + }; + + void restore(const Memento &memento) + { + pos = memento.position; + quat = memento.rotation; + size = memento.scale; + } + + [[nodiscard]] Memento save() const + { + return {pos, quat, size}; + } + glm::vec3 pos; - glm::vec3 size; + glm::vec3 size = glm::vec3(1.0f); glm::quat quat = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); }; diff --git a/engine/src/components/Uuid.hpp b/engine/src/components/Uuid.hpp index 916c7df5d..1dbdddc29 100644 --- a/engine/src/components/Uuid.hpp +++ b/engine/src/components/Uuid.hpp @@ -25,11 +25,11 @@ namespace nexo::components { std::uniform_int_distribution dist(0, 15); - const char *v = "0123456789abcdef"; constexpr bool dash[] = { false, false, false, false, true, false, true, false, true, false, true, false, false, false, false, false }; std::string res; for (const bool i : dash) { + const auto v = "0123456789abcdef"; if (i) res += "-"; res += v[dist(rng)]; res += v[dist(rng)]; @@ -37,7 +37,21 @@ namespace nexo::components { return res; } - struct UuidComponent { - std::string uuid = genUuid(); - }; + struct UuidComponent { + struct Memento { + std::string uuid; + }; + + void restore(const Memento &memento) + { + uuid = memento.uuid; + } + + [[nodiscard]] Memento save() const + { + return {uuid}; + } + + std::string uuid = genUuid(); + }; } diff --git a/engine/src/core/event/Input.cpp b/engine/src/core/event/Input.cpp index 0b6657c45..5be40d0dc 100644 --- a/engine/src/core/event/Input.cpp +++ b/engine/src/core/event/Input.cpp @@ -13,7 +13,7 @@ /////////////////////////////////////////////////////////////////////////////// #include "Input.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/InputOpenGl.hpp" #endif @@ -22,15 +22,15 @@ namespace nexo::event { std::shared_ptr Input::_instance = nullptr; - void Input::init(const std::shared_ptr& window) + void Input::init(const std::shared_ptr& window) { if (!_instance) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL _instance = std::make_shared(window); return; #endif - THROW_EXCEPTION(renderer::UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(renderer::NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/core/event/Input.hpp b/engine/src/core/event/Input.hpp index 635ca4c63..7bd9828f1 100644 --- a/engine/src/core/event/Input.hpp +++ b/engine/src/core/event/Input.hpp @@ -39,7 +39,7 @@ namespace nexo::event { * * @param window Shared pointer to the window for input polling. */ - explicit Input(const std::shared_ptr &window) : m_window(window) {}; + explicit Input(const std::shared_ptr &window) : m_window(window) {}; /** * @brief Checks if the specified key is currently pressed. @@ -100,10 +100,10 @@ namespace nexo::event { * * @param window Shared pointer to the window used for input. */ - static void init(const std::shared_ptr& window); + static void init(const std::shared_ptr& window); protected: - std::shared_ptr m_window; + std::shared_ptr m_window; private: static std::shared_ptr _instance; }; diff --git a/engine/src/core/event/KeyCodes.hpp b/engine/src/core/event/KeyCodes.hpp index d4f55206a..51a79e803 100644 --- a/engine/src/core/event/KeyCodes.hpp +++ b/engine/src/core/event/KeyCodes.hpp @@ -38,6 +38,8 @@ namespace nexo::event { #define NEXO_KEY_DOWN 264 #define NEXO_KEY_UP 265 + #define NEXO_KEY_SHIFT 340 + #define NEXO_MOUSE_LEFT 0 #define NEXO_MOUSE_RIGHT 1 } diff --git a/engine/src/core/event/SignalEvent.hpp b/engine/src/core/event/SignalEvent.hpp index 70f80b260..382302cb5 100644 --- a/engine/src/core/event/SignalEvent.hpp +++ b/engine/src/core/event/SignalEvent.hpp @@ -71,6 +71,8 @@ namespace nexo::event { static std::shared_ptr getInstance(); + void initSignals() const; + private: static void signalHandler(int signal); @@ -79,7 +81,7 @@ namespace nexo::event { template static void emitEventToAll(Args &&... args); - void initSignals() const; + std::vector > m_eventManagers; diff --git a/engine/src/core/event/opengl/InputOpenGl.hpp b/engine/src/core/event/opengl/InputOpenGl.hpp index 5e563bb57..655375342 100644 --- a/engine/src/core/event/opengl/InputOpenGl.hpp +++ b/engine/src/core/event/opengl/InputOpenGl.hpp @@ -19,7 +19,7 @@ namespace nexo::event { class InputOpenGl final : public Input { public: - explicit InputOpenGl(const std::shared_ptr& window) : Input(window) + explicit InputOpenGl(const std::shared_ptr& window) : Input(window) { LOG(NEXO_DEV, "Opengl input handler initialized"); }; diff --git a/engine/src/core/exceptions/Exceptions.hpp b/engine/src/core/exceptions/Exceptions.hpp index 7ebc9243a..3ab8870a0 100644 --- a/engine/src/core/exceptions/Exceptions.hpp +++ b/engine/src/core/exceptions/Exceptions.hpp @@ -15,8 +15,10 @@ #include #include +#include #include "Exception.hpp" +#include "components/Light.hpp" namespace nexo::core { class FileNotFoundException final : public Exception { @@ -39,4 +41,18 @@ namespace nexo::core { const std::source_location loc = std::source_location::current()) : Exception(message, loc) {} }; + + class TooManyPointLightsException : public Exception { + public: + explicit TooManyPointLightsException(unsigned int sceneRendered, size_t nbPointLights, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Too many point lights ({} > {}) in scene [{}]", nbPointLights, MAX_POINT_LIGHTS, sceneRendered), loc) {} + }; + + class TooManySpotLightsException : public Exception { + public: + explicit TooManySpotLightsException(unsigned int sceneRendered, size_t nbSpotLights, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Too many spot lights ({} > {}) in scene [{}]", nbSpotLights, MAX_SPOT_LIGHTS, sceneRendered), loc) {} + }; } diff --git a/engine/src/core/scene/Scene.hpp b/engine/src/core/scene/Scene.hpp index 4dffb8fbd..99d92bba9 100644 --- a/engine/src/core/scene/Scene.hpp +++ b/engine/src/core/scene/Scene.hpp @@ -91,6 +91,7 @@ namespace nexo::scene { void setName(std::string_view newName) { m_sceneName = newName; } [[nodiscard]] unsigned int getId() const {return m_id;}; [[nodiscard]] const std::string &getUuid() const {return m_uuid;} + [[nodiscard]] const std::set &getEntities() const {return m_entities;}; private: unsigned int m_id = nextSceneId++; std::string m_sceneName; diff --git a/engine/src/ecs/Access.hpp b/engine/src/ecs/Access.hpp new file mode 100644 index 000000000..7ed05c2a0 --- /dev/null +++ b/engine/src/ecs/Access.hpp @@ -0,0 +1,151 @@ +//// Access.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 06/04/2025 +// Description: Header file for access enforcement helpers +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include + +namespace nexo::ecs { + /** + * @brief Access type for components in systems + */ + enum class AccessType { + Read, ///< Read-only access + Write ///< Read-write access + }; + + /** + * @brief Template to specify component access type + * + * @tparam T The component type + * @tparam Access The access type (Read or Write) + */ + template + struct ComponentAccess { + using ComponentType = T; + static constexpr AccessType accessType = Access; + }; + + /** + * @brief Type alias for read-only component access + */ + template + using Read = ComponentAccess; + + /** + * @brief Type alias for read-write component access + */ + template + using Write = ComponentAccess; + + /** + * @brief Type alias for read-only singleton component access + */ + template + struct ReadSingleton { + using ComponentType = T; + static constexpr AccessType accessType = AccessType::Read; + }; + + /** + * @brief Type alias for read-write singleton component access + */ + template + struct WriteSingleton { + using ComponentType = T; + static constexpr AccessType accessType = AccessType::Write; + }; + + /** + * @brief Type wrapper for owned components in a group system + * + * @tparam Components Component access types (Read or Write) + */ + template + struct Owned { + using ComponentTypes = std::tuple; + }; + + /** + * @brief Type wrapper for non-owned components in a group system + * + * @tparam Components Component access types (Read or Write) + */ + template + struct NonOwned { + using ComponentTypes = std::tuple; + }; + + /** + * @brief Helper to extract component types from access types + * + * @tparam AccessTypes Pack of component access types + */ + template + struct ExtractComponentTypes; + + /** + * @brief Specialization for extracting component types + */ + template + struct ExtractComponentTypes> { + using Types = std::tuple; + }; + + /** + * @brief Helper to convert a tuple of component access types to a parameter pack + */ + template + void tuple_for_each_impl(Tuple&& tuple, Func&& func, std::index_sequence) + { + (func(std::get(std::forward(tuple))), ...); + } + + /** + * @brief Apply a function to each element of a tuple + */ + template + void tuple_for_each(Tuple&& tuple, Func&& func) + { + tuple_for_each_impl( + std::forward(tuple), + std::forward(func), + std::make_index_sequence>>{} + ); + } + + /** + * @brief Helper to check if a type is a ReadSingleton + */ + template + struct IsReadSingleton : std::false_type {}; + + template + struct IsReadSingleton> : std::true_type {}; + + /** + * @brief Helper to check if a type is a WriteSingleton + */ + template + struct IsWriteSingleton : std::false_type {}; + + template + struct IsWriteSingleton> : std::true_type {}; + + /** + * @brief Helper to check if a type is any kind of singleton component + */ + template + struct IsSingleton : std::bool_constant::value || IsWriteSingleton::value> {}; +} diff --git a/engine/src/ecs/ComponentArray.cpp b/engine/src/ecs/ComponentArray.cpp new file mode 100644 index 000000000..37c065cf3 --- /dev/null +++ b/engine/src/ecs/ComponentArray.cpp @@ -0,0 +1,278 @@ +//// ComponentArray.cpp /////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/06/2025 +// Description: Source file for the component array class +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ComponentArray.hpp" + +namespace nexo::ecs { + + TypeErasedComponentArray::TypeErasedComponentArray(const size_t componentSize, const size_t initialCapacity): m_componentSize(componentSize), m_capacity(initialCapacity) + { + if (componentSize == 0) { + throw std::invalid_argument("Component size cannot be zero"); + } + + m_sparse.resize(m_capacity, INVALID_ENTITY); + m_dense.reserve(m_capacity); + m_componentData.reserve(m_capacity * m_componentSize); + } + + void TypeErasedComponentArray::insert(Entity entity, const void* componentData) + { + if (entity >= MAX_ENTITIES) + THROW_EXCEPTION(OutOfRange, entity); + + ensureSparseCapacity(entity); + + if (hasComponent(entity)) { + LOG(NEXO_WARN, "Entity {} already has component", entity); + return; + } + + const size_t newIndex = m_size; + m_sparse[entity] = newIndex; + m_dense.push_back(entity); + + // Resize component data vector if needed + size_t requiredSize = (m_size + 1) * m_componentSize; + if (m_componentData.size() < requiredSize) { + m_componentData.resize(requiredSize); + } + + // Copy component data + std::memcpy(m_componentData.data() + newIndex * m_componentSize, + componentData, m_componentSize); + + ++m_size; + } + + void TypeErasedComponentArray::insertRaw(Entity entity, const void* componentData) + { + if (entity >= MAX_ENTITIES) + THROW_EXCEPTION(OutOfRange, entity); + + ensureSparseCapacity(entity); + + if (hasComponent(entity)) { + LOG(NEXO_WARN, "Entity {} already has component", entity); + return; + } + + const size_t newIndex = m_size; + m_sparse[entity] = newIndex; + m_dense.push_back(entity); + + // Resize component data vector if needed + size_t requiredSize = (m_size + 1) * m_componentSize; + if (m_componentData.size() < requiredSize) { + m_componentData.resize(requiredSize); + } + + // Copy component data + std::memcpy(m_componentData.data() + newIndex * m_componentSize, + componentData, m_componentSize); + + ++m_size; + } + + void TypeErasedComponentArray::remove(Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t indexToRemove = m_sparse[entity]; + + // Handle grouped components + if (indexToRemove < m_groupSize) { + size_t groupLastIndex = m_groupSize - 1; + if (indexToRemove != groupLastIndex) { + swapComponents(indexToRemove, groupLastIndex); + std::swap(m_dense[indexToRemove], m_dense[groupLastIndex]); + m_sparse[m_dense[indexToRemove]] = indexToRemove; + m_sparse[m_dense[groupLastIndex]] = groupLastIndex; + } + --m_groupSize; + indexToRemove = groupLastIndex; + } + + // Standard removal + const size_t lastIndex = m_size - 1; + if (indexToRemove != lastIndex) { + swapComponents(indexToRemove, lastIndex); + std::swap(m_dense[indexToRemove], m_dense[lastIndex]); + m_sparse[m_dense[indexToRemove]] = indexToRemove; + } + + m_sparse[entity] = INVALID_ENTITY; + m_dense.pop_back(); + --m_size; + + shrinkIfNeeded(); + } + + bool TypeErasedComponentArray::hasComponent(Entity entity) const + { + return (entity < m_sparse.size() && m_sparse[entity] != INVALID_ENTITY); + } + + void TypeErasedComponentArray::entityDestroyed(Entity entity) + { + if (hasComponent(entity)) + remove(entity); + } + + void TypeErasedComponentArray::duplicateComponent(Entity sourceEntity, Entity destEntity) + { + if (!hasComponent(sourceEntity)) + THROW_EXCEPTION(ComponentNotFound, sourceEntity); + + const void* sourceData = getRawComponent(sourceEntity); + insert(destEntity, sourceData); + } + + size_t TypeErasedComponentArray::getComponentSize() const + { + return m_componentSize; + } + + size_t TypeErasedComponentArray::size() const + { + return m_size; + } + + void* TypeErasedComponentArray::getRawComponent(Entity entity) + { + if (!hasComponent(entity)) + return nullptr; + return m_componentData.data() + m_sparse[entity] * m_componentSize; + } + + const void* TypeErasedComponentArray::getRawComponent(Entity entity) const + { + if (!hasComponent(entity)) + return nullptr; + return m_componentData.data() + m_sparse[entity] * m_componentSize; + } + + void* TypeErasedComponentArray::getRawData() + { + return m_componentData.data(); + } + + const void* TypeErasedComponentArray::getRawData() const + { + return m_componentData.data(); + } + + std::span TypeErasedComponentArray::entities() const + { + return {m_dense.data(), m_size}; + } + + Entity TypeErasedComponentArray::getEntityAtIndex(size_t index) const + { + if (index >= m_size) + THROW_EXCEPTION(OutOfRange, index); + return m_dense[index]; + } + + void TypeErasedComponentArray::addToGroup(Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t index = m_sparse[entity]; + if (index < m_groupSize) + return; + + if (index != m_groupSize) { + swapComponents(index, m_groupSize); + std::swap(m_dense[index], m_dense[m_groupSize]); + m_sparse[m_dense[index]] = index; + m_sparse[m_dense[m_groupSize]] = m_groupSize; + } + ++m_groupSize; + } + + void TypeErasedComponentArray::removeFromGroup(Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t index = m_sparse[entity]; + if (index >= m_groupSize) + return; + + --m_groupSize; + if (index != m_groupSize) { + swapComponents(index, m_groupSize); + std::swap(m_dense[index], m_dense[m_groupSize]); + m_sparse[m_dense[index]] = index; + m_sparse[m_dense[m_groupSize]] = m_groupSize; + } + } + + constexpr size_t TypeErasedComponentArray::groupSize() const + { + return m_groupSize; + } + + size_t TypeErasedComponentArray::memoryUsage() const + { + return m_componentData.capacity() + + sizeof(size_t) * m_sparse.capacity() + + sizeof(Entity) * m_dense.capacity(); + } + + void TypeErasedComponentArray::ensureSparseCapacity(Entity entity) + { + if (entity >= m_sparse.size()) { + size_t newSize = m_sparse.size(); + if (newSize == 0) + newSize = m_capacity; + while (entity >= newSize) + newSize *= 2; + m_sparse.resize(newSize, INVALID_ENTITY); + } + } + + void TypeErasedComponentArray::swapComponents(size_t index1, size_t index2) + { + if (index1 == index2) return; + + std::byte* data1 = m_componentData.data() + index1 * m_componentSize; + std::byte* data2 = m_componentData.data() + index2 * m_componentSize; + + // Use a temporary buffer for swapping + std::vector temp(m_componentSize); + std::memcpy(temp.data(), data1, m_componentSize); + std::memcpy(data1, data2, m_componentSize); + std::memcpy(temp.data(), data2, m_componentSize); + } + + void TypeErasedComponentArray::shrinkIfNeeded() + { + if (m_size < m_componentData.capacity() / 4 && m_componentData.capacity() > m_capacity * m_componentSize * 2) { + size_t newCapacity = std::max(m_size * 2, static_cast(m_capacity)) * m_componentSize; + if (newCapacity < m_capacity * m_componentSize) + newCapacity = m_capacity * m_componentSize; + + m_componentData.shrink_to_fit(); + m_dense.shrink_to_fit(); + + m_componentData.reserve(newCapacity); + m_dense.reserve(newCapacity / m_componentSize); + } + } + +} // namespace nexo::ecs \ No newline at end of file diff --git a/engine/src/ecs/ComponentArray.hpp b/engine/src/ecs/ComponentArray.hpp new file mode 100644 index 000000000..9dbde9bc1 --- /dev/null +++ b/engine/src/ecs/ComponentArray.hpp @@ -0,0 +1,723 @@ +//// ComponentArray.hpp /////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 01/04/2025 +// Description: Header file for the component array +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Definitions.hpp" +#include "ECSExceptions.hpp" +#include "Exception.hpp" +#include "Logger.hpp" + +#include +#include +#include +#include + +namespace nexo::ecs { + /** + * @class IComponentArray + * @brief Base interface for all component array types. + * + * Provides the common interface that all concrete component arrays must implement, + * allowing type-erased storage and manipulation of components. + * + * @note This class is not thread-safe. Access to a ComponentArray should be + * synchronized externally when used in multi-threaded contexts. + */ + class IComponentArray { + public: + virtual ~IComponentArray() = default; + + /** + * @brief Checks if an entity has a component in this array + * @param entity The entity to check + * @return true if the entity has a component, false otherwise + */ + [[nodiscard]] virtual bool hasComponent(Entity entity) const = 0; + + /** + * @brief Handles cleanup when an entity is destroyed + * @param entity The entity being destroyed + */ + virtual void entityDestroyed(Entity entity) = 0; + + virtual void duplicateComponent(Entity sourceEntity, Entity destEntity) = 0; + + /** + * @brief Gets the size of each component in bytes + * @return Size of individual component in bytes + */ + [[nodiscard]] virtual size_t getComponentSize() const = 0; + + /** + * @brief Gets the total number of components in the array + * @return The number of active components + */ + [[nodiscard]] virtual size_t size() const = 0; + + /** + * @brief Gets raw pointer to component data for an entity + * @param entity The entity to get the component from + * @return Raw pointer to component data, or nullptr if not found + */ + [[nodiscard]] virtual void* getRawComponent(Entity entity) = 0; + + /** + * @brief Gets const raw pointer to component data for an entity + * @param entity The entity to get the component from + * @return Const raw pointer to component data, or nullptr if not found + */ + [[nodiscard]] virtual const void* getRawComponent(Entity entity) const = 0; + + /** + * @brief Gets raw pointer to all component data + * @return Raw pointer to contiguous component data + */ + [[nodiscard]] virtual void* getRawData() = 0; + + /** + * @brief Gets const raw pointer to all component data + * @return Const raw pointer to contiguous component data + */ + [[nodiscard]] virtual const void* getRawData() const = 0; + + /** + * @brief Inserts a raw new component for the given entity. + * + * @param entity The entity to add the component to + * @param componentData Pointer to the raw component data + * @throws OutOfRange if entity ID exceeds MAX_ENTITIES + * + * @pre The entity must be a valid entity ID + * @pre componentData must point to valid memory of component's size + */ + virtual void insertRaw(Entity entity, const void *componentData) = 0; + + /** + * @brief Gets a span of all entities with this component + * @return Span of entity IDs + */ + [[nodiscard]] virtual std::span entities() const = 0; + }; + + /** + * @class ComponentArray + * @brief Stores and manages components of a specific type T. + * + * Implements a sparse-dense storage pattern for efficient component storage and retrieval. + * Components are stored contiguously for cache-friendly access, while maintaining + * O(1) lookups via entity IDs. + * + * @tparam T The component type stored in this array + * @tparam capacity Initial capacity for the sparse array + * + * @note This class is not thread-safe. Access should be synchronized externally when + * used in multi-threaded contexts. + */ + template + requires (capacity >= 1) + class alignas(64) ComponentArray final : public IComponentArray { + public: + /** + * @brief Type alias for the component type + */ + using component_type = T; + + /** + * @brief Constructs a new component array with initial capacity + * + * Initializes the sparse array with capacity elements, and reserves space + * for the dense arrays. + */ + ComponentArray() + { + m_sparse.resize(capacity, INVALID_ENTITY); + m_dense.reserve(capacity); + m_componentArray.reserve(capacity); + } + + [[nodiscard]] size_t getComponentSize() const override + { + return sizeof(T); + } + + [[nodiscard]] void* getRawComponent(Entity entity) override + { + if (!hasComponent(entity)) + return nullptr; + return &m_componentArray[m_sparse[entity]]; + } + + [[nodiscard]] const void* getRawComponent(Entity entity) const override + { + if (!hasComponent(entity)) + return nullptr; + return &m_componentArray[m_sparse[entity]]; + } + + [[nodiscard]] void* getRawData() override + { + return m_componentArray.data(); + } + + [[nodiscard]] const void* getRawData() const override + { + return m_componentArray.data(); + } + + /** + * @brief Inserts a new component for the given entity. + * + * @param entity The entity to add the component to + * @param component The component instance to add + * @throws OutOfRange if entity ID exceeds MAX_ENTITIES + * + * @pre The entity must be a valid entity ID + */ + void insert(Entity entity, T component) + { + if (entity >= MAX_ENTITIES) + THROW_EXCEPTION(OutOfRange, entity); + + // Ensure m_sparse can hold this entity index. + ensureSparseCapacity(entity); + + if (hasComponent(entity)) { + LOG(NEXO_WARN, "Entity {} already has component: {}", entity, typeid(T).name()); + return; + } + + const size_t newIndex = m_size; + m_sparse[entity] = newIndex; + m_dense.push_back(entity); + m_componentArray.push_back(component); + + ++m_size; + } + + /** + * @brief Inserts a raw new component for the given entity. + * + * @param entity The entity to add the component to + * @param componentData Pointer to the raw component data + * @throws OutOfRange if entity ID exceeds MAX_ENTITIES + * + * @pre The entity must be a valid entity ID + * @pre componentData must point to valid memory of component's size + */ + void insertRaw(Entity entity, const void *componentData) override + { + if (entity >= MAX_ENTITIES) + THROW_EXCEPTION(OutOfRange, entity); + + ensureSparseCapacity(entity); + + if (hasComponent(entity)) { + LOG(NEXO_WARN, "Entity {} already has component: {}", entity, typeid(T).name()); + return; + } + + const size_t newIndex = m_size; + m_sparse[entity] = newIndex; + m_dense.push_back(entity); + // allocate new component in the array + m_componentArray.emplace_back(); + // copy the raw data into the new component + std::memcpy(&m_componentArray[newIndex], componentData, sizeof(T)); + + ++m_size; + } + + /** + * @brief Removes the component for the given entity. + * + * If the entity is grouped (i.e. within the first m_groupSize entries), + * then adjusts the group boundary appropriately. + * + * @param entity The entity to remove the component from + * @throws ComponentNotFoundException if the entity doesn't have the component + * + * @pre The entity must have the component + */ + void remove(const Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t indexToRemove = m_sparse[entity]; + + // If the entity is part of the group, remove it from the group first. + if (indexToRemove < m_groupSize) { + // Swap with the last grouped element if not already at the end. + size_t groupLastIndex = m_groupSize - 1; + if (indexToRemove != groupLastIndex) { + std::swap(m_componentArray[indexToRemove], m_componentArray[groupLastIndex]); + std::swap(m_dense[indexToRemove], m_dense[groupLastIndex]); + m_sparse[m_dense[indexToRemove]] = indexToRemove; + m_sparse[m_dense[groupLastIndex]] = groupLastIndex; + } + --m_groupSize; + indexToRemove = groupLastIndex; + } + + // Standard removal from the overall array: + const size_t lastIndex = m_size - 1; + if (indexToRemove != lastIndex) { + std::swap(m_componentArray[indexToRemove], m_componentArray[lastIndex]); + std::swap(m_dense[indexToRemove], m_dense[lastIndex]); + m_sparse[m_dense[indexToRemove]] = indexToRemove; + } + m_sparse[entity] = INVALID_ENTITY; + m_componentArray.pop_back(); + m_dense.pop_back(); + --m_size; + + shrinkIfNeeded(); + } + + /** + * @brief Retrieves a component for the given entity + * + * @param entity The entity to get the component from + * @return Reference to the component + * @throws ComponentNotFoundException if the entity doesn't have the component + * + * @pre The entity must have the component + */ + [[nodiscard]] T& get(const Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + return m_componentArray[m_sparse[entity]]; + } + + /** + * @brief Retrieves a component for the given entity (const version) + * + * @param entity The entity to get the component from + * @return Const reference to the component + * @throws ComponentNotFoundException if the entity doesn't have the component + * + * @pre The entity must have the component + */ + [[nodiscard]] const T& get(const Entity entity) const + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + return m_componentArray[m_sparse[entity]]; + } + + void duplicateComponent(Entity sourceEntity, Entity destEntity) override + { + if (!hasComponent(sourceEntity)) + THROW_EXCEPTION(ComponentNotFound, sourceEntity); + + insert(destEntity, get(sourceEntity)); + } + + /** + * @brief Checks if an entity has a component in this array + * + * @param entity The entity to check + * @return true if the entity has a component, false otherwise + */ + [[nodiscard]] bool hasComponent(const Entity entity) const override + { + return (entity < m_sparse.size() && m_sparse[entity] != INVALID_ENTITY); + } + + /** + * @brief Removes the component from an entity when it's destroyed + * + * @param entity The entity being destroyed + */ + void entityDestroyed(const Entity entity) override + { + if (hasComponent(entity)) + remove(entity); + } + + /** + * @brief Gets the total number of components in the array + * + * @return The number of active components + */ + [[nodiscard]] constexpr size_t size() const override + { + return m_size; + } + + /** + * @brief Gets the entity at the given index in the dense array + * + * @param index The index to look up + * @return The entity at that index + * @throws OutOfRange if the index is invalid + * + * @pre The index must be less than the array size + */ + [[nodiscard]] Entity getEntityAtIndex(const size_t index) const + { + if (index >= m_size) + THROW_EXCEPTION(OutOfRange, index); + return m_dense[index]; + } + + /** + * @brief Gets a span view of all components + * + * @return Span of component data + */ + [[nodiscard]] std::span getAllComponents() + { + return std::span(m_componentArray.data(), m_size); + } + + /** + * @brief Gets a const span view of all components + * + * @return Const span of component data + */ + [[nodiscard]] std::span getAllComponents() const + { + return std::span(m_componentArray.data(), m_size); + } + + /** + * @brief Gets a const span view of all entities with this component + * + * @return Const span of entity IDs + */ + [[nodiscard]] std::span entities() const override + { + return {m_dense.data(), m_size}; + } + + /** + * @brief Moves the component for the given entity into the group region. + * + * This operation swaps the entity with the one at m_groupSize + * and then increments the group pointer. + * + * @param entity The entity to add to the group + * @throws ComponentNotFoundException if the entity doesn't have the component + * + * @pre The entity must have the component + */ + void addToGroup(const Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t index = m_sparse[entity]; + if (index < m_groupSize) + return; + // Swap with the element at the group boundary. + if (index != m_groupSize) { + std::swap(m_componentArray[index], m_componentArray[m_groupSize]); + std::swap(m_dense[index], m_dense[m_groupSize]); + m_sparse[m_dense[index]] = index; + m_sparse[m_dense[m_groupSize]] = m_groupSize; + } + ++m_groupSize; + } + + /** + * @brief Moves the component for the given entity out of the group region. + * + * This operation swaps the entity with the last grouped element + * and then decrements the group pointer. + * + * @param entity The entity to remove from the group + * @throws ComponentNotFoundException if the entity doesn't have the component + * + * @pre The entity must have the component + */ + void removeFromGroup(const Entity entity) + { + if (!hasComponent(entity)) + THROW_EXCEPTION(ComponentNotFound, entity); + + size_t index = m_sparse[entity]; + if (index >= m_groupSize) + return; + --m_groupSize; + if (index != m_groupSize) { + std::swap(m_componentArray[index], m_componentArray[m_groupSize]); + std::swap(m_dense[index], m_dense[m_groupSize]); + m_sparse[m_dense[index]] = index; + m_sparse[m_dense[m_groupSize]] = m_groupSize; + } + } + + /** + * @brief Forces a component to be set at a specific index (internal use only) + * + * Used primarily during group reordering operations. + * + * @param index The index to set the component at + * @param entity The entity to associate with this component + * @param component The component data to set + * @throws OutOfRange if the index is invalid + */ + void forceSetComponentAt(size_t index, const Entity entity, T component) + { + if (index >= m_size) + THROW_EXCEPTION(OutOfRange, index); + + m_sparse[entity] = index; + m_dense[index] = entity; + m_componentArray[index] = std::move(component); + } + + /** + * @brief Batch insertion of multiple components + * + * @tparam EntityIt Iterator type for entities + * @tparam CompIt Iterator type for components + * @param entitiesBegin Start iterator for entities + * @param entitiesEnd End iterator for entities + * @param componentsBegin Start iterator for components + * + * @pre The range [entitiesBegin, entitiesEnd) must be valid + * @pre componentsBegin must point to a valid range of at least (entitiesEnd - entitiesBegin) elements + */ + template + void insertBatch(EntityIt entitiesBegin, EntityIt entitiesEnd, CompIt componentsBegin) + { + CompIt compIt = componentsBegin; + for (EntityIt entityIt = entitiesBegin; entityIt != entitiesEnd; ++entityIt, ++compIt) { + insert(*entityIt, *compIt); + } + } + + /** + * @brief Apply a function to each entity-component pair + * + * @tparam Func Type of function to apply + * @param func Function taking (Entity, T&) to apply to each pair + */ + template + void forEach(Func&& func) + { + for (size_t i = 0; i < m_size; ++i) { + func(m_dense[i], m_componentArray[i]); + } + } + + /** + * @brief Apply a function to each entity-component pair (const version) + * + * @tparam Func Type of function to apply + * @param func Function taking (Entity, const T&) to apply to each pair + */ + template + void forEach(Func&& func) const + { + for (size_t i = 0; i < m_size; ++i) { + func(m_dense[i], m_componentArray[i]); + } + } + + /** + * @brief Gets the number of entities in the group region + * + * @return Number of grouped entities + */ + [[nodiscard]] constexpr size_t groupSize() const + { + return m_groupSize; + } + + /** + * @brief Get the estimated memory usage of this component array + * + * @return Size in bytes of memory used by this component array + */ + [[nodiscard]] size_t memoryUsage() const + { + return sizeof(T) * m_componentArray.capacity() + + sizeof(size_t) * m_sparse.capacity() + + sizeof(Entity) * m_dense.capacity(); + } + + private: + // Dense storage for components. + std::vector m_componentArray; + // Sparse mapping: maps entity ID to index in the dense arrays. + std::vector m_sparse; + // Dense storage for entity IDs. + std::vector m_dense; + // Current number of active components. + size_t m_size = 0; + // The first m_groupSize entries in m_dense/m_componentArray are considered "grouped". + size_t m_groupSize = 0; + + /** + * @brief Ensures m_sparse is large enough to index 'entity' + * + * @param entity The entity to ensure capacity for + */ + void ensureSparseCapacity(const Entity entity) + { + if (entity >= m_sparse.size()) { + size_t newSize = m_sparse.size(); + if (newSize == 0) + newSize = capacity; + while (entity >= newSize) + newSize *= 2; + m_sparse.resize(newSize, INVALID_ENTITY); + } + } + + /** + * @brief Shrinks vectors if they're significantly larger than needed + * + * Reduces memory usage by shrinking the dense vectors when size + * is less than half of their capacity. + */ + void shrinkIfNeeded() + { + if (m_size < m_componentArray.capacity() / 4 && m_componentArray.capacity() > capacity * 2) { + // Only shrink if vectors are significantly oversized to avoid frequent reallocations + size_t newCapacity = std::max(m_size * 2, static_cast(capacity)); + if (newCapacity < capacity) + newCapacity = capacity; + + m_componentArray.shrink_to_fit(); + m_dense.shrink_to_fit(); + + // Reserve the optimized capacity to ensure future growth is efficient + m_componentArray.reserve(newCapacity); + m_dense.reserve(newCapacity); + } + } + }; + + /** + * @class TypeErasedComponentArray + * @brief A type-erased component array that can store components of any size. + * + * This class allows you to create component arrays at runtime without knowing + * the component type at compile time. You only need to specify the size of + * each component. + */ + class alignas(64) TypeErasedComponentArray final : public IComponentArray { + public: + /** + * @brief Constructs a new type-erased component array + * @param componentSize Size of each component in bytes + * @param initialCapacity Initial capacity for the array + */ + explicit TypeErasedComponentArray(size_t componentSize, size_t initialCapacity = 1024); + + /** + * @brief Inserts a new component for the given entity + * @param entity The entity to add the component to + * @param componentData Raw pointer to the component data to copy + */ + void insert(Entity entity, const void* componentData); + + /** + * @brief Inserts a raw new component for the given entity. + * + * @param entity The entity to add the component to + * @param componentData Pointer to the raw component data + * @throws OutOfRange if entity ID exceeds MAX_ENTITIES + * + * @pre The entity must be a valid entity ID + * @pre componentData must point to valid memory of component's size + */ + void insertRaw(Entity entity, const void* componentData) override; + + /** + * @brief Removes the component for the given entity + * @param entity The entity to remove the component from + */ + void remove(Entity entity); + + [[nodiscard]] bool hasComponent(Entity entity) const override; + + void entityDestroyed(Entity entity) override; + + void duplicateComponent(Entity sourceEntity, Entity destEntity) override; + + [[nodiscard]] size_t getComponentSize() const override; + + [[nodiscard]] size_t size() const override; + + [[nodiscard]] void* getRawComponent(Entity entity) override; + + [[nodiscard]] const void* getRawComponent(Entity entity) const override; + + [[nodiscard]] void* getRawData() override; + + [[nodiscard]] const void* getRawData() const override; + + [[nodiscard]] std::span entities() const override; + + /** + * @brief Gets the entity at the given index in the dense array + * @param index The index to look up + * @return The entity at that index + */ + [[nodiscard]] Entity getEntityAtIndex(size_t index) const; + + /** + * @brief Adds an entity to the group region + * @param entity The entity to add to the group + */ + void addToGroup(Entity entity); + + /** + * @brief Removes an entity from the group region + * @param entity The entity to remove from the group + */ + void removeFromGroup(Entity entity); + + /** + * @brief Gets the number of entities in the group region + * @return Number of grouped entities + */ + [[nodiscard]] constexpr size_t groupSize() const; + + /** + * @brief Get the estimated memory usage of this component array + * @return Size in bytes of memory used by this component array + */ + [[nodiscard]] size_t memoryUsage() const; + + private: + // Component data storage + std::vector m_componentData; + // Sparse mapping: maps entity ID to index in the dense arrays + std::vector m_sparse; + // Dense storage for entity IDs + std::vector m_dense; + // Size of each component in bytes + size_t m_componentSize; + // Initial capacity + size_t m_capacity; + // Current number of active components + size_t m_size = 0; + // Group size for component grouping + size_t m_groupSize = 0; + + void ensureSparseCapacity(Entity entity); + + void swapComponents(size_t index1, size_t index2); + + void shrinkIfNeeded(); + }; + +} diff --git a/engine/src/ecs/Components.cpp b/engine/src/ecs/Components.cpp index 738a7334e..84e5072e4 100644 --- a/engine/src/ecs/Components.cpp +++ b/engine/src/ecs/Components.cpp @@ -8,7 +8,7 @@ // // Author: Mehdy MORVAN // Date: 10/11/2024 -// Description: Source file for the components array and component manager classes +// Description: Source file for the component manager class // /////////////////////////////////////////////////////////////////////////////// @@ -16,13 +16,16 @@ namespace nexo::ecs { - void ComponentManager::entityDestroyed(const Entity entity) const + void ComponentManager::entityDestroyed(const Entity entity, const Signature &entitySignature) { - for (const auto &[fst, snd]: m_componentArrays) - { - auto const &component = snd; - component->entityDestroyed(entity); + for (const auto &group: m_groupRegistry | std::views::values) { + if ((entitySignature & group->allSignature()) == group->allSignature()) + group->removeFromGroup(entity); + } + for (const auto& componentArray : m_componentArrays) { + if (componentArray) + componentArray->entityDestroyed(entity); } } -} \ No newline at end of file +} diff --git a/engine/src/ecs/Components.hpp b/engine/src/ecs/Components.hpp index f182cff5c..9edb48770 100644 --- a/engine/src/ecs/Components.hpp +++ b/engine/src/ecs/Components.hpp @@ -7,311 +7,701 @@ // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // // Author: Mehdy MORVAN -// Date: 08/11/2024 -// Description: Header file for the components and component manager class for the ecs +// Date: 10/11/2024 +// Description: Header file for the component manager class // /////////////////////////////////////////////////////////////////////////////// - #pragma once -#include #include #include -#include -#include +#include #include +#include #include +#include +#include +#include +#include +#include +#include +#include -#include "Logger.hpp" #include "ECSExceptions.hpp" +#include "Definitions.hpp" +#include "Exception.hpp" +#include "Logger.hpp" +#include "ComponentArray.hpp" +#include "Group.hpp" -namespace nexo::ecs -{ - using Entity = std::uint32_t; - - // Maximum entity count, given that 13 bits are used for ID - constexpr Entity MAX_ENTITIES = 8191; - using ComponentType = std::uint8_t; - - constexpr ComponentType MAX_COMPONENT_TYPE = 32; - +namespace nexo::ecs { /** - * @class IComponentArray - * - * @brief Interface for a component array in the ECS framework. - * - * This class defines the interface for component arrays, which are used to store - * components of entities in the ECS system. It provides the foundation for managing - * the lifecycle of components associated with entities. - */ - class IComponentArray - { - public: - virtual ~IComponentArray() = default; - - virtual void entityDestroyed(Entity entity) = 0; - }; + * @brief Helper template to tag non-owned component types + * + * This template is used for tag dispatching to distinguish between + * owned and non-owned components in group registration. + * + * @tparam NonOwning The non-owned component types + */ + template + struct get_t { }; /** - * @class ComponentArray - * - * @brief Templated class that manages a specific type of component for all entities. - * - * This class manages the storage, retrieval, and deletion of components of a specific type. - * It ensures efficient access and modification of components associated with entities. - * - * @tparam T - The type of the component this array will manage. - */ - template - class ComponentArray final : public IComponentArray - { - public: - /** - * @brief Inserts a component for a specific entity. - * - * @param entity - The entity to which the component will be added. - * @param component - The component to add. - */ - void insertData(const Entity entity, T component) - { - if (m_size == MAX_ENTITIES) - THROW_EXCEPTION(OutOfRange, MAX_ENTITIES); - if (m_entityToIndexMap.contains(entity)) - { - LOG(NEXO_WARN, "ECS::ComponentArray::insertData: Component already added to entity {}", entity); - return; - } - - size_t newIndex = m_size; - m_entityToIndexMap[entity] = newIndex; - m_indexToEntityMap[newIndex] = entity; - m_componentArray[newIndex] = component; - ++m_size; - } - - /** - * @brief Removes a component from a specific entity. - * - * @param entity - The entity from which the component will be removed. - */ - void removeData(const Entity entity) - { - if (!m_entityToIndexMap.contains(entity)) - THROW_EXCEPTION(ComponentNotFound, entity); - - size_t indexOfRemovedEntity = m_entityToIndexMap[entity]; - size_t indexOfLastElement = m_size - 1; - m_componentArray[indexOfRemovedEntity] = m_componentArray[indexOfLastElement]; - - const Entity entityOfLastElement = m_indexToEntityMap[indexOfLastElement]; - m_entityToIndexMap[entityOfLastElement] = indexOfRemovedEntity; - m_indexToEntityMap[indexOfRemovedEntity] = entityOfLastElement; - - m_entityToIndexMap.erase(entity); - m_indexToEntityMap.erase(indexOfLastElement); - - --m_size; - } - - /** - * @brief Retrieves a reference to a component associated with a specific entity. - * - * @param entity - The entity whose component is to be retrieved. - * @return T& - A reference to the requested component. - */ - T& getData(const Entity entity) - { - if (!m_entityToIndexMap.contains(entity)) - THROW_EXCEPTION(ComponentNotFound, entity); - - return m_componentArray[m_entityToIndexMap[entity]]; - } - - /** - * @brief Cleans up components associated with a destroyed entity. - * - * @param entity - The destroyed entity. - */ - void entityDestroyed(const Entity entity) override - { - if (m_entityToIndexMap.contains(entity)) - removeData(entity); - } - - bool hasComponent(const Entity entity) const - { - return m_entityToIndexMap.contains(entity); - } - - private: - std::array m_componentArray; - - std::unordered_map m_entityToIndexMap; - - std::unordered_map m_indexToEntityMap; - - size_t m_size = 0; - }; + * @brief Creates a type tag for specifying non-owned components + * + * This helper function is used to create a tag type that identifies + * which components should be accessible but not owned by a group. + * + * @tparam NonOwning The non-owned component types + * @return A tag object of get_t + */ + template + get_t get() + { + return {}; + } /** - * @class ComponentManager - * - * @brief Manages the registration and handling of components in an ECS architecture. - * - * The ComponentManager is responsible for managing different types of components in the ECS framework. - * It allows the registration of component types, adding and removing components to entities, and - * accessing components of entities. - */ - class ComponentManager - { - public: - /** - * @brief Registers a new component type in the system. - * - * Each component type is associated with a unique ComponentType ID and a ComponentArray - * to manage instances of the component. - */ - template - void registerComponent() - { - std::type_index typeName(typeid(T)); - if (m_componentTypes.contains(typeName)) - { - LOG(NEXO_WARN, "ECS::ComponentManager::registerComponent: Component already registered"); - return; - } + * @brief Type alias for a tuple of owned component arrays + * + * Used internally by the Group class to store owned components. + * + * @tparam Owned The owned component types + */ + template + using OwnedComponents = std::tuple>...>; - m_componentTypes.insert({typeName, _nextComponentType}); + /** + * @brief Type alias for a tuple of non-owned component arrays + * + * Used internally by the Group class to store references to non-owned components. + * + * @tparam NonOwned The non-owned component types + */ + template + using NonOwnedComponents = std::tuple>...>; - m_componentArrays.insert({typeName, std::make_shared>()}); + /** + * @brief Type alias for a shared pointer to a Group + * + * Simplifies the declaration of Group instances by hiding the complex template types. + * + * @tparam OwnedGroup Tuple type for owned component arrays + * @tparam NonOwnedGroup Tuple type for non-owned component arrays + */ + template + using GroupAlias = std::shared_ptr>; - ++_nextComponentType; - } + /** + * @brief Structure to represent a group key using numeric types + * + * Provides a more efficient way to identify groups compared to strings. + * Uses a combination of two separate signatures to represent owned and non-owned components. + */ + struct GroupKey { + Signature ownedSignature; ///< Bits set for components owned by the group + Signature nonOwnedSignature; ///< Bits set for components used but not owned by the group /** - * @brief Retrieves the ComponentType ID for a specific component type. - * - * @return ComponentType - The unique ID associated with the component type. - */ - template - ComponentType getComponentType() - { - const std::type_index typeName(typeid(T)); - if (!m_componentTypes.contains(typeName)) - THROW_EXCEPTION(ComponentNotRegistered); - - return m_componentTypes[typeName]; - } + * @brief Equality comparison operator + */ + bool operator==(const GroupKey& other) const { + return ownedSignature == other.ownedSignature && + nonOwnedSignature == other.nonOwnedSignature; + } /** - * @brief Adds a component of a specific type to an entity. - * - * @param entity - The entity to which the component will be added. - * @param component - The component to add to the entity. - */ - template - void addComponent(Entity entity, T component) - { - getComponentArray()->insertData(entity, component); - } + * @brief Returns a string representation of the component types in this key + * Used for error messages and debugging + * + * @return String describing the components + */ + [[nodiscard]] std::string toString() const { + std::stringstream ss; + ss << "Owned: {"; + bool first = true; + + // Add owned component IDs + for (ComponentType i = 0; i < MAX_COMPONENT_TYPE; ++i) { + if (ownedSignature.test(i)) { + if (!first) + ss << ", "; + ss << "Component#" << i; + first = false; + } + } + + ss << "}, Non-owned: {"; + first = true; + + // Add non-owned component IDs + for (ComponentType i = 0; i < MAX_COMPONENT_TYPE; ++i) { + if (nonOwnedSignature.test(i)) { + if (!first) + ss << ", "; + ss << "Component#" << i; + first = false; + } + } + + ss << "}"; + return ss.str(); + } + }; +} - /** - * @brief Removes a component of a specific type from an entity. - * - * @param entity - The entity from which the component will be removed. - */ - template - void removeComponent(Entity entity) - { - getComponentArray()->removeData(entity); - } - - template - bool tryRemoveComponent(Entity entity) - { - if (!getComponentArray()->hasComponent(entity)) - return false; - getComponentArray()->removeData(entity); - return true; - } +// Add hash function for GroupKey to use it in an unordered_map +namespace std { + /** + * @brief Hash function for GroupKey + * + * Allows GroupKey to be used as a key in unordered_map + */ + template<> + struct hash { + size_t operator()(const nexo::ecs::GroupKey& key) const noexcept + { + const size_t h1 = std::hash()(key.ownedSignature); + const size_t h2 = std::hash()(key.nonOwnedSignature); + return h1 ^ (h2 << 1); + } + }; +} - /** - * @brief Retrieves a reference to a component of a specific type from an entity. - * - * @param entity - The entity whose component is to be retrieved. - * @return T& - A reference to the requested component. - */ - template - T& getComponent(Entity entity) - { - return getComponentArray()->getData(entity); - } - - template - std::optional> tryGetComponent(Entity entity) - { - if (!getComponentArray()->hasComponent(entity)) - return std::nullopt; - return getComponent(entity); - } +namespace nexo::ecs { - /** - * @brief Retrieves a reference to all components of a specific type from an entity. - * - * @param entity - The entity whose component is to be retrieved. - */ - std::vector> getAllComponents(Entity entity) - { - std::vector> components; - - for (const auto& [type, array] : m_componentArrays) + /** + * @class ComponentManager + * + * @brief Central manager for all component types and their arrays + * + * The ComponentManager is responsible for: + * - Registering component types in the ECS + * - Creating and maintaining component arrays + * - Adding/removing components from entities + * - Managing component group registrations + * - Handling entity destruction with respect to components + */ + class ComponentManager { + public: + ComponentManager() = default; + + /** + * @brief Copy constructor (deleted) + * + * ComponentManager is not copyable to prevent duplication of component data. + */ + ComponentManager(const ComponentManager&) = delete; + + /** + * @brief Copy assignment operator (deleted) + * + * ComponentManager is not copyable to prevent duplication of component data. + */ + ComponentManager& operator=(const ComponentManager&) = delete; + + /** + * @brief Move constructor + * + * Allows transferring ownership of component arrays to a new manager. + */ + ComponentManager(ComponentManager&&) noexcept = default; + + /** + * @brief Move assignment operator + * + * Allows transferring ownership of component arrays to a new manager. + */ + ComponentManager& operator=(ComponentManager&&) noexcept = default; + + /** + * @brief Registers a component type in the ECS + * + * Creates a new component array for the specified component type. + * If the component type is already registered, a warning is logged. + * + * @tparam T The component type to register + */ + template + void registerComponent() + { + const ComponentType typeID = getComponentTypeID(); + + assert(typeID < m_componentArrays.size() && "Component type ID exceeds component array size"); + if (m_componentArrays[typeID] != nullptr) { + LOG(NEXO_WARN, "Component already registered"); + return; + } + + m_componentArrays[typeID] = std::make_shared>(); + } + + ComponentType registerComponent(const size_t componentSize, const size_t initialCapacity = 1024) + { + const ComponentType typeID = generateComponentTypeID(); + assert(typeID < m_componentArrays.size() && "Component type ID exceeds component array size"); + + assert(m_componentArrays[typeID] == nullptr && "TypeErasedComponent already registered, should really not happen"); + m_componentArrays[typeID] = std::make_shared(componentSize, initialCapacity); + return typeID; + } + + /** + * @brief Gets the unique identifier for a component type + * + * @tparam T The component type + * @return The component type ID + * @throws ComponentNotRegistered if the component type is not registered + */ + template + [[nodiscard]] ComponentType getComponentType() const + { + const ComponentType typeID = getComponentTypeID(); + + if (m_componentArrays[typeID] == nullptr) + THROW_EXCEPTION(ComponentNotRegistered); + + return typeID; + } + + /** + * @brief Adds a component to an entity + * + * Adds the component to the appropriate component array and + * updates any groups that match the entity's new signature. + * + * @tparam T The component type + * @param entity The entity to add the component to + * @param component The component instance to add + * @param oldSignature The entity's current component signature + * @param newSignature The entity's new component signature + */ + template + void addComponent(Entity entity, T component, const Signature oldSignature, const Signature newSignature) + { + getComponentArray()->insert(entity, std::move(component)); + + for (const auto& group : std::ranges::views::values(m_groupRegistry)) { + // Check if entity qualifies now but did not qualify before. + if (((oldSignature & group->allSignature()) != group->allSignature()) && + ((newSignature & group->allSignature()) == group->allSignature())) { + group->addToGroup(entity); + } + } + } + + /** + * @brief Adds a component to an entity using type ID and raw data + * + * Adds the component using the component type ID and raw data pointer, + * useful for runtime component type handling. Updates any groups that + * match the entity's new signature. + * + * @param entity The entity to add the component to + * @param componentType The type ID of the component to add + * @param componentData Pointer to the raw component data + * @param oldSignature The entity's signature before adding the component + * @param newSignature The entity's signature after adding the component + * + * @pre componentType must be a valid registered component type + * @pre componentData must point to valid memory of the component's size + */ + void addComponent(const Entity entity, const ComponentType componentType, const void *componentData, const Signature oldSignature, const Signature newSignature) + { + getComponentArray(componentType)->insertRaw(entity, componentData); + + for (const auto& group : std::ranges::views::values(m_groupRegistry)) { + // Check if entity qualifies now but did not qualify before. + if (((oldSignature & group->allSignature()) != group->allSignature()) && + ((newSignature & group->allSignature()) == group->allSignature())) { + group->addToGroup(entity); + } + } + } + + /** + * @brief Removes a component from an entity + * + * Removes the entity from any groups that required the component + * and then removes the component from its array. + * + * @tparam T The component type + * @param entity The entity to remove the component from + * @param previousSignature The entity's signature before removal + * @param newSignature The entity's signature after removal + */ + template + void removeComponent(Entity entity, const Signature previousSignature, const Signature newSignature) { - auto componentArray = std::dynamic_pointer_cast>>(array); - if (componentArray && componentArray->hasComponent(entity)) + for (const auto& group : std::ranges::views::values(m_groupRegistry)) { - components.push_back(componentArray->getData(entity)); + // If the entity no longer qualifies but did before, remove it. + if (((previousSignature & group->allSignature()) == group->allSignature()) && + ((newSignature & group->allSignature()) != group->allSignature())) { + group->removeFromGroup(entity); + } } + getComponentArray()->remove(entity); } - return components; - } - std::optional>> tryGetAllComponents(const Entity entity) - { - std::vector> components = getAllComponents(entity); - if (components.empty()) + /** + * @brief Attempts to remove a component from an entity + * + * Checks if the entity has the component first, then removes it + * if it exists. Updates groups as needed. + * + * @tparam T The component type + * @param entity The entity to remove the component from + * @param previousSignature The entity's signature before the attempted removal + * @param newSignature The entity's signature after the attempted removal + * @return true if the component was removed, false if it didn't exist + */ + template + bool tryRemoveComponent(Entity entity, const Signature previousSignature, const Signature newSignature) + { + auto componentArray = getComponentArray(); + if (!componentArray->hasComponent(entity)) + return false; + + for (const auto& group : std::ranges::views::values(m_groupRegistry)) + { + // If the entity no longer qualifies but did before, remove it. + if (((previousSignature & group->allSignature()) == group->allSignature()) && + ((newSignature & group->allSignature()) != group->allSignature())) { + group->removeFromGroup(entity); + } + } + componentArray->remove(entity); + return true; + } + + /** + * @brief Gets a component from an entity + * + * @tparam T The component type + * @param entity The entity to get the component from + * @return Reference to the component + * @throws ComponentNotFound if the entity doesn't have the component + */ + template + [[nodiscard]] T& getComponent(Entity entity) + { + return getComponentArray()->get(entity); + } + + template + void duplicateComponent( + Entity sourceEntity, + Entity destEntity, + const Signature oldSignature, + const Signature newSignature + ) { + const auto &componentArray = getComponentArray(); + const auto sourceComponent = componentArray->get(sourceEntity); + addComponent(destEntity, sourceComponent, oldSignature, newSignature); + } + + void duplicateComponent( + ComponentType componentType, + Entity sourceEntity, + Entity destEntity, + const Signature oldSignature, + const Signature newSignature + ) { + const auto& componentArray = m_componentArrays[componentType]; + componentArray->duplicateComponent(sourceEntity, destEntity); + + for (const auto& group : std::ranges::views::values(m_groupRegistry)) { + // Check if entity qualifies now but did not qualify before. + if (((oldSignature & group->allSignature()) != group->allSignature()) && + ((newSignature & group->allSignature()) == group->allSignature())) { + group->addToGroup(destEntity); + } + } + } + + /** + * @brief Gets the component array for a specific component type with ComponentType (const version) + * + * @param typeID The component type ID + * @return Const shared pointer to the component array + * @throws ComponentNotRegistered if the component type is not registered + */ + [[nodiscard]] std::shared_ptr getComponentArray(const ComponentType typeID) const + { + const auto& componentArray = m_componentArrays[typeID]; + if (componentArray == nullptr) + THROW_EXCEPTION(ComponentNotRegistered); + + return componentArray; + } + + /** + * @brief Gets the component array for a specific component type + * + * @tparam T The component type + * @return Shared pointer to the component array + * @throws ComponentNotRegistered if the component type is not registered + */ + template + [[nodiscard]] std::shared_ptr> getComponentArray() + { + const ComponentType typeID = getComponentTypeID(); + return std::static_pointer_cast>(getComponentArray(typeID)); + } + + /** + * @brief Gets the component array for a specific component type (const version) + * + * @tparam T The component type + * @return Const shared pointer to the component array + * @throws ComponentNotRegistered if the component type is not registered + */ + template + [[nodiscard]] std::shared_ptr> getComponentArray() const + { + const ComponentType typeID = getComponentTypeID(); + return std::static_pointer_cast>(getComponentArray(typeID)); + } + + /** + * @brief Safely attempts to get a component from an entity + * + * @tparam T The component type + * @param entity The entity to get the component from + * @return Optional reference to the component, or nullopt if not found + */ + template + [[nodiscard]] std::optional> tryGetComponent(Entity entity) + { + auto componentArray = getComponentArray(); + if (!componentArray->hasComponent(entity)) + return std::nullopt; + + return componentArray->get(entity); + } + + /** + * @brief Safely attempts to get a component from an entity + * + * @param entity The entity to get the component from + * @param typeID The component type ID + * @return Pointer to the component if it exists, or nullptr if not found + */ + [[nodiscard]] void *tryGetComponent(const Entity entity, const ComponentType typeID) const + { + const auto componentArray = getComponentArray(typeID); + if (!componentArray->hasComponent(entity)) + return nullptr; + + return componentArray->getRawComponent(entity); + } + + /** + * @brief Notifies all component arrays that an entity has been destroyed + * + * Removes the entity from all component arrays it exists in. + * + * @param entity The destroyed entity + * @param entitySignature Signature of the entity to be destroyed + */ + void entityDestroyed(Entity entity, const Signature &entitySignature); + + /** + * @brief Creates or retrieves a group for specific component combinations + * + * Creates a group that provides optimized access to entities having + * a specific combination of components, or returns an existing one. + * Components specified in the template parameter pack are "owned" (internal to the group), + * while those in the nonOwned parameter are "non-owned" (externally referenced). + * + * @tparam Owned Component types that are owned by the group + * @param nonOwned A get_t<...> tag specifying non-owned component types + * @return A shared pointer to the group (either existing or newly created) + * @throws ComponentNotRegistered if any component type is not registered + * @throws OverlappingGroupsException if the new group would have overlapping owned + * components with an existing group + */ + template + auto registerGroup(const auto& nonOwned) + { + const GroupKey newGroupKey = generateGroupKey(nonOwned); + + // Check if this exact group already exists + auto it = m_groupRegistry.find(newGroupKey); + if (it != m_groupRegistry.end()) { + using OwnedTuple = std::tuple>...>; + using NonOwnedTuple = decltype(getNonOwnedTuple(nonOwned)); + return std::static_pointer_cast>(it->second); + } + + // Check for conflicts with existing groups + for (const auto& existingKey : std::ranges::views::keys(m_groupRegistry)) { + if (hasCommonOwnedComponents(existingKey, newGroupKey)) { + for (ComponentType i = 0; i < MAX_COMPONENT_TYPE; i++) { + if (existingKey.ownedSignature.test(i) && newGroupKey.ownedSignature.test(i)) { + THROW_EXCEPTION(OverlappingGroupsException, + existingKey.toString(), + newGroupKey.toString(), + i); + } + } + } + } + + auto group = createNewGroup(nonOwned); + m_groupRegistry[newGroupKey] = group; + return group; + } + + /** + * @brief Retrieves an existing group for specific component combinations + * + * Gets a previously registered group that matches the specified + * owned and non-owned component types. + * + * @tparam Owned Component types that are owned by the group + * @param nonOwned A get_t<...> tag specifying non-owned component types + * @return A shared pointer to the existing group + * @throws std::runtime_error if the group doesn't exist + */ + template + auto getGroup(const auto& nonOwned) + { + const GroupKey groupKey = generateGroupKey(nonOwned); + + const auto it = m_groupRegistry.find(groupKey); + if (it == m_groupRegistry.end()) + THROW_EXCEPTION(GroupNotFound, "Group not found"); + + using OwnedTuple = std::tuple>...>; + using NonOwnedTuple = decltype(getNonOwnedTuple(nonOwned)); + return std::static_pointer_cast>(it->second); + } + + /** + * @brief Checks if two groups share any common owned components + * + * Determines if two groups have any overlap in their owned components. + * This is useful for determining if two groups might affect each other's sorting + * or partitioning when entities are updated. + * + * @param key1 First group key to compare + * @param key2 Second group key to compare + * @return true if the keys share at least one owned component, false otherwise + */ + [[nodiscard]] static bool hasCommonOwnedComponents(const GroupKey& key1, const GroupKey& key2) { - return std::nullopt; + return (key1.ownedSignature & key2.ownedSignature).any(); } - return components; - } - /** - * @brief Handles the destruction of an entity by ensuring all associated components are removed. - * - * @param entity - The entity that has been destroyed. - */ - void entityDestroyed(Entity entity) const; - - private: - std::unordered_map m_componentTypes{}; - std::unordered_map> m_componentArrays; - - ComponentType _nextComponentType{}; - - /** - * @brief Retrieves the ComponentArray associated with a specific component type. - * - * @return std::shared_ptr> - Shared pointer to the component array of the specified type. - */ - template - std::shared_ptr> getComponentArray() - { - const std::type_index typeName(typeid(T)); - - if (!m_componentArrays.contains(typeName)) - THROW_EXCEPTION(ComponentNotRegistered); - - return std::static_pointer_cast>(m_componentArrays[typeName]); - } - }; + private: + /** + * @brief Array of component arrays indexed by component type ID + * + * Provides O(1) lookup of component arrays by their type ID. + */ + std::array, MAX_COMPONENT_TYPE> m_componentArrays{}; + + /** + * @brief Registry of groups indexed by their component signatures + * + * Allows retrieval of previously created groups by their component types. + */ + std::unordered_map> m_groupRegistry; + + /** + * @brief Helper function to get the tuple of non-owned component arrays + * + * @param nonOwned A get_t<...> tag specifying non-owned component types + * @return Tuple of non-owned component arrays + */ + template + auto getNonOwnedTuple(const get_t&) + { + return std::make_tuple(getComponentArray()...); + } + + /** + * @brief Creates a new group for the specified component types + * + * @tparam Owned Component types owned by the group + * @param nonOwned Tag for non-owned component types + * @return Shared pointer to the new group + */ + template + auto createNewGroup(const auto& nonOwned) + { + auto nonOwnedArrays = getNonOwnedTuple(nonOwned); + + auto ownedArrays = std::make_tuple(getComponentArray()...); + using OwnedTuple = std::tuple>...>; + using NonOwnedTuple = decltype(nonOwnedArrays); + + // Find entities that should be in this group + auto driver = std::get<0>(ownedArrays); + const std::size_t minSize = std::apply([](auto&&... arrays) -> std::size_t { + return std::min({ static_cast(arrays->size())... }); + }, ownedArrays); + + for (std::size_t i = 0; i < minSize; ++i) { + Entity e = driver->getEntityAtIndex(i); + bool valid = true; + + // Check in owned arrays + std::apply([&](auto&&... arrays) { + valid = (valid && ... && arrays->hasComponent(e)); + }, ownedArrays); + + // Check in non-owned arrays + std::apply([&](auto&&... arrays) { + valid = (valid && ... && arrays->hasComponent(e)); + }, nonOwnedArrays); + + if (valid) { + std::apply([&](auto&&... arrays) { + ((arrays->addToGroup(e)), ...); + }, ownedArrays); + } + } + + return std::make_shared>(ownedArrays, nonOwnedArrays); + } + + /** + * @brief Generates a unique key for a group based on its component types + * + * Creates a GroupKey with separate signatures for owned and non-owned components. + * + * @tparam Owned Component types owned by the group + * @param nonOwned Tag for non-owned component types + * @return GroupKey uniquely identifying this group type combination + */ + template + GroupKey generateGroupKey(const auto& nonOwned) + { + GroupKey key; + + // Set bits for owned components + ((key.ownedSignature.set(getComponentTypeID())), ...); + + // Set bits for non-owned components + setNonOwnedBits(key.nonOwnedSignature, nonOwned); + + return key; + } + + /** + * @brief Sets bits in the non-owned signature for each non-owned component + * + * @tparam NonOwning Non-owned component types + * @param signature The signature to modify + * @param nonOwned The non-owned components tag + */ + template + static void setNonOwnedBits(Signature& signature, const get_t&) + { + ((signature.set(getComponentTypeID())), ...); + } + }; } diff --git a/engine/src/ecs/Coordinator.cpp b/engine/src/ecs/Coordinator.cpp index bb365c963..a061195e6 100644 --- a/engine/src/ecs/Coordinator.cpp +++ b/engine/src/ecs/Coordinator.cpp @@ -37,17 +37,21 @@ namespace nexo::ecs { void Coordinator::destroyEntity(const Entity entity) const { + const Signature signature = m_entityManager->getSignature(entity); m_entityManager->destroyEntity(entity); - m_componentManager->entityDestroyed(entity); - m_systemManager->entityDestroyed(entity); + m_componentManager->entityDestroyed(entity, signature); + m_systemManager->entityDestroyed(entity, signature); } - std::vector Coordinator::getAllComponentTypes(const Entity entity) const + std::vector Coordinator::getAllComponentTypes(const Entity entity) const { - std::vector types; + std::vector types; - for (const auto& [type, func] : m_hasComponentFunctions) { - if (func(entity)) { + Signature signature = m_entityManager->getSignature(entity); + + // We have a mapping from component type IDs to type_index + for (ComponentType type = 0; type < MAX_COMPONENT_TYPE; ++type) { + if (signature.test(type)) { types.emplace_back(type); } } @@ -55,16 +59,83 @@ namespace nexo::ecs { return types; } + std::vector Coordinator::getAllComponentTypeIndices(Entity entity) const + { + const std::vector& types = getAllComponentTypes(entity); + std::vector typeIndices; + typeIndices.reserve(types.size()); + + for (const auto& type : types) + { + typeIndices.push_back(m_typeIDtoTypeIndex.at(type)); + } + + return typeIndices; + } + std::vector> Coordinator::getAllComponents(const Entity entity) { std::vector> components; - for (const auto& [type, func] : m_hasComponentFunctions) { - if (func(entity)) { - components.emplace_back(type, m_getComponentFunctions[type](entity)); + // Get the entity's signature which already tells us which components it has + Signature signature = m_entityManager->getSignature(entity); + + // Iterate only through components that the entity actually has + for (ComponentType type = 0; type < MAX_COMPONENT_TYPE; ++type) { + if (signature.test(type) && m_typeIDtoTypeIndex.contains(type)) { + const auto& typeIndex = m_typeIDtoTypeIndex.at(type); + components.emplace_back(typeIndex, m_getComponentFunctions[typeIndex](entity)); } } return components; } + + Entity Coordinator::duplicateEntity(const Entity sourceEntity) const + { + const Entity newEntity = createEntity(); + const Signature signature = m_entityManager->getSignature(sourceEntity); + Signature destSignature = m_entityManager->getSignature(newEntity); + for (ComponentType type = 0; type < MAX_COMPONENT_TYPE; ++type) { + if (signature.test(type) && m_typeIDtoTypeIndex.contains(type)) { + const Signature previousSignature = destSignature; + destSignature.set(type, true); + m_componentManager->duplicateComponent(type, sourceEntity, newEntity, previousSignature, destSignature); + } + } + m_entityManager->setSignature(newEntity, destSignature); + m_systemManager->entitySignatureChanged(newEntity, Signature{}, destSignature); + return newEntity; + } + + bool Coordinator::supportsMementoPattern(const std::any& component) const + { + auto typeId = std::type_index(component.type()); + auto it = m_supportsMementoPattern.find(typeId); + return (it != m_supportsMementoPattern.end()) && it->second; + } + + std::any Coordinator::saveComponent(const std::any& component) const + { + auto typeId = std::type_index(component.type()); + auto it = m_saveComponentFunctions.find(typeId); + if (it != m_saveComponentFunctions.end()) + return it->second(component); + return std::any(); + } + + std::any Coordinator::restoreComponent(const std::any& memento, const std::type_index& componentType) const + { + auto it = m_restoreComponentFunctions.find(componentType); + if (it != m_restoreComponentFunctions.end()) + return it->second(memento); + return std::any(); + } + + void Coordinator::addComponentAny(Entity entity, const std::type_index& typeIndex, const std::any& component) + { + auto it = m_addComponentFunctions.find(typeIndex); + if (it != m_addComponentFunctions.end()) + it->second(entity, component); + } } \ No newline at end of file diff --git a/engine/src/ecs/Coordinator.hpp b/engine/src/ecs/Coordinator.hpp index c1a371652..b7ff44ac8 100644 --- a/engine/src/ecs/Coordinator.hpp +++ b/engine/src/ecs/Coordinator.hpp @@ -22,8 +22,86 @@ #include "SingletonComponent.hpp" #include "Entity.hpp" #include "Logger.hpp" +#include "TypeErasedComponent/ComponentDescription.hpp" namespace nexo::ecs { + + template + struct Exclude { + using type = T; + }; + + // Check if type is an Exclude specialization + template + struct is_exclude : std::false_type {}; + + template + struct is_exclude> : std::true_type {}; + + template + inline constexpr bool is_exclude_v = is_exclude::value; + + // Extract the actual type from Exclude + template + struct extract_type { + using type = T; + }; + + template + struct extract_type> { + using type = T; + }; + + template + using extract_type_t = typename extract_type::type; + + // Check if T has a nested Memento type + template + struct has_memento_type : std::false_type {}; + + template + struct has_memento_type> : std::true_type {}; + + // Check if T has a save() method that returns Memento + template + struct has_save_method : std::false_type {}; + + template + struct has_save_method().save())>> + : std::is_same().save()), typename T::Memento> {}; + + // Check if T::Memento has a restore() method that returns T + template + struct has_restore_method : std::false_type {}; + + template + struct has_restore_method().restore())>> + : std::is_same().restore()), T> {}; + + // Combined check for full memento pattern support + template + struct supports_memento_pattern : + std::conjunction< + has_memento_type, + has_save_method, + has_restore_method + > {}; + + template + inline constexpr bool has_memento_type_v = has_memento_type::value; + + template + inline constexpr bool has_save_method_v = has_save_method::value; + + template + inline constexpr bool has_restore_method_v = has_restore_method::value; + + template + inline constexpr bool supports_memento_pattern_v = supports_memento_pattern::value; + + /** * @class Coordinator * @@ -60,27 +138,67 @@ namespace nexo::ecs { * @brief Registers a new component type within the ComponentManager. */ template - void registerComponent() { + void registerComponent() + { m_componentManager->registerComponent(); - m_hasComponentFunctions[typeid(T)] = [this](Entity entity) -> bool { - return this->entityHasComponent(entity); - }; m_getComponentFunctions[typeid(T)] = [this](Entity entity) -> std::any { - return std::any(this->getComponent(entity)); + return this->getComponent(entity); }; + + m_getComponentPointers[typeid(T)] = [this](Entity entity) -> std::any { + auto opt = this->tryGetComponent(entity); + if (!opt.has_value()) + return std::any(); + T* ptr = &opt.value().get(); + return std::any(static_cast(ptr)); + }; + m_typeIDtoTypeIndex.emplace(getComponentType(), typeid(T)); + + m_addComponentFunctions[typeid(T)] = [this](Entity entity, const std::any& componentAny) { + T component = std::any_cast(componentAny); + this->addComponent(entity, component); + }; + + if constexpr (supports_memento_pattern_v) { + m_supportsMementoPattern.emplace(typeid(T), true); + + m_saveComponentFunctions[typeid(T)] = [](const std::any& componentAny) -> std::any { + const T& component = std::any_cast(componentAny); + return std::any(component.save()); + }; + + m_restoreComponentFunctions[typeid(T)] = [](const std::any& mementoAny) -> std::any { + const typename T::Memento& memento = std::any_cast(mementoAny); + return std::any(memento.restore()); + }; + } else { + m_supportsMementoPattern.emplace(typeid(T), false); + } + } + + void addComponentDescription(const ComponentType componentType, const ComponentDescription& description) + { + m_componentDescriptions[componentType] = std::make_shared(description); + } + + ComponentType registerComponent(const size_t componentSize, const size_t initialCapacity = 1024) + { + auto typeID = m_componentManager->registerComponent(componentSize, initialCapacity); + return typeID; } /** * @brief Registers a new singleton component * * @tparam T Class that should inherit from SingletonComponent class - * @param component + * @param args Optional argument to forward to the singleton component constructor */ - template - void registerSingletonComponent(Args&&... args) { - m_singletonComponentManager->registerSingletonComponent(std::forward(args)...); - } + template + void registerSingletonComponent(Args&&... args) + { + m_singletonComponentManager->registerSingletonComponent(std::forward(args)...); + } /** * @brief Adds a component to an entity, updates its signature, and notifies systems. @@ -89,14 +207,40 @@ namespace nexo::ecs { * @param component - The component to add to the entity. */ template - void addComponent(const Entity entity, T component) { - m_componentManager->addComponent(entity, component); - - auto signature = m_entityManager->getSignature(entity); + void addComponent(const Entity entity, T component) + { + Signature signature = m_entityManager->getSignature(entity); + const Signature oldSignature = signature; signature.set(m_componentManager->getComponentType(), true); + m_componentManager->addComponent(entity, component, oldSignature, signature); + m_entityManager->setSignature(entity, signature); - m_systemManager->entitySignatureChanged(entity, signature); + m_systemManager->entitySignatureChanged(entity, oldSignature, signature); + } + + /** + * @brief Adds a component to an entity, updates its signature, and notifies systems. + * + * This function allows adding a component by its type ID and raw data pointer. + * + * @param entity The ID of the entity to which the component will be added. + * @param componentType The type ID of the component to be added. + * @param componentData Pointer to the raw component data. + * + * @pre componentType must be a valid registered component type. + * @pre componentData must point to valid memory of the component's size. + */ + void addComponent(const Entity entity, const ComponentType componentType, const void *componentData) const + { + Signature signature = m_entityManager->getSignature(entity); + const Signature oldSignature = signature; + signature.set(componentType, true); + m_componentManager->addComponent(entity, componentType, componentData, oldSignature, signature); + + m_entityManager->setSignature(entity, signature); + + m_systemManager->entitySignatureChanged(entity, oldSignature, signature); } /** @@ -107,13 +251,15 @@ namespace nexo::ecs { template void removeComponent(const Entity entity) const { - m_componentManager->removeComponent(entity); - - auto signature = m_entityManager->getSignature(entity); + Signature signature = m_entityManager->getSignature(entity); + const Signature oldSignature = signature; signature.set(m_componentManager->getComponentType(), false); + m_componentManager->removeComponent(entity, oldSignature, signature); + + m_entityManager->setSignature(entity, signature); - m_systemManager->entitySignatureChanged(entity, signature); + m_systemManager->entitySignatureChanged(entity, oldSignature, signature); } /** @@ -127,13 +273,14 @@ namespace nexo::ecs { template void tryRemoveComponent(const Entity entity) const { - if (m_componentManager->tryRemoveComponent(entity)) + Signature signature = m_entityManager->getSignature(entity); + Signature oldSignature = signature; + signature.set(m_componentManager->getComponentType(), false); + if (m_componentManager->tryRemoveComponent(entity, oldSignature, signature)) { - auto signature = m_entityManager->getSignature(entity); - signature.set(m_componentManager->getComponentType(), false); m_entityManager->setSignature(entity, signature); - m_systemManager->entitySignatureChanged(entity, signature); + m_systemManager->entitySignatureChanged(entity, oldSignature, signature); } } @@ -155,10 +302,23 @@ namespace nexo::ecs { * @return T& - Reference to the requested component. */ template - T &getComponent(const Entity entity) { + T &getComponent(const Entity entity) + { return m_componentManager->getComponent(entity); } + /** + * @brief Retrieves the component array for a specific component type + * + * @tparam T The component type + * @return std::shared_ptr> Shared pointer to the component array + */ + template + std::shared_ptr> getComponentArray() + { + return m_componentManager->getComponentArray(); + } + /** * @brief Attempts to retrieve a component from an entity. * @@ -172,6 +332,23 @@ namespace nexo::ecs { return m_componentManager->tryGetComponent(entity); } + void *tryGetComponentById(const ComponentType componentType, const Entity entity)const + { + return m_componentManager->tryGetComponent(entity, componentType); + } + + const std::unordered_map& getTypeIdToTypeIndex() const { + return m_typeIDtoTypeIndex; + } + + const std::unordered_map>& getAddComponentFunctions() const { + return m_addComponentFunctions; + } + + Signature getSignature(Entity entity) const { + return m_entityManager->getSignature(entity); + } + /** * @brief Get the Singleton Component object * @@ -179,17 +356,32 @@ namespace nexo::ecs { * @return T& The instance of the desired singleton component */ template - T &getSingletonComponent() { + T &getSingletonComponent() + { return m_singletonComponentManager->getSingletonComponent(); } + /** + * @brief Get the Raw Singleton Component object + * + * @tparam T Class that should inherit from the SingletonComponent class + * @return std::shared_ptr The pointer to the desired singleton component + */ + template + std::shared_ptr getRawSingletonComponent() const + { + return m_singletonComponentManager->getRawSingletonComponent(); + } + /** * @brief Retrieves all component types associated with an entity. * * @param entity The target entity identifier. * @return std::vector A list of type indices for each component the entity has. */ - std::vector getAllComponentTypes(Entity entity) const; + std::vector getAllComponentTypes(Entity entity) const; + + std::vector getAllComponentTypeIndices(Entity entity) const; /** * @brief Retrieves all components associated with an entity. @@ -200,32 +392,41 @@ namespace nexo::ecs { std::vector> getAllComponents(Entity entity); /** - * @brief Retrieves all entities that have the specified components. - * - * @tparam Components The component types to filter by. - * @return std::set A set of entities that contain all the specified components. - */ + * @brief Retrieves all entities that have the specified components. + * + * @tparam Components The component types to filter by. + * @return std::set A set of entities that contain all the specified components. + */ template - std::set getAllEntitiesWith() const + std::vector getAllEntitiesWith() const { - Signature requiredSignature; - (requiredSignature.set(m_componentManager->getComponentType(), true), ...); - - std::uint32_t checkedEntities = 0; - std::uint32_t livingEntities = m_entityManager->getLivingEntityCount(); - std::set result; - for (Entity i = 0; i < MAX_ENTITIES; ++i) - { - Signature entitySignature = m_entityManager->getSignature(i); - if (entitySignature.none()) - continue; - if ((entitySignature & requiredSignature) == requiredSignature) - result.insert(i); - checkedEntities++; - if (checkedEntities > livingEntities) - break; - } - return result; + // Prepare signatures + Signature requiredSignature; + Signature excludeSignature; + + // Process each component type + (processComponentSignature(requiredSignature, excludeSignature), ...); + + // Query entities + std::span livingEntities = m_entityManager->getLivingEntities(); + std::vector result; + result.reserve(livingEntities.size()); + + for (Entity entity : livingEntities) + { + const Signature entitySignature = m_entityManager->getSignature(entity); + + // Entity must have all required components + bool hasAllRequired = (entitySignature & requiredSignature) == requiredSignature; + + // Entity must not have any excluded components + bool hasAnyExcluded = (entitySignature & excludeSignature).any(); + + if (hasAllRequired && !hasAnyExcluded) + result.push_back(entity); + } + + return result; } /** @@ -239,16 +440,70 @@ namespace nexo::ecs { return m_componentManager->getComponentType(); } + const std::unordered_map>& getComponentDescriptions() const + { + return m_componentDescriptions; + } + /** - * @brief Registers a new system within the SystemManager. + * @brief Registers a new query system * - * @return std::shared_ptr - Shared pointer to the newly registered system. + * @tparam T The system type to register + * @tparam Args Additional constructor arguments + * @return std::shared_ptr Shared pointer to the registered system */ - template - std::shared_ptr registerSystem() { - return m_systemManager->registerSystem(); + template + std::shared_ptr registerQuerySystem(Args&&... args) { + auto newQuerySystem = m_systemManager->registerQuerySystem(std::forward(args)...); + std::span livingEntities = m_entityManager->getLivingEntities(); + const Signature querySystemSignature = newQuerySystem->getSignature(); + for (Entity entity : livingEntities) { + const Signature entitySignature = m_entityManager->getSignature(entity); + if ((entitySignature & querySystemSignature) == querySystemSignature) { + newQuerySystem->entities.insert(entity); + } + } + return newQuerySystem; + } + + /** + * @brief Registers a new group system + * + * @tparam T The system type to register + * @tparam Args Additional constructor arguments + * @return std::shared_ptr Shared pointer to the registered system + */ + template + std::shared_ptr registerGroupSystem(Args&&... args) { + return m_systemManager->registerGroupSystem(std::forward(args)...); } + /** + * @brief Creates or retrieves a group for specific component combinations + * + * @tparam Owned Component types that are owned by the group + * @param nonOwned A get_t<...> tag specifying non-owned component types + * @return A shared pointer to the group (either existing or newly created) + */ + template + auto registerGroup(const auto & nonOwned) + { + return m_componentManager->registerGroup(nonOwned); + } + + /** + * @brief Retrieves an existing group for specific component combinations + * + * @tparam Owned Component types that are owned by the group + * @param nonOwned A get_t<...> tag specifying non-owned component types + * @return A shared pointer to the existing group + */ + template + auto getGroup(const auto& nonOwned) + { + return m_componentManager->getGroup(nonOwned); + } + /** * @brief Sets the signature for a system, defining which entities it will process. * @@ -269,22 +524,66 @@ namespace nexo::ecs { * @return false Otherwise. */ template - bool entityHasComponent(const Entity entity) const + [[nodiscard]] bool entityHasComponent(const Entity entity) const { - const auto signature = m_entityManager->getSignature(entity); const ComponentType componentType = m_componentManager->getComponentType(); + return entityHasComponent(entity, componentType); + } + + /** + * @brief Checks whether an entity has a specific component by its type ID. + * + * @param entity The target entity. + * @param componentType The type ID of the component. + * @return true If the entity has the component. + * @return false Otherwise. + */ + [[nodiscard]] bool entityHasComponent(const Entity entity, const ComponentType componentType) const + { + const Signature signature = m_entityManager->getSignature(entity); return signature.test(componentType); } - void updateSystemEntities() const; - private: + template + void setRestoreComponent() { + m_restoreComponentFunctions[typeid(T)] = [](const std::any&) -> std::any { + return std::any(T{}); // default-constructed + }; + } + + + bool supportsMementoPattern(const std::any& component) const; + std::any saveComponent(const std::any& component) const; + std::any restoreComponent(const std::any& memento, const std::type_index& componentType) const; + void addComponentAny(Entity entity, const std::type_index& typeIndex, const std::any& component); + + Entity duplicateEntity(Entity sourceEntity) const; + private: + template + void processComponentSignature(Signature& required, Signature& excluded) const { + if constexpr (is_exclude_v) { + // This is an excluded component + using ActualType = typename Component::type; + excluded.set(m_componentManager->getComponentType(), true); + } else { + // This is a required component + required.set(m_componentManager->getComponentType(), true); + } + } std::shared_ptr m_componentManager; std::shared_ptr m_entityManager; std::shared_ptr m_systemManager; std::shared_ptr m_singletonComponentManager; - std::unordered_map> m_hasComponentFunctions; + std::unordered_map m_typeIDtoTypeIndex; + std::unordered_map m_supportsMementoPattern; + std::unordered_map> m_saveComponentFunctions; + std::unordered_map> m_restoreComponentFunctions; + std::unordered_map> m_addComponentFunctions; std::unordered_map> m_getComponentFunctions; + std::unordered_map> m_getComponentPointers; + + std::unordered_map> m_componentDescriptions; }; } diff --git a/engine/src/ecs/Definitions.hpp b/engine/src/ecs/Definitions.hpp new file mode 100644 index 000000000..0d1fe0a7b --- /dev/null +++ b/engine/src/ecs/Definitions.hpp @@ -0,0 +1,120 @@ +//// Definitions.hpp ////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 31/03/2025 +// Description: Header file containing type definitions and constants +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +namespace nexo::ecs { + // Entity type definition + + /** + * @brief Entity identifier type + * + * Used to uniquely identify entities in the ECS. + */ + using Entity = std::uint32_t; + + /** + * @brief Maximum number of entities that can exist simultaneously + */ + constexpr Entity MAX_ENTITIES = 500000; + + /** + * @brief Special value representing an invalid or non-existent entity + */ + constexpr Entity INVALID_ENTITY = std::numeric_limits::max(); + + // Component type definitions + + /** + * @brief Component type identifier + * + * Used to uniquely identify different component types. + */ + using ComponentType = std::uint8_t; + + /** + * @brief Maximum number of different component types in the system + */ + constexpr ComponentType MAX_COMPONENT_TYPE = 32; + + /** + * @brief Global counter for generating unique component type IDs + */ + inline ComponentType globalComponentCounter = 0; + + inline ComponentType generateComponentTypeID() + { + assert(globalComponentCounter < MAX_COMPONENT_TYPE && "Maximum number of component types exceeded"); + return globalComponentCounter++; + } + + /** + * @brief Gets a unique ID for a component type + * + * Returns a statically allocated ID for each unique component type T. + * The first call for a type T will assign a new ID; subsequent calls + * for the same type will return the previously assigned ID. + * + * @tparam T Component type + * @return ComponentType Unique ID for the type + */ + template + ComponentType getUniqueComponentTypeID() + { + // This static variable is instantiated once per type T, + // but it will be assigned a unique value from the shared global counter. + // TODO: Warning! This is not thread-safe. (and it's a crappy implementation, but it works for now) + static const ComponentType id = generateComponentTypeID(); + return id; + } + + /** + * @brief Gets the component type ID, removing const/volatile/reference qualifiers + * + * @tparam T Component type + * @return ComponentType ID for the component type + */ + template + ComponentType getComponentTypeID() + { + return getUniqueComponentTypeID>(); + } + + // Group type definition + + /** + * @brief Group identifier type + * + * Used to uniquely identify different entity groups. + */ + using GroupType = std::uint8_t; + + /** + * @brief Maximum number of groups that can exist simultaneously + */ + constexpr GroupType MAX_GROUP_NUMBER = 32; + + /** + * @brief Signature type for component composition + * + * A bitset where each bit represents whether an entity has a specific component type. + */ + using Signature = std::bitset; + +} diff --git a/engine/src/ecs/ECSExceptions.hpp b/engine/src/ecs/ECSExceptions.hpp index 3cbc8bb31..01ed25ffe 100644 --- a/engine/src/ecs/ECSExceptions.hpp +++ b/engine/src/ecs/ECSExceptions.hpp @@ -14,18 +14,46 @@ #pragma once #include "Exception.hpp" +#include "Definitions.hpp" #include #include namespace nexo::ecs { - using Entity = std::uint32_t; + + class InternalError final : public Exception { + public: + explicit InternalError(const std::string& message, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Internal error: {}", message), loc) {} + }; class ComponentNotFound final : public Exception { public: - explicit ComponentNotFound(const Entity entity, + explicit ComponentNotFound(const Entity entity, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Component not found for: {}", entity), loc) {} + }; + + class OverlappingGroupsException final : public Exception { + public: + explicit OverlappingGroupsException(const std::string& existingGroup, + const std::string& newGroup, ComponentType conflictingComponent, + const std::source_location loc = std::source_location::current()) + : Exception(std::format("Cannot create group {} because it has overlapping owned component #{} with existing group {}", newGroup, conflictingComponent, existingGroup), loc) {} + }; + + class GroupNotFound final : public Exception { + public: + explicit GroupNotFound(const std::string &groupKey, const std::source_location loc = std::source_location::current()) - : Exception(std::format("Component not found for: {}", entity), loc) {} + : Exception(std::format("Group not found for key: {}", groupKey), loc) {} + }; + + class InvalidGroupComponent final : public Exception { + public: + explicit InvalidGroupComponent(const std::source_location loc = std::source_location::current()) + : Exception("Component has not been found in the group", loc) {} }; class ComponentNotRegistered final : public Exception { @@ -49,12 +77,12 @@ namespace nexo::ecs { class TooManyEntities final : public Exception { public: explicit TooManyEntities(const std::source_location loc = std::source_location::current()) - : Exception("Too many living entities, max is 8191", loc) {} + : Exception(std::format("Too many living entities, max is {}", MAX_ENTITIES), loc) {} }; class OutOfRange final : public Exception { public: - explicit OutOfRange(unsigned int index, const std::source_location loc = std::source_location::current()) - : Exception(std::format("Index {} is out of range", index), loc) {} + explicit OutOfRange(unsigned int index, const std::source_location loc = std::source_location::current()) + : Exception(std::format("Index {} is out of range", index), loc) {} }; } diff --git a/engine/src/ecs/Entity.cpp b/engine/src/ecs/Entity.cpp index 136044dc2..b54f745f8 100644 --- a/engine/src/ecs/Entity.cpp +++ b/engine/src/ecs/Entity.cpp @@ -11,25 +11,30 @@ // Description: Source file for the entity class // /////////////////////////////////////////////////////////////////////////////// + #include "Entity.hpp" #include "ECSExceptions.hpp" +#include +#include +#include + namespace nexo::ecs { EntityManager::EntityManager() { for (Entity entity = 0; entity < MAX_ENTITIES; ++entity) - m_availableEntities.push(entity); + m_availableEntities.push_back(entity); } Entity EntityManager::createEntity() { - if (m_livingEntityCount >= MAX_ENTITIES) + if (m_livingEntities.size() >= MAX_ENTITIES) THROW_EXCEPTION(TooManyEntities); const Entity id = m_availableEntities.front(); - m_availableEntities.pop(); - ++m_livingEntityCount; + m_availableEntities.pop_front(); + m_livingEntities.push_back(id); return id; } @@ -39,11 +44,15 @@ namespace nexo::ecs { if (entity >= MAX_ENTITIES) THROW_EXCEPTION(OutOfRange, entity); + const auto it = std::ranges::find(m_livingEntities, entity); + if (it != m_livingEntities.end()) + m_livingEntities.erase(it); + else + return; m_signatures[entity].reset(); - m_availableEntities.push(entity); - if (m_livingEntityCount > 0) - --m_livingEntityCount; + m_availableEntities.push_front(entity); + } void EntityManager::setSignature(const Entity entity, const Signature signature) @@ -62,8 +71,14 @@ namespace nexo::ecs { return m_signatures[entity]; } - std::uint32_t EntityManager::getLivingEntityCount() const + size_t EntityManager::getLivingEntityCount() const { - return m_livingEntityCount; + return m_livingEntities.size(); } + + std::span EntityManager::getLivingEntities() const + { + return {m_livingEntities}; + } + } diff --git a/engine/src/ecs/Entity.hpp b/engine/src/ecs/Entity.hpp index 944c58efd..9da777be7 100644 --- a/engine/src/ecs/Entity.hpp +++ b/engine/src/ecs/Entity.hpp @@ -14,10 +14,12 @@ #pragma once -#include +#include #include +#include +#include -#include "Signature.hpp" +#include "Definitions.hpp" namespace nexo::ecs { @@ -71,13 +73,24 @@ namespace nexo::ecs { */ [[nodiscard]] Signature getSignature(Entity entity) const; - std::uint32_t getLivingEntityCount() const; + /** + * @brief Returns the number of currently active entities + * + * @return size_t The count of living entities + */ + [[nodiscard]] size_t getLivingEntityCount() const; + + /** + * @brief Retrieves a view of all currently active entities + * + * @return std::span A span containing all living entity IDs + */ + [[nodiscard]] std::span getLivingEntities() const; private: - std::queue m_availableEntities{}; + std::deque m_availableEntities{}; + std::vector m_livingEntities{}; std::array m_signatures{}; - - std::uint32_t m_livingEntityCount{}; }; } diff --git a/engine/src/ecs/Group.hpp b/engine/src/ecs/Group.hpp new file mode 100644 index 000000000..b9bfa4964 --- /dev/null +++ b/engine/src/ecs/Group.hpp @@ -0,0 +1,961 @@ +//// Group.hpp //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 01/04/2025 +// Description: Header file for the ECS groups +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Definitions.hpp" +#include "ComponentArray.hpp" +#include "ECSExceptions.hpp" +#include "Exception.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nexo::ecs { + + /** + * @brief Interface for ECS groups. + * + * This interface defines the minimum requirements for groups that store a set + * of entities along with their associated component signatures. + */ + class IGroup { + public: + virtual ~IGroup() = default; + + /** + * @brief Returns the combined signature of all components in the group. + * + * @return const Signature& Combined signature. + */ + [[nodiscard]] virtual const Signature& allSignature() const = 0; + /** + * @brief Adds an entity to the group. + * + * @param e Entity to add. + */ + virtual void addToGroup(Entity e) = 0; + /** + * @brief Removes an entity from the group. + * + * @param e Entity to remove. + */ + virtual void removeFromGroup(Entity e) = 0; + }; + + /** + * @brief Helper template that always evaluates to false. + * + * This is used to trigger static_assert in template functions. + * + * @tparam T Type to test. + */ + template + struct dependent_false : std::false_type {}; + + /** + * @brief Metafunction to check if a tuple contains a specific component type. + * + * Example: if Tuple is std::tuple>, std::shared_ptr>> + * then tuple_contains_component::value is true if T is Position or Velocity. + * + * @tparam T Component type to check. + * @tparam Tuple Tuple type containing pointers to ComponentArray objects. + */ + template + struct tuple_contains_component; + + /** + * @brief Specialization of tuple_contains_component for std::tuple. + * + * @tparam T Component type to check. + * @tparam Ptrs Pointer types stored in the tuple. + */ + template + struct tuple_contains_component> + : std::disjunction())>::component_type>...> {}; + + /** + * @brief Convenience variable for tuple_contains_component. + * + * @tparam T Component type to check. + * @tparam Tuple Tuple type. + */ + template + constexpr bool tuple_contains_component_v = tuple_contains_component::value; + + /** + * @brief Represents a partition of entities based on a key. + * + * @tparam KeyType The type of the key used for partitioning. + */ + template + struct Partition { + KeyType key; ///< The partition key. + size_t startIndex; ///< The starting index of the partition. + size_t count; ///< The number of entities in the partition. + }; + + /** + * @brief Alias for a function that extracts a field from a component. + * + * @tparam T Component type. + * @tparam FieldType Type of the extracted field. + */ + template + using FieldExtractor = std::function; + + /** + * @brief Alias for a function that extracts a key from an entity. + * + * @tparam KeyType Type of the key. + */ + template + using EntityKeyExtractor = std::function; + + /** + * @brief Group class for a view over entities with both owned and non‑owned components. + * + * Two tuple types are taken: + * - OwnedTuple: std::tuple + * - NonOwnedTuple: std::tuple + * + * @tparam OwnedTuple Tuple of pointers (or smart pointers) to owned component arrays. + * @tparam NonOwnedTuple Tuple of pointers to non‑owned component arrays. + */ + template + class Group final : public IGroup { + public: + /** + * @brief Constructs a new Group. + * + * @tparam NonOwning Variadic template parameters for non‑owned components. + * @param ownedArrays Tuple of pointers to owned component arrays. + * @param nonOwnedArrays Tuple of pointers to non‑owned component arrays. + * + * The constructor computes the owned and non‑owned signatures and their combination. + */ + template + Group(OwnedTuple ownedArrays, NonOwnedTuple nonOwnedArrays) + : m_ownedArrays(std::move(ownedArrays)) + , m_nonOwnedArrays(std::move(nonOwnedArrays)) + { + if (std::tuple_size_v == 0) + THROW_EXCEPTION(InternalError, "Group must have at least one owned component"); + + m_ownedSignature = std::apply([]([[maybe_unused]] auto&&... arrays) { + Signature signature; + ((signature.set(getComponentTypeID::component_type>())), ...); + return signature; + }, m_ownedArrays); + + const Signature nonOwnedSignature = std::apply([]([[maybe_unused]] auto&&... arrays) { + Signature signature; + ((signature.set(getComponentTypeID::component_type>())), ...); + return signature; + }, m_nonOwnedArrays); + + m_allSignature = m_ownedSignature | nonOwnedSignature; + } + + // ======================================= + // Core Group API + // ======================================= + + /** + * @brief Returns the number of entities in the group. + * + * @return std::size_t Number of entities. + */ + [[nodiscard]] std::size_t size() const + { + auto firstArray = std::get<0>(m_ownedArrays); + if (!firstArray) { + THROW_EXCEPTION(InternalError, "Component array is null"); + } + return firstArray->groupSize(); + } + + /** + * @brief Checks if sorting has been invalidated. + * + * @return true If sorting is invalidated. + * @return false Otherwise. + */ + [[nodiscard]] bool sortingInvalidated() const { return m_sortingInvalidated; } + + /** + * @brief Returns the signature for owned components. + * + * @return const Signature& Owned signature. + */ + [[nodiscard]] const Signature& ownedSignature() const { return m_ownedSignature; } + + /** + * @brief Returns the overall signature for both owned and non‑owned components. + * + * @return const Signature& Combined signature. + */ + [[nodiscard]] const Signature& allSignature() const override { return m_allSignature; } + + /** + * @brief Iterator for Group. + * + * Allows iterating over entities along with their owned and non‑owned components. + */ + class GroupIterator { + public: + using iterator_category = std::forward_iterator_tag; + /// The type returned by dereferencing the iterator. + using value_type = decltype(std::declval().dereference(0)); + using reference = value_type; + using difference_type = std::ptrdiff_t; + using pointer = void; + + /** + * @brief Constructs a GroupIterator. + * + * @param view Pointer to the Group. + * @param index Starting index. + */ + GroupIterator(const Group* view, std::size_t index) + : m_view(view), m_index(index) {} + + /** + * @brief Dereferences the iterator to get the entity and its components. + * + * @return reference Tuple containing the entity and its component data. + */ + reference operator*() const + { + if (m_index >= m_view->size()) + THROW_EXCEPTION(OutOfRange, m_index); + + return m_view->dereference(m_index); + } + + /** + * @brief Pre-increment operator. + * + * @return GroupIterator& Reference to the iterator after increment. + */ + GroupIterator& operator++() + { + ++m_index; + return *this; + } + + /** + * @brief Post-increment operator. + * + * @return GroupIterator Iterator before increment. + */ + GroupIterator operator++(int) + { + GroupIterator tmp = *this; + ++(*this); + return tmp; + } + + /** + * @brief Equality operator. + * + * @param other Another iterator. + * @return true If both iterators are equal. + * @return false Otherwise. + */ + bool operator==(const GroupIterator& other) const + { + return m_index == other.m_index && m_view == other.m_view; + } + + private: + const Group* m_view; ///< Pointer to the group. + std::size_t m_index; ///< Current index in the group. + }; + + /** + * @brief Returns an iterator to the beginning of the group + * + * @return GroupIterator Iterator pointing to the first entity + */ + GroupIterator begin() const { return GroupIterator(this, 0); } + + /** + * @brief Returns an iterator to the end of the group + * + * @return GroupIterator Iterator pointing beyond the last entity + */ + GroupIterator end() const { return GroupIterator(this, size()); } + + /** + * @brief Returns an iterator to the beginning of the group (non-const version) + * + * @return GroupIterator Iterator pointing to the first entity + */ + GroupIterator begin() { return GroupIterator(this, 0); } + + /** + * @brief Returns an iterator to the end of the group (non-const version) + * + * @return GroupIterator Iterator pointing beyond the last entity + */ + GroupIterator end() { return GroupIterator(this, size()); } + + /** + * @brief Iterates over each entity in the group. + * + * The callable 'func' must accept parameters of the form: + * (Entity, Owned&..., NonOwned&...). + * + * @tparam Func Callable type. + * @param func Function to call for each entity. + */ + template + void each(Func func) const + { + auto firstArray = std::get<0>(m_ownedArrays); + if (!firstArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + for (std::size_t i = 0; i < firstArray->groupSize(); ++i) { + Entity e = firstArray->getEntityAtIndex(i); + callFunc(func, e, + std::make_index_sequence>{}, + std::make_index_sequence>{}); + } + } + + /** + * @brief Iterates over a sub-range of entities in the group. + * + * @tparam Func Callable type. + * @param startIndex Starting index. + * @param count Number of entities to process. + * @param func Function to call for each entity in range. + */ + template + void eachInRange(size_t startIndex, const size_t count, Func func) const + { + auto firstArray = std::get<0>(m_ownedArrays); + if (!firstArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + if (startIndex >= firstArray->groupSize()) + return; // Nothing to iterate + + const size_t endIndex = std::min(startIndex + count, firstArray->groupSize()); + + for (size_t i = startIndex; i < endIndex; i++) { + Entity e = firstArray->getEntityAtIndex(i); + callFunc(func, e, + std::make_index_sequence>{}, + std::make_index_sequence>{}); + } + } + + /** + * @brief Adds an entity to the group. + * + * This method calls addToGroup(e) on every owned component array. + * + * @param e Entity to add. + */ + void addToGroup(Entity e) override + { + std::apply([e](auto&&... arrays) { + ((arrays->addToGroup(e)), ...); + }, m_ownedArrays); + + m_sortingInvalidated = true; + invalidatePartitions(); + } + + /** + * @brief Removes an entity from the group. + * + * This method calls removeFromGroup(e) on every owned component array. + * + * @param e Entity to remove. + */ + void removeFromGroup(Entity e) override + { + std::apply([e](auto&&... arrays) { + ((arrays->removeFromGroup(e)), ...); + }, m_ownedArrays); + + m_sortingInvalidated = true; + invalidatePartitions(); + } + + /** + * @brief Retrieves a span of entity IDs corresponding to the group. + * + * This is taken from the first owned component array's entities() span, + * restricted to its group region. + * + * @return std::span Span of entity IDs. + */ + [[nodiscard]] std::span entities() const + { + const std::span entities = std::get<0>(m_ownedArrays)->entities(); + return entities.subspan(0, std::get<0>(m_ownedArrays)->groupSize()); + } + + /** + * @brief Retrieves the component array data for a given component type. + * + * This overload returns a const view of the data. + * + * @tparam T Component type. + * @return auto Span over component data. + */ + template + auto get() const + { + if constexpr (tuple_contains_component_v) { + auto compArray = getOwnedImpl(); // internal lookup in owned tuple + if (!compArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + return compArray->getAllComponents().subspan(0, compArray->groupSize()); + } else if constexpr (tuple_contains_component_v) + return getNonOwnedImpl(); // internal lookup in non‑owned tuple + else + static_assert(dependent_false::value, "Component type not found in group"); + } + + /** + * @brief Retrieves the component array data for a given component type. + * + * This overload returns a mutable view of the data. + * + * @tparam T Component type. + * @return auto Span over component data. + */ + template + auto get() + { + if constexpr (tuple_contains_component_v) { + auto compArray = getOwnedImpl(); // internal lookup in owned tuple + if (!compArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + return compArray->getAllComponents().subspan(0, compArray->groupSize()); + } else if constexpr (tuple_contains_component_v) + return getNonOwnedImpl(); // internal lookup in non‑owned tuple + else + static_assert(dependent_false::value, "Component type not found in group"); + } + + // ======================================= + // Sorting API + // ======================================= + + /** + * @brief Marks the group's sorting as invalidated + * Should be called when modifying a component that can affect the sorting + * + * When sorting is invalidated, the next call to sortBy() will perform a full resort. + */ + void invalidateSorting() + { + m_sortingInvalidated = true; + } + + /** + * @brief Sorts the group by a specified component field. + * + * The sorting is only performed if the sorting is invalidated. + * + * @tparam CompType Component type to sort by. + * @tparam FieldType Field type to compare. + * @param extractor Function to extract the field value. + * @param ascending Set to true for ascending order (default true). + */ + template + void sortBy(FieldExtractor extractor, bool ascending = true) + { + SortingOrder sortingOrder = ascending ? SortingOrder::ASCENDING : SortingOrder::DESCENDING; + + if (sortingOrder != m_sortingOrder) { + m_sortingOrder = sortingOrder; + m_sortingInvalidated = true; + } + + if (!m_sortingInvalidated) + return; + + std::shared_ptr> compArray; + + if constexpr (tuple_contains_component_v) { + compArray = getOwnedImpl(); + } else if constexpr (tuple_contains_component_v) { + compArray = getNonOwnedImpl(); + } else { + static_assert(dependent_false::value, "Component type not found in group"); + } + + if (!compArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + auto drivingArray = std::get<0>(m_ownedArrays); + const size_t groupSize = drivingArray->groupSize(); + + std::vector entities; + entities.reserve(groupSize); + + // Add all entities currently in the group + for (size_t i = 0; i < groupSize; i++) + entities.push_back(drivingArray->getEntityAtIndex(i)); + + // Sort entities based on the extracted field from the component + std::ranges::sort(entities, + [&](Entity a, Entity b) { + const auto& compA = compArray->get(a); + const auto& compB = compArray->get(b); + if (ascending) + return extractor(compA) < extractor(compB); + return extractor(compA) > extractor(compB); + }); + + reorderGroup(entities); + m_sortingInvalidated = false; + } + + // ======================================= + // Partitioning API + // ======================================= + + /** + * @brief A view over a partition of entities based on a key. + * + * @tparam KeyType The type of the partition key. + */ + template + class PartitionView { + public: + /** + * @brief Constructs a PartitionView. + * + * @param group Pointer to the group. + * @param partitions Reference to a vector of Partition objects. + */ + PartitionView(Group* group, const std::vector>& partitions) + : m_group(group), m_partitions(partitions) {} + + /** + * @brief Retrieves a partition by key. + * + * @param key Key to search for. + * @return const Partition* Pointer to the partition if found; nullptr otherwise. + */ + const Partition* getPartition(const KeyType& key) const + { + for (const auto& partition : m_partitions) { + if (partition.key == key) + return &partition; + } + return nullptr; + } + + /** + * @brief Iterates over entities in a specific partition. + * + * @tparam Func Callable type. + * @param key Key of the partition. + * @param func Function to apply to each entity. + */ + template + void each(const KeyType& key, Func func) const + { + const auto* partition = getPartition(key); + if (!partition) + return; + + m_group->eachInRange(partition->startIndex, partition->count, func); + } + + /** + * @brief Gets all partition keys. + * + * @return std::vector Vector of partition keys. + */ + std::vector getPartitionKeys() const + { + std::vector keys; + keys.reserve(m_partitions.size()); + for (const auto& partition : m_partitions) + keys.push_back(partition.key); + return keys; + } + + /** + * @brief Returns the number of partitions. + * + * @return size_t Partition count. + */ + [[nodiscard]] size_t partitionCount() const + { + return m_partitions.size(); + } + + private: + Group* m_group; ///< Pointer to the group. + const std::vector>& m_partitions; ///< Reference to partitions. + }; + + /** + * @brief Returns a partition view based on a component field. + * + * @tparam CompType Component type used to partition. + * @tparam KeyType Key type extracted from the component. + * @param keyExtractor Function to extract the key from the component. + * @return PartitionView View over the partitioned entities. + */ + template + PartitionView getPartitionView(FieldExtractor keyExtractor) + { + std::string typeId = typeid(KeyType).name(); + typeId += "_" + std::string(typeid(CompType).name()); + + EntityKeyExtractor entityKeyExtractor = [this, keyExtractor](Entity e) { + if constexpr (tuple_contains_component_v) { + auto compArray = getOwnedImpl(); + if (!compArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + return keyExtractor(compArray->get(e)); + } else if constexpr (tuple_contains_component_v) { + auto compArray = getNonOwnedImpl(); + if (!compArray) + THROW_EXCEPTION(InternalError, "Component array is null"); + + return keyExtractor(compArray->get(e)); + } else + static_assert(dependent_false::value, "Component type not found in group"); + }; + + return getEntityPartitionView(typeId, entityKeyExtractor); + } + + /** + * @brief Returns a partition view based directly on entity IDs. + * + * @tparam KeyType Key type. + * @param partitionId Identifier for the partition view. + * @param keyExtractor Function to extract the key from an entity. + * @return PartitionView View over the partitioned entities. + */ + template + PartitionView getEntityPartitionView(const std::string& partitionId, + EntityKeyExtractor keyExtractor) + { + // Check if we already have this partition view + auto it = m_partitionStorageMap.find(partitionId); + if (it == m_partitionStorageMap.end()) { + auto storage = std::make_unique>(this, keyExtractor); + auto* storagePtr = storage.get(); + m_partitionStorageMap[partitionId] = std::move(storage); + storagePtr->rebuild(); + + return PartitionView(this, storagePtr->getPartitions()); + } + + // Get the existing storage and cast to the right type + auto* storage = static_cast*>(it->second.get()); + + if (storage->isDirty()) + storage->rebuild(); + + return PartitionView(this, storage->getPartitions()); + } + + /** + * @brief Invalidates all partition caches. + */ + void invalidatePartitions() + { + for (auto& [_, storage] : m_partitionStorageMap) + storage->markDirty(); + } + + private: + + // ======================================= + // Internal structures and methods + // ======================================= + + /** + * @brief Interface for type-erased partition storage. + * + * This allows handling partition storage for different key types uniformly. + */ + struct IPartitionStorage { + virtual ~IPartitionStorage() = default; + /** + * @brief Checks if the partition storage is dirty (needs rebuilding). + * + * @return true If dirty. + * @return false Otherwise. + */ + [[nodiscard]] virtual bool isDirty() const = 0; + /** + * @brief Marks the partition storage as dirty. + */ + virtual void markDirty() = 0; + /** + * @brief Rebuilds the partition storage. + */ + virtual void rebuild() = 0; + }; + + /** + * @brief Concrete partition storage for a specific key type. + * + * @tparam KeyType Type of the partition key. + */ + template + class PartitionStorage final : public IPartitionStorage { + public: + /** + * @brief Constructs PartitionStorage. + * + * @param group Pointer to the group. + * @param keyExtractor Function to extract key from an entity. + */ + PartitionStorage(Group* group, EntityKeyExtractor keyExtractor) + : m_group(group), m_keyExtractor(std::move(keyExtractor)) {} + + [[nodiscard]] bool isDirty() const override { return m_isDirty; } + void markDirty() override { m_isDirty = true; } + + /** + * @brief Rebuilds the partitions. + * + * This collects all entity keys and creates partitions. It then reorders + * the group entities according to the new partition order. + */ + void rebuild() override + { + if (!m_isDirty) + return; + auto drivingArray = std::get<0>(m_group->m_ownedArrays); + const size_t groupSize = drivingArray->groupSize(); + + // Skip if no entities + if (groupSize == 0) { + m_partitions.clear(); + m_isDirty = false; + return; + } + + std::unordered_map> keyToEntities; + + for (size_t i = 0; i < groupSize; i++) { + Entity e = drivingArray->getEntityAtIndex(i); + KeyType key = m_keyExtractor(e); + keyToEntities[key].push_back(e); + } + + m_partitions.clear(); + m_partitions.reserve(keyToEntities.size()); + + std::vector newOrder; + newOrder.reserve(groupSize); + + size_t currentIndex = 0; + for (auto& [key, entities] : keyToEntities) { + Partition partition; + partition.key = key; + partition.startIndex = currentIndex; + partition.count = entities.size(); + m_partitions.push_back(partition); + + // Add these entities to the new order + newOrder.insert(newOrder.end(), entities.begin(), entities.end()); + + currentIndex += entities.size(); + } + + m_group->reorderGroup(newOrder); + m_isDirty = false; + } + + /** + * @brief Gets the current partitions. + * + * @return const std::vector>& Reference to the partitions. + */ + const std::vector>& getPartitions() const + { + return m_partitions; + } + + private: + Group* m_group; ///< Pointer to the group. + EntityKeyExtractor m_keyExtractor; ///< Function to extract a key from an entity. + std::vector> m_partitions; ///< Vector of partitions. + bool m_isDirty = true; ///< Flag indicating if partitions need rebuilding. + }; + + /** + * @brief Reorders the group entities based on a new order. + * + * @param newOrder New order of entities. + */ + void reorderGroup(const std::vector& newOrder) + { + std::apply([&](auto&&... arrays) { + ((reorderArray(arrays, newOrder)), ...); + }, m_ownedArrays); + } + + /** + * @brief Reorders a single component array based on the new entity order. + * + * @tparam ArrayPtr Type of the component array pointer. + * @param array Component array pointer. + * @param newOrder New order of entities. + */ + template + void reorderArray(ArrayPtr array, const std::vector& newOrder) const + { + size_t groupSize = array->groupSize(); + if (newOrder.size() != groupSize) + THROW_EXCEPTION(InternalError, "New order size doesn't match group size"); + + if (groupSize == 0) + return; + + // Create a temporary storage for components + using CompType = typename std::decay_t::component_type; + std::vector tempComponents; + tempComponents.reserve(groupSize); + + for (Entity e : newOrder) + tempComponents.push_back(array->get(e)); //Maybe we should not push back, does it make a copy ? + + for (size_t i = 0; i < groupSize; i++) { + Entity e = newOrder[i]; + array->forceSetComponentAt(i, e, std::move(tempComponents[i])); + } + } + + /** + * @brief Helper to dereference an entity and its components by index. + * + * @param index Index in the group. + * @return auto Tuple containing the entity and its owned component data. + */ + auto dereference(std::size_t index) const + { + Entity entity = std::get<0>(m_ownedArrays)->getEntityAtIndex(index); + + // Use std::forward_as_tuple to preserve references. + auto ownedData = std::apply([entity](auto&&... arrays) { + return std::forward_as_tuple(arrays->get(entity)...); + }, m_ownedArrays); + // We still need the entity by value, so use std::make_tuple for that. + return std::tuple_cat(std::make_tuple(entity), ownedData); + } + + /** + * @brief Helper: Recursively search the non‑owned tuple for the ComponentArray with component_type == T. + * + * @tparam T Component type. + * @tparam I Current index in the tuple. + * @return std::shared_ptr> Pointer to the component array. + */ + template + auto getNonOwnedImpl() const -> std::shared_ptr> + { + if constexpr (I < std::tuple_size_v) { + using CurrentArrayPtr = std::tuple_element_t; + using CurrentComponent = typename std::decay_t())>::component_type; + if constexpr (std::is_same_v) + return std::get(m_nonOwnedArrays); + else + return getNonOwnedImpl(); + } else + static_assert(I < std::tuple_size_v, "Component type not found in group non‑owned arrays"); + return nullptr; + } + + /** + * @brief Helper: Recursively search the owned tuple for the ComponentArray with component_type == T. + * + * @tparam T Component type. + * @tparam I Current index in the tuple. + * @return std::shared_ptr> Pointer to the component array. + */ + template + auto getOwnedImpl() const -> std::shared_ptr> + { + if constexpr (I < std::tuple_size_v) { + using CurrentArrayPtr = std::tuple_element_t; + using CurrentComponent = typename std::decay_t())>::component_type; + if constexpr (std::is_same_v) + return std::get(m_ownedArrays); + else + return getOwnedImpl(); + } else + static_assert(I < std::tuple_size_v, "Component type not found in group owned arrays"); + return nullptr; + } + + /** + * @brief Helper function to call a function with component data. + * + * This function unpacks the owned and non‑owned component arrays. + * + * @tparam Func Callable type. + * @tparam I Indices for the owned tuple. + * @tparam J Indices for the non‑owned tuple. + * @param func Callable to invoke. + * @param e Entity. + * @param index_sequence for owned components. + * @param index_sequence for non‑owned components. + */ + template + void callFunc(Func func, Entity e, std::index_sequence, std::index_sequence) const + { + func(e, (std::get(m_ownedArrays)->get(e))..., (std::get(m_nonOwnedArrays)->get(e))...); + } + + /** + * @brief Defines the direction for sorting operations + */ + enum class SortingOrder { + ASCENDING, + DESCENDING + }; + + // Member variables + OwnedTuple m_ownedArrays; ///< Tuple of pointers to owned component arrays. + NonOwnedTuple m_nonOwnedArrays; ///< Tuple of pointers to non‑owned component arrays. + Signature m_ownedSignature{}; ///< Signature for owned components. + Signature m_allSignature{}; ///< Combined signature for all components. + bool m_sortingInvalidated = true; ///< Flag indicating if sorting is invalidated. + SortingOrder m_sortingOrder = SortingOrder::ASCENDING; + std::unordered_map> m_partitionStorageMap; ///< Map storing partition data by ID. + + }; +} diff --git a/engine/src/ecs/GroupSystem.hpp b/engine/src/ecs/GroupSystem.hpp new file mode 100644 index 000000000..de88e3664 --- /dev/null +++ b/engine/src/ecs/GroupSystem.hpp @@ -0,0 +1,332 @@ +//// GroupSystem.hpp ////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 06/04/2025 +// Description: Header file for the Group system class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "System.hpp" +#include "Access.hpp" +#include "Group.hpp" +#include "ComponentArray.hpp" +#include "Coordinator.hpp" +#include "SingletonComponentMixin.hpp" +#include +#include +#include +#include + +namespace nexo::ecs { + + /** + * @class GroupSystem + * @brief System that uses component groups for optimized access with enforced permissions + * + * @tparam OwnedAccess Owned<> wrapper with component access types + * @tparam NonOwnedAccess NonOwned<> wrapper with component access types + * @tparam SingletonAccessTypes Singleton component access types (ReadSingleton or WriteSingleton) + */ + template, typename... SingletonAccessTypes> + class GroupSystem : public AGroupSystem, + public SingletonComponentMixin< + GroupSystem, SingletonAccessTypes...> { + private: + // Extract component access types + using OwnedAccessTypes = typename OwnedAccess::ComponentTypes; + using NonOwnedAccessTypes = typename NonOwnedAccess::ComponentTypes; + + // Extract raw component types for group creation + template + struct GetComponentTypes; + + template + struct GetComponentTypes> { + using Types = std::tuple; + }; + + using OwnedTypes = typename GetComponentTypes::Types; + using NonOwnedTypes = typename GetComponentTypes::Types; + + // Helper to unpack tuple types to parameter pack + template + auto tupleToTypeList(std::index_sequence) + { + return std::tuple...>{}; + } + + // Function to create a type list from a tuple type + template + auto tupleToTypeList() + { + return tupleToTypeList(std::make_index_sequence>{}); + } + + // Type aliases for the actual group + template + using ComponentArraysTuple = std::tuple>...>; + + // Group type is determined by the owned and non-owned component arrays + template + struct GroupTypeFromTuples; + + template + struct GroupTypeFromTuples, std::tuple> { + using Type = Group, ComponentArraysTuple>; + }; + + // The actual group type for this system + using ActualGroupType = typename GroupTypeFromTuples::Type; + + // Component access trait to find the access type for a component + template + struct ComponentAccessTrait { + static constexpr bool found = false; + static constexpr auto accessType = AccessType::Read; + }; + + template + struct ComponentAccessTrait, + std::void_t || ...)>>> { + static constexpr bool found = true; + + template + static constexpr AccessType GetAccessTypeFromPack() { + auto result = AccessType::Read; + ((std::is_same_v ? result = As::accessType : result), ...); + return result; + } + + static constexpr AccessType accessType = GetAccessTypeFromPack(); + }; + + template + struct GetComponentAccess { + using OwnedTrait = ComponentAccessTrait; + using NonOwnedTrait = ComponentAccessTrait; + + static constexpr bool found = OwnedTrait::found || NonOwnedTrait::found; + static constexpr AccessType accessType = + OwnedTrait::found ? OwnedTrait::accessType : + NonOwnedTrait::found ? NonOwnedTrait::accessType : + AccessType::Read; + }; + + /** + * @brief Access-controlled span wrapper for component arrays + * + * Provides enforced read-only or read-write access to components + * based on the access permissions specified in the system. + * + * @tparam T The component type + */ + template + class ComponentSpan { + private: + std::span m_span; + + public: + /** + * @brief Constructs a ComponentSpan from a raw span + * + * @param span The underlying component data span + */ + explicit ComponentSpan(std::span span) : m_span(span) {} + + /** + * @brief Returns the number of components in the span + * + * @return size_t Number of components + */ + [[nodiscard]] size_t size() const { return m_span.size(); } + + /** + * @brief Access operator with enforced permissions + * + * Returns a mutable reference if Write access is specified, + * otherwise returns a const reference. + * + * @param index Element index to access + * @return Reference to component with appropriate const qualification + */ + template + auto operator[](size_t index) -> std::conditional_t< + GetComponentAccess< + std::remove_const_t>::accessType == AccessType::Write, std::remove_const_t&, + const std::remove_const_t& + > + { + if constexpr (GetComponentAccess>::accessType == AccessType::Write) + return const_cast&>(m_span[index]); + else + return m_span[index]; + } + + /** + * @brief Const access operator + * + * Always returns a const reference regardless of permissions. + * + * @param index Element index to access + * @return Const reference to component + */ + template + auto operator[](size_t index) const -> const std::remove_const_t& + { + return m_span[index]; + } + + /** + * @brief Returns an iterator to the beginning of the span + * @return Iterator to the first element + */ + auto begin() { return m_span.begin(); } + + /** + * @brief Returns an iterator to the end of the span + * @return Iterator one past the last element + */ + auto end() { return m_span.end(); } + + /** + * @brief Returns a const iterator to the beginning of the span + * @return Const iterator to the first element + */ + auto begin() const { return m_span.begin(); } + + /** + * @brief Returns a const iterator to the end of the span + * @return Const iterator one past the last element + */ + auto end() const { return m_span.end(); } + }; + + public: + // Make the base class a friend to access protected members + friend class SingletonComponentMixin< + GroupSystem, + SingletonAccessTypes...>; + + /** + * @brief Constructs a new GroupSystem + * + * Creates the component group and initializes singleton components. + * @throws InternalError if the coordinator is null + */ + GroupSystem() + { + static_assert(std::tuple_size_v > 0, + "GroupSystem must have at least one owned component type"); + + if (!coord) + THROW_EXCEPTION(InternalError, "Coordinator is null in GroupSystem constructor"); + + m_group = createGroupImpl( + tupleToTypeList(), + tupleToTypeList() + ); + + this->initializeSingletonComponents(); + } + + /** + * @brief Get component array with correct access permissions + * + * @tparam T The component type + * @return ComponentSpan with enforced access control + */ + template + auto get() + { + if (!m_group) + THROW_EXCEPTION(InternalError, "Group is null in GroupSystem"); + + constexpr bool isOwned = isOwnedComponent(); + if constexpr (isOwned) { + // Get the span from the group + auto baseSpan = m_group->template get(); + + // Wrap it in our access-controlled span + return ComponentSpan::accessType == AccessType::Read, + const T, + T + >>(baseSpan); + } else { + // For non-owned components, return the component array itself + auto componentArray = m_group->template get(); + + // Apply access control by wrapping in a special pointer wrapper if needed + if constexpr (GetComponentAccess::accessType == AccessType::Read) + return std::shared_ptr>(m_group->template get()); + else + return componentArray; + } + } + + /** + * @brief Check if a component type is owned by this system + * + * @tparam T The component type to check + * @return true if owned, false if non-owned + */ + template + static constexpr bool isOwnedComponent() + { + using OwnedTraitResult = ComponentAccessTrait; + return OwnedTraitResult::found; + } + + /** + * @brief Get all entities in this group + * + * @return Span of entities in the group + */ + [[nodiscard]] std::span getEntities() const + { + if (!m_group) + THROW_EXCEPTION(InternalError, "Group is null in GroupSystem"); + + return m_group->entities(); + } + + protected: + std::shared_ptr m_group = nullptr; + + private: + /** + * @brief Implementation to create a group with the extracted component types + * + * @tparam OT Owned component types + * @tparam NOT Non-owned component types + * @return Shared pointer to the created group + */ + template + std::shared_ptr createGroupImpl(std::tuple, std::tuple) + { + if constexpr (sizeof...(OT) > 0) { + if constexpr (sizeof...(NOT) > 0) { + auto group = coord->registerGroup(nexo::ecs::get()); + if (!group) + THROW_EXCEPTION(InternalError, "Group is null in GroupSystem"); + return std::static_pointer_cast(group); + } else { + auto group = coord->registerGroup(nexo::ecs::get<>()); + if (!group) + THROW_EXCEPTION(InternalError, "Group is null in GroupSystem"); + return std::static_pointer_cast(group); + } + } + return nullptr; + } + }; +} diff --git a/engine/src/ecs/QuerySystem.hpp b/engine/src/ecs/QuerySystem.hpp new file mode 100644 index 000000000..d7bbd3738 --- /dev/null +++ b/engine/src/ecs/QuerySystem.hpp @@ -0,0 +1,221 @@ +//// QuerySystem.hpp ////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 06/04/2025 +// Description: Header file for the query system class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Definitions.hpp" +#include "ECSExceptions.hpp" +#include "System.hpp" +#include "Access.hpp" +#include "ComponentArray.hpp" +#include "Coordinator.hpp" +#include "SingletonComponentMixin.hpp" +#include +#include +#include + +namespace nexo::ecs { + /** + * @brief Metaprogramming helper to extract singleton components from a parameter pack + * + * This template recursively filters a list of components, extracting only the + * singleton components to pass to SingletonComponentMixin. This ensures that + * regular components aren't processed by the mixin. + * + * @tparam Components Component access specifiers to filter for singleton types + */ + template + struct ExtractSingletonComponents; + + /** + * @brief Base case specialization for empty component list + * + * When no components remain to be processed, this provides a void-specialized + * SingletonComponentMixin as the base type. + */ + template<> + struct ExtractSingletonComponents<> { + using type = SingletonComponentMixin; + }; + + /** + * @brief Recursive case for ExtractSingletonComponents + * + * Checks if the current component is a singleton. If it is, includes it in + * the mixin via RebindWithComponent. If not, skips it and continues with the rest. + * + * @tparam Component The current component to check + * @tparam Rest Remaining components to process + */ + template + struct ExtractSingletonComponents { + // If Component is a singleton access type, include it in the mixin + using type = std::conditional_t< + IsSingleton::value, + // Add this component to the mixin along with other singleton components + typename ExtractSingletonComponents::type::template RebindWithComponent, + // Skip this component and continue with the rest + typename ExtractSingletonComponents::type + >; + }; + /** + * @class QuerySystem + * @brief System that directly queries component arrays + * + * @tparam Components Component access specifiers (Read, Write, ReadSingleton, WriteSingleton) + */ + template + class QuerySystem : public AQuerySystem, + public ExtractSingletonComponents::type::template RebindWithDerived> { + private: + /** + * @brief Helper template to check if a component type has Read access in a parameter pack + * + * @tparam T The component type to check for Read access + * @tparam First The first component access type in the parameter pack + * @tparam Rest The remaining component access types + */ + template + struct HasReadAccess { + static constexpr bool value = + (std::is_same_v> || + HasReadAccess::value); + }; + + /** + * @brief Base case for HasReadAccess template recursion + * + * @tparam T The component type to check for Read access + * @tparam First The last component access type in the parameter pack + */ + template + struct HasReadAccess { + static constexpr bool value = std::is_same_v>; + }; + + /** + * @brief Convenience function to check if a component has Read-only access + * + * @tparam T The component type to check + * @return true if the component has Read-only access, false otherwise + */ + template + static constexpr bool hasReadAccess() + { + return HasReadAccess::value; + } + + public: + /** + * @brief Constructs a new QuerySystem + * + * Sets up the system signature based on required components, + * caches component arrays for faster access, and initializes + * singleton components. + * + * @throws InternalError if the coordinator is null + */ + QuerySystem() + { + if (!coord) + THROW_EXCEPTION(InternalError, "Coordinator is null in QuerySystem constructor"); + + // Set system signature based on required components (ignore singleton components) + (setComponentSignatureIfRegular(m_signature), ...); + + // Cache component arrays for faster access (ignore singleton components) + (cacheComponentArrayIfRegular(), ...); + + // Initialize singleton components + this->initializeSingletonComponents(); + } + + /** + * @brief Get a component for an entity with access type determined at compile time + * + * @tparam T The component type + * @param entity The entity to get the component from + * @return Reference to the component with appropriate const-ness + */ + template + std::conditional_t(), const T&, T&> getComponent(Entity entity) + { + const ComponentType typeIndex = getUniqueComponentTypeID(); + const auto it = m_componentArrays.find(typeIndex); + + if (it == m_componentArrays.end()) + THROW_EXCEPTION(InternalError, "Component array not found"); + + auto componentArray = std::static_pointer_cast>(it->second); + + if (!componentArray) + THROW_EXCEPTION(InternalError, "Failed to cast component array"); + + if (!componentArray->hasComponent(entity)) + THROW_EXCEPTION(InternalError, "Entity doesn't have requested component"); + return componentArray->get(entity); + } + + /** + * @brief Gets the component signature for this system + * + * @return const Signature& Reference to the system's component signature + */ + const Signature& getSignature() const override { return m_signature; } + + /** + * @brief Gets a mutable reference to the component signature for this system + * + * @return Signature& Reference to the system's component signature + */ + Signature& getSignature() { return m_signature; } + + protected: + /** + * @brief Caches component arrays for faster access (only for regular components) + * + * @tparam ComponentAccessType The component access type to cache + */ + template + void cacheComponentArrayIfRegular() + { + if constexpr (!IsSingleton::value) { + using T = typename ComponentAccessType::ComponentType; + auto componentArray = coord->getComponentArray(); + m_componentArrays[getUniqueComponentTypeID()] = componentArray; + } + } + + /** + * @brief Sets the component bit in the system signature (only for regular components) + * + * @tparam ComponentAccessType The component access type + * @param signature The signature to modify + */ + template + void setComponentSignatureIfRegular(Signature& signature) + { + if constexpr (!IsSingleton::value) { + using T = typename ComponentAccessType::ComponentType; + signature.set(coord->getComponentType(), true); + } + } + + private: + // Cache of component arrays for faster access + std::unordered_map> m_componentArrays; + + /// Component signature defining required components for this system + Signature m_signature; + }; +} diff --git a/engine/src/ecs/SingletonComponent.hpp b/engine/src/ecs/SingletonComponent.hpp index 2c0ee91a0..c37affc9b 100644 --- a/engine/src/ecs/SingletonComponent.hpp +++ b/engine/src/ecs/SingletonComponent.hpp @@ -15,9 +15,9 @@ #pragma once #include -#include #include +#include "Definitions.hpp" #include "Logger.hpp" #include "ECSExceptions.hpp" @@ -28,11 +28,10 @@ namespace nexo::ecs { * * All singleton components must derive from this interface. It ensures proper polymorphic destruction. */ - class ISingletonComponent { - public: - virtual ~ISingletonComponent() = default; - }; - + class ISingletonComponent { + public: + virtual ~ISingletonComponent() = default; + }; /** * @brief Template class representing a singleton component. @@ -41,9 +40,11 @@ namespace nexo::ecs { * * @tparam T The type of the singleton component. */ - template - class SingletonComponent final : public ISingletonComponent { - public: + template + class SingletonComponent final : public ISingletonComponent { + public: + static_assert(!std::is_copy_constructible_v, + "Singleton component types must have a deleted copy constructor"); /** * @brief Templated constructor that perfectly forwards arguments to construct the instance. * @@ -57,17 +58,31 @@ namespace nexo::ecs { template requires (std::is_constructible_v && (!std::is_same_v, T> && ...)) explicit SingletonComponent(Args&&... args) - : _instance(std::forward(args)...) + : _instance(std::forward(args)...) { } - T &getInstance() { - return _instance; - } - private: - T _instance; - }; + SingletonComponent() = default; + ~SingletonComponent() override = default; + + // Prevent copying + SingletonComponent(const SingletonComponent&) = delete; + SingletonComponent& operator=(const SingletonComponent&) = delete; + // Allow moving + SingletonComponent(SingletonComponent&&) noexcept = default; + SingletonComponent& operator=(SingletonComponent&&) noexcept = default; + + /** + * @brief Gets the singleton component instance + * + * @return T& Reference to the instance of the singleton component + */ + T &getInstance() { return _instance; } + + private: + T _instance; + }; /** * @brief Manager for singleton components in the ECS. @@ -75,9 +90,8 @@ namespace nexo::ecs { * The SingletonComponentManager is responsible for registering, retrieving, and unregistering * singleton components. Singleton components are globally unique and accessed via their type. */ - class SingletonComponentManager { - public: - + class SingletonComponentManager { + public: /** * @brief Registers a singleton component in place by forwarding constructor arguments. * @@ -88,50 +102,73 @@ namespace nexo::ecs { * @param args Arguments to construct an instance of T. */ template - void registerSingletonComponent(Args&&... args) { - using Decayed = std::decay_t; - std::type_index typeName(typeid(Decayed)); - if (m_singletonComponents.contains(typeName)) { - LOG(NEXO_WARN, "ECS::SingletonComponentManager::registerSingletonComponent: trying to register a singleton component more than once"); - return; + void registerSingletonComponent(Args&&... args) + { + ComponentType typeName = getUniqueComponentTypeID(); + if (m_singletonComponents.contains(typeName)) { + LOG(NEXO_WARN, "ECS::SingletonComponentManager::registerSingletonComponent: trying to register a singleton component more than once"); + return; + } + m_singletonComponents.insert( + {typeName, + std::make_shared>(std::forward(args)...)} + ); } - m_singletonComponents.insert({typeName, std::make_shared>(std::forward(args)...)}); + + /** + * @brief Retrieves a singleton component instance. + * + * @tparam T The type of the singleton component. + * @return T& A reference to the registered singleton component. + * @throws SingletonComponentNotRegistered if the component is not registered. + */ + template + T &getSingletonComponent() + { + ComponentType typeName = getUniqueComponentTypeID(); + if (!m_singletonComponents.contains(typeName)) + THROW_EXCEPTION(SingletonComponentNotRegistered); + + auto componentPtr = dynamic_cast*>(m_singletonComponents[typeName].get()); + + return componentPtr->getInstance(); } - /** - * @brief Retrieves a singleton component instance. - * - * @tparam T The type of the singleton component. - * @return T& A reference to the registered singleton component. - * @throws SingletonComponentNotRegistered if the component is not registered. - */ - template - T &getSingletonComponent() { - const std::type_index typeName(typeid(T)); - if (!m_singletonComponents.contains(typeName)) - THROW_EXCEPTION(SingletonComponentNotRegistered); - - auto componentPtr = dynamic_cast*>(m_singletonComponents[typeName].get()); - - return componentPtr->getInstance(); - } - - /** - * @brief Unregisters a singleton component. - * - * Removes the singleton component of type T from the manager. - * - * @tparam T The type of the singleton component. - * @throws SingletonComponentNotRegistered if the component is not registered. - */ - template - void unregisterSingletonComponent() { - const std::type_index typeName(typeid(T)); - if (!m_singletonComponents.contains(typeName)) - THROW_EXCEPTION(SingletonComponentNotRegistered); - m_singletonComponents.erase(typeName); - } - private: - std::unordered_map> m_singletonComponents{}; - }; + /** + * @brief Retrieves a singleton component pointer (internal use only). + * + * @tparam T The type of the singleton component. + * @return std::shared_ptr A shared pointer to the registered singleton component. + * @throws SingletonComponentNotRegistered if the component is not registered. + */ + template + std::shared_ptr getRawSingletonComponent() + { + ComponentType typeName = getUniqueComponentTypeID(); + if (!m_singletonComponents.contains(typeName)) + THROW_EXCEPTION(SingletonComponentNotRegistered); + + return m_singletonComponents[typeName]; + } + + /** + * @brief Unregisters a singleton component. + * + * Removes the singleton component of type T from the manager. + * + * @tparam T The type of the singleton component. + * @throws SingletonComponentNotRegistered if the component is not registered. + */ + template + void unregisterSingletonComponent() + { + ComponentType typeName = getUniqueComponentTypeID(); + if (!m_singletonComponents.contains(typeName)) + THROW_EXCEPTION(SingletonComponentNotRegistered); + + m_singletonComponents.erase(typeName); + } + private: + std::unordered_map> m_singletonComponents{}; + }; } diff --git a/engine/src/ecs/SingletonComponentMixin.hpp b/engine/src/ecs/SingletonComponentMixin.hpp new file mode 100644 index 000000000..b9cf1d628 --- /dev/null +++ b/engine/src/ecs/SingletonComponentMixin.hpp @@ -0,0 +1,192 @@ +//// SingletonComponentMixin.hpp ////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Header file for the singleton component mixin class +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "Access.hpp" +#include "SingletonComponent.hpp" +#include +#include + +namespace nexo::ecs { + + /** + * @brief Mixin class that provides singleton component functionality to systems + * + * @tparam Derived The derived system type + * @tparam SingletonAccessTypes Singleton component access types (ReadSingleton or WriteSingleton) + */ + template + class SingletonComponentMixin { + private: + + /** + * @brief Helper to check if a singleton component has read-only access in the parameter pack + * + * This checks all SingletonAccessTypes to see if any match the given type T with ReadSingleton access. + * + * @tparam T The singleton component type to check + */ + template + struct HasReadSingletonAccessImpl { + static constexpr bool value = (... || (IsReadSingleton::value && + std::is_same_v)); + }; + + public: + /** + * @brief Rebind this mixin with a different derived class + * + * @tparam NewDerived The new derived class to use + */ + template + using RebindWithDerived = SingletonComponentMixin; + + /** + * @brief Rebind this mixin with an additional singleton component + * + * @tparam NewComponent The new singleton component access type to add + */ + template + using RebindWithComponent = SingletonComponentMixin; + + protected: + /** + * @brief Cache of strongly-typed singleton components for faster access + * + * Stores components with their specific types to avoid dynamic casting + */ + template + using SingletonComponentPtr = std::shared_ptr>; + + // Type-specific cache for fast access without dynamic_cast + template + struct TypedComponentCache { + static inline SingletonComponentPtr instance = nullptr; + }; + + /** + * @brief Initializes singleton components for this system + */ + void initializeSingletonComponents() + { + // Cache singleton components for faster access + (cacheSingletonComponent(), ...); + } + + /** + * @brief Caches a specific singleton component + * + * @tparam T The singleton component type + */ + template + void cacheSingletonComponent() + { + auto* derived = static_cast(this); + std::shared_ptr instance = derived->coord->template getRawSingletonComponent(); + + // Store in the type-specific cache + auto typedInstance = std::static_pointer_cast>(instance); + TypedComponentCache::instance = typedInstance; + } + + public: + + /** + * @brief Checks if a singleton component has read-only access + * + * @tparam T The singleton component type to check + * @return true if the component has read-only access, false if it has read-write access + */ + template + static constexpr bool hasReadSingletonAccess() + { + return HasReadSingletonAccessImpl::value; + } + + /** + * @brief Get a singleton component with access type determined at compile time + * + * @tparam T The singleton component type + * @return Reference to the singleton component with appropriate const-ness + * + * @warning MUST be captured with auto& or const auto& to preserve access restrictions! + */ + template + std::conditional_t(), const T&, T&> getSingleton() + { + if (!TypedComponentCache::instance) { + // Late binding in case the singleton was registered after system creation + cacheSingletonComponent(); + } + + auto& typedComponent = TypedComponentCache::instance; + + if (!typedComponent) + THROW_EXCEPTION(SingletonComponentNotRegistered); + + // Return the reference with appropriate constness + if constexpr (hasReadSingletonAccess()) + return const_cast(typedComponent->getInstance()); + else + return typedComponent->getInstance(); + } + }; + + /** + * @brief Base case specialization for SingletonComponentMixin with no components + * + * This specialization serves as the termination point for template recursion + * when filtering singleton components. It provides a minimal implementation + * with placeholder rebinding methods that allow component accumulation during + * the template recursion process. + */ + template<> + class SingletonComponentMixin { + public: + /** + * @brief Rebind this base mixin with a derived class type + * + * This allows transitioning from the void placeholder to a concrete + * derived type when no singleton components were found. + * + * @tparam NewDerived The derived class type to bind to + */ + template + using RebindWithDerived = SingletonComponentMixin; + + /** + * @brief Begin building a mixin with a singleton component + * + * This is called when the first singleton component is found during + * template recursion, transitioning from the void base case to a + * mixin with actual components. + * + * @tparam NewComponent The first singleton component access type + */ + template + using RebindWithComponent = SingletonComponentMixin; + + protected: + /** + * @brief No-op implementation of singleton component initialization + * + * Since this specialization represents a mixin with no singleton components, + * there's nothing to initialize. + */ + void initializeSingletonComponents() + { + // No-op method + } + }; +} diff --git a/engine/src/ecs/System.cpp b/engine/src/ecs/System.cpp index 806aa2fb3..7f9b4344c 100644 --- a/engine/src/ecs/System.cpp +++ b/engine/src/ecs/System.cpp @@ -14,26 +14,64 @@ #include "System.hpp" +#include + namespace nexo::ecs { - void SystemManager::entityDestroyed(const Entity entity) const + void SparseSet::insert(Entity entity) + { + if (contains(entity)) + { + LOG(NEXO_WARN, "Entity {} already added to the sparse set", entity); + return; + } + + sparse[entity] = dense.size(); + dense.push_back(entity); + } + + void SparseSet::erase(Entity entity) { - for (const auto &[fst, snd] : m_systems) { - auto const &system = snd; - system->entities.erase(entity); + if (!contains(entity)) + { + LOG(NEXO_WARN, "Entity {} does not exist in the sparse set", entity); + return; } + + const size_t index = sparse[entity]; + const size_t lastIndex = dense.size() - 1; + const Entity lastEntity = dense[lastIndex]; + + dense[index] = lastEntity; + sparse[lastEntity] = index; + dense.pop_back(); + sparse.erase(entity); } - void SystemManager::entitySignatureChanged(const Entity entity, const Signature entitySignature) + void SystemManager::entityDestroyed(const Entity entity, const Signature signature) const { - for (const auto &[fst, snd] : m_systems) { - auto const &type = fst; - auto const &system = snd; + for (const auto& system : std::ranges::views::values(m_querySystems)) { + if (const Signature &systemSignature = system->getSignature(); (signature & systemSignature) == systemSignature) + system->entities.erase(entity); + } + } - if (auto const &systemSignature = m_signatures[type]; (entitySignature & systemSignature) == systemSignature) + void SystemManager::entitySignatureChanged(const Entity entity, + const Signature oldSignature, + const Signature newSignature) + { + for (const auto& system : std::ranges::views::values(m_querySystems)) { + const Signature systemSignature = system->getSignature(); + // Check if entity qualifies now but did not qualify before. + if (((oldSignature & systemSignature) != systemSignature) && + ((newSignature & systemSignature) == systemSignature)) { system->entities.insert(entity); - else + } + // Otherwise, if the entity no longer qualifies but did before, remove it. + else if (((oldSignature & systemSignature) == systemSignature) && + ((newSignature & systemSignature) != systemSignature)) { system->entities.erase(entity); + } } } -} \ No newline at end of file +} diff --git a/engine/src/ecs/System.hpp b/engine/src/ecs/System.hpp index 2fb0ccbee..b52914ed0 100644 --- a/engine/src/ecs/System.hpp +++ b/engine/src/ecs/System.hpp @@ -14,18 +14,100 @@ #pragma once -#include #include #include #include -#include "Signature.hpp" +#include "Definitions.hpp" +#include "Logger.hpp" +#include "ECSExceptions.hpp" namespace nexo::ecs { class Coordinator; } namespace nexo::ecs { + + /** + * @class SparseSet + * + * @brief A sparse set implementation for efficient entity storage and lookup + * + * This class provides O(1) insertion, removal, and lookup operations for entities. + * It uses a sparse-dense pattern where entities are stored contiguously in a dense array, + * while maintaining a sparse lookup map to quickly find entity positions. + */ + class SparseSet { + public: + /** + * @brief Insert an entity into the set + * + * @param entity The entity to insert + */ + void insert(Entity entity); + + /** + * @brief Remove an entity from the set + * + * @param entity The entity to remove + */ + void erase(Entity entity); + + /** + * @brief Check if the set is empty + * + * @return true if the set contains no entities, false otherwise + */ + bool empty() const { return dense.empty(); } + + /** + * @brief Check if an entity exists in the set + * + * @param entity The entity to check + * @return true if the entity exists in the set, false otherwise + */ + bool contains(const Entity entity) const { return sparse.contains(entity); } + + /** + * @brief Get the number of entities in the set + * + * @return The number of entities + */ + size_t size() const { return dense.size(); } + + /** + * @brief Get the dense array of entities + * + * @return Const reference to the vector of entities + */ + const std::vector& getDense() const { return dense; } + + /** + * @brief Get an iterator to the beginning of the entity collection + * + * @return Iterator to the first entity + */ + auto begin() const { return dense.begin(); } + + /** + * @brief Get an iterator to the end of the entity collection + * + * @return Iterator to the position after the last entity + */ + auto end() const { return dense.end(); } + + private: + /** + * @brief Dense array of entities in insertion order + */ + std::vector dense; + + /** + * @brief Sparse lookup map from entity ID to position in dense array + */ + std::unordered_map sparse; + }; + /** * @class System * @@ -36,10 +118,38 @@ namespace nexo::ecs { */ class System { public: - std::set entities; + virtual ~System() = default; + /** + * @brief Global coordinator instance shared by all systems + */ static std::shared_ptr coord; }; + /** + * @class AQuerySystem + * @brief Base abstract for all query-based systems + */ + class AQuerySystem : public System { + public: + ~AQuerySystem() override = default; + virtual const Signature &getSignature() const = 0; + + /** + * @brief Entities that match this system's signature + */ + SparseSet entities; + + }; + + /** + * @class AGroupSystem + * @brief Abstract base class for all group-based systems + */ + class AGroupSystem : public System { + public: + ~AGroupSystem() override = default; + }; + /** * @class SystemManager * @@ -56,18 +166,45 @@ namespace nexo::ecs { * @tparam T - The type of the system to be registered. * @return std::shared_ptr - Shared pointer to the newly registered system. */ - template - std::shared_ptr registerSystem() { + template + std::shared_ptr registerQuerySystem(Args&&... args) + { + static_assert(std::is_base_of_v, "T must derive from AQuerySystem"); + std::type_index typeName(typeid(T)); + + if (m_querySystems.contains(typeName)) + { + LOG(NEXO_WARN, "ECS::SystemManager::registerSystem: trying to register a system more than once"); + return nullptr; + } + + auto system = std::make_shared(std::forward(args)...); + m_querySystems.insert({typeName, system}); + return system; + } + + /** + * @brief Registers a new group-based system of type T in the ECS framework + * + * @tparam T The type of the system to be registered (must derive from AGroupSystem) + * @tparam Args Constructor argument types for the system + * @param args Constructor arguments for the system + * @return std::shared_ptr Shared pointer to the newly registered system + */ + template + std::shared_ptr registerGroupSystem(Args&&... args) + { + static_assert(std::is_base_of_v, "T must derive from AGroupSystem"); std::type_index typeName(typeid(T)); - if (m_systems.contains(typeName)) + if (m_groupSystems.contains(typeName)) { LOG(NEXO_WARN, "ECS::SystemManager::registerSystem: trying to register a system more than once"); return nullptr; } - auto system = std::make_shared(); - m_systems.insert({typeName, system}); + auto system = std::make_shared(std::forward(args)...); + m_groupSystems.insert({typeName, system}); return system; } @@ -79,12 +216,10 @@ namespace nexo::ecs { * @param signature - The signature to associate with the system. */ template - void setSignature(Signature signature) { + void setSignature(Signature signature) + { std::type_index typeName(typeid(T)); - if (!m_systems.contains(typeName)) - THROW_EXCEPTION(SystemNotRegistered); - m_signatures.insert({typeName, signature}); } @@ -92,19 +227,33 @@ namespace nexo::ecs { * @brief Handles the destruction of an entity by removing it from all systems. * * @param entity - The ID of the destroyed entity. + * @param signature - The signature of the entity. */ - void entityDestroyed(Entity entity) const; + void entityDestroyed(Entity entity, Signature signature) const; /** * @brief Updates the systems with an entity when its signature changes. * * This ensures that systems process only relevant entities based on their current components. * @param entity - The ID of the entity whose signature has changed. - * @param entitySignature - The new signature of the entity. + * @param oldSignature - The old signature of the entity. + * @param newSignature - The new signature of the entity. */ - void entitySignatureChanged(Entity entity, Signature entitySignature); + void entitySignatureChanged(Entity entity, Signature oldSignature, Signature newSignature); private: - std::unordered_map m_signatures{}; - std::unordered_map> m_systems{}; + /** + * @brief Map of system type to component signature + */ + std::unordered_map m_signatures{}; + + /** + * @brief Map of query system type to system instance + */ + std::unordered_map> m_querySystems{}; + + /** + * @brief Map of group system type to system instance + */ + std::unordered_map> m_groupSystems{}; }; } diff --git a/engine/src/ecs/TypeErasedComponent/ComponentDescription.hpp b/engine/src/ecs/TypeErasedComponent/ComponentDescription.hpp new file mode 100644 index 000000000..9d5ce75cb --- /dev/null +++ b/engine/src/ecs/TypeErasedComponent/ComponentDescription.hpp @@ -0,0 +1,31 @@ +//// ComponentDescription.hpp ///////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the field struct used in UI scripting, +// which represents a field in the UI with its properties +// this struct is passed by the C# code to the native code +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "Field.hpp" + +namespace nexo::ecs { + + struct ComponentDescription { + std::string name; // Name of the component + std::vector fields; // List of fields in the component + }; + +} // namespace nexo::ecs diff --git a/engine/src/ecs/TypeErasedComponent/Field.hpp b/engine/src/ecs/TypeErasedComponent/Field.hpp new file mode 100644 index 000000000..a0d7ae9ec --- /dev/null +++ b/engine/src/ecs/TypeErasedComponent/Field.hpp @@ -0,0 +1,33 @@ +//// Field.hpp //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the field struct used in UI scripting, +// which represents a field in the UI with its properties +// this struct is passed by the C# code to the native code +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "FieldType.hpp" + +namespace nexo::ecs { + + struct Field { + std::string name; // Pointer to the name of the field + FieldType type; // Type of the field (e.g., Int, Float, String, etc.) + uint64_t size; // Size of the field in bytes + uint64_t offset; // Offset of the field in the component + }; + +} // namespace nexo::ecs diff --git a/engine/src/ecs/TypeErasedComponent/FieldType.hpp b/engine/src/ecs/TypeErasedComponent/FieldType.hpp new file mode 100644 index 000000000..9a3c0d95a --- /dev/null +++ b/engine/src/ecs/TypeErasedComponent/FieldType.hpp @@ -0,0 +1,47 @@ +//// FieldType.hpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the field type enumeration +// used in UI components +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace nexo::ecs { + + enum class FieldType : uint64_t { + // Special type, if blank, the field is not used + Blank, + Section, // Used to create a section with title in the UI + + // Primitive types + Bool, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float, + Double, + + // Widgets + Vector3, + Vector4, + + _Count // Count of the number of field types, used for validation + }; + +} // namespace nexo::scripting diff --git a/engine/src/renderer/Buffer.cpp b/engine/src/renderer/Buffer.cpp index 62fa1752e..f71b1cc99 100644 --- a/engine/src/renderer/Buffer.cpp +++ b/engine/src/renderer/Buffer.cpp @@ -13,34 +13,34 @@ /////////////////////////////////////////////////////////////////////////////// #include "Buffer.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlBuffer.hpp" #endif namespace nexo::renderer { - std::shared_ptr createVertexBuffer(float *vertices, unsigned int size) + std::shared_ptr createVertexBuffer(float *vertices, unsigned int size) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(vertices, size); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(vertices, size); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::shared_ptr createVertexBuffer(unsigned int size) + std::shared_ptr createVertexBuffer(unsigned int size) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(size); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(size); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::shared_ptr createIndexBuffer() + std::shared_ptr createIndexBuffer() { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/Buffer.hpp b/engine/src/renderer/Buffer.hpp index df6025a5d..05bfd85a2 100644 --- a/engine/src/renderer/Buffer.hpp +++ b/engine/src/renderer/Buffer.hpp @@ -22,10 +22,10 @@ namespace nexo::renderer { /** - * @enum ShaderDataType + * @enum NxShaderDataType * @brief Enum representing the various data types supported in shaders. * - * ShaderDataType is used to define the type of data stored in a buffer element, + * NxShaderDataType is used to define the type of data stored in a buffer element, * such as float, integer, matrix, or boolean types. This enum ensures consistent * representation and handling of data types in vertex and index buffers. * @@ -36,7 +36,7 @@ namespace nexo::renderer { * - INT, INT2, INT3, INT4: Represents one or more integer values. * - BOOL: Represents a boolean value. */ - enum class ShaderDataType { + enum class NxShaderDataType { NONE = 0, FLOAT, FLOAT2, @@ -52,40 +52,40 @@ namespace nexo::renderer { }; /** - * @brief Returns the size (in bytes) of a given ShaderDataType. + * @brief Returns the size (in bytes) of a given NxShaderDataType. * - * This function calculates the memory size required for a specific ShaderDataType. + * This function calculates the memory size required for a specific NxShaderDataType. * It supports floats, integers, matrices, and boolean types. * - * @param type The ShaderDataType whose size is to be calculated. - * @return The size in bytes of the provided ShaderDataType. + * @param type The NxShaderDataType whose size is to be calculated. + * @return The size in bytes of the provided NxShaderDataType. * * Example: * - FLOAT3 will return 12 (3 floats, 4 bytes each). * - MAT4 will return 64 (4x4 matrix of floats). */ - static unsigned int shaderDataTypeSize(ShaderDataType type) + static unsigned int shaderDataTypeSize(NxShaderDataType type) { switch (type) { - case ShaderDataType::FLOAT: return 4; // 1 float (4 bytes) - case ShaderDataType::FLOAT2: return 4 * 2; // 2 floats (8 bytes) - case ShaderDataType::FLOAT3: return 4 * 3; // 3 floats (12 bytes) - case ShaderDataType::FLOAT4: return 4 * 4; // 4 floats (16 bytes) - case ShaderDataType::MAT3: return 4 * 3 * 3; // 3x3 matrix (36 bytes) - case ShaderDataType::MAT4: return 4 * 4 * 4; // 4x4 matrix (64 bytes) - case ShaderDataType::INT: return 4; // 1 int (4 bytes) - case ShaderDataType::INT2: return 4 * 2; // 2 ints (8 bytes) - case ShaderDataType::INT3: return 4 * 3; // 3 ints (12 bytes) - case ShaderDataType::INT4: return 4 * 4; // 4 ints (16 bytes) - case ShaderDataType::BOOL: return 1; // 1 byte (1 bool) - case ShaderDataType::NONE: return 0; // No type, return 0 + case NxShaderDataType::FLOAT: return 4; // 1 float (4 bytes) + case NxShaderDataType::FLOAT2: return 4 * 2; // 2 floats (8 bytes) + case NxShaderDataType::FLOAT3: return 4 * 3; // 3 floats (12 bytes) + case NxShaderDataType::FLOAT4: return 4 * 4; // 4 floats (16 bytes) + case NxShaderDataType::MAT3: return 4 * 3 * 3; // 3x3 matrix (36 bytes) + case NxShaderDataType::MAT4: return 4 * 4 * 4; // 4x4 matrix (64 bytes) + case NxShaderDataType::INT: return 4; // 1 int (4 bytes) + case NxShaderDataType::INT2: return 4 * 2; // 2 ints (8 bytes) + case NxShaderDataType::INT3: return 4 * 3; // 3 ints (12 bytes) + case NxShaderDataType::INT4: return 4 * 4; // 4 ints (16 bytes) + case NxShaderDataType::BOOL: return 1; // 1 byte (1 bool) + case NxShaderDataType::NONE: return 0; // No type, return 0 } return 0; // Default case for undefined types } /** - * @struct BufferElements + * @struct NxBufferElements * @brief Represents an individual element in a buffer layout. * * Each buffer element specifies its name, type, size, and offset within a buffer. @@ -101,15 +101,15 @@ namespace nexo::renderer { * Functions: * - @return getComponentCount() Retrieves the number of components (e.g., FLOAT3 = 3). */ - struct BufferElements { + struct NxBufferElements { std::string name; - ShaderDataType type{}; + NxShaderDataType type{}; unsigned int size{}; unsigned int offset{}; bool normalized{}; - BufferElements() = default; - BufferElements(const ShaderDataType Type, std::string name, const bool normalized = false) + NxBufferElements() = default; + NxBufferElements(const NxShaderDataType Type, std::string name, const bool normalized = false) : name(std::move(name)), type(Type), size(shaderDataTypeSize(type)), offset(0) , normalized(normalized) { @@ -119,27 +119,27 @@ namespace nexo::renderer { { switch(type) { - case ShaderDataType::FLOAT: return 1; - case ShaderDataType::FLOAT2: return 2; - case ShaderDataType::FLOAT3: return 3; - case ShaderDataType::FLOAT4: return 4; - case ShaderDataType::INT: return 1; - case ShaderDataType::INT2: return 2; - case ShaderDataType::INT3: return 3; - case ShaderDataType::INT4: return 4; - case ShaderDataType::MAT3: return 3 * 3; - case ShaderDataType::MAT4: return 4 * 4; - case ShaderDataType::BOOL: return 1; + case NxShaderDataType::FLOAT: return 1; + case NxShaderDataType::FLOAT2: return 2; + case NxShaderDataType::FLOAT3: return 3; + case NxShaderDataType::FLOAT4: return 4; + case NxShaderDataType::INT: return 1; + case NxShaderDataType::INT2: return 2; + case NxShaderDataType::INT3: return 3; + case NxShaderDataType::INT4: return 4; + case NxShaderDataType::MAT3: return 3 * 3; + case NxShaderDataType::MAT4: return 4 * 4; + case NxShaderDataType::BOOL: return 1; default: return -1; } } }; /** - * @class BufferLayout + * @class NxBufferLayout * @brief Defines the structure and layout of elements in a vertex buffer. * - * A BufferLayout is a collection of BufferElements, each specifying a data type, + * A NxBufferLayout is a collection of BufferElements, each specifying a data type, * size, and offset. The layout is essential for ensuring correct binding and * rendering of vertex attributes in a graphics pipeline. * @@ -148,7 +148,7 @@ namespace nexo::renderer { * - @param _stride The total size (in bytes) of one vertex in the layout. * * Functions: - * - @constructor BufferLayout(const std::initializer_list elements) + * - @constructor NxBufferLayout(const std::initializer_list elements) * Initializes the layout with a list of buffer elements and calculates offsets/stride. * * - @return getElements() Retrieves the list of BufferElements. @@ -158,24 +158,24 @@ namespace nexo::renderer { * Iterators: * - Supports begin() and end() iterators for easy iteration over elements. */ - class BufferLayout { + class NxBufferLayout { public: - BufferLayout() = default; - BufferLayout(const std::initializer_list elements) + NxBufferLayout() = default; + NxBufferLayout(const std::initializer_list elements) : _elements(elements) { calculateOffsetAndStride(); }; - [[nodiscard]] std::vector getElements() const { return _elements; }; + [[nodiscard]] std::vector getElements() const { return _elements; }; [[nodiscard]] unsigned int getStride() const { return _stride; }; - std::vector::iterator begin() { return _elements.begin(); }; - std::vector::iterator end() { return _elements.end(); }; - [[nodiscard]] std::vector::const_iterator begin() const { return _elements.begin(); }; - [[nodiscard]] std::vector::const_iterator end() const { return _elements.end(); }; + std::vector::iterator begin() { return _elements.begin(); }; + std::vector::iterator end() { return _elements.end(); }; + [[nodiscard]] std::vector::const_iterator begin() const { return _elements.begin(); }; + [[nodiscard]] std::vector::const_iterator end() const { return _elements.end(); }; private: - std::vector _elements; + std::vector _elements; unsigned int _stride{}; void calculateOffsetAndStride() @@ -192,7 +192,7 @@ namespace nexo::renderer { }; /** - * @class VertexBuffer + * @class NxVertexBuffer * @brief Abstract class representing a vertex buffer in the graphics pipeline. * * A vertex buffer is a GPU memory buffer that stores per-vertex attributes, such as @@ -200,7 +200,7 @@ namespace nexo::renderer { * the interface for creating, binding, and managing vertex buffers, allowing for * implementation across various graphics APIs. */ - class VertexBuffer { + class NxVertexBuffer { public: /** * @brief Destroys the vertex buffer. @@ -211,7 +211,7 @@ namespace nexo::renderer { * Usage: * - Automatically called when a VertexBuffer object goes out of scope. */ - virtual ~VertexBuffer() = default; + virtual ~NxVertexBuffer() = default; /** * @brief Binds the vertex buffer as the active buffer in the graphics pipeline. @@ -220,7 +220,7 @@ namespace nexo::renderer { * the data stored in this buffer. * * Pure Virtual Function: - * - Must be implemented by platform-specific subclasses (e.g., OpenGLVertexBuffer). + * - Must be implemented by platform-specific subclasses (e.g., NxOpenGlVertexBuffer). */ virtual void bind() const = 0; @@ -231,7 +231,7 @@ namespace nexo::renderer { * on the buffer. This is optional but useful for debugging or ensuring clean state management. * * Pure Virtual Function: - * - Must be implemented by platform-specific subclasses (e.g., OpenGLVertexBuffer). + * - Must be implemented by platform-specific subclasses (e.g., NxOpenGlVertexBuffer). */ virtual void unbind() const = 0; @@ -241,12 +241,12 @@ namespace nexo::renderer { * The layout defines the structure of the data stored in the buffer (e.g., positions, * normals, colors) and how they are passed to the vertex shader. * - * @param layout The BufferLayout object defining the structure of the buffer data. + * @param layout The NxBufferLayout object defining the structure of the buffer data. * * Pure Virtual Function: * - Must be implemented by platform-specific subclasses. */ - virtual void setLayout(const BufferLayout &layout) = 0; + virtual void setLayout(const NxBufferLayout &layout) = 0; /** * @brief Retrieves the layout of the vertex buffer. @@ -254,12 +254,12 @@ namespace nexo::renderer { * Provides information about the data structure stored in the buffer, including * element types, sizes, and offsets. * - * @return The BufferLayout object associated with this vertex buffer. + * @return The NxBufferLayout object associated with this vertex buffer. * * Pure Virtual Function: * - Must be implemented by platform-specific subclasses. */ - [[nodiscard]] virtual BufferLayout getLayout() const = 0; + [[nodiscard]] virtual NxBufferLayout getLayout() const = 0; /** * @brief Uploads new data to the vertex buffer. @@ -279,14 +279,14 @@ namespace nexo::renderer { }; /** - * @class IndexBuffer + * @class NxIndexBuffer * @brief Abstract class representing an index buffer in the graphics pipeline. * * An index buffer stores indices into a vertex buffer, allowing for efficient reuse * of vertex data. This class provides an abstract interface for creating, binding, * and managing index buffers, enabling compatibility with multiple graphics APIs. */ - class IndexBuffer { + class NxIndexBuffer { public: /** * @brief Destroys the index buffer. @@ -297,7 +297,7 @@ namespace nexo::renderer { * Usage: * - Automatically called when an IndexBuffer object goes out of scope. */ - virtual ~IndexBuffer() = default; + virtual ~NxIndexBuffer() = default; /** * @brief Binds the index buffer as the active buffer in the graphics pipeline. @@ -348,7 +348,7 @@ namespace nexo::renderer { */ [[nodiscard]] virtual unsigned int getCount() const = 0; - virtual unsigned int getId() const = 0; + [[nodiscard]] virtual unsigned int getId() const = 0; }; /** @@ -363,9 +363,9 @@ namespace nexo::renderer { * @return A shared pointer to the created VertexBuffer instance. * * Throws: - * - UnknownGraphicsApi exception if no graphics API is defined. + * - NxUnknownGraphicsApi exception if no graphics API is defined. */ - std::shared_ptr createVertexBuffer(float *vertices, unsigned int size); + std::shared_ptr createVertexBuffer(float *vertices, unsigned int size); /** * @function createVertexBuffer(unsigned int size) @@ -377,9 +377,9 @@ namespace nexo::renderer { * @return A shared pointer to the created VertexBuffer instance. * * Throws: - * - UnknownGraphicsApi exception if no graphics API is defined. + * - NxUnknownGraphicsApi exception if no graphics API is defined. */ - std::shared_ptr createVertexBuffer(unsigned int size); + std::shared_ptr createVertexBuffer(unsigned int size); /** * @function createIndexBuffer() @@ -391,9 +391,9 @@ namespace nexo::renderer { * @return A shared pointer to the created IndexBuffer instance. * * Throws: - * - UnknownGraphicsApi exception if no graphics API is defined. + * - NxUnknownGraphicsApi exception if no graphics API is defined. */ - std::shared_ptr createIndexBuffer(); + std::shared_ptr createIndexBuffer(); } diff --git a/engine/src/renderer/Framebuffer.cpp b/engine/src/renderer/Framebuffer.cpp index 6024c3c1d..32072173e 100644 --- a/engine/src/renderer/Framebuffer.cpp +++ b/engine/src/renderer/Framebuffer.cpp @@ -13,19 +13,18 @@ /////////////////////////////////////////////////////////////////////////////// #include "Framebuffer.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlFramebuffer.hpp" #endif namespace nexo::renderer { - std::shared_ptr Framebuffer::create(const FramebufferSpecs &specs) + std::shared_ptr NxFramebuffer::create(const NxFramebufferSpecs &specs) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(specs); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(specs); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); - + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/Framebuffer.hpp b/engine/src/renderer/Framebuffer.hpp index 22b0e576a..651d772db 100644 --- a/engine/src/renderer/Framebuffer.hpp +++ b/engine/src/renderer/Framebuffer.hpp @@ -20,7 +20,7 @@ namespace nexo::renderer { /** - * @enum FrameBufferTextureFormats + * @enum NxFrameBufferTextureFormats * @brief Enum representing the various texture formats supported for framebuffer attachments. * * Texture formats define how data is stored in framebuffer attachments. These include color, @@ -34,7 +34,7 @@ namespace nexo::renderer { * - Depth: Alias for DEPTH24STENCIL8. * - NB_TEXTURE_FORMATS: Tracks the number of texture formats (for internal use). */ - enum class FrameBufferTextureFormats { + enum class NxFrameBufferTextureFormats { NONE = 0, RGBA8, @@ -49,7 +49,7 @@ namespace nexo::renderer { }; /** - * @struct FrameBufferTextureSpecifications + * @struct NxFrameBufferTextureSpecifications * @brief Defines the format for a single framebuffer texture attachment. * * This struct specifies the properties of a single texture attachment, such as its format. @@ -59,17 +59,17 @@ namespace nexo::renderer { * * Constructors: * - FrameBufferTextureSpecifications(): Default constructor with no format. - * - FrameBufferTextureSpecifications(const FrameBufferTextureFormats format): Initializes with a specified texture format. + * - FrameBufferTextureSpecifications(const NxFrameBufferTextureFormats format): Initializes with a specified texture format. */ - struct FrameBufferTextureSpecifications { - FrameBufferTextureSpecifications() = default; - FrameBufferTextureSpecifications(const FrameBufferTextureFormats format) : textureFormat(format) {}; + struct NxFrameBufferTextureSpecifications { + NxFrameBufferTextureSpecifications() = default; + NxFrameBufferTextureSpecifications(const NxFrameBufferTextureFormats format) : textureFormat(format) {}; - FrameBufferTextureFormats textureFormat = FrameBufferTextureFormats::NONE; + NxFrameBufferTextureFormats textureFormat = NxFrameBufferTextureFormats::NONE; }; /** - * @struct FrameBufferAttachmentsSpecifications + * @struct NxFrameBufferAttachmentsSpecifications * @brief Defines the list of texture attachments for a framebuffer. * * A framebuffer can have multiple attachments (e.g., color and depth textures). This struct @@ -83,18 +83,18 @@ namespace nexo::renderer { * - FrameBufferAttachmentsSpecifications(std::initializer_list attachments): * Initializes the list with a set of predefined texture specifications. */ - struct FrameBufferAttachmentsSpecifications { - FrameBufferAttachmentsSpecifications() = default; - FrameBufferAttachmentsSpecifications(std::initializer_list attachments) : attachments(attachments) {}; + struct NxFrameBufferAttachmentsSpecifications { + NxFrameBufferAttachmentsSpecifications() = default; + NxFrameBufferAttachmentsSpecifications(std::initializer_list attachments) : attachments(attachments) {}; - std::vector attachments; + std::vector attachments; }; /** - * @struct FramebufferSpecs + * @struct NxFramebufferSpecs * @brief Represents the specifications for creating a framebuffer. * - * FramebufferSpecs encapsulates all the necessary properties for initializing a framebuffer, + * NxFramebufferSpecs encapsulates all the necessary properties for initializing a framebuffer, * including dimensions, attachments, sampling, and swap chain behavior. * * Members: @@ -104,10 +104,10 @@ namespace nexo::renderer { * - @param samples The number of samples for multisampling (default is 1). * - @param swapChainTarget Indicates whether the framebuffer is part of the swap chain (default is false). */ - struct FramebufferSpecs { + struct NxFramebufferSpecs { unsigned int width{}; unsigned int height{}; - FrameBufferAttachmentsSpecifications attachments; + NxFrameBufferAttachmentsSpecifications attachments; unsigned int samples = 1; @@ -115,12 +115,12 @@ namespace nexo::renderer { }; /** - * @class Framebuffer + * @class NxFramebuffer * @brief Abstract class representing a framebuffer in the rendering pipeline. * * A framebuffer is an off-screen rendering target that stores the results of * rendering operations. It can have multiple texture attachments, such as color, - * depth, and stencil buffers. The `Framebuffer` class provides an abstraction layer + * depth, and stencil buffers. The `NxFramebuffer` class provides an abstraction layer * for creating and managing framebuffers across different graphics APIs (e.g., OpenGL, Vulkan). * * Key Features: @@ -131,8 +131,8 @@ namespace nexo::renderer { * - Resizable: The framebuffer can be resized dynamically to match the viewport dimensions. * * Usage: - * - The `Framebuffer` class is an abstract base class. Platform or API-specific - * implementations (e.g., OpenGLFramebuffer) must inherit and implement the + * - The `NxFramebuffer` class is an abstract base class. Platform or API-specific + * implementations (e.g., NxOpenGLFramebuffer) must inherit and implement the * pure virtual methods. * * Responsibilities: @@ -146,7 +146,7 @@ namespace nexo::renderer { * 3. Access texture attachments (e.g., for post-processing) using `getColorAttachmentId`. * 4. Unbind the framebuffer to render to the default framebuffer (screen). */ - class Framebuffer { + class NxFramebuffer { public: /** * @brief Destroys the framebuffer and releases associated resources. @@ -154,7 +154,7 @@ namespace nexo::renderer { * This virtual destructor ensures that derived classes properly clean up * framebuffer resources (e.g., OpenGL framebuffers). */ - virtual ~Framebuffer() = default; + virtual ~NxFramebuffer() = default; /** * @brief Binds the framebuffer as the active rendering target. @@ -194,6 +194,8 @@ namespace nexo::renderer { */ virtual void resize(unsigned int width, unsigned int height) = 0; + [[nodiscard]] virtual glm::vec2 getSize() const = 0; + virtual void getPixelWrapper(unsigned int attachementIndex, int x, int y, void *result, const std::type_info &ti) const = 0; @@ -240,18 +242,18 @@ namespace nexo::renderer { * This method provides access to the framebuffer's specifications, including * dimensions, attachments, and sampling options. * - * @return A reference to the FramebufferSpecs struct. + * @return A reference to the NxFramebufferSpecs struct. */ - [[nodiscard]] virtual FramebufferSpecs &getSpecs() = 0; + [[nodiscard]] virtual NxFramebufferSpecs &getSpecs() = 0; /** * @brief Retrieves the specifications of the framebuffer (const version). * * This method provides read-only access to the framebuffer's specifications. * - * @return A constant reference to the FramebufferSpecs struct. + * @return A constant reference to the NxFramebufferSpecs struct. */ - [[nodiscard]] virtual const FramebufferSpecs &getSpecs() const = 0; + [[nodiscard]] virtual const NxFramebufferSpecs &getSpecs() const = 0; /** * @brief Retrieves the OpenGL ID of a specific color attachment. * @@ -267,7 +269,7 @@ namespace nexo::renderer { */ [[nodiscard]] virtual unsigned int getColorAttachmentId(unsigned int index = 0) const = 0; - virtual unsigned int getDepthAttachmentId() const = 0; + [[nodiscard]] virtual unsigned int getDepthAttachmentId() const = 0; /** * @brief Creates a framebuffer based on the provided specifications. @@ -277,7 +279,7 @@ namespace nexo::renderer { * * @param specs The specifications for creating the framebuffer, including dimensions, * attachments, and sampling options. - * @return A shared pointer to the created Framebuffer instance. + * @return A shared pointer to the created NxFramebuffer instance. * * Throws: * - Implementation-specific exceptions if framebuffer creation fails. @@ -285,7 +287,7 @@ namespace nexo::renderer { * Notes: * - This function is typically implemented in a platform-specific or API-specific source file. */ - static std::shared_ptr create(const FramebufferSpecs& specs); + static std::shared_ptr create(const NxFramebufferSpecs& specs); }; diff --git a/engine/src/renderer/RenderCommand.cpp b/engine/src/renderer/RenderCommand.cpp index 3199a6263..6f494617f 100644 --- a/engine/src/renderer/RenderCommand.cpp +++ b/engine/src/renderer/RenderCommand.cpp @@ -13,20 +13,20 @@ /////////////////////////////////////////////////////////////////////////////// #include "RenderCommand.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlRendererAPI.hpp" #endif namespace nexo::renderer { - #ifdef GRAPHICS_API_OPENGL - RendererApi *RenderCommand::_rendererApi = new OpenGlRendererApi; + #ifdef NX_GRAPHICS_API_OPENGL + NxRendererApi *NxRenderCommand::_rendererApi = new NxOpenGlRendererApi; #endif - void RenderCommand::init() + void NxRenderCommand::init() { if (!_rendererApi) - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); _rendererApi->init(); } } diff --git a/engine/src/renderer/RenderCommand.hpp b/engine/src/renderer/RenderCommand.hpp index 2ff1704c9..da2ee2dbc 100644 --- a/engine/src/renderer/RenderCommand.hpp +++ b/engine/src/renderer/RenderCommand.hpp @@ -17,10 +17,10 @@ namespace nexo::renderer { /** - * @class RenderCommand + * @class NxRenderCommand * @brief Provides a high-level interface for issuing rendering commands. * - * The `RenderCommand` class serves as an abstraction layer over the graphics API + * The `NxRenderCommand` class serves as an abstraction layer over the graphics API * (e.g., OpenGL, DirectX, Vulkan), allowing the application to issue rendering * commands without being tightly coupled to a specific API. * @@ -30,30 +30,30 @@ namespace nexo::renderer { * and drawing indexed primitives. * * Responsibilities: - * - Delegates rendering operations to the underlying `RendererApi` implementation. + * - Delegates rendering operations to the underlying `NxRendererApi` implementation. * - Provides static methods for commonly used rendering commands. * * Usage: * - Before issuing any rendering commands, call `init()` to initialize the underlying - * `RendererApi`. + * `NxRendererApi`. * - Use static methods like `setViewport`, `setClearColor`, `clear`, and `drawIndexed` * for rendering operations. * * Notes: - * - The specific implementation of `RendererApi` (e.g., `OpenGlRendererApi`) is - * determined by the preprocessor directive `GRAPHICS_API_OPENGL` or similar. + * - The specific implementation of `RendererApi` (e.g., `NxOpenGlRendererApi`) is + * determined by the preprocessor directive `NX_GRAPHICS_API_OPENGL` or similar. */ - class RenderCommand { + class NxRenderCommand { public: /** * @brief Initializes the rendering API. * - * This method initializes the underlying `RendererApi` instance, ensuring that the + * This method initializes the underlying `NxRendererApi` instance, ensuring that the * graphics API is ready to accept rendering commands. It must be called before issuing * any other render commands. * * Throws: - * - UnknownGraphicsApi exception if the `_rendererApi` instance is null. + * - NxUnknownGraphicsApi exception if the `_rendererApi` instance is null. * * Usage: * - Typically called once during application initialization. @@ -64,7 +64,7 @@ namespace nexo::renderer { * @brief Sets the viewport dimensions and position. * * The viewport defines the rectangular region of the window where rendering will - * take place. This command is delegated to the `RendererApi` implementation. + * take place. This command is delegated to the `NxRendererApi` implementation. * * @param x The x-coordinate of the lower-left corner of the viewport. * @param y The y-coordinate of the lower-left corner of the viewport. @@ -74,13 +74,13 @@ namespace nexo::renderer { * Usage: * - Call this method whenever the window is resized to adjust the rendering area. */ - static void setViewport(unsigned int x, unsigned int y, unsigned int width, unsigned int height) { _rendererApi->setViewport(x, y, width, height); }; + static void setViewport(const unsigned int x, const unsigned int y, const unsigned int width, const unsigned int height) { _rendererApi->setViewport(x, y, width, height); }; /** * @brief Sets the clear color for the rendering context. * * The clear color is used when clearing the color buffer (e.g., during a call to - * `clear`). This command is delegated to the `RendererApi` implementation. + * `clear`). This command is delegated to the `NxRendererApi` implementation. * * @param color The color to set as the clear color, represented as a `glm::vec4` * (RGBA format with components in the range [0, 1]). @@ -95,7 +95,7 @@ namespace nexo::renderer { * @brief Clears the screen using the current clear color. * * This method clears the color and/or depth buffers, preparing the screen for - * rendering the next frame. This command is delegated to the `RendererApi` + * rendering the next frame. This command is delegated to the `NxRendererApi` * implementation. * * Usage: @@ -108,7 +108,7 @@ namespace nexo::renderer { * * This method issues a draw call for rendering indexed geometry. The indices * are provided by the index buffer bound to the vertex array. This command is - * delegated to the `RendererApi` implementation. + * delegated to the `NxRendererApi` implementation. * * @param vertexArray A shared pointer to the vertex array containing the geometry data. * @param indexCount The number of indices to draw. If set to 0, the method will use @@ -117,25 +117,113 @@ namespace nexo::renderer { * Usage: * - Use this method to draw meshes or primitives with indexed geometry. */ - static void drawIndexed(const std::shared_ptr &vertexArray, unsigned int indexCount = 0) + static void drawIndexed(const std::shared_ptr &vertexArray, const unsigned int indexCount = 0) { _rendererApi->drawIndexed(vertexArray, indexCount); } + static void drawUnIndexed(const unsigned int verticesCount) + { + _rendererApi->drawUnIndexed(verticesCount); + } + + static void setDepthTest(const bool enable) + { + _rendererApi->setDepthTest(enable); + } + + static void setDepthMask(const bool enable) + { + _rendererApi->setDepthMask(enable); + } + + static void setDepthFunc(const unsigned int func) + { + _rendererApi->setDepthFunc(func); + } + + /** + * @brief Enables or disables the stencil test. + * + * The stencil test allows for masking certain portions of the screen during rendering. + * When enabled, fragments are drawn only if they pass a comparison test against the + * corresponding value in the stencil buffer. + * + * @param enable True to enable stencil testing, false to disable it. + * + * Usage: + * - Enable the stencil test before performing operations that will write to or use the stencil buffer. + * - Disable the stencil test when regular rendering should resume. + */ + static void setStencilTest(const bool enable) { _rendererApi->setStencilTest(enable); } + + /** + * @brief Sets the stencil mask that controls which bits of the stencil buffer are updated. + * + * The stencil mask determines which bits in the stencil buffer can be modified when + * stencil operations are performed. Only the bits that have a 1 in the corresponding + * position of the mask will be affected. + * + * @param mask The bit mask to use for stencil write operations. + * + * Usage: + * - Set a specific mask before performing stencil operations to control which bits are affected. + */ + static void setStencilMask(const unsigned int mask) { _rendererApi->setStencilMask(mask); } + + /** + * @brief Configures the stencil function used for stencil testing. + * + * The stencil function defines how the stencil test compares a reference value to the + * current value in the stencil buffer. The comparison result determines whether a fragment + * passes the stencil test and how the stencil buffer is updated. + * + * @param func The comparison function to use (e.g., GL_EQUAL, GL_ALWAYS, GL_LESS). + * @param ref The reference value to compare against. + * @param mask The mask that is ANDed with both the reference value and stored stencil value before comparison. + * + * Usage: + * - Configure before performing operations that rely on specific stencil buffer values. + */ + static void setStencilFunc(const unsigned int func, const int ref, const unsigned int mask) + { + _rendererApi->setStencilFunc(func, ref, mask); + } + + /** + * @brief Sets the operations to perform on the stencil buffer based on test outcomes. + * + * This method configures what happens to the stencil buffer value when the stencil test: + * - fails (sfail) + * - passes, but the depth test fails (dpfail) + * - passes, and the depth test also passes (dppass) + * + * @param sfail Operation to perform when the stencil test fails. + * @param dpfail Operation to perform when the stencil test passes but depth test fails. + * @param dppass Operation to perform when both stencil and depth tests pass. + * + * Usage: + * - Set before performing complex stencil operations like object outlining or shadow volumes. + */ + static void setStencilOp(const unsigned int sfail, const unsigned int dpfail, const unsigned int dppass) + { + _rendererApi->setStencilOp(sfail, dpfail, dppass); + } + private: /** - * @brief Static pointer to the active `RendererApi` implementation. + * @brief Static pointer to the active `NxRendererApi` implementation. * - * This member holds a pointer to the concrete `RendererApi` instance (e.g., - * `OpenGlRendererApi`). It is initialized based on the active graphics API, - * as determined by preprocessor directives (e.g., `GRAPHICS_API_OPENGL`). + * This member holds a pointer to the concrete `NxRendererApi` instance (e.g., + * `NxOpenGlRendererApi`). It is initialized based on the active graphics API, + * as determined by preprocessor directives (e.g., `NX_GRAPHICS_API_OPENGL`). * * Notes: * - The `_rendererApi` instance is statically allocated and shared across all - * `RenderCommand` methods. + * `NxRenderCommand` methods. * - The application must ensure that `_rendererApi` is initialized via `init()` * before issuing any render commands. */ - static RendererApi *_rendererApi; + static NxRendererApi *_rendererApi; }; } diff --git a/engine/src/renderer/Renderer.cpp b/engine/src/renderer/Renderer.cpp index 48511c716..40ee26a16 100644 --- a/engine/src/renderer/Renderer.cpp +++ b/engine/src/renderer/Renderer.cpp @@ -11,20 +11,21 @@ // Description: Source file for renderer class // /////////////////////////////////////////////////////////////////////////////// + #include "Renderer.hpp" -#include "Renderer2D.hpp" +#include "RenderCommand.hpp" namespace nexo::renderer { - Renderer::SceneData *Renderer::_sceneData = new Renderer::SceneData; + NxRenderer::NxSceneData *NxRenderer::_sceneData = new NxSceneData; - void Renderer::init() + void NxRenderer::init() { - RenderCommand::init(); + NxRenderCommand::init(); } - void Renderer::onWindowResize(unsigned int width, unsigned int height) + void NxRenderer::onWindowResize(const unsigned int width, const unsigned int height) { - RenderCommand::setViewport(0, 0, width, height); + NxRenderCommand::setViewport(0, 0, width, height); } } diff --git a/engine/src/renderer/Renderer.hpp b/engine/src/renderer/Renderer.hpp index 3b03f36cb..07e28b0c5 100644 --- a/engine/src/renderer/Renderer.hpp +++ b/engine/src/renderer/Renderer.hpp @@ -13,22 +13,19 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "RenderCommand.hpp" -#include "Shader.hpp" - #include namespace nexo::renderer { - class Renderer { + class NxRenderer { public: static void init(); static void onWindowResize(unsigned int width, unsigned int height); - struct SceneData { + struct NxSceneData { glm::mat4 projectionMatrix; }; - static SceneData *_sceneData; + static NxSceneData *_sceneData; }; } diff --git a/engine/src/renderer/Renderer2D.cpp b/engine/src/renderer/Renderer2D.cpp index f3e7630af..7b7aa4f90 100644 --- a/engine/src/renderer/Renderer2D.cpp +++ b/engine/src/renderer/Renderer2D.cpp @@ -23,21 +23,21 @@ #include namespace nexo::renderer { - void Renderer2D::init() + void NxRenderer2D::init() { - m_storage = std::make_shared(); + m_storage = std::make_shared(); // Initialize vertex array and buffer m_storage->vertexArray = createVertexArray(); - m_storage->vertexBuffer = createVertexBuffer(m_storage->maxVertices * sizeof(QuadVertex)); + m_storage->vertexBuffer = createVertexBuffer(m_storage->maxVertices * sizeof(NxQuadVertex)); // Define layout for the vertex buffer - const BufferLayout quadVertexBufferLayout = { - {ShaderDataType::FLOAT3, "aPos"}, // Position - {ShaderDataType::FLOAT4, "aColor"}, // Color - {ShaderDataType::FLOAT2, "aTexCoord"}, // Texture Coordinates - {ShaderDataType::FLOAT, "aTexIndex"}, // Texture Index - {ShaderDataType::INT, "aEntityID"} + const NxBufferLayout quadVertexBufferLayout = { + {NxShaderDataType::FLOAT3, "aPos"}, // Position + {NxShaderDataType::FLOAT4, "aColor"}, // Color + {NxShaderDataType::FLOAT2, "aTexCoord"}, // Texture Coordinates + {NxShaderDataType::FLOAT, "aTexIndex"}, // Texture Index + {NxShaderDataType::INT, "aEntityID"} }; m_storage->vertexBuffer->setLayout(quadVertexBufferLayout); m_storage->vertexArray->addVertexBuffer(m_storage->vertexBuffer); @@ -53,20 +53,20 @@ namespace nexo::renderer { m_storage->vertexArray->setIndexBuffer(m_storage->indexBuffer); // Initialize white texture - m_storage->whiteTexture = Texture2D::create(1, 1); + m_storage->whiteTexture = NxTexture2D::create(1, 1); unsigned int whiteTextureData = 0xffffffff; m_storage->whiteTexture->setData(&whiteTextureData, sizeof(unsigned int)); // Setup texture samplers - std::array samplers{}; - for (int i = 0; i < static_cast(Renderer2DStorage::maxTextureSlots); ++i) + std::array samplers{}; + for (int i = 0; i < static_cast(NxRenderer2DStorage::maxTextureSlots); ++i) samplers[i] = i; try { - m_storage->textureShader = Shader::create( + m_storage->textureShader = NxShader::create( Path::resolvePathRelativeToExe("../resources/shaders/texture.glsl").string()); m_storage->textureShader->bind(); - m_storage->textureShader->setUniformIntArray("uTexture", samplers.data(), Renderer2DStorage::maxTextureSlots); + m_storage->textureShader->setUniformIntArray("uTexture", samplers.data(), NxRenderer2DStorage::maxTextureSlots); } catch (const Exception &e) { LOG_EXCEPTION(e); } @@ -84,17 +84,17 @@ namespace nexo::renderer { } - void Renderer2D::shutdown() + void NxRenderer2D::shutdown() { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_2D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_2D); m_storage.reset(); } - void Renderer2D::beginScene(const glm::mat4 &viewProjection) + void NxRenderer2D::beginScene(const glm::mat4 &viewProjection) { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_2D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_2D); m_storage->textureShader->bind(); m_storage->vertexArray->bind(); m_storage->vertexBuffer->bind(); @@ -106,19 +106,19 @@ namespace nexo::renderer { m_renderingScene = true; } - void Renderer2D::flush() const + void NxRenderer2D::flush() const { m_storage->textureShader->bind(); for (unsigned int i = 0; i < m_storage->textureSlotIndex; ++i) { m_storage->textureSlots[i]->bind(i); } - RenderCommand::drawIndexed(m_storage->vertexArray, m_storage->indexCount); + NxRenderCommand::drawIndexed(m_storage->vertexArray, m_storage->indexCount); m_storage->stats.drawCalls++; m_storage->vertexArray->unbind(); m_storage->vertexBuffer->unbind(); } - void Renderer2D::flushAndReset() const + void NxRenderer2D::flushAndReset() const { flush(); m_storage->indexCount = 0; @@ -127,12 +127,12 @@ namespace nexo::renderer { m_storage->textureSlotIndex = 1; } - void Renderer2D::endScene() const + void NxRenderer2D::endScene() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_2D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_2D); if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); const auto vertexDataSize = static_cast( reinterpret_cast(m_storage->vertexBufferPtr) - @@ -146,7 +146,7 @@ namespace nexo::renderer { } - void Renderer2D::generateQuadVertices(const glm::mat4 &transform, const glm::vec4 color, const float textureIndex, + void NxRenderer2D::generateQuadVertices(const glm::mat4 &transform, const glm::vec4 color, const float textureIndex, const glm::vec2 *textureCoords, int entityID) const { constexpr unsigned int quadVertexCount = 4; @@ -185,7 +185,7 @@ namespace nexo::renderer { } - float Renderer2D::getTextureIndex(const std::shared_ptr &texture) const + float NxRenderer2D::getTextureIndex(const std::shared_ptr &texture) const { float textureIndex = 0.0f; @@ -205,18 +205,18 @@ namespace nexo::renderer { return textureIndex; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const glm::vec4 &color, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const glm::vec4 &color, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, color, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const glm::vec4 &color, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const glm::vec4 &color, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -236,20 +236,20 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, - const std::shared_ptr &texture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, + const std::shared_ptr &texture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, texture, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, - const std::shared_ptr &texture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, + const std::shared_ptr &texture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -271,20 +271,20 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, - const std::shared_ptr &subTexture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, + const std::shared_ptr &subTexture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, subTexture, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, - const std::shared_ptr &subTexture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, + const std::shared_ptr &subTexture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -301,19 +301,19 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, const glm::vec4 &color, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, rotation, color, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const glm::vec4 &color, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const glm::vec4 &color, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -333,20 +333,20 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, - const std::shared_ptr &texture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, + const std::shared_ptr &texture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, rotation, texture, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const float rotation, - const std::shared_ptr &texture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const float rotation, + const std::shared_ptr &texture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -369,20 +369,20 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, - const std::shared_ptr &subTexture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const float rotation, + const std::shared_ptr &subTexture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); drawQuad({pos.x, pos.y, 0.0f}, size, rotation, subTexture, entityID); } - void Renderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const float rotation, - const std::shared_ptr &subTexture, int entityID) const + void NxRenderer2D::drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const float rotation, + const std::shared_ptr &subTexture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_2D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_2D, "Renderer not rendering a scene, make sure to call beginScene first"); if (m_storage->indexCount >= m_storage->maxIndices) flushAndReset(); @@ -400,18 +400,18 @@ namespace nexo::renderer { m_storage->stats.quadCount++; } - void Renderer2D::resetStats() const + void NxRenderer2D::resetStats() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_2D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_2D); m_storage->stats.drawCalls = 0; m_storage->stats.quadCount = 0; } - RendererStats Renderer2D::getStats() const + NxRendererStats NxRenderer2D::getStats() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_2D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_2D); return m_storage->stats; } diff --git a/engine/src/renderer/Renderer2D.hpp b/engine/src/renderer/Renderer2D.hpp index 9619d0cda..fa54d8d45 100644 --- a/engine/src/renderer/Renderer2D.hpp +++ b/engine/src/renderer/Renderer2D.hpp @@ -22,7 +22,7 @@ namespace nexo::renderer { - struct QuadVertex { + struct NxQuadVertex { glm::vec3 position; glm::vec4 color; glm::vec2 texCoord; @@ -32,7 +32,7 @@ namespace nexo::renderer { int entityID; }; - struct RendererStats { + struct NxRendererStats { unsigned int drawCalls = 0; unsigned int quadCount = 0; @@ -40,37 +40,37 @@ namespace nexo::renderer { [[nodiscard]] unsigned int getTotalIndexCount() const { return quadCount * 6; } }; - struct Renderer2DStorage { + struct NxRenderer2DStorage { const unsigned int maxQuads = 10000; const unsigned int maxVertices = maxQuads * 4; const unsigned int maxIndices = maxQuads * 6; static constexpr unsigned int maxTextureSlots = 32; - std::shared_ptr textureShader; - std::shared_ptr vertexArray; - std::shared_ptr vertexBuffer; - std::shared_ptr indexBuffer; - std::shared_ptr whiteTexture; + std::shared_ptr textureShader; + std::shared_ptr vertexArray; + std::shared_ptr vertexBuffer; + std::shared_ptr indexBuffer; + std::shared_ptr whiteTexture; unsigned int indexCount = 0; - std::array vertexBufferBase; + std::array vertexBufferBase; std::array indexBufferBase; - QuadVertex* vertexBufferPtr = nullptr; + NxQuadVertex* vertexBufferPtr = nullptr; unsigned int *indexBufferPtr = nullptr; - std::array, maxTextureSlots> textureSlots; + std::array, maxTextureSlots> textureSlots; unsigned int textureSlotIndex = 1; glm::vec4 quadVertexPositions[4]; - RendererStats stats; + NxRendererStats stats; }; /** - * @class Renderer2D + * @class NxRenderer2D * @brief Provides a 2D rendering system for drawing quads, textures, and sprites. * - * The `Renderer2D` class is a high-performance 2D rendering engine that supports + * The `NxRenderer2D` class is a high-performance 2D rendering engine that supports * batching, texture binding, and transformation. * * Features: @@ -91,17 +91,17 @@ namespace nexo::renderer { * 4. Call `endScene()` to finalize the rendering and issue draw calls. * 5. Call `shutdown()` to release resources when the renderer is no longer needed. */ - class Renderer2D { + class NxRenderer2D { public: /** - * @brief Destroys the Renderer2D instance and releases resources. + * @brief Destroys the NxRenderer2D instance and releases resources. * * Ensures proper cleanup of the internal storage and associated buffers. */ - ~Renderer2D() = default; + ~NxRenderer2D() = default; /** - * @brief Initializes the Renderer2D and allocates required resources. + * @brief Initializes the NxRenderer2D and allocates required resources. * * This method sets up the internal storage, including vertex arrays, buffers, * textures, and shaders. It also predefines the vertex positions for quads. @@ -121,13 +121,13 @@ namespace nexo::renderer { void init(); /** - * @brief Shuts down the Renderer2D and releases allocated resources. + * @brief Shuts down the NxRenderer2D and releases allocated resources. * * This method deletes internal storage, including vertex and index buffers, * and resets the internal storage pointer. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ void shutdown(); @@ -140,8 +140,8 @@ namespace nexo::renderer { * @param viewProjection The combined view and projection matrix for the scene. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. - * - RendererSceneLifeCycleFailure if called without proper initialization. + * - NxRendererNotInitialized if the renderer is not initialized. + * - NxRendererSceneLifeCycleFailure if called without proper initialization. */ void beginScene(const glm::mat4 &viewProjection); @@ -152,8 +152,8 @@ namespace nexo::renderer { * and resets internal buffers for the next frame. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. - * - RendererSceneLifeCycleFailure if no scene was started with `beginScene()`. + * - NxRendererNotInitialized if the renderer is not initialized. + * - NxRendererSceneLifeCycleFailure if no scene was started with `beginScene()`. */ void endScene() const; void flush() const; @@ -181,11 +181,11 @@ namespace nexo::renderer { * Overloaded for: * - 2D position (`glm::vec2`) and 3D position (`glm::vec3`). */ - void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const std::shared_ptr &texture, int entityID = -1) const; - void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const std::shared_ptr &texture, int entityID = -1) const; + void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const std::shared_ptr &texture, int entityID = -1) const; + void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const std::shared_ptr &texture, int entityID = -1) const; - void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const std::shared_ptr &subTexture, int entityID = -1) const; - void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const std::shared_ptr &subTexture, int entityID = -1) const; + void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, const std::shared_ptr &subTexture, int entityID = -1) const; + void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, const std::shared_ptr &subTexture, int entityID = -1) const; /** @@ -203,43 +203,43 @@ namespace nexo::renderer { void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, float rotation, const glm::vec4 &color, int entityID = -1) const; void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const glm::vec4 &color, int entityID = -1) const; - void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &texture, int entityID = -1) const; - void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &texture, int entityID = -1) const; + void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &texture, int entityID = -1) const; + void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &texture, int entityID = -1) const; - void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &subTexture, int entityID = -1) const; - void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &subTexture, int entityID = -1) const; + void drawQuad(const glm::vec2 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &subTexture, int entityID = -1) const; + void drawQuad(const glm::vec3 &pos, const glm::vec2 &size, float rotation, const std::shared_ptr &subTexture, int entityID = -1) const; /** * @brief Resets rendering statistics. * - * Clears the draw call and quad counters in `RendererStats`. + * Clears the draw call and quad counters in `NxRendererStats`. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ void resetStats() const; /** * @brief Retrieves the current rendering statistics. * - * @return A `RendererStats` struct containing the number of draw calls and + * @return A `NxRendererStats` struct containing the number of draw calls and * quads rendered. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ - [[nodiscard]] RendererStats getStats() const; + [[nodiscard]] NxRendererStats getStats() const; - std::shared_ptr getInternalStorage() const { return m_storage; }; + std::shared_ptr getInternalStorage() const { return m_storage; }; private: - std::shared_ptr m_storage; + std::shared_ptr m_storage; bool m_renderingScene = false; void flushAndReset() const; // Helper functions void generateQuadVertices(const glm::mat4 &transform, glm::vec4 color, float textureIndex, const glm::vec2 *textureCoords, int entityID) const; - [[nodiscard]] float getTextureIndex(const std::shared_ptr &texture) const; + [[nodiscard]] float getTextureIndex(const std::shared_ptr &texture) const; }; } diff --git a/engine/src/renderer/Renderer3D.cpp b/engine/src/renderer/Renderer3D.cpp index 6318a6c88..e74a55bf2 100644 --- a/engine/src/renderer/Renderer3D.cpp +++ b/engine/src/renderer/Renderer3D.cpp @@ -12,34 +12,35 @@ // /////////////////////////////////////////////////////////////////////////////// +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + #include "Renderer3D.hpp" #include "RenderCommand.hpp" #include "Logger.hpp" +#include "Shader.hpp" #include "renderer/RendererExceptions.hpp" - -#include -#define GLM_ENABLE_EXPERIMENTAL -#include -#include -#include +#include "Path.hpp" namespace nexo::renderer { - void Renderer3D::init() + void NxRenderer3D::init() { - m_storage = std::make_shared(); + m_storage = std::make_shared(); m_storage->vertexArray = createVertexArray(); - m_storage->vertexBuffer = createVertexBuffer(m_storage->maxVertices * sizeof(Vertex)); + m_storage->vertexBuffer = createVertexBuffer(m_storage->maxVertices * sizeof(NxVertex)); // Layout - const BufferLayout cubeVertexBufferLayout = { - {ShaderDataType::FLOAT3, "aPos"}, - {ShaderDataType::FLOAT2, "aTexCoord"}, - {ShaderDataType::FLOAT3, "aNormal"}, - {ShaderDataType::FLOAT3, "aTangent"}, - {ShaderDataType::FLOAT3, "aBiTangent"}, - {ShaderDataType::INT, "aEntityID"} + const NxBufferLayout cubeVertexBufferLayout = { + {NxShaderDataType::FLOAT3, "aPos"}, + {NxShaderDataType::FLOAT2, "aTexCoord"}, + {NxShaderDataType::FLOAT3, "aNormal"}, + {NxShaderDataType::FLOAT3, "aTangent"}, + {NxShaderDataType::FLOAT3, "aBiTangent"}, + {NxShaderDataType::INT, "aEntityID"} }; m_storage->vertexBuffer->setLayout(cubeVertexBufferLayout); m_storage->vertexArray->addVertexBuffer(m_storage->vertexBuffer); @@ -48,41 +49,63 @@ namespace nexo::renderer { m_storage->vertexArray->setIndexBuffer(m_storage->indexBuffer); // Texture - m_storage->whiteTexture = Texture2D::create(1, 1); + m_storage->whiteTexture = NxTexture2D::create(1, 1); unsigned int whiteTextureData = 0xffffffff; m_storage->whiteTexture->setData(&whiteTextureData, sizeof(unsigned int)); // Shader - std::array samplers{}; - for (int i = 0; i < static_cast(Renderer3DStorage::maxTextureSlots); ++i) + std::array samplers{}; + for (int i = 0; i < static_cast(NxRenderer3DStorage::maxTextureSlots); ++i) samplers[i] = i; - m_storage->textureShader = Shader::create(Path::resolvePathRelativeToExe( - "../resources/shaders/texture.glsl").string()); - m_storage->textureShader->bind(); - m_storage->textureShader->setUniformIntArray("uTexture", samplers.data(), Renderer3DStorage::maxTextureSlots); + const auto phong = m_storage->shaderLibrary.load("Phong", Path::resolvePathRelativeToExe( + "../resources/shaders/phong.glsl").string()); + m_storage->shaderLibrary.load("Outline pulse flat", Path::resolvePathRelativeToExe( + "../resources/shaders/outline_pulse_flat.glsl").string()); + const auto outlinePulseTransparentFlat = m_storage->shaderLibrary.load("Outline pulse transparent flat", Path::resolvePathRelativeToExe( + "../resources/shaders/outline_pulse_transparent_flat.glsl").string()); + const auto albedoUnshadedTransparent = m_storage->shaderLibrary.load("Albedo unshaded transparent", Path::resolvePathRelativeToExe( + "../resources/shaders/albedo_unshaded_transparent.glsl").string()); + m_storage->shaderLibrary.load("Grid shader", Path::resolvePathRelativeToExe( + "../resources/shaders/grid_shader.glsl").string()); + m_storage->shaderLibrary.load("Flat color", Path::resolvePathRelativeToExe( + "../resources/shaders/flat_color.glsl").string()); + phong->bind(); + phong->setUniformIntArray(NxShaderUniforms::TEXTURE_SAMPLER, samplers.data(), NxRenderer3DStorage::maxTextureSlots); + phong->unbind(); + outlinePulseTransparentFlat->bind(); + outlinePulseTransparentFlat->setUniformIntArray(NxShaderUniforms::TEXTURE_SAMPLER, samplers.data(), NxRenderer3DStorage::maxTextureSlots); + outlinePulseTransparentFlat->unbind(); + albedoUnshadedTransparent->bind(); + albedoUnshadedTransparent->setUniformIntArray(NxShaderUniforms::TEXTURE_SAMPLER, samplers.data(), NxRenderer3DStorage::maxTextureSlots); + albedoUnshadedTransparent->unbind(); m_storage->textureSlots[0] = m_storage->whiteTexture; - LOG(NEXO_DEV, "Renderer3D initialized"); + LOG(NEXO_DEV, "NxRenderer3D initialized"); } - void Renderer3D::shutdown() + void NxRenderer3D::shutdown() { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); m_storage.reset(); } - void Renderer3D::beginScene(const glm::mat4 &viewProjection, const glm::vec3 &cameraPos) + void NxRenderer3D::beginScene(const glm::mat4 &viewProjection, const glm::vec3 &cameraPos, const std::string &shader) { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); - m_storage->textureShader->bind(); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); + if (shader.empty()) + m_storage->currentSceneShader = m_storage->shaderLibrary.get("Phong"); + else + m_storage->currentSceneShader = m_storage->shaderLibrary.get(shader); + m_storage->currentSceneShader->bind(); m_storage->vertexArray->bind(); m_storage->vertexBuffer->bind(); - m_storage->textureShader->setUniformMatrix("viewProjection", viewProjection); - m_storage->textureShader->setUniformFloat3("camPos", cameraPos); + m_storage->currentSceneShader->setUniformMatrix("uViewProjection", viewProjection); + m_storage->cameraPosition = cameraPos; + m_storage->currentSceneShader->setUniformFloat3("uCamPos", cameraPos); m_storage->indexCount = 0; m_storage->vertexBufferPtr = m_storage->vertexBufferBase.data(); m_storage->indexBufferPtr = m_storage->indexBufferBase.data(); @@ -90,12 +113,12 @@ namespace nexo::renderer { m_renderingScene = true; } - void Renderer3D::endScene() const + void NxRenderer3D::endScene() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); const auto vertexDataSize = static_cast( reinterpret_cast(m_storage->vertexBufferPtr) - @@ -110,25 +133,25 @@ namespace nexo::renderer { flushAndReset(); } - void Renderer3D::flush() const + void NxRenderer3D::flush() const { - m_storage->textureShader->bind(); + m_storage->currentSceneShader->bind(); for (unsigned int i = 0; i < m_storage->textureSlotIndex; ++i) { m_storage->textureSlots[i]->bind(i); } - RenderCommand::drawIndexed(m_storage->vertexArray, m_storage->indexCount); + NxRenderCommand::drawIndexed(m_storage->vertexArray, m_storage->indexCount); m_storage->stats.drawCalls++; m_storage->vertexArray->unbind(); m_storage->vertexBuffer->unbind(); - m_storage->textureShader->unbind(); + m_storage->currentSceneShader->unbind(); for (unsigned int i = 0; i < m_storage->textureSlotIndex; ++i) { m_storage->textureSlots[i]->unbind(i); } } - void Renderer3D::flushAndReset() const + void NxRenderer3D::flushAndReset() const { flush(); m_storage->indexCount = 0; @@ -137,9 +160,9 @@ namespace nexo::renderer { m_storage->textureSlotIndex = 1; } - int Renderer3D::getTextureIndex(const std::shared_ptr &texture) const + int NxRenderer3D::getTextureIndex(const std::shared_ptr &texture) const { - int textureIndex = 0.0f; + int textureIndex = 0; if (!texture) return textureIndex; @@ -148,14 +171,14 @@ namespace nexo::renderer { { if (*m_storage->textureSlots[i].get() == *texture) { - textureIndex = i; + textureIndex = static_cast(i); break; } } if (textureIndex == 0) { - textureIndex = m_storage->textureSlotIndex; + textureIndex = static_cast(m_storage->textureSlotIndex); m_storage->textureSlots[m_storage->textureSlotIndex] = texture; m_storage->textureSlotIndex++; } @@ -163,37 +186,37 @@ namespace nexo::renderer { return textureIndex; } - void Renderer3D::setMaterialUniforms(const renderer::Material& material) const + void NxRenderer3D::setMaterialUniforms(const NxIndexedMaterial& material) const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); - - m_storage->textureShader->setUniformFloat4("material.albedoColor", material.albedoColor); - m_storage->textureShader->setUniformInt("material.albedoTexIndex", material.albedoTexIndex); - m_storage->textureShader->setUniformFloat4("material.specularColor", material.specularColor); - m_storage->textureShader->setUniformInt("material.specularTexIndex", material.specularTexIndex); - m_storage->textureShader->setUniformFloat3("material.emissiveColor", material.emissiveColor); - m_storage->textureShader->setUniformInt("material.emissiveTexIndex", material.emissiveTexIndex); - m_storage->textureShader->setUniformFloat("material.roughness", material.roughness); - m_storage->textureShader->setUniformInt("material.roughnessTexIndex", material.roughnessTexIndex); - m_storage->textureShader->setUniformFloat("material.metallic", material.metallic); - m_storage->textureShader->setUniformInt("material.metallicTexIndex", material.metallicTexIndex); - m_storage->textureShader->setUniformFloat("material.opacity", material.opacity); - m_storage->textureShader->setUniformInt("material.opacityTexIndex", material.opacityTexIndex); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); + + m_storage->currentSceneShader->setUniformFloat4("uMaterial.albedoColor", material.albedoColor); + m_storage->currentSceneShader->setUniformInt("uMaterial.albedoTexIndex", material.albedoTexIndex); + m_storage->currentSceneShader->setUniformFloat4("uMaterial.specularColor", material.specularColor); + m_storage->currentSceneShader->setUniformInt("uMaterial.specularTexIndex", material.specularTexIndex); + m_storage->currentSceneShader->setUniformFloat3("uMaterial.emissiveColor", material.emissiveColor); + m_storage->currentSceneShader->setUniformInt("uMaterial.emissiveTexIndex", material.emissiveTexIndex); + m_storage->currentSceneShader->setUniformFloat("uMaterial.roughness", material.roughness); + m_storage->currentSceneShader->setUniformInt("uMaterial.roughnessTexIndex", material.roughnessTexIndex); + m_storage->currentSceneShader->setUniformFloat("uMaterial.metallic", material.metallic); + m_storage->currentSceneShader->setUniformInt("uMaterial.metallicTexIndex", material.metallicTexIndex); + m_storage->currentSceneShader->setUniformFloat("uMaterial.opacity", material.opacity); + m_storage->currentSceneShader->setUniformInt("uMaterial.opacityTexIndex", material.opacityTexIndex); } - void Renderer3D::resetStats() const + void NxRenderer3D::resetStats() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); m_storage->stats.drawCalls = 0; m_storage->stats.cubeCount = 0; } - Renderer3DStats Renderer3D::getStats() const + NxRenderer3DStats NxRenderer3D::getStats() const { if (!m_storage) - THROW_EXCEPTION(RendererNotInitialized, RendererType::RENDERER_3D); + THROW_EXCEPTION(NxRendererNotInitialized, NxRendererType::RENDERER_3D); return m_storage->stats; } diff --git a/engine/src/renderer/Renderer3D.hpp b/engine/src/renderer/Renderer3D.hpp index fd85aed9c..5be594349 100644 --- a/engine/src/renderer/Renderer3D.hpp +++ b/engine/src/renderer/Renderer3D.hpp @@ -16,14 +16,14 @@ #include "Shader.hpp" #include "VertexArray.hpp" #include "Texture.hpp" -#include "components/Render3D.hpp" +#include "ShaderLibrary.hpp" #include #include namespace nexo::renderer { - struct Vertex { + struct NxVertex { glm::vec3 position; glm::vec2 texCoord; glm::vec3 normal; @@ -33,7 +33,7 @@ namespace nexo::renderer { int entityID; }; - struct Material { + struct NxIndexedMaterial { glm::vec4 albedoColor = glm::vec4(1.0f); int albedoTexIndex = 0; // Default: 0 (white texture) glm::vec4 specularColor = glm::vec4(1.0f); @@ -48,8 +48,26 @@ namespace nexo::renderer { int opacityTexIndex = 0; // Default: 0 (white texture) }; + struct NxMaterial { + glm::vec4 albedoColor = glm::vec4(1.0f); + glm::vec4 specularColor = glm::vec4(1.0f); + glm::vec3 emissiveColor = glm::vec3(0.0f); + + float roughness = 0.0f; // 0 = smooth, 1 = rough + float metallic = 0.0f; // 0 = non-metal, 1 = fully metallic + float opacity = 1.0f; // 1 = opaque, 0 = fully transparent + + std::shared_ptr albedoTexture = nullptr; + std::shared_ptr normalMap = nullptr; + std::shared_ptr metallicMap = nullptr; + std::shared_ptr roughnessMap = nullptr; + std::shared_ptr emissiveMap = nullptr; + + std::string shader; + }; + //TODO: Add stats for the meshes - struct Renderer3DStats { + struct NxRenderer3DStats { unsigned int drawCalls = 0; unsigned int cubeCount = 0; @@ -58,8 +76,8 @@ namespace nexo::renderer { }; /** - * @struct Renderer3DStorage - * @brief Holds internal data and resources used by Renderer3D. + * @struct NxRenderer3DStorage + * @brief Holds internal data and resources used by NxRenderer3D. * * Members: * - `maxCubes`, `maxVertices`, `maxIndices`: Limits for cubes, vertices, and indices. @@ -71,36 +89,40 @@ namespace nexo::renderer { * - `vertexBufferPtr`, `indexBufferPtr`: Current pointers for batching vertices and indices. * - `stats`: Rendering statistics. */ - struct Renderer3DStorage { + struct NxRenderer3DStorage { const unsigned int maxCubes = 10000; const unsigned int maxVertices = maxCubes * 8; const unsigned int maxIndices = maxCubes * 36; static constexpr unsigned int maxTextureSlots = 32; static constexpr unsigned int maxTransforms = 1024; - std::shared_ptr textureShader; - std::shared_ptr vertexArray; - std::shared_ptr vertexBuffer; - std::shared_ptr indexBuffer; - std::shared_ptr whiteTexture; + glm::vec3 cameraPosition; + + ShaderLibrary shaderLibrary; + + std::shared_ptr currentSceneShader = nullptr; + std::shared_ptr vertexArray; + std::shared_ptr vertexBuffer; + std::shared_ptr indexBuffer; + std::shared_ptr whiteTexture; unsigned int indexCount = 0; - std::array vertexBufferBase; + std::array vertexBufferBase; std::array indexBufferBase; - Vertex* vertexBufferPtr = nullptr; + NxVertex* vertexBufferPtr = nullptr; unsigned int *indexBufferPtr = nullptr; - std::array, maxTextureSlots> textureSlots; + std::array, maxTextureSlots> textureSlots; unsigned int textureSlotIndex = 1; - Renderer3DStats stats; + NxRenderer3DStats stats; }; /** - * @class Renderer3D + * @class NxRenderer3D * @brief Provides a high-performance 3D rendering system for drawing cubes, textured objects, and meshes. * - * The `Renderer3D` class facilitates efficient rendering of 3D objects using batching, + * The `NxRenderer3D` class facilitates efficient rendering of 3D objects using batching, * texture binding, and transformation matrices. It supports dynamic vertex and index * buffers, enabling high performance for drawing multiple 3D primitives. * @@ -122,10 +144,10 @@ namespace nexo::renderer { * 4. Call `endScene()` to finalize the rendering and issue draw calls. * 5. Call `shutdown()` to release resources when the renderer is no longer needed. */ - class Renderer3D { + class NxRenderer3D { public: /** - * @brief Initializes the Renderer3D and allocates required resources. + * @brief Initializes the NxRenderer3D and allocates required resources. * * Sets up internal storage, vertex buffers, index buffers, and texture samplers. * Prepares the default white texture and initializes the texture shader. @@ -145,12 +167,12 @@ namespace nexo::renderer { void init(); /** - * @brief Releases all resources and cleans up the Renderer3D. + * @brief Releases all resources and cleans up the NxRenderer3D. * * Deletes allocated vertex and index buffers and resets internal storage pointers. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ void shutdown(); @@ -164,10 +186,10 @@ namespace nexo::renderer { * @param cameraPos The position of the camera in the scene. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. - * - RendererSceneLifeCycleFailure if called without proper initialization. + * - NxRendererNotInitialized if the renderer is not initialized. + * - NxRendererSceneLifeCycleFailure if called without proper initialization. */ - void beginScene(const glm::mat4& viewProjection, const glm::vec3 &cameraPos); + void beginScene(const glm::mat4& viewProjection, const glm::vec3 &cameraPos, const std::string &shader = ""); /** * @brief Ends the current 3D rendering scene. @@ -176,8 +198,8 @@ namespace nexo::renderer { * and resets buffers for the next frame. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. - * - RendererSceneLifeCycleFailure if no scene was started with `beginScene()`. + * - NxRendererNotInitialized if the renderer is not initialized. + * - NxRendererSceneLifeCycleFailure if no scene was started with `beginScene()`. */ void endScene() const; @@ -192,7 +214,7 @@ namespace nexo::renderer { * @param color The color (RGBA) of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ void drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec4& color, int entityID = -1) const; @@ -208,7 +230,7 @@ namespace nexo::renderer { * @param color The color (RGBA) of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ void drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3 &rotation, const glm::vec4& color, int entityID = -1) const; @@ -222,7 +244,7 @@ namespace nexo::renderer { * @param color The color (RGBA) of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ void drawCube(const glm::mat4& transform, const glm::vec4& color, int entityID = -1) const; @@ -237,9 +259,9 @@ namespace nexo::renderer { * @param material The material properties of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ - void drawCube(const glm::vec3& position, const glm::vec3& size, const components::Material &material, int entityID = -1) const; + void drawCube(const glm::vec3& position, const glm::vec3& size, const NxMaterial& material, int entityID = -1) const; /** * @brief Draws a cube using a specified transformation and material. @@ -253,9 +275,9 @@ namespace nexo::renderer { * @param material The material properties of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ - void drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3& rotation, const components::Material &material, int entityID = -1) const; + void drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3& rotation, const NxMaterial& material, int entityID = -1) const; /** * @brief Draws a cube using a specified transformation and material. @@ -269,9 +291,9 @@ namespace nexo::renderer { * @param material The material properties of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ - void drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::quat &rotation, const components::Material &material, int entityID = -1) const; + void drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::quat &rotation, const NxMaterial& material, int entityID = -1) const; /** * @brief Draws a cube using a specified transformation and color. @@ -283,9 +305,9 @@ namespace nexo::renderer { * @param material The material properties of the cube. * @param entityID An optional entity identifier (default is -1). * - * @throws RendererSceneLifeCycleFailure if the renderer is not in a valid scene. + * @throws NxRendererSceneLifeCycleFailure if the renderer is not in a valid scene. */ - void drawCube(const glm::mat4& transform, const components::Material &material, int entityID = -1) const; + void drawCube(const glm::mat4& transform, const NxMaterial& material, int entityID = -1) const; /** * @brief Draws a custom 3D mesh. @@ -297,39 +319,42 @@ namespace nexo::renderer { * @param texture Optional texture to apply to the mesh. * * Throws: - * - RendererSceneLifeCycleFailure if no scene was started with `beginScene()`. + * - NxRendererSceneLifeCycleFailure if no scene was started with `beginScene()`. */ - void drawMesh(const std::vector& vertices, const std::vector& indices, const std::shared_ptr& texture, int entityID = -1) const; + void drawMesh(const std::vector& vertices, const std::vector& indices, const std::shared_ptr& texture, int entityID = -1) const; + + void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::vec3& position, const glm::vec3& size, const NxMaterial& material, int entityID = -1) const; + void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& size, const NxMaterial& material, int entityID = -1) const; + void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::mat4& transform, const NxMaterial& material, int entityID = -1) const; - void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::vec3& position, const glm::vec3& size, const components::Material& material, int entityID = -1) const; - void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::vec3& position, const glm::vec3& rotation, const glm::vec3& size, const components::Material& material, int entityID = -1) const; - void drawMesh(const std::vector& vertices, const std::vector& indices, const glm::mat4& transform, const components::Material& material, int entityID = -1) const; + void drawBillboard(const glm::vec3& position, const glm::vec2& size, const glm::vec4& color, int entityID) const; + void drawBillboard(const glm::vec3& position, const glm::vec2& size, const NxMaterial& material, int entityID) const; /** * @brief Resets rendering statistics. * - * Clears the draw call and cube counters in `Renderer3DStats`. + * Clears the draw call and cube counters in `NxRenderer3DStats`. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ void resetStats() const; /** * @brief Retrieves the current rendering statistics. * - * @return A `Renderer3DStats` struct containing the number of draw calls and + * @return A `NxRenderer3DStats` struct containing the number of draw calls and * cubes rendered. * * Throws: - * - RendererNotInitialized if the renderer is not initialized. + * - NxRendererNotInitialized if the renderer is not initialized. */ - [[nodiscard]] Renderer3DStats getStats() const; + [[nodiscard]] NxRenderer3DStats getStats() const; - std::shared_ptr &getShader() const {return m_storage->textureShader;}; + [[nodiscard]] std::shared_ptr &getShader() const {return m_storage->currentSceneShader;}; - std::shared_ptr getInternalStorage() const { return m_storage; }; + [[nodiscard]] std::shared_ptr getInternalStorage() const { return m_storage; }; private: - std::shared_ptr m_storage; + std::shared_ptr m_storage; bool m_renderingScene = false; /** @@ -352,7 +377,7 @@ namespace nexo::renderer { * @param texture The texture to look up. * @return float The texture index. */ - [[nodiscard]] int getTextureIndex(const std::shared_ptr& texture) const; + [[nodiscard]] int getTextureIndex(const std::shared_ptr& texture) const; /** * @brief Sets material-related uniforms in the texture shader. @@ -361,9 +386,9 @@ namespace nexo::renderer { * * @param material The material whose properties are to be set. * - * @throws RendererNotInitialized if the renderer is not initialized. + * @throws NxRendererNotInitialized if the renderer is not initialized. */ - void setMaterialUniforms(const renderer::Material& material) const; + void setMaterialUniforms(const NxIndexedMaterial& material) const; }; } diff --git a/engine/src/renderer/RendererAPI.hpp b/engine/src/renderer/RendererAPI.hpp index 47493ba6d..c23bc5580 100644 --- a/engine/src/renderer/RendererAPI.hpp +++ b/engine/src/renderer/RendererAPI.hpp @@ -21,10 +21,10 @@ namespace nexo::renderer { /** - * @class RendererApi + * @class NxRendererApi * @brief Abstract interface for low-level rendering API implementations. * - * The `RendererApi` class defines the essential methods required for interacting + * The `NxRendererApi` class defines the essential methods required for interacting * with the graphics pipeline, such as initializing the API, configuring the * viewport, clearing buffers, and issuing draw commands. Specific graphics APIs, * like OpenGL, DirectX, or Vulkan, should implement this interface to ensure @@ -36,11 +36,11 @@ namespace nexo::renderer { * - Support commands for clearing buffers, setting viewport size, and drawing. * * Subclasses: - * - `OpenGlRendererApi`: Implements this interface using OpenGL commands. + * - `NxOpenGlRendererApi`: Implements this interface using OpenGL commands. */ - class RendererApi { + class NxRendererApi { public: - virtual ~RendererApi() = default; + virtual ~NxRendererApi() = default; /** * @brief Initializes the graphics API. @@ -111,17 +111,29 @@ namespace nexo::renderer { */ virtual void setClearDepth(float depth) = 0; + virtual void setDepthTest(bool enable) = 0; + virtual void setDepthFunc(unsigned int func) = 0; + virtual void setDepthMask(bool enable) = 0; + /** * @brief Issues a draw call for indexed geometry. * * Renders geometry using indices stored in the index buffer attached to the - * specified `VertexArray`. + * specified `NxVertexArray`. * - * @param vertexArray A shared pointer to the `VertexArray` containing vertex and index data. + * @param vertexArray A shared pointer to the `NxVertexArray` containing vertex and index data. * @param count The number of indices to draw. If zero, all indices in the buffer are used. * * Must be implemented by subclasses. */ - virtual void drawIndexed(const std::shared_ptr &vertexArray, unsigned int count = 0) = 0; + virtual void drawIndexed(const std::shared_ptr &vertexArray, unsigned int count = 0) = 0; + + virtual void drawUnIndexed(unsigned int verticesCount) = 0; + + virtual void setStencilTest(bool enable) = 0; + virtual void setStencilMask(unsigned int mask) = 0; + virtual void setStencilFunc(unsigned int func, int ref, unsigned int mask) = 0; + virtual void setStencilOp(unsigned int sfail, unsigned int dpfail, unsigned int dppass) = 0; + }; } diff --git a/engine/src/renderer/RendererContext.hpp b/engine/src/renderer/RendererContext.hpp index 093e36b83..18b904e2d 100644 --- a/engine/src/renderer/RendererContext.hpp +++ b/engine/src/renderer/RendererContext.hpp @@ -17,10 +17,10 @@ #include "Renderer3D.hpp" namespace nexo::renderer { - class RendererContext { + class NxRendererContext { public: - RendererContext() = default; - Renderer2D renderer2D; - Renderer3D renderer3D; + NxRendererContext() = default; + NxRenderer2D renderer2D; + NxRenderer3D renderer3D; }; } diff --git a/engine/src/renderer/RendererExceptions.hpp b/engine/src/renderer/RendererExceptions.hpp index 26e176ed6..e425bab18 100644 --- a/engine/src/renderer/RendererExceptions.hpp +++ b/engine/src/renderer/RendererExceptions.hpp @@ -20,46 +20,46 @@ namespace nexo::renderer { - class OutOfRangeException final : public Exception { + class NxOutOfRangeException final : public Exception { public: - explicit OutOfRangeException(unsigned int index, unsigned int size, + explicit NxOutOfRangeException(unsigned int index, unsigned int size, const std::source_location loc = std::source_location::current()) : Exception(std::format("Index {} is out of range [0, {})", index, size), loc) {} }; - class FileNotFoundException final : public Exception { + class NxFileNotFoundException final : public Exception { public: - explicit FileNotFoundException(const std::string &filePath, + explicit NxFileNotFoundException(const std::string &filePath, const std::source_location loc = std::source_location::current()) : Exception(std::format("File not found: {}", filePath), loc) {} }; - class UnknownGraphicsApi final : public Exception { + class NxUnknownGraphicsApi final : public Exception { public: - explicit UnknownGraphicsApi(const std::string &backendApiName, + explicit NxUnknownGraphicsApi(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("Unknown graphics API: {}", backendApiName), loc) {} }; - class GraphicsApiInitFailure final : public Exception { + class NxGraphicsApiInitFailure final : public Exception { public: - explicit GraphicsApiInitFailure(const std::string &backendApiName, + explicit NxGraphicsApiInitFailure(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("Failed to initialize graphics API: {}", backendApiName), loc) {} }; - class GraphicsApiNotInitialized final : public Exception { + class NxGraphicsApiNotInitialized final : public Exception { public: - explicit GraphicsApiNotInitialized(const std::string &backendApiName, + explicit NxGraphicsApiNotInitialized(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] API is not initialized, call the init function first", backendApiName), loc) {} }; - class GraphicsApiViewportResizingFailure final : public Exception { + class NxGraphicsApiViewportResizingFailure final : public Exception { public: - explicit GraphicsApiViewportResizingFailure(const std::string &backendApi, const bool tooBig, + explicit NxGraphicsApiViewportResizingFailure(const std::string &backendApi, const bool tooBig, const unsigned int width, const unsigned int height, const std::source_location loc = std::source_location::current()) @@ -67,49 +67,49 @@ namespace nexo::renderer { backendApi, width, height, (tooBig ? "big" : "small")), loc) {} }; - class GraphicsApiWindowInitFailure final : public Exception { + class NxGraphicsApiWindowInitFailure final : public Exception { public: - explicit GraphicsApiWindowInitFailure(const std::string &backendApiName, + explicit NxGraphicsApiWindowInitFailure(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("Failed to initialize graphics API: {}", backendApiName), loc) {} }; - class InvalidValue final : public Exception { + class NxInvalidValue final : public Exception { public: - explicit InvalidValue(const std::string &backendApiName, const std::string &msg, + explicit NxInvalidValue(const std::string &backendApiName, const std::string &msg, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Invalid value: {}", backendApiName, msg), loc) {} }; - class ShaderCreationFailed final : public Exception { + class NxShaderCreationFailed final : public Exception { public: - explicit ShaderCreationFailed(const std::string &backendApi, const std::string &message, + explicit NxShaderCreationFailed(const std::string &backendApi, const std::string &message, const std::string &path = "", const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Failed to create the shader ({}): {}", backendApi, path, message), loc) {} }; - class ShaderInvalidUniform final : public Exception { + class NxShaderInvalidUniform final : public Exception { public: - explicit ShaderInvalidUniform(const std::string &backendApi, const std::string &shaderName, + explicit NxShaderInvalidUniform(const std::string &backendApi, const std::string &shaderName, const std::string &uniformName, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Failed to retrieve uniform \"{}\" in shader: {}", backendApi, uniformName, shaderName), loc) {} }; - class FramebufferCreationFailed final : public Exception { + class NxFramebufferCreationFailed final : public Exception { public: - explicit FramebufferCreationFailed(const std::string &backendApi, + explicit NxFramebufferCreationFailed(const std::string &backendApi, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Failed to create the framebuffer", backendApi), loc) {} }; - class FramebufferResizingFailed final : public Exception { + class NxFramebufferResizingFailed final : public Exception { public: - explicit FramebufferResizingFailed(const std::string &backendApi, const bool tooBig, + explicit NxFramebufferResizingFailed(const std::string &backendApi, const bool tooBig, const unsigned int width, const unsigned int height, const std::source_location loc = std::source_location::current()) @@ -117,67 +117,67 @@ namespace nexo::renderer { backendApi, width, height, (tooBig ? "big" : "small")), loc) {} }; - class FramebufferUnsupportedColorFormat final : public Exception { + class NxFramebufferUnsupportedColorFormat final : public Exception { public: - explicit FramebufferUnsupportedColorFormat(const std::string &backendApiName, + explicit NxFramebufferUnsupportedColorFormat(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Unsupported framebuffer color attachment format", backendApiName), loc) {} }; - class FramebufferUnsupportedDepthFormat final : public Exception { + class NxFramebufferUnsupportedDepthFormat final : public Exception { public: - explicit FramebufferUnsupportedDepthFormat(const std::string &backendApiName, + explicit NxFramebufferUnsupportedDepthFormat(const std::string &backendApiName, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Unsupported framebuffer depth attachment format", backendApiName), loc) {} }; - class FramebufferReadFailure final : public Exception { + class NxFramebufferReadFailure final : public Exception { public: - explicit FramebufferReadFailure(const std::string &backendApiName, int index, int x, int y, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Unable to read framebuffer with index {} at coordinate ({}, {})", backendApiName, index, x, y), loc) {} + explicit NxFramebufferReadFailure(const std::string &backendApiName, int index, int x, int y, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Unable to read framebuffer with index {} at coordinate ({}, {})", backendApiName, index, x, y), loc) {} }; - class FramebufferInvalidIndex final : public Exception { + class NxFramebufferInvalidIndex final : public Exception { public: - explicit FramebufferInvalidIndex(const std::string &backendApiName, int index, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Invalid attachment index : {}", backendApiName, index), loc) {}; + explicit NxFramebufferInvalidIndex(const std::string &backendApiName, int index, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Invalid attachment index : {}", backendApiName, index), loc) {}; }; - class BufferLayoutEmpty final : public Exception { + class NxBufferLayoutEmpty final : public Exception { public: - explicit BufferLayoutEmpty(const std::string &backendApi, + explicit NxBufferLayoutEmpty(const std::string &backendApi, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Vertex buffer layout cannot be empty", backendApi), loc) {} }; - enum class RendererType { + enum class NxRendererType { RENDERER_2D, RENDERER_3D }; - class RendererNotInitialized final : public Exception { + class NxRendererNotInitialized final : public Exception { public: - explicit RendererNotInitialized(const RendererType type, + explicit NxRendererNotInitialized(const NxRendererType type, const std::source_location loc = std::source_location::current()) : Exception(std::format("{} Renderer not initialized, call the init function first", - (type == RendererType::RENDERER_2D ? "[RENDERER 2D]" : "[RENDERER 3D]")), loc) + (type == NxRendererType::RENDERER_2D ? "[RENDERER 2D]" : "[RENDERER 3D]")), loc) {} }; - class RendererSceneLifeCycleFailure : public Exception { + class NxRendererSceneLifeCycleFailure final : public Exception { public: - explicit RendererSceneLifeCycleFailure(const RendererType type, const std::string &msg, + explicit NxRendererSceneLifeCycleFailure(const NxRendererType type, const std::string &msg, const std::source_location loc = std::source_location::current()) : Exception(std::format("{} {}", - (type == RendererType::RENDERER_2D ? "[RENDERER 2D]" : "[RENDERER 3D]"), msg), + (type == NxRendererType::RENDERER_2D ? "[RENDERER 2D]" : "[RENDERER 3D]"), msg), loc) {} }; - class TextureInvalidSize final : public Exception { + class NxTextureInvalidSize final : public Exception { public: - explicit TextureInvalidSize(const std::string &backendApi, + explicit NxTextureInvalidSize(const std::string &backendApi, const unsigned int width, const unsigned int height, const unsigned int maxTextureSize, const std::source_location loc = std::source_location::current()) @@ -185,9 +185,9 @@ namespace nexo::renderer { backendApi, width, height, maxTextureSize), loc) {} }; - class TextureUnsupportedFormat final : public Exception { + class NxTextureUnsupportedFormat final : public Exception { public: - explicit TextureUnsupportedFormat(const std::string &backendApi, const int channels, + explicit NxTextureUnsupportedFormat(const std::string &backendApi, const int channels, const std::string &path, const std::source_location loc = std::source_location::current()) @@ -195,17 +195,17 @@ namespace nexo::renderer { backendApi, channels, path), loc) {} }; - class TextureSizeMismatch final : public Exception { + class NxTextureSizeMismatch final : public Exception { public: - explicit TextureSizeMismatch(const std::string &backendApi, const int dataSize, const int expectedSize, + explicit NxTextureSizeMismatch(const std::string &backendApi, const int dataSize, const int expectedSize, const std::source_location loc = std::source_location::current()) : Exception(std::format("[{}] Data size does not match the texture size: {} != {}", backendApi, dataSize, expectedSize), loc) {} }; - class StbiLoadException final : public Exception { + class NxStbiLoadException final : public Exception { public: - explicit StbiLoadException(const std::string &msg, + explicit NxStbiLoadException(const std::string &msg, const std::source_location loc = std::source_location::current()) : Exception(std::format("STBI load failed: {}", msg), loc) {} }; diff --git a/engine/src/renderer/Shader.cpp b/engine/src/renderer/Shader.cpp index 7c13c682c..18ccd1181 100644 --- a/engine/src/renderer/Shader.cpp +++ b/engine/src/renderer/Shader.cpp @@ -14,7 +14,7 @@ #include "Shader.hpp" #include "renderer/RendererExceptions.hpp" #include "Logger.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlShader.hpp" #endif @@ -22,25 +22,25 @@ namespace nexo::renderer { - std::shared_ptr Shader::create(const std::string &path) + std::shared_ptr NxShader::create(const std::string &path) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(path); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(path); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::shared_ptr Shader::create(const std::string& name, const std::string &vertexSource, const std::string &fragmentSource) + std::shared_ptr NxShader::create(const std::string& name, const std::string &vertexSource, const std::string &fragmentSource) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(name, vertexSource, fragmentSource); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(name, vertexSource, fragmentSource); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::string Shader::readFile(const std::string &filepath) + std::string NxShader::readFile(const std::string &filepath) { std::string result; if (std::ifstream in(filepath, std::ios::in | std::ios::binary); in) @@ -52,61 +52,18 @@ namespace nexo::renderer { in.close(); return result; } - THROW_EXCEPTION(FileNotFoundException, filepath); + THROW_EXCEPTION(NxFileNotFoundException, filepath); } - void Shader::addStorageBuffer(const std::shared_ptr &buffer) + void NxShader::addStorageBuffer(const std::shared_ptr &buffer) { m_storageBuffers.push_back(buffer); } - void Shader::setStorageBufferData(unsigned int index, void *data, unsigned int size) + void NxShader::setStorageBufferData(unsigned int index, void *data, unsigned int size) { if (index >= m_storageBuffers.size()) - THROW_EXCEPTION(OutOfRangeException, index, m_storageBuffers.size()); + THROW_EXCEPTION(NxOutOfRangeException, index, m_storageBuffers.size()); m_storageBuffers[index]->setData(data, size); } - - void ShaderLibrary::add(const std::shared_ptr &shader) - { - const std::string &name = shader->getName(); - m_shaders[name] = shader; - } - - void ShaderLibrary::add(const std::string &name, const std::shared_ptr &shader) - { - m_shaders[name] = shader; - } - - std::shared_ptr ShaderLibrary::load(const std::string &name, const std::string &path) - { - auto shader = Shader::create(path); - add(name, shader); - return shader; - } - - std::shared_ptr ShaderLibrary::load(const std::string &path) - { - auto shader = Shader::create(path); - add(shader); - return shader; - } - - std::shared_ptr ShaderLibrary::load(const std::string &name, const std::string &vertexSource, const std::string &fragmentSource) - { - auto shader = Shader::create(name, vertexSource, fragmentSource); - add(shader); - return shader; - } - - std::shared_ptr ShaderLibrary::get(const std::string &name) const - { - if (!m_shaders.contains(name)) - { - LOG(NEXO_WARN, "ShaderLibrary::get: shader {} not found", name); - return nullptr; - } - return m_shaders.at(name); - } - } diff --git a/engine/src/renderer/Shader.hpp b/engine/src/renderer/Shader.hpp index f20e52bc3..ec3a9bf0b 100644 --- a/engine/src/renderer/Shader.hpp +++ b/engine/src/renderer/Shader.hpp @@ -23,11 +23,42 @@ namespace nexo::renderer { + enum class NxShaderUniforms { + VIEW_PROJECTION, + MODEL_MATRIX, + CAMERA_POSITION, + + TEXTURE_SAMPLER, + + DIR_LIGHT, + AMBIENT_LIGHT, + POINT_LIGHT_ARRAY, + NB_POINT_LIGHT, + SPOT_LIGHT_ARRAY, + NB_SPOT_LIGHT, + + MATERIAL + }; + + inline const std::unordered_map ShaderUniformsName = { + {NxShaderUniforms::VIEW_PROJECTION, "uViewProjection"}, + {NxShaderUniforms::MODEL_MATRIX, "uMatModel"}, + {NxShaderUniforms::CAMERA_POSITION, "uCamPos"}, + {NxShaderUniforms::TEXTURE_SAMPLER, "uTexture"}, + {NxShaderUniforms::DIR_LIGHT, "uDirLight"}, + {NxShaderUniforms::AMBIENT_LIGHT, "uAmbientLight"}, + {NxShaderUniforms::POINT_LIGHT_ARRAY, "uPointLights"}, + {NxShaderUniforms::NB_POINT_LIGHT, "uNbPointLights"}, + {NxShaderUniforms::SPOT_LIGHT_ARRAY, "uSpotLights"}, + {NxShaderUniforms::NB_SPOT_LIGHT, "uNbSpotLights"}, + {NxShaderUniforms::MATERIAL, "uMaterial"} + }; + /** - * @class Shader + * @class NxShader * @brief Abstract class representing a shader program in the rendering pipeline. * - * The `Shader` class provides a generic interface for creating and managing shader + * The `NxShader` class provides a generic interface for creating and managing shader * programs. These programs are used to execute rendering operations on the GPU. * * Responsibilities: @@ -36,18 +67,18 @@ namespace nexo::renderer { * - Set uniform variables to pass data from the CPU to the GPU. * * Subclasses: - * - `OpenGlShader`: Implements this interface using OpenGL-specific functionality. + * - `NxOpenGlShader`: Implements this interface using OpenGL-specific functionality. * * Example Usage: * ```cpp - * auto shader = Shader::create("path/to/shader.glsl"); + * auto shader = NxShader::create("path/to/shader.glsl"); * shader->bind(); * shader->setUniformFloat("uTime", 1.0f); * ``` */ - class Shader { + class NxShader { public: - virtual ~Shader() = default; + virtual ~NxShader() = default; /** * @brief Creates a shader program from a source file. @@ -59,10 +90,10 @@ namespace nexo::renderer { * @return A shared pointer to the created `Shader` instance. * * Throws: - * - `UnknownGraphicsApi` if no graphics API is supported. - * - `ShaderCreationFailed` if shader compilation fails. + * - `NxUnknownGraphicsApi` if no graphics API is supported. + * - `NxShaderCreationFailed` if shader compilation fails. */ - static std::shared_ptr create(const std::string &path); + static std::shared_ptr create(const std::string &path); /** * @brief Creates a shader program from source code strings. @@ -75,10 +106,10 @@ namespace nexo::renderer { * @return A shared pointer to the created `Shader` instance. * * Throws: - * - `UnknownGraphicsApi` if no graphics API is supported. - * - `ShaderCreationFailed` if shader compilation fails. + * - `NxUnknownGraphicsApi` if no graphics API is supported. + * - `NxShaderCreationFailed` if shader compilation fails. */ - static std::shared_ptr create(const std::string& name, const std::string &vertexSource, const std::string &fragmentSource); + static std::shared_ptr create(const std::string& name, const std::string &vertexSource, const std::string &fragmentSource); /** * @brief Binds the shader program for use in the rendering pipeline. @@ -99,13 +130,22 @@ namespace nexo::renderer { virtual void unbind() const = 0; virtual bool setUniformFloat(const std::string &name, float value) const = 0; + virtual bool setUniformFloat2(const std::string &name, const glm::vec2 &values) const = 0; virtual bool setUniformFloat3(const std::string &name, const glm::vec3 &values) const = 0; virtual bool setUniformFloat4(const std::string &name, const glm::vec4 &values) const = 0; virtual bool setUniformMatrix(const std::string &name, const glm::mat4 &matrix) const = 0; + virtual bool setUniformBool(const std::string &name, bool value) const = 0; virtual bool setUniformInt(const std::string &name, int value) const = 0; virtual bool setUniformIntArray(const std::string &name, const int *values, unsigned int count) const = 0; - void addStorageBuffer(const std::shared_ptr &buffer); + virtual bool setUniformFloat(NxShaderUniforms uniform, float value) const = 0; + virtual bool setUniformFloat3(NxShaderUniforms uniform, const glm::vec3 &values) const = 0; + virtual bool setUniformFloat4(NxShaderUniforms uniform, const glm::vec4 &values) const = 0; + virtual bool setUniformMatrix(NxShaderUniforms uniform, const glm::mat4 &matrix) const = 0; + virtual bool setUniformInt(NxShaderUniforms uniform, int value) const = 0; + virtual bool setUniformIntArray(NxShaderUniforms uniform, const int *values, unsigned int count) const = 0; + + void addStorageBuffer(const std::shared_ptr &buffer); void setStorageBufferData(unsigned int index, void *data, unsigned int size); virtual void bindStorageBufferBase(unsigned int index, unsigned int bindingPoint) const = 0; virtual void bindStorageBuffer(unsigned int index) const = 0; @@ -115,18 +155,9 @@ namespace nexo::renderer { virtual unsigned int getProgramId() const = 0; protected: static std::string readFile(const std::string &filepath); - std::vector> m_storageBuffers; + std::vector> m_storageBuffers; + std::unordered_map m_uniformLocations; }; - class ShaderLibrary { - public: - void add(const std::shared_ptr &shader); - void add(const std::string &name, const std::shared_ptr &shader); - std::shared_ptr load(const std::string &path); - std::shared_ptr load(const std::string &name, const std::string &path); - std::shared_ptr load(const std::string &name, const std::string &vertexSource, const std::string &fragmentSource); - std::shared_ptr get(const std::string &name) const; - private: - std::unordered_map> m_shaders; - }; + } diff --git a/engine/src/renderer/ShaderLibrary.cpp b/engine/src/renderer/ShaderLibrary.cpp new file mode 100644 index 000000000..5b3394da8 --- /dev/null +++ b/engine/src/renderer/ShaderLibrary.cpp @@ -0,0 +1,60 @@ +//// ShaderLibrary.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 19/04/2025 +// Description: Source file for the shader library +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ShaderLibrary.hpp" +#include "Logger.hpp" + +namespace nexo::renderer { + void ShaderLibrary::add(const std::shared_ptr &shader) + { + const std::string &name = shader->getName(); + m_shaders[name] = shader; + } + + void ShaderLibrary::add(const std::string &name, const std::shared_ptr &shader) + { + m_shaders[name] = shader; + } + + std::shared_ptr ShaderLibrary::load(const std::string &name, const std::string &path) + { + auto shader = NxShader::create(path); + add(name, shader); + return shader; + } + + std::shared_ptr ShaderLibrary::load(const std::string &path) + { + auto shader = NxShader::create(path); + add(shader); + return shader; + } + + std::shared_ptr ShaderLibrary::load(const std::string &name, const std::string &vertexSource, const std::string &fragmentSource) + { + auto shader = NxShader::create(name, vertexSource, fragmentSource); + add(shader); + return shader; + } + + std::shared_ptr ShaderLibrary::get(const std::string &name) const + { + if (!m_shaders.contains(name)) + { + LOG(NEXO_WARN, "ShaderLibrary::get: shader {} not found", name); + return nullptr; + } + return m_shaders.at(name); + } +} diff --git a/engine/src/renderer/ShaderLibrary.hpp b/engine/src/renderer/ShaderLibrary.hpp new file mode 100644 index 000000000..1e19c9f5c --- /dev/null +++ b/engine/src/renderer/ShaderLibrary.hpp @@ -0,0 +1,48 @@ +//// ShaderLibrary.hpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 19/04/2025 +// Description: Header file for the shader library +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include "Shader.hpp" + +namespace nexo::renderer { + + struct TransparentStringHasher { + using is_transparent = void; // enable heterogeneous lookup + + size_t operator()(std::string_view sv) const noexcept { + return std::hash{}(sv); + } + size_t operator()(const std::string &s) const noexcept { + return operator()(std::string_view(s)); + } + }; + + class ShaderLibrary { + public: + void add(const std::shared_ptr &shader); + void add(const std::string &name, const std::shared_ptr &shader); + std::shared_ptr load(const std::string &path); + std::shared_ptr load(const std::string &name, const std::string &path); + std::shared_ptr load(const std::string &name, const std::string &vertexSource, const std::string &fragmentSource); + std::shared_ptr get(const std::string &name) const; + private: + std::unordered_map< + std::string, + std::shared_ptr, + TransparentStringHasher, + std::equal_to<> + > m_shaders; + }; +} diff --git a/engine/src/renderer/ShaderStorageBuffer.cpp b/engine/src/renderer/ShaderStorageBuffer.cpp index 6c0b11d80..878484e85 100644 --- a/engine/src/renderer/ShaderStorageBuffer.cpp +++ b/engine/src/renderer/ShaderStorageBuffer.cpp @@ -15,16 +15,16 @@ #include "ShaderStorageBuffer.hpp" #include "renderer/RendererExceptions.hpp" #include -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlShaderStorageBuffer.hpp" #endif namespace nexo::renderer { - std::shared_ptr ShaderStorageBuffer::create(unsigned int size) + std::shared_ptr NxShaderStorageBuffer::create(unsigned int size) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(size); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(size); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/ShaderStorageBuffer.hpp b/engine/src/renderer/ShaderStorageBuffer.hpp index a042df9aa..56b3a1e3e 100644 --- a/engine/src/renderer/ShaderStorageBuffer.hpp +++ b/engine/src/renderer/ShaderStorageBuffer.hpp @@ -17,11 +17,11 @@ #include namespace nexo::renderer { - class ShaderStorageBuffer { + class NxShaderStorageBuffer { public: - virtual ~ShaderStorageBuffer() = default; + virtual ~NxShaderStorageBuffer() = default; - static std::shared_ptr create(unsigned int size); + static std::shared_ptr create(unsigned int size); virtual void bind() const = 0; virtual void bindBase(unsigned int bindingLocation) const = 0; diff --git a/engine/src/renderer/SubTexture2D.cpp b/engine/src/renderer/SubTexture2D.cpp index 50cb31031..5ec3e1046 100644 --- a/engine/src/renderer/SubTexture2D.cpp +++ b/engine/src/renderer/SubTexture2D.cpp @@ -14,7 +14,7 @@ #include "SubTexture2D.hpp" namespace nexo::renderer { - SubTexture2D::SubTexture2D(const std::shared_ptr &texture, const glm::vec2 &min, const glm::vec2 &max) + NxSubTexture2D::NxSubTexture2D(const std::shared_ptr &texture, const glm::vec2 &min, const glm::vec2 &max) : m_texture(texture) { m_texCoords[0] = {min.x, min.y}; @@ -23,11 +23,11 @@ namespace nexo::renderer { m_texCoords[3] = {min.x, max.y}; } - std::shared_ptr SubTexture2D::createFromCoords(const std::shared_ptr &texture, const glm::vec2 &coords, const glm::vec2 &cellSize, const glm::vec2 &spriteSize) + std::shared_ptr NxSubTexture2D::createFromCoords(const std::shared_ptr &texture, const glm::vec2 &coords, const glm::vec2 &cellSize, const glm::vec2 &spriteSize) { glm::vec2 min = {(coords.x * cellSize.x) / static_cast(texture->getWidth()) , (coords.y * cellSize.y) / static_cast(texture->getHeight())}; glm::vec2 max = {((coords.x + spriteSize.x) * cellSize.x) / static_cast(texture->getWidth()), ((coords.y + spriteSize.y) * cellSize.y) / static_cast(texture->getHeight())}; - return std::make_shared(texture, min, max); + return std::make_shared(texture, min, max); } } \ No newline at end of file diff --git a/engine/src/renderer/SubTexture2D.hpp b/engine/src/renderer/SubTexture2D.hpp index 197a54daf..1062e5722 100644 --- a/engine/src/renderer/SubTexture2D.hpp +++ b/engine/src/renderer/SubTexture2D.hpp @@ -19,10 +19,10 @@ namespace nexo::renderer { /** - * @class SubTexture2D + * @class NxSubTexture2D * @brief Represents a portion of a 2D texture, useful for sprite rendering. * - * The `SubTexture2D` class allows defining a sub-region within a larger texture. + * The `NxSubTexture2D` class allows defining a sub-region within a larger texture. * This is commonly used in sprite sheets where a single texture contains multiple * sprites. The class provides texture coordinates to render only the specified region. * @@ -33,26 +33,26 @@ namespace nexo::renderer { * * Example Usage: * ```cpp - * auto texture = Texture2D::create("path/to/texture.png"); - * auto subTexture = SubTexture2D::createFromCoords(texture, {1, 1}, {64, 64}); + * auto texture = NxTexture2D::create("path/to/texture.png"); + * auto subTexture = NxSubTexture2D::createFromCoords(texture, {1, 1}, {64, 64}); * ``` */ - class SubTexture2D { + class NxSubTexture2D { public: /** - * @brief Constructs a `SubTexture2D` from specified texture coordinates. + * @brief Constructs a `NxSubTexture2D` from specified texture coordinates. * * Initializes the subtexture by defining its bounds using normalized minimum * and maximum coordinates. The coordinates should be normalized to the texture's size * (values between 0 and 1). * - * @param texture A shared pointer to the base `Texture2D`. + * @param texture A shared pointer to the base `NxTexture2D`. * @param min The normalized minimum coordinates (bottom-left corner) of the subtexture. * @param max The normalized maximum coordinates (top-right corner) of the subtexture. */ - SubTexture2D(const std::shared_ptr &texture, const glm::vec2 &min, const glm::vec2 &max); + NxSubTexture2D(const std::shared_ptr &texture, const glm::vec2 &min, const glm::vec2 &max); - [[nodiscard]] const std::shared_ptr &getTexture() const { return m_texture; }; + [[nodiscard]] const std::shared_ptr &getTexture() const { return m_texture; }; /** * @brief Retrieves the texture coordinates for the subtexture. * @@ -68,21 +68,21 @@ namespace nexo::renderer { [[nodiscard]] const glm::vec2 *getTextureCoords() const { return m_texCoords; }; /** - * @brief Creates a `SubTexture2D` from grid-based coordinates within a texture. + * @brief Creates a `NxSubTexture2D` from grid-based coordinates within a texture. * * Dynamically calculates the normalized minimum and maximum texture coordinates * for a subtexture based on its position and size in a sprite sheet. * - * @param texture A shared pointer to the base `Texture2D`. + * @param texture A shared pointer to the base `NxTexture2D`. * @param coords The grid-based coordinates (e.g., sprite index in a sprite sheet). * @param cellSize The size of each cell (sprite) in the sprite sheet, in pixels. * @param spriteSize The size of the sprite in grid units, defaulting to {1, 1}. - * @return A shared pointer to the created `SubTexture2D` instance. + * @return A shared pointer to the created `NxSubTexture2D` instance. * * Example: * ```cpp - * auto texture = Texture2D::create("path/to/spritesheet.png"); - * auto subTexture = SubTexture2D::createFromCoords(texture, {1, 1}, {64, 64}); + * auto texture = NxTexture2D::create("path/to/spritesheet.png"); + * auto subTexture = NxSubTexture2D::createFromCoords(texture, {1, 1}, {64, 64}); * ``` * * Example Explanation: @@ -90,9 +90,9 @@ namespace nexo::renderer { * - Each cell in the grid is 64x64 pixels. * - The sprite occupies one grid cell by default. */ - static std::shared_ptr createFromCoords(const std::shared_ptr &texture, const glm::vec2 &coords, const glm::vec2 &cellSize, const glm::vec2 &spriteSize = {1, 1}); + static std::shared_ptr createFromCoords(const std::shared_ptr &texture, const glm::vec2 &coords, const glm::vec2 &cellSize, const glm::vec2 &spriteSize = {1, 1}); private: - std::shared_ptr m_texture; + std::shared_ptr m_texture; glm::vec2 m_texCoords[4]{}; }; -} \ No newline at end of file +} diff --git a/engine/src/renderer/Texture.cpp b/engine/src/renderer/Texture.cpp index 2c69e4137..9549dc75f 100644 --- a/engine/src/renderer/Texture.cpp +++ b/engine/src/renderer/Texture.cpp @@ -15,34 +15,64 @@ #include "Texture.hpp" #include "Renderer.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#include "String.hpp" + +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlTexture2D.hpp" #endif namespace nexo::renderer { - std::shared_ptr Texture2D::create(unsigned int width, unsigned int height) + NxTextureFormat NxTextureFormatFromString(const std::string_view& format) + { + if (iequals(format, "R8")) return NxTextureFormat::R8; + if (iequals(format, "RG8")) return NxTextureFormat::RG8; + if (iequals(format, "RGB8")) return NxTextureFormat::RGB8; + if (iequals(format, "RGBA8")) return NxTextureFormat::RGBA8; + return NxTextureFormat::INVALID; + } + + void NxTextureFormatConvertArgb8ToRgba8(uint8_t *bytes, const size_t size) + { + auto *pixels = reinterpret_cast(bytes); + const size_t width = size / 4; + + for (size_t i = 0; i < width; ++i) { + pixels[i] = (pixels[i] << 8) | (pixels[i] >> 24); + } + } + + std::shared_ptr NxTexture2D::create(unsigned int width, unsigned int height) + { + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(width, height); + #endif + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); + } + + std::shared_ptr NxTexture2D::create(const uint8_t *buffer, unsigned int width, unsigned int height, + NxTextureFormat format) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(width, height); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(buffer, width, height, format); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::shared_ptr Texture2D::create(uint8_t* buffer, unsigned int len) + std::shared_ptr NxTexture2D::create(const uint8_t* buffer, unsigned int len) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(buffer, len); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(buffer, len); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } - std::shared_ptr Texture2D::create(const std::string &path) + std::shared_ptr NxTexture2D::create(const std::string &path) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(path); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(path); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/Texture.hpp b/engine/src/renderer/Texture.hpp index 9dc878c71..b78016d94 100644 --- a/engine/src/renderer/Texture.hpp +++ b/engine/src/renderer/Texture.hpp @@ -13,13 +13,14 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include #include +#include +#include namespace nexo::renderer { /** - * @class Texture + * @class NxTexture * @brief Abstract base class for representing textures in a rendering system. * * The `Texture` class provides a common interface for managing texture resources @@ -31,16 +32,16 @@ namespace nexo::renderer { * - Manage texture data. * - Bind and unbind textures to specific texture slots. * - * Derived classes (e.g., `OpenGlTexture2D`) implement platform-specific behavior for + * Derived classes (e.g., `NxOpenGlTexture2D`) implement platform-specific behavior for * managing textures in different rendering backends. */ - class Texture { + class NxTexture { public: - virtual ~Texture() = default; + virtual ~NxTexture() = default; [[nodiscard]] virtual unsigned int getWidth() const = 0; [[nodiscard]] virtual unsigned int getHeight() const = 0; - virtual unsigned int getMaxTextureSize() const = 0; + [[nodiscard]] virtual unsigned int getMaxTextureSize() const = 0; [[nodiscard]] virtual unsigned int getId() const = 0; @@ -49,10 +50,80 @@ namespace nexo::renderer { virtual void setData(void *data, unsigned int size) = 0; - bool operator==(const Texture &other) const { return this->getId() == other.getId(); }; + bool operator==(const NxTexture &other) const { return this->getId() == other.getId(); }; }; - class Texture2D : public Texture { + /** + * @enum NxTextureFormat + * @brief Enumeration of texture formats. + * + * This enum defines various texture formats that can be used in the rendering system. + * Each format corresponds to a specific pixel layout and color depth. + * + * For example: + * - `R8` represents a single-channel texture with 8 bits per channel. + * - `RG8` represents a two-channel texture with 8 bits per channel. + * - `RGB8` represents a three-channel texture with 8 bits per channel. + * - `RGBA8` represents a four-channel texture with 8 bits per channel. + * + * @note If a texture format is invalid, it is represented by `INVALID`, which value is 0. + */ + enum class NxTextureFormat { + INVALID = 0, // Invalid texture format, used for error reporting + + R8 = 1, // 1 channel RED, 8 bits per channel + RG8, // 2 channels RED GREEN, 8 bits per channel + RGB8, // 3 channels RED GREEN BLUE, 8 bits per channel + RGBA8, // 4 channels RED GREEN BLUE ALPHA, 8 bits per channel + + _NB_FORMATS_ // Number of texture formats, used for array sizing + }; + + /** + * @brief Converts a NxTextureFormat enum value to its string representation. + * + * This function takes a NxTextureFormat enum value and returns its corresponding + * string representation. The string is a human-readable format name (e.g., "R8", "RGBA8"). + * + * @param format The NxTextureFormat enum value to convert. + * @return A string_view representing the format name. + */ + [[nodiscard]] constexpr std::string_view NxTextureFormatToString(const NxTextureFormat format) + { + switch (format) { + case NxTextureFormat::R8: return "R8"; + case NxTextureFormat::RG8: return "RG8"; + case NxTextureFormat::RGB8: return "RGB8"; + case NxTextureFormat::RGBA8: return "RGBA8"; + default: return "INVALID"; + } + } + + /** + * @brief Converts a string representation of a texture format to its NxTextureFormat enum value. + * + * This function takes a string representation of a texture format (e.g., "R8", "RGBA8") and + * returns the corresponding NxTextureFormat enum value. If the string does not match any + * known format, it returns NxTextureFormat::INVALID. + * + * @param format The string representation of the texture format. + * @return The corresponding NxTextureFormat enum value. + */ + NxTextureFormat NxTextureFormatFromString(const std::string_view &format); + + /** + * @brief Converts ARGB8 format to RGBA8 format in place. + * + * This function takes a pointer to a byte array and its size, and converts the + * pixel data from ARGB8 format to RGBA8 format. The conversion is done in place, + * meaning the original data will be modified. + * + * @param bytes Pointer to the byte array containing ARGB8 pixel data. + * @param size Size of the byte array in bytes. + */ + void NxTextureFormatConvertArgb8ToRgba8(uint8_t *bytes, size_t size); + + class NxTexture2D : public NxTexture { public: /** * @brief Creates a blank 2D texture with the specified dimensions. @@ -62,14 +133,44 @@ namespace nexo::renderer { * * @param width The width of the texture in pixels. * @param height The height of the texture in pixels. - * @return A shared pointer to the created `Texture2D` instance. + * @return A shared pointer to the created `NxTexture2D` instance. + * + * Example: + * ```cpp + * auto blankTexture = NxTexture2D::create(512, 512); + * ``` + */ + static std::shared_ptr create(unsigned int width, unsigned int height); + + + /** + * @brief Creates a 2D texture from raw pixel data in memory. + * + * Creates a texture from a raw pixel buffer with the specified dimensions and format. + * This is useful when you have direct access to pixel data that wasn't loaded through + * compressed images files or when you want to create textures from procedurally generated data. + * + * @param buffer Pointer to the raw pixel data. The buffer should contain pixel data + * in a format that matches the specified NxTextureFormat. The data consists + * of height scanlines of width pixels, with each pixel consisting of N components + * (where N depends on the format). The first pixel pointed to is bottom-left-most + * in the image. There is no padding between image scanlines or between pixels. + * Each component is an 8-bit unsigned value (uint8_t). + * @param width The width of the texture in pixels. + * @param height The height of the texture in pixels. + * @param format The format of the pixel data, which determines the number of components + * per pixel. + * @return A shared pointer to the created NxTexture2D instance. * * Example: * ```cpp - * auto blankTexture = Texture2D::create(512, 512); + * // Create a 128x128 RGBA texture with custom data + * std::vector pixelData(128 * 128 * 4); // 4 components (RGBA) + * // Fill pixelData with your custom values... + * auto texture = NxTexture2D::create(pixelData.data(), 128, 128, NxTextureFormat::RGBA8); * ``` */ - static std::shared_ptr create(unsigned int width, unsigned int height); + static std::shared_ptr create(const uint8_t *buffer, unsigned int width, unsigned int height, NxTextureFormat format); /** * @brief Creates a 2D texture from file in memory. @@ -80,15 +181,15 @@ namespace nexo::renderer { * * @param buffer The memory buffer containing the texture image data. * @param len The length of the memory buffer in bytes. - * @return A shared pointer to the created `Texture2D` instance. + * @return A shared pointer to the created `NxTexture2D` instance. * * Example: * ```cpp * std::vector imageData = ...; // Load image data into a buffer - * auto texture = Texture2D::create(imageData.data(), imageData.size()); + * auto texture = NxTexture2D::create(imageData.data(), imageData.size()); * ``` */ - static std::shared_ptr create(uint8_t *buffer, unsigned int len); + static std::shared_ptr create(const uint8_t* buffer, unsigned int len); /** * @brief Creates a 2D texture from an image file. @@ -98,14 +199,14 @@ namespace nexo::renderer { * for rendering after creation. * * @param path The file path to the texture image. - * @return A shared pointer to the created `Texture2D` instance. + * @return A shared pointer to the created `NxTexture2D` instance. * * Example: * ```cpp - * auto texture = Texture2D::create("assets/textures/brick_wall.png"); + * auto texture = NxTexture2D::create("assets/textures/brick_wall.png"); * ``` */ - static std::shared_ptr create(const std::string &path); + static std::shared_ptr create(const std::string &path); }; } diff --git a/engine/src/renderer/VertexArray.cpp b/engine/src/renderer/VertexArray.cpp index 6c0bc499c..c60076238 100644 --- a/engine/src/renderer/VertexArray.cpp +++ b/engine/src/renderer/VertexArray.cpp @@ -13,18 +13,18 @@ /////////////////////////////////////////////////////////////////////////////// #include "VertexArray.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlVertexArray.hpp" #endif namespace nexo::renderer { - std::shared_ptr createVertexArray() + std::shared_ptr createVertexArray() { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/VertexArray.hpp b/engine/src/renderer/VertexArray.hpp index 6b4b372a7..a0efef9bc 100644 --- a/engine/src/renderer/VertexArray.hpp +++ b/engine/src/renderer/VertexArray.hpp @@ -18,10 +18,10 @@ namespace nexo::renderer { /** - * @class VertexArray + * @class NxVertexArray * @brief Abstract class representing a vertex array in the rendering system. * - * The `VertexArray` class manages the collection of vertex buffers and an optional + * The `NxVertexArray` class manages the collection of vertex buffers and an optional * index buffer. It provides the interface for binding, unbinding, and configuring * vertex attributes in the rendering pipeline. * @@ -30,32 +30,32 @@ namespace nexo::renderer { * - Bind/unbind the vertex array for rendering. * - Provide access to underlying buffers. * - * Derived classes (e.g., `OpenGlVertexArray`) implement platform-specific behavior + * Derived classes (e.g., `NxOpenGlVertexArray`) implement platform-specific behavior * for managing vertex arrays. */ - class VertexArray { + class NxVertexArray { public: - virtual ~VertexArray() = default; + virtual ~NxVertexArray() = default; virtual void bind() const = 0; virtual void unbind() const = 0; - virtual void addVertexBuffer(const std::shared_ptr &vertexBuffer) = 0; - virtual void setIndexBuffer(const std::shared_ptr &indexBuffer) = 0; + virtual void addVertexBuffer(const std::shared_ptr &vertexBuffer) = 0; + virtual void setIndexBuffer(const std::shared_ptr &indexBuffer) = 0; - [[nodiscard]] virtual const std::vector> &getVertexBuffers() const = 0; - [[nodiscard]] virtual const std::shared_ptr &getIndexBuffer() const = 0; + [[nodiscard]] virtual const std::vector> &getVertexBuffers() const = 0; + [[nodiscard]] virtual const std::shared_ptr &getIndexBuffer() const = 0; - virtual unsigned int getId() const = 0; + [[nodiscard]] virtual unsigned int getId() const = 0; }; /** * @brief Factory function to create a platform-specific vertex array object. * * Depending on the graphics API (e.g., OpenGL), this function creates an instance - * of the corresponding `VertexArray` implementation. + * of the corresponding `NxVertexArray` implementation. * - * @return A shared pointer to the created `VertexArray` instance. + * @return A shared pointer to the created `NxVertexArray` instance. */ - std::shared_ptr createVertexArray(); + std::shared_ptr createVertexArray(); } diff --git a/engine/src/renderer/Window.cpp b/engine/src/renderer/Window.cpp index 0ef65744e..aefad4add 100644 --- a/engine/src/renderer/Window.cpp +++ b/engine/src/renderer/Window.cpp @@ -14,17 +14,17 @@ #include "Window.hpp" #include "renderer/RendererExceptions.hpp" -#ifdef GRAPHICS_API_OPENGL +#ifdef NX_GRAPHICS_API_OPENGL #include "opengl/OpenGlWindow.hpp" #endif namespace nexo::renderer { - std::shared_ptr Window::create(int width, int height, const char *title) + std::shared_ptr NxWindow::create(int width, int height, const char *title) { - #ifdef GRAPHICS_API_OPENGL - return std::make_shared(width, height, title); + #ifdef NX_GRAPHICS_API_OPENGL + return std::make_shared(width, height, title); #endif - THROW_EXCEPTION(UnknownGraphicsApi, "UNKNOWN"); + THROW_EXCEPTION(NxUnknownGraphicsApi, "UNKNOWN"); } } diff --git a/engine/src/renderer/Window.hpp b/engine/src/renderer/Window.hpp index 8b8482857..976ffed41 100644 --- a/engine/src/renderer/Window.hpp +++ b/engine/src/renderer/Window.hpp @@ -30,7 +30,7 @@ namespace nexo::renderer { using MouseScrollCallback = std::function; using MouseMoveCallback = std::function; - struct WindowProperty + struct NxWindowProperty { unsigned int width; unsigned int height; @@ -44,14 +44,14 @@ namespace nexo::renderer { MouseScrollCallback mouseScrollCallback; MouseMoveCallback mouseMoveCallback; - WindowProperty(const unsigned int w, const unsigned h, const char * t) : width(w), height(h), title(t) {} + NxWindowProperty(const unsigned int w, const unsigned h, const char * t) : width(w), height(h), title(t) {} }; /** - * @class Window + * @class NxWindow * @brief Abstract class for managing window operations in the rendering system. * - * The `Window` class provides an interface for creating, configuring, and + * The `NxWindow` class provides an interface for creating, configuring, and * managing a window. It includes support for events like resizing, closing, * keyboard input, and mouse interactions. * @@ -60,14 +60,14 @@ namespace nexo::renderer { * - Handle window properties such as size, title, and VSync. * - Provide event handling through callbacks. * - * Derived classes (e.g., `OpenGlWindow`) implement platform-specific behavior + * Derived classes (e.g., `NxOpenGlWindow`) implement platform-specific behavior * for managing windows. */ - class Window { + class NxWindow { public: - Window() = default; + NxWindow() = default; - virtual ~Window() = default; + virtual ~NxWindow() = default; virtual void init() = 0; virtual void shutdown() = 0; @@ -92,14 +92,14 @@ namespace nexo::renderer { * @brief Factory function to create a platform-specific window. * * Depending on the graphics API (e.g., OpenGL), this function creates an - * instance of the corresponding `Window` implementation. + * instance of the corresponding `NxWindow` implementation. * * @param width Initial width of the window. * @param height Initial height of the window. * @param title Title of the window. - * @return A shared pointer to the created `Window` instance. + * @return A shared pointer to the created `NxWindow` instance. */ - static std::shared_ptr create(int width = 1920, int height = 1080, const char *title = "Nexo window"); + static std::shared_ptr create(int width = 1920, int height = 1080, const char *title = "Nexo window"); virtual void setErrorCallback(void *fctPtr) = 0; virtual void setResizeCallback(ResizeCallback callback) = 0; diff --git a/engine/src/renderer/opengl/OpenGlBuffer.cpp b/engine/src/renderer/opengl/OpenGlBuffer.cpp index 671785ef8..6028ec5bd 100644 --- a/engine/src/renderer/opengl/OpenGlBuffer.cpp +++ b/engine/src/renderer/opengl/OpenGlBuffer.cpp @@ -19,36 +19,36 @@ namespace nexo::renderer { // VERTEX BUFFER - OpenGlVertexBuffer::OpenGlVertexBuffer(const float *vertices, const unsigned int size) + NxOpenGlVertexBuffer::NxOpenGlVertexBuffer(const float *vertices, const unsigned int size) { glGenBuffers(1, &_id); glBindBuffer(GL_ARRAY_BUFFER, _id); glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW); } - OpenGlVertexBuffer::OpenGlVertexBuffer(const unsigned int size) + NxOpenGlVertexBuffer::NxOpenGlVertexBuffer(const unsigned int size) { glGenBuffers(1, &_id); glBindBuffer(GL_ARRAY_BUFFER, _id); glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_DRAW); } - OpenGlVertexBuffer::~OpenGlVertexBuffer() + NxOpenGlVertexBuffer::~NxOpenGlVertexBuffer() { glDeleteBuffers(1, &_id); } - void OpenGlVertexBuffer::bind() const + void NxOpenGlVertexBuffer::bind() const { glBindBuffer(GL_ARRAY_BUFFER, _id); } - void OpenGlVertexBuffer::unbind() const + void NxOpenGlVertexBuffer::unbind() const { glBindBuffer(GL_ARRAY_BUFFER, 0); } - void OpenGlVertexBuffer::setData(void *data, const unsigned int size) + void NxOpenGlVertexBuffer::setData(void *data, const unsigned int size) { glBindBuffer(GL_ARRAY_BUFFER, _id); glBufferSubData(GL_ARRAY_BUFFER, 0, size, data); @@ -57,34 +57,34 @@ namespace nexo::renderer { // INDEX BUFFER - OpenGlIndexBuffer::OpenGlIndexBuffer() + NxOpenGlIndexBuffer::NxOpenGlIndexBuffer() { glGenBuffers(1, &_id); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _id); } - OpenGlIndexBuffer::~OpenGlIndexBuffer() + NxOpenGlIndexBuffer::~NxOpenGlIndexBuffer() { glDeleteBuffers(1, &_id); } - void OpenGlIndexBuffer::bind() const + void NxOpenGlIndexBuffer::bind() const { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _id); } - void OpenGlIndexBuffer::unbind() const + void NxOpenGlIndexBuffer::unbind() const { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } - void OpenGlIndexBuffer::setData(unsigned int *indices, unsigned int count) + void NxOpenGlIndexBuffer::setData(unsigned int *indices, unsigned int count) { _count = count; glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), indices, GL_STATIC_DRAW); } - unsigned int OpenGlIndexBuffer::getCount() const + unsigned int NxOpenGlIndexBuffer::getCount() const { return _count; } diff --git a/engine/src/renderer/opengl/OpenGlBuffer.hpp b/engine/src/renderer/opengl/OpenGlBuffer.hpp index f960bcdbe..dfd71cca0 100644 --- a/engine/src/renderer/opengl/OpenGlBuffer.hpp +++ b/engine/src/renderer/opengl/OpenGlBuffer.hpp @@ -18,7 +18,7 @@ namespace nexo::renderer { - class OpenGlVertexBuffer final : public VertexBuffer { + class NxOpenGlVertexBuffer final : public NxVertexBuffer { public: /** * @brief Constructs a new vertex buffer and initializes it with vertex data. @@ -34,7 +34,7 @@ namespace nexo::renderer { * - `glBindBuffer`: Binds the buffer as the current vertex buffer (GL_ARRAY_BUFFER). * - `glBufferData`: Allocates GPU memory and uploads the vertex data. */ - OpenGlVertexBuffer(const float *vertices, unsigned int size); + NxOpenGlVertexBuffer(const float *vertices, unsigned int size); /** * @brief Constructs an empty vertex buffer with a specified size. @@ -52,7 +52,7 @@ namespace nexo::renderer { * Usage: * - Call `setData` later to populate the buffer with vertex data dynamically. */ - explicit OpenGlVertexBuffer(unsigned int size); + explicit NxOpenGlVertexBuffer(unsigned int size); /** * @brief Destroys the vertex buffer and releases GPU resources. @@ -62,7 +62,7 @@ namespace nexo::renderer { * OpenGL Calls: * - `glDeleteBuffers`: Deletes the buffer object associated with the buffer ID. */ - ~OpenGlVertexBuffer() override; + ~NxOpenGlVertexBuffer() override; /** * @brief Binds the vertex buffer as the active buffer in the OpenGL context. @@ -88,8 +88,8 @@ namespace nexo::renderer { */ void unbind() const override; - void setLayout(const BufferLayout &layout) override { _layout = layout; }; - [[nodiscard]] BufferLayout getLayout() const override { return _layout; }; + void setLayout(const NxBufferLayout &layout) override { _layout = layout; }; + [[nodiscard]] NxBufferLayout getLayout() const override { return _layout; }; /** * @brief Updates the data in the vertex buffer. @@ -113,10 +113,10 @@ namespace nexo::renderer { private: unsigned int _id{}; - BufferLayout _layout; + NxBufferLayout _layout; }; - class OpenGlIndexBuffer final : public IndexBuffer { + class NxOpenGlIndexBuffer final : public NxIndexBuffer { public: /** * @brief Constructs a new OpenGL index buffer. @@ -128,7 +128,7 @@ namespace nexo::renderer { * - `glGenBuffers`: Generates a new buffer object. * - `glBindBuffer`: Binds the buffer as the current index buffer (GL_ELEMENT_ARRAY_BUFFER). */ - OpenGlIndexBuffer(); + NxOpenGlIndexBuffer(); /** * @brief Destroys the index buffer and releases GPU resources. @@ -138,7 +138,7 @@ namespace nexo::renderer { * OpenGL Calls: * - `glDeleteBuffers`: Deletes the buffer object associated with the buffer ID. */ - ~OpenGlIndexBuffer() override; + ~NxOpenGlIndexBuffer() override; /** * @brief Binds the index buffer as the active buffer in the OpenGL context. diff --git a/engine/src/renderer/opengl/OpenGlFramebuffer.cpp b/engine/src/renderer/opengl/OpenGlFramebuffer.cpp index 2b8381cdd..290101184 100644 --- a/engine/src/renderer/opengl/OpenGlFramebuffer.cpp +++ b/engine/src/renderer/opengl/OpenGlFramebuffer.cpp @@ -19,7 +19,6 @@ #include #include - namespace nexo::renderer { static constexpr unsigned int sMaxFramebufferSize = 8192; @@ -30,14 +29,14 @@ namespace nexo::renderer { * Maps internal framebuffer texture formats (e.g., RGBA8, DEPTH24STENCIL8) to their * corresponding OpenGL formats. Used during texture attachment creation. * - * @param format The `FrameBufferTextureFormats` value to convert. + * @param format The `NxFrameBufferTextureFormats` value to convert. * @return The OpenGL format (GLenum) corresponding to the specified texture format, * or -1 if the format is invalid or unsupported. */ - static int framebufferTextureFormatToOpenGlInternalFormat(FrameBufferTextureFormats format) + static int framebufferTextureFormatToOpenGlInternalFormat(NxFrameBufferTextureFormats format) { constexpr GLenum internalFormats[] = {GL_NONE, GL_RGBA8, GL_RGBA16, GL_R32I, GL_DEPTH24_STENCIL8, GL_DEPTH24_STENCIL8}; - if (static_cast(format) == 0 || format >= FrameBufferTextureFormats::NB_TEXTURE_FORMATS) + if (static_cast(format) == 0 || format >= NxFrameBufferTextureFormats::NB_TEXTURE_FORMATS) return -1; return static_cast(internalFormats[static_cast(format)]); } @@ -172,27 +171,27 @@ namespace nexo::renderer { /** * @brief Checks if a texture format is a depth format. * - * Determines whether the specified `FrameBufferTextureFormats` value corresponds to + * Determines whether the specified `NxFrameBufferTextureFormats` value corresponds to * a depth or depth-stencil format. * * @param format The texture format to check. * @return True if the format is a depth format, false otherwise. */ - static bool isDepthFormat(const FrameBufferTextureFormats format) + static bool isDepthFormat(const NxFrameBufferTextureFormats format) { switch (format) { - case FrameBufferTextureFormats::DEPTH24STENCIL8: return true; + case NxFrameBufferTextureFormats::DEPTH24STENCIL8: return true; default: return false; } } - OpenGlFramebuffer::OpenGlFramebuffer(FramebufferSpecs specs) : m_specs(std::move(specs)) + NxOpenGlFramebuffer::NxOpenGlFramebuffer(NxFramebufferSpecs specs) : m_specs(std::move(specs)) { if (!m_specs.width || !m_specs.height) - THROW_EXCEPTION(FramebufferResizingFailed, "OPENGL", false, m_specs.width, m_specs.height); + THROW_EXCEPTION(NxFramebufferResizingFailed, "OPENGL", false, m_specs.width, m_specs.height); if (m_specs.width > sMaxFramebufferSize || m_specs.height > sMaxFramebufferSize) - THROW_EXCEPTION(FramebufferResizingFailed, "OPENGL", true, m_specs.width, m_specs.height); + THROW_EXCEPTION(NxFramebufferResizingFailed, "OPENGL", true, m_specs.width, m_specs.height); for (auto format: m_specs.attachments.attachments) { if (!isDepthFormat(format.textureFormat)) @@ -203,14 +202,14 @@ namespace nexo::renderer { invalidate(); } - OpenGlFramebuffer::~OpenGlFramebuffer() + NxOpenGlFramebuffer::~NxOpenGlFramebuffer() { glDeleteFramebuffers(1, &m_id); glDeleteTextures(static_cast(m_colorAttachments.size()), m_colorAttachments.data()); glDeleteTextures(1, &m_depthAttachment); } - void OpenGlFramebuffer::invalidate() + void NxOpenGlFramebuffer::invalidate() { if (m_id) { @@ -239,22 +238,22 @@ namespace nexo::renderer { const int glTextureInternalFormat = framebufferTextureFormatToOpenGlInternalFormat( m_colorAttachmentsSpecs[i].textureFormat); if (glTextureInternalFormat == -1) - THROW_EXCEPTION(FramebufferUnsupportedColorFormat, "OPENGL"); + THROW_EXCEPTION(NxFramebufferUnsupportedColorFormat, "OPENGL"); const int glTextureFormat = framebufferTextureFormatToOpenGlFormat(m_colorAttachmentsSpecs[i].textureFormat); if (glTextureFormat == -1) - THROW_EXCEPTION(FramebufferUnsupportedColorFormat, "OPENGL"); + THROW_EXCEPTION(NxFramebufferUnsupportedColorFormat, "OPENGL"); attachColorTexture(m_colorAttachments[i], m_specs.samples, glTextureInternalFormat, glTextureFormat, m_specs.width, m_specs.height, i); } } - if (m_depthAttachmentSpec.textureFormat != FrameBufferTextureFormats::NONE) + if (m_depthAttachmentSpec.textureFormat != NxFrameBufferTextureFormats::NONE) { createTextures(multisample, &m_depthAttachment, 1); bindTexture(multisample, m_depthAttachment); int glDepthFormat = framebufferTextureFormatToOpenGlInternalFormat(m_depthAttachmentSpec.textureFormat); if (glDepthFormat == -1) - THROW_EXCEPTION(FramebufferUnsupportedDepthFormat, "OPENGL"); + THROW_EXCEPTION(NxFramebufferUnsupportedDepthFormat, "OPENGL"); attachDepthTexture(m_depthAttachment, m_specs.samples, glDepthFormat, GL_DEPTH_STENCIL_ATTACHMENT, m_specs.width, m_specs.height); } @@ -262,7 +261,7 @@ namespace nexo::renderer { if (m_colorAttachments.size() > 1) { if (m_colorAttachments.size() >= 4) - THROW_EXCEPTION(FramebufferCreationFailed, "OPENGL"); + THROW_EXCEPTION(NxFramebufferCreationFailed, "OPENGL"); constexpr GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; @@ -271,12 +270,12 @@ namespace nexo::renderer { glDrawBuffer(GL_NONE); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - THROW_EXCEPTION(FramebufferCreationFailed, "OPENGL"); + THROW_EXCEPTION(NxFramebufferCreationFailed, "OPENGL"); glBindFramebuffer(GL_FRAMEBUFFER, 0); } - void OpenGlFramebuffer::bind() + void NxOpenGlFramebuffer::bind() { if (toResize) { @@ -291,38 +290,43 @@ namespace nexo::renderer { //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } - void OpenGlFramebuffer::unbind() + void NxOpenGlFramebuffer::unbind() { glBindFramebuffer(GL_FRAMEBUFFER, 0); } - unsigned int OpenGlFramebuffer::getFramebufferId() const + unsigned int NxOpenGlFramebuffer::getFramebufferId() const { return m_id; } - void OpenGlFramebuffer::resize(const unsigned int width, const unsigned int height) + void NxOpenGlFramebuffer::resize(const unsigned int width, const unsigned int height) { if (!width || !height) - THROW_EXCEPTION(FramebufferResizingFailed, "OPENGL", false, width, height); + THROW_EXCEPTION(NxFramebufferResizingFailed, "OPENGL", false, width, height); if (width > sMaxFramebufferSize || height > sMaxFramebufferSize) - THROW_EXCEPTION(FramebufferResizingFailed, "OPENGL", true, width, height); + THROW_EXCEPTION(NxFramebufferResizingFailed, "OPENGL", true, width, height); m_specs.width = width; m_specs.height = height; toResize = true; } - void OpenGlFramebuffer::getPixelWrapper(unsigned int attachementIndex, int x, int y, void *result, const std::type_info &ti) const + glm::vec2 NxOpenGlFramebuffer::getSize() const + { + return glm::vec2(m_specs.width, m_specs.height); + } + + void NxOpenGlFramebuffer::getPixelWrapper(unsigned int attachementIndex, int x, int y, void *result, const std::type_info &ti) const { // Add more types here when necessary if (ti == typeid(int)) *static_cast(result) = getPixelImpl(attachementIndex, x, y); else - THROW_EXCEPTION(FramebufferUnsupportedColorFormat, "OPENGL"); + THROW_EXCEPTION(NxFramebufferUnsupportedColorFormat, "OPENGL"); } - void OpenGlFramebuffer::clearAttachmentWrapper(unsigned int attachmentIndex, const void *value, const std::type_info &ti) const + void NxOpenGlFramebuffer::clearAttachmentWrapper(unsigned int attachmentIndex, const void *value, const std::type_info &ti) const { // Add more types here when necessary if (ti == typeid(int)) @@ -330,7 +334,7 @@ namespace nexo::renderer { else if (ti == typeid(glm::vec4)) clearAttachmentImpl(attachmentIndex, value); else - THROW_EXCEPTION(FramebufferUnsupportedColorFormat, "OPENGL"); + THROW_EXCEPTION(NxFramebufferUnsupportedColorFormat, "OPENGL"); } } diff --git a/engine/src/renderer/opengl/OpenGlFramebuffer.hpp b/engine/src/renderer/opengl/OpenGlFramebuffer.hpp index 5223d4dcf..b46bce374 100644 --- a/engine/src/renderer/opengl/OpenGlFramebuffer.hpp +++ b/engine/src/renderer/opengl/OpenGlFramebuffer.hpp @@ -37,15 +37,15 @@ namespace nexo::renderer { return 0; } - static int framebufferTextureFormatToOpenGlFormat(FrameBufferTextureFormats format) + static int framebufferTextureFormatToOpenGlFormat(NxFrameBufferTextureFormats format) { constexpr GLenum formats[] = {GL_NONE, GL_RGBA, GL_RGBA, GL_RED_INTEGER}; - if (static_cast(format) == 0 || format >= FrameBufferTextureFormats::DEPTH24STENCIL8) // Maybe change that later + if (static_cast(format) == 0 || format >= NxFrameBufferTextureFormats::DEPTH24STENCIL8) // Maybe change that later return -1; return static_cast(formats[static_cast(format)]); } - class OpenGlFramebuffer final : public Framebuffer { + class NxOpenGlFramebuffer final : public NxFramebuffer { public: /** * @brief Constructs an OpenGL framebuffer with the specified specifications. @@ -57,12 +57,12 @@ namespace nexo::renderer { * attachments, and sampling options. * * Throws: - * - FramebufferResizingFailed if the dimensions are invalid (e.g., zero or exceeding limits). - * - FramebufferUnsupportedColorFormat if the color attachment format is unsupported. - * - FramebufferUnsupportedDepthFormat if the depth attachment format is unsupported. - * - FramebufferCreationFailed if the framebuffer status is not complete. + * - NxFramebufferResizingFailed if the dimensions are invalid (e.g., zero or exceeding limits). + * - NxFramebufferUnsupportedColorFormat if the color attachment format is unsupported. + * - NxFramebufferUnsupportedDepthFormat if the depth attachment format is unsupported. + * - NxFramebufferCreationFailed if the framebuffer status is not complete. */ - explicit OpenGlFramebuffer(FramebufferSpecs specs); + explicit NxOpenGlFramebuffer(NxFramebufferSpecs specs); /** * @brief Destroys the OpenGL framebuffer and releases associated resources. @@ -74,7 +74,7 @@ namespace nexo::renderer { * - `glDeleteFramebuffers`: Deletes the framebuffer object. * - `glDeleteTextures`: Deletes the textures associated with color and depth attachments. */ - ~OpenGlFramebuffer() override; + ~NxOpenGlFramebuffer() override; /** * @brief Recreates the OpenGL framebuffer and its attachments. @@ -90,9 +90,9 @@ namespace nexo::renderer { * - Validates the framebuffer status. * * Throws: - * - FramebufferUnsupportedColorFormat if a specified color format is unsupported. - * - FramebufferUnsupportedDepthFormat if a specified depth format is unsupported. - * - FramebufferCreationFailed if the framebuffer is not complete. + * - NxFramebufferUnsupportedColorFormat if a specified color format is unsupported. + * - NxFramebufferUnsupportedDepthFormat if a specified depth format is unsupported. + * - NxFramebufferCreationFailed if the framebuffer is not complete. */ void invalidate(); @@ -134,10 +134,12 @@ namespace nexo::renderer { * @param height The new height of the framebuffer in pixels. * * Throws: - * - FramebufferResizingFailed if the new dimensions are zero or exceed the maximum supported size. + * - NxFramebufferResizingFailed if the new dimensions are zero or exceed the maximum supported size. */ void resize(unsigned int width, unsigned int height) override; + [[nodiscard]] glm::vec2 getSize() const override; + /** * @brief Reads a pixel value from a specified attachment. @@ -154,7 +156,7 @@ namespace nexo::renderer { T getPixelImpl(unsigned int attachmentIndex, int x, int y) const { if (attachmentIndex >= m_colorAttachments.size()) - THROW_EXCEPTION(FramebufferInvalidIndex, "OPENGL", attachmentIndex); + THROW_EXCEPTION(NxFramebufferInvalidIndex, "OPENGL", attachmentIndex); glReadBuffer(GL_COLOR_ATTACHMENT0 + attachmentIndex); @@ -183,7 +185,7 @@ namespace nexo::renderer { void clearAttachmentImpl(unsigned int attachmentIndex, const void *value) const { if (attachmentIndex >= m_colorAttachments.size()) - THROW_EXCEPTION(FramebufferInvalidIndex, "OPENGL", attachmentIndex); + THROW_EXCEPTION(NxFramebufferInvalidIndex, "OPENGL", attachmentIndex); auto &spec = m_colorAttachmentsSpecs[attachmentIndex]; constexpr GLenum type = getGLTypeFromTemplate(); @@ -191,20 +193,20 @@ namespace nexo::renderer { } void clearAttachmentWrapper(unsigned int attachmentIndex, const void *value, const std::type_info &ti) const override; - FramebufferSpecs &getSpecs() override {return m_specs;}; - [[nodiscard]] const FramebufferSpecs &getSpecs() const override {return m_specs;}; + NxFramebufferSpecs &getSpecs() override {return m_specs;}; + [[nodiscard]] const NxFramebufferSpecs &getSpecs() const override {return m_specs;}; [[nodiscard]] unsigned int getColorAttachmentId(const unsigned int index = 0) const override {return m_colorAttachments[index];}; [[nodiscard]] unsigned int getDepthAttachmentId() const override { return m_depthAttachment; } private: unsigned int m_id = 0; bool toResize = false; - FramebufferSpecs m_specs; + NxFramebufferSpecs m_specs; glm::vec4 m_clearColor = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - std::vector m_colorAttachmentsSpecs; - FrameBufferTextureSpecifications m_depthAttachmentSpec; + std::vector m_colorAttachmentsSpecs; + NxFrameBufferTextureSpecifications m_depthAttachmentSpec; std::vector m_colorAttachments; unsigned int m_depthAttachment = 0; diff --git a/engine/src/renderer/opengl/OpenGlRendererAPI.hpp b/engine/src/renderer/opengl/OpenGlRendererAPI.hpp index bcbbfbf57..9bf5acc7c 100644 --- a/engine/src/renderer/opengl/OpenGlRendererAPI.hpp +++ b/engine/src/renderer/opengl/OpenGlRendererAPI.hpp @@ -18,11 +18,11 @@ namespace nexo::renderer { /** - * @class OpenGlRendererApi + * @class NxOpenGlRendererApi * @brief Implementation of the RendererApi interface using OpenGL. * - * The `OpenGlRendererApi` class provides OpenGL-specific implementations for the - * methods defined in `RendererApi`. It interacts directly with OpenGL functions + * The `NxOpenGlRendererApi` class provides OpenGL-specific implementations for the + * methods defined in `NxRendererApi`. It interacts directly with OpenGL functions * to configure and manage rendering operations. * * Responsibilities: @@ -30,7 +30,7 @@ namespace nexo::renderer { * - Provide access to OpenGL-specific features like blending and depth testing. * - Issue draw calls for indexed geometry. */ - class OpenGlRendererApi final : public RendererApi { + class NxOpenGlRendererApi final : public NxRendererApi { public: /** * @brief Initializes the OpenGL renderer API. @@ -41,7 +41,7 @@ namespace nexo::renderer { * - Configuring maximum viewport dimensions. * * Throws: - * - GraphicsApiNotInitialized if OpenGL fails to initialize. + * - NxGraphicsApiNotInitialized if OpenGL fails to initialize. */ void init() override; @@ -57,7 +57,7 @@ namespace nexo::renderer { * @param height The height of the viewport in pixels. * * Throws: - * - GraphicsApiViewportResizingFailure if the dimensions exceed the maximum allowed size. + * - NxGraphicsApiViewportResizingFailure if the dimensions exceed the maximum allowed size. */ void setViewport(unsigned int x, unsigned int y, unsigned int width, unsigned int height) override; @@ -78,7 +78,7 @@ namespace nexo::renderer { * Resets the color and depth buffers using the current clear color and depth values. * * Throws: - * - GraphicsApiNotInitialized if OpenGL is not initialized. + * - NxGraphicsApiNotInitialized if OpenGL is not initialized. */ void clear() override; @@ -90,7 +90,7 @@ namespace nexo::renderer { * @param color A `glm::vec4` containing the red, green, blue, and alpha components of the clear color. * * Throws: - * - GraphicsApiNotInitialized if OpenGL is not initialized. + * - NxGraphicsApiNotInitialized if OpenGL is not initialized. */ void setClearColor(const glm::vec4 &color) override; @@ -102,23 +102,34 @@ namespace nexo::renderer { * @param depth A float value representing the clear depth. * * Throws: - * - GraphicsApiNotInitialized if OpenGL is not initialized. + * - NxGraphicsApiNotInitialized if OpenGL is not initialized. */ void setClearDepth(float depth) override; + void setDepthTest(bool enable) override; + void setDepthFunc(unsigned int func) override; + void setDepthMask(bool enable) override; + /** * @brief Renders indexed geometry using OpenGL. * - * Issues a draw call for indexed primitives using data from the specified `VertexArray`. + * Issues a draw call for indexed primitives using data from the specified `NxVertexArray`. * - * @param vertexArray A shared pointer to the `VertexArray` containing vertex and index data. + * @param vertexArray A shared pointer to the `NxVertexArray` containing vertex and index data. * @param indexCount The number of indices to draw. If zero, all indices in the buffer are used. * * Throws: - * - GraphicsApiNotInitialized if OpenGL is not initialized. - * - InvalidValue if the `vertexArray` is null. + * - NxGraphicsApiNotInitialized if OpenGL is not initialized. + * - NxInvalidValue if the `vertexArray` is null. */ - void drawIndexed(const std::shared_ptr &vertexArray, unsigned int indexCount = 0) override; + void drawIndexed(const std::shared_ptr &vertexArray, unsigned int indexCount = 0) override; + + void drawUnIndexed(unsigned int verticesCount) override; + + void setStencilTest(bool enable) override; + void setStencilMask(unsigned int mask) override; + void setStencilFunc(unsigned int func, int ref, unsigned int mask) override; + void setStencilOp(unsigned int sfail, unsigned int dpfail, unsigned int dppass) override; private: bool m_initialized = false; unsigned int m_maxWidth = 0; diff --git a/engine/src/renderer/opengl/OpenGlRendererApi.cpp b/engine/src/renderer/opengl/OpenGlRendererApi.cpp index c8270e210..50eaa31f1 100644 --- a/engine/src/renderer/opengl/OpenGlRendererApi.cpp +++ b/engine/src/renderer/opengl/OpenGlRendererApi.cpp @@ -11,6 +11,7 @@ // Description: Source file for renderer api class // /////////////////////////////////////////////////////////////////////////////// + #include #include @@ -21,16 +22,20 @@ namespace nexo::renderer { - void OpenGlRendererApi::init() + void NxOpenGlRendererApi::init() { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); - // glEnable(GL_CULL_FACE); - // glCullFace(GL_BACK); - // glFrontFace(GL_CCW); + glDepthMask(GL_TRUE); + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 0, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilMask(0xFF); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); int maxViewportSize[] = {0, 0}; glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportSize); m_maxWidth = static_cast(maxViewportSize[0]); @@ -39,51 +44,116 @@ namespace nexo::renderer { LOG(NEXO_DEV, "Opengl renderer api initialized"); } - void OpenGlRendererApi::setViewport(const unsigned int x, const unsigned int y, const unsigned int width, const unsigned int height) + void NxOpenGlRendererApi::setViewport(const unsigned int x, const unsigned int y, const unsigned int width, const unsigned int height) { if (!m_initialized) - THROW_EXCEPTION(GraphicsApiNotInitialized, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); if (!width || !height) - THROW_EXCEPTION(GraphicsApiViewportResizingFailure, "OPENGL", false, width, height); + THROW_EXCEPTION(NxGraphicsApiViewportResizingFailure, "OPENGL", false, width, height); if (width > m_maxWidth || height > m_maxHeight) - THROW_EXCEPTION(GraphicsApiViewportResizingFailure, "OPENGL", true, width, height); + THROW_EXCEPTION(NxGraphicsApiViewportResizingFailure, "OPENGL", true, width, height); glViewport(static_cast(x), static_cast(y), static_cast(width), static_cast(height)); } - void OpenGlRendererApi::getMaxViewportSize(unsigned int *width, unsigned int *height) + void NxOpenGlRendererApi::getMaxViewportSize(unsigned int *width, unsigned int *height) { *width = m_maxWidth; *height = m_maxHeight; } - void OpenGlRendererApi::clear() + void NxOpenGlRendererApi::clear() { if (!m_initialized) - THROW_EXCEPTION(GraphicsApiNotInitialized, "OPENGL"); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); } - void OpenGlRendererApi::setClearColor(const glm::vec4 &color) + void NxOpenGlRendererApi::setClearColor(const glm::vec4 &color) { if (!m_initialized) - THROW_EXCEPTION(GraphicsApiNotInitialized, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); glClearColor(color.r, color.g, color.b, color.a); } - void OpenGlRendererApi::setClearDepth(const float depth) + void NxOpenGlRendererApi::setClearDepth(const float depth) { if (!m_initialized) - THROW_EXCEPTION(GraphicsApiNotInitialized, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); glClearDepth(depth); } - void OpenGlRendererApi::drawIndexed(const std::shared_ptr &vertexArray, const unsigned int indexCount) + void NxOpenGlRendererApi::setDepthTest(const bool enable) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + if (enable) + glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); + } + + void NxOpenGlRendererApi::setDepthFunc(const unsigned int func) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glDepthFunc(func); + } + + void NxOpenGlRendererApi::setDepthMask(const bool enable) { if (!m_initialized) - THROW_EXCEPTION(GraphicsApiNotInitialized, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + if (enable) + glDepthMask(GL_TRUE); + else + glDepthMask(GL_FALSE); + } + + void NxOpenGlRendererApi::drawIndexed(const std::shared_ptr &vertexArray, const unsigned int indexCount) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); if (!vertexArray) - THROW_EXCEPTION(InvalidValue, "OPENGL", "Vertex array cannot be null"); + THROW_EXCEPTION(NxInvalidValue, "OPENGL", "Vertex array cannot be null"); const unsigned int count = indexCount ? vertexArray->getIndexBuffer()->getCount() : indexCount; glDrawElements(GL_TRIANGLES, static_cast(count), GL_UNSIGNED_INT, nullptr); } + + void NxOpenGlRendererApi::drawUnIndexed(const unsigned int verticesCount) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glDrawArrays(GL_TRIANGLES, 0, static_cast(verticesCount)); + } + + void NxOpenGlRendererApi::setStencilTest(const bool enable) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + if (enable) + glEnable(GL_STENCIL_TEST); + else + glDisable(GL_STENCIL_TEST); + } + + void NxOpenGlRendererApi::setStencilMask(const unsigned int mask) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glStencilMask(mask); + } + + void NxOpenGlRendererApi::setStencilFunc(const unsigned int func, const int ref, const unsigned int mask) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glStencilFunc(func, ref, mask); + } + + void NxOpenGlRendererApi::setStencilOp(const unsigned int sfail, const unsigned int dpfail, const unsigned int dppass) + { + if (!m_initialized) + THROW_EXCEPTION(NxGraphicsApiNotInitialized, "OPENGL"); + glStencilOp(sfail, dpfail, dppass); + } } diff --git a/engine/src/renderer/opengl/OpenGlShader.cpp b/engine/src/renderer/opengl/OpenGlShader.cpp index 70681f77e..4d66fb686 100644 --- a/engine/src/renderer/opengl/OpenGlShader.cpp +++ b/engine/src/renderer/opengl/OpenGlShader.cpp @@ -15,6 +15,7 @@ #include "OpenGlShader.hpp" #include "Exception.hpp" #include "Logger.hpp" +#include "Shader.hpp" #include "renderer/RendererExceptions.hpp" #include @@ -34,7 +35,7 @@ namespace nexo::renderer { return 0; } - OpenGlShader::OpenGlShader(const std::string &path) + NxOpenGlShader::NxOpenGlShader(const std::string &path) { const std::string src = readFile(path); auto shaderSources = preProcess(src, path); @@ -45,23 +46,25 @@ namespace nexo::renderer { const auto lastDot = path.rfind('.'); const auto count = lastDot == std::string::npos ? path.size() - lastSlash : lastDot - lastSlash; m_name = path.substr(lastSlash, count); + setupUniformLocations(); } - OpenGlShader::OpenGlShader(std::string name, const std::string_view &vertexSource, + NxOpenGlShader::NxOpenGlShader(std::string name, const std::string_view &vertexSource, const std::string_view &fragmentSource) : m_name(std::move(name)) { std::unordered_map preProcessedSource; preProcessedSource[GL_VERTEX_SHADER] = vertexSource; preProcessedSource[GL_FRAGMENT_SHADER] = fragmentSource; compile(preProcessedSource); + setupUniformLocations(); } - OpenGlShader::~OpenGlShader() + NxOpenGlShader::~NxOpenGlShader() { glDeleteProgram(m_id); } - std::unordered_map OpenGlShader::preProcess(const std::string_view &src, + std::unordered_map NxOpenGlShader::preProcess(const std::string_view &src, const std::string &filePath) { std::unordered_map shaderSources; @@ -74,18 +77,18 @@ namespace nexo::renderer { constexpr size_t typeTokenLength = 5; const size_t eol = src.find_first_of("\r\n", pos); if (eol == std::string::npos) - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Syntax error at line: " + std::to_string(currentLine), filePath); const size_t begin = pos + typeTokenLength + 1; std::string_view type = src.substr(begin, eol - begin); if (!shaderTypeFromString(type)) - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Invalid shader type encountered at line: " + std::to_string(currentLine), filePath); const size_t nextLinePos = src.find_first_not_of("\r\n", eol); if (nextLinePos == std::string::npos) - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Syntax error at line: " + std::to_string(currentLine), filePath); pos = src.find(typeToken, nextLinePos); @@ -100,13 +103,13 @@ namespace nexo::renderer { return shaderSources; } - void OpenGlShader::compile(const std::unordered_map &shaderSources) + void NxOpenGlShader::compile(const std::unordered_map &shaderSources) { // Vertex and fragment shaders are successfully compiled. // Now time to link them together into a program. // Get a program object. if (shaderSources.size() > 2) - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Only two shader type (vertex/fragment) are supported for now", ""); const GLuint program = glCreateProgram(); std::array glShaderIds{}; @@ -139,7 +142,7 @@ namespace nexo::renderer { // We don't need the shader anymore. glDeleteShader(shader); - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Opengl failed to compile the shader: " + std::string(infoLog.data()), ""); } glAttachShader(program, shader); @@ -168,7 +171,7 @@ namespace nexo::renderer { for (auto id: glShaderIds) glDeleteShader(id); - THROW_EXCEPTION(ShaderCreationFailed, "OPENGL", + THROW_EXCEPTION(NxShaderCreationFailed, "OPENGL", "Opengl failed to compile the shader: " + std::string(infoLog.data()), ""); } @@ -177,17 +180,27 @@ namespace nexo::renderer { glDetachShader(program, id); } - void OpenGlShader::bind() const + void NxOpenGlShader::setupUniformLocations() { glUseProgram(m_id); + for (const auto &[key, name] : ShaderUniformsName) { + const int loc = glGetUniformLocation(m_id, name.c_str()); + m_uniformLocations[key] = loc; + } + glUseProgram(0); } - void OpenGlShader::unbind() const + void NxOpenGlShader::bind() const + { + glUseProgram(m_id); + } + + void NxOpenGlShader::unbind() const { glUseProgram(0); } - bool OpenGlShader::setUniformFloat(const std::string &name, const float value) const + bool NxOpenGlShader::setUniformFloat(const std::string &name, const float value) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) @@ -197,7 +210,27 @@ namespace nexo::renderer { return true; } - bool OpenGlShader::setUniformFloat3(const std::string &name, const glm::vec3 &values) const + bool NxOpenGlShader::setUniformFloat(const NxShaderUniforms uniform, const float value) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + + glUniform1f(loc, value); + return true; + } + + bool NxOpenGlShader::setUniformFloat2(const std::string &name, const glm::vec2 &values) const + { + const int loc = glGetUniformLocation(m_id, name.c_str()); + if (loc == -1) + return false; + + glUniform2f(loc, values.x, values.y); + return true; + } + + bool NxOpenGlShader::setUniformFloat3(const std::string &name, const glm::vec3 &values) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) @@ -207,7 +240,17 @@ namespace nexo::renderer { return true; } - bool OpenGlShader::setUniformFloat4(const std::string &name, const glm::vec4 &values) const + bool NxOpenGlShader::setUniformFloat3(const NxShaderUniforms uniform, const glm::vec3 &values) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + + glUniform3f(loc, values.x, values.y, values.z); + return true; + } + + bool NxOpenGlShader::setUniformFloat4(const std::string &name, const glm::vec4 &values) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) @@ -217,7 +260,17 @@ namespace nexo::renderer { return true; } - bool OpenGlShader::setUniformMatrix(const std::string &name, const glm::mat4 &matrix) const + bool NxOpenGlShader::setUniformFloat4(const NxShaderUniforms uniform, const glm::vec4 &values) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + + glUniform4f(loc, values.x, values.y, values.z, values.w); + return true; + } + + bool NxOpenGlShader::setUniformMatrix(const std::string &name, const glm::mat4 &matrix) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) @@ -227,7 +280,17 @@ namespace nexo::renderer { return true; } - bool OpenGlShader::setUniformInt(const std::string &name, const int value) const + bool NxOpenGlShader::setUniformMatrix(const NxShaderUniforms uniform, const glm::mat4 &matrix) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + + glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(matrix)); + return true; + } + + bool NxOpenGlShader::setUniformInt(const std::string &name, const int value) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) @@ -237,34 +300,64 @@ namespace nexo::renderer { return true; } - bool OpenGlShader::setUniformIntArray(const std::string &name, const int *values, const unsigned int count) const + bool NxOpenGlShader::setUniformBool(const std::string &name, bool value) const { const int loc = glGetUniformLocation(m_id, name.c_str()); if (loc == -1) return false; + glUniform1i(loc, value); + return true; + } + + bool NxOpenGlShader::setUniformInt(const NxShaderUniforms uniform, const int value) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + + glUniform1i(loc, value); + return true; + } + + bool NxOpenGlShader::setUniformIntArray(const std::string &name, const int *values, const unsigned int count) const + { + const int loc = glGetUniformLocation(m_id, name.c_str()); + if (loc == -1) + return false; + + glUniform1iv(loc, static_cast(count), values); + return true; + } + + bool NxOpenGlShader::setUniformIntArray(const NxShaderUniforms uniform, const int *values, const unsigned int count) const + { + const int loc = m_uniformLocations.at(uniform); + if (loc == -1) + return false; + glUniform1iv(loc, static_cast(count), values); return true; } - void OpenGlShader::bindStorageBuffer(unsigned int index) const + void NxOpenGlShader::bindStorageBuffer(unsigned int index) const { if (index > m_storageBuffers.size()) - THROW_EXCEPTION(OutOfRangeException, index, m_storageBuffers.size()); + THROW_EXCEPTION(NxOutOfRangeException, index, m_storageBuffers.size()); m_storageBuffers[index]->bind(); } - void OpenGlShader::unbindStorageBuffer(unsigned int index) const + void NxOpenGlShader::unbindStorageBuffer(unsigned int index) const { if (index > m_storageBuffers.size()) - THROW_EXCEPTION(OutOfRangeException, index, m_storageBuffers.size()); + THROW_EXCEPTION(NxOutOfRangeException, index, m_storageBuffers.size()); m_storageBuffers[index]->unbind(); } - void OpenGlShader::bindStorageBufferBase(unsigned int index, unsigned int bindingLocation) const + void NxOpenGlShader::bindStorageBufferBase(unsigned int index, unsigned int bindingLocation) const { if (index > m_storageBuffers.size()) - THROW_EXCEPTION(OutOfRangeException, index, m_storageBuffers.size()); + THROW_EXCEPTION(NxOutOfRangeException, index, m_storageBuffers.size()); m_storageBuffers[index]->bindBase(bindingLocation); } } diff --git a/engine/src/renderer/opengl/OpenGlShader.hpp b/engine/src/renderer/opengl/OpenGlShader.hpp index 514bcc9c3..4422bc41b 100644 --- a/engine/src/renderer/opengl/OpenGlShader.hpp +++ b/engine/src/renderer/opengl/OpenGlShader.hpp @@ -19,10 +19,10 @@ namespace nexo::renderer { /** - * @class OpenGlShader + * @class NxOpenGlShader * @brief Implementation of the Shader interface using OpenGL. * - * The `OpenGlShader` class provides OpenGL-specific functionality for creating + * The `NxOpenGlShader` class provides OpenGL-specific functionality for creating * and managing shader programs. It supports setting uniform variables and compiling * shaders from source code or files. * @@ -31,7 +31,7 @@ namespace nexo::renderer { * - Set uniform variables for rendering operations. * - Manage the lifecycle of OpenGL shader programs. */ - class OpenGlShader final : public Shader { + class NxOpenGlShader final : public NxShader { public: /** * @brief Constructs a shader program from a source file. @@ -42,12 +42,12 @@ namespace nexo::renderer { * @param path The file path to the shader source code. * * Throws: - * - `FileNotFoundException` if the file cannot be found. - * - `ShaderCreationFailed` if shader compilation fails. + * - `NxFileNotFoundException` if the file cannot be found. + * - `NxShaderCreationFailed` if shader compilation fails. */ - OpenGlShader(const std::string &path); - OpenGlShader(std::string name, const std::string_view &vertexSource, const std::string_view &fragmentSource); - ~OpenGlShader() override; + explicit NxOpenGlShader(const std::string &path); + NxOpenGlShader(std::string name, const std::string_view &vertexSource, const std::string_view &fragmentSource); + ~NxOpenGlShader() override; /** * @brief Activates the shader program in OpenGL. @@ -57,13 +57,22 @@ namespace nexo::renderer { void bind() const override; void unbind() const override; - bool setUniformFloat(const std::string &name, const float value) const override; + bool setUniformFloat(const std::string &name, float value) const override; + bool setUniformFloat2(const std::string &name, const glm::vec2 &values) const override; bool setUniformFloat3(const std::string &name, const glm::vec3 &values) const override; bool setUniformFloat4(const std::string &name, const glm::vec4 &values) const override; bool setUniformMatrix(const std::string &name, const glm::mat4 &matrix) const override; + bool setUniformBool(const std::string &name, bool value) const override; bool setUniformInt(const std::string &name, int value) const override; bool setUniformIntArray(const std::string &name, const int *values, unsigned int count) const override; + bool setUniformFloat(NxShaderUniforms uniform, float value) const override; + bool setUniformFloat3(NxShaderUniforms uniform, const glm::vec3 &values) const override; + bool setUniformFloat4(NxShaderUniforms uniform, const glm::vec4 &values) const override; + bool setUniformMatrix(NxShaderUniforms uniform, const glm::mat4 &matrix) const override; + bool setUniformInt(NxShaderUniforms uniform, int value) const override; + bool setUniformIntArray(NxShaderUniforms uniform, const int *values, unsigned int count) const override; + void bindStorageBuffer(unsigned int index) const override; void bindStorageBufferBase(unsigned int index, unsigned int bindingLocation) const override; void unbindStorageBuffer(unsigned int index) const override; @@ -75,6 +84,7 @@ namespace nexo::renderer { unsigned int m_id = 0; static std::unordered_map preProcess(const std::string_view &src, const std::string &filePath); void compile(const std::unordered_map &shaderSources); + void setupUniformLocations(); }; } diff --git a/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.cpp b/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.cpp index c40ba1008..712f19c0b 100644 --- a/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.cpp +++ b/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.cpp @@ -16,7 +16,7 @@ #include "OpenGlShaderStorageBuffer.hpp" namespace nexo::renderer { - OpenGlShaderStorageBuffer::OpenGlShaderStorageBuffer(unsigned int size) + NxOpenGlShaderStorageBuffer::NxOpenGlShaderStorageBuffer(const unsigned int size) { glCreateBuffers(1, &m_id); glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_id); @@ -24,22 +24,22 @@ namespace nexo::renderer { glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } - void OpenGlShaderStorageBuffer::bind() const + void NxOpenGlShaderStorageBuffer::bind() const { glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_id); } - void OpenGlShaderStorageBuffer::bindBase(unsigned int bindingLocation) const + void NxOpenGlShaderStorageBuffer::bindBase(const unsigned int bindingLocation) const { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, bindingLocation, m_id); } - void OpenGlShaderStorageBuffer::unbind() const + void NxOpenGlShaderStorageBuffer::unbind() const { glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } - void OpenGlShaderStorageBuffer::setData(void* data, unsigned int size) + void NxOpenGlShaderStorageBuffer::setData(void* data, const unsigned int size) { glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_id); glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, size, data); diff --git a/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.hpp b/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.hpp index 6ac770ebc..c70259ef8 100644 --- a/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.hpp +++ b/engine/src/renderer/opengl/OpenGlShaderStorageBuffer.hpp @@ -16,10 +16,10 @@ #include "renderer/ShaderStorageBuffer.hpp" namespace nexo::renderer { - class OpenGlShaderStorageBuffer : public ShaderStorageBuffer { + class NxOpenGlShaderStorageBuffer final : public NxShaderStorageBuffer { public: - explicit OpenGlShaderStorageBuffer(unsigned int size); - ~OpenGlShaderStorageBuffer() override = default; + explicit NxOpenGlShaderStorageBuffer(unsigned int size); + ~NxOpenGlShaderStorageBuffer() override = default; void bind() const override; void bindBase(unsigned int bindingLocation) const override; @@ -30,6 +30,6 @@ namespace nexo::renderer { [[nodiscard]] unsigned int getId() const override { return m_id; }; private: - unsigned int m_id; + unsigned int m_id{}; }; } diff --git a/engine/src/renderer/opengl/OpenGlTexture2D.cpp b/engine/src/renderer/opengl/OpenGlTexture2D.cpp index a712c64df..76a9843d3 100644 --- a/engine/src/renderer/opengl/OpenGlTexture2D.cpp +++ b/engine/src/renderer/opengl/OpenGlTexture2D.cpp @@ -22,115 +22,168 @@ namespace nexo::renderer { - OpenGlTexture2D::OpenGlTexture2D(const unsigned int width, const unsigned int height) : m_width(width), m_height(height) + NxOpenGlTexture2D::NxOpenGlTexture2D(const unsigned int width, const unsigned int height) : m_width(width), m_height(height) { - const unsigned int maxTextureSize = getMaxTextureSize(); - if (width > maxTextureSize || height > maxTextureSize) - THROW_EXCEPTION(TextureInvalidSize, "OPENGL", width, height, maxTextureSize); - m_internalFormat = GL_RGBA8; - m_dataFormat = GL_RGBA; + createOpenGLTexture(nullptr, width, height, GL_RGBA8, GL_RGBA); + } - glGenTextures(1, &m_id); - glBindTexture(GL_TEXTURE_2D, m_id); - glTexImage2D(GL_TEXTURE_2D, 0, static_cast(m_internalFormat), static_cast(width), static_cast(height), 0, m_dataFormat, GL_UNSIGNED_BYTE, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - glBindTexture(GL_TEXTURE_2D, 0); + NxOpenGlTexture2D::NxOpenGlTexture2D(const uint8_t *buffer, const unsigned int width, const unsigned int height, + const NxTextureFormat format) : m_width(width), m_height(height) + { + if (!buffer) + THROW_EXCEPTION(NxInvalidValue, "OPENGL", "Buffer is null"); + + GLint internalFormat = 0; + GLenum dataFormat = 0; + switch (format) { + [[likely]] case NxTextureFormat::RGBA8: + internalFormat = GL_RGBA8; + dataFormat = GL_RGBA; + break; + [[likely]] case NxTextureFormat::RGB8: + internalFormat = GL_RGB8; + dataFormat = GL_RGB; + break; + case NxTextureFormat::RG8: + internalFormat = GL_RG8; + dataFormat = GL_RG; + break; + case NxTextureFormat::R8: + internalFormat = GL_R8; + dataFormat = GL_RED; + break; + + default: + THROW_EXCEPTION(NxTextureUnsupportedFormat, "OPENGL", static_cast(format), ""); + } + + createOpenGLTexture(buffer, width, height, internalFormat, dataFormat); } - OpenGlTexture2D::OpenGlTexture2D(const std::string &path) + NxOpenGlTexture2D::NxOpenGlTexture2D(const std::string &path) + : m_path(path) { int width = 0; int height = 0; int channels = 0; //TODO: Set this conditionnaly based on the type of texture - //stbi_set_flip_vertically_on_load(1); + stbi_set_flip_vertically_on_load(1); stbi_uc *data = stbi_load(path.c_str(), &width, &height, &channels, 0); if (!data) - THROW_EXCEPTION(FileNotFoundException, path); - ingestDataFromStb(data, width, height, channels, path); + THROW_EXCEPTION(NxFileNotFoundException, path); + + try { + ingestDataFromStb(data, width, height, channels, path); + } catch (const Exception&) { + stbi_image_free(data); + throw; + } + stbi_image_free(data); } - OpenGlTexture2D::~OpenGlTexture2D() + NxOpenGlTexture2D::~NxOpenGlTexture2D() { glDeleteTextures(1, &m_id); } - OpenGlTexture2D::OpenGlTexture2D(const uint8_t* buffer, unsigned int len) + NxOpenGlTexture2D::NxOpenGlTexture2D(const uint8_t* buffer, const unsigned int len) { int width = 0; int height = 0; int channels = 0; //TODO: Set this conditionnaly based on the type of texture //stbi_set_flip_vertically_on_load(1); - stbi_uc *data = stbi_load_from_memory(buffer, len, &width, &height, &channels, 0); + stbi_uc *data = stbi_load_from_memory(buffer, static_cast(len), &width, &height, &channels, 0); if (!data) - THROW_EXCEPTION(TextureUnsupportedFormat, "OPENGL", channels, "(buffer)"); - ingestDataFromStb(data, width, height, channels, "(buffer)"); + THROW_EXCEPTION(NxTextureUnsupportedFormat, "OPENGL", channels, "(buffer)"); + + try { + ingestDataFromStb(data, width, height, channels, "(buffer)"); + } catch (const Exception&) { + stbi_image_free(data); + throw; + } + stbi_image_free(data); } - unsigned int OpenGlTexture2D::getMaxTextureSize() const + unsigned int NxOpenGlTexture2D::getMaxTextureSize() const { int maxTextureSize = 0; glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); return static_cast(maxTextureSize); } - void OpenGlTexture2D::setData(void *data, const unsigned int size) + void NxOpenGlTexture2D::setData(void *data, const unsigned int size) { if (const unsigned int expectedSize = m_width * m_height * (m_dataFormat == GL_RGBA ? 4 : 3); size != expectedSize) - THROW_EXCEPTION(TextureSizeMismatch, "OPENGL", size, expectedSize); + THROW_EXCEPTION(NxTextureSizeMismatch, "OPENGL", size, expectedSize); glBindTexture(GL_TEXTURE_2D, m_id); // Update the entire texture with new data glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(m_width), static_cast(m_height), m_dataFormat, GL_UNSIGNED_BYTE, data); glBindTexture(GL_TEXTURE_2D, 0); } - void OpenGlTexture2D::ingestDataFromStb(uint8_t* data, int width, int height, int channels, + void NxOpenGlTexture2D::ingestDataFromStb(const uint8_t* data, const int width, const int height, const int channels, const std::string& debugPath) { - m_width = width; - m_height = height; - - GLenum internalFormat = 0; + GLint internalFormat = 0; GLenum dataFormat = 0; - - if (channels == 4) { - internalFormat = GL_RGBA8; - dataFormat = GL_RGBA; - } else if (channels == 3) { - internalFormat = GL_RGB8; - dataFormat = GL_RGB; - } else { - stbi_image_free(data); - THROW_EXCEPTION(TextureUnsupportedFormat, "OPENGL", channels, debugPath); + switch (channels) { + [[likely]] case 4: // red, green, blue, alpha + internalFormat = GL_RGBA8; + dataFormat = GL_RGBA; + break; + [[likely]] case 3: // red, green, blue + internalFormat = GL_RGB8; + dataFormat = GL_RGB; + break; + case 2: // grey, alpha + internalFormat = GL_RG8; + dataFormat = GL_RG; + break; + case 1: // grey + internalFormat = GL_R8; + dataFormat = GL_RED; + break; + default: + THROW_EXCEPTION(NxTextureUnsupportedFormat, "OPENGL", channels, debugPath); } + createOpenGLTexture(data, static_cast(width), static_cast(height), internalFormat, dataFormat); + } + + void NxOpenGlTexture2D::createOpenGLTexture(const uint8_t* buffer, const unsigned int width, const unsigned int height, + const GLint internalFormat, const GLenum dataFormat) + { + const unsigned int maxTextureSize = getMaxTextureSize(); + if (width > maxTextureSize || height > maxTextureSize) + THROW_EXCEPTION(NxTextureInvalidSize, "OPENGL", width, height, maxTextureSize); + + const auto glWidth = static_cast(width); + const auto glHeight = static_cast(height); + m_internalFormat = internalFormat; m_dataFormat = dataFormat; + m_width = width; + m_height = height; glGenTextures(1, &m_id); glBindTexture(GL_TEXTURE_2D, m_id); - glTexImage2D(GL_TEXTURE_2D, 0, static_cast(internalFormat), width, height, 0, dataFormat, GL_UNSIGNED_BYTE, data); + glTexImage2D(GL_TEXTURE_2D, 0, m_internalFormat, glWidth, glHeight, 0, m_dataFormat, GL_UNSIGNED_BYTE, buffer); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glBindTexture(GL_TEXTURE_2D, 0); - - stbi_image_free(data); } - - void OpenGlTexture2D::bind(const unsigned int slot) const + void NxOpenGlTexture2D::bind(const unsigned int slot) const { glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(GL_TEXTURE_2D, m_id); } - void OpenGlTexture2D::unbind(const unsigned int slot) const + void NxOpenGlTexture2D::unbind(const unsigned int slot) const { glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(GL_TEXTURE_2D, 0); diff --git a/engine/src/renderer/opengl/OpenGlTexture2D.hpp b/engine/src/renderer/opengl/OpenGlTexture2D.hpp index 82919ec78..f9e9e14ee 100644 --- a/engine/src/renderer/opengl/OpenGlTexture2D.hpp +++ b/engine/src/renderer/opengl/OpenGlTexture2D.hpp @@ -19,10 +19,10 @@ namespace nexo::renderer { /** - * @class OpenGlTexture2D - * @brief OpenGL-specific implementation of the `Texture2D` class. + * @class NxOpenGlTexture2D + * @brief OpenGL-specific implementation of the `NxTexture2D` class. * - * The `OpenGlTexture2D` class manages 2D textures in an OpenGL rendering context. + * The `NxOpenGlTexture2D` class manages 2D textures in an OpenGL rendering context. * It supports texture creation, data uploading, and binding/unbinding operations. * * Responsibilities: @@ -30,9 +30,9 @@ namespace nexo::renderer { * - Load texture data from files or raw memory. * - Provide texture binding and unbinding functionality. */ - class OpenGlTexture2D final : public Texture2D { + class NxOpenGlTexture2D final : public NxTexture2D { public: - ~OpenGlTexture2D() override; + ~NxOpenGlTexture2D() override; /** * @brief Loads an OpenGL 2D texture from an image file. @@ -42,15 +42,15 @@ namespace nexo::renderer { * internal and data formats based on the number of channels in the image. * * @param path The file path to the texture image. - * @throw FileNotFoundException If the file cannot be found. - * @throw TextureUnsupportedFormat If the image format is unsupported. + * @throw NxFileNotFoundException If the file cannot be found. + * @throw NxTextureUnsupportedFormat If the image format is unsupported. * * Example: * ```cpp - * auto texture = std::make_shared("textures/wood.jpg"); + * auto texture = std::make_shared("textures/wood.jpg"); * ``` */ - explicit OpenGlTexture2D(const std::string &path); + explicit NxOpenGlTexture2D(const std::string &path); /** * @brief Creates a blank OpenGL 2D texture with the specified dimensions. @@ -63,13 +63,51 @@ namespace nexo::renderer { * * Example: * ```cpp - * auto texture = std::make_shared(256, 256); + * auto texture = std::make_shared(256, 256); * ``` */ - OpenGlTexture2D(unsigned int width, unsigned int height); + NxOpenGlTexture2D(unsigned int width, unsigned int height); /** - * @brief Creates a OpenGL 2D texture from file in memory. + * @brief Creates an OpenGL 2D texture from raw pixel data. + * + * Creates a texture from a raw pixel buffer with the specified dimensions and format. + * This is useful for creating textures from procedurally generated data or when working + * with raw pixel data from other sources. + * + * @param buffer Pointer to the raw pixel data. The buffer should contain pixel data + * in a format that matches the specified NxTextureFormat. The data consists + * of height scanlines of width pixels, with each pixel consisting of N components + * (where N depends on the format). The first pixel pointed to is bottom-left-most + * in the image. There is no padding between image scanlines or between pixels. + * Each component is an 8-bit unsigned value (uint8_t). + * @param width The width of the texture in pixels. Must not exceed the maximum texture + * size supported by the GPU. + * @param height The height of the texture in pixels. Must not exceed the maximum texture + * size supported by the GPU. + * @param format The format of the pixel data, which determines the number of components + * per pixel and the internal OpenGL representation: + * - NxTextureFormat::R8: Single channel (GL_R8/GL_RED) + * - NxTextureFormat::RG8: Two channels (GL_RG8/GL_RG) + * - NxTextureFormat::RGB8: Three channels (GL_RGB8/GL_RGB) + * - NxTextureFormat::RGBA8: Four channels (GL_RGBA8/GL_RGBA) + * @return A shared pointer to the created NxTexture2D instance. + * + * @throws NxInvalidValue If the buffer is null. + * @throws NxTextureUnsupportedFormat If the specified format is not supported. + * @throws NxTextureInvalidSize If the dimensions exceed the maximum texture size. + * + * Example: + * ```cpp + * // Create a 256x256 RGBA texture with custom data + * std::vector pixelData(256 * 256 * 4, 255); // 4 components (RGBA) + * auto texture = std::make_shared(pixelData.data(), 256, 256, NxTextureFormat::RGBA8); + * ``` + */ + NxOpenGlTexture2D(const uint8_t *buffer, unsigned int width, unsigned int height, NxTextureFormat format); + + /** + * @brief Creates an OpenGL 2D texture from a file in memory (raw content). * * Loads the texture data from the specified memory buffer. The buffer must contain * image data in a supported format (e.g., PNG, JPG). The texture will be ready @@ -81,10 +119,10 @@ namespace nexo::renderer { * Example: * ```cpp * std::vector imageData = ...; // Load image data into a buffer - * auto texture = std::make_shared(imageData.data(), imageData.size()); + * auto texture = std::make_shared(imageData.data(), imageData.size()); * ``` */ - OpenGlTexture2D(const uint8_t *buffer, unsigned int len); + NxOpenGlTexture2D(const uint8_t *buffer, unsigned int len); [[nodiscard]] unsigned int getWidth() const override {return m_width;}; [[nodiscard]] unsigned int getHeight() const override {return m_height;}; @@ -97,7 +135,7 @@ namespace nexo::renderer { * * @return The maximum texture size in pixels. */ - unsigned int getMaxTextureSize() const override; + [[nodiscard]] unsigned int getMaxTextureSize() const override; [[nodiscard]] unsigned int getId() const override {return m_id;}; @@ -138,7 +176,7 @@ namespace nexo::renderer { * * @param data A pointer to the pixel data. * @param size The size of the data in bytes. - * @throw TextureSizeMismatch If the size of the data does not match the texture's dimensions. + * @throw NxTextureSizeMismatch If the size of the data does not match the texture's dimensions. * * Example: * ```cpp @@ -166,13 +204,29 @@ namespace nexo::renderer { * ingestDataFromStb(data, width, height, channels, path); * ``` */ - void ingestDataFromStb(uint8_t *data, int width, int height, int channels, const std::string& debugPath = "(buffer)"); + void ingestDataFromStb(const uint8_t *data, int width, int height, int channels, const std::string& debugPath = "(buffer)"); + + /** + * @brief Creates an OpenGL texture with the specified parameters. + * + * @param buffer Pointer to the texture data (can be nullptr, as per OpenGL spec). + * @param width Width of the texture. + * @param height Height of the texture. + * @param internalFormat Internal format of the texture. + * @param dataFormat Data format of the texture. + * + * Example: + * ```cpp + * createOpenGLTexture(buffer, width, height, GL_RGBA8, GL_RGBA); + * ``` + */ + void createOpenGLTexture(const uint8_t* buffer, unsigned int width, unsigned int height, GLint internalFormat, GLenum dataFormat); std::string m_path; - unsigned int m_width; - unsigned int m_height; + unsigned int m_width{}; + unsigned int m_height{}; unsigned int m_id{}; - GLenum m_internalFormat; - GLenum m_dataFormat; + GLint m_internalFormat{}; + GLenum m_dataFormat{}; }; } diff --git a/engine/src/renderer/opengl/OpenGlVertexArray.cpp b/engine/src/renderer/opengl/OpenGlVertexArray.cpp index 14ad3fe83..d5a2c61cc 100644 --- a/engine/src/renderer/opengl/OpenGlVertexArray.cpp +++ b/engine/src/renderer/opengl/OpenGlVertexArray.cpp @@ -21,73 +21,72 @@ namespace nexo::renderer { /** - * @brief Converts a `ShaderDataType` enum value to the corresponding OpenGL type. + * @brief Converts an `NxShaderDataType` enum value to the corresponding OpenGL type. * * @param type The shader data type to convert. * @return The OpenGL equivalent type (e.g., `GL_FLOAT`). * * Example: * ```cpp - * GLenum glType = shaderDataTypeToOpenGltype(ShaderDataType::FLOAT3); + * GLenum glType = nxShaderDataTypeToOpenGltype(NxShaderDataType::FLOAT3); * ``` */ - static GLenum shaderDataTypeToOpenGltype(const ShaderDataType type) + static GLenum nxShaderDataTypeToOpenGltype(const NxShaderDataType type) { switch (type) { - case ShaderDataType::FLOAT: return GL_FLOAT; - case ShaderDataType::FLOAT2: return GL_FLOAT; - case ShaderDataType::FLOAT3: return GL_FLOAT; - case ShaderDataType::FLOAT4: return GL_FLOAT; - case ShaderDataType::INT: return GL_INT; - case ShaderDataType::INT2: return GL_INT; - case ShaderDataType::INT3: return GL_INT; - case ShaderDataType::INT4: return GL_INT; - case ShaderDataType::MAT3: return GL_FLOAT; - case ShaderDataType::MAT4: return GL_FLOAT; - case ShaderDataType::BOOL: return GL_BOOL; + case NxShaderDataType::FLOAT: return GL_FLOAT; + case NxShaderDataType::FLOAT2: return GL_FLOAT; + case NxShaderDataType::FLOAT3: return GL_FLOAT; + case NxShaderDataType::FLOAT4: return GL_FLOAT; + case NxShaderDataType::INT: return GL_INT; + case NxShaderDataType::INT2: return GL_INT; + case NxShaderDataType::INT3: return GL_INT; + case NxShaderDataType::INT4: return GL_INT; + case NxShaderDataType::MAT3: return GL_FLOAT; + case NxShaderDataType::MAT4: return GL_FLOAT; + case NxShaderDataType::BOOL: return GL_BOOL; default: return 0; } } - static bool isInt(const ShaderDataType type) + static bool isInt(const NxShaderDataType type) { switch (type) { - case ShaderDataType::INT: return true; - case ShaderDataType::INT2: return true; - case ShaderDataType::INT3: return true; - case ShaderDataType::INT4: return true; - case ShaderDataType::BOOL: return true; + case NxShaderDataType::INT: return true; + case NxShaderDataType::INT2: return true; + case NxShaderDataType::INT3: return true; + case NxShaderDataType::INT4: return true; + case NxShaderDataType::BOOL: return true; default: return false; } - return false; } - OpenGlVertexArray::OpenGlVertexArray() + NxOpenGlVertexArray::NxOpenGlVertexArray() { glGenVertexArrays(1, &_id); } - void OpenGlVertexArray::bind() const + void NxOpenGlVertexArray::bind() const { glBindVertexArray(_id); } - void OpenGlVertexArray::unbind() const + void NxOpenGlVertexArray::unbind() const { glBindVertexArray(0); } - void OpenGlVertexArray::addVertexBuffer(const std::shared_ptr &vertexBuffer) + void NxOpenGlVertexArray::addVertexBuffer(const std::shared_ptr &vertexBuffer) { if (!vertexBuffer) - THROW_EXCEPTION(InvalidValue, "OPENGL", "Vertex buffer is null"); + THROW_EXCEPTION(NxInvalidValue, "OPENGL", "Vertex buffer is null"); glBindVertexArray(_id); vertexBuffer->bind(); if (vertexBuffer->getLayout().getElements().empty()) - THROW_EXCEPTION(BufferLayoutEmpty, "OPENGL"); + THROW_EXCEPTION(NxBufferLayoutEmpty, "OPENGL"); auto index = static_cast(!_vertexBuffers.empty() ? _vertexBuffers.back()->getLayout().getElements().size() @@ -100,7 +99,7 @@ namespace nexo::renderer { glVertexAttribIPointer( index, static_cast(element.getComponentCount()), - shaderDataTypeToOpenGltype(element.type), + nxShaderDataTypeToOpenGltype(element.type), static_cast(layout.getStride()), reinterpret_cast(static_cast(element.offset)) ); @@ -110,7 +109,7 @@ namespace nexo::renderer { glVertexAttribPointer( index, static_cast(element.getComponentCount()), - shaderDataTypeToOpenGltype(element.type), + nxShaderDataTypeToOpenGltype(element.type), element.normalized ? GL_TRUE : GL_FALSE, static_cast(layout.getStride()), reinterpret_cast(static_cast(element.offset)) @@ -121,27 +120,27 @@ namespace nexo::renderer { _vertexBuffers.push_back(vertexBuffer); } - void OpenGlVertexArray::setIndexBuffer(const std::shared_ptr &indexBuffer) + void NxOpenGlVertexArray::setIndexBuffer(const std::shared_ptr &indexBuffer) { if (!indexBuffer) - THROW_EXCEPTION(InvalidValue, "OPENGL", "Index buffer cannot be null"); + THROW_EXCEPTION(NxInvalidValue, "OPENGL", "Index buffer cannot be null"); glBindVertexArray(_id); indexBuffer->bind(); _indexBuffer = indexBuffer; } - const std::vector> &OpenGlVertexArray::getVertexBuffers() const + const std::vector> &NxOpenGlVertexArray::getVertexBuffers() const { return _vertexBuffers; } - const std::shared_ptr &OpenGlVertexArray::getIndexBuffer() const + const std::shared_ptr &NxOpenGlVertexArray::getIndexBuffer() const { return _indexBuffer; } - unsigned int OpenGlVertexArray::getId() const + unsigned int NxOpenGlVertexArray::getId() const { return _id; } diff --git a/engine/src/renderer/opengl/OpenGlVertexArray.hpp b/engine/src/renderer/opengl/OpenGlVertexArray.hpp index 94a43004f..051907575 100644 --- a/engine/src/renderer/opengl/OpenGlVertexArray.hpp +++ b/engine/src/renderer/opengl/OpenGlVertexArray.hpp @@ -19,10 +19,10 @@ namespace nexo::renderer { /** - * @class OpenGlVertexArray - * @brief OpenGL-specific implementation of the `VertexArray` class. + * @class NxOpenGlVertexArray + * @brief OpenGL-specific implementation of the `NxVertexArray` class. * - * The `OpenGlVertexArray` class manages vertex and index buffers in an OpenGL + * The `NxOpenGlVertexArray` class manages vertex and index buffers in an OpenGL * context. It handles the configuration of vertex attributes and facilitates * binding/unbinding of the vertex array for rendering. * @@ -31,7 +31,7 @@ namespace nexo::renderer { * - Configure vertex attributes using buffer layouts. * - Bind/unbind the VAO for rendering operations. */ - class OpenGlVertexArray final : public VertexArray { + class NxOpenGlVertexArray final : public NxVertexArray { public: /** * @brief Creates an OpenGL vertex array object (VAO). @@ -39,8 +39,8 @@ namespace nexo::renderer { * Initializes a new VAO and assigns it a unique ID. This ID is used to reference * the VAO in OpenGL operations. */ - OpenGlVertexArray(); - ~OpenGlVertexArray() override = default; + NxOpenGlVertexArray(); + ~NxOpenGlVertexArray() override = default; /** * @brief Binds the vertex array object (VAO) to the OpenGL context. @@ -65,10 +65,10 @@ namespace nexo::renderer { * buffer layout. The attributes are assigned sequential indices. * * @param vertexBuffer The vertex buffer to add. - * @throw InvalidValue If the vertex buffer is null. - * @throw BufferLayoutEmpty If the vertex buffer's layout is empty. + * @throw NxInvalidValue If the vertex buffer is null. + * @throw NxBufferLayoutEmpty If the vertex buffer's layout is empty. */ - void addVertexBuffer(const std::shared_ptr &vertexBuffer) override; + void addVertexBuffer(const std::shared_ptr &vertexBuffer) override; /** * @brief Sets the index buffer for the vertex array. @@ -76,17 +76,17 @@ namespace nexo::renderer { * Associates an index buffer with the vertex array, enabling indexed rendering. * * @param indexBuffer The index buffer to set. - * @throw InvalidValue If the index buffer is null. + * @throw NxInvalidValue If the index buffer is null. */ - void setIndexBuffer(const std::shared_ptr &indexBuffer) override; + void setIndexBuffer(const std::shared_ptr &indexBuffer) override; - [[nodiscard]] const std::vector> &getVertexBuffers() const override; - [[nodiscard]] const std::shared_ptr &getIndexBuffer() const override; + [[nodiscard]] const std::vector> &getVertexBuffers() const override; + [[nodiscard]] const std::shared_ptr &getIndexBuffer() const override; - unsigned int getId() const override; + [[nodiscard]] unsigned int getId() const override; private: - std::vector> _vertexBuffers; - std::shared_ptr _indexBuffer; + std::vector> _vertexBuffers; + std::shared_ptr _indexBuffer; unsigned int _id{}; }; diff --git a/engine/src/renderer/opengl/OpenGlWindow.cpp b/engine/src/renderer/opengl/OpenGlWindow.cpp index ef67b74f0..e6730e90b 100644 --- a/engine/src/renderer/opengl/OpenGlWindow.cpp +++ b/engine/src/renderer/opengl/OpenGlWindow.cpp @@ -27,17 +27,17 @@ namespace nexo::renderer { std::cerr << "[GLFW ERROR] Code : " << errorCode << " / Description : " << errorStr << std::endl; } - void OpenGlWindow::setupCallback() const + void NxOpenGlWindow::setupCallback() const { // Resize event glfwSetWindowSizeCallback(_openGlWindow, [](GLFWwindow *window, const int width, const int height) { if (width <= 0 || height <= 0) return; - auto *props = static_cast(glfwGetWindowUserPointer(window)); + auto *props = static_cast(glfwGetWindowUserPointer(window)); props->width = width; props->height = height; - Renderer::onWindowResize(width, height); + NxRenderer::onWindowResize(width, height); if (props->resizeCallback) props->resizeCallback(width, height); }); @@ -45,7 +45,7 @@ namespace nexo::renderer { // Close event glfwSetWindowCloseCallback(_openGlWindow, [](GLFWwindow *window) { - const auto *props = static_cast(glfwGetWindowUserPointer(window)); + const auto *props = static_cast(glfwGetWindowUserPointer(window)); if (props->closeCallback) props->closeCallback(); }); @@ -53,7 +53,7 @@ namespace nexo::renderer { // Keyboard events glfwSetKeyCallback(_openGlWindow, [](GLFWwindow *window, const int key, [[maybe_unused]]int scancode, const int action, const int mods) { - const auto *props = static_cast(glfwGetWindowUserPointer(window)); + const auto *props = static_cast(glfwGetWindowUserPointer(window)); if (props->keyCallback) props->keyCallback(key, action, mods); }); @@ -61,7 +61,7 @@ namespace nexo::renderer { // Mouse click callback glfwSetMouseButtonCallback(_openGlWindow, [](GLFWwindow *window, const int button, const int action, const int mods) { - const auto *props = static_cast(glfwGetWindowUserPointer(window)); + const auto *props = static_cast(glfwGetWindowUserPointer(window)); if (props->mouseClickCallback) props->mouseClickCallback(button, action, mods); }); @@ -69,7 +69,7 @@ namespace nexo::renderer { // Mouse scroll event glfwSetScrollCallback(_openGlWindow, [](GLFWwindow *window, const double xOffset, const double yOffset) { - const auto *props = static_cast(glfwGetWindowUserPointer(window)); + const auto *props = static_cast(glfwGetWindowUserPointer(window)); if (props->mouseScrollCallback) props->mouseScrollCallback(xOffset, yOffset); }); @@ -77,16 +77,16 @@ namespace nexo::renderer { // Mouse move event glfwSetCursorPosCallback(_openGlWindow, [](GLFWwindow *window, const double xpos, const double ypos) { - const auto *props = static_cast(glfwGetWindowUserPointer(window)); + const auto *props = static_cast(glfwGetWindowUserPointer(window)); if (props->mouseMoveCallback) props->mouseMoveCallback(xpos, ypos); }); } - void OpenGlWindow::init() + void NxOpenGlWindow::init() { if (!glfwInit()) - THROW_EXCEPTION(GraphicsApiInitFailure, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiInitFailure, "OPENGL"); LOG(NEXO_DEV, "Initializing opengl window"); glfwSetErrorCallback(glfwErrorCallback); @@ -109,7 +109,7 @@ namespace nexo::renderer { glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); _openGlWindow = glfwCreateWindow(static_cast(_props.width), static_cast(_props.height), _props.title, nullptr, nullptr); if (!_openGlWindow) - THROW_EXCEPTION(GraphicsApiWindowInitFailure, "OPENGL"); + THROW_EXCEPTION(NxGraphicsApiWindowInitFailure, "OPENGL"); glfwMakeContextCurrent(_openGlWindow); glfwSetWindowUserPointer(_openGlWindow, &_props); setVsync(true); @@ -117,19 +117,19 @@ namespace nexo::renderer { LOG(NEXO_DEV, "Opengl window ({}, {}) initialized", _props.width, _props.height); } - void OpenGlWindow::shutdown() + void NxOpenGlWindow::shutdown() { glfwDestroyWindow(_openGlWindow); glfwTerminate(); } - void OpenGlWindow::onUpdate() + void NxOpenGlWindow::onUpdate() { glfwSwapBuffers(_openGlWindow); glfwPollEvents(); } - void OpenGlWindow::setVsync(const bool enabled) + void NxOpenGlWindow::setVsync(const bool enabled) { if (enabled) glfwSwapInterval(1); @@ -138,23 +138,23 @@ namespace nexo::renderer { _props.vsync = enabled; } - bool OpenGlWindow::isVsync() const + bool NxOpenGlWindow::isVsync() const { return _props.vsync; } - void OpenGlWindow::getDpiScale(float *x, float *y) const + void NxOpenGlWindow::getDpiScale(float *x, float *y) const { glfwGetWindowContentScale(_openGlWindow, x, y); } - void OpenGlWindow::setWindowIcon(const std::filesystem::path& iconPath) + void NxOpenGlWindow::setWindowIcon(const std::filesystem::path& iconPath) { GLFWimage icon; const auto iconStringPath = iconPath.string(); icon.pixels = stbi_load(iconStringPath.c_str(), &icon.width, &icon.height, nullptr, 4); if (!icon.pixels) { - THROW_EXCEPTION(StbiLoadException, + THROW_EXCEPTION(NxStbiLoadException, std::format("Failed to load icon '{}': {}", iconStringPath, stbi_failure_reason())); } @@ -166,20 +166,20 @@ namespace nexo::renderer { stbi_image_free(icon.pixels); } - void OpenGlWindow::setErrorCallback(void *fctPtr) + void NxOpenGlWindow::setErrorCallback(void *fctPtr) { glfwSetErrorCallback(reinterpret_cast(fctPtr)); } // Linux specific method #ifdef __linux__ - void OpenGlWindow::setWaylandAppId(const char* appId) + void NxOpenGlWindow::setWaylandAppId(const char* appId) { _waylandAppId = appId; LOG(NEXO_DEV, "Wayland app id set to '{}'", appId); } - void OpenGlWindow::setWmClass(const char* className, const char* instanceName) + void NxOpenGlWindow::setWmClass(const char* className, const char* instanceName) { _x11ClassName = className; _x11InstanceName = instanceName; diff --git a/engine/src/renderer/opengl/OpenGlWindow.hpp b/engine/src/renderer/opengl/OpenGlWindow.hpp index bcf1ad5c7..73f4c1023 100644 --- a/engine/src/renderer/opengl/OpenGlWindow.hpp +++ b/engine/src/renderer/opengl/OpenGlWindow.hpp @@ -19,10 +19,10 @@ namespace nexo::renderer { /** - * @class OpenGlWindow - * @brief OpenGL-specific implementation of the `Window` class. + * @class NxOpenGlWindow + * @brief OpenGL-specific implementation of the `NxWindow` class. * - * The `OpenGlWindow` class manages the creation and behavior of a window in + * The `NxOpenGlWindow` class manages the creation and behavior of a window in * an OpenGL context. It integrates with GLFW for window management and event * handling. * @@ -31,19 +31,19 @@ namespace nexo::renderer { * - Provide event handling for window, keyboard, and mouse events. * - Manage OpenGL context initialization and VSync settings. */ - class OpenGlWindow final : public Window { + class NxOpenGlWindow final : public NxWindow { public: /** * @brief Creates an OpenGL window with the specified properties. * - * Initializes the `WindowProperty` structure with the given width, height, + * Initializes the `NxWindowProperty` structure with the given width, height, * and title. The window itself is created during the `init()` call. * * @param width Initial width of the window. * @param height Initial height of the window. * @param title Title of the window. */ - explicit OpenGlWindow(const int width = 1920, + explicit NxOpenGlWindow(const int width = 1920, const int height = 1080, const char *title = "Nexo window") : _props(width, height, title) {} @@ -54,8 +54,8 @@ namespace nexo::renderer { * Creates the window using GLFW, sets up the OpenGL context, and configures * callbacks for handling window events like resizing, closing, and input. * - * @throw GraphicsApiInitFailure If GLFW initialization fails. - * @throw GraphicsApiWindowInitFailure If the window creation fails. + * @throw NxGraphicsApiInitFailure If GLFW initialization fails. + * @throw NxGraphicsApiWindowInitFailure If the window creation fails. */ void init() override; @@ -116,7 +116,7 @@ namespace nexo::renderer { #endif private: GLFWwindow *_openGlWindow{}; - WindowProperty _props; + NxWindowProperty _props; void setupCallback() const; }; diff --git a/engine/src/renderer/primitives/Billboard.cpp b/engine/src/renderer/primitives/Billboard.cpp new file mode 100644 index 000000000..c561f4be1 --- /dev/null +++ b/engine/src/renderer/primitives/Billboard.cpp @@ -0,0 +1,218 @@ +//// Billboard.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 17/04/2025 +// Description: Source file for the billboard render function +// +/////////////////////////////////////////////////////////////////////////////// + +#include "renderer/Renderer3D.hpp" +#include "renderer/RendererExceptions.hpp" + +#include +#include +#include +#define GLM_ENABLE_EXPERIMENTAL +#include +#include + +namespace nexo::renderer { + // Quad vertices for a 1x1 billboard centered at origin + constexpr glm::vec3 billboardPositions[4] = { + {-0.5f, -0.5f, 0.0f}, // Bottom left + {0.5f, -0.5f, 0.0f}, // Bottom right + {0.5f, 0.5f, 0.0f}, // Top right + {-0.5f, 0.5f, 0.0f} // Top left + }; + + constexpr unsigned int billboardIndices[6] = { + 0, 1, 2, 2, 3, 0 // Two triangles forming a quad + }; + + constexpr glm::vec2 billboardTexCoords[4] = { + {0.0f, 0.0f}, // Bottom left + {1.0f, 0.0f}, // Bottom right + {1.0f, 1.0f}, // Top right + {0.0f, 1.0f}, // Top left + }; + + /** + * @brief Generates the vertex, texture coordinate, and normal data for a billboard mesh. + * + * Fills the provided arrays with 6 vertices, texture coordinates, and normals for a billboard quad. + * + * @param vertices Array to store generated vertex positions. + * @param texCoords Array to store generated texture coordinates. + * @param normals Array to store generated normals. + */ + static void genBillboardMesh(std::array &vertices, std::array &texCoords, std::array &normals) + { + // Vertex positions + vertices[0] = billboardPositions[0]; // Bottom left + vertices[1] = billboardPositions[1]; // Bottom right + vertices[2] = billboardPositions[2]; // Top right + vertices[3] = billboardPositions[2]; // Top right + vertices[4] = billboardPositions[3]; // Top left + vertices[5] = billboardPositions[0]; // Bottom left + + // Texture coordinates + texCoords[0] = billboardTexCoords[0]; // Bottom left + texCoords[1] = billboardTexCoords[1]; // Bottom right + texCoords[2] = billboardTexCoords[2]; // Top right + texCoords[3] = billboardTexCoords[2]; // Top right + texCoords[4] = billboardTexCoords[3]; // Top left + texCoords[5] = billboardTexCoords[0]; // Bottom left + + // All normals point forward for billboard (will be transformed to face camera) + for (int i = 0; i < 6; ++i) { + normals[i] = {0.0f, 0.0f, 1.0f}; + } + } + + /** + * @brief Calculates a billboard rotation matrix that makes the quad face the camera. + * + * @param billboardPosition The position of the billboard in world space. + * @param cameraPosition The position of the camera in world space. + * @param cameraUp The up vector of the camera (usually {0,1,0}). + * @param constrainToY Whether to only rotate around Y-axis (true) or do full rotation (false). + * @return glm::mat4 The rotation matrix for the billboard. + */ + static glm::mat4 calculateBillboardRotation( + const glm::vec3& billboardPosition, + const glm::vec3& cameraPosition, + const glm::vec3& cameraUp = glm::vec3(0.0f, 1.0f, 0.0f), + bool constrainToY = false) + { + glm::vec3 look = glm::normalize(cameraPosition - billboardPosition); + + if (constrainToY) { + look.y = 0.0f; + look = glm::normalize(look); + + glm::vec3 right = glm::normalize(glm::cross(cameraUp, look)); + glm::vec3 up = glm::cross(look, right); + + return {glm::vec4(right, 0.0f), + glm::vec4(up, 0.0f), + glm::vec4(-look, 0.0f), // Negative look preserves winding + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) + }; + } + const glm::vec3 right = glm::normalize(glm::cross(cameraUp, look)); + const glm::vec3 up = glm::cross(look, right); + + return {glm::vec4(right, 0.0f), + glm::vec4(up, 0.0f), + glm::vec4(-look, 0.0f), // Negative look preserves winding + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) + }; + } + + void NxRenderer3D::drawBillboard( + const glm::vec3& position, + const glm::vec2& size, + const glm::vec4& color, + const int entityID) const + { + if (!m_renderingScene) + { + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, + "Renderer not rendering a scene, make sure to call beginScene first"); + } + + const glm::vec3 cameraPos = m_storage->cameraPosition; + + const glm::mat4 billboardRotation = calculateBillboardRotation(position, cameraPos); + + const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * + billboardRotation * + glm::scale(glm::mat4(1.0f), glm::vec3(size.x, size.y, 1.0f)); + + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); + + NxIndexedMaterial mat; + mat.albedoColor = color; + setMaterialUniforms(mat); + + std::array verts{}; + std::array texCoords{}; + std::array normals{}; + std::array indices{}; + + genBillboardMesh(verts, texCoords, normals); + for (unsigned int i = 0; i < 6; ++i) + indices[i] = i; + + for (unsigned int i = 0; i < 6; ++i) + { + m_storage->vertexBufferPtr->position = glm::vec4(verts[i], 1.0f); + m_storage->vertexBufferPtr->texCoord = texCoords[i]; + m_storage->vertexBufferPtr->normal = normals[i]; + m_storage->vertexBufferPtr->entityID = entityID; + m_storage->vertexBufferPtr++; + } + + std::ranges::for_each(indices, [this](const unsigned int index) { + m_storage->indexBufferBase[m_storage->indexCount++] = index; + }); + } + + void NxRenderer3D::drawBillboard( + const glm::vec3& position, + const glm::vec2& size, + const NxMaterial& material, + const int entityID) const + { + if (!m_renderingScene) + { + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, + "Renderer not rendering a scene, make sure to call beginScene first"); + } + + const glm::vec3 cameraPos = m_storage->cameraPosition; + + const glm::mat4 billboardRotation = calculateBillboardRotation(position, cameraPos); + + const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * + billboardRotation * + glm::scale(glm::mat4(1.0f), glm::vec3(size.x, size.y, 1.0f)); + + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); + + NxIndexedMaterial mat; + mat.albedoColor = material.albedoColor; + mat.albedoTexIndex = getTextureIndex(material.albedoTexture); + mat.specularColor = material.specularColor; + mat.specularTexIndex = getTextureIndex(material.metallicMap); + setMaterialUniforms(mat); + + std::array verts{}; + std::array texCoords{}; + std::array normals{}; + std::array indices{}; + + genBillboardMesh(verts, texCoords, normals); + for (unsigned int i = 0; i < 6; ++i) + indices[i] = i; + + for (unsigned int i = 0; i < 6; ++i) + { + m_storage->vertexBufferPtr->position = glm::vec4(verts[i], 1.0f); + m_storage->vertexBufferPtr->texCoord = texCoords[i]; + m_storage->vertexBufferPtr->normal = normals[i]; + m_storage->vertexBufferPtr->entityID = entityID; + m_storage->vertexBufferPtr++; + } + + std::ranges::for_each(indices, [this](const unsigned int index) { + m_storage->indexBufferBase[m_storage->indexCount++] = index; + }); + } +} diff --git a/engine/src/renderer/primitives/Cube.cpp b/engine/src/renderer/primitives/Cube.cpp index 11a41e082..8ce704234 100644 --- a/engine/src/renderer/primitives/Cube.cpp +++ b/engine/src/renderer/primitives/Cube.cpp @@ -116,11 +116,11 @@ namespace nexo::renderer { std::ranges::copy(norm, normals.begin()); } - void Renderer3D::drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::vec4 &color, const int entityID) const + void NxRenderer3D::drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::vec4 &color, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } @@ -128,9 +128,9 @@ namespace nexo::renderer { const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), size); - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = color; setMaterialUniforms(mat); @@ -156,7 +156,7 @@ namespace nexo::renderer { } // Index data - std::ranges::for_each(indices, [this](unsigned int index) { + std::ranges::for_each(indices, [this](const unsigned int index) { m_storage->indexBufferBase[m_storage->indexCount++] = index; }); @@ -164,11 +164,11 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3 &rotation, const glm::vec4& color, int entityID) const + void NxRenderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3 &rotation, const glm::vec4& color, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } @@ -179,9 +179,9 @@ namespace nexo::renderer { rotationMat * glm::scale(glm::mat4(1.0f), size); - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = color; setMaterialUniforms(mat); @@ -215,18 +215,18 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::mat4& transform, const glm::vec4& color, int entityID) const + void NxRenderer3D::drawCube(const glm::mat4& transform, const glm::vec4& color, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = color; setMaterialUniforms(mat); @@ -260,11 +260,11 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const components::Material &material, int entityID) const + void NxRenderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const NxMaterial& material, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } @@ -272,9 +272,9 @@ namespace nexo::renderer { const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), size); - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = material.albedoColor; mat.albedoTexIndex = material.albedoTexture ? getTextureIndex(material.albedoTexture) : 0; mat.specularColor = material.specularColor; @@ -312,11 +312,11 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3& rotation, const components::Material &material, int entityID) const + void NxRenderer3D::drawCube(const glm::vec3& position, const glm::vec3& size, const glm::vec3& rotation, const NxMaterial& material, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } @@ -327,9 +327,9 @@ namespace nexo::renderer { rotationMat * glm::scale(glm::mat4(1.0f), size); - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = material.albedoColor; mat.albedoTexIndex = material.albedoTexture ? getTextureIndex(material.albedoTexture) : 0; mat.specularColor = material.specularColor; @@ -367,11 +367,11 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::quat &rotation, const components::Material &material, int entityID) const + void NxRenderer3D::drawCube(const glm::vec3 &position, const glm::vec3 &size, const glm::quat &rotation, const NxMaterial& material, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } @@ -382,9 +382,9 @@ namespace nexo::renderer { rotationMat * glm::scale(glm::mat4(1.0f), size); - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = material.albedoColor; mat.albedoTexIndex = material.albedoTexture ? getTextureIndex(material.albedoTexture) : 0; mat.specularColor = material.specularColor; @@ -422,17 +422,17 @@ namespace nexo::renderer { m_storage->stats.cubeCount++; } - void Renderer3D::drawCube(const glm::mat4& transform, const components::Material &material, int entityID) const + void NxRenderer3D::drawCube(const glm::mat4& transform, const NxMaterial& material, const int entityID) const { if (!m_renderingScene) { - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); } - m_storage->textureShader->setUniformMatrix("matModel", transform); + m_storage->currentSceneShader->setUniformMatrix("uMatModel", transform); - renderer::Material mat; + NxIndexedMaterial mat; mat.albedoColor = material.albedoColor; mat.albedoTexIndex = material.albedoTexture ? getTextureIndex(material.albedoTexture) : 0; mat.specularColor = material.specularColor; diff --git a/engine/src/renderer/primitives/Mesh.cpp b/engine/src/renderer/primitives/Mesh.cpp index 362cb9cfe..20a005138 100644 --- a/engine/src/renderer/primitives/Mesh.cpp +++ b/engine/src/renderer/primitives/Mesh.cpp @@ -18,26 +18,26 @@ namespace nexo::renderer { - void Renderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::vec3& position, [[maybe_unused]] const glm::vec3& size, [[maybe_unused]] const components::Material& material, [[maybe_unused]] int entityID) const + void NxRenderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::vec3& position, [[maybe_unused]] const glm::vec3& size, [[maybe_unused]] const renderer::NxMaterial& material, [[maybe_unused]] int entityID) const { } - void Renderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::vec3& position, [[maybe_unused]] const glm::vec3& rotation, [[maybe_unused]] const glm::vec3& size, [[maybe_unused]] const components::Material& material, [[maybe_unused]] int entityID) const + void NxRenderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::vec3& position, [[maybe_unused]] const glm::vec3& rotation, [[maybe_unused]] const glm::vec3& size, [[maybe_unused]] const renderer::NxMaterial& material, [[maybe_unused]] int entityID) const { } - void Renderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::mat4& transform, [[maybe_unused]] const components::Material& material, [[maybe_unused]] int entityID) const + void NxRenderer3D::drawMesh([[maybe_unused]] const std::vector& vertices, [[maybe_unused]] const std::vector& indices, [[maybe_unused]] const glm::mat4& transform, [[maybe_unused]] const renderer::NxMaterial& material, [[maybe_unused]] int entityID) const { } - void Renderer3D::drawMesh(const std::vector &vertices, const std::vector &indices, - [[maybe_unused]] const std::shared_ptr &texture, int entityID) const + void NxRenderer3D::drawMesh(const std::vector &vertices, const std::vector &indices, + [[maybe_unused]] const std::shared_ptr &texture, int entityID) const { if (!m_renderingScene) - THROW_EXCEPTION(RendererSceneLifeCycleFailure, RendererType::RENDERER_3D, + THROW_EXCEPTION(NxRendererSceneLifeCycleFailure, NxRendererType::RENDERER_3D, "Renderer not rendering a scene, make sure to call beginScene first"); if ((m_storage->vertexBufferPtr - m_storage->vertexBufferBase.data()) + vertices.size() > m_storage->maxVertices || m_storage->indexCount + indices.size() > m_storage->maxIndices) diff --git a/engine/src/scripting/managed/.gitignore b/engine/src/scripting/managed/.gitignore new file mode 100644 index 000000000..7ede49076 --- /dev/null +++ b/engine/src/scripting/managed/.gitignore @@ -0,0 +1,7 @@ +obj/ +bin/ +.idea/ + +Folder.DotSettings.user + +Nexo.csproj diff --git a/engine/src/scripting/managed/AssemblyManager.cs b/engine/src/scripting/managed/AssemblyManager.cs new file mode 100644 index 000000000..e87bc28a9 --- /dev/null +++ b/engine/src/scripting/managed/AssemblyManager.cs @@ -0,0 +1,9 @@ +namespace Nexo +{ + + public class AssemblyManager + { + + } + +} \ No newline at end of file diff --git a/engine/src/scripting/managed/CMakeLists.txt b/engine/src/scripting/managed/CMakeLists.txt new file mode 100644 index 000000000..4e5fd0016 --- /dev/null +++ b/engine/src/scripting/managed/CMakeLists.txt @@ -0,0 +1,108 @@ +#### CMakeLists.txt ########################################################### +# +# zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +# zzzzzzz zzz zzzz zzzz zzzz zzzz +# zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +# zzz zzz zzz z zzzz zzzz zzzz zzzz +# zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +# +# Author: Guillaume HEIN +# Date: 09/05/2025 +# Description: CMakeLists.txt for the NEXO managed library +# +############################################################################### + +cmake_minimum_required(VERSION 3.20) + +# Set project name +project(nexoManaged LANGUAGES NONE) + +# Define variables for configuration +set(NEXO_MANAGED_OUTPUT_DIR ${CMAKE_BINARY_DIR}) +file(RELATIVE_PATH NEXO_MANAGED_OUTPUT_DIR_REL "${CMAKE_CURRENT_LIST_DIR}" "${NEXO_MANAGED_OUTPUT_DIR}") +set(NEXO_FRAMEWORK "net9.0") + +set(SOURCES + Lib.cs + ObjectFactory.cs + NativeInterop.cs + Logger.cs + Input.cs + KeyCode.cs + Components/Camera.cs + Components/Light.cs + Components/Render.cs + Components/Scene.cs + Components/Transform.cs + Components/Uuid.cs + Components/ComponentBase.cs + Components/Ui/Field.cs + Components/Ui/FieldArray.cs + Components/Ui/FieldType.cs + Systems/SystemBase.cs + Systems/WorldState.cs + Scripts/CubeSystem.cs + Scripts/InputDemoSystem.cs +) + +# Locate the dotnet executable +find_program(DOTNET_EXECUTABLE dotnet HINTS ENV PATH) + +if(NOT DOTNET_EXECUTABLE) + message(FATAL_ERROR "The .NET SDK (dotnet CLI) is not installed or not in the PATH. Please install it from https://dotnet.microsoft.com/download.") +endif() + +message(STATUS "Using dotnet executable: ${DOTNET_EXECUTABLE}") + +# Generate Nexo.csproj from a template (Nexo.csproj.in) +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/Nexo.csproj.in # Input template + ${CMAKE_CURRENT_LIST_DIR}/Nexo.csproj # Output file + @ONLY # Only replace variables in @VAR@ format +) + +if(CMAKE_GENERATOR_PLATFORM) + set(ARCH_ARG --arch ${CMAKE_GENERATOR_PLATFORM}) +else() + set(ARCH_ARG "") +endif() + + +# Build step +add_custom_target(nexoManaged ALL + COMMAND ${DOTNET_EXECUTABLE} build Nexo.csproj + ${ARCH_ARG} + -c "$,Debug,Release>" # Matches Debug/Release configuration + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} # Working directory for the build + COMMENT "Building .NET managed project (Nexo.csproj)..." +) + +# Clean step +set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_CLEAN_FILES + ${NEXO_MANAGED_OUTPUT_DIR}/Nexo.dll + ${NEXO_MANAGED_OUTPUT_DIR}/Nexo.pdb + ${NEXO_MANAGED_OUTPUT_DIR}/Nexo.runtimeconfig.json + ${NEXO_MANAGED_OUTPUT_DIR}/Nexo.deps.json + ${CMAKE_CURRENT_LIST_DIR}/obj + ${CMAKE_CURRENT_LIST_DIR}/bin +) + +# Install step +install(CODE " + execute_process( + COMMAND ${DOTNET_EXECUTABLE} publish + -c ${CMAKE_BUILD_TYPE} + --output \"${NEXO_MANAGED_OUTPUT_DIR}/publish-managed\" + WORKING_DIRECTORY \"${CMAKE_CURRENT_LIST_DIR}\" + )" + COMPONENT scripts +) + +install(DIRECTORY "${NEXO_MANAGED_OUTPUT_DIR}/publish-managed/" # source directory + COMPONENT scripts + DESTINATION "bin/" + FILES_MATCHING # install only matched files + PATTERN "*.dll" # select dll files + PATTERN "*.runtimeconfig.json" # select runtimeconfig.json files + PATTERN "*.deps.json" # select deps.json files +) diff --git a/engine/src/scripting/managed/Components/Camera.cs b/engine/src/scripting/managed/Components/Camera.cs new file mode 100644 index 000000000..965b1242f --- /dev/null +++ b/engine/src/scripting/managed/Components/Camera.cs @@ -0,0 +1,62 @@ +//// Camera.cs //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Thomas PARENTEAU +// Date: 01/06/2025 +// Description: Source file for the Camera component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + public enum CameraType + { + Perspective = 0, + Orthographic = 1 + } + + [StructLayout(LayoutKind.Sequential)] + public struct CameraComponent + { + public uint width; + public uint height; + [MarshalAs(UnmanagedType.I1)] public bool viewportLocked; + public float fov; + public float nearPlane; + public float farPlane; + public CameraType type; + public Vector4 clearColor; + [MarshalAs(UnmanagedType.I1)] public bool active; + [MarshalAs(UnmanagedType.I1)] public bool render; + [MarshalAs(UnmanagedType.I1)] public bool main; + [MarshalAs(UnmanagedType.I1)] public bool resizing; + // Note: m_renderTarget is excluded (pointer to shared_ptr) + } + + [StructLayout(LayoutKind.Sequential)] + public struct PerspectiveCameraController + { + public Vector2 lastMousePosition; + public float mouseSensitivity; + public float translationSpeed; + [MarshalAs(UnmanagedType.I1)] public bool wasMouseReleased; + [MarshalAs(UnmanagedType.I1)] public bool wasActiveLastFrame; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PerspectiveCameraTarget + { + public Vector2 lastMousePosition; + public float mouseSensitivity; + public float distance; + public uint targetEntity; + } +} diff --git a/engine/src/scripting/managed/Components/ComponentBase.cs b/engine/src/scripting/managed/Components/ComponentBase.cs new file mode 100644 index 000000000..02d6649f0 --- /dev/null +++ b/engine/src/scripting/managed/Components/ComponentBase.cs @@ -0,0 +1,66 @@ +//// ComponentBase.cs ///////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 22/06/2025 +// Description: Interface for the user's components in NEXO's ECS framework +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Nexo; +using Nexo.Components.Ui; + +namespace Nexo.Components; + +public interface IComponentBase +{ + private static readonly List AllComponents = []; + + [UnmanagedCallersOnly] + public static Int32 InitializeComponents() + { + try + { + // Find all types that derive from IComponentBase + var componentTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => typeof(IComponentBase).IsAssignableFrom(type) && + !type.IsAbstract && + !type.IsInterface && + type != typeof(IComponentBase)) // Exclude the interface itself + .ToList(); + + Logger.Log(LogLevel.Info, $"Found {componentTypes.Count} component types to register."); + foreach (var componentType in componentTypes) + { + Logger.Log(LogLevel.Info, $"Component: {componentType.Name}"); + } + + // Register each component type + foreach (var componentType in componentTypes) + { + if (NativeInterop.RegisterComponent(componentType) < 0) + { + Logger.Log(LogLevel.Error, $"Failed to register component {componentType.Name}"); + return 1; + } + } + + return 0; + } + catch (Exception ex) + { + Logger.Log(LogLevel.Fatal, $"Error initializing components: {ex.Message}"); + return 1; + } + } + +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Components/Light.cs b/engine/src/scripting/managed/Components/Light.cs new file mode 100644 index 000000000..380b3aa07 --- /dev/null +++ b/engine/src/scripting/managed/Components/Light.cs @@ -0,0 +1,55 @@ +//// Light.cs ///////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Thomas PARENTEAU +// Date: 20/05/2025 +// Description: Source file for the Light component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + [StructLayout(LayoutKind.Sequential)] + public struct AmbientLightComponent + { + public Vector3 color; + } + + [StructLayout(LayoutKind.Sequential)] + public struct DirectionalLightComponent + { + public Vector3 direction; + public Vector3 color; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PointLightComponent + { + public Vector3 color; + public float linear; + public float quadratic; + public float maxDistance; + public float constant; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SpotLightComponent + { + public Vector3 direction; + public Vector3 color; + public float cutOff; + public float outerCutoff; + public float linear; + public float quadratic; + public float maxDistance; + public float constant; + } +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Components/Render.cs b/engine/src/scripting/managed/Components/Render.cs new file mode 100644 index 000000000..f89affa3d --- /dev/null +++ b/engine/src/scripting/managed/Components/Render.cs @@ -0,0 +1,32 @@ +//// Render.cs //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Thomas PARENTEAU +// Date: 01/06/2025 +// Description: Source file for the Render component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + public enum RenderType + { + Render2D = 0, + Render3D = 1 + } + + [StructLayout(LayoutKind.Sequential)] + public struct RenderComponent + { + [MarshalAs(UnmanagedType.I1)] public bool isRendered; + public RenderType type; + // renderable is ignored (unmanaged/shared_ptr) + } +} diff --git a/engine/src/scripting/managed/Components/Scene.cs b/engine/src/scripting/managed/Components/Scene.cs new file mode 100644 index 000000000..a6cf17c6e --- /dev/null +++ b/engine/src/scripting/managed/Components/Scene.cs @@ -0,0 +1,30 @@ +//// Scene.cs ///////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Thomas PARENTEAU +// Date: 03/06/2025 +// Description: Source file for the SceneTag component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + [StructLayout(LayoutKind.Sequential)] + public struct SceneTag + { + public uint id; + + [MarshalAs(UnmanagedType.I1)] + public bool isActive; + + [MarshalAs(UnmanagedType.I1)] + public bool isRendered; + } +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Components/Transform.cs b/engine/src/scripting/managed/Components/Transform.cs new file mode 100644 index 000000000..4ddab5a9d --- /dev/null +++ b/engine/src/scripting/managed/Components/Transform.cs @@ -0,0 +1,29 @@ +//// Transform.cs ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 03/05/2025 +// Description: Source file for the Transform component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + + [StructLayout(LayoutKind.Sequential)] + public struct Transform + { + public Vector3 pos; + public Vector3 size; + public Quaternion quat; + } + +} diff --git a/engine/src/scripting/managed/Components/Ui/Field.cs b/engine/src/scripting/managed/Components/Ui/Field.cs new file mode 100644 index 000000000..f308dc535 --- /dev/null +++ b/engine/src/scripting/managed/Components/Ui/Field.cs @@ -0,0 +1,96 @@ +//// Field.cs ///////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/06/2025 +// Description: Source file for the Field struct in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Nexo.Components.Ui; + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct Field +{ + // Use IntPtr instead of char* for better marshaling + public IntPtr Name; + public FieldType Type; + public UInt64 Size; // For blank fields or additional size information + public UInt64 Offset; + + public static readonly Dictionary TypeMap = new() + { + { typeof(Boolean), FieldType.Bool }, + { typeof(SByte), FieldType.Int8 }, + { typeof(Int16), FieldType.Int16 }, + { typeof(Int32), FieldType.Int32 }, + { typeof(Int64), FieldType.Int64 }, + { typeof(Byte), FieldType.UInt8 }, + { typeof(UInt16), FieldType.UInt16 }, + { typeof(UInt32), FieldType.UInt32 }, + { typeof(UInt64), FieldType.UInt64 }, + { typeof(Single), FieldType.Float }, + { typeof(Double), FieldType.Double }, + { typeof(Vector3), FieldType.Vector3 }, + { typeof(Vector4), FieldType.Vector4 } + }; + + public static Field CreateSectionField(String sectionName) + { + var field = new Field + { + Name = Marshal.StringToHGlobalAnsi(sectionName), + Type = FieldType.Section, + Size = 0 + }; + return field; + } + + public static Field CreateFieldFromFieldInfo(Type declaringType, System.Reflection.FieldInfo fieldInfo) + { + var fieldType = TypeMap.GetValueOrDefault(fieldInfo.FieldType, FieldType.Blank); + var fieldName = fieldInfo.Name; + var offset = Marshal.OffsetOf(fieldInfo.DeclaringType ?? throw new InvalidOperationException(), fieldName); + + var size = Marshal.SizeOf(fieldInfo.FieldType); + Logger.Log(LogLevel.Info, $"Creating field: {fieldName}, Type: {fieldType}, Size: {Marshal.SizeOf(fieldInfo.FieldType)}"); + + return new Field + { + Name = Marshal.StringToHGlobalAnsi(fieldName), + Type = fieldType, + Size = Convert.ToUInt64(size), + Offset = Convert.ToUInt64(offset) + }; + } + + // Property to safely get the name as a string + public String NameString => Name != IntPtr.Zero ? Marshal.PtrToStringAnsi(Name) ?? string.Empty : string.Empty; + + // Clean up allocated memory (call this when done with the struct) + public void Dispose() + { + if (Name != IntPtr.Zero) + { + Marshal.FreeHGlobal(Name); + Name = IntPtr.Zero; + } + } + + // Override ToString for debugging + public override string ToString() + { + return $"Field {{ Name = \"{NameString}\", Type = {Type}, Size = {Size} }}"; + } +} diff --git a/engine/src/scripting/managed/Components/Ui/FieldArray.cs b/engine/src/scripting/managed/Components/Ui/FieldArray.cs new file mode 100644 index 000000000..80c4caef8 --- /dev/null +++ b/engine/src/scripting/managed/Components/Ui/FieldArray.cs @@ -0,0 +1,144 @@ +//// FieldArray.cs //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/06/2025 +// Description: Source file for the FieldArray struct in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Nexo.Components.Ui; + +public unsafe class FieldArray : IDisposable +{ + private Field* _fields; + private readonly int _count; + private bool _disposed; + + public FieldArray(int capacity) + { + _count = capacity; + _fields = (Field*)Marshal.AllocHGlobal(capacity * sizeof(Field)); + + for (int i = 0; i < capacity; i++) + { + _fields[i] = default; + } + } + + public FieldArray(Field[] fields) + { + _count = fields.Length; + _fields = (Field*)Marshal.AllocHGlobal(_count * sizeof(Field)); + + for (int i = 0; i < _count; i++) + { + _fields[i] = fields[i]; + } + } + + public Field* GetPointer() => _fields; + public int Count => _count; + + public Field this[int index] + { + get + { + if (index < 0 || index >= _count) + throw new IndexOutOfRangeException(); + return _fields[index]; + } + set + { + if (index < 0 || index >= _count) + throw new IndexOutOfRangeException(); + _fields[index] = value; + } + } + + public void Dispose() + { + if (!_disposed && _fields != null) + { + for (int i = 0; i < _count; i++) + { + _fields[i].Dispose(); + } + + Marshal.FreeHGlobal((IntPtr)_fields); + _fields = null; + _disposed = true; + } + } + + public static FieldArray CreateFieldArrayFromType(Type type, Boolean flatten = true) + { + var flattenedFields = flatten ? GetFlattenedFields(type) : GetDirectFields(type); + var fieldArray = new FieldArray(flattenedFields.Count); + + for (int i = 0; i < flattenedFields.Count; i++) + { + fieldArray[i] = Field.CreateFieldFromFieldInfo(type, flattenedFields[i].FieldInfo); + } + + return fieldArray; + } + + private static List<(FieldInfo FieldInfo, String Name)> GetDirectFields(Type type) + { + return type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => !f.IsStatic && !f.IsLiteral) + .Select(f => (f, f.Name)) + .ToList(); + } + + private static List<(FieldInfo FieldInfo, String Name)> GetFlattenedFields(Type type, String prefix = "") + { + var result = new List<(FieldInfo, String)>(); + + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => !f.IsStatic && !f.IsLiteral); + + foreach (var field in fields) + { + String fieldName = String.IsNullOrEmpty(prefix) ? field.Name : $"{prefix}.{field.Name}"; + + if (Field.TypeMap.ContainsKey(field.FieldType)) + { + // If the field type is a known primitive or struct, add it directly + result.Add((field, fieldName)); + continue; + } + + // If it's a struct (but not a primitive or string), flatten it + if (field.FieldType.IsValueType && + !field.FieldType.IsPrimitive && + !field.FieldType.IsEnum && + field.FieldType != typeof(IntPtr) && + field.FieldType != typeof(UIntPtr) && + !field.FieldType.IsPointer) + { + // Recursively flatten nested struct + result.AddRange(GetFlattenedFields(field.FieldType, fieldName)); + } + else + { + // It's unknown, let Field handle it + result.Add((field, fieldName)); + } + } + + return result; + } +} diff --git a/engine/src/scripting/managed/Components/Ui/FieldType.cs b/engine/src/scripting/managed/Components/Ui/FieldType.cs new file mode 100644 index 000000000..cc6ce9f33 --- /dev/null +++ b/engine/src/scripting/managed/Components/Ui/FieldType.cs @@ -0,0 +1,41 @@ +//// FieldType.cs ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/06/2025 +// Description: Source file for the FieldType enum in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +namespace Nexo.Components.Ui; + +public enum FieldType : UInt64 +{ + // Special type, if blank, the field is not used + Blank, + Section, // Used to create a section with title in the UI + + // Primitive types + Bool, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float, + Double, + + // Widgets + Vector3, + Vector4, + + _Count // Count of the number of field types, used for validation +} diff --git a/engine/src/scripting/managed/Components/Uuid.cs b/engine/src/scripting/managed/Components/Uuid.cs new file mode 100644 index 000000000..3c8627f5a --- /dev/null +++ b/engine/src/scripting/managed/Components/Uuid.cs @@ -0,0 +1,26 @@ +//// Uuid.cs ////////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Thomas PARENTEAU +// Date: 03/06/2025 +// Description: Source file for the Uuid component in C#. +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo.Components +{ + [StructLayout(LayoutKind.Sequential)] + public struct UuidComponent + { + public IntPtr uuidPtr; + + public string Uuid => Marshal.PtrToStringAnsi(uuidPtr) ?? string.Empty; + } +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Input.cs b/engine/src/scripting/managed/Input.cs new file mode 100644 index 000000000..801803bf5 --- /dev/null +++ b/engine/src/scripting/managed/Input.cs @@ -0,0 +1,95 @@ +//// Input.cs ////////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Cardonne +// Date: 25/06/2025 +// Description: Static input class for easy keyboard and mouse input access +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; + +namespace Nexo +{ + /// + /// Provides static methods for querying keyboard and mouse input states. + /// Similar to Unity's Input class for familiar API usage. + /// + public static class Input + { + /// + /// Checks if the specified key is currently pressed. + /// + /// The key to check + /// true if the key is pressed, false otherwise + public static bool IsKeyPressed(KeyCode key) + { + return NativeInterop.IsKeyPressed((Int32)key); + } + + /// + /// Checks if the specified key is currently released. + /// + /// The key to check + /// true if the key is released, false otherwise + public static bool IsKeyReleased(KeyCode key) + { + return NativeInterop.IsKeyReleased((Int32)key); + } + + /// + /// Checks if the specified mouse button is currently pressed. + /// + /// The mouse button to check + /// true if the mouse button is pressed, false otherwise + public static bool IsMouseDown(MouseButton button) + { + return NativeInterop.IsMouseDown((Int32)button); + } + + /// + /// Checks if the specified mouse button is currently released. + /// + /// The mouse button to check + /// true if the mouse button is released, false otherwise + public static bool IsMouseReleased(MouseButton button) + { + return NativeInterop.IsMouseReleased((Int32)button); + } + + /// + /// Gets the current mouse position in screen coordinates. + /// + /// The current mouse position as a Vector2 + public static Vector2 GetMousePosition() + { + return NativeInterop.GetMousePosition(); + } + + /// + /// Checks if any key is currently pressed. + /// Useful for "Press any key to continue" scenarios. + /// + /// true if any key is pressed + public static bool IsAnyKeyPressed() + { + return NativeInterop.IsAnyKeyPressed(); + } + + /// + /// Checks if any mouse button is currently pressed. + /// + /// true if any mouse button is pressed + public static bool IsAnyMouseButtonPressed() + { + return IsMouseDown(MouseButton.Left) || + IsMouseDown(MouseButton.Right) || + IsMouseDown(MouseButton.Middle); + } + } +} \ No newline at end of file diff --git a/engine/src/scripting/managed/KeyCode.cs b/engine/src/scripting/managed/KeyCode.cs new file mode 100644 index 000000000..e98bcce29 --- /dev/null +++ b/engine/src/scripting/managed/KeyCode.cs @@ -0,0 +1,95 @@ +//// KeyCode.cs /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Cardonne +// Date: 25/06/2025 +// Description: Key codes and mouse button definitions for input handling +// +/////////////////////////////////////////////////////////////////////////////// + +namespace Nexo +{ + /// + /// Keyboard key codes matching the C++ definitions. + /// + /// These values follow GLFW's key code system exactly to enable direct pass-through + /// to the underlying windowing API without translation. GLFW uses a hybrid approach: + /// - Printable ASCII characters (32-126) use their ASCII values directly + /// - Special keys (arrows, function keys, etc.) use GLFW-specific codes starting from 256 + /// + /// This design choice allows the engine to pass key codes directly to GLFW's input + /// functions without any conversion overhead. When adding new keys, use the corresponding + /// GLFW key code value. + /// + /// For a complete list of GLFW key codes, see: + /// https://www.glfw.org/docs/latest/group__keys.html + /// + public enum KeyCode + { + Space = 32, + + // Number keys + Key1 = 49, + Key2 = 50, + Key3 = 51, + + // Letter keys (GLFW uses ASCII values for printable characters) + Q = 65, + D = 68, + E = 69, + I = 73, + J = 74, + K = 75, + L = 76, + A = 81, + S = 83, + Z = 87, + + // Special keys + Tab = 258, + + // Arrow keys + Right = 262, + Left = 263, + Down = 264, + Up = 265, + + // Modifier keys + Shift = 340, + + // Additional common keys (can be extended as needed) + Escape = 256, + Enter = 257, + Backspace = 259, + Delete = 261, + + // Function keys + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + } + + /// + /// Mouse button codes matching the C++ definitions + /// + public enum MouseButton + { + Left = 0, + Right = 1, + Middle = 2, + } +} diff --git a/engine/src/scripting/managed/Lib.cs b/engine/src/scripting/managed/Lib.cs new file mode 100644 index 000000000..64bbb2dd2 --- /dev/null +++ b/engine/src/scripting/managed/Lib.cs @@ -0,0 +1,111 @@ +//// Lib.cs /////////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 09/05/2025 +// Description: Source file for the NEXO managed library +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Runtime.InteropServices; + +namespace Nexo +{ + public static class Lib + { + private static Int32 _sCallCount = 1; + + [StructLayout(LayoutKind.Sequential)] + public struct LibArgs + { + public IntPtr Message; + public int Number; + } + + public static int Hello(IntPtr arg, int argLength) + { + if (argLength < System.Runtime.InteropServices.Marshal.SizeOf(typeof(LibArgs))) + { + return 1; + } + + LibArgs libArgs = Marshal.PtrToStructure(arg); + Console.WriteLine($"Hello, world! from {nameof(Lib)} [count: {_sCallCount++}]"); + PrintLibArgs(libArgs); + return 0; + } + + [UnmanagedCallersOnly] + public static Int32 Add(Int32 a, Int32 b) + { + return a + b; + } + + [UnmanagedCallersOnly] + public static Int32 AddToPtr(Int32 a, Int32 b, IntPtr result) + { + if (result == IntPtr.Zero) + { + return 1; + } + + Marshal.WriteInt32(result, a + b); + return 0; + } + + [UnmanagedCallersOnly] + public static Int32 AddNexoDllDirectory(IntPtr pPathString) + { + if (pPathString == IntPtr.Zero) + { + return 1; + } + string? pathString = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Marshal.PtrToStringUni(pPathString) + : Marshal.PtrToStringUTF8(pPathString); + if (string.IsNullOrEmpty(pathString)) + { + return 1; + } + string? path = System.IO.Path.GetDirectoryName(pathString); + if (string.IsNullOrEmpty(path)) + { + return 1; + } + + return 0; + } + + public delegate void CustomEntryPointDelegate(LibArgs libArgs); + public static void CustomEntryPoint(LibArgs libArgs) + { + Console.WriteLine($"Hello, world! from {nameof(CustomEntryPoint)} in {nameof(Lib)}"); + PrintLibArgs(libArgs); + } + + [UnmanagedCallersOnly] + public static void CustomEntryPointUnmanagedCallersOnly(LibArgs libArgs) + { + Console.WriteLine($"Hello, world! from {nameof(CustomEntryPointUnmanagedCallersOnly)} in {nameof(Lib)}"); + PrintLibArgs(libArgs); + } + + private static void PrintLibArgs(LibArgs libArgs) + { + string? message = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Marshal.PtrToStringUni(libArgs.Message) + : Marshal.PtrToStringUTF8(libArgs.Message); + + message = message ?? "[ERROR] Could not convert message"; + + Console.WriteLine($"-- message: {message}"); + Console.WriteLine($"-- number: {libArgs.Number}"); + } + } +} diff --git a/engine/src/scripting/managed/Logger.cs b/engine/src/scripting/managed/Logger.cs new file mode 100644 index 000000000..2a233f757 --- /dev/null +++ b/engine/src/scripting/managed/Logger.cs @@ -0,0 +1,39 @@ +//// Logger.cs //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 09/05/2025 +// Description: Source file for the logger class +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo; + +public enum LogLevel : UInt32 +{ + Fatal, + Error, + Warn, + Info, + Debug, + Dev, + User +} + +public static class Logger +{ + /// + /// Logs a message with the specified log level. + /// + /// Specifies the log level (e.g., Fatal, Error, Warn, Info, Debug, Dev, User). + /// The message to be logged. + public static void Log(LogLevel level, String message) => NativeInterop.Log((UInt32)level, message); + +} diff --git a/engine/src/scripting/managed/NativeInterop.cs b/engine/src/scripting/managed/NativeInterop.cs new file mode 100644 index 000000000..24617b68f --- /dev/null +++ b/engine/src/scripting/managed/NativeInterop.cs @@ -0,0 +1,535 @@ +//// NativeInterop.cs ///////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 09/05/2025 +// Description: Source file for the NativeInterop class in C#. +// This class provides interop functionality for calling native +// C++ functions. +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Nexo.Components; +using Nexo.Components.Ui; + +namespace Nexo +{ + [StructLayout(LayoutKind.Sequential)] + public struct ComponentTypeIds + { + public UInt32 Transform; + public UInt32 AmbientLightComponent; + public UInt32 DirectionalLightComponent; + public UInt32 PointLightComponent; + public UInt32 SpotLightComponent; + public UInt32 RenderComponent; + public UInt32 SceneTag; + public UInt32 CameraComponent; + public UInt32 UuidComponent; + public UInt32 PerspectiveCameraController; + public UInt32 PerspectiveCameraTarget; + } + + /// + /// Provides interop functionality for calling native C++ functions from C# using function pointers. + /// + public static class NativeInterop + { + /// + /// Native API struct that matches the C++ struct + /// + [StructLayout(LayoutKind.Sequential)] + private unsafe struct NativeApiCallbacks + { + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate void HelloFromNativeDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate Int32 AddNumbersDelegate(Int32 a, Int32 b); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate IntPtr GetNativeMessageDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate void NxLogDelegate(UInt32 level, String message); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate UInt32 CreateCubeDelegate(Vector3 position, Vector3 size, Vector3 rotation, Vector4 color); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate ref Transform GetTransformDelegate(UInt32 entityId); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate IntPtr NxGetComponentDelegate(UInt32 entityId, UInt32 typeId); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate void NxAddComponentDelegate(UInt32 entityId, UInt32 typeId, void *componentData); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxHasComponentDelegate(UInt32 entityId, UInt32 typeId); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate Int64 NxRegisterComponentDelegate(String name, UInt64 componentSize, Field *fields, UInt64 fieldCount); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate ComponentTypeIds NxGetComponentTypeIdsDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxIsKeyPressedDelegate(Int32 keycode); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxIsKeyReleasedDelegate(Int32 keycode); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxIsAnyKeyPressedDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxIsMouseDownDelegate(Int32 button); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public delegate bool NxIsMouseReleasedDelegate(Int32 button); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + public unsafe delegate void NxGetMousePositionDelegate(Vector2* position); + + // Function pointers + public HelloFromNativeDelegate NxHelloFromNative; + public AddNumbersDelegate NxAddNumbers; + public GetNativeMessageDelegate NxGetNativeMessage; + public NxLogDelegate NxLog; + public CreateCubeDelegate NxCreateCube; + public GetTransformDelegate NxGetTransform; + public NxGetComponentDelegate NxGetComponent; + public NxAddComponentDelegate NxAddComponent; + public NxHasComponentDelegate NxHasComponent; + public NxRegisterComponentDelegate NxRegisterComponent; + public NxGetComponentTypeIdsDelegate NxGetComponentTypeIds; + public NxIsKeyPressedDelegate NxIsKeyPressed; + public NxIsKeyReleasedDelegate NxIsKeyReleased; + public NxIsAnyKeyPressedDelegate NxIsAnyKeyPressed; + public NxIsMouseDownDelegate NxIsMouseDown; + public NxIsMouseReleasedDelegate NxIsMouseReleased; + public NxGetMousePositionDelegate NxGetMousePosition; + } + + private static NativeApiCallbacks s_callbacks; + private static ComponentTypeIds _componentTypeIds; + private static readonly Dictionary _typeToNativeIdMap = new(); + + /// + /// Initialize the native API with the provided struct pointer and size. + /// + /// Pointer to the struct + /// Size of the struct + [UnmanagedCallersOnly] + public static Int32 Initialize(IntPtr structPtr, UInt32 structSize) + { + if (structSize != Marshal.SizeOf()) + { + Logger.Log(LogLevel.Fatal, $"Struct size mismatch between C++ and C# for {nameof(NativeApiCallbacks)}, expected {Marshal.SizeOf()}, got {structSize}"); + return 1; + } + + // Marshal the struct from the IntPtr to the managed struct + s_callbacks = Marshal.PtrToStructure(structPtr); + _componentTypeIds = s_callbacks.NxGetComponentTypeIds.Invoke(); + if (InitializeTypeMap() != 0) + { + Logger.Log(LogLevel.Fatal, "Failed to initialize type map for component type IDs."); + return 1; + } + + Logger.Log(LogLevel.Info, "Native API initialized."); + return 0; + } + + private static Int32 InitializeTypeMap() + { + var fields = typeof(ComponentTypeIds).GetFields(); + foreach (var field in fields) + { + var expectedTypeName = $"Nexo.Components.{field.Name}"; + var type = Type.GetType(expectedTypeName); + + if (type != null) + { + var value = field.GetValue(_componentTypeIds); + if (value == null) + { + Logger.Log(LogLevel.Warn, $"Field {field.Name} in ComponentTypeIds is null"); + return 1; + } + _typeToNativeIdMap[type] = (UInt32)value; + Logger.Log(LogLevel.Debug, $"[Interop] Mapped {expectedTypeName} => {value}"); + } + else + { + Logger.Log(LogLevel.Warn, $"[Interop] Type not found for field {field.Name} (expected {expectedTypeName})"); + } + } + + return 0; + } + + /// + /// Calls the HelloFromNative function in the native library + /// + public static void HelloFromNative() + { + try + { + s_callbacks.NxHelloFromNative.Invoke(); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling HelloFromNative: {ex.Message}"); + } + } + + /// + /// Calls the AddNumbers function in the native library + /// + public static Int32 AddNumbers(Int32 a, Int32 b) + { + try + { + return s_callbacks.NxAddNumbers.Invoke(a, b); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling AddNumbers: {ex.Message}"); + return 0; + } + } + + /// + /// Calls the GetNativeMessage function in the native library + /// + public static String GetNativeMessage() + { + try + { + IntPtr messagePtr = s_callbacks.NxGetNativeMessage.Invoke(); + return messagePtr != IntPtr.Zero ? Marshal.PtrToStringAnsi(messagePtr) ?? string.Empty : string.Empty; + } + catch (Exception ex) + { + Console.WriteLine($"Error calling GetNativeMessage: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Logs a message using the native NxLog function + /// + /// The level of the log message + /// The message to log + public static void Log(UInt32 level, String message) + { + try + { + s_callbacks.NxLog.Invoke(level, message); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling NxLog: {ex.Message}"); + Console.WriteLine($"Fallback to WriteLine: Log Level: {level}, Message: {message}"); + } + } + + public static UInt32 CreateCube(in Vector3 position, in Vector3 size, in Vector3 rotation, in Vector4 color) + { + try + { + return s_callbacks.NxCreateCube.Invoke(position, size, rotation, color); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling CreateCube: {ex.Message}"); + return UInt32.MaxValue; + } + } + + public static ref Transform GetTransform(UInt32 entityId) + { + try + { + return ref s_callbacks.NxGetTransform.Invoke(entityId); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling GetTransform: {ex.Message}"); + throw new InvalidOperationException($"Failed to get transform for entity {entityId}", ex); + } + } + + public static unsafe ref T GetComponent(UInt32 entityId) where T : unmanaged + { + if (!_typeToNativeIdMap.TryGetValue(typeof(T), out var typeId)) + throw new InvalidOperationException($"Unsupported component type: {typeof(T)}"); + + IntPtr ptr = s_callbacks.NxGetComponent(entityId, typeId); + if (ptr == IntPtr.Zero) + throw new InvalidOperationException($"Component {typeof(T)} not found on entity {entityId}"); + + return ref Unsafe.AsRef((void*)ptr); + } + + public static unsafe void AddComponent(UInt32 entityId, ref T componentData) where T : unmanaged + { + if (!_typeToNativeIdMap.TryGetValue(typeof(T), out var typeId)) + throw new InvalidOperationException($"Unsupported component type: {typeof(T)}"); + + try + { + s_callbacks.NxAddComponent.Invoke(entityId, typeId, Unsafe.AsPointer(ref componentData)); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling AddComponent<{typeof(T)}>: {ex.Message}"); + } + } + + public static bool HasComponent(UInt32 entityId) + { + if (!_typeToNativeIdMap.TryGetValue(typeof(T), out var typeId)) + throw new InvalidOperationException($"Unsupported component type: {typeof(T)}"); + + try + { + return s_callbacks.NxHasComponent.Invoke(entityId, typeId); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling HasComponent<{typeof(T)}>: {ex.Message}"); + return false; + } + } + + public static bool IsKeyPressed(Int32 keycode) + { + try + { + return s_callbacks.NxIsKeyPressed.Invoke(keycode); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling IsKeyPressed: {ex.Message}"); + return false; + } + } + + public static bool IsKeyReleased(Int32 keycode) + { + try + { + return s_callbacks.NxIsKeyReleased.Invoke(keycode); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling IsKeyReleased: {ex.Message}"); + return false; + } + } + + public static bool IsAnyKeyPressed() + { + try + { + return s_callbacks.NxIsAnyKeyPressed.Invoke(); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling IsAnyKeyPressed: {ex.Message}"); + return false; + } + } + + public static bool IsMouseDown(Int32 button) + { + try + { + return s_callbacks.NxIsMouseDown.Invoke(button); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling IsMouseDown: {ex.Message}"); + return false; + } + } + + public static bool IsMouseReleased(Int32 button) + { + try + { + return s_callbacks.NxIsMouseReleased.Invoke(button); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling IsMouseReleased: {ex.Message}"); + return false; + } + } + + public static unsafe Vector2 GetMousePosition() + { + try + { + Vector2 position = new Vector2(); + s_callbacks.NxGetMousePosition.Invoke(&position); + return position; + } + catch (Exception ex) + { + Console.WriteLine($"Error calling GetMousePosition: {ex.Message}"); + return Vector2.Zero; + } + } + + + public static unsafe Int64 RegisterComponent(Type componentType) + { + var name = componentType.Name; + try + { + var size = (UInt32)Marshal.SizeOf(componentType); + + Logger.Log(LogLevel.Info, $"Registering component {name}"); + + var fieldArray = FieldArray.CreateFieldArrayFromType(componentType); + + var typeId = s_callbacks.NxRegisterComponent.Invoke(name, size, fieldArray.GetPointer(), (UInt64)fieldArray.Count); + if (typeId < 0) + { + Console.WriteLine($"Failed to register component {name}, returned: {typeId}"); + return typeId; + } + _typeToNativeIdMap[componentType] = (UInt32)typeId; + Logger.Log(LogLevel.Info, $"Registered component {name} with type ID {typeId}"); + for (int i = 0; i < fieldArray.Count; i++) + { + var field = fieldArray[i]; + Logger.Log(LogLevel.Info, $"Registered field {field.Name} of type {field.Type} for component {componentType.Name}"); + } + return typeId; + } + catch (Exception ex) + { + Console.WriteLine($"Error calling NxRegisterComponent for {name}: {ex.Message} {ex.StackTrace}"); + return -1; + } + } + + private static UInt32 _cubeId = 0; + + /// + /// Demonstrates calling native functions from C# + /// + [UnmanagedCallersOnly] + public static void DemonstrateNativeCalls() + { + Console.WriteLine("=== Starting Native Call Demonstration ==="); + + // Call the void function + Console.WriteLine("Calling HelloFromNative:"); + HelloFromNative(); + + // Call the function that returns an int + const Int32 a = 42; + const Int32 b = 123; + Console.WriteLine($"Calling AddNumbers({a}, {b}):"); + Int32 result = AddNumbers(a, b); + Console.WriteLine($"Result: {result}"); + + // Call the function that returns a string + Console.WriteLine("Calling GetNativeMessage:"); + String message = GetNativeMessage(); + Logger.Log(LogLevel.Info, $"Logging from C# :) got native message: {message}"); + + // Call the function that creates a cube + Console.WriteLine("Calling CreateCube:"); + UInt32 cubeId = CreateCube(new Vector3(1, 4.2f, 3), new Vector3(1, 1, 1), new Vector3(7, 8, 9), new Vector4(1, 0, 0, 1)); + _cubeId = cubeId; + Console.WriteLine($"Created cube with ID: {cubeId}"); + + // HasComponent test + if (HasComponent(cubeId)) + Console.WriteLine("Entity has a camera!"); + else + Console.WriteLine("Entity does NOT have a camera."); + + if (HasComponent(cubeId)) + Console.WriteLine("Entity has a Transform!"); + else + Console.WriteLine("Entity does NOT have a Transform."); + + if (HasComponent(cubeId)) + Console.WriteLine("Entity has a AmbientLight!"); + else + Console.WriteLine("Entity does NOT have a AmbientLight."); + + // Call the function that gets a transform + Console.WriteLine($"Calling GetComponent({cubeId}):"); + ref Transform transform = ref GetComponent(cubeId); + Console.WriteLine($"Transform for cube {cubeId}: Position: {transform.pos}, Scale: {transform.size}, Rotation Quat: {transform.quat}"); + + + Console.WriteLine("=== Native Call Demonstration Complete ==="); + } + + private static float _angle = 0.0f; + private static float _breathingScale = 1.0f; + + [UnmanagedCallersOnly] + public static void Update(Double deltaTime) + { + ref Transform transform = ref GetComponent(_cubeId); + + // Rotating cube effect + float rotationSpeed = 1.0f; // radians per second + transform.quat = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)deltaTime * rotationSpeed) * transform.quat; + + // Circling cube effect + float speed = 1.0f; + float radius = 7.0f; + Vector3 origin = new Vector3(0, 5, 0); + + _angle += (float)(speed * deltaTime); + + if (_angle > MathF.PI * 2.0f) + { + _angle = 0.0f; + } + + transform.pos = origin + new Vector3( + (float)Math.Cos(_angle) * radius, + 0, + (float)Math.Sin(_angle) * radius + ); + + // Breathing cube effect + float startScale = 1.0f; + float endScale = 2.0f; + float breathingSpeed = 0.5f; + + _breathingScale += (float)(breathingSpeed * deltaTime * MathF.PI * 2.0f); + if (_breathingScale > MathF.PI * 2.0f) + { + _breathingScale -= MathF.PI * 2.0f; + } + + // Update the size of the cube based on the breathing effect + transform.size.Z = startScale + ((MathF.Sin(_breathingScale) * 0.5f + 0.5f) * (endScale - startScale)); + } + + } +} diff --git a/engine/src/scripting/managed/Nexo.csproj.in b/engine/src/scripting/managed/Nexo.csproj.in new file mode 100644 index 000000000..52a074aaa --- /dev/null +++ b/engine/src/scripting/managed/Nexo.csproj.in @@ -0,0 +1,23 @@ + + + + Library + @NEXO_FRAMEWORK@ + enable + enable + true + false + true + + + + + + + + false + false + @NEXO_MANAGED_OUTPUT_DIR_REL@ + + + diff --git a/engine/src/scripting/managed/ObjectFactory.cs b/engine/src/scripting/managed/ObjectFactory.cs new file mode 100644 index 000000000..fce9eb120 --- /dev/null +++ b/engine/src/scripting/managed/ObjectFactory.cs @@ -0,0 +1,93 @@ +//// ObjectFactory.cs ///////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 03/05/2025 +// Description: Source file for the scripting object factory +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo +{ + public static class ObjectFactory + { + private static Dictionary _instances = new Dictionary(); + private static int _nextId = 1; + + // Generic factory method using Activator + public static int CreateInstance(string typeName) + { + try + { + Type? type = Type.GetType(typeName); + if (type == null) + return -1; + + object? instance = Activator.CreateInstance(type); + if (instance == null) + return -1; + int id = _nextId++; + _instances[id] = instance; + return id; + } + catch + { + return -1; + } + } + + // Create with parameters + public static int CreateInstanceWithParams(string typeName, object[] parameters) + { + try + { + Type? type = Type.GetType(typeName); + if (type == null) + return -1; + + object? instance = Activator.CreateInstance(type, parameters); + if (instance == null) + return -1; + int id = _nextId++; + _instances[id] = instance; + return id; + } + catch + { + return -1; + } + } + + // Method to release the instance + public static bool ReleaseInstance(int id) + { + return _instances.Remove(id); + } + + // Helper to invoke methods on instances + public static bool InvokeMethod(int id, string methodName, object[] parameters) + { + if (!_instances.TryGetValue(id, out object? instance)) + return false; + + try + { + if (instance == null) + return false; + instance?.GetType().GetMethod(methodName)?.Invoke(instance, parameters); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/engine/src/scripting/managed/Scripts/CubeSystem.cs b/engine/src/scripting/managed/Scripts/CubeSystem.cs new file mode 100644 index 000000000..f41af769e --- /dev/null +++ b/engine/src/scripting/managed/Scripts/CubeSystem.cs @@ -0,0 +1,132 @@ +//// CubeSystem.cs //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 21/06/2025 +// Description: Source file for the Cube system in NEXO's ECS framework +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; +using Nexo.Components; +using Nexo.Systems; + +namespace Nexo.Scripts; + +public struct TestComponent() : IComponentBase +{ + public struct Nested() + { + public Int32 NestedValue = 42; + public Vector3 NestedVector = new Vector3(1, 2, 3); + } + public Nested NestedComponent = new Nested(); + public Double RootValue = 84; +} + +public class CubeSystem : SystemBase +{ + private struct CubeAnimationState : IComponentBase + { + public Single Angle; + public Single BreathingPhase; + } + + private readonly List _cubes = []; + + protected override void OnInitialize(WorldState worldState) + { + Logger.Log(LogLevel.Info, $"Initializing {Name} system"); + } + + private void MoveCube(UInt32 cubeId, Single deltaTime) + { + ref Transform transform = ref NativeInterop.GetComponent(cubeId); + ref CubeAnimationState state = ref NativeInterop.GetComponent(cubeId); + + // Rotating cube effect + float rotationSpeed = 1.0f; + transform.quat = Quaternion.CreateFromAxisAngle(Vector3.UnitY, deltaTime * rotationSpeed) * transform.quat; + + // Circling cube effect + float speed = 1.0f; + float radius = 7.0f; + Vector3 origin = new Vector3(0, 5, 0); + + state.Angle += speed * deltaTime; + if (state.Angle > MathF.PI * 2.0f) + { + state.Angle -= MathF.PI * 2.0f; + } + + transform.pos = origin + new Vector3( + MathF.Cos(state.Angle) * radius, + 0, + MathF.Sin(state.Angle) * radius + ); + + // Breathing cube effect + float startScale = 1.0f; + float endScale = 2.0f; + float breathingSpeed = 0.5f; + + state.BreathingPhase += breathingSpeed * deltaTime * MathF.PI * 2.0f; + if (state.BreathingPhase > MathF.PI * 2.0f) + { + state.BreathingPhase -= MathF.PI * 2.0f; + } + + transform.size.Z = startScale + ((MathF.Sin(state.BreathingPhase) * 0.5f + 0.5f) * (endScale - startScale)); + } + + private void SpawnCube(Vector3 position, Vector3 size, Vector3 rotation, Vector4 color) + { + var cubeId = NativeInterop.CreateCube(position, size, rotation, color); + var state = new CubeAnimationState + { + Angle = Random.Shared.NextSingle() * MathF.PI * 2.0f, + BreathingPhase = Random.Shared.NextSingle() * MathF.PI * 2.0f + }; + NativeInterop.AddComponent(cubeId, ref state); + var testComponent = new TestComponent(); + NativeInterop.AddComponent(cubeId, ref testComponent); + _cubes.Add(cubeId); + } + + protected override void OnUpdate(WorldState worldState) + { + Single deltaTime = (Single)worldState.Time.DeltaTime; + + // If 2 seconds have passed since last spawn, spawn a new cube + if (worldState.Time.TotalTime % 2.0 < deltaTime) + { + Vector3 position = new Vector3(1, 4.2f, 3); + Vector3 size = new Vector3(1, 1, 1); + Vector3 rotation = new Vector3(7, 8, 9); + Vector4 color = new Vector4( + Random.Shared.NextSingle(), + Random.Shared.NextSingle(), + Random.Shared.NextSingle(), + 1.0f + ); + SpawnCube(position, size, rotation, color); + } + + foreach (var cubeId in _cubes) + { + MoveCube(cubeId, deltaTime); + } + } + + protected override void OnShutdown(WorldState worldState) + { + _cubes.Clear(); + Logger.Log(LogLevel.Info, $"Shutting down {Name} system"); + } + +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Scripts/InputDemoSystem.cs b/engine/src/scripting/managed/Scripts/InputDemoSystem.cs new file mode 100644 index 000000000..2c2d4bbb6 --- /dev/null +++ b/engine/src/scripting/managed/Scripts/InputDemoSystem.cs @@ -0,0 +1,221 @@ +//// InputDemoSystem.cs //////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Cardonne +// Date: 25/06/2025 +// Description: Demo system showing input handling capabilities +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Numerics; +using Nexo.Components; +using Nexo.Systems; + +namespace Nexo.Scripts; + +/// +/// Demonstration system that shows various input handling capabilities: +/// - Spawning cubes with keyboard input +/// - Deleting cubes with keyboard input +/// - Changing cube colors based on mouse position +/// - Different behaviors for key combinations +/// +public class InputDemoSystem : SystemBase +{ + private readonly List _cubes = []; + private bool _spaceWasPressed = false; + private bool _deleteWasPressed = false; + private float _timeSinceLastSpawn = 0.0f; + private const float SpawnCooldown = 0.5f; // Prevent spam spawning + + protected override void OnInitialize(WorldState worldState) + { + Logger.Log(LogLevel.Info, $"Initializing {Name} - Press SPACE to spawn cubes, DELETE to remove them!"); + Logger.Log(LogLevel.Info, "Hold SHIFT while pressing SPACE for rainbow cubes!"); + Logger.Log(LogLevel.Info, "Left click makes cubes jump, right click shows mouse position"); + } + + protected override void OnUpdate(WorldState worldState) + { + Single deltaTime = (Single)worldState.Time.DeltaTime; + _timeSinceLastSpawn += deltaTime; + + // Handle cube spawning with SPACE key + HandleCubeSpawning(); + + // Handle cube deletion with DELETE key + HandleCubeDeletion(); + + // Handle mouse-based color changes + HandleMouseColorChange(); + + // Handle arrow keys for moving last spawned cube + HandleCubeMovement(deltaTime); + + // Display help message when TAB is pressed + if (Input.IsKeyPressed(KeyCode.Tab)) + { + Logger.Log(LogLevel.Info, "=== Input Demo Controls ==="); + Logger.Log(LogLevel.Info, "SPACE: Spawn a cube"); + Logger.Log(LogLevel.Info, "SHIFT+SPACE: Spawn a rainbow cube"); + Logger.Log(LogLevel.Info, "DELETE: Remove last cube"); + Logger.Log(LogLevel.Info, "Arrow Keys: Move last cube"); + Logger.Log(LogLevel.Info, "Left Click: Make cubes jump"); + Logger.Log(LogLevel.Info, "Right Click: Log mouse position"); + } + } + + private void HandleCubeSpawning() + { + bool spacePressed = Input.IsKeyPressed(KeyCode.Space); + + // Detect key press (transition from released to pressed) + if (spacePressed && !_spaceWasPressed && _timeSinceLastSpawn >= SpawnCooldown) + { + bool shiftHeld = Input.IsKeyPressed(KeyCode.Shift); + SpawnCube(shiftHeld); + _timeSinceLastSpawn = 0.0f; + } + + _spaceWasPressed = spacePressed; + } + + private void HandleCubeDeletion() + { + bool deletePressed = Input.IsKeyPressed(KeyCode.Delete); + + // Detect key press (transition from released to pressed) + if (deletePressed && !_deleteWasPressed && _cubes.Count > 0) + { + UInt32 lastCube = _cubes[_cubes.Count - 1]; + _cubes.RemoveAt(_cubes.Count - 1); + Logger.Log(LogLevel.Info, $"Deleted cube {lastCube}. Remaining cubes: {_cubes.Count}"); + } + + _deleteWasPressed = deletePressed; + } + + private void HandleMouseColorChange() + { + Vector2 mousePos = Input.GetMousePosition(); + + // Left click makes cubes jump + if (Input.IsMouseDown(MouseButton.Left)) + { + foreach (var cubeId in _cubes) + { + ref Transform transform = ref NativeInterop.GetComponent(cubeId); + // Add a small upward impulse effect + transform.pos.Y = MathF.Max(transform.pos.Y, 5.0f + MathF.Sin((float)_timeSinceLastSpawn * 10.0f) * 2.0f); + } + } + + // Log mouse position when right clicking + if (Input.IsMouseDown(MouseButton.Right)) + { + Logger.Log(LogLevel.Info, $"Mouse position: {mousePos.X}, {mousePos.Y}"); + } + } + + private void HandleCubeMovement(float deltaTime) + { + if (_cubes.Count == 0) return; + + // Move the last spawned cube with arrow keys + UInt32 lastCube = _cubes[_cubes.Count - 1]; + ref Transform transform = ref NativeInterop.GetComponent(lastCube); + + float moveSpeed = 5.0f * deltaTime; + + if (Input.IsKeyPressed(KeyCode.Left)) + transform.pos.X -= moveSpeed; + if (Input.IsKeyPressed(KeyCode.Right)) + transform.pos.X += moveSpeed; + if (Input.IsKeyPressed(KeyCode.Up)) + transform.pos.Z -= moveSpeed; + if (Input.IsKeyPressed(KeyCode.Down)) + transform.pos.Z += moveSpeed; + } + + private void SpawnCube(bool rainbow) + { + // Random position around the center + Vector3 position = new Vector3( + Random.Shared.NextSingle() * 10.0f - 5.0f, + Random.Shared.NextSingle() * 5.0f + 2.0f, + Random.Shared.NextSingle() * 10.0f - 5.0f + ); + + Vector3 size = Vector3.One; + Vector3 rotation = Vector3.Zero; + + Vector4 color; + if (rainbow) + { + // Create rainbow effect + float hue = Random.Shared.NextSingle(); + color = HsvToRgb(hue, 1.0f, 1.0f); + } + else + { + // Random color + color = new Vector4( + Random.Shared.NextSingle(), + Random.Shared.NextSingle(), + Random.Shared.NextSingle(), + 1.0f + ); + } + + UInt32 cubeId = NativeInterop.CreateCube(position, size, rotation, color); + _cubes.Add(cubeId); + + Logger.Log(LogLevel.Info, $"Spawned {(rainbow ? "rainbow" : "regular")} cube {cubeId} at {position}. Total cubes: {_cubes.Count}"); + } + + private static Vector4 HsvToRgb(float h, float s, float v) + { + float c = v * s; + float x = c * (1 - MathF.Abs((h * 6) % 2 - 1)); + float m = v - c; + + float r, g, b; + if (h < 1.0f / 6.0f) + { + r = c; g = x; b = 0; + } + else if (h < 2.0f / 6.0f) + { + r = x; g = c; b = 0; + } + else if (h < 3.0f / 6.0f) + { + r = 0; g = c; b = x; + } + else if (h < 4.0f / 6.0f) + { + r = 0; g = x; b = c; + } + else if (h < 5.0f / 6.0f) + { + r = x; g = 0; b = c; + } + else + { + r = c; g = 0; b = x; + } + + return new Vector4(r + m, g + m, b + m, 1.0f); + } + + protected override void OnShutdown(WorldState worldState) + { + _cubes.Clear(); + Logger.Log(LogLevel.Info, $"Shutting down {Name} system"); + } +} diff --git a/engine/src/scripting/managed/Systems/SystemBase.cs b/engine/src/scripting/managed/Systems/SystemBase.cs new file mode 100644 index 000000000..0177fa4d0 --- /dev/null +++ b/engine/src/scripting/managed/Systems/SystemBase.cs @@ -0,0 +1,139 @@ +//// SystemBase.cs /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 11/06/2025 +// Description: Interface for the user's systems in NEXO's ECS framework +// +/////////////////////////////////////////////////////////////////////////////// + +using System; +using System.Runtime.InteropServices; + +namespace Nexo.Systems +{ + + public abstract class SystemBase + { + private static readonly List AllSystems = []; + private static Boolean _isInitialized = false; + + protected String Name => GetType().Name; + protected Boolean IsActive { get; set; } = true; + + [UnmanagedCallersOnly] + public static unsafe Int32 InitializeSystems(WorldState.NativeWorldState *nativeWorldState, UInt32 size) + { + if (_isInitialized) return 0; + + if (size != Marshal.SizeOf()) + { + Logger.Log(LogLevel.Fatal, $"Struct size mismatch between C++ and C# for {nameof(WorldState.NativeWorldState)}, expected {Marshal.SizeOf()}, got {size}"); + return 1; + } + + try + { + // Find all types that derive from SystemBase + var systemTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsSubclassOf(typeof(SystemBase)) && + !type.IsAbstract) + .ToList(); + + // Create instances of all system types + foreach (var systemType in systemTypes) + { + var instance = Activator.CreateInstance(systemType); + if (instance == null) + { + Logger.Log(LogLevel.Error, $"Failed to create instance of {systemType.Name}"); + return 1; + } + AllSystems.Add((SystemBase)instance); + } + + // Initialize all systems + foreach (var system in AllSystems) + { + system.OnInitialize(new WorldState(nativeWorldState)); + } + + _isInitialized = true; + return 0; + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, ex.Message); + return 1; + } + } + + [UnmanagedCallersOnly] + public static unsafe Int32 UpdateSystems(WorldState.NativeWorldState *nativeWorldState, UInt32 size) + { + if (!_isInitialized) + { + Logger.Log(LogLevel.Error, "Systems not initialized. Call InitializeSystems first."); + return 1; + } + if (size != Marshal.SizeOf()) + { + Logger.Log(LogLevel.Fatal, $"Struct size mismatch between C++ and C# for {nameof(WorldState.NativeWorldState)}, expected {Marshal.SizeOf()}, got {size}"); + return 1; + } + + try + { + // Update all active systems + foreach (var system in AllSystems.Where(s => s.IsActive)) + { + system.OnUpdate(new WorldState(nativeWorldState)); + } + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, ex.Message); + return 1; + } + + return 0; + } + + [UnmanagedCallersOnly] + public static unsafe Int32 ShutdownSystems(WorldState.NativeWorldState *nativeWorldState, UInt32 size) + { + if (size != Marshal.SizeOf()) + { + Logger.Log(LogLevel.Fatal, $"Struct size mismatch between C++ and C# for {nameof(WorldState.NativeWorldState)}, expected {Marshal.SizeOf()}, got {size}"); + return 1; + } + + foreach (var system in AllSystems) + { + system.OnShutdown(new WorldState(nativeWorldState)); + } + + AllSystems.Clear(); + _isInitialized = false; + return 0; + } + + protected virtual void OnInitialize(WorldState worldState) + { + } + + protected abstract void OnUpdate(WorldState worldState); + + protected virtual void OnShutdown(WorldState worldState) + { + } + + } + +} \ No newline at end of file diff --git a/engine/src/scripting/managed/Systems/WorldState.cs b/engine/src/scripting/managed/Systems/WorldState.cs new file mode 100644 index 000000000..cc18ab32d --- /dev/null +++ b/engine/src/scripting/managed/Systems/WorldState.cs @@ -0,0 +1,55 @@ +//// SystemBase.cs /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 11/06/2025 +// Description: Interface for the user's systems in NEXO's ECS framework +// +/////////////////////////////////////////////////////////////////////////////// + +using System.Runtime.InteropServices; + +namespace Nexo.Systems; + +public unsafe class WorldState +{ + [StructLayout(LayoutKind.Sequential)] + public struct NativeWorldState + { + [StructLayout(LayoutKind.Sequential)] + public struct WorldTime { + public Double DeltaTime; // Time since last update + public Double TotalTime; // Total time since the start of the world + } + + [StructLayout(LayoutKind.Sequential)] + public struct WorldStats + { + public UInt64 FrameCount; // Number of frames rendered + } + + public WorldTime Time; + public WorldStats Stats; + } + + private readonly NativeWorldState* _nativePtr; + + internal WorldState(IntPtr nativePtr) + { + _nativePtr = (NativeWorldState*)nativePtr.ToPointer(); + } + + internal WorldState(NativeWorldState* nativePtr) + { + _nativePtr = nativePtr; + } + + // Direct access to native structs (no copying) + public ref NativeWorldState.WorldTime Time => ref _nativePtr->Time; + public ref NativeWorldState.WorldStats Stats => ref _nativePtr->Stats; +} diff --git a/engine/src/scripting/native/HostString.cpp b/engine/src/scripting/native/HostString.cpp new file mode 100644 index 000000000..931bf0712 --- /dev/null +++ b/engine/src/scripting/native/HostString.cpp @@ -0,0 +1,74 @@ +//// HostString.cpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 04/05/2025 +// Description: char_t * string type wrapper for hostfxr +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "HostString.hpp" + +namespace nexo::scripting { + + void HostString::init_from_utf8(const std::string& utf8) + { + #ifdef _WIN32 + std::wstring utf16; + utf8::utf8to16(utf8.begin(), utf8.end(), std::back_inserter(utf16)); + m_buffer.resize(utf16.size() + 1); + std::ranges::copy(utf16, m_buffer.data()); + m_buffer.back() = L'\0'; + #else + m_buffer.resize(utf8.size() + 1); + std::ranges::copy(utf8, m_buffer.data()); + m_buffer.back() = '\0'; + #endif + } + + void HostString::init_from_wide(const std::wstring& wide) + { + #ifdef _WIN32 + m_buffer.resize(wide.size() + 1); + std::ranges::copy(wide, m_buffer.data()); + m_buffer.back() = L'\0'; + #else + std::string utf8; + utf8::utf16to8(wide.begin(), wide.end(), std::back_inserter(utf8)); + m_buffer.resize(utf8.size() + 1); + std::ranges::copy(utf8, m_buffer.data()); + m_buffer.back() = '\0'; + #endif + } + + std::string HostString::to_utf8() const + { + #ifdef _WIN32 + std::string utf8; + utf8::utf16to8(m_buffer.data(), m_buffer.data() + size(), std::back_inserter(utf8)); + return utf8; + #else + return std::string(m_buffer.data(), size()); + #endif + } + + std::wstring HostString::to_wide() const + { + #ifdef _WIN32 + return std::wstring(m_buffer.data(), size()); + #else + std::wstring utf16; + utf8::utf8to16(m_buffer.data(), m_buffer.data() + size(), std::back_inserter(utf16)); + return utf16; + #endif + } + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/HostString.hpp b/engine/src/scripting/native/HostString.hpp new file mode 100644 index 000000000..f43ec7ea6 --- /dev/null +++ b/engine/src/scripting/native/HostString.hpp @@ -0,0 +1,112 @@ +//// HostString.hpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 04/05/2025 +// Description: char_t * string type wrapper for hostfxr +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include +#include + +namespace nexo::scripting { + + class HostString { + public: + // Rule of Five with deep copy semantics + HostString() = default; + HostString(const HostString&) = default; + HostString& operator=(const HostString&) = default; + HostString(HostString&&) = default; + HostString& operator=(HostString&&) = default; + + explicit HostString(nullptr_t) : m_buffer{} + {} + + // Implicit constructors + explicit(false) HostString(const std::string& utf8) { + init_from_utf8(utf8); + } + explicit(false) HostString(const char *str) { + init_from_utf8(str); + } + + explicit(false) HostString(const std::wstring& wide) { + init_from_wide(wide); + } + explicit(false) HostString(const wchar_t *str) { + init_from_wide(str); + } + + // Access raw char_t* for hostfxr APIs + const char_t* c_str() const noexcept { + return m_buffer.data(); + } + + // Implicit conversions + explicit(false) operator std::string() const { + return to_utf8(); + } + + explicit(false) operator std::wstring() const { + return to_wide(); + } + + std::string to_utf8() const; + std::wstring to_wide() const; + + // Standard string interface + bool empty() const noexcept { return size() == 0; } + size_t size() const noexcept { return m_buffer.empty() ? 0 : m_buffer.size() - 1; } + + char_t& operator[](const size_t index) noexcept { return m_buffer[index]; } + const char_t& operator[](const size_t index) const noexcept { return m_buffer[index]; } + char_t& at(const size_t index) noexcept { return m_buffer.at(index); } + const char_t& at(const size_t index) const noexcept { return m_buffer.at(index); } + + // Iterators + auto begin() noexcept { return m_buffer.begin(); } + auto begin() const noexcept { return m_buffer.begin(); } + auto cbegin() const noexcept { return begin(); } + auto end() noexcept { return m_buffer.end() - 1; } // Exclude null terminator + auto end() const noexcept { return m_buffer.end() - 1; } // Exclude null terminator + auto cend() const noexcept { return end(); } + auto rbegin() noexcept { return std::reverse_iterator(end()); } + auto rbegin() const noexcept { return std::reverse_iterator(end()); } + auto crbegin() const noexcept { return rbegin(); } + auto rend() noexcept { return std::reverse_iterator(begin()); } + auto rend() const noexcept { return std::reverse_iterator(begin()); } + auto crend() const noexcept { return rend(); } + + // Add operators + HostString& operator+=(const HostString& other) { + m_buffer.insert(end(), other.begin(), other.end()); + return *this; + } + + HostString operator+(const HostString& other) const { + HostString result = *this; // Copy current object + result += other; + return result; + } + + private: + std::vector m_buffer = {'\0'}; + + void init_from_utf8(const std::string& utf8); + void init_from_wide(const std::wstring& wide); + }; + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/ManagedApi.hpp b/engine/src/scripting/native/ManagedApi.hpp new file mode 100644 index 000000000..6ae8d14fb --- /dev/null +++ b/engine/src/scripting/native/ManagedApi.hpp @@ -0,0 +1,118 @@ +//// ManagedApi.hpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 06/05/2025 +// Description: Header file for managed API functions exposed to native code +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "Entity.hpp" +#include "Exception.hpp" +#include "ManagedTypedef.hpp" +#include "components/Transform.hpp" +#include "systems/ManagedWorldState.hpp" + +namespace nexo::scripting { + + class InvalidManagedApi final : public Exception { + public: + explicit InvalidManagedApi( + std::string_view message, + const std::source_location loc = std::source_location::current() + ) : Exception(std::format("Invalid managed API call: {}", message), loc) {}; + }; + + template + struct ManagedApiFn; + + template + struct ManagedApiFn { + using Type = Ret (CORECLR_DELEGATE_CALLTYPE *)(Args...); + + // Constructor explicitly accepting function pointers + explicit(false) ManagedApiFn(Type f) : func(f) + { + if (!func) { + THROW_EXCEPTION(InvalidManagedApi, std::format("Function pointer is null: {}", typeid(Type).name())); + } + } + + Ret operator()(Args... args) const + { + assert(func != nullptr && "Called function pointer is null"); + return func(args...); + } + + explicit ManagedApiFn(nullptr_t) = delete; + + // Delete the default constructor to enforce initialization + ManagedApiFn() = default; + + private: + Type func = nullptr; + }; + + struct lib_args { + const char_t* message; + int number; + }; + + struct NativeApiCallbacks; + + /** + * @brief ManagedApi struct to hold function pointers to managed API functions + * + * This struct is used to hold function pointers to managed API functions that can be called from native code. + * + * @warning At runtime, all functions MUST be initialized to a none nullptr value. + * Otherwise, an exception will be thrown by \c HostHandler::checkManagedApi(). + */ + struct ManagedApi { + + struct NativeInteropApi { + ManagedApiFn Initialize; + + ManagedApiFn DemonstrateNativeCalls; + + ManagedApiFn Update; + } NativeInterop; + + struct LibApi { + /** + * @brief Example call from C++ to C# + * + * @param args Arguments passed to managed code + */ + ManagedApiFn CustomEntryPoint; + + ManagedApiFn CustomEntryPointUnmanagedCallersOnly; + + ManagedApiFn Hello; + + ManagedApiFn Add; + ManagedApiFn AddToPtr; + } Lib; + + struct SystemBaseApi { + ManagedApiFn InitializeSystems; + ManagedApiFn InitializeComponents; + + ManagedApiFn ShutdownSystems; + + ManagedApiFn UpdateSystems; + } SystemBase; + + + }; + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/ManagedTypedef.hpp b/engine/src/scripting/native/ManagedTypedef.hpp new file mode 100644 index 000000000..41f5fe3a9 --- /dev/null +++ b/engine/src/scripting/native/ManagedTypedef.hpp @@ -0,0 +1,61 @@ +//// ManagedTypedef.hpp /////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 07/05/2025 +// Description: Header file for defining typedef equivalent to C# managed types +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include + +namespace nexo::scripting { + + extern "C" { + + // Category: Integer + using Byte = uint8_t; // An 8-bit unsigned integer. + using SByte = int8_t; // An 8-bit signed integer. Not CLS-compliant. + using Int16 = int16_t; // A 16-bit signed integer. + using Int32 = int32_t; // A 32-bit signed integer. + using Int64 = int64_t; // A 64-bit signed integer. + using UInt16 = uint16_t; // A 16-bit unsigned integer. Not CLS-compliant. + using UInt32 = uint32_t; // A 32-bit unsigned integer. Not CLS-compliant. + using UInt64 = uint64_t; // A 64-bit unsigned integer. Not CLS-compliant. + + // Category: Floating Point + // Half A half-precision (16-bit) floating-point number. Unsupported in C++ + using Single = float; // A 32-bit single-precision floating point number. + using Double = double; // A 64-bit double-precision floating point number. + + // Category: Logical + using Boolean = bool; // A Boolean value (true or false). + + // Category: Other + using Char = uint16_t; // A Unicode (16-bit) character. + // Decimal A decimal (128-bit) value. Unsupported in C++ + using IntPtr = void*; // A pointer to an unspecified type. + + using Vector2 = glm::vec2; + using Vector3 = glm::vec3; + using Vector4 = glm::vec4; + + enum class NativeComponents : UInt32 { + Transform = 0, + AmbientLight = 1, + DirectionalLight = 2, + PointLight = 3, + SpotLight = 4, + }; + } + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/NativeApi.cpp b/engine/src/scripting/native/NativeApi.cpp new file mode 100644 index 000000000..82eed7001 --- /dev/null +++ b/engine/src/scripting/native/NativeApi.cpp @@ -0,0 +1,225 @@ +//// NativeApi.cpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 06/05/2025 +// Description: Implementation file for native API functions exposed to managed code +// +/////////////////////////////////////////////////////////////////////////////// + +#include + +#include "NativeApi.hpp" +#include "EntityFactory3D.hpp" +#include "Logger.hpp" +#include "Nexo.hpp" +#include "components/Uuid.hpp" +#include "ui/Field.hpp" +#include "core/event/Input.hpp" + +namespace nexo::scripting { + + // Define the global callbacks instance + NativeApiCallbacks nativeApiCallbacks; + + // Static message to return to C# + static const char* nativeMessage = "Hello from C++ native code!"; + + // Implementation of the native functions + extern "C" { + + void NxHelloFromNative() { + std::cout << "Hello World from C++ native code!" << std::endl; + } + + Int32 NxAddNumbers(const Int32 a, const Int32 b) { + std::cout << "Native AddNumbers called with " << a << " and " << b << std::endl; + return a + b; + } + + const char* NxGetNativeMessage() { + std::cout << "GetNativeMessage called from C#" << std::endl; + return nativeMessage; + } + + void NxLog(UInt32 level, const char *message) { + LOG(static_cast(level), "[Scripting] {}", message); + } + + ecs::Entity NxCreateCube(const Vector3 pos, const Vector3 size, const Vector3 rotation, const Vector4 color) + { + auto &app = getApp(); + const ecs::Entity basicCube = EntityFactory3D::createCube(std::move(pos), std::move(size), std::move(rotation), std::move(color)); + app.getSceneManager().getScene(0).addEntity(basicCube); + return basicCube; + } + + components::TransformComponent *NxGetTransformComponent(ecs::Entity entity) + { + const auto opt = Application::m_coordinator->tryGetComponent(entity); + if (!opt.has_value()) { + LOG(NEXO_WARN, "GetTransformComponent: Entity {} does not have a TransformComponent", entity); + return nullptr; + } + return &opt.value().get(); + } + + void* NxGetComponent(const ecs::Entity entity, const UInt32 componentTypeId) + { + auto& coordinator = *Application::m_coordinator; + const auto opt = coordinator.tryGetComponentById(componentTypeId, entity); + return opt; + } + + void NxAddComponent(const ecs::Entity entity, const UInt32 typeId, const void *componentData) + { + const auto& coordinator = *Application::m_coordinator; + + coordinator.addComponent(entity, typeId, componentData); + } + + bool NxHasComponent(const ecs::Entity entity, const UInt32 typeId) + { + const auto& coordinator = *Application::m_coordinator; + + return coordinator.entityHasComponent(entity, typeId); + } + + Int64 NxRegisterComponent(const char *name, const UInt64 componentSize, const Field *fields, const UInt64 fieldCount) + { + (void)name; // TODO: unused for now + auto& coordinator = *Application::m_coordinator; + + for (UInt64 i = 0; i < fieldCount; ++i) { + LOG(NEXO_DEV, "Registering field {}: {} of type {}", i, static_cast(fields[i].name), static_cast(fields[i].type)); + } + + const auto componentType = coordinator.registerComponent(componentSize); + + std::vector fieldVector; + fieldVector.reserve(fieldCount); + static_assert(sizeof(ecs::FieldType) == sizeof(FieldType), "FieldType enum size mismatch"); + static_assert(static_cast(ecs::FieldType::_Count) == static_cast(FieldType::_Count), "FieldType enum value count mismatch"); + for (UInt64 i = 0; i < fieldCount; ++i) { + fieldVector.emplace_back(ecs::Field { + .name = static_cast(fields[i].name), + .type = static_cast(fields[i].type), + .size = fields[i].size, + .offset = fields[i].offset, + }); + } + + coordinator.addComponentDescription( + componentType, + ecs::ComponentDescription { + .name = name, + .fields = std::move(fieldVector), + } + ); + + return componentType; + } + + ComponentTypeIds NxGetComponentTypeIds() + { + auto& coordinator = *Application::m_coordinator; + + return ComponentTypeIds { + .Transform = coordinator.getComponentType(), + .AmbientLight = coordinator.getComponentType(), + .DirectionalLight = coordinator.getComponentType(), + .PointLight = coordinator.getComponentType(), + .SpotLight = coordinator.getComponentType(), + .RenderComponent = coordinator.getComponentType(), + .SceneTag = coordinator.getComponentType(), + .CameraComponent = coordinator.getComponentType(), + .UuidComponent = coordinator.getComponentType(), + .PerspectiveCameraController = coordinator.getComponentType(), + .PerspectiveCameraTarget = coordinator.getComponentType(), + }; + } + + bool NxIsKeyPressed(const Int32 keycode) + { + return event::isKeyPressed(keycode); + } + + bool NxIsKeyReleased(const Int32 keycode) + { + return event::isKeyReleased(keycode); + } + + bool NxIsAnyKeyPressed() + { + // GLFW key range: 32-96 (printable characters), 256-348 (special keys) + // Check printable ASCII range + for (Int32 key = 32; key <= 96; ++key) { + if (event::isKeyPressed(key)) { + return true; + } + } + + // Check special keys range + for (Int32 key = 256; key <= 348; ++key) { + if (event::isKeyPressed(key)) { + return true; + } + } + + return false; + } + + bool NxIsMouseDown(const Int32 button) + { + return event::isMouseDown(button); + } + + bool NxIsMouseReleased(const Int32 button) + { + return event::isMouseReleased(button); + } + + void NxGetMousePosition(Vector2 *position) + { + if (position) { + const auto mousePos = event::getMousePosition(); + position->x = mousePos.x; + position->y = mousePos.y; + } + } + } + + // Initialize the callbacks with the extern "C" functions + void initializeNativeApiCallbacks() { + nativeApiCallbacks.NxHelloFromNative = &NxHelloFromNative; + nativeApiCallbacks.NxAddNumbers = &NxAddNumbers; + nativeApiCallbacks.NxGetNativeMessage = &NxGetNativeMessage; + nativeApiCallbacks.NxLog = &NxLog; + nativeApiCallbacks.NxCreateCube = &NxCreateCube; + nativeApiCallbacks.NxGetTransformComponent = &NxGetTransformComponent; + nativeApiCallbacks.NxGetComponent = &NxGetComponent; + nativeApiCallbacks.NxAddComponent = &NxAddComponent; + nativeApiCallbacks.NxHasComponent = &NxHasComponent; + nativeApiCallbacks.NxRegisterComponent = &NxRegisterComponent; + nativeApiCallbacks.NxGetComponentTypeIds = &NxGetComponentTypeIds; + nativeApiCallbacks.NxIsKeyPressed = &NxIsKeyPressed; + nativeApiCallbacks.NxIsKeyReleased = &NxIsKeyReleased; + nativeApiCallbacks.NxIsAnyKeyPressed = &NxIsAnyKeyPressed; + nativeApiCallbacks.NxIsMouseDown = &NxIsMouseDown; + nativeApiCallbacks.NxIsMouseReleased = &NxIsMouseReleased; + nativeApiCallbacks.NxGetMousePosition = &NxGetMousePosition; + } + + // Static initialization + static struct NativeApiCallbacksInitializer { + NativeApiCallbacksInitializer() { + initializeNativeApiCallbacks(); + } + } nativeApiCallbacksInitializer; + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/NativeApi.hpp b/engine/src/scripting/native/NativeApi.hpp new file mode 100644 index 000000000..630979e33 --- /dev/null +++ b/engine/src/scripting/native/NativeApi.hpp @@ -0,0 +1,137 @@ +//// NativeApi.hpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 06/05/2025 +// Description: Header file for native API functions exposed to managed code +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#ifdef NEXO_EXPORT + #ifdef WIN32 + #define NEXO_API __declspec(dllexport) + #elif defined(__GNUC__) || defined(__clang__) + #define NEXO_API __attribute__((visibility("default"))) + #else + #define NEXO_API + #endif +#else + #ifdef WIN32 + #define NEXO_API __declspec(dllimport) + #else + #define NEXO_API + #endif +#endif + +#ifdef WIN32 // Set calling convention according to .NET https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute.callingconvention?view=net-9.0#remarks + #define NEXO_CALL __stdcall +#elif defined(__GNUC__) || defined(__clang__) + #define NEXO_CALL __attribute__((cdecl)) +#else + #define NEXO_CALL __cdecl +#endif + +#define NEXO_RET(type) NEXO_API type NEXO_CALL + +#include "Entity.hpp" +#include "ManagedTypedef.hpp" +#include "components/Transform.hpp" + +namespace nexo::scripting { + struct Field; + + template + struct ApiCallback; + + template + struct ApiCallback { + using Type = Ret (NEXO_CALL *)(Args...); + + // Default constructor + ApiCallback() : func(nullptr) {} + + // Constructor explicitly accepting function pointers + explicit ApiCallback(Type f) : func(f) {} + + // Assignment operator for initialization + ApiCallback& operator=(Type f) { + func = f; + return *this; + } + + explicit ApiCallback(nullptr_t) = delete; + + private: + Type func; + }; + + struct ComponentTypeIds { + UInt32 Transform; + UInt32 AmbientLight; + UInt32 DirectionalLight; + UInt32 PointLight; + UInt32 SpotLight; + UInt32 RenderComponent; + UInt32 SceneTag; + UInt32 CameraComponent; + UInt32 UuidComponent; + UInt32 PerspectiveCameraController; + UInt32 PerspectiveCameraTarget; + }; + + // Forward declare the extern "C" functions within the namespace + extern "C" { + NEXO_RET(void) NxHelloFromNative(void); + NEXO_RET(Int32) NxAddNumbers(Int32 a, Int32 b); + NEXO_RET(const char*) NxGetNativeMessage(void); + NEXO_RET(void) NxLog(UInt32 level, const char *message); + + NEXO_RET(ecs::Entity) NxCreateCube(Vector3 pos, Vector3 size, Vector3 rotation, Vector4 color); + NEXO_RET(components::TransformComponent *) NxGetTransformComponent(ecs::Entity entity); + NEXO_RET(void *) NxGetComponent(ecs::Entity entity, UInt32 componentTypeId); + NEXO_RET(void) NxAddComponent(ecs::Entity entity, UInt32 typeId, const void *componentData); + NEXO_RET(bool) NxHasComponent(ecs::Entity entity, UInt32 typeId); + NEXO_RET(Int64) NxRegisterComponent(const char *name, UInt64 componentSize, const Field *fields, UInt64 fieldCount); + NEXO_RET(ComponentTypeIds) NxGetComponentTypeIds(); + + NEXO_RET(bool) NxIsKeyPressed(Int32 keycode); + NEXO_RET(bool) NxIsKeyReleased(Int32 keycode); + NEXO_RET(bool) NxIsAnyKeyPressed(void); + NEXO_RET(bool) NxIsMouseDown(Int32 button); + NEXO_RET(bool) NxIsMouseReleased(Int32 button); + NEXO_RET(void) NxGetMousePosition(Vector2 *position); + } + + struct NativeApiCallbacks { + ApiCallback NxHelloFromNative; + ApiCallback NxAddNumbers; + ApiCallback NxGetNativeMessage; + ApiCallback NxLog; + + ApiCallback NxCreateCube; + ApiCallback NxGetTransformComponent; + ApiCallback NxGetComponent; + ApiCallback NxAddComponent; + ApiCallback NxHasComponent; + ApiCallback NxRegisterComponent; + ApiCallback NxGetComponentTypeIds; + ApiCallback NxIsKeyPressed; + ApiCallback NxIsKeyReleased; + ApiCallback NxIsAnyKeyPressed; + ApiCallback NxIsMouseDown; + ApiCallback NxIsMouseReleased; + ApiCallback NxGetMousePosition; + }; + + extern NativeApiCallbacks nativeApiCallbacks; + + void initializeNativeApiCallbacks(); + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/Scripting.cpp b/engine/src/scripting/native/Scripting.cpp new file mode 100644 index 000000000..d69b3acd1 --- /dev/null +++ b/engine/src/scripting/native/Scripting.cpp @@ -0,0 +1,375 @@ +//// Scripting.cpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 30/04/2025 +// Description: Source file for the scripting system +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#ifdef WIN32 + #define NOMINMAX + #include +#endif + + +#include "Scripting.hpp" +#include "HostString.hpp" +#include "Logger.hpp" +#include "ManagedApi.hpp" +#include "NativeApi.hpp" +#include "ManagedTypedef.hpp" +#include "core/event/SignalEvent.hpp" + +namespace nexo::scripting { + + HostHandler::~HostHandler() + { + if (m_dll_handle) { + if (m_hostfxr_fn.close) { + m_hostfxr_fn.close(nullptr); + } + } + } + + HostHandler::Status HostHandler::initialize(Parameters parameters) + { + if (m_status == SUCCESS) + return m_status; + + m_params = std::move(parameters); + + if (loadHostfxr() != SUCCESS) + return m_status; + + currentErrorCallback = m_params.errorCallback; + m_hostfxr_fn.set_error_writer([](const char_t* message) { + currentErrorCallback(message); + }); + + if (initRuntime() != SUCCESS) + return m_status; + + if (getRuntimeDelegates() != SUCCESS) + return m_status; + + if (loadManagedAssembly() != SUCCESS) + return m_status; + + // Take over signal handling because the CoreCLR library overrides them?? + event::SignalHandler::getInstance()->initSignals(); + + if (initManagedApi() != SUCCESS) + return m_status; + + if (initCallbacks() != SUCCESS) + return m_status; + + return m_status = SUCCESS; + } + + void *HostHandler::getManagedFptrVoid(const char_t* typeName, const char_t* methodName, + const char_t* delegateTypeName) const + { + if (m_status != SUCCESS) { + m_params.errorCallback("getManagedFptr: HostHandler not initialized"); + return nullptr; + } + + void *fptr = nullptr; + unsigned int rc = m_delegates.load_assembly_and_get_function_pointer( + m_assembly_path.c_str(), typeName, methodName, delegateTypeName, nullptr, &fptr); + if (rc != 0 || fptr == nullptr) { + m_params.errorCallback(std::format("Failed to get function pointer Type({}) Method({}): 0x{:X}", + typeName ? HostString(typeName).to_utf8() : "", + methodName ? HostString(methodName).to_utf8() : "", + rc) + ); + return nullptr; + } + return fptr; + } + + void HostHandler::defaultErrorCallback(const HostString& message) + { + std::cerr << "[Scripting] Error: " << message.to_utf8() << std::endl; + } + + HostHandler::Status HostHandler::loadHostfxr() + { + const auto assemblyPath = HostString(m_params.assemblyPath.c_str()); + const auto dotnetRoot = HostString(m_params.dotnetRoot.c_str()); + + get_hostfxr_parameters params { + sizeof(get_hostfxr_parameters), + assemblyPath.empty() ? nullptr : assemblyPath.c_str(), + dotnetRoot.empty() ? nullptr : dotnetRoot.c_str() + }; + // Pre-allocate a large buffer for the path to hostfxr + char_t buffer[MAX_PATH] = {0}; + size_t buffer_size = sizeof(buffer) / sizeof(char_t); + if (unsigned int rc = get_hostfxr_path(buffer, &buffer_size, ¶ms)) { + m_params.errorCallback(std::format("Failed to get hostfxr path. Error code 0x{:X}.", rc)); + return m_status = HOSTFXR_NOT_FOUND; + } + + m_dll_handle = std::make_shared(buffer, boost::dll::load_mode::default_mode); + if (m_dll_handle == nullptr || !m_dll_handle->is_loaded()) { + m_params.errorCallback(std::format("Failed to load hostfxr library from path: {}", HostString(buffer).to_utf8())); + return m_status = HOSTFXR_LOAD_ERROR; + } + + m_hostfxr_fn.set_error_writer = m_dll_handle->get>("hostfxr_set_error_writer"); + m_hostfxr_fn.init_for_cmd_line = m_dll_handle->get>("hostfxr_initialize_for_dotnet_command_line"); + m_hostfxr_fn.init_for_config = m_dll_handle->get>("hostfxr_initialize_for_runtime_config"); + m_hostfxr_fn.get_delegate = m_dll_handle->get>("hostfxr_get_runtime_delegate"); + m_hostfxr_fn.run_app = m_dll_handle->get>("hostfxr_run_app"); + m_hostfxr_fn.close = m_dll_handle->get>("hostfxr_close"); + + if (not (m_hostfxr_fn.set_error_writer && m_hostfxr_fn.init_for_cmd_line && m_hostfxr_fn.init_for_config + && m_hostfxr_fn.get_delegate && m_hostfxr_fn.run_app && m_hostfxr_fn.close)) { + m_params.errorCallback(std::format("Failed to load hostfxr functions from path: {}", HostString(buffer).to_utf8())); + return m_status = HOSTFXR_LOAD_ERROR; + } + + return m_status = SUCCESS; + } + + HostHandler::Status HostHandler::initRuntime() + { + const std::filesystem::path runtimeConfigPath = + m_params.nexoManagedPath / NEXO_RUNTIMECONFIG_FILENAME; + + if (!std::filesystem::exists(runtimeConfigPath)) + { + m_params.errorCallback(std::format("Nexo runtime config file not found: {}", runtimeConfigPath.string())); + return m_status = RUNTIME_CONFIG_NOT_FOUND; + } + + const HostString configPath = runtimeConfigPath.c_str(); + + // Load .NET Core + unsigned int rc = m_hostfxr_fn.init_for_config(configPath.c_str(), nullptr, &m_host_ctx); + if (rc != 0 || m_host_ctx == nullptr) { + m_params.errorCallback(std::format("Init failed: 0x{:X}", rc)); + m_hostfxr_fn.close(m_host_ctx); + return m_status = INIT_DOTNET_RUNTIME_ERROR; + } + return m_status = SUCCESS; + } + + HostHandler::Status HostHandler::getRuntimeDelegates() + { + unsigned int rc = 0; + + rc = m_hostfxr_fn.get_delegate(m_host_ctx, hdt_load_assembly, reinterpret_cast(&m_delegates.load_assembly)); + if (rc != 0 || m_delegates.load_assembly == nullptr) { + m_params.errorCallback(std::format("Failed to get 'load_assembly' delegate: 0x{:X}", rc)); + return m_status = GET_DELEGATES_ERROR; + } + + rc = m_hostfxr_fn.get_delegate(m_host_ctx, hdt_load_assembly_and_get_function_pointer, + reinterpret_cast(&m_delegates.load_assembly_and_get_function_pointer)); + if (rc != 0 || m_delegates.load_assembly_and_get_function_pointer == nullptr) { + m_params.errorCallback(std::format("Failed to get 'load_assembly_and_get_function_pointer' delegate: 0x{:X}", rc)); + return m_status = GET_DELEGATES_ERROR; + } + + rc = m_hostfxr_fn.get_delegate(m_host_ctx, hdt_get_function_pointer, + reinterpret_cast(&m_delegates.get_function_pointer)); + if (rc != 0 || m_delegates.get_function_pointer == nullptr) { + m_params.errorCallback(std::format("Failed to get 'get_function_pointer' delegate: 0x{:X}", rc)); + return m_status = GET_DELEGATES_ERROR; + } + return m_status = SUCCESS; + } + + HostHandler::Status HostHandler::loadManagedAssembly() + { + unsigned int rc = 0; + + const std::filesystem::path assemblyPath = + m_params.nexoManagedPath / NEXO_ASSEMBLY_FILENAME; + + if (!std::filesystem::exists(assemblyPath)) { + m_params.errorCallback(std::format("Nexo assembly file not found: {}", assemblyPath.string())); + return m_status = ASSEMBLY_NOT_FOUND; + } + + const HostString assemblyPathStr = assemblyPath.c_str(); + + rc = m_delegates.load_assembly(assemblyPathStr.c_str(), nullptr, nullptr); + if (rc != 0) { + m_params.errorCallback(std::format("Failed to load assembly at {}: 0x{:X}", assemblyPathStr.to_utf8(), rc)); + return m_status = LOAD_ASSEMBLY_ERROR; + } + m_assembly_path = assemblyPathStr; + return m_status = SUCCESS; + } + + + HostHandler::Status HostHandler::initManagedApi() + try { + m_managedApi.NativeInterop = { + .Initialize = getManagedFptr( + "Nexo.NativeInterop, Nexo", + "Initialize", + UNMANAGEDCALLERSONLY + ), + .DemonstrateNativeCalls = getManagedFptr( + "Nexo.NativeInterop, Nexo", + "DemonstrateNativeCalls", + UNMANAGEDCALLERSONLY + ), + .Update = getManagedFptr( + "Nexo.NativeInterop, Nexo", + "Update", + UNMANAGEDCALLERSONLY + ) + }; + + m_managedApi.Lib = { + .CustomEntryPoint = getManagedFptr( + "Nexo.Lib, Nexo", + "CustomEntryPoint", + "Nexo.Lib+CustomEntryPointDelegate, Nexo" + ), + .CustomEntryPointUnmanagedCallersOnly = getManagedFptr( + "Nexo.Lib, Nexo", + "CustomEntryPointUnmanagedCallersOnly", + UNMANAGEDCALLERSONLY + ), + .Hello = getManagedFptr( + "Nexo.Lib, Nexo", + "Hello" + ), + .Add = getManagedFptr( + "Nexo.Lib, Nexo", + "Add", + UNMANAGEDCALLERSONLY + ), + .AddToPtr = getManagedFptr( + "Nexo.Lib, Nexo", + "AddToPtr", + UNMANAGEDCALLERSONLY + ) + }; + + m_managedApi.SystemBase = { + .InitializeSystems = getManagedFptr( + "Nexo.Systems.SystemBase, Nexo", + "InitializeSystems", + UNMANAGEDCALLERSONLY + ), + .InitializeComponents = getManagedFptr( + "Nexo.Components.IComponentBase, Nexo", + "InitializeComponents", + UNMANAGEDCALLERSONLY + ), + .ShutdownSystems = getManagedFptr( + "Nexo.Systems.SystemBase, Nexo", + "ShutdownSystems", + UNMANAGEDCALLERSONLY + ), + .UpdateSystems = getManagedFptr( + "Nexo.Systems.SystemBase, Nexo", + "UpdateSystems", + UNMANAGEDCALLERSONLY + ) + }; + + return m_status = checkManagedApi(); + + } catch (const std::exception& e) { + m_params.errorCallback(std::format("Failed to initialize managed API: {}", e.what())); + return m_status = INIT_MANAGED_API_ERROR; + } + + HostHandler::Status HostHandler::checkManagedApi() + { + // Assert that the ManagedApiFn struct is properly defined + static_assert(sizeof(ManagedApiFn) == sizeof(nullptr), "ManagedApiFn: struct size is not a pointer size"); + // Assert that the ManagedApi struct is properly defined (check that every field is void *) + if constexpr (sizeof(ManagedApi) % sizeof(nullptr) != 0) { + m_params.errorCallback("ManagedApi: struct size is not a multiple of pointer size"); + } + + // Check all fields of ManagedApi for nullptr + constexpr size_t nbFields = sizeof(ManagedApi) / sizeof(void*); + + for (size_t i = 0; i < nbFields; ++i) { + void **fieldPtr = reinterpret_cast(&m_managedApi) + i; + if (*fieldPtr == nullptr) { + m_params.errorCallback(std::format("ManagedApi: ManagedApiFn function pointer number {} is null in the struct", i)); + return m_status = INIT_MANAGED_API_ERROR; + } + } + + return m_status = SUCCESS; + } + + HostHandler::Status HostHandler::initCallbacks() + { + // Ensure callbacks are initialized + initializeNativeApiCallbacks(); + + // Initialize callbacks + if (m_managedApi.NativeInterop.Initialize(&nativeApiCallbacks, sizeof(nativeApiCallbacks))) { + m_params.errorCallback("Failed to initialize native API callbacks"); + return m_status = INIT_CALLBACKS_ERROR; + } + return m_status = SUCCESS; + } + + int HostHandler::runScriptExample() + { + // Run managed code + // Call the Hello method multiple times + for (int i = 0; i < 3; ++i) { + lib_args args { + STR("from host!"), + i + }; + + m_managedApi.Lib.Hello(&args, sizeof(args)); + } + + + // Call UnmanagedCallersOnly method + lib_args args_unmanaged { + STR("from host!"), + -1 + }; + m_managedApi.Lib.CustomEntryPointUnmanagedCallersOnly(args_unmanaged); + + + // Call custom delegate type method + m_managedApi.Lib.CustomEntryPoint(args_unmanaged); + + std::cout << "Testing Add(30, -10) = " << m_managedApi.Lib.Add(30, -10) << std::endl; + + int32_t result = 0; + if (m_managedApi.Lib.AddToPtr(1000, 234, &result)) { + std::cout << "addToPtr returned an error" << std::endl; + } else { + std::cout << "Testing AddToPtr(1000, 234, ptr), *ptr = " << result << std::endl; + } + + // Demonstrate C# calling C++ (managed to native) + // Call the method to demonstrate calling C++ from C# + std::cout << "\nDemonstrating calling C++ functions from C#:" << std::endl; + m_managedApi.NativeInterop.DemonstrateNativeCalls(); + + return EXIT_SUCCESS; + } + +} diff --git a/engine/src/scripting/native/Scripting.hpp b/engine/src/scripting/native/Scripting.hpp new file mode 100644 index 000000000..61e936b1a --- /dev/null +++ b/engine/src/scripting/native/Scripting.hpp @@ -0,0 +1,193 @@ +//// Scripting.hpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 30/04/2025 +// Description: Header file for the scripting system +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "HostString.hpp" +#include "ManagedApi.hpp" +#include "Path.hpp" + +#ifdef WIN32 + #define STR(s) L ## s + #define CH(c) L ## c + #define DIR_SEPARATOR L'\\' +#else + #include + #include + + #define STR(s) s + #define CH(c) c + #define DIR_SEPARATOR '/' + #define MAX_PATH PATH_MAX +#endif + +namespace nexo::scripting { + + struct HostfxrFn { + hostfxr_set_error_writer_fn set_error_writer; + hostfxr_initialize_for_dotnet_command_line_fn init_for_cmd_line; + hostfxr_initialize_for_runtime_config_fn init_for_config; + hostfxr_get_runtime_delegate_fn get_delegate; + hostfxr_run_app_fn run_app; + hostfxr_close_fn close; + }; + + struct CoreclrDelegate { + load_assembly_fn load_assembly; + load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer; + get_function_pointer_fn get_function_pointer; + }; + + class ScriptingBackendInitFailed final : public Exception { + public: + explicit ScriptingBackendInitFailed(const std::string &message, + const std::source_location loc = std::source_location::current()) + : Exception("Couldn't load scripting backend: " + message, loc) {} + }; + + class HostHandler { + protected: + static void defaultErrorCallback(const HostString& message); + public: + using ErrorCallBackFn = std::function; + + // Globals + static inline const std::filesystem::path DEFAULT_NEXO_MANAGED_PATH = + Path::resolvePathRelativeToExe("."); // TODO: Change it later for packing + static inline const std::string NEXO_RUNTIMECONFIG_FILENAME = "Nexo.runtimeconfig.json"; + static inline const std::string NEXO_ASSEMBLY_FILENAME = "Nexo.dll"; + static inline const ErrorCallBackFn DEFAULT_ERROR_CALLBACK = HostHandler::defaultErrorCallback; + + protected: + // Singleton: protected constructor and destructor + HostHandler() = default; + ~HostHandler(); + + public: + // Singleton: Meyers' Singleton Pattern + static HostHandler& getInstance() + { + static HostHandler s_instance; + return s_instance; + } + + // Singleton: delete copy constructor and assignment operator + HostHandler(HostHandler const&) = delete; + void operator=(HostHandler const&) = delete; + + enum Status { + SUCCESS, + UNINITIALIZED, + + HOSTFXR_NOT_FOUND, + HOSTFXR_LOAD_ERROR, + + RUNTIME_CONFIG_NOT_FOUND, + INIT_DOTNET_RUNTIME_ERROR, + + GET_DELEGATES_ERROR, + + ASSEMBLY_NOT_FOUND, + LOAD_ASSEMBLY_ERROR, + + INIT_MANAGED_API_ERROR, + + INIT_CALLBACKS_ERROR, + + }; + + + struct Parameters { + // Hostfxr parameters, see https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#initialize-host-context + std::filesystem::path assemblyPath; + std::filesystem::path dotnetRoot; + + // Nexo + std::filesystem::path nexoManagedPath = DEFAULT_NEXO_MANAGED_PATH; + + ErrorCallBackFn errorCallback = DEFAULT_ERROR_CALLBACK; + }; + static inline ErrorCallBackFn currentErrorCallback = nullptr; + + Status initialize(Parameters parameters); + + const ManagedApi& getManagedApi() const + { + return m_managedApi; + } + + inline void *getManagedFptrVoid(const char_t *typeName, const char_t *methodName, const char_t *delegateTypeName) const; + + // activate if you want to use the function pointer directly + template + inline T getManagedFptr(const HostString& typeName, const HostString& methodName, const HostString& delegateTypeName) + { + return reinterpret_cast(getManagedFptrVoid(typeName.c_str(), methodName.c_str(), delegateTypeName.c_str())); + } + + enum ManagedFptrFlags { + NONE = 0, + UNMANAGEDCALLERSONLY = 1 << 0, + }; + + template + inline T getManagedFptr(const HostString& typeName, const HostString& methodName, const ManagedFptrFlags flags = NONE) + { + const char_t *delegateTypeName = flags & UNMANAGEDCALLERSONLY ? UNMANAGEDCALLERSONLY_METHOD : nullptr; + return reinterpret_cast(getManagedFptrVoid(typeName.c_str(), methodName.c_str(), delegateTypeName)); + } + + + int runScriptExample(); + + protected: + + Status loadHostfxr(); + Status initRuntime(); + Status getRuntimeDelegates(); + Status loadManagedAssembly(); + Status initManagedApi(); + Status checkManagedApi(); + Status initCallbacks(); + + + + protected: + Status m_status = UNINITIALIZED; + Parameters m_params; + HostString m_assembly_path; + + HostfxrFn m_hostfxr_fn = {}; + CoreclrDelegate m_delegates = {}; + ManagedApi m_managedApi = {}; + + std::shared_ptr m_dll_handle = nullptr; + hostfxr_handle m_host_ctx = nullptr; + }; + + + int runScriptExample(const HostHandler::Parameters& params); + + // Function to register native callback functions + void registerNativeFunctions(); + +} // namespace nexo::scripting diff --git a/engine/src/scripting/native/systems/ManagedWorldState.hpp b/engine/src/scripting/native/systems/ManagedWorldState.hpp new file mode 100644 index 000000000..019bf99d9 --- /dev/null +++ b/engine/src/scripting/native/systems/ManagedWorldState.hpp @@ -0,0 +1,47 @@ +//// WorldState.hpp /////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 21/06/2025 +// Description: Header file for the WorldState class, +// which manages the state of the world in the game engine +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "Application.hpp" + +#include "scripting/native/ManagedTypedef.hpp" + +namespace nexo::scripting { + + /** + * @brief Represents the state of the world in the game engine. + * This structure is 1 to 1 equivalent to the WorldState class in the C# managed code. + * @warning If you modify this structure, ensure that the C# managed code is updated accordingly. + */ + struct ManagedWorldState { + struct WorldTime { + Double deltaTime = 0.0; // Time since last update + Double totalTime = 0.0; // Total time since the start of the world + } time; + + struct WorldStats { + UInt64 frameCount = 0; // Number of frames rendered + } stats; + + void update(const WorldState& worldState) { + // Update the world state with the current time and stats + time.deltaTime = worldState.time.deltaTime; + time.totalTime = worldState.time.totalTime; + stats.frameCount = worldState.stats.frameCount; + } + }; + +} // namespace nexo::scripting \ No newline at end of file diff --git a/engine/src/scripting/native/ui/Field.hpp b/engine/src/scripting/native/ui/Field.hpp new file mode 100644 index 000000000..72ba24c10 --- /dev/null +++ b/engine/src/scripting/native/ui/Field.hpp @@ -0,0 +1,33 @@ +//// Field.hpp //////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the field struct used in UI scripting, +// which represents a field in the UI with its properties +// this struct is passed by the C# code to the native code +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "FieldType.hpp" +#include "scripting/native/ManagedTypedef.hpp" + +namespace nexo::scripting { + + struct Field { + IntPtr name; // Pointer to the name of the field + FieldType type; // Type of the field (e.g., Int, Float, String, etc.) + UInt64 size; // Size of the field in bytes + UInt64 offset; // Offset of the field in the component + }; + +} // namespace nexo::ecs diff --git a/engine/src/scripting/native/ui/FieldType.hpp b/engine/src/scripting/native/ui/FieldType.hpp new file mode 100644 index 000000000..3e678dfcb --- /dev/null +++ b/engine/src/scripting/native/ui/FieldType.hpp @@ -0,0 +1,47 @@ +//// FieldType.hpp //////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 25/06/2025 +// Description: Header file for the field type enumeration +// used in UI components +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace nexo::scripting { + + enum class FieldType : uint64_t { + // Special type, if blank, the field is not used + Blank, + Section, // Used to create a section with title in the UI + + // Primitive types + Bool, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, + Float, + Double, + + // Widgets + Vector3, + Vector4, + + _Count // Count of the number of field types, used for validation + }; + +} // namespace nexo::scripting diff --git a/engine/src/systems/CameraSystem.cpp b/engine/src/systems/CameraSystem.cpp index fa3af485c..301435b58 100644 --- a/engine/src/systems/CameraSystem.cpp +++ b/engine/src/systems/CameraSystem.cpp @@ -1,4 +1,3 @@ -//// CameraSystem.cpp /////////////////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -21,30 +20,47 @@ #include "core/event/KeyCodes.hpp" #include "Application.hpp" #include "core/event/WindowEvent.hpp" -#include "math/Vector.hpp" +#include "core/exceptions/Exceptions.hpp" #include #include namespace nexo::system { - void CameraContextSystem::update() const + + void CameraContextSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto camera : entities) + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); + + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + if (!partition) { + LOG_ONCE(NEXO_WARN, "No camera found in scene {}, skipping", sceneName); + return; + } + nexo::Logger::resetOnce(NEXO_LOG_ONCE_KEY("No camera found in scene {}, skipping", sceneName)); + + const auto cameraSpan = get(); + const auto transformComponentArray = get(); + const auto entitySpan = m_group->entities(); + + for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { - auto tag = coord->getComponent(camera); - if (!tag.isRendered || sceneRendered != tag.id) + const auto &cameraComponent = cameraSpan[i]; + if (!cameraComponent.render) continue; - - auto cameraComponent = coord->getComponent(camera); - auto transformComponent = coord->getComponent(camera); + const auto &transformComponent = transformComponentArray->get(entitySpan[i]); glm::mat4 projectionMatrix = cameraComponent.getProjectionMatrix(); glm::mat4 viewMatrix = cameraComponent.getViewMatrix(transformComponent); - glm::mat4 viewProjectionMatrix = projectionMatrix * viewMatrix; + const glm::mat4 viewProjectionMatrix = projectionMatrix * viewMatrix; components::CameraContext context{viewProjectionMatrix, transformComponent.pos, cameraComponent.clearColor, cameraComponent.m_renderTarget}; renderContext.cameras.push(context); } @@ -52,110 +68,148 @@ namespace nexo::system { PerspectiveCameraControllerSystem::PerspectiveCameraControllerSystem() { - Application::getInstance().getEventManager()->registerListener(this); - Application::getInstance().getEventManager()->registerListener(this); + Application::getInstance().getEventManager()->registerListener(this); + Application::getInstance().getEventManager()->registerListener(this); } - void PerspectiveCameraControllerSystem::update(const Timestep ts) const - { - const auto &renderContext = coord->getSingletonComponent(); + void PerspectiveCameraControllerSystem::update(const Timestep ts) + { + const auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); + const auto deltaTime = static_cast(ts); - const auto deltaTime = static_cast(ts); - - for (const auto &entity : entities) - { - constexpr float translationSpeed = 5.0f; - auto tag = coord->getComponent(entity); - if (!tag.isActive || sceneRendered != tag.id) - continue; - - auto &cameraComponent = coord->getComponent(entity); - cameraComponent.resizing = false; - auto &transform = coord->getComponent(entity); - - glm::vec3 front = transform.quat * glm::vec3(0.0f, 0.0f, -1.0f); - glm::vec3 up = transform.quat * glm::vec3(0.0f, 1.0f, 0.0f); - glm::vec3 right = transform.quat * glm::vec3(1.0f, 0.0f, 0.0f); - - if (event::isKeyPressed(NEXO_KEY_Z)) - transform.pos += front * translationSpeed * deltaTime; // Forward - if (event::isKeyPressed(NEXO_KEY_S)) - transform.pos -= front * translationSpeed * deltaTime; // Backward - if (event::isKeyPressed(NEXO_KEY_Q)) - transform.pos -= right * translationSpeed * deltaTime; // Left - if (event::isKeyPressed(NEXO_KEY_D)) - transform.pos += right * translationSpeed * deltaTime; // Right - if (event::isKeyPressed(NEXO_KEY_SPACE)) - transform.pos += up * translationSpeed * deltaTime; // Up - if (event::isKeyPressed(NEXO_KEY_TAB)) - transform.pos -= up * translationSpeed * deltaTime; // Down - } - } + for (const ecs::Entity entity : entities) + { + auto &sceneTag = getComponent(entity); + if (!sceneTag.isActive || sceneTag.id != sceneRendered) + continue; + auto &cameraComponent = getComponent(entity); + if (!cameraComponent.active) + continue; + auto &transform = getComponent(entity); + auto &cameraController = getComponent(entity); + + cameraComponent.resizing = false; + + if (event::isKeyPressed(NEXO_KEY_SHIFT)) + cameraController.translationSpeed = 10.0f; + if (event::isKeyReleased(NEXO_KEY_SHIFT)) + cameraController.translationSpeed = 5.0f; + + glm::vec3 front = transform.quat * glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 up = transform.quat * glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 right = transform.quat * glm::vec3(1.0f, 0.0f, 0.0f); + + if (event::isKeyPressed(NEXO_KEY_Z)) + transform.pos += front * cameraController.translationSpeed * deltaTime; // Forward + if (event::isKeyPressed(NEXO_KEY_S)) + transform.pos -= front * cameraController.translationSpeed * deltaTime; // Backward + if (event::isKeyPressed(NEXO_KEY_Q)) + transform.pos -= right * cameraController.translationSpeed * deltaTime; // Left + if (event::isKeyPressed(NEXO_KEY_D)) + transform.pos += right * cameraController.translationSpeed * deltaTime; // Right + if (event::isKeyPressed(NEXO_KEY_SPACE)) + transform.pos += up * cameraController.translationSpeed * deltaTime; // Up + if (event::isKeyPressed(NEXO_KEY_TAB)) + transform.pos -= up * cameraController.translationSpeed * deltaTime; // Down + } + } - void PerspectiveCameraControllerSystem::handleEvent(event::EventMouseScroll &event) - { - auto const &renderContext = coord->getSingletonComponent(); + void PerspectiveCameraControllerSystem::handleEvent(event::EventMouseScroll &event) + { + const auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &camera : entities) - { - auto tag = coord->getComponent(camera); - if (!tag.isActive || sceneRendered != tag.id) - continue; - constexpr float zoomSpeed = 0.5f; - auto &transform = coord->getComponent(camera); - glm::vec3 front = transform.quat * glm::vec3(0.0f, 0.0f, -1.0f); - transform.pos += front * event.y * zoomSpeed; - event.consumed = true; - } - } + for (const ecs::Entity entity : entities) + { + constexpr float zoomSpeed = 0.5f; + auto &sceneTag = getComponent(entity); + const auto &cameraComponent = getComponent(entity); + if (!sceneTag.isActive || sceneTag.id != sceneRendered || !cameraComponent.active) + continue; + auto &transform = getComponent(entity); + glm::vec3 front = transform.quat * glm::vec3(0.0f, 0.0f, -1.0f); + transform.pos += front * event.y * zoomSpeed; + event.consumed = true; + } + } - void PerspectiveCameraControllerSystem::handleEvent(event::EventMouseMove &event) - { - auto const &renderContext = coord->getSingletonComponent(); - if (renderContext.sceneRendered == -1) - return; + void PerspectiveCameraControllerSystem::handleEvent(event::EventMouseMove &event) + { + auto const &renderContext = getSingleton(); + if (renderContext.sceneRendered == -1) + return; - const auto sceneRendered = static_cast(renderContext.sceneRendered); + const auto sceneRendered = static_cast(renderContext.sceneRendered); + const glm::vec2 currentMousePosition(event.x, event.y); - glm::vec2 currentMousePosition(event.x, event.y); - for (const auto &camera : entities) + for (const ecs::Entity entity : entities) { - auto &controller = coord->getComponent(camera); - auto const &cameraComponent = coord->getComponent(camera); - auto tag = coord->getComponent(camera); - const glm::vec2 mouseDelta = (currentMousePosition - controller.lastMousePosition) * controller.mouseSensitivity; - controller.lastMousePosition = currentMousePosition; + auto &controller = getComponent(entity); + const auto &sceneTag = getComponent(entity); + const auto &cameraComponent = getComponent(entity); + bool isActiveScene = sceneTag.isActive && sceneTag.id == sceneRendered; + bool isActiveCamera = isActiveScene && cameraComponent.active; + bool mouseDown = event::isMouseDown(NEXO_MOUSE_LEFT); + + // Check for scene transition - if the camera wasn't active before but is now + bool sceneTransition = isActiveCamera && !controller.wasActiveLastFrame; + controller.wasActiveLastFrame = isActiveCamera; + + // Reset position on scene transition to prevent abrupt rotation + if (sceneTransition) { + controller.lastMousePosition = currentMousePosition; + controller.wasMouseReleased = true; + continue; + } + if (!isActiveCamera) + continue; - if (!tag.isActive || sceneRendered != tag.id || cameraComponent.resizing || !event::isMouseDown(NEXO_MOUSE_LEFT)) + // Always update lastMousePosition if this is the active scene, even if not moving the camera + // This ensures the position is current when we start dragging + if (!mouseDown || controller.wasMouseReleased) { + controller.lastMousePosition = currentMousePosition; + controller.wasMouseReleased = false; continue; + } + + if (cameraComponent.resizing) { + controller.lastMousePosition = currentMousePosition; + continue; + } + + auto &transform = getComponent(entity); + const glm::vec2 mouseDelta = (currentMousePosition - controller.lastMousePosition) * controller.mouseSensitivity; - controller.yaw += -mouseDelta.x; - controller.pitch += -mouseDelta.y; + // Extract camera orientation vectors from current quaternion + glm::vec3 right = transform.quat * glm::vec3(1.0f, 0.0f, 0.0f); - // Clamp pitch to avoid flipping - if (controller.pitch > 89.0f) - controller.pitch = 89.0f; - if (controller.pitch < -89.0f) - controller.pitch = -89.0f; - // Rebuild the quaternion from yaw and pitch. - glm::quat qPitch = glm::angleAxis(glm::radians(controller.pitch), glm::vec3(1.0f, 0.0f, 0.0f)); - glm::quat qYaw = glm::angleAxis(glm::radians(controller.yaw), glm::vec3(0.0f, 1.0f, 0.0f)); + // Create rotation quaternions based on mouse movement + glm::quat pitchRotation = glm::angleAxis(glm::radians(-mouseDelta.y), right); + glm::quat yawRotation = glm::angleAxis(glm::radians(-mouseDelta.x), glm::vec3(0.0f, 1.0f, 0.0f)); // World up for yaw + glm::quat newQuat = glm::normalize(yawRotation * pitchRotation * transform.quat); + glm::vec3 newFront = newQuat * glm::vec3(0.0f, 0.0f, -1.0f); - auto &transform = coord->getComponent(camera); - transform.quat = glm::normalize(qYaw * qPitch); + // Check if the resulting orientation would flip the camera (pitch constraint) + float pitchAngle = glm::degrees(std::asin(newFront.y)); + if (pitchAngle < -85.0f || pitchAngle > 85.0f) + transform.quat = glm::normalize(yawRotation * transform.quat); + else + transform.quat = newQuat; + + // Update last position after processing + controller.lastMousePosition = currentMousePosition; event.consumed = true; } - } + } PerspectiveCameraTargetSystem::PerspectiveCameraTargetSystem() { @@ -165,110 +219,104 @@ namespace nexo::system { void PerspectiveCameraTargetSystem::handleEvent(event::EventMouseScroll &event) { - auto const &renderContext = coord->getSingletonComponent(); + auto const &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &camera : entities) - { - auto tag = coord->getComponent(camera); - if (!tag.isActive || sceneRendered != tag.id) - continue; - constexpr float zoomSpeed = 0.5f; - auto &target = coord->getComponent(camera); - target.distance -= event.y * zoomSpeed; - if(target.distance < 0.1f) - target.distance = 0.1f; + for (const ecs::Entity entity : entities) + { + constexpr float zoomSpeed = 0.5f; + auto &tag = getComponent(entity); + const auto &cameraComponent = getComponent(entity); + if (!tag.isActive || sceneRendered != tag.id || !cameraComponent.active) + continue; + auto &target = getComponent(entity); + target.distance -= event.y * zoomSpeed; + if (target.distance < 0.1f) + target.distance = 0.1f; - auto &transformCamera = coord->getComponent(camera); - auto const &transformTarget = coord->getComponent(target.targetEntity); + auto &transformCamera = getComponent(entity); + const auto &transformTarget = getComponent(target.targetEntity); - glm::vec3 offset = transformCamera.pos - transformTarget.pos; - // If offset is near zero, choose a default direction. - if(glm::length(offset) < 0.001f) - offset = glm::vec3(0, 0, 1); + glm::vec3 offset = transformCamera.pos - transformTarget.pos; + // If offset is near zero, choose a default direction. + if(glm::length(offset) < 0.001f) + offset = glm::vec3(0, 0, 1); - offset = glm::normalize(offset) * target.distance; + offset = glm::normalize(offset) * target.distance; - transformCamera.pos = transformTarget.pos + offset; + transformCamera.pos = transformTarget.pos + offset; - glm::vec3 newFront = glm::normalize(transformTarget.pos - transformCamera.pos); - transformCamera.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0,1,0))); + glm::vec3 newFront = glm::normalize(transformTarget.pos - transformCamera.pos); + transformCamera.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0,1,0))); - event.consumed = true; + event.consumed = true; } } void PerspectiveCameraTargetSystem::handleEvent(event::EventMouseMove &event) { - auto const &renderContext = coord->getSingletonComponent(); + const auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - glm::vec2 currentMousePosition(event.x, event.y); - - for (const auto &entity : entities) - { - auto &targetComp = coord->getComponent(entity); - auto tag = coord->getComponent(entity); - auto const &cameraComponent = coord->getComponent(entity); - - if (!tag.isActive || sceneRendered != tag.id || cameraComponent.resizing) - { - targetComp.lastMousePosition = currentMousePosition; - continue; - } + glm::vec2 currentMousePosition(event.x, event.y); - if (!event::isMouseDown(NEXO_MOUSE_RIGHT)) - { - targetComp.lastMousePosition = currentMousePosition; - continue; - } + for (const ecs::Entity entity : entities) + { + const auto &sceneTag = getComponent(entity); + const auto &cameraComponent = getComponent(entity); + auto &targetComponent = getComponent(entity); + if (!sceneTag.isActive || sceneTag.id != sceneRendered || cameraComponent.resizing || !event::isMouseDown(NEXO_MOUSE_RIGHT) || !cameraComponent.active) + { + targetComponent.lastMousePosition = currentMousePosition; + continue; + } - auto &transformCamera = coord->getComponent(entity); - auto const &transformTarget = coord->getComponent(targetComp.targetEntity); + auto &transformCameraComponent = getComponent(entity); + const auto &transformTargetComponent = getComponent(targetComponent.targetEntity); - float deltaX = targetComp.lastMousePosition.x - currentMousePosition.x; - float deltaY = targetComp.lastMousePosition.y - currentMousePosition.y; + float deltaX = targetComponent.lastMousePosition.x - currentMousePosition.x; + float deltaY = targetComponent.lastMousePosition.y - currentMousePosition.y; - // Compute rotation angles based on screen dimensions. - float xAngle = deltaX * (2.0f * std::numbers::pi_v / static_cast(cameraComponent.width)); - float yAngle = deltaY * (std::numbers::pi_v / static_cast(cameraComponent.height)); + // Compute rotation angles based on screen dimensions. + float xAngle = deltaX * (2.0f * std::numbers::pi_v / static_cast(cameraComponent.width)); + float yAngle = deltaY * (std::numbers::pi_v / static_cast(cameraComponent.height)); - // Prevent excessive pitch rotation when the camera is nearly vertical. - glm::vec3 front = glm::normalize(transformTarget.pos - transformCamera.pos); - auto sgn = [](float x) { return (x >= 0.0f ? 1.0f : -1.0f); }; - if (glm::dot(front, glm::vec3(0, 1, 0)) * sgn(yAngle) > 0.99f) - yAngle = 0.0f; + // Prevent excessive pitch rotation when the camera is nearly vertical. + glm::vec3 front = glm::normalize(transformTargetComponent.pos - transformCameraComponent.pos); + auto sgn = [](float x) { return (x >= 0.0f ? 1.0f : -1.0f); }; + if (glm::dot(front, glm::vec3(0, 1, 0)) * sgn(yAngle) > 0.99f) + yAngle = 0.0f; - glm::vec3 offset = (transformCamera.pos - transformTarget.pos); + glm::vec3 offset = (transformCameraComponent.pos - transformTargetComponent.pos); - glm::quat qYaw = glm::angleAxis(xAngle, glm::vec3(0, 1, 0)); + glm::quat qYaw = glm::angleAxis(xAngle, glm::vec3(0, 1, 0)); - // For the pitch (vertical rotation), compute the right axis. - // This is the normalized cross product between the world up and the offset vector. - glm::vec3 rightAxis = glm::normalize(glm::cross(glm::vec3(0, 1, 0), offset)); - if (glm::length(rightAxis) < 0.001f) // Fallback if the vector is degenerate. - rightAxis = glm::vec3(1, 0, 0); - glm::quat qPitch = glm::angleAxis(yAngle, rightAxis); + // For the pitch (vertical rotation), compute the right axis. + // This is the normalized cross product between the world up and the offset vector. + glm::vec3 rightAxis = glm::normalize(glm::cross(glm::vec3(0, 1, 0), offset)); + if (glm::length(rightAxis) < 0.001f) // Fallback if the vector is degenerate. + rightAxis = glm::vec3(1, 0, 0); + glm::quat qPitch = glm::angleAxis(yAngle, rightAxis); - glm::quat incrementalRotation = qYaw * qPitch; + glm::quat incrementalRotation = qYaw * qPitch; - glm::vec3 newOffset = incrementalRotation * offset; + glm::vec3 newOffset = incrementalRotation * offset; - newOffset = glm::normalize(newOffset) * targetComp.distance; + newOffset = glm::normalize(newOffset) * targetComponent.distance; - transformCamera.pos = transformTarget.pos + newOffset; + transformCameraComponent.pos = transformTargetComponent.pos + newOffset; - glm::vec3 newFront = glm::normalize(transformTarget.pos - transformCamera.pos); - transformCamera.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0, 1, 0))); + glm::vec3 newFront = glm::normalize(transformTargetComponent.pos - transformCameraComponent.pos); + transformCameraComponent.quat = glm::normalize(glm::quatLookAt(newFront, glm::vec3(0, 1, 0))); - targetComp.lastMousePosition = currentMousePosition; + targetComponent.lastMousePosition = currentMousePosition; event.consumed = true; - } + } } } diff --git a/engine/src/systems/CameraSystem.hpp b/engine/src/systems/CameraSystem.hpp index 817037f43..414f62fce 100644 --- a/engine/src/systems/CameraSystem.hpp +++ b/engine/src/systems/CameraSystem.hpp @@ -14,11 +14,17 @@ #pragma once #include "ecs/System.hpp" +#include "ecs/GroupSystem.hpp" +#include "ecs/QuerySystem.hpp" #include "Timestep.hpp" #include "core/event/Event.hpp" #include "core/event/WindowEvent.hpp" +#include "components/Camera.hpp" +#include "components/SceneComponents.hpp" +#include "components/RenderContext.hpp" namespace nexo::system { + /** * @brief System responsible for updating the camera context. * @@ -26,59 +32,92 @@ namespace nexo::system { * matrices using the CameraComponent and TransformComponent. The computed CameraContext is * then pushed into the RenderContext (a singleton component). * - * @note Required Components on camera entities: - * - components::SceneTag - * - components::CameraComponent - * - components::TransformComponent + * @note Component Access Rights: + * - READ access to components::CameraComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - READ access to components::TransformComponent (non-owned) + * - WRITE access to components::RenderContext (singleton) * - * @note Required Singleton Component: - * - components::RenderContext + * @note The system uses scene partitioning to only process camera entities belonging to the + * currently active scene (identified by RenderContext.sceneRendered). */ - class CameraContextSystem : public ecs::System { + class CameraContextSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read>, + ecs::NonOwned< + ecs::Read, + ecs::Read>, + ecs::WriteSingleton> { public: - void update() const; + void update(); }; /** - * @brief System for controlling perspective cameras via keyboard and mouse input. - * - * This system handles movement of perspective cameras based on keyboard input (e.g. WASD, - * space, tab) and adjusts camera orientation based on mouse movement. It also processes mouse - * scroll events for zooming. - * - * @note Required Components on camera entities: - * - components::PerspectiveCameraController - * - components::SceneTag - * - components::CameraComponent - * - components::TransformComponent - */ - class PerspectiveCameraControllerSystem : public ecs::System, LISTENS_TO( - event::EventMouseScroll, - event::EventMouseMove) { + * @brief System for controlling perspective cameras via keyboard and mouse input. + * + * This system handles movement of perspective cameras based on keyboard input (e.g. WASD, + * space, tab) and adjusts camera orientation based on mouse movement. It also processes mouse + * scroll events for zooming. + * + * @note Component Access Rights: + * - WRITE access to components::CameraComponent + * - WRITE access to components::PerspectiveCameraController + * - READ access to components::SceneTag + * - WRITE access to components::TransformComponent + * - READ access to components::RenderContext (singleton) + * + * @note Event Listeners: + * - event::EventMouseScroll - For camera zoom functionality + * - event::EventMouseMove - For camera rotation functionality + * + * @note The system only processes camera entities belonging to the currently active scene. + */ + class PerspectiveCameraControllerSystem final : public ecs::QuerySystem< + ecs::Write, + ecs::Write, + ecs::Read, + ecs::Write, + ecs::ReadSingleton>, + LISTENS_TO( + event::EventMouseScroll, + event::EventMouseMove) { public: PerspectiveCameraControllerSystem(); - void update(Timestep ts) const; + void update(Timestep ts); void handleEvent(event::EventMouseScroll &event) override; void handleEvent(event::EventMouseMove &event) override; }; /** - * @brief System for controlling perspective cameras that orbit around a target. - * - * This system processes mouse scroll and mouse move events to adjust the camera’s distance - * from its target entity as well as its orientation to always face the target. The camera's - * position is updated accordingly. - * - * @note Required Components on camera entities: - * - components::PerspectiveCameraTarget - * - components::SceneTag - * - components::CameraComponent - * - components::TransformComponent - */ - class PerspectiveCameraTargetSystem : public ecs::System, LISTENS_TO( - event::EventMouseScroll, - event::EventMouseMove) { + * @brief System for controlling perspective cameras that orbit around a target. + * + * This system processes mouse scroll and mouse move events to adjust the camera's distance + * from its target entity as well as its orientation to always face the target. The camera's + * position is updated accordingly. + * + * @note Component Access Rights: + * - WRITE access to components::CameraComponent + * - WRITE access to components::PerspectiveCameraTarget + * - READ access to components::SceneTag + * - WRITE access to components::TransformComponent + * - READ access to components::RenderContext (singleton) + * + * @note Event Listeners: + * - event::EventMouseScroll - For adjusting camera distance from target + * - event::EventMouseMove - For orbiting camera around target + * + * @note The system only processes camera entities belonging to the currently active scene. + */ + class PerspectiveCameraTargetSystem final : public ecs::QuerySystem< + ecs::Write, + ecs::Write, + ecs::Read, + ecs::Write, + ecs::ReadSingleton>, + LISTENS_TO( + event::EventMouseScroll, + event::EventMouseMove) { public: PerspectiveCameraTargetSystem(); void handleEvent(event::EventMouseMove &event) override; diff --git a/engine/src/systems/RenderSystem.cpp b/engine/src/systems/RenderSystem.cpp index a9c2c90b5..7d01fc99e 100644 --- a/engine/src/systems/RenderSystem.cpp +++ b/engine/src/systems/RenderSystem.cpp @@ -14,121 +14,345 @@ #include "RenderSystem.hpp" #include "RendererContext.hpp" +#include "components/Editor.hpp" +#include "components/Light.hpp" #include "components/RenderContext.hpp" #include "components/SceneComponents.hpp" #include "components/Camera.hpp" #include "components/Transform.hpp" #include "components/Render.hpp" +#include "core/event/Input.hpp" +#include "math/Projection.hpp" +#include "math/Vector.hpp" #include "renderer/RenderCommand.hpp" #include "ecs/Coordinator.hpp" +#include "core/exceptions/Exceptions.hpp" +#include "Application.hpp" -#include #include - namespace nexo::system { - /** - * @brief Sets up the lighting uniforms in the given shader. - * - * This static helper function binds the provided shader and sets uniforms for ambient, directional, - * point, and spot lights based on the current lightContext data. After updating the uniforms, the shader is unbound. - * - * @param shader Shared pointer to the shader used for rendering. - * @param lightContext The light context containing lighting information for the scene. - * - * @note The light context must contain valid values for: - * - ambientLight - * - directionalLights (and directionalLightCount) - * - pointLights (and pointLightCount) - * - spotLights (and spotLightCount) - */ - static void setupLights(std::shared_ptr shader, const components::LightContext& lightContext) - { - shader->bind(); - shader->setUniformFloat3("ambientLight", lightContext.ambientLight); - shader->setUniformInt("numDirLights", lightContext.directionalLightCount); - shader->setUniformInt("numPointLights", lightContext.pointLightCount); - shader->setUniformInt("numSpotLights", lightContext.spotLightCount); + RenderSystem::RenderSystem() + { + renderer::NxFramebufferSpecs maskFramebufferSpecs; + maskFramebufferSpecs.attachments = { renderer::NxFrameBufferTextureFormats::RGBA8 }; + maskFramebufferSpecs.width = 1280; // Default size, will be resized as needed + maskFramebufferSpecs.height = 720; + m_maskFramebuffer = renderer::NxFramebuffer::create(maskFramebufferSpecs); - for (unsigned int i = 0; i < lightContext.directionalLightCount; ++i) - { - auto directionalLight = lightContext.directionalLights[i]; - shader->setUniformFloat3(std::format("dirLights[{}].direction", i), directionalLight.direction); - shader->setUniformFloat4(std::format("dirLights[{}].color", i), glm::vec4(directionalLight.color, 1.0f)); - } + // Create fullscreen quad for post-processing + m_fullscreenQuad = renderer::createVertexArray(); + + // Define fullscreen quad vertices (position and texture coordinates) + float quadVertices[] = { + // positions // texture coords + -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, + + -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 1.0f, 1.0f + }; + + const auto quadVB = renderer::createVertexBuffer(sizeof(quadVertices)); + quadVB->setData(quadVertices, sizeof(quadVertices)); + const renderer::NxBufferLayout quadLayout = { + {renderer::NxShaderDataType::FLOAT3, "aPosition"}, + {renderer::NxShaderDataType::FLOAT2, "aTexCoord"} + }; + + quadVB->setLayout(quadLayout); + m_fullscreenQuad->addVertexBuffer(quadVB); + } + + /** + * @brief Sets up the lighting uniforms in the given shader. + * + * This static helper function binds the provided shader and sets uniforms for ambient, directional, + * point, and spot lights based on the current lightContext data. After updating the uniforms, the shader is unbound. + * + * @param shader Shared pointer to the shader used for rendering. + * @param lightContext The light context containing lighting information for the scene. + * + * @note The light context must contain valid values for: + * - ambientLight + * - directionalLights (and directionalLightCount) + * - pointLights (and pointLightCount) + * - spotLights (and spotLightCount) + */ + void RenderSystem::setupLights(const std::shared_ptr& shader, const components::LightContext& lightContext) + { + static std::shared_ptr lastShader = nullptr; + if (lastShader == shader) + return; + lastShader = shader; + if (!lastShader) + return; + shader->setUniformFloat3("uAmbientLight", lightContext.ambientLight); + shader->setUniformInt("uNumPointLights", static_cast(lightContext.pointLightCount)); + shader->setUniformInt("uNumSpotLights", static_cast(lightContext.spotLightCount)); + + const auto &directionalLight = lightContext.dirLight; + shader->setUniformFloat3("uDirLight.direction", directionalLight.direction); + shader->setUniformFloat4("uDirLight.color", glm::vec4(directionalLight.color, 1.0f)); + + // Well we are doing something very stupid here, but any way this render system is fucked + // In the future, we should have a material/light pre-pass that sets all uniforms of the the material + // But for now the material is embedded into the renderable which is also scuffed + const auto &pointLightComponentArray = coord->getComponentArray(); + const auto &transformComponentArray = coord->getComponentArray(); for (unsigned int i = 0; i < lightContext.pointLightCount; ++i) { - auto pointLight = lightContext.pointLights[i]; - shader->setUniformFloat3(std::format("pointLights[{}].position", i), pointLight.pos); - shader->setUniformFloat4(std::format("pointLights[{}].color", i), glm::vec4(pointLight.color, 1.0f)); - shader->setUniformFloat(std::format("pointLights[{}].constant", i), pointLight.constant); - shader->setUniformFloat(std::format("pointLights[{}].linear", i), pointLight.linear); - shader->setUniformFloat(std::format("pointLights[{}].quadratic", i), pointLight.quadratic); + const auto &pointLight = pointLightComponentArray->get(lightContext.pointLights[i]); + const auto &transform = transformComponentArray->get(lightContext.pointLights[i]); + shader->setUniformFloat3(std::format("uPointLights[{}].position", i), transform.pos); + shader->setUniformFloat4(std::format("uPointLights[{}].color", i), glm::vec4(pointLight.color, 1.0f)); + shader->setUniformFloat(std::format("uPointLights[{}].constant", i), pointLight.constant); + shader->setUniformFloat(std::format("uPointLights[{}].linear", i), pointLight.linear); + shader->setUniformFloat(std::format("uPointLights[{}].quadratic", i), pointLight.quadratic); } + const auto &spotLightComponentArray = coord->getComponentArray(); for (unsigned int i = 0; i < lightContext.spotLightCount; ++i) { - auto spotLight = lightContext.spotLights[i]; - shader->setUniformFloat3(std::format("spotLights[{}].position", i), spotLight.pos); - shader->setUniformFloat4(std::format("spotLights[{}].color", i), glm::vec4(spotLight.color, 1.0f)); - shader->setUniformFloat(std::format("spotLights[{}].constant", i), spotLight.constant); - shader->setUniformFloat(std::format("spotLights[{}].linear", i), spotLight.linear); - shader->setUniformFloat(std::format("spotLights[{}].quadratic", i), spotLight.quadratic); - shader->setUniformFloat3(std::format("spotLights[{}].direction", i), spotLight.direction); - shader->setUniformFloat(std::format("spotLights[{}].cutOff", i), spotLight.cutOff); - shader->setUniformFloat(std::format("spotLights[{}].outerCutoff", i), spotLight.outerCutoff); + const auto &spotLight = spotLightComponentArray->get(lightContext.spotLights[i]); + const auto &transform = transformComponentArray->get(lightContext.spotLights[i]); + shader->setUniformFloat3(std::format("uSpotLights[{}].position", i), transform.pos); + shader->setUniformFloat4(std::format("uSpotLights[{}].color", i), glm::vec4(spotLight.color, 1.0f)); + shader->setUniformFloat(std::format("uSpotLights[{}].constant", i), spotLight.constant); + shader->setUniformFloat(std::format("uSpotLights[{}].linear", i), spotLight.linear); + shader->setUniformFloat(std::format("uSpotLights[{}].quadratic", i), spotLight.quadratic); + shader->setUniformFloat3(std::format("uSpotLights[{}].direction", i), spotLight.direction); + shader->setUniformFloat(std::format("uSpotLights[{}].cutOff", i), spotLight.cutOff); + shader->setUniformFloat(std::format("uSpotLights[{}].outerCutoff", i), spotLight.outerCutoff); } - shader->unbind(); - } + } + + void RenderSystem::renderGrid(const components::CameraContext &camera, components::RenderContext &renderContext) + { + if (!camera.renderTarget) + return; + + renderContext.renderer3D.beginScene(camera.viewProjectionMatrix, camera.cameraPosition, "Grid shader"); + auto gridShader = renderContext.renderer3D.getShader(); + gridShader->bind(); + + // Grid appearance + const components::RenderContext::GridParams &gridParams = renderContext.gridParams; + gridShader->setUniformFloat("uGridSize", gridParams.gridSize); + gridShader->setUniformFloat("uGridCellSize", gridParams.cellSize); + gridShader->setUniformFloat("uGridMinPixelsBetweenCells", gridParams.minPixelsBetweenCells); + + gridShader->setUniformFloat4("uGridColorThin", {0.5f, 0.55f, 0.7f, 0.6f}); + gridShader->setUniformFloat4("uGridColorThick", {0.7f, 0.75f, 0.9f, 0.8f}); + + const glm::vec2 globalMousePos = event::getMousePosition(); + glm::vec3 mouseWorldPos = camera.cameraPosition; // Default position (camera position) + const glm::vec2 renderTargetSize = camera.renderTarget->getSize(); + + if (renderContext.isChildWindow) { + // viewportBounds[0] is min (top-left), viewportBounds[1] is max (bottom-right) + const glm::vec2& viewportMin = renderContext.viewportBounds[0]; + const glm::vec2& viewportMax = renderContext.viewportBounds[1]; + const glm::vec2 viewportSize(viewportMax.x - viewportMin.x, viewportMax.y - viewportMin.y); + + // Check if mouse is within the viewport bounds + if (math::isPosInBounds(globalMousePos, viewportMin, viewportMax)) { + + // Calculate relative mouse position within the viewport + glm::vec2 relativeMousePos( + globalMousePos.x - viewportMin.x, + globalMousePos.y - viewportMin.y + ); + + // Convert to normalized coordinates [0,1] + glm::vec2 normalizedPos( + relativeMousePos.x / viewportSize.x, + relativeMousePos.y / viewportSize.y + ); + + // Convert to framebuffer coordinates + glm::vec2 framebufferPos( + normalizedPos.x * renderTargetSize.x, + normalizedPos.y * renderTargetSize.y + ); + + // Project ray + const glm::vec3 rayDir = math::projectRayToWorld( + framebufferPos.x, framebufferPos.y, + camera.viewProjectionMatrix, camera.cameraPosition, + static_cast(renderTargetSize.x), static_cast(renderTargetSize.y) + ); + + // Calculate intersection with y=0 plane (grid plane) + if (rayDir.y != 0.0f) { + float t = -camera.cameraPosition.y / rayDir.y; + if (t > 0.0f) { + mouseWorldPos = camera.cameraPosition + rayDir * t; + } + } + } + } else { + const glm::vec3 rayDir = math::projectRayToWorld( + globalMousePos.x, globalMousePos.y, + camera.viewProjectionMatrix, camera.cameraPosition, + static_cast(renderTargetSize.x), static_cast(renderTargetSize.y) + ); + + if (rayDir.y != 0.0f) { + float t = -camera.cameraPosition.y / rayDir.y; + if (t > 0.0f) { + mouseWorldPos = camera.cameraPosition + rayDir * t; + } + } + } + + // Set uniforms for grid highlighting + gridShader->setUniformFloat3("uMouseWorldPos", mouseWorldPos); + gridShader->setUniformFloat("uTime", static_cast(glfwGetTime())); + + // Render the grid + renderer::NxRenderCommand::setDepthMask(false); + glDisable(GL_CULL_FACE); + renderer::NxRenderCommand::drawUnIndexed(6); + gridShader->unbind(); + renderer::NxRenderCommand::setDepthMask(true); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + } + + void RenderSystem::renderOutline( + components::RenderContext &renderContext, + const components::CameraContext &camera, + const components::RenderComponent &renderComponent, + const components::TransformComponent &transformComponent + ) const + { + if (m_maskFramebuffer->getSize().x != camera.renderTarget->getSize().x || + m_maskFramebuffer->getSize().y != camera.renderTarget->getSize().y) { + m_maskFramebuffer->resize( + static_cast(camera.renderTarget->getSize().x), + static_cast(camera.renderTarget->getSize().y) + ); + } + + // Step 2: Render selected object to mask texture + m_maskFramebuffer->bind(); + renderer::NxRenderCommand::setClearColor({0.0f, 0.0f, 0.0f, 0.0f}); + renderer::NxRenderCommand::clear(); + const auto &material = std::dynamic_pointer_cast(renderComponent.renderable)->material; + + + std::string maskShaderName = "Flat color"; + if (!material.isOpaque) + maskShaderName = "Albedo unshaded transparent"; + + renderContext.renderer3D.beginScene(camera.viewProjectionMatrix, camera.cameraPosition, maskShaderName); + auto context = std::make_shared(); + context->renderer3D = renderContext.renderer3D; + renderComponent.draw(context, transformComponent); + renderContext.renderer3D.endScene(); + + + m_maskFramebuffer->unbind(); + if (camera.renderTarget != nullptr) + camera.renderTarget->bind(); + + // Step 3: Draw full-screen quad with outline post-process shader + renderer::NxRenderCommand::setDepthMask(false); + renderContext.renderer3D.beginScene(camera.viewProjectionMatrix, camera.cameraPosition, "Outline pulse flat"); + const auto outlineShader = renderContext.renderer3D.getShader(); + outlineShader->bind(); + const unsigned int maskTexture = m_maskFramebuffer->getColorAttachmentId(0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, maskTexture); + outlineShader->setUniformInt("uMaskTexture", 0); + outlineShader->setUniformFloat("uTime", static_cast(glfwGetTime())); + outlineShader->setUniformFloat2("uScreenSize", {camera.renderTarget->getSize().x, camera.renderTarget->getSize().y}); + outlineShader->setUniformFloat("uOutlineWidth", 10.0f); - void RenderSystem::update() const + m_fullscreenQuad->bind(); + renderer::NxRenderCommand::drawUnIndexed(6); + m_fullscreenQuad->unbind(); + renderContext.renderer3D.endScene(); + + outlineShader->unbind(); + renderer::NxRenderCommand::setDepthMask(true); + } + + void RenderSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); + const SceneType sceneType = renderContext.sceneType; - setupLights(renderContext.renderer3D.getShader(), renderContext.sceneLights); + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); - while (!renderContext.cameras.empty()) - { - const auto &camera = renderContext.cameras.front(); - if (camera.renderTarget != nullptr) - { + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + + const auto transformSpan = get(); + const auto renderSpan = get(); + const std::span entitySpan = m_group->entities(); + + while (!renderContext.cameras.empty()) { + const components::CameraContext &camera = renderContext.cameras.front(); + if (camera.renderTarget != nullptr) { camera.renderTarget->bind(); //TODO: Find a way to automatically clear color attachments - renderer::RenderCommand::setClearColor(camera.clearColor); - renderer::RenderCommand::clear(); + renderer::NxRenderCommand::setClearColor(camera.clearColor); + renderer::NxRenderCommand::clear(); camera.renderTarget->clearAttachment(1, -1); + } + if (!partition) { + LOG_ONCE(NEXO_WARN, "Nothing to render in scene {}, skipping", sceneName); + camera.renderTarget->unbind(); + renderContext.cameras.pop(); + continue; } - for (const auto entity : entities) - { - auto tag = coord->getComponent(entity); - if (!tag.isRendered || sceneRendered != tag.id) - continue; - const auto transform = coord->getComponent(entity); - const auto renderComponent = coord->getComponent(entity); - if (renderComponent.isRendered) - { - //TODO: Pass to a single renderer - renderContext.renderer3D.beginScene(camera.viewProjectionMatrix, camera.cameraPosition); - auto context = std::make_shared(); + Logger::resetOnce(NEXO_LOG_ONCE_KEY("Nothing to render in scene {}, skipping", sceneName)); + + if (sceneType == SceneType::EDITOR && renderContext.gridParams.enabled) + renderGrid(camera, renderContext); + + for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { + const ecs::Entity entity = entitySpan[i]; + if (coord->entityHasComponent(entity) && sceneType != SceneType::EDITOR) + continue; + const auto &transform = transformSpan[i]; + const auto &render = renderSpan[i]; + // This needs to be changed, i guess we should go toward a static mesh/material components, way better + const auto &material = std::dynamic_pointer_cast(render.renderable)->material; + if (render.isRendered) + { + renderContext.renderer3D.beginScene(camera.viewProjectionMatrix, camera.cameraPosition, material.shader); + auto shader = renderContext.renderer3D.getShader(); + setupLights(shader, renderContext.sceneLights); + auto context = std::make_shared(); context->renderer3D = renderContext.renderer3D; - renderComponent.draw(context, transform, entity); + render.draw(context, transform, static_cast(entity)); renderContext.renderer3D.endScene(); - } - } + if (coord->entityHasComponent(entity)) + renderOutline(renderContext, camera, render, transform); + } + } + if (camera.renderTarget != nullptr) - { camera.renderTarget->unbind(); - - } renderContext.cameras.pop(); } - + // We have to do this for now to reset the shader stored as a static here, this will change later + setupLights(nullptr, renderContext.sceneLights); } } diff --git a/engine/src/systems/RenderSystem.hpp b/engine/src/systems/RenderSystem.hpp index 7fce365ba..8fef0509c 100644 --- a/engine/src/systems/RenderSystem.hpp +++ b/engine/src/systems/RenderSystem.hpp @@ -13,29 +13,54 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "ecs/System.hpp" +#include "Access.hpp" +#include "GroupSystem.hpp" +#include "components/Camera.hpp" +#include "components/Render.hpp" +#include "components/RenderContext.hpp" +#include "components/SceneComponents.hpp" +#include "components/Transform.hpp" namespace nexo::system { /** - * @brief System responsible for rendering the scene. - * - * The RenderSystem iterates over the active cameras stored in the RenderContext singleton, - * sets up lighting uniforms using the sceneLights data, and then renders entities that have - * a valid RenderComponent. The system binds each camera's render target, clears the buffers, - * and then draws each renderable entity. - * - * @note Required Singleton Component: - * - components::RenderContext - * - * @note Required Components on renderable entities: - * - components::SceneTag - * - components::CameraComponent (for camera entities stored in the RenderContext) - * - components::TransformComponent - * - components::RenderComponent - */ - class RenderSystem : public ecs::System { - public: - void update() const; + * @brief System responsible for rendering the scene. + * + * The RenderSystem iterates over the active cameras stored in the RenderContext singleton, + * sets up lighting uniforms using the sceneLights data, and then renders entities that have + * a valid RenderComponent. The system binds each camera's render target, clears the buffers, + * and then draws each renderable entity. + * + * @note Component Access Rights: + * - READ access to components::TransformComponent (owned) + * - READ access to components::RenderComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - WRITE access to components::RenderContext (singleton) + * + * @note The system uses scene partitioning to only render entities belonging to the + * currently active scene (identified by RenderContext.sceneRendered). + */ + class RenderSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read, + ecs::Read>, + ecs::NonOwned< + ecs::Read>, + ecs::WriteSingleton> { + public: + RenderSystem(); + void update(); + + private: + static void setupLights(const std::shared_ptr& shader, const components::LightContext& lightContext); + static void renderGrid(const components::CameraContext &camera, components::RenderContext &renderContext); + void renderOutline( + components::RenderContext &renderContext, + const components::CameraContext &camera, + const components::RenderComponent &renderComponent, + const components::TransformComponent &transformComponent + ) const; + std::shared_ptr m_fullscreenQuad; + std::shared_ptr m_maskFramebuffer; }; } diff --git a/engine/src/systems/ScriptingSystem.cpp b/engine/src/systems/ScriptingSystem.cpp new file mode 100644 index 000000000..43b5e30d2 --- /dev/null +++ b/engine/src/systems/ScriptingSystem.cpp @@ -0,0 +1,98 @@ +//// ScriptingSystem.cpp ////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 21/06/2025 +// Description: Source file for the scripting system +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ScriptingSystem.hpp" + +#include "scripting/native/Scripting.hpp" + +namespace nexo::system { + + ScriptingSystem::ScriptingSystem() + { + nexo::scripting::HostHandler::Parameters params; + params.errorCallback = [this](const nexo::scripting::HostString& message) { + LOG(NEXO_ERROR, "Scripting host error: {}", message.to_utf8()); + m_latestScriptingError = message.to_utf8(); + }; + + nexo::scripting::HostHandler& host = nexo::scripting::HostHandler::getInstance(); + + // Initialize the host + if (host.initialize(params) != nexo::scripting::HostHandler::SUCCESS) { + LOG(NEXO_ERROR, "Failed to initialize host"); + THROW_EXCEPTION(scripting::ScriptingBackendInitFailed, m_latestScriptingError); + } + } + + int ScriptingSystem::init() + { + + auto &scriptHost = scripting::HostHandler::getInstance(); + + if (scriptHost.runScriptExample() == EXIT_FAILURE) { + LOG(NEXO_ERROR, "Error in runScriptExample"); + return 1; + } + + LOG(NEXO_INFO, "Successfully ran runScriptExample"); + + updateWorldState(); + if (auto ret = scriptHost.getManagedApi().SystemBase.InitializeComponents(); ret != 0) { + LOG(NEXO_ERROR, "Failed to initialize scripting components, returned: {}", ret); + return ret; + } + LOG(NEXO_INFO, "Scripting components initialized successfully"); + if (auto ret = scriptHost.getManagedApi().SystemBase.InitializeSystems(&m_worldState, sizeof(m_worldState)); ret != 0) { + LOG(NEXO_ERROR, "Failed to initialize scripting systems, returned: {}", ret); + return ret; + } + LOG(NEXO_INFO, "Scripting systems initialized successfully"); + return 0; + } + + int ScriptingSystem::update() + { + const auto &scriptHost = scripting::HostHandler::getInstance(); + auto &api = scriptHost.getManagedApi(); + + updateWorldState(); + if (auto ret = api.SystemBase.UpdateSystems(&m_worldState, sizeof(m_worldState)); ret != 0) { + LOG_ONCE(NEXO_ERROR, "Failed to update scripting systems"); + return ret; + } + Logger::resetOnce(NEXO_LOG_ONCE_KEY("Failed to update scripting systems")); + return 0; + } + + int ScriptingSystem::shutdown() + { + const auto &scriptHost = scripting::HostHandler::getInstance(); + auto &api = scriptHost.getManagedApi(); + + updateWorldState(); + if (auto ret = api.SystemBase.ShutdownSystems(&m_worldState, sizeof(m_worldState)); ret != 0) { + LOG(NEXO_ERROR, "Failed to shutdown scripting systems: {}", ret); + return ret; + } + LOG(NEXO_INFO, "Scripting systems shutdown successfully"); + return 0; + } + + void ScriptingSystem::updateWorldState() + { + Application& app = Application::getInstance(); + m_worldState.update(app.getWorldState()); + } + +} // namespace nexo::system \ No newline at end of file diff --git a/engine/src/systems/ScriptingSystem.hpp b/engine/src/systems/ScriptingSystem.hpp new file mode 100644 index 000000000..4c12109d7 --- /dev/null +++ b/engine/src/systems/ScriptingSystem.hpp @@ -0,0 +1,41 @@ +//// ScriptingSystem.hpp ////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 21/06/2025 +// Description: Header file for the scripting system +// +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "scripting/native/systems/ManagedWorldState.hpp" + +namespace nexo::system { + + class ScriptingSystem { + public: + ScriptingSystem(); + ~ScriptingSystem() = default; + + int init(); + + int update(); + + int shutdown(); + + private: + void updateWorldState(); + + std::string m_latestScriptingError; + scripting::ManagedWorldState m_worldState = {}; + }; + +} \ No newline at end of file diff --git a/engine/src/systems/lights/AmbientLightSystem.cpp b/engine/src/systems/lights/AmbientLightSystem.cpp index 335e6322a..eb77fc492 100644 --- a/engine/src/systems/lights/AmbientLightSystem.cpp +++ b/engine/src/systems/lights/AmbientLightSystem.cpp @@ -13,27 +13,40 @@ /////////////////////////////////////////////////////////////////////////////// #include "AmbientLightSystem.hpp" -#include "components/RenderContext.hpp" -#include "components/SceneComponents.hpp" + +#include "Logger.hpp" +#include "Application.hpp" +#include "components/Light.hpp" #include "ecs/Coordinator.hpp" namespace nexo::system { - void AmbientLightSystem::update() const + void AmbientLightSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &ambientLights : entities) - { - auto tag = coord->getComponent(ambientLights); - if (!tag.isRendered || sceneRendered != tag.id) - continue; - const auto &ambientComponent = coord->getComponent(ambientLights); - renderContext.sceneLights.ambientLight = ambientComponent.color; - break; - } + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); + + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + if (!partition) { + LOG_ONCE(NEXO_WARN, "No ambient light found in scene {}, skipping", sceneName); + return; + } + Logger::resetOnce(NEXO_LOG_ONCE_KEY("No ambient light found in scene {}, skipping", sceneName)); + + if (partition->count != 1) + LOG_ONCE(NEXO_WARN, "For scene {}, found {} ambient lights, only one is supported, picking the first one", sceneName, partition->count); + else + Logger::resetOnce(NEXO_LOG_ONCE_KEY("For scene {}, found {} ambient lights, only one is supported, picking the first one", sceneName, partition->count)); + + renderContext.sceneLights.ambientLight = get()[0].color; } } diff --git a/engine/src/systems/lights/AmbientLightSystem.hpp b/engine/src/systems/lights/AmbientLightSystem.hpp index 335e65b8b..0a8881e4a 100644 --- a/engine/src/systems/lights/AmbientLightSystem.hpp +++ b/engine/src/systems/lights/AmbientLightSystem.hpp @@ -13,25 +13,33 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "ecs/System.hpp" +#include "GroupSystem.hpp" +#include "components/RenderContext.hpp" +#include "components/SceneComponents.hpp" namespace nexo::system { /** - * @brief System responsible for updating ambient light data in the scene. - * - * This system iterates over ambient light entities and updates the global ambient light - * in the RenderContext with the first valid ambient light component it finds. - * - * @note Required Components on ambient light entities: - * - components::SceneTag - * - components::AmbientLightComponent - * - * @note Required Singleton Component: - * - components::RenderContext - */ - class AmbientLightSystem : public ecs::System { - public: - void update() const ; + * @brief System responsible for updating ambient light data in the scene. + * + * This system identifies the first ambient light entity in the active scene and updates + * the RenderContext's global ambient light value with its ambient light component. + * + * @note Component Access Rights: + * - READ access to components::AmbientLightComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - WRITE access to components::RenderContext (singleton) + * + * @note The system uses scene partitioning to only process ambient light entities + * belonging to the currently active scene (identified by RenderContext.sceneRendered). + */ + class AmbientLightSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read>, + ecs::NonOwned< + ecs::Read>, + ecs::WriteSingleton> { + public: + void update(); }; } diff --git a/engine/src/systems/lights/DirectionalLightsSystem.cpp b/engine/src/systems/lights/DirectionalLightsSystem.cpp index 79b23517c..4c9d0d9f5 100644 --- a/engine/src/systems/lights/DirectionalLightsSystem.cpp +++ b/engine/src/systems/lights/DirectionalLightsSystem.cpp @@ -16,24 +16,40 @@ #include "components/Light.hpp" #include "components/RenderContext.hpp" #include "components/SceneComponents.hpp" +#include "core/exceptions/Exceptions.hpp" #include "ecs/Coordinator.hpp" +#include "Application.hpp" namespace nexo::system { - void DirectionalLightsSystem::update() const + void DirectionalLightsSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &directionalLights : entities) - { - auto tag = coord->getComponent(directionalLights); - if (!tag.isRendered || sceneRendered != tag.id) - continue; - const auto &directionalComponent = coord->getComponent(directionalLights); - renderContext.sceneLights.directionalLights[renderContext.sceneLights.directionalLightCount++] = directionalComponent; - } + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); + + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + if (!partition) { + LOG_ONCE(NEXO_WARN, "No directional light found in scene {}, skipping", sceneName); + return; + } + nexo::Logger::resetOnce(NEXO_LOG_ONCE_KEY("No directional light found in scene {}, skipping", sceneName)); + + if (partition->count != 1) + LOG_ONCE(NEXO_WARN, "For scene {}, found {} directional lights, only one is supported, picking the first one", sceneName, partition->count); + else + nexo::Logger::resetOnce(NEXO_LOG_ONCE_KEY("For scene {}, found {} directional lights, only one is supported, picking the first one", sceneName, partition->count)); + + const auto &lights = get(); // now 'lights' names the container + const auto &dirLight = lights[0]; + renderContext.sceneLights.dirLight = dirLight; } } diff --git a/engine/src/systems/lights/DirectionalLightsSystem.hpp b/engine/src/systems/lights/DirectionalLightsSystem.hpp index aaac9ea45..c9324a4b2 100644 --- a/engine/src/systems/lights/DirectionalLightsSystem.hpp +++ b/engine/src/systems/lights/DirectionalLightsSystem.hpp @@ -14,24 +14,36 @@ #pragma once #include "ecs/System.hpp" +#include "ecs/GroupSystem.hpp" +#include "components/Light.hpp" +#include "components/RenderContext.hpp" +#include "components/SceneComponents.hpp" namespace nexo::system { /** - * @brief System responsible for updating directional lights in the scene. - * - * This system iterates over all directional light entities and updates the RenderContext with the - * directional light components from those entities. - * - * @note Required Components on directional light entities: - * - components::SceneTag - * - components::DirectionalLightComponent - * - * @note Required Singleton Component: - * - components::RenderContext - */ - class DirectionalLightsSystem : public ecs::System { + * @brief System responsible for updating directional lights in the scene. + * + * This system iterates over all directional light entities in the active scene and updates + * the RenderContext's sceneLights collection with their directional light components. + * + * @note Component Access Rights: + * - READ access to components::DirectionalLightComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - WRITE access to components::RenderContext (singleton) + * + * @note The system uses scene partitioning to only process directional light entities + * belonging to the currently active scene (identified by RenderContext.sceneRendered). + * + * @throws TooManyDirectionalLightsException if the count of directional light entities exceeds MAX_DIRECTIONAL_LIGHTS. + */ + class DirectionalLightsSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read>, + ecs::NonOwned< + ecs::Read>, + ecs::WriteSingleton> { public: - void update() const; + void update(); }; } diff --git a/engine/src/systems/lights/PointLightsSystem.cpp b/engine/src/systems/lights/PointLightsSystem.cpp index 4e72cd292..ad1c611a3 100644 --- a/engine/src/systems/lights/PointLightsSystem.cpp +++ b/engine/src/systems/lights/PointLightsSystem.cpp @@ -13,27 +13,46 @@ /////////////////////////////////////////////////////////////////////////////// #include "PointLightsSystem.hpp" +#include "Exception.hpp" #include "components/Light.hpp" #include "components/RenderContext.hpp" #include "components/SceneComponents.hpp" +#include "core/exceptions/Exceptions.hpp" #include "ecs/Coordinator.hpp" +#include "Application.hpp" namespace nexo::system { - void PointLightsSystem::update() const + void PointLightsSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &pointLights : entities) + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); + + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + if (!partition) { + LOG_ONCE(NEXO_WARN, "No point light found in scene {}, skipping", sceneName); + return; + } + nexo::Logger::resetOnce(NEXO_LOG_ONCE_KEY("No point light found in scene {}, skipping", sceneName)); + + if (partition->count > MAX_POINT_LIGHTS) + THROW_EXCEPTION(core::TooManyPointLightsException, sceneRendered, partition->count); + + const std::span entitySpan = m_group->entities(); + + for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { - auto tag = coord->getComponent(pointLights); - if (!tag.isRendered || sceneRendered != tag.id) - continue; - const auto &pointComponent = coord->getComponent(pointLights); - renderContext.sceneLights.pointLights[renderContext.sceneLights.pointLightCount++] = pointComponent; + renderContext.sceneLights.pointLights[renderContext.sceneLights.pointLightCount] = entitySpan[i]; + renderContext.sceneLights.pointLightCount++; } } } diff --git a/engine/src/systems/lights/PointLightsSystem.hpp b/engine/src/systems/lights/PointLightsSystem.hpp index 1608c0e9c..5a9619927 100644 --- a/engine/src/systems/lights/PointLightsSystem.hpp +++ b/engine/src/systems/lights/PointLightsSystem.hpp @@ -14,24 +14,36 @@ #pragma once #include "ecs/System.hpp" +#include "components/Light.hpp" +#include "components/RenderContext.hpp" +#include "components/SceneComponents.hpp" +#include "ecs/GroupSystem.hpp" namespace nexo::system { /** - * @brief System responsible for updating point lights in the scene. - * - * This system iterates over all point light entities and updates the RenderContext with - * their point light components. - * - * @note Required Components on point light entities: - * - components::SceneTag - * - components::PointLightComponent - * - * @note Required Singleton Component: - * - components::RenderContext - */ - class PointLightsSystem : public ecs::System { + * @brief System responsible for updating point lights in the scene. + * + * This system iterates over all point light entities in the active scene and updates + * the RenderContext's sceneLights collection with their point light components. + * + * @note Component Access Rights: + * - READ access to components::PointLightComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - WRITE access to components::RenderContext (singleton) + * + * @note The system uses scene partitioning to only process point light entities + * belonging to the currently active scene (identified by RenderContext.sceneRendered). + * + * @throws TooManyPointLightsException if the count of point light entities exceeds MAX_POINT_LIGHTS. + */ + class PointLightsSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read>, + ecs::NonOwned< + ecs::Read>, + ecs::WriteSingleton> { public: - void update() const; + void update(); }; } diff --git a/engine/src/systems/lights/SpotLightsSystem.cpp b/engine/src/systems/lights/SpotLightsSystem.cpp index a9fa8eab4..6ebf5b50c 100644 --- a/engine/src/systems/lights/SpotLightsSystem.cpp +++ b/engine/src/systems/lights/SpotLightsSystem.cpp @@ -16,25 +16,42 @@ #include "components/Light.hpp" #include "components/RenderContext.hpp" #include "components/SceneComponents.hpp" +#include "core/exceptions/Exceptions.hpp" #include "ecs/Coordinator.hpp" - +#include "Application.hpp" namespace nexo::system { - void SpotLightsSystem::update() const + void SpotLightsSystem::update() { - auto &renderContext = coord->getSingletonComponent(); + auto &renderContext = getSingleton(); if (renderContext.sceneRendered == -1) return; const auto sceneRendered = static_cast(renderContext.sceneRendered); - for (const auto &spotLights : entities) + const auto scenePartition = m_group->getPartitionView( + [](const components::SceneTag& tag) { return tag.id; } + ); + + const auto *partition = scenePartition.getPartition(sceneRendered); + + auto &app = Application::getInstance(); + const std::string &sceneName = app.getSceneManager().getScene(sceneRendered).getName(); + if (!partition) { + LOG_ONCE(NEXO_WARN, "No spot light found in scene {}, skipping", sceneName); + return; + } + nexo::Logger::resetOnce(NEXO_LOG_ONCE_KEY("No spot light found in scene {}, skipping", sceneName)); + + if (partition->count > MAX_SPOT_LIGHTS) + THROW_EXCEPTION(core::TooManySpotLightsException, sceneRendered, partition->count); + + const std::span entitySpan = m_group->entities(); + + for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { - auto tag = coord->getComponent(spotLights); - if (!tag.isRendered || sceneRendered != tag.id) - continue; - const auto &spotComponent = coord->getComponent(spotLights); - renderContext.sceneLights.spotLights[renderContext.sceneLights.spotLightCount++] = spotComponent; + renderContext.sceneLights.spotLights[renderContext.sceneLights.spotLightCount] = entitySpan[i]; + renderContext.sceneLights.spotLightCount++; } } } diff --git a/engine/src/systems/lights/SpotLightsSystem.hpp b/engine/src/systems/lights/SpotLightsSystem.hpp index 947bcb68a..f292aae49 100644 --- a/engine/src/systems/lights/SpotLightsSystem.hpp +++ b/engine/src/systems/lights/SpotLightsSystem.hpp @@ -13,25 +13,36 @@ /////////////////////////////////////////////////////////////////////////////// #pragma once -#include "ecs/System.hpp" +#include "GroupSystem.hpp" +#include "components/Light.hpp" +#include "components/RenderContext.hpp" +#include "components/SceneComponents.hpp" namespace nexo::system { /** - * @brief System responsible for updating spot lights in the scene. - * - * This system iterates over all spot light entities and updates the RenderContext with their - * spot light components. - * - * @note Required Components on spot light entities: - * - components::SceneTag - * - components::SpotLightComponent - * - * @note Required Singleton Component: - * - components::RenderContext - */ - class SpotLightsSystem : public ecs::System { + * @brief System responsible for updating spot lights in the scene. + * + * This system iterates over all spot light entities in the active scene and updates + * the RenderContext's sceneLights collection with their spot light components. + * + * @note Component Access Rights: + * - READ access to components::SpotLightComponent (owned) + * - READ access to components::SceneTag (non-owned) + * - WRITE access to components::RenderContext (singleton) + * + * @note The system uses scene partitioning to only process spot light entities + * belonging to the currently active scene (identified by RenderContext.sceneRendered). + * + * @throws TooManySpotLightsException if the count of spot light entities exceeds MAX_SPOT_LIGHTS. + */ + class SpotLightsSystem final : public ecs::GroupSystem< + ecs::Owned< + ecs::Read>, + ecs::NonOwned< + ecs::Read>, + ecs::WriteSingleton> { public: - void update() const; + void update(); }; } diff --git a/examples/ecs/CMakeLists.txt b/examples/ecs/CMakeLists.txt index 3e2292ce1..49648f6a6 100644 --- a/examples/ecs/CMakeLists.txt +++ b/examples/ecs/CMakeLists.txt @@ -9,6 +9,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) set(SRCS examples/ecs/exampleBasic.cpp + common/Exception.cpp + engine/src/ecs/Components.cpp + engine/src/ecs/Entity.cpp + engine/src/ecs/Coordinator.cpp + engine/src/ecs/System.cpp ) add_executable(ecsExample ${SRCS}) @@ -20,8 +25,8 @@ set_target_properties(ecsExample RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/" ) -include_directories("./engine/src") -include_directories("./common") +include_directories("../../engine/src") +include_directories("../../common") #if(WIN32) # include_directories("${CMAKE_CURRENT_SOURCE_DIR}/vcpkg_installed/x64-windows/include") @@ -39,7 +44,6 @@ if(APPLE) endif() include_directories(include) -target_link_libraries(ecsExample PRIVATE nexoRenderer) # Set the output directory for the executable (prevents generator from creating Debug/Release folders) set_target_properties(ecsExample PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/$<0:>) diff --git a/examples/ecs/exampleBasic.cpp b/examples/ecs/exampleBasic.cpp index 8e38a37b9..f2b0bfaf5 100644 --- a/examples/ecs/exampleBasic.cpp +++ b/examples/ecs/exampleBasic.cpp @@ -1,87 +1,326 @@ -//// exampleBasic.cpp ///////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 27/11/2024 -// Description: Source file for the basic ecs example -// -/////////////////////////////////////////////////////////////////////////////// #include +#include +#include +#include +#include +#include #include "ecs/Coordinator.hpp" +#include "ecs/QuerySystem.hpp" +#include "ecs/GroupSystem.hpp" +// Component Definitions struct Position { - float x; - float y; - float z; + float x = 0.0f; + float y = 0.0f; }; struct Velocity { - float x; - float y; - float z; + float x = 0.0f; + float y = 0.0f; }; -class MovementSystem : public nexo::ecs::System { - public: - void update(float deltaTime) { - for (auto entity : entities) { - auto& position = coord->getComponent(entity); - auto& velocity = coord->getComponent(entity); - - // Update position using velocity - position.x += velocity.x * deltaTime; - position.y += velocity.y * deltaTime; - position.z += velocity.z * deltaTime; - } - } +// Define singleton components +// EVERY singleton components should have their copy constructor deleted to enforce singleton semantics +// This is statically checked when registering it +struct GameConfig { + int maxEntities = 1000; + float worldSize = 100.0f; + + // Default constructor and other constructors as needed + GameConfig() = default; + GameConfig(int entities, float size) : maxEntities(entities), worldSize(size) {} + + // Delete copy constructor to enforce singleton semantics + GameConfig(const GameConfig&) = delete; + GameConfig& operator=(const GameConfig&) = delete; + + // Move operations can be allowed if needed + GameConfig(GameConfig&&) = default; + GameConfig& operator=(GameConfig&&) = default; +}; + +struct GameState { + bool isPaused; + float gameTime; + + // Default constructor and other constructors + GameState() = default; + GameState(bool paused, float time) : isPaused(paused), gameTime(time) {} + + // Delete copy constructor to enforce singleton semantics + GameState(const GameState&) = delete; + GameState& operator=(const GameState&) = delete; + + // Move operations can be allowed if needed + GameState(GameState&&) = default; + GameState& operator=(GameState&&) = default; +}; + +void log(const std::string& message) +{ + std::cout << message << std::endl; +} + +// This query system will increment the position component by the velocity component for each entity having both +// Position is marked as a write component and Velocity as a read component +// We also retrieve GameConfig singleton component as read only and GameState as write +// This enforces constness at compile time to prevent accidental modification of Velocity +// The query system induces a small performance overhead because of the indirection required to access the components +// (because the entities we are iterating on does not necessarily have contiguous components in memory) +// This should be used when you want to create a group system that does not own any components +class QueryBenchmarkSystem : public nexo::ecs::QuerySystem< + nexo::ecs::Write, + nexo::ecs::Read, + nexo::ecs::ReadSingleton, + nexo::ecs::WriteSingleton +> { + public: + void runBenchmark() + { + log("Running query system benchmarks with " + std::to_string(entities.size()) + " entities"); + constexpr int NUM_ITERATIONS = 100; + auto queryTime = benchmarkQuery(NUM_ITERATIONS); + log("Query System: " + std::to_string(queryTime) + " milliseconds per iteration"); + } + + private: + double benchmarkQuery(int numIterations) + { + auto start = std::chrono::high_resolution_clock::now(); + + auto &gameConfig = getSingleton(); + log ("Max entities " + std::to_string(gameConfig.maxEntities)); + log("World size " + std::to_string(gameConfig.worldSize)); + // This does not compile because GameConfig is read-only + //gameConfig.worldSize += 1; + + auto &gameState = getSingleton(); + log("Game state: " + std::to_string(gameState.isPaused)); + log("Game time: " + std::to_string(gameState.gameTime)); + + for (int i = 0; i < numIterations; i++) { + // We can safely update the game state here + gameState.gameTime += 10; + for (nexo::ecs::Entity entity : entities) { + auto &position = getComponent(entity); + auto &velocity = getComponent(entity); + + + // This triggers a compiler error since Velocity is marked as read-only + //velocity.x += 1; + + position.x += velocity.x; + position.y += velocity.y; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end - start; + return duration.count() / numIterations; + } +}; + +// This a basic full-owning group system +// At startup, the system will automatically create a group of entities with Position and Velocity components +// Then we can safely iterate over the group and update the position of each entity +// Those system induces a huge overhead when you are adding/removing components or destroying entities often +// So make sure to use them wisely and avoid unnecessary operations. +// Also here we get the singleton components game config as write and game state as read +// But in most case, those are blazingly fast +// If unsure, you can try both a query system and a group system to test out what is best for your use case ! +class GroupBenchmarkSystem : public nexo::ecs::GroupSystem< + nexo::ecs::Owned< + nexo::ecs::Write, + nexo::ecs::Read + >, + nexo::ecs::NonOwned<>, + nexo::ecs::WriteSingleton, + nexo::ecs::ReadSingleton +> { + public: + void runBenchmark() + { + log("Running benchmarks with " + std::to_string(m_group->size()) + " entities"); + + constexpr int NUM_ITERATIONS = 100; + + // Benchmark each approach + auto eachTime = benchmarkEach(NUM_ITERATIONS); + log("Each method: " + std::to_string(eachTime) + " milliseconds per iteration"); + + // Benchmark spans approach + auto spansTime = benchmarkSpans(NUM_ITERATIONS); + log("Spans method: " + std::to_string(spansTime) + " milliseconds per iteration"); + + // Benchmark iterator approach + auto iteratorTime = benchmarkIterator(NUM_ITERATIONS); + log("Iterator method: " + std::to_string(iteratorTime) + " milliseconds per iteration"); + } + + private: + double benchmarkIterator(int numIterations) + { + // Method 1: Using the iterator, slowest solution of the three but more verbose + // Also not fully recommend since it does not enforce the constness for read-only components + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < numIterations; i++) { + for (const auto &[entity, position, velocity] : *m_group) { + position.x += velocity.x; + position.y += velocity.y; + + // This is highly dangerous since velocity is read-only and this is not enforced at compile time, + // we advise to use it at user's discretion + // velocity.x += 1; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end - start; + return duration.count() / numIterations; + } + + double benchmarkEach(int numIterations) + { + // Method 2: Using the each method, faster than the iterator, but does not necessarly enforce constness if not mentionned + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < numIterations; i++) { + m_group->each([](nexo::ecs::Entity entity, Position& position, const Velocity& velocity) { + position.x += velocity.x; + position.y += velocity.y; + + // This triggers a compilation error since we are using a const reference to velocity in the lambda function + // velocity.x += 1; + }); + + // But here, this would compile even though the user forgot to mention the const in the lambda function, + // which can be problematic in multihreaded systems + // m_group->each([](nexo::ecs::Entity entity, Position& position, Velocity& velocity) { + // position.x += velocity.x; + // position.y += velocity.y; + + // velocity.x += 1; + // }); + } + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end - start; + return duration.count() / numIterations; + } + + double benchmarkSpans(int numIterations) + { + // Method 3: Using the spans directly, this is the fasted method of the three + // Also, it automatically enforces constness on read-only components + // This solution should be your preferred one + auto start = std::chrono::high_resolution_clock::now(); + + auto &gameConfig = getSingleton(); + log ("Max entities " + std::to_string(gameConfig.maxEntities)); + log("World size " + std::to_string(gameConfig.worldSize)); + + auto &gameState = getSingleton(); + log("Game state: " + std::to_string(gameState.isPaused)); + log("Game time: " + std::to_string(gameState.gameTime)); + // This does not compile because GameState is read-only + // gameState.isPaused = false; + + + + for (int i = 0; i < numIterations; i++) { + auto positionSpan = get(); + // Constness is not enforced on the span itself since it is basically a non-owning view of the underlying data. + // But we consider it to be good practice to make more explicit that we are using read-only components + const auto velocitySpan = get(); + // We can safely update the game config + gameConfig.maxEntities += 1000; + + size_t size = positionSpan.size(); + for (size_t j = 0; j < size; ++j) { + auto& position = positionSpan[j]; + auto& velocity = velocitySpan[j]; + + // Of course for more clarity you should write: + // const auto& velocity = velocitySpan[j]; + // but here we are demonstrating that even if the user forgets to mark velocity as const, + // the compiler will raise an error for read-only components + + // This triggers a compilation error since velocity is read-only even though we did not explicitly mark it as const + // velocity.x += 1; + + position.x += velocity.x; + position.y += velocity.y; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end - start; + return duration.count() / numIterations; + } }; int main() { - // Initialize the ECS Coordinator + // Initialize ECS Coordinator nexo::ecs::Coordinator coordinator; coordinator.init(); + log("ECS initialized"); + // Register components coordinator.registerComponent(); coordinator.registerComponent(); - // Register and set up the MovementSystem - auto movementSystem = coordinator.registerSystem(); - nexo::ecs::Signature movementSignature; - movementSignature.set(coordinator.getComponentType(), true); - movementSignature.set(coordinator.getComponentType(), true); - coordinator.setSystemSignature(movementSignature); - - // Create entities and assign components - auto entity1 = coordinator.createEntity(); - coordinator.addComponent(entity1, Position{0.0f, 0.0f, 0.0f}); - coordinator.addComponent(entity1, Velocity{1.0f, 0.0f, 0.0f}); - - auto entity2 = coordinator.createEntity(); - coordinator.addComponent(entity2, Position{5.0f, 5.0f, 5.0f}); - coordinator.addComponent(entity2, Velocity{0.0f, -1.0f, 0.0f}); - - // Simulate a game loop - for (int frame = 0; frame < 10; ++frame) { - std::cout << "Frame " << frame << ":\n"; - - // Update the MovementSystem - movementSystem->update(0.016f); // Assuming 60 FPS, so ~16ms per frame - - // Output entity positions - for (auto entity : movementSystem->entities) { - auto& position = coordinator.getComponent(entity); - std::cout << "Entity " << entity << " Position: (" - << position.x << ", " << position.y << ", " << position.z << ")\n"; - } - - std::cout << "---------------------\n"; + coordinator.registerSingletonComponent(5000, 10); + coordinator.registerSingletonComponent(true, 10.0f); + log("Components registered"); + + // Register benchmark systems + auto queryBenchmarkSystem = coordinator.registerQuerySystem(); + auto groupBenchmarkSystem = coordinator.registerGroupSystem(); + + log("Benchmark systems registered"); + + // Create 5,000 entities for benchmarking + log("Creating 5,000 entities for benchmarking..."); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> velocityDist(-10.0, 10.0); + + std::vector entities; + entities.reserve(5000); + + for (int i = 0; i < 5000; ++i) { + nexo::ecs::Entity entity = coordinator.createEntity(); + + float velX = static_cast(velocityDist(gen)); + float velY = static_cast(velocityDist(gen)); + + coordinator.addComponent(entity, Position{0.0f, 0.0f}); + coordinator.addComponent(entity, Velocity{velX, velY}); + + entities.push_back(entity); } + log("Created " + std::to_string(entities.size()) + " entities"); + + // Run the benchmarks + log("\n=== Starting QuerySystem Benchmark ==="); + queryBenchmarkSystem->runBenchmark(); + log("=== QuerySystem Benchmark Complete ==="); + + log("\n=== Starting GroupSystem Benchmark ==="); + groupBenchmarkSystem->runBenchmark(); + log("=== GroupSystem Benchmark Complete ==="); + + // We make sure to check if the singleton component has been updated + auto &gameState = coordinator.getSingletonComponent(); + log("Game time: " + std::to_string(gameState.gameTime)); + + auto &gameConfig = coordinator.getSingletonComponent(); + log("Max entities: " + std::to_string(gameConfig.maxEntities)); + + return 0; } diff --git a/external/licenses/assimp b/external/licenses/assimp new file mode 100644 index 000000000..acaaf016e --- /dev/null +++ b/external/licenses/assimp @@ -0,0 +1,78 @@ +Open Asset Import Library (assimp) + +Copyright (c) 2006-2021, assimp team +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the assimp team, nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of the assimp team. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +****************************************************************************** + +AN EXCEPTION applies to all files in the ./test/models-nonbsd folder. +These are 3d models for testing purposes, from various free sources +on the internet. They are - unless otherwise stated - copyright of +their respective creators, which may impose additional requirements +on the use of their work. For any of these models, see +.source.txt for more legal information. Contact us if you +are a copyright holder and believe that we credited you inproperly or +if you don't want your files to appear in the repository. + + +****************************************************************************** + +Poly2Tri Copyright (c) 2009-2010, Poly2Tri Contributors +http://code.google.com/p/poly2tri/ + +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Poly2Tri nor the names of its contributors may be + used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/external/licenses/boost b/external/licenses/boost new file mode 100644 index 000000000..36b7cd93c --- /dev/null +++ b/external/licenses/boost @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/draco b/external/licenses/draco new file mode 100644 index 000000000..301095454 --- /dev/null +++ b/external/licenses/draco @@ -0,0 +1,252 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- +Files: docs/assets/js/ASCIIMathML.js + +Copyright (c) 2014 Peter Jipsen and other ASCIIMathML.js contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- +Files: docs/assets/css/pygments/* + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/external/licenses/egl-registry b/external/licenses/egl-registry new file mode 100644 index 000000000..8db79b8e2 --- /dev/null +++ b/external/licenses/egl-registry @@ -0,0 +1,28 @@ +## include/KHR/khrplatform.h + +Copyright (c) 2008-2018 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. + +## include/EGL/* +## share/opengl/egl.xml + +Copyright 2013-2020 The Khronos Group Inc. +SPDX-License-Identifier: Apache-2.0 diff --git a/external/licenses/glad b/external/licenses/glad new file mode 100644 index 000000000..ac5d8041b --- /dev/null +++ b/external/licenses/glad @@ -0,0 +1,63 @@ +The glad source code: + + The MIT License (MIT) + + Copyright (c) 2013-2021 David Herberth + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The Khronos Specifications: + + Copyright (c) 2013-2020 The Khronos Group Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +The EGL Specification and various headers: + + Copyright (c) 2007-2016 The Khronos Group Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and/or associated documentation files (the + "Materials"), to deal in the Materials without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Materials, and to + permit persons to whom the Materials are furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Materials. + + THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. diff --git a/external/licenses/glfw3 b/external/licenses/glfw3 new file mode 100644 index 000000000..7494a3f68 --- /dev/null +++ b/external/licenses/glfw3 @@ -0,0 +1,23 @@ +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Löwy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + diff --git a/external/licenses/glm b/external/licenses/glm new file mode 100644 index 000000000..779c32fb9 --- /dev/null +++ b/external/licenses/glm @@ -0,0 +1,54 @@ +================================================================================ +OpenGL Mathematics (GLM) +-------------------------------------------------------------------------------- +GLM is licensed under The Happy Bunny License or MIT License + +================================================================================ +The Happy Bunny License (Modified MIT License) +-------------------------------------------------------------------------------- +Copyright (c) 2005 - G-Truc Creation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +Restrictions: + By making use of the Software for military purposes, you choose to make a + Bunny unhappy. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +================================================================================ +The MIT License +-------------------------------------------------------------------------------- +Copyright (c) 2005 - G-Truc Creation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/external/licenses/gtest b/external/licenses/gtest new file mode 100644 index 000000000..1941a11f8 --- /dev/null +++ b/external/licenses/gtest @@ -0,0 +1,28 @@ +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/external/licenses/imgui b/external/licenses/imgui new file mode 100644 index 000000000..00ae473ab --- /dev/null +++ b/external/licenses/imgui @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2025 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/licenses/imguizmo b/external/licenses/imguizmo new file mode 100644 index 000000000..dcbee451c --- /dev/null +++ b/external/licenses/imguizmo @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Cedric Guillemet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/licenses/jhasse-poly2tri b/external/licenses/jhasse-poly2tri new file mode 100644 index 000000000..dddc3ccc3 --- /dev/null +++ b/external/licenses/jhasse-poly2tri @@ -0,0 +1,26 @@ +Copyright (c) 2009-2018, Poly2Tri Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Poly2Tri nor the names of its contributors may be + used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/external/licenses/joltphysics b/external/licenses/joltphysics new file mode 100644 index 000000000..4f0976848 --- /dev/null +++ b/external/licenses/joltphysics @@ -0,0 +1,7 @@ +Copyright 2021 Jorrit Rouwe + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/kubazip b/external/licenses/kubazip new file mode 100644 index 000000000..ed7cccdf2 --- /dev/null +++ b/external/licenses/kubazip @@ -0,0 +1,26 @@ +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + For more information, please refer to +*/ diff --git a/external/licenses/loguru b/external/licenses/loguru new file mode 100644 index 000000000..334edabf8 --- /dev/null +++ b/external/licenses/loguru @@ -0,0 +1,3 @@ +This software is in the public domain. Where that dedication is not recognized, you are granted a perpetual, irrevocable license to copy, modify and distribute it as you see fit. + +That being said, I would appreciate credit! If you find Loguru useful, tweet me at @ernerfeldt mail me at emil.ernerfeldt@gmail.com. \ No newline at end of file diff --git a/external/licenses/minizip b/external/licenses/minizip new file mode 100644 index 000000000..57d715242 --- /dev/null +++ b/external/licenses/minizip @@ -0,0 +1,74 @@ +MiniZip - Copyright (c) 1998-2010 - by Gilles Vollant - version 1.1 64 bits from Mathias Svensson + +Introduction +--------------------- +MiniZip 1.1 is built from MiniZip 1.0 by Gilles Vollant ( http://www.winimage.com/zLibDll/minizip.html ) + +When adding ZIP64 support into minizip it would result into risk of breaking compatibility with minizip 1.0. +All possible work was done for compatibility. + + +Background +--------------------- +When adding ZIP64 support Mathias Svensson found that Even Rouault have added ZIP64 +support for unzip.c into minizip for a open source project called gdal ( http://www.gdal.org/ ) + +That was used as a starting point. And after that ZIP64 support was added to zip.c +some refactoring and code cleanup was also done. + + +Changed from MiniZip 1.0 to MiniZip 1.1 +--------------------------------------- +* Added ZIP64 support for unzip ( by Even Rouault ) +* Added ZIP64 support for zip ( by Mathias Svensson ) +* Reverted some changed that Even Rouault did. +* Bunch of patches received from Gulles Vollant that he received for MiniZip from various users. +* Added unzip patch for BZIP Compression method (patch create by Daniel Borca) +* Added BZIP Compress method for zip +* Did some refactoring and code cleanup + + +Credits + + Gilles Vollant - Original MiniZip author + Even Rouault - ZIP64 unzip Support + Daniel Borca - BZip Compression method support in unzip + Mathias Svensson - ZIP64 zip support + Mathias Svensson - BZip Compression method support in zip + + Resources + + ZipLayout http://result42.com/projects/ZipFileLayout + Command line tool for Windows that shows the layout and information of the headers in a zip archive. + Used when debugging and validating the creation of zip files using MiniZip64 + + + ZIP App Note http://www.pkware.com/documents/casestudies/APPNOTE.TXT + Zip File specification + + +Notes. + * To be able to use BZip compression method in zip64.c or unzip64.c the BZIP2 lib is needed and HAVE_BZIP2 need to be defined. + +License +---------------------------------------------------------- + Condition of use and distribution are the same than zlib : + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +---------------------------------------------------------- + diff --git a/external/licenses/nlohmann-json b/external/licenses/nlohmann-json new file mode 100644 index 000000000..1c1f7a690 --- /dev/null +++ b/external/licenses/nlohmann-json @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/licenses/opengl-registry b/external/licenses/opengl-registry new file mode 100644 index 000000000..c42239352 --- /dev/null +++ b/external/licenses/opengl-registry @@ -0,0 +1,2 @@ +The files installed by the `opengl-registry` port are using different licenses. +Each file defines its license in a comment at the top of the file. diff --git a/external/licenses/polyclipping b/external/licenses/polyclipping new file mode 100644 index 000000000..3e3af47ba --- /dev/null +++ b/external/licenses/polyclipping @@ -0,0 +1,24 @@ +Boost Software License - Version 1.0 - August 17th, 2003 +http://www.boost.org/LICENSE_1_0.txt + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/external/licenses/pugixml b/external/licenses/pugixml new file mode 100644 index 000000000..91bdfc194 --- /dev/null +++ b/external/licenses/pugixml @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2006-2023 Arseny Kapoulkine + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/rapidjson b/external/licenses/rapidjson new file mode 100644 index 000000000..7ccc161c8 --- /dev/null +++ b/external/licenses/rapidjson @@ -0,0 +1,57 @@ +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +If you have downloaded a copy of the RapidJSON binary from Tencent, please note that the RapidJSON binary is licensed under the MIT License. +If you have downloaded a copy of the RapidJSON source code from Tencent, please note that RapidJSON source code is licensed under the MIT License, except for the third-party components listed below which are subject to different license terms. Your integration of RapidJSON into your own projects may require compliance with the MIT License, as well as the other licenses applicable to the third-party components included within RapidJSON. To avoid the problematic JSON license in your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as it's the only code under the JSON license. +A copy of the MIT License is included in this file. + +Other dependencies and licenses: + +Open Source Software Licensed Under the BSD License: +-------------------------------------------------------------------- + +The msinttypes r29 +Copyright (c) 2006-2013 Alexander Chemeris +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Open Source Software Licensed Under the JSON License: +-------------------------------------------------------------------- + +json.org +Copyright (c) 2002 JSON.org +All Rights Reserved. + +JSON_checker +Copyright (c) 2002 JSON.org +All Rights Reserved. + + +Terms of the JSON License: +--------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Terms of the MIT License: +-------------------------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/stb b/external/licenses/stb new file mode 100644 index 000000000..a77ae91f3 --- /dev/null +++ b/external/licenses/stb @@ -0,0 +1,37 @@ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/tinyfiledialogs b/external/licenses/tinyfiledialogs new file mode 100644 index 000000000..2b8ff4377 --- /dev/null +++ b/external/licenses/tinyfiledialogs @@ -0,0 +1,14 @@ + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/external/licenses/utfcpp b/external/licenses/utfcpp new file mode 100644 index 000000000..36b7cd93c --- /dev/null +++ b/external/licenses/utfcpp @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/vcpkg-cmake b/external/licenses/vcpkg-cmake new file mode 100644 index 000000000..4d23e0e39 --- /dev/null +++ b/external/licenses/vcpkg-cmake @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/vcpkg-cmake-config b/external/licenses/vcpkg-cmake-config new file mode 100644 index 000000000..2e4eac826 --- /dev/null +++ b/external/licenses/vcpkg-cmake-config @@ -0,0 +1,23 @@ +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/licenses/vcpkg-cmake-get-vars b/external/licenses/vcpkg-cmake-get-vars new file mode 100644 index 000000000..4d23e0e39 --- /dev/null +++ b/external/licenses/vcpkg-cmake-get-vars @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/licenses/zlib b/external/licenses/zlib new file mode 100644 index 000000000..ab8ee6f71 --- /dev/null +++ b/external/licenses/zlib @@ -0,0 +1,22 @@ +Copyright notice: + + (C) 1995-2022 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/resources/shaders/albedo_unshaded_transparent.glsl b/resources/shaders/albedo_unshaded_transparent.glsl new file mode 100644 index 000000000..f17e8148b --- /dev/null +++ b/resources/shaders/albedo_unshaded_transparent.glsl @@ -0,0 +1,47 @@ +#type vertex +#version 430 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec2 aTexCoord; +layout(location = 2) in vec3 aNormal; +layout(location = 3) in vec3 aTangent; +layout(location = 4) in vec3 aBiTangent; +layout(location = 5) in int aEntityID; + +uniform mat4 uViewProjection; +uniform mat4 uMatModel; + +out vec2 vTexCoord; +flat out int vEntityID; + +void main() +{ + vec4 worldPos = uMatModel * vec4(aPos, 1.0); + vTexCoord = aTexCoord; + vEntityID = aEntityID; + gl_Position = uViewProjection * worldPos; +} + +#type fragment +#version 430 core +layout(location = 0) out vec4 FragColor; +layout(location = 1) out int EntityID; + +in vec2 vTexCoord; +flat in int vEntityID; + +struct Material { + vec4 albedoColor; + int albedoTexIndex; // Default: 0 (white texture) +}; +uniform Material uMaterial; + +uniform sampler2D uTexture[32]; + +void main() +{ + if (texture(uTexture[uMaterial.albedoTexIndex], vTexCoord).a < 0.1) + discard; + vec3 color = uMaterial.albedoColor.rgb * vec3(texture(uTexture[uMaterial.albedoTexIndex], vTexCoord)); + FragColor = vec4(color, 1.0); + EntityID = vEntityID; +} diff --git a/resources/shaders/flat_color.glsl b/resources/shaders/flat_color.glsl new file mode 100644 index 000000000..01d65ebc5 --- /dev/null +++ b/resources/shaders/flat_color.glsl @@ -0,0 +1,21 @@ +#type vertex +#version 430 core +layout(location = 0) in vec3 aPos; + +uniform mat4 uViewProjection; +uniform mat4 uMatModel; + +void main() +{ + vec4 worldPos = uMatModel * vec4(aPos, 1.0); + gl_Position = uViewProjection * worldPos; +} + +#type fragment +#version 430 core +layout(location = 0) out vec4 FragColor; + +void main() +{ + FragColor = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/resources/shaders/grid_shader.glsl b/resources/shaders/grid_shader.glsl new file mode 100644 index 000000000..21e2a15ed --- /dev/null +++ b/resources/shaders/grid_shader.glsl @@ -0,0 +1,202 @@ +/* + + Copyright 2024 Etay Meiri + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#type vertex +#version 430 + +uniform mat4 uViewProjection = mat4(1.0); +uniform float uGridSize; +uniform vec3 uCamPos; + +out vec3 FragPos; + +const vec3 gridPos[4] = vec3[4]( + vec3(-1.0, 0.0, -1.0), // bottom left + vec3(1.0, 0.0, -1.0), // bottom right + vec3(1.0, 0.0, 1.0), // top right + vec3(-1.0, 0.0, 1.0) // top left + ); + +const int gridIndices[6] = int[6](0, 2, 1, 2, 0, 3); + +void main() +{ + int Index = gridIndices[gl_VertexID]; + vec3 position = gridPos[Index] * uGridSize; + + position.x += uCamPos.x; + position.z += uCamPos.z; + + gl_Position = uViewProjection * vec4(position, 1.0); + FragPos = position; +} + +#type fragment +/* + + Copyright 2024 Etay Meiri + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#version 420 +layout(location = 0) out vec4 FragColor; +in vec3 FragPos; + +uniform vec3 uMouseWorldPos; +uniform float uTime; +uniform vec3 uCamPos; +uniform float uGridSize = 100.0; +uniform float uGridMinPixelsBetweenCells = 2.0; +uniform float uGridCellSize = 0.025; +uniform vec4 uGridColorThin = vec4(0.5, 0.5, 0.5, 1.0); +uniform vec4 uGridColorThick = vec4(0.0, 0.0, 0.0, 1.0); + +float log10(float x) +{ + float f = log(x) / log(10.0); + return f; +} + +float satf(float x) +{ + float f = clamp(x, 0.0, 1.0); + return f; +} + +vec2 satv(vec2 x) +{ + vec2 v = clamp(x, vec2(0.0), vec2(1.0)); + return v; +} + +float max2(vec2 v) +{ + float f = max(v.x, v.y); + return f; +} + +float calculateLodAlpha(float cellSize, vec2 dudv) { + vec2 mod_div_dudv = mod(FragPos.xz, cellSize) / dudv; + return max2(vec2(1.0) - abs(satv(mod_div_dudv) * 2.0 - vec2(1.0))); +} + +void main() +{ + vec2 dvx = vec2(dFdx(FragPos.x), dFdy(FragPos.x)); + vec2 dvy = vec2(dFdx(FragPos.z), dFdy(FragPos.z)); + + float lx = length(dvx); + float ly = length(dvy); + + vec2 dudv = vec2(lx, ly); + + float l = length(dudv); + + float LOD = max(0.0, log10(l * uGridMinPixelsBetweenCells / uGridCellSize) + 1.0); + + float GridCellSizeLod0 = uGridCellSize * pow(10.0, floor(LOD)); + float GridCellSizeLod1 = GridCellSizeLod0 * 10.0; + float GridCellSizeLod2 = GridCellSizeLod1 * 10.0; + + dudv *= 4.0; + float Lod0a = calculateLodAlpha(GridCellSizeLod0, dudv); + float Lod1a = calculateLodAlpha(GridCellSizeLod1, dudv); + float Lod2a = calculateLodAlpha(GridCellSizeLod2, dudv); + + float LOD_fade = fract(LOD); + vec4 Color; + + if (Lod2a > 0.0) { + // Major grid lines with blue highlight + Color = mix(uGridColorThick, vec4(0.6, 0.7, 0.9, 0.7), 0.3); + Color.a *= Lod2a; + } else if (Lod1a > 0.0) { + // Medium grid lines with subtle color transition + Color = mix(uGridColorThick, uGridColorThin, LOD_fade); + Color.a *= Lod1a * 0.8; // Slightly more transparent + } else { + // Fine grid lines fade more with distance + Color = uGridColorThin; + Color.a *= (Lod0a * (1.0 - LOD_fade)) * 0.7; + } + + float distToMouse = length(FragPos.xz - uMouseWorldPos.xz); + + // Simple size pulse for the glow radius + float pulsePhase = uTime * 1.2; + float pulseSize = 1.0 + 0.3 * sin(pulsePhase); + float glowRadius = 8.0 * pulseSize; + + // Calculate how much the lines should glow based on distance + float glowFactor = 1.0 - smoothstep(0.0, glowRadius, distToMouse); + glowFactor = glowFactor * glowFactor; // Squared for smoother falloff + + // Colors for the glowing lines + vec3 glowColor1 = vec3(0.9, 0.3, 0.8); // Pink + vec3 glowColor2 = vec3(0.5, 0.3, 0.9); // Purple + float colorMix = 0.5 + 0.5 * sin(pulsePhase * 0.5); + vec3 finalGlowColor = mix(glowColor1, glowColor2, colorMix); + + float distance = length(FragPos.xz - uCamPos.xz); + float normalizedDist = distance / uGridSize; + + // More gradual falloff curve for distance + float edgeFadeStart = 0.65; // Start fading at 65% of grid size + float edgeFadeEnd = 0.92; // Complete fade by 92% of grid size + float distanceFactor = 1.0 - smoothstep(edgeFadeStart, edgeFadeEnd, normalizedDist); + + // Apply distance gradient to grid color (using an improved curve) + float fadeExponent = 1.5; // Controls how quickly the fade happens (higher = sharper edge) + float smoothFade = pow(distanceFactor, fadeExponent); + + // Apply the color gradient + vec3 nearColor = vec3(0.4, 0.45, 0.6); // Slightly blue tint for closer grid + vec3 farColor = vec3(0.3, 0.3, 0.5); // Subtle purple tint for distant grid + Color.rgb = mix(farColor, nearColor, smoothFade) * Color.rgb; + + // Apply the opacity falloff (more gradual, less abrupt) + Color.a *= smoothFade; + + // Apply the glow only to the grid lines - enhance their color and brightness + if (Lod2a > 0.1 || Lod1a > 0.1 || Lod0a > 0.1) { + // Scale the glow effect based on the distance fade + float distanceAdjustedGlow = glowFactor * smoothFade; + + // Only apply to actual grid lines, not the spaces between + Color.rgb = mix(Color.rgb, finalGlowColor, distanceAdjustedGlow * 0.8); + + // Brighten the lines (less intense at grid edges) + float brightnessFactor = distanceAdjustedGlow * 0.4; + Color.rgb = mix(Color.rgb, vec3(1.0), brightnessFactor * 0.4); + + // Make lines slightly more opaque when glowing (respects edge fade) + Color.a = mix(Color.a, min(1.0, Color.a * 1.2), distanceAdjustedGlow * 0.7); + } + FragColor = Color; +} diff --git a/resources/shaders/outline_pulse_flat.glsl b/resources/shaders/outline_pulse_flat.glsl new file mode 100644 index 000000000..53257729b --- /dev/null +++ b/resources/shaders/outline_pulse_flat.glsl @@ -0,0 +1,95 @@ +#type vertex +#version 430 core +layout(location = 0) in vec3 aPosition; +layout(location = 1) in vec2 aTexCoord; + +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4(aPosition, 1.0); + vTexCoord = aTexCoord; +} + +#type fragment +#version 430 core +layout(location = 0) out vec4 FragColor; + +in vec2 vTexCoord; + +uniform sampler2D uMaskTexture; +uniform vec2 uScreenSize; +uniform float uTime; +uniform float uOutlineWidth = 5.0; + +void main() +{ + float mask = texture(uMaskTexture, vTexCoord).r; + // Exit if inside the mask + if (mask > 0.5) { + FragColor = vec4(0.0, 0.0, 0.0, 0.0); + return; + } + + float minDist = uOutlineWidth * uOutlineWidth; + const int MAX_SAMPLES = 5; + const float MAX_DIST = uOutlineWidth; + + // Concentric ring sampling + for (int i = 1; i <= MAX_SAMPLES && minDist > 1.0; i++) { + float angle_step = 3.14159 * 0.25; // 45 degrees + float radius = float(i) / float(MAX_SAMPLES) * MAX_DIST; + + for (float angle = 0.0; angle < 6.28318; angle += angle_step) { + float x = cos(angle) * radius; + float y = sin(angle) * radius; + + // Convert to texture space (textures coordinates range from 0 to 1, so dividing pixel coords by screen size does that) + vec2 offset = vec2(x / uScreenSize.x, y / uScreenSize.y); + float sampleMask = texture(uMaskTexture, vTexCoord + offset).r; + + if (sampleMask > 0.5) { + float dist = dot(vec2(x, y), vec2(x, y)); + minDist = min(minDist, dist); + break; + } + } + } + + // Calculate outline alpha based on distance + float alpha = 0.0; + const float solid = 1.0; // Solid edge width + const float fuzzy = uOutlineWidth - solid; // Fuzzy part, transparent part of the outline + + if (minDist <= uOutlineWidth * uOutlineWidth) { + // if sqrt(minDist) <= solid, we get a negative value clamped to 0.0 -> 1.0 - 0.0, full solid part + // else, the alpha value gets smoothed along the fuzzy part (further = less opaque) + alpha = 1.0 - clamp((sqrt(minDist) - solid) / fuzzy, 0.0, 1.0); + } else { + // If not close enough to mask, discard + discard; + } + + vec4 purpleColor = vec4(0.5, 0.0, 1.0, 1.0); + vec4 blueColor = vec4(0.0, 0.4, 0.9, 1.0); + // 0.5Hz oscillation between -1 and 1 then remapped to 0 and 1 + float colorShift = (sin(uTime * 3.0) * 0.5 + 0.5); + vec4 baseColor = mix(purpleColor, blueColor, colorShift); + + // 0.365Hz oscillation between -1 and 1 then remapped to 0.5 and 1 + float contrastPulse = (sin(uTime * 2.3) * 0.5 + 0.5) * 0.5 + 0.5; + + // Increase the contrast by this pulse + vec4 highContrastColor = vec4( + pow(baseColor.r, contrastPulse), + pow(baseColor.g, contrastPulse), + pow(baseColor.b, contrastPulse), + 1.0 + ); + + // Increase the brightness by a 0.63Hz pulse,remap to 0.7 and 1.0 + float brightnessPulse = sin(uTime * 4.0) * 0.15 + 0.85; + + FragColor = highContrastColor * brightnessPulse; + FragColor.a = alpha; +} diff --git a/resources/shaders/outline_pulse_transparent_flat.glsl b/resources/shaders/outline_pulse_transparent_flat.glsl new file mode 100644 index 000000000..a472efba9 --- /dev/null +++ b/resources/shaders/outline_pulse_transparent_flat.glsl @@ -0,0 +1,60 @@ +#type vertex +#version 430 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec2 aTexCoord; + +uniform mat4 uViewProjection; +uniform mat4 uMatModel; + +out vec2 vTexCoord; + +void main() +{ + vec4 worldPos = uMatModel * vec4(aPos, 1.0); + vTexCoord = aTexCoord; + gl_Position = uViewProjection * worldPos; +} + +#type fragment +#version 430 core +layout(location = 0) out vec4 FragColor; + +in vec2 vTexCoord; + +struct Material { + vec4 albedoColor; + int albedoTexIndex; // Default: 0 (white texture) +}; +uniform Material uMaterial; + +uniform sampler2D uTexture[32]; +uniform float uTime; + +void main() +{ + if (texture(uTexture[uMaterial.albedoTexIndex], vTexCoord).a < 0.1) + discard; + + vec4 purpleColor = vec4(0.5, 0.0, 1.0, 1.0); + vec4 blueColor = vec4(0.0, 0.4, 0.9, 1.0); + + // Color shifting effect + float colorShift = (sin(uTime * 3.0) * 0.5 + 0.5); + vec4 baseColor = mix(purpleColor, blueColor, colorShift); + + // Contrast pulsation + float contrastPulse = (sin(uTime * 2.3) * 0.5 + 0.5) * 0.5 + 0.5; // Range from 0.5 to 1.0 + + // Apply contrast variation + vec4 highContrastColor = vec4( + pow(baseColor.r, contrastPulse), + pow(baseColor.g, contrastPulse), + pow(baseColor.b, contrastPulse), + 1.0 + ); + + // Add subtle brightness pulsation for a glowing effect + float brightnessPulse = sin(uTime * 4.0) * 0.15 + 0.85; // Range from 0.7 to 1.0 + + FragColor = highContrastColor * brightnessPulse; +} diff --git a/resources/shaders/texture.glsl b/resources/shaders/phong.glsl similarity index 60% rename from resources/shaders/texture.glsl rename to resources/shaders/phong.glsl index 46fa91506..323ff4558 100644 --- a/resources/shaders/texture.glsl +++ b/resources/shaders/phong.glsl @@ -7,8 +7,8 @@ layout(location = 3) in vec3 aTangent; layout(location = 4) in vec3 aBiTangent; layout(location = 5) in int aEntityID; -uniform mat4 viewProjection; -uniform mat4 matModel; +uniform mat4 uViewProjection; +uniform mat4 uMatModel; out vec3 vFragPos; out vec2 vTexCoord; @@ -17,16 +17,16 @@ flat out int vEntityID; void main() { - vec4 worldPos = matModel * vec4(aPos, 1.0); + vec4 worldPos = uMatModel * vec4(aPos, 1.0); vFragPos = worldPos.xyz; vTexCoord = aTexCoord; - vNormal = mat3(transpose(inverse(matModel))) * aNormal; + vNormal = mat3(transpose(inverse(uMatModel))) * aNormal; vEntityID = aEntityID; - gl_Position = viewProjection * vec4(vFragPos, 1.0); + gl_Position = uViewProjection * vec4(vFragPos, 1.0); } #type fragment @@ -34,7 +34,6 @@ void main() layout(location = 0) out vec4 FragColor; layout(location = 1) out int EntityID; -#define MAX_DIR_LIGHTS 4 #define MAX_POINT_LIGHTS 8 #define MAX_SPOT_LIGHTS 8 @@ -71,15 +70,14 @@ flat in int vEntityID; uniform sampler2D uTexture[32]; -uniform vec3 camPos; +uniform vec3 uCamPos; -uniform DirectionalLight dirLights[MAX_DIR_LIGHTS]; -uniform int numDirLights; -uniform PointLight pointLights[MAX_POINT_LIGHTS]; -uniform int numPointLights; -uniform SpotLight spotLights[MAX_SPOT_LIGHTS]; -uniform int numSpotLights; -uniform vec3 ambientLight; +uniform vec3 uAmbientLight; +uniform DirectionalLight uDirLight; +uniform PointLight uPointLights[MAX_POINT_LIGHTS]; +uniform int uNumPointLights; +uniform SpotLight uSpotLights[MAX_SPOT_LIGHTS]; +uniform int uNumSpotLights; struct Material { vec4 albedoColor; @@ -95,7 +93,7 @@ struct Material { float opacity; int opacityTexIndex; // Default: 0 (white texture) }; -uniform Material material; +uniform Material uMaterial; vec3 CalcDirLight(DirectionalLight light, vec3 normal, vec3 viewDir) { @@ -104,12 +102,11 @@ vec3 CalcDirLight(DirectionalLight light, vec3 normal, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float shininess = mix(128.0, 2.0, material.roughness); + float shininess = mix(128.0, 2.0, uMaterial.roughness); float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); // combine results - //vec3 ambient = ambientLight * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 diffuse = light.color.rgb * diff * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 specular = light.color.rgb * spec * material.specularColor.rgb * vec3(texture(uTexture[material.specularTexIndex], vTexCoord)); + vec3 diffuse = light.color.rgb * diff * uMaterial.albedoColor.rgb * vec3(texture(uTexture[uMaterial.albedoTexIndex], vTexCoord)); + vec3 specular = light.color.rgb * spec * uMaterial.specularColor.rgb * vec3(texture(uTexture[uMaterial.specularTexIndex], vTexCoord)); return (diffuse + specular); } @@ -120,16 +117,14 @@ vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float shininess = mix(128.0, 2.0, material.roughness); + float shininess = mix(128.0, 2.0, uMaterial.roughness); float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); // attenuation float distance = length(light.position - fragPos); float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); // combine results - //vec3 ambient = ambientLight * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 diffuse = light.color.rgb * diff * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 specular = light.color.rgb * spec * material.specularColor.rgb * vec3(texture(uTexture[material.specularTexIndex], vTexCoord)); - //ambient *= attenuation; + vec3 diffuse = light.color.rgb * diff * uMaterial.albedoColor.rgb * vec3(texture(uTexture[uMaterial.albedoTexIndex], vTexCoord)); + vec3 specular = light.color.rgb * spec * uMaterial.specularColor.rgb * vec3(texture(uTexture[uMaterial.specularTexIndex], vTexCoord)); diffuse *= attenuation; specular *= attenuation; return (diffuse + specular); @@ -142,7 +137,7 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); - float shininess = mix(128.0, 2.0, material.roughness); + float shininess = mix(128.0, 2.0, uMaterial.roughness); float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); // attenuation float distance = length(light.position - fragPos); @@ -152,10 +147,8 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) float epsilon = light.cutOff - light.outerCutoff; float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0); // combine results - //vec3 ambient = ambientLight * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 diffuse = light.color.rgb * diff * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); - vec3 specular = light.color.rgb * spec * material.specularColor.rgb * vec3(texture(uTexture[material.specularTexIndex], vTexCoord)); - //ambient *= attenuation * intensity; + vec3 diffuse = light.color.rgb * diff * uMaterial.albedoColor.rgb * vec3(texture(uTexture[uMaterial.albedoTexIndex], vTexCoord)); + vec3 specular = light.color.rgb * spec * uMaterial.specularColor.rgb * vec3(texture(uTexture[uMaterial.specularTexIndex], vTexCoord)); diffuse *= attenuation * intensity; specular *= attenuation * intensity; return (diffuse + specular); @@ -164,24 +157,23 @@ vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) void main() { vec3 norm = normalize(vNormal); - vec3 viewDir = normalize(camPos - vFragPos); + vec3 viewDir = normalize(uCamPos - vFragPos); vec3 result = vec3(0.0); - vec3 ambient = ambientLight * material.albedoColor.rgb * vec3(texture(uTexture[material.albedoTexIndex], vTexCoord)); + if (texture(uTexture[uMaterial.albedoTexIndex], vTexCoord).a < 0.1) + discard; + vec3 ambient = uAmbientLight * uMaterial.albedoColor.rgb * vec3(texture(uTexture[uMaterial.albedoTexIndex], vTexCoord)); result += ambient; - for (int i = 0; i < numDirLights; i++) - { - result += CalcDirLight(dirLights[i], norm, viewDir); - } + result += CalcDirLight(uDirLight, norm, viewDir); - for (int i = 0; i < numPointLights; i++) + for (int i = 0; i < uNumPointLights; i++) { - result += CalcPointLight(pointLights[i], norm, vFragPos, viewDir); + result += CalcPointLight(uPointLights[i], norm, vFragPos, viewDir); } - for (int i = 0; i < numSpotLights; i++) + for (int i = 0; i < uNumSpotLights; i++) { - result += CalcSpotLight(spotLights[i], norm, vFragPos, viewDir); + result += CalcSpotLight(uSpotLights[i], norm, vFragPos, viewDir); } FragColor = vec4(result, 1.0); diff --git a/resources/textures/cameraIcon.png b/resources/textures/cameraIcon.png new file mode 100644 index 000000000..f10d48720 Binary files /dev/null and b/resources/textures/cameraIcon.png differ diff --git a/resources/textures/pointLightIcon.png b/resources/textures/pointLightIcon.png new file mode 100644 index 000000000..4f89b18eb Binary files /dev/null and b/resources/textures/pointLightIcon.png differ diff --git a/resources/textures/spotLightIcon.png b/resources/textures/spotLightIcon.png new file mode 100644 index 000000000..c40bb4971 Binary files /dev/null and b/resources/textures/spotLightIcon.png differ diff --git a/scripts/CMakeCPackOptions.cmake.in b/scripts/CMakeCPackOptions.cmake.in index 20d23469d..e520010f9 100755 --- a/scripts/CMakeCPackOptions.cmake.in +++ b/scripts/CMakeCPackOptions.cmake.in @@ -41,7 +41,7 @@ if (CPACK_GENERATOR MATCHES "DEB") set(CPACK_PACKAGING_INSTALL_PREFIX "/usr/share/nexo-engine") message(STATUS "Setting CPACK_PACKAGING_INSTALL_PREFIX to ${CPACK_PACKAGING_INSTALL_PREFIX}") if("@NEXO_GRAPHICS_API@" STREQUAL "OpenGL") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "mesa-utils, libglfw3, libxrandr2 (>= 2:1.2.0), libxrender1") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "mesa-utils, libglfw3, libxrandr2 (>= 2:1.2.0), libxrender1, dotnet-sdk-9.0") else() message(WARNING "Unknown graphics API: @NEXO_GRAPHICS_API@, cannot set dependencies") endif() diff --git a/scripts/COPYRIGHT.in b/scripts/COPYRIGHT.in new file mode 100644 index 000000000..b8ff53cea --- /dev/null +++ b/scripts/COPYRIGHT.in @@ -0,0 +1,27 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Comment: + NEXO Engine Exhaustive list of all licenses used in the project + --------------------------------------------------------------- + This file is a template that is processed by CMake to generate the + final copyright file. See scripts/copyright.cmake for more information. + When generated some information might be missing and should be + verified and updated manually. + . + This file lists copyright holders and licenses for the NEXO project, its + source code, dependencies and libraries. + . + The format is based on the Debian copyright format 1.0, which is a + human and machine readable format for copyright information. + For more information, see: + https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Upstream-Name: @UPSTREAM_NAME@ +Upstream-Contact: @UPSTREAM_CONTACT@ +Source: @UPSTREAM_SOURCE@ + +Files: * +Comment: NEXO Engine +Copyright: 2025 NEXO Engine contributors +License: MIT + +@COPYRIGHT_LIST@ diff --git a/scripts/copyright.cmake b/scripts/copyright.cmake new file mode 100644 index 000000000..eb6ed35b3 --- /dev/null +++ b/scripts/copyright.cmake @@ -0,0 +1,61 @@ +#### copyright.cmake ########################################################## +# +# zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +# zzzzzzz zzz zzzz zzzz zzzz zzzz +# zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +# zzz zzz zzz z zzzz zzzz zzzz zzzz +# zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +# +# Author: Guillaume HEIN +# Date: 15/04/2025 +# Description: CMake to generate the copyright file for the NEXO project. +# +############################################################################### + +cmake_minimum_required(VERSION 3.28) + +message(STATUS "Generating copyright file...") + +set(UPSTREAM_NAME "NEXO Engine") +set(UPSTREAM_CONTACT "NEXO Engine Team ") +set(UPSTREAM_SOURCE "https://github.com/NexoEngine/game-engine") + +set(COPYRIGHT_LIST "") + +# find copyright line with regex +function(extract_copyright_line file outvar) + file(READ ${file} file_content) + string(REGEX MATCH "Copyright[^\n\r]*" regex_match ${file_content}) + string(REGEX REPLACE "(Copyright[\\t\\n\\r ]*(\\(c\\))?[\\t\\n\\r ]*)" "" regex_match "${regex_match}") + set(${outvar} ${regex_match} PARENT_SCOPE) +endfunction() + +file(GLOB LICENSE_PATHS "${CMAKE_INSTALL_PREFIX}/external/licenses/*") + +foreach(LICENSE_PATH ${LICENSE_PATHS}) + cmake_path(GET LICENSE_PATH FILENAME LICENSE_LIB_NAME) + message(STATUS "Processing license: ${LICENSE_PATH}") + extract_copyright_line("${LICENSE_PATH}" COPYRIGHT_LINE) + if (NOT COPYRIGHT_LINE OR COPYRIGHT_LINE STREQUAL "") + set(COPYRIGHT_LINE "${LICENSE_LIB_NAME} TODO: copyright line not found") + endif() + set(COPYRIGHT_ENTRY_OUTPUT " + +Files: * +Copyright: ${COPYRIGHT_LINE} +License: TODO: license to determine +Comment: + ${LICENSE_LIB_NAME} full license in /external/licenses/${LICENSE_LIB_NAME}") + + set(COPYRIGHT_LIST "${COPYRIGHT_LIST}${COPYRIGHT_ENTRY_OUTPUT}") +endforeach() + +string(STRIP "${COPYRIGHT_LIST}" COPYRIGHT_LIST) + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/COPYRIGHT.in" + "${CMAKE_INSTALL_PREFIX}/COPYRIGHT_generated" + @ONLY +) + +message(STATUS "Copyright file generated at ${CMAKE_INSTALL_PREFIX}/COPYRIGHT_generated") diff --git a/scripts/install.cmake b/scripts/install.cmake index fc237bac7..62daa14d1 100755 --- a/scripts/install.cmake +++ b/scripts/install.cmake @@ -34,6 +34,14 @@ install(DIRECTORY "${CMAKE_SOURCE_DIR}/resources/" DESTINATION resources) install(DIRECTORY "${CMAKE_SOURCE_DIR}/config/" DESTINATION config) install(DIRECTORY DESTINATION logs) +# Install licenses +install(DIRECTORY "${CMAKE_SOURCE_DIR}/external/licenses/" + DESTINATION "external/licenses/" +) +install(FILES "${CMAKE_SOURCE_DIR}/LICENSE" "${CMAKE_SOURCE_DIR}/COPYRIGHT" + DESTINATION "." +) + # Component documenation install(DIRECTORY "${CMAKE_SOURCE_DIR}/docs/" DESTINATION docs @@ -53,3 +61,48 @@ install(DIRECTORY "${CMAKE_SOURCE_DIR}/src" # source directory FILES_MATCHING # install only matched files PATTERN "*.hpp" # select header files ) + +# Generate license files +file(GLOB LICENSE_DIRS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/*") +set(LICENSE_LIST "" CACHE INTERNAL "") +foreach(LICENSE_DIR ${LICENSE_DIRS}) + if (EXISTS ${LICENSE_DIR}/copyright) + cmake_path(GET LICENSE_DIR FILENAME LICENSE_LIB_NAME) + if ("${LICENSE_LIB_NAME}" MATCHES "boost-*") + set(LICENSE_LIB_NAME "boost") + endif() + list(FIND LICENSE_LIST ${LICENSE_LIB_NAME} LICENSE_LIB_NAME_FOUND) + if (NOT LICENSE_LIB_NAME_FOUND EQUAL -1) + continue() + endif() + install(FILES ${LICENSE_DIR}/copyright DESTINATION "external/licenses" RENAME ${LICENSE_LIB_NAME} + COMPONENT generate-licenses EXCLUDE_FROM_ALL) + list(APPEND LICENSE_LIST ${LICENSE_LIB_NAME}) + endif() +endforeach() +list(LENGTH LICENSE_LIST LICENSE_LIST_LENGTH) + +# Generate copyright file +install(SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/copyright.cmake" + COMPONENT generate-copyright EXCLUDE_FROM_ALL +) + +# Install for scripting +install(CODE " + execute_process( + COMMAND dotnet publish + -c $ + --no-build + --output \"${CMAKE_BINARY_DIR}/publish\" + --no-self-contained + WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}/engine/src/scripting/managed\" + )" +) + +install(DIRECTORY "${CMAKE_BINARY_DIR}/publish/" # source directory + DESTINATION "bin" + FILES_MATCHING # install only matched files + PATTERN "*.dll" # select dll files + PATTERN "*.runtimeconfig.json" # select runtimeconfig.json files + PATTERN "*.deps.json" # select deps.json files +) diff --git a/scripts/pack.cmake b/scripts/pack.cmake index b4fe9faf2..3e6bf9f13 100755 --- a/scripts/pack.cmake +++ b/scripts/pack.cmake @@ -72,6 +72,7 @@ cpack_add_component(headers DISABLED GROUP optional ) + cpack_add_component_group(optional DISPLAY_NAME "Optional Components" DESCRIPTION "Optional components of the NEXO Engine." diff --git a/scripts/windows/nsis_config.cmake b/scripts/windows/nsis_config.cmake index 479664309..942c637c1 100755 --- a/scripts/windows/nsis_config.cmake +++ b/scripts/windows/nsis_config.cmake @@ -35,5 +35,6 @@ set(CPACK_PACKAGE_EXECUTABLES "nexoEditor" "NEXO Engine") set(CPACK_NSIS_MENU_LINKS "https://nexoengine.github.io/game-engine/" "NEXO Engine Website" ) -set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) -set(CPACK_NSIS_MODIFY_PATH ON) +set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) # Alert the user if the program is already installed +set(CPACK_NSIS_MODIFY_PATH ON) # Let user decide where to install +set(CPACK_NSIS_MANIFEST_DPI_AWARE ON) # Make the installer DPI aware, less blurry diff --git a/sonar-project.properties b/sonar-project.properties index d14439098..5c94fd60d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,11 +3,11 @@ sonar.organization=nexoengine sonar.projectVersion=1.0 # Path to sources (source files for coverage and analysis) -sonar.sources=engine/,editor/,common/,examples/ +sonar.sources=engine/,editor/,common/ sonar.sourceEncoding=UTF-8 # Exclude these folders from coverage calculations -sonar.coverage.exclusions=**/editor/**, **/tests/** +sonar.coverage.exclusions=**/editor/**, **/tests/**, **/examples/** # Path to tests (for test file classification, not used in coverage by default) sonar.tests=tests/ diff --git a/tests/ecs/CMakeLists.txt b/tests/ecs/CMakeLists.txt index 195dedfd8..5ccd078df 100644 --- a/tests/ecs/CMakeLists.txt +++ b/tests/ecs/CMakeLists.txt @@ -43,6 +43,13 @@ add_executable(ecs_tests ${BASEDIR}/System.test.cpp ${BASEDIR}/Coordinator.test.cpp ${BASEDIR}/Exceptions.test.cpp + ${BASEDIR}/SparseSet.test.cpp + ${BASEDIR}/SingletonComponent.test.cpp + ${BASEDIR}/Group.test.cpp + ${BASEDIR}/ComponentArray.test.cpp + ${BASEDIR}/Definitions.test.cpp + ${BASEDIR}/GroupSystem.test.cpp + ${BASEDIR}/QuerySystem.test.cpp ) # Find glm and add its include directories diff --git a/tests/ecs/ComponentArray.test.cpp b/tests/ecs/ComponentArray.test.cpp new file mode 100644 index 000000000..16ce41501 --- /dev/null +++ b/tests/ecs/ComponentArray.test.cpp @@ -0,0 +1,388 @@ +//// ComponentArray.test.cpp ////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Test file for the component array class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "ComponentArray.hpp" +#include "ECSExceptions.hpp" + +namespace nexo::ecs { + + struct TestComponent { + int value; + + bool operator==(const TestComponent& other) const { + return value == other.value; + } + }; + + class ComponentArrayTest : public ::testing::Test { + protected: + std::shared_ptr> componentArray = nullptr; + + void SetUp() override { + componentArray = std::make_shared>(); + for (Entity i = 0; i < 5; ++i) { + TestComponent newComp; + newComp.value = i * 10; + componentArray->insert(i, newComp); + } + } + }; + + // ========================================================= + // ================== BASIC OPERATIONS ===================== + // ========================================================= + + TEST_F(ComponentArrayTest, InsertAddsComponentCorrectly) { + const Entity testEntity = 10; + const TestComponent testComponent{100}; + + componentArray->insert(testEntity, testComponent); + + EXPECT_TRUE(componentArray->hasComponent(testEntity)); + EXPECT_EQ(componentArray->get(testEntity), testComponent); + EXPECT_EQ(componentArray->size(), 6); // 5 from setup + 1 new + } + + TEST_F(ComponentArrayTest, InsertDuplicateEntityIsIgnored) { + const Entity testEntity = 1; // Already exists from setup + const TestComponent newComponent{999}; + + // Original component should be preserved + TestComponent originalComponent = componentArray->get(testEntity); + + componentArray->insert(testEntity, newComponent); + + // Size should not change + EXPECT_EQ(componentArray->size(), 5); + // Component should not be updated + EXPECT_EQ(componentArray->get(testEntity), originalComponent); + } + + TEST_F(ComponentArrayTest, RemoveRemovesComponentCorrectly) { + const Entity testEntity = 2; + + EXPECT_TRUE(componentArray->hasComponent(testEntity)); + componentArray->remove(testEntity); + EXPECT_FALSE(componentArray->hasComponent(testEntity)); + EXPECT_EQ(componentArray->size(), 4); // 5 from setup - 1 removed + } + + TEST_F(ComponentArrayTest, GetReturnsCorrectComponent) { + for (Entity i = 0; i < 5; ++i) { + EXPECT_EQ(componentArray->get(i).value, i * 10); + } + } + + TEST_F(ComponentArrayTest, GetAllowsModification) { + const Entity testEntity = 3; + TestComponent& component = componentArray->get(testEntity); + component.value = 999; + + EXPECT_EQ(componentArray->get(testEntity).value, 999); + } + + TEST_F(ComponentArrayTest, EntityDestroyedRemovesComponent) { + const Entity testEntity = 4; + + EXPECT_TRUE(componentArray->hasComponent(testEntity)); + componentArray->entityDestroyed(testEntity); + EXPECT_FALSE(componentArray->hasComponent(testEntity)); + EXPECT_EQ(componentArray->size(), 4); + } + + TEST_F(ComponentArrayTest, EntityDestroyedIgnoresNonExistentEntity) { + const Entity nonExistentEntity = 100; + + EXPECT_FALSE(componentArray->hasComponent(nonExistentEntity)); + componentArray->entityDestroyed(nonExistentEntity); + EXPECT_EQ(componentArray->size(), 5); // Size unchanged + } + + TEST_F(ComponentArrayTest, GetEntityAtIndexReturnsCorrectEntity) { + for (size_t i = 0; i < 5; ++i) { + EXPECT_EQ(componentArray->getEntityAtIndex(i), i); + } + } + + TEST_F(ComponentArrayTest, GetAllComponentsReturnsCorrectSpan) { + auto span = componentArray->getAllComponents(); + EXPECT_EQ(span.size(), 5); + + for (size_t i = 0; i < 5; ++i) { + EXPECT_EQ(span[i].value, static_cast(i * 10)); + } + } + + TEST_F(ComponentArrayTest, EntitiesReturnsCorrectEntitySpan) { + auto entitiesSpan = componentArray->entities(); + EXPECT_EQ(entitiesSpan.size(), 5); + + for (size_t i = 0; i < 5; ++i) { + EXPECT_EQ(entitiesSpan[i], i); + } + } + + TEST_F(ComponentArrayTest, SizeReturnsCorrectCount) { + EXPECT_EQ(componentArray->size(), 5); + componentArray->remove(0); + EXPECT_EQ(componentArray->size(), 4); + TestComponent newComp; + newComp.value = 100; + componentArray->insert(10, newComp); + EXPECT_EQ(componentArray->size(), 5); + } + + // ========================================================= + // ================== GROUP OPERATIONS ===================== + // ========================================================= + + TEST_F(ComponentArrayTest, AddToGroupMovesEntityToGroupRegion) { + EXPECT_EQ(componentArray->groupSize(), 0); + + // Add entity 3 to group + componentArray->addToGroup(3); + EXPECT_EQ(componentArray->groupSize(), 1); + EXPECT_EQ(componentArray->getEntityAtIndex(0), 3); + + // Add entity 1 to group + componentArray->addToGroup(1); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Verify both entities are in the group region (first 2 entries) + auto entities = componentArray->entities(); + EXPECT_TRUE(entities[0] == 3 || entities[1] == 3); + EXPECT_TRUE(entities[0] == 1 || entities[1] == 1); + } + + TEST_F(ComponentArrayTest, AddToGroupIgnoresAlreadyGroupedEntity) { + componentArray->addToGroup(2); + EXPECT_EQ(componentArray->groupSize(), 1); + + // Add the same entity again + componentArray->addToGroup(2); + EXPECT_EQ(componentArray->groupSize(), 1); // Size should not change + EXPECT_EQ(componentArray->getEntityAtIndex(0), 2); + } + + TEST_F(ComponentArrayTest, RemoveFromGroupMovesEntityOutOfGroupRegion) { + // First add some entities to the group + componentArray->addToGroup(1); + componentArray->addToGroup(3); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Remove entity 1 from group + componentArray->removeFromGroup(1); + EXPECT_EQ(componentArray->groupSize(), 1); + EXPECT_EQ(componentArray->getEntityAtIndex(0), 3); + + // Remove entity 3 from group + componentArray->removeFromGroup(3); + EXPECT_EQ(componentArray->groupSize(), 0); + } + + TEST_F(ComponentArrayTest, RemoveFromGroupIgnoresNonGroupedEntity) { + componentArray->addToGroup(2); + EXPECT_EQ(componentArray->groupSize(), 1); + + // Try to remove an entity that's not in the group + componentArray->removeFromGroup(4); + EXPECT_EQ(componentArray->groupSize(), 1); // Size should not change + } + + TEST_F(ComponentArrayTest, RemoveHandlesGroupedEntityCorrectly) { + // Add some entities to group + componentArray->addToGroup(1); + componentArray->addToGroup(3); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Remove a grouped entity + componentArray->remove(1); + + // Group size should decrease + EXPECT_EQ(componentArray->groupSize(), 1); + // Total size should decrease + EXPECT_EQ(componentArray->size(), 4); + // Entity 3 should still be in the group + EXPECT_EQ(componentArray->getEntityAtIndex(0), 3); + } + + // ========================================================= + // ================== ERROR HANDLING ======================= + // ========================================================= + + TEST_F(ComponentArrayTest, InsertThrowsOnEntityBeyondMaxEntities) { + const Entity invalidEntity = MAX_ENTITIES; + TestComponent testComp; + testComp.value = 1; + EXPECT_THROW(componentArray->insert(invalidEntity, testComp), OutOfRange); + } + + TEST_F(ComponentArrayTest, RemoveThrowsOnNonExistentComponent) { + const Entity nonExistentEntity = 100; + EXPECT_THROW(componentArray->remove(nonExistentEntity), ComponentNotFound); + } + + TEST_F(ComponentArrayTest, GetThrowsOnNonExistentComponent) { + const Entity nonExistentEntity = 100; + EXPECT_THROW(static_cast(componentArray->get(nonExistentEntity)), ComponentNotFound); + } + + TEST_F(ComponentArrayTest, GetEntityAtIndexThrowsOnInvalidIndex) { + const size_t invalidIndex = 100; + EXPECT_THROW(static_cast(componentArray->getEntityAtIndex(invalidIndex)), OutOfRange); + } + + TEST_F(ComponentArrayTest, AddToGroupThrowsOnNonExistentComponent) { + const Entity nonExistentEntity = 100; + EXPECT_THROW(componentArray->addToGroup(nonExistentEntity), ComponentNotFound); + } + + TEST_F(ComponentArrayTest, RemoveFromGroupThrowsOnNonExistentComponent) { + const Entity nonExistentEntity = 100; + EXPECT_THROW(componentArray->removeFromGroup(nonExistentEntity), ComponentNotFound); + } + + // ========================================================= + // ================== CAPACITY AND MEMORY ================== + // ========================================================= + + TEST_F(ComponentArrayTest, ArrayShrinksWhenManyElementsRemoved) { + // First, add more elements to increase capacity + for (Entity i = 5; i < 20; ++i) { + TestComponent newComp; + newComp.value = i * 10; + componentArray->insert(i, newComp); + } + + // Now remove many elements to trigger shrink + for (Entity i = 0; i < 15; ++i) { + componentArray->remove(i); + } + + // Verify remaining elements are still accessible + for (Entity i = 15; i < 20; ++i) { + EXPECT_TRUE(componentArray->hasComponent(i)); + EXPECT_EQ(componentArray->get(i).value, i * 10); + } + } + + TEST_F(ComponentArrayTest, HandlesSparseEntityDistribution) { + // Insert components with large gaps between entity IDs + componentArray->insert(100, TestComponent{100}); + componentArray->insert(1000, TestComponent{1000}); + componentArray->insert(10000, TestComponent{10000}); + + // Verify components were stored correctly + EXPECT_TRUE(componentArray->hasComponent(100)); + EXPECT_TRUE(componentArray->hasComponent(1000)); + EXPECT_TRUE(componentArray->hasComponent(10000)); + + EXPECT_EQ(componentArray->get(100).value, 100); + EXPECT_EQ(componentArray->get(1000).value, 1000); + EXPECT_EQ(componentArray->get(10000).value, 10000); + + // Make sure non-existent entities in gaps return false + EXPECT_FALSE(componentArray->hasComponent(101)); + EXPECT_FALSE(componentArray->hasComponent(9999)); + } + + // ========================================================= + // ================== COMPLEX TESTS ======================== + // ========================================================= + + TEST_F(ComponentArrayTest, ComplexEntityLifecycle) { + // Initial state: 5 entities with components + EXPECT_EQ(componentArray->size(), 5); + + // Add some to group + componentArray->addToGroup(1); + componentArray->addToGroup(3); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Remove a non-grouped entity + componentArray->remove(4); + EXPECT_EQ(componentArray->size(), 4); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Remove a grouped entity + componentArray->remove(1); + EXPECT_EQ(componentArray->size(), 3); + EXPECT_EQ(componentArray->groupSize(), 1); + + // Add new entities + componentArray->insert(6, TestComponent{60}); + componentArray->insert(7, TestComponent{70}); + EXPECT_EQ(componentArray->size(), 5); + + // Destroy an entity via entityDestroyed + componentArray->entityDestroyed(0); + EXPECT_EQ(componentArray->size(), 4); + + // Add the new entities to group + componentArray->addToGroup(6); + componentArray->addToGroup(7); + EXPECT_EQ(componentArray->groupSize(), 3); + + // Verify the final state + EXPECT_TRUE(componentArray->hasComponent(2)); + EXPECT_TRUE(componentArray->hasComponent(3)); + EXPECT_TRUE(componentArray->hasComponent(6)); + EXPECT_TRUE(componentArray->hasComponent(7)); + + EXPECT_FALSE(componentArray->hasComponent(0)); + EXPECT_FALSE(componentArray->hasComponent(1)); + EXPECT_FALSE(componentArray->hasComponent(4)); + + // Check grouped entities through entity indexes + auto entitiesSpan = componentArray->entities(); + bool found3 = false, found6 = false, found7 = false; + + for (size_t i = 0; i < componentArray->groupSize(); ++i) { + Entity e = entitiesSpan[i]; + if (e == 3) found3 = true; + if (e == 6) found6 = true; + if (e == 7) found7 = true; + } + + EXPECT_TRUE(found3); + EXPECT_TRUE(found6); + EXPECT_TRUE(found7); + } + + TEST_F(ComponentArrayTest, ComplexForEachWithGroupOperations) { + // Add entities to group + componentArray->addToGroup(1); + componentArray->addToGroup(3); + EXPECT_EQ(componentArray->groupSize(), 2); + + // Modify only grouped components + int groupSum = 0; + componentArray->forEach([&](Entity e, TestComponent& comp) { + if (e == 1 || e == 3) { + comp.value *= 2; + groupSum += comp.value; + } + }); + + EXPECT_EQ(groupSum, 80); // (10*2) + (30*2) + EXPECT_EQ(componentArray->get(1).value, 20); + EXPECT_EQ(componentArray->get(3).value, 60); + + // Non-grouped components should be unchanged + EXPECT_EQ(componentArray->get(0).value, 0); + EXPECT_EQ(componentArray->get(2).value, 20); + EXPECT_EQ(componentArray->get(4).value, 40); + } +} diff --git a/tests/ecs/Components.test.cpp b/tests/ecs/Components.test.cpp index 86fe62652..c9c32a481 100644 --- a/tests/ecs/Components.test.cpp +++ b/tests/ecs/Components.test.cpp @@ -1,4 +1,4 @@ -//// Components.test.cpp ////////////////////////////////////////////////////// +//// Components.test.cpp /////////////////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -7,180 +7,606 @@ // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // // Author: Mehdy MORVAN -// Date: 26/11/2024 -// Description: Test file for the components ecs classes +// Date: 09/04/2025 +// Description: Test file for the component manager // /////////////////////////////////////////////////////////////////////////////// - #include -#include -#include "ecs/Components.hpp" +#include "Components.hpp" +#include "Definitions.hpp" +#include "ECSExceptions.hpp" +#include namespace nexo::ecs { - struct TestComponent { - int value; - }; - - struct AnotherComponent { - float data; - }; - - class ComponentManagerTest : public ::testing::Test { - protected: - void SetUp() override - { - componentManager = std::make_unique(); - } - - std::unique_ptr componentManager; - }; - - TEST(ComponentArrayTest, InsertAndRetrieveData) - { - ComponentArray array; - - Entity entity1 = 1; - Entity entity2 = 2; - - array.insertData(entity1, {42}); - array.insertData(entity2, {84}); - - EXPECT_EQ(array.getData(entity1).value, 42); - EXPECT_EQ(array.getData(entity2).value, 84); - } - - TEST(ComponentArrayTest, RemoveData) - { - ComponentArray array; - - Entity entity = 1; - - array.insertData(entity, {42}); - EXPECT_NO_THROW(array.removeData(entity)); - - EXPECT_THROW(array.getData(entity), ComponentNotFound); - } - - TEST(ComponentArrayTest, HandleEntityDestruction) - { - ComponentArray array; - - Entity entity = 1; - - array.insertData(entity, {42}); - array.entityDestroyed(entity); - - EXPECT_THROW(array.getData(entity), ComponentNotFound); - } - - TEST(ComponentArrayTest, InsertDuplicateEntity) - { - ComponentArray array; - - Entity entity = 1; - - array.insertData(entity, {42}); - EXPECT_NO_THROW(array.insertData(entity, {100})); - - EXPECT_EQ(array.getData(entity).value, 42); // Original value should remain - } - - TEST_F(ComponentManagerTest, RegisterAndRetrieveComponentType) - { - componentManager->registerComponent(); - ComponentType type = componentManager->getComponentType(); - - EXPECT_EQ(type, 0); // First registered component type - } - - TEST_F(ComponentManagerTest, AddAndRetrieveComponent) - { - componentManager->registerComponent(); - Entity entity = 1; - - componentManager->addComponent(entity, TestComponent{42}); - TestComponent &retrieved = componentManager->getComponent(entity); - - EXPECT_EQ(retrieved.value, 42); - } - - TEST_F(ComponentManagerTest, RemoveComponent) - { - componentManager->registerComponent(); - Entity entity = 1; - - componentManager->addComponent(entity, TestComponent{42}); - EXPECT_NO_THROW(componentManager->removeComponent(entity)); - EXPECT_THROW(componentManager->getComponent(entity), ComponentNotFound); - } - - TEST_F(ComponentManagerTest, TryRemoveComponent) - { - componentManager->registerComponent(); - Entity entity = 1; - - EXPECT_FALSE(componentManager->tryRemoveComponent(entity)); // No component yet - - componentManager->addComponent(entity, TestComponent{42}); - EXPECT_TRUE(componentManager->tryRemoveComponent(entity)); - EXPECT_FALSE(componentManager->tryRemoveComponent(entity)); - } - - TEST_F(ComponentManagerTest, EntityDestroyedCleansUpComponents) - { - componentManager->registerComponent(); - Entity entity1 = 1; - Entity entity2 = 2; - - componentManager->addComponent(entity1, TestComponent{42}); - componentManager->addComponent(entity2, TestComponent{84}); - - componentManager->entityDestroyed(entity1); - - EXPECT_THROW(componentManager->getComponent(entity1), ComponentNotFound); - EXPECT_EQ(componentManager->getComponent(entity2).value, 84); - } - - TEST_F(ComponentManagerTest, RetrieveUnregisteredComponentType) - { - EXPECT_THROW(componentManager->getComponentType(), ComponentNotRegistered); - } - - TEST_F(ComponentManagerTest, AddComponentWithoutRegistering) - { - Entity entity = 1; - EXPECT_THROW(componentManager->addComponent(entity, TestComponent{42}), ComponentNotRegistered); - } - - TEST_F(ComponentManagerTest, TryGetComponent) - { - componentManager->registerComponent(); - Entity entity = 1; - - auto result = componentManager->tryGetComponent(entity); - EXPECT_FALSE(result.has_value()); - - componentManager->addComponent(entity, TestComponent{42}); - result = componentManager->tryGetComponent(entity); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->get().value, 42); - } - - TEST(ComponentArrayTest, HandleOverflow) - { - ComponentArray array; - for (Entity entity = 0; entity < MAX_ENTITIES; ++entity) - { - EXPECT_NO_THROW(array.insertData(entity, {static_cast(entity)})); - } - - EXPECT_THROW(array.insertData(MAX_ENTITIES, {999}), OutOfRange); - } - - TEST_F(ComponentManagerTest, RegisterDuplicateComponent) - { - componentManager->registerComponent(); - EXPECT_NO_THROW(componentManager->registerComponent()); - } + struct TestComponentA { + int value; + + TestComponentA(int v = 0) : value(v) {} + + bool operator==(const TestComponentA& other) const { + return value == other.value; + } + }; + + struct TestComponentB { + float x, y; + + TestComponentB(float _x = 0.0f, float _y = 0.0f) : x(_x), y(_y) {} + + bool operator==(const TestComponentB& other) const { + return x == other.x && y == other.y; + } + }; + + struct TestComponentC { + std::string name; + + TestComponentC(const std::string& n = "") : name(n) {} + + bool operator==(const TestComponentC& other) const { + return name == other.name; + } + }; + + struct TestComponentD { + bool flag; + + TestComponentD(bool f = false) : flag(f) {} + + bool operator==(const TestComponentD& other) const { + return flag == other.flag; + } + }; + + class ComponentManagerTest : public ::testing::Test { + protected: + ComponentManager componentManager; + + void SetUp() override { + componentManager.registerComponent(); + componentManager.registerComponent(); + componentManager.registerComponent(); + } + }; + + class GroupKeyTest : public ::testing::Test { + protected: + GroupKey createGroupKey() { + GroupKey key; + key.ownedSignature.set(0); + key.ownedSignature.set(2); + key.nonOwnedSignature.set(1); + key.nonOwnedSignature.set(3); + return key; + } + }; + + // ========================================================= + // ================== COMPONENT REGISTRATION =============== + // ========================================================= + + TEST_F(ComponentManagerTest, RegisterComponentCreatesComponentArray) { + // Register a new component type + componentManager.registerComponent(); + + // Check that the component array was created + auto componentArray = componentManager.getComponentArray(); + EXPECT_NE(componentArray, nullptr); + EXPECT_EQ(componentArray->size(), 0); + } + + TEST_F(ComponentManagerTest, GetComponentTypeReturnsConsistentTypeID) { + // Get component types for registered components + ComponentType typeA1 = componentManager.getComponentType(); + ComponentType typeA2 = componentManager.getComponentType(); + ComponentType typeB = componentManager.getComponentType(); + + // Same component type should return the same ID + EXPECT_EQ(typeA1, typeA2); + + // Different component types should return different IDs + EXPECT_NE(typeA1, typeB); + } + + TEST_F(ComponentManagerTest, GetComponentTypeThrowsForUnregisteredComponent) { + // Try to get type ID for an unregistered component + EXPECT_THROW({ + static_cast(componentManager.getComponentType()); + }, ComponentNotRegistered); + } + + TEST_F(ComponentManagerTest, GetComponentArrayThrowsForUnregisteredComponent) { + // Try to get component array for an unregistered component + EXPECT_THROW({ + static_cast(componentManager.getComponentArray()); + }, ComponentNotRegistered); + } + + // ========================================================= + // ============= COMPONENT ADDITION/REMOVAL ================ + // ========================================================= + + TEST_F(ComponentManagerTest, AddComponentAddsComponentToEntity) { + const Entity entity = 1; + const TestComponentA component(42); + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add component to entity + componentManager.addComponent(entity, component, oldSignature, signature); + + // Check that the component was added + auto& retrievedComponent = componentManager.getComponent(entity); + EXPECT_EQ(retrievedComponent.value, component.value); + + // Check the size of the component array + auto componentArray = componentManager.getComponentArray(); + EXPECT_EQ(componentArray->size(), 1); + EXPECT_TRUE(componentArray->hasComponent(entity)); + } + + TEST_F(ComponentManagerTest, RemoveComponentRemovesComponentFromEntity) { + const Entity entity = 1; + const TestComponentA component(42); + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add component to entity + componentManager.addComponent(entity, component, oldSignature, signature); + + // Verify component exists + EXPECT_TRUE(componentManager.getComponentArray()->hasComponent(entity)); + + // Remove component + Signature newSignature = signature; + newSignature.set(componentManager.getComponentType(), false); + componentManager.removeComponent(entity, signature, newSignature); + + // Check that the component was removed + EXPECT_FALSE(componentManager.getComponentArray()->hasComponent(entity)); + EXPECT_EQ(componentManager.getComponentArray()->size(), 0); + } + + TEST_F(ComponentManagerTest, TryRemoveComponentReturnsTrueIfRemoved) { + const Entity entity = 1; + const TestComponentA component(42); + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add component to entity + componentManager.addComponent(entity, component, oldSignature, signature); + + // Try to remove component + Signature newSignature = signature; + newSignature.set(componentManager.getComponentType(), false); + bool removed = componentManager.tryRemoveComponent(entity, signature, newSignature); + + // Check that the component was removed and function returned true + EXPECT_TRUE(removed); + EXPECT_FALSE(componentManager.getComponentArray()->hasComponent(entity)); + } + + TEST_F(ComponentManagerTest, TryRemoveComponentReturnsFalseIfNotExist) { + const Entity entity = 1; + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Try to remove a component that doesn't exist + bool removed = componentManager.tryRemoveComponent(entity, oldSignature, signature); + + // Check that the function returned false + EXPECT_FALSE(removed); + } + + TEST_F(ComponentManagerTest, GetComponentReturnsCorrectComponent) { + const Entity entity = 1; + const TestComponentA component(42); + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add component to entity + componentManager.addComponent(entity, component, oldSignature, signature); + + // Get component + auto& retrievedComponent = componentManager.getComponent(entity); + + // Check that the correct component was returned + EXPECT_EQ(retrievedComponent.value, component.value); + + // Modify the component and verify it was changed in the array + retrievedComponent.value = 100; + auto& verifyComponent = componentManager.getComponent(entity); + EXPECT_EQ(verifyComponent.value, 100); + } + + TEST_F(ComponentManagerTest, TryGetComponentReturnsComponentIfExists) { + const Entity entity = 1; + const TestComponentA component(42); + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add component to entity + componentManager.addComponent(entity, component, oldSignature, signature); + + // Try to get component + auto optComponent = componentManager.tryGetComponent(entity); + + // Check that the component was returned + EXPECT_TRUE(optComponent.has_value()); + EXPECT_EQ(optComponent.value().get().value, component.value); + } + + TEST_F(ComponentManagerTest, TryGetComponentReturnsNulloptIfNotExists) { + const Entity entity = 1; + + // Try to get a component that doesn't exist + auto optComponent = componentManager.tryGetComponent(entity); + + // Check that nullopt was returned + EXPECT_FALSE(optComponent.has_value()); + } + + TEST_F(ComponentManagerTest, EntityDestroyedRemovesAllComponents) { + const Entity entity = 1; + Signature signature; + Signature oldSignature = signature; + signature.set(componentManager.getComponentType(), true); + + // Add multiple components to the entity + signature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentA(42), oldSignature, signature); + + Signature newSignature = signature; + newSignature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentB(1.0f, 2.0f), signature, newSignature); + + // Verify the components exist + EXPECT_TRUE(componentManager.getComponentArray()->hasComponent(entity)); + EXPECT_TRUE(componentManager.getComponentArray()->hasComponent(entity)); + + // Destroy the entity + componentManager.entityDestroyed(entity, signature); + + // Check that all components were removed + EXPECT_FALSE(componentManager.getComponentArray()->hasComponent(entity)); + EXPECT_FALSE(componentManager.getComponentArray()->hasComponent(entity)); + } + + // ========================================================= + // =================== GROUP KEYS ========================== + // ========================================================= + + TEST_F(GroupKeyTest, GroupKeyEquality) { + GroupKey key1 = createGroupKey(); + GroupKey key2 = createGroupKey(); + + // Same keys should be equal + EXPECT_EQ(key1, key2); + + // Different owned signature should make keys not equal + GroupKey key3 = createGroupKey(); + key3.ownedSignature.set(4); + EXPECT_NE(key1, key3); + + // Different non-owned signature should make keys not equal + GroupKey key4 = createGroupKey(); + key4.nonOwnedSignature.set(5); + EXPECT_NE(key1, key4); + } + + TEST_F(GroupKeyTest, GroupKeyHash) { + GroupKey key1 = createGroupKey(); + GroupKey key2 = createGroupKey(); + + // Same keys should have the same hash + std::hash hasher; + EXPECT_EQ(hasher(key1), hasher(key2)); + + // Different keys should have different hashes (not guaranteed, but likely) + GroupKey key3 = createGroupKey(); + key3.ownedSignature.set(4); + EXPECT_NE(hasher(key1), hasher(key3)); + } + + TEST_F(ComponentManagerTest, HasCommonOwnedComponents) { + GroupKey key1, key2, key3; + + // Set up signatures with overlapping components + key1.ownedSignature.set(0); + key1.ownedSignature.set(1); + + key2.ownedSignature.set(1); + key2.ownedSignature.set(2); + + key3.ownedSignature.set(3); + key3.ownedSignature.set(4); + + // Check for overlapping components + EXPECT_TRUE(componentManager.hasCommonOwnedComponents(key1, key2)); // Both have component 1 + EXPECT_FALSE(componentManager.hasCommonOwnedComponents(key1, key3)); // No common components + EXPECT_FALSE(componentManager.hasCommonOwnedComponents(key2, key3)); // No common components + } + + // ========================================================= + // ================ GROUP REGISTRATION ===================== + // ========================================================= + + TEST_F(ComponentManagerTest, RegisterGroupCreatesNewGroup) { + // Register a new group + auto group = componentManager.registerGroup(get()); + + // Check that the group was created + EXPECT_NE(group, nullptr); + + // Verify group properties + EXPECT_EQ(group->size(), 0); + + // Verify group signature + auto allSignature = group->allSignature(); + EXPECT_TRUE(allSignature.test(getComponentTypeID())); + EXPECT_TRUE(allSignature.test(getComponentTypeID())); + EXPECT_TRUE(allSignature.test(getComponentTypeID())); + } + + TEST_F(ComponentManagerTest, RegisterGroupReturnsSameGroupWhenCalledTwice) { + // Register group twice + auto group1 = componentManager.registerGroup(get()); + auto group2 = componentManager.registerGroup(get()); + + // Should return the same group object + EXPECT_EQ(group1, group2); + } + + TEST_F(ComponentManagerTest, RegisterGroupThrowsOnOverlappingOwnedComponents) { + // Register first group + componentManager.registerGroup(get()); + + // The EXPECT_THROW macro seems to dislike templating for some reason + bool exceptionThrown = false; + try { + componentManager.registerGroup(get()); + } catch (const OverlappingGroupsException& e) { + exceptionThrown = true; + } + EXPECT_TRUE(exceptionThrown); + } + + TEST_F(ComponentManagerTest, RegisterGroupThrowsOnUnregisteredComponents) { + bool exceptionThrown = false; + try { + componentManager.registerGroup(get()); + } catch (const ComponentNotRegistered& e) { + exceptionThrown = true; + } + EXPECT_TRUE(exceptionThrown); + } + + TEST_F(ComponentManagerTest, GetGroupReturnsRegisteredGroup) { + // Register a group + auto registeredGroup = componentManager.registerGroup(get()); + + // Get the group + auto retrievedGroup = componentManager.getGroup(get()); + + // Should return the same group object + EXPECT_EQ(registeredGroup, retrievedGroup); + } + + TEST_F(ComponentManagerTest, GetGroupThrowsOnNonexistentGroup) { + bool exceptionThrown = false; + try { + componentManager.getGroup(get()); + } catch (const GroupNotFound& e) { + exceptionThrown = true; + } + EXPECT_TRUE(exceptionThrown); + } + + TEST_F(ComponentManagerTest, GroupAddsEntitiesOnComponentsAdded) { + auto group = componentManager.registerGroup(get()); + + // Create required entity components + const Entity entity = 1; + Signature signature; + Signature oldSignature = signature; + + signature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentA(42), oldSignature, signature); + + Signature newSignature = signature; + newSignature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentB(1.0f, 2.0f), signature, newSignature); + + // Entity should be automatically added to the group + EXPECT_EQ(group->size(), 1); + + // Verify the entity is in the group + auto entitySpan = group->entities(); + EXPECT_EQ(entitySpan.size(), 1); + EXPECT_EQ(entitySpan[0], entity); + } + + TEST_F(ComponentManagerTest, GroupRemovesEntitiesOnComponentsRemoved) { + // Create entity with required components first + const Entity entity = 1; + Signature signature; + Signature oldSignature = signature; + signature.set(getComponentTypeID()); + + componentManager.addComponent(entity, TestComponentA(42), oldSignature, signature); + + Signature newSignature = signature; + newSignature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentB(1.0f, 2.0f), signature, newSignature); + signature = newSignature; + + // Register group + auto group = componentManager.registerGroup(get()); + + // Verify entity is in the group + EXPECT_EQ(group->size(), 1); + + + // Remove a required component + newSignature.set(getComponentTypeID(), false); + componentManager.removeComponent(entity, signature, newSignature); + + // Entity should be automatically removed from the group + EXPECT_EQ(group->size(), 0); + } + + TEST_F(ComponentManagerTest, GroupHandlesMultipleEntities) { + // Register group + auto group = componentManager.registerGroup(get()); + + for (Entity e = 1; e <= 5; ++e) { + Signature signature; + Signature oldSignature = signature; + signature.set(getComponentTypeID()); + componentManager.addComponent(e, TestComponentA(e * 10), oldSignature, signature); + + oldSignature = signature; + signature.set(getComponentTypeID()); + componentManager.addComponent(e, TestComponentB(e * 1.0f, e * 2.0f), oldSignature, signature); + } + + // All entities should be in the group + EXPECT_EQ(group->size(), 5); + Signature signature; + signature.set(getComponentTypeID()); + signature.set(getComponentTypeID()); + + Signature newSignature = signature; + signature.set(getComponentTypeID(), false); + + // Remove some entities + for (Entity e = 1; e <= 5; e += 2) { + componentManager.removeComponent(e, signature, newSignature); + } + + // Only remaining entities should be in the group + EXPECT_EQ(group->size(), 2); + + // Verify the correct entities remain + auto entitySpan = group->entities(); + std::set expectedEntities = {2, 4}; + std::set actualEntities(entitySpan.begin(), entitySpan.end()); + EXPECT_EQ(actualEntities, expectedEntities); + } + + TEST_F(ComponentManagerTest, EntityDestroyedRemovesFromGroups) { + // Create entity with required components + const Entity entity = 1; + Signature signature; + Signature oldSignature = signature; + + signature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentA(42), oldSignature, signature); + + oldSignature = signature; + signature.set(getComponentTypeID()); + componentManager.addComponent(entity, TestComponentB(1.0f, 2.0f), oldSignature, signature); + + // Register group + auto group = componentManager.registerGroup(get()); + + // Verify entity is in the group + EXPECT_EQ(group->size(), 1); + + // Destroy the entity + componentManager.entityDestroyed(entity, signature); + + // Entity should be removed from the group + EXPECT_EQ(group->size(), 0); + } + + // ========================================================= + // ================ INTEGRATION TEST ====================== + // ========================================================= + + TEST_F(ComponentManagerTest, ComplexIntegrationTest) { + // Register additional component + componentManager.registerComponent(); + + // Register multiple groups + auto groupAB = componentManager.registerGroup(get()); + auto groupCD = componentManager.registerGroup(get()); + + // Create entities with various component combinations + Signature signature1, signature2, signature3; + Signature oldSignature1, oldSignature2, oldSignature3; + + // Entity 1: A + B + C + oldSignature1 = signature1; + signature1.set(getComponentTypeID()); + componentManager.addComponent(1, TestComponentA(10), oldSignature1, signature1); + + oldSignature1 = signature1; + signature1.set(getComponentTypeID()); + componentManager.addComponent(1, TestComponentB(1.0f, 2.0f), oldSignature1, signature1); + + oldSignature1 = signature1; + signature1.set(getComponentTypeID()); + componentManager.addComponent(1, TestComponentC("Entity1"), oldSignature1, signature1); + + // Entity 2: A + B + D + oldSignature2 = signature2; + signature2.set(getComponentTypeID()); + componentManager.addComponent(2, TestComponentA(20), oldSignature2, signature2); + + oldSignature2 = signature2; + signature2.set(getComponentTypeID()); + componentManager.addComponent(2, TestComponentB(3.0f, 4.0f), oldSignature2, signature2); + + oldSignature2 = signature2; + signature2.set(getComponentTypeID()); + componentManager.addComponent(2, TestComponentD(true), oldSignature2, signature2); + + // Entity 3: C + D + oldSignature3 = signature3; + signature3.set(getComponentTypeID()); + componentManager.addComponent(3, TestComponentC("Entity3"), oldSignature3, signature3); + + oldSignature3 = signature3; + signature3.set(getComponentTypeID()); + componentManager.addComponent(3, TestComponentD(false), oldSignature3, signature3); + + // Verify group membership + EXPECT_EQ(groupAB->size(), 2); // Entities 1 and 2 + EXPECT_EQ(groupCD->size(), 1); // Entity 3 + + // Remove components to change group membership + Signature newSignature1 = signature1; + newSignature1.set(getComponentTypeID(), false); + componentManager.removeComponent(1, signature1, newSignature1); + signature1.reset(getComponentTypeID()); + + oldSignature1 = signature1; + signature1.set(getComponentTypeID()); + componentManager.addComponent(1, TestComponentD(true), oldSignature1, signature1); + + // Check updated group membership + EXPECT_EQ(groupAB->size(), 1); // Only Entity 2 now + EXPECT_EQ(groupCD->size(), 2); // Entities 1 and 3 now + + // Destroy an entity + componentManager.entityDestroyed(2, signature2); + + // Verify final state + EXPECT_EQ(groupAB->size(), 0); + EXPECT_EQ(groupCD->size(), 2); + + // Check component arrays + EXPECT_EQ(componentManager.getComponentArray()->size(), 1); + EXPECT_EQ(componentManager.getComponentArray()->size(), 0); + EXPECT_EQ(componentManager.getComponentArray()->size(), 2); + EXPECT_EQ(componentManager.getComponentArray()->size(), 2); + } } diff --git a/tests/ecs/Coordinator.test.cpp b/tests/ecs/Coordinator.test.cpp index 8d5bdafe7..ed8a0905f 100644 --- a/tests/ecs/Coordinator.test.cpp +++ b/tests/ecs/Coordinator.test.cpp @@ -15,8 +15,9 @@ #include #include #include "ecs/Coordinator.hpp" +#include "Components.hpp" #include "ecs/SingletonComponent.hpp" -#include "ecs/Signature.hpp" +#include "ecs/Definitions.hpp" #include "ecs/System.hpp" #include "ecs/Entity.hpp" @@ -36,12 +37,12 @@ namespace nexo::ecs { struct TestSingletonComponent { int value; - }; - // Mock System for testing - class MockSystem : public System { - public: - MOCK_METHOD(void, update, (), (const)); + TestSingletonComponent() = default; + TestSingletonComponent(int v) : value(v) {} + + TestSingletonComponent(const TestSingletonComponent&) = delete; + TestSingletonComponent& operator=(const TestSingletonComponent&) = delete; }; class CoordinatorTest : public ::testing::Test { @@ -68,7 +69,7 @@ namespace nexo::ecs { TEST_F(CoordinatorTest, DestroyNonexistentEntity) { Entity nonexistentEntity = 99999; - EXPECT_THROW(coordinator->destroyEntity(nonexistentEntity), OutOfRange); + EXPECT_NO_THROW(coordinator->destroyEntity(nonexistentEntity)); } TEST_F(CoordinatorTest, RegisterAndAddComponent) { @@ -99,73 +100,14 @@ namespace nexo::ecs { EXPECT_NO_THROW(coordinator->tryRemoveComponent(entity)); } - TEST_F(CoordinatorTest, AddComponentToNonexistentEntity) { - coordinator->registerComponent(); - - Entity nonexistentEntity = 99999; - TestComponent component{42}; - - EXPECT_THROW(coordinator->addComponent(nonexistentEntity, component), OutOfRange); - } - TEST_F(CoordinatorTest, RemoveComponentFromNonexistentEntity) { coordinator->registerComponent(); - Entity nonexistentEntity = 99999; + Entity nonexistentEntity = 100; EXPECT_THROW(coordinator->removeComponent(nonexistentEntity), ComponentNotFound); } - TEST_F(CoordinatorTest, RegisterSystemAndSetSignature) { - auto system = coordinator->registerSystem(); - - EXPECT_NE(system, nullptr); - - Signature signature; - signature.set(1); - - EXPECT_NO_THROW(coordinator->setSystemSignature(signature)); - } - - TEST_F(CoordinatorTest, UpdateSystemEntities) { - auto system = coordinator->registerSystem(); - coordinator->registerComponent(); - - Signature signature; - signature.set(coordinator->getComponentType()); - - coordinator->setSystemSignature(signature); - - Entity entity = coordinator->createEntity(); - TestComponent component{42}; - - coordinator->registerComponent(); - coordinator->addComponent(entity, component); - - EXPECT_TRUE(system->entities.contains(entity)); - } - - TEST_F(CoordinatorTest, SystemDoesNotIncludeMismatchedEntity) { - auto system = coordinator->registerSystem(); - - Signature signature; - signature.set(1); - - coordinator->setSystemSignature(signature); - - Entity entity = coordinator->createEntity(); - TestComponent component{42}; - - coordinator->registerComponent(); - // Entity signature does not match the system - Signature entitySignature; - entitySignature.set(2); - coordinator->setSystemSignature(entitySignature); - - coordinator->addComponent(entity, component); - EXPECT_FALSE(system->entities.contains(entity)); - } - TEST_F(CoordinatorTest, GetAllComponents) { coordinator->registerComponent(); @@ -186,7 +128,7 @@ namespace nexo::ecs { ComponentA compA{10}; coordinator->addComponent(e1, compA); - std::set result = coordinator->getAllEntitiesWith(); + std::vector result = coordinator->getAllEntitiesWith(); EXPECT_TRUE(result.empty()); } @@ -198,9 +140,9 @@ namespace nexo::ecs { coordinator->addComponent(e2, ComponentA{20}); coordinator->addComponent(e2, ComponentB{3.14f}); - std::set result = coordinator->getAllEntitiesWith(); + std::vector result = coordinator->getAllEntitiesWith(); EXPECT_EQ(result.size(), 1); - EXPECT_TRUE(result.find(e2) != result.end()); + EXPECT_TRUE(std::find(result.begin(), result.end(), e2) != result.end()); } TEST_F(CoordinatorTest, GetAllEntitiesWith_MultipleMatches) { @@ -216,28 +158,25 @@ namespace nexo::ecs { coordinator->addComponent(e3, ComponentA{3}); coordinator->addComponent(e3, ComponentB{3.0f}); - std::set result = coordinator->getAllEntitiesWith(); + std::vector result = coordinator->getAllEntitiesWith(); EXPECT_EQ(result.size(), 3); - EXPECT_TRUE(result.find(e1) != result.end()); - EXPECT_TRUE(result.find(e2) != result.end()); - EXPECT_TRUE(result.find(e3) != result.end()); + EXPECT_TRUE(std::find(result.begin(), result.end(), e1) != result.end()); + EXPECT_TRUE(std::find(result.begin(), result.end(), e2) != result.end()); + EXPECT_TRUE(std::find(result.begin(), result.end(), e3) != result.end()); } TEST_F(CoordinatorTest, DestroyedEntityNotReturned) { - // Create an entity with both components. Entity e1 = coordinator->createEntity(); coordinator->addComponent(e1, ComponentA{10}); coordinator->addComponent(e1, ComponentB{2.5f}); - // Verify it is returned. - std::set result = coordinator->getAllEntitiesWith(); - EXPECT_TRUE(result.find(e1) != result.end()); + std::vector result = coordinator->getAllEntitiesWith(); + EXPECT_TRUE(std::find(result.begin(), result.end(), e1) != result.end()); - // Destroy the entity. coordinator->destroyEntity(e1); result = coordinator->getAllEntitiesWith(); - EXPECT_TRUE(result.find(e1) == result.end()); + EXPECT_TRUE(std::find(result.begin(), result.end(), e1) == result.end()); } TEST_F(CoordinatorTest, TryGetComponentWorks) { @@ -267,33 +206,249 @@ namespace nexo::ecs { // Register the singleton. coordinator->registerSingletonComponent(77); - // Check that it can be retrieved. - { - TestSingletonComponent &retrieved = coordinator->getSingletonComponent(); - EXPECT_EQ(retrieved.value, 77); - } - // Remove the singleton. EXPECT_NO_THROW(coordinator->removeSingletonComponent()); // After removal, trying to get the singleton should throw. EXPECT_THROW({ coordinator->getSingletonComponent(); - }, std::exception); + }, SingletonComponentNotRegistered); } TEST_F(CoordinatorTest, SingletonComponent_ReRegister) { coordinator->registerSingletonComponent(100); - { - TestSingletonComponent &retrieved = coordinator->getSingletonComponent(); - EXPECT_EQ(retrieved.value, 100); - } // Remove and register a new value. coordinator->removeSingletonComponent(); coordinator->registerSingletonComponent(200); - { - TestSingletonComponent &retrieved = coordinator->getSingletonComponent(); - EXPECT_EQ(retrieved.value, 200); + + TestSingletonComponent &retrieved = coordinator->getSingletonComponent(); + EXPECT_EQ(retrieved.value, 200); + } + + TEST_F(CoordinatorTest, GetComponentArray) { + // Register a component type + coordinator->registerComponent(); + + // Get the component array + auto componentArray = coordinator->getComponentArray(); + ASSERT_NE(componentArray, nullptr); + + // Test basic functionality of the component array + Entity entity = coordinator->createEntity(); + componentArray->insert(entity, TestComponent{42}); + EXPECT_EQ(componentArray->get(entity).data, 42); + EXPECT_EQ(coordinator->getComponent(entity).data, 42); + } + + TEST_F(CoordinatorTest, GetAllComponentTypes) { + // Register multiple component types + coordinator->registerComponent(); + + Entity entity = coordinator->createEntity(); + + // Initially, the entity has no components + auto types = coordinator->getAllComponentTypes(entity); + EXPECT_TRUE(types.empty()); + + // Add components and check types + coordinator->addComponent(entity, TestComponent{42}); + coordinator->addComponent(entity, ComponentA{10}); + coordinator->addComponent(entity, ComponentB{3.14f}); + + types = coordinator->getAllComponentTypes(entity); + EXPECT_EQ(types.size(), 3); + + // Verify correct types are returned (specific order may vary) + bool hasTestComponent = false; + bool hasComponentA = false; + bool hasComponentB = false; + + for (const auto& typeIndex : types) { + if (typeIndex == std::type_index(typeid(TestComponent))) hasTestComponent = true; + else if (typeIndex == std::type_index(typeid(ComponentA))) hasComponentA = true; + else if (typeIndex == std::type_index(typeid(ComponentB))) hasComponentB = true; + } + + EXPECT_TRUE(hasTestComponent); + EXPECT_TRUE(hasComponentA); + EXPECT_TRUE(hasComponentB); + + // Remove a component and verify types are updated + coordinator->removeComponent(entity); + types = coordinator->getAllComponentTypes(entity); + EXPECT_EQ(types.size(), 2); + + hasTestComponent = false; + hasComponentA = false; + hasComponentB = false; + + for (const auto& typeIndex : types) { + if (typeIndex == std::type_index(typeid(TestComponent))) hasTestComponent = true; + else if (typeIndex == std::type_index(typeid(ComponentA))) hasComponentA = true; + else if (typeIndex == std::type_index(typeid(ComponentB))) hasComponentB = true; } + + EXPECT_FALSE(hasTestComponent); + EXPECT_TRUE(hasComponentA); + EXPECT_TRUE(hasComponentB); + } + + TEST_F(CoordinatorTest, EntityHasComponent) { + coordinator->registerComponent(); + coordinator->registerComponent(); + + Entity entity = coordinator->createEntity(); + + // Initially, the entity has no components + EXPECT_FALSE(coordinator->entityHasComponent(entity)); + EXPECT_FALSE(coordinator->entityHasComponent(entity)); + + // Add a component + coordinator->addComponent(entity, TestComponent{42}); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_FALSE(coordinator->entityHasComponent(entity)); + + // Add another component + coordinator->addComponent(entity, ComponentA{10}); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + + // Remove a component + coordinator->removeComponent(entity); + EXPECT_FALSE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + } + + TEST_F(CoordinatorTest, ModifyComponent) { + coordinator->registerComponent(); + + Entity entity = coordinator->createEntity(); + coordinator->addComponent(entity, TestComponent{42}); + + // Modify component through getComponent + coordinator->getComponent(entity).data = 100; + EXPECT_EQ(coordinator->getComponent(entity).data, 100); + + // Modify component through a reference + TestComponent& component = coordinator->getComponent(entity); + component.data = 200; + EXPECT_EQ(coordinator->getComponent(entity).data, 200); + } + + TEST_F(CoordinatorTest, ComponentArrayIntegration) { + coordinator->registerComponent(); + + // Get component array directly + auto componentArray = coordinator->getComponentArray(); + + // Create entity through coordinator + Entity entity = coordinator->createEntity(); + + // Add component directly through component array + componentArray->insert(entity, TestComponent{42}); + + // Verify we can access it through the coordinator + EXPECT_EQ(coordinator->getComponent(entity).data, 42); + + // Modify through component array + componentArray->get(entity).data = 100; + + // Verify change is visible through coordinator + EXPECT_EQ(coordinator->getComponent(entity).data, 100); + + // Remove through component array + componentArray->remove(entity); + + // Verify it's gone + EXPECT_THROW(coordinator->getComponent(entity), ComponentNotFound); + } + + TEST_F(CoordinatorTest, SingletonComponentEdgeCases) { + // Try getting a non-existent singleton + EXPECT_THROW(coordinator->getSingletonComponent(), SingletonComponentNotRegistered); + + // Register, remove, and try to get + coordinator->registerSingletonComponent(42); + coordinator->removeSingletonComponent(); + EXPECT_THROW(coordinator->getSingletonComponent(), SingletonComponentNotRegistered); + + // Remove a non-existent singleton + EXPECT_THROW(coordinator->removeSingletonComponent(), SingletonComponentNotRegistered); + + // Register multiple singleton types + struct AnotherSingletonComponent { + float value; + + AnotherSingletonComponent() = default; + AnotherSingletonComponent(float v) : value(v) {} + + AnotherSingletonComponent(const AnotherSingletonComponent&) = delete; + AnotherSingletonComponent& operator=(const AnotherSingletonComponent&) = delete; + }; + + coordinator->registerSingletonComponent(42); + coordinator->registerSingletonComponent(3.14f); + + EXPECT_EQ(coordinator->getSingletonComponent().value, 42); + EXPECT_FLOAT_EQ(coordinator->getSingletonComponent().value, 3.14f); + } + + TEST_F(CoordinatorTest, ComplexEntityComponentInteractions) { + // Register multiple components + struct Position { float x, y; }; + struct Velocity { float dx, dy; }; + struct Renderable { int spriteId; }; + struct Health { int current, max; }; + + coordinator->registerComponent(); + coordinator->registerComponent(); + coordinator->registerComponent(); + coordinator->registerComponent(); + + // Create a complex entity with multiple components + Entity entity = coordinator->createEntity(); + coordinator->addComponent(entity, Position{10.0f, 20.0f}); + coordinator->addComponent(entity, Velocity{1.0f, 2.0f}); + coordinator->addComponent(entity, Renderable{5}); + coordinator->addComponent(entity, Health{100, 100}); + + // Verify all components are accessible + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).x, 10.0f); + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).y, 20.0f); + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).dx, 1.0f); + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).dy, 2.0f); + EXPECT_EQ(coordinator->getComponent(entity).spriteId, 5); + EXPECT_EQ(coordinator->getComponent(entity).current, 100); + EXPECT_EQ(coordinator->getComponent(entity).max, 100); + + // Test entityHasComponent for all components + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + + // Get all component types and verify + auto types = coordinator->getAllComponentTypes(entity); + EXPECT_EQ(types.size(), 4); + + // Modify components + coordinator->getComponent(entity).x += coordinator->getComponent(entity).dx; + coordinator->getComponent(entity).y += coordinator->getComponent(entity).dy; + coordinator->getComponent(entity).current -= 10; + + // Verify modifications + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).x, 11.0f); + EXPECT_FLOAT_EQ(coordinator->getComponent(entity).y, 22.0f); + EXPECT_EQ(coordinator->getComponent(entity).current, 90); + + // Remove components and verify + coordinator->removeComponent(entity); + EXPECT_FALSE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + EXPECT_TRUE(coordinator->entityHasComponent(entity)); + + types = coordinator->getAllComponentTypes(entity); + EXPECT_EQ(types.size(), 3); } } diff --git a/tests/ecs/Definitions.test.cpp b/tests/ecs/Definitions.test.cpp new file mode 100644 index 000000000..aafd59058 --- /dev/null +++ b/tests/ecs/Definitions.test.cpp @@ -0,0 +1,89 @@ +//// Definitions.test.cpp /////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Test file for the utils in the definitions header +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "Definitions.hpp" +#include +#include +#include + +namespace nexo::ecs { + + class DefinitionsTest : public ::testing::Test {}; + + // Test structures for component type ID assignment + struct TestComponent1 {}; + struct TestComponent2 {}; + struct TestComponent3 {}; + + // A structure template to test type uniqueness + template + struct GenericComponent {}; + + // getUniqueComponentTypeID function tests + TEST_F(DefinitionsTest, GetUniqueComponentTypeIDAssignsUniqueIDs) { + // Get IDs for multiple component types + ComponentType id1 = getUniqueComponentTypeID(); + ComponentType id2 = getUniqueComponentTypeID(); + ComponentType id3 = getUniqueComponentTypeID(); + + // IDs should be unique + EXPECT_NE(id1, id2); + EXPECT_NE(id1, id3); + EXPECT_NE(id2, id3); + + // IDs should be assigned sequentially starting from 0 + EXPECT_EQ(id2, id1 + 1); + EXPECT_EQ(id3, id2 + 1); + } + + TEST_F(DefinitionsTest, GetUniqueComponentTypeIDReturnsSameIDForSameType) { + // Get the ID for the same type multiple times + ComponentType id1 = getUniqueComponentTypeID(); + ComponentType id2 = getUniqueComponentTypeID(); + ComponentType id3 = getUniqueComponentTypeID(); + + // Same type should get the same ID + EXPECT_EQ(id1, id2); + EXPECT_EQ(id1, id3); + } + + // getComponentTypeID function tests + TEST_F(DefinitionsTest, GetComponentTypeIDRemovesTypeQualifiers) { + // Get IDs for TestComponent1 with different qualifiers + ComponentType baseId = getComponentTypeID(); + ComponentType constId = getComponentTypeID(); + ComponentType volatileId = getComponentTypeID(); + ComponentType refId = getComponentTypeID(); + ComponentType constRefId = getComponentTypeID(); + + // All should return the same ID regardless of qualifiers + EXPECT_EQ(baseId, constId); + EXPECT_EQ(baseId, volatileId); + EXPECT_EQ(baseId, refId); + EXPECT_EQ(baseId, constRefId); + } + + TEST_F(DefinitionsTest, GetComponentTypeIDForTemplatedTypes) { + // Test with template types + ComponentType id1 = getComponentTypeID>(); + ComponentType id2 = getComponentTypeID>(); + ComponentType id3 = getComponentTypeID>(); + + // Different template instantiations should get different IDs + EXPECT_NE(id1, id2); + EXPECT_NE(id1, id3); + EXPECT_NE(id2, id3); + } +} diff --git a/tests/ecs/Entity.test.cpp b/tests/ecs/Entity.test.cpp index ad96dcb21..c703622a1 100644 --- a/tests/ecs/Entity.test.cpp +++ b/tests/ecs/Entity.test.cpp @@ -1,4 +1,4 @@ -//// Entity.test.cpp ////////////////////////////////////////////////////////// +//// Entity.test.cpp /////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -6,101 +6,284 @@ // zzz zzz zzz z zzzz zzzz zzzz zzzz // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // -// Author: Mehdy MORVAN -// Date: 26/11/2024 -// Description: Test file for the entity manager +// Author: iMeaNz +// Date: 2025-04-09 +// Description: Test file for Entity Manager class // /////////////////////////////////////////////////////////////////////////////// #include -#include -#include "ecs/Entity.hpp" +#include "Entity.hpp" +#include "ECSExceptions.hpp" +#include +#include +#include namespace nexo::ecs { - class EntityManagerTest : public ::testing::Test { - protected: - void SetUp() override { - entityManager = std::make_unique(); - } - std::unique_ptr entityManager; - }; + class EntityManagerTest : public ::testing::Test { + protected: + EntityManager entityManager; - TEST_F(EntityManagerTest, CreateAndDestroyEntity) { - Entity entity = entityManager->createEntity(); - EXPECT_EQ(entity, 0); // First entity should have ID 0 + // Helper to create multiple entities + std::vector createMultipleEntities(size_t count) { + std::vector entities; + entities.reserve(count); + for (size_t i = 0; i < count; ++i) { + entities.push_back(entityManager.createEntity()); + } + return entities; + } + }; - entityManager->destroyEntity(entity); + // Test constructor + TEST_F(EntityManagerTest, ConstructorInitializesCorrectly) { + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); - // Recreate the entity, it should use the ID in front - Entity reusedEntity = entityManager->createEntity(); - EXPECT_EQ(reusedEntity, 1); - } + // Create and then destroy an entity to verify the pool works + Entity e = entityManager.createEntity(); + EXPECT_EQ(e, 0); // First entity should be 0 + entityManager.destroyEntity(e); + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); - TEST_F(EntityManagerTest, TooManyEntities) { - for (Entity i = 0; i < MAX_ENTITIES; ++i) { - EXPECT_NO_THROW(entityManager->createEntity()); - } + // Creating again should reuse the entity ID + Entity e2 = entityManager.createEntity(); + EXPECT_EQ(e2, 0); + } - EXPECT_THROW(entityManager->createEntity(), TooManyEntities); - } + TEST_F(EntityManagerTest, CreateEntityReturnsUniqueIDs) { + const size_t numEntities = 100; + std::set uniqueEntities; - TEST_F(EntityManagerTest, SetAndGetSignature) { - Entity entity = entityManager->createEntity(); - Signature signature; - signature.set(1); // Set the 1st bit + for (size_t i = 0; i < numEntities; ++i) { + Entity e = entityManager.createEntity(); + EXPECT_EQ(uniqueEntities.count(e), 0) << "Entity ID " << e << " was issued multiple times"; + uniqueEntities.insert(e); + } - entityManager->setSignature(entity, signature); + EXPECT_EQ(entityManager.getLivingEntityCount(), numEntities); + } - Signature retrieved = entityManager->getSignature(entity); - EXPECT_EQ(retrieved, signature); - } + // Test destroyEntity + TEST_F(EntityManagerTest, DestroyEntityRemovesFromLivingEntities) { + // Create some entities + auto entities = createMultipleEntities(5); + EXPECT_EQ(entityManager.getLivingEntityCount(), 5); - TEST_F(EntityManagerTest, SetSignatureOutOfRange) { - Entity invalidEntity = MAX_ENTITIES; // Invalid entity ID - Signature signature; + // Destroy middle entity + entityManager.destroyEntity(entities[2]); + EXPECT_EQ(entityManager.getLivingEntityCount(), 4); - EXPECT_THROW(entityManager->setSignature(invalidEntity, signature), OutOfRange); - } + // Check it's removed from living entities + auto livingEntities = entityManager.getLivingEntities(); + EXPECT_FALSE(std::find(livingEntities.begin(), livingEntities.end(), entities[2]) != livingEntities.end()); - TEST_F(EntityManagerTest, GetSignatureOutOfRange) { - Entity invalidEntity = MAX_ENTITIES; // Invalid entity ID + // Other entities should still be there + EXPECT_TRUE(std::find(livingEntities.begin(), livingEntities.end(), entities[0]) != livingEntities.end()); + EXPECT_TRUE(std::find(livingEntities.begin(), livingEntities.end(), entities[1]) != livingEntities.end()); + EXPECT_TRUE(std::find(livingEntities.begin(), livingEntities.end(), entities[3]) != livingEntities.end()); + EXPECT_TRUE(std::find(livingEntities.begin(), livingEntities.end(), entities[4]) != livingEntities.end()); + } - EXPECT_THROW(entityManager->getSignature(invalidEntity), OutOfRange); - } + TEST_F(EntityManagerTest, DestroyEntityThrowsWithInvalidEntity) { + EXPECT_THROW({ entityManager.destroyEntity(MAX_ENTITIES); }, OutOfRange); + } - TEST_F(EntityManagerTest, DestroyEntityResetsSignature) { - Entity entity = entityManager->createEntity(); - Signature signature; - signature.set(1); // Set the 1st bit + // Test ID recycling + TEST_F(EntityManagerTest, DestroyedEntityIDsAreRecycled) { + // Create entities + auto entities = createMultipleEntities(5); - entityManager->setSignature(entity, signature); + // Destroy entities in reverse order + entityManager.destroyEntity(entities[4]); + entityManager.destroyEntity(entities[2]); + entityManager.destroyEntity(entities[0]); - entityManager->destroyEntity(entity); + // New entities should reuse IDs in LIFO order (stack behavior) + Entity e1 = entityManager.createEntity(); + Entity e2 = entityManager.createEntity(); + Entity e3 = entityManager.createEntity(); - Signature resetSignature = entityManager->getSignature(entity); - EXPECT_TRUE(resetSignature.none()); // Signature should be reset - } + // The IDs should be reused in reverse order of destruction + EXPECT_EQ(e1, entities[0]); + EXPECT_EQ(e2, entities[2]); + EXPECT_EQ(e3, entities[4]); + } - TEST_F(EntityManagerTest, DestroyEntityOutOfRange) { - Entity invalidEntity = MAX_ENTITIES; // Invalid entity ID + TEST_F(EntityManagerTest, SetAndGetSignatureWorkCorrectly) { + // Create an entity + Entity e = entityManager.createEntity(); - EXPECT_THROW(entityManager->destroyEntity(invalidEntity), OutOfRange); - } + // Create a test signature + Signature signature; + signature.set(1); // Set bit 1 + signature.set(3); // Set bit 3 - TEST_F(EntityManagerTest, CreateAndDestroyAllEntities) { - std::vector entities; + // Set signature + entityManager.setSignature(e, signature); - for (Entity i = 0; i < MAX_ENTITIES; ++i) { - entities.push_back(entityManager->createEntity()); - } + // Get signature and verify it matches + Signature retrievedSignature = entityManager.getSignature(e); + EXPECT_EQ(retrievedSignature, signature); + EXPECT_TRUE(retrievedSignature.test(1)); + EXPECT_TRUE(retrievedSignature.test(3)); + EXPECT_FALSE(retrievedSignature.test(0)); + EXPECT_FALSE(retrievedSignature.test(2)); + } - EXPECT_THROW(entityManager->createEntity(), TooManyEntities); + // Test setting signature for invalid entity + TEST_F(EntityManagerTest, SetSignatureThrowsWithInvalidEntity) { + Signature signature; + EXPECT_THROW({ entityManager.setSignature(MAX_ENTITIES, signature); }, OutOfRange); + } - for (Entity entity : entities) { - EXPECT_NO_THROW(entityManager->destroyEntity(entity)); - } + // Test getting signature for invalid entity + TEST_F(EntityManagerTest, GetSignatureThrowsWithInvalidEntity) { + EXPECT_THROW({ static_cast(entityManager.getSignature(MAX_ENTITIES)); }, OutOfRange); + } - EXPECT_NO_THROW(entityManager->createEntity()); - } -} \ No newline at end of file + // Test signature is reset on entity destruction + TEST_F(EntityManagerTest, DestroyEntityResetsSignature) { + // Create an entity and set signature + Entity e = entityManager.createEntity(); + + Signature signature; + signature.set(1); + signature.set(3); + + entityManager.setSignature(e, signature); + + // Destroy entity + entityManager.destroyEntity(e); + + // Create a new entity (should reuse the ID) + Entity newE = entityManager.createEntity(); + EXPECT_EQ(newE, e); + + // Signature should be reset + Signature newSignature = entityManager.getSignature(newE); + EXPECT_EQ(newSignature.count(), 0); + EXPECT_FALSE(newSignature.test(1)); + EXPECT_FALSE(newSignature.test(3)); + } + + // Test get living entities + TEST_F(EntityManagerTest, GetLivingEntitiesReturnsCorrectEntities) { + // Empty at start + EXPECT_EQ(entityManager.getLivingEntities().size(), 0); + + // Create some entities + auto entities = createMultipleEntities(5); + + // Check living entities + auto livingEntities = entityManager.getLivingEntities(); + EXPECT_EQ(livingEntities.size(), 5); + + // Verify all created entities are in the living set + for (auto e : entities) { + bool found = false; + for (auto le : livingEntities) { + if (le == e) { + found = true; + break; + } + } + EXPECT_TRUE(found) << "Entity " << e << " not found in living entities"; + } + + // Remove an entity + entityManager.destroyEntity(entities[2]); + + // Check updated living entities + livingEntities = entityManager.getLivingEntities(); + EXPECT_EQ(livingEntities.size(), 4); + + // Verify entity was removed + bool found = false; + for (auto le : livingEntities) { + if (le == entities[2]) { + found = true; + break; + } + } + EXPECT_FALSE(found) << "Destroyed entity " << entities[2] << " still found in living entities"; + } + + // Test get living entity count + TEST_F(EntityManagerTest, GetLivingEntityCountReturnsCorrectCount) { + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); + + auto e1 = entityManager.createEntity(); + EXPECT_EQ(entityManager.getLivingEntityCount(), 1); + + auto e2 = entityManager.createEntity(); + EXPECT_EQ(entityManager.getLivingEntityCount(), 2); + + entityManager.destroyEntity(e1); + EXPECT_EQ(entityManager.getLivingEntityCount(), 1); + + entityManager.destroyEntity(e2); + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); + } + + // Complex scenario test + TEST_F(EntityManagerTest, ComplexEntityLifecycleScenario) { + // Create a batch of entities + std::vector batch1 = createMultipleEntities(10); + EXPECT_EQ(entityManager.getLivingEntityCount(), 10); + + // Set signatures for some entities + for (size_t i = 0; i < batch1.size(); i += 2) { + Signature sig; + sig.set(i % MAX_COMPONENT_TYPE); + entityManager.setSignature(batch1[i], sig); + } + + // Destroy some entities + for (size_t i = 0; i < batch1.size(); i += 3) { + entityManager.destroyEntity(batch1[i]); + } + + // Create new entities (should reuse some IDs) + std::vector batch2 = createMultipleEntities(5); + + // Verify expected count + EXPECT_EQ(entityManager.getLivingEntityCount(), 11); // 10 - 4 + 5 = 11 (10 original - 4 destroyed + 5 new) + + // Check recycled IDs have clean signatures + for (Entity e : batch2) { + Signature sig = entityManager.getSignature(e); + EXPECT_EQ(sig.count(), 0) << "Recycled entity " << e << " has non-empty signature"; + } + + // Destroy all entities + auto livingEntities = entityManager.getLivingEntities(); + std::vector toDestroy; + toDestroy.reserve(livingEntities.size()); + for (Entity e : livingEntities) { + toDestroy.push_back(e); + } + for (Entity e : toDestroy) { + entityManager.destroyEntity(e); + } + + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); + } + + // Edge case - destroy entity that's already destroyed + TEST_F(EntityManagerTest, DestroyAlreadyDestroyedEntity) { + // This doesn't throw, just performs the operation + Entity e = entityManager.createEntity(); + entityManager.destroyEntity(e); + + // Should not throw, signature already reset + entityManager.destroyEntity(e); + + EXPECT_EQ(entityManager.getLivingEntityCount(), 0); + + // Create a new entity, should still get the same ID back + Entity newE = entityManager.createEntity(); + EXPECT_EQ(newE, e); + } + +} diff --git a/tests/ecs/Exceptions.test.cpp b/tests/ecs/Exceptions.test.cpp index 67f453d46..1f63c3289 100644 --- a/tests/ecs/Exceptions.test.cpp +++ b/tests/ecs/Exceptions.test.cpp @@ -1,4 +1,4 @@ -//// ECSExceptions.test.cpp /////////////////////////////////////////////////// +//// ECSExceptionsTest.cpp /////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -6,86 +6,208 @@ // zzz zzz zzz z zzzz zzzz zzzz zzzz // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // -// Author: Mehdy MORVAN -// Date: 02/12/2024 -// Description: Test file for the ECSExceptions class +// Author: iMeaNz +// Date: 2025-04-09 +// Description: Test file for ECS exceptions // /////////////////////////////////////////////////////////////////////////////// #include -#include "ecs/ECSExceptions.hpp" +#include "ECSExceptions.hpp" +#include +#include namespace nexo::ecs { - TEST(ECSExceptionsTest, ComponentNotFound) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - ComponentNotFound ex(42); - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("Component not found for: 42"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } - - TEST(ECSExceptionsTest, ComponentNotRegistered) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - ComponentNotRegistered ex; - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("Component has not been registered before use"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } - - TEST(ECSExceptionsTest, SingletonComponentNotRegistered) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - SingletonComponentNotRegistered ex; - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("Singleton component has not been registered before use"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } - - TEST(ECSExceptionsTest, SystemNotRegistered) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - SystemNotRegistered ex; - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("System has not been registered before use"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } - - TEST(ECSExceptionsTest, TooManyEntities) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - TooManyEntities ex; - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("Too many living entities, max is 8191"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } - - TEST(ECSExceptionsTest, OutOfRange) { - constexpr const char* expectedFile = __FILE__; - constexpr unsigned int expectedLine = __LINE__ + 2; - - OutOfRange ex(256); - std::string formattedMessage = ex.what(); - - EXPECT_NE(formattedMessage.find("Index 256 is out of range"), std::string::npos); - EXPECT_NE(formattedMessage.find(expectedFile), std::string::npos); - EXPECT_NE(formattedMessage.find(std::to_string(expectedLine)), std::string::npos); - } + // Base test fixture for exception tests + class ECSExceptionsTest : public ::testing::Test { + protected: + void SetUp() override { + // No specific setup needed + } + + // Helper method to verify exception inheritance + template + void verifyExceptionHierarchy() { + static_assert(std::is_base_of_v, + "Exception class must inherit from Exception"); + static_assert(std::is_final_v, + "Exception class should be marked final"); + } + + // Helper method to check exception message contains expected text + template + void verifyExceptionMessage(const std::string& expectedSubstring, Args&&... args) { + try { + throw ExceptionType(std::forward(args)...); + } catch (const Exception& e) { + std::string message = e.what(); + EXPECT_NE(message.find(expectedSubstring), std::string::npos) + << "Exception message '" << message << "' should contain '" + << expectedSubstring << "'"; + } catch (...) { + FAIL() << "Exception was not caught as Exception base class"; + } + } + }; + + // Test InternalError exception + TEST_F(ECSExceptionsTest, InternalErrorTest) { + verifyExceptionHierarchy(); + + const std::string errorMsg = "Something bad happened"; + verifyExceptionMessage(errorMsg, errorMsg); + verifyExceptionMessage("Internal error", errorMsg); + + // Test polymorphic catching + try { + throw InternalError("Test error"); + FAIL() << "Exception was not thrown"; + } catch (const InternalError& e) { + SUCCEED(); + } catch (...) { + FAIL() << "Wrong exception type caught"; + } + } + + // Test ComponentNotFound exception + TEST_F(ECSExceptionsTest, ComponentNotFoundTest) { + verifyExceptionHierarchy(); + + const Entity testEntity = 42; + verifyExceptionMessage(std::to_string(testEntity), testEntity); + verifyExceptionMessage("Component not found", testEntity); + + // Test with different entity values + verifyExceptionMessage("0", Entity(0)); + verifyExceptionMessage(std::to_string(MAX_ENTITIES-1), MAX_ENTITIES-1); + } + + // Test OverlappingGroupsException exception + TEST_F(ECSExceptionsTest, OverlappingGroupsExceptionTest) { + verifyExceptionHierarchy(); + + const std::string existingGroup = "Group1"; + const std::string newGroup = "Group2"; + const ComponentType conflictComponent = 5; + + verifyExceptionMessage(existingGroup, + existingGroup, newGroup, conflictComponent); + verifyExceptionMessage(newGroup, + existingGroup, newGroup, conflictComponent); + verifyExceptionMessage(std::to_string(conflictComponent), + existingGroup, newGroup, conflictComponent); + verifyExceptionMessage("overlapping owned component", + existingGroup, newGroup, conflictComponent); + + // Test with different component types + verifyExceptionMessage("component #0", + "GroupA", "GroupB", ComponentType(0)); + verifyExceptionMessage("component #31", + "GroupX", "GroupY", ComponentType(31)); + } + + // Test GroupNotFound exception + TEST_F(ECSExceptionsTest, GroupNotFoundTest) { + verifyExceptionHierarchy(); + + const std::string groupKey = "TestGroup"; + verifyExceptionMessage(groupKey, groupKey); + verifyExceptionMessage("Group not found", groupKey); + + // Test with empty key + verifyExceptionMessage("", ""); + } + + // Test ComponentNotRegistered exception + TEST_F(ECSExceptionsTest, ComponentNotRegisteredTest) { + verifyExceptionHierarchy(); + + verifyExceptionMessage("Component has not been registered"); + + // Test that no parameters are needed + try { + throw ComponentNotRegistered(); + FAIL() << "Exception was not thrown"; + } catch (const ComponentNotRegistered&) { + SUCCEED(); + } catch (...) { + FAIL() << "Wrong exception type caught"; + } + } + + // Test SingletonComponentNotRegistered exception + TEST_F(ECSExceptionsTest, SingletonComponentNotRegisteredTest) { + verifyExceptionHierarchy(); + + verifyExceptionMessage("Singleton component"); + verifyExceptionMessage("not been registered"); + + // Make sure it's distinct from ComponentNotRegistered + try { + throw SingletonComponentNotRegistered(); + } catch (const ComponentNotRegistered&) { + FAIL() << "SingletonComponentNotRegistered should not be caught as ComponentNotRegistered"; + } catch (const SingletonComponentNotRegistered&) { + SUCCEED(); + } catch (...) { + FAIL() << "Wrong exception type caught"; + } + } + + // Test SystemNotRegistered exception + TEST_F(ECSExceptionsTest, SystemNotRegisteredTest) { + verifyExceptionHierarchy(); + + verifyExceptionMessage("System has not been registered"); + } + + // Test TooManyEntities exception + TEST_F(ECSExceptionsTest, TooManyEntitiesTest) { + verifyExceptionHierarchy(); + + verifyExceptionMessage("Too many living entities"); + verifyExceptionMessage(std::to_string(MAX_ENTITIES)); + } + + // Test OutOfRange exception + TEST_F(ECSExceptionsTest, OutOfRangeTest) { + verifyExceptionHierarchy(); + + const unsigned int testIndex = 999; + verifyExceptionMessage(std::to_string(testIndex), testIndex); + verifyExceptionMessage("out of range", testIndex); + + // Test with different index values + verifyExceptionMessage("0", 0u); + verifyExceptionMessage(std::to_string(UINT_MAX), UINT_MAX); + } + + // Test that all exceptions can be caught polymorphically as Exception + TEST_F(ECSExceptionsTest, PolymorphicExceptionHandlingTest) { + int caughtCount = 0; + + try { + // Randomly choose one exception to throw + int choice = rand() % 8; + switch (choice) { + case 0: throw InternalError("Test"); + case 1: throw ComponentNotFound(5); + case 2: throw OverlappingGroupsException("G1", "G2", 3); + case 3: throw GroupNotFound("Key"); + case 4: throw ComponentNotRegistered(); + case 5: throw SingletonComponentNotRegistered(); + case 6: throw SystemNotRegistered(); + case 7: throw TooManyEntities(); + default: throw OutOfRange(10); + } + } catch (const Exception& e) { + // All exceptions should be caught here + caughtCount++; + } catch (...) { + FAIL() << "Exception not caught polymorphically"; + } + + EXPECT_EQ(caughtCount, 1) << "Exception should be caught exactly once"; + } } diff --git a/tests/ecs/Group.test.cpp b/tests/ecs/Group.test.cpp new file mode 100644 index 000000000..63b3dac1d --- /dev/null +++ b/tests/ecs/Group.test.cpp @@ -0,0 +1,617 @@ +//// Group.test.cpp ///////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Test file for the group class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "Group.hpp" +#include "ComponentArray.hpp" +#include "ECSExceptions.hpp" +#include +#include +#include +#include + +namespace nexo::ecs { + + // Test component types + struct PositionComponent { + float x, y, z; + + PositionComponent(float x = 0.0f, float y = 0.0f, float z = 0.0f) + : x(x), y(y), z(z) {} + + bool operator==(const PositionComponent& other) const { + return x == other.x && y == other.y && z == other.z; + } + }; + + struct VelocityComponent { + float vx, vy, vz; + + VelocityComponent(float vx = 0.0f, float vy = 0.0f, float vz = 0.0f) + : vx(vx), vy(vy), vz(vz) {} + + bool operator==(const VelocityComponent& other) const { + return vx == other.vx && vy == other.vy && vz == other.vz; + } + }; + + struct TagComponent { + std::string tag; + int category; + + TagComponent(const std::string& tag = "", int category = 0) + : tag(tag), category(category) {} + + bool operator==(const TagComponent& other) const { + return tag == other.tag && category == other.category; + } + }; + + struct HealthComponent { + int health; + int maxHealth; + + HealthComponent(int health = 100, int maxHealth = 100) + : health(health), maxHealth(maxHealth) {} + + bool operator==(const HealthComponent& other) const { + return health == other.health && maxHealth == other.maxHealth; + } + }; + + // Helper functions to setup component arrays and test entities + class GroupTest : public ::testing::Test { + protected: + // Component arrays + std::shared_ptr> positionArray; + std::shared_ptr> velocityArray; + std::shared_ptr> tagArray; + std::shared_ptr> healthArray; + + // Test entities + std::vector entities; + + void SetUp() override { + // Create component arrays + positionArray = std::make_shared>(); + velocityArray = std::make_shared>(); + tagArray = std::make_shared>(); + healthArray = std::make_shared>(); + + // Initialize with test entities + for (Entity i = 0; i < 5; ++i) { + entities.push_back(i); + + // Add components with test data + positionArray->insert(i, PositionComponent(i * 1.0f, i * 2.0f, i * 3.0f)); + velocityArray->insert(i, VelocityComponent(i * 0.5f, i * 1.0f, i * 1.5f)); + tagArray->insert(i, TagComponent("Entity_" + std::to_string(i), i % 3)); + healthArray->insert(i, HealthComponent(100 - i * 10, 100)); + } + } + + // Helper to create a group with owned and non-owned components + template + auto createGroup(auto nonOwned) { + using OwnedTuple = std::tuple>...>; + + auto ownedArrays = std::make_tuple(std::static_pointer_cast>(getNthArray())...); + auto nonOwnedArrays = nonOwned; + + return std::make_shared>(ownedArrays, nonOwnedArrays); + } + + // Helper to get component array by type + template + std::shared_ptr getNthArray() { + if constexpr (std::is_same_v) { + return positionArray; + } else if constexpr (std::is_same_v) { + return velocityArray; + } else if constexpr (std::is_same_v) { + return tagArray; + } else if constexpr (std::is_same_v) { + return healthArray; + } else { + static_assert(dependent_false::value, "Unknown component type"); + return nullptr; + } + } + }; + + TEST_F(GroupTest, ConstructorInitializesCorrectly) { + // Create a group with position as owned, velocity as non-owned + auto group = createGroup(std::make_tuple(velocityArray)); + + // Check signatures + auto ownedSignature = group->ownedSignature(); + auto allSignature = group->allSignature(); + + // Owned signature should only have position bit set + EXPECT_TRUE(ownedSignature.test(getComponentTypeID())); + EXPECT_FALSE(ownedSignature.test(getComponentTypeID())); + + // All signature should have both position and velocity bits set + EXPECT_TRUE(allSignature.test(getComponentTypeID())); + EXPECT_TRUE(allSignature.test(getComponentTypeID())); + } + + TEST_F(GroupTest, AddToGroupAddsEntities) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add a few entities to the group + for (Entity i = 0; i < 3; ++i) { + group->addToGroup(entities[i]); + } + + // Check size + EXPECT_EQ(group->size(), 3); + + // Check entities + auto groupEntities = group->entities(); + EXPECT_EQ(groupEntities.size(), 3); + + // Verify the entities are the ones we added + std::set expectedEntities = {0, 1, 2}; + std::set actualEntities(groupEntities.begin(), groupEntities.end()); + EXPECT_EQ(actualEntities, expectedEntities); + } + + TEST_F(GroupTest, RemoveFromGroupRemovesEntities) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add all entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Check initial size + EXPECT_EQ(group->size(), 5); + + // Remove some entities + group->removeFromGroup(entities[1]); + group->removeFromGroup(entities[3]); + + // Check updated size + EXPECT_EQ(group->size(), 3); + + // Check remaining entities + auto groupEntities = group->entities(); + EXPECT_EQ(groupEntities.size(), 3); + + // Verify the remaining entities + std::set expectedEntities = {0, 2, 4}; + std::set actualEntities(groupEntities.begin(), groupEntities.end()); + EXPECT_EQ(actualEntities, expectedEntities); + } + + TEST_F(GroupTest, GetReturnsSpanOfComponents) { + auto group = createGroup(std::make_tuple(velocityArray)); + + // Add some entities to the group + for (Entity i = 0; i < 3; ++i) { + group->addToGroup(entities[i]); + } + + // Get owned components + auto positions = group->get(); + // Compile-time check: positions should be a span type since it is owned. + static_assert(std::is_same_v>, + "positions should be a span of PositionComponent."); + EXPECT_EQ(positions.size(), 3); + + // Get non-owned components + auto velocities = group->get(); + // Compile-time check: velocities should be a component array type since it is not owned. + static_assert(std::is_same_v>>, + "velocities should be a component array of VelocityComponent."); + + // Check access to component data + for (size_t i = 0; i < 3; ++i) { + EXPECT_FLOAT_EQ(positions[i].x, i * 1.0f); + EXPECT_FLOAT_EQ(positions[i].y, i * 2.0f); + EXPECT_FLOAT_EQ(positions[i].z, i * 3.0f); + } + } + + TEST_F(GroupTest, IteratorBasicFunctionality) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add entities to the group + for (Entity i = 0; i < 3; ++i) { + group->addToGroup(entities[i]); + } + + // Use iterators to access entities and components + size_t count = 0; + for (auto [entity, position, velocity] : *group) { + EXPECT_TRUE(entity < 3); // Should be one of our first 3 entities + EXPECT_FLOAT_EQ(position.x, entity * 1.0f); + EXPECT_FLOAT_EQ(velocity.vx, entity * 0.5f); + count++; + } + + EXPECT_EQ(count, 3); + } + + TEST_F(GroupTest, IteratorEmptyGroup) { + auto group = createGroup(std::make_tuple(velocityArray)); + + size_t count = std::distance(group->begin(), group->end()); + + EXPECT_EQ(count, 0); + } + + TEST_F(GroupTest, EachMethodCallsFunction) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add entities to the group + for (Entity i = 0; i < 3; ++i) { + group->addToGroup(entities[i]); + } + + // Use each method to process entities + int callCount = 0; + group->each([&callCount](Entity e, PositionComponent& pos, VelocityComponent& vel, TagComponent& tag) { + EXPECT_EQ(pos.x, e * 1.0f); + EXPECT_EQ(vel.vx, e * 0.5f); + EXPECT_EQ(tag.tag, "Entity_" + std::to_string(e)); + callCount++; + }); + + EXPECT_EQ(callCount, 3); + } + + TEST_F(GroupTest, EachInRangeMethodCallsFunction) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Use eachInRange to process a subset of entities + int callCount = 0; + group->eachInRange(1, 2, [&callCount](Entity e, PositionComponent&, VelocityComponent&, TagComponent&) { + EXPECT_GE(e, 1); + EXPECT_LE(e, 3); + callCount++; + }); + + EXPECT_EQ(callCount, 2); + } + + TEST_F(GroupTest, SortByOwnedComponent) { + auto group = createGroup(std::make_tuple(tagArray)); + + // Add entities to the group in a specific order + group->addToGroup(entities[0]); // health = 100 + group->addToGroup(entities[2]); // health = 80 + group->addToGroup(entities[1]); // health = 90 + group->addToGroup(entities[4]); // health = 60 + group->addToGroup(entities[3]); // health = 70 + + // Sort by health (ascending) + group->sortBy([](const HealthComponent& h) { return h.health; }); + + // Check the new order + auto healthComponents = group->get(); + EXPECT_EQ(healthComponents.size(), 5); + + // Should be in ascending order of health: 60, 70, 80, 90, 100 + EXPECT_EQ(healthComponents[0].health, 60); + EXPECT_EQ(healthComponents[1].health, 70); + EXPECT_EQ(healthComponents[2].health, 80); + EXPECT_EQ(healthComponents[3].health, 90); + EXPECT_EQ(healthComponents[4].health, 100); + + // Sort by health (descending) + group->sortBy( + [](const HealthComponent& h) { return h.health; }, + false // descending + ); + + // Check the new order + healthComponents = group->get(); + + // Should be in descending order of health: 100, 90, 80, 70, 60 + EXPECT_EQ(healthComponents[0].health, 100); + EXPECT_EQ(healthComponents[1].health, 90); + EXPECT_EQ(healthComponents[2].health, 80); + EXPECT_EQ(healthComponents[3].health, 70); + EXPECT_EQ(healthComponents[4].health, 60); + + // Verify sorting invalidated flag is managed correctly + EXPECT_FALSE(group->sortingInvalidated()); + + // Add a new entity to invalidate sorting + group->addToGroup(entities[0]); // This is already in the group, but will still mark as invalidated + EXPECT_TRUE(group->sortingInvalidated()); + } + + TEST_F(GroupTest, SortByNonOwnedComponent) { + auto group = createGroup(std::make_tuple(tagArray, healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Sort by a non-owned component (health) + group->sortBy([](const HealthComponent& h) { return h.health; }); + + // Check that the sort was successful + auto groupEntities = group->entities(); + EXPECT_EQ(groupEntities[0], 4); // Entity 4 has lowest health (60) + EXPECT_EQ(groupEntities[4], 0); // Entity 0 has highest health (100) + } + + TEST_F(GroupTest, InvalidateSorting) { + auto group = createGroup(std::make_tuple(healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Sort by health (ascending) + group->sortBy([](const HealthComponent& h) { return h.health; }); + + // Now sort is not invalidated + EXPECT_FALSE(group->sortingInvalidated()); + + // Modify health values directly (this won't invalidate sorting in the Group) + for (int i = 4; i >= 0; --i) { + healthArray->get(i).health = 100 + i * 10; // Reverse the order + } + + group->invalidateSorting(); + + // Sort + group->sortBy([](const HealthComponent& h) { return h.health; }); + + // The original order should still be there, not the new values + auto groupEntities = group->entities(); + EXPECT_EQ(groupEntities[0], 0); + } + + ////////////////////////////////////////////////////////////////////////// + // Partition Tests + ////////////////////////////////////////////////////////////////////////// + + TEST_F(GroupTest, PartitionByComponentField) { + auto group = createGroup(std::make_tuple(healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Create a partition by category (0, 1, 2) + auto partitionView = group->getPartitionView( + [](const TagComponent& tag) { return tag.category; } + ); + + // Check the number of partitions + EXPECT_EQ(partitionView.partitionCount(), 3); + + // Get partition keys + auto keys = partitionView.getPartitionKeys(); + std::sort(keys.begin(), keys.end()); + EXPECT_EQ(keys[0], 0); + EXPECT_EQ(keys[1], 1); + EXPECT_EQ(keys[2], 2); + + // Check entities in each partition + int countCategory0 = 0; + partitionView.each(0, [&countCategory0](Entity, PositionComponent&, TagComponent& tag, HealthComponent&) { + EXPECT_EQ(tag.category, 0); + countCategory0++; + }); + EXPECT_EQ(countCategory0, 2); // Entities 0 and 3 have category 0 + + int countCategory1 = 0; + partitionView.each(1, [&countCategory1](Entity, PositionComponent&, TagComponent& tag, HealthComponent&) { + EXPECT_EQ(tag.category, 1); + countCategory1++; + }); + EXPECT_EQ(countCategory1, 2); // Entities 1 and 4 have category 1 + + int countCategory2 = 0; + partitionView.each(2, [&countCategory2](Entity, PositionComponent&, TagComponent& tag, HealthComponent&) { + EXPECT_EQ(tag.category, 2); + countCategory2++; + }); + EXPECT_EQ(countCategory2, 1); // Only entity 2 has category 2 + } + + TEST_F(GroupTest, PartitionInvalidation) { + auto group = createGroup(std::make_tuple(healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Create a partition + auto partitionView = group->getPartitionView( + [](const TagComponent& tag) { return tag.category; } + ); + + // Initial check + EXPECT_EQ(partitionView.partitionCount(), 3); + + // Modify tag category for entity 0 (from 0 to 3) + tagArray->get(0).category = 3; + + group->invalidatePartitions(); + + // Get the view again + auto newView = group->getPartitionView( + [](const TagComponent& tag) { return tag.category; } + ); + + // Should now have 4 partitions (0,1,2,3) + EXPECT_EQ(newView.partitionCount(), 4); + + // Check new partition + auto keys = newView.getPartitionKeys(); + EXPECT_TRUE(std::find(keys.begin(), keys.end(), 3) != keys.end()); + } + + TEST_F(GroupTest, PartitionWithNonExistentKey) { + auto group = createGroup(std::make_tuple(healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Create a partition + auto partitionView = group->getPartitionView( + [](const TagComponent& tag) { return tag.category; } + ); + + // Try to get a partition with a non-existent key + const auto* partition = partitionView.getPartition(99); + EXPECT_EQ(partition, nullptr); + + // Try to iterate through a non-existent partition + int callCount = 0; + partitionView.each(99, [&callCount](Entity, PositionComponent&, TagComponent&, HealthComponent&) { + callCount++; + }); + + // Should not call the function + EXPECT_EQ(callCount, 0); + } + + TEST_F(GroupTest, EntityPartitionView) { + auto group = createGroup(std::make_tuple(healthArray)); + + // Add entities to the group + for (Entity i = 0; i < 5; ++i) { + group->addToGroup(entities[i]); + } + + // Create a partition based directly on entity IDs + auto partitionView = group->getEntityPartitionView( + "test_partition", + [](Entity e) { return e % 2; } // Partition by even/odd + ); + + // Should have 2 partitions (0 for even, 1 for odd) + EXPECT_EQ(partitionView.partitionCount(), 2); + + // Check each partition + int evenCount = 0; + partitionView.each(0, [&evenCount](Entity e, PositionComponent&, TagComponent&, HealthComponent&) { + EXPECT_EQ(e % 2, 0); // Should be even + evenCount++; + }); + EXPECT_EQ(evenCount, 3); // Entities 0, 2, 4 + + int oddCount = 0; + partitionView.each(1, [&oddCount](Entity e, PositionComponent&, TagComponent&, HealthComponent&) { + EXPECT_EQ(e % 2, 1); // Should be odd + oddCount++; + }); + EXPECT_EQ(oddCount, 2); // Entities 1, 3 + } + + TEST_F(GroupTest, EmptyGroup) { + auto group = createGroup(std::make_tuple(velocityArray)); + + // Check size + EXPECT_EQ(group->size(), 0); + + // Check entities + auto groupEntities = group->entities(); + EXPECT_EQ(groupEntities.size(), 0); + + // Try using each method + int callCount = 0; + group->each([&callCount](Entity, PositionComponent&, VelocityComponent&) { + callCount++; + }); + EXPECT_EQ(callCount, 0); + + // Try sorting - shouldn't crash + group->sortBy([](const PositionComponent& p) { return p.x; }); + + // Try partitioning + auto partitionView = group->getPartitionView( + [](const PositionComponent& p) { return static_cast(p.x); } + ); + EXPECT_EQ(partitionView.partitionCount(), 0); + } + + TEST_F(GroupTest, RemoveNonExistentEntity) { + auto group = createGroup(std::make_tuple(velocityArray)); + + // Add some entities + group->addToGroup(entities[0]); + group->addToGroup(entities[1]); + + // Try removing an entity that's not in the group + group->removeFromGroup(entities[3]); + + // Size should remain unchanged + EXPECT_EQ(group->size(), 2); + } + + TEST_F(GroupTest, AddEntityTwice) { + auto group = createGroup(std::make_tuple(velocityArray)); + + // Add an entity + group->addToGroup(entities[0]); + EXPECT_EQ(group->size(), 1); + + // Add the same entity again + group->addToGroup(entities[0]); + + // Size should remain the same + EXPECT_EQ(group->size(), 1); + } + + TEST_F(GroupTest, ModifyComponentsViaSpan) { + auto group = createGroup(std::make_tuple(velocityArray)); + + // Add entities + group->addToGroup(entities[0]); + group->addToGroup(entities[1]); + + // Get span of position components + auto positions = group->get(); + + // Modify positions through the span + positions[0].x = 100.0f; + positions[1].x = 200.0f; + + // Verify changes were applied to the original component array + EXPECT_FLOAT_EQ(positionArray->get(entities[0]).x, 100.0f); + EXPECT_FLOAT_EQ(positionArray->get(entities[1]).x, 200.0f); + } + + TEST_F(GroupTest, GroupIteratorOutOfBounds) { + auto group = createGroup(std::make_tuple(velocityArray)); + group->addToGroup(entities[0]); + + auto it = group->begin(); + ASSERT_NO_THROW(*it); // First element is valid + + it = group->end(); + ASSERT_THROW(*it, OutOfRange); // Dereferencing end iterator should throw + } +} diff --git a/tests/ecs/GroupSystem.test.cpp b/tests/ecs/GroupSystem.test.cpp new file mode 100644 index 000000000..5f77f0b60 --- /dev/null +++ b/tests/ecs/GroupSystem.test.cpp @@ -0,0 +1,371 @@ +//// GroupSystem.test.cpp ///////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Test file for the GroupSystem class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "GroupSystem.hpp" +#include "Coordinator.hpp" +#include "Access.hpp" +#include "SingletonComponent.hpp" +#include +#include +#include + +namespace nexo::ecs { + + // Define test components + struct Position { + float x, y, z; + + Position(float x = 0.0f, float y = 0.0f, float z = 0.0f) + : x(x), y(y), z(z) {} + + bool operator==(const Position& other) const { + return x == other.x && y == other.y && z == other.z; + } + }; + + struct Velocity { + float vx, vy, vz; + + Velocity(float vx = 0.0f, float vy = 0.0f, float vz = 0.0f) + : vx(vx), vy(vy), vz(vz) {} + + bool operator==(const Velocity& other) const { + return vx == other.vx && vy == other.vy && vz == other.vz; + } + }; + + struct Tag { + std::string name; + int category; + + Tag(const std::string& name = "", int category = 0) + : name(name), category(category) {} + + bool operator==(const Tag& other) const { + return name == other.name && category == other.category; + } + }; + + // Define singleton components + class GameSettings { + public: + bool debugMode = false; + float gameSpeed = 1.0f; + + GameSettings() = default; + GameSettings(bool debug, float speed) : debugMode(debug), gameSpeed(speed) {} + + GameSettings(const GameSettings&) = delete; + GameSettings& operator=(const GameSettings&) = delete; + }; + + // Test fixture + class GroupSystemTest : public ::testing::Test { + protected: + std::shared_ptr coordinator; + std::vector entities; + + void SetUp() override { + // Initialize coordinator + coordinator = std::make_shared(); + coordinator->init(); + System::coord = coordinator; + + // Register components + coordinator->registerComponent(); + coordinator->registerComponent(); + coordinator->registerComponent(); + + // Register singleton + coordinator->registerSingletonComponent(true, 2.0f); + + // Create test entities + for (int i = 0; i < 5; ++i) { + Entity entity = coordinator->createEntity(); + entities.push_back(entity); + + // Add components + coordinator->addComponent(entity, Position(i * 1.0f, i * 2.0f, i * 3.0f)); + coordinator->addComponent(entity, Velocity(i * 0.5f, i * 1.0f, i * 1.5f)); + coordinator->addComponent(entity, Tag("Entity_" + std::to_string(i), i % 3)); + } + } + + void TearDown() override { + // Clean up entities + for (auto entity : entities) { + coordinator->destroyEntity(entity); + } + + // Reset coordinator + System::coord = nullptr; + } + }; + + // Test system classes + + // System with owned Position and read-only access to Velocity + class PositionSystem : public GroupSystem>, NonOwned>> { + public: + void updatePositions() { + auto positions = get(); + auto velocities = get(); + auto entities = getEntities(); + + for (size_t i = 0; i < positions.size(); ++i) { + const auto &vel = velocities->get(entities[i]); + positions[i].x += vel.vx; + positions[i].y += vel.vy; + positions[i].z += vel.vz; + } + } + }; + + // 2. System with read-only access to Position and Tag + class ReadOnlySystem : public GroupSystem>, NonOwned>> { + public: + int countEntitiesAboveThreshold(float threshold) { + auto positions = get(); + auto tags = get(); + int count = 0; + + for (size_t i = 0; i < positions.size(); ++i) { + if (positions[i].x > threshold) { + count++; + } + } + + return count; + } + }; + + // 3. System with singleton component access + class SystemWithSingleton : public GroupSystem< + Owned>, + NonOwned>, + ReadSingleton> { + public: + void scaleVelocities() { + auto positions = get(); + auto velocities = get(); + const auto entities = getEntities(); + const auto& settings = getSingleton(); + + for (size_t i = 0; i < positions.size(); ++i) { + const auto &vel = velocities->get(entities[i]); + positions[i].x += vel.vx * settings.gameSpeed; + positions[i].y += vel.vy * settings.gameSpeed; + positions[i].z += vel.vz * settings.gameSpeed; + } + } + }; + + // 4. System with both read and write access to different components + class MixedAccessSystem : public GroupSystem< + Owned, Read>, + NonOwned>> { + public: + void updatePositionsByCategory(int category, float multiplier) { + auto positions = get(); + auto velocities = get(); + auto tags = get(); + auto entities = getEntities(); + + for (size_t i = 0; i < positions.size(); ++i) { + if (tags[i].category == category) { + positions[i].x += velocities->get(entities[i]).vx * multiplier; + positions[i].y += velocities->get(entities[i]).vy * multiplier; + positions[i].z += velocities->get(entities[i]).vz * multiplier; + } + } + } + }; + + TEST_F(GroupSystemTest, SystemCreation) { + auto system = coordinator->registerGroupSystem(); + ASSERT_NE(system, nullptr); + + // Check static type checking for owned components + EXPECT_TRUE(PositionSystem::isOwnedComponent()); + EXPECT_FALSE(PositionSystem::isOwnedComponent()); + } + + TEST_F(GroupSystemTest, WriteAccessToOwnedComponents) { + auto system = coordinator->registerGroupSystem(); + + // Update positions + system->updatePositions(); + + // Verify changes were applied + for (size_t i = 0; i < entities.size(); ++i) { + Position& pos = coordinator->getComponent(entities[i]); + EXPECT_FLOAT_EQ(pos.x, i * 1.0f + i * 0.5f); + EXPECT_FLOAT_EQ(pos.y, i * 2.0f + i * 1.0f); + EXPECT_FLOAT_EQ(pos.z, i * 3.0f + i * 1.5f); + } + } + + TEST_F(GroupSystemTest, ReadOnlyComponents) { + auto system = coordinator->registerGroupSystem(); + + // Count entities above threshold + int count = system->countEntitiesAboveThreshold(2.0f); + + // Verify count + EXPECT_EQ(count, 2); // Entities 3 and 4 have x > 2.0 + + // This would cause a compilation error if uncommented - verifying access control: + // auto positions = system->get(); + // positions[0].x = 999.0f; // Cannot modify a read-only component + } + + ////////////////////////////////////////////////////////////////////////// + // Singleton Component Tests + ////////////////////////////////////////////////////////////////////////// + + TEST_F(GroupSystemTest, SingletonComponentAccess) { + auto system = coordinator->registerGroupSystem(); + + // Test accessing singleton component + system->scaleVelocities(); + + // Verify changes reflect the game speed setting (2.0) + for (size_t i = 0; i < entities.size(); ++i) { + Position& pos = coordinator->getComponent(entities[i]); + EXPECT_FLOAT_EQ(pos.x, i * 1.0f + i * 0.5f * 2.0f); + EXPECT_FLOAT_EQ(pos.y, i * 2.0f + i * 1.0f * 2.0f); + EXPECT_FLOAT_EQ(pos.z, i * 3.0f + i * 1.5f * 2.0f); + } + + // Update singleton and verify changes are reflected + auto& settings = coordinator->getSingletonComponent(); + settings.gameSpeed = 3.0f; + + // Reset positions + for (size_t i = 0; i < entities.size(); ++i) { + Position& pos = coordinator->getComponent(entities[i]); + pos.x = i * 1.0f; + pos.y = i * 2.0f; + pos.z = i * 3.0f; + } + + system->scaleVelocities(); + + // Verify changes reflect the updated game speed (3.0) + for (size_t i = 0; i < entities.size(); ++i) { + Position& pos = coordinator->getComponent(entities[i]); + EXPECT_FLOAT_EQ(pos.x, i * 1.0f + i * 0.5f * 3.0f); + EXPECT_FLOAT_EQ(pos.y, i * 2.0f + i * 1.0f * 3.0f); + EXPECT_FLOAT_EQ(pos.z, i * 3.0f + i * 1.5f * 3.0f); + } + } + + TEST_F(GroupSystemTest, MixedAccessToComponents) { + auto system = coordinator->registerGroupSystem(); + + // Update positions for category 1 with multiplier 2.0 + system->updatePositionsByCategory(1, 2.0f); + + // Verify changes for entities in category 1 + for (size_t i = 0; i < entities.size(); ++i) { + Position& pos = coordinator->getComponent(entities[i]); + Tag& tag = coordinator->getComponent(entities[i]); + + if (tag.category == 1) { + EXPECT_FLOAT_EQ(pos.x, i * 1.0f + i * 0.5f * 2.0f); + EXPECT_FLOAT_EQ(pos.y, i * 2.0f + i * 1.0f * 2.0f); + EXPECT_FLOAT_EQ(pos.z, i * 3.0f + i * 1.5f * 2.0f); + } else { + EXPECT_FLOAT_EQ(pos.x, i * 1.0f); + EXPECT_FLOAT_EQ(pos.y, i * 2.0f); + EXPECT_FLOAT_EQ(pos.z, i * 3.0f); + } + } + } + + TEST_F(GroupSystemTest, EntityRetrieval) { + auto system = coordinator->registerGroupSystem(); + + // Get entities + auto systemEntities = system->getEntities(); + + // Verify all entities are accessible + EXPECT_EQ(systemEntities.size(), entities.size()); + + // Verify the entities match + std::vector entityVec(systemEntities.begin(), systemEntities.end()); + std::sort(entityVec.begin(), entityVec.end()); + std::sort(entities.begin(), entities.end()); + + for (size_t i = 0; i < entities.size(); ++i) { + EXPECT_EQ(entityVec[i], entities[i]); + } + } + + TEST_F(GroupSystemTest, EntityRemoval) { + auto system = coordinator->registerGroupSystem(); + + // Initial entity count + EXPECT_EQ(system->getEntities().size(), entities.size()); + + // Remove a component from an entity + coordinator->removeComponent(entities[0]); + + // Verify entity was removed from the group + EXPECT_EQ(system->getEntities().size(), entities.size() - 1); + + // Verify the right entity was removed + bool found = false; + for (auto entity : system->getEntities()) { + if (entity == entities[0]) { + found = true; + break; + } + } + EXPECT_FALSE(found); + } + + TEST_F(GroupSystemTest, EmptyGroup) { + auto system = coordinator->registerGroupSystem(); + + // Remove all Position components + for (auto entity : entities) { + coordinator->removeComponent(entity); + } + + // Verify empty group + EXPECT_EQ(system->getEntities().size(), 0); + + // Verify component spans are empty + auto positions = system->get(); + EXPECT_EQ(positions.size(), 0); + + // Test operations on empty group - should not crash + system->updatePositions(); + } + + // Test with system that accesses non-registered component + struct Unregistered { + int value = 0; + }; + + class SystemWithUnregisteredComponent : public GroupSystem>> { + }; + + TEST_F(GroupSystemTest, UnregisteredComponentAccess) { + // This should fail at runtime + EXPECT_THROW(coordinator->registerGroupSystem(), ComponentNotRegistered); + } +} diff --git a/tests/ecs/QuerySystem.test.cpp b/tests/ecs/QuerySystem.test.cpp new file mode 100644 index 000000000..3b217b736 --- /dev/null +++ b/tests/ecs/QuerySystem.test.cpp @@ -0,0 +1,301 @@ +//// QuerySystem.test.cpp ///////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 09/04/2025 +// Description: Test file for the QuerySystem class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include "QuerySystem.hpp" +#include "Coordinator.hpp" +#include "Access.hpp" +#include "../utils/comparison.hpp" +#include "SingletonComponent.hpp" +#include +#include +#include + +namespace nexo::ecs { + + // Define test components + struct Position { + float x, y, z; + + Position(float x = 0.0f, float y = 0.0f, float z = 0.0f) + : x(x), y(y), z(z) {} + + bool operator==(const Position& other) const { + return x == other.x && y == other.y && z == other.z; + } + }; + + struct Velocity { + float vx, vy, vz; + + Velocity(float vx = 0.0f, float vy = 0.0f, float vz = 0.0f) + : vx(vx), vy(vy), vz(vz) {} + + bool operator==(const Velocity& other) const { + return vx == other.vx && vy == other.vy && vz == other.vz; + } + }; + + struct Tag { + std::string name; + int category; + + Tag(const std::string& name = "", int category = 0) + : name(name), category(category) {} + + bool operator==(const Tag& other) const { + return name == other.name && category == other.category; + } + }; + + // Define singleton components + class GameSettings { + public: + bool debugMode = false; + float gameSpeed = 1.0f; + + GameSettings() = default; + GameSettings(bool debug, float speed) : debugMode(debug), gameSpeed(speed) {} + + GameSettings(const GameSettings&) = delete; + GameSettings& operator=(const GameSettings&) = delete; + }; + + // Test fixture + class QuerySystemTest : public ::testing::Test { + protected: + std::shared_ptr coordinator; + std::vector entities; + + void SetUp() override { + // Initialize coordinator + coordinator = std::make_shared(); + coordinator->init(); + System::coord = coordinator; + + // Register components + coordinator->registerComponent(); + coordinator->registerComponent(); + coordinator->registerComponent(); + + // Register singleton + coordinator->registerSingletonComponent(true, 2.0f); + + // Create test entities + for (int i = 0; i < 5; ++i) { + Entity entity = coordinator->createEntity(); + entities.push_back(entity); + + // Add components + coordinator->addComponent(entity, Position(i * 1.0f, i * 2.0f, i * 3.0f)); + coordinator->addComponent(entity, Velocity(i * 0.5f, i * 1.0f, i * 1.5f)); + coordinator->addComponent(entity, Tag("Entity_" + std::to_string(i), i % 3)); + } + + // Create an entity with only Position and Tag + Entity posTagEntity = coordinator->createEntity(); + coordinator->addComponent(posTagEntity, Position(10.0f, 20.0f, 30.0f)); + coordinator->addComponent(posTagEntity, Tag("PosTagOnly", 99)); + entities.push_back(posTagEntity); + } + + void TearDown() override { + // Clean up entities + for (auto entity : entities) { + coordinator->destroyEntity(entity); + } + + // Reset coordinator + System::coord = nullptr; + } + }; + + // Test system classes + + // 1. System with read access to Position and write access to Velocity + class MovementSystem : public QuerySystem, Write> { + public: + void applyGravity(float gravity) { + for (Entity entity : entities) { + const Position& pos = getComponent(entity); + Velocity& vel = getComponent(entity); + + // Apply gravity based on height + vel.vy -= gravity * (pos.y / 10.0f); + } + } + }; + + // 2. System with singleton component access + class PhysicsSystem : public QuerySystem< + Read, + Write, + ReadSingleton> { + public: + void updateVelocities() { + const auto& settings = getSingleton(); + + for (Entity entity : entities) { + Velocity& vel = getComponent(entity); + + // Apply game speed scaling + vel.vx *= settings.gameSpeed; + vel.vy *= settings.gameSpeed; + vel.vz *= settings.gameSpeed; + } + } + }; + + TEST_F(QuerySystemTest, SystemCreation) { + auto system = coordinator->registerQuerySystem(); + ASSERT_NE(system, nullptr); + + // Verify system signature is set correctly + Signature expectedSignature; + expectedSignature.set(coordinator->getComponentType()); + expectedSignature.set(coordinator->getComponentType()); + + EXPECT_EQ(system->getSignature(), expectedSignature); + + // Verify system's entity set gets populated + EXPECT_EQ(system->entities.size(), 5); // Only first 5 entities have Position and Velocity + } + + TEST_F(QuerySystemTest, ComponentAccess) { + auto system = coordinator->registerQuerySystem(); + + // Apply gravity (0.5) + system->applyGravity(0.5f); + + // Verify velocities were modified + for (size_t i = 0; i < 5; ++i) { + Velocity& vel = coordinator->getComponent(entities[i]); + + // Expected: original vy - (gravity * height / 10.0f) + float expectedVy = i * 1.0f - 0.5f * (i * 2.0f / 10.0f); + EXPECT_FLOAT_EQ(vel.vy, expectedVy); + + // vx and vz should be unchanged + EXPECT_FLOAT_EQ(vel.vx, i * 0.5f); + EXPECT_FLOAT_EQ(vel.vz, i * 1.5f); + } + } + + ////////////////////////////////////////////////////////////////////////// + // Singleton Component Tests + ////////////////////////////////////////////////////////////////////////// + + TEST_F(QuerySystemTest, SingletonComponentAccess) { + auto system = coordinator->registerQuerySystem(); + + // Update velocities using game speed from singleton + system->updateVelocities(); + + // Verify velocities were scaled by game speed (2.0) + for (size_t i = 0; i < 5; ++i) { + Velocity& vel = coordinator->getComponent(entities[i]); + + EXPECT_FLOAT_EQ(vel.vx, i * 0.5f * 2.0f); + EXPECT_FLOAT_EQ(vel.vy, i * 1.0f * 2.0f); + EXPECT_FLOAT_EQ(vel.vz, i * 1.5f * 2.0f); + } + + // Update singleton and verify changes are reflected + auto& settings = coordinator->getSingletonComponent(); + settings.gameSpeed = 3.0f; + + // Reset velocities + for (size_t i = 0; i < 5; ++i) { + auto &vel = coordinator->getComponent(entities[i]); + vel.vx = i * 0.5f; + vel.vy = i * 1.0f; + vel.vz = i * 1.5f; + } + + system->updateVelocities(); + + // Verify velocities were scaled by the new game speed (3.0) + for (size_t i = 0; i < 5; ++i) { + Velocity& vel = coordinator->getComponent(entities[i]); + + EXPECT_FLOAT_EQ(vel.vx, i * 0.5f * 3.0f); + EXPECT_FLOAT_EQ(vel.vy, i * 1.0f * 3.0f); + EXPECT_FLOAT_EQ(vel.vz, i * 1.5f * 3.0f); + } + } + + TEST_F(QuerySystemTest, EntityUpdates) { + auto system = coordinator->registerQuerySystem(); + + // Initially system should have 5 entities + EXPECT_EQ(system->entities.size(), 5); + + // Remove a component from an entity + coordinator->removeComponent(entities[0]); + + // Verify entity was removed from the system + EXPECT_EQ(system->entities.size(), 4); + + // Add the component back + coordinator->addComponent(entities[0], Velocity(99.0f, 99.0f, 99.0f)); + + // Verify entity was added back to the system + EXPECT_EQ(system->entities.size(), 5); + + // Apply gravity again and verify it works for all entities + system->applyGravity(1.0f); + + // Check the newly re-added entity + Velocity& vel = coordinator->getComponent(entities[0]); + EXPECT_FLOAT_EQ(vel.vy, 99.0f - 1.0f * (0.0f / 10.0f)); + } + + TEST_F(QuerySystemTest, AccessingMissingComponent) { + auto system = coordinator->registerQuerySystem(); + + // Remove a component + coordinator->removeComponent(entities[0]); + + // Trying to access the removed component should throw + EXPECT_THROW(system->getComponent(entities[0]), InternalError); + } + + TEST_F(QuerySystemTest, EmptySystem) { + // Remove all relevant components + for (size_t i = 0; i < 5; ++i) { + coordinator->removeComponent(entities[i]); + } + + auto system = coordinator->registerQuerySystem(); + + // Verify system has no entities + EXPECT_EQ(system->entities.size(), 0); + + // Operations on empty system should not crash + EXPECT_NO_THROW(system->applyGravity(1.0f)); + } + + // Define a system with non-registered component + struct Unregistered { + int value = 0; + }; + + class SystemWithUnregisteredComponent : public QuerySystem> { + }; + + TEST_F(QuerySystemTest, UnregisteredComponentAccess) { + // Creating system with unregistered component should fail + EXPECT_THROW(coordinator->registerQuerySystem(), ComponentNotRegistered); + } +} diff --git a/tests/ecs/SingletonComponent.test.cpp b/tests/ecs/SingletonComponent.test.cpp new file mode 100644 index 000000000..25ac4b93b --- /dev/null +++ b/tests/ecs/SingletonComponent.test.cpp @@ -0,0 +1,222 @@ +//// SingletonComponent.test.cpp ////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/03/2025 +// Description: Test file for the singleton component classes +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include "ecs/SingletonComponent.hpp" + +namespace nexo::ecs { + + // Test components + struct TestComponent { + int value; + std::string name; + + TestComponent(int v) : value(v), name("default") {} + TestComponent(int v, std::string n) : value(v), name(n) {} + + TestComponent(const TestComponent&) = delete; + TestComponent& operator=(const TestComponent&) = delete; + + }; + + struct ComplexComponent { + std::vector data; + bool flag; + + ComplexComponent() : flag(false) {} + ComplexComponent(const std::vector& d, bool f) : data(d), flag(f) {} + + ComplexComponent(const ComplexComponent&) = delete; + ComplexComponent& operator=(const ComplexComponent&) = delete; + }; + + class SingletonComponentTest : public ::testing::Test {}; + + class SingletonComponentManagerTest : public ::testing::Test { + protected: + void SetUp() override { + manager = std::make_unique(); + } + + std::unique_ptr manager; + }; + + TEST_F(SingletonComponentTest, ConstructWithSingleArgument) { + SingletonComponent component(42); + EXPECT_EQ(component.getInstance().value, 42); + EXPECT_EQ(component.getInstance().name, "default"); + } + + TEST_F(SingletonComponentTest, ConstructWithMultipleArguments) { + SingletonComponent component(42, "test"); + EXPECT_EQ(component.getInstance().value, 42); + EXPECT_EQ(component.getInstance().name, "test"); + } + + TEST_F(SingletonComponentTest, GetInstanceReturnsReference) { + SingletonComponent component(42); + TestComponent& instance = component.getInstance(); + + // Modify through reference + instance.value = 100; + instance.name = "modified"; + + // Verify changes + EXPECT_EQ(component.getInstance().value, 100); + EXPECT_EQ(component.getInstance().name, "modified"); + } + + TEST_F(SingletonComponentTest, ComplexComponentConstruction) { + std::vector testData = {1, 2, 3, 4, 5}; + SingletonComponent component(testData, true); + + EXPECT_EQ(component.getInstance().data, testData); + EXPECT_TRUE(component.getInstance().flag); + } + + TEST_F(SingletonComponentTest, DefaultConstructible) { + SingletonComponent component; + EXPECT_TRUE(component.getInstance().data.empty()); + EXPECT_FALSE(component.getInstance().flag); + } + + TEST_F(SingletonComponentManagerTest, RegisterAndGetSingletonComponent) { + manager->registerSingletonComponent(42); + + TestComponent& component = manager->getSingletonComponent(); + EXPECT_EQ(component.value, 42); + EXPECT_EQ(component.name, "default"); + } + + TEST_F(SingletonComponentManagerTest, RegisterWithMultipleArguments) { + manager->registerSingletonComponent(42, "test"); + + TestComponent& component = manager->getSingletonComponent(); + EXPECT_EQ(component.value, 42); + EXPECT_EQ(component.name, "test"); + } + + TEST_F(SingletonComponentManagerTest, GetNonexistentComponent) { + EXPECT_THROW(manager->getSingletonComponent(), SingletonComponentNotRegistered); + } + + TEST_F(SingletonComponentManagerTest, UnregisterComponent) { + // Register + manager->registerSingletonComponent(42); + EXPECT_NO_THROW(manager->getSingletonComponent()); + + // Unregister + manager->unregisterSingletonComponent(); + + // Should now throw + EXPECT_THROW(manager->getSingletonComponent(), SingletonComponentNotRegistered); + } + + TEST_F(SingletonComponentManagerTest, UnregisterNonexistentComponent) { + EXPECT_THROW(manager->unregisterSingletonComponent(), SingletonComponentNotRegistered); + } + + TEST_F(SingletonComponentManagerTest, RegisterSameComponentTwice) { + // First registration + manager->registerSingletonComponent(42); + + // Second registration should log warning but not throw + EXPECT_NO_THROW(manager->registerSingletonComponent(100)); + + // Should still have the original value + TestComponent& component = manager->getSingletonComponent(); + EXPECT_EQ(component.value, 42); + } + + TEST_F(SingletonComponentManagerTest, RegisterMultipleComponentTypes) { + manager->registerSingletonComponent(42); + manager->registerSingletonComponent(std::vector{1, 2, 3}, true); + + // Both should be retrievable + TestComponent& comp1 = manager->getSingletonComponent(); + ComplexComponent& comp2 = manager->getSingletonComponent(); + + EXPECT_EQ(comp1.value, 42); + EXPECT_EQ(comp2.data, std::vector({1, 2, 3})); + EXPECT_TRUE(comp2.flag); + } + + TEST_F(SingletonComponentManagerTest, ModifyComponent) { + manager->registerSingletonComponent(42); + + // Get and modify + TestComponent& component = manager->getSingletonComponent(); + component.value = 100; + component.name = "modified"; + + // Should reflect changes when retrieved again + TestComponent& retrieved = manager->getSingletonComponent(); + EXPECT_EQ(retrieved.value, 100); + EXPECT_EQ(retrieved.name, "modified"); + } + + TEST_F(SingletonComponentManagerTest, RegisterAfterUnregister) { + // Register + manager->registerSingletonComponent(42); + + // Unregister + manager->unregisterSingletonComponent(); + + // Register again with different value + manager->registerSingletonComponent(100); + + // Should have new value + TestComponent& component = manager->getSingletonComponent(); + EXPECT_EQ(component.value, 100); + } + + TEST_F(SingletonComponentManagerTest, ComplexComponentCycle) { + // Register complex component + std::vector originalData = {1, 2, 3}; + manager->registerSingletonComponent(originalData, true); + + // Get and modify + ComplexComponent& comp = manager->getSingletonComponent(); + comp.data.push_back(4); + comp.flag = false; + + // Verify modifications + ComplexComponent& modified = manager->getSingletonComponent(); + std::vector expectedData = {1, 2, 3, 4}; + EXPECT_EQ(modified.data, expectedData); + EXPECT_FALSE(modified.flag); + + // Unregister + manager->unregisterSingletonComponent(); + + // Register new instance + std::vector newData = {5, 6, 7}; + manager->registerSingletonComponent(newData, true); + + // Verify new instance + ComplexComponent& newComp = manager->getSingletonComponent(); + EXPECT_EQ(newComp.data, newData); + EXPECT_TRUE(newComp.flag); + } + + TEST_F(SingletonComponentManagerTest, MultipleUnregistrations) { + manager->registerSingletonComponent(42); + manager->unregisterSingletonComponent(); + + // Second unregistration should throw + EXPECT_THROW(manager->unregisterSingletonComponent(), SingletonComponentNotRegistered); + } + +} diff --git a/tests/ecs/SparseSet.test.cpp b/tests/ecs/SparseSet.test.cpp new file mode 100644 index 000000000..f87f7fb8b --- /dev/null +++ b/tests/ecs/SparseSet.test.cpp @@ -0,0 +1,272 @@ +//// SparseSet.test.cpp /////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 28/03/2025 +// Description: Test file for the sparse set class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include "ecs/System.hpp" + +namespace nexo::ecs { + class SparseSetTest : public ::testing::Test { + protected: + SparseSet sparseSet; + }; + + TEST_F(SparseSetTest, InitiallyEmpty) { + EXPECT_TRUE(sparseSet.empty()); + EXPECT_TRUE(sparseSet.getDense().empty()); + EXPECT_EQ(sparseSet.begin(), sparseSet.end()); + } + + TEST_F(SparseSetTest, InsertSingleEntity) { + Entity entity = 42; + sparseSet.insert(entity); + + EXPECT_FALSE(sparseSet.empty()); + EXPECT_TRUE(sparseSet.contains(entity)); + EXPECT_EQ(sparseSet.getDense().size(), 1); + EXPECT_EQ(sparseSet.getDense()[0], entity); + } + + TEST_F(SparseSetTest, InsertMultipleEntities) { + Entity entity1 = 42; + Entity entity2 = 100; + Entity entity3 = 255; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + sparseSet.insert(entity3); + + EXPECT_FALSE(sparseSet.empty()); + EXPECT_TRUE(sparseSet.contains(entity1)); + EXPECT_TRUE(sparseSet.contains(entity2)); + EXPECT_TRUE(sparseSet.contains(entity3)); + EXPECT_EQ(sparseSet.getDense().size(), 3); + + // Check that all entities are in the dense array + auto& dense = sparseSet.getDense(); + EXPECT_THAT(dense, ::testing::UnorderedElementsAre(entity1, entity2, entity3)); + } + + TEST_F(SparseSetTest, EraseSingleEntity) { + Entity entity = 42; + sparseSet.insert(entity); + EXPECT_TRUE(sparseSet.contains(entity)); + + sparseSet.erase(entity); + EXPECT_FALSE(sparseSet.contains(entity)); + EXPECT_TRUE(sparseSet.empty()); + } + + TEST_F(SparseSetTest, EraseMultipleEntities) { + // Insert multiple entities + Entity entity1 = 42; + Entity entity2 = 100; + Entity entity3 = 255; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + sparseSet.insert(entity3); + + // Erase middle entity + sparseSet.erase(entity2); + EXPECT_FALSE(sparseSet.contains(entity2)); + EXPECT_TRUE(sparseSet.contains(entity1)); + EXPECT_TRUE(sparseSet.contains(entity3)); + EXPECT_EQ(sparseSet.getDense().size(), 2); + + // Erase first entity + sparseSet.erase(entity1); + EXPECT_FALSE(sparseSet.contains(entity1)); + EXPECT_TRUE(sparseSet.contains(entity3)); + EXPECT_EQ(sparseSet.getDense().size(), 1); + + // Erase last entity + sparseSet.erase(entity3); + EXPECT_FALSE(sparseSet.contains(entity3)); + EXPECT_TRUE(sparseSet.empty()); + } + + TEST_F(SparseSetTest, SwapAndPopMechanism) { + // Insert entities in order to test the swap-and-pop mechanism + Entity entity1 = 1; + Entity entity2 = 2; + Entity entity3 = 3; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + sparseSet.insert(entity3); + + // Erase the first entity + sparseSet.erase(entity1); + + // The last entity should now be in the first entity's position + auto& dense = sparseSet.getDense(); + EXPECT_EQ(dense.size(), 2); + + // Since order isn't guaranteed with unordered_map, we just check that both + // remaining entities are in the dense array + EXPECT_THAT(dense, ::testing::UnorderedElementsAre(entity2, entity3)); + EXPECT_TRUE(sparseSet.contains(entity2)); + EXPECT_TRUE(sparseSet.contains(entity3)); + } + + TEST_F(SparseSetTest, InsertDuplicateEntity) { + Entity entity = 42; + + sparseSet.insert(entity); + // Try to insert the same entity again + sparseSet.insert(entity); + + // Should only be one entity in the set + EXPECT_EQ(sparseSet.getDense().size(), 1); + EXPECT_TRUE(sparseSet.contains(entity)); + } + + TEST_F(SparseSetTest, EraseNonExistentEntity) { + Entity entity = 42; + + // Try to erase an entity that doesn't exist + sparseSet.erase(entity); + + // Should still be empty + EXPECT_TRUE(sparseSet.empty()); + + // Insert and then erase + sparseSet.insert(entity); + sparseSet.erase(entity); + + // Try to erase again + sparseSet.erase(entity); + + // Should still be empty + EXPECT_TRUE(sparseSet.empty()); + } + + TEST_F(SparseSetTest, IteratorFunctionality) { + Entity entity1 = 42; + Entity entity2 = 100; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + + // Test begin/end iteration + std::vector entities; + for (auto it = sparseSet.begin(); it != sparseSet.end(); ++it) { + entities.push_back(*it); + } + + EXPECT_EQ(entities.size(), 2); + EXPECT_THAT(entities, ::testing::UnorderedElementsAre(entity1, entity2)); + + // Test range-based for loop + entities.clear(); + for (Entity e : sparseSet) { + entities.push_back(e); + } + + EXPECT_EQ(entities.size(), 2); + EXPECT_THAT(entities, ::testing::UnorderedElementsAre(entity1, entity2)); + } + + TEST_F(SparseSetTest, GetDenseArray) { + Entity entity1 = 42; + Entity entity2 = 100; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + + const auto& dense = sparseSet.getDense(); + EXPECT_EQ(dense.size(), 2); + EXPECT_THAT(dense, ::testing::UnorderedElementsAre(entity1, entity2)); + } + + TEST_F(SparseSetTest, LargeNumberOfEntities) { + // Insert a large number of entities + const size_t numEntities = 1000; + + for (Entity i = 0; i < numEntities; ++i) { + sparseSet.insert(i); + } + + EXPECT_EQ(sparseSet.getDense().size(), numEntities); + + // Check each entity is contained + for (Entity i = 0; i < numEntities; ++i) { + EXPECT_TRUE(sparseSet.contains(i)); + } + + // Remove all entities in reverse order + for (Entity i = numEntities - 1; i != static_cast(-1); --i) { + sparseSet.erase(i); + } + + EXPECT_TRUE(sparseSet.empty()); + } + + TEST_F(SparseSetTest, MixedOperations) { + // Perform a mix of insert and erase operations + sparseSet.insert(1); + sparseSet.insert(2); + sparseSet.insert(3); + sparseSet.erase(2); + sparseSet.insert(4); + sparseSet.erase(1); + sparseSet.insert(5); + sparseSet.erase(3); + sparseSet.insert(6); + + EXPECT_EQ(sparseSet.getDense().size(), 3); + EXPECT_TRUE(sparseSet.contains(4)); + EXPECT_TRUE(sparseSet.contains(5)); + EXPECT_TRUE(sparseSet.contains(6)); + EXPECT_FALSE(sparseSet.contains(1)); + EXPECT_FALSE(sparseSet.contains(2)); + EXPECT_FALSE(sparseSet.contains(3)); + } + + TEST_F(SparseSetTest, EmptyAfterEraseAll) { + // Insert and then erase all + sparseSet.insert(1); + sparseSet.insert(2); + sparseSet.insert(3); + + sparseSet.erase(1); + sparseSet.erase(2); + sparseSet.erase(3); + + EXPECT_TRUE(sparseSet.empty()); + EXPECT_EQ(sparseSet.begin(), sparseSet.end()); + } + + TEST_F(SparseSetTest, NonSequentialEntities) { + // Test with large, scattered entity IDs + Entity entity1 = 1000000; + Entity entity2 = 2000000; + Entity entity3 = 3000000; + + sparseSet.insert(entity1); + sparseSet.insert(entity2); + sparseSet.insert(entity3); + + EXPECT_TRUE(sparseSet.contains(entity1)); + EXPECT_TRUE(sparseSet.contains(entity2)); + EXPECT_TRUE(sparseSet.contains(entity3)); + + sparseSet.erase(entity2); + + EXPECT_TRUE(sparseSet.contains(entity1)); + EXPECT_FALSE(sparseSet.contains(entity2)); + EXPECT_TRUE(sparseSet.contains(entity3)); + } +} diff --git a/tests/ecs/System.test.cpp b/tests/ecs/System.test.cpp index c5bff2c9a..95278c899 100644 --- a/tests/ecs/System.test.cpp +++ b/tests/ecs/System.test.cpp @@ -1,4 +1,4 @@ -//// System.test.cpp ////////////////////////////////////////////////////////// +//// System.test.cpp /////////////////////////////////////////////////////////////// // // zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz // zzzzzzz zzz zzzz zzzz zzzz zzzz @@ -7,187 +7,215 @@ // zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz // // Author: Mehdy MORVAN -// Date: 26/11/2024 +// Date: 09/04/2025 // Description: Test file for the system manager // /////////////////////////////////////////////////////////////////////////////// - #include -#include -#include "ecs/System.hpp" -#include "ecs/Signature.hpp" +#include "System.hpp" +#include "Coordinator.hpp" namespace nexo::ecs { - // Mock system for testing - class MockSystem : public System { - public: - MOCK_METHOD(void, update, (), (const)); - }; - - class MockSystem2 : public System { - public: - MOCK_METHOD(void, update, (), (const)); - }; + class MockCoordinator : public Coordinator {}; - class SystemManagerTest : public ::testing::Test { - protected: - void SetUp() override { - systemManager = std::make_unique(); + // Mock systems for testing + class MockQuerySystem : public AQuerySystem { + public: + const Signature& getSignature() const override { + return signature; } - std::unique_ptr systemManager; + Signature signature; }; - TEST_F(SystemManagerTest, RegisterSystem) { - auto mockSystem = systemManager->registerSystem(); - - EXPECT_NE(mockSystem, nullptr); - EXPECT_TRUE(mockSystem->entities.empty()); - } + class MockGroupSystem : public AGroupSystem {}; - TEST_F(SystemManagerTest, RegisterSystemTwice) { - systemManager->registerSystem(); + // Invalid system that doesn't inherit correctly + class InvalidSystem {}; - // Attempting to register the same system twice - EXPECT_NO_THROW(systemManager->registerSystem()); - } - - TEST_F(SystemManagerTest, SetSignatureForSystem) { - auto mockSystem = systemManager->registerSystem(); - - Signature signature; - signature.set(1); // Set the 1st bit + class SystemTest : public ::testing::Test { + protected: + void SetUp() override { + // Initialize the static coordinator + System::coord = std::make_shared(); + } - EXPECT_NO_THROW(systemManager->setSignature(signature)); - } + void TearDown() override { + // Clean up + System::coord.reset(); + } - TEST_F(SystemManagerTest, SetSignatureForUnregisteredSystem) { - Signature signature; - signature.set(1); + SystemManager systemManager; + }; - EXPECT_THROW(systemManager->setSignature(signature), SystemNotRegistered); + // System Base Class Tests + TEST_F(SystemTest, CoordinatorInitialization) { + ASSERT_NE(System::coord, nullptr); } - TEST_F(SystemManagerTest, EntityDestroyed) { - auto mockSystem = systemManager->registerSystem(); - - Signature signature; - signature.set(1); // Set the 1st bit - systemManager->setSignature(signature); - - // Simulate an entity with the matching signature - Entity entity = 1; - mockSystem->entities.insert(entity); + // AQuerySystem Tests + TEST_F(SystemTest, QuerySystemSignature) { + auto mockSystem = std::make_shared(); - systemManager->entityDestroyed(entity); + // Test initial signature + Signature emptySignature; + EXPECT_EQ(mockSystem->getSignature(), emptySignature); - EXPECT_TRUE(mockSystem->entities.empty()); + // Test modified signature + Signature newSignature; + newSignature.set(1, true); // Set some bit + mockSystem->signature = newSignature; + EXPECT_EQ(mockSystem->getSignature(), newSignature); } - TEST_F(SystemManagerTest, EntitySignatureChanged_AddEntityToSystem) { - auto mockSystem = systemManager->registerSystem(); - - Signature systemSignature; - systemSignature.set(1); // Set the 1st bit - systemManager->setSignature(systemSignature); + TEST_F(SystemTest, QuerySystemEntities) { + auto mockSystem = std::make_shared(); - Entity entity = 1; - Signature entitySignature; - entitySignature.set(1); // Matches the system's signature + // Test initial state + EXPECT_TRUE(mockSystem->entities.empty()); - systemManager->entitySignatureChanged(entity, entitySignature); + // Test adding entities + Entity entity1 = 1; + mockSystem->entities.insert(entity1); + EXPECT_EQ(mockSystem->entities.size(), 1); + EXPECT_TRUE(mockSystem->entities.contains(entity1)); - EXPECT_TRUE(mockSystem->entities.contains(entity)); + // Test adding more entities + Entity entity2 = 2; + mockSystem->entities.insert(entity2); + EXPECT_EQ(mockSystem->entities.size(), 2); + EXPECT_TRUE(mockSystem->entities.contains(entity2)); + + // Test removing entities + mockSystem->entities.erase(entity1); + EXPECT_EQ(mockSystem->entities.size(), 1); + EXPECT_FALSE(mockSystem->entities.contains(entity1)); + EXPECT_TRUE(mockSystem->entities.contains(entity2)); } - TEST_F(SystemManagerTest, EntitySignatureChanged_RemoveEntityFromSystem) { - auto mockSystem = systemManager->registerSystem(); + // SystemManager Tests + TEST_F(SystemTest, RegisterQuerySystem) { + // Register a system + auto system = systemManager.registerQuerySystem(); + ASSERT_NE(system, nullptr); - Signature systemSignature; - systemSignature.set(1); // Set the 1st bit - systemManager->setSignature(systemSignature); - - Entity entity = 1; - Signature entitySignature; - entitySignature.set(1); // Matches the system's signature - - // Add the entity - systemManager->entitySignatureChanged(entity, entitySignature); - EXPECT_TRUE(mockSystem->entities.contains(entity)); - - // Change signature to no longer match - entitySignature.reset(1); // Clear the 1st bit - systemManager->entitySignatureChanged(entity, entitySignature); - - EXPECT_FALSE(mockSystem->entities.contains(entity)); + // Try to register the same system again + auto duplicateSystem = systemManager.registerQuerySystem(); + EXPECT_EQ(duplicateSystem, nullptr); } - TEST_F(SystemManagerTest, EntitySignatureChanged_IrrelevantEntity) { - auto mockSystem = systemManager->registerSystem(); + TEST_F(SystemTest, RegisterGroupSystem) { + // Register a system + auto system = systemManager.registerGroupSystem(); + ASSERT_NE(system, nullptr); - Signature systemSignature; - systemSignature.set(1); // Set the 1st bit - systemManager->setSignature(systemSignature); + // Try to register the same system again + auto duplicateSystem = systemManager.registerGroupSystem(); + EXPECT_EQ(duplicateSystem, nullptr); + } - Entity entity = 1; - Signature entitySignature; - entitySignature.set(2); // Does not match the system's signature + class SystemImplementationTest : public ::testing::Test { + protected: + void SetUp() override { + // Setup code + nexo::ecs::System::coord = std::make_shared(); - systemManager->entitySignatureChanged(entity, entitySignature); + // Register systems + querySystem = systemManager.registerQuerySystem(); + groupSystem = systemManager.registerGroupSystem(); - EXPECT_FALSE(mockSystem->entities.contains(entity)); - } + // Set signatures + querySignature.set(0, true); // System requires component 0 + querySystem->signature = querySignature; + systemManager.setSignature(querySignature); + } - TEST_F(SystemManagerTest, EntityDestroyedTwice) { - auto mockSystem = systemManager->registerSystem(); + void TearDown() override { + nexo::ecs::System::coord.reset(); + } - Signature signature; - signature.set(1); // Set the 1st bit - systemManager->setSignature(signature); + nexo::ecs::SystemManager systemManager; + std::shared_ptr querySystem; + std::shared_ptr groupSystem; + nexo::ecs::Signature querySignature; + }; - Entity entity = 1; - mockSystem->entities.insert(entity); + TEST_F(SystemImplementationTest, EntityDestroyedRemovesFromAllSystems) { + // Add entity to the system + nexo::ecs::Entity entity = 1; + querySystem->entities.insert(entity); - systemManager->entityDestroyed(entity); - EXPECT_TRUE(mockSystem->entities.empty()); + // Destroy entity + systemManager.entityDestroyed(entity, querySignature); - // Destroying the same entity again should not throw an error - EXPECT_NO_THROW(systemManager->entityDestroyed(entity)); + // Verify entity was removed + EXPECT_FALSE(querySystem->entities.contains(entity)); } - TEST_F(SystemManagerTest, AddAndRemoveEntitiesFromMultipleSystems) { - auto system1 = systemManager->registerSystem(); - auto system2 = systemManager->registerSystem(); + TEST_F(SystemImplementationTest, EntitySignatureChangedAddsToMatchingSystems) { + nexo::ecs::Entity entity = 1; + nexo::ecs::Signature oldSignature; // Empty + nexo::ecs::Signature newSignature; + newSignature.set(0, true); // Now matches querySystem - Signature signature1; - signature1.set(1); // Matches entities with component 1 - systemManager->setSignature(signature1); + // Initially not in system + ASSERT_FALSE(querySystem->entities.contains(entity)); - Signature signature2; - signature2.set(2); // Matches entities with component 2 - systemManager->setSignature(signature2); + // Change signature + systemManager.entitySignatureChanged(entity, oldSignature, newSignature); - // Add entities with different signatures - Entity entity1 = 1; - Entity entity2 = 2; + // Should be added to system + EXPECT_TRUE(querySystem->entities.contains(entity)); + } - Signature entitySignature1; - entitySignature1.set(1); // Matches system1 + TEST_F(SystemImplementationTest, EntitySignatureChangedRemovesFromNonMatchingSystems) { + nexo::ecs::Entity entity = 1; + nexo::ecs::Signature oldSignature; + oldSignature.set(0, true); // Initially matches querySystem + nexo::ecs::Signature newSignature; // Empty, no longer matches - Signature entitySignature2; - entitySignature2.set(2); // Matches system2 + // Add to system + querySystem->entities.insert(entity); - systemManager->entitySignatureChanged(entity1, entitySignature1); - systemManager->entitySignatureChanged(entity2, entitySignature2); + // Change signature + systemManager.entitySignatureChanged(entity, oldSignature, newSignature); - EXPECT_TRUE(system1->entities.contains(entity1)); - EXPECT_FALSE(system1->entities.contains(entity2)); - EXPECT_FALSE(system2->entities.contains(entity1)); - EXPECT_TRUE(system2->entities.contains(entity2)); + // Should be removed from system + EXPECT_FALSE(querySystem->entities.contains(entity)); + } - // Change signature of entity1 to match system2 - systemManager->entitySignatureChanged(entity1, entitySignature2); - EXPECT_FALSE(system1->entities.contains(entity1)); - EXPECT_TRUE(system2->entities.contains(entity1)); + TEST_F(SystemImplementationTest, EntitySignatureChangedHandlesMultipleSystems) { + // Register another system with different signature + class AnotherMockQuerySystem : public AQuerySystem { + public: + const Signature& getSignature() const override { + return signature; + } + + Signature signature; + }; + auto otherSystem = systemManager.registerQuerySystem(); + nexo::ecs::Signature otherSignature; + otherSignature.set(1, true); // This system requires component 1 + otherSystem->signature = otherSignature; + + nexo::ecs::Entity entity = 1; + nexo::ecs::Signature oldSignature; + oldSignature.set(0, true); // Matches only querySystem + + nexo::ecs::Signature newSignature; + newSignature.set(1, true); // Matches only otherSystem + + // Add to first system + querySystem->entities.insert(entity); + ASSERT_TRUE(querySystem->entities.contains(entity)); + ASSERT_FALSE(otherSystem->entities.contains(entity)); + + // Change signature + systemManager.entitySignatureChanged(entity, oldSignature, newSignature); + + // Should move from first to second system + EXPECT_FALSE(querySystem->entities.contains(entity)); + EXPECT_TRUE(otherSystem->entities.contains(entity)); } -} \ No newline at end of file +} diff --git a/tests/editor/CameraInspector.test b/tests/editor/CameraInspector.test new file mode 100644 index 000000000..e05caee47 --- /dev/null +++ b/tests/editor/CameraInspector.test @@ -0,0 +1,14 @@ +# Camera Inspector +- Should be able to modify transform component +- Should be able to modify camera component +- Should be able to add camera controller component +- Should be able to modify camera controller +- Should be able to add camera target component +- Should be able to modify camera target component +- Undo/Redo should work properly in this inspector +- An error should be showed if no name is provided +- Nothing should be added when cancelling +- Undo/Redo should reset when cancelling +- A new camera should be added when confirming +- Undo should delete the newly camera created +- Camera preview should display a preview of the scene from the new camera diff --git a/tests/editor/Console.test b/tests/editor/Console.test new file mode 100644 index 000000000..7929e59f7 --- /dev/null +++ b/tests/editor/Console.test @@ -0,0 +1,5 @@ +# Console Window +- Should see logs +- Should be able to filter the logs +- Should be able to write in it +- Log file should be generated when program closes diff --git a/tests/editor/Docking.test b/tests/editor/Docking.test new file mode 100644 index 000000000..a70587c62 --- /dev/null +++ b/tests/editor/Docking.test @@ -0,0 +1,5 @@ +# Docking +- When no config file, layout should be set by default +- When config file, layout should persist +- Should be able to move windows +- Should be able to resize windows diff --git a/tests/editor/EditorScene.test b/tests/editor/EditorScene.test new file mode 100644 index 000000000..059cc242b --- /dev/null +++ b/tests/editor/EditorScene.test @@ -0,0 +1,40 @@ +# Editor Scene Window +## Toolbar +- Should be able to click on any toolbar button +- Should be able to add a cube +- Should be able to edit editor camera +- Should be able to switch gizmo mode +- Should be able to switch between local/global coordinates +- Should be able to enable translation/rotate snap +- Should be able to edit snap settings by right clicking +- Should be able to enable/disable grid +- Should be able to edit grid setting by right clicking +## Shortcuts +- Should be able to use SHIFT + A to add new object +- Should be able to add a cube +- Should be able to add a directional light +- Should be able to add a point light +- Should be able to add a spot light +- Should be able to add a camera +- When adding camera, should open a camera inspector popup +- Should be able to unhide all with CTRL + H +- Should be able to select all with A +- Should be able to delete selected object with DELETE +- Should be able to delete multiple objects with DELETE +- Should be able to hide object with H +- Should be able to hide multiple objects with H +- Should be able to switch Gizmo mode with G/R/S/U +- Should be able to toggle snap with SHIFT + S +- Should be able to hide everything but selected object with SHIFT + H +- Should be able to hide everything but selected objects with multiple selection +## Selection +- Should be able to click on an entity to select it +- Selected entities should have an outline +- Should be able to use SHIFT + LEFT CLICK to select multiple entity +- Should be able to use CTRL + LEFT CLICK to select multiple entity +- Should be able to use CTRL + LEFT CLICK to unselect an entity +## Gizmo +- Should be able to move object (every different axis/plane) +- Should be able to rotate object (every axis/screen) +- Should be able to scale object (every axis) +- Should be able to perform all interactions above on multiple objects diff --git a/tests/editor/Global.test b/tests/editor/Global.test new file mode 100644 index 000000000..2f80ff805 --- /dev/null +++ b/tests/editor/Global.test @@ -0,0 +1,3 @@ +# Global +- Should be able to undo action (CTRL + Z) +- Should be able to redo ation (CTRL + SHIFT + Z) diff --git a/tests/editor/Inspector.test b/tests/editor/Inspector.test new file mode 100644 index 000000000..d4746bd1b --- /dev/null +++ b/tests/editor/Inspector.test @@ -0,0 +1,45 @@ +# Inspector Window +- Should be able to view selected entity components +- When multiple entity selected, only the first one should display +## Transform property +- Should be able to modify position values +- Should be able to manually write a position +- Should be able to modify rotation values +- Should be able to manually write a rotation +- Should be able to modify scale values +- Should be able to manually write a scale +## Render Component +- Should be able to hide a an entity +- Material preview should display +- Create new material should open a popup +- Modify material should open the material inspector on its last docked position +## Camera component +- Viewport should not be modifiable if locked +- If viewport unlocked, should be able to modify values +- If viewport unlocked, should be able to write values +- Should be able to modify FOV +- Should be able to modify near plane +- Should be able to modify far plane +- Should be able to modify clear color +## Camera controller +- Should be able to modify mouse sensitivity +## Camera target +- Should be able to modify mouse sensitivity +- Should be able to modify distance from targeted entity +- Should be able to modify targeted entity +## Ambient light +- Should be able to modify color +## Directional light +- Should be able to modify color +- Should be able to modify direction +## Point light +- Should be able to modify color +- Should be able to modify position +- Should be able to modify distance (intensity) +## Spot light +- Should be able to modify color +- Should be able to modify position +- Should be able to modify direction +- Should be able to modify distance (intensity) +- Should be able to modify inner cut off +- Should be able to modify outer cut off diff --git a/tests/editor/MaterialInspector.test b/tests/editor/MaterialInspector.test new file mode 100644 index 000000000..9d7213ea6 --- /dev/null +++ b/tests/editor/MaterialInspector.test @@ -0,0 +1,12 @@ +# Material Inspector +## Modify Material +- Should be able to modify albedo color +- Should be able to modify specular color +- Should be able to set albedo texture +- Should be able to set specular texture +## New Material +- Should be able to modify albedo color +- Should be able to modify specular color +- Should be able to set albedo texture +- Should be able to set specular texture +- Material preview should display a preview of the material with a camera target diff --git a/tests/editor/SceneTree.test b/tests/editor/SceneTree.test new file mode 100644 index 000000000..05cf3b754 --- /dev/null +++ b/tests/editor/SceneTree.test @@ -0,0 +1,40 @@ +# Scene Tree Window +- Should be able to expand tree nodes +- Should be able to create new scene when right clicking outside a scene +## Scene Creation +- Should open a popup when creating a new scene +- New scene should get docked to existing scene if one exists +- New scene should get docked to last position of a scene if none exists +## Scene context +- Should be able to add a cube +- Should be able to add a directional light +- Should be able to add a point light +- Should be able to add a spot light +- Should be able to add a camera +- When adding camera, should open a camera inspector popup +- Should be able to view ambient light +- Should be able to view directional light +- Should be able to view point light +- Should be able to view spot light +- Should be able to view camera +- Should be able to view camera preview when hovering +- Should be able to rename entities +- Should be able to delete entity +- Should be able to delete multiple entity +## Selection +- Should be able to select multiple entities with CTRL + LEFT CLICK +- Should be able to deselect entity with CTRL + LEFT CLICK +- Should be able to select multiple entities with SHIFT + LEFT CLICK +## Shortcuts +- Should be able to expand all with down arrow +- Should be able to collapse all with up arrow +- Should be able to select all with CTRL + A +- Should be able to create scene with CTRL + N +- Should be able to duplicate entity with CTRL + D +- Should be able to duplicate multiple entities with CTRL + D +- Should be able to add entity with A +- Should be able to hide entity with H +- Should be able to hide multiple entities with H +- Should be able to unhide all with CTRL + H +- Should be able to delete entity with DELETE +- Should be able to delete multiple entities with DELETE diff --git a/tests/engine/CMakeLists.txt b/tests/engine/CMakeLists.txt index 5b9bac150..d94e01f9b 100644 --- a/tests/engine/CMakeLists.txt +++ b/tests/engine/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(engine_tests ${BASEDIR}/assets/AssetRef.test.cpp ${BASEDIR}/assets/AssetImporterContext.test.cpp ${BASEDIR}/assets/AssetImporter.test.cpp + ${BASEDIR}/assets/Assets/Model/ModelImporter.test.cpp # Add other engine test files here ) diff --git a/tests/engine/assets/AssetCatalog.test.cpp b/tests/engine/assets/AssetCatalog.test.cpp index 51df76063..e0a6816fb 100644 --- a/tests/engine/assets/AssetCatalog.test.cpp +++ b/tests/engine/assets/AssetCatalog.test.cpp @@ -48,8 +48,8 @@ namespace nexo::assets { TEST_F(AssetCatalogTest, RegisterAndRetrieveAssetById) { // Register an asset const AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = assetCatalog.registerAsset(location, textureAsset); + auto textureAsset = std::make_unique(); + const auto ref = assetCatalog.registerAsset(location, std::move(textureAsset)); ASSERT_TRUE(ref.isValid()); const auto id = ref.lock()->getID(); @@ -64,8 +64,8 @@ namespace nexo::assets { TEST_F(AssetCatalogTest, RegisterAndRetrieveAssetByLocation) { // Register an asset const AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = assetCatalog.registerAsset(location, textureAsset); + auto textureAsset = std::make_unique(); + const auto ref = assetCatalog.registerAsset(location, std::move(textureAsset)); ASSERT_TRUE(ref.isValid()); ASSERT_TRUE(ref); @@ -79,9 +79,9 @@ namespace nexo::assets { TEST_F(AssetCatalogTest, DeleteAssetById) { AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = assetCatalog.registerAsset(location, textureAsset); - const auto id = ref.lock()->getID(); + auto textureAsset = std::make_unique(); + const auto ref = assetCatalog.registerAsset(location, std::move(textureAsset)); + const auto id = ref.lock()->getID(); // Delete by ID assetCatalog.deleteAsset(id); @@ -99,8 +99,8 @@ namespace nexo::assets { TEST_F(AssetCatalogTest, DeleteAssetByReference) { AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = assetCatalog.registerAsset(location, textureAsset); + auto textureAsset = std::make_unique(); + const auto ref = assetCatalog.registerAsset(location, std::move(textureAsset)); // Delete by reference assetCatalog.deleteAsset(ref); @@ -117,12 +117,12 @@ namespace nexo::assets { } TEST_F(AssetCatalogTest, GetAssetsReturnsAllAssets) { - const auto textureAsset = new Texture(); - const auto modelAsset = new Model(); + auto textureAsset = std::make_unique(); + auto modelAsset = std::make_unique(); // Register multiple assets - assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset); - assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset); + assetCatalog.registerAsset(AssetLocation("text@test/texture"), std::move(textureAsset)); + assetCatalog.registerAsset(AssetLocation("model@test/model"), std::move(modelAsset)); // Get all assets auto assets = assetCatalog.getAssets(); @@ -137,12 +137,12 @@ namespace nexo::assets { } TEST_F(AssetCatalogTest, GetAssetsReturnsAllAssetsViews) { - const auto textureAsset = new Texture(); - const auto modelAsset = new Model(); + auto textureAsset = std::make_unique(); + auto modelAsset = std::make_unique(); // Register multiple assets - const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset); - const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset); + const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), std::move(textureAsset)); + const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), std::move(modelAsset)); // Get all assets as a view auto assetsView = assetCatalog.getAssetsView(); @@ -165,13 +165,13 @@ namespace nexo::assets { } TEST_F(AssetCatalogTest, MultipleAssetsDeleteOne) { - const auto textureAsset = new Texture(); - const auto modelAsset = new Model(); + auto textureAsset = std::make_unique(); + auto modelAsset = std::make_unique(); // Register multiple assets - const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset); - const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset); - const auto modelId = modelRef.lock()->getID(); + const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), std::move(textureAsset)); + const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), std::move(modelAsset)); + const auto modelId = modelRef.lock()->getID(); // Get all assets auto assets = assetCatalog.getAssets(); @@ -303,8 +303,8 @@ namespace nexo::assets { auto& instance = AssetCatalog::getInstance(); const AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = instance.registerAsset(location, textureAsset); + auto textureAsset = std::make_unique(); + const auto ref = instance.registerAsset(location, std::move(textureAsset)); ASSERT_TRUE(ref.isValid()); const auto id = ref.lock()->getID(); @@ -326,8 +326,8 @@ namespace nexo::assets { auto& instance = AssetCatalog::getInstance(); const AssetLocation location("text@test/texture"); - const auto textureAsset = new Texture(); - const auto ref = instance.registerAsset(location, textureAsset); + auto textureAsset = std::make_unique(); + const auto ref = instance.registerAsset(location, std::move(textureAsset)); ASSERT_TRUE(ref.isValid()); const auto id = ref.lock()->getID(); diff --git a/tests/engine/assets/AssetImporter.test.cpp b/tests/engine/assets/AssetImporter.test.cpp index 41b3fa0c3..7c79bb684 100644 --- a/tests/engine/assets/AssetImporter.test.cpp +++ b/tests/engine/assets/AssetImporter.test.cpp @@ -87,7 +87,7 @@ namespace nexo::assets { EXPECT_CALL(*mockImporter, canRead(testing::_)).Times(0); // Never called EXPECT_CALL(*mockImporter, importImpl(testing::_)) .WillOnce(Invoke([&](AssetImporterContext& ctx) { - ctx.setMainAsset(expectedAsset); + ctx.setMainAsset(std::unique_ptr(expectedAsset)); })); importer.registerImporter(mockImporter, 100); @@ -114,7 +114,7 @@ namespace nexo::assets { EXPECT_CALL(*mockImporter, importImpl(testing::_)) .After(canReadCall) .WillOnce(Invoke([&](AssetImporterContext& ctx) { - ctx.setMainAsset(expectedAsset); + ctx.setMainAsset(std::unique_ptr(expectedAsset)); })); // Register the mock importer @@ -161,7 +161,7 @@ namespace nexo::assets { EXPECT_CALL(*bestImporter, importImpl(testing::_)) .After(canReadCall) .WillOnce(Invoke([&](AssetImporterContext& ctx) { - ctx.setMainAsset(expectedAsset); + ctx.setMainAsset(std::unique_ptr(expectedAsset)); })); EXPECT_CALL(*wrongImporter, canRead(testing::_)).Times(0); @@ -253,7 +253,7 @@ namespace nexo::assets { EXPECT_CALL(*validModelImporter, importImpl(testing::_)) .After(validCanReadCall) .WillOnce(Invoke([](AssetImporterContext& ctx) { - ctx.setMainAsset(new Model()); + ctx.setMainAsset(std::make_unique()); })); EXPECT_CALL(*cannotReadModelImporter, importImpl(testing::_)).Times(0); @@ -298,7 +298,7 @@ namespace nexo::assets { EXPECT_CALL(*bestImporter, importImpl(testing::_)) .After(wrongCanReadCall) .WillOnce(Invoke([&](AssetImporterContext& ctx) { - ctx.setMainAsset(expectedAsset); + ctx.setMainAsset(std::unique_ptr(expectedAsset)); })); EXPECT_CALL(*wrongImporter, importImpl(testing::_)).Times(0); diff --git a/tests/engine/assets/AssetImporterContext.test.cpp b/tests/engine/assets/AssetImporterContext.test.cpp index da2d80111..36b3cc186 100644 --- a/tests/engine/assets/AssetImporterContext.test.cpp +++ b/tests/engine/assets/AssetImporterContext.test.cpp @@ -64,9 +64,10 @@ namespace nexo::assets { TEST_F(AssetImporterContextTest, SetAndGetMainAsset) { - Texture asset; - context.setMainAsset(&asset); - EXPECT_EQ(context.getMainAsset(), &asset); + auto asset = std::make_unique(); + Texture* texturePtr = asset.get(); + context.setMainAsset(std::move(asset)); + EXPECT_EQ(context.getMainAsset().get(), texturePtr); } TEST_F(AssetImporterContextTest, GetDependenciesEmptyOnCreation) @@ -78,8 +79,8 @@ namespace nexo::assets { { // Register an asset first to get a valid reference auto& catalog = AssetCatalog::getInstance(); - auto* asset = new Texture(); - const auto ref = catalog.registerAsset(AssetLocation("test@texture/dependency"), asset); + auto asset = std::make_unique(); + const auto ref = catalog.registerAsset(AssetLocation("test@texture/dependency"), std::move(asset)); EXPECT_TRUE(ref); // Add as dependency @@ -96,10 +97,10 @@ namespace nexo::assets { auto& catalog = AssetCatalog::getInstance(); // Create and register multiple assets - auto* texture = new Texture(); - auto* model = new Model(); - const auto textureRef = catalog.registerAsset(AssetLocation("text@path"), texture); - const auto modelRef = catalog.registerAsset(AssetLocation("model@path"), model); + auto* texture = new Texture(); + auto* model = new Model(); + const auto textureRef = catalog.registerAsset(AssetLocation("text@path"), std::unique_ptr(texture)); + const auto modelRef = catalog.registerAsset(AssetLocation("model@path"), std::unique_ptr(model)); EXPECT_TRUE(textureRef); EXPECT_TRUE(modelRef); @@ -173,7 +174,7 @@ namespace nexo::assets { // Register an asset with that name auto& catalog = AssetCatalog::getInstance(); auto* asset = new Texture(); - EXPECT_TRUE(catalog.registerAsset(depName1, asset)); + EXPECT_TRUE(catalog.registerAsset(depName1, std::unique_ptr(asset))); // Generate another name - should be different const auto depName2 = context.genUniqueDependencyLocation(); @@ -193,13 +194,13 @@ namespace nexo::assets { // Register an asset with that name auto& catalog = AssetCatalog::getInstance(); - auto* asset = new Model(); - EXPECT_TRUE(catalog.registerAsset(depName1, asset)); + auto asset = std::make_unique(); + EXPECT_TRUE(catalog.registerAsset(depName1, std::move(asset))); EXPECT_EQ(depName1.getFullLocation(), "test_MODEL1@folder/main"); // Let's register an asset with the same name as a future dependency - auto* asset2 = new Model(); - EXPECT_TRUE(catalog.registerAsset(AssetLocation("test_MODEL2@folder/main"), asset2)); + auto asset2 = std::make_unique(); + EXPECT_TRUE(catalog.registerAsset(AssetLocation("test_MODEL2@folder/main"), std::move(asset2))); // Generate another dep name: should prevent collision const auto depName2 = context.genUniqueDependencyLocation(); diff --git a/tests/engine/assets/Assets/Model/ModelImporter.test.cpp b/tests/engine/assets/Assets/Model/ModelImporter.test.cpp new file mode 100644 index 000000000..67ee0ca33 --- /dev/null +++ b/tests/engine/assets/Assets/Model/ModelImporter.test.cpp @@ -0,0 +1,238 @@ +//// ModelImporter.test.cpp ///////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Guillaume HEIN +// Date: 24/04/2025 +// Description: Test file for the ModelImporter class +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "assets/Assets/Model/ModelImporter.hpp" +#include "assets/AssetImporterContext.hpp" +#include "assets/AssetRef.hpp" +#include "assets/Assets/Model/Model.hpp" +#include "assets/Assets/Model/ModelParameters.hpp" + +#include +#include +#include +#include +#include + +#include "Path.hpp" +#include "../tests/renderer/contexts/opengl.hpp" + +using namespace testing; +using namespace nexo::assets; + +class TestModelImporter : public ModelImporter { + FRIEND_TEST(ModelImporterTestFixture, CanReadSupportsValidExtensions); + FRIEND_TEST(ModelImporterTestFixture, ConvertAssimpMatrixToGLM); + FRIEND_TEST(ModelImporterTestFixture, ConvertAssimpHintToNxTextureFormat); + FRIEND_TEST(ModelImporterTestFixture, ImportImplSetsMainAsset); + FRIEND_TEST(ModelImporterTestFixture, ProcessMeshHandlesEmptyMesh); + +}; + +// Test fixture for ModelImporter tests +class ModelImporterTestFixture : public nexo::renderer::OpenGLTest { +protected: + void SetUp() override { + OpenGLTest::SetUp(); + // Clean up the catalog before each test + auto& catalog = AssetCatalog::getInstance(); + for (auto& asset : catalog.getAssets()) { + catalog.deleteAsset(asset); + } + } + + void TearDown() override + { + // Clean up the catalog after each test + auto& catalog = AssetCatalog::getInstance(); + for (auto& asset : catalog.getAssets()) { + catalog.deleteAsset(asset); + } + OpenGLTest::TearDown(); + } + + TestModelImporter importer; +}; + +// Test canRead method +TEST_F(ModelImporterTestFixture, CanReadSupportsValidExtensions) { + // Test with valid extension + ImporterFileInput fileInput{std::filesystem::path("model.fbx")}; + ImporterInputVariant inputVariant = fileInput; + EXPECT_TRUE(importer.canRead(inputVariant)); + + // Test with another valid extension + fileInput = ImporterFileInput{std::filesystem::path("model.obj")}; + inputVariant = fileInput; + EXPECT_TRUE(importer.canRead(inputVariant)); + + // Test with invalid extension + fileInput = ImporterFileInput{std::filesystem::path("model.invalid")}; + inputVariant = fileInput; + EXPECT_FALSE(importer.canRead(inputVariant)); +} + +// Test convertAssimpMatrixToGLM method +TEST_F(ModelImporterTestFixture, ConvertAssimpMatrixToGLM) { + // Create a test aiMatrix4x4 + aiMatrix4x4 aiMat; + aiMat.a1 = 1.0f; aiMat.a2 = 2.0f; aiMat.a3 = 3.0f; aiMat.a4 = 4.0f; + aiMat.b1 = 5.0f; aiMat.b2 = 6.0f; aiMat.b3 = 7.0f; aiMat.b4 = 8.0f; + aiMat.c1 = 9.0f; aiMat.c2 = 10.0f; aiMat.c3 = 11.0f; aiMat.c4 = 12.0f; + aiMat.d1 = 13.0f; aiMat.d2 = 14.0f; aiMat.d3 = 15.0f; aiMat.d4 = 16.0f; + + // Use the static method to convert the matrix + glm::mat4 glmMat = TestModelImporter::convertAssimpMatrixToGLM(aiMat); + + // Check if the conversion is correct + EXPECT_FLOAT_EQ(glmMat[0][0], 1.0f); + EXPECT_FLOAT_EQ(glmMat[0][1], 5.0f); + EXPECT_FLOAT_EQ(glmMat[0][2], 9.0f); + EXPECT_FLOAT_EQ(glmMat[0][3], 13.0f); + + EXPECT_FLOAT_EQ(glmMat[1][0], 2.0f); + EXPECT_FLOAT_EQ(glmMat[1][1], 6.0f); + EXPECT_FLOAT_EQ(glmMat[1][2], 10.0f); + EXPECT_FLOAT_EQ(glmMat[1][3], 14.0f); + + EXPECT_FLOAT_EQ(glmMat[2][0], 3.0f); + EXPECT_FLOAT_EQ(glmMat[2][1], 7.0f); + EXPECT_FLOAT_EQ(glmMat[2][2], 11.0f); + EXPECT_FLOAT_EQ(glmMat[2][3], 15.0f); + + EXPECT_FLOAT_EQ(glmMat[3][0], 4.0f); + EXPECT_FLOAT_EQ(glmMat[3][1], 8.0f); + EXPECT_FLOAT_EQ(glmMat[3][2], 12.0f); + EXPECT_FLOAT_EQ(glmMat[3][3], 16.0f); +} + +// Test convertAssimpHintToNxTextureFormat method +TEST_F(ModelImporterTestFixture, ConvertAssimpHintToNxTextureFormat) { + // Test various format hints + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba8888"), nexo::renderer::NxTextureFormat::RGBA8); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba8880"), nexo::renderer::NxTextureFormat::RGB8); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba8800"), nexo::renderer::NxTextureFormat::RG8); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba8000"), nexo::renderer::NxTextureFormat::R8); + + // Tests invalid format because of length + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba88888"), nexo::renderer::NxTextureFormat::INVALID); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba888"), nexo::renderer::NxTextureFormat::INVALID); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba88"), nexo::renderer::NxTextureFormat::INVALID); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat(""), nexo::renderer::NxTextureFormat::INVALID); + + // Test invalid format hints + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("invalid0"), nexo::renderer::NxTextureFormat::INVALID); + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat("rgba7777"), nexo::renderer::NxTextureFormat::INVALID); + + // Test all permutations of rgba + std::string hint = "abgr0000"; + do { + hint.replace(hint.begin() + 4, hint.begin() + 8, "0000"); + + const auto rPos = std::ranges::find(hint, 'r'); + const auto gPos = std::ranges::find(hint, 'g'); + const auto bPos = std::ranges::find(hint, 'b'); + const auto aPos = std::ranges::find(hint, 'a'); + + hint[rPos - hint.begin() + 4] = '8'; + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat(hint.c_str()), nexo::renderer::NxTextureFormat::R8); + + if (rPos >= gPos) + continue; + hint[gPos - hint.begin() + 4] = '8'; + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat(hint.c_str()), nexo::renderer::NxTextureFormat::RG8); + + if (gPos >= bPos) + continue; + hint[bPos - hint.begin() + 4] = '8'; + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat(hint.c_str()), nexo::renderer::NxTextureFormat::RGB8); + + if (bPos >= aPos) + continue; + hint[aPos - hint.begin() + 4] = '8'; + EXPECT_EQ(importer.convertAssimpHintToNxTextureFormat(hint.c_str()), nexo::renderer::NxTextureFormat::RGBA8); + GTEST_LOG_(INFO) << "HINT: " << hint; + } while (std::next_permutation(hint.begin(), hint.begin() + 4)); +} + +// Additional test for processing empty mesh +TEST_F(ModelImporterTestFixture, ImportCubeModel) { + auto& catalog = AssetCatalog::getInstance(); + + AssetImporterContext ctx = {}; + ctx.location = AssetLocation("test::cube_model@test_folder"); + ctx.input = ImporterFileInput{std::filesystem::path(nexo::Path::resolvePathRelativeToExe("../tests/engine/assets/Assets/Model/cube.obj"))}; + importer.import(ctx); + + EXPECT_NE(ctx.getMainAsset(), nullptr); + EXPECT_EQ(ctx.getMainAsset()->getType(), Model::TYPE); + EXPECT_EQ(ctx.getMainAsset()->isLoaded(), true); + + const auto allAssets = catalog.getAssets(); + EXPECT_EQ(allAssets.size(), 3); // 2 materials + 1 texture + + auto model = static_cast(ctx.getMainAsset().get()); + // Now verify the model data + const auto& modelData = model->getData(); + ASSERT_NE(modelData, nullptr); + + const auto root = modelData->root; + + // Root mesh in this case has zero mesh but 1 child + ASSERT_EQ(modelData->root->meshes.size(), 0); + ASSERT_EQ(root->children.size(), 1); + const auto child = root->children[0]; + EXPECT_EQ(child->children.size(), 0); + EXPECT_EQ(child->meshes.size(), 1); + const auto childMesh = child->meshes[0]; + + // Get the mesh and verify its properties + EXPECT_EQ(childMesh.name, "Cube"); + + // A cube should have 8 vertices and 12 triangles (36 indices) + EXPECT_EQ(childMesh.vertices.size(), 24); // 24 because each vertex is duplicated for different face normals/UVs + EXPECT_EQ(childMesh.indices.size(), 36); // 6 faces × 2 triangles × 3 vertices + + // Check the Material reference + const auto material = childMesh.material.lock(); + EXPECT_NE(material, nullptr); + + const auto materialData = material->getData().get(); + EXPECT_NE(materialData, nullptr); + + EXPECT_NE(materialData->albedoTexture, nullptr); + const auto albedoTextureAsset = materialData->albedoTexture.lock(); + EXPECT_NE(albedoTextureAsset, nullptr); + EXPECT_EQ(albedoTextureAsset->getType(), Texture::TYPE); + + const auto albedoTexture = albedoTextureAsset->getData().get(); + EXPECT_NE(albedoTexture->texture, nullptr); + EXPECT_EQ(albedoTexture->texture->getWidth(), 64); + EXPECT_EQ(albedoTexture->texture->getHeight(), 64); + + // Check material properties from the MTL file + EXPECT_FLOAT_EQ(materialData->specularColor.r, 0.5f); + EXPECT_FLOAT_EQ(materialData->specularColor.g, 0.5f); + EXPECT_FLOAT_EQ(materialData->specularColor.b, 0.5f); + + EXPECT_FLOAT_EQ(materialData->emissiveColor.r, 0.0f); + EXPECT_FLOAT_EQ(materialData->emissiveColor.g, 0.0f); + EXPECT_FLOAT_EQ(materialData->emissiveColor.b, 0.0f); + + // Check roughness and metallic properties if supported + EXPECT_FLOAT_EQ(materialData->roughness, 0.5f); // Pr in MTL + EXPECT_FLOAT_EQ(materialData->metallic, 0.7f); // Pm in MTL +} diff --git a/tests/engine/assets/Assets/Model/checkerboard.png b/tests/engine/assets/Assets/Model/checkerboard.png new file mode 100644 index 000000000..a384354d4 Binary files /dev/null and b/tests/engine/assets/Assets/Model/checkerboard.png differ diff --git a/tests/engine/assets/Assets/Model/cube.mtl b/tests/engine/assets/Assets/Model/cube.mtl new file mode 100644 index 000000000..160ccf845 --- /dev/null +++ b/tests/engine/assets/Assets/Model/cube.mtl @@ -0,0 +1,15 @@ +# Blender 4.4.1 MTL File: 'cube.blend' +# www.blender.org + +newmtl Material +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.500000 +d 1.000000 +illum 3 +Pr 0.500000 +Pm 0.700000 +Ps 0.000000 +Pc 0.000000 +Pcr 0.030000 +map_Kd checkerboard.png diff --git a/tests/engine/assets/Assets/Model/cube.obj b/tests/engine/assets/Assets/Model/cube.obj new file mode 100644 index 000000000..19a5f3663 --- /dev/null +++ b/tests/engine/assets/Assets/Model/cube.obj @@ -0,0 +1,40 @@ +# Blender 4.4.1 +# www.blender.org +mtllib cube.mtl +o Cube +v 1.000000 1.000000 -1.000000 +v 1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +v -1.000000 -1.000000 -1.000000 +v -1.000000 1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.875000 0.500000 +vt 0.875000 0.750000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.625000 1.000000 +vt 0.375000 1.000000 +vt 0.375000 0.000000 +vt 0.625000 0.000000 +vt 0.625000 0.250000 +vt 0.375000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.500000 +vt 0.125000 0.750000 +s 0 +usemtl Material +f 1/1/1 5/2/1 7/3/1 3/4/1 +f 4/5/2 3/4/2 7/6/2 8/7/2 +f 8/8/3 7/9/3 5/10/3 6/11/3 +f 6/12/4 2/13/4 4/5/4 8/14/4 +f 2/13/5 1/1/5 3/4/5 4/5/5 +f 6/11/6 5/10/6 1/1/6 2/13/6 diff --git a/tests/engine/components/Camera.test.cpp b/tests/engine/components/Camera.test.cpp index b95f5af30..786a12888 100644 --- a/tests/engine/components/Camera.test.cpp +++ b/tests/engine/components/Camera.test.cpp @@ -22,22 +22,23 @@ #include // Dummy implementation of the Framebuffer interface for testing. -class DummyFramebuffer : public nexo::renderer::Framebuffer { +class DummyFramebuffer : public nexo::renderer::NxFramebuffer { public: void bind() override {} void unbind() override {} - void setClearColor(const glm::vec4 &color) override {} + void setClearColor(const glm::vec4 &) override {} unsigned int getFramebufferId() const override { return 0; } - void resize(unsigned int width, unsigned int height) override {} - void getPixelWrapper(unsigned int attachmentIndex, int x, int y, void *result, const std::type_info &ti) const override {} - void clearAttachmentWrapper(unsigned int attachmentIndex, const void *value, const std::type_info &ti) const override {} - [[nodiscard]] nexo::renderer::FramebufferSpecs &getSpecs() override { static nexo::renderer::FramebufferSpecs specs; return specs; } - [[nodiscard]] const nexo::renderer::FramebufferSpecs &getSpecs() const override { static nexo::renderer::FramebufferSpecs specs; return specs; } - [[nodiscard]] unsigned int getColorAttachmentId(unsigned int index = 0) const override { return 0; } + glm::vec2 getSize() const override { return glm::vec2(0.0f); } + void resize(unsigned int, unsigned int ) override {} + void getPixelWrapper(unsigned int, int, int, void *, const std::type_info &) const override {} + void clearAttachmentWrapper(unsigned int, const void *, const std::type_info &) const override {} + [[nodiscard]] nexo::renderer::NxFramebufferSpecs &getSpecs() override { static nexo::renderer::NxFramebufferSpecs specs; return specs; } + [[nodiscard]] const nexo::renderer::NxFramebufferSpecs &getSpecs() const override { static nexo::renderer::NxFramebufferSpecs specs; return specs; } + [[nodiscard]] unsigned int getColorAttachmentId(unsigned int) const override { return 0; } unsigned int getDepthAttachmentId() const override { return 0; } }; -std::shared_ptr createDummyFramebuffer() { +std::shared_ptr createDummyFramebuffer() { return std::make_shared(); } diff --git a/tests/renderer/Buffer.test.cpp b/tests/renderer/Buffer.test.cpp index 328a2f97c..33516cb77 100644 --- a/tests/renderer/Buffer.test.cpp +++ b/tests/renderer/Buffer.test.cpp @@ -24,17 +24,17 @@ namespace nexo::renderer { // Mock for testing abstract classes - class MockVertexBuffer : public VertexBuffer { + class MockVertexBuffer : public NxVertexBuffer { public: MOCK_METHOD(void, bind, (), (const, override)); MOCK_METHOD(void, unbind, (), (const, override)); - MOCK_METHOD(void, setLayout, (const BufferLayout&), (override)); - MOCK_METHOD(BufferLayout, getLayout, (), (const, override)); + MOCK_METHOD(void, setLayout, (const NxBufferLayout&), (override)); + MOCK_METHOD(NxBufferLayout, getLayout, (), (const, override)); MOCK_METHOD(void, setData, (void*, unsigned int), (override)); MOCK_METHOD(unsigned int, getId, (), (const, override)); }; - class MockIndexBuffer : public IndexBuffer { + class MockIndexBuffer : public NxIndexBuffer { public: MOCK_METHOD(void, bind, (), (const, override)); MOCK_METHOD(void, unbind, (), (const, override)); @@ -135,52 +135,52 @@ namespace nexo::renderer { // Tests for ShaderDataType operations TEST(ShaderDataTypeTest, ShaderDataTypeSizeReturnsCorrectSizes) { - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::FLOAT), 4); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::FLOAT2), 8); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::FLOAT3), 12); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::FLOAT4), 16); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::MAT3), 36); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::MAT4), 64); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::INT), 4); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::INT2), 8); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::INT3), 12); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::INT4), 16); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::BOOL), 1); - EXPECT_EQ(shaderDataTypeSize(ShaderDataType::NONE), 0); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::FLOAT), 4); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::FLOAT2), 8); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::FLOAT3), 12); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::FLOAT4), 16); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::MAT3), 36); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::MAT4), 64); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::INT), 4); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::INT2), 8); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::INT3), 12); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::INT4), 16); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::BOOL), 1); + EXPECT_EQ(shaderDataTypeSize(NxShaderDataType::NONE), 0); } // Tests for BufferElements TEST(BufferElementsTest, ConstructorSetsProperties) { - BufferElements element(ShaderDataType::FLOAT3, "Position", false); + NxBufferElements element(NxShaderDataType::FLOAT3, "Position", false); EXPECT_EQ(element.name, "Position"); - EXPECT_EQ(element.type, ShaderDataType::FLOAT3); + EXPECT_EQ(element.type, NxShaderDataType::FLOAT3); EXPECT_EQ(element.size, 12); // FLOAT3 = 3 * 4 bytes EXPECT_EQ(element.offset, 0); // Initial offset is 0 EXPECT_FALSE(element.normalized); } TEST(BufferElementsTest, GetComponentCountReturnsCorrectCount) { - EXPECT_EQ(BufferElements(ShaderDataType::FLOAT, "").getComponentCount(), 1); - EXPECT_EQ(BufferElements(ShaderDataType::FLOAT2, "").getComponentCount(), 2); - EXPECT_EQ(BufferElements(ShaderDataType::FLOAT3, "").getComponentCount(), 3); - EXPECT_EQ(BufferElements(ShaderDataType::FLOAT4, "").getComponentCount(), 4); - EXPECT_EQ(BufferElements(ShaderDataType::INT, "").getComponentCount(), 1); - EXPECT_EQ(BufferElements(ShaderDataType::INT2, "").getComponentCount(), 2); - EXPECT_EQ(BufferElements(ShaderDataType::INT3, "").getComponentCount(), 3); - EXPECT_EQ(BufferElements(ShaderDataType::INT4, "").getComponentCount(), 4); - EXPECT_EQ(BufferElements(ShaderDataType::MAT3, "").getComponentCount(), 9); - EXPECT_EQ(BufferElements(ShaderDataType::MAT4, "").getComponentCount(), 16); - EXPECT_EQ(BufferElements(ShaderDataType::BOOL, "").getComponentCount(), 1); - EXPECT_EQ(BufferElements(ShaderDataType::NONE, "").getComponentCount(), -1); + EXPECT_EQ(NxBufferElements(NxShaderDataType::FLOAT, "").getComponentCount(), 1); + EXPECT_EQ(NxBufferElements(NxShaderDataType::FLOAT2, "").getComponentCount(), 2); + EXPECT_EQ(NxBufferElements(NxShaderDataType::FLOAT3, "").getComponentCount(), 3); + EXPECT_EQ(NxBufferElements(NxShaderDataType::FLOAT4, "").getComponentCount(), 4); + EXPECT_EQ(NxBufferElements(NxShaderDataType::INT, "").getComponentCount(), 1); + EXPECT_EQ(NxBufferElements(NxShaderDataType::INT2, "").getComponentCount(), 2); + EXPECT_EQ(NxBufferElements(NxShaderDataType::INT3, "").getComponentCount(), 3); + EXPECT_EQ(NxBufferElements(NxShaderDataType::INT4, "").getComponentCount(), 4); + EXPECT_EQ(NxBufferElements(NxShaderDataType::MAT3, "").getComponentCount(), 9); + EXPECT_EQ(NxBufferElements(NxShaderDataType::MAT4, "").getComponentCount(), 16); + EXPECT_EQ(NxBufferElements(NxShaderDataType::BOOL, "").getComponentCount(), 1); + EXPECT_EQ(NxBufferElements(NxShaderDataType::NONE, "").getComponentCount(), -1); } // Tests for BufferLayout TEST(BufferLayoutTest, ConstructorWithInitializerListCalculatesOffsetsAndStride) { - BufferLayout layout = { - {ShaderDataType::FLOAT3, "Position"}, - {ShaderDataType::FLOAT4, "Color"}, - {ShaderDataType::FLOAT2, "TexCoord"} + NxBufferLayout layout = { + {NxShaderDataType::FLOAT3, "Position"}, + {NxShaderDataType::FLOAT4, "Color"}, + {NxShaderDataType::FLOAT2, "TexCoord"} }; EXPECT_EQ(layout.getStride(), 36); // 12 + 16 + 8 @@ -194,19 +194,19 @@ namespace nexo::renderer { } TEST(BufferLayoutTest, EmptyConstructorCreatesEmptyLayout) { - BufferLayout layout; + NxBufferLayout layout; EXPECT_EQ(layout.getStride(), 0); EXPECT_TRUE(layout.getElements().empty()); } TEST(BufferLayoutTest, IteratorFunctionsWork) { - BufferLayout layout = { - {ShaderDataType::FLOAT3, "Position"}, - {ShaderDataType::FLOAT4, "Color"} + NxBufferLayout layout = { + {NxShaderDataType::FLOAT3, "Position"}, + {NxShaderDataType::FLOAT4, "Color"} }; int count = 0; - for (auto& element : layout) { + for (auto& _ : layout) { count++; } EXPECT_EQ(count, 2); @@ -215,8 +215,8 @@ namespace nexo::renderer { // Mocking tests for abstract classes TEST(BufferMockTest, MockVertexBufferCanCallMethods) { MockVertexBuffer buffer; - BufferLayout layout = { - {ShaderDataType::FLOAT3, "Position"} + NxBufferLayout layout = { + {NxShaderDataType::FLOAT3, "Position"} }; EXPECT_CALL(buffer, setLayout(testing::_)).Times(1); @@ -227,7 +227,7 @@ namespace nexo::renderer { EXPECT_CALL(buffer, getId()).WillOnce(testing::Return(1)); buffer.setLayout(layout); - BufferLayout retrievedLayout = buffer.getLayout(); + NxBufferLayout retrievedLayout = buffer.getLayout(); buffer.bind(); buffer.unbind(); float data[] = {1.0f, 2.0f, 3.0f}; @@ -262,7 +262,7 @@ namespace nexo::renderer { TEST_F(BufferFactoryTest, CreateVertexBufferWithDataReturnsValidBuffer) { float vertices[] = {1.0f, 2.0f, 3.0f}; - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL auto buffer = createVertexBuffer(vertices, sizeof(vertices)); ASSERT_NE(buffer, nullptr); EXPECT_NE(buffer->getId(), 0); @@ -272,7 +272,7 @@ namespace nexo::renderer { } TEST_F(BufferFactoryTest, CreateVertexBufferWithSizeReturnsValidBuffer) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL auto buffer = createVertexBuffer(1024); ASSERT_NE(buffer, nullptr); EXPECT_NE(buffer->getId(), 0); @@ -282,7 +282,7 @@ namespace nexo::renderer { } TEST_F(BufferFactoryTest, CreateIndexBufferReturnsValidBuffer) { - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL auto buffer = createIndexBuffer(); ASSERT_NE(buffer, nullptr); EXPECT_NE(buffer->getId(), 0); @@ -292,11 +292,11 @@ namespace nexo::renderer { } // OpenGL specific implementation tests - using TEST_F with OpenGLBufferTest fixture - #ifdef GRAPHICS_API_OPENGL + #ifdef NX_GRAPHICS_API_OPENGL TEST_F(OpenGLBufferTest, OpenGlVertexBufferWithDataWorksCorrectly) { float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; - OpenGlVertexBuffer buffer(vertices, sizeof(vertices)); + NxOpenGlVertexBuffer buffer(vertices, sizeof(vertices)); EXPECT_NE(buffer.getId(), 0); @@ -305,11 +305,11 @@ namespace nexo::renderer { EXPECT_NO_THROW(buffer.unbind()); // Test layout setting and retrieval - BufferLayout layout = { - {ShaderDataType::FLOAT3, "Position"} + NxBufferLayout layout = { + {NxShaderDataType::FLOAT3, "Position"} }; EXPECT_NO_THROW(buffer.setLayout(layout)); - BufferLayout retrievedLayout = buffer.getLayout(); + NxBufferLayout retrievedLayout = buffer.getLayout(); EXPECT_EQ(retrievedLayout.getStride(), layout.getStride()); // Test setting new data @@ -319,7 +319,7 @@ namespace nexo::renderer { TEST_F(OpenGLBufferTest, OpenGlVertexBufferEmptyConstructorWorksCorrectly) { - OpenGlVertexBuffer buffer(1024); + NxOpenGlVertexBuffer buffer(1024); EXPECT_NE(buffer.getId(), 0); @@ -334,7 +334,7 @@ namespace nexo::renderer { TEST_F(OpenGLBufferTest, OpenGlIndexBufferWorksCorrectly) { - OpenGlIndexBuffer buffer; + NxOpenGlIndexBuffer buffer; EXPECT_NE(buffer.getId(), 0); @@ -347,5 +347,5 @@ namespace nexo::renderer { EXPECT_NO_THROW(buffer.setData(indices, 6)); EXPECT_EQ(buffer.getCount(), 6); } - #endif // GRAPHICS_API_OPENGL + #endif // NX_GRAPHICS_API_OPENGL } diff --git a/tests/renderer/CMakeLists.txt b/tests/renderer/CMakeLists.txt index 944de208b..507db26a9 100644 --- a/tests/renderer/CMakeLists.txt +++ b/tests/renderer/CMakeLists.txt @@ -31,6 +31,7 @@ set(RENDERER_SOURCES engine/src/renderer/Window.cpp engine/src/renderer/Buffer.cpp engine/src/renderer/Shader.cpp + engine/src/renderer/ShaderLibrary.cpp engine/src/renderer/VertexArray.cpp engine/src/renderer/RendererAPI.cpp engine/src/renderer/Renderer.cpp @@ -61,7 +62,6 @@ add_executable(renderer_tests ${BASEDIR}/Shader.test.cpp ${BASEDIR}/RendererAPI.test.cpp ${BASEDIR}/Texture.test.cpp - ${BASEDIR}/Renderer2D.test.cpp ${BASEDIR}/Renderer3D.test.cpp ${BASEDIR}/Exceptions.test.cpp ) @@ -74,7 +74,7 @@ find_package(Stb REQUIRED) target_include_directories(renderer_tests PRIVATE ${Stb_INCLUDE_DIR}) target_sources(renderer_tests PRIVATE ${CMAKE_SOURCE_DIR}/engine/external/stb_image.cpp) -target_compile_definitions(renderer_tests PRIVATE GRAPHICS_API_OPENGL) +target_compile_definitions(renderer_tests PRIVATE NX_GRAPHICS_API_OPENGL) find_package(OpenGL REQUIRED) find_package(glfw3 3.3 REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/tests/renderer/Exceptions.test.cpp b/tests/renderer/Exceptions.test.cpp index ffd9d4786..a0a8eeb75 100644 --- a/tests/renderer/Exceptions.test.cpp +++ b/tests/renderer/Exceptions.test.cpp @@ -21,7 +21,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; // Account for the next line - FileNotFoundException ex("test_file.txt"); + NxFileNotFoundException ex("test_file.txt"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("File not found: test_file.txt"), std::string::npos); @@ -33,7 +33,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - UnknownGraphicsApi ex("Vulkan"); + NxUnknownGraphicsApi ex("Vulkan"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("Unknown graphics API: Vulkan"), std::string::npos); @@ -45,7 +45,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - GraphicsApiInitFailure ex("OpenGL"); + NxGraphicsApiInitFailure ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("Failed to initialize graphics API: OpenGL"), std::string::npos); @@ -57,7 +57,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - ShaderCreationFailed ex("OpenGL", "Compilation error", "shader.glsl"); + NxShaderCreationFailed ex("OpenGL", "Compilation error", "shader.glsl"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Failed to create the shader (shader.glsl): Compilation error"), std::string::npos); @@ -69,7 +69,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferResizingFailed ex("Vulkan", false, 800, 600); + NxFramebufferResizingFailed ex("Vulkan", false, 800, 600); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[Vulkan] Framebuffer resizing failed: 800x600 is too small"), std::string::npos); @@ -81,7 +81,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - RendererNotInitialized ex(RendererType::RENDERER_3D); + NxRendererNotInitialized ex(NxRendererType::RENDERER_3D); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[RENDERER 3D] Renderer not initialized, call the init function first"), std::string::npos); @@ -93,7 +93,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - TextureInvalidSize ex("OpenGL", 4096, 4096, 2048); + NxTextureInvalidSize ex("OpenGL", 4096, 4096, 2048); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Invalid size for texture: 4096x4096 is too big, max texture size is : 2048"), std::string::npos); @@ -105,7 +105,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - StbiLoadException ex("Invalid PNG file"); + NxStbiLoadException ex("Invalid PNG file"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("STBI load failed: Invalid PNG file"), std::string::npos); @@ -117,7 +117,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - OutOfRangeException ex(10, 5); + NxOutOfRangeException ex(10, 5); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("Index 10 is out of range [0, 5)"), std::string::npos); @@ -129,7 +129,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - GraphicsApiNotInitialized ex("OpenGL"); + NxGraphicsApiNotInitialized ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] API is not initialized, call the init function first"), std::string::npos); @@ -141,7 +141,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - GraphicsApiViewportResizingFailure ex("OpenGL", true, 4096, 4096); + NxGraphicsApiViewportResizingFailure ex("OpenGL", true, 4096, 4096); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Viewport resizing failed: 4096x4096 is too big"), std::string::npos); @@ -153,7 +153,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - GraphicsApiWindowInitFailure ex("OpenGL"); + NxGraphicsApiWindowInitFailure ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("Failed to initialize graphics API: OpenGL"), std::string::npos); @@ -165,7 +165,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - InvalidValue ex("OpenGL", "Negative width value"); + NxInvalidValue ex("OpenGL", "Negative width value"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Invalid value: Negative width value"), std::string::npos); @@ -177,7 +177,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - ShaderInvalidUniform ex("OpenGL", "main.glsl", "u_ViewProjection"); + NxShaderInvalidUniform ex("OpenGL", "main.glsl", "u_ViewProjection"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Failed to retrieve uniform \"u_ViewProjection\" in shader: main.glsl"), std::string::npos); @@ -189,7 +189,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferCreationFailed ex("OpenGL"); + NxFramebufferCreationFailed ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Failed to create the framebuffer"), std::string::npos); @@ -201,7 +201,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferUnsupportedColorFormat ex("OpenGL"); + NxFramebufferUnsupportedColorFormat ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Unsupported framebuffer color attachment format"), std::string::npos); @@ -213,7 +213,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferUnsupportedDepthFormat ex("OpenGL"); + NxFramebufferUnsupportedDepthFormat ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Unsupported framebuffer depth attachment format"), std::string::npos); @@ -225,7 +225,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferReadFailure ex("OpenGL", 0, 100, 200); + NxFramebufferReadFailure ex("OpenGL", 0, 100, 200); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Unable to read framebuffer with index 0 at coordinate (100, 200)"), std::string::npos); @@ -237,7 +237,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - FramebufferInvalidIndex ex("OpenGL", 5); + NxFramebufferInvalidIndex ex("OpenGL", 5); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Invalid attachment index : 5"), std::string::npos); @@ -249,7 +249,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - BufferLayoutEmpty ex("OpenGL"); + NxBufferLayoutEmpty ex("OpenGL"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Vertex buffer layout cannot be empty"), std::string::npos); @@ -261,7 +261,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - TextureUnsupportedFormat ex("OpenGL", 5, "texture.exr"); + NxTextureUnsupportedFormat ex("OpenGL", 5, "texture.exr"); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Unsupported image format with 5 channels in texture.exr"), std::string::npos); @@ -273,7 +273,7 @@ namespace nexo::renderer { constexpr const char* expectedFile = __FILE__; constexpr unsigned int expectedLine = __LINE__ + 2; - TextureSizeMismatch ex("OpenGL", 1024, 2048); + NxTextureSizeMismatch ex("OpenGL", 1024, 2048); std::string formattedMessage = ex.what(); EXPECT_NE(formattedMessage.find("[OpenGL] Data size does not match the texture size: 1024 != 2048"), std::string::npos); diff --git a/tests/renderer/Framebuffer.test.cpp b/tests/renderer/Framebuffer.test.cpp index a301fd06e..3dcefe245 100644 --- a/tests/renderer/Framebuffer.test.cpp +++ b/tests/renderer/Framebuffer.test.cpp @@ -27,15 +27,15 @@ namespace nexo::renderer { TEST_F(OpenGLTest, FramebufferCreationAndBinding) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 800; specs.height = 600; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8}, - {FrameBufferTextureFormats::DEPTH24STENCIL8} + {NxFrameBufferTextureFormats::RGBA8}, + {NxFrameBufferTextureFormats::DEPTH24STENCIL8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); // Validate framebuffer id EXPECT_NE(framebuffer.getFramebufferId(), 0); @@ -70,15 +70,15 @@ namespace nexo::renderer { TEST_F(OpenGLTest, FramebufferResize) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 800; specs.height = 600; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8}, - {FrameBufferTextureFormats::DEPTH24STENCIL8} + {NxFrameBufferTextureFormats::RGBA8}, + {NxFrameBufferTextureFormats::DEPTH24STENCIL8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); framebuffer.resize(1024, 768); @@ -90,50 +90,50 @@ namespace nexo::renderer { TEST_F(OpenGLTest, ResizeWithInvalidDimensions) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 800; specs.height = 600; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8}, - {FrameBufferTextureFormats::DEPTH24STENCIL8} + {NxFrameBufferTextureFormats::RGBA8}, + {NxFrameBufferTextureFormats::DEPTH24STENCIL8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); - EXPECT_THROW(framebuffer.resize(0, 600), FramebufferResizingFailed); - EXPECT_THROW(framebuffer.resize(800, 0), FramebufferResizingFailed); - EXPECT_THROW(framebuffer.resize(9000, 600), FramebufferResizingFailed); - EXPECT_THROW(framebuffer.resize(800, 9000), FramebufferResizingFailed); + EXPECT_THROW(framebuffer.resize(0, 600), NxFramebufferResizingFailed); + EXPECT_THROW(framebuffer.resize(800, 0), NxFramebufferResizingFailed); + EXPECT_THROW(framebuffer.resize(9000, 600), NxFramebufferResizingFailed); + EXPECT_THROW(framebuffer.resize(800, 9000), NxFramebufferResizingFailed); } TEST_F(OpenGLTest, InvalidFramebufferCreation) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 0; specs.height = 600; - EXPECT_THROW(OpenGlFramebuffer framebuffer(specs), FramebufferResizingFailed); + EXPECT_THROW(NxOpenGlFramebuffer framebuffer(specs), NxFramebufferResizingFailed); specs.width = 800; specs.height = 0; - EXPECT_THROW(OpenGlFramebuffer framebuffer(specs), FramebufferResizingFailed); + EXPECT_THROW(NxOpenGlFramebuffer framebuffer(specs), NxFramebufferResizingFailed); specs.width = 9000; specs.height = 600; - EXPECT_THROW(OpenGlFramebuffer framebuffer(specs), FramebufferResizingFailed); + EXPECT_THROW(NxOpenGlFramebuffer framebuffer(specs), NxFramebufferResizingFailed); specs.width = 800; specs.height = 9000; - EXPECT_THROW(OpenGlFramebuffer framebuffer(specs), FramebufferResizingFailed); + EXPECT_THROW(NxOpenGlFramebuffer framebuffer(specs), NxFramebufferResizingFailed); } TEST_F(OpenGLTest, MultipleColorAttachments) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 800; specs.height = 600; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8}, - {FrameBufferTextureFormats::RGBA16}, - {FrameBufferTextureFormats::DEPTH24STENCIL8} + {NxFrameBufferTextureFormats::RGBA8}, + {NxFrameBufferTextureFormats::RGBA16}, + {NxFrameBufferTextureFormats::DEPTH24STENCIL8} }; // Check if the hardware supports at least the required number of attachments @@ -141,7 +141,7 @@ namespace nexo::renderer { glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxAttachments); EXPECT_GE(maxAttachments, static_cast(specs.attachments.attachments.size())); - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); // Verify the IDs of the color attachments EXPECT_NE(framebuffer.getColorAttachmentId(0), 0); @@ -169,7 +169,7 @@ namespace nexo::renderer { { EXPECT_EQ(static_cast(boundTexture), framebuffer.getColorAttachmentId(i)); } else if (framebuffer.getSpecs().attachments.attachments[i].textureFormat == - FrameBufferTextureFormats::DEPTH24STENCIL8) + NxFrameBufferTextureFormats::DEPTH24STENCIL8) { EXPECT_EQ(static_cast(boundTexture), framebuffer.getDepthAttachmentId()); @@ -265,19 +265,19 @@ namespace nexo::renderer { TEST_F(OpenGLTest, InvalidFormat) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 800; specs.height = 600; specs.samples = 1; // Test unsupported color format specs.attachments.attachments = { - {static_cast(999)} // Invalid format + {static_cast(999)} // Invalid format }; EXPECT_THROW( - OpenGlFramebuffer framebuffer(specs); - , FramebufferUnsupportedColorFormat); + NxOpenGlFramebuffer framebuffer(specs); + , NxFramebufferUnsupportedColorFormat); } TEST_F(OpenGLTest, GetPixelWrapperValid) @@ -286,15 +286,15 @@ namespace nexo::renderer { // TODO: fix test (see #99) GTEST_SKIP() << "This test infinitely loops on the CI on Windows, skipping for now."; #endif - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); framebuffer.bind(); int pixelValue = 0; @@ -306,55 +306,55 @@ namespace nexo::renderer { TEST_F(OpenGLTest, GetPixelWrapperUnsupportedType) { // Verify that getPixelWrapper throws when provided with a type other than int. - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); int dummy = 0; EXPECT_THROW( framebuffer.getPixelWrapper(0, 50, 50, &dummy, typeid(float)), - FramebufferUnsupportedColorFormat + NxFramebufferUnsupportedColorFormat ); } TEST_F(OpenGLTest, GetPixelWrapperInvalidAttachmentIndex) { // Verify that getPixelWrapper throws if the attachment index is out of bounds. - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; // Only one color attachment. specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); int dummy = 0; // Attachment index 1 is invalid because only index 0 exists. EXPECT_THROW( framebuffer.getPixelWrapper(1, 50, 50, &dummy, typeid(int)), - FramebufferInvalidIndex + NxFramebufferInvalidIndex ); } TEST_F(OpenGLTest, ClearAttachmentWrapperValid) { // Test that clearAttachmentWrapper does not throw when clearing a valid attachment with a supported type. - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); int clearValue = 0; EXPECT_NO_THROW(framebuffer.clearAttachmentWrapper(0, &clearValue, typeid(int))); } @@ -362,51 +362,51 @@ namespace nexo::renderer { TEST_F(OpenGLTest, ClearAttachmentWrapperUnsupportedType) { // Test that clearAttachmentWrapper throws if called with an unsupported type. - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); int clearValue = 0; EXPECT_THROW( framebuffer.clearAttachmentWrapper(0, &clearValue, typeid(float)), - FramebufferUnsupportedColorFormat + NxFramebufferUnsupportedColorFormat ); } TEST_F(OpenGLTest, ClearAttachmentWrapperInvalidAttachmentIndex) { // Test that clearAttachmentWrapper throws when the attachment index is out of range. - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; // Only one color attachment exists. specs.attachments.attachments = { - {FrameBufferTextureFormats::RGBA8} + {NxFrameBufferTextureFormats::RGBA8} }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); int clearValue = 0; // Attachment index 1 is invalid. EXPECT_THROW( framebuffer.clearAttachmentWrapper(1, &clearValue, typeid(int)), - FramebufferInvalidIndex + NxFramebufferInvalidIndex ); } TEST_F(OpenGLTest, ClearAndGetPixelRedIntegerAttachment) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; - specs.attachments.attachments = { FrameBufferTextureFormats::RED_INTEGER }; + specs.attachments.attachments = { NxFrameBufferTextureFormats::RED_INTEGER }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); framebuffer.bind(); int clearValue = 123; EXPECT_NO_THROW(framebuffer.clearAttachmentWrapper(0, &clearValue, typeid(int))); @@ -418,16 +418,16 @@ namespace nexo::renderer { } TEST_F(OpenGLTest, ClearAndGetPixelMultipleAttachments) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 100; specs.height = 100; specs.samples = 1; specs.attachments.attachments = { - { FrameBufferTextureFormats::RGBA8 }, - { FrameBufferTextureFormats::RED_INTEGER } + { NxFrameBufferTextureFormats::RGBA8 }, + { NxFrameBufferTextureFormats::RED_INTEGER } }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); framebuffer.bind(); // Clear the second (red integer) attachment to a known value. @@ -443,13 +443,13 @@ namespace nexo::renderer { // While OpenGL's glReadPixels does not throw exceptions for out–of–bounds reads, we ensure // that our wrapper does not crash. (The returned value may be undefined.) TEST_F(OpenGLTest, GetPixelOutOfBoundsRedIntegerAttachment) { - FramebufferSpecs specs; + NxFramebufferSpecs specs; specs.width = 50; specs.height = 50; specs.samples = 1; - specs.attachments.attachments = { { static_cast(3) } }; + specs.attachments.attachments = { { static_cast(3) } }; - OpenGlFramebuffer framebuffer(specs); + NxOpenGlFramebuffer framebuffer(specs); framebuffer.bind(); int pixelValue = 0; // Attempt to read a pixel well outside the 50x50 region. diff --git a/tests/renderer/Renderer2D.test.cpp b/tests/renderer/Renderer2D.test.cpp deleted file mode 100644 index b26b6a5b7..000000000 --- a/tests/renderer/Renderer2D.test.cpp +++ /dev/null @@ -1,496 +0,0 @@ -//// Renderer2D.test.cpp ////////////////////////////////////////////////////// -// -// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz -// zzzzzzz zzz zzzz zzzz zzzz zzzz -// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz -// zzz zzz zzz z zzzz zzzz zzzz zzzz -// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz -// -// Author: Mehdy MORVAN -// Date: 25/11/2024 -// Description: Test file for the renderer 2D -// -/////////////////////////////////////////////////////////////////////////////// - -#include -#include -#include -#include "renderer/Renderer2D.hpp" -#include "renderer/Texture.hpp" -#include "renderer/SubTexture2D.hpp" -#include "renderer/RendererExceptions.hpp" -#include "renderer/Renderer.hpp" -#include "contexts/opengl.hpp" -#include "../utils/comparison.hpp" - -namespace nexo::renderer { - class Renderer2DTest : public ::testing::Test { - GLFWwindow *window = nullptr; - - protected: - void SetUp() override - { - if (!glfwInit()) - { - GTEST_SKIP() << "GLFW initialization failed. Skipping OpenGL tests."; - } - - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - window = glfwCreateWindow(800, 600, "Test Window", nullptr, nullptr); - if (!window) - { - glfwTerminate(); - GTEST_SKIP() << "Failed to create GLFW window. Skipping OpenGL tests."; - } - - glfwMakeContextCurrent(window); - - if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) - { - glfwDestroyWindow(window); - glfwTerminate(); - GTEST_SKIP() << "Failed to initialize GLAD. Skipping OpenGL tests."; - } - - GLint major = 0, minor = 0; - glGetIntegerv(GL_MAJOR_VERSION, &major); - glGetIntegerv(GL_MINOR_VERSION, &minor); - if (major < 4 || (major == 4 && minor < 5)) - { - glfwDestroyWindow(window); - glfwTerminate(); - GTEST_SKIP() << "OpenGL 4.5 is required. Skipping OpenGL tests."; - } - renderer2D = std::make_unique(); - Renderer::init(); - renderer2D->init(); - } - - void TearDown() override - { - renderer2D->shutdown(); - if (window) - { - glfwDestroyWindow(window); - } - } - - std::unique_ptr renderer2D; - }; - - TEST_F(Renderer2DTest, BeginEndScene) - { - glm::mat4 viewProjection = glm::mat4(1.0f); - - EXPECT_NO_THROW(renderer2D->beginScene(viewProjection)); - EXPECT_NO_THROW(renderer2D->endScene()); - } - - TEST_F(Renderer2DTest, DrawQuadWithoutTexture) - { - glm::vec2 position = {0.0f, 0.0f}; - glm::vec2 size = {1.0f, 1.0f}; - glm::vec4 color = {1.0f, 0.0f, 0.0f, 1.0f}; // Red color - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw quad without texture - EXPECT_NO_THROW(renderer2D->drawQuad(position, size, color)); - - // Validate number of primitives drawn - GLuint query; - glGenQueries(1, &query); - glBeginQuery(GL_PRIMITIVES_GENERATED, query); - renderer2D->endScene(); - glEndQuery(GL_PRIMITIVES_GENERATED); - GLuint primitivesRendered = 0; - glGetQueryObjectuiv(query, GL_QUERY_RESULT, &primitivesRendered); - EXPECT_EQ(primitivesRendered, 2); // 2 triangles - glDeleteQueries(1, &query); - - // Validate vertex and index buffers content - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - GLuint indexBufferId = renderer2D->getInternalStorage()->indexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(4); // Expecting 4 vertices - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(QuadVertex), vertexData.data()); - - // Validate vertex positions - EXPECT_EQ(vertexData[0].position, glm::vec3(-0.5f, -0.5f, 0.0f)); // Bottom-left - EXPECT_EQ(vertexData[1].position, glm::vec3(0.5f, -0.5f, 0.0f)); // Bottom-right - EXPECT_EQ(vertexData[2].position, glm::vec3(0.5f, 0.5f, 0.0f)); // Top-right - EXPECT_EQ(vertexData[3].position, glm::vec3(-0.5f, 0.5f, 0.0f)); // Top-left - // Validate vertex colors - for (int i = 0; i < 4; ++i) - { - EXPECT_EQ(vertexData[i].color, color); - } - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferId); - std::vector indexData(6); // Expecting 6 indices - glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, 6 * sizeof(unsigned int), indexData.data()); - // Validate indices - EXPECT_EQ(indexData[0], 0); // First triangle - EXPECT_EQ(indexData[1], 1); - EXPECT_EQ(indexData[2], 2); - EXPECT_EQ(indexData[3], 2); // Second triangle - EXPECT_EQ(indexData[4], 3); - EXPECT_EQ(indexData[5], 0); - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 1); - EXPECT_EQ(stats.getTotalVertexCount(), 4); // 1 quad * 4 vertices - EXPECT_EQ(stats.getTotalIndexCount(), 6); // 1 quad * 6 indices - } - - TEST_F(Renderer2DTest, DrawQuadWithTexture) - { - glm::vec2 position = {0.0f, 0.0f}; - glm::vec2 size = {1.0f, 1.0f}; - auto texture = Texture2D::create(2, 2); // Create a simple 2x2 texture - glm::vec4 expectedColor = {1.0f, 1.0f, 1.0f, 1.0f}; // White color for textured quads - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw quad with texture - EXPECT_NO_THROW(renderer2D->drawQuad(position, size, texture)); - renderer2D->endScene(); - - // Validate vertex buffer content - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - - // Read back vertex data - std::vector vertexData(4); // Expecting 4 vertices for the quad - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(QuadVertex), vertexData.data()); - - // Expected vertex positions (untransformed quad) - glm::vec3 expectedPositions[] = { - {-0.5f, -0.5f, 0.0f}, // Bottom-left - {0.5f, -0.5f, 0.0f}, // Bottom-right - {0.5f, 0.5f, 0.0f}, // Top-right - {-0.5f, 0.5f, 0.0f} // Top-left - }; - - // Expected texture coordinates - glm::vec2 expectedTexCoords[] = { - {0.0f, 0.0f}, // Bottom-left - {1.0f, 0.0f}, // Bottom-right - {1.0f, 1.0f}, // Top-right - {0.0f, 1.0f} // Top-left - }; - - // Validate each vertex - for (int i = 0; i < 4; ++i) - { - EXPECT_VEC3_NEAR(vertexData[i].position, expectedPositions[i], 0.01f); - EXPECT_VEC2_NEAR(vertexData[i].texCoord, expectedTexCoords[i], 0.01f); - EXPECT_VEC4_NEAR(vertexData[i].color, expectedColor, 0.01f); - EXPECT_EQ(vertexData[i].texIndex, 1.0f); // Texture index in the shader - } - - // Validate index buffer content - GLuint indexBufferId = renderer2D->getInternalStorage()->indexBuffer->getId(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferId); - - std::vector indexData(6); // Expecting 6 indices for the quad - glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, 6 * sizeof(unsigned int), indexData.data()); - - // Expected indices for two triangles - unsigned int expectedIndices[] = {0, 1, 2, 2, 3, 0}; - for (int i = 0; i < 6; ++i) - { - EXPECT_EQ(indexData[i], expectedIndices[i]); - } - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 1); // One quad drawn - EXPECT_EQ(stats.getTotalVertexCount(), 4); // 1 quad * 4 vertices - EXPECT_EQ(stats.getTotalIndexCount(), 6); // 1 quad * 6 indices - } - - - TEST_F(Renderer2DTest, DrawQuadWithSubTexture) - { - glm::vec2 position = {0.0f, 0.0f}; - glm::vec2 size = {1.0f, 1.0f}; - auto texture = Texture2D::create(4, 4); // Base texture - auto subTexture = SubTexture2D::createFromCoords(texture, {1, 1}, {2, 2}, {1, 1}); // SubTexture - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw quad with SubTexture - EXPECT_NO_THROW(renderer2D->drawQuad(position, size, subTexture)); - - renderer2D->endScene(); - - // Validate vertex buffer content - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(4); // Expecting 4 vertices - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(QuadVertex), vertexData.data()); - - // Normalize expected texture coordinates - float texWidth = static_cast(texture->getWidth()); - float texHeight = static_cast(texture->getHeight()); - glm::vec2 expectedTexCoords[] = { - {2.0f / texWidth, 2.0f / texHeight}, // Bottom-left - {4.0f / texWidth, 2.0f / texHeight}, // Bottom-right - {4.0f / texWidth, 4.0f / texHeight}, // Top-right - {2.0f / texWidth, 4.0f / texHeight}, // Top-left - }; - // Validate texture coordinates - for (int i = 0; i < 4; ++i) - { - EXPECT_VEC2_NEAR(vertexData[i].texCoord, expectedTexCoords[i], 0.01f); - } - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 1); - } - - - TEST_F(Renderer2DTest, DrawQuadWithRotation) - { - glm::vec2 position = {0.0f, 0.0f}; - glm::vec2 size = {1.0f, 1.0f}; - float rotation = 45.0f; // Degrees - glm::vec4 color = {1.0f, 0.0f, 0.0f, 1.0f}; - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw quad with rotation - EXPECT_NO_THROW(renderer2D->drawQuad(position, size, rotation, color)); - - renderer2D->endScene(); - - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(4); // Expecting 4 vertices - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(QuadVertex), vertexData.data()); - - glm::mat4 transform = glm::translate(glm::mat4(1.0f), glm::vec3(position, 0.0f)) * - glm::rotate(glm::mat4(1.0f), glm::radians(rotation), glm::vec3(0.0f, 0.0f, 1.0f)) * - glm::scale(glm::mat4(1.0f), glm::vec3(size, 1.0f)); - - // Validate rotated positions - glm::vec4 expectedPositions[] = { - transform * glm::vec4(-0.5f, -0.5f, 0.0f, 1.0f), - transform * glm::vec4(0.5f, -0.5f, 0.0f, 1.0f), - transform * glm::vec4(0.5f, 0.5f, 0.0f, 1.0f), - transform * glm::vec4(-0.5f, 0.5f, 0.0f, 1.0f), - }; - for (int i = 0; i < 4; ++i) - { - EXPECT_VEC3_NEAR(vertexData[i].position, expectedPositions[i], 0.01f); - } - - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 1); - } - - TEST_F(Renderer2DTest, DrawMultipleQuads) - { - glm::vec2 position1 = {0.0f, 0.0f}; - glm::vec2 position2 = {2.0f, 2.0f}; - glm::vec2 size = {1.0f, 1.0f}; - glm::vec4 color1 = {1.0f, 0.0f, 0.0f, 1.0f}; - glm::vec4 color2 = {0.0f, 1.0f, 0.0f, 1.0f}; - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw two quads - EXPECT_NO_THROW(renderer2D->drawQuad(position1, size, color1)); - EXPECT_NO_THROW(renderer2D->drawQuad(position2, size, color2)); - - renderer2D->endScene(); - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 2); - EXPECT_EQ(stats.getTotalVertexCount(), 8); // 2 quads * 4 vertices - EXPECT_EQ(stats.getTotalIndexCount(), 12); // 2 quads * 6 indices - - // Validate vertex buffer content - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(8); // Expecting 8 vertices (2 quads * 4 vertices) - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(QuadVertex), vertexData.data()); - - // Expected vertex positions for the two quads - glm::vec3 expectedPositions[] = { - // Quad 1 (position1) - {-0.5f, -0.5f, 0.0f}, {0.5f, -0.5f, 0.0f}, {0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f}, - // Quad 2 (position2) - {1.5f, 1.5f, 0.0f}, {2.5f, 1.5f, 0.0f}, {2.5f, 2.5f, 0.0f}, {1.5f, 2.5f, 0.0f} - }; - // Expected colors for the two quads - glm::vec4 expectedColors[] = { - color1, color1, color1, color1, // Quad 1 - color2, color2, color2, color2 // Quad 2 - }; - // Validate vertex positions and colors - for (int i = 0; i < 8; ++i) - { - EXPECT_VEC3_NEAR(vertexData[i].position, expectedPositions[i], 0.01f); - EXPECT_VEC4_NEAR(vertexData[i].color, expectedColors[i], 0.01f); - } - - // Validate index buffer content - GLuint indexBufferId = renderer2D->getInternalStorage()->indexBuffer->getId(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferId); - std::vector indexData(12); // Expecting 12 indices (2 quads * 6 indices) - glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, 12 * sizeof(unsigned int), indexData.data()); - - // Expected indices for the two quads - unsigned int expectedIndices[] = { - // Quad 1 - 0, 1, 2, 2, 3, 0, - // Quad 2 - 4, 5, 6, 6, 7, 4 - }; - - // Validate indices - for (int i = 0; i < 12; ++i) - { - EXPECT_EQ(indexData[i], expectedIndices[i]); - } - } - - TEST_F(Renderer2DTest, DrawMultipleTexturedQuads) - { - glm::vec2 position1 = {0.0f, 0.0f}; - glm::vec2 position2 = {2.0f, 2.0f}; - glm::vec2 size = {1.0f, 1.0f}; - auto texture1 = Texture2D::create(4, 4); // Texture for the first quad - auto texture2 = Texture2D::create(8, 8); // Texture for the second quad - - renderer2D->beginScene(glm::mat4(1.0f)); - - // Draw two textured quads - EXPECT_NO_THROW(renderer2D->drawQuad(position1, size, texture1)); - EXPECT_NO_THROW(renderer2D->drawQuad(position2, size, texture2)); - - renderer2D->endScene(); - - // Validate stats - RendererStats stats = renderer2D->getStats(); - EXPECT_EQ(stats.quadCount, 2); - EXPECT_EQ(stats.getTotalVertexCount(), 8); // 2 quads * 4 vertices - EXPECT_EQ(stats.getTotalIndexCount(), 12); // 2 quads * 6 indices - - // Validate vertex buffer content - GLuint vertexBufferId = renderer2D->getInternalStorage()->vertexBuffer->getId(); - glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - - // Read back vertex data - std::vector vertexData(8); // Expecting 8 vertices (2 quads * 4 vertices) - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 8 * sizeof(QuadVertex), vertexData.data()); - - // Expected vertex positions for the two quads - glm::vec3 expectedPositions[] = { - // Quad 1 (position1) - {-0.5f, -0.5f, 0.0f}, {0.5f, -0.5f, 0.0f}, {0.5f, 0.5f, 0.0f}, {-0.5f, 0.5f, 0.0f}, - // Quad 2 (position2) - {1.5f, 1.5f, 0.0f}, {2.5f, 1.5f, 0.0f}, {2.5f, 2.5f, 0.0f}, {1.5f, 2.5f, 0.0f} - }; - - // Expected texture coordinates (default for full texture) - glm::vec2 expectedTexCoords[] = { - {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}, // Quad 1 - {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f} // Quad 2 - }; - - // Expected texture indices - float expectedTexIndices[] = { - 1.0f, 1.0f, 1.0f, 1.0f, // Quad 1 - 2.0f, 2.0f, 2.0f, 2.0f // Quad 2 - }; - - // Validate vertex positions, texture coordinates, and texture indices - for (int i = 0; i < 8; ++i) - { - EXPECT_VEC3_NEAR(vertexData[i].position, expectedPositions[i], 0.01f); - EXPECT_VEC2_NEAR(vertexData[i].texCoord, expectedTexCoords[i], 0.01f); - EXPECT_EQ(vertexData[i].texIndex, expectedTexIndices[i]); - } - - // Validate index buffer content - GLuint indexBufferId = renderer2D->getInternalStorage()->indexBuffer->getId(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferId); - - // Read back index data - std::vector indexData(12); // Expecting 12 indices (2 quads * 6 indices) - glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, 12 * sizeof(unsigned int), indexData.data()); - - // Expected indices for the two quads - unsigned int expectedIndices[] = { - // Quad 1 - 0, 1, 2, 2, 3, 0, - // Quad 2 - 4, 5, 6, 6, 7, 4 - }; - - // Validate indices - for (int i = 0; i < 12; ++i) - { - EXPECT_EQ(indexData[i], expectedIndices[i]); - } - } - - TEST_F(Renderer2DTest, BeginSceneWithoutInit) { - // Manually delete the storage to simulate an uninitialized renderer - renderer2D->shutdown(); - - glm::mat4 viewProjection = glm::mat4(1.0f); - - // Expect RendererNotInitialized exception - EXPECT_THROW(renderer2D->beginScene(viewProjection), RendererNotInitialized); - // Re-init for TearDown function - renderer2D->init(); - } - - TEST_F(Renderer2DTest, EndSceneWithoutBeginScene) { - // Expect RendererSceneLifeCycleFailure exception - EXPECT_THROW(renderer2D->endScene(), RendererSceneLifeCycleFailure); - } - - TEST_F(Renderer2DTest, DrawQuadWithoutBeginScene) { - glm::vec2 position = {0.0f, 0.0f}; - glm::vec2 size = {1.0f, 1.0f}; - glm::vec4 color = {1.0f, 0.0f, 0.0f, 1.0f}; - - // Expect RendererSceneLifeCycleFailure exception - EXPECT_THROW(renderer2D->drawQuad(position, size, color), RendererSceneLifeCycleFailure); - } - - TEST_F(Renderer2DTest, ResetStatsWithoutInit) { - // Manually delete the storage to simulate an uninitialized renderer - renderer2D->shutdown(); - - // Expect RendererNotInitialized exception - EXPECT_THROW(renderer2D->resetStats(), RendererNotInitialized); - // Re-init for TearDown function - renderer2D->init(); - } - - TEST_F(Renderer2DTest, GetStatsWithoutInit) { - // Manually delete the storage to simulate an uninitialized renderer - renderer2D->shutdown(); - - // Expect RendererNotInitialized exception - EXPECT_THROW(renderer2D->getStats(), RendererNotInitialized); - // Re-init for TearDown function - renderer2D->init(); - } - - -} diff --git a/tests/renderer/Renderer3D.test.cpp b/tests/renderer/Renderer3D.test.cpp index f62dc618f..627b287e8 100644 --- a/tests/renderer/Renderer3D.test.cpp +++ b/tests/renderer/Renderer3D.test.cpp @@ -66,8 +66,8 @@ namespace nexo::renderer { glfwTerminate(); GTEST_SKIP() << "OpenGL 4.5 is required. Skipping OpenGL tests."; } - renderer3D = std::make_unique(); - Renderer::init(); + renderer3D = std::make_unique(); + NxRenderer::init(); EXPECT_NO_THROW(renderer3D->init()); } @@ -80,7 +80,7 @@ namespace nexo::renderer { } } - std::unique_ptr renderer3D; + std::unique_ptr renderer3D; }; TEST_F(Renderer3DTest, BeginEndScene) @@ -119,8 +119,8 @@ namespace nexo::renderer { // Validate vertex buffer data: GLuint vertexBufferId = renderer3D->getInternalStorage()->vertexBuffer->getId(); glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(36); // Expecting 36 vertices - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 36 * sizeof(Vertex), vertexData.data()); + std::vector vertexData(36); // Expecting 36 vertices + glGetBufferSubData(GL_ARRAY_BUFFER, 0, 36 * sizeof(NxVertex), vertexData.data()); // Expected vertex positions for a unit cube const glm::vec3 expectedPositions[36] = { @@ -221,9 +221,9 @@ namespace nexo::renderer { glm::vec3 position = {0.0f, 0.0f, 0.0f}; glm::vec3 size = {1.0f, 1.0f, 1.0f}; - components::Material material; + renderer::NxMaterial material; material.albedoColor = {1.0f, 0.0f, 0.0f, 1.0f}; // Red color - material.albedoTexture = Texture2D::create(4, 4); // Example texture + material.albedoTexture = NxTexture2D::create(4, 4); // Example texture GLuint query; glGenQueries(1, &query); @@ -241,7 +241,7 @@ namespace nexo::renderer { glDeleteQueries(1, &query); // Validate render stats - Renderer3DStats stats = renderer3D->getStats(); + NxRenderer3DStats stats = renderer3D->getStats(); EXPECT_EQ(stats.cubeCount, 1); EXPECT_EQ(stats.getTotalVertexCount(), 8); // 1 cube * 8 vertices per cube (as defined in struct) EXPECT_EQ(stats.getTotalIndexCount(), 36); // 1 cube * 36 indices @@ -271,7 +271,7 @@ namespace nexo::renderer { glDeleteQueries(1, &query); // Validate render stats - Renderer3DStats stats = renderer3D->getStats(); + NxRenderer3DStats stats = renderer3D->getStats(); EXPECT_EQ(stats.cubeCount, 1); EXPECT_EQ(stats.getTotalVertexCount(), 8); // 1 cube * 8 vertices EXPECT_EQ(stats.getTotalIndexCount(), 36); // 1 cube * 36 indices @@ -300,7 +300,7 @@ namespace nexo::renderer { glDeleteQueries(1, &query); // Validate render stats - Renderer3DStats stats = renderer3D->getStats(); + NxRenderer3DStats stats = renderer3D->getStats(); EXPECT_EQ(stats.cubeCount, 1); EXPECT_EQ(stats.getTotalVertexCount(), 8); // 1 cube * 8 vertices EXPECT_EQ(stats.getTotalIndexCount(), 36); // 1 cube * 36 indices @@ -312,11 +312,11 @@ namespace nexo::renderer { glm::vec3 size = {2.0f, 2.0f, 2.0f}; glm::vec3 rotation = {45.0f, 30.0f, 60.0f}; - components::Material material; + renderer::NxMaterial material; material.albedoColor = {0.0f, 1.0f, 1.0f, 1.0f}; // Cyan color - material.albedoTexture = Texture2D::create(4, 4); // Example texture + material.albedoTexture = NxTexture2D::create(4, 4); // Example texture material.specularColor = {1.0f, 1.0f, 1.0f, 1.0f}; - material.metallicMap = Texture2D::create(2, 2); // Example specular texture + material.metallicMap = NxTexture2D::create(2, 2); // Example specular texture GLuint query; glGenQueries(1, &query); @@ -334,7 +334,7 @@ namespace nexo::renderer { glDeleteQueries(1, &query); // Validate render stats - Renderer3DStats stats = renderer3D->getStats(); + NxRenderer3DStats stats = renderer3D->getStats(); EXPECT_EQ(stats.cubeCount, 1); EXPECT_EQ(stats.getTotalVertexCount(), 8); // 1 cube * 8 vertices EXPECT_EQ(stats.getTotalIndexCount(), 36); // 1 cube * 36 indices @@ -346,9 +346,9 @@ namespace nexo::renderer { glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), {0.0f, 1.0f, 0.0f}) * glm::scale(glm::mat4(1.0f), {2.0f, 2.0f, 2.0f}); - components::Material material; + renderer::NxMaterial material; material.albedoColor = {1.0f, 1.0f, 0.0f, 1.0f}; // Yellow color - material.albedoTexture = Texture2D::create(4, 4); // Example texture + material.albedoTexture = NxTexture2D::create(4, 4); // Example texture GLuint query; glGenQueries(1, &query); @@ -366,7 +366,7 @@ namespace nexo::renderer { glDeleteQueries(1, &query); // Validate render stats - Renderer3DStats stats = renderer3D->getStats(); + NxRenderer3DStats stats = renderer3D->getStats(); EXPECT_EQ(stats.cubeCount, 1); EXPECT_EQ(stats.getTotalVertexCount(), 8); // 1 cube * 8 vertices EXPECT_EQ(stats.getTotalIndexCount(), 36); // 1 cube * 36 indices @@ -375,13 +375,13 @@ namespace nexo::renderer { TEST_F(Renderer3DTest, DrawMesh) { // Create a simple mesh (a triangle) - std::vector vertices = { + std::vector vertices = { {{-0.5f, -0.5f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, -1}, {{ 0.5f, -0.5f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, -1}, {{ 0.0f, 0.5f, 0.0f}, {0.5f, 1.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, -1} }; std::vector indices = {0, 1, 2}; - auto texture = Texture2D::create(4, 4); + auto texture = NxTexture2D::create(4, 4); // Use an OpenGL query to count the number of triangles drawn GLuint query; @@ -402,8 +402,8 @@ namespace nexo::renderer { // Validate vertex buffer data GLuint vertexBufferId = renderer3D->getInternalStorage()->vertexBuffer->getId(); glBindBuffer(GL_ARRAY_BUFFER, vertexBufferId); - std::vector vertexData(3); // Expecting 3 vertices for a triangle - glGetBufferSubData(GL_ARRAY_BUFFER, 0, 3 * sizeof(Vertex), vertexData.data()); + std::vector vertexData(3); // Expecting 3 vertices for a triangle + glGetBufferSubData(GL_ARRAY_BUFFER, 0, 3 * sizeof(NxVertex), vertexData.data()); // Check vertex data for (unsigned int i = 0; i < 3; ++i) @@ -440,7 +440,7 @@ namespace nexo::renderer { glm::mat4 viewProjection = glm::mat4(1.0f); glm::vec3 cameraPosition = {0.0f, 0.0f, 0.0f}; - EXPECT_THROW(renderer3D->beginScene(viewProjection, cameraPosition), RendererNotInitialized); + EXPECT_THROW(renderer3D->beginScene(viewProjection, cameraPosition), NxRendererNotInitialized); // Re-init for TearDown function renderer3D->init(); } @@ -451,6 +451,6 @@ namespace nexo::renderer { glm::vec3 size = {1.0f, 1.0f, 1.0f}; glm::vec4 color = {1.0f, 0.0f, 0.0f, 1.0f}; - EXPECT_THROW(renderer3D->drawCube(position, size, color), RendererSceneLifeCycleFailure); + EXPECT_THROW(renderer3D->drawCube(position, size, color), NxRendererSceneLifeCycleFailure); } } diff --git a/tests/renderer/RendererAPI.test.cpp b/tests/renderer/RendererAPI.test.cpp index 17f418aad..a9ad0ceea 100644 --- a/tests/renderer/RendererAPI.test.cpp +++ b/tests/renderer/RendererAPI.test.cpp @@ -22,13 +22,13 @@ namespace nexo::renderer { TEST_F(OpenGLTest, InitializationTest) { - OpenGlRendererApi rendererApi; + NxOpenGlRendererApi rendererApi; // Validate init EXPECT_NO_THROW(rendererApi.init()); } TEST_F(OpenGLTest, ViewportSetup) { - OpenGlRendererApi rendererApi; + NxOpenGlRendererApi rendererApi; rendererApi.init(); // Validate viewport resizing @@ -50,19 +50,19 @@ namespace nexo::renderer { // Validate invalid viewport values - EXPECT_THROW(rendererApi.setViewport(0, 0, 0, 600), GraphicsApiViewportResizingFailure); - EXPECT_THROW(rendererApi.setViewport(0, 0, 800, 0), GraphicsApiViewportResizingFailure); + EXPECT_THROW(rendererApi.setViewport(0, 0, 0, 600), NxGraphicsApiViewportResizingFailure); + EXPECT_THROW(rendererApi.setViewport(0, 0, 800, 0), NxGraphicsApiViewportResizingFailure); // Validate too big dimensions unsigned int width = 0; unsigned int height = 0; rendererApi.getMaxViewportSize(&width, &height); EXPECT_THROW(rendererApi.setViewport(0, 0, width + 1, height), - GraphicsApiViewportResizingFailure); + NxGraphicsApiViewportResizingFailure); } TEST_F(OpenGLTest, ClearTest) { - OpenGlRendererApi rendererApi; + NxOpenGlRendererApi rendererApi; rendererApi.init(); // Validate clear color via opengl getters @@ -104,19 +104,19 @@ namespace nexo::renderer { } TEST_F(OpenGLTest, ExceptionOnUninitializedAPI) { - OpenGlRendererApi rendererApi; + NxOpenGlRendererApi rendererApi; // Validate exception is thrown for uninitialized API methods - EXPECT_THROW(rendererApi.setViewport(0, 0, 800, 600), GraphicsApiNotInitialized); - EXPECT_THROW(rendererApi.clear(), GraphicsApiNotInitialized); - EXPECT_THROW(rendererApi.setClearColor(glm::vec4(1.0f)), GraphicsApiNotInitialized); + EXPECT_THROW(rendererApi.setViewport(0, 0, 800, 600), NxGraphicsApiNotInitialized); + EXPECT_THROW(rendererApi.clear(), NxGraphicsApiNotInitialized); + EXPECT_THROW(rendererApi.setClearColor(glm::vec4(1.0f)), NxGraphicsApiNotInitialized); - auto vertexArray = std::make_shared(); - EXPECT_THROW(rendererApi.drawIndexed(vertexArray), GraphicsApiNotInitialized); + auto vertexArray = std::make_shared(); + EXPECT_THROW(rendererApi.drawIndexed(vertexArray), NxGraphicsApiNotInitialized); // Validate exception is thrown when passing a null vertex array rendererApi.init(); - EXPECT_THROW(rendererApi.drawIndexed(nullptr), InvalidValue); + EXPECT_THROW(rendererApi.drawIndexed(nullptr), NxInvalidValue); } } diff --git a/tests/renderer/Shader.test.cpp b/tests/renderer/Shader.test.cpp index c619bf424..018afc065 100644 --- a/tests/renderer/Shader.test.cpp +++ b/tests/renderer/Shader.test.cpp @@ -63,7 +63,7 @@ namespace nexo::renderer { TEST_F(ShaderTest, ShaderCreationFromSource) { - OpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); + NxOpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); // Validate that the shader is bound EXPECT_NO_THROW(shader.bind()); @@ -96,7 +96,7 @@ namespace nexo::renderer { createTemporaryShaderFile(shaderFileContent); - OpenGlShader shader(temporaryShaderFilePath); + NxOpenGlShader shader(temporaryShaderFilePath); // Validate that the shader is bound EXPECT_NO_THROW(shader.bind()); @@ -114,7 +114,7 @@ namespace nexo::renderer { TEST_F(ShaderTest, InvalidShaderFile) { - EXPECT_THROW(OpenGlShader("non_existing_file.glsl"), FileNotFoundException); + EXPECT_THROW(NxOpenGlShader("non_existing_file.glsl"), NxFileNotFoundException); } TEST_F(ShaderTest, InvalidShaderSource) @@ -131,7 +131,7 @@ namespace nexo::renderer { createTemporaryShaderFile(invalidShaderSource); // Validate shader compiling failure - EXPECT_THROW(OpenGlShader shader(temporaryShaderFilePath), ShaderCreationFailed); + EXPECT_THROW(NxOpenGlShader shader(temporaryShaderFilePath), NxShaderCreationFailed); deleteTemporaryShaderFile(); } @@ -166,11 +166,25 @@ namespace nexo::renderer { // Uniforms values constexpr unsigned int nbUniforms = 6; - const char *uniformsName[nbUniforms] = {"uFloat", "uVec3", "uVec4", "uInt", "uModel", "uIntArray"}; - constexpr GLenum uniformsType[nbUniforms] = {GL_FLOAT, GL_FLOAT_VEC3, GL_FLOAT_VEC4, GL_INT, GL_FLOAT_MAT4, GL_SAMPLER_2D}; - constexpr int uniformsSize[nbUniforms] ={1, 1, 1, 1, 1, 3}; - - OpenGlShader shader("TestShader", vertexShaderSourceTestUniforms, fragmentShaderSourceTestUniforms); + const std::unordered_map uniformsTypeMap = { + {"uFloat", GL_FLOAT}, + {"uVec3", GL_FLOAT_VEC3}, + {"uVec4", GL_FLOAT_VEC4}, + {"uInt", GL_INT}, + {"uModel", GL_FLOAT_MAT4}, + {"uIntArray", GL_SAMPLER_2D} + }; + + const std::unordered_map uniformsSizeMap = { + {"uFloat", 1}, + {"uVec3", 1}, + {"uVec4", 1}, + {"uInt", 1}, + {"uModel", 1}, + {"uIntArray", 3} + }; + + NxOpenGlShader shader("TestShader", vertexShaderSourceTestUniforms, fragmentShaderSourceTestUniforms); shader.bind(); /////////////////////// BASE UNIFORM CHECKUP /////////////////////////// @@ -190,21 +204,17 @@ namespace nexo::renderer { // The uniform is not an array if (std::string(name).find('[') == std::string::npos) { - // Validate name - EXPECT_EQ(std::string(name), uniformsName[i]); // Validate size (should be always one) - EXPECT_EQ(size, uniformsSize[i]); + EXPECT_EQ(size, uniformsSizeMap.at(name)); // Validate type - EXPECT_EQ(type, uniformsType[i]); + EXPECT_EQ(type, uniformsTypeMap.at(name)); } else { // Retrieve the base name of the array std::string baseName = std::string(name).substr(0, std::string(name).find('[')); - // Validate name - EXPECT_EQ(baseName, uniformsName[i]); // Validate size - EXPECT_EQ(size, uniformsSize[i]); + EXPECT_EQ(size, uniformsSizeMap.at(baseName.c_str())); // Validate type - EXPECT_EQ(type, uniformsType[i]); + EXPECT_EQ(type, uniformsTypeMap.at(baseName.c_str())); } } @@ -285,13 +295,13 @@ namespace nexo::renderer { TEST_F(ShaderTest, GetShaderName) { - OpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); + NxOpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); EXPECT_EQ(shader.getName(), "TestShader"); } TEST_F(ShaderTest, InvalidUniformName) { - OpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); + NxOpenGlShader shader("TestShader", vertexShaderSource, fragmentShaderSource); shader.bind(); // Validate failure on invalid uniform name diff --git a/tests/renderer/Texture.test.cpp b/tests/renderer/Texture.test.cpp index 4a76c57b6..dc4e113af 100644 --- a/tests/renderer/Texture.test.cpp +++ b/tests/renderer/Texture.test.cpp @@ -41,8 +41,8 @@ namespace nexo::renderer { }; TEST_F(OpenGlTexture2DTest, CreateTextureFromDimensions) { - OpenGlTexture2D texture1(256, 520); - OpenGlTexture2D texture2(520, 256); + NxOpenGlTexture2D texture1(256, 520); + NxOpenGlTexture2D texture2(520, 256); // Validate that each texture is unique EXPECT_NE(texture1.getId(), texture2.getId()); @@ -71,7 +71,7 @@ namespace nexo::renderer { TEST_F(OpenGlTexture2DTest, CreateTextureFromFile) { //TODO: make this test with a real texture file createTemporaryTextureFile(); - OpenGlTexture2D texture(temporaryTextureFilePath); + NxOpenGlTexture2D texture(temporaryTextureFilePath); // Validate dimensions std::cout << texture.getWidth() << std::endl; @@ -82,13 +82,13 @@ namespace nexo::renderer { } TEST_F(OpenGlTexture2DTest, CreateTextureFromInvalidFile) { - EXPECT_THROW(OpenGlTexture2D texture("InvalidFile");, FileNotFoundException); + EXPECT_THROW(NxOpenGlTexture2D texture("InvalidFile");, NxFileNotFoundException); } TEST_F(OpenGlTexture2DTest, SetDataValidSize) { unsigned int width = 128; unsigned int height = 128; - OpenGlTexture2D texture(width, height); + NxOpenGlTexture2D texture(width, height); // Validate setting data with correct size std::vector data(width * height * 4, 255); // RGBA white @@ -105,17 +105,17 @@ namespace nexo::renderer { TEST_F(OpenGlTexture2DTest, SetDataInvalidSize) { unsigned int width = 128; unsigned int height = 128; - OpenGlTexture2D texture(width, height); + NxOpenGlTexture2D texture(width, height); // Create invalid data (size mismatch) std::vector invalidData(width * height * 3, 255); // Missing alpha channel - EXPECT_THROW(texture.setData(invalidData.data(), invalidData.size()), TextureSizeMismatch); + EXPECT_THROW(texture.setData(invalidData.data(), invalidData.size()), NxTextureSizeMismatch); } TEST_F(OpenGlTexture2DTest, BindTextureToSlot) { unsigned int width = 64; unsigned int height = 64; - OpenGlTexture2D texture(width, height); + NxOpenGlTexture2D texture(width, height); unsigned int slot = 5; texture.bind(slot); @@ -133,8 +133,8 @@ namespace nexo::renderer { TEST_F(OpenGlTexture2DTest, TextureEqualityOperator) { unsigned int width = 64; unsigned int height = 64; - OpenGlTexture2D texture1(width, height); - OpenGlTexture2D texture2(width, height); + NxOpenGlTexture2D texture1(width, height); + NxOpenGlTexture2D texture2(width, height); // Validate equality operator EXPECT_FALSE(texture1 == texture2); // Different textures diff --git a/tests/renderer/VertexArray.test.cpp b/tests/renderer/VertexArray.test.cpp index f42e039d9..9a98ac2c3 100644 --- a/tests/renderer/VertexArray.test.cpp +++ b/tests/renderer/VertexArray.test.cpp @@ -26,8 +26,8 @@ namespace nexo::renderer { TEST_F(OpenGLTest, VertexArrayCreationAndBinding) { - auto vertexArray1 = std::make_shared(); - auto vertexArray2 = std::make_shared(); + auto vertexArray1 = std::make_shared(); + auto vertexArray2 = std::make_shared(); EXPECT_NE(vertexArray1->getId(), vertexArray2->getId()); @@ -47,18 +47,18 @@ namespace nexo::renderer { TEST_F(OpenGLTest, AddVertexBuffer) { - auto vertexArray = std::make_shared(); + auto vertexArray = std::make_shared(); float vertices[] = { 0.0f, 0.0f, 0.0f, // Position 1.0f, 1.0f, 1.0f, 1.0f, // Color 3, // Texture index }; - auto vertexBuffer = std::make_shared(vertices, sizeof(vertices)); - BufferLayout layout = { - {ShaderDataType::FLOAT3, "Position"}, - {ShaderDataType::FLOAT4, "Color", true}, - {ShaderDataType::INT, "TextureIndex"}, + auto vertexBuffer = std::make_shared(vertices, sizeof(vertices)); + NxBufferLayout layout = { + {NxShaderDataType::FLOAT3, "Position"}, + {NxShaderDataType::FLOAT4, "Color", true}, + {NxShaderDataType::INT, "TextureIndex"}, }; vertexBuffer->setLayout(layout); @@ -102,42 +102,42 @@ namespace nexo::renderer { TEST_F(OpenGLTest, InvalidVertexBuffer) { - auto vertexArray = std::make_shared(); + auto vertexArray = std::make_shared(); float vertices[] = { 0.0f, 0.0f, 0.0f, // Position 1.0f, 1.0f, 1.0f, 1.0f, // Color 3, // Texture index }; - auto vertexBuffer = std::make_shared(vertices, sizeof(vertices)); + auto vertexBuffer = std::make_shared(vertices, sizeof(vertices)); // Empty layout EXPECT_THROW( vertexArray->addVertexBuffer(vertexBuffer), - BufferLayoutEmpty + NxBufferLayoutEmpty ); // Null vertex buffer EXPECT_THROW( vertexArray->addVertexBuffer(nullptr), - InvalidValue + NxInvalidValue ); } TEST_F(OpenGLTest, MultipleVertexBuffers) { - auto vertexArray = std::make_shared(); + auto vertexArray = std::make_shared(); float positions[] = {0.0f, 1.0f, 2.0f}; - auto positionBuffer = std::make_shared(positions, sizeof(positions)); + auto positionBuffer = std::make_shared(positions, sizeof(positions)); positionBuffer->setLayout({ - {ShaderDataType::FLOAT3, "Position"} + {NxShaderDataType::FLOAT3, "Position"} }); float colors[] = {1.0f, 0.0f, 0.0f}; - auto colorBuffer = std::make_shared(colors, sizeof(colors)); + auto colorBuffer = std::make_shared(colors, sizeof(colors)); colorBuffer->setLayout({ - {ShaderDataType::FLOAT3, "Color"} + {NxShaderDataType::FLOAT3, "Color"} }); vertexArray->addVertexBuffer(positionBuffer); @@ -161,10 +161,10 @@ namespace nexo::renderer { TEST_F(OpenGLTest, SetIndexBuffer) { - auto vertexArray = std::make_shared(); + auto vertexArray = std::make_shared(); unsigned int indices[] = {0, 1, 2}; - auto indexBuffer = std::make_shared(); + auto indexBuffer = std::make_shared(); indexBuffer->setData(indices, 3); vertexArray->setIndexBuffer(indexBuffer); @@ -183,11 +183,11 @@ namespace nexo::renderer { TEST_F(OpenGLTest, InvalidIndexBuffer) { - auto vertexArray = std::make_shared(); + auto vertexArray = std::make_shared(); EXPECT_THROW( vertexArray->setIndexBuffer(nullptr), - InvalidValue + NxInvalidValue ); } } diff --git a/tests/renderer/contexts/opengl.hpp b/tests/renderer/contexts/opengl.hpp index cab3b6e9c..d35e06c0d 100644 --- a/tests/renderer/contexts/opengl.hpp +++ b/tests/renderer/contexts/opengl.hpp @@ -28,7 +28,7 @@ namespace nexo::renderer { void SetUp() override { if (!glfwInit()) { - GTEST_SKIP() << "GLFW initialization failed. Skipping OpenGL tests."; + GTEST_FAIL() << "GLFW initialization failed. Failing OpenGL tests."; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); @@ -38,7 +38,7 @@ namespace nexo::renderer { window = glfwCreateWindow(800, 600, "Test Window", nullptr, nullptr); if (!window) { glfwTerminate(); - GTEST_SKIP() << "Failed to create GLFW window. Skipping OpenGL tests."; + GTEST_FAIL() << "Failed to create GLFW window. Failing OpenGL tests."; } glfwMakeContextCurrent(window); @@ -46,7 +46,7 @@ namespace nexo::renderer { if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { glfwDestroyWindow(window); glfwTerminate(); - GTEST_SKIP() << "Failed to initialize GLAD. Skipping OpenGL tests."; + GTEST_FAIL() << "Failed to initialize GLAD. Failing OpenGL tests."; } GLint major = 0, minor = 0; @@ -55,7 +55,7 @@ namespace nexo::renderer { if (major < 4 || (major == 4 && minor < 5)) { glfwDestroyWindow(window); glfwTerminate(); - GTEST_SKIP() << "OpenGL 4.5 is required. Skipping OpenGL tests."; + GTEST_FAIL() << "OpenGL 4.5 is required. Failing OpenGL tests."; } } @@ -67,17 +67,17 @@ namespace nexo::renderer { } }; - class MockVertexBuffer : public VertexBuffer { + class MockVertexBuffer : public NxVertexBuffer { public: MOCK_METHOD(void, bind, (), (const, override)); MOCK_METHOD(void, unbind, (), (const, override)); - MOCK_METHOD(void, setLayout, (const BufferLayout &layout), (override)); - MOCK_METHOD(BufferLayout, getLayout, (), (const, override)); + MOCK_METHOD(void, setLayout, (const NxBufferLayout &layout), (override)); + MOCK_METHOD(NxBufferLayout, getLayout, (), (const, override)); MOCK_METHOD(void, setData, (void *data, unsigned int size), (override)); MOCK_METHOD(unsigned int, getId, (), (const override)); }; - class MockIndexBuffer : public IndexBuffer { + class MockIndexBuffer : public NxIndexBuffer { public: MOCK_METHOD(void, bind, (), (const, override)); MOCK_METHOD(void, unbind, (), (const, override)); @@ -85,4 +85,4 @@ namespace nexo::renderer { MOCK_METHOD(unsigned int, getCount, (), (const, override)); MOCK_METHOD(unsigned int, getId, (), (const, override)); }; -} \ No newline at end of file +} diff --git a/vcpkg b/vcpkg index cd124b84f..96d5fb3de 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit cd124b84feb0c02a24a2d90981e8358fdee0e077 +Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b diff --git a/vcpkg.json b/vcpkg.json index 3b856270b..58965187d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,7 +3,7 @@ "version-string": "0.1.0", "description": "Nexo Engine", "documentation": "https://nexoengine.github.io/game-engine/", - "builtin-baseline": "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c", + "builtin-baseline": "96d5fb3de135b86d7222c53f2352ca92827a156b", "dependencies": [ { "name": "imgui", @@ -23,9 +23,11 @@ { "name": "glfw3", "version>=": "3.4#1" }, { "name": "glad", "version>=": "0.1.36#0" }, { "name": "stb", "version>=": "2024-07-29#1" }, - { "name": "tinyfiledialogs", "version>=": "3.8.8#4" }, + { "name": "tinyfiledialogs", "version>=": "3.19.1#0" }, { "name": "gtest", "version>=": "1.15.2#0" }, { "name": "assimp", "version>=": "5.4.3#0" }, - {"name": "nlohmann-json", "version>=": "3.11.3#1"} + { "name": "nlohmann-json", "version>=": "3.11.3#1"}, + { "name": "nethost", "version>=": "8.0.3#0"}, + { "name": "utfcpp", "version>=": "4.0.6#0"} ] }