Переглянути джерело

Merge pull request #14047 from aws-lumberyard-dev/shape-offset-part-nine

Shape translation offset manipulators
greerdv 2 роки тому
батько
коміт
8dedfe8c43
31 змінених файлів з 931 додано та 118 видалено
  1. 2 0
      Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h
  2. 3 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.cpp
  3. 4 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.h
  4. 24 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseViewportEdit.h
  5. 318 4
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.cpp
  6. 28 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.h
  7. 19 13
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.cpp
  8. 8 11
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.h
  9. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.cpp
  10. 40 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeComponentModeBus.h
  11. 96 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.cpp
  12. 31 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h
  13. 25 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ViewportEditUtilities.h
  14. 0 6
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h
  15. 37 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h
  16. 6 0
      Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake
  17. 13 1
      Gems/LmbrCentral/Code/Source/LmbrCentralEditor.cpp
  18. 3 0
      Gems/LmbrCentral/Code/Source/Shape/EditorAxisAlignedBoxShapeComponent.cpp
  19. 5 1
      Gems/LmbrCentral/Code/Source/Shape/EditorAxisAlignedBoxShapeComponent.h
  20. 3 0
      Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.cpp
  21. 5 2
      Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.h
  22. 120 19
      Gems/LmbrCentral/Code/Tests/EditorAxisAlignedBoxShapeComponentTests.cpp
  23. 61 29
      Gems/LmbrCentral/Code/Tests/EditorBoxShapeComponentTests.cpp
  24. 40 7
      Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.cpp
  25. 14 4
      Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.h
  26. 9 4
      Gems/PhysX/Code/Editor/ColliderBoxMode.cpp
  27. 3 1
      Gems/PhysX/Code/Editor/ColliderBoxMode.h
  28. 1 1
      Gems/PhysX/Code/Editor/ColliderSphereMode.cpp
  29. 3 0
      Gems/PhysX/Code/Source/EditorColliderComponent.cpp
  30. 5 1
      Gems/PhysX/Code/Source/EditorColliderComponent.h
  31. 4 4
      Gems/PhysX/Code/Tests/PhysXColliderComponentModeTests.cpp

+ 2 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h

@@ -124,6 +124,8 @@ namespace AzToolsFramework
 
         /*!
          * Fired just after applying a requested undo or redo operation.
+         * Note that prefab propagation will not have occurred at this point, so data may not yet be updated.
+         * Consider listening to OnPrefabInstancePropagationEnd on PrefabPublicNotificationBus instead.
          */
         virtual void AfterUndoRedo() {}
 

+ 3 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.cpp

@@ -32,12 +32,12 @@ namespace AzToolsFramework
             }
 
             ComponentModeRequestBus::Handler::BusConnect(m_entityComponentIdPair);
-            ToolsApplicationNotificationBus::Handler::BusConnect();
+            Prefab::PrefabPublicNotificationBus::Handler::BusConnect();
         }
 
         EditorBaseComponentMode::~EditorBaseComponentMode()
         {
-            ToolsApplicationNotificationBus::Handler::BusDisconnect();
+            Prefab::PrefabPublicNotificationBus::Handler::BusDisconnect();
             ComponentModeRequestBus::Handler::BusDisconnect();
         }
 
@@ -49,7 +49,7 @@ namespace AzToolsFramework
             }
         }
 
-        void EditorBaseComponentMode::AfterUndoRedo()
+        void EditorBaseComponentMode::OnPrefabInstancePropagationEnd()
         {
             Refresh();
         }

+ 4 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.h

@@ -9,6 +9,7 @@
 #pragma once
 
 #include <AzToolsFramework/ComponentMode/EditorComponentModeBus.h>
+#include <AzToolsFramework/Prefab/PrefabPublicNotificationBus.h>
 #include <AzToolsFramework/Viewport/ActionBus.h>
 
 namespace AzToolsFramework
@@ -20,7 +21,7 @@ namespace AzToolsFramework
         /// functionality all ComponentModes require.
         class EditorBaseComponentMode
             : public ComponentModeRequestBus::Handler
