diff --git a/Code/Editor/EditorViewportWidget.cpp b/Code/Editor/EditorViewportWidget.cpp index 7631ac6237..f009fe7602 100644 --- a/Code/Editor/EditorViewportWidget.cpp +++ b/Code/Editor/EditorViewportWidget.cpp @@ -1039,6 +1039,7 @@ void EditorViewportWidget::ConnectViewportInteractionRequestBus() { AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusConnect(GetViewportId()); AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusConnect(GetViewportId()); + AzToolsFramework::ViewportInteraction::EditorModifierKeyRequestBus::Handler::BusConnect(); m_viewportUi.ConnectViewportUiBus(GetViewportId()); AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusConnect(); @@ -1049,6 +1050,7 @@ void EditorViewportWidget::DisconnectViewportInteractionRequestBus() AzFramework::InputSystemCursorConstraintRequestBus::Handler::BusDisconnect(); m_viewportUi.DisconnectViewportUiBus(); + AzToolsFramework::ViewportInteraction::EditorModifierKeyRequestBus::Handler::BusDisconnect(); AzToolsFramework::ViewportInteraction::EditorEntityViewportInteractionRequestBus::Handler::BusDisconnect(); AzToolsFramework::ViewportInteraction::MainEditorViewportInteractionRequestBus::Handler::BusDisconnect(); } diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ActionDispatcher.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ActionDispatcher.h index 1b555a4a81..6bd72f5101 100644 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ActionDispatcher.h +++ b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ActionDispatcher.h @@ -39,6 +39,8 @@ namespace AzManipulatorTestFramework DerivedDispatcherT* MouseLButtonDown(); //! Set the left mouse button up. DerivedDispatcherT* MouseLButtonUp(); + //! Send a double click event. + DerivedDispatcherT* MouseLButtonDoubleClick(); //! Set the keyboard modifier button down. DerivedDispatcherT* KeyboardModifierDown(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier); //! Set the keyboard modifier button up. @@ -71,6 +73,7 @@ namespace AzManipulatorTestFramework virtual void CameraStateImpl(const AzFramework::CameraState& cameraState) = 0; virtual void MouseLButtonDownImpl() = 0; virtual void MouseLButtonUpImpl() = 0; + virtual void MouseLButtonDoubleClickImpl() = 0; virtual void MousePositionImpl(const AzFramework::ScreenPoint& position) = 0; virtual void KeyboardModifierDownImpl(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) = 0; virtual void KeyboardModifierUpImpl(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) = 0; @@ -167,7 +170,7 @@ namespace AzManipulatorTestFramework template DerivedDispatcherT* ActionDispatcher::MouseLButtonDown() { - Log("%s", "Mouse left button down"); + Log("Mouse left button down"); MouseLButtonDownImpl(); return static_cast(this); } @@ -175,11 +178,19 @@ namespace AzManipulatorTestFramework template DerivedDispatcherT* ActionDispatcher::MouseLButtonUp() { - Log("%s", "Mouse left button up"); + Log("Mouse left button up"); MouseLButtonUpImpl(); return static_cast(this); } + template + DerivedDispatcherT* ActionDispatcher::MouseLButtonDoubleClick() + { + Log("Mouse left button double click"); + MouseLButtonDoubleClickImpl(); + return static_cast(this); + } + template const char* ActionDispatcher::KeyboardModifierString( const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ImmediateModeActionDispatcher.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ImmediateModeActionDispatcher.h index 7a4773a37c..9e11a7543c 100644 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ImmediateModeActionDispatcher.h +++ b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ImmediateModeActionDispatcher.h @@ -58,6 +58,7 @@ namespace AzManipulatorTestFramework void CameraStateImpl(const AzFramework::CameraState& cameraState) override; void MouseLButtonDownImpl() override; void MouseLButtonUpImpl() override; + void MouseLButtonDoubleClickImpl() override; void MousePositionImpl(const AzFramework::ScreenPoint& position) override; void KeyboardModifierDownImpl(const KeyboardModifier& keyModifier) override; void KeyboardModifierUpImpl(const KeyboardModifier& keyModifier) override; diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/RetainedModeActionDispatcher.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/RetainedModeActionDispatcher.h deleted file mode 100644 index d9aceedf8a..0000000000 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/RetainedModeActionDispatcher.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace AzManipulatorTestFramework -{ - //! Buffers actions to be dispatched upon a call to Execute(). - class RetainedModeActionDispatcher - : public ActionDispatcher - { - public: - explicit RetainedModeActionDispatcher(ManipulatorViewportInteraction& viewportManipulatorInteraction); - //! Execute the sequence of actions and lock the dispatcher from adding further actions. - RetainedModeActionDispatcher* Execute(); - //! Reset the sequence of actions and unlock the dispatcher from adding further actions. - RetainedModeActionDispatcher* ResetSequence(); - - protected: - // ActionDispatcher ... - void EnableSnapToGridImpl() override; - void DisableSnapToGridImpl() override; - void GridSizeImpl(float size) override; - void CameraStateImpl(const AzFramework::CameraState& cameraState) override; - void MouseLButtonDownImpl() override; - void MouseLButtonUpImpl() override; - void MousePositionImpl(const AzFramework::ScreenPoint& position) override; - void KeyboardModifierDownImpl(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) override; - void KeyboardModifierUpImpl(const AzToolsFramework::ViewportInteraction::KeyboardModifier& keyModifier) override; - void ExpectManipulatorBeingInteractedImpl() override; - void ExpectManipulatorNotBeingInteractedImpl() override; - void SetEntityWorldTransformImpl(AZ::EntityId entityId, const AZ::Transform& transform) override; - void SetSelectedEntityImpl(AZ::EntityId entity) override; - void SetSelectedEntitiesImpl(const AzToolsFramework::EntityIdList& entities) override; - void EnterComponentModeImpl(const AZ::Uuid& uuid) override; - - private: - using Action = AZStd::function; - void AddActionToSequence(Action&& action); - ImmediateModeActionDispatcher m_dispatcher; - AZStd::list m_actions; - bool m_locked = false; - }; -} // namespace AzManipulatorTestFramework diff --git a/Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp b/Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp index 895899cf3f..23cc2a21f5 100644 --- a/Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp +++ b/Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp @@ -83,7 +83,17 @@ namespace AzManipulatorTestFramework void ImmediateModeActionDispatcher::MouseLButtonUpImpl() { GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Up; - m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*GetMouseInteractionEvent()); + m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event); + ToggleOff(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left); + // the mouse position will be the same as the previous event, thus the delta will be 0 + MouseMoveAfterButton(); + } + + void ImmediateModeActionDispatcher::MouseLButtonDoubleClickImpl() + { + GetMouseInteractionEvent()->m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::DoubleClick; + ToggleOn(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left); + m_viewportManipulatorInteraction.GetManipulatorManager().ConsumeMouseInteractionEvent(*m_event); ToggleOff(GetMouseInteractionEvent()->m_mouseInteraction.m_mouseButtons.m_mouseButtons, MouseButton::Left); // the mouse position will be the same as the previous event, thus the delta will be 0 MouseMoveAfterButton(); diff --git a/Code/Framework/AzManipulatorTestFramework/Source/RetainedModeActionDispatcher.cpp b/Code/Framework/AzManipulatorTestFramework/Source/RetainedModeActionDispatcher.cpp deleted file mode 100644 index 34ae4aaf4a..0000000000 --- a/Code/Framework/AzManipulatorTestFramework/Source/RetainedModeActionDispatcher.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) Contributors to the Open 3D Engine Project. - * For complete copyright and license terms please see the LICENSE at the root of this distribution. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - * - */ - -#include - -namespace AzManipulatorTestFramework -{ - using KeyboardModifier = AzToolsFramework::ViewportInteraction::KeyboardModifier; - - RetainedModeActionDispatcher::RetainedModeActionDispatcher( - ManipulatorViewportInteraction& viewportManipulatorInteraction) - : m_dispatcher(viewportManipulatorInteraction) - { - } - - void RetainedModeActionDispatcher::AddActionToSequence(Action&& action) - { - if (m_locked) - { - const char* error = "Couldn't add action to sequence, dispatcher is locked (you must call ResetSequence() \ - before adding actions to this dispatcher)"; - Log("%s", error); - AZ_Assert(false, "Error: %s", error); - } - - m_actions.emplace_back(action); - } - - void RetainedModeActionDispatcher::EnableSnapToGridImpl() - { - AddActionToSequence([=]() { m_dispatcher.EnableSnapToGrid(); }); - } - - void RetainedModeActionDispatcher::DisableSnapToGridImpl() - { - AddActionToSequence([=]() { m_dispatcher.DisableSnapToGrid(); }); - } - - void RetainedModeActionDispatcher::GridSizeImpl(float size) - { - AddActionToSequence([=]() { m_dispatcher.GridSize(size); }); - } - - void RetainedModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState) - { - AddActionToSequence([=]() { m_dispatcher.CameraState(cameraState); }); - } - - void RetainedModeActionDispatcher::MouseLButtonDownImpl() - { - AddActionToSequence([=]() { m_dispatcher.MouseLButtonDown(); }); - } - - void RetainedModeActionDispatcher::MouseLButtonUpImpl() - { - AddActionToSequence([=]() { m_dispatcher.MouseLButtonUp(); }); - } - - void RetainedModeActionDispatcher::MousePositionImpl(const AzFramework::ScreenPoint& position) - { - AddActionToSequence([=]() { m_dispatcher.MousePosition(position); }); - } - - void RetainedModeActionDispatcher::KeyboardModifierDownImpl(const KeyboardModifier& keyModifier) - { - AddActionToSequence([=]() { m_dispatcher.KeyboardModifierDown(keyModifier); }); - } - - void RetainedModeActionDispatcher::KeyboardModifierUpImpl(const KeyboardModifier& keyModifier) - { - AddActionToSequence([=]() { m_dispatcher.KeyboardModifierUp(keyModifier); }); - } - - void RetainedModeActionDispatcher::ExpectManipulatorBeingInteractedImpl() - { - AddActionToSequence([=]() { m_dispatcher.ExpectManipulatorBeingInteracted(); }); - } - - void RetainedModeActionDispatcher::ExpectManipulatorNotBeingInteractedImpl() - { - AddActionToSequence([=]() { m_dispatcher.ExpectManipulatorNotBeingInteracted(); }); - } - - void RetainedModeActionDispatcher::SetEntityWorldTransformImpl(AZ::EntityId entityId, const AZ::Transform& transform) - { - AddActionToSequence([=]() { m_dispatcher.SetEntityWorldTransform(entityId, transform); }); - } - - void RetainedModeActionDispatcher::SetSelectedEntityImpl(AZ::EntityId entity) - { - AddActionToSequence([=]() { m_dispatcher.SetSelectedEntity(entity); }); - } - - void RetainedModeActionDispatcher::SetSelectedEntitiesImpl(const AzToolsFramework::EntityIdList& entities) - { - AddActionToSequence([=]() { m_dispatcher.SetSelectedEntities(entities); }); - } - - void RetainedModeActionDispatcher::EnterComponentModeImpl(const AZ::Uuid& uuid) - { - AddActionToSequence([=]() { m_dispatcher.EnterComponentMode(uuid); }); - } - - RetainedModeActionDispatcher* RetainedModeActionDispatcher::ResetSequence() - { - Log("%s", "Resetting the action sequence"); - m_actions.clear(); - m_dispatcher.ResetEvent(); - m_locked = false; - return this; - } - - RetainedModeActionDispatcher* RetainedModeActionDispatcher::Execute() - { - Log("Executing %u actions", m_actions.size()); - for (auto& action : m_actions) - { - action(); - } - m_dispatcher.ResetEvent(); - m_locked = true; - return this; - } -} // namespace AzManipulatorTestFramework diff --git a/Code/Framework/AzManipulatorTestFramework/azmanipulatortestframework_files.cmake b/Code/Framework/AzManipulatorTestFramework/azmanipulatortestframework_files.cmake index 3fe9f4266c..9f480a3e2a 100644 --- a/Code/Framework/AzManipulatorTestFramework/azmanipulatortestframework_files.cmake +++ b/Code/Framework/AzManipulatorTestFramework/azmanipulatortestframework_files.cmake @@ -14,12 +14,10 @@ set(FILES Include/AzManipulatorTestFramework/DirectManipulatorViewportInteraction.h Include/AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h Include/AzManipulatorTestFramework/ImmediateModeActionDispatcher.h - Include/AzManipulatorTestFramework/RetainedModeActionDispatcher.h Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h Source/ViewportInteraction.cpp Source/DirectManipulatorViewportInteraction.cpp Source/IndirectManipulatorViewportInteraction.cpp Source/ImmediateModeActionDispatcher.cpp - Source/RetainedModeActionDispatcher.cpp Source/AzManipulatorTestFrameworkUtils.cpp ) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp index 3db8492d70..5a7b097e29 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp @@ -1770,6 +1770,12 @@ namespace AzToolsFramework return false; } + void EditorTransformComponentSelection::ChangeSelectedEntity(const AZ::EntityId entityId) + { + DeselectEntities(); + SelectDeselect(entityId); + } + bool EditorTransformComponentSelection::HandleMouseInteraction(const ViewportInteraction::MouseInteractionEvent& mouseInteraction) { AZ_PROFILE_FUNCTION(AzToolsFramework); @@ -1821,202 +1827,239 @@ namespace AzToolsFramework return true; } - // double click to deselect all - if (Input::DeselectAll(mouseInteraction)) + if (ed_viewportStickySelect) { - // note: even if m_selectedEntityIds is technically empty, we - // may still have an entity selected that was clicked in the - // entity outliner - we still want to make sure the deselect all - // action clears the selection - DeselectEntities(); - return false; + // double click to deselect all + if (Input::DeselectAll(mouseInteraction)) + { + // note: even if m_selectedEntityIds is technically empty, we + // may still have an entity selected that was clicked in the + // entity outliner - we still want to make sure the deselect all + // action clears the selection + DeselectEntities(); + return false; + } + } + + // select/deselect (add/remove) entities with ctrl held + if (Input::AdditiveIndividualSelect(clickOutcome, mouseInteraction)) + { + if (SelectDeselect(entityIdUnderCursor)) + { + if (m_selectedEntityIds.empty()) + { + m_pivotOverrideFrame.Reset(); + } + + return false; + } } if (!m_selectedEntityIds.empty()) { - // select/deselect (add/remove) entities with ctrl held - if (Input::AdditiveIndividualSelect(clickOutcome, mouseInteraction)) - { - if (SelectDeselect(entityIdUnderCursor)) - { - if (m_selectedEntityIds.empty()) - { - m_pivotOverrideFrame.Reset(); - } - - return false; - } - } - // group copying/alignment to specific entity - 'ditto' position/orientation for group - if (Input::GroupDitto(mouseInteraction)) + if (Input::GroupDitto(mouseInteraction) && PerformGroupDitto(entityIdUnderCursor)) { - if (entityIdUnderCursor.IsValid()) - { - AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); - AZ::TransformBus::EventResult(worldFromLocal, entityIdUnderCursor, &AZ::TransformBus::Events::GetWorldTM); - - switch (m_mode) - { - case Mode::Rotation: - CopyOrientationToSelectedEntitiesGroup(QuaternionFromTransformNoScaling(worldFromLocal)); - break; - case Mode::Scale: - CopyScaleToSelectedEntitiesIndividualWorld(worldFromLocal.GetUniformScale()); - break; - case Mode::Translation: - CopyTranslationToSelectedEntitiesGroup(worldFromLocal.GetTranslation()); - break; - default: - // do nothing - break; - } - - return false; - } + return false; } // individual copying/alignment to specific entity - 'ditto' position/orientation for individual - if (Input::IndividualDitto(mouseInteraction)) + if (Input::IndividualDitto(mouseInteraction) && PerformIndividualDitto(entityIdUnderCursor)) { - if (entityIdUnderCursor.IsValid()) - { - AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); - AZ::TransformBus::EventResult(worldFromLocal, entityIdUnderCursor, &AZ::TransformBus::Events::GetWorldTM); - - switch (m_mode) - { - case Mode::Rotation: - CopyOrientationToSelectedEntitiesIndividual(QuaternionFromTransformNoScaling(worldFromLocal)); - break; - case Mode::Scale: - CopyScaleToSelectedEntitiesIndividualWorld(worldFromLocal.GetUniformScale()); - break; - case Mode::Translation: - CopyTranslationToSelectedEntitiesIndividual(worldFromLocal.GetTranslation()); - break; - default: - // do nothing - break; - } - - return false; - } + return false; } // try snapping to the terrain (if in Translation mode) and entity wasn't picked if (Input::SnapTerrain(mouseInteraction)) { - for (AZ::EntityId entityId : m_selectedEntityIds) - { - ScopedUndoBatch::MarkEntityDirty(entityId); - } - - if (m_mode == Mode::Translation) - { - const AZ::Vector3 finalSurfacePosition = PickTerrainPosition(mouseInteraction.m_mouseInteraction); - - // handle modifier alternatives - if (Input::IndividualDitto(mouseInteraction)) - { - CopyTranslationToSelectedEntitiesIndividual(finalSurfacePosition); - } - else if (Input::GroupDitto(mouseInteraction)) - { - CopyTranslationToSelectedEntitiesGroup(finalSurfacePosition); - } - } - else if (m_mode == Mode::Rotation) - { - // handle modifier alternatives - if (Input::IndividualDitto(mouseInteraction)) - { - CopyOrientationToSelectedEntitiesIndividual(AZ::Quaternion::CreateIdentity()); - } - else if (Input::GroupDitto(mouseInteraction)) - { - CopyOrientationToSelectedEntitiesGroup(AZ::Quaternion::CreateIdentity()); - } - } - + PerformSnapToTerrain(mouseInteraction); return false; } // set manipulator pivot override translation or orientation (update manipulators) if (Input::ManipulatorDitto(mouseInteraction)) { - if (m_entityIdManipulators.m_manipulators) - { - ScopedUndoBatch undoBatch(s_dittoManipulatorUndoRedoDesc); - - auto manipulatorCommand = - AZStd::make_unique(CreateManipulatorCommandStateFromSelf(), s_manipulatorUndoRedoName); - - if (entityIdUnderCursor.IsValid()) - { - AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); - AZ::TransformBus::EventResult(worldFromLocal, entityIdUnderCursor, &AZ::TransformBus::Events::GetWorldTM); - - // set orientation/translation to match picked entity - switch (m_mode) - { - case Mode::Rotation: - OverrideManipulatorOrientation(QuaternionFromTransformNoScaling(worldFromLocal)); - break; - case Mode::Translation: - OverrideManipulatorTranslation(worldFromLocal.GetTranslation()); - break; - case Mode::Scale: - // do nothing - break; - default: - break; - } - - // only update pivot override when in translation or rotation mode - switch (m_mode) - { - case Mode::Rotation: - m_pivotOverrideFrame.m_pickTypes |= OptionalFrame::PickType::Orientation; - [[fallthrough]]; - case Mode::Translation: - m_pivotOverrideFrame.m_pickTypes |= OptionalFrame::PickType::Translation; - m_pivotOverrideFrame.m_pickedEntityIdOverride = entityIdUnderCursor; - break; - case Mode::Scale: - // do nothing - break; - default: - break; - } - } - else - { - // match the same behavior as if we pressed Ctrl+R to reset the manipulator - DelegateClearManipulatorOverride(); - } - - manipulatorCommand->SetManipulatorAfter(EntityManipulatorCommand::State( - BuildPivotOverride(m_pivotOverrideFrame.HasTranslationOverride(), m_pivotOverrideFrame.HasOrientationOverride()), - m_entityIdManipulators.m_manipulators->GetLocalTransform(), entityIdUnderCursor)); - - manipulatorCommand->SetParent(undoBatch.GetUndoBatch()); - manipulatorCommand.release(); - } + PerformManipulatorDitto(entityIdUnderCursor); + return false; } - return false; + if (ed_viewportStickySelect) + { + return false; + } } // standard toggle selection if (Input::IndividualSelect(clickOutcome)) { - SelectDeselect(entityIdUnderCursor); + if (!ed_viewportStickySelect) + { + ChangeSelectedEntity(entityIdUnderCursor); + } + else + { + SelectDeselect(entityIdUnderCursor); + } } return false; } + bool EditorTransformComponentSelection::PerformGroupDitto(const AZ::EntityId entityId) + { + if (entityId.IsValid()) + { + AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); + AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM); + + switch (m_mode) + { + case Mode::Rotation: + CopyOrientationToSelectedEntitiesGroup(QuaternionFromTransformNoScaling(worldFromLocal)); + break; + case Mode::Scale: + CopyScaleToSelectedEntitiesIndividualWorld(worldFromLocal.GetUniformScale()); + break; + case Mode::Translation: + CopyTranslationToSelectedEntitiesGroup(worldFromLocal.GetTranslation()); + break; + default: + // do nothing + break; + } + + return true; + } + + return false; + } + + bool EditorTransformComponentSelection::PerformIndividualDitto(const AZ::EntityId entityId) + { + if (entityId.IsValid()) + { + AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); + AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM); + + switch (m_mode) + { + case Mode::Rotation: + CopyOrientationToSelectedEntitiesIndividual(QuaternionFromTransformNoScaling(worldFromLocal)); + break; + case Mode::Scale: + CopyScaleToSelectedEntitiesIndividualWorld(worldFromLocal.GetUniformScale()); + break; + case Mode::Translation: + CopyTranslationToSelectedEntitiesIndividual(worldFromLocal.GetTranslation()); + break; + default: + // do nothing + break; + } + + return true; + } + + return false; + } + + void EditorTransformComponentSelection::PerformSnapToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction) + { + for (AZ::EntityId entityId : m_selectedEntityIds) + { + ScopedUndoBatch::MarkEntityDirty(entityId); + } + + if (m_mode == Mode::Translation) + { + const AZ::Vector3 finalSurfacePosition = PickTerrainPosition(mouseInteraction.m_mouseInteraction); + + // handle modifier alternatives + if (Input::IndividualDitto(mouseInteraction)) + { + CopyTranslationToSelectedEntitiesIndividual(finalSurfacePosition); + } + else if (Input::GroupDitto(mouseInteraction)) + { + CopyTranslationToSelectedEntitiesGroup(finalSurfacePosition); + } + } + else if (m_mode == Mode::Rotation) + { + // handle modifier alternatives + if (Input::IndividualDitto(mouseInteraction)) + { + CopyOrientationToSelectedEntitiesIndividual(AZ::Quaternion::CreateIdentity()); + } + else if (Input::GroupDitto(mouseInteraction)) + { + CopyOrientationToSelectedEntitiesGroup(AZ::Quaternion::CreateIdentity()); + } + } + } + + void EditorTransformComponentSelection::PerformManipulatorDitto(const AZ::EntityId entityId) + { + if (m_entityIdManipulators.m_manipulators) + { + ScopedUndoBatch undoBatch(s_dittoManipulatorUndoRedoDesc); + + auto manipulatorCommand = + AZStd::make_unique(CreateManipulatorCommandStateFromSelf(), s_manipulatorUndoRedoName); + + if (entityId.IsValid()) + { + AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); + AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM); + + // set orientation/translation to match picked entity + switch (m_mode) + { + case Mode::Rotation: + OverrideManipulatorOrientation(QuaternionFromTransformNoScaling(worldFromLocal)); + break; + case Mode::Translation: + OverrideManipulatorTranslation(worldFromLocal.GetTranslation()); + break; + case Mode::Scale: + // do nothing + break; + default: + break; + } + + // only update pivot override when in translation or rotation mode + switch (m_mode) + { + case Mode::Rotation: + m_pivotOverrideFrame.m_pickTypes |= OptionalFrame::PickType::Orientation; + [[fallthrough]]; + case Mode::Translation: + m_pivotOverrideFrame.m_pickTypes |= OptionalFrame::PickType::Translation; + m_pivotOverrideFrame.m_pickedEntityIdOverride = entityId; + break; + case Mode::Scale: + // do nothing + break; + default: + break; + } + } + else + { + // match the same behavior as if we pressed Ctrl+R to reset the manipulator + DelegateClearManipulatorOverride(); + } + + manipulatorCommand->SetManipulatorAfter(EntityManipulatorCommand::State( + BuildPivotOverride(m_pivotOverrideFrame.HasTranslationOverride(), m_pivotOverrideFrame.HasOrientationOverride()), + m_entityIdManipulators.m_manipulators->GetLocalTransform(), entityId)); + + manipulatorCommand->SetParent(undoBatch.GetUndoBatch()); + manipulatorCommand.release(); + } + } + template static void AddAction( AZStd::vector>& actions, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h index 35d3587d0b..478c0b775e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h @@ -207,6 +207,7 @@ namespace AzToolsFramework void SetSelectedEntities(const EntityIdList& entityIds); void DeselectEntities(); bool SelectDeselect(AZ::EntityId entityId); + void ChangeSelectedEntity(AZ::EntityId entityId); void RefreshSelectedEntityIds(); void RefreshSelectedEntityIds(const EntityIdList& selectedEntityIds); @@ -298,6 +299,11 @@ namespace AzToolsFramework void SetEntityLocalRotation(AZ::EntityId entityId, const AZ::Vector3& localRotation); void SetEntityLocalRotation(AZ::EntityId entityId, const AZ::Quaternion& localRotation); + bool PerformGroupDitto(AZ::EntityId entityId); + bool PerformIndividualDitto(AZ::EntityId entityId); + void PerformManipulatorDitto(AZ::EntityId entityId); + void PerformSnapToTerrain(const ViewportInteraction::MouseInteractionEvent& mouseInteraction); + //! Responsible for keeping the space cluster in sync with the current reference frame. void UpdateSpaceCluster(ReferenceFrame referenceFrame); diff --git a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp index 8a958af567..241ee3f576 100644 --- a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp @@ -614,7 +614,7 @@ namespace UnitTest using EditorTransformComponentSelectionViewportPickingManipulatorTestFixture = IndirectCallManipulatorViewportInteractionFixtureMixin; - TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, SingleClickWithNoSelectionWillSelectEntity) + TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, StickySingleClickWithNoSelectionWillSelectEntity) { AzToolsFramework::ed_viewportStickySelect = true; @@ -637,19 +637,44 @@ namespace UnitTest EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); } - TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, SingleClickOffEntityWithSelectionWillNotDeselectEntity) + TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, UnstickySingleClickWithNoSelectionWillSelectEntity) + { + AzToolsFramework::ed_viewportStickySelect = false; + + PositionEntities(); + PositionCamera(m_cameraState); + + using ::testing::Eq; + auto selectedEntitiesBefore = SelectedEntities(); + EXPECT_TRUE(selectedEntitiesBefore.empty()); + + // calculate the position in screen space of the initial entity position + const auto entity1ScreenPosition = AzFramework::WorldToScreen(m_entity1WorldTranslation, m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState)->MousePosition(entity1ScreenPosition)->MouseLButtonDown()->MouseLButtonUp(); + + // entity is selected + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter.size(), Eq(1)); + EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + StickySingleClickOffEntityWithSelectionWillNotDeselectEntity) { AzToolsFramework::ed_viewportStickySelect = true; PositionEntities(); PositionCamera(m_cameraState); - // position in space above the entity + // position in space above the entities const auto clickOffPositionWorld = AZ::Vector3(5.0f, 15.0f, 12.0f); AzToolsFramework::SelectEntity(m_entityId1); - // calculate the position in screen space of the initial position of the entity + // calculate the screen space position of the click const auto clickOffPositionScreen = AzFramework::WorldToScreen(clickOffPositionWorld, m_cameraState); // click the empty space in the viewport @@ -662,9 +687,32 @@ namespace UnitTest EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); } + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, UnstickySingleClickOffEntityWithSelectionWillDeselectEntity) + { + AzToolsFramework::ed_viewportStickySelect = false; + + PositionEntities(); + PositionCamera(m_cameraState); + + AzToolsFramework::SelectEntity(m_entityId1); + + // position in space above the entities + const auto clickOffPositionWorld = AZ::Vector3(5.0f, 15.0f, 12.0f); + // calculate the screen space position of the click + const auto clickOffPositionScreen = AzFramework::WorldToScreen(clickOffPositionWorld, m_cameraState); + + // click the empty space in the viewport + m_actionDispatcher->CameraState(m_cameraState)->MousePosition(clickOffPositionScreen)->MouseLButtonDown()->MouseLButtonUp(); + + // entity was deselected + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_TRUE(selectedEntitiesAfter.empty()); + } + TEST_F( EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, - SingleClickOnNewEntityWithSelectionWillNotChangeSelectedEntity) + StickySingleClickOnNewEntityWithSelectionWillNotChangeSelectedEntity) { AzToolsFramework::ed_viewportStickySelect = true; @@ -688,7 +736,31 @@ namespace UnitTest TEST_F( EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, - CtrlSingleClickOnNewEntityWithSelectionWillAppendSelectedEntityToSelection) + UnstickySingleClickOnNewEntityWithSelectionWillChangeSelectedEntity) + { + AzToolsFramework::ed_viewportStickySelect = false; + + PositionEntities(); + PositionCamera(m_cameraState); + + AzToolsFramework::SelectEntity(m_entityId1); + + // calculate the position in screen space of the second entity + const auto entity2ScreenPosition = AzFramework::WorldToScreen(m_entity2WorldTranslation, m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState)->MousePosition(entity2ScreenPosition)->MouseLButtonDown()->MouseLButtonUp(); + + // entity selection was changed + using ::testing::Eq; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter.size(), Eq(1)); + EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId2)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + StickyCtrlSingleClickOnNewEntityWithSelectionWillAppendSelectedEntityToSelection) { AzToolsFramework::ed_viewportStickySelect = true; @@ -715,7 +787,34 @@ namespace UnitTest TEST_F( EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, - CtrlSingleClickOnEntityInSelectionWillRemoveEntityFromSelection) + UnstickyCtrlSingleClickOnNewEntityWithSelectionWillAppendSelectedEntityToSelection) + { + AzToolsFramework::ed_viewportStickySelect = false; + + PositionEntities(); + PositionCamera(m_cameraState); + + AzToolsFramework::SelectEntity(m_entityId1); + + // calculate the position in screen space of the second entity + const auto entity2ScreenPosition = AzFramework::WorldToScreen(m_entity2WorldTranslation, m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(entity2ScreenPosition) + ->KeyboardModifierDown(AzToolsFramework::ViewportInteraction::KeyboardModifier::Control) + ->MouseLButtonDown() + ->MouseLButtonUp(); + + // entity selection was changed (one entity selected to two) + using ::testing::UnorderedElementsAre; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1, m_entityId2)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + StickyCtrlSingleClickOnEntityInSelectionWillRemoveEntityFromSelection) { AzToolsFramework::ed_viewportStickySelect = true; @@ -740,7 +839,36 @@ namespace UnitTest EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1)); } - TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, BoxSelectWithNoInitialSelectionAddsEntitiesToSelection) + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + UnstickyCtrlSingleClickOnEntityInSelectionWillRemoveEntityFromSelection) + { + AzToolsFramework::ed_viewportStickySelect = false; + + PositionEntities(); + PositionCamera(m_cameraState); + + AzToolsFramework::SelectEntities({ m_entityId1, m_entityId2 }); + + // calculate the position in screen space of the second entity + const auto entity2ScreenPosition = AzFramework::WorldToScreen(m_entity2WorldTranslation, m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(entity2ScreenPosition) + ->KeyboardModifierDown(AzToolsFramework::ViewportInteraction::KeyboardModifier::Control) + ->MouseLButtonDown() + ->MouseLButtonUp(); + + // entity selection was changed (entity2 was deselected) + using ::testing::UnorderedElementsAre; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + BoxSelectWithNoInitialSelectionAddsEntitiesToSelection) { AzToolsFramework::ed_viewportStickySelect = true; @@ -835,6 +963,32 @@ namespace UnitTest EXPECT_TRUE(selectedEntitiesAfter.empty()); } + TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, StickyDoubleClickWithSelectionWillDeselectEntities) + { + AzToolsFramework::ed_viewportStickySelect = true; + + PositionEntities(); + PositionCamera(m_cameraState); + + AzToolsFramework::SelectEntities({ m_entityId1, m_entityId2, m_entityId3 }); + + using ::testing::UnorderedElementsAre; + auto selectedEntitiesBefore = SelectedEntities(); + EXPECT_THAT(selectedEntitiesBefore, UnorderedElementsAre(m_entityId1, m_entityId2, m_entityId3)); + + // position in space above the entities + const auto clickOffPositionWorld = AZ::Vector3(5.0f, 15.0f, 12.0f); + // calculate the screen space position of the click + const auto clickOffPositionScreen = AzFramework::WorldToScreen(clickOffPositionWorld, m_cameraState); + + // double click to deselect entities + m_actionDispatcher->CameraState(m_cameraState)->MousePosition(clickOffPositionScreen)->MouseLButtonDoubleClick(); + + // no entities are selected + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_TRUE(selectedEntitiesAfter.empty()); + } + using EditorTransformComponentSelectionManipulatorTestFixture = IndirectCallManipulatorViewportInteractionFixtureMixin;