Files
Destrum/destrum/src/Util/ImGuiUtils.cpp
T

368 lines
14 KiB
C++

#include <destrum/Util/ImGuiUtils.h>
#include <algorithm>
#include <cfloat>
#include <exception>
#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>
#include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.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) {
RegisterEngineComponents();
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);
}
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<std::string> 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()) {
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();
}
}