-            , protected ToolsApplicationNotificationBus::Handler
+            , public Prefab::PrefabPublicNotificationBus::Handler
         {
         public:
             AZ_CLASS_ALLOCATOR_DECL
@@ -89,8 +90,8 @@ namespace AzToolsFramework
             /// @see To be overridden by derived ComponentModes
             virtual AZStd::vector<ActionOverride> PopulateActionsImpl();
 
-            // ToolsApplicationNotificationBus
-            void AfterUndoRedo() override;
+            // PrefabPublicNotificationBus overrides ...
+            void OnPrefabInstancePropagationEnd() override;
 
             AZ::EntityComponentIdPair m_entityComponentIdPair; ///< Entity and Component Id associated with this ComponentMode.
             AZ::Uuid m_componentType; ///< The underlying type of the Component this ComponentMode is for.

+ 24 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseViewportEdit.h

@@ -0,0 +1,24 @@
+/*
+ * 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
+
+namespace AzToolsFramework
+{
+    //! Base class for objects used in sub component modes which use viewport editing.
+    class BaseViewportEdit
+    {
+    public:
+        virtual ~BaseViewportEdit() = default;
+
+        virtual void Setup(const AZ::EntityComponentIdPair& entityComponentIdPair) = 0;
+        virtual void Teardown() = 0;
+        virtual void UpdateManipulators() = 0;
+        virtual void ResetValues() = 0;
+    };
+} // namespace AzToolsFramework

+ 318 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.cpp

@@ -7,31 +7,253 @@
  */
 
 #include "BoxComponentMode.h"
+#include <AzToolsFramework/API/ComponentModeCollectionInterface.h>
+#include <AzToolsFramework/ActionManager/Action/ActionManagerInterface.h>
+#include <AzToolsFramework/ActionManager/HotKey/HotKeyManagerInterface.h>
+#include <AzToolsFramework/ActionManager/Menu/MenuManagerInterface.h>
 
 namespace AzToolsFramework
 {
+    static constexpr AZStd::string_view EditorMainWindowActionContextIdentifier = "o3de.context.editor.mainwindow";
+    static constexpr AZStd::string_view EditMenuIdentifier = "o3de.menu.editor.edit";
+
+    namespace
+    {
+        //! Uri's for shortcut actions.
+        const AZ::Crc32 SetDimensionsSubModeActionUri = AZ_CRC_CE("org.o3de.action.box.setdimensionssubmode");
+        const AZ::Crc32 SetTranslationOffsetSubModeActionUri = AZ_CRC_CE("org.o3de.action.box.settranslationoffsetsubmode");
+        const AZ::Crc32 ResetSubModeActionUri = AZ_CRC_CE("org.o3de.action.box.resetsubmode");
+    } // namespace
+
     AZ_CLASS_ALLOCATOR_IMPL(BoxComponentMode, AZ::SystemAllocator, 0)
 
     void BoxComponentMode::Reflect(AZ::ReflectContext* context)
     {
-        AzToolsFramework::ComponentModeFramework::ReflectEditorBaseComponentModeDescendant<BoxComponentMode>(context);
+        ComponentModeFramework::ReflectEditorBaseComponentModeDescendant<BoxComponentMode>(context);
     }
 
     BoxComponentMode::BoxComponentMode(
         const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid componentType, bool allowAsymmetricalEditing)
         : EditorBaseComponentMode(entityComponentIdPair, componentType)
+        , m_entityComponentIdPair(entityComponentIdPair)
+        , m_allowAsymmetricalEditing(allowAsymmetricalEditing)
+    {
+        m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)] =
+            AZStd::make_unique<BoxViewportEdit>(m_allowAsymmetricalEditing);
+        if (m_allowAsymmetricalEditing)
+        {
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::TranslationOffset)] =
+                AZStd::make_unique<ShapeTranslationOffsetViewportEdit>();
+            SetupCluster();
+            SetShapeSubMode(ShapeComponentModeRequests::SubMode::Dimensions);
+        }
+        else
+        {
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->Setup(entityComponentIdPair);
+        }
+        ShapeComponentModeRequestBus::Handler::BusConnect(m_entityComponentIdPair);
+    }
+
+    AZStd::vector<AzToolsFramework::ActionOverride> BoxComponentMode::PopulateActionsImpl()
+    {
+        if (!m_allowAsymmetricalEditing)
+        {
+            return {};
+        }
+
+        AzToolsFramework::ActionOverride setDimensionsModeAction;
+        setDimensionsModeAction.SetUri(SetDimensionsSubModeActionUri);
+        setDimensionsModeAction.SetKeySequence(QKeySequence(Qt::Key_1));
+        setDimensionsModeAction.SetTitle("Set Dimensions Mode");
+        setDimensionsModeAction.SetTip("Set dimensions mode");
+        setDimensionsModeAction.SetEntityComponentIdPair(GetEntityComponentIdPair());
+        setDimensionsModeAction.SetCallback(
+            [this]()
+            {
+                SetShapeSubMode(SubMode::Dimensions);
+            });
+
+        AzToolsFramework::ActionOverride setTranslationOffsetModeAction;
+        setTranslationOffsetModeAction.SetUri(SetTranslationOffsetSubModeActionUri);
+        setTranslationOffsetModeAction.SetKeySequence(QKeySequence(Qt::Key_2));
+        setTranslationOffsetModeAction.SetTitle("Set Translation Offset Mode");
+        setTranslationOffsetModeAction.SetTip("Set translation offset mode");
+        setTranslationOffsetModeAction.SetEntityComponentIdPair(GetEntityComponentIdPair());
+        setTranslationOffsetModeAction.SetCallback(
+            [this]()
+            {
+                SetShapeSubMode(SubMode::TranslationOffset);
+            });
+
+        AzToolsFramework::ActionOverride resetModeAction;
+        resetModeAction.SetUri(ResetSubModeActionUri);
+        resetModeAction.SetKeySequence(QKeySequence(Qt::Key_R));
+        resetModeAction.SetTitle("Reset Current Mode");
+        resetModeAction.SetTip("Reset current mode");
+        resetModeAction.SetEntityComponentIdPair(GetEntityComponentIdPair());
+        resetModeAction.SetCallback(
+            [this]()
+            {
+                ResetShapeSubMode();
+            });
+
+        return { setDimensionsModeAction, setTranslationOffsetModeAction, resetModeAction };
+    }
+
+    void BoxComponentMode::RegisterActions()
+    {
+        auto actionManagerInterface = AZ::Interface<AzToolsFramework::ActionManagerInterface>::Get();
+        AZ_Assert(actionManagerInterface, "BoxComponentMode - could not get ActionManagerInterface on RegisterActions.");
+
+        auto hotKeyManagerInterface = AZ::Interface<AzToolsFramework::HotKeyManagerInterface>::Get();
+        AZ_Assert(hotKeyManagerInterface, "BoxComponentMode - could not get HotKeyManagerInterface on RegisterActions.");
+
+        // Dimensions sub-mode
+        {
+            constexpr AZStd::string_view actionIdentifier = "o3de.action.boxComponentMode.setDimensionsSubMode";
+            AzToolsFramework::ActionProperties actionProperties;
+            actionProperties.m_name = "Set Dimensions Mode";
+            actionProperties.m_description = "Set Dimensions Mode";
+            actionProperties.m_category = "Box Component Mode";
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<AzToolsFramework::ComponentModeCollectionInterface>::Get();
+                    AZ_Assert(componentModeCollectionInterface, "Could not retrieve component mode collection.");
+
+                    componentModeCollectionInterface->EnumerateActiveComponents(
+                        [](const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid&)
+                        {
+                            ShapeComponentModeRequestBus::Event(
+                                entityComponentIdPair,
+                                &ShapeComponentModeRequests::SetShapeSubMode,
+                                ShapeComponentModeRequests::SubMode::Dimensions);
+                        });
+                });
+
+            hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "1");
+        }
+
+        // Translation offset sub-mode
+        {
+            constexpr AZStd::string_view actionIdentifier = "o3de.action.boxComponentMode.setTranslationOffsetSubMode";
+            AzToolsFramework::ActionProperties actionProperties;
+            actionProperties.m_name = "Set Translation Offset Mode";
+            actionProperties.m_description = "Set Translation Offset Mode";
+            actionProperties.m_category = "Box Component Mode";
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<AzToolsFramework::ComponentModeCollectionInterface>::Get();
+                    AZ_Assert(componentModeCollectionInterface, "Could not retrieve component mode collection.");
+
+                    componentModeCollectionInterface->EnumerateActiveComponents(
+                        [](const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid&)
+                        {
+                            ShapeComponentModeRequestBus::Event(
+                                entityComponentIdPair,
+                                &ShapeComponentModeRequests::SetShapeSubMode,
+                                ShapeComponentModeRequests::SubMode::TranslationOffset);
+                        });
+                });
+
+            hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "2");
+        }
+
+        // Reset current mode
+        {
+            constexpr AZStd::string_view actionIdentifier = "o3de.action.boxComponentMode.resetCurrentMode";
+            AzToolsFramework::ActionProperties actionProperties;
+            actionProperties.m_name = "Reset Current Mode";
+            actionProperties.m_description = "Reset Current Mode";
+            actionProperties.m_category = "Box Component Mode";
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<AzToolsFramework::ComponentModeCollectionInterface>::Get();
+                    AZ_Assert(componentModeCollectionInterface, "Could not retrieve component mode collection.");
+
+                    componentModeCollectionInterface->EnumerateActiveComponents(
+                        [](const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid&)
+                        {
+                            ShapeComponentModeRequestBus::Event(entityComponentIdPair, &ShapeComponentModeRequests::ResetShapeSubMode);
+                        });
+                });
+
+            hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "R");
+        }
+    }
+
+    void BoxComponentMode::BindActionsToModes()
+    {
+        auto actionManagerInterface = AZ::Interface<AzToolsFramework::ActionManagerInterface>::Get();
+        AZ_Assert(actionManagerInterface, "BoxComponentMode - could not get ActionManagerInterface on RegisterActions.");
+
+        AZ::SerializeContext* serializeContext = nullptr;
+        AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
+
+        AZStd::string modeIdentifier =
+            AZStd::string::format("o3de.context.mode.%s", serializeContext->FindClassData(azrtti_typeid<BoxComponentMode>())->m_name);
+
+        actionManagerInterface->AssignModeToAction(modeIdentifier, "o3de.action.boxComponentMode.setDimensionsSubMode");
+        actionManagerInterface->AssignModeToAction(modeIdentifier, "o3de.action.boxComponentMode.setTranslationOffsetSubMode");
+        actionManagerInterface->AssignModeToAction(modeIdentifier, "o3de.action.boxComponentMode.resetCurrentMode");
+    }
+
+    void BoxComponentMode::BindActionsToMenus()
+    {
+        auto menuManagerInterface = AZ::Interface<AzToolsFramework::MenuManagerInterface>::Get();
+        AZ_Assert(menuManagerInterface, "BoxComponentMode - could not get MenuManagerInterface on BindActionsToMenus.");
+
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, "o3de.action.boxComponentMode.setDimensionsSubMode", 6000);
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, "o3de.action.boxComponentMode.setTranslationOffsetSubMode", 6001);
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, "o3de.action.boxComponentMode.resetCurrentMode", 6002);
+    }
+
+    static ViewportUi::ButtonId RegisterClusterButton(
+        AZ::s32 viewportId, ViewportUi::ClusterId clusterId, const char* iconName, const char* tooltip)
     {
-        m_boxEdit.Setup(entityComponentIdPair, allowAsymmetricalEditing);
+        ViewportUi::ButtonId buttonId;
+        ViewportUi::ViewportUiRequestBus::EventResult(
+            buttonId,
+            viewportId,
+            &ViewportUi::ViewportUiRequestBus::Events::CreateClusterButton,
+            clusterId,
+            AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s.svg", iconName));
+
+        ViewportUi::ViewportUiRequestBus::Event(
+            viewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, clusterId, buttonId, tooltip);
+
+        return buttonId;
     }
 
     BoxComponentMode::~BoxComponentMode()
     {
-        m_boxEdit.Teardown();
+        ShapeComponentModeRequestBus::Handler::BusDisconnect();
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->Teardown();
+        if (m_allowAsymmetricalEditing)
+        {
+            ViewportUi::ViewportUiRequestBus::Event(
+                ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveCluster, m_clusterId);
+            m_clusterId = ViewportUi::InvalidClusterId;
+        }
     }
 
     void BoxComponentMode::Refresh()
     {
-        m_boxEdit.UpdateManipulators();
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->UpdateManipulators();
     }
 
     AZStd::string BoxComponentMode::GetComponentModeName() const
@@ -43,4 +265,96 @@ namespace AzToolsFramework
     {
         return azrtti_typeid<BoxComponentMode>();
     }
+
+    void BoxComponentMode::SetupCluster()
+    {
+        ViewportUi::ViewportUiRequestBus::EventResult(
+            m_clusterId,
+            ViewportUi::DefaultViewportId,
+            &ViewportUi::ViewportUiRequestBus::Events::CreateCluster,
+            ViewportUi::Alignment::TopLeft);
+
+        m_buttonIds[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)] =
+            RegisterClusterButton(ViewportUi::DefaultViewportId, m_clusterId, "Scale", DimensionsTooltip);
+        m_buttonIds[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::TranslationOffset)] =
+            RegisterClusterButton(ViewportUi::DefaultViewportId, m_clusterId, "Move", TranslationOffsetTooltip);
+
+        const auto onButtonClicked = [this](ViewportUi::ButtonId buttonId)
+        {
+            if (buttonId == m_buttonIds[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)])
+            {
+                SetShapeSubMode(ShapeComponentModeRequests::SubMode::Dimensions);
+            }
+            else if (buttonId == m_buttonIds[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::TranslationOffset)])
+            {
+                SetShapeSubMode(ShapeComponentModeRequests::SubMode::TranslationOffset);
+            }
+        };
+
+        m_modeSelectionHandler = AZ::Event<ViewportUi::ButtonId>::Handler(onButtonClicked);
+        ViewportUi::ViewportUiRequestBus::Event(
+            ViewportUi::DefaultViewportId,
+            &ViewportUi::ViewportUiRequestBus::Events::RegisterClusterEventHandler,
+            m_clusterId,
+            m_modeSelectionHandler);
+    }
+
+    ShapeComponentModeRequests::SubMode BoxComponentMode::GetShapeSubMode() const
+    {
+        return m_subMode;
+    }
+
+    void BoxComponentMode::SetShapeSubMode(ShapeComponentModeRequests::SubMode mode)
+    {
+        AZ_Assert(mode < ShapeComponentModeRequests::SubMode::NumModes, "Submode not found:%d", static_cast<AZ::u32>(mode));
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->Teardown();
+        const auto modeIndex = static_cast<AZ::u32>(mode);
+        AZ_Assert(modeIndex < m_buttonIds.size(), "Invalid mode index %i.", modeIndex);
+        m_subMode = mode;
+        m_subModes[modeIndex]->Setup(m_entityComponentIdPair);
+
+        ViewportUi::ViewportUiRequestBus::Event(
+            ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::ClearClusterActiveButton, m_clusterId);
+
+        AzToolsFramework::ViewportUi::ViewportUiRequestBus::Event(
+            ViewportUi::DefaultViewportId,
+            &AzToolsFramework::ViewportUi::ViewportUiRequestBus::Events::SetClusterActiveButton,
+            m_clusterId,
+            m_buttonIds[modeIndex]);
+    }
+
+    void BoxComponentMode::ResetShapeSubMode()
+    {
+        UndoSystem::URSequencePoint* undoBatch = nullptr;
+        ToolsApplicationRequests::Bus::BroadcastResult(
+            undoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Reset box component sub mode");
+        ToolsApplicationRequests::Bus::Broadcast(
+            &ToolsApplicationRequests::Bus::Events::AddDirtyEntity, m_entityComponentIdPair.GetEntityId());
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->ResetValues();
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->UpdateManipulators();
+        AzToolsFramework::ToolsApplicationNotificationBus::Broadcast(
+            &AzToolsFramework::ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, AzToolsFramework::Refresh_Values);
+        ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::Bus::Events::EndUndoBatch);
+    }
+
+    bool BoxComponentMode::HandleMouseInteraction(const AzToolsFramework::ViewportInteraction::MouseInteractionEvent& mouseInteraction)
+    {
+        if (!m_allowAsymmetricalEditing)
+        {
+            return false;
+        }
+
+        if (mouseInteraction.m_mouseEvent == AzToolsFramework::ViewportInteraction::MouseEvent::Wheel &&
+            mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl())
+        {
+            const int direction = MouseWheelDelta(mouseInteraction) > 0.0f ? -1 : 1;
+            AZ::u32 currentModeIndex = static_cast<AZ::u32>(m_subMode);
+            AZ::u32 numSubModes = static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::NumModes);
+            AZ::u32 nextModeIndex = (currentModeIndex + numSubModes + direction) % m_subModes.size();
+            ShapeComponentModeRequests::SubMode nextMode = static_cast<ShapeComponentModeRequests::SubMode>(nextModeIndex);
+            SetShapeSubMode(nextMode);
+            return true;
+        }
+        return false;
+    }
 } // namespace AzToolsFramework

