diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9dbb5c59..f19dd03c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(${PROJECT_NAME} WIN32 src/panels/InspectorPanel.cpp src/panels/ConsolePanel.cpp src/panels/ProjectPanel.cpp + src/Layers/EditorLayer.cpp ${IMGUI_SOURCES} ) @@ -61,7 +62,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE d3d12.lib dxgi.lib d3dcompiler.lib - ${CMAKE_SOURCE_DIR}/engine/build/Release/XCEngine.lib + ${CMAKE_CURRENT_SOURCE_DIR}/../engine/build/Release/XCEngine.lib ) set_target_properties(${PROJECT_NAME} PROPERTIES diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index 9dcfef22..21ddd136 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -1,4 +1,5 @@ #include "Application.h" +#include "Layers/EditorLayer.h" #include #include #include @@ -45,14 +46,6 @@ bool Application::Initialize(HWND hwnd) { m_srvHeap->GetCPUDescriptorHandleForHeapStart(), m_srvHeap->GetGPUDescriptorHandleForHeapStart()); - m_menuBar = std::make_unique(); - m_hierarchyPanel = std::make_unique(); - m_sceneViewPanel = std::make_unique(); - m_gameViewPanel = std::make_unique(); - m_inspectorPanel = std::make_unique(); - m_consolePanel = std::make_unique(); - m_projectPanel = std::make_unique(); - wchar_t exePath[MAX_PATH]; GetModuleFileNameW(nullptr, exePath, MAX_PATH); std::wstring exeDirW(exePath); @@ -66,12 +59,18 @@ bool Application::Initialize(HWND hwnd) { exeDir.resize(len - 1); WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, &exeDir[0], len, nullptr, nullptr); } - m_projectPanel->Initialize(exeDir); + + m_editorLayer = new EditorLayer(); + m_editorLayer->SetProjectPath(exeDir); + m_layerStack.pushLayer(std::unique_ptr(m_editorLayer)); + m_layerStack.onAttach(); return true; } void Application::Shutdown() { + m_layerStack.onDetach(); + ImGui_ImplDX12_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -93,8 +92,7 @@ void Application::Render() { ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); - SetupDockspace(); - RenderUI(); + m_layerStack.onImGuiRender(); ImGui::Render(); @@ -243,63 +241,5 @@ void Application::CleanupRenderTarget() { } } -void Application::SetupDockspace() { - static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_NoWindowMenuButton; - - ImGuiWindowFlags windowFlags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; - - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->Pos); - ImGui::SetNextWindowSize(viewport->Size); - ImGui::SetNextWindowViewport(viewport->ID); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - windowFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin("MainDockspace", nullptr, windowFlags); - ImGui::PopStyleVar(); - ImGui::PopStyleVar(2); - - ImGuiID dockspaceId = ImGui::GetID("MyDockspace"); - ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), dockspaceFlags); - - static bool firstTime = true; - if (firstTime) { - firstTime = false; - ImGui::DockBuilderRemoveNode(dockspaceId); - ImGui::DockBuilderAddNode(dockspaceId, dockspaceFlags | ImGuiDockNodeFlags_DockSpace); - ImGui::DockBuilderSetNodeSize(dockspaceId, viewport->Size); - - ImGuiID dockMain = dockspaceId; - ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.25f, nullptr, &dockMain); - ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.15f, nullptr, &dockMain); - ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.25f, nullptr, &dockMain); - - ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); - ImGui::DockBuilderDockWindow("Scene", dockMain); - ImGui::DockBuilderDockWindow("Game", dockMain); - ImGui::DockBuilderDockWindow("Inspector", dockRight); - ImGui::DockBuilderDockWindow("Console", dockBottom); - ImGui::DockBuilderDockWindow("Project", dockBottom); - - ImGui::DockBuilderFinish(dockspaceId); - } - - ImGui::End(); -} - -void Application::RenderUI() { - m_menuBar->Render(); - - m_hierarchyPanel->Render(); - m_sceneViewPanel->Render(); - m_gameViewPanel->Render(); - m_inspectorPanel->Render(); - m_consolePanel->Render(); - m_projectPanel->Render(); -} - } } \ No newline at end of file diff --git a/editor/src/Application.h b/editor/src/Application.h index bb3c3972..4000b7af 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -5,19 +5,15 @@ #include #include +#include + #include "Theme.h" -#include "panels/Panel.h" -#include "panels/MenuBar.h" -#include "panels/HierarchyPanel.h" -#include "panels/SceneViewPanel.h" -#include "panels/GameViewPanel.h" -#include "panels/InspectorPanel.h" -#include "panels/ConsolePanel.h" -#include "panels/ProjectPanel.h" namespace XCEngine { namespace Editor { +class EditorLayer; + class Application { public: static Application& Get(); @@ -34,8 +30,6 @@ private: bool CreateDevice(); bool CreateRenderTarget(); void CleanupRenderTarget(); - void SetupDockspace(); - void RenderUI(); HWND m_hwnd = nullptr; int m_width = 1280; @@ -54,13 +48,8 @@ private: UINT m_rtvDescriptorSize = 0; UINT m_frameIndex = 0; - std::unique_ptr m_menuBar; - std::unique_ptr m_hierarchyPanel; - std::unique_ptr m_sceneViewPanel; - std::unique_ptr m_gameViewPanel; - std::unique_ptr m_inspectorPanel; - std::unique_ptr m_consolePanel; - std::unique_ptr m_projectPanel; + Core::LayerStack m_layerStack; + EditorLayer* m_editorLayer = nullptr; }; } diff --git a/editor/src/Layers/EditorLayer.cpp b/editor/src/Layers/EditorLayer.cpp new file mode 100644 index 00000000..7ac2301d --- /dev/null +++ b/editor/src/Layers/EditorLayer.cpp @@ -0,0 +1,112 @@ +#include "EditorLayer.h" +#include "panels/MenuBar.h" +#include "panels/HierarchyPanel.h" +#include "panels/SceneViewPanel.h" +#include "panels/GameViewPanel.h" +#include "panels/InspectorPanel.h" +#include "panels/ConsolePanel.h" +#include "panels/ProjectPanel.h" +#include "Managers/SelectionManager.h" +#include "Managers/SceneManager.h" +#include +#include + +namespace XCEngine { +namespace Editor { + +EditorLayer::EditorLayer() : Layer("Editor") {} + +void EditorLayer::onAttach() { + m_menuBar = std::make_unique(); + m_hierarchyPanel = std::make_unique(); + m_sceneViewPanel = std::make_unique(); + m_gameViewPanel = std::make_unique(); + m_inspectorPanel = std::make_unique(); + m_consolePanel = std::make_unique(); + m_projectPanel = std::make_unique(); + + m_projectPanel->Initialize(m_projectPath); + + auto& selection = SelectionManager::Get(); + selection.OnSelectionChanged.Subscribe([](uint64_t id) { + }); +} + +void EditorLayer::onDetach() { +} + +void EditorLayer::onUpdate(float dt) { +} + +void EditorLayer::onEvent(void* event) { +} + +void EditorLayer::onImGuiRender() { + setupDockspace(); + renderAllPanels(); +} + +void EditorLayer::SetProjectPath(const std::string& path) { + m_projectPath = path; +} + +void EditorLayer::setupDockspace() { + static ImGuiDockNodeFlags dockspaceFlags = ImGuiDockNodeFlags_NoWindowMenuButton; + + ImGuiWindowFlags windowFlags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; + + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + windowFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + windowFlags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("MainDockspace", nullptr, windowFlags); + ImGui::PopStyleVar(); + ImGui::PopStyleVar(2); + + ImGuiID dockspaceId = ImGui::GetID("MyDockspace"); + ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), dockspaceFlags); + + static bool firstTime = true; + if (firstTime) { + firstTime = false; + ImGui::DockBuilderRemoveNode(dockspaceId); + ImGui::DockBuilderAddNode(dockspaceId, dockspaceFlags | ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspaceId, viewport->Size); + + ImGuiID dockMain = dockspaceId; + ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.25f, nullptr, &dockMain); + ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.15f, nullptr, &dockMain); + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.25f, nullptr, &dockMain); + + ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("Scene", dockMain); + ImGui::DockBuilderDockWindow("Game", dockMain); + ImGui::DockBuilderDockWindow("Inspector", dockRight); + ImGui::DockBuilderDockWindow("Console", dockBottom); + ImGui::DockBuilderDockWindow("Project", dockBottom); + + ImGui::DockBuilderFinish(dockspaceId); + } + + ImGui::End(); +} + +void EditorLayer::renderAllPanels() { + m_menuBar->Render(); + + m_hierarchyPanel->Render(); + m_sceneViewPanel->Render(); + m_gameViewPanel->Render(); + m_inspectorPanel->Render(); + m_consolePanel->Render(); + m_projectPanel->Render(); +} + +} +} diff --git a/editor/src/Layers/EditorLayer.h b/editor/src/Layers/EditorLayer.h new file mode 100644 index 00000000..a975dc00 --- /dev/null +++ b/editor/src/Layers/EditorLayer.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +namespace XCEngine { +namespace Editor { + +class MenuBar; +class HierarchyPanel; +class SceneViewPanel; +class GameViewPanel; +class InspectorPanel; +class ConsolePanel; +class ProjectPanel; + +class EditorLayer : public Core::Layer { +public: + EditorLayer(); + ~EditorLayer() override = default; + + void onAttach() override; + void onDetach() override; + void onUpdate(float dt) override; + void onEvent(void* event) override; + void onImGuiRender() override; + + void SetProjectPath(const std::string& path); + +private: + void setupDockspace(); + void renderAllPanels(); + + std::string m_projectPath; + + std::unique_ptr m_menuBar; + std::unique_ptr m_hierarchyPanel; + std::unique_ptr m_sceneViewPanel; + std::unique_ptr m_gameViewPanel; + std::unique_ptr m_inspectorPanel; + std::unique_ptr m_consolePanel; + std::unique_ptr m_projectPanel; +}; + +} +} diff --git a/editor/src/Managers/SceneManager.cpp b/editor/src/Managers/SceneManager.cpp index 075b0d30..83adf52a 100644 --- a/editor/src/Managers/SceneManager.cpp +++ b/editor/src/Managers/SceneManager.cpp @@ -48,7 +48,9 @@ EditorSceneManager::ClipboardData EditorSceneManager::CopyEntityRecursive(const data.name = entity->GetName(); if (auto* transform = entity->GetComponent<::XCEngine::Components::TransformComponent>()) { - // Transform 数据会被复制 + data.localPosition = transform->GetLocalPosition(); + data.localRotation = transform->GetLocalRotation(); + data.localScale = transform->GetLocalScale(); } for (auto* child : entity->GetChildren()) { @@ -75,6 +77,12 @@ void EditorSceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { ::XCEngine::Components::GameObject* newEntity = m_scene->CreateGameObject(data.name, parentObj); + if (auto* transform = newEntity->GetComponent<::XCEngine::Components::TransformComponent>()) { + transform->SetLocalPosition(data.localPosition); + transform->SetLocalRotation(data.localRotation); + transform->SetLocalScale(data.localScale); + } + if (parentObj == nullptr) { m_rootEntities.push_back(newEntity); } diff --git a/editor/src/Managers/SceneManager.h b/editor/src/Managers/SceneManager.h index 5194769c..c0086dc4 100644 --- a/editor/src/Managers/SceneManager.h +++ b/editor/src/Managers/SceneManager.h @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include @@ -61,7 +63,9 @@ private: struct ClipboardData { std::string name; - std::vector> components; + Math::Vector3 localPosition = Math::Vector3::Zero(); + Math::Quaternion localRotation = Math::Quaternion::Identity(); + Math::Vector3 localScale = Math::Vector3::One(); std::vector children; }; diff --git a/editor/src/Managers/SelectionManager.h b/editor/src/Managers/SelectionManager.h index 376f4236..6b1468f2 100644 --- a/editor/src/Managers/SelectionManager.h +++ b/editor/src/Managers/SelectionManager.h @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include @@ -15,26 +16,65 @@ public: return instance; } - ::XCEngine::Components::GameObject* GetSelectedEntity() const { return m_selectedEntity; } - void SetSelectedEntity(::XCEngine::Components::GameObject* entity) { - m_selectedEntity = entity; + if (m_selectedEntities.empty()) { + m_selectedEntities.push_back(entity); + } else { + m_selectedEntities[0] = entity; + m_selectedEntities.resize(1); + } OnSelectionChanged.Invoke(entity ? entity->GetID() : 0); } - void ClearSelection() { - SetSelectedEntity(nullptr); + void SetSelectedEntities(const std::vector<::XCEngine::Components::GameObject*>& entities) { + m_selectedEntities = entities; + if (!entities.empty()) { + OnSelectionChanged.Invoke(entities.back()->GetID()); + } else { + OnSelectionChanged.Invoke(0); + } } + void AddToSelection(::XCEngine::Components::GameObject* entity) { + if (entity && !IsSelected(entity)) { + m_selectedEntities.push_back(entity); + OnSelectionChanged.Invoke(entity->GetID()); + } + } + + void RemoveFromSelection(::XCEngine::Components::GameObject* entity) { + auto it = std::find(m_selectedEntities.begin(), m_selectedEntities.end(), entity); + if (it != m_selectedEntities.end()) { + m_selectedEntities.erase(it); + OnSelectionChanged.Invoke(entity ? entity->GetID() : 0); + } + } + + void ClearSelection() { + m_selectedEntities.clear(); + OnSelectionChanged.Invoke(0); + } + + ::XCEngine::Components::GameObject* GetSelectedEntity() const { + return m_selectedEntities.empty() ? nullptr : m_selectedEntities.back(); + } + + const std::vector<::XCEngine::Components::GameObject*>& GetSelectedEntities() const { + return m_selectedEntities; + } + + bool HasSelection() const { return !m_selectedEntities.empty(); } + size_t GetSelectionCount() const { return m_selectedEntities.size(); } + bool IsSelected(::XCEngine::Components::GameObject* entity) const { - return m_selectedEntity == entity; + return std::find(m_selectedEntities.begin(), m_selectedEntities.end(), entity) != m_selectedEntities.end(); } ::XCEngine::Core::Event OnSelectionChanged; private: SelectionManager() = default; - ::XCEngine::Components::GameObject* m_selectedEntity = nullptr; + std::vector<::XCEngine::Components::GameObject*> m_selectedEntities; }; } diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 582e7036..00643c65 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -93,4 +93,4 @@ LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { return 0; } return DefWindowProcW(hWnd, msg, wParam, lParam); -} \ No newline at end of file +} diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 0ef21551..20e51186 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -1,6 +1,8 @@ #include "HierarchyPanel.h" #include "Managers/SceneManager.h" #include "Managers/SelectionManager.h" +#include +#include #include #include @@ -31,8 +33,8 @@ void HierarchyPanel::Render() { ImGui::BeginChild("EntityList"); - for (auto* entity : EditorSceneManager::Get().GetRootEntities()) { - RenderEntity(entity, filter); + for (auto* gameObject : EditorSceneManager::Get().GetRootEntities()) { + RenderEntity(gameObject, filter); } if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(0) && !ImGui::IsAnyItemHovered()) { @@ -49,9 +51,9 @@ void HierarchyPanel::Render() { ImGui::InvisibleButton("##DragTarget", ImVec2(-1, -1)); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { - ::XCEngine::Components::GameObject* sourceEntity = *(::XCEngine::Components::GameObject**)payload->Data; - if (sourceEntity && sourceEntity->GetParent() != nullptr) { - EditorSceneManager::Get().MoveEntity(sourceEntity->GetID(), 0); + ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; + if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { + EditorSceneManager::Get().MoveEntity(sourceGameObject->GetID(), 0); } } ImGui::EndDragDropTarget(); @@ -67,26 +69,26 @@ void HierarchyPanel::RenderSearchBar() { ImGui::InputTextWithHint("##Search", "Search...", m_searchBuffer, sizeof(m_searchBuffer)); } -void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* entity, const std::string& filter) { - if (!entity) return; +void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { + if (!gameObject) return; - if (!filter.empty() && !PassesFilter(entity, filter)) { + if (!filter.empty() && !PassesFilter(gameObject, filter)) { return; } - ImGui::PushID(static_cast(entity->GetID())); + ImGui::PushID(static_cast(gameObject->GetID())); ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth; - if (entity->GetChildCount() == 0) { + if (gameObject->GetChildCount() == 0) { flags |= ImGuiTreeNodeFlags_Leaf; } - if (SelectionManager::Get().IsSelected(entity)) { + if (SelectionManager::Get().IsSelected(gameObject)) { flags |= ImGuiTreeNodeFlags_Selected; } - if (m_renaming && m_renamingEntity == entity) { + if (m_renaming && m_renamingEntity == gameObject) { if (m_renameJustStarted) { ImGui::SetKeyboardFocusHere(); m_renameJustStarted = false; @@ -95,7 +97,7 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* entity, co ImGui::SetNextItemWidth(-1); if (ImGui::InputText("##Rename", m_renameBuffer, sizeof(m_renameBuffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) { if (strlen(m_renameBuffer) > 0) { - EditorSceneManager::Get().RenameEntity(entity->GetID(), m_renameBuffer); + EditorSceneManager::Get().RenameEntity(gameObject->GetID(), m_renameBuffer); } m_renaming = false; m_renamingEntity = nullptr; @@ -103,35 +105,42 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* entity, co if (!ImGui::IsItemActive() && ImGui::IsMouseClicked(0)) { if (strlen(m_renameBuffer) > 0) { - EditorSceneManager::Get().RenameEntity(entity->GetID(), m_renameBuffer); + EditorSceneManager::Get().RenameEntity(gameObject->GetID(), m_renameBuffer); } m_renaming = false; m_renamingEntity = nullptr; } } else { - bool isOpen = ImGui::TreeNodeEx(entity->GetName().c_str(), flags); + bool isOpen = ImGui::TreeNodeEx((void*)gameObject->GetUUID(), flags, "%s", gameObject->GetName().c_str()); if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { - SelectionManager::Get().SetSelectedEntity(entity); + ImGuiIO& io = ImGui::GetIO(); + if (io.KeyCtrl) { + if (!SelectionManager::Get().IsSelected(gameObject)) { + SelectionManager::Get().AddToSelection(gameObject); + } + } else { + SelectionManager::Get().SetSelectedEntity(gameObject); + } } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { m_renaming = true; - m_renamingEntity = entity; - strcpy_s(m_renameBuffer, entity->GetName().c_str()); + m_renamingEntity = gameObject; + strcpy_s(m_renameBuffer, gameObject->GetName().c_str()); m_renameJustStarted = true; } - HandleDragDrop(entity); + HandleDragDrop(gameObject); if (ImGui::BeginPopupContextItem("EntityContextMenu")) { - RenderContextMenu(entity); + RenderContextMenu(gameObject); ImGui::EndPopup(); } if (isOpen) { - for (size_t i = 0; i < entity->GetChildCount(); i++) { - RenderEntity(entity->GetChild(i), filter); + for (size_t i = 0; i < gameObject->GetChildCount(); i++) { + RenderEntity(gameObject->GetChild(i), filter); } ImGui::TreePop(); } @@ -140,44 +149,54 @@ void HierarchyPanel::RenderEntity(::XCEngine::Components::GameObject* entity, co ImGui::PopID(); } -void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* entity) { +void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameObject) { auto& sceneManager = EditorSceneManager::Get(); auto& selectionManager = SelectionManager::Get(); if (ImGui::BeginMenu("Create")) { - RenderCreateMenu(entity); + RenderCreateMenu(gameObject); ImGui::EndMenu(); } + if (gameObject != nullptr && ImGui::MenuItem("Create Child")) { + auto* child = sceneManager.CreateEntity("GameObject", gameObject); + selectionManager.SetSelectedEntity(child); + } + ImGui::Separator(); + if (gameObject != nullptr && gameObject->GetParent() != nullptr) { + if (ImGui::MenuItem("Detach from Parent")) { + gameObject->DetachFromParent(); + } + } + if (ImGui::MenuItem("Rename", "F2")) { - if (entity) { + if (gameObject) { m_renaming = true; - m_renamingEntity = entity; - strcpy_s(m_renameBuffer, entity->GetName().c_str()); + m_renamingEntity = gameObject; + strcpy_s(m_renameBuffer, gameObject->GetName().c_str()); m_renameJustStarted = true; } } if (ImGui::MenuItem("Delete", "Delete")) { - sceneManager.DeleteEntity(entity->GetID()); + sceneManager.DeleteEntity(gameObject->GetID()); } ImGui::Separator(); if (ImGui::MenuItem("Copy", "Ctrl+C")) { - sceneManager.CopyEntity(entity->GetID()); + sceneManager.CopyEntity(gameObject->GetID()); } if (ImGui::MenuItem("Paste", "Ctrl+V", false, sceneManager.HasClipboardData())) { - sceneManager.PasteEntity(entity->GetID()); + sceneManager.PasteEntity(gameObject->GetID()); } if (ImGui::MenuItem("Duplicate", "Ctrl+D")) { - uint64_t newId = sceneManager.DuplicateEntity(entity->GetID()); + uint64_t newId = sceneManager.DuplicateEntity(gameObject->GetID()); if (newId != 0) { - // Selection handled via callback } } } @@ -225,24 +244,23 @@ void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent } } -void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* entity) { +void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* gameObject) { auto& sceneManager = EditorSceneManager::Get(); if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { - m_dragSource = entity; - ImGui::SetDragDropPayload("ENTITY_PTR", &entity, sizeof(::XCEngine::Components::GameObject*)); - ImGui::Text("%s", entity->GetName().c_str()); + ImGui::SetDragDropPayload("ENTITY_PTR", &gameObject, sizeof(::XCEngine::Components::GameObject*)); + ImGui::Text("%s", gameObject->GetName().c_str()); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("ENTITY_PTR")) { - ::XCEngine::Components::GameObject* sourceEntity = *(::XCEngine::Components::GameObject**)payload->Data; - if (sourceEntity != entity && sourceEntity != nullptr) { + ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; + if (sourceGameObject != gameObject && sourceGameObject != nullptr) { bool isValidMove = true; - ::XCEngine::Components::GameObject* checkParent = entity; + ::XCEngine::Components::GameObject* checkParent = gameObject; while (checkParent != nullptr) { - if (checkParent == sourceEntity) { + if (checkParent == sourceGameObject) { isValidMove = false; break; } @@ -250,7 +268,16 @@ void HierarchyPanel::HandleDragDrop(::XCEngine::Components::GameObject* entity) } if (isValidMove) { - sceneManager.MoveEntity(sourceEntity->GetID(), entity->GetID()); + auto* srcTransform = sourceGameObject->GetTransform(); + Math::Vector3 worldPos = srcTransform->GetPosition(); + Math::Quaternion worldRot = srcTransform->GetRotation(); + Math::Vector3 worldScale = srcTransform->GetScale(); + + sceneManager.MoveEntity(sourceGameObject->GetID(), gameObject->GetID()); + + srcTransform->SetPosition(worldPos); + srcTransform->SetRotation(worldRot); + srcTransform->SetScale(worldScale); } } } @@ -262,20 +289,20 @@ void HierarchyPanel::HandleKeyboardShortcuts() { auto& sceneManager = EditorSceneManager::Get(); auto& selectionManager = SelectionManager::Get(); - ::XCEngine::Components::GameObject* selectedEntity = selectionManager.GetSelectedEntity(); + ::XCEngine::Components::GameObject* selectedGameObject = selectionManager.GetSelectedEntity(); if (ImGui::IsWindowFocused()) { if (ImGui::IsKeyPressed(ImGuiKey_Delete)) { - if (selectedEntity != nullptr) { - sceneManager.DeleteEntity(selectedEntity->GetID()); + if (selectedGameObject != nullptr) { + sceneManager.DeleteEntity(selectedGameObject->GetID()); } } if (ImGui::IsKeyPressed(ImGuiKey_F2)) { - if (selectedEntity != nullptr) { + if (selectedGameObject != nullptr) { m_renaming = true; - m_renamingEntity = selectedEntity; - strcpy_s(m_renameBuffer, selectedEntity->GetName().c_str()); + m_renamingEntity = selectedGameObject; + strcpy_s(m_renameBuffer, selectedGameObject->GetName().c_str()); m_renameJustStarted = true; } } @@ -283,35 +310,35 @@ void HierarchyPanel::HandleKeyboardShortcuts() { ImGuiIO& io = ImGui::GetIO(); if (io.KeyCtrl) { if (ImGui::IsKeyPressed(ImGuiKey_C)) { - if (selectedEntity != nullptr) { - sceneManager.CopyEntity(selectedEntity->GetID()); + if (selectedGameObject != nullptr) { + sceneManager.CopyEntity(selectedGameObject->GetID()); } } if (ImGui::IsKeyPressed(ImGuiKey_V)) { if (sceneManager.HasClipboardData()) { - sceneManager.PasteEntity(selectedEntity ? selectedEntity->GetID() : 0); + sceneManager.PasteEntity(selectedGameObject ? selectedGameObject->GetID() : 0); } } if (ImGui::IsKeyPressed(ImGuiKey_D)) { - if (selectedEntity != nullptr) { - sceneManager.DuplicateEntity(selectedEntity->GetID()); + if (selectedGameObject != nullptr) { + sceneManager.DuplicateEntity(selectedGameObject->GetID()); } } } } } -bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* entity, const std::string& filter) { - if (!entity) return false; +bool HierarchyPanel::PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter) { + if (!gameObject) return false; - if (entity->GetName().find(filter) != std::string::npos) { + if (gameObject->GetName().find(filter) != std::string::npos) { return true; } - for (size_t i = 0; i < entity->GetChildCount(); i++) { - if (PassesFilter(entity->GetChild(i), filter)) { + for (size_t i = 0; i < gameObject->GetChildCount(); i++) { + if (PassesFilter(gameObject->GetChild(i), filter)) { return true; } } diff --git a/editor/src/panels/HierarchyPanel.h b/editor/src/panels/HierarchyPanel.h index a9a9099a..01ff6c65 100644 --- a/editor/src/panels/HierarchyPanel.h +++ b/editor/src/panels/HierarchyPanel.h @@ -15,12 +15,12 @@ public: private: void RenderSearchBar(); - void RenderEntity(::XCEngine::Components::GameObject* entity, const std::string& filter); - void RenderContextMenu(::XCEngine::Components::GameObject* entity); + void RenderEntity(::XCEngine::Components::GameObject* gameObject, const std::string& filter); + void RenderContextMenu(::XCEngine::Components::GameObject* gameObject); void RenderCreateMenu(::XCEngine::Components::GameObject* parent); - void HandleDragDrop(::XCEngine::Components::GameObject* entity); + void HandleDragDrop(::XCEngine::Components::GameObject* gameObject); void HandleKeyboardShortcuts(); - bool PassesFilter(::XCEngine::Components::GameObject* entity, const std::string& filter); + bool PassesFilter(::XCEngine::Components::GameObject* gameObject, const std::string& filter); uint64_t m_selectionHandlerId = 0; @@ -29,7 +29,6 @@ private: ::XCEngine::Components::GameObject* m_renamingEntity = nullptr; char m_renameBuffer[256] = ""; bool m_renameJustStarted = false; - ::XCEngine::Components::GameObject* m_dragSource = nullptr; }; } diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 8d782c45..885ef375 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -10,6 +10,7 @@ namespace Editor { InspectorPanel::InspectorPanel() : Panel("Inspector") { m_selectionHandlerId = SelectionManager::Get().OnSelectionChanged.Subscribe([this](uint64_t) { + m_selectedGameObject = SelectionManager::Get().GetSelectedEntity(); }); } @@ -20,10 +21,10 @@ InspectorPanel::~InspectorPanel() { void InspectorPanel::Render() { ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); - ::XCEngine::Components::GameObject* entity = SelectionManager::Get().GetSelectedEntity(); + m_selectedGameObject = SelectionManager::Get().GetSelectedEntity(); - if (entity) { - RenderEntity(entity); + if (m_selectedGameObject) { + RenderGameObject(m_selectedGameObject); } else { ImGui::Text("No object selected"); ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Select an object in Hierarchy"); @@ -32,27 +33,80 @@ void InspectorPanel::Render() { ImGui::End(); } -void InspectorPanel::RenderEntity(::XCEngine::Components::GameObject* entity) { - ImGui::Text("%s", entity->GetName().c_str()); - ImGui::Separator(); +void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameObject) { + char nameBuffer[256]; + strcpy_s(nameBuffer, gameObject->GetName().c_str()); + ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer)); + if (ImGui::IsItemDeactivatedAfterEdit()) { + gameObject->SetName(nameBuffer); + } - auto components = entity->GetComponents<::XCEngine::Components::Component>(); + ImGui::SameLine(); + if (ImGui::Button("Add Component")) { + ImGui::OpenPopup("AddComponent"); + } + + RenderAddComponentPopup(gameObject); + + auto components = gameObject->GetComponents<::XCEngine::Components::Component>(); for (auto* component : components) { - RenderComponent(component); - ImGui::Separator(); + RenderComponent(component, gameObject); } } -void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component) { +void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { + if (!ImGui::BeginPopup("AddComponent")) { + return; + } + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); + + ImGui::SeparatorText("Components"); + + if (ImGui::MenuItem("Transform", nullptr, false, !gameObject->GetComponent<::XCEngine::Components::TransformComponent>())) { + gameObject->AddComponent<::XCEngine::Components::TransformComponent>(); + ImGui::CloseCurrentPopup(); + } + + ImGui::SeparatorText("Other"); + ImGui::TextDisabled("No more components available"); + + ImGui::PopStyleVar(); + ImGui::EndPopup(); +} + +void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { if (!component) return; const char* name = component->GetName().c_str(); - std::string headerId = std::string(name) + "##" + std::to_string(reinterpret_cast(component)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 2}); - if (ImGui::CollapsingHeader(headerId.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(10.0f); - + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_DefaultOpen | + ImGuiTreeNodeFlags_Framed | + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_FramePadding | + ImGuiTreeNodeFlags_AllowOverlap; + + bool open = ImGui::TreeNodeEx((void*)typeid(*component).hash_code(), flags, "%s", name); + + ImGui::PopStyleVar(); + + bool removeComponent = false; + if (ImGui::BeginPopupContextItem("ComponentSettings")) { + if (ImGui::MenuItem("Remove Component")) { + removeComponent = true; + } + ImGui::EndPopup(); + } + + if (removeComponent) { + RemoveComponentByType(component, gameObject); + return; + } + + if (open) { if (auto* transform = dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { ::XCEngine::Math::Vector3 position = transform->GetLocalPosition(); ::XCEngine::Math::Vector3 rotation = transform->GetLocalEulerAngles(); @@ -71,7 +125,23 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen } } - ImGui::Unindent(10.0f); + ImGui::TreePop(); + } +} + +void InspectorPanel::RemoveComponentByType(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { + if (!component || !gameObject) return; + + if (dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { + return; + } + + auto components = gameObject->GetComponents<::XCEngine::Components::Component>(); + for (auto* comp : components) { + if (comp == component) { + gameObject->RemoveComponent<::XCEngine::Components::Component>(); + break; + } } } diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index 6cbca1d0..9921f6bb 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -15,10 +15,13 @@ public: void Render() override; private: - void RenderEntity(::XCEngine::Components::GameObject* entity); - void RenderComponent(::XCEngine::Components::Component* component); + void RenderGameObject(::XCEngine::Components::GameObject* gameObject); + void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject); + void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); + void RemoveComponentByType(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); uint64_t m_selectionHandlerId = 0; + ::XCEngine::Components::GameObject* m_selectedGameObject = nullptr; }; } diff --git a/editor/src/panels/Panel.cpp b/editor/src/panels/Panel.cpp index 95ec7a32..8bb8aaa3 100644 --- a/editor/src/panels/Panel.cpp +++ b/editor/src/panels/Panel.cpp @@ -2,5 +2,6 @@ namespace XCEngine { namespace Editor { + +} } -} \ No newline at end of file diff --git a/editor/src/panels/Panel.h b/editor/src/panels/Panel.h index fdbb8c8f..ba3a987c 100644 --- a/editor/src/panels/Panel.h +++ b/editor/src/panels/Panel.h @@ -1,27 +1,27 @@ #pragma once +#include #include -#include namespace XCEngine { namespace Editor { -class Panel { +class Panel : public Core::Layer { public: - Panel(const std::string& name) : m_name(name), m_isOpen(true) {} + Panel(const std::string& name) : Layer(name), m_isOpen(true) {} virtual ~Panel() = default; virtual void Render() = 0; - const std::string& GetName() const { return m_name; } bool IsOpen() const { return m_isOpen; } void SetOpen(bool open) { m_isOpen = open; } void Toggle() { m_isOpen = !m_isOpen; } + void onImGuiRender() override { Render(); } + protected: - std::string m_name; bool m_isOpen; }; } -} \ No newline at end of file +} diff --git a/engine/include/XCEngine/Components/GameObject.h b/engine/include/XCEngine/Components/GameObject.h index efe85c3a..6974a1d7 100644 --- a/engine/include/XCEngine/Components/GameObject.h +++ b/engine/include/XCEngine/Components/GameObject.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace XCEngine { namespace Components { @@ -24,9 +25,12 @@ public: ~GameObject(); ID GetID() const { return m_id; } + uint64_t GetUUID() const { return m_uuid; } const std::string& GetName() const { return m_name; } void SetName(const std::string& name) { m_name = name; } + void DetachFromParent(); + Scene* GetScene() const { return m_scene; } TransformComponent* GetTransform() { return m_transform; } @@ -83,6 +87,16 @@ public: return result; } + template + void RemoveComponent() { + for (auto it = m_components.begin(); it != m_components.end(); ++it) { + if (T* casted = dynamic_cast(it->get())) { + m_components.erase(it); + return; + } + } + } + template T* GetComponentInChildren() { T* comp = GetComponent(); @@ -154,6 +168,7 @@ public: private: ID m_id = INVALID_ID; + uint64_t m_uuid = 0; std::string m_name; bool m_activeSelf = true; diff --git a/engine/include/XCEngine/Core/Layer.h b/engine/include/XCEngine/Core/Layer.h new file mode 100644 index 00000000..8473bdde --- /dev/null +++ b/engine/include/XCEngine/Core/Layer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Core { + +class Layer { +public: + Layer(const std::string& name = "Layer") : m_name(name) {} + virtual ~Layer() = default; + + virtual void onAttach() {} + virtual void onDetach() {} + virtual void onUpdate(float dt) {} + virtual void onEvent(void* event) {} + virtual void onImGuiRender() {} + + const std::string& getName() const { return m_name; } + +protected: + std::string m_name; +}; + +} +} diff --git a/engine/include/XCEngine/Core/LayerStack.h b/engine/include/XCEngine/Core/LayerStack.h new file mode 100644 index 00000000..f331cfff --- /dev/null +++ b/engine/include/XCEngine/Core/LayerStack.h @@ -0,0 +1,100 @@ +#pragma once + +#include "Layer.h" +#include +#include +#include + +namespace XCEngine { +namespace Core { + +class LayerStack { +public: + LayerStack() = default; + ~LayerStack() = default; + + void pushLayer(std::unique_ptr layer); + void pushOverlay(std::unique_ptr overlay); + void popLayer(Layer* layer); + void popOverlay(Layer* overlay); + + void onUpdate(float dt); + void onEvent(void* event); + void onImGuiRender(); + void onAttach(); + void onDetach(); + + std::vector>::iterator begin() { return m_layers.begin(); } + std::vector>::iterator end() { return m_layers.end(); } + std::vector>::reverse_iterator rbegin() { return m_layers.rbegin(); } + std::vector>::reverse_iterator rend() { return m_layers.rend(); } + std::vector>::const_iterator cbegin() const { return m_layers.begin(); } + std::vector>::const_iterator cend() const { return m_layers.end(); } + std::vector>::const_reverse_iterator crbegin() const { return m_layers.crbegin(); } + std::vector>::const_reverse_iterator crend() const { return m_layers.crend(); } + +private: + std::vector> m_layers; + unsigned int m_layerInsertIndex = 0; +}; + +inline void LayerStack::pushLayer(std::unique_ptr layer) { + m_layers.emplace(m_layers.begin() + m_layerInsertIndex, std::move(layer)); + m_layerInsertIndex++; +} + +inline void LayerStack::pushOverlay(std::unique_ptr overlay) { + m_layers.emplace_back(std::move(overlay)); +} + +inline void LayerStack::popLayer(Layer* layer) { + auto it = std::find_if(m_layers.begin(), m_layers.begin() + m_layerInsertIndex, + [layer](const std::unique_ptr& ptr) { return ptr.get() == layer; }); + if (it != m_layers.begin() + m_layerInsertIndex) { + (*it)->onDetach(); + m_layers.erase(it); + m_layerInsertIndex--; + } +} + +inline void LayerStack::popOverlay(Layer* overlay) { + auto it = std::find_if(m_layers.begin() + m_layerInsertIndex, m_layers.end(), + [overlay](const std::unique_ptr& ptr) { return ptr.get() == overlay; }); + if (it != m_layers.end()) { + (*it)->onDetach(); + m_layers.erase(it); + } +} + +inline void LayerStack::onUpdate(float dt) { + for (auto& layer : m_layers) { + layer->onUpdate(dt); + } +} + +inline void LayerStack::onEvent(void* event) { + for (auto& layer : m_layers) { + layer->onEvent(event); + } +} + +inline void LayerStack::onImGuiRender() { + for (auto& layer : m_layers) { + layer->onImGuiRender(); + } +} + +inline void LayerStack::onAttach() { + for (auto& layer : m_layers) { + layer->onAttach(); + } +} + +inline void LayerStack::onDetach() { + for (auto& layer : m_layers) { + layer->onDetach(); + } +} + +} +} diff --git a/engine/src/Components/GameObject.cpp b/engine/src/Components/GameObject.cpp index 157699ba..cad22f2a 100644 --- a/engine/src/Components/GameObject.cpp +++ b/engine/src/Components/GameObject.cpp @@ -10,6 +10,10 @@ GameObject::ID GameObject::s_nextID = 1; GameObject::GameObject() : m_name("GameObject") { m_id = s_nextID++; + static std::random_device rd; + static std::mt19937_64 gen(rd()); + static std::uniform_int_distribution dis(1, UINT64_MAX); + m_uuid = dis(gen); m_transform = new TransformComponent(); m_transform->m_gameObject = this; } @@ -17,6 +21,10 @@ GameObject::GameObject() GameObject::GameObject(const std::string& name) : m_name(name) { m_id = s_nextID++; + static std::random_device rd; + static std::mt19937_64 gen(rd()); + static std::uniform_int_distribution dis(1, UINT64_MAX); + m_uuid = dis(gen); m_transform = new TransformComponent(); m_transform->m_gameObject = this; } @@ -88,6 +96,27 @@ void GameObject::DetachChildren() { m_children.clear(); } +void GameObject::DetachFromParent() { + if (m_parent) { + Math::Vector3 worldPos = GetTransform()->GetPosition(); + Math::Quaternion worldRot = GetTransform()->GetRotation(); + Math::Vector3 worldScale = GetTransform()->GetScale(); + + auto& siblings = m_parent->m_children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end()); + m_parent = nullptr; + + if (m_scene) { + m_scene->m_rootGameObjects.push_back(m_id); + } + + GetTransform()->SetPosition(worldPos); + GetTransform()->SetRotation(worldRot); + GetTransform()->SetScale(worldScale); + GetTransform()->SetDirty(); + } +} + void GameObject::SetActive(bool active) { if (m_activeSelf != active) { m_activeSelf = active; diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 00000000..75f563a1 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,55 @@ +[Window][Hierarchy] +Pos=0,24 +Size=189,485 +Collapsed=0 +DockId=0x00000003,0 + +[Window][Scene] +Pos=191,24 +Size=594,485 +Collapsed=0 +DockId=0x00000005,0 + +[Window][Game] +Pos=191,24 +Size=594,485 +Collapsed=0 +DockId=0x00000005,1 + +[Window][Inspector] +Pos=787,24 +Size=533,485 +Collapsed=0 +DockId=0x00000006,0 + +[Window][Console] +Pos=0,511 +Size=1320,170 +Collapsed=0 +DockId=0x00000002,0 + +[Window][Project] +Pos=0,511 +Size=1320,170 +Collapsed=0 +DockId=0x00000002,1 + +[Window][MainDockspace] +Pos=0,0 +Size=1320,681 +Collapsed=0 + +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Docking][Data] +DockSpace ID=0x8D32E0A4 Window=0x1C358F53 Pos=0,24 Size=1320,657 Split=Y + DockNode ID=0x00000001 Parent=0x8D32E0A4 SizeRef=1264,509 Split=X + DockNode ID=0x00000003 Parent=0x00000001 SizeRef=189,509 Selected=0xBABDAE5E + DockNode ID=0x00000004 Parent=0x00000001 SizeRef=1073,509 Split=X Selected=0xE601B12F + DockNode ID=0x00000005 Parent=0x00000004 SizeRef=538,485 CentralNode=1 Selected=0xE601B12F + DockNode ID=0x00000006 Parent=0x00000004 SizeRef=533,485 Selected=0x36DC96AB + DockNode ID=0x00000002 Parent=0x8D32E0A4 SizeRef=1264,170 Selected=0x9C21DE82 + diff --git a/minimal.ppm b/minimal.ppm new file mode 100644 index 00000000..2217d8f0 Binary files /dev/null and b/minimal.ppm differ