Browse Source

Merge pull request #14379 from aws-lumberyard-dev/shape-offset-part-twelve

Asymmetrical capsule manipulators and capsule component mode
greerdv 2 years ago
parent
commit
fb7ad0dba0
41 changed files with 1336 additions and 717 deletions
  1. 12 0
      Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h
  2. 19 0
      Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp
  3. 366 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeComponentMode.cpp
  4. 67 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeComponentMode.h
  5. 28 5
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeViewportEdit.cpp
  6. 10 4
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeViewportEdit.h
  7. 35 366
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.cpp
  8. 5 35
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.h
  9. 5 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.cpp
  10. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.h
  11. 114 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleComponentMode.cpp
  12. 45 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleComponentMode.h
  13. 103 83
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.cpp
  14. 13 11
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.h
  15. 5 0
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.cpp
  16. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h
  17. 38 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/CapsuleManipulatorRequestBus.h
  18. 30 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/RadiusManipulatorRequestBus.h
  19. 6 0
      Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake
  20. 5 4
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderCapsuleManipulators.cpp
  21. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderCapsuleManipulators.h
  22. 3 3
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderRotationManipulators.cpp
  23. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderRotationManipulators.h
  24. 5 5
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderTranslationManipulators.cpp
  25. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderTranslationManipulators.h
  26. 3 3
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointLimitRotationManipulators.cpp
  27. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointLimitRotationManipulators.h
  28. 4 4
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointSwingLimitManipulators.cpp
  29. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointSwingLimitManipulators.h
  30. 4 4
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointTwistLimitManipulators.cpp
  31. 1 1
      Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointTwistLimitManipulators.h
  32. 5 7
      Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.cpp
  33. 96 15
      Gems/LmbrCentral/Code/Source/Shape/EditorCapsuleShapeComponent.cpp
  34. 34 6
      Gems/LmbrCentral/Code/Source/Shape/EditorCapsuleShapeComponent.h
  35. 137 2
      Gems/LmbrCentral/Code/Tests/EditorCapsuleShapeComponentTests.cpp
  36. 15 19
      Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.cpp
  37. 5 8
      Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.h
  38. 3 5
      Gems/PhysX/Code/Editor/ColliderBoxMode.cpp
  39. 22 15
      Gems/PhysX/Code/Editor/ColliderCapsuleMode.cpp
  40. 14 4
      Gems/PhysX/Code/Editor/ColliderComponentMode.cpp
  41. 72 101
      Gems/PhysX/Code/Tests/PhysXColliderComponentModeTests.cpp

+ 12 - 0
Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h

@@ -12,6 +12,7 @@
 #include <AzToolsFramework/Manipulators/ManipulatorBus.h>
 #include <AzToolsFramework/Manipulators/ManipulatorManager.h>
 #include <AzToolsFramework/Manipulators/PlanarManipulator.h>
+#include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
 
 namespace AzManipulatorTestFramework
 {
@@ -41,4 +42,15 @@ namespace AzManipulatorTestFramework
 
     //! Default viewport size (1080p) in 16:9 aspect ratio.
     inline const auto DefaultViewportSize = AzFramework::ScreenSize(1920, 1080);
+
+    //! Converts the provided world start and end positions into screen space co-ordinates for the given camera state,
+    //! and drags the mouse from the start to the end position.
+    //! A keyboard modifier can optionally be provided.
+    void DragMouse(
+        const AzFramework::CameraState& cameraState,
+        AzManipulatorTestFramework::ImmediateModeActionDispatcher* actionDispatcher,
+        const AZ::Vector3& worldStart,
+        const AZ::Vector3& worldEnd,
+        const AzToolsFramework::ViewportInteraction::KeyboardModifier keyboardModifier =
+            AzToolsFramework::ViewportInteraction::KeyboardModifier::None);
 } // namespace AzManipulatorTestFramework

+ 19 - 0
Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp

@@ -106,4 +106,23 @@ namespace AzManipulatorTestFramework
     {
         return { cameraState.m_viewportSize.m_width / 2, cameraState.m_viewportSize.m_height / 2 };
     }
+
+    void DragMouse(
+        const AzFramework::CameraState& cameraState,
+        AzManipulatorTestFramework::ImmediateModeActionDispatcher* actionDispatcher,
+        const AZ::Vector3& worldStart,
+        const AZ::Vector3& worldEnd,
+        const AzToolsFramework::ViewportInteraction::KeyboardModifier keyboardModifier)
+    {
+        const auto screenStart = AzFramework::WorldToScreen(worldStart, cameraState);
+        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, cameraState);
+
+        actionDispatcher
+            ->CameraState(cameraState)
+            ->MousePosition(screenStart)
+            ->KeyboardModifierDown(keyboardModifier)
+            ->MouseLButtonDown()
+            ->MousePosition(screenEnd)
+            ->MouseLButtonUp();
+    }
 } // namespace AzManipulatorTestFramework

+ 366 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeComponentMode.cpp

@@ -0,0 +1,366 @@
+/*
+ * 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 <AzToolsFramework/ComponentModes/BaseShapeComponentMode.h>
+
+#include <AzCore/Component/NonUniformScaleBus.h>
+#include <AzToolsFramework/API/ComponentModeCollectionInterface.h>
+#include <AzToolsFramework/ActionManager/Action/ActionManagerInterface.h>
+#include <AzToolsFramework/ActionManager/HotKey/HotKeyManagerInterface.h>
+#include <AzToolsFramework/ActionManager/Menu/MenuManagerInterface.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.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.shape.setdimensionssubmode");
+        const AZ::Crc32 SetTranslationOffsetSubModeActionUri = AZ_CRC_CE("org.o3de.action.shape.settranslationoffsetsubmode");
+        const AZ::Crc32 ResetSubModeActionUri = AZ_CRC_CE("org.o3de.action.shape.resetsubmode");
+    } // namespace
+
+    void InstallBaseShapeViewportEditFunctions(
+        BaseShapeViewportEdit* baseShapeViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair)
+    {
+        auto getManipulatorSpace = [entityComponentIdPair]()
+        {
+            AZ::Transform manipulatorSpace = AZ::Transform::CreateIdentity();
+            ShapeManipulatorRequestBus::EventResult(
+                manipulatorSpace, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetManipulatorSpace);
+            return manipulatorSpace;
+        };
+        auto getNonUniformScale = [entityComponentIdPair]()
+        {
+            AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
+            AZ::NonUniformScaleRequestBus::EventResult(
+                nonUniformScale, entityComponentIdPair.GetEntityId(), &AZ::NonUniformScaleRequestBus::Events::GetScale);
+            return nonUniformScale;
+        };
+        auto getTranslationOffset = [entityComponentIdPair]()
+        {
+            AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
+            ShapeManipulatorRequestBus::EventResult(
+                translationOffset, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
+            return translationOffset;
+        };
+        auto setTranslationOffset = [entityComponentIdPair](const AZ::Vector3& translationOffset)
+        {
+            ShapeManipulatorRequestBus::Event(
+                entityComponentIdPair, &ShapeManipulatorRequestBus::Events::SetTranslationOffset, translationOffset);
+        };
+        baseShapeViewportEdit->InstallGetManipulatorSpace(AZStd::move(getManipulatorSpace));
+        baseShapeViewportEdit->InstallGetNonUniformScale(AZStd::move(getNonUniformScale));
+        baseShapeViewportEdit->InstallGetTranslationOffset(AZStd::move(getTranslationOffset));
+        baseShapeViewportEdit->InstallSetTranslationOffset(AZStd::move(setTranslationOffset));
+    }
+
+    static ViewportUi::ButtonId RegisterClusterButton(
+        AZ::s32 viewportId, ViewportUi::ClusterId clusterId, const char* iconName, const char* tooltip)
+    {
+        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;
+    }
+
+    AZ_CLASS_ALLOCATOR_IMPL(BaseShapeComponentMode, AZ::SystemAllocator, 0)
+
+    BaseShapeComponentMode::BaseShapeComponentMode(
+        const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid componentType, bool allowAsymmetricalEditing)
+        : EditorBaseComponentMode::EditorBaseComponentMode(entityComponentIdPair, componentType)
+        , m_entityComponentIdPair(entityComponentIdPair)
+        , m_allowAsymmetricalEditing(allowAsymmetricalEditing)
+    {
+    }
+
+    BaseShapeComponentMode::~BaseShapeComponentMode()
+    {
+        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 BaseShapeComponentMode::Refresh()
+    {
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->UpdateManipulators();
+    }
+
+    void BaseShapeComponentMode::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 BaseShapeComponentMode::GetShapeSubMode() const
+    {
+        return m_subMode;
+    }
+
+    void BaseShapeComponentMode::SetShapeSubMode(ShapeComponentModeRequests::SubMode mode)
+    {
+        const auto modeIndex = static_cast<AZ::u32>(mode);
+        AZ_Assert(modeIndex < m_subModes.size(), "Submode not found:%d", modeIndex);
+        m_subModes[static_cast<AZ::u32>(m_subMode)]->Teardown();
+        AZ_Assert(modeIndex < m_buttonIds.size(), "Invalid mode index %i.", modeIndex);
+        m_subMode = mode;
+        m_subModes[modeIndex]->Setup(g_mainManipulatorManagerId);
+        m_subModes[modeIndex]->AddEntityComponentIdPair(m_entityComponentIdPair);
+
+        ViewportUi::ViewportUiRequestBus::Event(
+            ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::ClearClusterActiveButton, m_clusterId);
+
+        ViewportUi::ViewportUiRequestBus::Event(
+            ViewportUi::DefaultViewportId,
+            &ViewportUi::ViewportUiRequestBus::Events::SetClusterActiveButton,
+            m_clusterId,
+            m_buttonIds[modeIndex]);
+    }
+
+    void BaseShapeComponentMode::ResetShapeSubMode()
+    {
+        const auto modeIndex = static_cast<AZ::u32>(m_subMode);
+        m_subModes[modeIndex]->ResetValues();
+        m_subModes[modeIndex]->UpdateManipulators();
+    }
+
+    bool BaseShapeComponentMode::HandleMouseInteraction(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
+    {
+        if (!m_allowAsymmetricalEditing)
+        {
+            return false;
+        }
+
+        if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Wheel &&
+            mouseInteraction.m_mouseInteraction.m_keyboardModifiers.Ctrl())
+        {
+            const int direction = MouseWheelDelta(mouseInteraction) > 0.0f ? -1 : 1;
+            const AZ::u32 currentModeIndex = static_cast<AZ::u32>(m_subMode);
+            const AZ::u32 numSubModes = static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::NumModes);
+            const AZ::u32 nextModeIndex = (currentModeIndex + numSubModes + direction) % m_subModes.size();
+            ShapeComponentModeRequests::SubMode nextMode = static_cast<ShapeComponentModeRequests::SubMode>(nextModeIndex);
+            SetShapeSubMode(nextMode);
+            return true;
+        }
+        return false;
+    }
+
+    AZStd::vector<ActionOverride> BaseShapeComponentMode::PopulateActionsImpl()
+    {
+        if (!m_allowAsymmetricalEditing)
+        {
+            return {};
+        }
+
+        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);
+            });
+
+        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);
+            });
+
+        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 BaseShapeComponentMode::RegisterActions(const char* shapeName)
+    {
+        AZStd::string category = AZStd::string::format("%s Component Mode", shapeName);
+        AZStd::to_upper(category.begin(), category.begin() + 1);
+
+        auto actionManagerInterface = AZ::Interface<ActionManagerInterface>::Get();
+        AZ_Assert(actionManagerInterface, "BaseShapeComponentMode - could not get ActionManagerInterface on RegisterActions.");
+
+        auto hotKeyManagerInterface = AZ::Interface<HotKeyManagerInterface>::Get();
+        AZ_Assert(hotKeyManagerInterface, "BaseShapeComponentMode - could not get HotKeyManagerInterface on RegisterActions.");
+
+        // Dimensions sub-mode
+        {
+            const auto actionIdentifier = AZStd::string::format("o3de.action.%sComponentMode.setDimensionsSubMode", shapeName);
+            ActionProperties actionProperties;
+            actionProperties.m_name = "Set Dimensions Mode";
+            actionProperties.m_description = "Set Dimensions Mode";
+            actionProperties.m_category = category;
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<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
+        {
+            const auto actionIdentifier = AZStd::string::format("o3de.action.%sComponentMode.setTranslationOffsetSubMode", shapeName);
+            ActionProperties actionProperties;
+            actionProperties.m_name = "Set Translation Offset Mode";
+            actionProperties.m_description = "Set Translation Offset Mode";
+            actionProperties.m_category = category;
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<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
+        {
+            const auto actionIdentifier = AZStd::string::format("o3de.action.%sComponentMode.resetCurrentMode", shapeName);
+            ActionProperties actionProperties;
+            actionProperties.m_name = "Reset Current Mode";
+            actionProperties.m_description = "Reset Current Mode";
+            actionProperties.m_category = category;
+
+            actionManagerInterface->RegisterAction(
+                EditorMainWindowActionContextIdentifier,
+                actionIdentifier,
+                actionProperties,
+                []
+                {
+                    auto componentModeCollectionInterface = AZ::Interface<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 BaseShapeComponentMode::BindActionsToModes(const char* shapeName, const char* className)
+    {
+        auto actionManagerInterface = AZ::Interface<ActionManagerInterface>::Get();
+        AZ_Assert(actionManagerInterface, "BaseShapeComponentMode - could not get ActionManagerInterface on RegisterActions.");
+
+        const AZStd::string modeIdentifier = AZStd::string::format("o3de.context.mode.%s", className);
+        const AZStd::string prefix = AZStd::string::format("o3de.action.%sComponentMode", shapeName);
+
+        actionManagerInterface->AssignModeToAction(modeIdentifier, prefix + ".setDimensionsSubMode");
+        actionManagerInterface->AssignModeToAction(modeIdentifier, prefix + ".setTranslationOffsetSubMode");
+        actionManagerInterface->AssignModeToAction(modeIdentifier, prefix + ".resetCurrentMode");
+    }
+
+    void BaseShapeComponentMode::BindActionsToMenus(const char* shapeName)
+    {
+        auto menuManagerInterface = AZ::Interface<MenuManagerInterface>::Get();
+        AZ_Assert(menuManagerInterface, "BaseShapeComponentMode - could not get MenuManagerInterface on BindActionsToMenus.");
+
+        const AZStd::string prefix = AZStd::string::format("o3de.action.%sComponentMode", shapeName);
+
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, prefix + ".setDimensionsSubMode", 6000);
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, prefix + ".setTranslationOffsetSubMode", 6001);
+        menuManagerInterface->AddActionToMenu(EditMenuIdentifier, prefix + ".resetCurrentMode", 6002);
+    }
+} // namespace AzToolsFramework
+

+ 67 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeComponentMode.h

@@ -0,0 +1,67 @@
+/*
+ * 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/ComponentModes/BaseShapeViewportEdit.h>
+#include <AzToolsFramework/ComponentModes/ShapeComponentModeBus.h>
+#include <AzToolsFramework/ComponentMode/EditorBaseComponentMode.h>
+
+namespace AzToolsFramework
+{
+    void InstallBaseShapeViewportEditFunctions(
+        BaseShapeViewportEdit* baseShapeViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair);
+
+    //! Base class for shape component modes.
+    //! Handles common logic such as setting up sub-modes for dimensions and translation offset, handling mode
+    //! selection, registering actions, etc.
+    class BaseShapeComponentMode
+        : public ComponentModeFramework::EditorBaseComponentMode
+        , public ShapeComponentModeRequestBus::Handler
+    {
+    public:
+        AZ_CLASS_ALLOCATOR_DECL
+        AZ_RTTI(BaseShapeComponentMode, "{19DE4C0C-4918-4D1B-8F3D-365D731AC20C}", EditorBaseComponentMode)
+
+        static void RegisterActions(const char* shapeName);
+        static void BindActionsToModes(const char* shapeName, const char* className);
+        static void BindActionsToMenus(const char* shapeName);
+
+        BaseShapeComponentMode(
+            const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid componentType, bool allowAsymmetricalEditing = false);
+        BaseShapeComponentMode(const BaseShapeComponentMode&) = delete;
+        BaseShapeComponentMode& operator=(const BaseShapeComponentMode&) = delete;
+        BaseShapeComponentMode(BaseShapeComponentMode&&) = delete;
+        BaseShapeComponentMode& operator=(BaseShapeComponentMode&&) = delete;
+        virtual ~BaseShapeComponentMode();
+
+        // EditorBaseComponentMode overrides ...
+        void Refresh() override;
+        AZStd::vector<AzToolsFramework::ActionOverride> PopulateActionsImpl() 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";
+
+    protected:
+        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<BaseShapeViewportEdit>, 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

+ 28 - 5
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeViewportEdit.cpp

@@ -36,9 +36,9 @@ namespace AzToolsFramework
         m_beginEditing = AZStd::move(beginEditing);
     }
 
-    void BaseShapeViewportEdit::InstallFinishEditing(AZStd::function<void()> finishEditing)
+    void BaseShapeViewportEdit::InstallEndEditing(AZStd::function<void()> endEditing)
     {
-        m_finishEditing = AZStd::move(finishEditing);
+        m_endEditing = AZStd::move(endEditing);
     }
 
     AZ::Transform BaseShapeViewportEdit::GetManipulatorSpace() const
@@ -91,11 +91,34 @@ namespace AzToolsFramework
         }
     }
 
-    void BaseShapeViewportEdit::FinishEditing()
+    void BaseShapeViewportEdit::EndEditing()
     {
-        if (m_finishEditing)
+        if (m_endEditing)
         {
-            m_finishEditing();
+            m_endEditing();
+        }
+    }
+
+    void BaseShapeViewportEdit::BeginUndoBatch(const char* label)
+    {
+        if (!m_entityIds.empty())
+        {
+            ToolsApplicationRequests::Bus::BroadcastResult(
+                m_undoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, label);
+
+            for (const AZ::EntityId& entityId : m_entityIds)
+            {
+                ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entityId);
+            }
+        }
+    }
+
+    void BaseShapeViewportEdit::EndUndoBatch()
+    {
+        if (m_undoBatch)
+        {
+            ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::Bus::Events::EndUndoBatch);
+            m_undoBatch = nullptr;
         }
     }
 } // namespace AzToolsFramework

+ 10 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BaseShapeViewportEdit.h

@@ -43,12 +43,12 @@ namespace AzToolsFramework
         //! but they may be useful in other viewports.
         //! @{
         void InstallBeginEditing(AZStd::function<void()> beginEditing);
-        void InstallFinishEditing(AZStd::function<void()> finishEditing);
+        void InstallEndEditing(AZStd::function<void()> endEditing);
         //! @}
 
         //! Create manipulators for the shape properties to be edited.
         //! Make sure to install all the required functions before calling Setup.
-        virtual void Setup(const ManipulatorManagerId manipulatorManagerId = g_mainManipulatorManagerId) = 0;
+        virtual void Setup(const ManipulatorManagerId manipulatorManagerId) = 0;
         //! Destroy the manipulators for the shape properties being edited.
         virtual void Teardown() = 0;
         //! Call after modifying the shape to ensure that the space the manipulators operate in is updated, along with other properties.
@@ -66,7 +66,10 @@ namespace AzToolsFramework
         AZ::Vector3 GetTranslationOffset() const;
         void SetTranslationOffset(const AZ::Vector3& translationOffset);
         void BeginEditing();
-        void FinishEditing();
+        void EndEditing();
+
+        void BeginUndoBatch(const char* label);
+        void EndUndoBatch();
 
         AZStd::function<AZ::Transform()> m_getManipulatorSpace;
         AZStd::function<AZ::Vector3()> m_getNonUniformScale;
@@ -74,6 +77,9 @@ namespace AzToolsFramework
         AZStd::function<void(const AZ::Vector3&)> m_setTranslationOffset;
 
         AZStd::function<void()> m_beginEditing;
-        AZStd::function<void()> m_finishEditing;
+        AZStd::function<void()> m_endEditing;
+
+        AZStd::unordered_set<AZ::EntityId> m_entityIds;
+        UndoSystem::URSequencePoint* m_undoBatch = nullptr;
     };
 } // namespace AzToolsFramework

+ 35 - 366
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.cpp

@@ -6,28 +6,36 @@
  *
  */
 