+ 28 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.h

@@ -10,14 +10,17 @@
 
 #include <AzToolsFramework/ComponentMode/EditorBaseComponentMode.h>
 #include <AzToolsFramework/ComponentModes/BoxViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ShapeComponentModeBus.h>
+#include <AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h>
 
 namespace AzToolsFramework
 {
     class LinearManipulator;
 
-    /// The specific ComponentMode responsible for handling box editing.
+    //! The specific ComponentMode responsible for handling box editing.
     class BoxComponentMode
         : public ComponentModeFramework::EditorBaseComponentMode
+        , public ShapeComponentModeRequestBus::Handler
     {
     public:
         AZ_CLASS_ALLOCATOR_DECL
@@ -25,6 +28,10 @@ namespace AzToolsFramework
 
         static void Reflect(AZ::ReflectContext* context);
 
+        static void RegisterActions();
+        static void BindActionsToModes();
+        static void BindActionsToMenus();
+
         BoxComponentMode(
             const AZ::EntityComponentIdPair& entityComponentIdPair, AZ::Uuid componentType, bool allowAsymmetricalEditing = false);
         BoxComponentMode(const BoxComponentMode&) = delete;
@@ -33,12 +40,30 @@ namespace AzToolsFramework
         BoxComponentMode& operator=(BoxComponentMode&&) = delete;
         ~BoxComponentMode();
 
-        // EditorBaseComponentMode
+        // EditorBaseComponentMode overrides ...
         void Refresh() override;
+        AZStd::vector<AzToolsFramework::ActionOverride> PopulateActionsImpl() override;
         AZStd::string GetComponentModeName() const override;
         AZ::Uuid GetComponentModeType() const override;
+        bool HandleMouseInteraction(const AzToolsFramework::ViewportInteraction::MouseInteractionEvent& mouseInteraction) override;
+
+        // ShapeComponentModeRequestBus overrides ...
+        ShapeComponentModeRequests::SubMode GetShapeSubMode() const override;
+        void SetShapeSubMode(ShapeComponentModeRequests::SubMode mode) override;
+        void ResetShapeSubMode() override;
+
+        constexpr static const char* const DimensionsTooltip = "Switch to dimensions mode";
+        constexpr static const char* const TranslationOffsetTooltip = "Switch to translation offset mode";
 
     private:
-        BoxViewportEdit m_boxEdit;
+        void SetupCluster();
+
+        ViewportUi::ClusterId m_clusterId; //! Id for viewport cluster used to switch between modes.
+        AZStd::array<ViewportUi::ButtonId, 2> m_buttonIds;
+        AZStd::array<AZStd::unique_ptr<BaseViewportEdit>, 2> m_subModes;
+        ShapeComponentModeRequests::SubMode m_subMode = ShapeComponentModeRequests::SubMode::Dimensions;
+        bool m_allowAsymmetricalEditing = false;
+        AZ::Event<AzToolsFramework::ViewportUi::ButtonId>::Handler m_modeSelectionHandler;
+        AZ::EntityComponentIdPair m_entityComponentIdPair;
     };
 } // namespace AzToolsFramework

+ 19 - 13
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.cpp

@@ -7,11 +7,13 @@
  */
 
 #include <AzToolsFramework/ComponentModes/BoxViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ViewportEditUtilities.h>
 #include <AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h>
 #include <AzToolsFramework/Manipulators/LinearManipulator.h>
 #include <AzToolsFramework/Manipulators/ManipulatorManager.h>
 #include <AzToolsFramework/Manipulators/ManipulatorView.h>
 #include <AzToolsFramework/Manipulators/ManipulatorSnapping.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 #include <AzToolsFramework/Viewport/ViewportSettings.h>
 #include <AzFramework/Viewport/ViewportColors.h>
 #include <AzFramework/Viewport/ViewportConstants.h>
@@ -27,11 +29,16 @@ namespace AzToolsFramework
         AZ::Vector3::CreateAxisZ(), -AZ::Vector3::CreateAxisZ()
     } };
 
+    BoxViewportEdit::BoxViewportEdit(bool allowAsymmetricalEditing)
+        : m_allowAsymmetricalEditing(allowAsymmetricalEditing)
+    {
+    }
+
     void BoxViewportEdit::UpdateManipulators()
     {
         AZ::Transform manipulatorSpace = AZ::Transform::CreateIdentity();
-        BoxManipulatorRequestBus::EventResult(
-            manipulatorSpace, m_entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetManipulatorSpace);
+        ShapeManipulatorRequestBus::EventResult(
+            manipulatorSpace, m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetManipulatorSpace);
 
         AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
         AZ::NonUniformScaleRequestBus::EventResult(
@@ -58,10 +65,9 @@ namespace AzToolsFramework
         }
     }
 
-    void BoxViewportEdit::Setup(const AZ::EntityComponentIdPair& entityComponentIdPair, bool allowAsymmetricalEditing)
+    void BoxViewportEdit::Setup(const AZ::EntityComponentIdPair& entityComponentIdPair)
     {
         m_entityComponentIdPair = entityComponentIdPair;
-        m_allowAsymmetricalEditing = allowAsymmetricalEditing;
 
         AZ::Transform worldFromLocal = AZ::Transform::CreateIdentity();
         AZ::TransformBus::EventResult(
@@ -117,11 +123,11 @@ namespace AzToolsFramework
                         const AZ::Vector3 transformedAxis = nonUniformScale * boxLocalTransform.TransformVector(action.m_fixed.m_axis);
                         const AZ::Vector3 translationOffsetDelta = 0.5f * (newAxisLength - oldAxisLength) * transformedAxis;
                         AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
-                        BoxManipulatorRequestBus::EventResult(
-                            translationOffset, entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetTranslationOffset);
-                        BoxManipulatorRequestBus::Event(
+                        ShapeManipulatorRequestBus::EventResult(
+                            translationOffset, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
+                        ShapeManipulatorRequestBus::Event(
                             entityComponentIdPair,
-                            &BoxManipulatorRequestBus::Events::SetTranslationOffset,
+                            &ShapeManipulatorRequestBus::Events::SetTranslationOffset,
                             translationOffset + translationOffsetDelta);
                     }
 
@@ -147,11 +153,11 @@ namespace AzToolsFramework
         }
     }
 
-    AZ::Vector3 GetPositionInManipulatorFrame(float worldUniformScale, const AZ::Transform& manipulatorLocalTransform,
-        const LinearManipulator::Action& action)
+    void BoxViewportEdit::ResetValues()
     {
-        return manipulatorLocalTransform.GetInverse().TransformPoint(
-            action.m_start.m_localPosition +
-            action.m_current.m_localPositionOffset / AZ::GetClamp(worldUniformScale, AZ::MinTransformScale, AZ::MaxTransformScale));
+        BoxManipulatorRequestBus::Event(
+            m_entityComponentIdPair, &BoxManipulatorRequestBus::Events::SetDimensions, AZ::Vector3::CreateOne());
+        ShapeManipulatorRequestBus::Event(
+            m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::SetTranslationOffset, AZ::Vector3::CreateZero());
     }
 } // namespace AzToolsFramework

+ 8 - 11
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.h

@@ -9,6 +9,7 @@
 #pragma once
 
 #include <AzCore/Component/ComponentBus.h>
+#include <AzToolsFramework/ComponentModes/BaseViewportEdit.h>
 #include <AzToolsFramework/Manipulators/LinearManipulator.h>
 
 namespace AzToolsFramework
@@ -17,14 +18,16 @@ namespace AzToolsFramework
 
     /// Wraps 6 linear manipulators, providing a viewport experience for 
     /// modifying the extents of a box
-    class BoxViewportEdit
+    class BoxViewportEdit : public BaseViewportEdit
     {
     public:
-        BoxViewportEdit() = default;
+        BoxViewportEdit(bool allowAsymmetricalEditing = false);
 
-        void Setup(const AZ::EntityComponentIdPair& entityComponentIdPair, bool allowAsymmetricalEditing = false);
-        void Teardown();
-        void UpdateManipulators();
+        // BaseViewportEdit overrides ...
+        void Setup(const AZ::EntityComponentIdPair& entityComponentIdPair) override;
+        void Teardown() override;
+        void UpdateManipulators() override;
+        void ResetValues() override;
 
     private:
         AZ::EntityComponentIdPair m_entityComponentIdPair;
@@ -32,10 +35,4 @@ namespace AzToolsFramework
         BoxManipulators m_linearManipulators; ///< Manipulators for editing box size.
         bool m_allowAsymmetricalEditing = false; ///< Whether moving individual faces independently is allowed.
     };
-
-    /// Calculates the position of the manipulator in its own reference frame.
-    /// Removes the effects of the manipulator local transform, and accounts for world transform scale in
-    /// the action local offset.
-    AZ::Vector3 GetPositionInManipulatorFrame(float worldUniformScale, const AZ::Transform& manipulatorLocalTransform,
-        const LinearManipulator::Action& action);
 } // namespace AzToolsFramework

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.cpp

