368 lines
14 KiB
C++
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();
|
|
}
|
|
}
|