-#include "BoxComponentMode.h"
-#include <AzCore/Component/NonUniformScaleBus.h>
-#include <AzCore/Component/TransformBus.h>
-#include <AzToolsFramework/API/ComponentModeCollectionInterface.h>
-#include <AzToolsFramework/ActionManager/Action/ActionManagerInterface.h>
-#include <AzToolsFramework/ActionManager/HotKey/HotKeyManagerInterface.h>
-#include <AzToolsFramework/ActionManager/Menu/MenuManagerInterface.h>
+#include <AzToolsFramework/ComponentModes/BoxComponentMode.h>
+
+#include <AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h>
 #include <AzToolsFramework/Manipulators/BoxManipulatorRequestBus.h>
-#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 
 namespace AzToolsFramework
 {
-    static constexpr AZStd::string_view EditorMainWindowActionContextIdentifier = "o3de.context.editor.mainwindow";
-    static constexpr AZStd::string_view EditMenuIdentifier = "o3de.menu.editor.edit";
-
-    namespace
+    void InstallBoxViewportEditFunctions(BoxViewportEdit* boxViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair)
     {
-        //! 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
+        auto getLocalTransform = [entityComponentIdPair]()
+        {
+            AZ::Transform boxLocalTransform = AZ::Transform::CreateIdentity();
+            BoxManipulatorRequestBus::EventResult(
+                boxLocalTransform, entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetCurrentLocalTransform);
+            return boxLocalTransform;
+        };
+        auto getBoxDimensions = [entityComponentIdPair]()
+        {
+            AZ::Vector3 boxDimensions = AZ::Vector3::CreateOne();
+            BoxManipulatorRequestBus::EventResult(boxDimensions, entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetDimensions);
+            return boxDimensions;
+        };
+        auto setBoxDimensions = [entityComponentIdPair](const AZ::Vector3& boxDimensions)
+        {
+            BoxManipulatorRequestBus::Event(entityComponentIdPair, &BoxManipulatorRequestBus::Events::SetDimensions, boxDimensions);
+        };
+        boxViewportEdit->InstallGetLocalTransform(AZStd::move(getLocalTransform));
+        boxViewportEdit->InstallGetBoxDimensions(AZStd::move(getBoxDimensions));
+        boxViewportEdit->InstallSetBoxDimensions(AZStd::move(setBoxDimensions));
+    }
 
     AZ_CLASS_ALLOCATOR_IMPL(BoxComponentMode, AZ::SystemAllocator, 0)
 
@@ -38,9 +46,7 @@ namespace AzToolsFramework
 
     BoxComponentMode::BoxComponentMode(
         const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid componentType, bool allowAsymmetricalEditing)
-        : EditorBaseComponentMode(entityComponentIdPair, componentType)
-        , m_entityComponentIdPair(entityComponentIdPair)
-        , m_allowAsymmetricalEditing(allowAsymmetricalEditing)
+        : BaseShapeComponentMode(entityComponentIdPair, componentType, allowAsymmetricalEditing)
     {
         auto boxViewportEdit = AZStd::make_unique<BoxViewportEdit>(m_allowAsymmetricalEditing);
         InstallBaseShapeViewportEditFunctions(boxViewportEdit.get(), m_entityComponentIdPair);
@@ -58,215 +64,13 @@ namespace AzToolsFramework
         }
         else
         {
-            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->Setup();
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->Setup(g_mainManipulatorManagerId);
             m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->AddEntityComponentIdPair(
                 m_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)
-    {
-        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()
-    {
-        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_subModes[static_cast<AZ::u32>(m_subMode)]->UpdateManipulators();
-    }
-
     AZStd::string BoxComponentMode::GetComponentModeName() const
     {
         return "Box Edit Mode";
@@ -277,155 +81,20 @@ 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_subModes[modeIndex]->AddEntityComponentIdPair(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()
+    void BoxComponentMode::RegisterActions()
     {
-        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);
+        BaseShapeComponentMode::RegisterActions("box");
     }
 
-    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;
-    }
-
-    void InstallBaseShapeViewportEditFunctions(
-        BaseShapeViewportEdit* baseShapeViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair)
+    void BoxComponentMode::BindActionsToModes()
     {
-        auto getManipulatorSpace = [entityComponentIdPair]()
-        {
-            AZ::Transform manipulatorSpace = AZ::Transform::CreateIdentity();
-            ShapeManipulatorRequestBus::EventResult(
-                manipulatorSpace, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetManipulatorSpace);
-            return manipulatorSpace;
-        };
-        auto getNonUniformScale = [entityComponentIdPair]()
-        {
-            AZ::Vector3 nonUniformScale = AZ::Vector3::CreateOne();
-            AZ::NonUniformScaleRequestBus::EventResult(
-                nonUniformScale, entityComponentIdPair.GetEntityId(), &AZ::NonUniformScaleRequestBus::Events::GetScale);
-            return nonUniformScale;
-        };
-        auto getTranslationOffset = [entityComponentIdPair]()
-        {
-            AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();
-            ShapeManipulatorRequestBus::EventResult(
-                translationOffset, entityComponentIdPair, &ShapeManipulatorRequestBus::Events::GetTranslationOffset);
-            return translationOffset;
-        };
-        auto setTranslationOffset = [entityComponentIdPair](const AZ::Vector3& translationOffset)
-        {
-            ShapeManipulatorRequestBus::Event(
-                entityComponentIdPair, &ShapeManipulatorRequestBus::Events::SetTranslationOffset, translationOffset);
-        };
-        baseShapeViewportEdit->InstallGetManipulatorSpace(AZStd::move(getManipulatorSpace));
-        baseShapeViewportEdit->InstallGetNonUniformScale(AZStd::move(getNonUniformScale));
-        baseShapeViewportEdit->InstallGetTranslationOffset(AZStd::move(getTranslationOffset));
-        baseShapeViewportEdit->InstallSetTranslationOffset(AZStd::move(setTranslationOffset));
+        AZ::SerializeContext* serializeContext = nullptr;
+        AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
+        BaseShapeComponentMode::BindActionsToModes("box", serializeContext->FindClassData(azrtti_typeid<BoxComponentMode>())->m_name);
     }
 
-    void InstallBoxViewportEditFunctions(BoxViewportEdit* boxViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair)
+    void BoxComponentMode::BindActionsToMenus()
     {
-        auto getLocalTransform = [entityComponentIdPair]()
-        {
-            AZ::Transform boxLocalTransform = AZ::Transform::CreateIdentity();
-            BoxManipulatorRequestBus::EventResult(
-                boxLocalTransform, entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetCurrentLocalTransform);
-            return boxLocalTransform;
-        };
-        auto getBoxDimensions = [entityComponentIdPair]()
-        {
-            AZ::Vector3 boxDimensions = AZ::Vector3::CreateOne();
-            BoxManipulatorRequestBus::EventResult(boxDimensions, entityComponentIdPair, &BoxManipulatorRequestBus::Events::GetDimensions);
-            return boxDimensions;
-        };
-        auto setBoxDimensions = [entityComponentIdPair](const AZ::Vector3& boxDimensions)
-        {
-            BoxManipulatorRequestBus::Event(entityComponentIdPair, &BoxManipulatorRequestBus::Events::SetDimensions, boxDimensions);
-        };
-        boxViewportEdit->InstallGetLocalTransform(AZStd::move(getLocalTransform));
-        boxViewportEdit->InstallGetBoxDimensions(AZStd::move(getBoxDimensions));
-        boxViewportEdit->InstallSetBoxDimensions(AZStd::move(setBoxDimensions));
+        BaseShapeComponentMode::BindActionsToMenus("box");
     }
 } // namespace AzToolsFramework

+ 5 - 35
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxComponentMode.h

@@ -8,28 +8,20 @@
 
 #pragma once
 
-#include <AzToolsFramework/ComponentMode/EditorBaseComponentMode.h>
+#include <AzToolsFramework/ComponentModes/BaseShapeComponentMode.h>
 #include <AzToolsFramework/ComponentModes/BoxViewportEdit.h>
-#include <AzToolsFramework/ComponentModes/ShapeComponentModeBus.h>
-#include <AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h>
 
 namespace AzToolsFramework
 {
-    class LinearManipulator;
-
-    void InstallBaseShapeViewportEditFunctions(
-        BaseShapeViewportEdit* baseShapeViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair);
-
     void InstallBoxViewportEditFunctions(BoxViewportEdit* boxViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair);
 
     //! The specific ComponentMode responsible for handling box editing.
     class BoxComponentMode
-        : public ComponentModeFramework::EditorBaseComponentMode
-        , public ShapeComponentModeRequestBus::Handler
+        : public BaseShapeComponentMode
     {
     public:
         AZ_CLASS_ALLOCATOR_DECL
-        AZ_RTTI(BoxComponentMode, "{8E09B2C1-ED99-4945-A0B1-C4AFE6FE2FA9}", EditorBaseComponentMode)
+        AZ_RTTI(BoxComponentMode, "{8E09B2C1-ED99-4945-A0B1-C4AFE6FE2FA9}", BaseShapeComponentMode)
 
         static void Reflect(AZ::ReflectContext* context);
 
@@ -43,32 +35,10 @@ namespace AzToolsFramework
         BoxComponentMode& operator=(const BoxComponentMode&) = delete;
         BoxComponentMode(BoxComponentMode&&) = delete;
         BoxComponentMode& operator=(BoxComponentMode&&) = delete;
-        ~BoxComponentMode();
+        ~BoxComponentMode() = default;
 
         // 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:
-        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<BaseShapeViewportEdit>, 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

+ 5 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.cpp

@@ -99,6 +99,7 @@ namespace AzToolsFramework
 
     void BoxViewportEdit::AddEntityComponentIdPair(const AZ::EntityComponentIdPair& entityComponentIdPair)
     {
+        m_entityIds.insert(entityComponentIdPair.GetEntityId());
         for (size_t manipulatorIndex = 0; manipulatorIndex < m_linearManipulators.size(); ++manipulatorIndex)
         {
             if (auto& linearManipulator = m_linearManipulators[manipulatorIndex]; linearManipulator)
@@ -183,7 +184,11 @@ namespace AzToolsFramework
 
     void BoxViewportEdit::ResetValues()
     {
+        // manipulators handle undo batches themselves, but this function does not work via manipulators so needs its own undo batch
+        BeginUndoBatch("BoxViewportEdit Reset");
         SetBoxDimensions(AZ::Vector3::CreateOne());
         SetTranslationOffset(AZ::Vector3::CreateZero());
+        ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values);
+        EndUndoBatch();
     }
 } // namespace AzToolsFramework

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/BoxViewportEdit.h

@@ -24,7 +24,7 @@ namespace AzToolsFramework
         BoxViewportEdit(bool allowAsymmetricalEditing = false);
 
         // BaseShapeViewportEdit overrides ...
-        void Setup(const ManipulatorManagerId manipulatorManagerId = g_mainManipulatorManagerId) override;
+        void Setup(const ManipulatorManagerId manipulatorManagerId) override;
         void Teardown() override;
         void UpdateManipulators() override;
         void ResetValues() override;

+ 114 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleComponentMode.cpp

@@ -0,0 +1,114 @@
+/*
+ * 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 <AzToolsFramework/ComponentModes/CapsuleComponentMode.h>
+
+#include <AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h>
+#include <AzToolsFramework/Manipulators/CapsuleManipulatorRequestBus.h>
+#include <AzToolsFramework/Manipulators/RadiusManipulatorRequestBus.h>
+
+namespace AzToolsFramework
+{
+    void InstallCapsuleViewportEditFunctions(CapsuleViewportEdit* capsuleViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair)
+    {
+        auto getCapsuleHeight = [entityComponentIdPair]()
+        {
+            float height = 1.0f;
+            CapsuleManipulatorRequestBus::EventResult(height, entityComponentIdPair, &CapsuleManipulatorRequestBus::Events::GetHeight);
+            return height;
+        };
+        auto getCapsuleRadius = [entityComponentIdPair]()
+        {
+            float radius = 0.25f;
+            RadiusManipulatorRequestBus::EventResult(radius, entityComponentIdPair, &RadiusManipulatorRequestBus::Events::GetRadius);
+            return radius;
+        };
+        auto getRotationOffset = [entityComponentIdPair]()
+        {
+            AZ::Quaternion rotationOffset = AZ::Quaternion::CreateIdentity();
+            CapsuleManipulatorRequestBus::EventResult(
+                rotationOffset, entityComponentIdPair, &CapsuleManipulatorRequestBus::Events::GetRotationOffset);
+            return rotationOffset;
+        };
+        auto setCapsuleHeight = [entityComponentIdPair](float height)
+        {
+            CapsuleManipulatorRequestBus::Event(entityComponentIdPair, &CapsuleManipulatorRequestBus::Events::SetHeight, height);
+        };
+        auto setCapsuleRadius = [entityComponentIdPair](float radius)
+        {
+            RadiusManipulatorRequestBus::Event(entityComponentIdPair, &RadiusManipulatorRequestBus::Events::SetRadius, radius);
+        };
+        capsuleViewportEdit->InstallGetCapsuleHeight(AZStd::move(getCapsuleHeight));
+        capsuleViewportEdit->InstallGetCapsuleRadius(AZStd::move(getCapsuleRadius));
+        capsuleViewportEdit->InstallGetRotationOffset(AZStd::move(getRotationOffset));
+        capsuleViewportEdit->InstallSetCapsuleHeight(AZStd::move(setCapsuleHeight));
+        capsuleViewportEdit->InstallSetCapsuleRadius(AZStd::move(setCapsuleRadius));
+    }
+
+    AZ_CLASS_ALLOCATOR_IMPL(CapsuleComponentMode, AZ::SystemAllocator, 0)
+
+    void CapsuleComponentMode::Reflect(AZ::ReflectContext* context)
+    {
+        ComponentModeFramework::ReflectEditorBaseComponentModeDescendant<CapsuleComponentMode>(context);
+    }
+
+    CapsuleComponentMode::CapsuleComponentMode(
+        const AZ::EntityComponentIdPair& entityComponentIdPair, const AZ::Uuid componentType, bool allowAsymmetricalEditing)
+        : BaseShapeComponentMode(entityComponentIdPair, componentType, allowAsymmetricalEditing)
+    {
+        auto capsuleViewportEdit = AZStd::make_unique<CapsuleViewportEdit>(m_allowAsymmetricalEditing);
+        InstallBaseShapeViewportEditFunctions(capsuleViewportEdit.get(), m_entityComponentIdPair);
+        InstallCapsuleViewportEditFunctions(capsuleViewportEdit.get(), m_entityComponentIdPair);
+        m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)] = AZStd::move(capsuleViewportEdit);
+
+        if (m_allowAsymmetricalEditing)
+        {
+            auto shapeTranslationOffsetViewportEdit = AZStd::make_unique<ShapeTranslationOffsetViewportEdit>();
+            InstallBaseShapeViewportEditFunctions(shapeTranslationOffsetViewportEdit.get(), m_entityComponentIdPair);
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::TranslationOffset)] =
+                AZStd::move(shapeTranslationOffsetViewportEdit);
+            SetupCluster();
+            SetShapeSubMode(ShapeComponentModeRequests::SubMode::Dimensions);
+        }
+        else
+        {
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->Setup(g_mainManipulatorManagerId);
+            m_subModes[static_cast<AZ::u32>(ShapeComponentModeRequests::SubMode::Dimensions)]->AddEntityComponentIdPair(
+                m_entityComponentIdPair);
+        }
+        ShapeComponentModeRequestBus::Handler::BusConnect(m_entityComponentIdPair);
+    }
+
+    AZStd::string CapsuleComponentMode::GetComponentModeName() const
+    {
+        return "Capsule Edit Mode";
+    }
+
+    AZ::Uuid CapsuleComponentMode::GetComponentModeType() const
+    {
+        return azrtti_typeid<CapsuleComponentMode>();
+    }
+
+    void CapsuleComponentMode::RegisterActions()
+    {
+        BaseShapeComponentMode::RegisterActions("capsule");
+    }
+
+    void CapsuleComponentMode::BindActionsToModes()
+    {
+        AZ::SerializeContext* serializeContext = nullptr;
+        AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
+        BaseShapeComponentMode::BindActionsToModes(
+            "capsule", serializeContext->FindClassData(azrtti_typeid<CapsuleComponentMode>())->m_name);
+    }
+
+    void CapsuleComponentMode::BindActionsToMenus()
+    {
+        BaseShapeComponentMode::BindActionsToMenus("capsule");
+    }
+} // namespace AzToolsFramework

+ 45 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleComponentMode.h

@@ -0,0 +1,45 @@
+/*
+ * 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/ComponentModes/BaseShapeComponentMode.h>
+#include <AzToolsFramework/ComponentModes/CapsuleViewportEdit.h>
+
+namespace AzToolsFramework
+{
+    void InstallCapsuleViewportEditFunctions(
+        CapsuleViewportEdit* capsuleViewportEdit, const AZ::EntityComponentIdPair& entityComponentIdPair);
+
+    //! The specific ComponentMode responsible for handling capsule editing.
+    class CapsuleComponentMode
+        : public BaseShapeComponentMode
+    {
+    public:
+        AZ_CLASS_ALLOCATOR_DECL
+        AZ_RTTI(CapsuleComponentMode, "{17036B78-EB62-4F2B-8F74-C9FB037D8973}", BaseShapeComponentMode)
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        static void RegisterActions();
+        static void BindActionsToModes();
+        static void BindActionsToMenus();
+
+        CapsuleComponentMode(
+            const AZ::EntityComponentIdPair& entityComponentIdPair, AZ::Uuid componentType, bool allowAsymmetricalEditing = false);
+        CapsuleComponentMode(const CapsuleComponentMode&) = delete;
+        CapsuleComponentMode& operator=(const CapsuleComponentMode&) = delete;
+        CapsuleComponentMode(CapsuleComponentMode&&) = delete;
+        CapsuleComponentMode& operator=(CapsuleComponentMode&&) = delete;
+        ~CapsuleComponentMode() = default;
+
+        // EditorBaseComponentMode overrides ...
+        AZStd::string GetComponentModeName() const override;
+        AZ::Uuid GetComponentModeType() const override;
+    };
+} // namespace AzToolsFramework

+ 103 - 83
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.cpp

@@ -10,6 +10,7 @@
 #include <AzFramework/Viewport/ViewportConstants.h>
 #include <AzToolsFramework/ComponentModes/CapsuleViewportEdit.h>
 #include <AzToolsFramework/ComponentModes/ViewportEditUtilities.h>
+#include <AzToolsFramework/Viewport/ViewportSettings.h>
 
 namespace AzToolsFramework
 {
@@ -19,11 +20,15 @@ namespace AzToolsFramework
         const AZ::Vector3 HeightManipulatorAxis = AZ::Vector3::CreateAxisZ();
         const float MinCapsuleRadius = 0.001f;
         const float MinCapsuleHeight = 0.002f;
-        const float HalfHeight = 0.5f;
         const float ResetCapsuleHeight = 1.0f;
         const float ResetCapsuleRadius = 0.25f;
     } // namespace
 
+    CapsuleViewportEdit::CapsuleViewportEdit(bool allowAsymmetricalEditing)
+        : m_allowAsymmetricalEditing(allowAsymmetricalEditing)
+    {
+    }
+
     void CapsuleViewportEdit::InstallGetRotationOffset(AZStd::function<AZ::Quaternion()> getRotationOffset)
     {
         m_getRotationOffset = AZStd::move(getRotationOffset);
@@ -138,22 +143,25 @@ namespace AzToolsFramework
         const AZ::Transform localTransform = GetLocalTransform();
 
         SetupRadiusManipulator(manipulatorManagerId, worldTransform, localTransform, nonUniformScale);
-        SetupHeightManipulator(manipulatorManagerId, worldTransform, localTransform, nonUniformScale);
+        m_topManipulator = SetupHeightManipulator(manipulatorManagerId, worldTransform, localTransform, nonUniformScale, 1.0f);
+        if (m_allowAsymmetricalEditing)
+        {
+            m_bottomManipulator = SetupHeightManipulator(manipulatorManagerId, worldTransform, localTransform, nonUniformScale, -1.0f);
+        }
     }
 
     void CapsuleViewportEdit::AddEntityComponentIdPair(const AZ::EntityComponentIdPair& entityComponentIdPair)
     {
-        if (m_heightManipulator)
+        for (auto manipulator : { m_radiusManipulator.get(), m_topManipulator.get(), m_bottomManipulator.get() })
         {
-            m_heightManipulator->AddEntityComponentIdPair(entityComponentIdPair);
-        }
-        if (m_radiusManipulator)
-        {
-            m_radiusManipulator->AddEntityComponentIdPair(entityComponentIdPair);
+            if (manipulator)
+            {
+                manipulator->AddEntityComponentIdPair(entityComponentIdPair);
+            }
         }
         AZ_WarningOnce(
             "CapsuleViewportEdit",
-            m_heightManipulator && m_radiusManipulator,
+            m_radiusManipulator && m_topManipulator && (!m_allowAsymmetricalEditing || m_bottomManipulator),
             "Attempting to AddEntityComponentIdPair before manipulators have been created");
     }
 
@@ -172,13 +180,18 @@ namespace AzToolsFramework
             m_radiusManipulator->SetLocalTransform(
                 localTransform * AZ::Transform::CreateTranslation(m_radiusManipulator->GetAxis() * capsuleRadius));
             m_radiusManipulator->SetNonUniformScale(nonUniformScale);
+            m_radiusManipulator->SetBoundsDirty();
         }
-        if (m_heightManipulator)
+        for (auto heightManipulator : { m_topManipulator.get(), m_bottomManipulator.get() })
         {
-            m_heightManipulator->SetSpace(worldTransform);
-            m_heightManipulator->SetLocalTransform(
-                localTransform * AZ::Transform::CreateTranslation(m_heightManipulator->GetAxis() * capsuleHeight * HalfHeight));
-            m_heightManipulator->SetNonUniformScale(nonUniformScale);
+            if (heightManipulator)
+            {
+                heightManipulator->SetSpace(worldTransform);
+                heightManipulator->SetLocalTransform(
+                    localTransform * AZ::Transform::CreateTranslation(0.5f * capsuleHeight * heightManipulator->GetAxis()));
+                heightManipulator->SetNonUniformScale(nonUniformScale);
+                heightManipulator->SetBoundsDirty();
+            }
         }
     }
 
@@ -187,18 +200,17 @@ namespace AzToolsFramework
         BeginEditing();
         SetCapsuleHeight(ResetCapsuleHeight);
         SetCapsuleRadius(ResetCapsuleRadius);
-        FinishEditing();
+        EndEditing();
     }
 
     void CapsuleViewportEdit::Teardown()
     {
-        if (m_radiusManipulator)
+        for (auto manipulator : { m_radiusManipulator.get(), m_topManipulator.get(), m_bottomManipulator.get() })
         {
-            m_radiusManipulator->Unregister();
-        }
-        if (m_heightManipulator)
-        {
-            m_heightManipulator->Unregister();
+            if (manipulator)
+            {
+                manipulator->Unregister();
+            }
         }
     }
 
@@ -209,144 +221,152 @@ namespace AzToolsFramework
         const AZ::Vector3& nonUniformScale)
     {
         float capsuleRadius = GetCapsuleRadius();
-        m_radiusManipulator = AzToolsFramework::LinearManipulator::MakeShared(worldTransform);
+        m_radiusManipulator = LinearManipulator::MakeShared(worldTransform);
         m_radiusManipulator->SetAxis(RadiusManipulatorAxis);
         m_radiusManipulator->Register(manipulatorManagerId);
         m_radiusManipulator->SetLocalTransform(localTransform * AZ::Transform::CreateTranslation(RadiusManipulatorAxis * capsuleRadius));
         m_radiusManipulator->SetNonUniformScale(nonUniformScale);
         {
-            AzToolsFramework::ManipulatorViews views;
-            views.emplace_back(AzToolsFramework::CreateManipulatorViewQuadBillboard(
+            ManipulatorViews views;
+            views.emplace_back(CreateManipulatorViewQuadBillboard(
                 AzFramework::ViewportColors::DefaultManipulatorHandleColor, AzFramework::ViewportConstants::DefaultManipulatorHandleSize));
             m_radiusManipulator->SetViews(AZStd::move(views));
         }
         m_radiusManipulator->InstallLeftMouseDownCallback(
-            [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
+            [this]([[maybe_unused]] const LinearManipulator::Action& action)
             {
                 BeginEditing();
             });
         m_radiusManipulator->InstallMouseMoveCallback(
-            [this](const AzToolsFramework::LinearManipulator::Action& action)
+            [this](const LinearManipulator::Action& action)
             {
                 OnRadiusManipulatorMoved(action);
             });
         m_radiusManipulator->InstallLeftMouseUpCallback(
-            [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
+            [this]([[maybe_unused]] const LinearManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
     }
 
-    void CapsuleViewportEdit::SetupHeightManipulator(
+    AZStd::shared_ptr<LinearManipulator> CapsuleViewportEdit::SetupHeightManipulator(
         const ManipulatorManagerId manipulatorManagerId,
         const AZ::Transform& worldTransform,
         const AZ::Transform& localTransform,
-        const AZ::Vector3& nonUniformScale)
+        const AZ::Vector3& nonUniformScale,
+        float axisDirection)
     {
         float capsuleHeight = GetCapsuleHeight();
-        m_heightManipulator = AzToolsFramework::LinearManipulator::MakeShared(worldTransform);
-        m_heightManipulator->SetAxis(HeightManipulatorAxis);
-        m_heightManipulator->Register(manipulatorManagerId);
-        m_heightManipulator->SetLocalTransform(
-            localTransform * AZ::Transform::CreateTranslation(HeightManipulatorAxis * capsuleHeight * HalfHeight));
-        m_heightManipulator->SetNonUniformScale(nonUniformScale);
+        auto manipulator = LinearManipulator::MakeShared(worldTransform);
+        manipulator->SetAxis(axisDirection * HeightManipulatorAxis);
+        manipulator->Register(manipulatorManagerId);
+        manipulator->SetLocalTransform(
+            localTransform * AZ::Transform::CreateTranslation(axisDirection * 0.5f * capsuleHeight * HeightManipulatorAxis));
+        manipulator->SetNonUniformScale(nonUniformScale);
         {
-            AzToolsFramework::ManipulatorViews views;
-            views.emplace_back(AzToolsFramework::CreateManipulatorViewQuadBillboard(
+            ManipulatorViews views;
+            views.emplace_back(CreateManipulatorViewQuadBillboard(
                 AzFramework::ViewportColors::DefaultManipulatorHandleColor, AzFramework::ViewportConstants::DefaultManipulatorHandleSize));
-            m_heightManipulator->SetViews(AZStd::move(views));
+            manipulator->SetViews(AZStd::move(views));
         }
-        m_heightManipulator->InstallLeftMouseDownCallback(
-            [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
+        manipulator->InstallLeftMouseDownCallback(
+            [this]([[maybe_unused]] const LinearManipulator::Action& action)
             {
                 BeginEditing();
             });
-        m_heightManipulator->InstallMouseMoveCallback(
-            [this](const AzToolsFramework::LinearManipulator::Action& action)
+        manipulator->InstallMouseMoveCallback(
+            [this](const LinearManipulator::Action& action)
             {
                 OnHeightManipulatorMoved(action);
             });
-        m_heightManipulator->InstallLeftMouseUpCallback(
-            [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
+        manipulator->InstallLeftMouseUpCallback(
+            [this]([[maybe_unused]] const LinearManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
+        return manipulator;
     }
 
-    void CapsuleViewportEdit::OnRadiusManipulatorMoved(const AzToolsFramework::LinearManipulator::Action& action)
+    void CapsuleViewportEdit::OnRadiusManipulatorMoved(const LinearManipulator::Action& action)
     {
         // manipulator action offsets do not take entity transform scale into account, so need to apply it here
         const AZ::Transform localTransform = GetLocalTransform();
-        const AZ::Vector3 manipulatorPosition = AzToolsFramework::GetPositionInManipulatorFrame(
+        const AZ::Vector3 manipulatorPosition = GetPositionInManipulatorFrame(
             m_radiusManipulator->GetSpace().GetUniformScale(), localTransform, action);
 
-        // Get the distance the manipulator has moved along the axis.
         float extent = manipulatorPosition.Dot(action.m_fixed.m_axis);
-
-        // Clamp radius to a small value.
         extent = AZ::GetMax(extent, MinCapsuleRadius);
-
-        // Update the manipulator and capsule radius.
         m_radiusManipulator->SetLocalTransform(localTransform * AZ::Transform::CreateTranslation(extent * action.m_fixed.m_axis));
 
-        // Adjust the height manipulator so it is always clamped to twice the radius.
-        AdjustHeightManipulator(extent);
+        // adjust the height manipulator so it is always clamped to twice the radius.
+        AdjustHeightManipulators(extent);
 
-        // The final radius of the capsule is the manipulator's extent.
         SetCapsuleRadius(extent);
     }
 
-    void CapsuleViewportEdit::OnHeightManipulatorMoved(const AzToolsFramework::LinearManipulator::Action& action)
+    void CapsuleViewportEdit::OnHeightManipulatorMoved(const LinearManipulator::Action& action)
     {
+        const bool symmetrical = !m_allowAsymmetricalEditing || action.m_modifiers.IsHeld(DefaultSymmetricalEditingModifier);
+
         // manipulator action offsets do not take entity transform scale into account, so need to apply it here
         const AZ::Transform localTransform = GetLocalTransform();
         const AZ::Vector3 manipulatorPosition =
-            AzToolsFramework::GetPositionInManipulatorFrame(m_heightManipulator->GetSpace().GetUniformScale(), localTransform, action);
+            GetPositionInManipulatorFrame(GetManipulatorSpace().GetUniformScale(), localTransform, action);
 
-        // Get the distance the manipulator has moved along the axis.
-        float extent = manipulatorPosition.Dot(action.m_fixed.m_axis);
+        // factor of 2 for symmetrical editing because both ends of the capsule move
+        const float symmetryFactor = symmetrical ? 2.0f : 1.0f;
 
-        // Ensure capsule's half height is always greater than the radius.
-        extent = AZ::GetMax(extent, MinCapsuleHeight);
+        const float oldCapsuleHeight = GetCapsuleHeight();
+        const float newAxisLength = symmetryFactor * manipulatorPosition.Dot(action.m_fixed.m_axis);
+        const float oldAxisLength = 0.5f * symmetryFactor * oldCapsuleHeight;
+        const float capsuleHeightDelta = newAxisLength - oldAxisLength;
 
-        // Update the manipulator and capsule height.
-        m_heightManipulator->SetLocalTransform(localTransform * AZ::Transform::CreateTranslation(extent * action.m_fixed.m_axis));
+        const float newCapsuleHeight = AZ::GetMax(oldCapsuleHeight + capsuleHeightDelta, MinCapsuleHeight);
 
-        // The final height of the capsule is twice the manipulator's extent.
-        float capsuleHeight = extent / HalfHeight;
+        // adjust the radius manipulator so it is always clamped to half the capsule height.
+        AdjustRadiusManipulator(newCapsuleHeight);
 
-        // Adjust the radius manipulator so it is always clamped to half the capsule height.
-        AdjustRadiusManipulator(capsuleHeight);
+        SetCapsuleHeight(newCapsuleHeight);
 
-        // Finally adjust the capsule height
-        SetCapsuleHeight(capsuleHeight);
+        if (!symmetrical)
+        {
+            const AZ::Vector3 transformedAxis = localTransform.TransformVector(action.m_fixed.m_axis);
+            const AZ::Vector3 translationOffsetDelta = 0.5f * (newAxisLength - oldAxisLength) * transformedAxis;
+            const AZ::Vector3 translationOffset = GetTranslationOffset();
+            SetTranslationOffset(translationOffset + translationOffsetDelta);
+        }
+
+        for (auto heightManipulator : { m_topManipulator.get(), m_bottomManipulator.get() })
+        {
+            const AZ::Transform updatedLocalTransform = GetLocalTransform();
+            heightManipulator->SetLocalTransform(
+                updatedLocalTransform * AZ::Transform::CreateTranslation(0.5f * newCapsuleHeight * heightManipulator->GetAxis()));
+        }
     }
 
     void CapsuleViewportEdit::AdjustRadiusManipulator(float capsuleHeight)
     {
         float capsuleRadius = GetCapsuleRadius();
-
-        // Clamp the radius to half height.
-        capsuleRadius = AZ::GetMin(capsuleRadius, capsuleHeight * HalfHeight);
-
-        // Update manipulator and the capsule radius.
+        capsuleRadius = AZ::GetMin(capsuleRadius, 0.5f * capsuleHeight);
         const AZ::Transform localTransform = GetLocalTransform();
         m_radiusManipulator->SetLocalTransform(
             localTransform * AZ::Transform::CreateTranslation(capsuleRadius * m_radiusManipulator->GetAxis()));
         SetCapsuleRadius(capsuleRadius);
     }
 
-    void CapsuleViewportEdit::AdjustHeightManipulator(float capsuleRadius)
+    void CapsuleViewportEdit::AdjustHeightManipulators(float capsuleRadius)
     {
         float capsuleHeight = GetCapsuleHeight();
-
-        // Clamp the height to twice the radius.
-        capsuleHeight = AZ::GetMax(capsuleHeight, capsuleRadius / HalfHeight);
-
-        // Update the manipulator and capsule height.
+        capsuleHeight = AZ::GetMax(capsuleHeight, 2.0f * capsuleRadius);
         const AZ::Transform localTransform = GetLocalTransform();
-        m_heightManipulator->SetLocalTransform(
-            localTransform * AZ::Transform::CreateTranslation(capsuleHeight * HalfHeight * m_heightManipulator->GetAxis()));
+        for (auto heightManipulator : { m_topManipulator.get(), m_bottomManipulator.get() })
+        {
+            if (heightManipulator)
+            {
+                heightManipulator->SetLocalTransform(
+                    localTransform * AZ::Transform::CreateTranslation(0.5f * capsuleHeight * heightManipulator->GetAxis()));
+            }
+        }        
         SetCapsuleHeight(capsuleHeight);
     }
 } // namespace AzToolsFramework

+ 13 - 11
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/CapsuleViewportEdit.h

@@ -13,7 +13,7 @@
 
 namespace AzToolsFramework
 {
-    //! Wraps 2 linear manipulators, providing a viewport experience for 
+    //! Wraps linear manipulators, providing a viewport experience for 
     //! modifying the radius and height of a capsule.
     //! It is designed to be usable either by a component mode or by other contexts which are not associated with a
     //! particular component, so it does not contain any reference to an EntityComponentIdPair or other component-based
@@ -21,8 +21,7 @@ namespace AzToolsFramework
     class CapsuleViewportEdit : public BaseShapeViewportEdit
     {
     public:
-        CapsuleViewportEdit() = default;
-        virtual ~CapsuleViewportEdit() = default;
+        CapsuleViewportEdit(bool allowAsymmetricalEditing = false);
 
         void InstallGetRotationOffset(AZStd::function<AZ::Quaternion()> getRotationOffset);
         void InstallGetCapsuleRadius(AZStd::function<float()> getCapsuleRadius);
@@ -31,7 +30,7 @@ namespace AzToolsFramework
         void InstallSetCapsuleHeight(AZStd::function<void(float)> setCapsuleHeight);
 
         // BaseShapeViewportEdit overrides ...
-        void Setup(const ManipulatorManagerId manipulatorManagerId = g_mainManipulatorManagerId) override;
+        void Setup(const ManipulatorManagerId manipulatorManagerId) override;
         void Teardown() override;
         void UpdateManipulators() override;
         void ResetValues() override;
@@ -52,18 +51,21 @@ namespace AzToolsFramework
             const AZ::Transform& worldTransform,
             const AZ::Transform& localTransform,
             const AZ::Vector3& nonUniformScale);
-        void SetupHeightManipulator(
+        AZStd::shared_ptr<LinearManipulator> SetupHeightManipulator(
             const ManipulatorManagerId manipulatorManagerId,
             const AZ::Transform& worldTransform,
             const AZ::Transform& localTransform,
-            const AZ::Vector3& nonUniformScale);
-        void OnRadiusManipulatorMoved(const AzToolsFramework::LinearManipulator::Action& action);
-        void OnHeightManipulatorMoved(const AzToolsFramework::LinearManipulator::Action& action);
+            const AZ::Vector3& nonUniformScale,
+            float axisDirection);
+        void OnRadiusManipulatorMoved(const LinearManipulator::Action& action);
+        void OnHeightManipulatorMoved(const LinearManipulator::Action& action);
         void AdjustRadiusManipulator(const float capsuleHeight);
-        void AdjustHeightManipulator(const float capsuleRadius);
+        void AdjustHeightManipulators(const float capsuleRadius);
 
-        AZStd::shared_ptr<AzToolsFramework::LinearManipulator> m_radiusManipulator;
-        AZStd::shared_ptr<AzToolsFramework::LinearManipulator> m_heightManipulator;
+        AZStd::shared_ptr<LinearManipulator> m_radiusManipulator;
+        AZStd::shared_ptr<LinearManipulator> m_topManipulator;
+        AZStd::shared_ptr<LinearManipulator> m_bottomManipulator;
+        bool m_allowAsymmetricalEditing = false; ///< Whether moving the ends of the capsule independently is allowed.
 
         AZStd::function<AZ::Quaternion()> m_getRotationOffset;
         AZStd::function<float()> m_getCapsuleRadius;

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

@@ -17,6 +17,7 @@ namespace AzToolsFramework
 {
     void ShapeTranslationOffsetViewportEdit::AddEntityComponentIdPair(const AZ::EntityComponentIdPair& entityComponentIdPair)
     {
+        m_entityIds.insert(entityComponentIdPair.GetEntityId());
         if (m_translationManipulators)
         {
             m_translationManipulators->AddEntityComponentIdPair(entityComponentIdPair);
@@ -74,6 +75,10 @@ namespace AzToolsFramework
 
     void ShapeTranslationOffsetViewportEdit::ResetValues()
     {
+        // manipulators handle undo batches themselves, but this function does not work via manipulators so needs its own undo batch
+        BeginUndoBatch("ShapeTranslationOffsetViewportEdit Reset");
         SetTranslationOffset(AZ::Vector3::CreateZero());
+        ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values);
+        EndUndoBatch();
     }
 } // namespace AzToolsFramework

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentModes/ShapeTranslationOffsetViewportEdit.h

@@ -21,7 +21,7 @@ namespace AzToolsFramework
         ShapeTranslationOffsetViewportEdit() = default;
 
         // BaseShapeViewportEdit overrides ...
-        void Setup(const ManipulatorManagerId manipulatorManagerId = g_mainManipulatorManagerId) override;
+        void Setup(const ManipulatorManagerId manipulatorManagerId) override;
         void Teardown() override;
         void UpdateManipulators() override;
         void ResetValues() override;

+ 38 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/CapsuleManipulatorRequestBus.h

@@ -0,0 +1,38 @@
+/*
+ * 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 Quaternion;
+} // namespace AZ
+
+namespace AzToolsFramework
+{
+    //! Interface for handling capsule manipulator requests.
+    //! Note that radius requests are handled by RadiusManipulatorRequests.
+    class CapsuleManipulatorRequests: public AZ::EntityComponentBus
+    {
+    public:
+        //! Get the height of the capsule shape.
+        virtual float GetHeight() const = 0;
+        //! Get the rotation of the capsule shape relative to the manipulator space.
+        virtual AZ::Quaternion GetRotationOffset() const = 0;
+        //! Set the height of the capsule shape.
+        virtual void SetHeight(float height) = 0;
+
+    protected:
+        ~CapsuleManipulatorRequests() = default;
+    };
+
+    //! Type to inherit to implement CapsuleManipulatorRequests
+    using CapsuleManipulatorRequestBus = AZ::EBus<CapsuleManipulatorRequests>;
+} // namespace AzToolsFramework

+ 30 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/RadiusManipulatorRequestBus.h

@@ -0,0 +1,30 @@
+/*
+ * 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
+{
+    //! Interface for handling radius manipulator requests.
+    class RadiusManipulatorRequests : public AZ::EntityComponentBus
+    {
+    public:
+        //! Get the radius of the shape.
+        virtual float GetRadius() const = 0;
+        //! Set the radius of the shape.
+        virtual void SetRadius(float radius) = 0;
+
+    protected:
+        ~RadiusManipulatorRequests() = default;
+    };
+
+    //! Type to inherit to implement RadiusManipulatorRequests
+    using RadiusManipulatorRequestBus = AZ::EBus<RadiusManipulatorRequests>;
+} // namespace AzToolsFramework

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

@@ -220,6 +220,7 @@ set(FILES
     Manipulators/BaseManipulator.cpp
     Manipulators/BaseManipulator.h
     Manipulators/BoxManipulatorRequestBus.h
+    Manipulators/CapsuleManipulatorRequestBus.h
     Manipulators/EditorVertexSelection.h
     Manipulators/EditorVertexSelection.cpp
     Manipulators/EditorVertexSelectionBus.h
@@ -247,6 +248,7 @@ set(FILES
     Manipulators/PaintBrushManipulator.h
     Manipulators/PlanarManipulator.cpp
     Manipulators/PlanarManipulator.h
+    Manipulators/RadiusManipulatorRequestBus.h
     Manipulators/RotationManipulators.cpp
     Manipulators/RotationManipulators.h
     Manipulators/ScaleManipulators.cpp
@@ -644,9 +646,13 @@ set(FILES
     ComponentModes/BoxComponentMode.cpp
     ComponentModes/BoxViewportEdit.h
     ComponentModes/BoxViewportEdit.cpp
+    ComponentModes/CapsuleComponentMode.h
+    ComponentModes/CapsuleComponentMode.cpp
     ComponentModes/CapsuleViewportEdit.h
     ComponentModes/CapsuleViewportEdit.cpp
     ComponentModes/ShapeComponentModeBus.h
+    ComponentModes/BaseShapeComponentMode.h
+    ComponentModes/BaseShapeComponentMode.cpp
     ComponentModes/ShapeTranslationOffsetViewportEdit.h
     ComponentModes/ShapeTranslationOffsetViewportEdit.cpp
     ComponentModes/ViewportEditUtilities.h

+ 5 - 4
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderCapsuleManipulators.cpp

@@ -129,10 +129,10 @@ namespace EMotionFX
             {
                 BeginEditing();
             });
-        m_capsuleViewportEdit->InstallFinishEditing(
+        m_capsuleViewportEdit->InstallEndEditing(
             [this]()
             {
-                FinishEditing();
+                EndEditing();
             });
     }
 
@@ -145,7 +145,8 @@ namespace EMotionFX
             return;
         }
 
-        m_capsuleViewportEdit = AZStd::make_unique<AzToolsFramework::CapsuleViewportEdit>();
+        const bool allowAsymmetricalEditing = true;
+        m_capsuleViewportEdit = AZStd::make_unique<AzToolsFramework::CapsuleViewportEdit>(allowAsymmetricalEditing);
         InstallCapsuleViewportEditFunctions();
         m_capsuleViewportEdit->Setup(EMStudio::g_animManipulatorManagerId);
 
@@ -211,7 +212,7 @@ namespace EMotionFX
         }
     }
 
-    void ColliderCapsuleManipulators::FinishEditing()
+    void ColliderCapsuleManipulators::EndEditing()
     {
         if (m_commandGroup.IsEmpty())
         {

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderCapsuleManipulators.h

@@ -39,7 +39,7 @@ namespace EMotionFX
 
         void InstallCapsuleViewportEditFunctions();
         void BeginEditing();
-        void FinishEditing();
+        void EndEditing();
 
         PhysicsSetupManipulatorData m_physicsSetupManipulatorData;
         MCore::CommandGroup m_commandGroup;

+ 3 - 3
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderRotationManipulators.cpp

@@ -58,7 +58,7 @@ namespace EMotionFX
         m_rotationManipulators.InstallLeftMouseUpCallback(
             [this](const AzToolsFramework::AngularManipulator::Action& action)
             {
-                FinishEditing(action.LocalOrientation());
+                EndEditing(action.LocalOrientation());
             });
 
         AZ::TickBus::Handler::BusConnect();
@@ -97,7 +97,7 @@ namespace EMotionFX
         if (m_physicsSetupManipulatorData.HasColliders())
         {
             BeginEditing(m_physicsSetupManipulatorData.m_colliderNodeConfiguration->m_shapes[0].first->m_rotation);
-            FinishEditing(AZ::Quaternion::CreateIdentity());
+            EndEditing(AZ::Quaternion::CreateIdentity());
             Refresh();
         }
     }
@@ -129,7 +129,7 @@ namespace EMotionFX
         command->SetOldRotation(rotation);
     }
 
-    void ColliderRotationManipulators::FinishEditing(const AZ::Quaternion& rotation)
+    void ColliderRotationManipulators::EndEditing(const AZ::Quaternion& rotation)
     {
         if (m_commandGroup.IsEmpty())
         {

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderRotationManipulators.h

@@ -39,7 +39,7 @@ namespace EMotionFX
 
         void OnManipulatorMoved(const AZ::Quaternion& rotation);
         void BeginEditing(const AZ::Quaternion& rotation);
-        void FinishEditing(const AZ::Quaternion& rotation);
+        void EndEditing(const AZ::Quaternion& rotation);
 
         AzToolsFramework::RotationManipulators m_rotationManipulators;
         PhysicsSetupManipulatorData m_physicsSetupManipulatorData;

+ 5 - 5
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderTranslationManipulators.cpp

@@ -78,19 +78,19 @@ namespace EMotionFX
         m_translationManipulators.InstallLinearManipulatorMouseUpCallback(
             [this](const AzToolsFramework::LinearManipulator::Action& action)
             {
-                FinishEditing(action.m_start.m_localPosition, action.m_current.m_localPositionOffset);
+                EndEditing(action.m_start.m_localPosition, action.m_current.m_localPositionOffset);
             });
 
         m_translationManipulators.InstallPlanarManipulatorMouseUpCallback(
             [this](const AzToolsFramework::PlanarManipulator::Action& action)
             {
-                FinishEditing(action.m_start.m_localPosition, action.m_current.m_localOffset);
+                EndEditing(action.m_start.m_localPosition, action.m_current.m_localOffset);
             });
 
         m_translationManipulators.InstallSurfaceManipulatorMouseUpCallback(
             [this](const AzToolsFramework::SurfaceManipulator::Action& action)
             {
-                FinishEditing(action.m_start.m_localPosition, action.m_current.m_localOffset);
+                EndEditing(action.m_start.m_localPosition, action.m_current.m_localOffset);
             });
 
         PhysicsSetupManipulatorRequestBus::Handler::BusConnect();
@@ -126,7 +126,7 @@ namespace EMotionFX
         {
             BeginEditing(
                 m_physicsSetupManipulatorData.m_colliderNodeConfiguration->m_shapes[0].first->m_position, AZ::Vector3::CreateZero());
-            FinishEditing(AZ::Vector3::CreateZero(), AZ::Vector3::CreateZero());
+            EndEditing(AZ::Vector3::CreateZero(), AZ::Vector3::CreateZero());
             Refresh();
         }
     }
@@ -165,7 +165,7 @@ namespace EMotionFX
         command->SetOldPosition(GetPosition(startPosition, offset));
     }
 
-    void ColliderTranslationManipulators::FinishEditing(const AZ::Vector3& startPosition, const AZ::Vector3& offset)
+    void ColliderTranslationManipulators::EndEditing(const AZ::Vector3& startPosition, const AZ::Vector3& offset)
     {
         if (m_commandGroup.IsEmpty())
         {

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/ColliderTranslationManipulators.h

@@ -32,7 +32,7 @@ namespace EMotionFX
         AZ::Vector3 GetPosition(const AZ::Vector3& startPosition, const AZ::Vector3& offset) const;
         void OnManipulatorMoved(const AZ::Vector3& startPosition, const AZ::Vector3& offset);
         void BeginEditing(const AZ::Vector3& startPosition, const AZ::Vector3& offset);
-        void FinishEditing(const AZ::Vector3& startPosition, const AZ::Vector3& offset);
+        void EndEditing(const AZ::Vector3& startPosition, const AZ::Vector3& offset);
 
         // PhysicsSetupManipulatorRequestBus::Handler overrides ...
         void OnUnderlyingPropertiesChanged() override;

+ 3 - 3
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointLimitRotationManipulators.cpp

@@ -52,7 +52,7 @@ namespace EMotionFX
         m_rotationManipulators.InstallLeftMouseUpCallback(
             [this]([[maybe_unused]] const AzToolsFramework::AngularManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
 
         AZ::TickBus::Handler::BusConnect();
@@ -101,7 +101,7 @@ namespace EMotionFX
         {
             BeginEditing();
             GetLocalOrientation() = AZ::Quaternion::CreateIdentity();
-            FinishEditing();
+            EndEditing();
             Refresh();
         }
     }
@@ -121,7 +121,7 @@ namespace EMotionFX
         CreateCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }
 
-    void JointLimitRotationManipulators::FinishEditing()
+    void JointLimitRotationManipulators::EndEditing()
     {
         ExecuteCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointLimitRotationManipulators.h

@@ -45,7 +45,7 @@ namespace EMotionFX
 
         void OnManipulatorMoved(const AZ::Quaternion& rotation);
         void BeginEditing();
-        void FinishEditing();
+        void EndEditing();
 
         AZ::Quaternion& GetLocalOrientation();
         const AZ::Quaternion& GetLocalOrientation() const;

+ 4 - 4
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointSwingLimitManipulators.cpp

@@ -73,7 +73,7 @@ namespace EMotionFX
         m_swingYManipulator->InstallLeftMouseUpCallback(
             [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
 
         // swing limit Z manipulator
@@ -113,7 +113,7 @@ namespace EMotionFX
         m_swingZManipulator->InstallLeftMouseUpCallback(
             [this]([[maybe_unused]] const AzToolsFramework::LinearManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
 
         Refresh();
@@ -178,7 +178,7 @@ namespace EMotionFX
             BeginEditing();
             m_physicsSetupManipulatorData.m_jointConfiguration->SetPropertyValue(AZ::Name("SwingLimitY"), 45.0f);
             m_physicsSetupManipulatorData.m_jointConfiguration->SetPropertyValue(AZ::Name("SwingLimitZ"), 45.0f);
-            FinishEditing();
+            EndEditing();
             Refresh();
         }
     }
@@ -206,7 +206,7 @@ namespace EMotionFX
         CreateCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }
 
-    void JointSwingLimitManipulators::FinishEditing()
+    void JointSwingLimitManipulators::EndEditing()
     {
         ExecuteCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointSwingLimitManipulators.h

@@ -44,7 +44,7 @@ namespace EMotionFX
         void OnUnderlyingPropertiesChanged() override;
 
         void BeginEditing();
-        void FinishEditing();
+        void EndEditing();
 
         AZStd::shared_ptr<AzToolsFramework::LinearManipulator> m_swingYManipulator;
         AZStd::shared_ptr<AzToolsFramework::LinearManipulator> m_swingZManipulator;

+ 4 - 4
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointTwistLimitManipulators.cpp

@@ -84,7 +84,7 @@ namespace EMotionFX
         m_twistLimitLowerManipulator->InstallLeftMouseUpCallback(
             [this]([[maybe_unused]] const AzToolsFramework::AngularManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
 
         m_twistLimitLowerManipulator->Register(EMStudio::g_animManipulatorManagerId);
@@ -121,7 +121,7 @@ namespace EMotionFX
         m_twistLimitUpperManipulator->InstallLeftMouseUpCallback(
             [this]([[maybe_unused]] const AzToolsFramework::AngularManipulator::Action& action)
             {
-                FinishEditing();
+                EndEditing();
             });
 
         m_twistLimitUpperManipulator->Register(EMStudio::g_animManipulatorManagerId);
@@ -168,7 +168,7 @@ namespace EMotionFX
             BeginEditing();
             m_physicsSetupManipulatorData.m_jointConfiguration->SetPropertyValue(AZ::Name("TwistLimitLower"), -45.0f);
             m_physicsSetupManipulatorData.m_jointConfiguration->SetPropertyValue(AZ::Name("TwistLimitUpper"), 45.0f);
-            FinishEditing();
+            EndEditing();
             Refresh();
         }
     }
@@ -243,7 +243,7 @@ namespace EMotionFX
         CreateCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }
 
-    void JointTwistLimitManipulators::FinishEditing()
+    void JointTwistLimitManipulators::EndEditing()
     {
         ExecuteCommandAdjustJointLimit(m_commandGroup, m_physicsSetupManipulatorData);
     }

+ 1 - 1
Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/JointTwistLimitManipulators.h

@@ -44,7 +44,7 @@ namespace EMotionFX
         void OnUnderlyingPropertiesChanged() override;
 
         void BeginEditing();
-        void FinishEditing();
+        void EndEditing();
 
         AZStd::shared_ptr<AzToolsFramework::AngularManipulator> m_twistLimitLowerManipulator;
         AZStd::shared_ptr<AzToolsFramework::AngularManipulator> m_twistLimitUpperManipulator;

+ 5 - 7
Gems/LmbrCentral/Code/Source/Shape/EditorBoxShapeComponent.cpp

@@ -70,16 +70,14 @@ namespace LmbrCentral
         EditorBaseShapeComponent::Activate();
         m_boxShape.Activate(GetEntityId());
         AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
-        AzToolsFramework::BoxManipulatorRequestBus::Handler::BusConnect(
-            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
-        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(
-            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+        const AZ::EntityComponentIdPair entityComponentIdPair(GetEntityId(), GetId());
+        AzToolsFramework::BoxManipulatorRequestBus::Handler::BusConnect(entityComponentIdPair);
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(entityComponentIdPair);
 
         // ComponentMode
         const bool allowAsymmetricalEditing = IsShapeComponentTranslationEnabled();
-        m_componentModeDelegate.ConnectWithSingleComponentMode<
-            EditorBoxShapeComponent, AzToolsFramework::BoxComponentMode>(
-                AZ::EntityComponentIdPair(GetEntityId(), GetId()), this, allowAsymmetricalEditing);
+        m_componentModeDelegate.ConnectWithSingleComponentMode<EditorBoxShapeComponent, AzToolsFramework::BoxComponentMode>(
+            entityComponentIdPair, this, allowAsymmetricalEditing);
     }
 
     void EditorBoxShapeComponent::Deactivate()

+ 96 - 15
Gems/LmbrCentral/Code/Source/Shape/EditorCapsuleShapeComponent.cpp

@@ -6,13 +6,13 @@
  *
  */
 
-#include "EditorCapsuleShapeComponent.h"
+#include <Shape/EditorCapsuleShapeComponent.h>
 
 #include <AzCore/Serialization/EditContext.h>
-#include "CapsuleShapeComponent.h"
-#include "EditorShapeComponentConverters.h"
-#include "ShapeDisplay.h"
+#include <AzToolsFramework/ComponentModes/CapsuleComponentMode.h>
 #include <LmbrCentral/Geometry/GeometrySystemComponentBus.h>
+#include <Shape/EditorShapeComponentConverters.h>
+#include <Shape/ShapeDisplay.h>
 
 namespace LmbrCentral
 {
@@ -30,22 +30,35 @@ namespace LmbrCentral
             serializeContext->Class<EditorCapsuleShapeComponent, EditorBaseShapeComponent>()
                 ->Version(3, &ClassConverters::UpgradeEditorCapsuleShapeComponent)
                 ->Field("CapsuleShape", &EditorCapsuleShapeComponent::m_capsuleShape)
+                ->Field("ComponentMode", &EditorCapsuleShapeComponent::m_componentModeDelegate)
                 ;
 
             if (AZ::EditContext* editContext = serializeContext->GetEditContext())
             {
-                editContext->Class<EditorCapsuleShapeComponent>("Capsule Shape", "The Capsule Shape component creates a capsule around the associated entity")
+                editContext
+                    ->Class<EditorCapsuleShapeComponent>(
+                        "Capsule Shape", "The Capsule Shape component creates a capsule around the associated entity")
                     ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
-                        ->Attribute(AZ::Edit::Attributes::Category, "Shape")
-                        ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Capsule_Shape.svg")
-                        ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Capsule_Shape.svg")
-                        ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c))
-                        ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
-                        ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/shape/capsule-shape/")
-                    ->DataElement(AZ::Edit::UIHandlers::Default, &EditorCapsuleShapeComponent::m_capsuleShape, "Capsule Shape", "Capsule Shape Configuration")
-                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorCapsuleShapeComponent::ConfigurationChanged)
-                        ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
-                        ;
+                    ->Attribute(AZ::Edit::Attributes::Category, "Shape")
+                    ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Capsule_Shape.svg")
+                    ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Capsule_Shape.svg")
+                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c))
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->Attribute(
+                        AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/shape/capsule-shape/")
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &EditorCapsuleShapeComponent::m_capsuleShape,
+                        "Capsule Shape",
+                        "Capsule Shape Configuration")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorCapsuleShapeComponent::ConfigurationChanged)
+                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &EditorCapsuleShapeComponent::m_componentModeDelegate,
+                        "Component Mode",
+                        "Capsule Shape Component Mode")
+                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly);
             }
         }
     }