@@ -8,8 +8,8 @@
 
 #include <AzFramework/Viewport/ViewportColors.h>
 #include <AzFramework/Viewport/ViewportConstants.h>
-#include <AzToolsFramework/ComponentModes/BoxViewportEdit.h>
 #include <AzToolsFramework/ComponentModes/CapsuleViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ViewportEditUtilities.h>
 
 namespace AzToolsFramework
 {

+ 40 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeComponentModeBus.h

@@ -0,0 +1,40 @@
+/*
+ * 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 <AzCore/Component/ComponentBus.h>
+
+namespace AzToolsFramework
+{
+    //! Bus used to communicate with shape component mode.
+    class ShapeComponentModeRequests
+        : public AZ::EntityComponentBus
+    {
+    public:
+        enum class SubMode : AZ::u32
+        {
+            Dimensions,
+            TranslationOffset,
+            NumModes
+        };
+
+        //! Gets the current shape component mode sub mode.
+        virtual SubMode GetShapeSubMode() const = 0;
+
+        //! Sets the current shape component mode sub mode.
+        //! @param mode The new sub mode to set.
+        virtual void SetShapeSubMode(SubMode mode) = 0;
+
+        //! Resets the UI for the current shape component mode sub mode.
+        virtual void ResetShapeSubMode() = 0;
+    };
+
+    using ShapeComponentModeRequestBus = AZ::EBus<ShapeComponentModeRequests>;
+} // namespace AzToolsFramework
+

+ 96 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.cpp

@@ -0,0 +1,96 @@
+/*
+ * 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 <AzCore/Component/NonUniformScaleBus.h>
+#include <AzCore/Component/TransformBus.h>
+#include <AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ViewportEditUtilities.h>
+#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
+
+namespace AzToolsFramework
+{
+    void ShapeTranslationOffsetViewportEdit::Setup(const AZ::EntityComponentIdPair& entityComponentIdPair)
+    {
+        m_entityComponentIdPair = entityComponentIdPair;
+
+        AZ::Transform manipulatorSpace = AZ::Transform::CreateIdentity();
+        ShapeManipulatorRequestBus::EventResult(
+            manipulatorSpace, m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetManipulatorSpace);
+
+        AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
+        AZ::NonUniformScaleRequestBus::EventResult(
+            nonUniformScale, entityComponentIdPair.GetEntityId(), &AZ::NonUniformScaleRequestBus::Events::GetScale);
+
+        AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
+        ShapeManipulatorRequestBus::EventResult(
+            translationOffset, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
+
+        m_translationManipulators = AZStd::make_shared<AzToolsFramework::TranslationManipulators>(
+        AzToolsFramework::TranslationManipulators::Dimensions::Three, manipulatorSpace, nonUniformScale);
+
+        m_translationManipulators->SetLocalTransform(AZ::Transform::CreateTranslation(translationOffset));
+        m_translationManipulators->SetLineBoundWidth(AzToolsFramework::ManipulatorLineBoundWidth());
+        m_translationManipulators->AddEntityComponentIdPair(m_entityComponentIdPair);
+        AzToolsFramework::ConfigureTranslationManipulatorAppearance3d(m_translationManipulators.get());
+
+        auto mouseMoveHandlerFn = [this, transformScale{ m_translationManipulators->GetSpace().GetUniformScale() }](const auto& action)
+        {
+            AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
+            ShapeManipulatorRequestBus::EventResult(
+                translationOffset, m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
+
+            const AZ::Transform manipulatorLocalTransform = AZ::Transform::CreateTranslation(translationOffset);
+            const AZ::Vector3 manipulatorPosition = GetPositionInManipulatorFrame(transformScale, manipulatorLocalTransform, action);
+
+            ShapeManipulatorRequestBus::Event(
+                m_entityComponentIdPair,
+                &ShapeManipulatorRequestBus::Events::SetTranslationOffset,
+                translationOffset + manipulatorPosition);
+
+            UpdateManipulators();
+        };
+
+        m_translationManipulators->InstallLinearManipulatorMouseMoveCallback(mouseMoveHandlerFn);
+        m_translationManipulators->InstallPlanarManipulatorMouseMoveCallback(mouseMoveHandlerFn);
+
+        m_translationManipulators->Register(g_mainManipulatorManagerId);
+    }
+
+    void ShapeTranslationOffsetViewportEdit::Teardown()
+    {
+        m_translationManipulators->Unregister();
+        m_translationManipulators.reset();
+    }
+
+    void ShapeTranslationOffsetViewportEdit::UpdateManipulators()
+    {
+        AZ::Transform manipulatorSpace = AZ::Transform::CreateIdentity();
+        ShapeManipulatorRequestBus::EventResult(
+            manipulatorSpace, m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetManipulatorSpace);
+
+        AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
+        AZ::NonUniformScaleRequestBus::EventResult(
+            nonUniformScale, m_entityComponentIdPair.GetEntityId(), &AZ::NonUniformScaleRequestBus::Events::GetScale);
+
+        AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
+        ShapeManipulatorRequestBus::EventResult(
+            translationOffset, m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
+
+        m_translationManipulators->SetSpace(manipulatorSpace);
+        m_translationManipulators->SetLocalTransform(AZ::Transform::CreateTranslation(translationOffset));
+        m_translationManipulators->SetNonUniformScale(nonUniformScale);
+        m_translationManipulators->SetBoundsDirty();
+    }
+
+    void ShapeTranslationOffsetViewportEdit::ResetValues()
+    {
+        ShapeManipulatorRequestBus::Event(
+            m_entityComponentIdPair, &ShapeManipulatorRequestBus::Events::SetTranslationOffset, AZ::Vector3::CreateZero());
+    }
+} // namespace AzToolsFramework

+ 31 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h

@@ -0,0 +1,31 @@
+/*
+ * 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 <AzCore/Component/ComponentBus.h>
+#include <AzToolsFramework/ComponentModes/BaseViewportEdit.h>
+#include <AzToolsFramework/Manipulators/TranslationManipulators.h>
+
+namespace AzToolsFramework
+{
+    //! Wraps translation manipulators for editing shape translation offsets.
+    class ShapeTranslationOffsetViewportEdit : public BaseViewportEdit
+    {
+    public:
+        ShapeTranslationOffsetViewportEdit() = default;
+
+        // BaseViewportEdit overrides ...
+        void Setup(const AZ::EntityComponentIdPair& entityComponentIdPair) override;
+        void Teardown() override;
+        void UpdateManipulators() override;
+        void ResetValues() override;
+
+    private:
+        AZ::EntityComponentIdPair m_entityComponentIdPair;
+        AZStd::shared_ptr<TranslationManipulators> m_translationManipulators; //!< Manipulators for editing shape offset.
+    };
+} // namespace AzToolsFramework

+ 25 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ViewportEditUtilities.h

@@ -0,0 +1,25 @@
+/*
+ * 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 <AzToolsFramework/Manipulators/LinearManipulator.h>
+
+namespace AzToolsFramework
+{
+    //! Calculates the position of the manipulator in its own reference frame.
+    //! Removes the effects of the manipulator local transform, and accounts for world transform scale in
+    //! the action local offset.
+    template<typename T>
+    AZ::Vector3 GetPositionInManipulatorFrame(float worldUniformScale, const AZ::Transform& manipulatorLocalTransform, const T& action)
+    {
+        return manipulatorLocalTransform.GetInverse().TransformPoint(
+            action.m_start.m_localPosition +
+            action.LocalPositionOffset() / AZ::GetClamp(worldUniformScale, AZ::MinTransformScale, AZ::MaxTransformScale));
+    }
+} // namespace AzToolsFramework

+ 0 - 6
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h

@@ -26,14 +26,8 @@ namespace AzToolsFramework
         virtual AZ::Vector3 GetDimensions() const = 0;
         //! Set the X/Y/Z dimensions of the box shape/collider.
         virtual void SetDimensions(const AZ::Vector3& dimensions) = 0;
-        //! Get the translation offset of the box relative to the entity position.
-        virtual AZ::Vector3 GetTranslationOffset() const = 0;
-        //! Set the translation offset of the box relative to the entity position.
-        virtual void SetTranslationOffset(const AZ::Vector3& translationOffset) = 0;
         //! Get the transform of the box relative to the entity.
         virtual AZ::Transform GetCurrentLocalTransform() const = 0;
-        //! Get the space in which the manipulators are defined.
-        virtual AZ::Transform GetManipulatorSpace() const = 0;
 
     protected:
         ~BoxManipulatorRequests() = default;

+ 37 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h

@@ -0,0 +1,37 @@
+/*
+ * 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 <AzCore/Component/ComponentBus.h>
+
+namespace AZ
+{
+    class Vector3;
+}
+
+namespace AzToolsFramework
+{
+    //! Interface for handling shape offset manipulator requests.
+    class ShapeManipulatorRequests : public AZ::EntityComponentBus
+    {
+    public:
+        //! Get the translation offset of the shape relative to the entity position.
+        virtual AZ::Vector3 GetTranslationOffset() const = 0;
+        //! Set the translation offset of the shape relative to the entity position.
+        virtual void SetTranslationOffset(const AZ::Vector3& translationOffset) = 0;
+        //! Get the space in which the manipulators are defined.
+        virtual AZ::Transform GetManipulatorSpace() const = 0;
+
+    protected:
+        ~ShapeManipulatorRequests() = default;
+    };
+
+    //! Type to inherit to implement ShapeOffsetManipulatorRequests
+    using ShapeManipulatorRequestBus = AZ::EBus<ShapeManipulatorRequests>;
+} // namespace AzToolsFramework

+ 6 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake

@@ -251,6 +251,7 @@ set(FILES
     Manipulators/ScaleManipulators.h
     Manipulators/SelectionManipulator.cpp
     Manipulators/SelectionManipulator.h
+    Manipulators/ShapeManipulatorRequestBus.h
     Manipulators/SplineHoverSelection.h
     Manipulators/SplineHoverSelection.cpp
     Manipulators/SplineSelectionManipulator.h
@@ -635,12 +636,17 @@ set(FILES
     ComponentMode/ComponentModeViewportUiRequestBus.h
     ComponentMode/EditorBaseComponentMode.h
     ComponentMode/EditorBaseComponentMode.cpp
+    ComponentModes/BaseViewportEdit.h
     ComponentModes/BoxComponentMode.h
     ComponentModes/BoxComponentMode.cpp
     ComponentModes/BoxViewportEdit.h
     ComponentModes/BoxViewportEdit.cpp
     ComponentModes/CapsuleViewportEdit.h
     ComponentModes/CapsuleViewportEdit.cpp
+    ComponentModes/ShapeComponentModeBus.h
+    ComponentModes/ShapeTranslationOffsetViewportEdit.h
+    ComponentModes/ShapeTranslationOffsetViewportEdit.cpp
+    ComponentModes/ViewportEditUtilities.h
     ViewportSelection/EditorBoxSelect.h
     ViewportSelection/EditorBoxSelect.cpp
     ViewportSelection/EditorDefaultSelection.h

+ 13 - 1
Gems/LmbrCentral/Code/Source/LmbrCentralEditor.cpp

@@ -43,6 +43,7 @@
 
 #include <AzFramework/Metrics/MetricsPlainTextNameRegistration.h>
 #include <AzToolsFramework/ToolsComponents/EditorSelectionAccentSystemComponent.h>
+#include <AzToolsFramework/ComponentModes/BoxComponentMode.h>
 #include <Builders/BenchmarkAssetBuilder/BenchmarkAssetBuilderComponent.h>
 #include <Builders/LevelBuilder/LevelBuilderComponent.h>
 #include <Builders/LuaBuilder/LuaBuilderComponent.h>
@@ -121,20 +122,31 @@ namespace LmbrCentral
     {
         EditorSplineComponentMode::RegisterActions();
         EditorTubeShapeComponentMode::RegisterActions();
+        if (IsShapeComponentTranslationEnabled())
+        {
+            AzToolsFramework::BoxComponentMode::RegisterActions();
+        }
     }
 
     void LmbrCentralEditorModule::OnActionContextModeBindingHook()
     {
         EditorSplineComponentMode::BindActionsToModes();
         EditorTubeShapeComponentMode::BindActionsToModes();
+        if (IsShapeComponentTranslationEnabled())
+        {
+            AzToolsFramework::BoxComponentMode::BindActionsToModes();
+        }
     }
 
     void LmbrCentralEditorModule::OnMenuBindingHook()
     {
         EditorSplineComponentMode::BindActionsToMenus();
         EditorTubeShapeComponentMode::BindActionsToMenus();
+        if (IsShapeComponentTranslationEnabled())
+        {
+            AzToolsFramework::BoxComponentMode::BindActionsToMenus();
+        }
     }
-
 } // namespace LmbrCentral
 
 AZ_DECLARE_MODULE_CLASS(Gem_LmbrCentralEditor, LmbrCentral::LmbrCentralEditorModule)

+ 3 - 0
Gems/LmbrCentral/Code/Source/Shape/EditorAxisAlignedBoxShapeComponent.cpp

@@ -65,6 +65,8 @@ namespace LmbrCentral
         AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusConnect(
             AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(
+            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
 
         // ComponentMode
         const bool allowAsymmetricalEditing = IsShapeComponentTranslationEnabled();
@@ -77,6 +79,7 @@ namespace LmbrCentral
     {
         m_componentModeDelegate.Disconnect();
 
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusDisconnect();
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusDisconnect();
         AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect();
         m_aaboxShape.Deactivate();

+ 5 - 1
Gems/LmbrCentral/Code/Source/Shape/EditorAxisAlignedBoxShapeComponent.h

@@ -14,6 +14,7 @@
 #include <AzFramework/Entity/EntityDebugDisplayBus.h>
 #include <AzToolsFramework/ComponentMode/ComponentModeDelegate.h>
 #include <AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 
 namespace LmbrCentral
 {
@@ -22,6 +23,7 @@ namespace LmbrCentral
         : public EditorBaseShapeComponent
         , private AzFramework::EntityDebugDisplayEventBus::Handler
         , private AzToolsFramework::BoxManipulatorRequestBus::Handler
+        , private AzToolsFramework::ShapeManipulatorRequestBus::Handler
     {
     public:
         AZ_EDITOR_COMPONENT(EditorAxisAlignedBoxShapeComponent, EditorAxisAlignedBoxShapeComponentTypeId, EditorBaseShapeComponent);
@@ -58,9 +60,11 @@ namespace LmbrCentral
         // AzToolsFramework::BoxManipulatorRequestBus
         AZ::Vector3 GetDimensions() const override;
         void SetDimensions(const AZ::Vector3& dimensions) override;
+        AZ::Transform GetCurrentLocalTransform() const override;
+        
+        // AzToolsFramework::ShapeManipulatorRequestBus overrides ...
         AZ::Vector3 GetTranslationOffset() const override;
         void SetTranslationOffset(const AZ::Vector3& translationOffset) override;
-        AZ::Transform GetCurrentLocalTransform() const override;
         AZ::Transform GetManipulatorSpace() const override;
 
         void ConfigurationChanged();        

+ 3 - 0
Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.cpp

@@ -72,6 +72,8 @@ namespace LmbrCentral
         AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusConnect(
             AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(
+            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
 
         // ComponentMode
         const bool allowAsymmetricalEditing = IsShapeComponentTranslationEnabled();
@@ -84,6 +86,7 @@ namespace LmbrCentral
     {
         m_componentModeDelegate.Disconnect();
 
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusDisconnect();
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusDisconnect();
         AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect();
         m_boxShape.Deactivate();

+ 5 - 2
Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.h

@@ -14,7 +14,7 @@
 #include <AzFramework/Entity/EntityDebugDisplayBus.h>
 #include <AzToolsFramework/ComponentMode/ComponentModeDelegate.h>
 #include <AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h>
-
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 
 namespace LmbrCentral
 {
@@ -23,6 +23,7 @@ namespace LmbrCentral
         : public EditorBaseShapeComponent
         , private AzFramework::EntityDebugDisplayEventBus::Handler
         , private AzToolsFramework::BoxManipulatorRequestBus::Handler
+        , private AzToolsFramework::ShapeManipulatorRequestBus::Handler
     {
     public:
         AZ_EDITOR_COMPONENT(EditorBoxShapeComponent, EditorBoxShapeComponentTypeId, EditorBaseShapeComponent);
@@ -56,9 +57,11 @@ namespace LmbrCentral
         // AzToolsFramework::BoxManipulatorRequestBus
         AZ::Vector3 GetDimensions() const override;
         void SetDimensions(const AZ::Vector3& dimensions) override;
+        AZ::Transform GetCurrentLocalTransform() const override;
+
+        // AzToolsFramework::ShapeManipulatorRequestBus overrides ...
         AZ::Vector3 GetTranslationOffset() const override;
         void SetTranslationOffset(const AZ::Vector3& translationOffset) override;
-        AZ::Transform GetCurrentLocalTransform() const override;
         AZ::Transform GetManipulatorSpace() const override;
 
         void ConfigurationChanged();        

+ 120 - 19
Gems/LmbrCentral/Code/Tests/EditorAxisAlignedBoxShapeComponentTests.cpp

@@ -26,6 +26,8 @@ namespace LmbrCentral
         AZStd::unique_ptr<AZ::ComponentDescriptor> m_editorSphereShapeComponentDescriptor;
 
         AZ::Entity* m_entity = nullptr;
+        AZ::EntityId m_entityId;
+        AZ::EntityComponentIdPair m_entityComponentIdPair;
     };
 
     void EditorAxisAlignedBoxShapeComponentFixture::SetUpEditorFixtureImpl()
@@ -47,16 +49,19 @@ namespace LmbrCentral
         m_editorAxisAlignedBoxShapeComponentDescriptor->Reflect(serializeContext);
 
         UnitTest::CreateDefaultEditorEntity("AxisAlignedBoxShapeComponentEntity", &m_entity);
+        m_entityId = m_entity->GetId();
         m_entity->Deactivate();
-        m_entity->CreateComponent(EditorAxisAlignedBoxShapeComponentTypeId);
+        m_entityComponentIdPair =
+            AZ::EntityComponentIdPair(m_entityId, m_entity->CreateComponent(EditorAxisAlignedBoxShapeComponentTypeId)->GetId());
         m_entity->Activate();
     }
 
     void EditorAxisAlignedBoxShapeComponentFixture::TearDownEditorFixtureImpl()
     {
         AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
-            &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entity->GetId());
+            &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entityId);
         m_entity = nullptr;
+        m_entityId.SetInvalid();
 
         m_editorAxisAlignedBoxShapeComponentDescriptor.reset();
         m_editorSphereShapeComponentDescriptor.reset();
@@ -68,20 +73,20 @@ namespace LmbrCentral
         UnitTest::IndirectCallManipulatorViewportInteractionFixtureMixin<EditorAxisAlignedBoxShapeComponentFixture>;
 
     void SetUpAxisAlignedBoxShapeComponent(
-        AZ::Entity* entity, const AZ::Transform& transform, const AZ::Vector3& translationOffset, const AZ::Vector3& boxDimensions)
+        AZ::EntityId entityId, const AZ::Transform& transform, const AZ::Vector3& translationOffset, const AZ::Vector3& boxDimensions)
     {
-        AZ::TransformBus::Event(entity->GetId(), &AZ::TransformBus::Events::SetWorldTM, transform);
-        ShapeComponentRequestsBus::Event(entity->GetId(), &ShapeComponentRequests::SetTranslationOffset, translationOffset);
-        BoxShapeComponentRequestsBus::Event(entity->GetId(), &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
+        AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetWorldTM, transform);
+        ShapeComponentRequestsBus::Event(entityId, &ShapeComponentRequests::SetTranslationOffset, translationOffset);
+        BoxShapeComponentRequestsBus::Event(entityId, &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
     }
 
-    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeSymmetricalEditingManipulatorsScaleCorrectly)
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeSymmetricalDimensionManipulatorsScaleCorrectly)
     {
         const AZ::Transform transform(AZ::Vector3(7.0f, 5.0f, -2.0f), AZ::Quaternion::CreateIdentity(), 0.5f);
         const AZ::Vector3 translationOffset(-4.0f, -4.0f, 3.0f);
         const AZ::Vector3 boxDimensions(4.0f, 2.0f, 3.0f);
-        SetUpAxisAlignedBoxShapeComponent(m_entity, transform, translationOffset, boxDimensions);
-        EnterComponentMode(m_entity, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetUpAxisAlignedBoxShapeComponent(m_entityId, transform, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
 
         // position the camera so it is looking down at the box
         AzFramework::SetCameraTransform(
@@ -95,16 +100,16 @@ namespace LmbrCentral
 
         DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd, AzToolsFramework::DefaultSymmetricalEditingModifier);
 
-        ExpectBoxDimensions(m_entity, AZ::Vector3(4.0f, 4.0f, 3.0f));
+        ExpectBoxDimensions(m_entityId, AZ::Vector3(4.0f, 4.0f, 3.0f));
     }
 
-    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeAsymmetricalEditingManipulatorsScaleCorrectly)
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeAsymmetricalDimensionManipulatorsScaleCorrectly)
     {
         const AZ::Transform transform(AZ::Vector3(2.0f, 4.0f, -7.0f), AZ::Quaternion::CreateIdentity(), 1.5f);
         const AZ::Vector3 translationOffset(-5.0f, 3.0f, 1.0f);
         const AZ::Vector3 boxDimensions(2.0f, 6.0f, 4.0f);
-        SetUpAxisAlignedBoxShapeComponent(m_entity, transform, translationOffset, boxDimensions);
-        EnterComponentMode(m_entity, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetUpAxisAlignedBoxShapeComponent(m_entityId, transform, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
 
         // position the camera so it is looking down at the box
         AzFramework::SetCameraTransform(
@@ -118,9 +123,9 @@ namespace LmbrCentral
 
         DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
-        ExpectBoxDimensions(m_entity, AZ::Vector3(3.0f, 6.0f, 4.0f));
+        ExpectBoxDimensions(m_entityId, AZ::Vector3(3.0f, 6.0f, 4.0f));
         // the offset should have changed because the editing was asymmetrical
-        ExpectTranslationOffset(m_entity, translationOffset - AZ::Vector3::CreateAxisX(0.5f));
+        ExpectTranslationOffset(m_entityId, translationOffset - AZ::Vector3::CreateAxisX(0.5f));
     }
 
     TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeRotatedEntityManipulatorSpaceCorrect)
@@ -128,8 +133,8 @@ namespace LmbrCentral
         const AZ::Transform transform(AZ::Vector3(7.0f, -6.0f, -2.0f), AZ::Quaternion(0.7f, 0.1f, -0.1f, 0.7f), 2.0f);
         const AZ::Vector3 translationOffset(-4.0f, 4.0f, 2.0f);
         const AZ::Vector3 boxDimensions(2.0f, 3.0f, 4.0f);
-        SetUpAxisAlignedBoxShapeComponent(m_entity, transform, translationOffset, boxDimensions);
-        EnterComponentMode(m_entity, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetUpAxisAlignedBoxShapeComponent(m_entityId, transform, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
 
         // position the camera so it is looking down at the box
         AzFramework::SetCameraTransform(
@@ -144,9 +149,105 @@ namespace LmbrCentral
 
         DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
-        ExpectBoxDimensions(m_entity, AZ::Vector3(3.0f, 3.0f, 4.0f));
+        ExpectBoxDimensions(m_entityId, AZ::Vector3(3.0f, 3.0f, 4.0f));
         // the offset should have changed because the editing was asymmetrical
-        ExpectTranslationOffset(m_entity, translationOffset + AZ::Vector3::CreateAxisX(0.5f));
+        ExpectTranslationOffset(m_entityId, translationOffset + AZ::Vector3::CreateAxisX(0.5f));
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, AxisAlignedBoxShapeTranslationOffsetManipulatorsScaleCorrectly)
+    {
+        const AZ::Transform boxTransform(AZ::Vector3(-5.0f, 2.0f, 2.0f), AZ::Quaternion(0.3f, 0.3f, 0.1f, 0.9f), 1.5f);
+        const AZ::Vector3 translationOffset(3.0f, 1.0f, -4.0f);
+        const AZ::Vector3 boxDimensions(1.0f, 4.0f, 2.0f);
+        SetUpAxisAlignedBoxShapeComponent(m_entityId, boxTransform, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetComponentSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+
+        // position the camera so it is looking horizontally at the box
+        AzFramework::SetCameraTransform(
+            m_cameraState,
+            AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, -10.0f, -3.0f)));
+
+        // position in world space which should allow grabbing the box's z translation offset manipulator
+        const AZ::Vector3 worldStart(-0.5f, 3.5f, -3.0f);
+
+        // position in world space to move to 
+        const AZ::Vector3 worldEnd(-0.5f, 3.5f, -1.5f);
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
+
+        ExpectTranslationOffset(m_entityId, translationOffset + AZ::Vector3::CreateAxisZ());
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, PressingKey1ShouldSetDimensionMode)
+    {
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetComponentSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+
+        QTest::keyPress(&m_editorActions.m_componentModeWidget, Qt::Key_1);
+
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::Dimensions);
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, PressingKey2ShouldSetTranslationOffsetMode)
+    {
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::Dimensions);
+
+        QTest::keyPress(&m_editorActions.m_componentModeWidget, Qt::Key_2);
+
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, PressingKeyRInDimensionModeShouldResetBoxDimensions)
+    {
+        const AZ::Vector3 boxDimensions(2.0f, 2.0f, 2.0f);
+        BoxShapeComponentRequestsBus::Event(m_entityId, &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+
+        ExpectBoxDimensions(m_entityId, boxDimensions);
+
+        QTest::keyPress(&m_editorActions.m_componentModeWidget, Qt::Key_R);
+
+        ExpectBoxDimensions(m_entityId, AZ::Vector3::CreateOne());
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, PressingKeyRInTranslationOffsetModeShouldResetTranslationOffset)
+    {
+        const AZ::Vector3 translationOffset(3.0f, 4.0f, 5.0f);
+        ShapeComponentRequestsBus::Event(m_entityId, &ShapeComponentRequests::SetTranslationOffset, translationOffset);
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetComponentSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+
+        ExpectTranslationOffset(m_entityId, translationOffset);
+
+        QTest::keyPress(&m_editorActions.m_componentModeWidget, Qt::Key_R);
+
+        ExpectTranslationOffset(m_entityId, AZ::Vector3::CreateZero());
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, CtrlMouseWheelUpShouldSetNextMode)
+    {
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::Dimensions);
+
+        const auto handled = CtrlScroll(1.0f);
+
+        EXPECT_EQ(handled, AzToolsFramework::ViewportInteraction::MouseInteractionResult::Viewport);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+    }
+
+    TEST_F(EditorAxisAlignedBoxShapeComponentManipulatorFixture, CtrlMouseWheelDownShouldSetNextMode)
+    {
+        EnterComponentMode(m_entityId, EditorAxisAlignedBoxShapeComponentTypeId);
+        SetComponentSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+
+        const auto handled = CtrlScroll(-1.0f);
+
+        EXPECT_EQ(handled, AzToolsFramework::ViewportInteraction::MouseInteractionResult::Viewport);
+        ExpectSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::Dimensions);
     }
 }
 

+ 61 - 29
Gems/LmbrCentral/Code/Tests/EditorBoxShapeComponentTests.cpp

@@ -11,6 +11,7 @@
 #include "Shape/EditorSphereShapeComponent.h"
 #include <AZTestShared/Utils/Utils.h>
 #include <AzCore/Component/NonUniformScaleBus.h>
+#include <AzToolsFramework/ComponentModes/ShapeComponentModeBus.h>
 #include <AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.h>
 #include <AzToolsFramework/Viewport/ViewportSettings.h>
 #include <EditorShapeTestUtils.h>
@@ -76,6 +77,8 @@ namespace LmbrCentral
         AZStd::unique_ptr<AZ::ComponentDescriptor> m_editorSphereShapeComponentDescriptor;
 
         AZ::Entity* m_entity = nullptr;
+        AZ::EntityId m_entityId;
+        AZ::EntityComponentIdPair m_entityComponentIdPair;
     };
 
     void EditorBoxShapeComponentFixture::SetUpEditorFixtureImpl()
@@ -97,17 +100,20 @@ namespace LmbrCentral
         m_editorBoxShapeComponentDescriptor->Reflect(serializeContext);
 
         UnitTest::CreateDefaultEditorEntity("BoxShapeComponentEntity", &m_entity);
+        m_entityId = m_entity->GetId();
         m_entity->Deactivate();
         m_entity->CreateComponent(AzToolsFramework::Components::EditorNonUniformScaleComponent::RTTI_Type());
-        m_entity->CreateComponent(EditorBoxShapeComponentTypeId);
+        m_entityComponentIdPair =
+            AZ::EntityComponentIdPair(m_entityId, m_entity->CreateComponent(EditorBoxShapeComponentTypeId)->GetId());
         m_entity->Activate();
     }
 
     void EditorBoxShapeComponentFixture::TearDownEditorFixtureImpl()
     {
         AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
-            &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entity->GetId());
+            &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entityId);
         m_entity = nullptr;
+        m_entityId.SetInvalid();
 
         m_editorBoxShapeComponentDescriptor.reset();
         m_editorSphereShapeComponentDescriptor.reset();
@@ -118,21 +124,29 @@ namespace LmbrCentral
     using EditorBoxShapeComponentManipulatorFixture =
         UnitTest::IndirectCallManipulatorViewportInteractionFixtureMixin<EditorBoxShapeComponentFixture>;
 
-    TEST_F(EditorBoxShapeComponentManipulatorFixture, BoxShapeNonUniformScaleSymmetricalEditingManipulatorsScaleCorrectly)
+    void SetUpBoxShapeComponent(
+        AZ::EntityId entityId,
+        const AZ::Transform& transform,
+        const AZ::Vector3& nonUniformScale,
+        const AZ::Vector3& translationOffset,
+        const AZ::Vector3& boxDimensions)
+    {
+        AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetWorldTM, transform);
+        AZ::NonUniformScaleRequestBus::Event(entityId, &AZ::NonUniformScaleRequests::SetScale, nonUniformScale);
+        ShapeComponentRequestsBus::Event(entityId, &ShapeComponentRequests::SetTranslationOffset, translationOffset);
+        BoxShapeComponentRequestsBus::Event(entityId, &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
+    }
+
+    TEST_F(EditorBoxShapeComponentManipulatorFixture, BoxShapeNonUniformScaleSymmetricalDimensionManipulatorsScaleCorrectly)
     {
         // a rotation which rotates the x-axis to (0.8, 0.6, 0)
         const AZ::Quaternion boxRotation(0.0f, 0.0f, 0.316228f, 0.948683f);
-        AZ::Transform boxTransform = AZ::Transform::CreateFromQuaternionAndTranslation(boxRotation, AZ::Vector3(2.0f, 3.0f, 4.0f));
-        boxTransform.SetUniformScale(1.5f);
-        AZ::TransformBus::Event(m_entity->GetId(), &AZ::TransformBus::Events::SetWorldTM, boxTransform);
-
+        AZ::Transform boxTransform(AZ::Vector3(2.0f, 3.0f, 4.0f), boxRotation, 1.5f);
         const AZ::Vector3 nonUniformScale(4.0f, 1.5f, 2.0f);
-        AZ::NonUniformScaleRequestBus::Event(m_entity->GetId(), &AZ::NonUniformScaleRequests::SetScale, nonUniformScale);
-
         const AZ::Vector3 boxDimensions(1.0f, 2.0f, 2.5f);
-        BoxShapeComponentRequestsBus::Event(m_entity->GetId(), &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
-
-        EnterComponentMode(m_entity, EditorBoxShapeComponentTypeId);
+        const AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
+        SetUpBoxShapeComponent(m_entity->GetId(), boxTransform, nonUniformScale, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorBoxShapeComponentTypeId);
 
         // position the camera so it is looking down at the box
         AzFramework::SetCameraTransform(
@@ -153,27 +167,18 @@ namespace LmbrCentral
 
         DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd, AzToolsFramework::DefaultSymmetricalEditingModifier);
 
-        ExpectBoxDimensions(m_entity, AZ::Vector3(2.0f, 2.0f, 2.5f));
+        ExpectBoxDimensions(m_entityId, AZ::Vector3(2.0f, 2.0f, 2.5f));
     }
 
-    TEST_F(EditorBoxShapeComponentManipulatorFixture, BoxShapeNonUniformScaleAsymmetricalEditingManipulatorsScaleCorrectly)
+    TEST_F(EditorBoxShapeComponentManipulatorFixture, BoxShapeNonUniformScaleAsymmetricalDimensionManipulatorsScaleCorrectly)
     {
         const AZ::Quaternion boxRotation(0.2f, 0.4f, -0.4f, 0.8f);
-        AZ::Transform boxTransform = AZ::Transform::CreateFromQuaternionAndTranslation(boxRotation, AZ::Vector3(4.0f, -6.0f, -5.0f));
-        boxTransform.SetUniformScale(0.5f);
-        AZ::TransformBus::Event(m_entity->GetId(), &AZ::TransformBus::Events::SetWorldTM, boxTransform);
-
+        AZ::Transform boxTransform(AZ::Vector3(4.0f, -6.0f, -5.0f), boxRotation, 0.5f);
         const AZ::Vector3 nonUniformScale(2.0f, 0.5f, 1.5f);
-        AZ::NonUniformScaleRequestBus::Event(m_entity->GetId(), &AZ::NonUniformScaleRequests::SetScale, nonUniformScale);
-
         const AZ::Vector3 boxDimensions(3.0f, 6.0f, 2.0f);
-        BoxShapeComponentRequestsBus::Event(m_entity->GetId(), &BoxShapeComponentRequests::SetBoxDimensions, boxDimensions);
-
         const AZ::Vector3 translationOffset(2.0f, -5.0f, 4.0f);
-        LmbrCentral::ShapeComponentRequestsBus::Event(
-            m_entity->GetId(), &LmbrCentral::ShapeComponentRequests::SetTranslationOffset, translationOffset);
-
-        EnterComponentMode(m_entity, EditorBoxShapeComponentTypeId);
+        SetUpBoxShapeComponent(m_entityId, boxTransform, nonUniformScale, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorBoxShapeComponentTypeId);
 
         // position the camera so it is looking down at the box
         AzFramework::SetCameraTransform(
@@ -189,9 +194,36 @@ namespace LmbrCentral
 
         DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
-        ExpectBoxDimensions(m_entity, AZ::Vector3(3.0f, 9.0f, 2.0f));
+        ExpectBoxDimensions(m_entityId, AZ::Vector3(3.0f, 9.0f, 2.0f));
         // the offset should have changed because the editing was asymmetrical
-        ExpectTranslationOffset(m_entity, translationOffset - AZ::Vector3::CreateAxisY(1.5f));
+        ExpectTranslationOffset(m_entityId, translationOffset - AZ::Vector3::CreateAxisY(1.5f));
+    }
+
+    TEST_F(EditorBoxShapeComponentManipulatorFixture, BoxShapeNonUniformScaleTranslationOffsetManipulatorsScaleCorrectly)
+    {
+        const AZ::Quaternion boxRotation(0.7f, 0.1f, -0.7f, 0.1f);
+        AZ::Transform boxTransform(AZ::Vector3(-3.0f, 5.0f, 2.0f), boxRotation, 2.5f);
+        const AZ::Vector3 nonUniformScale(0.5f, 2.0f, 3.0f);
+        const AZ::Vector3 boxDimensions(6.0f, 2.0f, 5.0f);
+        const AZ::Vector3 translationOffset(4.0f, 5.0f, -3.0f);
+        SetUpBoxShapeComponent(m_entityId, boxTransform, nonUniformScale, translationOffset, boxDimensions);
+        EnterComponentMode(m_entityId, EditorBoxShapeComponentTypeId);
+        SetComponentSubMode(m_entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode::TranslationOffset);
+
+        // position the camera so it is looking horizontally at the box
+        AzFramework::SetCameraTransform(
+            m_cameraState,
+            AZ::Transform::CreateTranslation(AZ::Vector3(25.0f, -25.0f, -4.0f)));
+
+        // position in world space which should allow grabbing the box's x translation offset manipulator
+        const AZ::Vector3 worldStart(25.6f, -12.7f, -3.5f);
+
+        // position in world space to move to 
+        const AZ::Vector3 worldEnd(25.6f, -12.7f, -4.75f);
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
+
+        ExpectTranslationOffset(m_entityId, translationOffset + AZ::Vector3::CreateAxisX());
     }
-}
+} // namespace LmbrCentral
 

+ 40 - 7
Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.cpp

@@ -36,26 +36,59 @@ namespace LmbrCentral
             ->MouseLButtonUp();
     }
 
-    void EnterComponentMode(AZ::Entity* entity, const AZ::Uuid& componentType)
+    void EnterComponentMode(AZ::EntityId entityId, const AZ::Uuid& componentType)
     {
-        AzToolsFramework::SelectEntity(entity->GetId());
+        AzToolsFramework::SelectEntity(entityId);
         AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast(
             &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Events::AddSelectedComponentModesOfType,
             componentType);
     }
 
-    void ExpectBoxDimensions(AZ::Entity* entity, const AZ::Vector3& expectedBoxDimensions)
+    void ExpectBoxDimensions(AZ::EntityId entityId, const AZ::Vector3& expectedBoxDimensions)
     {
-        AZ::Vector3 boxDimensions;
-        BoxShapeComponentRequestsBus::EventResult(boxDimensions, entity->GetId(), &BoxShapeComponentRequests::GetBoxDimensions);
+        AZ::Vector3 boxDimensions = AZ::Vector3::CreateZero();
+        BoxShapeComponentRequestsBus::EventResult(boxDimensions, entityId, &BoxShapeComponentRequests::GetBoxDimensions);
         EXPECT_THAT(boxDimensions, UnitTest::IsCloseTolerance(expectedBoxDimensions, ManipulatorTolerance));
     }
 
-    void ExpectTranslationOffset(AZ::Entity* entity, const AZ::Vector3& expectedTranslationOffset)
+    void ExpectTranslationOffset(AZ::EntityId entityId, const AZ::Vector3& expectedTranslationOffset)
     {
         AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
         LmbrCentral::ShapeComponentRequestsBus::EventResult(
-            translationOffset, entity->GetId(), &LmbrCentral::ShapeComponentRequests::GetTranslationOffset);
+            translationOffset, entityId, &LmbrCentral::ShapeComponentRequests::GetTranslationOffset);
         EXPECT_THAT(translationOffset, UnitTest::IsCloseTolerance(expectedTranslationOffset, ManipulatorTolerance));
     }
+
+    void SetComponentSubMode(AZ::EntityComponentIdPair entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode subMode)
+    {
+        AzToolsFramework::ShapeComponentModeRequestBus::Event(
+            entityComponentIdPair, &AzToolsFramework::ShapeComponentModeRequests::SetShapeSubMode, subMode);
+    }
+
+    void ExpectSubMode(
+        AZ::EntityComponentIdPair entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode expectedSubMode)
+    {
+        AzToolsFramework::ShapeComponentModeRequests::SubMode subMode = AzToolsFramework::ShapeComponentModeRequests::SubMode::NumModes;
+        AzToolsFramework::ShapeComponentModeRequestBus::EventResult(
+            subMode, entityComponentIdPair, &AzToolsFramework::ShapeComponentModeRequests::GetShapeSubMode);
+        EXPECT_EQ(subMode, expectedSubMode);
+    }
+
+    AzToolsFramework::ViewportInteraction::MouseInteractionResult CtrlScroll(float wheelDelta)
+    {
+        AzToolsFramework::ViewportInteraction::MouseInteractionEvent interactionEvent(
+            AzToolsFramework::ViewportInteraction::MouseInteraction(), wheelDelta);
+        interactionEvent.m_mouseEvent = AzToolsFramework::ViewportInteraction::MouseEvent::Wheel;
+        interactionEvent.m_mouseInteraction.m_keyboardModifiers.m_keyModifiers =
+            static_cast<AZ::u32>(AzToolsFramework::ViewportInteraction::KeyboardModifier::Ctrl);
+
+        AzToolsFramework::ViewportInteraction::MouseInteractionResult handled =
+            AzToolsFramework::ViewportInteraction::MouseInteractionResult::None;
+        AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus::BroadcastResult(
+            handled,
+            &AzToolsFramework::ViewportInteraction::InternalMouseViewportRequests::InternalHandleAllMouseInteractions,
+            interactionEvent);
+
+        return handled;
+    }
 } // namespace LmbrCentral

+ 14 - 4
Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.h

@@ -7,12 +7,14 @@
  */
 
 #pragma once
+
+#include <AZTestShared/Utils/Utils.h>
 #include <AzManipulatorTestFramework/AzManipulatorTestFramework.h>
 #include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
 #include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
 #include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
 #include <AzManipulatorTestFramework/ViewportInteraction.h>
-#include <AZTestShared/Utils/Utils.h>
+#include <AzToolsFramework/ComponentModes/ShapeComponentModeBus.h>
 
 namespace LmbrCentral
 {
@@ -24,9 +26,17 @@ namespace LmbrCentral
         const AzToolsFramework::ViewportInteraction::KeyboardModifier keyboardModifier =
         AzToolsFramework::ViewportInteraction::KeyboardModifier::None);
 
-    void EnterComponentMode(AZ::Entity* entity, const AZ::Uuid& componentType);
+    void EnterComponentMode(AZ::EntityId entityId, const AZ::Uuid& componentType);
+
+    void SetComponentSubMode(
+        AZ::EntityComponentIdPair entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode subMode);
+
+    void ExpectBoxDimensions(AZ::EntityId entityId, const AZ::Vector3& expectedBoxDimensions);
+
+    void ExpectTranslationOffset(AZ::EntityId entityId, const AZ::Vector3& expectedTranslationOffset);
 
-    void ExpectBoxDimensions(AZ::Entity* entity, const AZ::Vector3& expectedBoxDimensions);
+    void ExpectSubMode(
+        AZ::EntityComponentIdPair entityComponentIdPair, AzToolsFramework::ShapeComponentModeRequests::SubMode expectedSubMode);
 
-    void ExpectTranslationOffset(AZ::Entity* entity, const AZ::Vector3& expectedTranslationOffset);
+    AzToolsFramework::ViewportInteraction::MouseInteractionResult CtrlScroll(float wheelDelta);
 } // namespace LmbrCentral

