diff --git a/editor/src/ComponentEditors/AssetReferenceEditorUtils.h b/editor/src/ComponentEditors/AssetReferenceEditorUtils.h new file mode 100644 index 00000000..da824ffd --- /dev/null +++ b/editor/src/ComponentEditors/AssetReferenceEditorUtils.h @@ -0,0 +1,111 @@ +#pragma once + +#include "Application.h" +#include "Actions/ProjectActionRouter.h" +#include "UI/UI.h" +#include "Utils/ProjectFileUtils.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace ComponentEditorAssetUI { + +struct AssetReferenceInteraction { + std::string assignedPath; + bool clearRequested = false; +}; + +inline std::string ToProjectRelativeAssetPath(const std::string& assetPath) { + if (assetPath.empty()) { + return {}; + } + + const std::string& projectPath = Application::Get().GetEditorContext().GetProjectPath(); + if (projectPath.empty()) { + return assetPath; + } + + return ProjectFileUtils::MakeProjectRelativePath(projectPath, assetPath); +} + +inline bool HasSupportedExtension( + const std::string& path, + std::initializer_list supportedExtensions) { + std::string extension = std::filesystem::path(path).extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + + for (const char* supportedExtension : supportedExtensions) { + if (supportedExtension != nullptr && extension == supportedExtension) { + return true; + } + } + + return false; +} + +inline AssetReferenceInteraction DrawAssetReferenceProperty( + const char* label, + const std::string& currentPath, + const char* emptyHint, + std::initializer_list supportedExtensions) { + AssetReferenceInteraction interaction; + + UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { + constexpr float kClearButtonWidth = 52.0f; + + std::array buffer{}; + if (!currentPath.empty()) { + strncpy_s(buffer.data(), buffer.size(), currentPath.c_str(), _TRUNCATE); + } + + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float fieldWidth = (std::max)(layout.controlWidth - kClearButtonWidth - spacing, 1.0f); + ImGui::SetNextItemWidth(fieldWidth); + ImGui::InputTextWithHint( + "##AssetPath", + emptyHint, + buffer.data(), + buffer.size(), + ImGuiInputTextFlags_ReadOnly); + + if (ImGui::IsItemHovered() && !currentPath.empty()) { + ImGui::SetTooltip("%s", currentPath.c_str()); + } + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload( + Actions::ProjectAssetPayloadType(), + ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) { + if (payload->Data != nullptr) { + const std::string droppedPath(static_cast(payload->Data)); + if (HasSupportedExtension(droppedPath, supportedExtensions)) { + interaction.assignedPath = ToProjectRelativeAssetPath(droppedPath); + } + } + } + ImGui::EndDragDropTarget(); + } + + ImGui::SameLine(0.0f, spacing); + ImGui::BeginDisabled(currentPath.empty()); + interaction.clearRequested = UI::InspectorActionButton("Clear", ImVec2(kClearButtonWidth, 0.0f)); + ImGui::EndDisabled(); + + return false; + }); + + return interaction; +} + +} // namespace ComponentEditorAssetUI +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/ComponentEditorRegistry.cpp b/editor/src/ComponentEditors/ComponentEditorRegistry.cpp index f0321274..ae0a4265 100644 --- a/editor/src/ComponentEditors/ComponentEditorRegistry.cpp +++ b/editor/src/ComponentEditors/ComponentEditorRegistry.cpp @@ -2,6 +2,8 @@ #include "ComponentEditors/CameraComponentEditor.h" #include "ComponentEditors/LightComponentEditor.h" +#include "ComponentEditors/MeshFilterComponentEditor.h" +#include "ComponentEditors/MeshRendererComponentEditor.h" #include "ComponentEditors/TransformComponentEditor.h" namespace XCEngine { @@ -16,6 +18,8 @@ ComponentEditorRegistry::ComponentEditorRegistry() { RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); + RegisterEditor(std::make_unique()); } void ComponentEditorRegistry::RegisterEditor(std::unique_ptr editor) { diff --git a/editor/src/ComponentEditors/MeshFilterComponentEditor.h b/editor/src/ComponentEditors/MeshFilterComponentEditor.h new file mode 100644 index 00000000..b112dafd --- /dev/null +++ b/editor/src/ComponentEditors/MeshFilterComponentEditor.h @@ -0,0 +1,77 @@ +#pragma once + +#include "AssetReferenceEditorUtils.h" +#include "IComponentEditor.h" +#include "Core/IUndoManager.h" +#include "UI/UI.h" + +#include + +namespace XCEngine { +namespace Editor { + +class MeshFilterComponentEditor : public IComponentEditor { +public: + const char* GetComponentTypeName() const override { + return "MeshFilter"; + } + + const char* GetDisplayName() const override { + return "Mesh Filter"; + } + + bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override { + auto* meshFilter = dynamic_cast<::XCEngine::Components::MeshFilterComponent*>(component); + if (!meshFilter) { + return false; + } + + constexpr const char* kUndoLabel = "Modify Mesh Filter"; + + const ComponentEditorAssetUI::AssetReferenceInteraction interaction = + ComponentEditorAssetUI::DrawAssetReferenceProperty( + "Mesh", + meshFilter->GetMeshPath(), + "Drop Model Asset", + { ".fbx", ".obj", ".gltf", ".glb" }); + + bool changed = false; + changed |= UI::ApplyPropertyChange( + interaction.clearRequested && !meshFilter->GetMeshPath().empty(), + undoManager, + kUndoLabel, + [&]() { + meshFilter->ClearMesh(); + }); + changed |= UI::ApplyPropertyChange( + !interaction.assignedPath.empty() && interaction.assignedPath != meshFilter->GetMeshPath(), + undoManager, + kUndoLabel, + [&]() { + meshFilter->SetMeshPath(interaction.assignedPath); + }); + + return changed; + } + + bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject && !gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>(); + } + + const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override { + if (!gameObject) { + return "Invalid"; + } + + return gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>() + ? "Already Added" + : nullptr; + } + + bool CanRemove(::XCEngine::Components::Component* component) const override { + return CanEdit(component); + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/MeshRendererComponentEditor.h b/editor/src/ComponentEditors/MeshRendererComponentEditor.h new file mode 100644 index 00000000..92adc657 --- /dev/null +++ b/editor/src/ComponentEditors/MeshRendererComponentEditor.h @@ -0,0 +1,116 @@ +#pragma once + +#include "AssetReferenceEditorUtils.h" +#include "IComponentEditor.h" +#include "Core/IUndoManager.h" +#include "UI/UI.h" + +#include +#include + +#include +#include + +namespace XCEngine { +namespace Editor { + +class MeshRendererComponentEditor : public IComponentEditor { +public: + const char* GetComponentTypeName() const override { + return "MeshRenderer"; + } + + const char* GetDisplayName() const override { + return "Mesh Renderer"; + } + + bool Render(::XCEngine::Components::Component* component, IUndoManager* undoManager) override { + auto* meshRenderer = dynamic_cast<::XCEngine::Components::MeshRendererComponent*>(component); + if (!meshRenderer) { + return false; + } + + constexpr const char* kUndoLabel = "Modify Mesh Renderer"; + + bool changed = false; + const size_t slotCount = GetVisibleMaterialSlotCount(meshRenderer); + for (size_t slotIndex = 0; slotIndex < slotCount; ++slotIndex) { + const std::string label = "Material " + std::to_string(slotIndex); + const std::string& currentPath = meshRenderer->GetMaterialPath(slotIndex); + const ComponentEditorAssetUI::AssetReferenceInteraction interaction = + ComponentEditorAssetUI::DrawAssetReferenceProperty( + label.c_str(), + currentPath, + "Drop Material Asset", + { ".mat" }); + + changed |= UI::ApplyPropertyChange( + interaction.clearRequested && !currentPath.empty(), + undoManager, + kUndoLabel, + [meshRenderer, slotIndex]() { + meshRenderer->SetMaterialPath(slotIndex, std::string()); + }); + changed |= UI::ApplyPropertyChange( + !interaction.assignedPath.empty() && interaction.assignedPath != currentPath, + undoManager, + kUndoLabel, + [meshRenderer, slotIndex, assignedPath = interaction.assignedPath]() { + meshRenderer->SetMaterialPath(slotIndex, assignedPath); + }); + } + + return changed; + } + + bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject && !gameObject->GetComponent<::XCEngine::Components::MeshRendererComponent>(); + } + + const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override { + if (!gameObject) { + return "Invalid"; + } + + return gameObject->GetComponent<::XCEngine::Components::MeshRendererComponent>() + ? "Already Added" + : nullptr; + } + + bool CanRemove(::XCEngine::Components::Component* component) const override { + return CanEdit(component); + } + +private: + static size_t GetVisibleMaterialSlotCount(::XCEngine::Components::MeshRendererComponent* meshRenderer) { + size_t slotCount = (std::max)(static_cast(1), meshRenderer->GetMaterialCount()); + + ::XCEngine::Components::GameObject* gameObject = meshRenderer->GetGameObject(); + if (!gameObject) { + return slotCount; + } + + auto* meshFilter = gameObject->GetComponent<::XCEngine::Components::MeshFilterComponent>(); + if (!meshFilter) { + return slotCount; + } + + ::XCEngine::Resources::Mesh* mesh = meshFilter->GetMesh(); + if (!mesh || !mesh->IsValid()) { + return slotCount; + } + + slotCount = (std::max)(slotCount, static_cast(mesh->GetMaterials().Size())); + const auto& sections = mesh->GetSections(); + for (size_t sectionIndex = 0; sectionIndex < sections.Size(); ++sectionIndex) { + slotCount = (std::max)( + slotCount, + static_cast(sections[sectionIndex].materialID) + 1u); + } + + return slotCount; + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/engine/src/Components/GameObject.cpp b/engine/src/Components/GameObject.cpp index da9180fb..19b0494d 100644 --- a/engine/src/Components/GameObject.cpp +++ b/engine/src/Components/GameObject.cpp @@ -30,7 +30,6 @@ GameObject::GameObject(const std::string& name) } GameObject::~GameObject() { - GetGlobalRegistry().erase(m_id); if (m_transform) { delete m_transform; m_transform = nullptr; @@ -39,8 +38,10 @@ GameObject::~GameObject() { } std::unordered_map& GameObject::GetGlobalRegistry() { - static std::unordered_map registry; - return registry; + // Keep the registry alive until process teardown to avoid static destruction + // order issues when GameObject instances are released from other singletons. + static auto* registry = new std::unordered_map(); + return *registry; } void GameObject::NotifyComponentsBecameActive() { diff --git a/engine/src/Scene/Scene.cpp b/engine/src/Scene/Scene.cpp index c8bf774a..aa23bb81 100644 --- a/engine/src/Scene/Scene.cpp +++ b/engine/src/Scene/Scene.cpp @@ -108,6 +108,10 @@ Scene::Scene(const std::string& name) } Scene::~Scene() { + auto& registry = GameObject::GetGlobalRegistry(); + for (const auto& entry : m_gameObjects) { + registry.erase(entry.first); + } m_gameObjects.clear(); } @@ -156,6 +160,7 @@ void Scene::DestroyGameObject(GameObject* gameObject) { gameObject->OnDestroy(); m_gameObjectIDs.erase(gameObject->m_id); + GameObject::GetGlobalRegistry().erase(gameObject->m_id); m_gameObjects.erase(gameObject->m_id); } @@ -244,6 +249,10 @@ void Scene::LateUpdate(float deltaTime) { } void Scene::DeserializeFromString(const std::string& data) { + auto& registry = GameObject::GetGlobalRegistry(); + for (const auto& entry : m_gameObjects) { + registry.erase(entry.first); + } m_gameObjects.clear(); m_rootGameObjects.clear(); m_gameObjectIDs.clear(); diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index 07ae459f..e9deb80e 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -554,7 +554,7 @@ TEST_F(SceneTest, SaveAndLoad_PreservesMeshComponentPaths) { std::filesystem::remove(scenePath); } -TEST(Scene_ProjectSample, MainSceneLoadsBackpackMeshAsset) { +TEST(Scene_ProjectSample, BackpackSceneLoadsBackpackMeshAsset) { namespace fs = std::filesystem; XCEngine::Resources::ResourceManager& resourceManager = XCEngine::Resources::ResourceManager::Get(); @@ -580,11 +580,11 @@ TEST(Scene_ProjectSample, MainSceneLoadsBackpackMeshAsset) { const fs::path repositoryRoot = GetRepositoryRoot(); const fs::path projectRoot = repositoryRoot / "project"; - const fs::path mainScenePath = projectRoot / "Assets" / "Scenes" / "Main.xc"; + const fs::path backpackScenePath = projectRoot / "Assets" / "Scenes" / "Backpack.xc"; const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll"; const fs::path backpackMeshPath = projectRoot / "Assets" / "Models" / "backpack" / "backpack.obj"; - ASSERT_TRUE(fs::exists(mainScenePath)); + ASSERT_TRUE(fs::exists(backpackScenePath)); ASSERT_TRUE(fs::exists(assimpDllPath)); ASSERT_TRUE(fs::exists(backpackMeshPath)); @@ -617,7 +617,7 @@ TEST(Scene_ProjectSample, MainSceneLoadsBackpackMeshAsset) { ASSERT_GT(directMeshHandle->GetVertexCount(), 0u); Scene loadedScene; - loadedScene.Load(mainScenePath.string()); + loadedScene.Load(backpackScenePath.string()); GameObject* backpackObject = loadedScene.Find("BackpackMesh"); ASSERT_NE(backpackObject, nullptr); @@ -634,4 +634,20 @@ TEST(Scene_ProjectSample, MainSceneLoadsBackpackMeshAsset) { EXPECT_EQ(meshFilter->GetMeshPath(), "Assets/Models/backpack/backpack.obj"); } +TEST(Scene_ProjectSample, MainSceneStaysLightweightForEditorStartup) { + namespace fs = std::filesystem; + + const fs::path repositoryRoot = GetRepositoryRoot(); + const fs::path mainScenePath = repositoryRoot / "project" / "Assets" / "Scenes" / "Main.xc"; + + ASSERT_TRUE(fs::exists(mainScenePath)); + + Scene loadedScene; + loadedScene.Load(mainScenePath.string()); + + EXPECT_NE(loadedScene.Find("Main Camera"), nullptr); + EXPECT_NE(loadedScene.Find("Directional Light"), nullptr); + EXPECT_EQ(loadedScene.Find("BackpackMesh"), nullptr); +} + } // namespace