@@ -62,12 +75,25 @@ namespace LmbrCentral
         EditorBaseShapeComponent::Activate();
         m_capsuleShape.Activate(GetEntityId());
         AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
+        const AZ::EntityComponentIdPair entityComponentIdPair(GetEntityId(), GetId());
+        AzToolsFramework::CapsuleManipulatorRequestBus::Handler::BusConnect(entityComponentIdPair);
+        AzToolsFramework::RadiusManipulatorRequestBus::Handler::BusConnect(entityComponentIdPair);
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusConnect(entityComponentIdPair);
 
         GenerateVertices();
+
+        const bool allowAsymmetricalEditing = IsShapeComponentTranslationEnabled();
+        m_componentModeDelegate.ConnectWithSingleComponentMode<EditorCapsuleShapeComponent, AzToolsFramework::CapsuleComponentMode>(
+            entityComponentIdPair, this, allowAsymmetricalEditing);
     }
 
     void EditorCapsuleShapeComponent::Deactivate()
     {
+        m_componentModeDelegate.Disconnect();
+
+        AzToolsFramework::ShapeManipulatorRequestBus::Handler::BusDisconnect();
+        AzToolsFramework::RadiusManipulatorRequestBus::Handler::BusDisconnect();
+        AzToolsFramework::CapsuleManipulatorRequestBus::Handler::BusDisconnect();
         AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect();
         m_capsuleShape.Deactivate();
         EditorBaseShapeComponent::Deactivate();
@@ -116,9 +142,21 @@ namespace LmbrCentral
         ShapeComponentNotificationsBus::Event(
             GetEntityId(), &ShapeComponentNotificationsBus::Events::OnShapeChanged,
             ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged);
+
+        AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast(
+            &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::Refresh,
+            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+
         return AZ::Edit::PropertyRefreshLevels::ValuesOnly;
     }
 
+    void EditorCapsuleShapeComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, [[maybe_unused]] const AZ::Transform&)
+    {
+        AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast(
+            &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::Refresh,
+            AZ::EntityComponentIdPair(GetEntityId(), GetId()));
+    }
+
     void EditorCapsuleShapeComponent::BuildGameEntity(AZ::Entity* gameEntity)
     {
         if (auto component = gameEntity->CreateComponent<CapsuleShapeComponent>())
@@ -147,4 +185,47 @@ namespace LmbrCentral
             m_capsuleShapeMesh.m_indexBuffer,
             m_capsuleShapeMesh.m_lineBuffer);
     }
