749 lines
26 KiB
C++
749 lines
26 KiB
C++
#include <destrum/Util/SceneInspector.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cfloat>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <imgui.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <destrum/Components/UI/Canvas.h>
|
|
#include <destrum/Components/UI/RectTransform.h>
|
|
#include <destrum/Components/UI/UIImage.h>
|
|
#include <destrum/Components/UI/UIText.h>
|
|
#include <destrum/ObjectModel/Component.h>
|
|
#include <destrum/ObjectModel/GameObject.h>
|
|
#include <destrum/ObjectModel/Transform.h>
|
|
#include <destrum/Scene/Scene.h>
|
|
#include <destrum/Serialization/ComponentFactory.h>
|
|
#include <destrum/Serialization/ComponentRegistry.h>
|
|
#include <destrum/Serialization/PrefabSerializer.h>
|
|
#include <destrum/Util/ImGuiUtils.h>
|
|
|
|
namespace {
|
|
[[nodiscard]] bool IsInspectable(const GameObject* object)
|
|
{
|
|
return object != nullptr && !object->IsBeingDestroyed();
|
|
}
|
|
|
|
[[nodiscard]] GameObject* ParentOf(const GameObject& object)
|
|
{
|
|
const Transform* parent = object.GetTransform().GetParent();
|
|
return parent != nullptr ? parent->GetOwner() : nullptr;
|
|
}
|
|
|
|
[[nodiscard]] bool HasInspectableChildren(const GameObject& object)
|
|
{
|
|
return std::ranges::any_of(object.GetTransform().GetChildren(), [](const Transform* child) {
|
|
return child != nullptr && IsInspectable(child->GetOwner());
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] std::vector<GameObject*> CollectObjects(const Scene& scene)
|
|
{
|
|
std::vector<GameObject*> objects;
|
|
objects.reserve(scene.GetObjects().size() + scene.GetPendingAdditions().size());
|
|
for (const auto* source : {&scene.GetObjects(), &scene.GetPendingAdditions()}) {
|
|
for (const auto& object : *source) {
|
|
if (IsInspectable(object.get())) {
|
|
objects.push_back(object.get());
|
|
}
|
|
}
|
|
}
|
|
return objects;
|
|
}
|
|
|
|
[[nodiscard]] bool IsRoot(const GameObject& object)
|
|
{
|
|
return !IsInspectable(ParentOf(object));
|
|
}
|
|
|
|
[[nodiscard]] GameObject* NextSibling(const Scene& scene, const GameObject& object)
|
|
{
|
|
if (const Transform* parent = object.GetTransform().GetParent()) {
|
|
const auto& siblings = parent->GetChildren();
|
|
const auto it = std::ranges::find(siblings, &object.GetTransform());
|
|
for (auto next = it == siblings.end() ? it : it + 1; next != siblings.end(); ++next) {
|
|
if (IsInspectable((*next)->GetOwner())) {
|
|
return (*next)->GetOwner();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const auto objects = CollectObjects(scene);
|
|
const auto it = std::ranges::find(objects, &object);
|
|
for (auto next = it == objects.end() ? it : it + 1; next != objects.end(); ++next) {
|
|
if (IsRoot(**next)) {
|
|
return *next;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
[[nodiscard]] bool ContainsIgnoreCase(std::string_view text, std::string_view query)
|
|
{
|
|
const auto it = std::search(text.begin(), text.end(), query.begin(), query.end(), [](char a, char b) {
|
|
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
|
});
|
|
return it != text.end();
|
|
}
|
|
|
|
[[nodiscard]] const char* DisplayName(const GameObject& object)
|
|
{
|
|
return object.GetName().empty() ? "<unnamed>" : object.GetName().c_str();
|
|
}
|
|
|
|
GameObject* CreateChild(Scene& scene, GameObject* parent, const std::string& name)
|
|
{
|
|
GameObject* object = scene.CreateGameObject(name);
|
|
if (parent != nullptr) {
|
|
object->GetTransform().SetParent(&parent->GetTransform(), false);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
GameObject* CreateUIChild(Scene& scene, GameObject* parent, const std::string& name)
|
|
{
|
|
if (parent == nullptr || Canvas::FindFor(*parent) == nullptr) {
|
|
parent = CreateChild(scene, parent, "Canvas");
|
|
parent->AddComponent<Canvas>();
|
|
}
|
|
GameObject* object = CreateChild(scene, parent, name);
|
|
object->AddComponent<RectTransform>();
|
|
return object;
|
|
}
|
|
|
|
// Returns the created object so the caller can select and rename it
|
|
GameObject* DrawCreateMenu(Scene& scene, GameObject* parent)
|
|
{
|
|
GameObject* created = nullptr;
|
|
if (ImGui::MenuItem("Empty")) {
|
|
created = CreateChild(scene, parent, "GameObject");
|
|
}
|
|
if (ImGui::BeginMenu("UI")) {
|
|
if (ImGui::MenuItem("Canvas")) {
|
|
created = CreateChild(scene, parent, "Canvas");
|
|
created->AddComponent<Canvas>();
|
|
}
|
|
if (ImGui::MenuItem("Rect")) {
|
|
created = CreateUIChild(scene, parent, "Rect");
|
|
}
|
|
if (ImGui::MenuItem("Image")) {
|
|
created = CreateUIChild(scene, parent, "Image");
|
|
created->AddComponent<UIImage>();
|
|
}
|
|
if (ImGui::MenuItem("Text")) {
|
|
created = CreateUIChild(scene, parent, "Text");
|
|
created->GetComponent<RectTransform>()->SetSize({300.0f, 60.0f});
|
|
created->AddComponent<UIText>("Text", 32.0f);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
return created;
|
|
}
|
|
|
|
// Right aligned controls drawn over a collapsing header, x is from the start of the line
|
|
[[nodiscard]] float RightAlignedX(float lineStartX, float lineWidth, float itemsWidth)
|
|
{
|
|
return lineStartX + lineWidth - itemsWidth;
|
|
}
|
|
}
|
|
|
|
void SceneInspector::Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open)
|
|
{
|
|
RegisterEngineComponents();
|
|
|
|
if (scene.FindGameObject(selectedObjectId) == nullptr) {
|
|
selectedObjectId = InvalidObjectId;
|
|
}
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(720.0f, 560.0f), ImGuiCond_FirstUseEver);
|
|
if (!ImGui::Begin("Inspector", open)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
HandleShortcuts(scene, selectedObjectId);
|
|
|
|
if (ImGui::BeginChild("Hierarchy", ImVec2(std::max(220.0f, ImGui::GetContentRegionAvail().x * 0.4f), 0.0f),
|
|
ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX)) {
|
|
DrawHierarchy(scene, selectedObjectId);
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::BeginChild("Properties", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) {
|
|
if (GameObject* selected = scene.FindGameObject(selectedObjectId)) {
|
|
DrawObject(*selected);
|
|
} else if (drawSceneSettings) {
|
|
drawSceneSettings();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::End();
|
|
|
|
ApplyPendingActions(scene, selectedObjectId);
|
|
}
|
|
|
|
void SceneInspector::HandleShortcuts(Scene& scene, ObjectId& selectedObjectId)
|
|
{
|
|
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || ImGui::GetIO().WantTextInput ||
|
|
selectedObjectId == InvalidObjectId) {
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
|
|
m_PendingDelete = selectedObjectId;
|
|
}
|
|
if (ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D)) {
|
|
m_PendingDuplicate = selectedObjectId;
|
|
}
|
|
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
|
|
if (const GameObject* selected = scene.FindGameObject(selectedObjectId)) {
|
|
BeginRename(*selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawHierarchy(Scene& scene, ObjectId& selectedObjectId)
|
|
{
|
|
if (ImGui::Button("+")) {
|
|
ImGui::OpenPopup("CreateObject");
|
|
}
|
|
if (ImGui::BeginPopup("CreateObject")) {
|
|
if (GameObject* created = DrawCreateMenu(scene, nullptr)) {
|
|
selectedObjectId = created->GetId();
|
|
BeginRename(*created);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
|
ImGuiUtils::InputString("##Search", m_Filter, 0, "Search");
|
|
|
|
if (!ImGui::BeginChild("Tree")) {
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
const std::vector<GameObject*> objects = CollectObjects(scene);
|
|
std::unordered_set<const GameObject*> visited;
|
|
visited.reserve(objects.size());
|
|
|
|
if (m_Filter.empty()) {
|
|
for (GameObject* object : objects) {
|
|
if (IsRoot(*object)) {
|
|
DrawNode(scene, *object, selectedObjectId, visited);
|
|
}
|
|
}
|
|
// Anything not reachable from a root is stuck in a parent cycle, still show it
|
|
std::unordered_set<const GameObject*> reachable;
|
|
std::vector<const GameObject*> pending;
|
|
for (const GameObject* object : objects) {
|
|
if (IsRoot(*object)) {
|
|
pending.push_back(object);
|
|
}
|
|
}
|
|
while (!pending.empty()) {
|
|
const GameObject* object = pending.back();
|
|
pending.pop_back();
|
|
if (!reachable.insert(object).second) {
|
|
continue;
|
|
}
|
|
for (const Transform* child : object->GetTransform().GetChildren()) {
|
|
if (child != nullptr && IsInspectable(child->GetOwner())) {
|
|
pending.push_back(child->GetOwner());
|
|
}
|
|
}
|
|
}
|
|
for (GameObject* object : objects) {
|
|
if (!reachable.contains(object)) {
|
|
DrawNode(scene, *object, selectedObjectId, visited);
|
|
}
|
|
}
|
|
} else {
|
|
for (GameObject* object : objects) {
|
|
if (ContainsIgnoreCase(object->GetName(), m_Filter)) {
|
|
DrawNode(scene, *object, selectedObjectId, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The empty space below the tree: click to deselect, drop to move to the root, right click to create
|
|
const ImVec2 remaining = ImGui::GetContentRegionAvail();
|
|
ImGui::InvisibleButton("##Background", ImVec2(std::max(remaining.x, 1.0f), std::max(remaining.y, ImGui::GetFrameHeight())));
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
|
selectedObjectId = InvalidObjectId;
|
|
}
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
|
|
m_PendingMove = {.object = *static_cast<const ObjectId*>(payload->Data)};
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
if (ImGui::BeginPopupContextItem("BackgroundMenu")) {
|
|
DrawObjectMenu(scene, nullptr, selectedObjectId);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void SceneInspector::DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId,
|
|
std::unordered_set<const GameObject*>& visited)
|
|
{
|
|
if (!IsInspectable(&object) || !visited.insert(&object).second) {
|
|
return;
|
|
}
|
|
|
|
const bool flat = !m_Filter.empty();
|
|
const bool hasChildren = !flat && HasInspectableChildren(object);
|
|
const bool renaming = m_RenamingId == object.GetId();
|
|
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth |
|
|
ImGuiTreeNodeFlags_DrawLinesToNodes;
|
|
if (object.GetId() == selectedObjectId) {
|
|
flags |= ImGuiTreeNodeFlags_Selected;
|
|
}
|
|
if (!hasChildren) {
|
|
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
|
}
|
|
if (m_OpenNextFrame == object.GetId()) {
|
|
ImGui::SetNextItemOpen(true);
|
|
m_OpenNextFrame = InvalidObjectId;
|
|
}
|
|
|
|
const bool inactive = !object.IsActiveInHierarchy();
|
|
if (inactive) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
}
|
|
const bool open = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())), flags, "%s",
|
|
renaming ? "" : DisplayName(object));
|
|
if (inactive) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
if ((ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) &&
|
|
!ImGui::IsItemToggledOpen()) {
|
|
selectedObjectId = object.GetId();
|
|
}
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && !ImGui::IsItemToggledOpen()) {
|
|
BeginRename(object);
|
|
}
|
|
|
|
DrawNodeDragDrop(object);
|
|
|
|
// Leaf nodes push no ID, without this every sibling shares one popup and draws it again
|
|
ImGui::PushID(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())));
|
|
if (ImGui::BeginPopupContextItem("ObjectMenu")) {
|
|
DrawObjectMenu(scene, &object, selectedObjectId);
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
|
|
if (renaming) {
|
|
ImGui::SameLine();
|
|
DrawRenameField(object);
|
|
}
|
|
|
|
if (open && hasChildren) {
|
|
for (Transform* child : object.GetTransform().GetChildren()) {
|
|
if (child != nullptr && IsInspectable(child->GetOwner())) {
|
|
DrawNode(scene, *child->GetOwner(), selectedObjectId, visited);
|
|
}
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawNodeDragDrop(GameObject& object)
|
|
{
|
|
const ObjectId id = object.GetId();
|
|
if (ImGui::BeginDragDropSource()) {
|
|
ImGui::SetDragDropPayload(ImGuiUtils::GameObjectPayload, &id, sizeof(id));
|
|
ImGui::TextUnformatted(DisplayName(object));
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
|
|
if (!ImGui::BeginDragDropTarget()) {
|
|
return;
|
|
}
|
|
|
|
const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
|
|
ImGuiUtils::GameObjectPayload,
|
|
ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect);
|
|
if (payload != nullptr) {
|
|
const ObjectId dragged = *static_cast<const ObjectId*>(payload->Data);
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
const float zone = (max.y - min.y) * 0.25f;
|
|
const float mouseY = ImGui::GetMousePos().y;
|
|
const bool before = mouseY < min.y + zone;
|
|
const bool after = mouseY > max.y - zone;
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
const ImU32 color = ImGui::GetColorU32(ImGuiCol_DragDropTarget);
|
|
if (before) {
|
|
drawList->AddLine(min, ImVec2(max.x, min.y), color, 2.0f);
|
|
} else if (after) {
|
|
drawList->AddLine(ImVec2(min.x, max.y), max, color, 2.0f);
|
|
} else {
|
|
drawList->AddRect(min, max, color, 0.0f, 0, 2.0f);
|
|
}
|
|
|
|
if (payload->IsDelivery() && dragged != id) {
|
|
const GameObject* parent = ParentOf(object);
|
|
const ObjectId parentId = IsInspectable(parent) ? parent->GetId() : InvalidObjectId;
|
|
if (before || after) {
|
|
m_PendingMove = {.object = dragged, .parent = parentId, .anchor = id, .after = after};
|
|
} else {
|
|
m_PendingMove = {.object = dragged, .parent = id};
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
void SceneInspector::DrawRenameField(GameObject& object)
|
|
{
|
|
if (m_FocusRename) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
m_FocusRename = false;
|
|
}
|
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
|
const bool submitted = ImGuiUtils::InputString("##Rename", m_RenameBuffer,
|
|
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
|
m_RenamingId = InvalidObjectId;
|
|
return;
|
|
}
|
|
if (submitted || ImGui::IsItemDeactivated()) {
|
|
if (!m_RenameBuffer.empty()) {
|
|
object.SetName(m_RenameBuffer);
|
|
}
|
|
m_RenamingId = InvalidObjectId;
|
|
}
|
|
}
|
|
|
|
void SceneInspector::BeginRename(const GameObject& object)
|
|
{
|
|
m_RenamingId = object.GetId();
|
|
m_RenameBuffer = object.GetName();
|
|
m_FocusRename = true;
|
|
}
|
|
|
|
void SceneInspector::DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId)
|
|
{
|
|
if (ImGui::BeginMenu(object != nullptr ? "Create Child" : "Create")) {
|
|
if (GameObject* created = DrawCreateMenu(scene, object)) {
|
|
selectedObjectId = created->GetId();
|
|
if (object != nullptr) {
|
|
m_OpenNextFrame = object->GetId();
|
|
}
|
|
BeginRename(*created);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (object == nullptr) {
|
|
return;
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Rename", "F2")) {
|
|
BeginRename(*object);
|
|
}
|
|
if (ImGui::MenuItem("Duplicate", "Ctrl+D")) {
|
|
m_PendingDuplicate = object->GetId();
|
|
}
|
|
if (ImGui::MenuItem("Unparent", nullptr, false, !IsRoot(*object))) {
|
|
m_PendingMove = {.object = object->GetId()};
|
|
}
|
|
bool active = object->IsActive();
|
|
if (ImGui::MenuItem("Active", nullptr, &active)) {
|
|
object->SetActive(active);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Delete", "Del")) {
|
|
m_PendingDelete = object->GetId();
|
|
}
|
|
}
|
|
|
|
void SceneInspector::ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId)
|
|
{
|
|
if (m_PendingMove.object != InvalidObjectId) {
|
|
GameObject* object = scene.FindGameObject(m_PendingMove.object);
|
|
GameObject* parent = scene.FindGameObject(m_PendingMove.parent);
|
|
GameObject* insertBefore = scene.FindGameObject(m_PendingMove.anchor);
|
|
if (insertBefore != nullptr && m_PendingMove.after) {
|
|
insertBefore = NextSibling(scene, *insertBefore);
|
|
}
|
|
if (insertBefore == object && object != nullptr) {
|
|
insertBefore = NextSibling(scene, *object);
|
|
}
|
|
if (object != nullptr && scene.MoveInHierarchy(*object, parent, insertBefore) && parent != nullptr) {
|
|
m_OpenNextFrame = parent->GetId();
|
|
}
|
|
m_PendingMove = {};
|
|
}
|
|
|
|
if (m_PendingDuplicate != InvalidObjectId) {
|
|
if (GameObject* object = scene.FindGameObject(m_PendingDuplicate)) {
|
|
if (const GameObject* duplicate = PrefabSerializer::Duplicate(*object)) {
|
|
selectedObjectId = duplicate->GetId();
|
|
}
|
|
}
|
|
m_PendingDuplicate = InvalidObjectId;
|
|
}
|
|
|
|
if (m_PendingDelete != InvalidObjectId) {
|
|
if (GameObject* object = scene.FindGameObject(m_PendingDelete)) {
|
|
object->Destroy();
|
|
}
|
|
if (selectedObjectId == m_PendingDelete) {
|
|
selectedObjectId = InvalidObjectId;
|
|
}
|
|
m_PendingDelete = InvalidObjectId;
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawObject(GameObject& object)
|
|
{
|
|
bool active = object.IsActive();
|
|
if (ImGui::Checkbox("##Active", &active)) {
|
|
object.SetActive(active);
|
|
}
|
|
ImGui::SetItemTooltip("Active");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
|
std::string name = object.GetName();
|
|
if (ImGuiUtils::InputString("##Name", name)) {
|
|
object.SetName(name);
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
DrawTransform(object);
|
|
DrawComponents(object);
|
|
DrawAddComponent(object);
|
|
}
|
|
|
|
void SceneInspector::DrawTransform(GameObject& object)
|
|
{
|
|
auto* rectTransform = object.GetComponent<RectTransform>();
|
|
// Everything under a canvas has a RectTransform, a stretched one keeps the old layout
|
|
if (rectTransform == nullptr && object.GetComponent<Canvas>() == nullptr && Canvas::FindFor(object) != nullptr) {
|
|
rectTransform = object.AddComponent<RectTransform>();
|
|
rectTransform->Stretch();
|
|
}
|
|
|
|
if (rectTransform != nullptr) {
|
|
const bool open = ImGui::CollapsingHeader("Rect Transform", ImGuiTreeNodeFlags_DefaultOpen);
|
|
if (ImGui::BeginPopupContextItem("RectTransformMenu")) {
|
|
if (ImGui::MenuItem("Stretch")) {
|
|
rectTransform->Stretch();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
if (open) {
|
|
ImGui::PushID(rectTransform);
|
|
rectTransform->ImGuiInspector();
|
|
ImGui::PopID();
|
|
}
|
|
return;
|
|
}
|
|
|
|
Transform& transform = object.GetTransform();
|
|
const bool open = ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen);
|
|
bool changed = false;
|
|
if (ImGui::BeginPopupContextItem("TransformMenu")) {
|
|
if (ImGui::MenuItem("Reset Position")) {
|
|
transform.SetLocalPosition(glm::vec3{0.0f});
|
|
changed = true;
|
|
}
|
|
if (ImGui::MenuItem("Reset Rotation")) {
|
|
transform.SetLocalRotation(glm::quat{1.0f, 0.0f, 0.0f, 0.0f});
|
|
changed = true;
|
|
}
|
|
if (ImGui::MenuItem("Reset Scale")) {
|
|
transform.SetLocalScale(glm::vec3{1.0f});
|
|
changed = true;
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (open) {
|
|
glm::vec3 position = transform.GetLocalPosition();
|
|
if (ImGuiUtils::DrawVec3Control("Position", position, 0.05f)) {
|
|
transform.SetLocalPosition(position);
|
|
changed = true;
|
|
}
|
|
glm::vec3 rotation = glm::degrees(glm::eulerAngles(transform.GetLocalRotation()));
|
|
if (ImGuiUtils::DrawVec3Control("Rotation", rotation, 0.5f)) {
|
|
transform.SetLocalRotation(rotation);
|
|
changed = true;
|
|
}
|
|
glm::vec3 scale = transform.GetLocalScale();
|
|
if (ImGuiUtils::DrawVec3Control("Scale", scale, 0.05f)) {
|
|
transform.SetLocalScale(scale);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
object.RefreshPhysicsInHierarchy();
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawComponents(GameObject& object)
|
|
{
|
|
auto& components = object.GetComponents();
|
|
const auto* rectTransform = object.GetComponent<RectTransform>();
|
|
const bool underCanvas = Canvas::FindFor(object) != nullptr;
|
|
|
|
for (std::size_t i = 0; i < components.size(); ++i) {
|
|
Component* component = components[i].get();
|
|
if (component == nullptr || component->IsBeingDestroyed() || component == rectTransform) {
|
|
continue;
|
|
}
|
|
|
|
ImGui::PushID(component);
|
|
|
|
const float lineStart = ImGui::GetCursorPosX();
|
|
const float lineWidth = ImGui::GetContentRegionAvail().x;
|
|
const bool enabled = component->isEnabled();
|
|
if (!enabled) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
}
|
|
const std::string typeName = component->GetTypeName();
|
|
const bool open = ImGui::CollapsingHeader(typeName.c_str(),
|
|
ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_AllowOverlap);
|
|
if (!enabled) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
const bool removable = !(underCanvas && dynamic_cast<RectTransform*>(component) != nullptr);
|
|
if (ImGui::BeginPopupContextItem("ComponentMenu")) {
|
|
DrawComponentMenu(object, *component, i, removable);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
const ImGuiStyle& style = ImGui::GetStyle();
|
|
const float buttonWidth = ImGui::CalcTextSize("...").x + style.FramePadding.x * 2.0f;
|
|
const float controlsWidth = ImGui::GetFrameHeight() + style.ItemSpacing.x + buttonWidth + style.FramePadding.x;
|
|
ImGui::SameLine(RightAlignedX(lineStart, lineWidth, controlsWidth));
|
|
bool enabledToggle = enabled;
|
|
if (ImGui::Checkbox("##Enabled", &enabledToggle)) {
|
|
component->SetEnabled(enabledToggle);
|
|
}
|
|
ImGui::SetItemTooltip("Enabled");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("...")) {
|
|
ImGui::OpenPopup("ComponentMenu");
|
|
}
|
|
|
|
if (open) {
|
|
component->ImGuiInspector();
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
|
|
// A move or remove from the menu changed the list, the rest is drawn next frame
|
|
if (components.size() <= i || components[i].get() != component) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable)
|
|
{
|
|
auto& components = object.GetComponents();
|
|
if (ImGui::MenuItem("Move Up", nullptr, false, index > 0)) {
|
|
std::swap(components[index], components[index - 1]);
|
|
}
|
|
if (ImGui::MenuItem("Move Down", nullptr, false, index + 1 < components.size())) {
|
|
std::swap(components[index], components[index + 1]);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
const std::string typeName = component.GetTypeName();
|
|
if (ImGui::MenuItem("Copy Values")) {
|
|
m_ClipboardType = typeName;
|
|
m_Clipboard = component.Serialize();
|
|
}
|
|
if (ImGui::MenuItem("Paste Values", nullptr, false, m_ClipboardType == typeName)) {
|
|
try {
|
|
component.Deserialize(m_Clipboard);
|
|
if (component.AffectsPhysics()) {
|
|
object.RefreshPhysics();
|
|
}
|
|
} catch (const std::exception& exception) {
|
|
spdlog::warn("Pasting {} values failed: {}", typeName, exception.what());
|
|
}
|
|
}
|
|
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Remove", nullptr, false, removable)) {
|
|
const bool affectsPhysics = component.AffectsPhysics();
|
|
component.Destroy();
|
|
if (affectsPhysics) {
|
|
object.RefreshPhysics();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SceneInspector::DrawAddComponent(GameObject& object)
|
|
{
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Add Component", ImVec2(-FLT_MIN, 0.0f))) {
|
|
m_ComponentFilter.clear();
|
|
ImGui::OpenPopup("AddComponent");
|
|
}
|
|
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetItemRectSize().x, 0.0f), ImVec2(FLT_MAX, 320.0f));
|
|
if (!ImGui::BeginPopup("AddComponent")) {
|
|
return;
|
|
}
|
|
|
|
if (ImGui::IsWindowAppearing()) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
}
|
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
|
const bool submitted = ImGuiUtils::InputString("##ComponentSearch", m_ComponentFilter,
|
|
ImGuiInputTextFlags_EnterReturnsTrue, "Search");
|
|
|
|
std::vector<std::string> types = ComponentFactory::GetRegisteredTypeNames();
|
|
std::ranges::sort(types);
|
|
std::erase_if(types, [&](const std::string& type) {
|
|
const bool added = std::ranges::any_of(object.GetComponents(), [&](const auto& component) {
|
|
return component != nullptr && !component->IsBeingDestroyed() && component->GetTypeName() == type;
|
|
});
|
|
return added || !ContainsIgnoreCase(type, m_ComponentFilter);
|
|
});
|
|
|
|
std::string chosen = submitted && !types.empty() ? types.front() : std::string{};
|
|
for (const std::string& type : types) {
|
|
if (ImGui::Selectable(type.c_str())) {
|
|
chosen = type;
|
|
}
|
|
}
|
|
|
|
if (!chosen.empty()) {
|
|
try {
|
|
if (ComponentFactory::Create(chosen, object) == nullptr) {
|
|
spdlog::warn("Component type is not registered: {}", chosen);
|
|
}
|
|
} catch (const std::exception& exception) {
|
|
spdlog::warn("Adding {} failed: {}", chosen, exception.what());
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|