feat: Add "simple" inspector to the imgui panels
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
|
||||
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<const GameObject*>& 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()
|
||||
? "<unnamed>"
|
||||
: object.GetName();
|
||||
const bool isOpen = ImGui::TreeNodeEx(
|
||||
static_cast<const void*>(&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<GameObject*>& 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<std::shared_ptr<GameObject>>& objectsSource,
|
||||
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||
ObjectId& selectedObjectId) {
|
||||
std::vector<GameObject*> 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<GameObject*> 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()
|
||||
? "<unnamed>"
|
||||
: 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<const GameObject*> 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()
|
||||
? "<unnamed>"
|
||||
: selectedObject->GetName();
|
||||
ImGui::TextUnformatted(displayName.c_str());
|
||||
ImGui::TextDisabled(
|
||||
"GameObject | ID %llu",
|
||||
static_cast<unsigned long long>(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: <scene root>");
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user