+
+    float EditorCapsuleShapeComponent::GetHeight() const
+    {
+        return m_capsuleShape.GetCapsuleConfiguration().m_height;
+    }
+
+    void EditorCapsuleShapeComponent::SetHeight(float height)
+    {
+        m_capsuleShape.SetHeight(height);
+        GenerateVertices();
+    }
+
+    AZ::Quaternion EditorCapsuleShapeComponent::GetRotationOffset() const
+    {
+        return AZ::Quaternion::CreateIdentity();
+    }
+
+    float EditorCapsuleShapeComponent::GetRadius() const
+    {
+        return m_capsuleShape.GetCapsuleConfiguration().m_radius;
+    }
+
+    void EditorCapsuleShapeComponent::SetRadius(float radius)
+    {
+        m_capsuleShape.SetRadius(radius);
+        GenerateVertices();
+    }
+
+    AZ::Vector3 EditorCapsuleShapeComponent::GetTranslationOffset() const
+    {
+        return m_capsuleShape.GetTranslationOffset();
+    }
+
+    void EditorCapsuleShapeComponent::SetTranslationOffset(const AZ::Vector3& translationOffset)
+    {
+        m_capsuleShape.SetTranslationOffset(translationOffset);
+        GenerateVertices();
+    }
+
+    AZ::Transform EditorCapsuleShapeComponent::GetManipulatorSpace() const
+    {
+        return GetWorldTM();
+    }
 } // namespace LmbrCentral

