Improve selection in the viewport (#720)

* improve selection in the viewport

* remove debug code

* updates following review feedback

- update API comments from /// to //! from
- add [[nodiscard]] attribute to member function
- move constructor implementations to .cpp files

* use lambda instead of ternary operator

* fix unit test failure caused by typo
This commit is contained in:
Tom Hulton-Harrop
2021-05-13 16:19:53 +01:00
committed by GitHub
parent 4aff32e719
commit 795aa114e6
11 changed files with 573 additions and 143 deletions
@@ -15,6 +15,7 @@
#include <AzCore/Console/IConsole.h>
#include <AzCore/Math/MathUtils.h>
#include <AzCore/Math/Plane.h>
#include <AzCore/std/numeric.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Windowing/WindowBus.h>
@@ -156,35 +157,25 @@ namespace AzFramework
camera.m_lookAt = transform.GetTranslation() + (camera.Rotation().GetBasisY() * -camera.m_lookDist);
}
static ScreenVector CursorDelta(const AZStd::optional<ScreenPoint>& currentPosition, const AZStd::optional<ScreenPoint>& lastPosition)
{
return currentPosition.has_value() && lastPosition.has_value() ? currentPosition.value() - lastPosition.value()
: ScreenVector(0, 0);
}
bool CameraSystem::HandleEvents(const InputEvent& event)
{
if (const auto& cursor = AZStd::get_if<CursorEvent>(&event))
{
m_currentCursorPosition = cursor->m_position;
m_cursorState.SetCurrentPosition(cursor->m_position);
}
else if (const auto& scroll = AZStd::get_if<ScrollEvent>(&event))
{
m_scrollDelta = scroll->m_delta;
}
return m_cameras.HandleEvents(event, CursorDelta(m_currentCursorPosition, m_lastCursorPosition), m_scrollDelta);
return m_cameras.HandleEvents(event, m_cursorState.CursorDelta(), m_scrollDelta);
}
Camera CameraSystem::StepCamera(const Camera& targetCamera, const float deltaTime)
{
const auto cursorDelta = CursorDelta(m_currentCursorPosition, m_lastCursorPosition);
if (m_currentCursorPosition.has_value())
{
m_lastCursorPosition = m_currentCursorPosition;
}
const auto nextCamera = m_cameras.StepCamera(targetCamera, m_cursorState.CursorDelta(), m_scrollDelta, deltaTime);
const auto nextCamera = m_cameras.StepCamera(targetCamera, cursorDelta, m_scrollDelta, deltaTime);
m_cursorState.Update();
m_scrollDelta = 0.0f;
@@ -236,12 +227,12 @@ namespace AzFramework
}
}
// accumulate
Camera nextCamera = targetCamera;
for (auto& cameraInput : m_activeCameraInputs)
{
nextCamera = cameraInput->StepCamera(nextCamera, cursorDelta, scrollDelta, deltaTime);
}
const Camera nextCamera = AZStd::accumulate(
AZStd::begin(m_activeCameraInputs), AZStd::end(m_activeCameraInputs), targetCamera,
[cursorDelta, scrollDelta, deltaTime](Camera acc, auto& camera) {
acc = camera->StepCamera(acc, cursorDelta, scrollDelta, deltaTime);
return acc;
});
for (int i = 0; i < m_activeCameraInputs.size();)
{
@@ -275,34 +266,42 @@ namespace AzFramework
}
}
RotateCameraInput::RotateCameraInput(const InputChannelId rotateChannelId)
: m_rotateChannelId(rotateChannelId)
{
}
void RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_channelId == m_rotateChannelId)
const ClickDetector::ClickEvent clickEvent = [&event, this] {
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
{
if (input->m_state == InputChannel::State::Began)
if (input->m_channelId == m_rotateChannelId)
{
m_tryingToBegin = true;
m_moveAccumulator = 0.0f;
}
else if (input->m_state == InputChannel::State::Ended)
{
m_tryingToBegin = false;
EndActivation();
if (input->m_state == InputChannel::State::Began)
{
return ClickDetector::ClickEvent::Down;
}
else if (input->m_state == InputChannel::State::Ended)
{
return ClickDetector::ClickEvent::Up;
}
}
}
}
return ClickDetector::ClickEvent::Nil;
}();
if (m_tryingToBegin)
switch (const auto outcome = m_clickDetector.DetectClick(clickEvent, cursorDelta); outcome)
{
// only allow the action to begin if the mouse has been moved a small amount
m_moveAccumulator += ScreenVectorLength(cursorDelta);
if (m_moveAccumulator > ed_cameraSystemLookDeadzone)
{
BeginActivation();
m_tryingToBegin = false;
}
case ClickDetector::ClickOutcome::Move:
BeginActivation();
break;
case ClickDetector::ClickOutcome::Release:
EndActivation();
break;
default:
// noop
break;
}
}
@@ -324,6 +323,12 @@ namespace AzFramework
return nextCamera;
}
PanCameraInput::PanCameraInput(const InputChannelId panChannelId, PanAxesFn panAxesFn)
: m_panAxesFn(AZStd::move(panAxesFn))
, m_panChannelId(panChannelId)
{
}
void PanCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
@@ -400,6 +405,11 @@ namespace AzFramework
return TranslationType::Nil;
}
TranslateCameraInput::TranslateCameraInput(TranslationAxesFn translationAxesFn)
: m_translationAxesFn(AZStd::move(translationAxesFn))
{
}
void TranslateCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
@@ -574,6 +584,11 @@ namespace AzFramework
return nextCamera;
}
OrbitDollyCursorMoveCameraInput::OrbitDollyCursorMoveCameraInput(const InputChannelId dollyChannelId)
: m_dollyChannelId(dollyChannelId)
{
}
void OrbitDollyCursorMoveCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
@@ -17,6 +17,8 @@
#include <AzCore/std/containers/variant.h>
#include <AzCore/std/optional.h>
#include <AzFramework/Input/Channels/InputChannel.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/CursorState.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzFramework/Viewport/ViewportId.h>
@@ -188,26 +190,21 @@ namespace AzFramework
Cameras m_cameras;
private:
CursorState m_cursorState;
float m_scrollDelta = 0.0f;
AZStd::optional<ScreenPoint> m_lastCursorPosition;
AZStd::optional<ScreenPoint> m_currentCursorPosition;
};
class RotateCameraInput : public CameraInput
{
public:
explicit RotateCameraInput(const InputChannelId rotateChannelId)
: m_rotateChannelId(rotateChannelId)
{
}
explicit RotateCameraInput(InputChannelId rotateChannelId);
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
private:
InputChannelId m_rotateChannelId;
float m_moveAccumulator = 0.0f;
bool m_tryingToBegin = false;
ClickDetector m_clickDetector;
};
struct PanAxes
@@ -240,11 +237,8 @@ namespace AzFramework
class PanCameraInput : public CameraInput
{
public:
PanCameraInput(const InputChannelId panChannelId, PanAxesFn panAxesFn)
: m_panAxesFn(AZStd::move(panAxesFn))
, m_panChannelId(panChannelId)
{
}
PanCameraInput(InputChannelId panChannelId, PanAxesFn panAxesFn);
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
@@ -283,10 +277,8 @@ namespace AzFramework
class TranslateCameraInput : public CameraInput
{
public:
explicit TranslateCameraInput(TranslationAxesFn translationAxesFn)
: m_translationAxesFn(AZStd::move(translationAxesFn))
{
}
explicit TranslateCameraInput(TranslationAxesFn translationAxesFn);
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
void ResetImpl() override;
@@ -363,8 +355,7 @@ namespace AzFramework
class OrbitDollyCursorMoveCameraInput : public CameraInput
{
public:
explicit OrbitDollyCursorMoveCameraInput(const InputChannelId dollyChannelId)
: m_dollyChannelId(dollyChannelId) {}
explicit OrbitDollyCursorMoveCameraInput(InputChannelId dollyChannelId);
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
@@ -0,0 +1,68 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
namespace AzFramework
{
ClickDetector::ClickOutcome ClickDetector::DetectClick(const ClickEvent clickEvent, const ScreenVector& cursorDelta)
{
if (clickEvent == ClickEvent::Down)
{
const auto now = std::chrono::steady_clock::now();
if (m_tryBeginTime)
{
const std::chrono::duration<float> diff = now - m_tryBeginTime.value();
if (diff.count() < m_doubleClickInterval)
{
return ClickOutcome::Nil;
}
}
m_detectionState = DetectionState::WaitingForMove;
m_moveAccumulator = 0.0f;
m_tryBeginTime = now;
}
else if (clickEvent == ClickEvent::Up)
{
const auto clickOutcome = [detectionState = m_detectionState] {
if (detectionState == DetectionState::WaitingForMove)
{
return ClickOutcome::Click;
}
if (detectionState == DetectionState::Moved)
{
return ClickOutcome::Release;
}
return ClickOutcome::Nil;
}();
m_detectionState = DetectionState::Nil;
return clickOutcome;
}
if (m_detectionState == DetectionState::WaitingForMove)
{
// only allow the action to begin if the mouse has been moved a small amount
m_moveAccumulator += ScreenVectorLength(cursorDelta);
if (m_moveAccumulator > m_deadZone)
{
m_detectionState = DetectionState::Moved;
return ClickOutcome::Move;
}
}
return ClickOutcome::Nil;
}
} // namespace AzFramework
@@ -0,0 +1,75 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/optional.h>
#include <chrono>
namespace AzFramework
{
struct ScreenVector;
//! Utility class to help detect different types of mouse click (mouse down and up with
//! no movement), mouse move (down and initial move after some threshold) and mouse release
//! (mouse down with movement and then mouse up).
class ClickDetector
{
//! Alias for recording time of mouse down events
using Time = std::chrono::time_point<std::chrono::steady_clock>;
public:
//! Internal representation of click event (map from external event for this when
//! calling DetectClick).
enum class ClickEvent
{
Nil,
Down,
Up
};
//! The type of mouse click.
enum class ClickOutcome
{
Nil, //!< Not recognized.
Move, //!< Initial move after mouse down.
Click, //!< Mouse down and up with no intermediate movement.
Release //!< Mouse down with movement and then mouse up.
};
//! Called from any type of 'handle event' function.
ClickOutcome DetectClick(ClickEvent clickEvent, const ScreenVector& cursorDelta);
void SetDoubleClickInterval(float doubleClickInterval);
private:
//! Internal state of ClickDetector based on incoming events.
enum class DetectionState
{
Nil, //!< Initial state
WaitingForMove, //! Mouse down has happened but mouse hasn't yet moved.
Moved //! Mouse has moved, no longer will be counted as a click.
};
float m_moveAccumulator = 0.0f; //!< How far the mouse has moved after mouse down.
float m_deadZone = 2.0f; //!< How far to move before a click is cancelled (when Move will fire).
float m_doubleClickInterval = 0.4f; //!< Default double click interval, can be overridden.
DetectionState m_detectionState; //!< Internal state of ClickDetector.
AZStd::optional<Time> m_tryBeginTime; //!< Mouse down time (happens each mouse down, helps with double click handling).
};
inline void ClickDetector::SetDoubleClickInterval(const float doubleClickInterval)
{
m_doubleClickInterval = doubleClickInterval;
}
} // namespace AzFramework
@@ -0,0 +1,56 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzCore/std/optional.h>
namespace AzFramework
{
//! Utility type to wrap a current and last cursor position.
struct CursorState
{
//! Returns the delta between the current and last cursor position.
[[nodiscard]] ScreenVector CursorDelta() const;
//! Call this in a 'handle event' call to update the most recent cursor position.
void SetCurrentPosition(const ScreenPoint& currentPosition);
//! Call this in an 'update' call to copy the current cursor position to the last
//! cursor position.
void Update();
private:
AZStd::optional<ScreenPoint> m_lastCursorPosition;
AZStd::optional<ScreenPoint> m_currentCursorPosition;
};
inline void CursorState::SetCurrentPosition(const ScreenPoint& currentPosition)
{
m_currentCursorPosition = currentPosition;
}
inline ScreenVector CursorState::CursorDelta() const
{
return m_currentCursorPosition.has_value() && m_lastCursorPosition.has_value()
? m_currentCursorPosition.value() - m_lastCursorPosition.value()
: ScreenVector(0, 0);
}
inline void CursorState::Update()
{
if (m_currentCursorPosition.has_value())
{
m_lastCursorPosition = m_currentCursorPosition;
}
}
} // namespace AzFramework
@@ -103,6 +103,9 @@ set(FILES
Viewport/CameraState.cpp
Viewport/CameraInput.h
Viewport/CameraInput.cpp
Viewport/ClickDetector.h
Viewport/ClickDetector.cpp
Viewport/CursorState.h
Viewport/DisplayContextRequestBus.h
Entity/BehaviorEntity.cpp
Entity/BehaviorEntity.h
@@ -237,16 +237,15 @@ namespace AzToolsFramework
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Alt();
}
static bool IndividualSelect(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
static bool IndividualSelect(const AzFramework::ClickDetector::ClickOutcome clickOutcome)
{
return mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down;
return clickOutcome == AzFramework::ClickDetector::ClickOutcome::Click;
}
static bool AdditiveIndividualSelect(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
static bool AdditiveIndividualSelect(
const AzFramework::ClickDetector::ClickOutcome clickOutcome, const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
return mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down &&
return clickOutcome == AzFramework::ClickDetector::ClickOutcome::Click &&
mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl() &&
!mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Alt();
}
@@ -1783,6 +1782,25 @@ namespace AzToolsFramework
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
const AzFramework::ClickDetector::ClickEvent selectClickEvent = [&mouseInteraction] {
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left())
{
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down)
{
return AzFramework::ClickDetector::ClickEvent::Down;
}
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Up)
{
return AzFramework::ClickDetector::ClickEvent::Up;
}
}
return AzFramework::ClickDetector::ClickEvent::Nil;
}();
m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
const auto clickOutcome = m_clickDetector.DetectClick(selectClickEvent, m_cursorState.CursorDelta());
// for entities selected with no bounds of their own (just TransformComponent)
// check selection against the selection indicator aabb
for (AZ::EntityId entityId : m_selectedEntityIds)
@@ -1841,7 +1859,7 @@ namespace AzToolsFramework
if (!m_selectedEntityIds.empty())
{
// select/deselect (add/remove) entities with ctrl held
if (Input::AdditiveIndividualSelect(mouseInteraction))
if (Input::AdditiveIndividualSelect(clickOutcome, mouseInteraction))
{
if (SelectDeselect(entityIdUnderCursor))
{
@@ -2023,7 +2041,7 @@ namespace AzToolsFramework
}
// standard toggle selection
if (Input::IndividualSelect(mouseInteraction))
if (Input::IndividualSelect(clickOutcome))
{
SelectDeselect(entityIdUnderCursor);
}
@@ -3267,6 +3285,8 @@ namespace AzToolsFramework
const auto modifiers = ViewportInteraction::KeyboardModifiers(
ViewportInteraction::TranslateKeyboardModifiers(QApplication::queryKeyboardModifiers()));
m_cursorState.Update();
HandleAccents(
!m_selectedEntityIds.empty(), m_cachedEntityIdUnderCursor,
modifiers.Ctrl(), m_hoveredEntityId,
@@ -17,6 +17,8 @@
#include <AzCore/std/optional.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Components/CameraBus.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/CursorState.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <AzToolsFramework/Commands/EntityManipulatorCommand.h>
#include <AzToolsFramework/ComponentMode/EditorComponentModeBus.h>
@@ -35,37 +37,37 @@ namespace AzToolsFramework
{
class EditorVisibleEntityDataCache;
using EntityIdSet = AZStd::unordered_set<AZ::EntityId>; ///< Alias for unordered_set of EntityIds.
using EntityIdSet = AZStd::unordered_set<AZ::EntityId>; //!< Alias for unordered_set of EntityIds.
/// Entity related data required by manipulators during action.
//! Entity related data required by manipulators during action.
struct EntityIdManipulatorLookup
{
AZ::Transform m_initial; /// Transform of Entity at mouse down on manipulator.
AZ::Transform m_initial; //!< Transform of Entity at mouse down on manipulator.
};
/// Alias for a mapping between EntityIds and Entity related data required by manipulators.
//! Alias for a mapping between EntityIds and Entity related data required by manipulators.
using EntityIdManipulatorLookups = AZStd::unordered_map<AZ::EntityId, EntityIdManipulatorLookup>;
/// Generic wrapper to handle specific manipulators controlling 1-* entities.
//! Generic wrapper to handle specific manipulators controlling 1-* entities.
struct EntityIdManipulators
{
EntityIdManipulatorLookups m_lookups; ///< Mapping between the EntityId and the transform of the Entity at
///< the point a manipulator started adjusting it.
AZStd::unique_ptr<Manipulators> m_manipulators; ///< The aggregate manipulator currently in use.
EntityIdManipulatorLookups m_lookups; //!< Mapping between the EntityId and the transform of the Entity at
//!< the point a manipulator started adjusting it.
AZStd::unique_ptr<Manipulators> m_manipulators; //!< The aggregate manipulator currently in use.
};
/// Store translation and orientation only (no scale).
//! Store translation and orientation only (no scale).
struct Frame
{
AZ::Vector3 m_translation = AZ::Vector3::CreateZero(); ///< Position of frame.
AZ::Quaternion m_orientation = AZ::Quaternion::CreateIdentity(); ///< Orientation of frame.
AZ::Vector3 m_translation = AZ::Vector3::CreateZero(); //!< Position of frame.
AZ::Quaternion m_orientation = AZ::Quaternion::CreateIdentity(); //!< Orientation of frame.
};
/// Temporary manipulator frame used during selection.
//! Temporary manipulator frame used during selection.
struct OptionalFrame
{
/// What part of the transform did we pick (when using ditto on
/// the manipulator). This will depend on the transform mode we're in.
//! What part of the transform did we pick (when using ditto on
//! the manipulator). This will depend on the transform mode we're in.
struct PickType
{
enum : AZ::u8
@@ -83,29 +85,29 @@ namespace AzToolsFramework
bool PickedTranslation() const;
bool PickedOrientation() const;
/// Clear all state associated with the frame.
//! Clear all state associated with the frame.
void Reset();
/// Clear only picked translation state.
//! Clear only picked translation state.
void ResetPickedTranslation();
/// Clear only picked orientation state.
//! Clear only picked orientation state.
void ResetPickedOrientation();
AZ::EntityId m_pickedEntityIdOverride; ///< 'Picked' Entity - frame and parent space relative to this if active.
AZStd::optional<AZ::Vector3> m_translationOverride; ///< Translation override, if set, reset when selection is empty.
AZStd::optional<AZ::Quaternion> m_orientationOverride; ///< Orientation override, if set, reset when selection is empty.
AZ::u8 m_pickTypes = PickType::None; ///< What mode(s) were we in when picking an EntityId override.
AZ::EntityId m_pickedEntityIdOverride; //!< 'Picked' Entity - frame and parent space relative to this if active.
AZStd::optional<AZ::Vector3> m_translationOverride; //!< Translation override, if set, reset when selection is empty.
AZStd::optional<AZ::Quaternion> m_orientationOverride; //!< Orientation override, if set, reset when selection is empty.
AZ::u8 m_pickTypes = PickType::None; //!< What mode(s) were we in when picking an EntityId override.
};
/// What frame/space is the manipulator currently operating in.
//! What frame/space is the manipulator currently operating in.
enum class ReferenceFrame
{
Local, /// The local space of the individual entity.
Parent, /// The parent space of the individual entity (world space if no parent exists).
World, /// World space (space aligned to world axes - identity).
Local, //!< The local space of the individual entity.
Parent, //!< The parent space of the individual entity (world space if no parent exists).
World, //!< World space (space aligned to world axes - identity).
};
/// Entity selection/interaction handling.
/// Provide a suite of functionality for manipulating entities, primarily through their TransformComponent.
//! Entity selection/interaction handling.
//! Provide a suite of functionality for manipulating entities, primarily through their TransformComponent.
class EditorTransformComponentSelection
: public ViewportInteraction::ViewportSelectionRequests
, private EditorEventsBus::Handler
@@ -127,15 +129,15 @@ namespace AzToolsFramework
EditorTransformComponentSelection& operator=(const EditorTransformComponentSelection&) = delete;
virtual ~EditorTransformComponentSelection();
/// Register entity manipulators with the ManipulatorManager.
/// After being registered, the entity manipulators will draw and check for input.
//! Register entity manipulators with the ManipulatorManager.
//! After being registered, the entity manipulators will draw and check for input.
void RegisterManipulator();
/// Unregister entity manipulators with the ManipulatorManager.
/// No longer draw or respond to input.
//! Unregister entity manipulators with the ManipulatorManager.
//! No longer draw or respond to input.
void UnregisterManipulator();
/// ViewportInteraction::ViewportSelectionRequests
/// Intercept all viewport mouse events and respond to inputs.
//! ViewportInteraction::ViewportSelectionRequests
//! Intercept all viewport mouse events and respond to inputs.
bool HandleMouseInteraction(
const ViewportInteraction::MouseInteractionEvent& mouseInteraction) override;
void DisplayViewportSelection(
@@ -145,9 +147,9 @@ namespace AzToolsFramework
const AzFramework::ViewportInfo& viewportInfo,
AzFramework::DebugDisplayRequests& debugDisplay) override;
/// Add an entity to the current selection
//! Add an entity to the current selection
void AddEntityToSelection(AZ::EntityId entityId);
/// Remove an entity from the current selection
//! Remove an entity from the current selection
void RemoveEntityFromSelection(AZ::EntityId entityId);
private:
@@ -161,8 +163,8 @@ namespace AzToolsFramework
void ClearManipulatorTranslationOverride();
void ClearManipulatorOrientationOverride();
/// Handle an event triggered by the user to clear any manipulator overrides.
/// Delegate to either translation or orientation reset/clear depending on the state we're in.
//! Handle an event triggered by the user to clear any manipulator overrides.
//! Delegate to either translation or orientation reset/clear depending on the state we're in.
void DelegateClearManipulatorOverride();
void ToggleCenterPivotSelection();
@@ -251,63 +253,65 @@ namespace AzToolsFramework
void SetEntityLocalScale(AZ::EntityId entityId, const AZ::Vector3& localScale);
void SetEntityLocalRotation(AZ::EntityId entityId, const AZ::Vector3& localRotation);
AZ::EntityId m_hoveredEntityId; ///< What EntityId is the mouse currently hovering over (if any).
AZ::EntityId m_cachedEntityIdUnderCursor; ///< Store the EntityId on each mouse move for use in Display.
AZ::EntityId m_editorCameraComponentEntityId; ///< The EditorCameraComponent EntityId if it is set.
EntityIdSet m_selectedEntityIds; ///< Represents the current entities in the selection.
AZ::EntityId m_hoveredEntityId; //!< What EntityId is the mouse currently hovering over (if any).
AZ::EntityId m_cachedEntityIdUnderCursor; //!< Store the EntityId on each mouse move for use in Display.
AZ::EntityId m_editorCameraComponentEntityId; //!< The EditorCameraComponent EntityId if it is set.
EntityIdSet m_selectedEntityIds; //!< Represents the current entities in the selection.
const EditorVisibleEntityDataCache* m_entityDataCache = nullptr; ///< A cache of packed EntityData that can be
///< iterated over efficiently without the need
///< to make individual EBus calls.
AZStd::unique_ptr<EditorHelpers> m_editorHelpers; ///< Editor visualization of entities (icons, shapes, debug visuals etc).
EntityIdManipulators m_entityIdManipulators; ///< Mapping from a Manipulator to potentially many EntityIds.
const EditorVisibleEntityDataCache* m_entityDataCache = nullptr; //!< A cache of packed EntityData that can be
//!< iterated over efficiently without the need
//!< to make individual EBus calls.
AZStd::unique_ptr<EditorHelpers> m_editorHelpers; //!< Editor visualization of entities (icons, shapes, debug visuals etc).
EntityIdManipulators m_entityIdManipulators; //!< Mapping from a Manipulator to potentially many EntityIds.
EditorBoxSelect m_boxSelect; ///< Type responsible for handling box select.
AZStd::unique_ptr<EntityManipulatorCommand> m_manipulatorMoveCommand; ///< Track adjustments to manipulator translation and orientation (during mouse press/move).
AZStd::vector<AZStd::unique_ptr<QAction>> 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.
OptionalFrame m_pivotOverrideFrame; ///< Has a pivot override been set.
Mode m_mode = Mode::Translation; ///< Manipulator mode - default to translation.
Pivot m_pivotMode = Pivot::Object; ///< Entity pivot mode - default to object (authored root).
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).
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.
ViewportUi::ButtonId m_rotateButtonId; ///< Id of the Viewport UI button for rotate mode.
ViewportUi::ButtonId m_scaleButtonId; ///< Id of the Viewport UI button for scale mode.
AZ::Event<ViewportUi::ButtonId>::Handler m_transformModeSelectionHandler; ///< Event handler for the Viewport UI cluster.
EditorBoxSelect m_boxSelect; //!< Type responsible for handling box select.
AZStd::unique_ptr<EntityManipulatorCommand> m_manipulatorMoveCommand; //!< Track adjustments to manipulator translation and orientation (during mouse press/move).
AZStd::vector<AZStd::unique_ptr<QAction>> 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.
OptionalFrame m_pivotOverrideFrame; //!< Has a pivot override been set.
Mode m_mode = Mode::Translation; //!< Manipulator mode - default to translation.
Pivot m_pivotMode = Pivot::Object; //!< Entity pivot mode - default to object (authored root).
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).
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.
ViewportUi::ButtonId m_rotateButtonId; //!< Id of the Viewport UI button for rotate mode.
ViewportUi::ButtonId m_scaleButtonId; //!< Id of the Viewport UI button for scale mode.
AZ::Event<ViewportUi::ButtonId>::Handler m_transformModeSelectionHandler; //!< Event handler for the Viewport UI cluster.
AzFramework::ClickDetector m_clickDetector; //!< Detect different types of mouse click.
AzFramework::CursorState m_cursorState; //!< Track the mouse position and delta movement each frame.
};
/// The ETCS (EntityTransformComponentSelection) namespace contains functions and data used exclusively by
/// the EditorTransformComponentSelection type. Functions in this namespace are exposed to facilitate testing
/// and should not be used outside of EditorTransformComponentSelection or EditorTransformComponentSelectionTests.
//! The ETCS (EntityTransformComponentSelection) namespace contains functions and data used exclusively by
//! the EditorTransformComponentSelection type. Functions in this namespace are exposed to facilitate testing
//! and should not be used outside of EditorTransformComponentSelection or EditorTransformComponentSelectionTests.
namespace ETCS
{
/// The result from calculating the entity (transform component) orientation.
/// Does the entity have a parent or not, and what orientation should the manipulator have when
/// displayed at the object pivot (determined by the entity hierarchy and what modifiers are held).
//! The result from calculating the entity (transform component) orientation.
//! Does the entity have a parent or not, and what orientation should the manipulator have when
//! displayed at the object pivot (determined by the entity hierarchy and what modifiers are held).
struct PivotOrientationResult
{
AZ::Quaternion m_worldOrientation;
AZ::EntityId m_parentId;
};
/// Calculate the orientation for an individual entity based on the incoming reference frame.
/// Note: If the entity is in a hierarchy the Parent reference frame will return the orientation of the parent.
//! Calculate the orientation for an individual entity based on the incoming reference frame.
//! Note: If the entity is in a hierarchy the Parent reference frame will return the orientation of the parent.
PivotOrientationResult CalculatePivotOrientation(AZ::EntityId entityId, ReferenceFrame referenceFrame);
/// Calculate the orientation for a group of entities based on the incoming reference frame.
//! Calculate the orientation for a group of entities based on the incoming reference frame.
template<typename EntityIdMap>
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.
//! Calculate the orientation for a group of entities based on the incoming
//! reference frame with possible pivot override.
template<typename EntityIdMap>
PivotOrientationResult CalculateSelectionPivotOrientation(
const EntityIdMap& entityIdMap, const OptionalFrame& pivotOverrideFrame,
+142
View File
@@ -0,0 +1,142 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
namespace AzFramework
{
std::ostream& operator<<(std::ostream& os, const ClickDetector::ClickOutcome clickOutcome)
{
switch (clickOutcome)
{
case ClickDetector::ClickOutcome::Click:
os << "ClickOutcome::Click";
break;
case ClickDetector::ClickOutcome::Move:
os << "ClickOutcome::Move";
break;
case ClickDetector::ClickOutcome::Release:
os << "ClickOutcome::Release";
break;
case ClickDetector::ClickOutcome::Nil:
os << "ClickOutcome::Nil";
break;
}
return os;
}
} // namespace AzFramework
namespace UnitTest
{
using AzFramework::ClickDetector;
using AzFramework::ScreenVector;
class ClickDetectorFixture : public ::testing::Test
{
public:
ClickDetector m_clickDetector;
};
TEST_F(ClickDetectorFixture, ClickIsDetectedWithNoMouseMovementOnMouseUp)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome initialUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
}
TEST_F(ClickDetectorFixture, MoveIsDetectedWithMouseMovementAfterMouseDown)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome initialMoveOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Nil, ScreenVector(10, 10));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialMoveOutcome, Eq(ClickDetector::ClickOutcome::Move));
}
TEST_F(ClickDetectorFixture, ReleaseIsDetectedAfterMouseMovementOnMouseUp)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
// move
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Nil, ScreenVector(10, 10));
const ClickDetector::ClickOutcome initialUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialUpOutcome, Eq(ClickDetector::ClickOutcome::Release));
}
TEST_F(ClickDetectorFixture, MoveIsReturnedOnlyAfterFirstMouseMove)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome initialMoveOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Nil, ScreenVector(10, 10));
const ClickDetector::ClickOutcome secondaryMoveOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Nil, ScreenVector(10, 10));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialMoveOutcome, Eq(ClickDetector::ClickOutcome::Move));
EXPECT_THAT(secondaryMoveOutcome, Eq(ClickDetector::ClickOutcome::Nil));
}
TEST_F(ClickDetectorFixture, ClickIsNotRegisteredAfterDoubleClick)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome initialUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondaryDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondaryUpOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
EXPECT_THAT(secondaryDownOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // double click
EXPECT_THAT(secondaryUpOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // click not registered
}
TEST_F(ClickDetectorFixture, ClickIsNotRegisteredAfterIgnoredDoubleClick)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome initialDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome initialUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondaryDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Nil, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondaryUpOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(initialDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(initialUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
EXPECT_THAT(secondaryDownOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // ignored double click
EXPECT_THAT(secondaryUpOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // click not registered
}
} // namespace UnitTest
+54
View File
@@ -0,0 +1,54 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/CursorState.h>
namespace UnitTest
{
using AzFramework::CursorState;
using AzFramework::ScreenVector;
using AzFramework::ScreenPoint;
class CursorStateFixture : public ::testing::Test
{
public:
CursorState m_cursorState;
};
TEST_F(CursorStateFixture, CursorStateHasZeroDeltaInitially)
{
using ::testing::Eq;
EXPECT_THAT(m_cursorState.CursorDelta(), Eq(ScreenVector(0, 0)));
}
TEST_F(CursorStateFixture, CursorStateReturnsZeroDeltaAfterSingleMoveAndUpdate)
{
using ::testing::Eq;
m_cursorState.SetCurrentPosition(ScreenPoint(10, 10));
m_cursorState.Update();
EXPECT_THAT(m_cursorState.CursorDelta(), Eq(ScreenVector(0, 0)));
}
TEST_F(CursorStateFixture, CursorStateReturnsDeltaAfterSecondMoveAndUpdate)
{
using ::testing::Eq;
m_cursorState.SetCurrentPosition(ScreenPoint(10, 10));
m_cursorState.Update();
m_cursorState.SetCurrentPosition(ScreenPoint(15, 22));
EXPECT_THAT(m_cursorState.CursorDelta(), Eq(ScreenVector(5, 12)));
}
} // namespace UnitTest
@@ -17,6 +17,8 @@ set(FILES
BinToTextEncode.cpp
ComponentAddRemove.cpp
ComponentAdapterTests.cpp
ClickDetectorTests.cpp
CursorStateTests.cpp
EntityContext.cpp
EntityTestbed.h
FileFunc.cpp