diff --git a/Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp b/Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp index 14723800ff..730c106301 100644 --- a/Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp +++ b/Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp @@ -40,6 +40,12 @@ namespace AzManipulatorTestFramework { m_viewportInteraction.UpdateVisibility(); + // ensure we call display viewport 2d to simulate this update step (some state may be + // updated here, e.g. box select) + AzFramework::ViewportDebugDisplayEventBus::Event( + AzToolsFramework::GetEntityContextId(), &AzFramework::ViewportDebugDisplayEvents::DisplayViewport2d, + AzFramework::ViewportInfo{ m_viewportInteraction.GetViewportId() }, m_viewportInteraction.GetDebugDisplay()); + DrawManipulators(); AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus::Event( AzToolsFramework::GetEntityContextId(), diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp index c39b2c0ebc..eafd37f199 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.cpp @@ -9,8 +9,8 @@ #include "EditorBoxSelect.h" #include -#include #include +#include #include @@ -19,11 +19,15 @@ namespace AzToolsFramework static const AZ::Color s_boxSelectColor = AZ::Color(1.0f, 1.0f, 1.0f, 0.4f); static const float s_boxSelectLineWidth = 2.0f; - void EditorBoxSelect::HandleMouseInteraction( - const ViewportInteraction::MouseInteractionEvent& mouseInteraction) + void EditorBoxSelect::HandleMouseInteraction(const ViewportInteraction::MouseInteractionEvent& mouseInteraction) { AZ_PROFILE_FUNCTION(AzToolsFramework); + if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down) + { + m_cursorPositionAtDownEvent = mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates; + } + m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates); const auto selectClickEvent = ClickDetectorEventFromViewportInteraction(mouseInteraction); @@ -35,12 +39,7 @@ namespace AzToolsFramework m_leftMouseDown(mouseInteraction); } - m_boxSelectRegion = QRect - { - ViewportInteraction::QPointFromScreenPoint( - mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates), - QSize { 0, 0 } - }; + m_boxSelectRegion = QRect{ ViewportInteraction::QPointFromScreenPoint(m_cursorPositionAtDownEvent), QSize{ 0, 0 } }; } if (m_boxSelectRegion) @@ -87,11 +86,11 @@ namespace AzToolsFramework AZ::Vector2 viewportSize = AzToolsFramework::GetCameraState(viewportInfo.m_viewportId).m_viewportSize; debugDisplay.DrawWireQuad2d( - AZ::Vector2( - aznumeric_cast(m_boxSelectRegion->x()), aznumeric_cast(m_boxSelectRegion->y())) / viewportSize, + AZ::Vector2(aznumeric_cast(m_boxSelectRegion->x()), aznumeric_cast(m_boxSelectRegion->y())) / viewportSize, AZ::Vector2( aznumeric_cast(m_boxSelectRegion->x()) + aznumeric_cast(m_boxSelectRegion->width()), - aznumeric_cast(m_boxSelectRegion->y()) + aznumeric_cast(m_boxSelectRegion->height())) / viewportSize, + aznumeric_cast(m_boxSelectRegion->y()) + aznumeric_cast(m_boxSelectRegion->height())) / + viewportSize, 0.f); debugDisplay.DepthTestOn(); @@ -101,8 +100,7 @@ namespace AzToolsFramework } } - void EditorBoxSelect::DisplayScene( - const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) + void EditorBoxSelect::DisplayScene(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) { if (m_displayScene) { @@ -122,15 +120,14 @@ namespace AzToolsFramework m_mouseMove = mouseMove; } - void EditorBoxSelect::InstallLeftMouseUp( - const AZStd::function& leftMouseUp) + void EditorBoxSelect::InstallLeftMouseUp(const AZStd::function& leftMouseUp) { m_leftMouseUp = leftMouseUp; } void EditorBoxSelect::InstallDisplayScene( - const AZStd::function& displayScene) + const AZStd::function& + displayScene) { m_displayScene = displayScene; } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.h index 8b36ec0a32..f1c59f9fd8 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorBoxSelect.h @@ -81,5 +81,6 @@ namespace AzToolsFramework ViewportInteraction::KeyboardModifiers m_previousModifiers; //!< Modifier keys active on the previous frame. AzFramework::ClickDetector m_clickDetector; //!< Utility type to detect if a mouse click or move has occurred. AzFramework::CursorState m_cursorState; //!< Utility type to track the current cursor position (and movement/delta). + AzFramework::ScreenPoint m_cursorPositionAtDownEvent; //!< The position of the cursor when first potentially starting a box select. }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp index 101d7094db..afcade944a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp @@ -44,39 +44,46 @@ namespace AzToolsFramework AZ_CVAR( float, - cl_viewportGizmoAxisLineWidth, + ed_viewportGizmoAxisLineWidth, 4.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The width of the line for the viewport axis gizmo"); AZ_CVAR( float, - cl_viewportGizmoAxisLineLength, + ed_viewportGizmoAxisLineLength, 0.7f, nullptr, AZ::ConsoleFunctorFlags::Null, "The length of the line for the viewport axis gizmo"); AZ_CVAR( float, - cl_viewportGizmoAxisLabelOffset, + ed_viewportGizmoAxisLabelOffset, 1.15f, nullptr, AZ::ConsoleFunctorFlags::Null, "The offset of the label for the viewport axis gizmo"); AZ_CVAR( float, - cl_viewportGizmoAxisLabelSize, + ed_viewportGizmoAxisLabelSize, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The size of each label for the viewport axis gizmo"); AZ_CVAR( AZ::Vector2, - cl_viewportGizmoAxisScreenPosition, + ed_viewportGizmoAxisScreenPosition, AZ::Vector2(0.045f, 0.9f), nullptr, AZ::ConsoleFunctorFlags::Null, "The screen position of the gizmo in normalized (0-1) ndc space"); + AZ_CVAR( + bool, + ed_viewportStickySelect, + true, + nullptr, + AZ::ConsoleFunctorFlags::Null, + "Sticky select implies a single click will not change selection with an entity already selected"); // strings related to new viewport interaction model (EditorTransformComponentSelection) static const char* const s_togglePivotTitleRightClick = "Toggle pivot"; @@ -991,14 +998,19 @@ namespace AzToolsFramework // ask the visible entity data cache if the entity is selectable in the viewport // (useful in the context of drawing when we only care about entities we can see) - static bool SelectableInVisibleViewportCache(const EditorVisibleEntityDataCache& entityDataCache, const AZ::EntityId entityId) + // note: return the index if it is selectable, nullopt otherwise + static AZStd::optional SelectableInVisibleViewportCache( + const EditorVisibleEntityDataCache& entityDataCache, const AZ::EntityId entityId) { if (auto entityIndex = entityDataCache.GetVisibleEntityIndexFromId(entityId)) { - return entityDataCache.IsVisibleEntitySelectableInViewport(*entityIndex); + if (entityDataCache.IsVisibleEntitySelectableInViewport(*entityIndex)) + { + return *entityIndex; + } } - return false; + return AZStd::nullopt; } static AZ::ComponentId GetTransformComponentId(const AZ::EntityId entityId) @@ -1709,17 +1721,17 @@ namespace AzToolsFramework m_pivotOverrideFrame.Reset(); } - bool EditorTransformComponentSelection::SelectDeselect(const AZ::EntityId entityIdUnderCursor) + bool EditorTransformComponentSelection::SelectDeselect(const AZ::EntityId entityId) { AZ_PROFILE_FUNCTION(AzToolsFramework); - if (entityIdUnderCursor.IsValid()) + if (entityId.IsValid()) { - if (IsEntitySelectedInternal(entityIdUnderCursor, m_selectedEntityIds)) + if (IsEntitySelectedInternal(entityId, m_selectedEntityIds)) { if (!UndoRedoOperationInProgress()) { - RemoveEntityFromSelection(entityIdUnderCursor); + RemoveEntityFromSelection(entityId); const auto nextEntityIds = EntityIdVectorFromContainer(m_selectedEntityIds); @@ -1742,7 +1754,7 @@ namespace AzToolsFramework { if (!UndoRedoOperationInProgress()) { - AddEntityToSelection(entityIdUnderCursor); + AddEntityToSelection(entityId); const auto nextEntityIds = EntityIdVectorFromContainer(m_selectedEntityIds); @@ -1783,25 +1795,21 @@ namespace AzToolsFramework // for entities selected with no bounds of their own (just TransformComponent) // check selection against the selection indicator aabb - for (AZ::EntityId entityId : m_selectedEntityIds) + for (const AZ::EntityId& entityId : m_selectedEntityIds) { - if (!SelectableInVisibleViewportCache(*m_entityDataCache, entityId)) + if (const auto entityIndex = SelectableInVisibleViewportCache(*m_entityDataCache, entityId); entityIndex.has_value()) { - continue; - } + const AZ::Transform& worldFromLocal = m_entityDataCache->GetVisibleEntityTransform(*entityIndex); + const AZ::Vector3 boxPosition = worldFromLocal.TransformPoint(CalculateCenterOffset(entityId, m_pivotMode)); + const AZ::Vector3 scaledSize = + AZ::Vector3(s_pivotSize) * CalculateScreenToWorldMultiplier(worldFromLocal.GetTranslation(), cameraState); - AZ::Transform worldFromLocal; - AZ::TransformBus::EventResult(worldFromLocal, entityId, &AZ::TransformBus::Events::GetWorldTM); - - const AZ::Vector3 boxPosition = worldFromLocal.TransformPoint(CalculateCenterOffset(entityId, m_pivotMode)); - - const AZ::Vector3 scaledSize = - AZ::Vector3(s_pivotSize) * CalculateScreenToWorldMultiplier(worldFromLocal.GetTranslation(), cameraState); - - if (AabbIntersectMouseRay( - mouseInteraction.m_mouseInteraction, AZ::Aabb::CreateFromMinMax(boxPosition - scaledSize, boxPosition + scaledSize))) - { - m_cachedEntityIdUnderCursor = entityId; + if (AabbIntersectMouseRay( + mouseInteraction.m_mouseInteraction, + AZ::Aabb::CreateFromMinMax(boxPosition - scaledSize, boxPosition + scaledSize))) + { + m_cachedEntityIdUnderCursor = entityId; + } } } @@ -3490,7 +3498,7 @@ namespace AzToolsFramework const auto cameraProjection = AzFramework::CameraProjection(gizmoCameraState); // screen space offset to move the 2d gizmo around - const AZ::Vector2 screenOffset = AZ::Vector2(cl_viewportGizmoAxisScreenPosition) - AZ::Vector2(0.5f, 0.5f); + const AZ::Vector2 screenOffset = AZ::Vector2(ed_viewportGizmoAxisScreenPosition) - AZ::Vector2(0.5f, 0.5f); // map from a position in world space (relative to the the gizmo camera near the origin) to a position in // screen space @@ -3502,7 +3510,7 @@ namespace AzToolsFramework }; // get all important axis positions in screen space - const float lineLength = cl_viewportGizmoAxisLineLength; + const float lineLength = ed_viewportGizmoAxisLineLength; const auto gizmoStart = calculateGizmoAxis(AZ::Vector3::CreateZero()); const auto gizmoEndAxisX = calculateGizmoAxis(-AZ::Vector3::CreateAxisX() * lineLength); const auto gizmoEndAxisY = calculateGizmoAxis(-AZ::Vector3::CreateAxisY() * lineLength); @@ -3513,7 +3521,7 @@ namespace AzToolsFramework const AZ::Vector2 gizmoAxisZ = gizmoEndAxisZ - gizmoStart; // draw the axes of the gizmo - debugDisplay.SetLineWidth(cl_viewportGizmoAxisLineWidth); + debugDisplay.SetLineWidth(ed_viewportGizmoAxisLineWidth); debugDisplay.SetColor(AZ::Colors::Red); debugDisplay.DrawLine2d(gizmoStart, gizmoEndAxisX, 1.0f); debugDisplay.SetColor(AZ::Colors::Lime); @@ -3522,14 +3530,14 @@ namespace AzToolsFramework debugDisplay.DrawLine2d(gizmoStart, gizmoEndAxisZ, 1.0f); debugDisplay.SetLineWidth(1.0f); - const float labelOffset = cl_viewportGizmoAxisLabelOffset; + const float labelOffset = ed_viewportGizmoAxisLabelOffset; const float screenScale = GetScreenDisplayScaling(viewportId); const auto labelXScreenPosition = (gizmoStart + (gizmoAxisX * labelOffset)) * editorCameraState.m_viewportSize * screenScale; const auto labelYScreenPosition = (gizmoStart + (gizmoAxisY * labelOffset)) * editorCameraState.m_viewportSize * screenScale; const auto labelZScreenPosition = (gizmoStart + (gizmoAxisZ * labelOffset)) * editorCameraState.m_viewportSize * screenScale; // draw the label of of each axis for the gizmo - const float labelSize = cl_viewportGizmoAxisLabelSize; + const float labelSize = ed_viewportGizmoAxisLabelSize; debugDisplay.SetColor(AZ::Colors::White); debugDisplay.Draw2dTextLabel(labelXScreenPosition.GetX(), labelXScreenPosition.GetY(), labelSize, "X", true); debugDisplay.Draw2dTextLabel(labelYScreenPosition.GetX(), labelYScreenPosition.GetY(), labelSize, "Y", true); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h index 42ec423b03..f528a11688 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -32,6 +33,8 @@ namespace AzToolsFramework { + AZ_CVAR_EXTERNED(bool, ed_viewportStickySelect); + class EditorVisibleEntityDataCache; using EntityIdSet = AZStd::unordered_set; //!< Alias for unordered_set of EntityIds. @@ -170,14 +173,11 @@ namespace AzToolsFramework //! ViewportInteraction::ViewportSelectionRequests //! Intercept all viewport mouse events and respond to inputs. - bool HandleMouseInteraction( - const ViewportInteraction::MouseInteractionEvent& mouseInteraction) override; + bool HandleMouseInteraction(const ViewportInteraction::MouseInteractionEvent& mouseInteraction) override; void DisplayViewportSelection( - const AzFramework::ViewportInfo& viewportInfo, - AzFramework::DebugDisplayRequests& debugDisplay) override; + const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override; void DisplayViewportSelection2d( - const AzFramework::ViewportInfo& viewportInfo, - AzFramework::DebugDisplayRequests& debugDisplay) override; + const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override; //! Add an entity to the current selection void AddEntityToSelection(AZ::EntityId entityId); @@ -206,7 +206,7 @@ namespace AzToolsFramework bool IsEntitySelected(AZ::EntityId entityId) const; void SetSelectedEntities(const EntityIdList& entityIds); void DeselectEntities(); - bool SelectDeselect(AZ::EntityId entityIdUnderCursor); + bool SelectDeselect(AZ::EntityId entityId); void RefreshSelectedEntityIds(); void RefreshSelectedEntityIds(const EntityIdList& selectedEntityIds); @@ -253,11 +253,10 @@ namespace AzToolsFramework void SnapSelectedEntitiesToWorldGrid(float gridSize) override; // EditorManipulatorCommandUndoRedoRequestBus ... - void UndoRedoEntityManipulatorCommand( - AZ::u8 pivotOverride, const AZ::Transform& transform, AZ::EntityId entityId) override; + void UndoRedoEntityManipulatorCommand(AZ::u8 pivotOverride, const AZ::Transform& transform, AZ::EntityId entityId) override; // EditorContextMenuBus... - void PopulateEditorGlobalContextMenu(QMenu* menu, const AZ::Vector2 & point, int flags) override; + void PopulateEditorGlobalContextMenu(QMenu* menu, const AZ::Vector2& point, int flags) override; int GetMenuPosition() const override; AZStd::string GetMenuIdentifier() const override; @@ -266,8 +265,7 @@ namespace AzToolsFramework // ToolsApplicationNotificationBus ... void BeforeEntitySelectionChanged() override; - void AfterEntitySelectionChanged( - const EntityIdList& newlySelectedEntities, const EntityIdList& newlyDeselectedEntities) override; + void AfterEntitySelectionChanged(const EntityIdList& newlySelectedEntities, const EntityIdList& newlyDeselectedEntities) override; // TransformNotificationBus ... void OnTransformChanged(const AZ::Transform& localTM, const AZ::Transform& worldTM) override; @@ -318,7 +316,8 @@ namespace AzToolsFramework EntityIdManipulators m_entityIdManipulators; //!< Mapping from a Manipulator to potentially many EntityIds. EditorBoxSelect m_boxSelect; //!< Type responsible for handling box select. - AZStd::unique_ptr m_manipulatorMoveCommand; //!< Track adjustments to manipulator translation and orientation (during mouse press/move). + //! Track adjustments to manipulator translation and orientation (during mouse press/move). + AZStd::unique_ptr m_manipulatorMoveCommand; AZStd::vector> m_actions; //!< What actions are tied to this handler. ViewportInteraction::KeyboardModifiers m_previousModifiers; //!< What modifiers were held last frame. EditorContextMenu m_contextMenu; //!< Viewport right click context menu. @@ -328,8 +327,10 @@ namespace AzToolsFramework ReferenceFrame m_referenceFrame = ReferenceFrame::Parent; //!< What reference frame is the Manipulator currently operating in. Frame m_axisPreview; //!< Axes of entity at the time of mouse down to indicate delta of translation. bool m_triedToRefresh = false; //!< Did a refresh event occur to recalculate the current Manipulator transform. - bool m_didSetSelectedEntities = false; //!< Was EditorTransformComponentSelection responsible for the most recent entity selection change. - bool m_selectedEntityIdsAndManipulatorsDirty = false; //!< Do the active manipulators need to recalculated after a modification (lock/visibility etc). + //! Was EditorTransformComponentSelection responsible for the most recent entity selection change. + bool m_didSetSelectedEntities = false; + //! Do the active manipulators need to recalculated after a modification (lock/visibility etc). + bool m_selectedEntityIdsAndManipulatorsDirty = false; bool m_transformChangedInternally = false; //!< Was an OnTransformChanged event triggered internally or not. ViewportUi::ClusterId m_transformModeClusterId; //!< Id of the Viewport UI cluster for changing transform mode. ViewportUi::ButtonId m_translateButtonId; //!< Id of the Viewport UI button for translate mode. @@ -363,15 +364,13 @@ namespace AzToolsFramework //! Calculate the orientation for a group of entities based on the incoming reference frame. template - PivotOrientationResult CalculatePivotOrientationForEntityIds( - const EntityIdMap& entityIdMap, const ReferenceFrame referenceFrame); + PivotOrientationResult CalculatePivotOrientationForEntityIds(const EntityIdMap& entityIdMap, const ReferenceFrame referenceFrame); //! Calculate the orientation for a group of entities based on the incoming //! reference frame with possible pivot override. template PivotOrientationResult CalculateSelectionPivotOrientation( - const EntityIdMap& entityIdMap, const OptionalFrame& pivotOverrideFrame, - const ReferenceFrame referenceFrame); + const EntityIdMap& entityIdMap, const OptionalFrame& pivotOverrideFrame, const ReferenceFrame referenceFrame); void SetEntityWorldTranslation(AZ::EntityId entityId, const AZ::Vector3& worldTranslation, bool& internal); void SetEntityLocalTranslation(AZ::EntityId entityId, const AZ::Vector3& localTranslation, bool& internal); diff --git a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp index c1c158d0f9..dc6e38ecd5 100644 --- a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp @@ -6,12 +6,14 @@ * */ +#include #include #include #include #include #include #include +#include #include #include #include @@ -20,10 +22,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -32,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +51,14 @@ namespace AZ namespace UnitTest { + AzToolsFramework::EntityIdList SelectedEntities() + { + AzToolsFramework::EntityIdList selectedEntitiesBefore; + AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult( + selectedEntitiesBefore, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities); + return selectedEntitiesBefore; + } + class EditorEntityVisibilityCacheFixture : public ToolsApplicationFixture { public: @@ -110,6 +123,80 @@ namespace UnitTest EXPECT_FALSE(m_cache.IsVisibleEntityVisible(m_cache.GetVisibleEntityIndexFromId(m_entityIds[2]).value())); } + //! Basic component that implements BoundsRequestBus and EditorComponentSelectionRequestsBus to be compatible + //! with the Editor visibility system. + //! Note: Used for simulating selection (picking) in the viewport. + class BoundsTestComponent + : public AzToolsFramework::Components::EditorComponentBase + , public AzFramework::BoundsRequestBus::Handler + , public AzToolsFramework::EditorComponentSelectionRequestsBus::Handler + { + public: + AZ_EDITOR_COMPONENT( + BoundsTestComponent, "{E6312E9D-8489-4677-9980-C93C328BC92C}", AzToolsFramework::Components::EditorComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + // AZ::Component overrides ... + void Activate() override; + void Deactivate() override; + + // EditorComponentSelectionRequestsBus overrides ... + AZ::Aabb GetEditorSelectionBoundsViewport(const AzFramework::ViewportInfo& viewportInfo) override; + bool EditorSelectionIntersectRayViewport( + const AzFramework::ViewportInfo& viewportInfo, const AZ::Vector3& src, const AZ::Vector3& dir, float& distance) override; + bool SupportsEditorRayIntersect() override; + + // BoundsRequestBus overrides ... + AZ::Aabb GetWorldBounds() override; + AZ::Aabb GetLocalBounds() override; + }; + + AZ::Aabb BoundsTestComponent::GetEditorSelectionBoundsViewport([[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo) + { + return GetWorldBounds(); + } + + bool BoundsTestComponent::EditorSelectionIntersectRayViewport( + [[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, const AZ::Vector3& src, const AZ::Vector3& dir, float& distance) + { + return AzToolsFramework::AabbIntersectRay(src, dir, GetWorldBounds(), distance); + } + + bool BoundsTestComponent::SupportsEditorRayIntersect() + { + return true; + } + + void BoundsTestComponent::Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + // noop + } + + void BoundsTestComponent::Activate() + { + AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId()); + AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(GetEntityId()); + } + + void BoundsTestComponent::Deactivate() + { + AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusDisconnect(); + AzFramework::BoundsRequestBus::Handler::BusDisconnect(); + } + + AZ::Aabb BoundsTestComponent::GetWorldBounds() + { + AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity(); + AZ::TransformBus::EventResult(worldFromLocal, GetEntityId(), &AZ::TransformBus::Events::GetWorldTM); + return GetLocalBounds().GetTransformedAabb(worldFromLocal); + } + + AZ::Aabb BoundsTestComponent::GetLocalBounds() + { + return AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f)); + } + // Fixture to support testing EditorTransformComponentSelection functionality on an Entity selection. class EditorTransformComponentSelectionFixture : public ToolsApplicationFixture { @@ -120,27 +207,52 @@ namespace UnitTest m_entityIds.push_back(m_entityId1); } - void ArrangeIndividualRotatedEntitySelection(const AZ::Quaternion& orientation); - AZStd::optional GetManipulatorTransform() const; - void RefreshManipulators(AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::RefreshType refreshType); - void SetTransformMode(AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::Mode transformMode); - void OverrideManipulatorOrientation(const AZ::Quaternion& orientation); - void OverrideManipulatorTranslation(const AZ::Vector3& translation); - public: AZ::EntityId m_entityId1; AzToolsFramework::EntityIdList m_entityIds; }; - void EditorTransformComponentSelectionFixture::ArrangeIndividualRotatedEntitySelection(const AZ::Quaternion& orientation) + class EditorTransformComponentSelectionViewportPickingFixture : public ToolsApplicationFixture { - for (auto entityId : m_entityIds) + public: + void SetUpEditorFixtureImpl() override + { + auto* app = GetApplication(); + // register a simple component implementing BoundsRequestBus and EditorComponentSelectionRequestsBus + app->RegisterComponentDescriptor(BoundsTestComponent::CreateDescriptor()); + + auto createEntityWithBoundsFn = [](const char* entityName) + { + AZ::Entity* entity = nullptr; + AZ::EntityId entityId = CreateDefaultEditorEntity(entityName, &entity); + + entity->Deactivate(); + entity->CreateComponent(); + entity->Activate(); + + return entityId; + }; + + m_entityId1 = createEntityWithBoundsFn("Entity1"); + m_entityId2 = createEntityWithBoundsFn("Entity2"); + m_entityId3 = createEntityWithBoundsFn("Entity3"); + } + + public: + AZ::EntityId m_entityId1; + AZ::EntityId m_entityId2; + AZ::EntityId m_entityId3; + }; + + void ArrangeIndividualRotatedEntitySelection(const AzToolsFramework::EntityIdList& entityIds, const AZ::Quaternion& orientation) + { + for (auto entityId : entityIds) { AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, orientation); } } - AZStd::optional EditorTransformComponentSelectionFixture::GetManipulatorTransform() const + AZStd::optional GetManipulatorTransform() { using AzToolsFramework::EditorTransformComponentSelectionRequestBus; @@ -151,8 +263,7 @@ namespace UnitTest return manipulatorTransform; } - void EditorTransformComponentSelectionFixture::RefreshManipulators( - AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::RefreshType refreshType) + void RefreshManipulators(const AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::RefreshType refreshType) { using AzToolsFramework::EditorTransformComponentSelectionRequestBus; @@ -160,8 +271,7 @@ namespace UnitTest AzToolsFramework::GetEntityContextId(), &EditorTransformComponentSelectionRequestBus::Events::RefreshManipulators, refreshType); } - void EditorTransformComponentSelectionFixture::SetTransformMode( - AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::Mode transformMode) + void SetTransformMode(const AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::Mode transformMode) { using AzToolsFramework::EditorTransformComponentSelectionRequestBus; @@ -169,7 +279,7 @@ namespace UnitTest AzToolsFramework::GetEntityContextId(), &EditorTransformComponentSelectionRequestBus::Events::SetTransformMode, transformMode); } - void EditorTransformComponentSelectionFixture::OverrideManipulatorOrientation(const AZ::Quaternion& orientation) + void OverrideManipulatorOrientation(const AZ::Quaternion& orientation) { using AzToolsFramework::EditorTransformComponentSelectionRequestBus; @@ -178,7 +288,7 @@ namespace UnitTest orientation); } - void EditorTransformComponentSelectionFixture::OverrideManipulatorTranslation(const AZ::Vector3& translation) + void OverrideManipulatorTranslation(const AZ::Vector3& translation) { using AzToolsFramework::EditorTransformComponentSelectionRequestBus; @@ -190,7 +300,7 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // EditorTransformComponentSelection Tests - TEST_F(EditorTransformComponentSelectionFixture, Focus_is_not_changed_while_switching_viewport_interaction_request_instance) + TEST_F(EditorTransformComponentSelectionFixture, FocusIsNotChangedWhileSwitchingViewportInteractionRequestInstance) { // setup a dummy widget and make it the active window to ensure focus in/out events are fired auto dummyWidget = AZStd::make_unique(); @@ -239,7 +349,7 @@ namespace UnitTest // Given AzToolsFramework::SelectEntity(m_entityId1); - ArrangeIndividualRotatedEntitySelection(AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f))); + ArrangeIndividualRotatedEntitySelection(m_entityIds, AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f))); RefreshManipulators(EditorTransformComponentSelectionRequestBus::Events::RefreshType::All); SetTransformMode(EditorTransformComponentSelectionRequestBus::Events::Mode::Rotation); @@ -286,7 +396,7 @@ namespace UnitTest AzToolsFramework::SelectEntity(m_entityId1); const AZ::Quaternion initialEntityOrientation = AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)); - ArrangeIndividualRotatedEntitySelection(initialEntityOrientation); + ArrangeIndividualRotatedEntitySelection(m_entityIds, initialEntityOrientation); // assign new orientation to manipulator which does not match entity orientation OverrideManipulatorOrientation(AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f))); @@ -478,6 +588,227 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// } + // fixture for use with the indirect manipulator test framework + using EditorTransformComponentSelectionViewportPickingManipulatorTestFixture = + IndirectCallManipulatorViewportInteractionFixtureMixin; + + TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, SingleClickWithNoSelectionWillSelectEntity) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entity + const auto initialTransformWorld = AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f)); + AZ::TransformBus::Event(m_entityId1, &AZ::TransformBus::Events::SetWorldTM, initialTransformWorld); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + using ::testing::Eq; + auto selectedEntitiesBefore = SelectedEntities(); + EXPECT_TRUE(selectedEntitiesBefore.empty()); + + // calculate the position in screen space of the initial entity position + const auto initialPositionScreen = AzFramework::WorldToScreen(initialTransformWorld.GetTranslation(), m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState)->MousePosition(initialPositionScreen)->MouseLButtonDown()->MouseLButtonUp(); + + // entity is selected + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter.size(), Eq(1)); + EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); + } + + TEST_F(EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, SingleClickOffEntityWithSelectionWillNotDeselectEntity) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entity + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f))); + + // position in space above the entity + const auto clickOffPositionWorld = AZ::Vector3(5.0f, 15.0f, 12.0f); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + AzToolsFramework::SelectEntity(m_entityId1); + + // calculate the position in screen space of the initial position of the entity + 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 not deselected + using ::testing::Eq; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter.size(), Eq(1)); + EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + SingleClickOnNewEntityWithSelectionWillNotChangeSelectedEntity) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entity + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f))); + + const auto initialTransformWorldSecondEntity = AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 10.0f, 10.0f)); + AZ::TransformBus::Event(m_entityId2, &AZ::TransformBus::Events::SetWorldTM, initialTransformWorldSecondEntity); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + AzToolsFramework::SelectEntity(m_entityId1); + + // calculate the position in screen space of the second entity + const auto initialPositionScreenSecondEntity = + AzFramework::WorldToScreen(initialTransformWorldSecondEntity.GetTranslation(), m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(initialPositionScreenSecondEntity) + ->MouseLButtonDown() + ->MouseLButtonUp(); + + // entity selection was not changed + using ::testing::Eq; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter.size(), Eq(1)); + EXPECT_THAT(selectedEntitiesAfter.front(), Eq(m_entityId1)); + } + + TEST_F( + EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, + CtrlSingleClickOnNewEntityWithSelectionWillAppendSelectedEntityToSelection) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entity + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f))); + + const auto initialTransformWorldSecondEntity = AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 10.0f, 10.0f)); + AZ::TransformBus::Event(m_entityId2, &AZ::TransformBus::Events::SetWorldTM, initialTransformWorldSecondEntity); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + AzToolsFramework::SelectEntity(m_entityId1); + + // calculate the position in screen space of the second entity + const auto initialPositionScreenSecondEntity = + AzFramework::WorldToScreen(initialTransformWorldSecondEntity.GetTranslation(), m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(initialPositionScreenSecondEntity) + ->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, + CtrlSingleClickOnEntityInSelectionWillRemoveEntityFromSelection) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entity + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f))); + + const auto initialTransformWorldSecondEntity = AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 10.0f, 10.0f)); + AZ::TransformBus::Event(m_entityId2, &AZ::TransformBus::Events::SetWorldTM, initialTransformWorldSecondEntity); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + AzToolsFramework::SelectEntities({ m_entityId1, m_entityId2 }); + + // calculate the position in screen space of the second entity + const auto initialPositionScreenSecondEntity = + AzFramework::WorldToScreen(initialTransformWorldSecondEntity.GetTranslation(), m_cameraState); + + // click the entity in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(initialPositionScreenSecondEntity) + ->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, DISABLED_BoxSelectWithNoInitialSelectionAddsEntitiesToSelection) + { + AzToolsFramework::ed_viewportStickySelect = true; + + // the initial starting position of the entities + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 15.0f, 10.0f))); + AZ::TransformBus::Event( + m_entityId2, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 14.0f, 10.0f))); + AZ::TransformBus::Event( + m_entityId3, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, 16.0f, 10.0f))); + + // initial camera position (looking down the negative x-axis) + AzFramework::SetCameraTransform( + m_cameraState, + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(AZ::Vector3(0.0f, 0.0f, 90.0f)), AZ::Vector3(10.0f, 15.0f, 10.0f))); + + using ::testing::Eq; + auto selectedEntitiesBefore = SelectedEntities(); + EXPECT_THAT(selectedEntitiesBefore.size(), Eq(0)); + + // calculate the position in screen space of where to begin and end the box select action + const auto beginningPositionWorldBoxSelectStart = AzFramework::WorldToScreen(AZ::Vector3(5.0f, 13.5f, 10.5f), m_cameraState); + const auto middlePositionWorldBoxSelectStart = AzFramework::WorldToScreen(AZ::Vector3(5.0f, 15.0f, 10.0f), m_cameraState); + const auto endingPositionWorldBoxSelectStart = AzFramework::WorldToScreen(AZ::Vector3(5.0f, 16.5f, 9.5f), m_cameraState); + + // perform a box select in the viewport + m_actionDispatcher->CameraState(m_cameraState) + ->MousePosition(beginningPositionWorldBoxSelectStart) + ->MouseLButtonDown() + ->MousePosition(middlePositionWorldBoxSelectStart) + ->MousePosition(endingPositionWorldBoxSelectStart) + ->MouseLButtonUp(); + + // entities are selected + using ::testing::UnorderedElementsAre; + auto selectedEntitiesAfter = SelectedEntities(); + EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1, m_entityId2, m_entityId3)); + } + using EditorTransformComponentSelectionManipulatorTestFixture = IndirectCallManipulatorViewportInteractionFixtureMixin;