+ 34 - 6
Gems/LmbrCentral/Code/Source/Shape/EditorCapsuleShapeComponent.h

@@ -7,16 +7,23 @@
  */
 #pragma once
 
-#include "EditorBaseShapeComponent.h"
-#include "CapsuleShapeComponent.h"
 #include <AzFramework/Entity/EntityDebugDisplayBus.h>
+#include <AzToolsFramework/ComponentMode/ComponentModeDelegate.h>
+#include <AzToolsFramework/Manipulators/CapsuleManipulatorRequestBus.h>
+#include <AzToolsFramework/Manipulators/RadiusManipulatorRequestBus.h>
+#include <AzToolsFramework/Manipulators/ShapeManipulatorRequestBus.h>
 #include <LmbrCentral/Shape/CapsuleShapeComponentBus.h>
+#include <Shape/CapsuleShapeComponent.h>
+#include <Shape/EditorBaseShapeComponent.h>
 
 namespace LmbrCentral
 {
     class EditorCapsuleShapeComponent
         : public EditorBaseShapeComponent
         , private AzFramework::EntityDebugDisplayEventBus::Handler
+        , private AzToolsFramework::CapsuleManipulatorRequestBus::Handler
+        , private AzToolsFramework::RadiusManipulatorRequestBus::Handler
+        , private AzToolsFramework::ShapeManipulatorRequestBus::Handler
     {
     public:
         AZ_EDITOR_COMPONENT(EditorCapsuleShapeComponent, EditorCapsuleShapeComponentTypeId, EditorBaseShapeComponent);
@@ -38,22 +45,43 @@ namespace LmbrCentral
 
         static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
 
-        // EditorComponentBase
+        // EditorComponentBase overrides ...
         void BuildGameEntity(AZ::Entity* gameEntity) override;
 
+        // AZ::TransformNotificationBus overrides ...
+        void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
+
     private:
         AZ_DISABLE_COPY_MOVE(EditorCapsuleShapeComponent)
 
-        // AzFramework::EntityDebugDisplayEventBus
+        // AzFramework::EntityDebugDisplayEventBus overrides ...
         void DisplayEntityViewport(
             const AzFramework::ViewportInfo& viewportInfo,
             AzFramework::DebugDisplayRequests& debugDisplay) override;
 
+        // AzToolsFramework::CapsuleManipulatorRequestBus overrides ...
+        float GetHeight() const override;
+        void SetHeight(float height) override;
+        AZ::Quaternion GetRotationOffset() const override;
+
+        // AzToolsFramework::RadiusManipulatorRequestBus overrides ...
+        float GetRadius() const override;
+        void SetRadius(float radius) override;
+
+        // AzToolsFramework::ShapeManipulatorRequestBus overrides ...
+        AZ::Vector3 GetTranslationOffset() const override;
+        void SetTranslationOffset(const AZ::Vector3& translationOffset) override;
+        AZ::Transform GetManipulatorSpace() const override;
+
         AZ::Crc32 ConfigurationChanged();
         void ClampHeight();
         void GenerateVertices();
 
-        CapsuleShape m_capsuleShape; ///< Stores underlying capsule representation for this component.
-        ShapeMesh m_capsuleShapeMesh; ///< Buffer to hold index and vertex data for CapsuleShape when drawing.
+        CapsuleShape m_capsuleShape; //!< Stores underlying capsule representation for this component.
+        ShapeMesh m_capsuleShapeMesh; //!< Buffer to hold index and vertex data for CapsuleShape when drawing.
+
+        using ComponentModeDelegate = AzToolsFramework::ComponentModeFramework::ComponentModeDelegate;
+        ComponentModeDelegate
+            m_componentModeDelegate; //!< Responsible for detecting ComponentMode activation and creating a concrete ComponentMode.
     };
 } // namespace LmbrCentral

