#include "EditorCorePrivate.hpp" #include "App.hpp" #include "AppConfig.hpp" #include "EditorAccessories.hpp" #include "EditorAttachmentImpl.hpp" #include "EditorCommandPalette.hpp" #include "EditorNotification.hpp" #include "EditorUtils.hpp" #include "GameObject.hpp" #include "Macros.hpp" #include "Mesh.hpp" #include "Player.hpp" #include "SceneThings.hpp" #include "ScopeGuard.hpp" #include "VertexIndex.hpp" #include "YCombinator.hpp" #define GLFW_INCLUDE_NONE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::literals; namespace ProjectBrussel_UNITY_ID { // TODO handle internal state internally and move this to EditorUtils.hpp enum RenamableSelectableAction { RSA_None, RSA_Selected, RSA_RenameCommitted, RSA_RenameCancelled, }; RenamableSelectableAction RenamableSelectable(const char* displayName, bool selected, bool& renaming, std::string& renamingScratchBuffer) // { RenamableSelectableAction result = RSA_None; ImGuiSelectableFlags flags = 0; // When renaming, disable all other entries that is not the one being renamed if (renaming && !selected) { flags |= ImGuiSelectableFlags_Disabled; } if (renaming && selected) { // State: being renamed ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 0, 0 }); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0); ImGui::SetKeyboardFocusHere(); if (ImGui::InputText("##Rename", &renamingScratchBuffer, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue)) { // Confirm renaming = false; result = RSA_RenameCommitted; } ImGui::PopStyleVar(2); if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { // Cancel renaming = false; result = RSA_RenameCancelled; } } else { // State: normal if (ImGui::Selectable(displayName, selected, flags)) { result = RSA_Selected; } } return result; } } // namespace ProjectBrussel_UNITY_ID void EditorInspector::SelectTarget(TargetType type, void* object) { selectedItt = type; selectedItPtr = object; renaming = false; renamingScratchBuffer.clear(); } EditorContentBrowser::EditorContentBrowser(EditorInspector* inspector) : mInspector{ inspector } { } EditorContentBrowser::~EditorContentBrowser() { } void EditorContentBrowser::Show(bool* open) { using namespace ProjectBrussel_UNITY_ID; ImGuiWindowFlags windowFlags; if (mDocked) { // Center window horizontally, align bottom vertically auto& viewportSize = ImGui::GetMainViewport()->Size; ImGui::SetNextWindowPos(ImVec2(viewportSize.x / 2, viewportSize.y), ImGuiCond_Always, ImVec2(0.5f, 1.0f)); ImGui::SetNextWindowSizeRelScreen(0.8f, mBrowserHeight); windowFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; } else { windowFlags = 0; } ImGui::Begin("Content Browser", open, windowFlags); ImGui::Splitter(true, kSplitterThickness, &mSplitterLeft, &mSplitterRight, kLeftPaneMinWidth, kRightPaneMinWidth); ImGui::BeginChild("LeftPane", ImVec2(mSplitterLeft - kPadding, 0.0f)); { if (ImGui::Selectable("Settings", mPane == P_Settings)) { mPane = P_Settings; } if (ImGui::Selectable("Ires", mPane == P_Ires)) { mPane = P_Ires; } if (ImGui::Selectable("Levels", mPane == P_Level)) { mPane = P_Level; } } ImGui::EndChild(); ImGui::SameLine(0.0f, kPadding + kSplitterThickness + kPadding); ImGui::BeginChild("RightPane"); // Fill remaining space auto origItt = mInspector->selectedItt; auto origItPtr = mInspector->selectedItPtr; switch (mPane) { case P_Settings: { ImGui::Checkbox("Docked", &mDocked); ImGui::SliderFloat("Height", &mBrowserHeight, 0.1f, 1.0f); } break; case P_Ires: { bool isIttIres = origItt == EditorInspector::ITT_Ires; if (ImGui::Button("New")) { ImGui::OpenPopup("New Ires"); } if (ImGui::BeginPopup("New Ires")) { for (int i = 0; i < IresObject::KD_COUNT; ++i) { auto kind = static_cast(i); if (ImGui::MenuItem(IresObject::ToString(kind).data())) { auto ires = IresObject::Create(kind); auto [DISCARD, success] = IresManager::instance->Add(ires.get()); if (success) { (void)ires.release(); } } } ImGui::EndPopup(); } ImGui::SameLine(); if (ImGui::Button("Refresh list") || ImGui::IsKeyPressed(ImGuiKey_F5)) { // TODO } ImGui::SameLine(); if (ImGui::Button("Save", !isIttIres)) { auto ires = static_cast(origItPtr); IresManager::instance->Save(ires); } ImGui::SameLine(); if (ImGui::Button("Reload", !isIttIres)) { auto ires = static_cast(origItPtr); IresManager::instance->Reload(ires); } ImGui::SameLine(); if (ImGui::Button("Rename", !isIttIres) || (isIttIres && ImGui::IsKeyPressed(ImGuiKey_F2, false))) { auto ires = static_cast(origItPtr); mInspector->renaming = true; mInspector->renamingScratchBuffer = ires->GetName(); } ImGui::SameLine(); if (ImGui::Button("Delete", !isIttIres) || (isIttIres && ImGui::IsKeyPressed(ImGuiKey_Delete, false))) { ImGui::OpenPopup("Delete Ires"); } bool openedDummy = true; if (ImGui::BeginPopupModal("Delete Ires", &openedDummy, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) { if (ImGui::Button("Confirm")) { auto ires = static_cast(origItPtr); IresManager::instance->Delete(ires); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } auto& objects = IresManager::instance->GetObjects(); for (auto it = objects.begin(); it != objects.end(); ++it) { auto ires = it->second.Get(); auto& name = ires->GetName(); bool selected = origItPtr == ires; switch (RenamableSelectable(name.c_str(), selected, mInspector->renaming, mInspector->renamingScratchBuffer)) { case RSA_Selected: { mInspector->SelectTarget(EditorInspector::ITT_Ires, ires); } break; case RSA_RenameCommitted: { ires->SetName(std::move(mInspector->renamingScratchBuffer)); } break; // Do nothing case RSA_RenameCancelled: case RSA_None: break; } if (!mInspector->renaming) { if (ImGui::BeginDragDropSource()) { auto kindName = IresObject::ToString(ires->GetKind()); // Reason: intentionally using pointer as payload ImGui::SetDragDropPayload(kindName.data(), &ires, sizeof(ires)); // NOLINT(bugprone-sizeof-expression) ImGui::Text("%s '%s'", kindName.data(), name.c_str()); ImGui::EndDragDropSource(); } } } } break; case P_Level: { bool isIttLevel = origItt == EditorInspector::ITT_Level; if (ImGui::Button("New")) { auto uid = Uid::Create(); auto& ldObj = LevelManager::instance->AddLevel(uid); mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj); mInspector->renaming = true; mInspector->renamingScratchBuffer = ldObj.name; } auto& objects = LevelManager::instance->mObjByUid; for (auto it = objects.begin(); it != objects.end(); ++it) { auto& uid = it->first; auto& ldObj = it->second; auto* level = ldObj.level.Get(); bool selected = origItPtr == &ldObj; const char* displayName = ldObj.name.c_str(); if (strcmp(displayName, "") == 0) { displayName = ""; } switch (RenamableSelectable(displayName, selected, mInspector->renaming, mInspector->renamingScratchBuffer)) { case RSA_Selected: { mInspector->SelectTarget(EditorInspector::ITT_Level, &ldObj); } break; case RSA_RenameCommitted: { ldObj.name = std::move(mInspector->renamingScratchBuffer); } break; // Do nothing case RSA_RenameCancelled: case RSA_None: break; } if (!mInspector->renaming) { if (ImGui::BeginDragDropSource()) { // Reason: intentionally using pointer as payload ImGui::SetDragDropPayload(BRUSSEL_TAG_Level, &ldObj, sizeof(ldObj)); // NOLINT(bugprone-sizeof-expression) ImGui::Text(BRUSSEL_Uid_FORMAT_STR, BRUSSEL_Uid_FORMAT_EXPAND(uid)); ImGui::TextUnformatted(ldObj.name.c_str()); ImGui::EndDragDropSource(); } } } } break; } ImGui::EndChild(); ImGui::End(); } namespace ProjectBrussel_UNITY_ID { glm::quat CalcQuaternionFromDegreesEulerAngle(glm::vec3 eulerAngleDegrees) { glm::vec3 eulerAngleRadians; eulerAngleRadians.x = eulerAngleDegrees.x / 180 * M_PI; eulerAngleRadians.y = eulerAngleDegrees.y / 180 * M_PI; eulerAngleRadians.z = eulerAngleDegrees.z / 180 * M_PI; return glm::quat(eulerAngleRadians); } void PushKeyCodeRecorder(App* app, int* writeKey, bool* writeKeyStatus) { app->PushKeyCaptureCallback([=](int key, int action) { // Allow the user to cancel by pressing Esc if (key == GLFW_KEY_ESCAPE) { return true; } if (action == GLFW_PRESS) { *writeKey = key; *writeKeyStatus = writeKeyStatus; return true; } return false; }); } struct GobjTreeNodeShowInfo { EditorInstance* in_editor; GameObject* out_openPopup = nullptr; }; void GobjTreeNode(GobjTreeNodeShowInfo& showInfo, GameObject* object) { auto& inspector = showInfo.in_editor->GetInspector(); auto attachment = object->GetEditorAttachment(); if (!attachment) { attachment = EaGameObject::Create(object).release(); object->SetEditorAttachment(attachment); // NOTE: takes ownership } ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_NoTreePushOnOpen; if (inspector.selectedItPtr == object) { flags |= ImGuiTreeNodeFlags_Selected; } ImGui::PushID(reinterpret_cast(object)); // BEGIN tree node bool opened = ImGui::TreeNodeEx(attachment->name.c_str(), flags); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { inspector.SelectTarget(EditorInspector::ITT_GameObject, object); } if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) { showInfo.out_openPopup = object; } if (opened) { ImGui::Indent(); for (auto& child : object->GetChildren()) { GobjTreeNode(showInfo, child); } ImGui::Unindent(); } // END tree node ImGui::PopID(); }; #define GAMEOBJECT_CONSTRUCTOR(ClassName) [](GameWorld* world) -> GameObject* { return new ClassName(world); } struct CreatableGameObject { GameObject* (*factory)(GameWorld*); const char* name; GameObject::Kind kind; } creatableGameObjects[] = { { .factory = GAMEOBJECT_CONSTRUCTOR(GameObject), .name = "GameObject", .kind = GameObject::KD_Generic, }, { .factory = GAMEOBJECT_CONSTRUCTOR(SimpleGeometryObject), .name = "Simple Geometry", .kind = GameObject::KD_SimpleGeometry, }, { .factory = GAMEOBJECT_CONSTRUCTOR(BuildingObject), .name = "Building", .kind = GameObject::KD_Building, }, { .factory = GAMEOBJECT_CONSTRUCTOR(LevelWrapperObject), .name = "Level Wrapper", .kind = GameObject::KD_LevelWrapper, }, }; #undef GAMEOBJECT_CONSTRUCTOR } // namespace ProjectBrussel_UNITY_ID std::unique_ptr IEditor::CreateInstance(App* app) { return std::make_unique(app); } EditorInstance::EditorInstance(App* app) : mApp{ app } , mEdContentBrowser(&mEdInspector) { mEditorCamera.name = "Editor Camera"s; mEditorCamera.SetEyePos(glm::vec3(0, 0, 1)); mEditorCamera.SetTargetDirection(glm::vec3(0, 0, -1)); app->BindActiveCamera(&mEditorCamera); } EditorInstance::~EditorInstance() { } void EditorInstance::OnGameStateChanged(bool running) { if (running) { mApp->UnbindActiveCamera(); } else { mApp->BindActiveCamera(&mEditorCamera); } } void EditorInstance::Show() { using namespace ProjectBrussel_UNITY_ID; using namespace Tags; auto world = mApp->GetWorld(); auto& io = ImGui::GetIO(); ImGui::BeginMainMenuBar(); if (ImGui::BeginMenu("View")) { ImGui::MenuItem("ImGui Demo", nullptr, &mWindowVisible_ImGuiDemo); ImGui::MenuItem("Command Palette", "Ctrl+Shift+P", &mWindowVisible_CommandPalette); ImGui::MenuItem("Inspector", nullptr, &mWindowVisible_Inspector); ImGui::MenuItem("Content Browser", "Ctrl+Space", &mWindowVisible_ContentBrowser); ImGui::MenuItem("World Structure", nullptr, &mWindowVisible_WorldStructure); ImGui::MenuItem("World Properties", nullptr, &mWindowVisible_WorldProperties); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); if (mWindowVisible_ImGuiDemo) { ImGui::ShowDemoWindow(&mWindowVisible_ImGuiDemo); } if (io.KeyCtrl && io.KeyShift && ImGui::IsKeyPressed(GLFW_KEY_P, false)) { mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette; } if (mWindowVisible_CommandPalette) { mEdCommandPalette.Show(&mWindowVisible_CommandPalette); } if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { mWindowVisible_ContentBrowser = !mWindowVisible_ContentBrowser; } if (mWindowVisible_ContentBrowser) { mEdContentBrowser.Show(&mWindowVisible_ContentBrowser); } auto& camera = *mApp->GetActiveCamera(); ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); ImGuizmo::SetDrawlist(ImGui::GetBackgroundDrawList()); if (IsCurrentCameraEditor() && mEcm == ECM_Side3D) { float viewManipulateRight = io.DisplaySize.x; float viewManipulateTop = 0; // TODO get rid of this massive hack: how to manage const better for intuitively read-only, but write doesn't-care data? auto& lastFrameInfo = const_cast(mApp->GetWorldRenderer()->GetLastFrameInfo()); auto& view = lastFrameInfo.matrixView; auto& proj = lastFrameInfo.matrixProj; #if 0 ImGuizmo::ViewManipulate( glm::value_ptr(view), 200.0f, // TODO ImVec2(viewManipulateRight - 128, viewManipulateTop), ImVec2(128, 128), 0x10101010); // Extract eye and target position from view matrix // - View matrix transforms world space to view space // - Inverse view matrix should transform view space into world space // - In view space, camera's pos is (0,0,0) and the look/forward vector should be (0,0,-1) auto invView = glm::inverse(view); camera.eye = invView * glm::vec4(0, 0, 0, 1); camera.target = camera.eye + glm::vec3(invView * glm::vec4(0, 0, -1, 1)); #endif // TODO draw this as a part of the world so it doesn't block objects #if 0 glm::mat4 identity(1.00f); ImGuizmo::DrawGrid( glm::value_ptr(view), glm::value_ptr(proj), glm::value_ptr(identity), 100.f); #endif { // Camera controls auto cameraPos = camera.eye; auto cameraForward = glm::normalize(camera.target - camera.eye); // Always move on the horzontal flat plane cameraForward.y = 0.0f; if (mMoveCamKeyboard) { if (ImGui::IsKeyDown(ImGuiKey_W)) { cameraPos += mMoveCamSlideSpeed * cameraForward; } if (ImGui::IsKeyDown(ImGuiKey_S)) { auto cameraBack = glm::normalize(-cameraForward); cameraPos += mMoveCamSlideSpeed * cameraBack; } if (ImGui::IsKeyDown(ImGuiKey_A)) { auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0))); auto cameraLeft = -cameraRight; cameraPos += mMoveCamSlideSpeed * cameraLeft; } if (ImGui::IsKeyDown(ImGuiKey_D)) { auto cameraRight = glm::normalize(glm::cross(cameraForward, glm::vec3(0, 1, 0))); cameraPos += mMoveCamSlideSpeed * cameraRight; } if (ImGui::IsKeyDown(ImGuiKey_Space)) { cameraPos.y += mMoveCamSlideSpeed; } if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) { cameraPos.y -= mMoveCamSlideSpeed; } } if (mMoveCamScrollWheel) { cameraPos += cameraForward * io.MouseWheel * mMoveCamScrollSpeed; } camera.SetEyePos(cameraPos); } } else { { // Camera controls auto cameraPos = camera.eye; if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !io.WantCaptureMouse && !mDragCam_Happening) { mDragCam_CamInitial = camera.eye; mDragCam_CursorInitial = ImGui::GetMousePos(); mDragCam_Happening = true; } if (mDragCam_Happening) { auto newPos = ImGui::GetMousePos(); // NOTE: we are emulating as if the mouse is dragging the "canvas", through moving the camera in the opposite direction of the natural position delta float deltaX = mDragCam_CursorInitial.x - newPos.x; cameraPos.x = mDragCam_CamInitial.x + deltaX / camera.pixelsPerMeter; float deltaY = -(mDragCam_CursorInitial.y - newPos.y); // Invert Y delta because ImGui uses top-left origin (mouse moving down translates to positive value, but in our coordinate system down is negative) cameraPos.y = mDragCam_CamInitial.y + deltaY / camera.pixelsPerMeter; } if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { mDragCam_Happening = false; } if (mMoveCamKeyboard) { if (ImGui::IsKeyDown(ImGuiKey_W)) { cameraPos.y += mMoveCamSlideSpeed; } if (ImGui::IsKeyDown(ImGuiKey_S)) { cameraPos.y -= mMoveCamSlideSpeed; } if (ImGui::IsKeyDown(ImGuiKey_A)) { cameraPos.x -= mMoveCamSlideSpeed; } if (ImGui::IsKeyDown(ImGuiKey_D)) { cameraPos.x += mMoveCamSlideSpeed; } } if (mMoveCamScrollWheel) { cameraPos.z = std::clamp(cameraPos.z + io.MouseWheel, 0.1f, 100.0f); } camera.SetEyePos(cameraPos); } } if (mWindowVisible_Inspector) { ImGui::Begin("Inspector"); switch (mEdInspector.selectedItt) { case EditorInspector::ITT_GameObject: { auto object = static_cast(mEdInspector.selectedItPtr); ShowInspector(object); } break; case EditorInspector::ITT_Ires: { auto ires = static_cast(mEdInspector.selectedItPtr); ShowInspector(ires); } break; case EditorInspector::ITT_Level: { auto ldObj = static_cast(mEdInspector.selectedItPtr); ShowInspector(ldObj); } break; case EditorInspector::ITT_None: break; } ImGui::End(); } if (mWindowVisible_WorldProperties) { ImGui::Begin("World properties"); ShowWorldProperties(); ImGui::End(); } if (mWindowVisible_WorldStructure) { ImGui::Begin("World structure"); GobjTreeNodeShowInfo showInfo{ .in_editor = this, }; GobjTreeNode(showInfo, &world->GetRoot()); if (showInfo.out_openPopup) { mPopupCurrent_GameObject = showInfo.out_openPopup; ImGui::OpenPopup("GameObject Popup"); ImGui::SetNextWindowPos(ImGui::GetMousePos()); } if (ImGui::BeginPopup("GameObject Popup")) { // Target no longer selected during popup open if (!mPopupCurrent_GameObject) { ImGui::CloseCurrentPopup(); } if (ImGui::BeginMenu("Add child")) { for (size_t i = 0; i < std::size(creatableGameObjects); ++i) { auto& info = creatableGameObjects[i]; if (ImGui::MenuItem(info.name)) { auto object = info.factory(world); mPopupCurrent_GameObject->AddChild(object); } } ImGui::EndMenu(); } ImGui::Separator(); if (ImGui::MenuItem("Remove")) { ImGui::DialogConfirmation("Are you sure you want to delete this GameObject?", [object = mPopupCurrent_GameObject](bool yes) { object->RemoveSelfFromParent(); delete object; }); } ImGui::EndPopup(); } ImGui::End(); } ShowSpriteViewer(); ImGui::ShowDialogs(); ImGui::ShowNotifications(); } void EditorInstance::ShowWorldProperties() { if (mApp->IsGameRunning()) { if (ImGui::Button("Pause")) { mApp->SetGameRunning(false); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("The game is currently running. Click to pause."); } } else { if (ImGui::Button("Play")) { mApp->SetGameRunning(true); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("The game is currently paused. Click to run."); } } auto& camera = *mApp->GetActiveCamera(); // vvv Camera settings (per instance) ImGui::TextUnformatted("Active camera:"); ImGui::Indent(); ImGui::TextUnformatted(camera.name.c_str()); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Object at <%p>", &camera); } ImGui::InputFloat3("Eye", glm::value_ptr(camera.eye)); if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) { ImGui::OpenPopup("##CTXMENU"); } ImGui::InputFloat3("Target", glm::value_ptr(camera.target)); if (ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) { ImGui::OpenPopup("##CTXMENU"); } if (ImGui::BeginPopup("##CTXMENU", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) { if (ImGui::MenuItem("Reset to origin")) { camera.SetEyePos(glm::vec3(1.0f, 1.0f, 1.0f)); } if (ImGui::MenuItem("Reset completely")) { camera.eye = glm::vec3(0, 0, 1); camera.target = glm::vec3(0, 0, 0); } ImGui::EndPopup(); } if (IsCurrentCameraEditor()) { const char* preview; switch (mEcm) { case ECM_2D: preview = "2D view"; break; case ECM_Side3D: preview = "Side 3D view"; break; } if (ImGui::BeginCombo("Mode", preview)) { if (ImGui::Selectable("2D view", mEcm == ECM_2D)) { if (mEcm != ECM_2D) { mEcm = ECM_2D; // TODO project eye to world plane camera.SetHasPerspective(false); } } if (ImGui::Selectable("Side 3D view", mEcm == ECM_Side3D, ImGuiSelectableFlags_None)) { if (mEcm != ECM_Side3D) { mEcm = ECM_Side3D; auto origEye = camera.eye; auto origTarget = camera.target; // New setup: focus on the point of world plane that we were originally "hovering above" camera.target = origEye; camera.target.z = 0.0f; // New setup: move the eye back at an angle camera.eye = camera.target; camera.eye.x += 4.0f * std::cos(60.0f); camera.eye.y += 0.0f; camera.eye.z += 4.0f * std::sin(60.0f); camera.SetHasPerspective(true); } } ImGui::EndCombo(); } } ImGui::Checkbox("Perspective", &camera.perspective); if (camera.perspective) { float fovDegress = camera.fov / M_PI * 180.0f; if (ImGui::SliderFloat("FOV", &fovDegress, 1.0f, 180.0f)) { camera.fov = fovDegress / 180.0f * M_PI; } } else { if (ImGui::InputFloat("Pixels per meter", &camera.pixelsPerMeter)) { camera.pixelsPerMeter = std::max(camera.pixelsPerMeter, 0.0f); } } ImGui::Unindent(); // ^^^ Camera settings (per instance) // vvv Camera control settings ImGui::TextUnformatted("Camera controls:"); ImGui::Indent(); ImGui::Checkbox("Move camera with WASD", &mMoveCamKeyboard); ImGui::SliderFloat("Slide speed", &mMoveCamSlideSpeed, 0.1f, 10.0f); ImGui::Checkbox("Move camera with scoll wheel", &mMoveCamScrollWheel); ImGui::SliderFloat("Scroll speed", &mMoveCamScrollSpeed, 0.01, 10.0f); ImGui::Unindent(); // ^^^ Camera control settings } // TOOD move resource-specific and gameobject-specific inspector code into attachments mechanism void EditorInstance::ShowInspector(IresObject* ires) { ires->ShowEditor(*this); } void EditorInstance::ShowInspector(LevelManager::LoadableObject* ldObj) { using namespace Tags; using namespace ProjectBrussel_UNITY_ID; ImGui::InputText("Name", &ldObj->name); ImGui::InputTextMultiline("Desciption", &ldObj->description); // TODO level object explorer } void EditorInstance::ShowInspector(GameObject* object) { using namespace Tags; using namespace ProjectBrussel_UNITY_ID; auto& io = ImGui::GetIO(); auto objectEa = static_cast(object->GetEditorAttachment()); auto ShowInspector = [&](/*array[6]*/ float* bounds = nullptr) { glm::mat4 identityMatrix(1.00f); if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_T)) mGuizmo.currOperation = ImGuizmo::TRANSLATE; if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_R)) mGuizmo.currOperation = ImGuizmo::ROTATE; if (io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_S)) mGuizmo.currOperation = ImGuizmo::SCALE; if (ImGui::RadioButton("Translate", mGuizmo.currOperation == ImGuizmo::TRANSLATE)) mGuizmo.currOperation = ImGuizmo::TRANSLATE; ImGui::SameLine(); if (ImGui::RadioButton("Rotate", mGuizmo.currOperation == ImGuizmo::ROTATE)) mGuizmo.currOperation = ImGuizmo::ROTATE; ImGui::SameLine(); if (ImGui::RadioButton("Scale", mGuizmo.currOperation == ImGuizmo::SCALE)) mGuizmo.currOperation = ImGuizmo::SCALE; ImGui::SameLine(); if (ImGui::RadioButton("Universal", mGuizmo.currOperation == ImGuizmo::UNIVERSAL)) mGuizmo.currOperation = ImGuizmo::UNIVERSAL; if (mGuizmo.currOperation != ImGuizmo::SCALE) { if (ImGui::RadioButton("Local", mGuizmo.currMode == ImGuizmo::LOCAL)) mGuizmo.currMode = ImGuizmo::LOCAL; ImGui::SameLine(); if (ImGui::RadioButton("World", mGuizmo.currMode == ImGuizmo::WORLD)) mGuizmo.currMode = ImGuizmo::WORLD; } ImGui::Checkbox("##UseSnap", &mGuizmo.useSnap); ImGui::SameLine(); switch (mGuizmo.currOperation) { case ImGuizmo::TRANSLATE: ImGui::InputFloat3("Pos Snap", &mGuizmo.snap[0]); break; case ImGuizmo::ROTATE: ImGui::InputFloat("Angle Snap", &mGuizmo.snap[0]); break; case ImGuizmo::SCALE: ImGui::InputFloat("Scale Snap", &mGuizmo.snap[0]); break; default: break; } if (bounds) { ImGui::Checkbox("Bound Sizing", &mGuizmo.boundSizing); if (mGuizmo.boundSizing) { ImGui::PushID(3); ImGui::Checkbox("##BoundSizing", &mGuizmo.boundSizingSnap); ImGui::SameLine(); ImGui::InputFloat3("Bound Snap", mGuizmo.boundsSnap); ImGui::PopID(); } } ImGui::Separator(); auto objectPos = object->GetPos(); if (ImGui::InputFloat3("Translation", &objectPos.x)) { object->SetPos(objectPos); } auto objectRot = object->GetRotation(); if (ImGui::InputFloat3("Rotation", &objectEa->eulerAnglesRotation.x)) { objectRot = CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation); object->SetRotation(objectRot); } auto objectScale = object->GetScale(); if (ImGui::InputFloat3("Scale", &objectScale.x)) { object->SetScale(objectScale); } }; auto ShowGuizmo = [&](/*array[6]*/ float* bounds = nullptr) { glm::mat4 identityMatrix(1.00f); auto& lastFrameInfo = mApp->GetWorldRenderer()->GetLastFrameInfo(); auto& cameraViewMat = lastFrameInfo.matrixView; const float* cameraView = &cameraViewMat[0][0]; auto& cameraProjMat = lastFrameInfo.matrixProj; const float* cameraProj = &cameraProjMat[0][0]; auto objectPos = object->GetPos(); auto objectScale = object->GetScale(); float matrix[16]; ImGuizmo::RecomposeMatrixFromComponents(&objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x, matrix); bool edited = ImGuizmo::Manipulate( cameraView, cameraProj, mGuizmo.currOperation, mGuizmo.currMode, matrix, nullptr, mGuizmo.useSnap ? mGuizmo.snap : nullptr, mGuizmo.boundSizing ? bounds : nullptr, (bounds && mGuizmo.boundSizingSnap) ? mGuizmo.boundsSnap : nullptr); if (edited) { ImGuizmo::DecomposeMatrixToComponents(matrix, &objectPos.x, &objectEa->eulerAnglesRotation.x, &objectScale.x); object->SetPos(objectPos); object->SetRotation(CalcQuaternionFromDegreesEulerAngle(objectEa->eulerAnglesRotation)); object->SetScale(objectScale); } }; auto type = object->GetKind(); switch (type) { case GameObject::KD_Player: { ShowInspector(); ShowGuizmo(); ImGui::Separator(); auto player = static_cast(object); auto ea = static_cast(objectEa); auto& kb = player->keybinds; ImGui::Text("Player #%d", player->GetId()); ImGui::TextUnformatted("Spritesheet: "); ImGui::SameLine(); IresObject::ShowReferenceSafe(*this, ea->confSprite.Get()); if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Spritesheet).data())) { auto spritesheet = *static_cast(payload->Data); ea->confSprite.Attach(spritesheet); auto def = spritesheet->GetInstance(); player->sprite.SetDefinition(def); player->renderObject.autofill_TextureAtlas.Attach(def->GetAtlas()); } ImGui::EndDragDropTarget(); } ImGui::TextUnformatted("Material: "); ImGui::SameLine(); IresObject::ShowReferenceSafe(*this, ea->confMaterial.Get()); if (ImGui::BeginDragDropTarget()) { if (auto payload = ImGui::AcceptDragDropPayload(IresObject::ToString(IresObject::KD_Material).data())) { auto material = *static_cast(payload->Data); ea->confMaterial.Attach(material); player->SetMaterial(material->GetInstance()); } ImGui::EndDragDropTarget(); } if (ImGui::Button("Load config")) { bool success = player->LoadFromFile(); if (success) { ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully loaded player config")); } } ImGui::SameLine(); if (ImGui::Button("Save config")) { bool success = player->SaveToFile(); if (success) { ImGui::AddNotification(ImGuiToast(ImGuiToastType_Success, "Successfully saved player config")); } } ImGui::Text("Move left (%s)", ImGui::GetKeyNameGlfw(kb.keyLeft)); ImGui::SameLine(); if (ImGui::Button("Change##Move left")) { PushKeyCodeRecorder(mApp, &kb.keyLeft, &kb.pressedLeft); } ImGui::Text("Move right (%s)", ImGui::GetKeyNameGlfw(kb.keyRight)); ImGui::SameLine(); if (ImGui::Button("Change##Move right")) { PushKeyCodeRecorder(mApp, &kb.keyRight, &kb.pressedRight); } ImGui::Text("Jump (%s)", ImGui::GetKeyNameGlfw(kb.keyJump)); ImGui::SameLine(); if (ImGui::Button("Change##Jump")) { PushKeyCodeRecorder(mApp, &kb.keyJump, &kb.pressedJump); } ImGui::Text("Attack (%s)", ImGui::GetKeyNameGlfw(kb.keyAttack)); ImGui::SameLine(); if (ImGui::Button("Change##Attack")) { PushKeyCodeRecorder(mApp, &kb.keyAttack, &kb.pressedAttack); } } break; case GameObject::KD_SimpleGeometry: { auto sg = static_cast(object); float bounds[6] = { /*x1*/ -sg->GetSize().x / 2.0f, /*y1*/ -sg->GetSize().y / 2.0f, /*z1*/ -sg->GetSize().z / 2.0f, /*x2*/ +sg->GetSize().x / 2.0f, /*y2*/ +sg->GetSize().y / 2.0f, /*z2*/ +sg->GetSize().z / 2.0f, }; ShowInspector(bounds); ShowGuizmo(bounds); ImGui::Separator(); sg->SetSize(glm::vec3(bounds[3] - bounds[0], bounds[4] - bounds[1], bounds[5] - bounds[2])); auto size = sg->GetSize(); if (ImGui::InputFloat3("Size", &size.x)) { sg->SetSize(size); } auto xFaceColor = sg->GetXFaceColor(); if (ImGui::ColorEdit4("X color", &xFaceColor)) { sg->SetXFaceColor(xFaceColor); } auto yFaceColor = sg->GetYFaceColor(); if (ImGui::ColorEdit4("Y color", &yFaceColor)) { sg->SetYFaceColor(yFaceColor); } auto zFaceColor = sg->GetZFaceColor(); if (ImGui::ColorEdit4("Z color", &zFaceColor)) { sg->SetZFaceColor(zFaceColor); } if (ImGui::Button("Sync from X color")) { sg->SetYFaceColor(xFaceColor); sg->SetZFaceColor(xFaceColor); } ImGui::SameLine(); if (ImGui::Button("Reset")) { sg->SetXFaceColor(kXAxisColor); sg->SetYFaceColor(kYAxisColor); sg->SetZFaceColor(kZAxisColor); } } break; case GameObject::KD_Building: { ShowInspector(); ShowGuizmo(); ImGui::Separator(); auto b = static_cast(object); // TODO } break; case GameObject::KD_LevelWrapper: { ShowInspector(); ShowGuizmo(); ImGui::Separator(); auto lwo = static_cast(object); // TODO } break; default: { ShowInspector(); ShowGuizmo(); } break; } } void EditorInstance::OpenSpriteViewer(SpriteDefinition* sprite) { mSpriteView_Instance.Attach(sprite); mSpriteView_Frame = 0; mSpriteView_OpenNextFrame = true; } void EditorInstance::ShowSpriteViewer() { if (mSpriteView_Instance == nullptr) return; if (!mSpriteView_Instance->IsValid()) return; if (mSpriteView_OpenNextFrame) { mSpriteView_OpenNextFrame = false; ImGui::OpenPopup("Sprite Viewer"); } bool windowOpen = true; if (ImGui::BeginPopupModal("Sprite Viewer", &windowOpen)) { auto atlas = mSpriteView_Instance->GetAtlas(); auto atlasSize = atlas->GetInfo().size; auto& frames = mSpriteView_Instance->GetFrames(); int frameCount = mSpriteView_Instance->GetFrames().size(); if (ImGui::Button("<")) { --mSpriteView_Frame; } ImGui::SameLine(); ImGui::Text("%d/%d", mSpriteView_Frame + 1, frameCount); ImGui::SameLine(); if (ImGui::Button(">")) { ++mSpriteView_Frame; } // Scrolling down (negative value) should advance, so invert the sign mSpriteView_Frame += -std::round(ImGui::GetIO().MouseWheel); // Clamp mSpriteView_Frame to range [0, frameCount) if (mSpriteView_Frame < 0) { mSpriteView_Frame = frameCount - 1; } else if (mSpriteView_Frame >= frameCount) { mSpriteView_Frame = 0; } auto& currFrame = frames[mSpriteView_Frame]; auto boundingBox = mSpriteView_Instance->GetBoundingBox(); ImGui::Text("Frame size: (%d, %d)", boundingBox.x, boundingBox.y); ImGui::Text("Frame location: (%f, %f) to (%f, %f)", currFrame.u0, currFrame.v0, currFrame.u1, currFrame.v1); ImGui::Image( (ImTextureID)(uintptr_t)atlas->GetHandle(), Utils::FitImage(atlasSize), ImVec2(currFrame.u0, currFrame.v0), ImVec2(currFrame.u1, currFrame.v1)); ImGui::EndPopup(); } }