+ 9 - 4
Gems/PhysX/Code/Editor/ColliderBoxMode.cpp

@@ -13,20 +13,25 @@ namespace PhysX
 {
     AZ_CLASS_ALLOCATOR_IMPL(ColliderBoxMode, AZ::SystemAllocator, 0);
 
-    void ColliderBoxMode::Setup(const AZ::EntityComponentIdPair& idPair)
+    ColliderBoxMode::ColliderBoxMode()
     {
         const bool allowAsymmetricalEditing = true;
-        m_boxEdit.Setup(idPair, allowAsymmetricalEditing);
+        m_boxEdit = AZStd::make_unique<AzToolsFramework::BoxViewportEdit>(allowAsymmetricalEditing);
+    }
+
+    void ColliderBoxMode::Setup(const AZ::EntityComponentIdPair& idPair)
+    {
+        m_boxEdit->Setup(idPair);
     }
 
     void ColliderBoxMode::Refresh([[maybe_unused]] const AZ::EntityComponentIdPair& idPair)
     {
-        m_boxEdit.UpdateManipulators();
+        m_boxEdit->UpdateManipulators();
     }
 
     void ColliderBoxMode::Teardown([[maybe_unused]] const AZ::EntityComponentIdPair& idPair)
     {
-        m_boxEdit.Teardown();
+        m_boxEdit->Teardown();
     }
 
     void ColliderBoxMode::ResetValues(const AZ::EntityComponentIdPair& idPair)

+ 3 - 1
Gems/PhysX/Code/Editor/ColliderBoxMode.h

@@ -19,6 +19,8 @@ namespace PhysX
     public:
         AZ_CLASS_ALLOCATOR_DECL
 
+        ColliderBoxMode();
+
         // PhysXSubComponentModeBase ...
         void Setup(const AZ::EntityComponentIdPair& idPair) override;
         void Refresh(const AZ::EntityComponentIdPair& idPair) override;
@@ -26,6 +28,6 @@ namespace PhysX
         void ResetValues(const AZ::EntityComponentIdPair& idPair) override;
 
     private:
-        AzToolsFramework::BoxViewportEdit m_boxEdit;
+        AZStd::unique_ptr<AzToolsFramework::BoxViewportEdit> m_boxEdit;
     };
 } //namespace PhysX