+ 137 - 2
Gems/LmbrCentral/Code/Tests/EditorCapsuleShapeComponentTests.cpp

@@ -5,8 +5,12 @@
  * SPDX-License-Identifier: Apache-2.0 OR MIT
  *
  */
-#include "LmbrCentralReflectionTest.h"
-#include "Shape/EditorCapsuleShapeComponent.h"
+
+#include <AzToolsFramework/Viewport/ViewportSettings.h>
+#include <EditorShapeTestUtils.h>
+#include <LmbrCentralReflectionTest.h>
+#include <Shape/EditorCapsuleShapeComponent.h>
+#include <Shape/EditorSphereShapeComponent.h>
 
 namespace LmbrCentral
 {
@@ -66,5 +70,136 @@ namespace LmbrCentral
 
         EXPECT_FLOAT_EQ(radius, 1.57f);
     }
+
+    class EditorCapsuleShapeComponentFixture
+        : public UnitTest::ToolsApplicationFixture<>
+        , public UnitTest::RegistryTestHelper
+    {
+    public:
+        void SetUpEditorFixtureImpl() override;
+        void TearDownEditorFixtureImpl() override;
+
+        AZStd::unique_ptr<AZ::ComponentDescriptor> m_editorCapsuleShapeComponentDescriptor;
+        AZStd::unique_ptr<AZ::ComponentDescriptor> m_editorSphereShapeComponentDescriptor;
+
+        AZ::Entity* m_entity = nullptr;
+        AZ::EntityId m_entityId;
+        AZ::EntityComponentIdPair m_entityComponentIdPair;
+    };
+
+    void EditorCapsuleShapeComponentFixture::SetUpEditorFixtureImpl()
+    {
+        RegistryTestHelper::SetUp(LmbrCentral::ShapeComponentTranslationOffsetEnabled, true);
+
+        AZ::SerializeContext* serializeContext = nullptr;
+        AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
+
+        // need to reflect EditorSphereShapeComponent in order for EditorBaseShapeComponent to be reflected
+        m_editorSphereShapeComponentDescriptor = AZStd::unique_ptr<AZ::ComponentDescriptor>(EditorSphereShapeComponent::CreateDescriptor());
+
+        m_editorCapsuleShapeComponentDescriptor =
+            AZStd::unique_ptr<AZ::ComponentDescriptor>(EditorCapsuleShapeComponent::CreateDescriptor());
+
+        ShapeComponentConfig::Reflect(serializeContext);
+        CapsuleShape::Reflect(serializeContext);
+        m_editorSphereShapeComponentDescriptor->Reflect(serializeContext);
+        m_editorCapsuleShapeComponentDescriptor->Reflect(serializeContext);
+
+        UnitTest::CreateDefaultEditorEntity("CapsuleShapeComponentEntity", &m_entity);
+        m_entityId = m_entity->GetId();
+        m_entity->Deactivate();
+        m_entityComponentIdPair =
+            AZ::EntityComponentIdPair(m_entityId, m_entity->CreateComponent(EditorCapsuleShapeComponentTypeId)->GetId());
+        m_entity->Activate();
+    }
+
+    void EditorCapsuleShapeComponentFixture::TearDownEditorFixtureImpl()
+    {
+        AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
+            &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entityId);
+        m_entity = nullptr;
+        m_entityId.SetInvalid();
+
+        m_editorCapsuleShapeComponentDescriptor.reset();
+        m_editorSphereShapeComponentDescriptor.reset();
+
+        RegistryTestHelper::TearDown();
+    }
+
+    using EditorCapsuleShapeComponentManipulatorFixture =
+        UnitTest::IndirectCallManipulatorViewportInteractionFixtureMixin<EditorCapsuleShapeComponentFixture>;
+
+    void SetUpCapsuleShapeComponent(
+        AZ::EntityId entityId,
+        const AZ::Transform& transform,
+        const AZ::Vector3& translationOffset,
+        float radius,
+        float height)
+    {
+        AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetWorldTM, transform);
+        ShapeComponentRequestsBus::Event(entityId, &ShapeComponentRequests::SetTranslationOffset, translationOffset);
+        CapsuleShapeComponentRequestsBus::Event(entityId, &CapsuleShapeComponentRequests::SetRadius, radius);
+        CapsuleShapeComponentRequestsBus::Event(entityId, &CapsuleShapeComponentRequests::SetHeight, height);
+    }
+
+    TEST_F(EditorCapsuleShapeComponentManipulatorFixture, CapsuleShapeSymmetricalHeightManipulatorsScaleCorrectly)
+    {
+        AZ::Transform capsuleTransform(AZ::Vector3(6.0f, -3.0f, 4.0f), AZ::Quaternion(0.3f, 0.1f, -0.3f, 0.9f), 2.0f);
+        const float radius = 0.5f;
+        const float height = 2.0f;
+        const AZ::Vector3 translationOffset(-5.0f, 3.0f, -2.0f);
+        SetUpCapsuleShapeComponent(m_entity->GetId(), capsuleTransform, translationOffset, radius, height);
+        EnterComponentMode(m_entityId, EditorCapsuleShapeComponentTypeId);
+
+        // position the camera so it is looking at the capsule
+        AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, -5.0f, 10.0f)));
+
+        const AZ::Vector3 worldStart(1.6f, 6.84f, 8.88f);
+        const AZ::Vector3 worldEnd(1.6f, 6.6f, 9.2f);
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd, AzToolsFramework::DefaultSymmetricalEditingModifier);
+
+        ExpectCapsuleHeight(m_entity->GetId(), 2.4f);
+    }
+
+    TEST_F(EditorCapsuleShapeComponentManipulatorFixture, CapsuleShapeAsymmetricalHeightManipulatorsScaleCorrectly)
+    {
+        AZ::Transform capsuleTransform(AZ::Vector3(2.0f, -6.0f, 5.0f), AZ::Quaternion(0.7f, -0.1f, -0.1f, 0.7f), 0.5f);
+        const float radius = 2.0f;
+        const float height = 7.0f;
+        const AZ::Vector3 translationOffset(2.0f, 5.0f, -3.0f);
+        SetUpCapsuleShapeComponent(m_entity->GetId(), capsuleTransform, translationOffset, radius, height);
+        EnterComponentMode(m_entityId, EditorCapsuleShapeComponentTypeId);
+
+        // position the camera so it is looking at the capsule
+        AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3(5.0f, -10.0f, 7.5f)));
+
+        const AZ::Vector3 worldStart(3.87f, -3.16f, 7.5f);
+        const AZ::Vector3 worldEnd(3.73f, -3.64f, 7.5f);
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
+
+        ExpectCapsuleHeight(m_entity->GetId(), 6.0f);
+    }
+
+    TEST_F(EditorCapsuleShapeComponentManipulatorFixture, CapsuleShapeRadiusManipulatorScalesCorrectly)
+    {
+        AZ::Transform capsuleTransform(AZ::Vector3(-4.0f, -5.0f, 1.0f), AZ::Quaternion::CreateIdentity(), 2.5f);
+        const float radius = 1.0f;
+        const float height = 5.0f;
+        const AZ::Vector3 translationOffset(6.0f, 3.0f, -2.0f);
+        SetUpCapsuleShapeComponent(m_entity->GetId(), capsuleTransform, translationOffset, radius, height);
+        EnterComponentMode(m_entityId, EditorCapsuleShapeComponentTypeId);
+
+        // position the camera so it is looking at the capsule
+        AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3(15.0f, -5.0f, -5.0f)));
+
+        const AZ::Vector3 worldStart(13.5f, 2.5f, -4.0f);
+        const AZ::Vector3 worldEnd(14.75f, 2.5f, -4.0f);
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
+
+        ExpectCapsuleRadius(m_entity->GetId(), 1.5f);
+    }
 }
 

