From c3c0a9b66b89287e91596ed48746370809b0e83d Mon Sep 17 00:00:00 2001 From: Bosheng Li Date: Sat, 6 Jun 2026 13:08:50 -0700 Subject: [PATCH] Add launcher project hub and editor project flow --- .gitignore | 2 + AGENTS.md | 43 +- EvoEngine_App/CMakeLists.txt | 65 +- EvoEngine_App/include/LauncherUtils.hpp | 58 + EvoEngine_App/resources/EmptyApp.rc | 1 - EvoEngine_App/resources/EvoEngineEditor.rc | 1 + EvoEngine_App/resources/EvoEngineLauncher.rc | 1 + EvoEngine_App/src/DemoApp.cpp | 2 + EvoEngine_App/src/DigitalAgricultureApp.cpp | 2 + EvoEngine_App/src/EcoSysLabApp.cpp | 2 + EvoEngine_App/src/EmptyApp.cpp | 28 - EvoEngine_App/src/EvoEngineEditor.cpp | 136 ++ EvoEngine_App/src/EvoEngineLauncher.cpp | 720 +++++++++++ EvoEngine_App/src/LSystemApp.cpp | 2 + EvoEngine_App/src/LauncherUtils.cpp | 209 ++++ EvoEngine_App/src/LogGradingApp.cpp | 2 + EvoEngine_App/src/SorghumDataGeneratorApp.cpp | 2 + EvoEngine_App/src/TreeDataGeneratorApp.cpp | 2 + EvoEngine_Packages/LSystem/CMakeLists.txt | 34 + EvoEngine_Packages/LSystem/PackageInfo.cmake | 2 + .../LSystem/include/LSystem_PCH.hpp | 27 + .../LSystem/include/OrganInstanceChannel.hpp | 173 +++ .../LSystem/include/OrganStrandsChannel.hpp | 187 +++ .../LSystem/include/RenderPublishPolicy.hpp | 11 + .../LSystem/src/LSystemPackage.cpp | 36 + .../ApplicationInitializationSettings.hpp | 1 + EvoEngine_SDK/include/Core/ProjectManager.hpp | 47 + EvoEngine_SDK/include/Layers/EditorLayer.hpp | 26 +- EvoEngine_SDK/include/Layers/EditorTheme.hpp | 7 + EvoEngine_SDK/include/Layers/ImGuiLayer.hpp | 11 + EvoEngine_SDK/src/Application.cpp | 32 +- EvoEngine_SDK/src/Camera.cpp | 7 +- EvoEngine_SDK/src/EditorLayer.cpp | 1090 +++++++++-------- EvoEngine_SDK/src/EditorTheme.cpp | 195 +++ EvoEngine_SDK/src/ImGuiLayer.cpp | 20 + EvoEngine_SDK/src/Input.cpp | 3 + EvoEngine_SDK/src/Platform.cpp | 3 +- EvoEngine_SDK/src/ProjectManager.cpp | 276 ++++- EvoEngine_SDK/src/WindowLayer.cpp | 3 +- EvoEngine_Tests/CMakeLists.txt | 43 + EvoEngine_Tests/Core/AssetManagerTest.cpp | 252 ++++ EvoEngine_Tests/Core/LauncherUtilsTest.cpp | 152 +++ .../Launcher/launcher_process_smoke.py | 125 ++ EvoEngine_Tests/Launcher/launcher_ui_smoke.py | 233 ++++ PythonBinding/src/PyEcoSysLabModule.cpp | 5 +- PythonBinding/src/PyEvoEngine.cpp | 4 + README.md | 10 +- Resources/LSystemProject/test.eveproj | 5 + Scripts/install_apps.py | 32 +- 49 files changed, 3651 insertions(+), 679 deletions(-) create mode 100644 EvoEngine_App/include/LauncherUtils.hpp delete mode 100644 EvoEngine_App/resources/EmptyApp.rc create mode 100644 EvoEngine_App/resources/EvoEngineEditor.rc create mode 100644 EvoEngine_App/resources/EvoEngineLauncher.rc delete mode 100644 EvoEngine_App/src/EmptyApp.cpp create mode 100644 EvoEngine_App/src/EvoEngineEditor.cpp create mode 100644 EvoEngine_App/src/EvoEngineLauncher.cpp create mode 100644 EvoEngine_App/src/LauncherUtils.cpp create mode 100644 EvoEngine_Packages/LSystem/CMakeLists.txt create mode 100644 EvoEngine_Packages/LSystem/PackageInfo.cmake create mode 100644 EvoEngine_Packages/LSystem/include/LSystem_PCH.hpp create mode 100644 EvoEngine_Packages/LSystem/include/OrganInstanceChannel.hpp create mode 100644 EvoEngine_Packages/LSystem/include/OrganStrandsChannel.hpp create mode 100644 EvoEngine_Packages/LSystem/include/RenderPublishPolicy.hpp create mode 100644 EvoEngine_Packages/LSystem/src/LSystemPackage.cpp create mode 100644 EvoEngine_SDK/include/Layers/EditorTheme.hpp create mode 100644 EvoEngine_SDK/include/Layers/ImGuiLayer.hpp create mode 100644 EvoEngine_SDK/src/EditorTheme.cpp create mode 100644 EvoEngine_SDK/src/ImGuiLayer.cpp create mode 100644 EvoEngine_Tests/Core/LauncherUtilsTest.cpp create mode 100644 EvoEngine_Tests/Launcher/launcher_process_smoke.py create mode 100644 EvoEngine_Tests/Launcher/launcher_ui_smoke.py diff --git a/.gitignore b/.gitignore index d182a778..9abcac2d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ Resources/Example Projects # Prerequisites *.d +__pycache__/ +*.py[cod] # Compiled Object files *.slo diff --git a/AGENTS.md b/AGENTS.md index 5993fd27..45b6831b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,16 @@ -# AGENTS.md +# Guidelines -This file provides repository-wide guidance for coding agents working in EvoEngine. +## Before Writing Code + +**NEVER GUESS - ALWAYS VERIFY:** -## Branch Task Tracking +- Check codebase for existing solutions +- Read actual source code/docs to verify APIs, signatures, parameters +- Search for built-in features or libraries that already solve this +- Prefer language/framework built-ins over custom implementations +- When uncertain about behavior, write and run a small test script to verify instead of reasoning about it + +**When uncertain:** Ask before implementing if requirements are ambiguous, multiple approaches exist, or trade-offs are significant. When working on a new branch, create a local `./tasks` directory with these files: @@ -12,6 +20,34 @@ When working on a new branch, create a local `./tasks` directory with these file Use these files to keep a brief, current record of planned work, active work, and completed work for the branch. The `./tasks` directory is intentionally git-ignored and should remain local to the working branch/worktree. +Record the current branch at the top of `tasks/todo.md` as `# Branch: `. Before reusing an existing `./tasks` directory, compare that branch name with `git branch --show-current`. If `tasks/todo.md` names a different branch, remove the local `./tasks` directory and recreate fresh `todo.md`, `in-progress.md`, and `done.md` files for the current branch. If no branch is recorded yet and the tasks clearly belong to the current branch, add the branch line instead of deleting them. + +## Implementation + +**Write minimal, simple code:** + +- Minimize lines and complexity. Remove unnecessary variables and combine operations. +- Bias toward REMOVING code, not adding +- No new abstractions for one-time operations +- Minimize comments (only for non-obvious logic) +- Write testable code (prefer pure functions, dependency injection over globals) + +**Iterate toward perfection:** + +- Refactor and simplify code before finalizing. Aim for improvements to conciseness, readability, maintainability, and best practices with each pass. + +**Avoid scope creep:** + +- Minimal, targeted changes only +- No error handling for impossible scenarios + +## After Implementation + +- Grep for usages of any removed or renamed symbols to clean up dead references +- Add unit tests for new logic when a test framework is available + +This file provides repository-wide guidance for coding agents working in EvoEngine. + ## Commit Workflow When the user asks you to make a commit: @@ -20,3 +56,4 @@ When the user asks you to make a commit: - If a full test run is not practical or cannot be completed, run the most relevant subset and clearly report what was and was not verified. - Always update the README or other documentation when the change makes documentation inaccurate, incomplete, or missing. - Do not include signs of AI/tool usage in commit summaries, commit messages, pull request titles, or pull request descriptions. +- Make sure perform code format check right before commit. diff --git a/EvoEngine_App/CMakeLists.txt b/EvoEngine_App/CMakeLists.txt index a6aba4f2..d608c5d0 100644 --- a/EvoEngine_App/CMakeLists.txt +++ b/EvoEngine_App/CMakeLists.txt @@ -75,6 +75,12 @@ function(register_evoengine_app app_name default_enable) ${EVOENGINE_APP_SOURCE_DIR}/${app_name}.cpp ) endif() + if (${app_name} STREQUAL "EvoEngineLauncher") + target_sources(${app_name} + PRIVATE + ${EVOENGINE_APP_SOURCE_DIR}/LauncherUtils.cpp + ) + endif() target_compile_definitions(${app_name} PRIVATE ${DEFAULT_PROJECT_DEF} @@ -123,6 +129,9 @@ function(register_evoengine_app app_name default_enable) endif () endfunction() +register_evoengine_app(EvoEngineEditor ON) +register_evoengine_app(EvoEngineLauncher ON) + register_evoengine_app(DemoApp ON) register_evoengine_app(EcoSysLabApp ON) register_evoengine_app(DigitalAgricultureApp ON) @@ -132,62 +141,6 @@ register_evoengine_app(LSystemApp ON) register_evoengine_app(TreeDataGeneratorApp ON) register_evoengine_app(SorghumDataGeneratorApp ON) - - - -# Setup an empty application for quicker compilation for testing SDK -if (${CMAKE_BINARY_DIR} STREQUAL ${PROJECT_BINARY_DIR}) - set(option_name EvoEngine_App-EmptyApp) - option(${option_name} "Build Empty application" ON) - if(${option_name}) - if (WIN32) - add_executable(EmptyApp - ${EVOENGINE_APP_SOURCE_DIR}/EmptyApp.cpp - ${EVOENGINE_APP_RESOURCE_DIR}/EmptyApp.rc - ) - else() - add_executable(EmptyApp - ${EVOENGINE_APP_SOURCE_DIR}/EmptyApp.cpp - ) - endif() - target_compile_definitions(EmptyApp - PRIVATE - ${DEFAULT_PROJECT_DEF} - ${EVOENGINE_DEFS} - ) - target_include_directories(EmptyApp - PRIVATE - ${EVOENGINE_INCLUDES} - ${EVOENGINE_APP_RESOURCE_DIR} - ) - target_precompile_headers(EmptyApp - PRIVATE - ${EVOENGINE_PCHS} - ) - target_link_libraries(EmptyApp - PRIVATE - EvoEngine_SDK - ) - if (EVOENGINE_ENABLE_RUNTIME_PACKAGES AND EvoEngine_RuntimePackages) - add_dependencies(EmptyApp ${EvoEngine_RuntimePackages}) - endif() - - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) - target_compile_options(EmptyApp PRIVATE -Werror) - elseif (MSVC) - target_compile_options(EmptyApp PRIVATE /EHsc /W2 /c) - elseif (CMAKE_CXX_COMPILER_ID STREQUAL Clang) - target_compile_options(EmptyApp PRIVATE -fPIC --no-warnings -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-newline-eof -Wno-padded -Wno-exit-time-destructors -Wno-global-constructors -Wno-constant-conversion) - endif () - - add_dependencies(EmptyApp AppResourceCopy) - install(TARGETS EmptyApp - RUNTIME DESTINATION "${EVOENGINE_INSTALL_APP_RUNTIME_DIR}") - evoengine_install_target_pdb(EmptyApp "${EVOENGINE_INSTALL_APP_RUNTIME_DIR}") - set_property(TARGET EmptyApp PROPERTY FOLDER Executables) - endif () -endif () - if (${CMAKE_BINARY_DIR} STREQUAL ${PROJECT_BINARY_DIR}) evoengine_install_runtime_payload("${EVOENGINE_INSTALL_APP_RUNTIME_DIR}") install(FILES ${EVOENGINE_APP_RESOURCE_DIR}/imgui.ini diff --git a/EvoEngine_App/include/LauncherUtils.hpp b/EvoEngine_App/include/LauncherUtils.hpp new file mode 100644 index 00000000..a751f6f3 --- /dev/null +++ b/EvoEngine_App/include/LauncherUtils.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "PackageManager.hpp" +#include "ProjectManager.hpp" + +#include +#include +#include +#include + +namespace evo_engine::launcher { +constexpr size_t kMaxRecentProjectCount = 8; + +struct ProjectTemplate { + std::string name; + std::vector startup_runtime_packages; +}; + +struct DerivedProjectPath { + std::filesystem::path folder; + std::filesystem::path project_file; +}; + +using PackageAvailability = std::unordered_map; + +[[nodiscard]] const std::vector& ProjectTemplates(); +[[nodiscard]] ProjectLaunchMetadata BuildProjectLaunchMetadata(const std::string& project_name, + const ProjectTemplate& project_template); +[[nodiscard]] std::string Trim(const std::string& value); +[[nodiscard]] bool IsValidProjectName(const std::string& project_name); +[[nodiscard]] std::string JoinPackages(const std::vector& package_names); +[[nodiscard]] PackageAvailability BuildPackageAvailability(const std::vector& packages); +[[nodiscard]] bool IsPackageAvailable(const PackageAvailability& availability, const std::string& package_name); +[[nodiscard]] std::vector MissingPackages(const PackageAvailability& availability, + const std::vector& package_names); +[[nodiscard]] bool ArePackagesAvailable(const PackageAvailability& availability, + const std::vector& package_names); +[[nodiscard]] bool IsTemplateAvailable(const ProjectTemplate& project_template, + const PackageAvailability& availability); +[[nodiscard]] int SelectAvailableTemplateIndex(const std::vector& templates, + const PackageAvailability& availability, int selected_index); +[[nodiscard]] DerivedProjectPath BuildDerivedProjectPath(const std::filesystem::path& parent_folder, + const std::string& project_name); +[[nodiscard]] std::string ValidateCreateProjectRequest(const std::string& project_name, + const std::filesystem::path& parent_folder, + const std::filesystem::path& project_folder, + const std::filesystem::path& project_path, + const ProjectLaunchMetadata& metadata, + const PackageAvailability& availability); +[[nodiscard]] std::filesystem::path NormalizeProjectPath(const std::filesystem::path& path); +[[nodiscard]] std::vector LoadRecentProjects(const std::filesystem::path& settings_path, + bool& pruned, + size_t max_count = kMaxRecentProjectCount); +void SaveRecentProjects(const std::filesystem::path& settings_path, + const std::vector& recent_project_paths); +void AddRecentProject(std::vector& recent_project_paths, const std::filesystem::path& path, + size_t max_count = kMaxRecentProjectCount); +} // namespace evo_engine::launcher diff --git a/EvoEngine_App/resources/EmptyApp.rc b/EvoEngine_App/resources/EmptyApp.rc deleted file mode 100644 index ff49b83c..00000000 --- a/EvoEngine_App/resources/EmptyApp.rc +++ /dev/null @@ -1 +0,0 @@ -IDI_ICON1 ICON DISCARDABLE "EvoEngineApp.ico" \ No newline at end of file diff --git a/EvoEngine_App/resources/EvoEngineEditor.rc b/EvoEngine_App/resources/EvoEngineEditor.rc new file mode 100644 index 00000000..aa4dc835 --- /dev/null +++ b/EvoEngine_App/resources/EvoEngineEditor.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "EvoEngineApp.ico" diff --git a/EvoEngine_App/resources/EvoEngineLauncher.rc b/EvoEngine_App/resources/EvoEngineLauncher.rc new file mode 100644 index 00000000..aa4dc835 --- /dev/null +++ b/EvoEngine_App/resources/EvoEngineLauncher.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "EvoEngineApp.ico" diff --git a/EvoEngine_App/src/DemoApp.cpp b/EvoEngine_App/src/DemoApp.cpp index ae6e825f..cdc1ffcd 100644 --- a/EvoEngine_App/src/DemoApp.cpp +++ b/EvoEngine_App/src/DemoApp.cpp @@ -1,6 +1,7 @@ #include "Application.hpp" #include "DemoScene.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "Platform.hpp" #include "ProjectManager.hpp" #include "RenderLayer.hpp" @@ -175,6 +176,7 @@ int main(const int argc, char** argv) { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); #ifdef PHYSX_PHYSICS_SERVICE ApplicationContext::Get().PushLayer(); diff --git a/EvoEngine_App/src/DigitalAgricultureApp.cpp b/EvoEngine_App/src/DigitalAgricultureApp.cpp index 42daf649..3b15b3b0 100644 --- a/EvoEngine_App/src/DigitalAgricultureApp.cpp +++ b/EvoEngine_App/src/DigitalAgricultureApp.cpp @@ -13,6 +13,7 @@ #include "ProjectManager.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" using namespace evo_engine; @@ -70,6 +71,7 @@ int main() { ApplicationContext::Get().PushLayer("Ray Tracer Layer"); #endif ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); ApplicationInitializationSettings application_configs; diff --git a/EvoEngine_App/src/EcoSysLabApp.cpp b/EvoEngine_App/src/EcoSysLabApp.cpp index 4da108c9..d83dea5a 100644 --- a/EvoEngine_App/src/EcoSysLabApp.cpp +++ b/EvoEngine_App/src/EcoSysLabApp.cpp @@ -10,6 +10,7 @@ #include "ProjectManager.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" using namespace evo_engine; @@ -64,6 +65,7 @@ int main() { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); #ifdef PHYSX_PHYSICS_SERVICE diff --git a/EvoEngine_App/src/EmptyApp.cpp b/EvoEngine_App/src/EmptyApp.cpp deleted file mode 100644 index 17a01e7b..00000000 --- a/EvoEngine_App/src/EmptyApp.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "AnimationPlayer.hpp" -#include "Application.hpp" -#include "ClassRegistry.hpp" - -#include "EditorLayer.hpp" -#include "MeshRenderer.hpp" - -#include "PlayerController.hpp" -#include "Prefab.hpp" -#include "RenderLayer.hpp" - -#include "Times.hpp" -#include "WindowLayer.hpp" -using namespace evo_engine; -int main() { - Application application; - ApplicationContext::Get().PushLayer("Render Layer"); - ApplicationContext::Get().PushLayer("Window Layer"); - ApplicationContext::Get().PushLayer("Editor Layer"); - - ApplicationInitializationSettings application_info{}; - ApplicationContext::Get().Initialize(application_info); - - ApplicationContext::Get().Start(); - ApplicationContext::Get().Run(); - ApplicationContext::Get().Terminate(); - return 0; -} diff --git a/EvoEngine_App/src/EvoEngineEditor.cpp b/EvoEngine_App/src/EvoEngineEditor.cpp new file mode 100644 index 00000000..55465884 --- /dev/null +++ b/EvoEngine_App/src/EvoEngineEditor.cpp @@ -0,0 +1,136 @@ +#include "Application.hpp" +#include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" +#include "ProjectManager.hpp" +#include "RenderLayer.hpp" +#include "WindowLayer.hpp" + +#include +#include + +#ifdef EVOENGINE_WINDOWS +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#endif + +using namespace evo_engine; + +namespace { +std::optional ParseProjectPath(const int argc, char** argv) { + std::optional project_path; + for (int arg_index = 1; arg_index < argc; ++arg_index) { + const std::string argument = argv[arg_index] ? argv[arg_index] : ""; + if (argument == "--project" || argument == "-p") { + if (arg_index + 1 >= argc) { + throw std::invalid_argument(argument + " requires a project path."); + } + project_path = std::filesystem::absolute(argv[++arg_index]); + } else if (!project_path) { + project_path = std::filesystem::absolute(argument); + } else { + throw std::invalid_argument("Unknown EvoEngineEditor argument: " + argument); + } + } + return project_path; +} + +std::filesystem::path CurrentExecutablePath() { +#ifdef EVOENGINE_WINDOWS + std::wstring path(MAX_PATH, L'\0'); + const DWORD size = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (size == 0 || size == path.size()) { + return std::filesystem::absolute("EvoEngineEditor.exe"); + } + path.resize(size); + return path; +#else + return std::filesystem::absolute("EvoEngineEditor"); +#endif +} + +std::filesystem::path LauncherExecutablePath() { +#ifdef EVOENGINE_WINDOWS + return CurrentExecutablePath().parent_path() / "EvoEngineLauncher.exe"; +#else + return CurrentExecutablePath().parent_path() / "EvoEngineLauncher"; +#endif +} + +bool LaunchLauncherProcess(std::string& error) { + const auto launcher_path = LauncherExecutablePath(); + if (!std::filesystem::exists(launcher_path)) { + error = "Could not find EvoEngineLauncher next to EvoEngineEditor."; + return false; + } + +#ifdef EVOENGINE_WINDOWS + std::wstring command_line = L"\"" + launcher_path.wstring() + L"\""; + STARTUPINFOW startup_info{}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_info{}; + const auto working_directory = launcher_path.parent_path().wstring(); + if (!CreateProcessW(nullptr, command_line.data(), nullptr, nullptr, FALSE, 0, nullptr, working_directory.c_str(), + &startup_info, &process_info)) { + error = "Failed to launch EvoEngineLauncher."; + return false; + } + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + return true; +#else + const auto command = "\"" + launcher_path.string() + "\" &"; + if (std::system(command.c_str()) != 0) { + error = "Failed to launch EvoEngineLauncher."; + return false; + } + return true; +#endif +} +} // namespace + +int main(const int argc, char** argv) { + Application application; + bool initialized = false; + try { + const auto project_path = ParseProjectPath(argc, argv); + if (!project_path) { + std::string error; + if (!LaunchLauncherProcess(error)) { + EVOENGINE_ERROR(error) + return 1; + } + return 0; + } + if (project_path->extension() != ".eveproj") { + EVOENGINE_ERROR("EvoEngineEditor requires --project .") + return 1; + } + + ApplicationContext::Get().PushLayer("Render Layer"); + ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); + ApplicationContext::Get().PushLayer("Editor Layer"); + + ApplicationInitializationSettings application_info{}; + const auto launch_metadata = ProjectManager::LoadProjectLaunchMetadata(*project_path); + application_info.application_name = launch_metadata.application_name; + application_info.project_path = *project_path; + application_info.startup_runtime_packages = launch_metadata.startup_runtime_packages; + application_info.enable_runtime_packages = !application_info.startup_runtime_packages.empty(); + ApplicationContext::Get().Initialize(application_info); + initialized = true; + + ApplicationContext::Get().Start(); + ApplicationContext::Get().Run(); + ApplicationContext::Get().Terminate(); + return 0; + } catch (const std::exception& error) { + EVOENGINE_ERROR(error.what()) + if (initialized) { + ApplicationContext::Get().Terminate(); + } + return 1; + } +} diff --git a/EvoEngine_App/src/EvoEngineLauncher.cpp b/EvoEngine_App/src/EvoEngineLauncher.cpp new file mode 100644 index 00000000..b35b2ed4 --- /dev/null +++ b/EvoEngine_App/src/EvoEngineLauncher.cpp @@ -0,0 +1,720 @@ +#include "Application.hpp" +#include "ILayer.hpp" +#include "ImGuiLayer.hpp" +#include "LauncherUtils.hpp" +#include "PackageManager.hpp" +#include "ProjectManager.hpp" +#include "RenderLayer.hpp" +#include "Utilities.hpp" +#include "WindowLayer.hpp" + +#include +#include +#include +#include +#include + +#ifdef EVOENGINE_WINDOWS +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +#endif + +using namespace evo_engine; + +namespace { +std::filesystem::path LauncherSettingsPath() { +#ifdef EVOENGINE_WINDOWS + if (const char* local_app_data = std::getenv("LOCALAPPDATA")) { + return std::filesystem::path(local_app_data) / "EvoEngine" / "EditorSettings.yaml"; + } + if (const char* user_profile = std::getenv("USERPROFILE")) { + return std::filesystem::path(user_profile) / "AppData" / "Local" / "EvoEngine" / "EditorSettings.yaml"; + } +#else + if (const char* config_home = std::getenv("XDG_CONFIG_HOME")) { + return std::filesystem::path(config_home) / "EvoEngine" / "EditorSettings.yaml"; + } + if (const char* home = std::getenv("HOME")) { + return std::filesystem::path(home) / ".config" / "EvoEngine" / "EditorSettings.yaml"; + } +#endif + return std::filesystem::absolute("EvoEngineEditorSettings.yaml"); +} + +std::filesystem::path CurrentExecutablePath() { +#ifdef EVOENGINE_WINDOWS + std::wstring path(MAX_PATH, L'\0'); + const DWORD size = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (size == 0 || size == path.size()) { + return std::filesystem::absolute("EvoEngineLauncher.exe"); + } + path.resize(size); + return path; +#else + return std::filesystem::absolute("EvoEngineLauncher"); +#endif +} + +void AppendTestLog(const std::string& line) { + const char* log_path = std::getenv("EVOENGINE_LAUNCHER_TEST_LOG"); + if (!log_path || std::string(log_path).empty()) { + return; + } + std::ofstream log_file(log_path, std::ios::app); + log_file << line << "\n"; +} + +ImVec4 ColorTextMuted() { + return {0.62f, 0.66f, 0.72f, 1.0f}; +} + +ImVec4 ColorSuccess() { + return {0.35f, 0.78f, 0.48f, 1.0f}; +} + +ImVec4 ColorWarning() { + return {0.95f, 0.67f, 0.24f, 1.0f}; +} + +ImVec4 ColorError() { + return {1.0f, 0.35f, 0.35f, 1.0f}; +} + +ImVec4 ColorPanel() { + return {0.105f, 0.115f, 0.135f, 1.0f}; +} + +ImVec4 ColorPanelAlt() { + return {0.13f, 0.145f, 0.17f, 1.0f}; +} + +ImVec4 ColorBorder() { + return {0.25f, 0.28f, 0.33f, 1.0f}; +} + +constexpr const char* kRecentProjectsWindow = "Recent Projects"; +constexpr const char* kTemplateWindow = "Choose Template"; +constexpr const char* kAvailablePackagesWindow = "Available Packages"; +constexpr const char* kProjectDetailsWindow = "Project Details"; + +std::filesystem::path EditorExecutablePath() { +#ifdef EVOENGINE_WINDOWS + return CurrentExecutablePath().parent_path() / "EvoEngineEditor.exe"; +#else + return CurrentExecutablePath().parent_path() / "EvoEngineEditor"; +#endif +} + +bool LaunchEditorProcess(const std::filesystem::path& project_path, std::string& error) { + const auto editor_path = EditorExecutablePath(); + if (!std::filesystem::exists(editor_path)) { + error = "Could not find EvoEngineEditor next to EvoEngineLauncher."; + return false; + } + +#ifdef EVOENGINE_WINDOWS + std::wstring command_line = + L"\"" + editor_path.wstring() + L"\" --project \"" + std::filesystem::absolute(project_path).wstring() + L"\""; + STARTUPINFOW startup_info{}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_info{}; + const auto working_directory = editor_path.parent_path().wstring(); + if (!CreateProcessW(nullptr, command_line.data(), nullptr, nullptr, FALSE, 0, nullptr, working_directory.c_str(), + &startup_info, &process_info)) { + error = "Failed to launch EvoEngineEditor."; + return false; + } + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + return true; +#else + const auto command = + "\"" + editor_path.string() + "\" --project \"" + std::filesystem::absolute(project_path).string() + "\" &"; + if (std::system(command.c_str()) != 0) { + error = "Failed to launch EvoEngineEditor."; + return false; + } + return true; +#endif +} + +bool RevealProjectInExplorer(const std::filesystem::path& project_path, std::string& error) { + const auto absolute_path = std::filesystem::absolute(project_path); + if (!std::filesystem::exists(absolute_path) || std::filesystem::is_directory(absolute_path)) { + error = "Project file is missing."; + return false; + } +#ifdef EVOENGINE_WINDOWS + const auto parameters = L"/select,\"" + absolute_path.wstring() + L"\""; + const auto result = ShellExecuteW(nullptr, L"open", L"explorer.exe", parameters.c_str(), nullptr, SW_SHOWNORMAL); + if (reinterpret_cast(result) <= 32) { + error = "Failed to reveal project in Explorer."; + return false; + } + return true; +#else + error = "Reveal is only supported on Windows."; + return false; +#endif +} + +class LauncherLayer final : public ILayer { + protected: + void OnCreate() override { + if (const char* parent_folder = std::getenv("EVOENGINE_LAUNCHER_TEST_PARENT_FOLDER")) { + parent_folder_ = parent_folder; + } + RefreshPackageAvailability(); + LoadRecentProjects(); + AppendRecentProjectCountTestLog(); + AppendTemplateAvailabilityTestLog(); + AppendTestLog("mode:hub"); + if (const char* open_project = std::getenv("EVOENGINE_LAUNCHER_TEST_OPEN_PROJECT")) { + pending_test_open_project_ = open_project; + } + } + + void PreUpdate() override { + if (!pending_test_open_project_.empty()) { + const auto project_path = pending_test_open_project_; + pending_test_open_project_.clear(); + OpenProject(project_path); + return; + } + DrawMainMenuBar(); + DrawWorkspace(); + } + + private: + char project_name_[256] = {}; + int selected_project_template_index_ = 0; + std::filesystem::path parent_folder_; + std::string create_error_; + std::string launch_error_; + std::vector recent_project_paths_; + std::vector available_packages_; + launcher::PackageAvailability package_availability_; + bool dock_layout_dirty_ = true; + ImGuiID dock_space_id_ = 0; + std::filesystem::path pending_test_open_project_; + + void DrawMainMenuBar() { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("Project")) { + if (ImGui::MenuItem("Exit")) { + ApplicationContext::Get().End(); + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + ImGui::PopStyleVar(); + } + + void DrawWorkspace() { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + if (ImGui::Begin("Launcher Workspace", nullptr, flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 8.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ColorBorder()); + const ImVec2 workspace_size = ImGui::GetContentRegionAvail(); + const float padding = 28.0f; + ImGui::SetCursorPos(ImVec2(padding, 24.0f)); + DrawHeader(workspace_size.x - padding * 2.0f); + ImGui::SetCursorPos(ImVec2(0.0f, 92.0f)); + if (ImGui::BeginChild("LauncherDockHost", ImVec2(0.0f, 0.0f), false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + DrawLauncherDockspace(); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); + } + ImGui::End(); + ImGui::PopStyleVar(3); + + DrawProjectHub(); + } + + void DrawHeader(const float width) { + const float header_top = ImGui::GetCursorPosY(); + ImGui::BeginGroup(); + ImGui::TextUnformatted("EvoEngine Launcher"); + ImGui::TextColored(ColorTextMuted(), "Open an existing project or create a new workspace."); + ImGui::EndGroup(); + + ImGui::SetCursorPosY(header_top); + ImGui::SetCursorPosX(std::max(width - 136.0f, 0.0f)); + ImGui::PushID("HeaderOpenProject"); + FileUtils::OpenFile( + "Open Project", "Project", {".eveproj"}, + [this](const std::filesystem::path& path) { + OpenProject(path); + }, + false); + ImGui::PopID(); + ImGui::SameLine(); + if (ImGui::Button("Exit")) { + ApplicationContext::Get().End(); + } + } + + void DrawLauncherDockspace() { + dock_space_id_ = ImGui::GetID("LauncherDockSpace"); + const ImVec2 dock_size = ImGui::GetContentRegionAvail(); + if ((dock_layout_dirty_ || ImGui::DockBuilderGetNode(dock_space_id_) == nullptr) && dock_size.x > 1.0f && + dock_size.y > 1.0f) { + RebuildLauncherDockLayout(dock_size); + } + ImGui::DockSpace(dock_space_id_, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + } + + void RebuildLauncherDockLayout(const ImVec2& dock_size) { + ImGui::DockBuilderRemoveNode(dock_space_id_); + ImGui::DockBuilderAddNode(dock_space_id_, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dock_space_id_, dock_size); + ImGuiID main_node = dock_space_id_; + const ImGuiID recent_node = ImGui::DockBuilderSplitNode(main_node, ImGuiDir_Left, 0.30f, nullptr, &main_node); + const ImGuiID details_node = ImGui::DockBuilderSplitNode(main_node, ImGuiDir_Right, 0.32f, nullptr, &main_node); + const ImGuiID packages_node = ImGui::DockBuilderSplitNode(main_node, ImGuiDir_Down, 0.36f, nullptr, &main_node); + ImGui::DockBuilderDockWindow(kRecentProjectsWindow, recent_node); + ImGui::DockBuilderDockWindow(kTemplateWindow, main_node); + ImGui::DockBuilderDockWindow(kAvailablePackagesWindow, packages_node); + ImGui::DockBuilderDockWindow(kProjectDetailsWindow, details_node); + ImGui::DockBuilderFinish(dock_space_id_); + dock_layout_dirty_ = false; + } + + void DrawProjectHub() { + DrawDockedPanel(kRecentProjectsWindow, [this](const float width) { + DrawRecentProjectsPanel(width); + }); + DrawDockedPanel(kTemplateWindow, [this](const float width) { + DrawTemplateCards(width); + }); + DrawDockedPanel(kAvailablePackagesWindow, [this](const float width) { + DrawAvailablePackagesPanel(width); + }); + DrawDockedPanel(kProjectDetailsWindow, [this](const float width) { + DrawCreateProjectForm(ImVec2(width, 0.0f)); + }); + } + + template + void DrawDockedPanel(const char* title, DrawBody&& draw_body) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 12.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ColorPanel()); + ImGui::PushStyleColor(ImGuiCol_Border, ColorBorder()); + if (ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoCollapse)) { + draw_body(std::max(ImGui::GetContentRegionAvail().x, 260.0f)); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } + + void DrawRecentProjectsPanel(const float content_width) { + if (!launch_error_.empty()) { + ImGui::TextColored(ColorError(), "%s", launch_error_.c_str()); + } + if (ImGui::Button("Refresh", ImVec2(92.0f, 28.0f))) { + RefreshRecentProjects(); + } + ImGui::Spacing(); + if (recent_project_paths_.empty()) { + ImGui::Dummy(ImVec2(0.0f, 24.0f)); + ImGui::TextUnformatted("No recent projects"); + ImGui::TextColored(ColorTextMuted(), "Open a .eveproj file to add it here."); + ImGui::Spacing(); + ImGui::PushID("EmptyRecentOpenProject"); + FileUtils::OpenFile( + "Open Project", "Project", {".eveproj"}, + [this](const std::filesystem::path& path) { + OpenProject(path); + }, + false); + ImGui::PopID(); + return; + } + + if (ImGui::BeginChild("RecentProjectRows", ImVec2(content_width - 22.0f, 0.0f), false)) { + for (size_t i = 0; i < recent_project_paths_.size(); ++i) { + if (DrawRecentProjectRow(i, recent_project_paths_[i], content_width - 36.0f)) { + break; + } + } + } + ImGui::EndChild(); + } + + bool DrawRecentProjectRow(const size_t index, const std::filesystem::path& path, const float row_width) { + const bool available = std::filesystem::exists(path) && !std::filesystem::is_directory(path); + const auto metadata = ProjectManager::LoadProjectLaunchMetadata(path); + auto label = metadata.application_name; + if (label.empty() || label == "EvoEngine Editor") { + label = path.stem().string(); + } + if (!available) { + label += " (missing)"; + } + + ImGui::PushID(static_cast(index)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ColorPanelAlt()); + ImGui::BeginChild("RecentRow", ImVec2(row_width, 124.0f), true); + ImGui::TextUnformatted(label.c_str()); + ImGui::TextColored(ColorTextMuted(), "%s", path.parent_path().string().c_str()); + if (!metadata.startup_runtime_packages.empty()) { + const auto missing_packages = launcher::MissingPackages(package_availability_, metadata.startup_runtime_packages); + const auto package_text = launcher::JoinPackages(metadata.startup_runtime_packages); + if (missing_packages.empty()) { + ImGui::TextColored(ColorSuccess(), "Packages: %s", package_text.c_str()); + } else { + ImGui::TextColored(ColorWarning(), "Missing: %s", launcher::JoinPackages(missing_packages).c_str()); + } + } + if (!available) { + ImGui::TextColored(ColorWarning(), "Project file is missing."); + } + ImGui::Spacing(); + ImGui::BeginDisabled(!available); + const bool open_project = ImGui::Button("Open", ImVec2(58.0f, 26.0f)); + ImGui::SameLine(); + const bool reveal_project = ImGui::Button("Reveal", ImVec2(66.0f, 26.0f)); + ImGui::EndDisabled(); + ImGui::SameLine(); + const bool remove_project = ImGui::Button("Remove", ImVec2(74.0f, 26.0f)); + if (ImGui::IsWindowHovered()) { + ImGui::SetTooltip("%s", path.string().c_str()); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopID(); + if (open_project) { + OpenProject(path); + return true; + } + if (reveal_project) { + RevealProject(path); + } + if (remove_project) { + RemoveRecentProject(index); + return true; + } + return false; + } + + void DrawCreateProjectForm(const ImVec2& content_size) { + EnsureSelectedTemplateAvailable(); + ImGui::SetNextItemWidth(content_size.x); + ImGui::InputText("Project Name", project_name_, sizeof(project_name_)); + DrawTemplateSummary(SelectedProjectTemplate(), content_size.x); + FileUtils::OpenFolder( + "Parent Folder", + [this](const std::filesystem::path& path) { + parent_folder_ = path; + create_error_.clear(); + }, + false); + + const auto project_name = launcher::Trim(project_name_); + const auto derived_project_path = launcher::BuildDerivedProjectPath(parent_folder_, project_name); + ImGui::Spacing(); + ImGui::TextUnformatted("Project Path"); + ImGui::TextWrapped("%s", derived_project_path.project_file.string().c_str()); + if (!create_error_.empty()) { + ImGui::TextColored(ColorError(), "%s", create_error_.c_str()); + } + if (!launch_error_.empty()) { + ImGui::TextColored(ColorError(), "%s", launch_error_.c_str()); + } + + ImGui::Spacing(); + if (ImGui::Button("Create", ImVec2(120.0f, 32.0f))) { + CreateProject(project_name, derived_project_path.folder, derived_project_path.project_file, + launcher::BuildProjectLaunchMetadata(project_name, SelectedProjectTemplate())); + } + } + + const launcher::ProjectTemplate& SelectedProjectTemplate() const { + const auto& templates = launcher::ProjectTemplates(); + if (selected_project_template_index_ < 0 || + selected_project_template_index_ >= static_cast(templates.size())) { + return templates.front(); + } + return templates[static_cast(selected_project_template_index_)]; + } + + void DrawTemplateCards(const float content_width) { + const auto& templates = launcher::ProjectTemplates(); + const bool two_columns = content_width >= 500.0f; + const float card_width = two_columns ? (content_width - 42.0f) * 0.5f : content_width - 22.0f; + for (size_t i = 0; i < templates.size(); ++i) { + if (two_columns && i % 2 == 1) { + ImGui::SameLine(); + } + DrawTemplateCard(i, card_width); + } + } + + void DrawTemplateCard(const size_t template_index, const float width) { + const auto& project_template = launcher::ProjectTemplates()[template_index]; + const bool selected = selected_project_template_index_ == static_cast(template_index); + const bool available = IsTemplateAvailable(project_template); + ImGui::PushID(static_cast(template_index)); + ImGui::PushStyleColor(ImGuiCol_Border, selected ? ImVec4(0.42f, 0.63f, 0.92f, 1.0f) : ColorBorder()); + ImGui::PushStyleColor(ImGuiCol_ChildBg, selected ? ImVec4(0.15f, 0.18f, 0.23f, 1.0f) : ColorPanelAlt()); + ImGui::BeginChild("TemplateCard", ImVec2(width, 124.0f), true); + ImGui::TextUnformatted(project_template.name.c_str()); + if (project_template.startup_runtime_packages.empty()) { + ImGui::TextColored(ColorTextMuted(), "No runtime packages"); + } else { + ImGui::TextColored(ColorTextMuted(), "Required packages:"); + ImGui::TextWrapped("%s", launcher::JoinPackages(project_template.startup_runtime_packages).c_str()); + if (available) { + ImGui::TextColored(ColorSuccess(), "Packages available"); + } else { + ImGui::TextColored(ColorWarning(), "Missing packages: %s", + launcher::JoinPackages(launcher::MissingPackages(package_availability_, + project_template.startup_runtime_packages)) + .c_str()); + } + } + ImGui::EndChild(); + const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled); + if (hovered && available && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + selected_project_template_index_ = static_cast(template_index); + create_error_.clear(); + } + if (hovered && !available) { + ImGui::SetTooltip("Missing packages: %s", + launcher::JoinPackages( + launcher::MissingPackages(package_availability_, project_template.startup_runtime_packages)) + .c_str()); + } + ImGui::PopStyleColor(2); + ImGui::PopID(); + } + + bool IsTemplateAvailable(const launcher::ProjectTemplate& project_template) const { + return launcher::IsTemplateAvailable(project_template, package_availability_); + } + + void EnsureSelectedTemplateAvailable() { + const int selected_index = launcher::SelectAvailableTemplateIndex( + launcher::ProjectTemplates(), package_availability_, selected_project_template_index_); + if (selected_index != selected_project_template_index_) { + selected_project_template_index_ = selected_index; + create_error_.clear(); + } + } + + void DrawPackageStatusList(const std::vector& package_names) const { + for (const auto& package_name : package_names) { + const bool available = launcher::IsPackageAvailable(package_availability_, package_name); + ImGui::TextColored(available ? ColorSuccess() : ColorWarning(), "%s: %s", package_name.c_str(), + available ? "available" : "missing"); + } + } + + void DrawTemplateSummary(const launcher::ProjectTemplate& project_template, const float width) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ColorPanelAlt()); + ImGui::BeginChild("TemplateSummary", + ImVec2(width, project_template.startup_runtime_packages.empty() ? 72.0f : 112.0f), true); + ImGui::TextUnformatted(project_template.name.c_str()); + ImGui::TextColored( + ColorTextMuted(), "%s", + project_template.startup_runtime_packages.empty() ? "Generic project" : "Package-backed template"); + if (project_template.startup_runtime_packages.empty()) { + ImGui::TextColored(ColorTextMuted(), "Packages: none"); + } else { + DrawPackageStatusList(project_template.startup_runtime_packages); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + void DrawAvailablePackagesPanel(const float content_width) { + if (ImGui::Button("Refresh Packages", ImVec2(138.0f, 28.0f))) { + RefreshPackageAvailability(); + } + ImGui::SameLine(); + ImGui::TextColored(ColorTextMuted(), "%zu manifest%s", available_packages_.size(), + available_packages_.size() == 1 ? "" : "s"); + ImGui::Spacing(); + + const auto& selected_template = SelectedProjectTemplate(); + if (selected_template.startup_runtime_packages.empty()) { + ImGui::TextColored(ColorTextMuted(), "Selected template does not require runtime packages."); + } else { + ImGui::TextUnformatted("Selected template requires:"); + DrawPackageStatusList(selected_template.startup_runtime_packages); + } + ImGui::Separator(); + + if (available_packages_.empty()) { + ImGui::TextUnformatted("No package manifests found."); + return; + } + if (ImGui::BeginChild("AvailablePackageRows", ImVec2(0.0f, 0.0f), false)) { + for (const auto& package : available_packages_) { + DrawAvailablePackageRow(package, content_width - 16.0f); + } + } + ImGui::EndChild(); + } + + void DrawAvailablePackageRow(const AvailablePackageInfo& package, const float width) const { + ImGui::PushID(package.name.c_str()); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ColorPanelAlt()); + ImGui::BeginChild("PackageRow", ImVec2(width, 42.0f), true); + ImGui::TextUnformatted(package.name.c_str()); + ImGui::SameLine(); + ImGui::TextColored(ColorTextMuted(), "Version: %s", package.version.empty() ? "unknown" : package.version.c_str()); + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + + void RefreshPackageAvailability() { + PackageManager::ScanAvailablePackages(); + available_packages_ = PackageManager::GetAvailablePackages(); + package_availability_ = launcher::BuildPackageAvailability(available_packages_); + EnsureSelectedTemplateAvailable(); + } + + void CreateProject(const std::string& project_name, const std::filesystem::path& project_folder, + const std::filesystem::path& project_path, const ProjectLaunchMetadata& metadata) { + create_error_.clear(); + launch_error_.clear(); + create_error_ = launcher::ValidateCreateProjectRequest(project_name, parent_folder_, project_folder, project_path, + metadata, package_availability_); + if (create_error_.empty()) { + try { + std::filesystem::create_directories(project_folder); + ProjectManager::SaveProjectLaunchMetadata(project_path, metadata); + OpenProject(project_path); + } catch (const std::exception& error) { + create_error_ = error.what(); + } + } + } + + void OpenProject(const std::filesystem::path& path) { + const auto project_path = launcher::NormalizeProjectPath(path); + launch_error_.clear(); + if (project_path.extension() != ".eveproj" || std::filesystem::is_directory(project_path)) { + launch_error_ = "Select a valid .eveproj file."; + return; + } + + std::string error; + if (!LaunchEditorProcess(project_path, error)) { + launch_error_ = error; + return; + } + + AddRecentProject(project_path); + ApplicationContext::Get().End(); + } + + void LoadRecentProjects() { + const auto settings_path = LauncherSettingsPath(); + bool pruned = false; + recent_project_paths_ = launcher::LoadRecentProjects(settings_path, pruned); + if (pruned) { + SaveRecentProjects(); + } + } + + void RefreshRecentProjects() { + LoadRecentProjects(); + AppendRecentProjectCountTestLog(); + launch_error_.clear(); + } + + void SaveRecentProjects() const { + try { + launcher::SaveRecentProjects(LauncherSettingsPath(), recent_project_paths_); + } catch (const std::exception&) { + } + } + + void AppendTemplateAvailabilityTestLog() const { + for (const auto& project_template : launcher::ProjectTemplates()) { + AppendTestLog("template:" + project_template.name + ":" + + (IsTemplateAvailable(project_template) ? "available" : "missing")); + } + } + + void AppendRecentProjectCountTestLog() const { + AppendTestLog("recent-count:" + std::to_string(recent_project_paths_.size())); + } + + void AddRecentProject(const std::filesystem::path& path) { + launcher::AddRecentProject(recent_project_paths_, path); + SaveRecentProjects(); + AppendRecentProjectCountTestLog(); + } + + void RemoveRecentProject(const size_t index) { + if (index >= recent_project_paths_.size()) { + return; + } + recent_project_paths_.erase(recent_project_paths_.begin() + static_cast(index)); + SaveRecentProjects(); + AppendRecentProjectCountTestLog(); + } + + void RevealProject(const std::filesystem::path& path) { + launch_error_.clear(); + std::string error; + if (!RevealProjectInExplorer(path, error)) { + launch_error_ = error; + } + } +}; +} // namespace + +int main() { + Application application; + bool initialized = false; + try { + ApplicationContext::Get().PushLayer("Render Layer"); + ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); + ApplicationContext::Get().PushLayer("Launcher Layer"); + + ApplicationInitializationSettings application_info{}; + application_info.application_name = "EvoEngine Launcher"; + application_info.allow_empty_project = true; + ApplicationContext::Get().Initialize(application_info); + initialized = true; + + ApplicationContext::Get().Start(); + ApplicationContext::Get().Run(); + ApplicationContext::Get().Terminate(); + return 0; + } catch (const std::exception& error) { + EVOENGINE_ERROR(error.what()) + if (initialized) { + ApplicationContext::Get().Terminate(); + } + return 1; + } +} diff --git a/EvoEngine_App/src/LSystemApp.cpp b/EvoEngine_App/src/LSystemApp.cpp index 0ab1d566..c59054d4 100644 --- a/EvoEngine_App/src/LSystemApp.cpp +++ b/EvoEngine_App/src/LSystemApp.cpp @@ -14,6 +14,7 @@ #include "ProjectManager.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" @@ -62,6 +63,7 @@ int main() { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); ApplicationInitializationSettings application_configs; diff --git a/EvoEngine_App/src/LauncherUtils.cpp b/EvoEngine_App/src/LauncherUtils.cpp new file mode 100644 index 00000000..83528610 --- /dev/null +++ b/EvoEngine_App/src/LauncherUtils.cpp @@ -0,0 +1,209 @@ +#include "LauncherUtils.hpp" +#include "EvoEngine_SDK_PCH.hpp" + +#include +#include +#include + +using namespace evo_engine; + +namespace evo_engine::launcher { +const std::vector& ProjectTemplates() { + static const std::vector project_templates = {{"Generic", {}}, + {"LSystem", {"LSystem", "DigitalAgriculture"}}, + {"EcoSysLab", {"EcoSysLab"}}, + {"Digital Agriculture", {"DigitalAgriculture"}}, + {"Log Grading", {"LogGrading"}}}; + return project_templates; +} + +ProjectLaunchMetadata BuildProjectLaunchMetadata(const std::string& project_name, + const ProjectTemplate& project_template) { + ProjectLaunchMetadata metadata; + metadata.application_name = project_name; + metadata.preferred_editor = "EvoEngineEditor"; + metadata.startup_runtime_packages = project_template.startup_runtime_packages; + return metadata; +} + +std::string Trim(const std::string& value) { + const auto begin = std::find_if_not(value.begin(), value.end(), [](const unsigned char c) { + return std::isspace(c); + }); + const auto end = std::find_if_not(value.rbegin(), value.rend(), [](const unsigned char c) { + return std::isspace(c); + }).base(); + if (begin >= end) { + return ""; + } + return {begin, end}; +} + +bool IsValidProjectName(const std::string& project_name) { + if (project_name.empty()) { + return false; + } + static constexpr std::string_view invalid_chars = R"(<>:"/\|?*)"; + return project_name.find_first_of(invalid_chars) == std::string::npos; +} + +std::string JoinPackages(const std::vector& package_names) { + std::string text; + for (const auto& package_name : package_names) { + if (!text.empty()) { + text += ", "; + } + text += package_name; + } + return text; +} + +PackageAvailability BuildPackageAvailability(const std::vector& packages) { + PackageAvailability availability; + for (const auto& package : packages) { + availability[package.name] = package.library_exists; + } + return availability; +} + +bool IsPackageAvailable(const PackageAvailability& availability, const std::string& package_name) { + const auto search = availability.find(package_name); + return search != availability.end() && search->second; +} + +std::vector MissingPackages(const PackageAvailability& availability, + const std::vector& package_names) { + std::vector missing_packages; + for (const auto& package_name : package_names) { + if (!IsPackageAvailable(availability, package_name)) { + missing_packages.emplace_back(package_name); + } + } + return missing_packages; +} + +bool ArePackagesAvailable(const PackageAvailability& availability, const std::vector& package_names) { + return MissingPackages(availability, package_names).empty(); +} + +bool IsTemplateAvailable(const ProjectTemplate& project_template, const PackageAvailability& availability) { + return project_template.startup_runtime_packages.empty() || + ArePackagesAvailable(availability, project_template.startup_runtime_packages); +} + +int SelectAvailableTemplateIndex(const std::vector& templates, const PackageAvailability& availability, + const int selected_index) { + if (selected_index < 0 || selected_index >= static_cast(templates.size()) || + !IsTemplateAvailable(templates[static_cast(selected_index)], availability)) { + return 0; + } + return selected_index; +} + +DerivedProjectPath BuildDerivedProjectPath(const std::filesystem::path& parent_folder, + const std::string& project_name) { + const auto project_folder = parent_folder / project_name; + return {project_folder, project_folder / (project_name + ".eveproj")}; +} + +std::string ValidateCreateProjectRequest(const std::string& project_name, const std::filesystem::path& parent_folder, + const std::filesystem::path& project_folder, + const std::filesystem::path& project_path, + const ProjectLaunchMetadata& metadata, + const PackageAvailability& availability) { + if (!IsValidProjectName(project_name)) { + return "Project name is empty or contains invalid filename characters."; + } + if (!ArePackagesAvailable(availability, metadata.startup_runtime_packages)) { + return "Selected template has missing runtime packages."; + } + if (parent_folder.empty() || !std::filesystem::exists(parent_folder) || + !std::filesystem::is_directory(parent_folder)) { + return "Select an existing parent folder."; + } + if (std::filesystem::exists(project_folder)) { + return "Project folder already exists."; + } + if (std::filesystem::exists(project_path)) { + return "Project file already exists."; + } + return ""; +} + +std::filesystem::path NormalizeProjectPath(const std::filesystem::path& path) { + return std::filesystem::absolute(path).lexically_normal(); +} + +std::vector LoadRecentProjects(const std::filesystem::path& settings_path, bool& pruned, + const size_t max_count) { + std::vector recent_project_paths; + pruned = false; + if (!std::filesystem::exists(settings_path)) { + return recent_project_paths; + } + + try { + const auto in = YAML::LoadFile(settings_path.string()); + const auto recent_projects = in["recent_projects"]; + if (!recent_projects || !recent_projects.IsSequence()) { + return recent_project_paths; + } + for (const auto& entry : recent_projects) { + if (!entry.IsScalar()) { + pruned = true; + continue; + } + const auto path = NormalizeProjectPath(entry.as()); + if (path.extension() != ".eveproj" || !std::filesystem::exists(path) || std::filesystem::is_directory(path)) { + pruned = true; + continue; + } + if (std::find(recent_project_paths.begin(), recent_project_paths.end(), path) == recent_project_paths.end()) { + recent_project_paths.emplace_back(path); + } else { + pruned = true; + } + if (recent_project_paths.size() >= max_count) { + break; + } + } + } catch (const std::exception&) { + recent_project_paths.clear(); + } + return recent_project_paths; +} + +void SaveRecentProjects(const std::filesystem::path& settings_path, + const std::vector& recent_project_paths) { + if (!settings_path.parent_path().empty()) { + std::filesystem::create_directories(settings_path.parent_path()); + } + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "recent_projects" << YAML::Value << YAML::BeginSeq; + for (const auto& path : recent_project_paths) { + out << path.string(); + } + out << YAML::EndSeq; + out << YAML::EndMap; + + std::ofstream file_out(settings_path.string()); + file_out << out.c_str(); +} + +void AddRecentProject(std::vector& recent_project_paths, const std::filesystem::path& path, + const size_t max_count) { + const auto project_path = NormalizeProjectPath(path); + if (project_path.extension() != ".eveproj" || std::filesystem::is_directory(project_path)) { + return; + } + + recent_project_paths.erase(std::remove(recent_project_paths.begin(), recent_project_paths.end(), project_path), + recent_project_paths.end()); + recent_project_paths.insert(recent_project_paths.begin(), project_path); + if (recent_project_paths.size() > max_count) { + recent_project_paths.resize(max_count); + } +} +} // namespace evo_engine::launcher diff --git a/EvoEngine_App/src/LogGradingApp.cpp b/EvoEngine_App/src/LogGradingApp.cpp index 9ce8fe13..19e84486 100644 --- a/EvoEngine_App/src/LogGradingApp.cpp +++ b/EvoEngine_App/src/LogGradingApp.cpp @@ -5,6 +5,7 @@ #include "ClassRegistry.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" using namespace evo_engine; @@ -56,6 +57,7 @@ int main() { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); ApplicationInitializationSettings application_configs; application_configs.application_name = "Log Grader"; diff --git a/EvoEngine_App/src/SorghumDataGeneratorApp.cpp b/EvoEngine_App/src/SorghumDataGeneratorApp.cpp index bebfde2a..eed39003 100644 --- a/EvoEngine_App/src/SorghumDataGeneratorApp.cpp +++ b/EvoEngine_App/src/SorghumDataGeneratorApp.cpp @@ -1,6 +1,7 @@ #include "Application.hpp" #include "ClassRegistry.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "ProjectManager.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" @@ -27,6 +28,7 @@ int main() { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); ApplicationInitializationSettings application_info{}; diff --git a/EvoEngine_App/src/TreeDataGeneratorApp.cpp b/EvoEngine_App/src/TreeDataGeneratorApp.cpp index f36f7520..fa08c3c8 100644 --- a/EvoEngine_App/src/TreeDataGeneratorApp.cpp +++ b/EvoEngine_App/src/TreeDataGeneratorApp.cpp @@ -1,6 +1,7 @@ #include "Application.hpp" #include "ClassRegistry.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "ProjectManager.hpp" #include "RenderLayer.hpp" #include "WindowLayer.hpp" @@ -27,6 +28,7 @@ int main() { ApplicationContext::Get().PushLayer("Render Layer"); ApplicationContext::Get().PushLayer("Window Layer"); + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); ApplicationInitializationSettings application_info{}; diff --git a/EvoEngine_Packages/LSystem/CMakeLists.txt b/EvoEngine_Packages/LSystem/CMakeLists.txt new file mode 100644 index 00000000..311f6995 --- /dev/null +++ b/EvoEngine_Packages/LSystem/CMakeLists.txt @@ -0,0 +1,34 @@ +set(LSYSTEM_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +set(LSYSTEM_INCLUDES + ${LSYSTEM_DIRECTORY}/include + ) + +file(GLOB_RECURSE LSYSTEM_HEADERS ${LSYSTEM_DIRECTORY}/include/*.hpp) +file(GLOB_RECURSE LSYSTEM_SOURCES ${LSYSTEM_DIRECTORY}/src/*.cpp) + +add_library(LSystemPackage + SHARED + ${LSYSTEM_HEADERS} + ${LSYSTEM_SOURCES} + ) +target_compile_definitions(LSystemPackage + PUBLIC + LSYSTEM_PACKAGE + PRIVATE + ${EVOENGINE_SDK_DEFS} + ) +target_include_directories(LSystemPackage + PUBLIC + ${EVOENGINE_INCLUDES} + ${LSYSTEM_INCLUDES} + ) +target_link_libraries(LSystemPackage + PRIVATE + EvoEngine_SDK + ) + +set(LSYSTEM_PCH ${LSYSTEM_DIRECTORY}/include/LSystem_PCH.hpp) +evoengine_configure_runtime_package(LSystemPackage + EXTRA_PCH ${LSYSTEM_PCH} + ) diff --git a/EvoEngine_Packages/LSystem/PackageInfo.cmake b/EvoEngine_Packages/LSystem/PackageInfo.cmake new file mode 100644 index 00000000..6455e9f9 --- /dev/null +++ b/EvoEngine_Packages/LSystem/PackageInfo.cmake @@ -0,0 +1,2 @@ +set(EVOENGINE_PACKAGE_VERSION 0.1.0) +set(EVOENGINE_PACKAGE_DESCRIPTION "L-system grammar runtime package (plant-agnostic core).") diff --git a/EvoEngine_Packages/LSystem/include/LSystem_PCH.hpp b/EvoEngine_Packages/LSystem/include/LSystem_PCH.hpp new file mode 100644 index 00000000..3f787f12 --- /dev/null +++ b/EvoEngine_Packages/LSystem/include/LSystem_PCH.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace l_system_package { +inline std::uint64_t HashBytes(const void* data, const std::size_t byte_count) { + constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL; + constexpr std::uint64_t kPrime = 1099511628211ULL; + std::uint64_t hash = kOffsetBasis; + const auto* bytes = static_cast(data); + for (std::size_t i = 0; i < byte_count; ++i) { + hash ^= bytes[i]; + hash *= kPrime; + } + return hash; +} +} // namespace l_system_package diff --git a/EvoEngine_Packages/LSystem/include/OrganInstanceChannel.hpp b/EvoEngine_Packages/LSystem/include/OrganInstanceChannel.hpp new file mode 100644 index 00000000..e2f4c80d --- /dev/null +++ b/EvoEngine_Packages/LSystem/include/OrganInstanceChannel.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include "LSystem_PCH.hpp" +#include "RenderPublishPolicy.hpp" + +#include "AssetManager.hpp" +#include "Material.hpp" +#include "Mesh.hpp" +#include "Particles.hpp" +#include "Scene.hpp" + +namespace l_system_package { +using namespace evo_engine; + +class OrganInstanceChannel { + public: + struct Stats { + std::size_t high_water_instance_count = 0; + std::uint64_t flush_count = 0; + std::uint64_t skipped_by_dedup = 0; + std::uint64_t skipped_by_rate_limit = 0; + }; + + OrganInstanceChannel(const std::shared_ptr& scene, const Entity& parent, const std::string& name, + std::shared_ptr instance_mesh, std::shared_ptr instance_material) + : scene_(scene), + name_(name), + instance_mesh_(std::move(instance_mesh)), + instance_material_(std::move(instance_material)) { + entity_ = scene->CreateEntity(name); + scene->SetParent(entity_, parent); + particle_info_list_ = AssetManager::CreateTemporaryAsset(); + particles_ = scene->GetOrSetPrivateComponent(entity_).lock(); + particles_->mesh = instance_mesh_; + particles_->material = instance_material_; + particles_->particle_info_list = particle_info_list_; + } + + OrganInstanceChannel(const OrganInstanceChannel&) = delete; + OrganInstanceChannel& operator=(const OrganInstanceChannel&) = delete; + OrganInstanceChannel(OrganInstanceChannel&&) = delete; + OrganInstanceChannel& operator=(OrganInstanceChannel&&) = delete; + + ~OrganInstanceChannel() { + if (const auto scene = scene_.lock()) { + if (scene->IsEntityValid(entity_)) { + scene->DeleteEntity(entity_); + } + } + } + + void Stage(std::vector particle_infos) { + const std::uint64_t payload_hash = HashBytes(particle_infos.data(), particle_infos.size() * sizeof(ParticleInfo)); + { + std::lock_guard guard(pending_mutex_); + pending_.particle_infos = std::move(particle_infos); + pending_.hash = payload_hash; + pending_.present = true; + } + if (!policy.defer_to_main_thread) { + Flush(); + } + } + + void Publish(std::vector particle_infos) { + Stage(std::move(particle_infos)); + Flush(); + } + + void Clear() { + Stage({}); + } + + bool Flush() { + PendingPayload payload; + { + std::lock_guard guard(pending_mutex_); + if (!pending_.present) { + return false; + } + payload = std::move(pending_); + pending_ = PendingPayload{}; + } + + if (policy.deduplicate_identical_payloads && payload.hash == last_published_hash_ && + payload.particle_infos.size() == last_published_instance_count_) { + ++stats_.skipped_by_dedup; + return false; + } + + if (policy.min_republish_interval_seconds > 0.0f) { + const double now = NowSeconds(); + if (now - last_publish_time_seconds_ < static_cast(policy.min_republish_interval_seconds)) { + std::lock_guard guard(pending_mutex_); + if (!pending_.present) { + pending_ = std::move(payload); + } + ++stats_.skipped_by_rate_limit; + return false; + } + last_publish_time_seconds_ = now; + } + + if (!particle_info_list_) { + return false; + } + particle_info_list_->SetParticleInfos(payload.particle_infos); + + last_published_hash_ = payload.hash; + last_published_instance_count_ = payload.particle_infos.size(); + stats_.high_water_instance_count = std::max(stats_.high_water_instance_count, payload.particle_infos.size()); + ++stats_.flush_count; + return true; + } + + [[nodiscard]] bool HasPending() const { + std::lock_guard guard(pending_mutex_); + return pending_.present; + } + + [[nodiscard]] Entity GetEntity() const noexcept { + return entity_; + } + [[nodiscard]] const std::shared_ptr& GetInstanceMesh() const noexcept { + return instance_mesh_; + } + [[nodiscard]] const std::shared_ptr& GetInstanceMaterial() const noexcept { + return instance_material_; + } + [[nodiscard]] const std::shared_ptr& GetParticleInfoList() const noexcept { + return particle_info_list_; + } + [[nodiscard]] const std::string& GetName() const noexcept { + return name_; + } + [[nodiscard]] Stats GetStats() const noexcept { + return stats_; + } + + RenderPublishPolicy policy{}; + + private: + struct PendingPayload { + std::vector particle_infos; + std::uint64_t hash = 0; + bool present = false; + }; + + static double NowSeconds() { + using clock = std::chrono::steady_clock; + const auto now = clock::now().time_since_epoch(); + return std::chrono::duration(now).count(); + } + + std::weak_ptr scene_; + std::string name_; + Entity entity_{}; + std::shared_ptr instance_mesh_; + std::shared_ptr instance_material_; + std::shared_ptr particle_info_list_; + std::shared_ptr particles_; + + mutable std::mutex pending_mutex_; + PendingPayload pending_; + + std::uint64_t last_published_hash_ = 0; + std::size_t last_published_instance_count_ = 0; + double last_publish_time_seconds_ = -1.0e18; + + Stats stats_{}; +}; + +} // namespace l_system_package diff --git a/EvoEngine_Packages/LSystem/include/OrganStrandsChannel.hpp b/EvoEngine_Packages/LSystem/include/OrganStrandsChannel.hpp new file mode 100644 index 00000000..4f133d13 --- /dev/null +++ b/EvoEngine_Packages/LSystem/include/OrganStrandsChannel.hpp @@ -0,0 +1,187 @@ +#pragma once + +#include "LSystem_PCH.hpp" +#include "RenderPublishPolicy.hpp" + +#include "AssetManager.hpp" +#include "Material.hpp" +#include "Scene.hpp" +#include "Strands.hpp" +#include "StrandsRenderer.hpp" + +namespace l_system_package { +using namespace evo_engine; + +class OrganStrandsChannel { + public: + struct Stats { + std::size_t high_water_point_count = 0; + std::size_t high_water_index_count = 0; + std::uint64_t flush_count = 0; + std::uint64_t skipped_by_dedup = 0; + std::uint64_t skipped_by_rate_limit = 0; + }; + + OrganStrandsChannel(const std::shared_ptr& scene, const Entity& parent, const std::string& name) + : scene_(scene), name_(name) { + entity_ = scene->CreateEntity(name); + scene->SetParent(entity_, parent); + strands_ = AssetManager::CreateTemporaryAsset(); + material_ = AssetManager::CreateTemporaryAsset(); + const auto renderer = scene->GetOrSetPrivateComponent(entity_).lock(); + renderer->strands = strands_; + renderer->material = material_; + } + + OrganStrandsChannel(const OrganStrandsChannel&) = delete; + OrganStrandsChannel& operator=(const OrganStrandsChannel&) = delete; + OrganStrandsChannel(OrganStrandsChannel&&) = delete; + OrganStrandsChannel& operator=(OrganStrandsChannel&&) = delete; + + ~OrganStrandsChannel() { + if (const auto scene = scene_.lock()) { + if (scene->IsEntityValid(entity_)) { + scene->DeleteEntity(entity_); + } + } + } + + void StageSegments(StrandPointAttributes attributes, std::vector segments, + std::vector points) { + Stage(PublishMode::Segments, attributes, std::move(segments), std::move(points)); + } + + void StageStrands(StrandPointAttributes attributes, std::vector strands, std::vector points) { + Stage(PublishMode::Strands, attributes, std::move(strands), std::move(points)); + } + + void Clear() { + StageSegments({}, {}, {}); + } + + bool Flush() { + PendingPayload payload; + { + std::lock_guard guard(pending_mutex_); + if (!pending_.present) { + return false; + } + payload = std::move(pending_); + pending_ = PendingPayload{}; + } + + if (policy.deduplicate_identical_payloads && payload.hash == last_published_hash_ && + payload.indices.size() == last_published_index_count_ && payload.points.size() == last_published_point_count_) { + ++stats_.skipped_by_dedup; + return false; + } + + if (policy.min_republish_interval_seconds > 0.0f) { + const double now = NowSeconds(); + if (now - last_publish_time_seconds_ < static_cast(policy.min_republish_interval_seconds)) { + std::lock_guard guard(pending_mutex_); + if (!pending_.present) { + pending_ = std::move(payload); + } + ++stats_.skipped_by_rate_limit; + return false; + } + last_publish_time_seconds_ = now; + } + + if (!strands_) { + return false; + } + if (payload.mode == PublishMode::Segments) { + strands_->SetSegments(payload.attributes, payload.indices, payload.points); + } else { + strands_->SetStrands(payload.attributes, payload.indices, payload.points); + } + + last_published_hash_ = payload.hash; + last_published_index_count_ = payload.indices.size(); + last_published_point_count_ = payload.points.size(); + stats_.high_water_index_count = std::max(stats_.high_water_index_count, payload.indices.size()); + stats_.high_water_point_count = std::max(stats_.high_water_point_count, payload.points.size()); + ++stats_.flush_count; + return true; + } + + [[nodiscard]] bool HasPending() const { + std::lock_guard guard(pending_mutex_); + return pending_.present; + } + + [[nodiscard]] Entity GetEntity() const noexcept { + return entity_; + } + [[nodiscard]] const std::shared_ptr& GetStrands() const noexcept { + return strands_; + } + [[nodiscard]] const std::shared_ptr& GetMaterial() const noexcept { + return material_; + } + [[nodiscard]] const std::string& GetName() const noexcept { + return name_; + } + [[nodiscard]] Stats GetStats() const noexcept { + return stats_; + } + + RenderPublishPolicy policy{}; + + private: + enum class PublishMode { Segments, Strands }; + + struct PendingPayload { + PublishMode mode = PublishMode::Segments; + StrandPointAttributes attributes{}; + std::vector indices; + std::vector points; + std::uint64_t hash = 0; + bool present = false; + }; + + void Stage(const PublishMode mode, StrandPointAttributes attributes, std::vector indices, + std::vector points) { + std::uint64_t payload_hash = HashBytes(indices.data(), indices.size() * sizeof(glm::uint)); + payload_hash ^= HashBytes(points.data(), points.size() * sizeof(StrandPoint)) + 0x9e3779b97f4a7c15ULL + + (payload_hash << 6) + (payload_hash >> 2); + { + std::lock_guard guard(pending_mutex_); + pending_.mode = mode; + pending_.attributes = attributes; + pending_.indices = std::move(indices); + pending_.points = std::move(points); + pending_.hash = payload_hash; + pending_.present = true; + } + if (!policy.defer_to_main_thread) { + Flush(); + } + } + + static double NowSeconds() { + using clock = std::chrono::steady_clock; + const auto now = clock::now().time_since_epoch(); + return std::chrono::duration(now).count(); + } + + std::weak_ptr scene_; + std::string name_; + Entity entity_{}; + std::shared_ptr strands_; + std::shared_ptr material_; + + mutable std::mutex pending_mutex_; + PendingPayload pending_; + + std::uint64_t last_published_hash_ = 0; + std::size_t last_published_index_count_ = 0; + std::size_t last_published_point_count_ = 0; + double last_publish_time_seconds_ = -1.0e18; + + Stats stats_{}; +}; + +} // namespace l_system_package diff --git a/EvoEngine_Packages/LSystem/include/RenderPublishPolicy.hpp b/EvoEngine_Packages/LSystem/include/RenderPublishPolicy.hpp new file mode 100644 index 00000000..ef5c6f58 --- /dev/null +++ b/EvoEngine_Packages/LSystem/include/RenderPublishPolicy.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace l_system_package { + +struct RenderPublishPolicy { + bool defer_to_main_thread = true; + bool deduplicate_identical_payloads = true; + float min_republish_interval_seconds = 0.0f; +}; + +} // namespace l_system_package diff --git a/EvoEngine_Packages/LSystem/src/LSystemPackage.cpp b/EvoEngine_Packages/LSystem/src/LSystemPackage.cpp new file mode 100644 index 00000000..61365efa --- /dev/null +++ b/EvoEngine_Packages/LSystem/src/LSystemPackage.cpp @@ -0,0 +1,36 @@ +#include "PackageManager.hpp" + +#include "LSystemDescriptor.hpp" +#include "LSystemLayer.hpp" +#include "ScotsPine.hpp" +#include "ScotsPineDescriptor.hpp" + +using namespace evo_engine; +using namespace l_system_package; + +namespace { +PackageDescriptor descriptor{EVOENGINE_PACKAGE_API_VERSION, "LSystem", "0.1.0", + "L-system grammar runtime package (plant-agnostic core)."}; +} // namespace + +EVOENGINE_PACKAGE_EXPORT const PackageDescriptor* EvoEnginePackageGetDescriptor() { + return &descriptor; +} + +EVOENGINE_PACKAGE_EXPORT bool EvoEnginePackageRegisterTypes(PackageRegistrar* registrar) { + if (!registrar) { + return false; + } + + return registrar->RegisterAsset("LSystemDescriptor", {".lsys"}) && + registrar->RegisterAsset("ScotsPineDescriptor", {".spine"}) && + registrar->RegisterPrivateComponent("ScotsPine") && + registrar->RegisterLayer("LSystem Layer"); +} + +EVOENGINE_PACKAGE_EXPORT bool EvoEnginePackageLoad(PackageRegistrar*) { + return true; +} + +EVOENGINE_PACKAGE_EXPORT void EvoEnginePackageUnload(PackageRegistrar*) { +} diff --git a/EvoEngine_SDK/include/ApplicationInitializationSettings.hpp b/EvoEngine_SDK/include/ApplicationInitializationSettings.hpp index 3c974647..4759cbe2 100644 --- a/EvoEngine_SDK/include/ApplicationInitializationSettings.hpp +++ b/EvoEngine_SDK/include/ApplicationInitializationSettings.hpp @@ -45,6 +45,7 @@ struct ApplicationInitializationSettings { std::string application_name = "Evo Engine"; /**< The name of the application. */ std::vector icon_paths; /**< Paths to application icons. */ glm::ivec2 default_window_size = {1280, 720}; /**< The default size of the application window. */ + bool allow_empty_project = false; /**< Whether initialization may proceed without a project path. */ bool enable_docking = true; /**< Whether to enable docking in the application. */ bool enable_viewport = true; /**< Whether to enable the viewport feature. */ bool full_screen = false; /**< Whether the application starts in full-screen mode. */ diff --git a/EvoEngine_SDK/include/Core/ProjectManager.hpp b/EvoEngine_SDK/include/Core/ProjectManager.hpp index 0d751823..32ed30a2 100644 --- a/EvoEngine_SDK/include/Core/ProjectManager.hpp +++ b/EvoEngine_SDK/include/Core/ProjectManager.hpp @@ -4,8 +4,19 @@ #include "FileManager.hpp" #include "IAsset.hpp" +#include +#include + namespace evo_engine { +enum class ProjectState { NoProject, Loading, Loaded }; + +struct ProjectLaunchMetadata { + std::string application_name = "EvoEngine Editor"; + std::vector startup_runtime_packages; + std::string preferred_editor = "EvoEngineEditor"; +}; + /** * @class ProjectManager * @brief A singleton class responsible for managing project-related operations, including asset management, @@ -34,6 +45,8 @@ class ProjectManager { std::weak_ptr current_focused_folder_; ///< A weak pointer to the currently focused folder. + ProjectLaunchMetadata project_launch_metadata_; ///< Launcher/editor metadata persisted in the project file. + friend class ClassRegistry; std::shared_ptr start_scene_; ///< The starting scene of the project. int max_thumbnail_size_ = 256; ///< The maximum size in pixels for asset thumbnails. @@ -101,11 +114,45 @@ class ProjectManager { */ [[nodiscard]] static std::weak_ptr GetStartScene(); + /** + * @brief Retrieves the current project loading state. + */ + [[nodiscard]] static ProjectState GetProjectState(); + + /** + * @brief Returns true once a project path has been selected, even if loading is still in progress. + */ + [[nodiscard]] static bool HasProject(); + + /** + * @brief Returns true when a project path is selected and its start scene is ready. + */ + [[nodiscard]] static bool IsProjectLoaded(); + /** * @brief Returns true when project scanning, project asset loading, and start-scene setup are complete. */ [[nodiscard]] static bool IsProjectIdle(); + /** + * @brief Loads launcher/editor metadata from a project file without opening the project. + * @param path Project file path. + * @return Parsed metadata, or default metadata if the file is missing metadata. + */ + [[nodiscard]] static ProjectLaunchMetadata LoadProjectLaunchMetadata(const std::filesystem::path& path); + + /** + * @brief Saves launcher/editor metadata to a project file without opening the project. + * @param path Project file path. + * @param metadata Metadata to persist. + */ + static void SaveProjectLaunchMetadata(const std::filesystem::path& path, const ProjectLaunchMetadata& metadata); + + /** + * @brief Returns metadata for the currently selected project. + */ + [[nodiscard]] static ProjectLaunchMetadata GetProjectLaunchMetadata(); + /** * @brief Sets the starting scene for the project. * @param scene The new starting scene. diff --git a/EvoEngine_SDK/include/Layers/EditorLayer.hpp b/EvoEngine_SDK/include/Layers/EditorLayer.hpp index bb75dbc4..edf48a1f 100644 --- a/EvoEngine_SDK/include/Layers/EditorLayer.hpp +++ b/EvoEngine_SDK/include/Layers/EditorLayer.hpp @@ -15,7 +15,9 @@ #include "Strands.hpp" #include "Texture2D.hpp" +#include #include +#include namespace evo_engine { @@ -806,9 +808,29 @@ class EditorLayer : public ILayer { void OnInspect(const std::shared_ptr& editor_layer) override; ImGuiID dock_space_id; /** - * @brief Initializes the ImGui interface for the editor. + * @brief Draws the root ImGui dockspace for the editor. */ - void InitializeImGui(); + void DrawDockspace(); + + /** + * @brief Draws the editor menu bar. + */ + void DrawMainMenuBar(); + + void UpdateCameraTransition(); + void PrepareFrameState(); + void CaptureSceneWindowMousePosition(); + void CaptureMainCameraWindowMousePosition(); + void UpdateSceneState(const std::shared_ptr& scene); + void DrawEntityExplorerWindow(const std::shared_ptr& scene); + void DrawEntityInspectorWindow(const std::shared_ptr& scene, const std::shared_ptr& editor_layer); + void DrawConsoleWindow(); + void DrawRuntimePackageManagerWindow(); + void HandleSceneDeleteShortcut(const std::shared_ptr& scene); + void DrawViewportWindows(const std::shared_ptr& scene); + void DrawLayerInspectionWindows(const std::shared_ptr& scene, + const std::shared_ptr& editor_layer); + void DrawProjectInspectionWindows(const std::shared_ptr& editor_layer); /** * @brief Displays the scene camera window. diff --git a/EvoEngine_SDK/include/Layers/EditorTheme.hpp b/EvoEngine_SDK/include/Layers/EditorTheme.hpp new file mode 100644 index 00000000..f1cd52b2 --- /dev/null +++ b/EvoEngine_SDK/include/Layers/EditorTheme.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace evo_engine::editor_theme { +void ApplyDefault(); +void ApplyEvoEngineLegacy(); +void ApplyEvoEngineDark(); +} // namespace evo_engine::editor_theme diff --git a/EvoEngine_SDK/include/Layers/ImGuiLayer.hpp b/EvoEngine_SDK/include/Layers/ImGuiLayer.hpp new file mode 100644 index 00000000..1aaf0088 --- /dev/null +++ b/EvoEngine_SDK/include/Layers/ImGuiLayer.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ILayer.hpp" + +namespace evo_engine { +class ImGuiLayer final : public ILayer { + protected: + void OnDestroy() override; + void PreUpdate() override; +}; +} // namespace evo_engine diff --git a/EvoEngine_SDK/src/Application.cpp b/EvoEngine_SDK/src/Application.cpp index 6854e8c5..0b4c8226 100644 --- a/EvoEngine_SDK/src/Application.cpp +++ b/EvoEngine_SDK/src/Application.cpp @@ -40,8 +40,34 @@ #include "WayPoints.hpp" #include "WindowLayer.hpp" +#include + using namespace evo_engine; +namespace { +void AddUniqueStartupPackage(ApplicationInitializationSettings& settings, const std::string& package_name) { + if (!package_name.empty() && + std::find(settings.startup_runtime_packages.begin(), settings.startup_runtime_packages.end(), package_name) == + settings.startup_runtime_packages.end()) { + settings.startup_runtime_packages.emplace_back(package_name); + } +} + +void MergeProjectLaunchMetadata(ApplicationInitializationSettings& settings) { + if (settings.project_path.empty()) { + return; + } + + const auto metadata = ProjectManager::LoadProjectLaunchMetadata(settings.project_path); + for (const auto& package_name : metadata.startup_runtime_packages) { + AddUniqueStartupPackage(settings, package_name); + } + if (!settings.startup_runtime_packages.empty()) { + settings.enable_runtime_packages = true; + } +} +} // namespace + Application::Application() : asset_manager_(std::make_unique()), console_(std::make_unique()), @@ -361,16 +387,16 @@ void Application::Initialize(const ApplicationInitializationSettings& applicatio this->initialization_settings = application_create_info; const auto render_layer = GetLayer(); const auto window_layer = GetLayer(); - const auto editor_layer = GetLayer(); if (!this->initialization_settings.project_path.empty()) { if (this->initialization_settings.project_path.extension().string() != ".eveproj") { EVOENGINE_ERROR("Project file extension is not eveproj!") return; } - } else if (!window_layer || !editor_layer) { - EVOENGINE_ERROR("Project filepath must present when there's no EditorLayer or WindowLayer!") + } else if (!this->initialization_settings.allow_empty_project) { + EVOENGINE_ERROR("Project filepath must be present unless empty project startup is explicitly allowed!") return; } + MergeProjectLaunchMetadata(this->initialization_settings); const auto hardware_thread_size = std::thread::hardware_concurrency(); const size_t default_thread_size = hardware_thread_size > 2 ? hardware_thread_size - 2 : 1; for (const auto& layer : this->layers_) { diff --git a/EvoEngine_SDK/src/Camera.cpp b/EvoEngine_SDK/src/Camera.cpp index 52e88c76..258f7ff0 100644 --- a/EvoEngine_SDK/src/Camera.cpp +++ b/EvoEngine_SDK/src/Camera.cpp @@ -294,14 +294,17 @@ void Camera::OnCreate() { size_ = glm::uvec2(1, 1); frame_count_ = 0; camera_settings = {}; + const auto render_layer = ApplicationContext::Get().GetLayer(); + if (!render_layer || !Platform::Initialized()) { + return; + } RenderTextureCreateInfo render_texture_create_info{}; render_texture_create_info.extent.width = size_.x; render_texture_create_info.extent.height = size_.y; render_texture_create_info.extent.depth = 1; render_texture_ = std::make_unique(render_texture_create_info); - g_buffer_descriptor_set_ = std::make_shared( - ApplicationContext::Get().GetLayer()->GetCameraGBufferDescriptorSetLayout()); + g_buffer_descriptor_set_ = std::make_shared(render_layer->GetCameraGBufferDescriptorSetLayout()); post_processing_stack_ref = AssetManager::CreateTemporaryAsset(); UpdateGBuffer(); diff --git a/EvoEngine_SDK/src/EditorLayer.cpp b/EvoEngine_SDK/src/EditorLayer.cpp index 95b40b8c..5beeba29 100644 --- a/EvoEngine_SDK/src/EditorLayer.cpp +++ b/EvoEngine_SDK/src/EditorLayer.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "AssetManager.hpp" #include "Cubemap.hpp" +#include "EditorTheme.hpp" #include "EnvironmentalMap.hpp" #include "ILayer.hpp" #include "Material.hpp" @@ -18,57 +19,15 @@ #include "StrandsRenderer.hpp" #include "Times.hpp" #include "WindowLayer.hpp" + using namespace evo_engine; + void EditorLayer::OnCreate() { const auto window_layer = ApplicationContext::Get().GetLayer(); if (!window_layer) { throw std::runtime_error("WindowLayer not present!"); } -#pragma region Default ImGui Style - ImGuiStyle& style = ImGui::GetStyle(); - style.WindowRounding = 5.3f; - style.FrameRounding = 2.3f; - style.ScrollbarRounding = 0; - - style.Colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 0.90f); - style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.30f, 0.30f, 0.30f, 0.90f); - style.Colors[ImGuiCol_WindowBg] = ImVec4(0.09f, 0.09f, 0.15f, 1.00f); - style.Colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - style.Colors[ImGuiCol_PopupBg] = ImVec4(0.05f, 0.05f, 0.10f, 0.85f); - style.Colors[ImGuiCol_Border] = ImVec4(0.70f, 0.70f, 0.70f, 0.65f); - style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - style.Colors[ImGuiCol_FrameBg] = ImVec4(0.00f, 0.00f, 0.01f, 1.00f); - style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.90f, 0.80f, 0.80f, 0.40f); - style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.90f, 0.65f, 0.65f, 0.45f); - style.Colors[ImGuiCol_TitleBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.83f); - style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); - style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.00f, 0.00f, 0.00f, 0.87f); - style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.01f, 0.01f, 0.02f, 0.80f); - style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); - style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.55f, 0.53f, 0.55f, 0.51f); - style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); - style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.56f, 0.56f, 0.56f, 0.91f); - style.Colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.83f); - style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.70f, 0.70f, 0.70f, 0.62f); - style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.30f, 0.30f, 0.30f, 0.84f); - style.Colors[ImGuiCol_Button] = ImVec4(0.48f, 0.72f, 0.89f, 0.49f); - style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.50f, 0.69f, 0.99f, 0.68f); - style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.80f, 0.50f, 0.50f, 1.00f); - style.Colors[ImGuiCol_Header] = ImVec4(0.30f, 0.69f, 1.00f, 0.53f); - style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.44f, 0.61f, 0.86f, 1.00f); - style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.62f, 0.83f, 1.00f); - style.Colors[ImGuiCol_TabSelectedOverline] = ImVec4(0.70f, 0.30f, 0.30f, 1.00f); - style.Colors[ImGuiCol_TabHovered] = ImVec4(0.70f, 0.00f, 0.30f, 1.00f); - style.Colors[ImGuiCol_TabSelected] = ImVec4(0.50f, 0.00f, 0.50f, 1.00f); - style.Colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.85f); - style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(1.00f, 1.00f, 1.00f, 0.60f); - style.Colors[ImGuiCol_ResizeGripActive] = ImVec4(1.00f, 1.00f, 1.00f, 0.90f); - style.Colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - style.Colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - style.Colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - style.Colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); - style.Colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); -#pragma endregion + editor_theme::ApplyDefault(); basic_entity_archetype_ = Entities::CreateEntityArchetype("General", GlobalTransform(), Transform()); RegisterComponentDataInspector([](Entity, IDataComponent* data, bool) { @@ -190,114 +149,58 @@ void EditorLayer::OnDestroy() { gizmo_strands_tasks_.clear(); vmaUnmapMemory(Platform::GetVmaAllocator(), entity_index_read_buffer_->GetVmaAllocation()); entity_index_read_buffer_.reset(); - ImGui_ImplVulkan_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImNodes::DestroyContext(); - ImGui::DestroyContext(); } void EditorLayer::PreUpdate() { - InitializeImGui(); + DrawDockspace(); + DrawMainMenuBar(); + const auto scene = ApplicationContext::Get().GetActiveScene(); - if (lock_camera) { - auto& [sceneCameraRotation, sceneCameraPosition, sceneCamera] = editor_cameras_.at(scene_camera_handle_); - const float elapsed_time = static_cast(ApplicationContext::Get().GetTimes().Now()) - transition_timer_; - float a = 1.0f - glm::pow(1.0 - elapsed_time / transition_time_, 4.0f); - if (elapsed_time >= transition_time_) - a = 1.0f; - sceneCameraRotation = glm::mix(previous_rotation_, target_rotation_, a); - sceneCameraPosition = glm::mix(previous_position_, target_position_, a); - if (a >= 1.0f) { - lock_camera = false; - sceneCameraRotation = target_rotation_; - sceneCameraPosition = target_position_; - // Camera::ReverseAngle(target_rotation_, m_sceneCameraPitchAngle, m_sceneCameraYawAngle); - } + UpdateCameraTransition(); + PrepareFrameState(); + CaptureSceneWindowMousePosition(); + CaptureMainCameraWindowMousePosition(); + UpdateSceneState(scene); + + const auto editor_layer = std::dynamic_pointer_cast(GetSelf()); + DrawEntityExplorerWindow(scene); + DrawEntityInspectorWindow(scene, editor_layer); + DrawConsoleWindow(); + DrawRuntimePackageManagerWindow(); + HandleSceneDeleteShortcut(scene); + DrawViewportWindows(scene); + DrawLayerInspectionWindows(scene, editor_layer); + DrawProjectInspectionWindows(editor_layer); +} + +void EditorLayer::UpdateCameraTransition() { + if (!lock_camera) { + return; } + auto& [sceneCameraRotation, sceneCameraPosition, sceneCamera] = editor_cameras_.at(scene_camera_handle_); + const float elapsed_time = static_cast(ApplicationContext::Get().GetTimes().Now()) - transition_timer_; + float a = 1.0f - glm::pow(1.0 - elapsed_time / transition_time_, 4.0f); + if (elapsed_time >= transition_time_) + a = 1.0f; + sceneCameraRotation = glm::mix(previous_rotation_, target_rotation_, a); + sceneCameraPosition = glm::mix(previous_position_, target_position_, a); + if (a >= 1.0f) { + lock_camera = false; + sceneCameraRotation = target_rotation_; + sceneCameraPosition = target_position_; + // Camera::ReverseAngle(target_rotation_, m_sceneCameraPitchAngle, m_sceneCameraYawAngle); + } +} + +void EditorLayer::PrepareFrameState() { gizmo_mesh_tasks_.clear(); gizmo_instanced_mesh_tasks_.clear(); gizmo_strands_tasks_.clear(); main_camera_focus_override = false; scene_camera_focus_override = false; - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("View")) { - if (ImGui::BeginMenu("Layer Inspection")) { - for (const auto& layer : ApplicationContext::Get().GetLayers()) { - ImGui::Checkbox(layer->layer_name_.c_str(), &layer->enable_inspection); - } - ImGui::EndMenu(); - } - ImGui::MenuItem("Runtime Packages", nullptr, &show_package_manager_window); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Project")) { - ImGui::EndMenu(); - } - ImGui::Separator(); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)2, (float)2)); - switch (ApplicationContext::Get().GetApplicationStatus()) { - case Application::ExecutionStatus::NotPlaying: { - ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["PlayButton"]->GetImTextureId()); - ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StepButton"]->GetImTextureId()); - - if (ImGui::ImageButton("PlayButton", editor_icons_["PlayButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Play(); - } - if (ImGui::ImageButton("StepButton", editor_icons_["StepButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Step(); - } - - ImGui::PopID(); - ImGui::PopID(); - break; - } - case Application::ExecutionStatus::Playing: { - ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["PauseButton"]->GetImTextureId()); - ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StopButton"]->GetImTextureId()); - - if (ImGui::ImageButton("PauseButton", editor_icons_["PauseButton"]->GetImTextureId(), {20, 20}, {0, 1}, - {1, 0})) { - ApplicationContext::Get().Pause(); - } - if (ImGui::ImageButton("StopButton", editor_icons_["StopButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Stop(); - } - ImGui::PopID(); - ImGui::PopID(); - break; - } - case Application::ExecutionStatus::Pause: { - ImGui::PushID((ImTextureID)((intptr_t)(editor_icons_["PlayButton"]->GetImTextureId()))); - ImGui::PushID((ImTextureID)((intptr_t)(editor_icons_["StepButton"]->GetImTextureId()))); - ImGui::PushID((ImTextureID)((intptr_t)(editor_icons_["StopButton"]->GetImTextureId()))); - if (ImGui::ImageButton("PlayButton", editor_icons_["PlayButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Play(); - } - if (ImGui::ImageButton("StepButton", editor_icons_["StepButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Step(); - } - if (ImGui::ImageButton("StopButton", editor_icons_["StopButton"]->GetImTextureId(), {20, 20}, {0, 1}, {1, 0})) { - ApplicationContext::Get().Stop(); - } - ImGui::PopID(); - ImGui::PopID(); - ImGui::PopID(); - break; - } - case Application::ExecutionStatus::Uninitialized: - break; - case Application::ExecutionStatus::Step: - break; - case Application::ExecutionStatus::OnDestroy: - break; - } - ImGui::PopStyleVar(); - ImGui::EndMainMenuBar(); - } +} - ImGui::PopStyleVar(1); +void EditorLayer::CaptureSceneWindowMousePosition() { mouse_scene_window_position_ = glm::vec2(FLT_MAX, -FLT_MAX); if (show_scene_window) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0, 0}); @@ -316,7 +219,9 @@ void EditorLayer::PreUpdate() { ImGui::End(); ImGui::PopStyleVar(); } +} +void EditorLayer::CaptureMainCameraWindowMousePosition() { mouse_camera_window_position_ = glm::vec2(FLT_MAX, -FLT_MAX); if (show_camera_window) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0, 0}); @@ -335,6 +240,9 @@ void EditorLayer::PreUpdate() { ImGui::End(); ImGui::PopStyleVar(); } +} + +void EditorLayer::UpdateSceneState(const std::shared_ptr& scene) { if (scene && show_scene_window) ResizeCameras(); @@ -362,453 +270,472 @@ void EditorLayer::PreUpdate() { } selection_alpha_ = glm::clamp(selection_alpha_, 0, 256); +} - const auto editor_layer = std::dynamic_pointer_cast(GetSelf()); - if (show_entity_explorer_window) { - ImGui::Begin("Entity Explorer"); - if (scene) { - if (ImGui::BeginPopupContextWindow("NewEntityPopup")) { - if (ImGui::Button("Create new entity")) { - scene->CreateEntity(basic_entity_archetype_); - } - ImGui::EndPopup(); +void EditorLayer::DrawEntityExplorerWindow(const std::shared_ptr& scene) { + if (!show_entity_explorer_window) { + return; + } + ImGui::Begin("Entity Explorer"); + if (scene) { + if (ImGui::BeginPopupContextWindow("NewEntityPopup")) { + if (ImGui::Button("Create new entity")) { + scene->CreateEntity(basic_entity_archetype_); } - const char* hierarchy_display_mode[]{"Archetype", "Hierarchy"}; - - ImGui::Combo("Display mode", &selected_hierarchy_display_mode, hierarchy_display_mode, - IM_ARRAYSIZE(hierarchy_display_mode)); - std::string title = scene->GetTitle(); - if (ImGui::CollapsingHeader(title.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow)) { - DraggableAsset(scene); - RenameAsset(scene); - if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { - inspecting_asset = scene; - } - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Entity")) { - IM_ASSERT(payload->DataSize == sizeof(Handle)); - const auto payload_n = *static_cast(payload->Data); - const auto new_entity = scene->GetEntity(payload_n); - if (const auto parent = scene->GetParent(new_entity); parent.GetIndex() != 0) - scene->RemoveChild(new_entity, parent); - } - ImGui::EndDragDropTarget(); + ImGui::EndPopup(); + } + const char* hierarchy_display_mode[]{"Archetype", "Hierarchy"}; + + ImGui::Combo("Display mode", &selected_hierarchy_display_mode, hierarchy_display_mode, + IM_ARRAYSIZE(hierarchy_display_mode)); + std::string title = scene->GetTitle(); + if (ImGui::CollapsingHeader(title.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow)) { + DraggableAsset(scene); + RenameAsset(scene); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { + inspecting_asset = scene; + } + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Entity")) { + IM_ASSERT(payload->DataSize == sizeof(Handle)); + const auto payload_n = *static_cast(payload->Data); + const auto new_entity = scene->GetEntity(payload_n); + if (const auto parent = scene->GetParent(new_entity); parent.GetIndex() != 0) + scene->RemoveChild(new_entity, parent); } - if (selected_hierarchy_display_mode == 0) { - scene->UnsafeForEachEntityStorage([&](size_t i, const std::string& name, - const DataComponentStorage& storage) { - if (i == 0) - return; - ImGui::Separator(); - const std::string title1 = std::to_string(i) + ". " + name; - if (ImGui::TreeNode(title1.c_str())) { - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.2f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.2f, 0.3f, 1.0f)); - for (size_t j = 0; j < storage.entity_alive_count; j++) { - Entity entity = storage.chunk_array.entity_array.at(j); - std::string title2 = std::to_string(entity.GetIndex()) + ": "; - title2 += scene->GetEntityName(entity); - const bool enabled = scene->IsEntityEnabled(entity); - if (enabled) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4({1, 1, 1, 1})); - } - ImGui::TreeNodeEx( - title2.c_str(), - ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoAutoOpenOnLog | - (selected_entity_ == entity ? ImGuiTreeNodeFlags_Framed : ImGuiTreeNodeFlags_FramePadding)); - if (enabled) { - ImGui::PopStyleColor(); - } - DrawEntityMenu(enabled, entity); - if (!lock_entity_selection_ && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { - SetSelectedEntity(entity, false); - } + ImGui::EndDragDropTarget(); + } + if (selected_hierarchy_display_mode == 0) { + scene->UnsafeForEachEntityStorage([&](size_t i, const std::string& name, const DataComponentStorage& storage) { + if (i == 0) + return; + ImGui::Separator(); + const std::string title1 = std::to_string(i) + ". " + name; + if (ImGui::TreeNode(title1.c_str())) { + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.2f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.2f, 0.3f, 1.0f)); + for (size_t j = 0; j < storage.entity_alive_count; j++) { + Entity entity = storage.chunk_array.entity_array.at(j); + std::string title2 = std::to_string(entity.GetIndex()) + ": "; + title2 += scene->GetEntityName(entity); + const bool enabled = scene->IsEntityEnabled(entity); + if (enabled) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4({1, 1, 1, 1})); + } + ImGui::TreeNodeEx( + title2.c_str(), + ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoAutoOpenOnLog | + (selected_entity_ == entity ? ImGuiTreeNodeFlags_Framed : ImGuiTreeNodeFlags_FramePadding)); + if (enabled) { + ImGui::PopStyleColor(); + } + DrawEntityMenu(enabled, entity); + if (!lock_entity_selection_ && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) { + SetSelectedEntity(entity, false); } - ImGui::PopStyleColor(); - ImGui::PopStyleColor(); - ImGui::PopStyleColor(); - ImGui::TreePop(); } - }); - } else if (selected_hierarchy_display_mode == 1) { - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.2f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.2f, 0.3f, 1.0f)); - scene->ForAllEntities([&](size_t, const Entity entity) { - if (scene->GetParent(entity).GetIndex() == 0) - DrawEntityNode(entity, 0); - }); - selected_entity_hierarchy_list_.clear(); - ImGui::PopStyleColor(); - ImGui::PopStyleColor(); - ImGui::PopStyleColor(); - } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::TreePop(); + } + }); + } else if (selected_hierarchy_display_mode == 1) { + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.2f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.2f, 0.3f, 1.0f)); + scene->ForAllEntities([&](size_t, const Entity entity) { + if (scene->GetParent(entity).GetIndex() == 0) + DrawEntityNode(entity, 0); + }); + selected_entity_hierarchy_list_.clear(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); } - } else { - ImGui::Text("No Scene!"); } - ImGui::End(); + } else { + ImGui::Text("No Scene!"); } - if (show_entity_inspector_window) { - ImGui::Begin("Entity Inspector"); - if (scene) { - ImGui::Text("Selection:"); - ImGui::SameLine(); - ImGui::Checkbox("Lock", &lock_entity_selection_); - ImGui::SameLine(); - ImGui::Checkbox("Focus", &highlight_selection_); - ImGui::SameLine(); - ImGui::Checkbox("Gizmos", &enable_gizmos); - ImGui::SameLine(); - if (ImGui::Button("Clear")) { - SetSelectedEntity({}); - } - ImGui::Separator(); - if (scene->IsEntityValid(selected_entity_)) { - std::string title = std::to_string(selected_entity_.GetIndex()) + ": "; - title += scene->GetEntityName(selected_entity_); - bool enabled = scene->IsEntityEnabled(selected_entity_); - if (ImGui::Checkbox((title + "##EnabledCheckbox").c_str(), &enabled)) { - if (scene->IsEntityEnabled(selected_entity_) != enabled) { - scene->SetEnable(selected_entity_, enabled); - } + ImGui::End(); +} + +void EditorLayer::DrawEntityInspectorWindow(const std::shared_ptr& scene, + const std::shared_ptr& editor_layer) { + if (!show_entity_inspector_window) { + return; + } + ImGui::Begin("Entity Inspector"); + if (scene) { + ImGui::Text("Selection:"); + ImGui::SameLine(); + ImGui::Checkbox("Lock", &lock_entity_selection_); + ImGui::SameLine(); + ImGui::Checkbox("Focus", &highlight_selection_); + ImGui::SameLine(); + ImGui::Checkbox("Gizmos", &enable_gizmos); + ImGui::SameLine(); + if (ImGui::Button("Clear")) { + SetSelectedEntity({}); + } + ImGui::Separator(); + if (scene->IsEntityValid(selected_entity_)) { + std::string title = std::to_string(selected_entity_.GetIndex()) + ": "; + title += scene->GetEntityName(selected_entity_); + bool enabled = scene->IsEntityEnabled(selected_entity_); + if (ImGui::Checkbox((title + "##EnabledCheckbox").c_str(), &enabled)) { + if (scene->IsEntityEnabled(selected_entity_) != enabled) { + scene->SetEnable(selected_entity_, enabled); } - ImGui::SameLine(); - bool is_static = scene->IsEntityStatic(selected_entity_); - if (ImGui::Checkbox("Static##StaticCheckbox", &is_static)) { - if (scene->IsEntityStatic(selected_entity_) != is_static) { - scene->SetEntityStatic(selected_entity_, enabled); - } + } + ImGui::SameLine(); + bool is_static = scene->IsEntityStatic(selected_entity_); + if (ImGui::Checkbox("Static##StaticCheckbox", &is_static)) { + if (scene->IsEntityStatic(selected_entity_) != is_static) { + scene->SetEntityStatic(selected_entity_, enabled); } + } + + if (const bool deleted = DrawEntityMenu(scene->IsEntityEnabled(selected_entity_), selected_entity_); !deleted) { + if (ImGui::CollapsingHeader("Data components", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginPopupContextItem("DataComponentInspectorPopup")) { + ImGui::Text("Add data component: "); + ImGui::Separator(); + + for (const auto& i : Serialization::GetInstance().data_component_ids_) { + const auto id = i.second; + const auto name = i.first; + if (id == typeid(Transform).hash_code() || id == typeid(GlobalTransform).hash_code() || + id == typeid(TransformUpdateFlag).hash_code()) + continue; - if (const bool deleted = DrawEntityMenu(scene->IsEntityEnabled(selected_entity_), selected_entity_); !deleted) { - if (ImGui::CollapsingHeader("Data components", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginPopupContextItem("DataComponentInspectorPopup")) { - ImGui::Text("Add data component: "); - ImGui::Separator(); - - for (const auto& i : Serialization::GetInstance().data_component_ids_) { - const auto id = i.second; - const auto name = i.first; - if (id == typeid(Transform).hash_code() || id == typeid(GlobalTransform).hash_code() || - id == typeid(TransformUpdateFlag).hash_code()) - continue; - - if (!scene->HasDataComponent(selected_entity_, id) && ImGui::Button(name.c_str())) { - scene->AddDataComponent(selected_entity_, id); - } + if (!scene->HasDataComponent(selected_entity_, id) && ImGui::Button(name.c_str())) { + scene->AddDataComponent(selected_entity_, id); + } + } + ImGui::Separator(); + ImGui::EndPopup(); + } + bool skip = false; + int i = 0; + scene->UnsafeForEachDataComponent(selected_entity_, [&](const DataComponentType& type, void* data) { + if (skip) + return; + std::string info = type.type_name; + if (info == "TransformUpdateFlag" || info == "GlobalTransform") + return; + info += " Size: " + std::to_string(type.type_size); + ImGui::Text(info.c_str()); + ImGui::PushID(i); + if (ImGui::BeginPopupContextItem(("DataComponentDeletePopup" + std::to_string(i)).c_str())) { + if (ImGui::Button("Remove")) { + skip = true; + scene->RemoveDataComponent(selected_entity_, type.type_index); } - ImGui::Separator(); ImGui::EndPopup(); } - bool skip = false; - int i = 0; - scene->UnsafeForEachDataComponent(selected_entity_, [&](const DataComponentType& type, void* data) { - if (skip) - return; - std::string info = type.type_name; - if (info == "TransformUpdateFlag" || info == "GlobalTransform") - return; - info += " Size: " + std::to_string(type.type_size); - ImGui::Text(info.c_str()); - ImGui::PushID(i); - if (ImGui::BeginPopupContextItem(("DataComponentDeletePopup" + std::to_string(i)).c_str())) { - if (ImGui::Button("Remove")) { - skip = true; - scene->RemoveDataComponent(selected_entity_, type.type_index); - } - ImGui::EndPopup(); + ImGui::PopID(); + InspectComponentData(selected_entity_, static_cast(data), type, + scene->GetParent(selected_entity_).GetIndex() != 0); + ImGui::Separator(); + i++; + }); + } + + if (ImGui::CollapsingHeader("Private components", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginPopupContextItem("PrivateComponentInspectorPopup")) { + ImGui::Text("Add private component: "); + ImGui::Separator(); + for (const auto& i : Serialization::GetInstance().private_component_ids_) { + const auto id = i.second; + const auto name = i.first; + if (!scene->HasPrivateComponent(selected_entity_, id) && ImGui::Button(name.c_str())) { + scene->AddPrivateComponent(selected_entity_, id); } - ImGui::PopID(); - InspectComponentData(selected_entity_, static_cast(data), type, - scene->GetParent(selected_entity_).GetIndex() != 0); - ImGui::Separator(); - i++; - }); + } + ImGui::Separator(); + ImGui::EndPopup(); } - if (ImGui::CollapsingHeader("Private components", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginPopupContextItem("PrivateComponentInspectorPopup")) { - ImGui::Text("Add private component: "); - ImGui::Separator(); - for (const auto& i : Serialization::GetInstance().private_component_ids_) { - const auto id = i.second; - const auto name = i.first; - if (!scene->HasPrivateComponent(selected_entity_, id) && ImGui::Button(name.c_str())) { - scene->AddPrivateComponent(selected_entity_, id); - } + int i = 0; + bool skip = false; + scene->ForEachPrivateComponent(selected_entity_, [&](const PrivateComponentElement& data) { + if (skip) + return; + ImGui::Checkbox(data.private_component_data->GetTypeName().c_str(), &data.private_component_data->enabled_); + DraggablePrivateComponent(data.private_component_data); + const std::string tag = "##" + data.private_component_data->GetTypeName() + + std::to_string(data.private_component_data->GetHandle()); + if (ImGui::BeginPopupContextItem(tag.c_str())) { + if (ImGui::Button(("Remove" + tag).c_str())) { + skip = true; + scene->RemovePrivateComponent(selected_entity_, data.type_index); } - ImGui::Separator(); ImGui::EndPopup(); } - - int i = 0; - bool skip = false; - scene->ForEachPrivateComponent(selected_entity_, [&](const PrivateComponentElement& data) { - if (skip) - return; - ImGui::Checkbox(data.private_component_data->GetTypeName().c_str(), - &data.private_component_data->enabled_); - DraggablePrivateComponent(data.private_component_data); - const std::string tag = "##" + data.private_component_data->GetTypeName() + - std::to_string(data.private_component_data->GetHandle()); - if (ImGui::BeginPopupContextItem(tag.c_str())) { - if (ImGui::Button(("Remove" + tag).c_str())) { - skip = true; - scene->RemovePrivateComponent(selected_entity_, data.type_index); - } - ImGui::EndPopup(); - } - if (!skip) { - if (ImGui::TreeNodeEx(("Component Settings##" + std::to_string(i)).c_str(), - ImGuiTreeNodeFlags_DefaultOpen)) { - if (data.private_component_data->OnInspect(editor_layer)) - scene->SetUnsaved(); - ImGui::TreePop(); - } + if (!skip) { + if (ImGui::TreeNodeEx(("Component Settings##" + std::to_string(i)).c_str(), + ImGuiTreeNodeFlags_DefaultOpen)) { + if (data.private_component_data->OnInspect(editor_layer)) + scene->SetUnsaved(); + ImGui::TreePop(); } - ImGui::Separator(); - i++; - }); - } + } + ImGui::Separator(); + i++; + }); } - } else { - SetSelectedEntity(Entity()); } } else { - ImGui::Text("No Scene!"); + SetSelectedEntity(Entity()); } - ImGui::End(); + } else { + ImGui::Text("No Scene!"); } - if (show_console_window) { - if (ImGui::Begin("Console")) { - ImGui::Checkbox("Log", &enable_console_logs_); - ImGui::SameLine(); - ImGui::Checkbox("Warning", &enable_console_warnings_); - ImGui::SameLine(); - ImGui::Checkbox("Error", &enable_console_errors_); - ImGui::SameLine(); - if (ImGui::Button("Clear all")) { - console_messages_.clear(); - } - int i = 0; - for (auto msg = console_messages_.rbegin(); msg != console_messages_.rend(); ++msg) { - if (i > 999) + ImGui::End(); +} + +void EditorLayer::DrawConsoleWindow() { + if (!show_console_window) { + return; + } + if (ImGui::Begin("Console")) { + ImGui::Checkbox("Log", &enable_console_logs_); + ImGui::SameLine(); + ImGui::Checkbox("Warning", &enable_console_warnings_); + ImGui::SameLine(); + ImGui::Checkbox("Error", &enable_console_errors_); + ImGui::SameLine(); + if (ImGui::Button("Clear all")) { + console_messages_.clear(); + } + int i = 0; + for (auto msg = console_messages_.rbegin(); msg != console_messages_.rend(); ++msg) { + if (i > 999) + break; + i++; + switch (msg->m_type) { + case ConsoleMessageType::Log: + if (enable_console_logs_) { + ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 1, 1), msg->m_value.c_str()); + ImGui::Separator(); + } + break; + case ConsoleMessageType::Warning: + if (enable_console_warnings_) { + ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 0, 1), msg->m_value.c_str()); + ImGui::Separator(); + } + break; + case ConsoleMessageType::Error: + if (enable_console_errors_) { + ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), msg->m_value.c_str()); + ImGui::Separator(); + } break; - i++; - switch (msg->m_type) { - case ConsoleMessageType::Log: - if (enable_console_logs_) { - ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1, 1, 1, 1), msg->m_value.c_str()); - ImGui::Separator(); - } - break; - case ConsoleMessageType::Warning: - if (enable_console_warnings_) { - ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1, 1, 0, 1), msg->m_value.c_str()); - ImGui::Separator(); - } - break; - case ConsoleMessageType::Error: - if (enable_console_errors_) { - ImGui::TextColored(ImVec4(0, 0, 1, 1), "%.2f: ", msg->m_time); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1, 0, 0, 1), msg->m_value.c_str()); - ImGui::Separator(); - } - break; - } } } - ImGui::End(); } - if (show_package_manager_window) { - if (!runtime_package_manager_scanned_) { + ImGui::End(); +} + +void EditorLayer::DrawRuntimePackageManagerWindow() { + if (!show_package_manager_window) { + return; + } + if (!runtime_package_manager_scanned_) { + PackageManager::ScanAvailablePackages(); + runtime_package_manager_scanned_ = true; + } + + bool package_manager_open = show_package_manager_window; + if (ImGui::Begin("Runtime Package Manager", &package_manager_open)) { + if (ImGui::Button("Scan")) { PackageManager::ScanAvailablePackages(); runtime_package_manager_scanned_ = true; } + ImGui::SameLine(); + ImGui::BeginDisabled(selected_runtime_package_names_.empty()); + if (ImGui::Button("Load Selected")) { + std::vector selected_packages(selected_runtime_package_names_.begin(), + selected_runtime_package_names_.end()); + selected_runtime_package_names_.clear(); + ApplicationContext::Get().QueueEndOfLoopAction([selected_packages]() { + for (const auto& package_name : selected_packages) { + PackageManager::Load(package_name); + } + }); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Load All")) { + ApplicationContext::Get().QueueEndOfLoopAction([]() { + PackageManager::LoadAll(); + }); + } - bool package_manager_open = show_package_manager_window; - if (ImGui::Begin("Runtime Package Manager", &package_manager_open)) { - if (ImGui::Button("Scan")) { - PackageManager::ScanAvailablePackages(); - runtime_package_manager_scanned_ = true; - } - ImGui::SameLine(); - ImGui::BeginDisabled(selected_runtime_package_names_.empty()); - if (ImGui::Button("Load Selected")) { - std::vector selected_packages(selected_runtime_package_names_.begin(), - selected_runtime_package_names_.end()); - selected_runtime_package_names_.clear(); - ApplicationContext::Get().QueueEndOfLoopAction([selected_packages]() { - for (const auto& package_name : selected_packages) { - PackageManager::Load(package_name); - } - }); - } - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Load All")) { - ApplicationContext::Get().QueueEndOfLoopAction([]() { - PackageManager::LoadAll(); - }); - } - - const auto search_paths = PackageManager::GetSearchPaths(); - const auto available_packages = PackageManager::GetAvailablePackages(); - const auto loaded_packages = PackageManager::GetLoadedPackages(); + const auto search_paths = PackageManager::GetSearchPaths(); + const auto available_packages = PackageManager::GetAvailablePackages(); + const auto loaded_packages = PackageManager::GetLoadedPackages(); - ImGui::Text("Available: %zu", available_packages.size()); - ImGui::SameLine(); - ImGui::Text("Loaded: %zu", loaded_packages.size()); + ImGui::Text("Available: %zu", available_packages.size()); + ImGui::SameLine(); + ImGui::Text("Loaded: %zu", loaded_packages.size()); - if (ImGui::TreeNode("Search paths")) { - for (const auto& path : search_paths) { - ImGui::BulletText("%s", path.string().c_str()); - } - ImGui::TreePop(); + if (ImGui::TreeNode("Search paths")) { + for (const auto& path : search_paths) { + ImGui::BulletText("%s", path.string().c_str()); } + ImGui::TreePop(); + } - ImGui::Separator(); - ImGui::TextUnformatted("Available Packages"); - if (ImGui::BeginChild("AvailablePackages", ImVec2(0, 260), true)) { - if (available_packages.empty()) { - ImGui::TextUnformatted("No package manifests found."); + ImGui::Separator(); + ImGui::TextUnformatted("Available Packages"); + if (ImGui::BeginChild("AvailablePackages", ImVec2(0, 260), true)) { + if (available_packages.empty()) { + ImGui::TextUnformatted("No package manifests found."); + } + for (const auto& package : available_packages) { + const bool can_load = !package.loaded && package.library_exists; + if (!can_load) { + selected_runtime_package_names_.erase(package.name); } - for (const auto& package : available_packages) { - const bool can_load = !package.loaded && package.library_exists; - if (!can_load) { + + bool selected = selected_runtime_package_names_.find(package.name) != selected_runtime_package_names_.end(); + ImGui::PushID(package.name.c_str()); + ImGui::BeginDisabled(!can_load); + if (ImGui::Checkbox("##Select", &selected)) { + if (selected) { + selected_runtime_package_names_.insert(package.name); + } else { selected_runtime_package_names_.erase(package.name); } + } + ImGui::EndDisabled(); + ImGui::SameLine(); - bool selected = selected_runtime_package_names_.find(package.name) != selected_runtime_package_names_.end(); - ImGui::PushID(package.name.c_str()); - ImGui::BeginDisabled(!can_load); - if (ImGui::Checkbox("##Select", &selected)) { - if (selected) { - selected_runtime_package_names_.insert(package.name); - } else { - selected_runtime_package_names_.erase(package.name); + auto package_label = package.name; + if (package.loaded) { + package_label += " (loaded)"; + } else if (!package.library_exists) { + package_label += " (missing library)"; + } + + if (ImGui::TreeNodeEx("Package", ImGuiTreeNodeFlags_SpanAvailWidth, "%s", package_label.c_str())) { + ImGui::Text("Version: %s", package.version.empty() ? "Unknown" : package.version.c_str()); + if (!package.description.empty()) { + ImGui::TextWrapped("%s", package.description.c_str()); + } + if (!package.dependencies.empty() && ImGui::TreeNode("Dependencies")) { + for (const auto& dependency : package.dependencies) { + ImGui::BulletText("%s", dependency.c_str()); } + ImGui::TreePop(); } - ImGui::EndDisabled(); + ImGui::TextUnformatted("Manifest:"); ImGui::SameLine(); - - auto package_label = package.name; - if (package.loaded) { - package_label += " (loaded)"; - } else if (!package.library_exists) { - package_label += " (missing library)"; + ImGui::TextWrapped("%s", package.manifest_path.string().c_str()); + ImGui::TextUnformatted("Library:"); + ImGui::SameLine(); + ImGui::TextWrapped("%s", package.library_path.string().c_str()); + ImGui::BeginDisabled(!can_load); + if (ImGui::Button("Load")) { + const auto package_name = package.name; + selected_runtime_package_names_.erase(package.name); + ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { + PackageManager::Load(package_name); + }); } + ImGui::EndDisabled(); + ImGui::TreePop(); + } + ImGui::PopID(); + } + } + ImGui::EndChild(); - if (ImGui::TreeNodeEx("Package", ImGuiTreeNodeFlags_SpanAvailWidth, "%s", package_label.c_str())) { - ImGui::Text("Version: %s", package.version.empty() ? "Unknown" : package.version.c_str()); - if (!package.description.empty()) { - ImGui::TextWrapped("%s", package.description.c_str()); - } - if (!package.dependencies.empty() && ImGui::TreeNode("Dependencies")) { - for (const auto& dependency : package.dependencies) { - ImGui::BulletText("%s", dependency.c_str()); - } - ImGui::TreePop(); - } - ImGui::TextUnformatted("Manifest:"); - ImGui::SameLine(); - ImGui::TextWrapped("%s", package.manifest_path.string().c_str()); - ImGui::TextUnformatted("Library:"); - ImGui::SameLine(); - ImGui::TextWrapped("%s", package.library_path.string().c_str()); - ImGui::BeginDisabled(!can_load); - if (ImGui::Button("Load")) { - const auto package_name = package.name; - selected_runtime_package_names_.erase(package.name); - ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { - PackageManager::Load(package_name); - }); + ImGui::Separator(); + ImGui::TextUnformatted("Loaded Packages"); + if (ImGui::BeginChild("LoadedPackages", ImVec2(0, 0), true)) { + if (loaded_packages.empty()) { + ImGui::TextUnformatted("No runtime packages loaded."); + } + for (const auto& package : loaded_packages) { + if (ImGui::TreeNode(package.name.c_str())) { + ImGui::Text("Version: %s", package.version.empty() ? "Unknown" : package.version.c_str()); + ImGui::Text("Live objects: %zu", package.live_object_count); + if (!package.description.empty()) { + ImGui::TextWrapped("%s", package.description.c_str()); + } + if (!package.dependencies.empty() && ImGui::TreeNode("Dependencies")) { + for (const auto& dependency : package.dependencies) { + ImGui::BulletText("%s", dependency.c_str()); } - ImGui::EndDisabled(); ImGui::TreePop(); } - ImGui::PopID(); - } - } - ImGui::EndChild(); + ImGui::TextUnformatted("Original path:"); + ImGui::SameLine(); + ImGui::TextWrapped("%s", package.original_path.string().c_str()); + ImGui::TextUnformatted("Loaded path:"); + ImGui::SameLine(); + ImGui::TextWrapped("%s", package.loaded_path.string().c_str()); - ImGui::Separator(); - ImGui::TextUnformatted("Loaded Packages"); - if (ImGui::BeginChild("LoadedPackages", ImVec2(0, 0), true)) { - if (loaded_packages.empty()) { - ImGui::TextUnformatted("No runtime packages loaded."); - } - for (const auto& package : loaded_packages) { - if (ImGui::TreeNode(package.name.c_str())) { - ImGui::Text("Version: %s", package.version.empty() ? "Unknown" : package.version.c_str()); - ImGui::Text("Live objects: %zu", package.live_object_count); - if (!package.description.empty()) { - ImGui::TextWrapped("%s", package.description.c_str()); + const auto draw_type_list = [](const char* label, const std::vector& type_names) { + if (type_names.empty()) { + return; } - if (!package.dependencies.empty() && ImGui::TreeNode("Dependencies")) { - for (const auto& dependency : package.dependencies) { - ImGui::BulletText("%s", dependency.c_str()); + if (ImGui::TreeNode(label)) { + for (const auto& type_name : type_names) { + ImGui::BulletText("%s", type_name.c_str()); } ImGui::TreePop(); } - ImGui::TextUnformatted("Original path:"); - ImGui::SameLine(); - ImGui::TextWrapped("%s", package.original_path.string().c_str()); - ImGui::TextUnformatted("Loaded path:"); - ImGui::SameLine(); - ImGui::TextWrapped("%s", package.loaded_path.string().c_str()); - - const auto draw_type_list = [](const char* label, const std::vector& type_names) { - if (type_names.empty()) { - return; - } - if (ImGui::TreeNode(label)) { - for (const auto& type_name : type_names) { - ImGui::BulletText("%s", type_name.c_str()); - } - ImGui::TreePop(); - } - }; - draw_type_list("Private components", package.private_component_types); - draw_type_list("Assets", package.asset_types); - draw_type_list("Data components", package.data_component_types); - draw_type_list("Systems", package.system_types); - draw_type_list("Layers", package.layer_types); - - if (ImGui::Button(("Reload##" + package.name).c_str())) { - const auto package_name = package.name; - ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { - PackageManager::Reload(package_name); - }); - } - ImGui::SameLine(); - if (ImGui::Button(("Unload##" + package.name).c_str())) { - const auto package_name = package.name; - ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { - PackageManager::Unload(package_name); - }); - } - ImGui::TreePop(); + }; + draw_type_list("Private components", package.private_component_types); + draw_type_list("Assets", package.asset_types); + draw_type_list("Data components", package.data_component_types); + draw_type_list("Systems", package.system_types); + draw_type_list("Layers", package.layer_types); + + if (ImGui::Button(("Reload##" + package.name).c_str())) { + const auto package_name = package.name; + ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { + PackageManager::Reload(package_name); + }); } + ImGui::SameLine(); + if (ImGui::Button(("Unload##" + package.name).c_str())) { + const auto package_name = package.name; + ApplicationContext::Get().QueueEndOfLoopAction([package_name]() { + PackageManager::Unload(package_name); + }); + } + ImGui::TreePop(); } } - ImGui::EndChild(); } - show_package_manager_window = package_manager_open; - ImGui::End(); + ImGui::EndChild(); } + show_package_manager_window = package_manager_open; + ImGui::End(); +} + +void EditorLayer::HandleSceneDeleteShortcut(const std::shared_ptr& scene) { if (scene && scene_camera_window_focused_ && Input::GetKey(GLFW_KEY_DELETE) == Input::KeyActionType::Press) { if (scene->IsEntityValid(selected_entity_)) { scene->DeleteEntity(selected_entity_); } } +} + +void EditorLayer::DrawViewportWindows(const std::shared_ptr& scene) { if (show_scene_window) SceneCameraWindow(); if (show_camera_window) @@ -823,22 +750,29 @@ void EditorLayer::PreUpdate() { } ImGui::End(); } +} - if (scene) { - const auto layers = ApplicationContext::Get().GetLayers(); - for (const auto& layer : layers) { - if (layer->enable_inspection) { - ImGui::Begin(layer->layer_name_.c_str()); - layer->OnInspect(editor_layer); - ImGui::End(); - } +void EditorLayer::DrawLayerInspectionWindows(const std::shared_ptr& scene, + const std::shared_ptr& editor_layer) { + if (!scene) { + return; + } + const auto layers = ApplicationContext::Get().GetLayers(); + for (const auto& layer : layers) { + if (layer->enable_inspection) { + ImGui::Begin(layer->layer_name_.c_str()); + layer->OnInspect(editor_layer); + ImGui::End(); } } +} +void EditorLayer::DrawProjectInspectionWindows(const std::shared_ptr& editor_layer) { Resources::OnInspect(editor_layer); AssetManager::OnInspect(editor_layer); ProjectManager::OnInspect(editor_layer); } + void EditorLayer::OnInspect(const std::shared_ptr& editor_layer) { ImGui::Checkbox("Scene Window", &show_scene_window); if (show_scene_window) { @@ -878,12 +812,94 @@ void EditorLayer::OnInspect(const std::shared_ptr& editor_layer) { } } -void EditorLayer::InitializeImGui() { - ImGui_ImplVulkan_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - ImGuizmo::BeginFrame(); +void EditorLayer::DrawMainMenuBar() { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(5, 5)); + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("View")) { + if (ImGui::BeginMenu("Layer Inspection")) { + for (const auto& layer : ApplicationContext::Get().GetLayers()) { + ImGui::Checkbox(layer->layer_name_.c_str(), &layer->enable_inspection); + } + ImGui::EndMenu(); + } + ImGui::MenuItem("Runtime Packages", nullptr, &show_package_manager_window); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Application")) { + if (ImGui::MenuItem("Exit")) { + ApplicationContext::Get().End(); + } + ImGui::EndMenu(); + } + + if (show_play_buttons) { + ImGui::Separator(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 2.0f)); + switch (ApplicationContext::Get().GetApplicationStatus()) { + case Application::ExecutionStatus::NotPlaying: { + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["PlayButton"]->GetImTextureId()); + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StepButton"]->GetImTextureId()); + if (ImGui::ImageButton("PlayButton", editor_icons_["PlayButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Play(); + } + if (ImGui::ImageButton("StepButton", editor_icons_["StepButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Step(); + } + ImGui::PopID(); + ImGui::PopID(); + break; + } + case Application::ExecutionStatus::Playing: { + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["PauseButton"]->GetImTextureId()); + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StopButton"]->GetImTextureId()); + if (ImGui::ImageButton("PauseButton", editor_icons_["PauseButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Pause(); + } + if (ImGui::ImageButton("StopButton", editor_icons_["StopButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Stop(); + } + ImGui::PopID(); + ImGui::PopID(); + break; + } + case Application::ExecutionStatus::Pause: { + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["PlayButton"]->GetImTextureId()); + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StepButton"]->GetImTextureId()); + ImGui::PushID((ImTextureID)(intptr_t)editor_icons_["StopButton"]->GetImTextureId()); + if (ImGui::ImageButton("PlayButton", editor_icons_["PlayButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Play(); + } + if (ImGui::ImageButton("StepButton", editor_icons_["StepButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Step(); + } + if (ImGui::ImageButton("StopButton", editor_icons_["StopButton"]->GetImTextureId(), {20, 20}, {0, 1}, + {1, 0})) { + ApplicationContext::Get().Stop(); + } + ImGui::PopID(); + ImGui::PopID(); + ImGui::PopID(); + break; + } + case Application::ExecutionStatus::Uninitialized: + case Application::ExecutionStatus::Step: + case Application::ExecutionStatus::OnDestroy: + break; + } + ImGui::PopStyleVar(); + } + ImGui::EndMainMenuBar(); + } + ImGui::PopStyleVar(); +} +void EditorLayer::DrawDockspace() { #pragma region Dock static bool opt_fullscreen_persistent = true; const bool opt_fullscreen = opt_fullscreen_persistent; diff --git a/EvoEngine_SDK/src/EditorTheme.cpp b/EvoEngine_SDK/src/EditorTheme.cpp new file mode 100644 index 00000000..e374b6ca --- /dev/null +++ b/EvoEngine_SDK/src/EditorTheme.cpp @@ -0,0 +1,195 @@ +#include "EditorTheme.hpp" + +using namespace evo_engine; + +namespace { +ImU32 ColorU32(const int r, const int g, const int b, const int a = 255) { + return IM_COL32(r, g, b, a); +} +} // namespace + +void editor_theme::ApplyDefault() { + ApplyEvoEngineLegacy(); +} + +void editor_theme::ApplyEvoEngineLegacy() { + ImGui::StyleColorsDark(); + ImNodes::StyleColorsDark(); + + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4* colors = style.Colors; + + style.WindowRounding = 5.3f; + style.ChildRounding = 0.0f; + style.FrameRounding = 2.3f; + style.GrabRounding = 0.0f; + style.PopupRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + style.TabRounding = 5.0f; + style.WindowMenuButtonPosition = ImGuiDir_Left; + style.ScrollbarSize = 14.0f; + style.GrabMinSize = 12.0f; + style.DockingSeparatorSize = 2.0f; + style.SeparatorTextBorderSize = 3.0f; + style.FrameBorderSize = 0.0f; + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.PopupBorderSize = 1.0f; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 0.90f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.30f, 0.30f, 0.30f, 0.90f); + colors[ImGuiCol_WindowBg] = ImVec4(0.09f, 0.09f, 0.15f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.05f, 0.05f, 0.10f, 0.85f); + colors[ImGuiCol_Border] = ImVec4(0.70f, 0.70f, 0.70f, 0.65f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.00f, 0.00f, 0.01f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.90f, 0.80f, 0.80f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.90f, 0.65f, 0.65f, 0.45f); + colors[ImGuiCol_TitleBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.83f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.00f, 0.00f, 0.00f, 0.87f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.01f, 0.01f, 0.02f, 0.80f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.55f, 0.53f, 0.55f, 0.51f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.56f, 0.56f, 0.56f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.56f, 0.56f, 0.56f, 0.91f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.83f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.70f, 0.70f, 0.70f, 0.62f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.30f, 0.30f, 0.30f, 0.84f); + colors[ImGuiCol_Button] = ImVec4(0.48f, 0.72f, 0.89f, 0.49f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.50f, 0.69f, 0.99f, 0.68f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.80f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.30f, 0.69f, 1.00f, 0.53f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.44f, 0.61f, 0.86f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.38f, 0.62f, 0.83f, 1.00f); + colors[ImGuiCol_TabSelectedOverline] = ImVec4(0.70f, 0.30f, 0.30f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.70f, 0.00f, 0.30f, 1.00f); + colors[ImGuiCol_TabSelected] = ImVec4(0.50f, 0.00f, 0.50f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.85f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(1.00f, 1.00f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(1.00f, 1.00f, 1.00f, 0.90f); + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); +} + +void editor_theme::ApplyEvoEngineDark() { + ImGui::StyleColorsDark(); + ImNodes::StyleColorsDark(); + + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4* colors = style.Colors; + + colors[ImGuiCol_Text] = ImVec4(0.92f, 0.94f, 0.95f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.42f, 0.45f, 0.47f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.045f, 0.050f, 0.058f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.055f, 0.062f, 0.072f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.065f, 0.072f, 0.084f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.18f, 0.20f, 0.22f, 0.90f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.085f, 0.095f, 0.110f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.18f, 0.27f, 0.26f, 0.90f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.16f, 0.34f, 0.24f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.032f, 0.036f, 0.042f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.055f, 0.062f, 0.072f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.032f, 0.036f, 0.042f, 0.82f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.032f, 0.036f, 0.042f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.040f, 0.045f, 0.052f, 0.90f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.18f, 0.20f, 0.22f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.27f, 0.30f, 0.32f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.34f, 0.38f, 0.40f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.46f, 0.78f, 0.32f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.40f, 0.68f, 0.28f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.56f, 0.88f, 0.36f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.13f, 0.145f, 0.16f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.20f, 0.29f, 0.28f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.25f, 0.39f, 0.30f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.105f, 0.118f, 0.136f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.18f, 0.27f, 0.26f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.34f, 0.26f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.18f, 0.20f, 0.22f, 0.75f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.24f, 0.46f, 0.36f, 0.95f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.35f, 0.68f, 0.42f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.30f, 0.54f, 0.34f, 0.25f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.38f, 0.72f, 0.42f, 0.70f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.46f, 0.82f, 0.48f, 0.95f); + colors[ImGuiCol_Tab] = ImVec4(0.050f, 0.056f, 0.065f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.16f, 0.22f, 0.22f, 1.00f); + colors[ImGuiCol_TabSelected] = ImVec4(0.075f, 0.085f, 0.098f, 1.00f); + colors[ImGuiCol_TabSelectedOverline] = ImVec4(0.42f, 0.78f, 0.26f, 1.00f); + colors[ImGuiCol_TabDimmed] = ImVec4(0.040f, 0.045f, 0.052f, 1.00f); + colors[ImGuiCol_TabDimmedSelected] = ImVec4(0.060f, 0.068f, 0.078f, 1.00f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.30f, 0.58f, 0.24f, 1.00f); + colors[ImGuiCol_DockingPreview] = ImVec4(0.38f, 0.78f, 0.34f, 0.35f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.030f, 0.034f, 0.040f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.62f, 0.66f, 0.68f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.52f, 0.88f, 0.32f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.42f, 0.70f, 0.30f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.54f, 0.88f, 0.34f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.12f, 0.14f, 0.16f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.22f, 0.25f, 0.28f, 1.00f); + colors[ImGuiCol_TableBorderLight] = ImVec4(0.16f, 0.18f, 0.20f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.035f); + colors[ImGuiCol_TextLink] = ImVec4(0.50f, 0.78f, 0.48f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.34f, 0.62f, 0.36f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.50f, 0.86f, 0.32f, 0.90f); + colors[ImGuiCol_NavCursor] = ImVec4(0.48f, 0.82f, 0.36f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.92f, 0.94f, 0.95f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.35f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.55f); + + style.WindowRounding = 4.0f; + style.ChildRounding = 4.0f; + style.FrameRounding = 4.0f; + style.GrabRounding = 3.0f; + style.PopupRounding = 4.0f; + style.ScrollbarRounding = 3.0f; + style.TabRounding = 4.0f; + style.WindowMenuButtonPosition = ImGuiDir_Right; + style.ScrollbarSize = 10.0f; + style.GrabMinSize = 10.0f; + style.DockingSeparatorSize = 1.0f; + style.SeparatorTextBorderSize = 2.0f; + style.FrameBorderSize = 0.0f; + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.PopupBorderSize = 1.0f; + + ImNodesStyle& node_style = ImNodes::GetStyle(); + node_style.NodeCornerRounding = 4.0f; + node_style.NodeBorderThickness = 1.0f; + node_style.LinkThickness = 2.5f; + node_style.Colors[ImNodesCol_NodeBackground] = ColorU32(22, 25, 29); + node_style.Colors[ImNodesCol_NodeBackgroundHovered] = ColorU32(32, 42, 40); + node_style.Colors[ImNodesCol_NodeBackgroundSelected] = ColorU32(34, 52, 42); + node_style.Colors[ImNodesCol_NodeOutline] = ColorU32(58, 64, 68); + node_style.Colors[ImNodesCol_TitleBar] = ColorU32(14, 16, 19); + node_style.Colors[ImNodesCol_TitleBarHovered] = ColorU32(28, 48, 38); + node_style.Colors[ImNodesCol_TitleBarSelected] = ColorU32(34, 64, 42); + node_style.Colors[ImNodesCol_Link] = ColorU32(86, 142, 88, 210); + node_style.Colors[ImNodesCol_LinkHovered] = ColorU32(122, 205, 92); + node_style.Colors[ImNodesCol_LinkSelected] = ColorU32(142, 230, 104); + node_style.Colors[ImNodesCol_Pin] = ColorU32(98, 170, 86, 190); + node_style.Colors[ImNodesCol_PinHovered] = ColorU32(144, 226, 104); + node_style.Colors[ImNodesCol_BoxSelector] = ColorU32(102, 190, 96, 38); + node_style.Colors[ImNodesCol_BoxSelectorOutline] = ColorU32(128, 220, 104, 160); + node_style.Colors[ImNodesCol_GridBackground] = ColorU32(12, 14, 17, 230); + node_style.Colors[ImNodesCol_GridLine] = ColorU32(150, 160, 150, 28); + node_style.Colors[ImNodesCol_GridLinePrimary] = ColorU32(170, 190, 170, 46); + node_style.Colors[ImNodesCol_MiniMapBackground] = ColorU32(10, 12, 14, 180); + node_style.Colors[ImNodesCol_MiniMapBackgroundHovered] = ColorU32(14, 17, 19, 220); + node_style.Colors[ImNodesCol_MiniMapOutline] = ColorU32(80, 92, 84, 140); + node_style.Colors[ImNodesCol_MiniMapOutlineHovered] = ColorU32(120, 150, 126, 220); + node_style.Colors[ImNodesCol_MiniMapNodeBackground] = ColorU32(140, 160, 144, 110); + node_style.Colors[ImNodesCol_MiniMapNodeBackgroundHovered] = ColorU32(180, 220, 174, 220); + node_style.Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = ColorU32(150, 230, 114, 255); + node_style.Colors[ImNodesCol_MiniMapNodeOutline] = ColorU32(180, 210, 180, 120); + node_style.Colors[ImNodesCol_MiniMapLink] = node_style.Colors[ImNodesCol_Link]; + node_style.Colors[ImNodesCol_MiniMapLinkSelected] = node_style.Colors[ImNodesCol_LinkSelected]; + node_style.Colors[ImNodesCol_MiniMapCanvas] = ColorU32(180, 220, 180, 28); + node_style.Colors[ImNodesCol_MiniMapCanvasOutline] = ColorU32(180, 220, 180, 200); +} diff --git a/EvoEngine_SDK/src/ImGuiLayer.cpp b/EvoEngine_SDK/src/ImGuiLayer.cpp new file mode 100644 index 00000000..93fbba2d --- /dev/null +++ b/EvoEngine_SDK/src/ImGuiLayer.cpp @@ -0,0 +1,20 @@ +#include "ImGuiLayer.hpp" + +using namespace evo_engine; + +void ImGuiLayer::OnDestroy() { + if (!ImGui::GetCurrentContext()) + return; + + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImNodes::DestroyContext(); + ImGui::DestroyContext(); +} + +void ImGuiLayer::PreUpdate() { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + ImGuizmo::BeginFrame(); +} diff --git a/EvoEngine_SDK/src/Input.cpp b/EvoEngine_SDK/src/Input.cpp index e075febb..622947d2 100644 --- a/EvoEngine_SDK/src/Input.cpp +++ b/EvoEngine_SDK/src/Input.cpp @@ -39,6 +39,9 @@ void Input::Dispatch(const InputEvent& event) { } if (!ApplicationContext::Get().GetLayer()) { const auto active_scene = ApplicationContext::Get().GetActiveScene(); + if (!active_scene) { + return; + } auto& scene_pressed_keys = active_scene->pressed_keys_; if (event.key_action == KeyActionType::Press) { diff --git a/EvoEngine_SDK/src/Platform.cpp b/EvoEngine_SDK/src/Platform.cpp index c7956192..df5f8505 100644 --- a/EvoEngine_SDK/src/Platform.cpp +++ b/EvoEngine_SDK/src/Platform.cpp @@ -5,6 +5,7 @@ #include "EditorLayer.hpp" #include "GeometryStorage.hpp" #include "GpuService.hpp" +#include "ImGuiLayer.hpp" #include "Mesh.hpp" #include "RenderLayer.hpp" #include "Resources.hpp" @@ -132,7 +133,7 @@ void Platform::Initialize(const ApplicationInitializationSettings& application_i graphics.render_texture_present_pipeline->color_attachment_formats = {1, graphics.swapchain_->GetImageFormat()}; graphics.render_texture_present_pipeline->Initialize(); } - if (const auto editor_layer = ApplicationContext::Get().GetLayer(); editor_layer) { + if (const auto imgui_layer = ApplicationContext::Get().GetLayer(); imgui_layer) { // Setup Dear ImGui context IMGUI_CHECKVERSION(); diff --git a/EvoEngine_SDK/src/ProjectManager.cpp b/EvoEngine_SDK/src/ProjectManager.cpp index f0793558..dc2a8344 100644 --- a/EvoEngine_SDK/src/ProjectManager.cpp +++ b/EvoEngine_SDK/src/ProjectManager.cpp @@ -10,10 +10,160 @@ #endif #include +#include +#include +#include #include using namespace evo_engine; +namespace { +constexpr const char* kDefaultEditorName = "EvoEngineEditor"; +constexpr const char* kDefaultApplicationName = "EvoEngine Editor"; + +void AddUnique(std::vector& values, const std::string& value) { + if (!value.empty() && std::find(values.begin(), values.end(), value) == values.end()) { + values.emplace_back(value); + } +} + +void ReadStringKey(const YAML::Node& in, const char* key, std::string& value) { + const auto node = in[key]; + if (node && node.IsScalar()) { + value = node.as(); + } +} + +void ReadStringSequenceKey(const YAML::Node& in, const char* key, std::vector& values) { + const auto node = in[key]; + if (!node || !node.IsSequence()) { + return; + } + values.clear(); + for (const auto& entry : node) { + if (entry.IsScalar()) { + AddUnique(values, entry.as()); + } + } +} + +uint64_t ReadStartSceneHandle(const YAML::Node& in) { + if (const auto start_scene_handle = in["start_scene_handle"]) { + return start_scene_handle.as(); + } + if (const auto start_scene_handle = in["m_startSceneHandle"]) { + return start_scene_handle.as(); + } + return 0; +} + +std::optional ReadExistingStartSceneHandle(const std::filesystem::path& path) { + if (!std::filesystem::exists(path) || std::filesystem::is_directory(path)) { + return std::nullopt; + } + try { + const auto scene_handle = ReadStartSceneHandle(YAML::LoadFile(path.string())); + if (scene_handle != 0) { + return scene_handle; + } + } catch (const std::exception& error) { + EVOENGINE_ERROR("Failed to read project start scene handle: " + std::string(error.what())) + } + return std::nullopt; +} + +void WriteProjectFile(const std::filesystem::path& path, const ProjectLaunchMetadata& metadata, + const std::optional start_scene_handle) { + if (const auto directory = path.parent_path(); !directory.empty() && !std::filesystem::exists(directory)) { + std::filesystem::create_directories(directory); + } + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "application_name" << YAML::Value << metadata.application_name; + out << YAML::Key << "preferred_editor" << YAML::Value << metadata.preferred_editor; + out << YAML::Key << "startup_runtime_packages" << YAML::Value << YAML::BeginSeq; + for (const auto& package_name : metadata.startup_runtime_packages) { + out << package_name; + } + out << YAML::EndSeq; + if (start_scene_handle) { + out << YAML::Key << "start_scene_handle" << YAML::Value << *start_scene_handle; + } + out << YAML::EndMap; + + std::ofstream file_out(path.string()); + file_out << out.c_str(); + file_out.flush(); +} + +void MergeApplicationLaunchMetadata(ProjectLaunchMetadata& metadata) { + const auto& application_info = ApplicationContext::Get().GetApplicationInfo(); + if (metadata.application_name == kDefaultApplicationName && !application_info.application_name.empty()) { + metadata.application_name = application_info.application_name; + } + for (const auto& package_name : application_info.startup_runtime_packages) { + AddUnique(metadata.startup_runtime_packages, package_name); + } + if (metadata.preferred_editor.empty()) { + metadata.preferred_editor = kDefaultEditorName; + } +} + +std::filesystem::path CurrentExecutablePath() { +#ifdef EVOENGINE_WINDOWS + std::wstring path(MAX_PATH, L'\0'); + const DWORD size = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (size == 0 || size == path.size()) { + return std::filesystem::absolute("EvoEngineEditor.exe"); + } + path.resize(size); + return path; +#else + return std::filesystem::absolute(kDefaultEditorName); +#endif +} + +std::filesystem::path LauncherExecutablePath() { +#ifdef EVOENGINE_WINDOWS + return CurrentExecutablePath().parent_path() / "EvoEngineLauncher.exe"; +#else + return CurrentExecutablePath().parent_path() / "EvoEngineLauncher"; +#endif +} + +bool LaunchLauncherProcess(std::string& error) { + const auto launcher_path = LauncherExecutablePath(); + if (!std::filesystem::exists(launcher_path)) { + error = "Could not find EvoEngineLauncher next to the editor executable."; + return false; + } + +#ifdef EVOENGINE_WINDOWS + std::wstring command_line = L"\"" + launcher_path.wstring() + L"\""; + STARTUPINFOW startup_info{}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_info{}; + const auto working_directory = launcher_path.parent_path().wstring(); + if (!CreateProcessW(nullptr, command_line.data(), nullptr, nullptr, FALSE, 0, nullptr, working_directory.c_str(), + &startup_info, &process_info)) { + error = "Failed to launch EvoEngineLauncher."; + return false; + } + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + return true; +#else + const auto command = "\"" + launcher_path.string() + "\" &"; + if (std::system(command.c_str()) != 0) { + error = "Failed to launch EvoEngineLauncher."; + return false; + } + return true; +#endif +} +} // namespace + std::weak_ptr ProjectManager::GetOrCreateFolder(const std::filesystem::path& assets_relative_path) { const auto& project_manager = GetInstance(); if (!assets_relative_path.is_relative()) { @@ -55,15 +205,15 @@ void ProjectManager::SetupDefaultScene() { std::stringstream string_stream; string_stream << stream.rdbuf(); YAML::Node in = YAML::Load(string_stream.str()); - uint64_t scene_handle = 0; - if (in["start_scene_handle"]) - scene_handle = in["start_scene_handle"].as(); - if (auto temp = AssetManager::GetAssetImpl(scene_handle)) { - scene = std::dynamic_pointer_cast(temp); - SetStartScene(scene); - SaveProject(); - ApplicationContext::Get().Attach(scene); - found_scene = true; + const uint64_t scene_handle = ReadStartSceneHandle(in); + if (scene_handle != 0) { + if (auto temp = AssetManager::GetAssetImpl(scene_handle)) { + scene = std::dynamic_pointer_cast(temp); + SetStartScene(scene); + SaveProject(); + ApplicationContext::Get().Attach(scene); + found_scene = true; + } } EVOENGINE_LOG("Found and loaded project") if (found_scene && project_manager.scene_post_load_function_.has_value()) { @@ -124,11 +274,64 @@ void ProjectManager::PreUpdate() { } bool ProjectManager::IsProjectIdle() { + return IsProjectLoaded(); +} + +ProjectState ProjectManager::GetProjectState() { const auto& project_manager = GetInstance(); + if (!HasProject()) { + return ProjectState::NoProject; + } + const auto asset_load_snapshot = AssetManager::GetAssetLoadSnapshot(); - return project_manager.new_project_path_.empty() && !project_manager.scan_assets_pending && - project_manager.pending_assets.empty() && !project_manager.project_asset_load_dispatched && - project_manager.pending_asset_size == 0 && !asset_load_snapshot.Active() && project_manager.start_scene_; + if (project_manager.new_project_path_.empty() && !project_manager.scan_assets_pending && + project_manager.pending_assets.empty() && !project_manager.project_asset_load_dispatched && + project_manager.pending_asset_size == 0 && !asset_load_snapshot.Active() && project_manager.start_scene_) { + return ProjectState::Loaded; + } + return ProjectState::Loading; +} + +bool ProjectManager::HasProject() { + const auto& project_manager = GetInstance(); + return !project_manager.project_path_.empty() || !project_manager.new_project_path_.empty(); +} + +bool ProjectManager::IsProjectLoaded() { + return GetProjectState() == ProjectState::Loaded; +} + +ProjectLaunchMetadata ProjectManager::LoadProjectLaunchMetadata(const std::filesystem::path& path) { + ProjectLaunchMetadata metadata; + if (path.empty() || !std::filesystem::exists(path) || std::filesystem::is_directory(path)) { + return metadata; + } + + try { + const auto in = YAML::LoadFile(path.string()); + ReadStringKey(in, "application_name", metadata.application_name); + ReadStringKey(in, "preferred_editor", metadata.preferred_editor); + ReadStringSequenceKey(in, "startup_runtime_packages", metadata.startup_runtime_packages); + } catch (const std::exception& error) { + EVOENGINE_ERROR("Failed to read project launch metadata: " + std::string(error.what())) + } + if (metadata.application_name.empty()) { + metadata.application_name = kDefaultApplicationName; + } + if (metadata.preferred_editor.empty()) { + metadata.preferred_editor = kDefaultEditorName; + } + return metadata; +} + +void ProjectManager::SaveProjectLaunchMetadata(const std::filesystem::path& path, + const ProjectLaunchMetadata& metadata) { + WriteProjectFile(path, metadata, ReadExistingStartSceneHandle(path)); +} + +ProjectLaunchMetadata ProjectManager::GetProjectLaunchMetadata() { + const auto& project_manager = GetInstance(); + return project_manager.project_launch_metadata_; } void ProjectManager::LoadAllPendingAssets() { @@ -161,17 +364,10 @@ void ProjectManager::LoadAllPendingAssets() { void ProjectManager::SaveProject() { const auto& project_manager = GetInstance(); - if (const auto directory = project_manager.project_path_.parent_path(); !std::filesystem::exists(directory)) { - std::filesystem::create_directories(directory); - } - YAML::Emitter out; - out << YAML::BeginMap; - out << YAML::Key << "start_scene_handle" << YAML::Value << project_manager.start_scene_->GetHandle(); - out << YAML::EndMap; - std::ofstream file_out(project_manager.project_path_.string()); - file_out << out.c_str(); - file_out.flush(); + WriteProjectFile(project_manager.project_path_, project_manager.project_launch_metadata_, + static_cast(project_manager.start_scene_->GetHandle())); } + std::filesystem::path ProjectManager::GetProjectPath() { auto& project_manager = GetInstance(); return project_manager.project_path_; @@ -221,8 +417,7 @@ bool ProjectManager::IsValidAssetFileName(const std::filesystem::path& path) { void ProjectManager::GetOrCreateProject(const std::filesystem::path& path) { auto& project_manager = GetInstance(); - project_manager.new_project_path_ = path; - auto project_absolute_path = std::filesystem::absolute(project_manager.new_project_path_); + auto project_absolute_path = std::filesystem::absolute(path); if (std::filesystem::is_directory(project_absolute_path)) { EVOENGINE_ERROR("Path is directory!") return; @@ -235,8 +430,11 @@ void ProjectManager::GetOrCreateProject(const std::filesystem::path& path) { EVOENGINE_ERROR("Wrong extension!") return; } + project_manager.new_project_path_ = project_absolute_path; project_manager.project_path_ = project_absolute_path; project_manager.assets_folder_path = project_absolute_path.parent_path() / "Assets"; + project_manager.project_launch_metadata_ = LoadProjectLaunchMetadata(project_absolute_path); + MergeApplicationLaunchMetadata(project_manager.project_launch_metadata_); AssetManager::Clear(); FileManager::Clear(); ApplicationContext::Get().Reset(); @@ -345,31 +543,37 @@ void ProjectManager::OnDestroy() { project_manager.new_scene_customizer_.reset(); project_manager.current_focused_folder_.reset(); project_manager.start_scene_.reset(); + project_manager.project_launch_metadata_ = {}; + project_manager.new_project_path_ = ""; + project_manager.project_path_ = ""; + project_manager.assets_folder_path = ""; project_manager.initialized = false; } void ProjectManager::OnInspect(const std::shared_ptr& editor_layer) { auto& project_manager = GetInstance(); - auto& asset_manager = AssetManager::GetInstance(); + static std::string close_project_error; if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("Project")) { ImGui::Text(("Current Project path: " + project_manager.project_path_.string()).c_str()); - FileUtils::SaveFile( - "Create or load New Project", "Project", {".eveproj"}, - [](const std::filesystem::path& file_path) { - try { - GetOrCreateProject(file_path); - } catch (const std::exception& e) { - EVOENGINE_ERROR(std::string(e.what()) + ": Failed to create/load from " + file_path.string()) - } - }, - false); - if (ImGui::Button("Save")) { SaveProject(); } + if (ImGui::Button("Close Project")) { + close_project_error.clear(); + SaveProject(); + std::string error; + if (LaunchLauncherProcess(error)) { + ApplicationContext::Get().End(); + } else { + close_project_error = error; + } + } + if (!close_project_error.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "%s", close_project_error.c_str()); + } ImGui::EndMenu(); } diff --git a/EvoEngine_SDK/src/WindowLayer.cpp b/EvoEngine_SDK/src/WindowLayer.cpp index 504a1e8a..0fa3f1fa 100644 --- a/EvoEngine_SDK/src/WindowLayer.cpp +++ b/EvoEngine_SDK/src/WindowLayer.cpp @@ -1,6 +1,7 @@ #include "WindowLayer.hpp" #include "Application.hpp" #include "EditorLayer.hpp" +#include "ImGuiLayer.hpp" #include "Platform.hpp" #include "ProjectManager.hpp" #include "RenderLayer.hpp" @@ -54,7 +55,7 @@ void WindowLayer::OnDestroy() { } void WindowLayer::Render() { - if (const auto editor_layer = ApplicationContext::Get().GetLayer()) { + if (const auto imgui_layer = ApplicationContext::Get().GetLayer()) { Platform::RecordCommandsMainQueue([&](const VkCommandBuffer vk_command_buffer) { Platform::EverythingBarrier(vk_command_buffer); Platform::TransitImageLayout(vk_command_buffer, Platform::GetSwapchain()->GetVkImage(), diff --git a/EvoEngine_Tests/CMakeLists.txt b/EvoEngine_Tests/CMakeLists.txt index 74746b98..9bc023c0 100644 --- a/EvoEngine_Tests/CMakeLists.txt +++ b/EvoEngine_Tests/CMakeLists.txt @@ -18,8 +18,10 @@ file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Rendering" EVOENGINE_TEST_SCRIPT add_executable(EvoEngine_Tests Core/TaskRuntimeTest.cpp Core/AssetManagerTest.cpp + Core/LauncherUtilsTest.cpp Core/GpuServiceTest.cpp Rendering/RenderDemoPythonApiTest.cpp + ${CMAKE_SOURCE_DIR}/EvoEngine_App/src/LauncherUtils.cpp ) target_link_libraries(EvoEngine_Tests PRIVATE @@ -29,6 +31,7 @@ target_link_libraries(EvoEngine_Tests target_include_directories(EvoEngine_Tests PRIVATE ${EVOENGINE_SDK_INCLUDES} + ${CMAKE_SOURCE_DIR}/EvoEngine_App/include ${3RDPARTY_DIR}/stb/stb ) target_compile_definitions(EvoEngine_Tests @@ -57,6 +60,21 @@ gtest_discover_tests(EvoEngine_Tests TEST_FILTER AssetManager.* PROPERTIES LABELS "core,assets,jobs" ) +gtest_discover_tests(EvoEngine_Tests + WORKING_DIRECTORY "$" + TEST_FILTER ProjectManager.* + PROPERTIES LABELS "core,project" + ) +gtest_discover_tests(EvoEngine_Tests + WORKING_DIRECTORY "$" + TEST_FILTER PackageManager.* + PROPERTIES LABELS "core,project" + ) +gtest_discover_tests(EvoEngine_Tests + WORKING_DIRECTORY "$" + TEST_FILTER LauncherUtils.* + PROPERTIES LABELS "core,launcher" + ) gtest_discover_tests(EvoEngine_Tests WORKING_DIRECTORY "$" TEST_FILTER GpuService.* @@ -89,3 +107,28 @@ exit_on_complete: true TIMEOUT 240 ) endif() + +if(WIN32 AND TARGET EvoEngineLauncher AND TARGET EvoEngineEditor) + add_test(NAME Launcher.ProcessBoundaries + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/Launcher/launcher_process_smoke.py" + --launcher "$" + --editor "$" + --project "${CMAKE_SOURCE_DIR}/Resources/EvoEngine-DemoProjects/Rendering/Rendering.eveproj" + ) + set_tests_properties(Launcher.ProcessBoundaries + PROPERTIES + WORKING_DIRECTORY "$" + LABELS "launcher,app" + TIMEOUT 90 + ) + add_test(NAME Launcher.WindowsUiSmoke + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/Launcher/launcher_ui_smoke.py" + --launcher "$" + ) + set_tests_properties(Launcher.WindowsUiSmoke + PROPERTIES + WORKING_DIRECTORY "$" + LABELS "launcher,ui,app" + TIMEOUT 90 + ) +endif() diff --git a/EvoEngine_Tests/Core/AssetManagerTest.cpp b/EvoEngine_Tests/Core/AssetManagerTest.cpp index f65da26c..e3e4f3f1 100644 --- a/EvoEngine_Tests/Core/AssetManagerTest.cpp +++ b/EvoEngine_Tests/Core/AssetManagerTest.cpp @@ -7,6 +7,9 @@ #include "AssetManager.hpp" #include "IAsset.hpp" #include "Jobs.hpp" +#include "PackageManager.hpp" +#include "ProjectManager.hpp" +#include "Scene.hpp" #include #include @@ -404,6 +407,41 @@ class TempProject { std::filesystem::path root_; }; +class TempPackageDirectory { + public: + TempPackageDirectory() { + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + root_ = std::filesystem::temp_directory_path() / ("EvoEnginePackageManagerTest_" + std::to_string(now)); + std::filesystem::create_directories(root_); + } + + ~TempPackageDirectory() { + std::error_code error; + std::filesystem::remove_all(root_, error); + } + + [[nodiscard]] std::filesystem::path RootPath() const { + return root_; + } + + void WritePackageManifest(const std::string& package_name, const std::string& library_name, + const bool create_library) const { + std::ofstream manifest_file(root_ / (package_name + ".evepackage")); + manifest_file << "name: " << package_name << "\n"; + manifest_file << "library: " << library_name << "\n"; + manifest_file << "version: 0.1.0\n"; + manifest_file << "description: Test package.\n"; + manifest_file.close(); + if (create_library) { + std::ofstream library_file(root_ / library_name); + library_file << "test package library placeholder"; + } + } + + private: + std::filesystem::path root_; +}; + void WriteBlockingAssetFixture(const TempProject& project) { const auto asset_path = project.AssetsPath() / ("Coalesced" + std::string(kBlockingAssetExtension)); { @@ -459,6 +497,220 @@ ApplicationInitializationSettings TestApplicationSettings(const TempProject& pro } } // namespace +TEST(ProjectManager, ReportsNoProjectBeforeProjectSelection) { + Application app; + ApplicationContextScope scope(app); + + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::NoProject); + EXPECT_FALSE(ProjectManager::HasProject()); + EXPECT_FALSE(ProjectManager::IsProjectLoaded()); + EXPECT_FALSE(ProjectManager::IsProjectIdle()); +} + +TEST(ProjectManager, ReportsLoadingAfterProjectSelectionWithoutStartScene) { + TempProject project; + + Application app; + ApplicationContextScope scope(app); + app.Initialize(TestApplicationSettings(project)); + + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::Loading); + EXPECT_TRUE(ProjectManager::HasProject()); + EXPECT_FALSE(ProjectManager::IsProjectLoaded()); + EXPECT_FALSE(ProjectManager::IsProjectIdle()); +} + +TEST(ProjectManager, ReportsLoadedProjectAndResetsOnTerminate) { + TempProject project; + + Application app; + ApplicationContextScope scope(app); + app.Initialize(TestApplicationSettings(project)); + ProjectManager::SetStartScene(std::make_shared()); + + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::Loaded); + EXPECT_TRUE(ProjectManager::HasProject()); + EXPECT_TRUE(ProjectManager::IsProjectLoaded()); + EXPECT_TRUE(ProjectManager::IsProjectIdle()); + + app.Terminate(); + + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::NoProject); + EXPECT_FALSE(ProjectManager::HasProject()); + EXPECT_FALSE(ProjectManager::IsProjectLoaded()); + EXPECT_FALSE(ProjectManager::IsProjectIdle()); +} + +TEST(ProjectManager, LoadsDefaultLaunchMetadataForLegacyProjectFile) { + TempProject project; + std::ofstream project_file(project.ProjectPath()); + project_file << "start_scene_handle: 42\n"; + project_file.close(); + + const auto metadata = ProjectManager::LoadProjectLaunchMetadata(project.ProjectPath()); + + EXPECT_EQ(metadata.application_name, "EvoEngine Editor"); + EXPECT_EQ(metadata.preferred_editor, "EvoEngineEditor"); + EXPECT_TRUE(metadata.startup_runtime_packages.empty()); +} + +TEST(ProjectManager, LoadsLaunchMetadataFromProjectFile) { + TempProject project; + std::ofstream project_file(project.ProjectPath()); + project_file << "application_name: Metadata Test\n"; + project_file << "preferred_editor: EvoEngineEditor\n"; + project_file << "startup_runtime_packages:\n"; + project_file << " - PackageA\n"; + project_file << " - PackageB\n"; + project_file << "start_scene_handle: 42\n"; + project_file.close(); + + const auto metadata = ProjectManager::LoadProjectLaunchMetadata(project.ProjectPath()); + + EXPECT_EQ(metadata.application_name, "Metadata Test"); + EXPECT_EQ(metadata.preferred_editor, "EvoEngineEditor"); + ASSERT_EQ(metadata.startup_runtime_packages.size(), 2); + EXPECT_EQ(metadata.startup_runtime_packages[0], "PackageA"); + EXPECT_EQ(metadata.startup_runtime_packages[1], "PackageB"); +} + +TEST(ProjectManager, SaveLaunchMetadataCreatesProjectManifestWithoutOpeningProject) { + TempProject project; + ProjectLaunchMetadata metadata; + metadata.application_name = "Template Project"; + metadata.preferred_editor = "EvoEngineEditor"; + metadata.startup_runtime_packages = {"EcoSysLab", "DigitalAgriculture"}; + + ProjectManager::SaveProjectLaunchMetadata(project.ProjectPath(), metadata); + + const auto loaded_metadata = ProjectManager::LoadProjectLaunchMetadata(project.ProjectPath()); + EXPECT_EQ(loaded_metadata.application_name, "Template Project"); + ASSERT_EQ(loaded_metadata.startup_runtime_packages.size(), 2); + EXPECT_EQ(loaded_metadata.startup_runtime_packages[0], "EcoSysLab"); + EXPECT_EQ(loaded_metadata.startup_runtime_packages[1], "DigitalAgriculture"); + Application app; + ApplicationContextScope scope(app); + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::NoProject); + EXPECT_FALSE(YAML::LoadFile(project.ProjectPath().string())["start_scene_handle"]); +} + +TEST(ProjectManager, OpensMetadataOnlyProjectByCreatingDefaultStartScene) { + TempProject project; + ProjectLaunchMetadata metadata; + metadata.application_name = "Template Project"; + metadata.preferred_editor = "EvoEngineEditor"; + + ProjectManager::SaveProjectLaunchMetadata(project.ProjectPath(), metadata); + ASSERT_FALSE(YAML::LoadFile(project.ProjectPath().string())["start_scene_handle"]); + + Application app; + ApplicationContextScope scope(app); + auto settings = TestApplicationSettings(project); + settings.load_project_start_scene = true; + + ASSERT_NO_THROW(app.Initialize(settings)); + + EXPECT_EQ(ProjectManager::GetProjectState(), ProjectState::Loaded); + EXPECT_TRUE(ProjectManager::IsProjectLoaded()); + EXPECT_TRUE(ProjectManager::GetStartScene().lock()); + + const auto project_yaml = YAML::LoadFile(project.ProjectPath().string()); + ASSERT_TRUE(project_yaml["start_scene_handle"]); + EXPECT_NE(project_yaml["start_scene_handle"].as(), 0); +} + +TEST(ProjectManager, SaveLaunchMetadataPreservesExistingStartSceneHandle) { + TempProject project; + std::ofstream project_file(project.ProjectPath()); + project_file << "start_scene_handle: 42\n"; + project_file.close(); + + ProjectLaunchMetadata metadata; + metadata.application_name = "Template Project"; + metadata.preferred_editor = "EvoEngineEditor"; + metadata.startup_runtime_packages = {"LogGrading"}; + ProjectManager::SaveProjectLaunchMetadata(project.ProjectPath(), metadata); + + const auto project_yaml = YAML::LoadFile(project.ProjectPath().string()); + EXPECT_EQ(project_yaml["start_scene_handle"].as(), 42); + const auto loaded_metadata = ProjectManager::LoadProjectLaunchMetadata(project.ProjectPath()); + EXPECT_EQ(loaded_metadata.application_name, "Template Project"); + ASSERT_EQ(loaded_metadata.startup_runtime_packages.size(), 1); + EXPECT_EQ(loaded_metadata.startup_runtime_packages[0], "LogGrading"); +} + +TEST(ProjectManager, MergesProjectLaunchMetadataIntoApplicationSettings) { + TempProject project; + std::ofstream project_file(project.ProjectPath()); + project_file << "application_name: Metadata Test\n"; + project_file << "preferred_editor: EvoEngineEditor\n"; + project_file << "startup_runtime_packages:\n"; + project_file << " - MissingPackageForMetadataTest\n"; + project_file << "start_scene_handle: 42\n"; + project_file.close(); + + Application app; + ApplicationContextScope scope(app); + app.Initialize(TestApplicationSettings(project)); + + const auto& settings = app.GetApplicationInfo(); + EXPECT_TRUE(settings.enable_runtime_packages); + ASSERT_EQ(settings.startup_runtime_packages.size(), 1); + EXPECT_EQ(settings.startup_runtime_packages[0], "MissingPackageForMetadataTest"); + + const auto metadata = ProjectManager::GetProjectLaunchMetadata(); + EXPECT_EQ(metadata.application_name, "Metadata Test"); + ASSERT_EQ(metadata.startup_runtime_packages.size(), 1); + EXPECT_EQ(metadata.startup_runtime_packages[0], "MissingPackageForMetadataTest"); +} + +TEST(ProjectManager, SaveProjectPersistsLaunchMetadata) { + TempProject project; + std::ofstream project_file(project.ProjectPath()); + project_file << "application_name: Metadata Test\n"; + project_file << "preferred_editor: EvoEngineEditor\n"; + project_file << "startup_runtime_packages:\n"; + project_file << " - MissingPackageForMetadataTest\n"; + project_file << "start_scene_handle: 42\n"; + project_file.close(); + + Application app; + ApplicationContextScope scope(app); + app.Initialize(TestApplicationSettings(project)); + ProjectManager::SetStartScene(std::make_shared()); + ProjectManager::SaveProject(); + + const auto metadata = ProjectManager::LoadProjectLaunchMetadata(project.ProjectPath()); + EXPECT_EQ(metadata.application_name, "Metadata Test"); + ASSERT_EQ(metadata.startup_runtime_packages.size(), 1); + EXPECT_EQ(metadata.startup_runtime_packages[0], "MissingPackageForMetadataTest"); +} + +TEST(PackageManager, ReportsManifestLibraryAvailability) { + TempPackageDirectory package_directory; + package_directory.WritePackageManifest("AvailablePackage", "AvailablePackage.dll", true); + package_directory.WritePackageManifest("MissingLibraryPackage", "MissingLibraryPackage.dll", false); + + Application app; + ApplicationContextScope scope(app); + PackageManager::Initialize({package_directory.RootPath()}, {}); + + const auto packages = PackageManager::GetAvailablePackages(); + auto find_package = [&](const std::string& package_name) { + return std::find_if(packages.begin(), packages.end(), [&](const AvailablePackageInfo& package) { + return package.name == package_name; + }); + }; + + const auto available_package = find_package("AvailablePackage"); + ASSERT_NE(available_package, packages.end()); + EXPECT_TRUE(available_package->library_exists); + + const auto missing_library_package = find_package("MissingLibraryPackage"); + ASSERT_NE(missing_library_package, packages.end()); + EXPECT_FALSE(missing_library_package->library_exists); +} + TEST(AssetManager, BlockingAccessJoinsInFlightSynchronousProjectLoad) { ResetBlockingLoadState(); TempProject project; diff --git a/EvoEngine_Tests/Core/LauncherUtilsTest.cpp b/EvoEngine_Tests/Core/LauncherUtilsTest.cpp new file mode 100644 index 00000000..4b1666b1 --- /dev/null +++ b/EvoEngine_Tests/Core/LauncherUtilsTest.cpp @@ -0,0 +1,152 @@ +#include "EvoEngine_SDK_PCH.hpp" + +#include + +#include "LauncherUtils.hpp" + +#include +#include +#include + +using namespace evo_engine; + +namespace { +class TempLauncherDirectory { + public: + TempLauncherDirectory() { + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + root_ = std::filesystem::temp_directory_path() / ("EvoEngineLauncherTest_" + std::to_string(now)); + std::filesystem::create_directories(root_); + } + + ~TempLauncherDirectory() { + std::error_code error; + std::filesystem::remove_all(root_, error); + } + + [[nodiscard]] std::filesystem::path RootPath() const { + return root_; + } + + [[nodiscard]] std::filesystem::path WriteProject(const std::string& name) const { + const auto path = root_ / (name + ".eveproj"); + std::ofstream project_file(path); + project_file << "application_name: " << name << "\n"; + return path; + } + + private: + std::filesystem::path root_; +}; + +AvailablePackageInfo PackageInfo(const std::string& name, const bool library_exists) { + AvailablePackageInfo package; + package.name = name; + package.library_exists = library_exists; + return package; +} +} // namespace + +TEST(LauncherUtils, GenericTemplateIsAlwaysAvailable) { + const auto& templates = launcher::ProjectTemplates(); + ASSERT_FALSE(templates.empty()); + EXPECT_EQ(templates.front().name, "Generic"); + EXPECT_TRUE(launcher::IsTemplateAvailable(templates.front(), {})); +} + +TEST(LauncherUtils, PackageBackedTemplatesRequireAllPackages) { + const launcher::ProjectTemplate lsystem_template{"LSystem", {"LSystem", "DigitalAgriculture"}}; + const auto full_availability = + launcher::BuildPackageAvailability({PackageInfo("LSystem", true), PackageInfo("DigitalAgriculture", true)}); + const auto missing_manifest = launcher::BuildPackageAvailability({PackageInfo("LSystem", true)}); + const auto missing_library = + launcher::BuildPackageAvailability({PackageInfo("LSystem", true), PackageInfo("DigitalAgriculture", false)}); + + EXPECT_TRUE(launcher::IsTemplateAvailable(lsystem_template, full_availability)); + EXPECT_FALSE(launcher::IsTemplateAvailable(lsystem_template, missing_manifest)); + EXPECT_FALSE(launcher::IsTemplateAvailable(lsystem_template, missing_library)); + EXPECT_EQ(launcher::MissingPackages(missing_library, lsystem_template.startup_runtime_packages), + std::vector{"DigitalAgriculture"}); +} + +TEST(LauncherUtils, UnavailableSelectedTemplateFallsBackToGeneric) { + const auto& templates = launcher::ProjectTemplates(); + ASSERT_GT(templates.size(), 1); + + EXPECT_EQ(launcher::SelectAvailableTemplateIndex(templates, {}, 1), 0); + EXPECT_EQ(launcher::SelectAvailableTemplateIndex(templates, {}, -1), 0); + EXPECT_EQ(launcher::SelectAvailableTemplateIndex(templates, {}, static_cast(templates.size())), 0); + EXPECT_EQ(launcher::SelectAvailableTemplateIndex(templates, {}, 0), 0); +} + +TEST(LauncherUtils, ValidatesProjectNamesAndCreatePaths) { + TempLauncherDirectory temp; + const launcher::PackageAvailability availability; + const auto metadata = launcher::BuildProjectLaunchMetadata("NewProject", launcher::ProjectTemplates().front()); + const auto derived_path = launcher::BuildDerivedProjectPath(temp.RootPath(), "NewProject"); + + EXPECT_EQ(launcher::Trim(" NewProject\t"), "NewProject"); + EXPECT_TRUE(launcher::ValidateCreateProjectRequest("NewProject", temp.RootPath(), derived_path.folder, + derived_path.project_file, metadata, availability) + .empty()); + EXPECT_FALSE(launcher::ValidateCreateProjectRequest("", temp.RootPath(), derived_path.folder, + derived_path.project_file, metadata, availability) + .empty()); + EXPECT_FALSE(launcher::ValidateCreateProjectRequest("Bad/Name", temp.RootPath(), derived_path.folder, + derived_path.project_file, metadata, availability) + .empty()); + EXPECT_FALSE(launcher::ValidateCreateProjectRequest("Bad*Name", temp.RootPath(), derived_path.folder, + derived_path.project_file, metadata, availability) + .empty()); + EXPECT_FALSE(launcher::ValidateCreateProjectRequest("NewProject", temp.RootPath() / "missing", derived_path.folder, + derived_path.project_file, metadata, availability) + .empty()); + + std::filesystem::create_directories(derived_path.folder); + EXPECT_EQ(launcher::ValidateCreateProjectRequest("NewProject", temp.RootPath(), derived_path.folder, + derived_path.project_file, metadata, availability), + "Project folder already exists."); + + const auto existing_project_file = temp.WriteProject("ExistingProject"); + EXPECT_EQ(launcher::ValidateCreateProjectRequest("ExistingProject", temp.RootPath(), temp.RootPath() / "OtherFolder", + existing_project_file, metadata, availability), + "Project file already exists."); +} + +TEST(LauncherUtils, RecentProjectsAreNormalizedDeduplicatedPrunedAndCapped) { + TempLauncherDirectory temp; + std::vector valid_projects; + for (int i = 0; i < 9; ++i) { + valid_projects.emplace_back(temp.WriteProject("Project" + std::to_string(i))); + } + const auto settings_path = temp.RootPath() / "settings.yaml"; + { + std::ofstream settings(settings_path); + settings << "recent_projects:\n"; + settings << " - " << valid_projects[0].string() << "\n"; + settings << " - " << valid_projects[0].string() << "\n"; + settings << " - " << (temp.RootPath() / "Missing.eveproj").string() << "\n"; + settings << " - " << (temp.RootPath() / "Wrong.txt").string() << "\n"; + for (int i = 1; i < 9; ++i) { + settings << " - " << valid_projects[static_cast(i)].string() << "\n"; + } + } + + bool pruned = false; + auto recent_projects = launcher::LoadRecentProjects(settings_path, pruned); + EXPECT_TRUE(pruned); + ASSERT_EQ(recent_projects.size(), launcher::kMaxRecentProjectCount); + EXPECT_EQ(recent_projects.front(), launcher::NormalizeProjectPath(valid_projects[0])); + EXPECT_EQ( + std::count(recent_projects.begin(), recent_projects.end(), launcher::NormalizeProjectPath(valid_projects[0])), 1); + + launcher::AddRecentProject(recent_projects, valid_projects[8]); + EXPECT_EQ(recent_projects.front(), launcher::NormalizeProjectPath(valid_projects[8])); + EXPECT_EQ(recent_projects.size(), launcher::kMaxRecentProjectCount); + + launcher::SaveRecentProjects(settings_path, recent_projects); + bool saved_pruned = false; + const auto reloaded_projects = launcher::LoadRecentProjects(settings_path, saved_pruned); + EXPECT_FALSE(saved_pruned); + EXPECT_EQ(reloaded_projects, recent_projects); +} diff --git a/EvoEngine_Tests/Launcher/launcher_process_smoke.py b/EvoEngine_Tests/Launcher/launcher_process_smoke.py new file mode 100644 index 00000000..f6d7dd6f --- /dev/null +++ b/EvoEngine_Tests/Launcher/launcher_process_smoke.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import os +import subprocess +import sys +import tempfile +import time +from pathlib import Path + + +def process_ids(image_name: str) -> set[int]: + output = subprocess.check_output( + ["tasklist", "/FI", f"IMAGENAME eq {image_name}", "/FO", "CSV", "/NH"], + text=True, + stderr=subprocess.DEVNULL, + ) + ids: set[int] = set() + for row in csv.reader(output.splitlines()): + if len(row) >= 2 and row[0].lower() == image_name.lower(): + ids.add(int(row[1])) + return ids + + +def kill_new_processes(image_name: str, before: set[int]) -> None: + for pid in process_ids(image_name) - before: + subprocess.run(["taskkill", "/PID", str(pid), "/F", "/T"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def kill_process(process: subprocess.Popen[object]) -> None: + if process.poll() is None: + subprocess.run(["taskkill", "/PID", str(process.pid), "/F", "/T"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + pass + + +def write_metadata_only_project(root: Path) -> Path: + project_dir = root / "MetadataOnlyProject" + (project_dir / "Assets").mkdir(parents=True) + project_path = project_dir / "MetadataOnlyProject.eveproj" + project_path.write_text( + "application_name: Metadata Only Project\n" + "preferred_editor: EvoEngineEditor\n" + "startup_runtime_packages: []\n", + encoding="utf-8", + ) + return project_path + + +def has_start_scene_handle(project_path: Path) -> bool: + for line in project_path.read_text(encoding="utf-8").splitlines(): + key, separator, value = line.partition(":") + if key.strip() == "start_scene_handle" and separator: + try: + return int(value.strip()) != 0 + except ValueError: + return False + return False + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--launcher", required=True, type=Path) + parser.add_argument("--editor", required=True, type=Path) + parser.add_argument("--project", required=True, type=Path) + args = parser.parse_args() + + launcher_before = process_ids("EvoEngineLauncher.exe") + editor_before = process_ids("EvoEngineEditor.exe") + project_path = args.project.resolve() + + try: + completed = subprocess.run([str(args.editor)], cwd=args.editor.parent, timeout=20) + if completed.returncode != 0: + print(f"Editor without project returned {completed.returncode}, expected 0.") + return 1 + time.sleep(2) + if not process_ids("EvoEngineLauncher.exe") - launcher_before: + print("Editor without project did not spawn EvoEngineLauncher.") + return 1 + kill_new_processes("EvoEngineLauncher.exe", launcher_before) + + editor = subprocess.Popen([str(args.editor), "--project", str(project_path)], cwd=args.editor.parent) + time.sleep(4) + if editor.poll() is not None: + print(f"Editor with project exited early with code {editor.returncode}.") + return 1 + kill_process(editor) + + with tempfile.TemporaryDirectory(prefix="EvoEngineLauncherProcessSmoke_") as temp_dir: + metadata_only_project = write_metadata_only_project(Path(temp_dir)) + editor = subprocess.Popen([str(args.editor), "--project", str(metadata_only_project)], cwd=args.editor.parent) + time.sleep(6) + if editor.poll() is not None: + print(f"Editor with metadata-only project exited early with code {editor.returncode}.") + return 1 + kill_process(editor) + if not has_start_scene_handle(metadata_only_project): + print("Editor did not persist start_scene_handle for metadata-only project.") + return 1 + + env = os.environ.copy() + env["EVOENGINE_LAUNCHER_TEST_OPEN_PROJECT"] = str(project_path) + launcher = subprocess.Popen([str(args.launcher)], cwd=args.launcher.parent, env=env) + try: + launcher.wait(timeout=20) + except subprocess.TimeoutExpired: + print("Launcher did not exit after test open-project hook.") + return 1 + time.sleep(2) + if not process_ids("EvoEngineEditor.exe") - editor_before: + print("Launcher open-project hook did not spawn EvoEngineEditor.") + return 1 + return 0 + finally: + kill_new_processes("EvoEngineLauncher.exe", launcher_before) + kill_new_processes("EvoEngineEditor.exe", editor_before) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/EvoEngine_Tests/Launcher/launcher_ui_smoke.py b/EvoEngine_Tests/Launcher/launcher_ui_smoke.py new file mode 100644 index 00000000..09f0b211 --- /dev/null +++ b/EvoEngine_Tests/Launcher/launcher_ui_smoke.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import ctypes +import os +import subprocess +import tempfile +import time +from ctypes import wintypes +from pathlib import Path + + +user32 = ctypes.windll.user32 +gdi32 = ctypes.windll.gdi32 + + +class RECT(ctypes.Structure): + _fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long), ("right", ctypes.c_long), ("bottom", ctypes.c_long)] + + +class BITMAPINFOHEADER(ctypes.Structure): + _fields_ = [ + ("biSize", wintypes.DWORD), + ("biWidth", ctypes.c_long), + ("biHeight", ctypes.c_long), + ("biPlanes", wintypes.WORD), + ("biBitCount", wintypes.WORD), + ("biCompression", wintypes.DWORD), + ("biSizeImage", wintypes.DWORD), + ("biXPelsPerMeter", ctypes.c_long), + ("biYPelsPerMeter", ctypes.c_long), + ("biClrUsed", wintypes.DWORD), + ("biClrImportant", wintypes.DWORD), + ] + + +class BITMAPINFO(ctypes.Structure): + _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wintypes.DWORD * 1)] + + +user32.GetWindowDC.argtypes = [wintypes.HWND] +user32.GetWindowDC.restype = wintypes.HDC +user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC] +gdi32.CreateCompatibleDC.argtypes = [wintypes.HDC] +gdi32.CreateCompatibleDC.restype = wintypes.HDC +gdi32.CreateCompatibleBitmap.argtypes = [wintypes.HDC, ctypes.c_int, ctypes.c_int] +gdi32.CreateCompatibleBitmap.restype = wintypes.HBITMAP +gdi32.SelectObject.argtypes = [wintypes.HDC, wintypes.HGDIOBJ] +gdi32.SelectObject.restype = wintypes.HGDIOBJ +gdi32.BitBlt.argtypes = [ + wintypes.HDC, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + wintypes.HDC, + ctypes.c_int, + ctypes.c_int, + wintypes.DWORD, +] +gdi32.GetDIBits.argtypes = [ + wintypes.HDC, + wintypes.HBITMAP, + wintypes.UINT, + wintypes.UINT, + wintypes.LPVOID, + ctypes.POINTER(BITMAPINFO), + wintypes.UINT, +] +gdi32.DeleteObject.argtypes = [wintypes.HGDIOBJ] +gdi32.DeleteDC.argtypes = [wintypes.HDC] + + +def wait_for_window(process: subprocess.Popen[bytes], timeout: float = 15.0) -> tuple[int, RECT]: + deadline = time.time() + timeout + while time.time() < deadline: + if process.poll() is not None: + raise RuntimeError(f"Launcher exited early with code {process.returncode}.") + hwnd = find_window_for_pid(process.pid) + if hwnd: + rect = RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rect)) + return hwnd, rect + time.sleep(0.25) + raise RuntimeError("Timed out waiting for launcher window.") + + +def find_window_for_pid(pid: int) -> int: + hwnd_result = ctypes.c_void_p() + + @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) + def enum_proc(hwnd: int, lparam: int) -> bool: + if not user32.IsWindowVisible(hwnd): + return True + window_pid = wintypes.DWORD() + user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid)) + if window_pid.value == pid: + hwnd_result.value = hwnd + return False + return True + + user32.EnumWindows(enum_proc, 0) + return int(hwnd_result.value or 0) + + +def click(x: int, y: int) -> None: + user32.SetCursorPos(x, y) + user32.mouse_event(0x0002, 0, 0, 0, None) + time.sleep(0.1) + user32.mouse_event(0x0004, 0, 0, 0, None) + + +def capture_window_pixels(hwnd: int) -> tuple[int, int, bytes]: + rect = RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rect)) + width = rect.right - rect.left + height = rect.bottom - rect.top + if width <= 0 or height <= 0: + return width, height, b"" + + window_dc = user32.GetWindowDC(hwnd) + memory_dc = gdi32.CreateCompatibleDC(window_dc) + bitmap = gdi32.CreateCompatibleBitmap(window_dc, width, height) + old_bitmap = gdi32.SelectObject(memory_dc, bitmap) + try: + gdi32.BitBlt(memory_dc, 0, 0, width, height, window_dc, 0, 0, 0x00CC0020) + info = BITMAPINFO() + info.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) + info.bmiHeader.biWidth = width + info.bmiHeader.biHeight = -height + info.bmiHeader.biPlanes = 1 + info.bmiHeader.biBitCount = 32 + info.bmiHeader.biCompression = 0 + buffer = ctypes.create_string_buffer(width * height * 4) + rows = gdi32.GetDIBits(memory_dc, bitmap, 0, height, buffer, ctypes.byref(info), 0) + if rows != height: + raise RuntimeError("Failed to capture launcher window pixels.") + return width, height, bytes(buffer) + finally: + gdi32.SelectObject(memory_dc, old_bitmap) + gdi32.DeleteObject(bitmap) + gdi32.DeleteDC(memory_dc) + user32.ReleaseDC(hwnd, window_dc) + + +def assert_nonblank_window(hwnd: int, label: str) -> None: + width, height, pixels = capture_window_pixels(hwnd) + if width <= 100 or height <= 100 or not pixels: + raise RuntimeError(f"{label} screenshot dimensions are invalid: {width}x{height}.") + stride = max(4, len(pixels) // 4096 // 4 * 4) + samples = {pixels[i:i + 3] for i in range(0, len(pixels), stride)} + if len(samples) < 8: + raise RuntimeError(f"{label} screenshot appears blank.") + + +def log_contains(path: Path, needle: str) -> bool: + return path.exists() and needle in path.read_text(encoding="utf-8", errors="ignore") + + +def wait_for_log(path: Path, needle: str, timeout: float = 8.0) -> bool: + deadline = time.time() + timeout + while time.time() < deadline: + if log_contains(path, needle): + return True + time.sleep(0.2) + return False + + +def start_launcher(launcher_path: Path, env: dict[str, str]) -> subprocess.Popen[bytes]: + return subprocess.Popen([str(launcher_path)], cwd=launcher_path.parent, env=env.copy()) + + +def terminate(process: subprocess.Popen[bytes]) -> None: + if process.poll() is None: + subprocess.run(["taskkill", "/PID", str(process.pid), "/F", "/T"], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--launcher", required=True, type=Path) + args = parser.parse_args() + + temp_dir = Path(tempfile.mkdtemp(prefix="EvoEngineLauncherUiSmoke_")) + log_path = temp_dir / "launcher.log" + env = os.environ.copy() + env["LOCALAPPDATA"] = str(temp_dir / "LocalAppData") + env["EVOENGINE_LAUNCHER_TEST_LOG"] = str(log_path) + env["EVOENGINE_LAUNCHER_TEST_PARENT_FOLDER"] = str(temp_dir) + recent_project = temp_dir / "RecentProject.eveproj" + recent_project.write_text("application_name: Recent Smoke Project\n", encoding="utf-8") + settings_path = Path(env["LOCALAPPDATA"]) / "EvoEngine" / "EditorSettings.yaml" + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text(f"recent_projects:\n - {recent_project}\n", encoding="utf-8") + + launcher = start_launcher(args.launcher, env) + try: + hwnd, rect = wait_for_window(launcher) + user32.SetForegroundWindow(hwnd) + if not wait_for_log(log_path, "mode:hub"): + print("Launcher did not log project-hub mode.") + return 1 + if not wait_for_log(log_path, "recent-count:1"): + print("Launcher did not load seeded recent project.") + return 1 + assert_nonblank_window(hwnd, "Launcher project hub") + center_x = (rect.left + rect.right) // 2 + center_y = (rect.top + rect.bottom) // 2 + click(center_x, center_y) + time.sleep(1) + if launcher.poll() is not None: + print(f"Launcher exited after workspace click with code {launcher.returncode}.") + return 1 + + if not log_contains(log_path, "template:Generic:available"): + print("Launcher did not log Generic template availability.") + return 1 + if "template:" not in log_path.read_text(encoding="utf-8", errors="ignore"): + print("Launcher did not log template availability.") + return 1 + click((rect.left + rect.right) // 2 + 70, (rect.top + rect.bottom) // 2 + 135) + time.sleep(1) + if launcher.poll() is not None: + print(f"Launcher exited after project-hub interaction with code {launcher.returncode}.") + return 1 + return 0 + finally: + terminate(launcher) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/PythonBinding/src/PyEcoSysLabModule.cpp b/PythonBinding/src/PyEcoSysLabModule.cpp index 679cb691..e0812d86 100644 --- a/PythonBinding/src/PyEcoSysLabModule.cpp +++ b/PythonBinding/src/PyEcoSysLabModule.cpp @@ -1,3 +1,4 @@ +#include "ImGuiLayer.hpp" #include "PyEcoSysLab.hpp" #include "PyEvoEngine.hpp" @@ -18,8 +19,10 @@ void push_layers(const bool enable_window_layer, const bool enable_editor_layer) ApplicationContext::Get().PushLayer("Render Layer"); if (enable_window_layer) ApplicationContext::Get().PushLayer("Window Layer"); - if (enable_window_layer && enable_editor_layer) + if (enable_window_layer && enable_editor_layer) { + ApplicationContext::Get().PushLayer("ImGui Layer"); ApplicationContext::Get().PushLayer("Editor Layer"); + } ApplicationContext::Get().PushLayer("EcoSysLab Layer"); } diff --git a/PythonBinding/src/PyEvoEngine.cpp b/PythonBinding/src/PyEvoEngine.cpp index 5b36d1c4..2d2b61a4 100644 --- a/PythonBinding/src/PyEvoEngine.cpp +++ b/PythonBinding/src/PyEvoEngine.cpp @@ -1,5 +1,6 @@ #include "PyEvoEngine.hpp" #include "GeometryStorage.hpp" +#include "ImGuiLayer.hpp" #include "TextureStorage.hpp" #ifdef CUDA_MODULE_SERVICE # include "RayTracerLayer.hpp" @@ -310,6 +311,9 @@ void PyEvoEngine::PushWindowLayer() { } } void PyEvoEngine::PushEditorLayer() { + if (!ApplicationContext::Get().GetLayer()) { + ApplicationContext::Get().PushLayer("ImGui Layer"); + } if (!ApplicationContext::Get().GetLayer()) { ApplicationContext::Get().PushLayer("Editor Layer"); } diff --git a/README.md b/README.md index 5cbcea47..9ae4ab95 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,19 @@ Interactive project asset loading now dispatches scanned assets as an `AssetMana | Target | Purpose | | --- | --- | +| `EvoEngineLauncher` | Project launcher for opening or creating projects before starting the editor. | +| `EvoEngineEditor` | Generic project-required editor shell launched with a `.eveproj` path. | | `DemoApp` | General renderer/framework demo with multiple Service registrations. | | `EcoSysLabApp` | Interactive digital forestry and ecosystem workflow. | | `DigitalAgricultureApp` | Interactive sorghum and agriculture workflow. | | `LogGradingApp` | Log grading workflow; LogGrading and LogScanning features are supplied by runtime packages. | | `TreeDataGeneratorApp` | Batch-oriented tree dataset generation. | | `SorghumDataGeneratorApp` | Batch-oriented sorghum dataset generation. | -| `EmptyApp` | Minimal SDK app with render/window/editor layers for quick experiments. | + +`.eveproj` files can carry launch metadata used by `EvoEngineLauncher` and `EvoEngineEditor`: `application_name`, +`preferred_editor`, and `startup_runtime_packages`. The generic editor reads package names from this metadata before +project assets load, so package-backed projects can be opened from the launcher without hardcoding an app-specific entry +point. Launcher-created projects write this metadata from the selected template before the editor starts. ### Python Bindings @@ -281,7 +287,7 @@ When adding new work: Runtime packages live under `EvoEngine_Packages`. The package CMake entry scans package folders automatically, reads optional metadata from `PackageInfo.cmake`, creates an `EVOENGINE_ENABLE__PACKAGE` option, and builds a shared library target named `Package` by default. Disable individual package targets with `EVOENGINE_ENABLE__PACKAGE=OFF` when a build should skip them. -Apps do not load runtime packages by default. Set `ApplicationInitializationSettings::enable_runtime_packages = true` and add names to `ApplicationInitializationSettings::startup_runtime_packages`, or call `PackageManager::Load/LoadAll` from runtime/editor tooling when a workflow needs additional package functionality. +Apps do not load runtime packages by default. Set `ApplicationInitializationSettings::enable_runtime_packages = true` and add names to `ApplicationInitializationSettings::startup_runtime_packages`, declare `startup_runtime_packages` in a project `.eveproj`, or call `PackageManager::Load/LoadAll` from runtime/editor tooling when a workflow needs additional package functionality. Headless tools and focused tests that only need project scanning or asset metadata can set `ApplicationInitializationSettings::load_default_resources = false` and `ApplicationInitializationSettings::load_project_start_scene = false` to avoid creating render defaults or attaching a scene during initialization. diff --git a/Resources/LSystemProject/test.eveproj b/Resources/LSystemProject/test.eveproj index 301b72b9..b11f21d0 100644 --- a/Resources/LSystemProject/test.eveproj +++ b/Resources/LSystemProject/test.eveproj @@ -1 +1,6 @@ +application_name: LSystem +preferred_editor: EvoEngineEditor +startup_runtime_packages: + - LSystem + - DigitalAgriculture start_scene_handle: 10542394882133086192 \ No newline at end of file diff --git a/Scripts/install_apps.py b/Scripts/install_apps.py index 227f4d15..591b7552 100644 --- a/Scripts/install_apps.py +++ b/Scripts/install_apps.py @@ -58,6 +58,14 @@ def build_preset_exists(presets: dict[str, Any], name: str) -> bool: return any(preset.get("name") == name for preset in presets.get("buildPresets", [])) +def build_dir_for_preset(root: Path, presets: dict[str, Any], preset_name: str) -> Path: + preset = configure_preset(presets, preset_name) + binary_dir = preset.get("binaryDir") + if not binary_dir: + raise SystemExit(f"Configure preset '{preset_name}' does not define binaryDir.") + return expand_preset_path(binary_dir, root) + + def install_dir_for_preset(root: Path, presets: dict[str, Any], preset_name: str) -> Path: preset = configure_preset(presets, preset_name) install_dir = preset.get("installDir") @@ -116,6 +124,14 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Do not clean the install directory before building the install target.", ) + parser.add_argument( + "--incremental", + action="store_true", + help=( + "Reuse the existing build and install directories. Skips configure when " + "the preset build cache already exists." + ), + ) parser.add_argument( "--cmake-arg", action="append", @@ -138,19 +154,23 @@ def main() -> int: args = parse_args() root = repo_root() presets = load_cmake_presets(root) + build_dir = build_dir_for_preset(root, presets, args.preset) install_dir = install_dir_for_preset(root, presets, args.preset) build_preset = f"install-{args.preset}-{args.config}" if not build_preset_exists(presets, build_preset): raise SystemExit(f"Build preset not found: {build_preset}") - configure_command = ["cmake", "--preset", args.preset, "-DBUILD_TESTING=OFF"] - if args.verbose: - configure_command.append("--log-level=VERBOSE") - configure_command.extend(args.cmake_arg) - run_step("Configure Visual Studio project", configure_command) + if args.incremental and not args.cmake_arg and (build_dir / "CMakeCache.txt").exists(): + print(f"Skipping configure; reusing {build_dir / 'CMakeCache.txt'}", flush=True) + else: + configure_command = ["cmake", "--preset", args.preset, "-DBUILD_TESTING=OFF"] + if args.verbose: + configure_command.append("--log-level=VERBOSE") + configure_command.extend(args.cmake_arg) + run_step("Configure Visual Studio project", configure_command) - if not args.no_clean_install: + if not args.incremental and not args.no_clean_install: clean_install_dir(root, install_dir) build_command = ["cmake", "--build", "--preset", build_preset]