diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 906ec16..ffaf8c5 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -12,6 +12,8 @@ set(SRC_FILES "src/Components/Physics/Rigidbody.cpp" "src/Components/Physics/BoxCollider.cpp" + "src/Components/Physics/CapsuleCollider.cpp" + "src/Components/Physics/Collider.cpp" "src/Components/Physics/SphereCollider.cpp" "src/Graphics/BindlessSetManager.cpp" @@ -64,6 +66,7 @@ set(SRC_FILES "src/Serialization/ComponentRegistry.cpp" "src/Util/DeltaTime.cpp" + "src/Util/ImGuiUtils.cpp" "src/Physics/PhysicsWorld.cpp" "src/Physics/SimplePhysicsWorld.cpp" diff --git a/destrum/include/destrum/Components/MeshRendererComponent.h b/destrum/include/destrum/Components/MeshRendererComponent.h index 005cefe..979c937 100644 --- a/destrum/include/destrum/Components/MeshRendererComponent.h +++ b/destrum/include/destrum/Components/MeshRendererComponent.h @@ -17,6 +17,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; void ResolveReferences(const ObjectMap& objects) override; + void ImGuiInspector() override; void SetMeshID(MeshID id); MeshID GetMeshID() const { return meshID; } diff --git a/destrum/include/destrum/Components/OrbitAndSpin.h b/destrum/include/destrum/Components/OrbitAndSpin.h index 53c34c5..f356cd7 100644 --- a/destrum/include/destrum/Components/OrbitAndSpin.h +++ b/destrum/include/destrum/Components/OrbitAndSpin.h @@ -28,6 +28,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; // optional setters void SetRadius(float r) { m_Radius = r; } diff --git a/destrum/include/destrum/Components/Physics/BoxCollider.h b/destrum/include/destrum/Components/Physics/BoxCollider.h index 79f9e8f..497b28f 100644 --- a/destrum/include/destrum/Components/Physics/BoxCollider.h +++ b/destrum/include/destrum/Components/Physics/BoxCollider.h @@ -13,6 +13,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; std::string GetTypeName() const override { diff --git a/destrum/include/destrum/Components/Physics/CapsuleCollider.h b/destrum/include/destrum/Components/Physics/CapsuleCollider.h index 15b2879..52ab2c6 100644 --- a/destrum/include/destrum/Components/Physics/CapsuleCollider.h +++ b/destrum/include/destrum/Components/Physics/CapsuleCollider.h @@ -17,6 +17,8 @@ public: return "CapsuleCollider"; } + void ImGuiInspector() override; + nlohmann::json Serialize() const override { auto data = SerializeCollider(); data["radius"] = m_Radius; diff --git a/destrum/include/destrum/Components/Physics/Collider.h b/destrum/include/destrum/Components/Physics/Collider.h index 0b8a55f..c4b2350 100644 --- a/destrum/include/destrum/Components/Physics/Collider.h +++ b/destrum/include/destrum/Components/Physics/Collider.h @@ -12,6 +12,8 @@ public: ~Collider() override = default; + void ImGuiInspector() override; + [[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0; [[nodiscard]] bool IsTrigger() const { return m_IsTrigger; } diff --git a/destrum/include/destrum/Components/Physics/Rigidbody.h b/destrum/include/destrum/Components/Physics/Rigidbody.h index 45752ff..155b52b 100644 --- a/destrum/include/destrum/Components/Physics/Rigidbody.h +++ b/destrum/include/destrum/Components/Physics/Rigidbody.h @@ -21,6 +21,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; std::string GetTypeName() const override { diff --git a/destrum/include/destrum/Components/Physics/SphereCollider.h b/destrum/include/destrum/Components/Physics/SphereCollider.h index a550f43..ff1d114 100644 --- a/destrum/include/destrum/Components/Physics/SphereCollider.h +++ b/destrum/include/destrum/Components/Physics/SphereCollider.h @@ -14,6 +14,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; std::string GetTypeName() const override { return "SphereCollider"; diff --git a/destrum/include/destrum/Components/Rotator.h b/destrum/include/destrum/Components/Rotator.h index 28a076f..2662e19 100644 --- a/destrum/include/destrum/Components/Rotator.h +++ b/destrum/include/destrum/Components/Rotator.h @@ -22,6 +22,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; void SetDistance(float distance); void SetSpeed(float speed) { m_Speed = speed; } diff --git a/destrum/include/destrum/Components/Spinner.h b/destrum/include/destrum/Components/Spinner.h index a0e794c..02aac1d 100644 --- a/destrum/include/destrum/Components/Spinner.h +++ b/destrum/include/destrum/Components/Spinner.h @@ -20,6 +20,7 @@ public: nlohmann::json Serialize() const override; void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; void SetAxis(const glm::vec3& axis) { m_Axis = glm::dot(axis, axis) > 0.000001f diff --git a/destrum/include/destrum/ObjectModel/Component.h b/destrum/include/destrum/ObjectModel/Component.h index 17a46a9..2db3990 100644 --- a/destrum/include/destrum/ObjectModel/Component.h +++ b/destrum/include/destrum/ObjectModel/Component.h @@ -39,6 +39,8 @@ public: virtual void LateUpdate(float dt); virtual void FixedUpdate(float fixedDt); + // Called by the editor inspector while this component is selected. A + // component owns the controls for its editable runtime properties. virtual void ImGuiInspector(); virtual void ImGuiRender(); diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 5f688dd..4efb887 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -4,6 +4,7 @@ #include #include +#include #include #include "destrum/Physics/JoltPhysicsWorld.h" @@ -120,6 +121,7 @@ private: //Imgui vars bool m_ShowDemoWindow{false}; + ObjectId m_SelectedObjectId{InvalidObjectId}; std::function m_registerBindings; std::function m_unregisterBindings; diff --git a/destrum/include/destrum/Util/ImGuiUtils.h b/destrum/include/destrum/Util/ImGuiUtils.h new file mode 100644 index 0000000..96e4d83 --- /dev/null +++ b/destrum/include/destrum/Util/ImGuiUtils.h @@ -0,0 +1,27 @@ +#ifndef DESTRUM_IMGUIUTILS_H +#define DESTRUM_IMGUIUTILS_H + +#include +#include +#include + +#include + +#include + +class GameObject; + +namespace ImGuiUtils { + [[nodiscard]] bool DrawVec3Control( + const char* label, + glm::vec3& value, + float speed = 0.1f); + + void RenderSceneInspector( + std::string_view sceneName, + const std::vector>& objects, + const std::vector>& pendingAdditions, + ObjectId& selectedObjectId); +} + +#endif // DESTRUM_IMGUIUTILS_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 6288578..d6d976d 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -194,6 +194,8 @@ void App::run() { ZoneScopedN("Debug ImGui"); + SceneManager::GetInstance().RenderImgui(); + ImGui::Begin("Debug"); ImGui::Text("FPS: %.2f", avgFPS); ImGui::End(); diff --git a/destrum/src/Components/Animator.cpp b/destrum/src/Components/Animator.cpp index c8d81b3..d1e13ed 100644 --- a/destrum/src/Components/Animator.cpp +++ b/destrum/src/Components/Animator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -327,24 +328,58 @@ void Animator::Update(float dt) { } void Animator::ImGuiInspector() { - // ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str()); - // ImGui::Text("Time: %.2f", m_current.time); - // ImGui::Text("Blend: %.2f", m_blendT); - // - // if (!isPlaying()) { - // ImGui::BeginDisabled(); - // ImGui::Button("Stop"); - // ImGui::EndDisabled(); - // } else if (ImGui::Button("Stop")) { - // stop(); - // } - // - // ImGui::Separator(); - // ImGui::Text("Clips:"); - // for (auto& [name, _] : m_clips) { - // if (ImGui::Selectable(name.c_str(), name == m_currentClipName)) - // play(name); - // } + if (m_skeletonAsset.empty()) { + ImGui::TextDisabled("Skeleton: "); + } else { + ImGui::TextWrapped("Skeleton: %s", m_skeletonAsset.path.c_str()); + if (!m_skeletonAsset.subresource.empty()) { + ImGui::TextWrapped( + "Skeleton subresource: %s", + m_skeletonAsset.subresource.c_str()); + } + } + ImGui::Text("Joints: %zu", m_skeleton.joints.size()); + + if (!m_referencesResolved) { + ImGui::TextDisabled("Asset references are waiting to be resolved."); + } + + if (m_current.clip == nullptr) { + ImGui::TextDisabled("Playback: stopped"); + } else { + ImGui::Text("Playing: %s", m_currentClipName.c_str()); + float speed = m_current.speed; + if (ImGui::DragFloat("Playback speed", &speed, 0.05f)) { + m_current.speed = speed; + } + ImGui::Text("Time: %.2f / %.2f s", m_current.time, m_current.clip->duration); + ImGui::Text("Blend: %.2f", m_blendT); + } + + const bool wasPlaying = isPlaying(); + if (!wasPlaying) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Stop")) { + stop(); + } + if (!wasPlaying) { + ImGui::EndDisabled(); + } + + ImGui::Separator(); + ImGui::Text("Clips: %zu", m_clips.size()); + for (const std::string& name : m_clipOrder) { + if (!m_clips.contains(name)) { + continue; + } + + ImGui::PushID(name.c_str()); + if (ImGui::Selectable(name.c_str(), name == m_currentClipName)) { + play(name, 0.15f); + } + ImGui::PopID(); + } } void Animator::addClip(std::shared_ptr clip) { diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index 44dd7de..9e53686 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -8,6 +8,8 @@ #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/GameState.h" +#include + #include MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") { @@ -139,6 +141,30 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) { } } +void MeshRendererComponent::ImGuiInspector() { + if (meshID == NULL_MESH_ID) { + ImGui::TextDisabled("Mesh: "); + } else { + ImGui::Text("Mesh ID: %zu", meshID); + ImGui::TextWrapped( + "Mesh key: %s", + meshKey.empty() ? "" : meshKey.c_str()); + } + + if (materialID == NULL_MATERIAL_ID) { + ImGui::TextDisabled("Material: "); + } else { + ImGui::Text("Material ID: %u", materialID); + ImGui::TextWrapped( + "Material key: %s", + materialKey.empty() ? "" : materialKey.c_str()); + } + + ImGui::Text( + "Skinning: %s", + m_skinnedMesh != nullptr ? "active" : "static"); +} + void MeshRendererComponent::SetMeshID(MeshID id) { meshID = id; meshKey.clear(); diff --git a/destrum/src/Components/OrbitAndSpin.cpp b/destrum/src/Components/OrbitAndSpin.cpp index 3381865..8f22aac 100644 --- a/destrum/src/Components/OrbitAndSpin.cpp +++ b/destrum/src/Components/OrbitAndSpin.cpp @@ -9,6 +9,11 @@ #include "destrum/Components/MeshRendererComponent.h" #include "destrum/Util/GameState.h" #include "destrum/Util/DeltaTime.h" +#include "destrum/Util/ImGuiUtils.h" + +#include + +#include static glm::vec3 RandomUnitVector(std::mt19937& rng) { @@ -175,3 +180,64 @@ void OrbitAndSpin::Deserialize(const nlohmann::json& data) { } BuildOrbitBasis(); } + +void OrbitAndSpin::ImGuiInspector() { + float radius = m_Radius; + if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0f)) { + SetRadius(radius); + } + + glm::vec3 center = m_Center; + if (ImGuiUtils::DrawVec3Control("Center", center)) { + SetCenter(center); + } + + glm::vec3 orbitAxis = m_OrbitAxis; + if (ImGuiUtils::DrawVec3Control("Orbit axis", orbitAxis)) { + if (glm::dot(orbitAxis, orbitAxis) >= 1e-8f) { + m_OrbitAxis = glm::normalize(orbitAxis); + BuildOrbitBasis(); + } + } + + float orbitSpeed = m_OrbitSpeed; + if (ImGui::DragFloat("Orbit speed", &orbitSpeed, 0.05f)) { + m_OrbitSpeed = orbitSpeed; + } + ImGui::DragFloat("Orbit phase", &m_OrbitPhase, 0.05f); + ImGui::Text("Orbit angle: %.2f rad", m_OrbitAngle); + + float growSpeed = m_GrowSpeed; + if (ImGui::DragFloat("Grow speed", &growSpeed, 0.05f, 0.0f)) { + m_GrowSpeed = growSpeed; + } + + float growMin = m_GrowMin; + if (ImGui::DragFloat("Grow min", &growMin, 0.01f, 0.0f)) { + m_GrowMin = std::min(growMin, m_GrowMax); + } + + float growMax = m_GrowMax; + if (ImGui::DragFloat("Grow max", &growMax, 0.01f, 0.0f)) { + m_GrowMax = std::max(growMax, m_GrowMin); + } + + glm::vec3 baseScale = m_BaseScale; + if (ImGuiUtils::DrawVec3Control("Base scale", baseScale, 0.05f)) { + m_BaseScale = baseScale; + } + + glm::vec3 spinAxis = m_SpinAxis; + if (ImGuiUtils::DrawVec3Control("Spin axis", spinAxis)) { + if (glm::dot(spinAxis, spinAxis) >= 1e-8f) { + m_SpinAxis = glm::normalize(spinAxis); + } + } + + float spinSpeed = m_SpinSpeed; + if (ImGui::DragFloat("Spin speed", &spinSpeed, 0.05f)) { + m_SpinSpeed = spinSpeed; + } + ImGui::Text("Grow phase: %.2f rad", m_GrowPhase); + ImGui::Text("Material ID: %u", m_MaterialID); +} diff --git a/destrum/src/Components/Physics/BoxCollider.cpp b/destrum/src/Components/Physics/BoxCollider.cpp index 9849989..ec91385 100644 --- a/destrum/src/Components/Physics/BoxCollider.cpp +++ b/destrum/src/Components/Physics/BoxCollider.cpp @@ -1,5 +1,9 @@ #include +#include + +#include + nlohmann::json BoxCollider::Serialize() const { auto data = SerializeCollider(); data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z}; @@ -17,3 +21,12 @@ void BoxCollider::Deserialize(const nlohmann::json& data) { }); } } + +void BoxCollider::ImGuiInspector() { + Collider::ImGuiInspector(); + + glm::vec3 halfExtents = m_HalfExtents; + if (ImGuiUtils::DrawVec3Control("Half extents", halfExtents)) { + SetHalfExtents(halfExtents); + } +} diff --git a/destrum/src/Components/Physics/CapsuleCollider.cpp b/destrum/src/Components/Physics/CapsuleCollider.cpp new file mode 100644 index 0000000..5034d8a --- /dev/null +++ b/destrum/src/Components/Physics/CapsuleCollider.cpp @@ -0,0 +1,17 @@ +#include + +#include + +void CapsuleCollider::ImGuiInspector() { + Collider::ImGuiInspector(); + + float radius = m_Radius; + if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) { + SetRadius(radius); + } + + float height = m_Height; + if (ImGui::DragFloat("Height", &height, 0.05f, 0.0001f)) { + SetHeight(height); + } +} diff --git a/destrum/src/Components/Physics/Collider.cpp b/destrum/src/Components/Physics/Collider.cpp new file mode 100644 index 0000000..2944840 --- /dev/null +++ b/destrum/src/Components/Physics/Collider.cpp @@ -0,0 +1,17 @@ +#include + +#include + +#include + +void Collider::ImGuiInspector() { + bool isTrigger = m_IsTrigger; + if (ImGui::Checkbox("Is trigger", &isTrigger)) { + SetTrigger(isTrigger); + } + + glm::vec3 centerOffset = m_CenterOffset; + if (ImGuiUtils::DrawVec3Control("Center offset", centerOffset)) { + SetCenterOffset(centerOffset); + } +} diff --git a/destrum/src/Components/Physics/Rigidbody.cpp b/destrum/src/Components/Physics/Rigidbody.cpp index 86dc1ba..80198ca 100644 --- a/destrum/src/Components/Physics/Rigidbody.cpp +++ b/destrum/src/Components/Physics/Rigidbody.cpp @@ -4,6 +4,8 @@ #include #include +#include + namespace { [[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) { switch (type) { @@ -128,3 +130,48 @@ void Rigidbody::Deserialize(const nlohmann::json& data) { m_AllowSleep = data.at("allowSleep").get(); } } + +void Rigidbody::ImGuiInspector() { + static const char* typeNames[] = {"Static", "Dynamic", "Kinematic"}; + int type = static_cast(m_Type); + if (ImGui::Combo("Body type", &type, typeNames, IM_ARRAYSIZE(typeNames))) { + SetType(static_cast(type)); + } + + float mass = m_Mass; + if (ImGui::DragFloat("Mass", &mass, 0.1f, 0.0001f)) { + SetMass(mass); + } + + float friction = m_Friction; + if (ImGui::DragFloat("Friction", &friction, 0.01f, 0.0f)) { + SetFriction(friction); + } + + float restitution = m_Restitution; + if (ImGui::SliderFloat("Restitution", &restitution, 0.0f, 1.0f)) { + SetRestitution(restitution); + } + + bool useGravity = m_UseGravity; + if (ImGui::Checkbox("Use gravity", &useGravity)) { + SetUseGravity(useGravity); + } + + bool allowSleep = m_AllowSleep; + if (ImGui::Checkbox("Allow sleep", &allowSleep)) { + SetAllowSleep(allowSleep); + } + + if (HasPhysicsBody()) { + const glm::vec3 velocity = GetLinearVelocity(); + ImGui::Text( + "Linear velocity: %.2f, %.2f, %.2f", + velocity.x, + velocity.y, + velocity.z); + ImGui::Text("Physics body: %u", m_Body.id); + } else { + ImGui::TextDisabled("Physics body: not registered"); + } +} diff --git a/destrum/src/Components/Physics/SphereCollider.cpp b/destrum/src/Components/Physics/SphereCollider.cpp index f83a64c..509d6ce 100644 --- a/destrum/src/Components/Physics/SphereCollider.cpp +++ b/destrum/src/Components/Physics/SphereCollider.cpp @@ -1,5 +1,7 @@ #include +#include + nlohmann::json SphereCollider::Serialize() const { auto data = SerializeCollider(); data["radius"] = m_Radius; @@ -12,3 +14,12 @@ void SphereCollider::Deserialize(const nlohmann::json& data) { SetRadius(data.at("radius").get()); } } + +void SphereCollider::ImGuiInspector() { + Collider::ImGuiInspector(); + + float radius = m_Radius; + if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) { + SetRadius(radius); + } +} diff --git a/destrum/src/Components/Rotator.cpp b/destrum/src/Components/Rotator.cpp index d54b4d7..e202f74 100644 --- a/destrum/src/Components/Rotator.cpp +++ b/destrum/src/Components/Rotator.cpp @@ -6,6 +6,9 @@ #include // glm::quat, glm::angleAxis #include // operator*(quat, vec3) #include +#include + +#include glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis) { @@ -128,3 +131,27 @@ void Rotator::Deserialize(const nlohmann::json& data) { m_InitialOffset = {value.at(0).get(), value.at(1).get(), value.at(2).get()}; } } + +void Rotator::ImGuiInspector() { + float distance = m_Distance; + if (ImGui::DragFloat("Distance", &distance, 0.05f, 0.0f)) { + SetDistance(distance); + } + + float speed = m_Speed; + if (ImGui::DragFloat("Speed", &speed, 0.05f)) { + SetSpeed(speed); + } + + glm::vec3 pivot = m_Pivot; + if (ImGuiUtils::DrawVec3Control("Pivot", pivot)) { + SetPivotPosition(pivot); + } + + glm::vec3 axis = m_Axis; + if (ImGuiUtils::DrawVec3Control("Axis", axis)) { + SetAxis(axis); + } + + ImGui::Text("Current angle: %.2f rad", m_CurrentAngle); +} diff --git a/destrum/src/Components/Spinner.cpp b/destrum/src/Components/Spinner.cpp index bf9c116..253baf9 100644 --- a/destrum/src/Components/Spinner.cpp +++ b/destrum/src/Components/Spinner.cpp @@ -4,6 +4,9 @@ #include "destrum/ObjectModel/Transform.h" #include "destrum/Util/DeltaTime.h" +#include "destrum/Util/ImGuiUtils.h" + +#include void Spinner::Update(float dt) { @@ -33,3 +36,17 @@ void Spinner::Deserialize(const nlohmann::json& data) { m_Angle = data.at("angle").get(); } } + +void Spinner::ImGuiInspector() { + glm::vec3 axis = m_Axis; + if (ImGuiUtils::DrawVec3Control("Axis", axis)) { + SetAxis(axis); + } + + float speed = m_Speed; + if (ImGui::DragFloat("Speed", &speed, 0.05f)) { + SetSpeed(speed); + } + + ImGui::Text("Current angle: %.2f rad", m_Angle); +} diff --git a/destrum/src/Scene/Scene.cpp b/destrum/src/Scene/Scene.cpp index bf41ea7..60543ee 100644 --- a/destrum/src/Scene/Scene.cpp +++ b/destrum/src/Scene/Scene.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include @@ -160,6 +160,11 @@ void Scene::Render(const RenderContext& ctx) { } void Scene::RenderImgui() { + ImGuiUtils::RenderSceneInspector( + m_name, + m_objects, + m_pendingAdditions, + m_SelectedObjectId); } void Scene::CleanupDestroyedGameObjects() { diff --git a/destrum/src/Util/ImGuiUtils.cpp b/destrum/src/Util/ImGuiUtils.cpp new file mode 100644 index 0000000..d405004 --- /dev/null +++ b/destrum/src/Util/ImGuiUtils.cpp @@ -0,0 +1,302 @@ +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +namespace { + [[nodiscard]] bool IsInspectableObject(const GameObject* object) { + return object != nullptr && !object->IsBeingDestroyed(); + } + + [[nodiscard]] bool HasInspectableChildren(const GameObject& object) { + return std::ranges::any_of(object.GetTransform().GetChildren(), + [](const Transform* child) { + return child != nullptr && IsInspectableObject(child->GetOwner()); + }); + } + + void DrawGameObjectNode( + GameObject& object, + ObjectId& selectedObjectId, + std::unordered_set& visited, + bool defaultOpen = false) { + if (!IsInspectableObject(&object) || !visited.insert(&object).second) { + return; + } + + const bool hasChildren = HasInspectableChildren(object); + ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_SpanAvailWidth; + if (object.GetId() == selectedObjectId) { + treeFlags |= ImGuiTreeNodeFlags_Selected; + } + if (defaultOpen) { + treeFlags |= ImGuiTreeNodeFlags_DefaultOpen; + } + if (!hasChildren) { + treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + } + + const std::string displayName = object.GetName().empty() + ? "" + : object.GetName(); + const bool isOpen = ImGui::TreeNodeEx( + static_cast(&object), + treeFlags, + "%s", + displayName.c_str()); + + if (ImGui::IsItemClicked()) { + selectedObjectId = object.GetId(); + } + + if (!object.IsActive()) { + ImGui::SameLine(); + ImGui::TextDisabled("[inactive]"); + } else if (!object.IsActiveInHierarchy()) { + ImGui::SameLine(); + ImGui::TextDisabled("[parent inactive]"); + } + + if (isOpen && hasChildren) { + for (Transform* childTransform : object.GetTransform().GetChildren()) { + if (childTransform == nullptr) { + continue; + } + + GameObject* child = childTransform->GetOwner(); + if (IsInspectableObject(child)) { + DrawGameObjectNode(*child, selectedObjectId, visited); + } + } + ImGui::TreePop(); + } + } + + [[nodiscard]] GameObject* FindSelectedObject( + const std::vector& objects, + ObjectId selectedObjectId) { + const auto it = std::find_if( + objects.begin(), + objects.end(), + [selectedObjectId](const GameObject* object) { + return object != nullptr && object->GetId() == selectedObjectId; + }); + return it == objects.end() ? nullptr : *it; + } +} + +namespace ImGuiUtils { + bool DrawVec3Control(const char* label, glm::vec3& value, float speed) { + if (label == nullptr) { + return false; + } + + ImGui::PushID(label); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(label); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + const bool changed = ImGui::DragFloat3( + "##value", + glm::value_ptr(value), + speed); + ImGui::PopID(); + return changed; + } + + void RenderSceneInspector( + std::string_view sceneName, + const std::vector>& objectsSource, + const std::vector>& pendingAdditions, + ObjectId& selectedObjectId) { + std::vector objects; + objects.reserve(objectsSource.size() + pendingAdditions.size()); + + const auto appendInspectableObjects = [&objects](const auto& source) { + for (const auto& object : source) { + if (IsInspectableObject(object.get())) { + objects.push_back(object.get()); + } + } + }; + + appendInspectableObjects(objectsSource); + appendInspectableObjects(pendingAdditions); + + std::vector roots; + roots.reserve(objects.size()); + for (GameObject* object : objects) { + Transform* parent = object->GetTransform().GetParent(); + if (parent == nullptr || !IsInspectableObject(parent->GetOwner())) { + roots.push_back(object); + } + } + + if (FindSelectedObject(objects, selectedObjectId) == nullptr) { + selectedObjectId = InvalidObjectId; + } + + ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Inspector")) { + ImGui::End(); + return; + } + + const std::string displaySceneName = sceneName.empty() + ? "" + : std::string{sceneName}; + ImGui::Text("Scene: %s", displaySceneName.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size()); + ImGui::Separator(); + + const float availableWidth = ImGui::GetContentRegionAvail().x; + const float hierarchyWidth = std::max(180.0f, availableWidth * 0.45f); + + if (ImGui::BeginChild( + "SceneHierarchy", + ImVec2(hierarchyWidth, 0.0f), + ImGuiChildFlags_Borders)) { + std::unordered_set visited; + visited.reserve(objects.size()); + for (GameObject* root : roots) { + DrawGameObjectNode(*root, selectedObjectId, visited, true); + } + + // Keep malformed or partially loaded hierarchies visible instead of + // silently dropping objects whose parent is not in this list. + bool hasUnvisitedObjects = false; + for (GameObject* object : objects) { + if (!visited.contains(object)) { + if (!hasUnvisitedObjects) { + ImGui::Separator(); + ImGui::TextDisabled("OTHER OBJECTS"); + hasUnvisitedObjects = true; + } + DrawGameObjectNode(*object, selectedObjectId, visited); + } + } + + if (objects.empty()) { + ImGui::TextDisabled("This scene has no GameObjects."); + } + } + ImGui::EndChild(); + + GameObject* selectedObject = FindSelectedObject(objects, selectedObjectId); + if (selectedObject == nullptr) { + selectedObjectId = InvalidObjectId; + } + + ImGui::SameLine(); + + if (ImGui::BeginChild( + "ObjectInspector", + ImVec2(0.0f, 0.0f), + ImGuiChildFlags_Borders)) { + if (selectedObject == nullptr) { + ImGui::TextDisabled("Select a GameObject to inspect it."); + } else { + const std::string displayName = selectedObject->GetName().empty() + ? "" + : selectedObject->GetName(); + ImGui::TextUnformatted(displayName.c_str()); + ImGui::TextDisabled( + "GameObject | ID %llu", + static_cast(selectedObject->GetId())); + ImGui::Separator(); + + bool active = selectedObject->IsActive(); + if (ImGui::Checkbox("Active", &active)) { + selectedObject->SetActive(active); + } + + const Transform* parent = selectedObject->GetTransform().GetParent(); + if (parent != nullptr && parent->GetOwner() != nullptr) { + ImGui::Text("Parent: %s", parent->GetOwner()->GetName().c_str()); + } else { + ImGui::Text("Parent: "); + } + ImGui::Text("Children: %d", selectedObject->GetTransform().GetChildCount()); + + if (ImGui::CollapsingHeader( + "Transform", + ImGuiTreeNodeFlags_DefaultOpen)) { + Transform& transform = selectedObject->GetTransform(); + bool transformChanged = false; + + glm::vec3 localPosition = transform.GetLocalPosition(); + if (DrawVec3Control("Position", localPosition, 0.05f)) { + transform.SetLocalPosition(localPosition); + transformChanged = true; + } + + glm::vec3 localRotation = glm::degrees( + glm::eulerAngles(transform.GetLocalRotation())); + if (DrawVec3Control("Rotation", localRotation, 0.5f)) { + transform.SetLocalRotation(localRotation); + transformChanged = true; + } + + glm::vec3 localScale = transform.GetLocalScale(); + if (DrawVec3Control("Scale", localScale, 0.05f)) { + transform.SetLocalScale(localScale); + transformChanged = true; + } + + if (transformChanged) { + selectedObject->RefreshPhysics(); + } + + const glm::vec3& worldPosition = transform.GetWorldPosition(); + ImGui::Text( + "World position: %.2f, %.2f, %.2f", + worldPosition.x, + worldPosition.y, + worldPosition.z); + } + + if (ImGui::CollapsingHeader( + "Components", + ImGuiTreeNodeFlags_DefaultOpen)) { + bool hasComponents = false; + for (const auto& component : selectedObject->GetComponents()) { + if (component == nullptr || component->IsBeingDestroyed()) { + continue; + } + + hasComponents = true; + ImGui::PushID(component.get()); + const std::string typeName = component->GetTypeName(); + if (ImGui::CollapsingHeader(typeName.c_str())) { + bool enabled = component->isEnabled(); + if (ImGui::Checkbox("Enabled", &enabled)) { + component->SetEnabled(enabled); + } + component->ImGuiInspector(); + } + ImGui::PopID(); + } + if (!hasComponents) { + ImGui::TextDisabled("No components."); + } + } + } + } + ImGui::EndChild(); + + ImGui::End(); + } +}