+ 15 - 19
Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.cpp

@@ -9,6 +9,7 @@
 #include <EditorShapeTestUtils.h>
 #include <AzToolsFramework/Entity/EditorEntityHelpers.h>
 #include <LmbrCentral/Shape/BoxShapeComponentBus.h>
+#include <LmbrCentral/Shape/CapsuleShapeComponentBus.h>
 #include <LmbrCentral/Shape/ShapeComponentBus.h>
 #include <Shape/EditorSphereShapeComponent.h>
 
@@ -17,25 +18,6 @@ namespace LmbrCentral
     // use a large tolerance for manipulator tests, because accuracy is limited by viewport resolution
     static constexpr float ManipulatorTolerance = 0.1f;
 
-    void DragMouse(
-        const AzFramework::CameraState& cameraState,
-        AzManipulatorTestFramework::ImmediateModeActionDispatcher* actionDispatcher,
-        const AZ::Vector3& worldStart,
-        const AZ::Vector3& worldEnd,
-        const AzToolsFramework::ViewportInteraction::KeyboardModifier keyboardModifier)
-    {
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, cameraState);
-
-        actionDispatcher
-            ->CameraState(cameraState)
-            ->MousePosition(screenStart)
-            ->KeyboardModifierDown(keyboardModifier)
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
-    }
-
     void EnterComponentMode(AZ::EntityId entityId, const AZ::Uuid& componentType)
     {
         AzToolsFramework::SelectEntity(entityId);
@@ -51,6 +33,20 @@ namespace LmbrCentral
         EXPECT_THAT(boxDimensions, UnitTest::IsCloseTolerance(expectedBoxDimensions, ManipulatorTolerance));
     }
 
+    void ExpectCapsuleRadius(AZ::EntityId entityId, float expectedRadius)
+    {
+        float radius = 0.0f;
+        CapsuleShapeComponentRequestsBus::EventResult(radius, entityId, &CapsuleShapeComponentRequests::GetRadius);
+        EXPECT_NEAR(radius, expectedRadius, ManipulatorTolerance);
+    }
+
+    void ExpectCapsuleHeight(AZ::EntityId entityId, float expectedHeight)
+    {
+        float height = 0.0f;
+        CapsuleShapeComponentRequestsBus::EventResult(height, entityId, &CapsuleShapeComponentRequests::GetHeight);
+        EXPECT_NEAR(height, expectedHeight, ManipulatorTolerance);
+    }
+
     void ExpectTranslationOffset(AZ::EntityId entityId, const AZ::Vector3& expectedTranslationOffset)
     {
         AZ::Vector3 translationOffset = AZ::Vector3::CreateZero();

+ 5 - 8
Gems/LmbrCentral/Code/Tests/EditorShapeTestUtils.h

@@ -11,6 +11,7 @@
 #include <AZTestShared/Utils/Utils.h>
 #include <AzManipulatorTestFramework/AzManipulatorTestFramework.h>
 #include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
+#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h>
 #include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
 #include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
 #include <AzManipulatorTestFramework/ViewportInteraction.h>
@@ -18,14 +19,6 @@
 
 namespace LmbrCentral
 {
-    void DragMouse(
-        const AzFramework::CameraState& cameraState,
-        AzManipulatorTestFramework::ImmediateModeActionDispatcher* actionDispatcher,
-        const AZ::Vector3& worldStart,
-        const AZ::Vector3& worldEnd,
-        const AzToolsFramework::ViewportInteraction::KeyboardModifier keyboardModifier =
-        AzToolsFramework::ViewportInteraction::KeyboardModifier::None);
-
     void EnterComponentMode(AZ::EntityId entityId, const AZ::Uuid& componentType);
 
     void SetComponentSubMode(
@@ -33,6 +26,10 @@ namespace LmbrCentral
 
     void ExpectBoxDimensions(AZ::EntityId entityId, const AZ::Vector3& expectedBoxDimensions);
 
+    void ExpectCapsuleRadius(AZ::EntityId entityId, float expectedRadius);
+
+    void ExpectCapsuleHeight(AZ::EntityId entityId, float expectedHeight);
+
     void ExpectTranslationOffset(AZ::EntityId entityId, const AZ::Vector3& expectedTranslationOffset);
 
     void ExpectSubMode(

+ 3 - 5
Gems/PhysX/Code/Editor/ColliderBoxMode.cpp

@@ -24,7 +24,7 @@ namespace PhysX
     {
         AzToolsFramework::InstallBaseShapeViewportEditFunctions(m_boxEdit.get(), idPair);
         AzToolsFramework::InstallBoxViewportEditFunctions(m_boxEdit.get(), idPair);
-        m_boxEdit->Setup();
+        m_boxEdit->Setup(AzToolsFramework::g_mainManipulatorManagerId);
         m_boxEdit->AddEntityComponentIdPair(idPair);
     }
 
@@ -38,10 +38,8 @@ namespace PhysX
         m_boxEdit->Teardown();
     }
 
-    void ColliderBoxMode::ResetValues(const AZ::EntityComponentIdPair& idPair)
+    void ColliderBoxMode::ResetValues([[maybe_unused]] const AZ::EntityComponentIdPair& idPair)
     {
-        AzToolsFramework::BoxManipulatorRequestBus::Event(
-            idPair, &AzToolsFramework::BoxManipulatorRequests::SetDimensions,
-            AZ::Vector3::CreateOne());
+        m_boxEdit->ResetValues();
     }
 }

+ 22 - 15
Gems/PhysX/Code/Editor/ColliderCapsuleMode.cpp

@@ -24,7 +24,8 @@ namespace PhysX
     void ColliderCapsuleMode::Setup(const AZ::EntityComponentIdPair& idPair)
     {
         m_entityComponentIdPair = idPair;
-        m_capsuleViewportEdit = AZStd::make_unique<AzToolsFramework::CapsuleViewportEdit>();
+        const bool allowAsymmetricalEditing = true;
+        m_capsuleViewportEdit = AZStd::make_unique<AzToolsFramework::CapsuleViewportEdit>(allowAsymmetricalEditing);
         m_capsuleViewportEdit->InstallGetManipulatorSpace(
             [this]()
             {
@@ -44,47 +45,53 @@ namespace PhysX
             [this]()
             {
                 AZ::Vector3 colliderTranslation = AZ::Vector3::CreateZero();
-                PhysX::EditorColliderComponentRequestBus::EventResult(
-                    colliderTranslation, m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::GetColliderOffset);
+                EditorColliderComponentRequestBus::EventResult(
+                    colliderTranslation, m_entityComponentIdPair, &EditorColliderComponentRequests::GetColliderOffset);
                 return colliderTranslation;
             });
         m_capsuleViewportEdit->InstallGetRotationOffset(
             [this]()
             {
                 AZ::Quaternion colliderRotation = AZ::Quaternion::CreateIdentity();
-                PhysX::EditorColliderComponentRequestBus::EventResult(
-                    colliderRotation, m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::GetColliderRotation);
+                EditorColliderComponentRequestBus::EventResult(
+                    colliderRotation, m_entityComponentIdPair, &EditorColliderComponentRequests::GetColliderRotation);
                 return colliderRotation;
             });
         m_capsuleViewportEdit->InstallGetCapsuleRadius(
             [this]()
             {
                 float capsuleRadius = 0.0f;
-                PhysX::EditorColliderComponentRequestBus::EventResult(
-                    capsuleRadius, m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::GetCapsuleRadius);
+                EditorColliderComponentRequestBus::EventResult(
+                    capsuleRadius, m_entityComponentIdPair, &EditorColliderComponentRequests::GetCapsuleRadius);
                 return capsuleRadius;
             });
         m_capsuleViewportEdit->InstallGetCapsuleHeight(
             [this]()
             {
                 float capsuleHeight = 0.0f;
-                PhysX::EditorColliderComponentRequestBus::EventResult(
-                    capsuleHeight, m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::GetCapsuleHeight);
+                EditorColliderComponentRequestBus::EventResult(
+                    capsuleHeight, m_entityComponentIdPair, &EditorColliderComponentRequests::GetCapsuleHeight);
                 return capsuleHeight;
             });
         m_capsuleViewportEdit->InstallSetCapsuleRadius(
             [this](float radius)
             {
-                PhysX::EditorColliderComponentRequestBus::Event(
-                    m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::SetCapsuleRadius, radius);
+                EditorColliderComponentRequestBus::Event(
+                    m_entityComponentIdPair, &EditorColliderComponentRequests::SetCapsuleRadius, radius);
             });
         m_capsuleViewportEdit->InstallSetCapsuleHeight(
             [this](float height)
             {
-                PhysX::EditorColliderComponentRequestBus::Event(
-                    m_entityComponentIdPair, &PhysX::EditorColliderComponentRequests::SetCapsuleHeight, height);
+                EditorColliderComponentRequestBus::Event(
+                    m_entityComponentIdPair, &EditorColliderComponentRequests::SetCapsuleHeight, height);
             });
-        m_capsuleViewportEdit->Setup();
+        m_capsuleViewportEdit->InstallSetTranslationOffset(
+            [this](const AZ::Vector3& translationOffset)
+            {
+                EditorColliderComponentRequestBus::Event(
+                    m_entityComponentIdPair, &EditorColliderComponentRequests::SetColliderOffset, translationOffset);
+            });
+        m_capsuleViewportEdit->Setup(AzToolsFramework::g_mainManipulatorManagerId);
         m_capsuleViewportEdit->AddEntityComponentIdPair(idPair);
         AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(idPair.GetEntityId());
     }
@@ -126,4 +133,4 @@ namespace PhysX
             m_capsuleViewportEdit->OnCameraStateChanged(cameraState);
         }
     }
-}
+} // namespace PhysX

