From 206e021a51c0b885742ee4b7263622aff0c0f65b Mon Sep 17 00:00:00 2001 From: Fredrik Lindahl Date: Fri, 15 Mar 2024 14:16:07 +0100 Subject: [PATCH] Improved the new translation feature in the selection tool. Drag entities along the XZ plane and hold ALT to translate along Y. --- toolkit/editor/editor/tools/camera.cc | 2 +- toolkit/editor/editor/tools/camera.h | 2 +- toolkit/editor/editor/tools/selectiontool.cc | 169 ++++++++++++++++--- toolkit/editor/editor/tools/selectiontool.h | 17 +- 4 files changed, 162 insertions(+), 28 deletions(-) diff --git a/toolkit/editor/editor/tools/camera.cc b/toolkit/editor/editor/tools/camera.cc index 070f8fec9..35a688918 100644 --- a/toolkit/editor/editor/tools/camera.cc +++ b/toolkit/editor/editor/tools/camera.cc @@ -97,7 +97,7 @@ Camera::Update() this->freeCamUtil.SetMouseMovement({ -io.MouseDelta.x, -io.MouseDelta.y }); this->freeCamUtil.SetAccelerateButton(io.KeyShift); - this->freeCamUtil.SetRotateButton(io.MouseDown[Input::MouseButton::LeftButton]); + this->freeCamUtil.SetRotateButton(io.MouseDown[Input::MouseButton::RightButton]); this->freeCamUtil.Update(); switch (this->cameraMode) diff --git a/toolkit/editor/editor/tools/camera.h b/toolkit/editor/editor/tools/camera.h index fca8d2a3a..98a7e3f54 100644 --- a/toolkit/editor/editor/tools/camera.h +++ b/toolkit/editor/editor/tools/camera.h @@ -68,7 +68,7 @@ class Camera SizeT screenWidth = 0; SizeT screenHeight = 0; - CameraMode cameraMode = CameraMode::ORBIT; + CameraMode cameraMode = CameraMode::FREECAM; ProjectionMode projectionMode = ProjectionMode::PERSPECTIVE; float zoomIn = 0.0f; float zoomOut = 0.0f; diff --git a/toolkit/editor/editor/tools/selectiontool.cc b/toolkit/editor/editor/tools/selectiontool.cc index 6fa34d0c6..7441f7121 100644 --- a/toolkit/editor/editor/tools/selectiontool.cc +++ b/toolkit/editor/editor/tools/selectiontool.cc @@ -56,7 +56,7 @@ SelectionTool::Update(Math::vec2 const& viewPortPosition, Math::vec2 const& view Ptr mouse = Input::InputServer::Instance()->GetDefaultMouse(); Ptr keyboard = Input::InputServer::Instance()->GetDefaultKeyboard(); - bool performPicking = mouse->ButtonUp(Input::MouseButton::Code::LeftButton) && !state.translation.isTransforming; + bool performPicking = mouse->ButtonUp(Input::MouseButton::Code::LeftButton) && state.picking.pauseCounter == 0; SelectionMode mode = keyboard->KeyPressed(Input::Key::LeftControl) ? SelectionMode::Append : SelectionMode::Replace; if (performPicking) @@ -166,10 +166,30 @@ SelectionTool::RenderGizmo(Math::vec2 const& viewPortPosition, Math::vec2 const& Ptr mouse = Input::InputServer::Instance()->GetDefaultMouse(); Ptr keyboard = Input::InputServer::Instance()->GetDefaultKeyboard(); - // TODO: Only start translating if the mouse has moved more that a small distance - // Store mouse pos on button down, then check for > 4 pixel diff, then start transforming if button pressed. - state.translation.isTransforming = mouse->ButtonPressed(Input::MouseButton::Code::LeftButton); + if (mouse->ButtonDown(Input::MouseButton::Code::LeftButton)) + { + state.translation.mousePosOnStart = mouse->GetScreenPosition(); + } + + if (!state.translation.dragTimer.Running() && state.translation.pickingPaused) + { + state.translation.pickingPaused = false; + state.picking.pauseCounter--; + } + // Only start translating if the mouse has moved more that a small distance + if (!state.translation.dragTimer.Running()) + { + float mouseDistance = (mouse->GetScreenPosition() - state.translation.mousePosOnStart).length(); + if (mouseDistance > 0.0015f && mouse->ButtonPressed(Input::MouseButton::Code::LeftButton)) + { + state.translation.dragTimer.Reset(); + state.translation.dragTimer.Start(); + state.picking.pauseCounter++; + state.translation.pickingPaused = true; + } + } + Math::vec2 mousePos = mouse->GetScreenPosition(); mousePos -= viewPortPosition; mousePos = {mousePos.x / viewPortSize.x, mousePos.y / viewPortSize.y}; @@ -179,32 +199,73 @@ SelectionTool::RenderGizmo(Math::vec2 const& viewPortPosition, Math::vec2 const& Math::line ray = RenderUtil::MouseRayUtil::ComputeWorldMouseRay(mousePos, 10000, camTransform, invProj, 0.01f); - if (state.translation.isTransforming && !state.translation.isDirty) + if (state.translation.dragTimer.Running() && !state.translation.isDirty) { - state.translation.isDirty = true; - - // TODO: get the entity that is under the mouse and base the plane on it's center instead of the last selected. - Game::Position entityPos = Editor::state.editorWorld->GetComponent(state.selection.Back()); - state.translation.plane = Math::plane(entityPos, Math::vector(0, 1, 0)); - Math::point startPos; - if (state.translation.plane.intersect(ray, startPos)) + state.translation.originEntity = GetSelectedEntityUnderMouse(viewPortPosition, viewPortSize, camera); + if (state.translation.originEntity == Editor::Entity::Invalid()) { - state.translation.startPos = startPos.vec; + // failed to find entity. Cancel translation attempt + state.translation.dragTimer.Stop(); } else { - // failed to find plane. Cancel translation attempt - state.translation.isTransforming = false; - state.translation.isDirty = false; + Game::Position entityPos = Editor::state.editorWorld->GetComponent(state.translation.originEntity); + state.translation.plane = Math::plane(entityPos, Math::vector(0, 1, 0)); + Math::point startPos; + if (state.translation.plane.intersect(ray, startPos)) + { + state.translation.startPos = startPos.vec; + state.translation.isDirty = true; + } + else + { + // failed to find plane. Cancel translation attempt + state.translation.dragTimer.Stop(); + } } } - if (state.translation.isTransforming) + bool const applyTransform = mouse->ButtonUp(Input::MouseButton::Code::LeftButton) && state.translation.dragTimer.Running(); + if (applyTransform) + { + state.translation.dragTimer.Stop(); + } + + if (state.translation.dragTimer.Running()) { - Math::point mousePosOnWorldPlane; - if (state.translation.plane.intersect(ray, mousePosOnWorldPlane)) + bool xzAxis = !keyboard->KeyPressed(Input::Key::LeftMenu); + + if (xzAxis) + { + Math::point mousePosOnWorldPlane; + if (state.translation.plane.intersect(ray, mousePosOnWorldPlane)) + { + state.translation.delta = mousePosOnWorldPlane.vec - state.translation.startPos; + } + } + else // y axis translation { - state.translation.delta = mousePosOnWorldPlane.vec - state.translation.startPos; + // find a good plane + Game::Position gameEntityPos = defaultWorld->GetComponent(Editor::state.editables[state.translation.originEntity.index].gameEntity); + + Math::vector xDir = Math::vector(1, 0, 0); + Math::vector zDir = Math::vector(0, 0, 1); + float px = Math::abs(Math::dot(ray.m, xDir)); + float pz = Math::abs(Math::dot(ray.m, zDir)); + + Math::vector planeNormal; + if (px > pz) + planeNormal = xDir; + else + planeNormal = zDir; + + Math::plane plane = Math::plane(gameEntityPos, planeNormal); + Math::point mousePosOnWorldPlane; + if (plane.intersect(ray, mousePosOnWorldPlane)) + { + state.translation.delta.y = (mousePosOnWorldPlane.vec - state.translation.startPos).y; + state.translation.plane = Math::plane(state.translation.startPos + state.translation.delta, Math::vector(0, 1, 0)); + } } for (IndexT i = 0; i < state.selection.Size(); i++) @@ -219,8 +280,9 @@ SelectionTool::RenderGizmo(Math::vec2 const& viewPortPosition, Math::vec2 const& } else if (state.translation.isDirty) { - // TODO: Only apply the translation if the translation has happened for more than ~200ms - if (state.translation.delta != Math::vec3(0)) + // Only apply the translation if the translation has happened for more than some threshold duration + constexpr float minDragTime = 0.1f; + if (state.translation.delta != Math::vec3(0) && state.translation.dragTimer.GetTime() > minDragTime) { // User has release gizmo, we can set real transform and add to undo queue Edit::CommandManager::BeginMacro("Translate entities", false); @@ -231,9 +293,19 @@ SelectionTool::RenderGizmo(Math::vec2 const& viewPortPosition, Math::vec2 const& Edit::SetComponent(state.selection[i], Game::GetComponentId(), &pos); } Edit::CommandManager::EndMacro(); - state.translation.isTransforming = false; state.translation.isDirty = false; } + else + { + // Reset game entity position to be same as editors + for (IndexT i = 0; i < state.selection.Size(); i++) + { + Game::Position pos = Editor::state.editorWorld->GetComponent(state.selection[i]); + Game::Entity const gameEntity = Editor::state.editables[state.selection[i].index].gameEntity; + defaultWorld->SetComponent(gameEntity, pos); + defaultWorld->MarkAsModified(gameEntity); + } + } } for (auto const editorEntity : state.selection) @@ -256,7 +328,56 @@ SelectionTool::RenderGizmo(Math::vec2 const& viewPortPosition, Math::vec2 const& bool SelectionTool::IsTransforming() { - return state.translation.isTransforming; + return state.translation.dragTimer.Running(); +} + +//------------------------------------------------------------------------------ +/** +*/ +Editor::Entity +SelectionTool::GetSelectedEntityUnderMouse( + Math::vec2 const& viewPortPosition, Math::vec2 const& viewPortSize, Editor::Camera const* camera +) +{ + Game::World* defaultWorld = Game::GetWorld(WORLD_DEFAULT); + + float closestDistance = 1e30f; + Editor::Entity closestEntity = Editor::Entity::Invalid(); + + Ptr mouse = Input::InputServer::Instance()->GetDefaultMouse(); + Math::vec2 mousePos = mouse->GetScreenPosition(); + + mousePos -= viewPortPosition; + mousePos = {mousePos.x / viewPortSize.x, mousePos.y / viewPortSize.y}; + + Math::mat4 const camTransform = Math::inverse(camera->GetViewTransform()); + Math::mat4 const invProj = Math::inverse(camera->GetProjectionTransform()); + + Math::line ray = RenderUtil::MouseRayUtil::ComputeWorldMouseRay(mousePos, 10000, camTransform, invProj, 0.01f); + + for (IndexT i = 0; i < state.selection.Size(); i++) + { + Editor::Entity editorEntity = state.selection[i]; + Game::Entity const gameEntity = Editor::state.editables[editorEntity.index].gameEntity; + if (defaultWorld->HasComponent(gameEntity)) + { + Graphics::GraphicsEntityId const gfxEntity = + defaultWorld->GetComponent(gameEntity).graphicsEntityId; + Math::bbox const bbox = Models::ModelContext::ComputeBoundingBox(gfxEntity); + + float dist; + if (bbox.intersects(ray, dist)) + { + if (dist < closestDistance) + { + closestDistance = dist; + closestEntity = editorEntity; + } + } + } + } + + return closestEntity; } } // namespace Tools diff --git a/toolkit/editor/editor/tools/selectiontool.h b/toolkit/editor/editor/tools/selectiontool.h index 53f8906e0..892963d9c 100644 --- a/toolkit/editor/editor/tools/selectiontool.h +++ b/toolkit/editor/editor/tools/selectiontool.h @@ -32,6 +32,11 @@ class SelectionTool static void Update(Math::vec2 const& viewPortPosition, Math::vec2 const& viewPortSize, Editor::Camera const* camera); static bool IsTransforming(); + /// returns the selected entity that is directly under the mouse, or invalid if none is under. + static Editor::Entity GetSelectedEntityUnderMouse( + Math::vec2 const& viewPortPosition, Math::vec2 const& viewPortSize, Editor::Camera const* camera + ); + private: friend Edit::CMDSetSelection; @@ -50,20 +55,28 @@ class SelectionTool bool useGridIncrements = false; /// used to check if any changes should be applied to the entities bool isDirty = false; - /// will be true if the entities are being dragged around - bool isTransforming = false; /// entity translation delta when dragging entities around. Applied and reset when mouse is released Math::vec3 delta = Math::vec3(0); /// start position of the the mouse in worldspace along the XZ plane that cuts the hovered objects origin when we start translating Math::vec3 startPos; /// the current translation plane Math::plane plane; + /// position of mouse when starting translation + Math::vec2 mousePosOnStart; + /// timer that starts when the mouse starts dragging + Timing::Timer dragTimer; + /// the entity that is used to calculate translation planes. + Editor::Entity originEntity = Editor::Entity::Invalid(); + /// prolongs picking pause for one frame after entity has been dragged + bool pickingPaused = false; } translation; struct { /// entities that are omitted from being selected when picking Util::Array omittedEntities; + /// increment to disallow picking temporarily. Remember to decrement when done. + int pauseCounter = 0; } picking; };