+ 1 - 1
Gems/PhysX/Code/Editor/ColliderSphereMode.cpp

@@ -13,7 +13,7 @@
 #include <AzToolsFramework/Manipulators/LinearManipulator.h>
 #include <AzToolsFramework/Manipulators/ManipulatorManager.h>
 #include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
-#include <AzToolsFramework/ComponentModes/BoxViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ViewportEditUtilities.h>
 #include <AzCore/Component/TransformBus.h>
 #include <AzCore/Component/NonUniformScaleBus.h>
 #include <AzFramework/Viewport/ViewportColors.h>

+ 3 - 0
Gems/PhysX/Code/Source/EditorColliderComponent.cpp

@@ -403,6 +403,8 @@ namespace PhysX
         AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusConnect(
             AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(
+            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
         ColliderShapeRequestBus::Handler::BusConnect(GetEntityId());
         AZ::Render::MeshComponentNotificationBus::Handler::BusConnect(GetEntityId());
         EditorColliderComponentRequestBus::Handler::BusConnect(AZ::EntityComponentIdPair(GetEntityId(), GetId()));
@@ -459,6 +461,7 @@ namespace PhysX
         EditorColliderComponentRequestBus::Handler::BusDisconnect();
         AZ::Render::MeshComponentNotificationBus::Handler::BusDisconnect();
         ColliderShapeRequestBus::Handler::BusDisconnect();
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusDisconnect();
         AzToolsFramework::BoxManipulatorRequestBus::Handler::BusDisconnect();
         AZ::TransformNotificationBus::Handler::BusDisconnect();
         PhysX::MeshColliderComponentRequestsBus::Handler::BusDisconnect();

+ 5 - 1
Gems/PhysX/Code/Source/EditorColliderComponent.h

@@ -24,6 +24,7 @@
 #include <AzToolsFramework/API/ToolsApplicationAPI.h>
 #include <AzToolsFramework/ComponentMode/ComponentModeDelegate.h>
 #include <AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 #include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
 
 #include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
@@ -118,6 +119,7 @@ namespace PhysX
         , protected DebugDraw::DisplayCallback
         , protected AzToolsFramework::EntitySelectionEvents::Bus::Handler
         , private AzToolsFramework::BoxManipulatorRequestBus::Handler
+        , private AzToolsFramework::ShapeManipulatorRequestBus::Handler
         , private AZ::Data::AssetBus::Handler
         , private PhysX::MeshColliderComponentRequestsBus::Handler
         , private AZ::TransformNotificationBus::Handler
@@ -198,9 +200,11 @@ namespace PhysX
         // AzToolsFramework::BoxManipulatorRequestBus
         AZ::Vector3 GetDimensions() const override;
         void SetDimensions(const AZ::Vector3& dimensions) override;
+        AZ::Transform GetCurrentLocalTransform() const override;
+
+        // AzToolsFramework::ShapeManipulatorRequestBus overrides ...
         AZ::Vector3 GetTranslationOffset() const override;
         void SetTranslationOffset(const AZ::Vector3& translationOffset) override;
-        AZ::Transform GetCurrentLocalTransform() const override;
         AZ::Transform GetManipulatorSpace() const override;
 
         // AZ::Render::MeshComponentNotificationBus

+ 4 - 4
Gems/PhysX/Code/Tests/PhysXColliderComponentModeTests.cpp

@@ -669,8 +669,8 @@ namespace UnitTest
 
         // the offset should not have changed, because the editing was symmetrical
         AZ::Vector3 newBoxOffset = AZ::Vector3::CreateZero();
-        AzToolsFramework::BoxManipulatorRequestBus::EventResult(
-            newBoxOffset, m_idPair, &AzToolsFramework::BoxManipulatorRequests::GetTranslationOffset);
+        AzToolsFramework::ShapeManipulatorRequestBus::EventResult(
+            newBoxOffset, m_idPair, &AzToolsFramework::ShapeManipulatorRequests::GetTranslationOffset);
 
         EXPECT_THAT(newBoxOffset, IsCloseTolerance(boxOffset, ManipulatorTolerance));
     }
@@ -725,8 +725,8 @@ namespace UnitTest
 
         // the offset should have changed, because the editing was asymmetrical
         AZ::Vector3 newBoxOffset = AZ::Vector3::CreateZero();
-        AzToolsFramework::BoxManipulatorRequestBus::EventResult(
-            newBoxOffset, m_idPair, &AzToolsFramework::BoxManipulatorRequests::GetTranslationOffset);
+        AzToolsFramework::ShapeManipulatorRequestBus::EventResult(
+            newBoxOffset, m_idPair, &AzToolsFramework::ShapeManipulatorRequests::GetTranslationOffset);
 
         // the offset should have moved 0.25 units (half the change in the z dimension)
         // along the -z axis, tranformed by the local rotation of the box