+ 14 - 4
Gems/PhysX/Code/Editor/ColliderComponentMode.cpp

@@ -399,7 +399,7 @@ namespace PhysX
     }
 
     static AzToolsFramework::ViewportUi::ButtonId RegisterClusterButton(
-        AzToolsFramework::ViewportUi::ClusterId clusterId, const char* iconName)
+        AzToolsFramework::ViewportUi::ClusterId clusterId, const char* iconName, const char* tooltip)
     {
         AzToolsFramework::ViewportUi::ButtonId buttonId;
         AzToolsFramework::ViewportUi::ViewportUiRequestBus::EventResult(
@@ -407,6 +407,13 @@ namespace PhysX
             &AzToolsFramework::ViewportUi::ViewportUiRequestBus::Events::CreateClusterButton, clusterId,
             AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s.svg", iconName));
 
+        AzToolsFramework::ViewportUi::ViewportUiRequestBus::Event(
+            AzToolsFramework::ViewportUi::DefaultViewportId,
+            &AzToolsFramework::ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip,
+            clusterId,
+            buttonId,
+            tooltip);
+
         return buttonId;
     }
 
@@ -426,9 +433,12 @@ namespace PhysX
 
         // create and register the buttons
         m_buttonIds.resize(static_cast<size_t>(SubMode::NumModes));
-        m_buttonIds[static_cast<size_t>(SubMode::Offset)] = RegisterClusterButton(m_modeSelectionClusterId, "Move");
-        m_buttonIds[static_cast<size_t>(SubMode::Rotation)] = RegisterClusterButton(m_modeSelectionClusterId, "Rotate");
-        m_buttonIds[static_cast<size_t>(SubMode::Dimensions)] = RegisterClusterButton(m_modeSelectionClusterId, "Scale");
+        m_buttonIds[static_cast<size_t>(SubMode::Offset)] =
+            RegisterClusterButton(m_modeSelectionClusterId, "Move", "Switch to translation offset mode");
+        m_buttonIds[static_cast<size_t>(SubMode::Rotation)] =
+            RegisterClusterButton(m_modeSelectionClusterId, "Rotate", "Switch to rotation offset mode");
+        m_buttonIds[static_cast<size_t>(SubMode::Dimensions)] =
+            RegisterClusterButton(m_modeSelectionClusterId, "Scale", "Switch to dimensions mode");
 
         SetCurrentMode(SubMode::Offset);
 

+ 72 - 101
Gems/PhysX/Code/Tests/PhysXColliderComponentModeTests.cpp

@@ -10,6 +10,7 @@
 
 #include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
 #include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
+#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkUtils.h>
 #include <AZTestShared/Math/MathTestHelpers.h>
 #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
 #include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
@@ -421,17 +422,7 @@ namespace UnitTest
         // position in world space to drag to
         const AZ::Vector3 worldEnd(-(x + xDelta), 0.0f, 0.0f);
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to interact with the x scale manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         const auto worldToScreenMultiplier = 1.0f / AzToolsFramework::CalculateScreenToWorldMultiplier(worldStart, m_cameraState);
         const auto assetScale = colliderEntity->FindComponent<TestColliderComponentMode>()->GetAssetScale();
@@ -530,7 +521,7 @@ namespace UnitTest
         SetupTransform(entityRotation, entityTranslation, uniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Offset);
 
-        // the expected position of the collider centre based on the combination of entity transform and collider offset
+        // the expected position of the central point of the collider based on the combination of entity transform and collider offset
         const AZ::Vector3 expectedColliderPosition(8.8f, -2.28f, 3.54f);
 
         // the expected world space direction of the collider offset x-axis based on the entity transform
@@ -548,17 +539,7 @@ namespace UnitTest
         // position in world space to move to
         const AZ::Vector3 worldEnd = worldStart + 2.0f * expectedXAxis;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the x offset manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         AZ::Vector3 newColliderOffset = AZ::Vector3::CreateZero();
         PhysX::EditorColliderComponentRequestBus::EventResult(
@@ -582,7 +563,8 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Offset);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(4.13f, 4.84f, -4.75f);
 
         // the expected world space direction of the collider offset z-axis based on the entity transform
@@ -601,17 +583,7 @@ namespace UnitTest
         // position in world space to move to
         const AZ::Vector3 worldEnd = worldStart - 2.25f * expectedZAxis;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the z offset manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         AZ::Vector3 newColliderOffset = AZ::Vector3::CreateZero();
         PhysX::EditorColliderComponentRequestBus::EventResult(
@@ -634,10 +606,11 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Dimensions);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(4.37f, -4.285f, -1.1f);
 
-        // the expected position of the y scale manipulator relative to the centre of the collider, based on collider
+        // the expected position of the y scale manipulator relative to the central point of the collider, based on collider
         // rotation, entity rotation and scale, and non-uniform scale
         const AZ::Vector3 scaleManipulatorYDelta(0.54f, -0.72f, -1.2f);
 
@@ -650,18 +623,7 @@ namespace UnitTest
         const AZ::Vector3 worldStart = expectedColliderPosition + scaleManipulatorYDelta;
         const AZ::Vector3 worldEnd = worldStart + 0.1f * scaleManipulatorYDelta;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the y scale manipulator
-            ->MousePosition(screenStart)
-            ->KeyboardModifierDown(AzToolsFramework::DefaultSymmetricalEditingModifier)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd, AzToolsFramework::DefaultSymmetricalEditingModifier);
 
         AZ::Vector3 newBoxDimensions = AZ::Vector3::CreateZero();
         AzToolsFramework::BoxManipulatorRequestBus::EventResult(
@@ -691,10 +653,11 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Dimensions);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(-1.1f, 21.94f, -11.08f);
 
-        // the expected position of the -z scale manipulator relative to the centre of the collider, based on collider
+        // the expected position of the -z scale manipulator relative to the central point of the collider, based on collider
         // rotation, entity rotation and scale, and non-uniform scale
         const AZ::Vector3 scaleManipulatorMinusZDelta(-4.608f, 2.5752f, -0.8064f);
 
@@ -707,17 +670,7 @@ namespace UnitTest
             AZ::Transform::CreateFromQuaternionAndTranslation(
                 AZ::Quaternion::CreateRotationZ(3.0f * AZ::Constants::QuarterPi), worldStart + AZ::Vector3(5.0f, 5.0f, 0.0f)));
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the -z scale manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         AZ::Vector3 newBoxDimensions = AZ::Vector3::CreateZero();
         AzToolsFramework::BoxManipulatorRequestBus::EventResult(
@@ -750,31 +703,22 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Dimensions);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(1.7f, -10.65f, -3.0f);
 
         // position the camera to look at the collider along the y-axis
         AzFramework::SetCameraTransform(
             m_cameraState, AZ::Transform::CreateTranslation(expectedColliderPosition - AZ::Vector3(0.0f, 5.0f, 0.0f)));
 
-        // the expected position of the scale manipulator relative to the centre of the collider, based on collider
+        // the expected position of the scale manipulator relative to the central point of the collider, based on collider
         // rotation, entity scale, non-uniform scale and camera state
         const AZ::Vector3 scaleManipulatorDelta(-1.1952f, -1.8036f, 0.168f);
 
         const AZ::Vector3 worldStart = expectedColliderPosition + scaleManipulatorDelta;
         const AZ::Vector3 worldEnd = worldStart - 0.1f * scaleManipulatorDelta;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the y scale manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         float newSphereRadius = 0.0f;
         PhysX::EditorColliderComponentRequestBus::EventResult(
@@ -783,7 +727,9 @@ namespace UnitTest
         EXPECT_NEAR(newSphereRadius, 0.9f, ManipulatorTolerance);
     }
 
-    TEST_F(PhysXEditorColliderComponentManipulatorFixture, CapsuleColliderScaleManipulatorsCorrectlyLocatedRelativeToColliderWithNonUniformScale)
+    TEST_F(
+        PhysXEditorColliderComponentManipulatorFixture,
+        CapsuleColliderSymmetricalScaleManipulatorsCorrectlyLocatedRelativeToColliderWithNonUniformScale)
     {
         const float capsuleRadius = 0.2f;
         const float capsuleHeight = 1.0f;
@@ -798,10 +744,11 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Dimensions);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(-0.92f, -2.44f, -5.0f);
 
-        // the expected position of the height manipulator relative to the centre of the collider, based on collider
+        // the expected position of the height manipulator relative to the central point of the collider, based on collider
         // rotation, entity scale and non-uniform scale
         const AZ::Vector3 heightManipulatorDelta(-0.3096f, 0.6528f, 0.4f);
 
@@ -814,17 +761,7 @@ namespace UnitTest
         const AZ::Vector3 worldStart = expectedColliderPosition + heightManipulatorDelta;
         const AZ::Vector3 worldEnd = worldStart + 0.2f * heightManipulatorDelta;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to the position of the height manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd, AzToolsFramework::DefaultSymmetricalEditingModifier);
 
         float newCapsuleHeight = 0.0f;
         PhysX::EditorColliderComponentRequestBus::EventResult(
@@ -833,6 +770,49 @@ namespace UnitTest
         EXPECT_NEAR(newCapsuleHeight, 1.2f, ManipulatorTolerance);
     }
 
+    TEST_F(
+        PhysXEditorColliderComponentManipulatorFixture,
+        CapsuleColliderAsymmetricalScaleManipulatorsCorrectlyLocatedRelativeToColliderWithNonUniformScale)
+    {
+        const float capsuleRadius = 0.2f;
+        const float capsuleHeight = 1.0f;
+        const AZ::Quaternion capsuleRotation(-0.2f, -0.8f, -0.4f, 0.4f);
+        const AZ::Vector3 capsuleOffset(1.0f, -2.0f, 1.0f);
+        SetupCollider(Physics::CapsuleShapeConfiguration(capsuleHeight, capsuleRadius), capsuleRotation, capsuleOffset);
+        const AZ::Quaternion entityRotation(0.7f, -0.1f, -0.1f, 0.7f);
+        const AZ::Vector3 entityTranslation(-2.0f, 1.0f, -3.0f);
+        const float uniformScale = 2.0f;
+        SetupTransform(entityRotation, entityTranslation, uniformScale);
+        const AZ::Vector3 nonUniformScale(1.0f, 0.5f, 1.5f);
+        SetupNonUniformScale(nonUniformScale);
+        EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Dimensions);
+
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
+        const AZ::Vector3 expectedColliderPosition(-0.92f, -2.44f, -5.0f);
+
+        // the expected position of the height manipulator relative to the central point of the collider, based on collider
+        // rotation, entity scale and non-uniform scale
+        const AZ::Vector3 heightManipulatorDelta(-0.3096f, 0.6528f, 0.4f);
+
+        // position the camera to look at the collider along the y-z diagonal
+        AzFramework::SetCameraTransform(
+            m_cameraState,
+            AZ::Transform::CreateFromQuaternionAndTranslation(
+                AZ::Quaternion::CreateRotationX(-AZ::Constants::QuarterPi), expectedColliderPosition + AZ::Vector3(0.0f, -1.0f, 1.0f)));
+
+        const AZ::Vector3 worldStart = expectedColliderPosition + heightManipulatorDelta;
+        const AZ::Vector3 worldEnd = worldStart + 0.2f * heightManipulatorDelta;
+
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
+
+        float newCapsuleHeight = 0.0f;
+        PhysX::EditorColliderComponentRequestBus::EventResult(
+            newCapsuleHeight, m_idPair, &PhysX::EditorColliderComponentRequests::GetCapsuleHeight);
+
+        EXPECT_NEAR(newCapsuleHeight, 1.1f, ManipulatorTolerance);
+    }
+
     TEST_F(PhysXEditorColliderComponentManipulatorFixture, ColliderRotationManipulatorsCorrectlyLocatedRelativeToColliderWithNonUniformScale)
     {
         const float capsuleRadius = 1.2f;
@@ -848,7 +828,8 @@ namespace UnitTest
         SetupNonUniformScale(nonUniformScale);
         EnterColliderSubMode(PhysX::ColliderComponentModeRequests::SubMode::Rotation);
 
-        // the expected position of the collider centre based on the combination of entity transform, collider offset and non-uniform scale
+        // the expected position of the central point of the collider based on the combination of entity transform, collider offset and
+        // non-uniform scale
         const AZ::Vector3 expectedColliderPosition(-0.86f, 4.8f, -0.52f);
 
         // the y and z axes of the collider's frame in world space, used to locate points on the x rotation manipulator arc to interact with
@@ -865,17 +846,7 @@ namespace UnitTest
         const AZ::Vector3 worldStart = expectedColliderPosition + screenToWorldMultiplier * manipulatorViewRadius * yDirection;
         const AZ::Vector3 worldEnd = expectedColliderPosition + screenToWorldMultiplier * manipulatorViewRadius * zDirection;
 
-        const auto screenStart = AzFramework::WorldToScreen(worldStart, m_cameraState);
-        const auto screenEnd = AzFramework::WorldToScreen(worldEnd, m_cameraState);
-
-        m_actionDispatcher
-            ->CameraState(m_cameraState)
-            // move the mouse to a position on the angular manipulator
-            ->MousePosition(screenStart)
-            // drag to move the manipulator
-            ->MouseLButtonDown()
-            ->MousePosition(screenEnd)
-            ->MouseLButtonUp();
+        DragMouse(m_cameraState, m_actionDispatcher.get(), worldStart, worldEnd);
 
         AZ::Quaternion newColliderRotation = AZ::Quaternion::CreateIdentity();
         PhysX::EditorColliderComponentRequestBus::EventResult(