From c3c1d5ae286c864200994d1b18fee44d871d463f Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Wed, 12 Aug 2026 01:18:47 +0200 Subject: [PATCH] feat: add "Add component" button --- .../destrum/Serialization/ComponentFactory.h | 17 +++++ .../Serialization/EngineComponentList.h | 27 +++++++ .../src/Serialization/ComponentRegistry.cpp | 61 ++-------------- destrum/src/Util/ImGuiUtils.cpp | 71 ++++++++++++++++++- tests/destrum_tests.cpp | 29 ++++++++ 5 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 destrum/include/destrum/Serialization/EngineComponentList.h diff --git a/destrum/include/destrum/Serialization/ComponentFactory.h b/destrum/include/destrum/Serialization/ComponentFactory.h index 0d86f86..9a31fd1 100644 --- a/destrum/include/destrum/Serialization/ComponentFactory.h +++ b/destrum/include/destrum/Serialization/ComponentFactory.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,13 @@ public: Registry()[typeName] = std::move(createFn); } + template + static void Register(const char* typeName) { + Register(std::string{typeName}, [](GameObject& owner) -> Component* { + return owner.AddComponent(); + }); + } + static Component* Create(const std::string& typeName, GameObject& owner) { const auto it = Registry().find(typeName); @@ -31,6 +39,15 @@ public: return Registry().contains(typeName); } + [[nodiscard]] static std::vector GetRegisteredTypeNames() { + std::vector typeNames; + typeNames.reserve(Registry().size()); + for (const auto& [typeName, _] : Registry()) { + typeNames.push_back(typeName); + } + return typeNames; + } + private: static std::unordered_map& Registry() { static std::unordered_map registry; diff --git a/destrum/include/destrum/Serialization/EngineComponentList.h b/destrum/include/destrum/Serialization/EngineComponentList.h new file mode 100644 index 0000000..88eca6e --- /dev/null +++ b/destrum/include/destrum/Serialization/EngineComponentList.h @@ -0,0 +1,27 @@ +#ifndef DESTRUM_ENGINECOMPONENTLIST_H +#define DESTRUM_ENGINECOMPONENTLIST_H + +// Keep the component manifest in one place. Consumers define X before +// expanding one of these lists to generate registration or other metadata. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DESTRUM_ENGINE_COMPONENTS(X) \ + X(MeshRendererComponent) \ + X(Rotator) \ + X(Spinner) \ + X(OrbitAndSpin) \ + X(Animator) \ + X(Rigidbody) \ + X(BoxCollider) \ + X(SphereCollider) \ + X(CapsuleCollider) + +#endif // DESTRUM_ENGINECOMPONENTLIST_H diff --git a/destrum/src/Serialization/ComponentRegistry.cpp b/destrum/src/Serialization/ComponentRegistry.cpp index 7eb5feb..428d4e3 100644 --- a/destrum/src/Serialization/ComponentRegistry.cpp +++ b/destrum/src/Serialization/ComponentRegistry.cpp @@ -1,18 +1,6 @@ -// destrum/Serialization/ComponentRegistry.cpp - #include #include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include +#include void RegisterEngineComponents() { @@ -23,47 +11,8 @@ void RegisterEngineComponents() registered = true; - ComponentFactory::Register("MeshRendererComponent", [](GameObject& owner) { - return owner.AddComponent(); - }); - // Keep the old serialized spelling readable while writing the canonical - // component name returned by MeshRendererComponent. - ComponentFactory::Register("MeshRenderer", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("Rotator", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("Spinner", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("OrbitAndSpin", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("Animator", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("Rigidbody", [](GameObject& owner) { - return owner.AddComponent(); - }); - ComponentFactory::Register("RigidBody", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("BoxCollider", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("SphereCollider", [](GameObject& owner) { - return owner.AddComponent(); - }); - - ComponentFactory::Register("CapsuleCollider", [](GameObject& owner) { - return owner.AddComponent(); - }); +#define DESTRUM_REGISTER_COMPONENT(ComponentType) \ + ComponentFactory::Register(#ComponentType); + DESTRUM_ENGINE_COMPONENTS(DESTRUM_REGISTER_COMPONENT) +#undef DESTRUM_REGISTER_COMPONENT } diff --git a/destrum/src/Util/ImGuiUtils.cpp b/destrum/src/Util/ImGuiUtils.cpp index d405004..854ce18 100644 --- a/destrum/src/Util/ImGuiUtils.cpp +++ b/destrum/src/Util/ImGuiUtils.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,8 @@ #include #include #include +#include +#include namespace { [[nodiscard]] bool IsInspectableObject(const GameObject* object) { @@ -121,6 +124,8 @@ namespace ImGuiUtils { const std::vector>& objectsSource, const std::vector>& pendingAdditions, ObjectId& selectedObjectId) { + RegisterEngineComponents(); + std::vector objects; objects.reserve(objectsSource.size() + pendingAdditions.size()); @@ -268,9 +273,69 @@ namespace ImGuiUtils { worldPosition.z); } - if (ImGui::CollapsingHeader( - "Components", - ImGuiTreeNodeFlags_DefaultOpen)) { + const bool componentsOpen = ImGui::CollapsingHeader( + "Components", + ImGuiTreeNodeFlags_DefaultOpen); + + if (componentsOpen) { + if (ImGui::Button("+ Add Component")) { + ImGui::OpenPopup("AddComponentPopup"); + } + + std::string addComponentError; + if (ImGui::BeginPopup("AddComponentPopup")) { + std::vector componentTypes = + ComponentFactory::GetRegisteredTypeNames(); + std::sort(componentTypes.begin(), componentTypes.end()); + + bool hasAvailableComponent = false; + for (const std::string& componentType : componentTypes) { + const bool alreadyAdded = std::any_of( + selectedObject->GetComponents().begin(), + selectedObject->GetComponents().end(), + [&componentType](const auto& component) { + return component != nullptr && + !component->IsBeingDestroyed() && + component->GetTypeName() == componentType; + }); + + if (alreadyAdded) { + const std::string label = + componentType + " (already added)"; + ImGui::BeginDisabled(); + ImGui::MenuItem(label.c_str()); + ImGui::EndDisabled(); + continue; + } + + hasAvailableComponent = true; + if (ImGui::MenuItem(componentType.c_str())) { + try { + if (ComponentFactory::Create( + componentType, + *selectedObject) == nullptr) { + addComponentError = + "Component type is not registered: " + + componentType; + } else { + ImGui::CloseCurrentPopup(); + } + } catch (const std::exception& exception) { + addComponentError = exception.what(); + } + } + } + + if (!hasAvailableComponent) { + ImGui::TextDisabled("All components are already added."); + } + if (!addComponentError.empty()) { + ImGui::TextWrapped("Failed to add component: %s", + addComponentError.c_str()); + } + ImGui::EndPopup(); + } + bool hasComponents = false; for (const auto& component : selectedObject->GetComponents()) { if (component == nullptr || component->IsBeingDestroyed()) { diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index 6253001..25eb481 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -161,6 +163,32 @@ namespace { SceneManager::GetInstance().Destroy(); } + void testEngineComponentRegistration() + { + RegisterEngineComponents(); + + const char* canonicalNames[] = { + "MeshRendererComponent", + "Rotator", + "Spinner", + "OrbitAndSpin", + "Animator", + "Rigidbody", + "BoxCollider", + "SphereCollider", + "CapsuleCollider" + }; + for (const char* name : canonicalNames) { + check(ComponentFactory::IsRegistered(name), + "all canonical engine component names must be registered"); + } + + check(!ComponentFactory::IsRegistered("MeshRenderer"), + "MeshRenderer legacy alias must not be registered"); + check(!ComponentFactory::IsRegistered("RigidBody"), + "RigidBody legacy alias must not be registered"); + } + void testAssetPathValidation() { const auto root = std::filesystem::temp_directory_path() / "destrum_asset_test"; @@ -440,6 +468,7 @@ int main() testEventMutation(); testEventRemoval(); testComponentRemoval(); + testEngineComponentRegistration(); testAssetPathValidation(); testLegacySceneLoad(); testSceneLoadRollback();