Explorar o código

Merge branch 'o3de:development' into ShaderVariantListSaveAll

Ethan Chen %!s(int64=2) %!d(string=hai) anos
pai
achega
14dd2ddd17
Modificáronse 100 ficheiros con 4555 adicións e 2372 borrados
  1. 5 0
      Code/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp
  2. 43 0
      Code/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui
  3. 0 5
      Code/Editor/CryEdit.cpp
  4. 7 0
      Code/Editor/EditorToolsApplication.cpp
  5. 2 2
      Code/Editor/Style/Editor.qss
  6. 230 88
      Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp
  7. 21 1
      Code/Framework/AzCore/AzCore/Component/ComponentApplication.h
  8. 12 11
      Code/Framework/AzCore/AzCore/Debug/PerformanceCollector.h
  9. 63 0
      Code/Framework/AzCore/AzCore/Metrics/IEventLogger.h
  10. 116 3
      Code/Framework/AzCore/AzCore/Metrics/JsonTraceEventLogger.cpp
  11. 41 5
      Code/Framework/AzCore/AzCore/Metrics/JsonTraceEventLogger.h
  12. 13 4
      Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.cpp
  13. 231 12
      Code/Framework/AzCore/Tests/Metrics/JsonTraceEventLoggerTests.cpp
  14. 2 0
      Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/Internal/RectangleWidget.h
  15. 13 0
      Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Eyedropper.svg
  16. 25 0
      Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Smooth.svg
  17. 2 0
      Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc
  18. 3 0
      Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/entity_overridden.svg
  19. 1 0
      Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc
  20. 13 15
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeSwitcher.cpp
  21. 2 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeSwitcher.h
  22. 2 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h
  23. 405 209
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushManipulator.cpp
  24. 65 8
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushManipulator.h
  25. 33 4
      Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushNotificationBus.h
  26. 163 62
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettings.cpp
  27. 68 20
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettings.h
  28. 3 6
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsNotificationBus.h
  29. 13 0
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsRequestBus.h
  30. 20 0
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsSystemComponent.cpp
  31. 4 0
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsSystemComponent.h
  32. 2 2
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsWindow.cpp
  33. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsWindow_Internals.h
  34. 5 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplateInterface.h
  35. 44 20
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplatePropagator.cpp
  36. 3 2
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplatePropagator.h
  37. 109 48
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Link/Link.cpp
  38. 77 10
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Link/Link.h
  39. 30 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverrideHandler.cpp
  40. 28 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverrideHandler.h
  41. 65 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.cpp
  42. 42 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.h
  43. 30 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicInterface.h
  44. 1 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomTypes.h
  45. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabInstanceUtils.cpp
  46. 4 3
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp
  47. 18 32
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp
  48. 8 4
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp
  49. 2 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h
  50. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Undo/PrefabUndoUpdateLink.cpp
  51. 36 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp
  52. 7 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h
  53. 5 0
      Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake
  54. 16 7
      Code/Framework/AzToolsFramework/Tests/ComponentModeSwitcherTests.cpp
  55. 39 0
      Code/Framework/AzToolsFramework/Tests/ComponentModeTestFixture.cpp
  56. 17 0
      Code/Framework/AzToolsFramework/Tests/ComponentModeTestFixture.h
  57. 2 1
      Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/Link/SingleInstanceMultiplePatchesBenchmarks.cpp
  58. 37 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTestFixture.cpp
  59. 31 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTestFixture.h
  60. 72 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTests.cpp
  61. 39 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverridePublicInterfaceTests.cpp
  62. 87 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverrideTestFixture.cpp
  63. 49 0
      Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverrideTestFixture.h
  64. 3 2
      Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestDataUtils.cpp
  65. 30 5
      Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp
  66. 3 0
      Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h
  67. 6 0
      Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake
  68. 1 1
      Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h
  69. 5 1
      Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialTypeBuilder.cpp
  70. 2 2
      Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp
  71. 0 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/Lib/3.x/3.10.x/site-packages/stub
  72. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/rocks_grid.mb
  73. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_BaseColor.png
  74. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Height.png
  75. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Metallic.png
  76. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Normal.png
  77. 3 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Roughness.png
  78. 1 1
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/ui.py
  79. 0 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/LICENSE
  80. 37 31
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/README.md
  81. 0 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/__init__.py
  82. 37 37
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/constants.py
  83. 174 174
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/fbx_exporter.py
  84. 93 93
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/o3de_utils.py
  85. 0 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/projects.json
  86. 678 678
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/ui.py
  87. 557 557
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/utils.py
  88. 9 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/modules/__init__.py
  89. 34 28
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py
  90. 6 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/constants.py
  91. 55 14
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/readme.md
  92. 17 50
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py
  93. 41 21
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/scene_exporter/export_tool.py
  94. 42 25
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/scene_exporter/readme.md
  95. 8 8
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py
  96. 59 23
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.md
  97. 0 2
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/__init__.py
  98. 18 0
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/.solutions/DCCsi_8x.wpr
  99. 0 1
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/config.py
  100. 92 29
      Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/readme.md

+ 5 - 0
Code/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp

@@ -145,6 +145,11 @@ AzAssetBrowserWindow::AzAssetBrowserWindow(QWidget* parent)
         m_ui->m_listViewButton->hide();
     }
 
+    m_ui->horizontalLayout->setAlignment(m_ui->m_listViewButton, Qt::AlignTop);
+    m_ui->horizontalLayout->setAlignment(m_ui->m_thumbnailViewButton, Qt::AlignTop);
+    m_ui->horizontalLayout->setAlignment(m_ui->m_toggleDisplayViewBtn, Qt::AlignTop);
+    m_ui->horizontalLayout->setAlignment(m_ui->m_collapseAllButton, Qt::AlignTop);
+
     connect(m_ui->m_thumbnailViewButton, &QAbstractButton::clicked, this, [this] { m_ui->m_middleStackWidget->setCurrentIndex(0); });
     connect(m_ui->m_listViewButton, &QAbstractButton::clicked, this, [this] { m_ui->m_middleStackWidget->setCurrentIndex(1); });
 

+ 43 - 0
Code/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui

@@ -17,6 +17,18 @@
    <property name="spacing">
     <number>0</number>
    </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
    <item>
     <widget class="QScrollArea" name="scrollArea">
      <property name="minimumSize">
@@ -25,6 +37,9 @@
        <height>1</height>
       </size>
      </property>
+     <property name="frameShape">
+      <enum>QFrame::NoFrame</enum>
+     </property>
      <property name="widgetResizable">
       <bool>true</bool>
      </property>
@@ -55,6 +70,18 @@
        </property>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout">
+         <property name="leftMargin">
+          <number>5</number>
+         </property>
+         <property name="topMargin">
+          <number>5</number>
+         </property>
+         <property name="rightMargin">
+          <number>5</number>
+         </property>
+         <property name="bottomMargin">
+          <number>5</number>
+         </property>
          <item>
           <widget class="AzToolsFramework::AssetBrowser::SearchWidget" name="m_searchWidget" native="true">
            <property name="sizePolicy">
@@ -124,6 +151,22 @@
          </item>
         </layout>
        </item>
+       <item>
+        <widget class="QWidget" name="m_separator" native="true">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>1</height>
+          </size>
+         </property>
+        </widget>
+       </item>
        <item>
         <widget class="QSplitter" name="m_splitter">
          <property name="sizePolicy">

+ 0 - 5
Code/Editor/CryEdit.cpp

@@ -1653,11 +1653,6 @@ bool CCryEditApp::InitInstance()
         return false;
     }
 
-    // Reflect property control classes to the serialize context...
-    AZ::SerializeContext* serializeContext = nullptr;
-    AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext);
-    AZ_Assert(serializeContext, "Serialization context not available");
-    ReflectedVarInit::setupReflection(serializeContext);
     RegisterReflectedVarHandlers();
 
     CreateSplashScreen();

+ 7 - 0
Code/Editor/EditorToolsApplication.cpp

@@ -20,6 +20,7 @@
 
 // Editor
 #include "MainWindow.h"
+#include "Controls/ReflectedPropertyControl/ReflectedVar.h"
 #include "CryEdit.h"
 #include "DisplaySettingsPythonFuncs.h"
 #include "GameEngine.h"
@@ -129,6 +130,12 @@ namespace EditorInternal
     {
         ToolsApplication::Reflect(context);
 
+        // Reflect property control classes to the serialize context...
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            ReflectedVarInit::setupReflection(serializeContext);
+        }
+
         if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
         {
             behaviorContext->EBus<EditorToolsApplicationRequestBus>("EditorToolsApplicationRequestBus")

+ 2 - 2
Code/Editor/Style/Editor.qss

@@ -17,9 +17,9 @@ AzAssetBrowserWindow #previewWidgetWrapper
     border: 0 none;
 }
 
-AzAssetBrowserWindow QSplitter
+AzAssetBrowserWindow #m_separator
 {
-    margin-top: 2px;
+    background-color: #222222;
 }
 
 /* Component Palette Widget */

+ 230 - 88
Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp

@@ -17,13 +17,15 @@
 #include <AzCore/Component/ComponentApplicationLifecycle.h>
 #include <AzCore/Component/TickBus.h>
 
-
 #include <AzCore/Memory/AllocationRecords.h>
 
 #include <AzCore/Memory/OverrunDetectionAllocator.h>
 #include <AzCore/Memory/AllocatorManager.h>
 #include <AzCore/Memory/MallocSchema.h>
+
 #include <AzCore/Metrics/EventLoggerFactoryImpl.h>
+#include <AzCore/Metrics/JsonTraceEventLogger.h>
+#include <AzCore/Metrics/EventLoggerUtils.h>
 
 #include <AzCore/NativeUI/NativeUIRequests.h>
 
@@ -71,31 +73,45 @@
 
 #include <AzCore/Module/Environment.h>
 #include <AzCore/std/string/conversions.h>
+#include <AzCore/std/utility/charconv.h>
 #include <AzCore/std/ranges/ranges_algorithm.h>
 #include <AzCore/Time/TimeSystem.h>
 
-static void PrintEntityName(const AZ::ConsoleCommandContainer& arguments)
+
+namespace AZ::Metrics
+{
+    const EventLoggerId CoreEventLoggerId{ static_cast<AZ::u32>(AZStd::hash<AZStd::string_view>{}("Core")) };
+    constexpr const char* CoreMetricsFilenameStem = "Metrics/core_metrics";
+    // Settings key used which indicates the rate in microseconds seconds to record core metrics in the Tick() function
+    constexpr AZStd::string_view CoreMetricsRecordRateMicrosecondsKey = "/O3DE/Metrics/Core/RecordRateMicroseconds";
+}
+
+namespace AZ
 {
-    if (arguments.empty())
+    static void PrintEntityName(const AZ::ConsoleCommandContainer& arguments)
     {
-        return;
-    }
+        if (arguments.empty())
+        {
+            return;
+        }
 
-    const auto entityIdStr = AZStd::string(arguments.front());
-    const auto entityIdValue = AZStd::stoull(entityIdStr);
+        AZStd::string_view entityIdStr(arguments.front());
+        AZ::u64 entityIdValue;
+        AZStd::from_chars(entityIdStr.begin(), entityIdStr.end(), entityIdValue);
 
-    AZStd::string entityName;
-    AZ::ComponentApplicationBus::BroadcastResult(
-        entityName, &AZ::ComponentApplicationBus::Events::GetEntityName, AZ::EntityId(entityIdValue));
+        AZStd::string entityName;
+        if (auto componentApplicationRequests = AZ::Interface<ComponentApplicationRequests>::Get();
+            componentApplicationRequests != nullptr)
+        {
+            entityName = componentApplicationRequests->GetEntityName(AZ::EntityId(entityIdValue));
+        }
 
-    AZ_Printf("Entity Debug", "EntityId: %" PRIu64 ", Entity Name: %s", entityIdValue, entityName.c_str());
-}
+        AZ_Printf("Entity Debug", "EntityId: %llu, Entity Name: %s", entityIdValue, entityName.c_str());
+    }
 
-AZ_CONSOLEFREEFUNC(
-    PrintEntityName, AZ::ConsoleFunctorFlags::Null, "Parameter: EntityId value, Prints the name of the entity to the console");
+    AZ_CONSOLEFREEFUNC(PrintEntityName, AZ::ConsoleFunctorFlags::Null,
+        "Parameter: EntityId value, Prints the name of the entity to the console");
 
-namespace AZ
-{
     static EnvironmentVariable<OverrunDetectionSchema> s_overrunDetectionSchema;
 
     static EnvironmentVariable<MallocSchema> s_mallocSchema;
@@ -419,17 +435,9 @@ namespace AZ
         CreateOSAllocator();
         CreateSystemAllocator();
 
-        // Create the EventLoggerFactory as soon as the Allocators are available
-        m_eventLoggerFactory = AZStd::make_unique<AZ::Metrics::EventLoggerFactoryImpl>();
-        if (AZ::Metrics::EventLoggerFactory::Get() == nullptr)
-        {
-            AZ::Metrics::EventLoggerFactory::Register(m_eventLoggerFactory.get());
-        }
-
         // Now that the Allocators are initialized, the Command Line parameters can be parsed
         m_commandLine.Parse(m_argC, m_argV);
 
-
         m_nameDictionary = AZStd::make_unique<NameDictionary>();
 
         // Register the Name Dictionary with the AZ Interface system
@@ -440,76 +448,16 @@ namespace AZ
             m_nameDictionary->LoadDeferredNames(AZ::Name::GetDeferredHead());
         }
 
-        SettingsRegistryMergeUtils::ParseCommandLine(m_commandLine);
-
-        // Create the settings registry and register it with the AZ interface system
-        // This is done after the AppRoot has been calculated so that the Bootstrap.cfg
-        // can be read to determine the Game folder and the asset platform
-        m_settingsRegistry = AZStd::make_unique<SettingsRegistryImpl>();
-
-        // Register the Settings Registry with the AZ Interface if there isn't one registered already
-        if (SettingsRegistry::Get() == nullptr)
-        {
-            SettingsRegistry::Register(m_settingsRegistry.get());
-        }
-
-        m_settingsRegistryOriginTracker = AZStd::make_unique<SettingsRegistryOriginTracker>(*m_settingsRegistry);
-
-        // Register the Settings Registry Origin Tracker with the AZ Interface system
-        if (AZ::Interface<AZ::SettingsRegistryOriginTracker>::Get() == nullptr)
-        {
-            AZ::Interface<AZ::SettingsRegistryOriginTracker>::Register(m_settingsRegistryOriginTracker.get());
-        }
-
-        // Add the Command Line arguments into the SettingsRegistry
-        SettingsRegistryMergeUtils::StoreCommandLineToRegistry(*m_settingsRegistry, m_commandLine);
-
-        // Add a notifier to update the project_settings when
-        // 1. The 'project_path' key changes
-        // 2. The project specialization when the 'project-name' key changes
-        // 3. The ComponentApplication command line when the command line is stored to the registry
-        m_projectPathChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectPathChangedEventHandler{
-            *m_settingsRegistry });
-        m_projectNameChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectNameChangedEventHandler{
-            *m_settingsRegistry });
-        m_commandLineUpdatedHandler = m_settingsRegistry->RegisterNotifier(UpdateCommandLineEventHandler{
-            *m_settingsRegistry, m_commandLine });
-
-        // Merge Command Line arguments
-        constexpr bool executeRegDumpCommands = false;
+        InitializeSettingsRegistry();
 
-#if defined(AZ_DEBUG_BUILD) || defined(AZ_PROFILE_BUILD)
-        // Skip over merging the User Registry in non-debug and profile configurations
-        SettingsRegistryMergeUtils::MergeSettingsToRegistry_O3deUserRegistry(*m_settingsRegistry, AZ_TRAIT_OS_PLATFORM_CODENAME, {});
-#endif
-        SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(*m_settingsRegistry, m_commandLine, executeRegDumpCommands);
-        SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*m_settingsRegistry);
+        InitializeEventLoggerFactory();
 
-        // The /O3DE/Application/LifecycleEvents array contains a valid set of lifecycle events
-        // Those lifecycle events are normally read from the <engine-root>/Registry
-        // which isn't merged until ComponentApplication::Create invokes MergeSettingsToRegistry
-        // So pre-populate the valid lifecycle even entries
-        ComponentApplicationLifecycle::RegisterEvent(*m_settingsRegistry, "SystemAllocatorCreated");
-        ComponentApplicationLifecycle::RegisterEvent(*m_settingsRegistry, "SettingsRegistryAvailable");
-        ComponentApplicationLifecycle::RegisterEvent(*m_settingsRegistry, "ConsoleAvailable");
-        ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "SystemAllocatorCreated", R"({})");
-        ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "SettingsRegistryAvailable", R"({})");
+        InitializeLifecyleEvents(*m_settingsRegistry);
 
         // Create the Module Manager
         m_moduleManager = AZStd::make_unique<ModuleManager>();
 
-        // Az Console initialization..
-        // note that tests destroy and construct the application over and over, which is not a desirable pattern
-        // so we allow the console to construct once and skip destruction / construction on consecutive runs
-        m_console = AZStd::make_unique<AZ::Console>(*m_settingsRegistry);
-        if (AZ::Interface<AZ::IConsole>::Get() == nullptr)
-        {
-            AZ::Interface<AZ::IConsole>::Register(m_console.get());
-            m_console->LinkDeferredFunctors(AZ::ConsoleFunctorBase::GetDeferredHead());
-            m_settingsRegistryConsoleFunctors = AZ::SettingsRegistryConsoleUtils::RegisterAzConsoleCommands(*m_settingsRegistry, *m_console);
-            m_settingsRegistryOriginTrackerConsoleFunctors = AZ::SettingsRegistryConsoleUtils::RegisterAzConsoleCommands(*m_settingsRegistryOriginTracker, *m_console);
-            ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "ConsoleAvailable", R"({})");
-        }
+        InitializeConsole(*m_settingsRegistry);
     }
 
     //=========================================================================
@@ -587,6 +535,170 @@ namespace AZ
         DestroyAllocator();
     }
 
+    void ComponentApplication::InitializeSettingsRegistry()
+    {
+        SettingsRegistryMergeUtils::ParseCommandLine(m_commandLine);
+
+        // Create the settings registry and register it with the AZ interface system
+        // This is done after the AppRoot has been calculated so that the Bootstrap.cfg
+        // can be read to determine the Game folder and the asset platform
+        m_settingsRegistry = AZStd::make_unique<SettingsRegistryImpl>();
+
+        // Register the Settings Registry with the AZ Interface if there isn't one registered already
+        if (SettingsRegistry::Get() == nullptr)
+        {
+            SettingsRegistry::Register(m_settingsRegistry.get());
+        }
+
+        m_settingsRegistryOriginTracker = AZStd::make_unique<SettingsRegistryOriginTracker>(*m_settingsRegistry);
+
+        // Register the Settings Registry Origin Tracker with the AZ Interface system
+        if (AZ::Interface<AZ::SettingsRegistryOriginTracker>::Get() == nullptr)
+        {
+            AZ::Interface<AZ::SettingsRegistryOriginTracker>::Register(m_settingsRegistryOriginTracker.get());
+        }
+
+        // Add the Command Line arguments into the SettingsRegistry
+        SettingsRegistryMergeUtils::StoreCommandLineToRegistry(*m_settingsRegistry, m_commandLine);
+
+        // Add a notifier to update the project_settings when
+        // 1. The 'project_path' key changes
+        // 2. The project specialization when the 'project-name' key changes
+        // 3. The ComponentApplication command line when the command line is stored to the registry
+        m_projectPathChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectPathChangedEventHandler{
+            *m_settingsRegistry });
+        m_projectNameChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectNameChangedEventHandler{
+            *m_settingsRegistry });
+        m_commandLineUpdatedHandler = m_settingsRegistry->RegisterNotifier(UpdateCommandLineEventHandler{
+            *m_settingsRegistry, m_commandLine });
+
+        // Merge Command Line arguments
+        constexpr bool executeRegDumpCommands = false;
+
+#if defined(AZ_DEBUG_BUILD) || defined(AZ_PROFILE_BUILD)
+        // Only merge the Global User Registry (~/.o3de/Registry) in debug and profile configurations
+        SettingsRegistryMergeUtils::MergeSettingsToRegistry_O3deUserRegistry(*m_settingsRegistry, AZ_TRAIT_OS_PLATFORM_CODENAME, {});
+#endif
+        SettingsRegistryMergeUtils::MergeSettingsToRegistry_CommandLine(*m_settingsRegistry, m_commandLine, executeRegDumpCommands);
+        SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*m_settingsRegistry);
+    }
+
+    void ComponentApplication::InitializeEventLoggerFactory()
+    {
+        // Create the EventLoggerFactory as soon as the Allocators are available
+        m_eventLoggerFactory = AZStd::make_unique<AZ::Metrics::EventLoggerFactoryImpl>();
+        if (AZ::Metrics::EventLoggerFactory::Get() == nullptr)
+        {
+            AZ::Metrics::EventLoggerFactory::Register(m_eventLoggerFactory.get());
+        }
+    }
+
+    void ComponentApplication::InitializeLifecyleEvents(AZ::SettingsRegistryInterface& settingsRegistry)
+    {
+        // The /O3DE/Application/LifecycleEvents array contains a valid set of lifecycle events
+        // Those lifecycle events are normally read from the <engine-root>/Registry
+        // which isn't merged until ComponentApplication::Create invokes MergeSettingsToRegistry
+        // So pre-populate the valid lifecycle even entries
+        ComponentApplicationLifecycle::RegisterEvent(settingsRegistry, "SystemAllocatorCreated");
+        ComponentApplicationLifecycle::RegisterEvent(settingsRegistry, "SettingsRegistryAvailable");
+        ComponentApplicationLifecycle::RegisterEvent(settingsRegistry, "ConsoleAvailable");
+        ComponentApplicationLifecycle::SignalEvent(settingsRegistry, "SystemAllocatorCreated", R"({})");
+        ComponentApplicationLifecycle::SignalEvent(settingsRegistry, "SettingsRegistryAvailable", R"({})");
+    }
+
+    void ComponentApplication::InitializeConsole(SettingsRegistryInterface& settingsRegistry)
+    {
+        // Az Console initialization.
+        // note that tests destroy and construct the application over and over, which is not a desirable pattern
+        m_console = AZStd::make_unique<AZ::Console>(settingsRegistry);
+        if (AZ::Interface<AZ::IConsole>::Get() == nullptr)
+        {
+            AZ::Interface<AZ::IConsole>::Register(m_console.get());
+            m_console->LinkDeferredFunctors(AZ::ConsoleFunctorBase::GetDeferredHead());
+            m_settingsRegistryConsoleFunctors = AZ::SettingsRegistryConsoleUtils::RegisterAzConsoleCommands(settingsRegistry, *m_console);
+            m_settingsRegistryOriginTrackerConsoleFunctors = AZ::SettingsRegistryConsoleUtils::RegisterAzConsoleCommands(*m_settingsRegistryOriginTracker, *m_console);
+            ComponentApplicationLifecycle::SignalEvent(settingsRegistry, "ConsoleAvailable", R"({})");
+        }
+    }
+
+    void ComponentApplication::RegisterCoreEventLogger()
+    {
+        // Register Core Event logger with Component Application
+
+        // Use the name of the running build target as part of the event logger name
+        // If it is not available, then an event logger will not be created
+        AZ::IO::FixedMaxPath uniqueFilenameSuffix = AZ::Metrics::CoreMetricsFilenameStem;
+        if (AZ::IO::FixedMaxPathString buildTargetName;
+            m_settingsRegistry->Get(buildTargetName, AZ::SettingsRegistryMergeUtils::BuildTargetNameKey))
+        {
+            // append the build target name as injected from CMake if known
+            uniqueFilenameSuffix.Native() += AZ::IO::FixedMaxPathString::format(".%s", buildTargetName.c_str());
+        }
+        else
+        {
+            return;
+        }
+
+        // Append the build configuration(debug, release, profile) to the metrics filename
+        if (AZStd::string_view buildConfig{ AZ_BUILD_CONFIGURATION_TYPE }; !buildConfig.empty())
+        {
+            uniqueFilenameSuffix.Native() += AZ::IO::FixedMaxPathString::format(".%.*s", AZ_STRING_ARG(buildConfig));
+        }
+
+        // Use the process ID to provide uniqueness to the metrics json files
+        AZStd::fixed_string<32> processIdString;
+        AZStd::to_string(processIdString, AZ::Platform::GetCurrentProcessId());
+        uniqueFilenameSuffix.Native() += AZ::IO::FixedMaxPathString::format(".%s", processIdString.c_str());
+        // Append .json extension
+        uniqueFilenameSuffix.Native() += ".json";
+
+        // Append the relative file name portion to the <project-root>/user directory
+        auto metricsFilePath = (AZ::IO::FixedMaxPath{ AZ::Utils::GetProjectUserPath(m_settingsRegistry.get()) }
+            / uniqueFilenameSuffix).LexicallyNormal();
+
+        // Open up the metrics file in write mode and truncate the contents if it exist(it shouldn't since a millisecond
+        // is being used
+        constexpr AZ::IO::OpenMode openMode = AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath;
+        if (auto fileStream = AZStd::make_unique<AZ::IO::SystemFileStream>(metricsFilePath.c_str(), openMode);
+            fileStream != nullptr && fileStream->IsOpen())
+        {
+            // Configure core event logger with the name of "Core"
+            AZ::Metrics::JsonTraceLoggerEventConfig config{ "Core" };
+            auto coreEventLogger = AZStd::make_unique<AZ::Metrics::JsonTraceEventLogger>(AZStd::move(fileStream), config);
+            m_eventLoggerFactory->RegisterEventLogger(AZ::Metrics::CoreEventLoggerId, AZStd::move(coreEventLogger));
+        }
+        else
+        {
+            AZ_Error("ComponentApplication", false, R"(unable to open core metrics with with path "%s")",
+                metricsFilePath.c_str());
+        }
+
+        // Record metrics every X microseconds based on the /O3DE/Metrics/Core/RecordRateMicroseconds setting
+        // or every 10 secondsif not supplied
+        m_recordMetricsOnTickCallback = [&registry = *m_settingsRegistry.get(),
+            lastRecordTime = AZStd::chrono::steady_clock::now()]
+            (AZStd::chrono::steady_clock::time_point monotonicTime) mutable -> bool
+        {
+            // Retrieve the record rate setting from the Setting Registry
+            using namespace AZStd::chrono_literals;
+            AZStd::chrono::microseconds recordTickMicroseconds = 10s;
+            if (AZ::s64 recordRateMicrosecondValue;
+                registry.Get(recordRateMicrosecondValue, AZ::Metrics::CoreMetricsRecordRateMicrosecondsKey))
+            {
+                recordTickMicroseconds = AZStd::chrono::microseconds(recordRateMicrosecondValue);
+            }
+
+            if ((monotonicTime - lastRecordTime) >= recordTickMicroseconds)
+            {
+                // Reset the recordTime to the current steady clock time and return true to record the metrics
+                lastRecordTime = monotonicTime;
+                return true;
+            }
+
+            return false;
+        };
+    }
+
     void ReportBadEngineRoot()
     {
         AZStd::string errorMessage = {"Unable to determine a valid path to the engine.\n"
@@ -642,6 +754,9 @@ namespace AZ
 
         MergeSettingsToRegistry(*m_settingsRegistry);
 
+        // Register the Core metrics Event logger with the IEventLoggerFactory
+        RegisterCoreEventLogger();
+
         m_systemEntity = AZStd::make_unique<AZ::Entity>(SystemEntityId, "SystemEntity");
         CreateCommon();
         AZ_Assert(m_systemEntity, "SystemEntity failed to initialize!");
@@ -1353,6 +1468,33 @@ namespace AZ
     {
         AZ_PROFILE_SCOPE(System, "Component application simulation tick");
 
+        // Only record when the record metrics on tick callback is set
+        if (m_recordMetricsOnTickCallback)
+        {
+            auto currentMonotonicTime = AZStd::chrono::steady_clock::now();
+
+            if (m_recordMetricsOnTickCallback(currentMonotonicTime))
+            {
+                AZ::Metrics::EventObjectStorage argsContainer;
+                argsContainer.emplace_back("frameTimeMicroseconds", static_cast<AZ::u64>(
+                    AZStd::chrono::duration_cast<AZStd::chrono::microseconds>(currentMonotonicTime - m_lastTickTime).count()));
+                AZ::Metrics::AsyncArgs asyncArgs;
+                asyncArgs.m_name = "FrameTime";
+                asyncArgs.m_cat = "Core";
+                asyncArgs.m_args = argsContainer;
+                asyncArgs.m_id = "Simulation";
+                asyncArgs.m_scope = "Engine";
+
+                [[maybe_unused]] auto metricsOutcome = AZ::Metrics::RecordAsyncEventInstant(AZ::Metrics::CoreEventLoggerId, asyncArgs, m_eventLoggerFactory.get());
+
+                AZ_ErrorOnce("ComponentApplication", metricsOutcome.IsSuccess(),
+                    "Failed to record frame time metrics. Error %s", metricsOutcome.GetError().c_str());
+            }
+
+            // Update the m_lastTickTime to the current monotonic time
+            m_lastTickTime = currentMonotonicTime;
+        }
+
         {
             AZ_PROFILE_SCOPE(AzCore, "ComponentApplication::Tick:ExecuteQueuedEvents");
             TickBus::ExecuteQueuedEvents();

+ 21 - 1
Code/Framework/AzCore/AzCore/Component/ComponentApplication.h

@@ -17,7 +17,6 @@
 #include <AzCore/Memory/OSAllocator.h>
 #include <AzCore/Module/DynamicModuleHandle.h>
 #include <AzCore/Module/ModuleManager.h>
-#include <AzCore/Outcome/Outcome.h>
 #include <AzCore/IO/Path/Path.h>
 #include <AzCore/IO/SystemFile.h>
 #include <AzCore/Serialization/SerializeContext.h>
@@ -43,6 +42,10 @@ namespace AZ
 namespace AZ::Metrics
 {
     class IEventLoggerFactory;
+
+    enum class EventLoggerId : AZ::u32;
+
+    extern const EventLoggerId CoreEventLoggerId;
 }
 
 namespace AZ
@@ -300,6 +303,13 @@ namespace AZ
         void LoadDynamicModules();
 
     protected:
+        void InitializeSettingsRegistry();
+        void InitializeEventLoggerFactory();
+        void InitializeLifecyleEvents(SettingsRegistryInterface& settingsRegistry);
+        void InitializeConsole(SettingsRegistryInterface& settingsRegistry);
+
+        void RegisterCoreEventLogger();
+
         virtual void CreateReflectionManager();
         void DestroyReflectionManager();
 
@@ -404,5 +414,15 @@ namespace AZ
         AZStd::unique_ptr<AZ::Entity>               m_systemEntity; ///< Track the system entity to ensure we free it on shutdown.
 
         AZStd::unique_ptr<AZ::Metrics::IEventLoggerFactory> m_eventLoggerFactory;
+
+        using TickTimepoint = AZStd::chrono::steady_clock::time_point;
+        TickTimepoint m_lastTickTime{};
+
+        //! Callback function for determining whether a call to record metrics in the Tick() member function
+        //! functionshould take place
+        //! @param currentMonotonicTime - The monotonic tick time of the application since launch
+        //! @return true to indicate a record event operation should occur in the current Tick() call
+        using RecordMetricsCallback = AZStd::function<bool(AZStd::chrono::steady_clock::time_point)>;
+        RecordMetricsCallback m_recordMetricsOnTickCallback;
     };
 }

+ 12 - 11
Code/Framework/AzCore/AzCore/Debug/PerformanceCollector.h

@@ -7,6 +7,7 @@
  */
 #pragma once
 
+#include <AzCore/IO/Path/Path.h>
 #include <AzCore/Metrics/JsonTraceEventLogger.h>
 #include <AzCore/Statistics/StatisticsManager.h>
 #include <AzCore/std/chrono/chrono.h>
@@ -47,7 +48,7 @@ namespace AZ::Debug
         ~PerformanceCollector() = default;
 
         static constexpr char LogName[] = "PerformanceCollector";
-    
+
         enum class DataLogType
         {
             LogStatistics, //! Aggregates each sampled data using the StatiscalProfiler API.
@@ -55,15 +56,15 @@ namespace AZ::Debug
                            //! the IEventLogger API.
             LogAllSamples, //! Each sample becomes a unique record in the output file using the IEventLogger API.
         };
-    
+
         //! Returns true if the user has disabled performance capture or
         //! the performance collector is waiting for certain amount of time
         //! before starting to measure performance.
         bool IsWaitingBeforeCapture();
-    
+
         //! Records a measured value according to the current CaptureType.
         void RecordSample(AZStd::string_view metricName, AZStd::chrono::microseconds microSeconds);
-    
+
         //! This is similar to RecordSample(). Captures the elapsed time between
         //! two consecutive calls to this function for any given @metricName.
         //! The time delta is recorded according to the current CaptureType.
@@ -96,7 +97,7 @@ namespace AZ::Debug
         const AZ::IO::Path& GetOutputFilePath() { return m_outputFilePath;  }
 
         const AZStd::string& GetOutputDataBuffer() { return m_outputDataBuffer; }
-    
+
     private:
         //! A helper function that loops across all statistics in @m_statisticsManager
         //! and reports each result into @m_eventLogger.
@@ -117,10 +118,10 @@ namespace AZ::Debug
         AZStd::chrono::steady_clock::time_point m_startWaitTime;
         bool m_isWaitingBeforeNextBatch = true;
         OnBatchCompleteCallback m_onBatchCompleteCallback; // A notification will be sent each time a batch of frames is performance collected.
-   
+
         //! Only used when @m_captureType == CaptureType::LogStatistics.
         AZ::Statistics::StatisticsManager<AZStd::string> m_statisticsManager;
-    
+
         //! Only used to store the previous value when RecordPeriodicEvent() is called
         //! for any given metrics.
         AZStd::unordered_map<AZStd::string, AZStd::chrono::steady_clock::time_point> m_periodicEventStamps;
@@ -132,14 +133,14 @@ namespace AZ::Debug
         AZ::IO::Path m_outputFilePath; // We store here the file path of the most recently created output file.
         Metrics::JsonTraceEventLogger m_eventLogger;
     }; // class PerformanceCollector
-    
+
     //! A Convenience class used to measure time performance of scopes of code
     //! with constructor/destructor.
     class ScopeDuration
     {
     public:
         ScopeDuration() = delete;
-    
+
         ScopeDuration(PerformanceCollector* performanceCollector, const AZStd::string_view metricName)
             : m_performanceCollector(performanceCollector)
             , m_metricName(metricName)
@@ -155,7 +156,7 @@ namespace AZ::Debug
                 m_startTime = AZStd::chrono::steady_clock::now();
             }
         }
-    
+
         ~ScopeDuration()
         {
             if (!m_performanceCollector || !m_pushSample)
@@ -167,7 +168,7 @@ namespace AZ::Debug
             auto duration = AZStd::chrono::duration_cast<AZStd::chrono::microseconds>(stopTime - m_startTime);
             m_performanceCollector->RecordSample(m_metricName, duration);
         }
-    
+
     private:
         bool m_pushSample;
         PerformanceCollector* m_performanceCollector;

+ 63 - 0
Code/Framework/AzCore/AzCore/Metrics/IEventLogger.h

@@ -15,6 +15,7 @@
 #include <AzCore/std/chrono/chrono.h>
 #include <AzCore/std/containers/span.h>
 #include <AzCore/std/containers/variant.h>
+#include <AzCore/std/string/fixed_string.h>
 #include <AzCore/std/string/string_view.h>
 #include <AzCore/std/utils.h>
 
@@ -23,8 +24,56 @@ namespace AZ::IO
     class GenericStream;
 }
 
+namespace AZ::Metrics::Internal
+{
+    //! Wraps the Metrics settings prefix key of "/O3DE/Metrics"
+    //! which is used as the parent object of any "/O3DE/Metrics/<EventLoggerName>" keys
+    //! The "/O3DE/Metrics/<EventLoggerName>" is the anchor object where settings
+    //! for an Event Logger are queried.
+    //! Currently  supports the following settings
+    //!
+    //! * "/O3DE/Metrics/<EventLoggerName>/Active" - If set to false, the event logger will not record new events
+    //!   If not set or true, the event logger will record events
+    struct SettingsKey_t
+    {
+        using StringType = AZStd::fixed_string<128>;
+
+        constexpr StringType operator()(AZStd::string_view name) const
+        {
+            constexpr size_t MaxTotalKeySize = StringType{}.max_size();
+            // The +1 is for the '/' separator
+            [[maybe_unused]] const size_t maxNameSize = MaxTotalKeySize - (MetricsSettingsPrefix.size() + 1);
+
+            AZ_Assert(name.size() <= maxNameSize,
+                R"(The size of the event logger name "%.*s" is too long. It must be <= %zu characters)",
+                AZ_STRING_ARG(name), maxNameSize);
+            StringType settingsKey(MetricsSettingsPrefix);
+            settingsKey += '/';
+            settingsKey += name;
+
+            return settingsKey;
+        }
+
+        constexpr operator AZStd::string_view() const
+        {
+            return MetricsSettingsPrefix;
+        }
+
+    private:
+        AZStd::string_view MetricsSettingsPrefix = "/O3DE/Metrics";
+    };
+}
+
 namespace AZ::Metrics
 {
+    //! Settings key object which supports a call operator
+    //! which accepts the name of an event logger and returns
+    //! a fixed_string acting as an anchor object for all settings
+    //! associated with an event logger using that name
+    constexpr Internal::SettingsKey_t SettingsKey{};
+
+    //! Represents the "args" field where key, value entries
+    //! within the per event data is stored
     constexpr AZStd::string_view ArgsKey = "args";
 
     //! Event fields structure that can be used to record event argument data
@@ -112,6 +161,10 @@ namespace AZ::Metrics
         EventValue m_value;
     };
 
+    // Helper Type aliases that can be used to provide storage for JSON array and JSON object types inside of a function
+    // The fixed_vector capacity can be upped if more storage is needed
+    using EventArrayStorage = AZStd::fixed_vector<AZ::Metrics::EventValue, 32>;
+    using EventObjectStorage = AZStd::fixed_vector<AZ::Metrics::EventField, 32>;
 
     enum class EventPhase : char
     {
@@ -296,6 +349,16 @@ namespace AZ::Metrics
         AZ_RTTI(IEventLogger, "{D39D09FA-DEA0-4874-BC45-4B310C3DD52E}");
         virtual ~IEventLogger() = default;
 
+        //! Provides a qualified name for the Event Logger
+        //! This is used to as part of the for the "/O3DE/Metrics/<Name>"
+        //! to contain settings associated with any event logger with the name
+        virtual void SetName([[maybe_unused]] AZStd::string_view name) {}
+
+        //! Returns the qualified name for the Event Logger
+        //! This can be used to query settings from event loggers with the name
+        //! through queury the "/O3DE/Metrics/<Name>" object
+        virtual AZStd::string_view GetName() const { return {}; }
+
         //! Provides a hook for implemented Event Loggers to flush recorded metrics
         //! to an associated stream(disk stream, network stream, etc...)
         virtual void Flush() = 0;

+ 116 - 3
Code/Framework/AzCore/AzCore/Metrics/JsonTraceEventLogger.cpp

@@ -14,6 +14,7 @@
 #include <AzCore/Metrics/JsonTraceEventLogger.h>
 #include <AzCore/std/parallel/scoped_lock.h>
 #include <AzCore/std/ranges/repeat_view.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
 
 namespace AZ::Metrics
 {
@@ -146,13 +147,45 @@ namespace AZ::Metrics
         JsonTraceLoggerEventConfig config)
         : m_stream(AZStd::move(stream))
         , m_name(AZStd::move(config.m_loggerName))
+        , m_settingsRegistry{ config.m_settingsRegistry }
     {
+        ResetSettingsHandler();
         if (m_stream != nullptr)
         {
             Start(*m_stream);
         }
     }
 
+
+    // Static function which is used to initialize the active state of this JsonTraceEventLogger
+    // based on the build configuration
+    bool JsonTraceEventLogger::GetDefaultActiveState()
+    {
+#if !defined(AZ_RELEASE_BUILD)
+        return true;
+#else
+        return false;
+#endif
+    }
+
+    void JsonTraceEventLogger::SetName(AZStd::string_view name)
+    {
+        // Detect if the name has changed and reset
+        // the active state and the settings handler if so
+        const bool nameChanged = m_name != name;
+        m_name = name;
+        if (nameChanged)
+        {
+            // Reset the settings handler if the name has changed
+            ResetSettingsHandler();
+        }
+    }
+
+    AZStd::string_view JsonTraceEventLogger::GetName() const
+    {
+        return m_name;
+    }
+
     void JsonTraceEventLogger::Flush()
     {
         // No-op - The data is written directly to the stream
@@ -160,6 +193,12 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordDurationEventBegin(const DurationArgs& durationArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            // Event logger isn't active, return success
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The duration begin event cannot be recorded"));
@@ -189,6 +228,12 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordDurationEventEnd(const DurationArgs& durationArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            // Event logger isn't active, return success
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The duration end event cannot be recorded"));
@@ -218,6 +263,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordCompleteEvent(const CompleteArgs& completeArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The complete event cannot be recorded"));
@@ -260,6 +310,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordInstantEvent(const InstantArgs& instantArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The instant event cannot be recorded"));
@@ -284,7 +339,7 @@ namespace AZ::Metrics
         constexpr size_t MaxExtraFieldCount = 8;
         AZStd::fixed_vector<EventField, MaxExtraFieldCount> extraParams;
         char scopeChar = static_cast<char>(instantArgs.m_scope);
-        extraParams.emplace_back(ScopeKey, EventValue{ AZStd::in_place_type<AZStd::string_view>, &scopeChar });
+        extraParams.emplace_back(ScopeKey, EventValue{ AZStd::in_place_type<AZStd::string_view>, &scopeChar, 1 });
 
         eventDesc.SetExtraParams(extraParams);
 
@@ -298,6 +353,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordCounterEvent(const CounterArgs& counterArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The counter event cannot be recorded"));
@@ -327,6 +387,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordAsyncEventStart(const AsyncArgs& asyncArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The async start event cannot be recorded"));
@@ -367,6 +432,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordAsyncEventInstant(const AsyncArgs& asyncArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The async instant event cannot be recorded"));
@@ -407,6 +477,11 @@ namespace AZ::Metrics
 
     auto JsonTraceEventLogger::RecordAsyncEventEnd(const AsyncArgs& asyncArgs) -> ResultOutcome
     {
+        if (!m_active)
+        {
+            return AZ::Success();
+        }
+
         if (m_stream == nullptr)
         {
             return AZ::Failure(ErrorString("Logger has no output stream associated. The async end event cannot be recorded"));
@@ -540,6 +615,11 @@ namespace AZ::Metrics
         return result;
     }
 
+    bool JsonTraceEventLogger::Start(AZ::IO::GenericStream& stream)
+    {
+        return stream.Write(ArrayStart.size(), ArrayStart.data()) == ArrayStart.size();
+    }
+
     bool JsonTraceEventLogger::Complete(AZ::IO::GenericStream& stream)
     {
         constexpr auto jsonCompleteString = []()
@@ -552,8 +632,41 @@ namespace AZ::Metrics
         return stream.Write(jsonCompleteString.size(), jsonCompleteString.data()) == jsonCompleteString.size();
     }
 
-    bool JsonTraceEventLogger::Start(AZ::IO::GenericStream& stream)
+    void JsonTraceEventLogger::ResetSettingsHandler()
     {
-        return stream.Write(ArrayStart.size(), ArrayStart.data()) == ArrayStart.size();
+        // Reset the active option back to default active state based on the build configuration
+        // and then query it from the Settings Registry again
+        m_active = GetDefaultActiveState();
+
+        if (auto settingsRegistry = m_settingsRegistry != nullptr ? m_settingsRegistry : AZ::SettingsRegistry::Get();
+            settingsRegistry != nullptr)
+        {
+            // Read the "/O3DE/Metrics/<Name>/Active" setting from the Settings Registry
+            const AZStd::fixed_string<128> eventLoggerActiveSettingKey(SettingsKey(m_name + "/Active"));
+            settingsRegistry->Get(m_active, eventLoggerActiveSettingKey);
+
+            auto ActiveStateUpdateFunc = [this](const AZ::SettingsRegistryInterface::NotifyEventArgs& notifyArgs)
+            {
+                const AZStd::fixed_string<128> activeSettingKey(SettingsKey(m_name + "/Active"));
+                if (AZ::SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(notifyArgs.m_jsonKeyPath, activeSettingKey))
+                {
+                    if (auto settingsRegistry = m_settingsRegistry != nullptr ? m_settingsRegistry : AZ::SettingsRegistry::Get();
+                        settingsRegistry != nullptr)
+                    {
+                        // If the key has been deleted, then reset the active state to the default active state
+                        if (settingsRegistry->GetType(activeSettingKey).m_type == AZ::SettingsRegistryInterface::Type::NoType)
+                        {
+                            m_active = GetDefaultActiveState();
+                        }
+                        else
+                        {
+                            settingsRegistry->Get(m_active, activeSettingKey);
+                        }
+                    }
+                }
+            };
+            m_settingsHandler = settingsRegistry->RegisterNotifier(ActiveStateUpdateFunc);
+        }
     }
+
 } // namespace AZ::Metrics

+ 41 - 5
Code/Framework/AzCore/AzCore/Metrics/JsonTraceEventLogger.h

@@ -10,9 +10,7 @@
 
 #include <AzCore/Metrics/IEventLogger.h>
 
-#include <AzCore/IO/Path/Path.h>
-#include <AzCore/std/containers/fixed_vector.h>
-#include <AzCore/std/containers/ring_buffer.h>
+#include <AzCore/Settings/SettingsRegistry.h>
 #include <AzCore/std/functional.h>
 #include <AzCore/std/parallel/mutex.h>
 #include <AzCore/std/smart_ptr/unique_ptr.h>
@@ -27,7 +25,13 @@ namespace AZ::Metrics
     // Contains JsonTraceEventLogger specific configuration
     struct JsonTraceLoggerEventConfig
     {
+        //! Name of the JsonTraceEventLogger
         AZStd::string_view m_loggerName;
+        //! Settings Registry reference used to query
+        //! to register an EventHandler for the JsonTraceEventLogger
+        //! to get updates on setting modifications below the "/O3DE/Metrics/<LoggerName>" key
+        //! If nullptr, a handler is installed on the global settings registry
+        AZ::SettingsRegistryInterface* m_settingsRegistry{};
     };
 
     class JsonTraceEventLogger
@@ -42,6 +46,12 @@ namespace AZ::Metrics
 
         ~JsonTraceEventLogger();
 
+        //! Set the name associated of this event logger
+        void SetName(AZStd::string_view) override;
+
+        //! Returns the name associated with this event logger
+        AZStd::string_view GetName() const override;
+
         //! Writes and json data to the stream
         void Flush() override;
 
@@ -81,11 +91,23 @@ namespace AZ::Metrics
         //! Responsible for writing the recorded event data to JSON
         bool FlushRequest(const EventDesc&);
 
+        //! Start the JSON document by adding the opening '[' bracket
+        bool Start(AZ::IO::GenericStream& stream);
+
         //! Complete the JSON document by adding the ending ']' bracket
         bool Complete(AZ::IO::GenericStream& stream);
 
-        //! Start the JSON document by adding the opening '[' bracket
-        bool Start(AZ::IO::GenericStream& stream);
+        //! Reads the event logger "/O3DE/Metrics/<Name>/Active" setting from the Settings Registry
+        //! and resets a handler to listen for changes to any setting below "/O3DE/Metrics/<Name>" key
+        void ResetSettingsHandler();
+
+    private:
+        //! Sets the default value for the m_active member, which determines if the event logger
+        //! should record events to the stream member
+        //! In non-release configurations, the event logger defaults to active.
+        //! In release configurations, the event logger defaults to inactive
+        //! This can be overrided through the settings registry
+        static bool GetDefaultActiveState();
 
     protected:
         AZStd::mutex m_flushToStreamMutex;
@@ -93,10 +115,24 @@ namespace AZ::Metrics
 
         //! Provides a user friendly name for the event logger
         AZStd::string m_name;
+
+        //! Active flag to to allow the record functions to write event data to the stream member
+        //! The default is true
+        //! When the name of the event logger is set, the value is updated from the settings registry
+        //! "/O3DE/Metrics/<Name>/Active" bool
+        bool m_active{ GetDefaultActiveState() };
+
+        //! Stores a pointer to the SettingsRegistry used to query settings associated with
+        //! this event logger instance
+        //! If nullptr, the global SettingsRegistry is queried
+        AZ::SettingsRegistryInterface* m_settingsRegistry{};
+        AZ::SettingsRegistryInterface::NotifyEventHandler m_settingsHandler;
+
         //! Keep track of whether this is the first event being logged
         //! This is used to prepend a leading comma before the event entry in the trace events array
         AZStd::atomic_bool m_prependComma{ false };
 
+
         //! Tracks the number of events written so far
         size_t m_eventCount{};
     };

+ 13 - 4
Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.cpp

@@ -564,7 +564,7 @@ namespace AZ
         if (pointer.IsValid())
         {
             rapidjson::Value store;
-            JsonSerializationResult::ResultCode jsonResult = JsonSerialization::Store(store, m_settings.GetAllocator(), 
+            JsonSerializationResult::ResultCode jsonResult = JsonSerialization::Store(store, m_settings.GetAllocator(),
                 value, nullptr, valueTypeID, m_serializationSettings);
             if (jsonResult.GetProcessing() != JsonSerializationResult::Processing::Halted)
             {
@@ -597,8 +597,17 @@ namespace AZ
             return false;
         }
 
-        AZStd::scoped_lock lock(LockForWriting());
-        return pointerPath.Erase(m_settings);
+        bool removeSuccess;
+        {
+            AZStd::scoped_lock lock(LockForWriting());
+            removeSuccess = pointerPath.Erase(m_settings);
+        }
+
+        // The removal type is Type::NoType
+        constexpr SettingsType removeType;
+        SignalNotifier(path, removeType);
+
+        return removeSuccess;
     }
 
     bool SettingsRegistryImpl::MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view rootKey,
@@ -1162,7 +1171,7 @@ namespace AZ
         const Specializations& specializations, const rapidjson::Pointer& historyPointer, AZStd::string_view folderPath)
     {
         using namespace rapidjson;
-        
+
         if (&lhs == &rhs)
         {
             // Early return to avoid setting the collisionFound reference to true

+ 231 - 12
Code/Framework/AzCore/Tests/Metrics/JsonTraceEventLoggerTests.cpp

@@ -11,10 +11,9 @@
 #include <AzCore/IO/Path/Path.h>
 #include <AzCore/JSON/document.h>
 #include <AzCore/JSON/error/en.h>
+#include <AzCore/Settings/SettingsRegistryImpl.h>
 #include <AzCore/std/string/conversions.h>
 #include <AzCore/std/smart_ptr/unique_ptr.h>
-#include <AzCore/std/ranges/ranges_algorithm.h>
-#include <AzCore/std/ranges/filter_view.h>
 #include <AzCore/std/ranges/zip_view.h>
 #include <AzCore/UnitTest/TestTypes.h>
 
@@ -31,12 +30,34 @@ namespace UnitTest
         {
             return !::isspace(element);
         };
-        return AZStd::ranges::contains_subrange(lhs | AZStd::views::filter(TrimWhitespace),
-            rhs | AZStd::views::filter(TrimWhitespace));
+        AZStd::string sourceString;
+        for (char elem : lhs)
+        {
+            if (TrimWhitespace(elem))
+            {
+                sourceString += elem;
+            }
+        }
+
+        AZStd::string containsString;
+        for (char elem : rhs)
+        {
+            if (TrimWhitespace(elem))
+            {
+                containsString += elem;
+            }
+        }
+
+        return sourceString.contains(containsString);
     }
 
     size_t JsonStringCountAll(AZStd::string_view sourceView, AZStd::string_view containsView)
     {
+        if (containsView.empty())
+        {
+            return 0;
+        }
+
         auto TrimWhitespace = [](char element) -> bool
         {
             return !::isspace(element);
@@ -44,19 +65,37 @@ namespace UnitTest
 
         // Need persistent storage in this case to count all occurrences of JSON string inside of source string
         // Remove all whitespace from both strings
-        AZStd::string sourceString(AZStd::from_range, sourceView | AZStd::views::filter(TrimWhitespace));
-        AZStd::string containsString(AZStd::from_range, containsView | AZStd::views::filter(TrimWhitespace));
+
+        AZStd::string sourceString;
+        for (char elem : sourceView)
+        {
+            if (TrimWhitespace(elem))
+            {
+                sourceString += elem;
+            }
+        }
+
+        AZStd::string containsString;
+        for (char elem : containsView)
+        {
+            if (TrimWhitespace(elem))
+            {
+                containsString += elem;
+            }
+        }
 
         size_t count{};
-        AZStd::ranges::borrowed_subrange_t<decltype(sourceString)&> remainingRange(sourceString);
+        AZStd::string_view remainingRange(sourceString);
         for (;;)
         {
-            auto foundRange = AZStd::ranges::search(remainingRange, containsString);
-            if (foundRange.empty())
+            auto foundFirst = remainingRange.find(containsString);
+            if (foundFirst == AZStd::string_view::npos)
             {
                 break;
             }
 
+            AZStd::string_view foundRange = remainingRange.substr(foundFirst, containsString.size());
+
             // If the contains string has been found reduce the
             // remaining search range to be the end of the found string until the end of the source string
             remainingRange = { foundRange.end(), sourceString.end() };
@@ -300,10 +339,190 @@ namespace UnitTest
         rapidjson::ParseResult parseResult = validateDoc.Parse(metricsOutput.c_str());
         EXPECT_TRUE(parseResult) << R"(JSON parse error ")" << rapidjson::GetParseError_En(parseResult.Code())
             << R"(" at offset (%u))";
+    }
+
+
+    TEST_F(JsonTraceEventLoggerTest, TogglingActiveSetting_CanTurnOrOffEventRecording_Succeeds)
+    {
+        // local Settings Registry used for validating changes to the event logger active state
+        AZ::SettingsRegistryImpl settingsRegistry;
+
+        // Event Logger name used to read settings and set settings associated with the Event Logger
+        constexpr AZStd::string_view EventLoggerName = "JsonTraceEventTest";
+
+        AZStd::string metricsOutput;
+        auto metricsStream = AZStd::make_unique<AZ::IO::ByteContainerStream<AZStd::string>>(&metricsOutput);
+        {
+            AZ::Metrics::JsonTraceEventLogger googleTraceLogger(AZStd::move(metricsStream), { EventLoggerName, &settingsRegistry });
+
+            // Event logger is be active by default if there is no `/O3DE/Metrics/<Name>/Active` in the settings registry
+            AZStd::string eventMessage = "Hello world";
+            AZ::Metrics::EventObjectStorage argContainer{ {"Field1", eventMessage} };
+
+            AZ::Metrics::CompleteArgs completeArgs;
+
+            completeArgs.m_name = "StringEvent";
+            completeArgs.m_cat = "Test";
+            completeArgs.m_args = argContainer;
+            using ResultOutcome = AZ::Metrics::IEventLogger::ResultOutcome;
+            ResultOutcome resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // As the event logger is active, the event data should have been recorded to the stream
+            EXPECT_TRUE(JsonStringContains(metricsOutput, R"("Field1":"Hello world")"));
+
+            // Set any event loggers with a name that matches EventLoggerName to inactive
+            settingsRegistry.Set(AZ::Metrics::SettingsKey(EventLoggerName) + "/Active", false);
+
+            // Update the event string for the complete event and attempt to record it again
+            eventMessage = "Unrecorded world";
+            argContainer[0].m_value = eventMessage;
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // No recording of the complete event to the stream should have occured
+            EXPECT_FALSE(JsonStringContains(metricsOutput, R"("Field1":"Unrecorded world")"));
+
+            // Set the active state for event loggers with the name matching the EventLoggerName constant
+            settingsRegistry.Set(AZ::Metrics::SettingsKey(EventLoggerName) + "/Active", true);
+
+            // Change the event string to 3rd different string and attempt to record again
+            eventMessage = "Re-recorded world";
+            argContainer[0].m_value = eventMessage;
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // The recording of the complete event should now have occured
+            EXPECT_TRUE(JsonStringContains(metricsOutput, R"("Field1":"Re-recorded world")"));
+        }
+    }
+
+    TEST_F(JsonTraceEventLoggerTest, InitialActiveSetting_SetToFalse_DoesNotRecord_Succeeds)
+    {
+        // Event Logger name used to read settings and set settings associated with the Event Logger
+        constexpr AZStd::string_view EventLoggerName = "JsonTraceEventTest";
+
+        // local Settings Registry used for validating changes to the event logger active state
+        AZ::SettingsRegistryImpl settingsRegistry;
+
+        // Default to the inactive state for event loggers matching the name of the EventLoggerName constant
+        settingsRegistry.Set(AZ::Metrics::SettingsKey(EventLoggerName) + "/Active", false);
+
+
+        AZStd::string metricsOutput;
+        auto metricsStream = AZStd::make_unique<AZ::IO::ByteContainerStream<AZStd::string>>(&metricsOutput);
+        {
+            AZ::Metrics::JsonTraceEventLogger googleTraceLogger(AZStd::move(metricsStream), { EventLoggerName, &settingsRegistry });
+
+            AZStd::string eventMessage = "Hello world";
+            AZ::Metrics::EventObjectStorage argContainer{ {"Field1", eventMessage} };
+
+            AZ::Metrics::CompleteArgs completeArgs;
+
+            completeArgs.m_name = "StringEvent";
+            completeArgs.m_cat = "Test";
+            completeArgs.m_args = argContainer;
+            using ResultOutcome = AZ::Metrics::IEventLogger::ResultOutcome;
+            ResultOutcome resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // The event logger should be inactive, so recording to the output stream should not occur
+            EXPECT_FALSE(JsonStringContains(metricsOutput, R"("Field1":"Hello world")"));
+
+            // Set the active state for event loggers with the name matching the EventLoggerName constant
+            settingsRegistry.Set(AZ::Metrics::SettingsKey(EventLoggerName) + "/Active", true);
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // The recording to the event logger stream should have succeeded this time
+            EXPECT_TRUE(JsonStringContains(metricsOutput, R"("Field1":"Hello world")"));
+        }
+    }
+    TEST_F(JsonTraceEventLoggerTest, ChangingEventLoggerName_ResetsRecordingState_Succeeds)
+    {
+        // Event Logger name used to read settings and set settings associated with the Event Logger
+        constexpr AZStd::string_view EventLoggerName = "JsonTraceEventTest";
+        // name to use to reading a different section of the "/O3DE/Metrics/<Name>" setting
+        constexpr AZStd::string_view NewLoggerName = "NewName";
+
+        // local Settings Registry used for validating changes to the event logger active state
+        AZ::SettingsRegistryImpl settingsRegistry;
+
+        // Default to the inactive state
+        settingsRegistry.Set(AZ::Metrics::SettingsKey(EventLoggerName) + "/Active", false);
+
 
-        // Log the metrics output to stdout so it appears for visualizing purposes
-        metricsOutput += '\n';
-        AZ::Debug::Trace::Instance().RawOutput("metrics", metricsOutput.c_str());
+        AZStd::string metricsOutput;
+        auto metricsStream = AZStd::make_unique<AZ::IO::ByteContainerStream<AZStd::string>>(&metricsOutput);
+        {
+            AZ::Metrics::JsonTraceEventLogger googleTraceLogger(AZStd::move(metricsStream), { EventLoggerName, &settingsRegistry });
+
+            AZStd::string eventMessage = "Hello world";
+            AZ::Metrics::EventObjectStorage argContainer{ {"Field1", eventMessage} };
+
+            AZ::Metrics::CompleteArgs completeArgs;
+
+            completeArgs.m_name = "StringEvent";
+            completeArgs.m_cat = "Test";
+            completeArgs.m_args = argContainer;
+            using ResultOutcome = AZ::Metrics::IEventLogger::ResultOutcome;
+            ResultOutcome resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // The "JsonTraceEventTest" event logger should be inactive
+            EXPECT_FALSE(JsonStringContains(metricsOutput, R"("Field1":"Hello world")"));
+
+            // Change the name of the event loger to "NewName", it should be active by default
+            googleTraceLogger.SetName(NewLoggerName);
+            EXPECT_EQ(NewLoggerName, googleTraceLogger.GetName());
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // Recording to the event logger with "NewName" should have succeeded
+            EXPECT_TRUE(JsonStringContains(metricsOutput, R"("Field1":"Hello world")"));
+
+            // Set the "NewName" event logger to inactive
+            settingsRegistry.Set(AZ::Metrics::SettingsKey(NewLoggerName) + "/Active", false);
+
+            // Change event string value to validate that recording is not occuring
+            eventMessage = "Goodbye world";
+            argContainer[0].m_value = eventMessage;
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // Recording to the "NewName" event logger should not occur
+            EXPECT_FALSE(JsonStringContains(metricsOutput, R"("Field1":"Goodbye world")"));
+
+            // Reset the "NewName" event logger to active again by deleting 'Active' settings key
+            // (Setting the 'Active' setting to true also works
+            settingsRegistry.Remove(AZ::Metrics::SettingsKey(NewLoggerName) + "/Active");
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            // Recording of the complete event should be succeeding again
+            EXPECT_TRUE(JsonStringContains(metricsOutput, R"("Field1":"Goodbye world")"));
+
+            // Finally change the name back to the original
+            // This will reset the active state back to the value associated with the "JsonTraceEventTest"
+            // name.
+            googleTraceLogger.SetName(EventLoggerName);
+
+            // As the original event logger had a setting with the active setting to false
+            // recording should not occur
+            eventMessage = "Metrics Elided";
+            argContainer[0].m_value = eventMessage;
+
+            resultOutcome = googleTraceLogger.RecordCompleteEvent(completeArgs);
+            EXPECT_TRUE(resultOutcome);
+
+            EXPECT_FALSE(JsonStringContains(metricsOutput, R"("Field1":"Metrics Elided")"));
+        }
     }
 } // namespace UnitTest
 

+ 2 - 0
Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/Internal/RectangleWidget.h

@@ -23,6 +23,8 @@ namespace AzQtComponents::Internal
     class AZ_QT_COMPONENTS_API RectangleWidget : public QWidget
     {
         Q_OBJECT
+
+        Q_PROPERTY(QColor color READ color WRITE setColor)
     public:
         explicit RectangleWidget(QWidget* parent);
 

+ 13 - 0
Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Eyedropper.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 62 (91390) - https://sketch.com -->
+    <title>Icons / System / Info</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Icons-/-System-/-Info" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <g id="Group" transform="translate(10.000000, 2.000000)" fill="#FFFFFF">
+            <rect id="Combined-Shape" x="0.5" y="7" width="4" height="13" rx="1"></rect>
+            <circle id="Oval" cx="2.5" cy="2.5" r="2.5"></circle>
+        </g>
+    </g>
+</svg>

+ 25 - 0
Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Smooth.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 62 (91390) - https://sketch.com -->
+    <title>Icons / Toolbar / Vertex Snapping</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Icons-/-Toolbar-/-Vertex-Snapping" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <g id="Group-4" transform="translate(2.500000, 4.000000)" fill="#FFFFFF">
+            <rect id="Rectangle" x="0.5" y="7.5" width="18" height="1"></rect>
+            <rect id="Rectangle" x="6.5" y="5" width="6" height="6"></rect>
+            <g id="Group-3" transform="translate(9.000000, 0.000000)">
+                <rect id="Rectangle" x="0" y="0" width="1" height="3" rx="0.5"></rect>
+                <rect id="Rectangle-Copy" x="0" y="13" width="1" height="3" rx="0.5"></rect>
+            </g>
+            <g id="Group-3-Copy" transform="translate(9.750000, 8.274094) rotate(45.000000) translate(-9.750000, -8.274094) translate(8.750000, -1.725906)">
+                <rect id="Rectangle" x="0.129409733" y="0.704788109" width="1" height="3" rx="0.5"></rect>
+                <rect id="Rectangle-Copy" x="0.129409733" y="16.2611373" width="1" height="3" rx="0.5"></rect>
+            </g>
+            <g id="Group-3-Copy-2" transform="translate(9.500000, 8.000000) rotate(-45.000000) translate(-9.500000, -8.000000) translate(9.000000, -2.000000)">
+                <rect id="Rectangle" x="1.15463195e-14" y="0.721825407" width="1" height="3" rx="0.5"></rect>
+                <rect id="Rectangle-Copy" x="1.0658141e-14" y="16.2781746" width="1" height="3" rx="0.5"></rect>
+            </g>
+        </g>
+    </g>
+</svg>

+ 2 - 0
Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc

@@ -350,6 +350,7 @@
     <file>img/UI20/toolbar/Debugging.svg</file>
     <file>img/UI20/toolbar/Deploy.svg</file>
     <file>img/UI20/toolbar/Environment.svg</file>
+    <file>img/UI20/toolbar/Eyedropper.svg</file>
     <file>img/UI20/toolbar/Flowgraph.svg</file>
     <file>img/UI20/toolbar/Follow_terrain.svg</file>
     <file>img/UI20/toolbar/Get_physics_state.svg</file>
@@ -383,6 +384,7 @@
     <file>img/UI20/toolbar/Simulate_Physics.svg</file>
     <file>img/UI20/toolbar/Simulate_Physics_on_selected_objects.svg</file>
     <file>img/UI20/toolbar/SketchMode.svg</file>
+    <file>img/UI20/toolbar/Smooth.svg</file>
     <file>img/UI20/toolbar/Terrain.svg</file>
     <file>img/UI20/toolbar/Terrain_Texture.svg</file>
     <file>img/UI20/toolbar/Rotate.svg</file>

+ 3 - 0
Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/entity_overridden.svg

@@ -0,0 +1,3 @@
+<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="4.41956" cy="4.45947" r="3.5" fill="#FF8F00" />
+</svg>

+ 1 - 0
Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc

@@ -15,6 +15,7 @@
         <file alias="entity.svg">Entity/entity.svg</file>
         <file alias="entity_editoronly.svg">Entity/entity_editoronly.svg</file>
         <file alias="entity_notactive.svg">Entity/entity_notactive.svg</file>
+        <file alias="entity_overridden.svg">Entity/entity_overridden.svg</file>
         <file alias="layer.svg">Entity/layer.svg</file>
         <file alias="prefab.svg">Entity/prefab.svg</file>
         <file alias="prefab_edit.svg">Entity/prefab_edit.svg</file>

+ 13 - 15
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeSwitcher.cpp

@@ -103,7 +103,7 @@ namespace AzToolsFramework::ComponentModeFramework
             ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveSwitcher, m_switcherId);
     }
 
-    void ComponentModeSwitcher::UpdateSwitcherOnEntitySelectionChange(
+    void ComponentModeSwitcher::UpdateSwitcher(
         const EntityIdList& newlySelectedEntityIds, const EntityIdList& newlyDeselectedEntityIds)
     {
         auto* toolsApplicationRequests = AzToolsFramework::ToolsApplicationRequestBus::FindFirstHandler();
@@ -116,7 +116,7 @@ namespace AzToolsFramework::ComponentModeFramework
 
             if (!selectedEntityIds.empty())
             {
-                UpdateSwitcherOnEntitySelectionChange(selectedEntityIds, EntityIdList{});
+                UpdateSwitcher(selectedEntityIds, EntityIdList{});
             }
         }
 
@@ -353,13 +353,13 @@ namespace AzToolsFramework::ComponentModeFramework
 
     void ComponentModeSwitcher::OnEntityComponentDisabled(const AZ::EntityId& entityId, [[maybe_unused]] const AZ::ComponentId& componentId)
     {
-        UpdateSwitcherOnEntitySelectionChange({ entityId }, {});
-        RemoveComponentButton(AZ::EntityComponentIdPair(entityId, componentId));
+        ClearSwitcher();
+        UpdateSwitcher({ entityId }, {});
     }
 
-    void ComponentModeSwitcher::OnEntityComponentEnabled(const AZ::EntityId& entityId, const AZ::ComponentId& componentId)
+    void ComponentModeSwitcher::OnEntityComponentEnabled(const AZ::EntityId& entityId, [[maybe_unused]] const AZ::ComponentId& componentId)
     {
-        AddComponentButton(AZ::EntityComponentIdPair(entityId, componentId));
+        UpdateSwitcher({ entityId }, {});
     }
 
     void ComponentModeSwitcher::OnEntityComponentAdded(const AZ::EntityId& entity, const AZ::ComponentId& component)
@@ -376,19 +376,17 @@ namespace AzToolsFramework::ComponentModeFramework
     }
 
     // this is called twice when a component is added, once while the component is pending and
-    // once when it has been added fully to the entity. This is handled in UpdateComponentButton
+    // once when it has been added fully to the entity. 
     void ComponentModeSwitcher::OnEntityCompositionChanged(const AzToolsFramework::EntityIdList& entityIdList)
     {
         if (AZStd::ranges::find(entityIdList, m_componentModePair.GetEntityId()) != entityIdList.end())
         {
-            if (m_addOrRemove == AddOrRemoveComponent::Add)
+            if (m_addOrRemove == AddOrRemoveComponent::Remove)
             {
-                AddComponentButton(m_componentModePair);
-            }
-            else
-            {
-                RemoveComponentButton(m_componentModePair);
+                ClearSwitcher();
             }
+
+            UpdateSwitcher(entityIdList, {});
         }
     }
 
@@ -403,7 +401,7 @@ namespace AzToolsFramework::ComponentModeFramework
             m_transformButtonId);
 
         // send a list of selected and deselected entities to the switcher to deal with updating the switcher view
-        UpdateSwitcherOnEntitySelectionChange(newlySelectedEntities, newlyDeselectedEntities);
+        UpdateSwitcher(newlySelectedEntities, newlyDeselectedEntities);
     }
 
     void ComponentModeSwitcher::AfterUndoRedo()
@@ -424,7 +422,7 @@ namespace AzToolsFramework::ComponentModeFramework
                         if (!inComponentMode)
                         {
                             ClearSwitcher();
-                            UpdateSwitcherOnEntitySelectionChange(selectedEntityIds, {});
+                            UpdateSwitcher(selectedEntityIds, {});
                         }
                     }
                 }

+ 2 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeSwitcher.h

@@ -87,7 +87,7 @@ namespace AzToolsFramework
             //! Removes component button from switcher.
             void RemoveComponentButton(const AZ::EntityComponentIdPair pairId);
             //! Add or remove component buttons to/from the switcher based on entities selected.
-            void UpdateSwitcherOnEntitySelectionChange(
+            void UpdateSwitcher(
                 const EntityIdList& newlyselectedEntityIds, const EntityIdList& newlydeselectedEntityIds);
             //! Clears all buttons from switcher.
             void ClearSwitcher();
@@ -105,7 +105,7 @@ namespace AzToolsFramework
             void OnEntityComponentAdded(const AZ::EntityId& entityId, const AZ::ComponentId& componentId) override;
             void OnEntityComponentRemoved(const AZ::EntityId& entityId, const AZ::ComponentId& componentId) override;
             void OnEntityComponentEnabled(const AZ::EntityId& entityId, const AZ::ComponentId& componentId) override;
-            void OnEntityComponentDisabled(const AZ::EntityId& entityId, [[maybe_unused]] const AZ::ComponentId& componentId) override;
+            void OnEntityComponentDisabled(const AZ::EntityId& entityId, const AZ::ComponentId& componentId) override;
             void OnEntityCompositionChanged(const AzToolsFramework::EntityIdList& entityIdList) override;
 
             // ToolsApplicationBus overrides ...

+ 2 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h

@@ -15,6 +15,7 @@
 #include <AzToolsFramework/Entity/EntityTypes.h>
 #include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
 #include <AzToolsFramework/Entity/SliceEditorEntityOwnershipServiceBus.h>
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.h>
 #include <AzToolsFramework/Prefab/Spawnable/PrefabInMemorySpawnableConverter.h>
 
 namespace AzToolsFramework
@@ -219,6 +220,7 @@ namespace AzToolsFramework
 
         AZStd::string m_rootPath;
         AZStd::unique_ptr<Prefab::Instance> m_rootInstance;
+        Prefab::PrefabOverridePublicHandler m_prefabOverridePublicHandler;
 
         Prefab::PrefabFocusInterface* m_prefabFocusInterface = nullptr;
         Prefab::PrefabSystemComponentInterface* m_prefabSystemComponent = nullptr;

+ 405 - 209
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushManipulator.cpp

@@ -7,6 +7,7 @@
  */
 
 #include <AzCore/Math/Geometry2DUtils.h>
+#include <AzCore/std/sort.h>
 #include <AzFramework/Entity/EntityDebugDisplayBus.h>
 #include <AzToolsFramework/Manipulators/PaintBrushManipulator.h>
 #include <AzToolsFramework/Manipulators/PaintBrushNotificationBus.h>
@@ -59,15 +60,19 @@ namespace AzToolsFramework
         // Set the paint brush settings to use the requested color mode.
         PaintBrushSettingsRequestBus::Broadcast(&PaintBrushSettingsRequestBus::Events::SetBrushColorMode, colorMode);
 
-        // Get the diameter from the global Paint Brush Settings.
-        float diameter = 0.0f;
-        PaintBrushSettingsRequestBus::BroadcastResult(diameter, &PaintBrushSettingsRequestBus::Events::GetSize);
-        const float radius = diameter / 2.0f;
+        // Get the global Paint Brush Settings so that we can calculate our brush circle sizes.
+        PaintBrushSettings brushSettings;
+        PaintBrushSettingsRequestBus::BroadcastResult(brushSettings, &PaintBrushSettingsRequestBus::Events::GetSettings);
 
-        // The PaintBrush manipulator uses a circle projected into world space to represent the brush.
-        const AZ::Color manipulatorColor = AZ::Colors::Red;
-        const float manipulatorWidth = 0.05f;
-        SetView(AzToolsFramework::CreateManipulatorViewProjectedCircle(*this, manipulatorColor, radius, manipulatorWidth));
+        const auto [innerRadius, outerRadius] = GetBrushRadii(brushSettings);
+
+        // The PaintBrush manipulator uses two circles projected into world space to represent the brush.
+        const AZ::Color innerCircleColor = AZ::Colors::Red;
+        const AZ::Color outerCircleColor = AZ::Colors::Red;
+        const float circleWidth = 0.05f;
+        SetView(
+            AzToolsFramework::CreateManipulatorViewProjectedCircle(*this, innerCircleColor, innerRadius, circleWidth),
+            AzToolsFramework::CreateManipulatorViewProjectedCircle(*this, outerCircleColor, outerRadius, circleWidth));
 
         // Start listening for any changes to the Paint Brush Settings
         PaintBrushSettingsNotificationBus::Handler::BusConnect();
@@ -88,25 +93,58 @@ namespace AzToolsFramework
         AzToolsFramework::CloseViewPane(PaintBrush::s_paintBrushSettingsName);
     }
 
+    AZStd::pair<float, float> PaintBrushManipulator::GetBrushRadii(const PaintBrushSettings& settings) const
+    {
+        const float outerRadius = settings.GetSize() / 2.0f;
+
+        if (settings.GetBrushMode() == PaintBrushMode::Eyedropper)
+        {
+            // For the eyedropper, we'll set the inner radius to an arbitrarily small percentage of the full brush size to help
+            // visualize that we're only picking from the very center of the brush.
+            const float eyedropperRadius = outerRadius * 0.02f;
+            return { eyedropperRadius, outerRadius };
+        }
+
+        // For paint/smooth brushes, the inner circle represents the start of the hardness falloff,
+        // and the outer circle is the full paintbrush size.
+        const float hardnessRadius = outerRadius * (settings.GetHardnessPercent() / 100.0f);
+        return { hardnessRadius, outerRadius };
+    }
+
+
     void PaintBrushManipulator::Draw(
         const ManipulatorManagerState& managerState, AzFramework::DebugDisplayRequests& debugDisplay,
         const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteraction& mouseInteraction)
     {
-        m_manipulatorView->Draw(
+        // Always set our manipulator state to say that the mouse isn't over the manipulator so that we always use our base
+        // manipulator color. The paintbrush isn't a "selectable" manipulator, so it wouldn't make sense for it to change color when the
+        // mouse is over it.
+        constexpr bool mouseOver = false;
+
+        m_innerCircle->Draw(
             GetManipulatorManagerId(), managerState, GetManipulatorId(),
-            { GetSpace(), GetNonUniformScale(), AZ::Vector3::CreateZero(), MouseOver() }, debugDisplay, cameraState,
+            { GetSpace(), GetNonUniformScale(), AZ::Vector3::CreateZero(), mouseOver }, debugDisplay, cameraState,
+            mouseInteraction);
+
+        m_outerCircle->Draw(
+            GetManipulatorManagerId(), managerState, GetManipulatorId(),
+            { GetSpace(), GetNonUniformScale(), AZ::Vector3::CreateZero(), mouseOver }, debugDisplay, cameraState,
             mouseInteraction);
     }
 
-    void PaintBrushManipulator::SetView(AZStd::shared_ptr<ManipulatorViewProjectedCircle> view)
+    void PaintBrushManipulator::SetView(
+        AZStd::shared_ptr<ManipulatorViewProjectedCircle> innerCircle, AZStd::shared_ptr<ManipulatorViewProjectedCircle> outerCircle)
     {
-        m_manipulatorView = AZStd::move(view);
+        m_innerCircle = AZStd::move(innerCircle);
+        m_outerCircle = AZStd::move(outerCircle);
     }
 
     void PaintBrushManipulator::OnSettingsChanged(const PaintBrushSettings& newSettings)
     {
-        float diameter = newSettings.GetSize();
-        m_manipulatorView->SetRadius(diameter / 2.0f);
+        const auto [innerRadius, outerRadius] = GetBrushRadii(newSettings);
+
+        m_innerCircle->SetRadius(innerRadius);
+        m_outerCircle->SetRadius(outerRadius);
     }
 
     bool PaintBrushManipulator::HandleMouseInteraction(const AzToolsFramework::ViewportInteraction::MouseInteractionEvent& mouseInteraction)
@@ -133,7 +171,7 @@ namespace AzToolsFramework
                 PaintBrushSettingsRequestBus::BroadcastResult(color, &PaintBrushSettingsRequestBus::Events::GetColor);
 
                 // Notify that a paint stroke has begun, and provide the paint color including opacity.
-                m_isPainting = true;
+                m_isInBrushStroke = true;
                 PaintBrushNotificationBus::Event(m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnPaintStrokeBegin, color);
 
                 const bool isFirstPaintedPoint = true;
@@ -150,9 +188,9 @@ namespace AzToolsFramework
                 // Notify that the paint stroke has ended.
                 // We need to verify that we're currently painting, because clicks/double-clicks can cause us to end up here
                 // without ever getting the mouse Down event.
-                if (m_isPainting)
+                if (m_isInBrushStroke)
                 {
-                    m_isPainting = false;
+                    m_isInBrushStroke = false;
                     PaintBrushNotificationBus::Event(m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnPaintStrokeEnd);
                 }
 
@@ -163,7 +201,8 @@ namespace AzToolsFramework
         return false;
     }
 
-    void PaintBrushManipulator::MovePaintBrush(int viewportId, const AzFramework::ScreenPoint& screenCoordinates, bool isFirstPaintedPoint)
+    void PaintBrushManipulator::MovePaintBrush(
+        int viewportId, const AzFramework::ScreenPoint& screenCoordinates, bool isFirstBrushStrokePoint)
     {
         // Ray cast into the screen to find the closest collision point for the current mouse location.
         auto worldSurfacePosition =
@@ -181,233 +220,390 @@ namespace AzToolsFramework
         SetSpace(space);
         PaintBrushNotificationBus::Event(m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnWorldSpaceChanged, space);
 
-        // If we're currently painting, send off a paint notification.
-        if (m_isPainting)
+        // If we're currently performing a brush stroke, then trigger the appropriate brush action.
+        if (m_isInBrushStroke)
         {
-            AZ::Vector2 currentCenter2D(brushCenter);
-
             // Get our current paint brush settings.
+            PaintBrushSettings brushSettings;
+            PaintBrushSettingsRequestBus::BroadcastResult(brushSettings, &PaintBrushSettingsRequestBus::Events::GetSettings);
 
-            PaintBrushSettings currentSettings;
-            PaintBrushSettingsRequestBus::BroadcastResult(currentSettings, &PaintBrushSettingsRequestBus::Events::GetSettings);
-
-            // Early out if we're completely transparent, there's no distance between brush stamps, or the brush stamp size is 0.
-            if ((currentSettings.GetColor().GetA() == 0.0f) || (currentSettings.GetFlowPercent() == 0.0f) ||
-                (currentSettings.GetDistancePercent() == 0.0f) || (currentSettings.GetSize() == 0.0f))
+            switch (brushSettings.GetBrushMode())
             {
-                return;
+            case PaintBrushMode::Paintbrush:
+                PerformPaintAction(brushCenter, brushSettings, isFirstBrushStrokePoint);
+                break;
+            case PaintBrushMode::Smooth:
+                PerformSmoothAction(brushCenter, brushSettings, isFirstBrushStrokePoint);
+                break;
+            case PaintBrushMode::Eyedropper:
+                PerformEyedropperAction(brushCenter, brushSettings);
+                break;
+            default:
+                AZ_Assert(false, "Unsupported brush mode type: %u", brushSettings.GetBrushMode());
+                break;
             }
+        }
+    }
 
-            // Convert our settings into the 0-1 range
-            const float hardness = currentSettings.GetHardnessPercent() / 100.0f;
-            const float flow = currentSettings.GetFlowPercent() / 100.0f;
+    void PaintBrushManipulator::CalculateBrushStampCentersAndStrokeRegion(
+        const AZ::Vector3& brushCenter,
+        const PaintBrushSettings& brushSettings,
+        bool isFirstBrushStrokePoint,
+        AZStd::vector<AZ::Vector2>& brushStampCenters,
+        AZ::Aabb& strokeRegion)
+    {
+        AZ::Vector2 currentCenter2D(brushCenter);
 
-            // Get the distance between each brush stamp in world space.
-            const float distanceBetweenBrushStamps = currentSettings.GetSize() * (currentSettings.GetDistancePercent() / 100.0f);
+        brushStampCenters.clear();
+        strokeRegion = AZ::Aabb::CreateNull();
 
-            // Track the list of center points for each brush stamp to draw for this mouse movement.
-            AZStd::vector<AZ::Vector2> brushStamps;
+        // Early out if we're completely transparent, there's no distance between brush stamps, or the brush stamp size is 0.
+        if ((brushSettings.GetColor().GetA() == 0.0f) || (brushSettings.GetFlowPercent() == 0.0f) ||
+            (brushSettings.GetDistancePercent() == 0.0f) || (brushSettings.GetSize() == 0.0f))
+        {
+            return;
+        }
 
-            // If this is the first point that we're painting, add this location to the list of brush stamps and use it
-            // as the starting point.
-            if (isFirstPaintedPoint)
-            {
-                brushStamps.emplace_back(currentCenter2D);
-                m_lastBrushCenter = currentCenter2D;
-                m_distanceSinceLastDraw = 0.0f;
-            }
+        // Get the distance between each brush stamp in world space.
+        const float distanceBetweenBrushStamps = brushSettings.GetSize() * (brushSettings.GetDistancePercent() / 100.0f);
 
-            // Get the direction that we've moved the mouse since the last mouse movement we handled.
-            AZ::Vector2 direction = (currentCenter2D - m_lastBrushCenter).GetNormalizedSafe();
+        // If this is the first point that we're painting, add this location to the list of brush stamps and use it
+        // as the starting point.
+        if (isFirstBrushStrokePoint)
+        {
+            brushStampCenters.emplace_back(currentCenter2D);
+            m_lastBrushCenter = currentCenter2D;
+            m_distanceSinceLastDraw = 0.0f;
+        }
 
-            // Get the total distance that we've moved since the last time we drew a brush stamp (which might
-            // have been many small mouse movements ago).
-            float totalDistance = m_lastBrushCenter.GetDistance(currentCenter2D) + m_distanceSinceLastDraw;
+        // Get the direction that we've moved the mouse since the last mouse movement we handled.
+        AZ::Vector2 direction = (currentCenter2D - m_lastBrushCenter).GetNormalizedSafe();
 
-            // Keep adding brush stamps to the list for as long as the total remaining mouse distance is
-            // greater than the stamp distance. If the mouse moved a large enough distance in one frame,
-            // this will add multiple stamps. If the mouse moved a tiny amount, it's possible that no stamps
-            // will get added, and we'll just save the accumulated distance for next frame.
-            for (; totalDistance >= distanceBetweenBrushStamps; totalDistance -= distanceBetweenBrushStamps)
-            {
-                // Add another stamp to the list to draw this time.
-                AZ::Vector2 stampCenter = m_lastBrushCenter + direction * (distanceBetweenBrushStamps - m_distanceSinceLastDraw);
-                brushStamps.emplace_back(stampCenter);
+        // Get the total distance that we've moved since the last time we drew a brush stamp (which might
+        // have been many small mouse movements ago).
+        float totalDistance = m_lastBrushCenter.GetDistance(currentCenter2D) + m_distanceSinceLastDraw;
 
-                // Reset our tracking so that our next stamp location will be based off of this one.
-                m_distanceSinceLastDraw = 0.0f;
-                m_lastBrushCenter = stampCenter;
-            }
+        // Keep adding brush stamps to the list for as long as the total remaining mouse distance is
+        // greater than the stamp distance. If the mouse moved a large enough distance in one frame,
+        // this will add multiple stamps. If the mouse moved a tiny amount, it's possible that no stamps
+        // will get added, and we'll just save the accumulated distance for next frame.
+        for (; totalDistance >= distanceBetweenBrushStamps; totalDistance -= distanceBetweenBrushStamps)
+        {
+            // Add another stamp to the list to draw this time.
+            AZ::Vector2 stampCenter = m_lastBrushCenter + direction * (distanceBetweenBrushStamps - m_distanceSinceLastDraw);
+            brushStampCenters.emplace_back(stampCenter);
+
+            // Reset our tracking so that our next stamp location will be based off of this one.
+            m_distanceSinceLastDraw = 0.0f;
+            m_lastBrushCenter = stampCenter;
+        }
 
-            // If we have any distance remaining that we haven't used, keep it for next time.
-            // Note that totalDistance already includes the previous m_distanceSinceLastDraw, so we just replace it with our
-            // leftovers here, we don't add them.
-            m_distanceSinceLastDraw = totalDistance;
+        // If we have any distance remaining that we haven't used, keep it for next time.
+        // Note that totalDistance already includes the previous m_distanceSinceLastDraw, so we just replace it with our
+        // leftovers here, we don't add them.
+        m_distanceSinceLastDraw = totalDistance;
 
-            // Save the current location as the last one we processed.
-            m_lastBrushCenter = currentCenter2D;
+        // Save the current location as the last one we processed.
+        m_lastBrushCenter = currentCenter2D;
+
+        // If we don't have any stamps on this mouse movement, then we're done.
+        if (brushStampCenters.empty())
+        {
+            return;
+        }
+
+        const float radius = brushSettings.GetSize() / 2.0f;
+
+        // Create an AABB that contains every brush stamp.
+        for (auto& brushStamp : brushStampCenters)
+        {
+            strokeRegion.AddAabb(AZ::Aabb::CreateCenterRadius(AZ::Vector3(brushStamp, 0.0f), radius));
+        }
+    }
+
+    void PaintBrushManipulator::CalculatePointsInBrush(
+        const PaintBrushSettings& brushSettings,
+        const AZStd::vector<AZ::Vector2>& brushStampCenters,
+        AZStd::span<const AZ::Vector3> points,
+        AZStd::vector<AZ::Vector3>& validPoints,
+        AZStd::vector<float>& opacities)
+    {
+        validPoints.clear();
+        opacities.clear();
 
-            // If we don't have any stamps on this mouse movement, then we're done.
-            if (brushStamps.empty())
+        validPoints.reserve(points.size());
+        opacities.reserve(points.size());
+
+        // Convert our settings into the 0-1 range
+        const float hardness = brushSettings.GetHardnessPercent() / 100.0f;
+        const float flow = brushSettings.GetFlowPercent() / 100.0f;
+
+        const float radius = brushSettings.GetSize() / 2.0f;
+        const float manipulatorRadiusSq = radius * radius;
+
+        // Calculate 1/(1 - hardness) once to use for all points. If hardness is 1, set this to 0 instead of 1/0.
+        const float inverseHardnessReciprocal = (hardness < 1.0f) ? (1.0f / (1.0f - hardness)) : 0.0f;
+
+        // Loop through every point that's been provided and see if it has a valid paint opacity.
+        for (size_t index = 0; index < points.size(); index++)
+        {
+            float opacity = 0.0f;
+            AZ::Vector2 point2D(points[index]);
+
+            // Loop through each stamp that we're drawing and accumulate the results for this point.
+            for (auto& brushCenter : brushStampCenters)
             {
-                return;
+                // Since each stamp is a circle, we can just compare distance to the center of the circle vs radius.
+                if (float shortestDistanceSquared = brushCenter.GetDistanceSq(point2D); shortestDistanceSquared <= manipulatorRadiusSq)
+                {
+                    // It's a valid point, so calculate the opacity. The per-point opacity for a paint stamp is a combination
+                    // of the hardness falloff and the flow. The flow value gives the overall opacity for each stamp, and the
+                    // hardness falloff gives per-pixel opacity within the stamp.
+
+                    // Normalize the distance into the 0-1 range, where 0 is the center of the stamp, and 1 is the edge.
+                    float shortestDistanceNormalized = AZStd::sqrt(shortestDistanceSquared) / radius;
+
+                    // The hardness parameter describes what % of the total distance gets the falloff curve.
+                    // i.e. hardness=0.25 means that distances < 0.25 will have no falloff, and the falloff curve will
+                    // be mapped to distances of 0.25 to 1.
+                    // This scaling basically just uses the ratio "(dist - hardness) / (1 - hardness)" and clamps the
+                    // minimum to 0, so our output hardnessDistance is the 0 to 1 number that we input into the falloff function.
+                    float hardnessDistance = AZStd::max(shortestDistanceNormalized - hardness, 0.0f) * inverseHardnessReciprocal;
+
+                    // For the falloff function itself, we use a nonlinear falloff that's approximately the same
+                    // as a squared cosine: 2x^3 - 3x^2 + 1 . This produces a nice backwards S curve that starts at 1, ends at 0,
+                    // and has a midpoint at (0.5, 0.5).
+                    float perPixelOpacity = ((hardnessDistance * hardnessDistance) * (2.0f * hardnessDistance - 3.0f)) + 1.0f;
+
+                    // For the opacity at this point, combine any opacity from previous stamps with the
+                    // currently-computed perPixelOpacity and flow.
+                    opacity = AZStd::min(opacity + (1.0f - opacity) * (perPixelOpacity * flow), 1.0f);
+                }
             }
 
-            const float radius = currentSettings.GetSize() / 2.0f;
-
-            // Create an AABB that contains every brush stamp.
-            AZ::Aabb strokeRegion = AZ::Aabb::CreateNull(); 
-            for (auto& brushStamp : brushStamps)
+            // As long as our opacity isn't transparent, return this as a valid point and opacity.
+            if (opacity > 0.0f)
             {
-                strokeRegion.AddAabb(AZ::Aabb::CreateCenterRadius(AZ::Vector3(brushStamp, 0.0f), radius));
+                validPoints.emplace_back(points[index]);
+                opacities.emplace_back(opacity);
             }
+        }
+    }
 
-            const float manipulatorRadiusSq = radius * radius;
 
-            // Callback function that we pass into OnPaint so that paint handling code can request specific paint values
-            // for the world positions it cares about.
-            PaintBrushNotifications::ValueLookupFn valueLookupFn(
-                [radius, manipulatorRadiusSq, &brushStamps, hardness, flow](
-                AZStd::span<const AZ::Vector3> points,
-                AZStd::vector<AZ::Vector3>& validPoints, AZStd::vector<float>& opacities)
+    void PaintBrushManipulator::PerformPaintAction(
+        const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings, bool isFirstBrushStrokePoint)
+    {
+        // Track the list of center points for each brush stamp to draw for this mouse movement and the AABB around the stamps.
+        AZStd::vector<AZ::Vector2> brushStampCenters;
+        AZ::Aabb strokeRegion = AZ::Aabb::CreateNull(); 
+
+        CalculateBrushStampCentersAndStrokeRegion(brushCenter, brushSettings, isFirstBrushStrokePoint, brushStampCenters, strokeRegion);
+
+        // If we don't have any stamps on this mouse movement, then we're done.
+        if (brushStampCenters.empty())
+        {
+            return;
+        }
+
+        // Callback function that we pass into OnPaint so that paint handling code can request specific paint values
+        // for the world positions it cares about.
+        PaintBrushNotifications::ValueLookupFn valueLookupFn(
+            [&brushStampCenters, &brushSettings](
+            AZStd::span<const AZ::Vector3> points,
+            AZStd::vector<AZ::Vector3>& validPoints, AZStd::vector<float>& opacities)
+        {
+            CalculatePointsInBrush(brushSettings, brushStampCenters, points, validPoints, opacities);
+        });
+
+        // Set the blending operation based on the current paintbrush blend mode setting.
+        PaintBrushNotifications::BlendFn blendFn;
+        switch (brushSettings.GetBlendMode())
+        {
+            case PaintBrushBlendMode::Normal:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = intensity;
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Add:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = baseValue + intensity;
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Subtract:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = baseValue - intensity;
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Multiply:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = baseValue * intensity;
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Screen:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = 1.0f - ((1.0f - baseValue) * (1.0f - intensity));
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Darken:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = AZStd::min(baseValue, intensity);
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Lighten:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = AZStd::max(baseValue, intensity);
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Average:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = (baseValue + intensity) / 2.0f;
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            case PaintBrushBlendMode::Overlay:
+                blendFn = [](float baseValue, float intensity, float opacity)
+                {
+                    const float operationResult = (baseValue >= 0.5f) ? (1.0f - (2.0f * (1.0f - baseValue) * (1.0f - intensity)))
+                                                                        : (2.0f * baseValue * intensity);
+                    return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
+                };
+                break;
+            default:
+                AZ_Assert(false, "Unknown PaintBrushBlendMode type: %u", brushSettings.GetBlendMode());
+                break;
+        }
+
+        // Trigger the OnPaint notification, provide the listener with the valueLookupFn to find out the paint brush
+        // values at specific world positions, and provide the blendFn to perform blending operations based on the provided base and
+        // paint brush values.
+        PaintBrushNotificationBus::Event(
+            m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnPaint, strokeRegion, valueLookupFn, blendFn);
+    }
+
+    void PaintBrushManipulator::PerformEyedropperAction(
+        const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings)
+    {
+        AZ::Color brushColor = brushSettings.GetColor();
+
+        // Trigger the OnGetColor notification to get the current color at the given point.
+        PaintBrushNotificationBus::EventResult(brushColor,
+            m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnGetColor, brushCenter);
+
+        // Set the color in our paintbrush settings to the color selected by the eyedropper.
+        PaintBrushSettingsRequestBus::Broadcast(&PaintBrushSettingsRequestBus::Events::SetColor, brushColor);
+    }
+
+    void PaintBrushManipulator::PerformSmoothAction(
+        const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings, bool isFirstBrushStrokePoint)
+    {
+        // Track the list of center points for each brush stamp to draw for this mouse movement and the AABB around the stamps.
+        AZStd::vector<AZ::Vector2> brushStampCenters;
+        AZ::Aabb strokeRegion = AZ::Aabb::CreateNull();
+
+        CalculateBrushStampCentersAndStrokeRegion(brushCenter, brushSettings, isFirstBrushStrokePoint, brushStampCenters, strokeRegion);
+
+        // If we don't have any stamps on this mouse movement, then we're done.
+        if (brushStampCenters.empty())
+        {
+            return;
+        }
+
+        // Callback function that we pass into OnSmooth so that smoothing code can request specific brush values
+        // for the world positions it cares about.
+        PaintBrushNotifications::ValueLookupFn valueLookupFn(
+            [&brushStampCenters, &brushSettings](
+                AZStd::span<const AZ::Vector3> points, AZStd::vector<AZ::Vector3>& validPoints, AZStd::vector<float>& opacities)
             {
-                validPoints.clear();
-                opacities.clear();
+                CalculatePointsInBrush(brushSettings, brushStampCenters, points, validPoints, opacities);
+            });
+
+
+        // Set the smoothing function to use a Gaussian blur.
 
-                validPoints.reserve(points.size());
-                opacities.reserve(points.size());
+        PaintBrushNotifications::SmoothFn smoothFn;
+        size_t kernelSize = 1;
 
-                // Calculate 1/(1 - hardness) once to use for all points. If hardness is 1, set this to 0 instead of 1/0.
-                const float inverseHardnessReciprocal = (hardness < 1.0f) ? (1.0f / (1.0f - hardness)) : 0.0f;
+        switch (brushSettings.GetSmoothMode())
+        {
+        case AzToolsFramework::PaintBrushSmoothMode::Gaussian:
+            // We'll use a 3x3 kernel for Gaussian smoothing
+            kernelSize = 3;
+            smoothFn = [](float baseValue, AZStd::span<float> kernelValues, float opacity) -> float
+            {
+                AZ_Assert(kernelValues.size() == 9, "Invalid number of points to smooth.");
 
-                // Loop through every point that's been provided and see if it has a valid paint opacity.
-                for (size_t index = 0; index < points.size(); index++)
+                // Calculate a weighted Gaussian average value from the neighborhood of values surrounding (and including) the baseValue.
+                constexpr float gaussianMultipliers[] = { 1.0f, 2.0f, 1.0f, 2.0f, 4.0f, 2.0f, 1.0f, 2.0f, 1.0f };
+                constexpr float gaussianDivisor = 16.0f;
+
+                float smoothedValue = 0.0f;
+                for (size_t index = 0; index < kernelValues.size(); index++)
                 {
-                    float opacity = 0.0f;
-                    AZ::Vector2 point2D(points[index]);
+                    smoothedValue += (kernelValues[index] * gaussianMultipliers[index]);
+                }
 
-                    // Loop through each stamp that we're drawing and accumulate the results for this point.
-                    for (auto& brushCenter : brushStamps)
-                    {
-                        // Since each stamp is a circle, we can just compare distance to the center of the circle vs radius.
-                        if (float shortestDistanceSquared = brushCenter.GetDistanceSq(point2D);
-                            shortestDistanceSquared <= manipulatorRadiusSq)
-                        {
-                            // It's a valid point, so calculate the opacity. The per-point opacity for a paint stamp is a combination
-                            // of the hardness falloff and the flow. The flow value gives the overall opacity for each stamp, and the
-                            // hardness falloff gives per-pixel opacity within the stamp.
-
-                            // Normalize the distance into the 0-1 range, where 0 is the center of the stamp, and 1 is the edge.
-                            float shortestDistanceNormalized = AZStd::sqrt(shortestDistanceSquared) / radius;
-
-                            // The hardness parameter describes what % of the total distance gets the falloff curve.
-                            // i.e. hardness=0.25 means that distances < 0.25 will have no falloff, and the falloff curve will
-                            // be mapped to distances of 0.25 to 1.
-                            // This scaling basically just uses the ratio "(dist - hardness) / (1 - hardness)" and clamps the
-                            // minimum to 0, so our output hardnessDistance is the 0 to 1 number that we input into the falloff function.
-                            float hardnessDistance = AZStd::max(shortestDistanceNormalized - hardness, 0.0f) * inverseHardnessReciprocal;
-
-                            // For the falloff function itself, we use a nonlinear falloff that's approximately the same
-                            // as a squared cosine: 2x^3 - 3x^2 + 1 . This produces a nice backwards S curve that starts at 1, ends at 0,
-                            // and has a midpoint at (0.5, 0.5).
-                            float perPixelOpacity = ((hardnessDistance * hardnessDistance) * (2.0f * hardnessDistance - 3.0f)) + 1.0f;
-
-                            // For the opacity at this point, combine any opacity from previous stamps with the
-                            // currently-computed perPixelOpacity and flow.
-                            opacity = AZStd::min(opacity + (1.0f - opacity) * (perPixelOpacity * flow), 1.0f);
-                        }
-                    }
-
-                    // As long as our opacity isn't transparent, return this as a valid point and opacity.
-                    if (opacity > 0.0f)
-                    {
-                        validPoints.emplace_back(points[index]);
-                        opacities.emplace_back(opacity);
-                    }
+                return AZStd::clamp(AZStd::lerp(baseValue, smoothedValue / gaussianDivisor, opacity), 0.0f, 1.0f);
+            };
+            break;
+        case AzToolsFramework::PaintBrushSmoothMode::Mean:
+            // We'll use a 3x3 kernel, but any kernel size here would work.
+            kernelSize = 3;
+            smoothFn = [](float baseValue, AZStd::span<float> kernelValues, float opacity) -> float
+            {
+                // Calculate the average value from the neighborhood of values surrounding (and including) the baseValue.
+                float smoothedValue = 0.0f;
+                for (size_t index = 0; index < kernelValues.size(); index++)
+                {
+                    smoothedValue += kernelValues[index];
                 }
-            });
 
-            // Set the blending operation based on the current paintbrush blend mode setting.
-            PaintBrushNotifications::BlendFn blendFn;
-            switch (currentSettings.GetBlendMode())
+                return AZStd::clamp(AZStd::lerp(baseValue, smoothedValue / kernelValues.size(), opacity), 0.0f, 1.0f);
+            };
+            break;
+        case AzToolsFramework::PaintBrushSmoothMode::Median:
+            kernelSize = 3;
+            smoothFn = [](float baseValue, AZStd::span<float> kernelValues, float opacity) -> float
             {
-                case PaintBrushBlendMode::Normal:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = intensity;
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Add:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = baseValue + intensity;
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Subtract:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = baseValue - intensity;
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Multiply:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = baseValue * intensity;
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Screen:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = 1.0f - ((1.0f - baseValue) * (1.0f - intensity));
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Darken:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = AZStd::min(baseValue, intensity);
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Lighten:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = AZStd::max(baseValue, intensity);
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Average:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = (baseValue + intensity) / 2.0f;
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                case PaintBrushBlendMode::Overlay:
-                    blendFn = [](float baseValue, float intensity, float opacity)
-                    {
-                        const float operationResult = (baseValue >= 0.5f) ? (1.0f - (2.0f * (1.0f - baseValue) * (1.0f - intensity)))
-                                                                          : (2.0f * baseValue * intensity);
-                        return AZStd::clamp(AZStd::lerp(baseValue, operationResult, opacity), 0.0f, 1.0f);
-                    };
-                    break;
-                default:
-                    AZ_Assert(false, "Unknown PaintBrushBlendMode type: %u", currentSettings.GetBlendMode());
-                    break;
-            }
+                AZ_Assert(kernelValues.size() == 9, "Invalid number of points to smooth.");
 
-            // Trigger the OnPaint notification, and provider the listener with the valueLookupFn to find out the paint brush
-            // values at specific world positions, and the blendFn to perform blending operations based on the provided base and
-            // paint brush values.
-            PaintBrushNotificationBus::Event(
-                m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnPaint, strokeRegion, valueLookupFn, blendFn);
+                // Find the middle value from the neighborhood of values surrounding (and including) the baseValue.
+                AZStd::vector<float> sortedValues(kernelValues.begin(), kernelValues.end());
+                AZStd::sort(sortedValues.begin(), sortedValues.end());
+
+                float medianValue = sortedValues[4];
+
+                return AZStd::clamp(AZStd::lerp(baseValue, medianValue, opacity), 0.0f, 1.0f);
+            };
+            break;
         }
+
+
+        // Trigger the OnSmooth notification. Provide the listener with the strokeRegion to describe the general area of the paint stroke,
+        // the valueLookupFn to find out the paint brush values at specific world positions, the kernelSize to describe how many values
+        // to smooth, and the smoothFn to perform smoothing operations based on the provided base and paint brush values.
+        PaintBrushNotificationBus::Event(
+            m_ownerEntityComponentId, &PaintBrushNotificationBus::Events::OnSmooth, strokeRegion, valueLookupFn, kernelSize, smoothFn);
     }
 
+
+
+
     AZStd::vector<AzToolsFramework::ActionOverride> PaintBrushManipulator::PopulateActionsImpl()
     {
         // Paint brush manipulators should be able to easily adjust the radius of the brush with the [ and ] keys

+ 65 - 8
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushManipulator.h

@@ -66,28 +66,85 @@ namespace AzToolsFramework
             const ManipulatorManagerState& managerState, AzFramework::DebugDisplayRequests& debugDisplay,
             const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteraction& mouseInteraction) override;
 
-        void SetView(AZStd::shared_ptr<ManipulatorViewProjectedCircle> view);
-
         //! Handle mouse events
         bool HandleMouseInteraction(const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
 
         //! Returns the actions that we want any Component Mode using the Paint Brush Manipulator to support.
         AZStd::vector<AzToolsFramework::ActionOverride> PopulateActionsImpl();
- 
+
+        //! Adjusts the size of the paintbrush
         void AdjustSize(float sizeDelta);
 
     private:
-        void OnSettingsChanged(const PaintBrushSettings& newSettings) override;
+        //! Create the manipulator view(s) for the paintbrush.
+        void SetView(
+            AZStd::shared_ptr<ManipulatorViewProjectedCircle> innerCircle, AZStd::shared_ptr<ManipulatorViewProjectedCircle> outerCircle);
 
-        void MovePaintBrush(int viewportId, const AzFramework::ScreenPoint& screenCoordinates, bool isFirstPaintedPoint);
+        //! Calculate the radius for the inner and out circles for the paintbrush manipulator views based on the given brush settings.
+        //! @param settings The paint brush settings to use for calculating the two radii
+        //! @return The inner radius and the outer radius for the brush manipulator views
+        AZStd::pair<float, float> GetBrushRadii(const PaintBrushSettings& settings) const;
+
+        //! PaintBrushSettingsNotificationBus overrides...
+        void OnSettingsChanged(const PaintBrushSettings& newSettings) override;
 
-        AZStd::shared_ptr<ManipulatorViewProjectedCircle> m_manipulatorView;
+        //! Move the paint brush and perform any appropriate brush actions if in the middle of a brush stroke.
+        //! @param viewportId The viewport to move the paint brush in.
+        //! @param screenCoordinates The screen coordinates of the current mouse location.
+        //! @param isFirstBrushStrokePoint True if the stroke is just starting, false if not.
+        void MovePaintBrush(int viewportId, const AzFramework::ScreenPoint& screenCoordinates, bool isFirstBrushStrokePoint);
+
+        //! Apply a paint color to the underlying data based on brush movement and settings.
+        //! @param brushCenter The current center of the paintbrush.
+        //! @param brushSettings The current paintbrush settings.
+        //! @param isFirstBrushStrokePoint True if the stroke is just starting, false if not.
+        void PerformPaintAction(const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings, bool isFirstBrushStokePoint);
+
+        //! Get the color from the underlying data that's located at the brush center and set it in our paintbrush settings.
+        //! @param brushCenter The current center of the paintbrush.
+        //! @param brushSettings The current paintbrush settings.
+        void PerformEyedropperAction(const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings);
+
+        //! Smooth the underlying data based on brush movement and settings.
+        //! @param brushCenter The current center of the paintbrush.
+        //! @param brushSettings The current paintbrush settings.
+        //! @param isFirstBrushStrokePoint True if the stroke is just starting, false if not.
+        void PerformSmoothAction(const AZ::Vector3& brushCenter, const PaintBrushSettings& brushSettings, bool isFirstBrushStrokePoint);
+
+        //! Generates a list of brush stamp centers and an AABB around the brush stamps for the current brush stroke movement.
+        //! @param brushCenter The current center of the paintbrush.
+        //! @param brushSettings The current paintbrush settings.
+        //! @param isFirstBrushStrokePoint True if the stroke is just starting, false if not.
+        //! @param brushStampCenters [out] The list of brush centers to use for this brush stroke movement.
+        //! @param strokeRegion [out] The AABB around the brush stamps in the brushStampCenters list.
+        void CalculateBrushStampCentersAndStrokeRegion(
+            const AZ::Vector3& brushCenter,
+            const PaintBrushSettings& brushSettings,
+            bool isFirstBrushStrokePoint,
+            AZStd::vector<AZ::Vector2>& brushStampCenters,
+            AZ::Aabb& strokeRegion);
+
+        //! Determine which of the passed-in points are within our current brush stroke, and calculate the opacity at each point.
+        //! @param brushSettings The current paintbrush settings.
+        //! @param brushStampCenters The list of brush centers for each stamp in our current brush stroke
+        //! @param points The list of points to calculate values for within our brush stroke
+        //! @param validPoints [out] The subset of the input points that are within the brush stroke
+        //! @param opacities [out] For each point in validPoints, the opacity of the brush at that point
+        static void CalculatePointsInBrush(
+            const PaintBrushSettings& brushSettings,
+            const AZStd::vector<AZ::Vector2>& brushStampCenters,
+            AZStd::span<const AZ::Vector3> points,
+            AZStd::vector<AZ::Vector3>& validPoints,
+            AZStd::vector<float>& opacities);
+
+        AZStd::shared_ptr<ManipulatorViewProjectedCircle> m_innerCircle;
+        AZStd::shared_ptr<ManipulatorViewProjectedCircle> m_outerCircle;
 
         //! The entity/component that owns this paintbrush.
         AZ::EntityComponentIdPair m_ownerEntityComponentId;
 
-        //! True if we're currently painting, false if not.
-        bool m_isPainting = false;
+        //! True if we're currently in a brush stroke, false if not.
+        bool m_isInBrushStroke = false;
 
         //! Location of the last mouse point that we processed while painting.
         AZ::Vector2 m_lastBrushCenter;

+ 33 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/PaintBrushNotificationBus.h

@@ -45,6 +45,16 @@ namespace AzToolsFramework
         //! @return The blended value from 0-1.
         using BlendFn = AZStd::function<float(float baseValue, float intensity, float opacity)>;
 
+        //! Returns the NxN kernel values smoothed together and combined with the base value using the opacity setting.
+        //! This should get called in response to receiving a
+        //! PaintBrushNotificationBus::OnSmooth(dirtyRegion, valueLookupFn, smoothFn) event to smooth the base values together.
+        //! @baseValue The input base value from whatever data is being painted (0-1).
+        //! @kernelValues The NxN kernel input base values (with baseValue at the center) from whatever data is being painted (0-1).
+        //! These values may get sorted and/or modified.
+        //! @opacity The paint brush opacity at this position (0-1).
+        //! @return The smoothed value from 0-1.
+        using SmoothFn = AZStd::function<float(float baseValue, AZStd::span<float> kernelValues, float opacity)>;
+
         //! Notifies listeners that the paintbrush transform has changed,
         //! typically due to the brush moving around in world space.
         //! This will get called in each frame that the brush transform changes.
@@ -86,10 +96,29 @@ namespace AzToolsFramework
         //! @param valueLookupFn The paintbrush value callback to use to get the intensities / opacities / valid flags for
         //! specific positions.
         //! @param blendFn The paintbrush callback to use to blend values together.
-        virtual void OnPaint(
-            [[maybe_unused]] const AZ::Aabb& dirtyArea, [[maybe_unused]] ValueLookupFn& valueLookupFn, [[maybe_unused]] BlendFn& blendFn)
-        {
-        }
+        virtual void OnPaint(const AZ::Aabb& dirtyArea, ValueLookupFn& valueLookupFn, BlendFn& blendFn) = 0;
+
+        //! Notifies listeners that the paintbrush eyedropper has requested a color from a point.
+        //! This will get called in each frame that the paintbrush continues its brush stroke and the brush has moved.
+        //! @param brushCenterPoint the point to get the color from.
+        //! @return The color stored in the data source at that point.
+        virtual AZ::Color OnGetColor([[maybe_unused]] const AZ::Vector3& brushCenterPoint) = 0;
+
+        //! Notifies listeners that the paintbrush is smoothing a region.
+        //! This will get called in each frame that the paintbrush continues to smooth and the brush has moved.
+        //! Since the paintbrush doesn't know how it's being used, and the system using a paintbrush doesn't know the specifics of the
+        //! paintbrush shape and pattern, this works through a back-and-forth handshake.
+        //! 1. The paintbrush sends the OnSmooth message with the AABB of the region that has changed and a paintbrush value callback.
+        //! 2. The listener calls the paintbrush value callback for each position in the region that it cares about.
+        //! 3. The paintbrush responds with the specific brush values for each of those positions based on the brush shape and settings.
+        //! 4. The listener gets an NxN set of values around each valid brush position.
+        //! 4. The listener uses the smoothFn to smooth those values together using the paint brush smoothing method.
+        //! @param dirtyArea The AABB of the area that has been painted in.
+        //! @param valueLookupFn The paintbrush value callback to use to get the intensities / opacities / valid flags for
+        //! specific positions.
+        //! @param kernelSize The size of the NxN kernel to smooth together
+        //! @param smoothFn The paintbrush callback to use to smooth values together.
+        virtual void OnSmooth(const AZ::Aabb& dirtyArea, ValueLookupFn& valueLookupFn, size_t kernelSize, SmoothFn& smoothFn) = 0;
     };
 
     using PaintBrushNotificationBus = AZ::EBus<PaintBrushNotifications>;

+ 163 - 62
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettings.cpp

@@ -61,7 +61,8 @@ namespace AzToolsFramework
         if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
         {
             serializeContext->Class<PaintBrushSettings>()
-                ->Version(4)
+                ->Version(5)
+                ->Field("BrushMode", &PaintBrushSettings::m_brushMode)
                 ->Field("Size", &PaintBrushSettings::m_size)
                 ->Field("Color", &PaintBrushSettings::m_brushColor)
                 ->Field("Intensity", &PaintBrushSettings::m_intensityPercent)
@@ -70,6 +71,7 @@ namespace AzToolsFramework
                 ->Field("Flow", &PaintBrushSettings::m_flowPercent)
                 ->Field("DistancePercent", &PaintBrushSettings::m_distancePercent)
                 ->Field("BlendMode", &PaintBrushSettings::m_blendMode)
+                ->Field("SmoothMode", &PaintBrushSettings::m_smoothMode)
                 ;
 
 
@@ -77,105 +79,198 @@ namespace AzToolsFramework
             {
                 editContext->Class<PaintBrushSettings>("Paint Brush", "")
                     ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
-                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
-                    ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
-                    ->DataElement(AZ::Edit::UIHandlers::Slider, &PaintBrushSettings::m_size, "Size",
+                        ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                        ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &PaintBrushSettings::m_brushMode, "Brush Mode", "Brush functionality.")
+                        ->Attribute(
+                            AZ::Edit::Attributes::EnumValues, AZ::Edit::GetEnumConstantsFromTraits<PaintBrushMode>())
+                        ->Attribute(AZ::Edit::Attributes::ReadOnly, true)
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Slider, &PaintBrushSettings::m_size, "Size",
                         "Size/diameter of the brush stamp in meters.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::SoftMin, 1.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 1024.0f)
-                    ->Attribute(AZ::Edit::Attributes::SoftMax, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.25f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 2)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " m")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMin, 1.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 1024.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMax, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.25f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 2)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " m")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetSizeVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
                     ->DataElement(AZ::Edit::UIHandlers::Default, &PaintBrushSettings::m_brushColor, "Color", "Color of the paint brush.")
-                    ->Attribute("ColorEditorConfiguration", &PaintBrushSettings::GetColorEditorConfig)
-                    ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetColorVisibility)
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnColorChanged)
+                        ->Attribute("ColorEditorConfiguration", &PaintBrushSettings::GetColorEditorConfig)
+                        ->Attribute(AZ::Edit::Attributes::ReadOnly, &PaintBrushSettings::GetColorReadOnly)
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetColorVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnColorChanged)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Slider,
                         &PaintBrushSettings::m_intensityPercent,
                         "Intensity",
                         "Intensity/color percent of the paint brush. 0% = black, 100% = white.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " %")
-                    ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetIntensityVisibility)
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnIntensityChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " %")
+                        ->Attribute(AZ::Edit::Attributes::ReadOnly, &PaintBrushSettings::GetIntensityReadOnly)
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetIntensityVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnIntensityChanged)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Slider,
                         &PaintBrushSettings::m_opacityPercent,
                         "Opacity",
                         "Opacity percent of each paint brush stroke. 0% = transparent, 100% = opaque.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " %")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnOpacityChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " %")
+                        ->Attribute(AZ::Edit::Attributes::ReadOnly, &PaintBrushSettings::GetOpacityReadOnly)
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetOpacityVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnOpacityChanged)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Slider,
                         &PaintBrushSettings::m_hardnessPercent,
                         "Hardness",
                         "Falloff percent around the edges of each paint brush stamp. 0% = soft falloff, 100% = hard edges.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " %")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " %")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetHardnessVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
                     ->DataElement(AZ::Edit::UIHandlers::Slider, &PaintBrushSettings::m_flowPercent, "Flow",
                         "The opacity percent of each paint brush stamp. 0% = transparent, 100% = opaque.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " %")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " %")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetFlowVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
                     ->DataElement(AZ::Edit::UIHandlers::Slider, &PaintBrushSettings::m_distancePercent, "Distance",
                         "Brush distance to move between stamps in % of brush size. 1% = high overlap, 100% = non-overlapping stamps.")
-                    ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
-                    ->Attribute(AZ::Edit::Attributes::SoftMin, 1.0f)
-                    ->Attribute(AZ::Edit::Attributes::SoftMax, 100.0f)
-                    ->Attribute(AZ::Edit::Attributes::Max, 300.0f)
-                    ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
-                    ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
-                    ->Attribute(AZ::Edit::Attributes::Suffix, " %")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMin, 1.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMax, 100.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 300.0f)
+                        ->Attribute(AZ::Edit::Attributes::Step, 0.5f)
+                        ->Attribute(AZ::Edit::Attributes::DisplayDecimals, 1)
+                        ->Attribute(AZ::Edit::Attributes::Suffix, " %")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetDistanceVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::ComboBox, &PaintBrushSettings::m_blendMode, "Blend Mode", "Blend mode of the brush stroke.")
+                        ->EnumAttribute(PaintBrushBlendMode::Normal, "Normal")
+                        ->EnumAttribute(PaintBrushBlendMode::Multiply, "Multiply")
+                        ->EnumAttribute(PaintBrushBlendMode::Screen, "Screen")
+                        ->EnumAttribute(PaintBrushBlendMode::Add, "Linear Dodge (Add)")
+                        ->EnumAttribute(PaintBrushBlendMode::Subtract, "Subtract")
+                        ->EnumAttribute(PaintBrushBlendMode::Darken, "Darken (Min)")
+                        ->EnumAttribute(PaintBrushBlendMode::Lighten, "Lighten (Max)")
+                        ->EnumAttribute(PaintBrushBlendMode::Average, "Average")
+                        ->EnumAttribute(PaintBrushBlendMode::Overlay, "Overlay")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetBlendModeVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
                     ->DataElement(
-                        AZ::Edit::UIHandlers::ComboBox, &PaintBrushSettings::m_blendMode, "Mode", "Blend mode of the brush stroke.")
-                    ->EnumAttribute(PaintBrushBlendMode::Normal, "Normal")
-                    ->EnumAttribute(PaintBrushBlendMode::Multiply, "Multiply")
-                    ->EnumAttribute(PaintBrushBlendMode::Screen, "Screen")
-                    ->EnumAttribute(PaintBrushBlendMode::Add, "Linear Dodge (Add)")
-                    ->EnumAttribute(PaintBrushBlendMode::Subtract, "Subtract")
-                    ->EnumAttribute(PaintBrushBlendMode::Darken, "Darken (Min)")
-                    ->EnumAttribute(PaintBrushBlendMode::Lighten, "Lighten (Max)")
-                    ->EnumAttribute(PaintBrushBlendMode::Average, "Average")
-                    ->EnumAttribute(PaintBrushBlendMode::Overlay, "Overlay")
-                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
+                        AZ::Edit::UIHandlers::ComboBox, &PaintBrushSettings::m_smoothMode,
+                        "Smooth Mode", "Smooth mode of the brush stroke.")
+                        ->EnumAttribute(PaintBrushSmoothMode::Gaussian, "Weighted Average (Gaussian)")
+                        ->EnumAttribute(PaintBrushSmoothMode::Mean, "Average (Mean)")
+                        ->EnumAttribute(PaintBrushSmoothMode::Median, "Middle Value (Median)")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &PaintBrushSettings::GetSmoothModeVisibility)
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintBrushSettings::OnSettingsChanged)
                     ;
             }
         }
     }
 
+    // The following settings are visible but read-only when in the Eyedropper mode
+
+    bool PaintBrushSettings::GetColorReadOnly() const
+    {
+        return (m_brushMode == PaintBrushMode::Eyedropper);
+    }
+
+    bool PaintBrushSettings::GetIntensityReadOnly() const
+    {
+        return (m_brushMode == PaintBrushMode::Eyedropper);
+    }
+
+    bool PaintBrushSettings::GetOpacityReadOnly() const
+    {
+        return (m_brushMode == PaintBrushMode::Eyedropper);
+    }
+
+    // The following settings aren't visible in Eyedropper mode, but are available in Paint / Smooth modes
+
+    bool PaintBrushSettings::GetSizeVisibility() const
+    {
+        return (m_brushMode != PaintBrushMode::Eyedropper);
+    }
+
+    bool PaintBrushSettings::GetHardnessVisibility() const
+    {
+        return (m_brushMode != PaintBrushMode::Eyedropper);
+    }
+
+    bool PaintBrushSettings::GetFlowVisibility() const
+    {
+        return (m_brushMode != PaintBrushMode::Eyedropper);
+    }
+
+    bool PaintBrushSettings::GetDistanceVisibility() const
+    {
+        return (m_brushMode != PaintBrushMode::Eyedropper);
+    }
+
+    // The following settings are only visible in Paint mode
+
+    bool PaintBrushSettings::GetBlendModeVisibility() const
+    {
+        return (m_brushMode == PaintBrushMode::Paintbrush);
+    }
+
+    // The following settings are only visible in Smooth mode
+
+    bool PaintBrushSettings::GetSmoothModeVisibility() const
+    {
+        return (m_brushMode == PaintBrushMode::Smooth);
+    }
+
+    // The color / intensity settings have their visibility controlled by both the color mode and the brush mode.
+
     bool PaintBrushSettings::GetColorVisibility() const
     {
-        return (m_colorMode != PaintBrushColorMode::Greyscale);
+        return (m_colorMode != PaintBrushColorMode::Greyscale) && (m_brushMode != PaintBrushMode::Smooth);
     }
 
     bool PaintBrushSettings::GetIntensityVisibility() const
     {
-        return (m_colorMode == PaintBrushColorMode::Greyscale);
+        return (m_colorMode == PaintBrushColorMode::Greyscale) && (m_brushMode != PaintBrushMode::Smooth);
+    }
+
+    // Opacity is always visible, regardless of brush mode or color mode.
+
+    bool PaintBrushSettings::GetOpacityVisibility() const
+    {
+        return true;
+    }
+
+
+    void PaintBrushSettings::SetBrushMode(PaintBrushMode brushMode)
+    {
+        m_brushMode = brushMode;
+        PaintBrushSettingsNotificationBus::Broadcast(&PaintBrushSettingsNotificationBus::Events::OnVisiblePropertiesChanged);
+        OnSettingsChanged();
     }
 
     void PaintBrushSettings::SetColorMode(PaintBrushColorMode colorMode)
     {
         m_colorMode = colorMode;
-        PaintBrushSettingsNotificationBus::Broadcast(&PaintBrushSettingsNotificationBus::Events::OnColorModeChanged, *this);
+        PaintBrushSettingsNotificationBus::Broadcast(&PaintBrushSettingsNotificationBus::Events::OnVisiblePropertiesChanged);
+        OnSettingsChanged();
     }
 
     void PaintBrushSettings::SetBlendMode(PaintBrushBlendMode blendMode)
@@ -184,6 +279,12 @@ namespace AzToolsFramework
         OnSettingsChanged();
     }
 
+    void PaintBrushSettings::SetSmoothMode(PaintBrushSmoothMode smoothMode)
+    {
+        m_smoothMode = smoothMode;
+        OnSettingsChanged();
+    }
+
     void PaintBrushSettings::SetColor(const AZ::Color& color)
     {
         m_brushColor = color;

+ 68 - 20
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettings.h

@@ -11,7 +11,6 @@
 #include <AzCore/Component/Entity.h>
 #include <AzCore/Math/Color.h>
 #include <AzCore/Serialization/SerializeContext.h>
-#include <AzToolsFramework/PaintBrushSettings/PaintBrushSettings.h>
 #include <AzToolsFramework/UI/PropertyEditor/PropertyColorCtrl.hxx>
 
 namespace AzToolsFramework
@@ -54,27 +53,39 @@ namespace AzToolsFramework
        then blended down using the Intensity, Opacity, and Blend Mode.
     */
 
+    //! The different types of functionality offered by the paint brush tool
+    AZ_ENUM_CLASS_WITH_UNDERLYING_TYPE(PaintBrushMode, uint8_t,
+        (Paintbrush, 0),    //!< Uses color, opacity, and other brush settings to 'paint' values for an abstract data source
+        (Eyedropper, 1),    //!< Gets the current value underneath the brush from an abstract data source
+        (Smooth, 2)         //!< Smooths/blurs the existing values in an abstract data source
+    );
+
     //! The different types of blend modes supported by the paint brush tool.
-    enum class PaintBrushColorMode : uint8_t
-    {
-        Greyscale,
-        SRGB,
-        LinearColor
-    };
+    AZ_ENUM_CLASS_WITH_UNDERLYING_TYPE(PaintBrushColorMode, uint8_t,
+        (Greyscale, 0), 
+        (SRGB, 1), 
+        (LinearColor, 2) 
+    );
 
     //! The different types of blend modes supported by the paint brush tool.
-    enum class PaintBrushBlendMode : uint8_t
-    {
-        Normal,  //!< Alpha blends between the paint brush value and the existing value 
-        Add,        //!< Adds the paint brush value to the existing value
-        Subtract,   //!< Subtracts the paint brush value from the existing value
-        Multiply,   //!< Multiplies the paint brush value with the existing value. (Darkens)
-        Screen,     //!< Inverts the two values, multiplies, then inverts again. (Lightens)
-        Darken,     //!< Keeps the minimum of the paint brush value and the existing value
-        Lighten,    //!< Keeps the maximum of the paint brush value and the existing value
-        Average,    //!< Takes the average of the paint brush value and the existing value
-        Overlay     //!< Combines Multiply and Screen - darkens when brush < 0.5, lightens when brush > 0.5
-    };
+    AZ_ENUM_CLASS_WITH_UNDERLYING_TYPE(PaintBrushBlendMode, uint8_t,
+        (Normal, 0),    //!< Alpha blends between the paint brush value and the existing value 
+        (Add, 1),       //!< Adds the paint brush value to the existing value
+        (Subtract, 2),  //!< Subtracts the paint brush value from the existing value
+        (Multiply, 3),  //!< Multiplies the paint brush value with the existing value. (Darkens)
+        (Screen, 4),    //!< Inverts the two values, multiplies, then inverts again. (Lightens)
+        (Darken, 5),    //!< Keeps the minimum of the paint brush value and the existing value
+        (Lighten, 6),   //!< Keeps the maximum of the paint brush value and the existing value
+        (Average, 7),   //!< Takes the average of the paint brush value and the existing value
+        (Overlay, 8)    //!< Combines Multiply and Screen - darkens when brush < 0.5, lightens when brush > 0.5
+    );
+
+    //! The different types of smoothing modes supported by the paint brush tool.
+    AZ_ENUM_CLASS_WITH_UNDERLYING_TYPE(PaintBrushSmoothMode, uint8_t,
+        (Gaussian, 0), 
+        (Mean, 1), 
+        (Median, 2)
+    );
 
     //! Defines the specific global paintbrush settings.
     //! They can be modified directly through the Property Editor or indirectly via the PaintBrushSettingsRequestBus.
@@ -90,6 +101,13 @@ namespace AzToolsFramework
 
         // Overall paintbrush settings
 
+        PaintBrushMode GetBrushMode() const
+        {
+            return m_brushMode;
+        }
+
+        void SetBrushMode(PaintBrushMode brushMode);
+
         PaintBrushColorMode GetColorMode() const
         {
             return m_colorMode;
@@ -109,8 +127,14 @@ namespace AzToolsFramework
             return m_blendMode;
         }
 
+        PaintBrushSmoothMode GetSmoothMode() const
+        {
+            return m_smoothMode;
+        }
+
         void SetColor(const AZ::Color& color);
         void SetBlendMode(PaintBrushBlendMode blendMode);
+        void SetSmoothMode(PaintBrushSmoothMode smoothMode);
 
         // Stamp settings
 
@@ -144,8 +168,22 @@ namespace AzToolsFramework
         AZ::u32 OnIntensityChanged();
         AZ::u32 OnOpacityChanged();
 
+        bool GetColorReadOnly() const;
+        bool GetIntensityReadOnly() const;
+        bool GetOpacityReadOnly() const;
+
+        bool GetSizeVisibility() const;
         bool GetColorVisibility() const;
         bool GetIntensityVisibility() const;
+        bool GetOpacityVisibility() const;
+        bool GetHardnessVisibility() const;
+        bool GetFlowVisibility() const;
+        bool GetDistanceVisibility() const;
+        bool GetBlendModeVisibility() const;
+        bool GetSmoothModeVisibility() const;
+
+        //! Brush settings brush mode
+        PaintBrushMode m_brushMode = PaintBrushMode::Paintbrush;
 
         //! Brush settings color mode
         PaintBrushColorMode m_colorMode = PaintBrushColorMode::Greyscale;
@@ -154,6 +192,8 @@ namespace AzToolsFramework
         AZ::Color m_brushColor = AZ::Color::CreateOne();
         //! Brush stroke blend mode
         PaintBrushBlendMode m_blendMode = PaintBrushBlendMode::Normal;
+        //! Brush stroke smooth mode
+        PaintBrushSmoothMode m_smoothMode = PaintBrushSmoothMode::Gaussian;
 
         //! Brush stamp diameter in meters
         float m_size = 10.0f;
@@ -175,4 +215,12 @@ namespace AzToolsFramework
         AZ::u32 OnSettingsChanged();
     };
 
-}; // namespace AzToolsFramework
+} // namespace AzToolsFramework
+
+namespace AZ
+{
+    AZ_TYPE_INFO_SPECIALIZE(AzToolsFramework::PaintBrushMode, "{88C6AEA1-5424-4F3A-9E22-6D55C050F06C}");
+    AZ_TYPE_INFO_SPECIALIZE(AzToolsFramework::PaintBrushColorMode, "{0D3B0981-BFB3-47E0-9E28-99CFB540D5AC}");
+    AZ_TYPE_INFO_SPECIALIZE(AzToolsFramework::PaintBrushBlendMode, "{8C52DEAF-C45B-4C3B-8300-5DBC44CE30AF}");
+    AZ_TYPE_INFO_SPECIALIZE(AzToolsFramework::PaintBrushSmoothMode, "{7FEF32F1-54B8-419C-A11E-1CE821BEDF1D}");
+} // namespace AZ

+ 3 - 6
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsNotificationBus.h

@@ -10,21 +10,18 @@
 
 #include <AzCore/Component/ComponentBus.h>
 #include <AzCore/EBus/EBus.h>
+#include <AzToolsFramework/PaintBrushSettings/PaintBrushSettings.h>
 
 namespace AzToolsFramework
 {
-    enum class PaintBrushBlendMode : uint8_t;
-    class PaintBrushSettings;
-
     //! PaintBrushSettingsNotificationBus is used to send out notifications whenever the global paintbrush settings have changed.
     class PaintBrushSettingsNotifications : public AZ::EBusTraits
     {
     public:
         static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
 
-        //! Notifies listeners that the paintbrush color mode has changed.
-        //! @param newSettings The settings after the change
-        virtual void OnColorModeChanged([[maybe_unused]] const PaintBrushSettings& newSettings)
+        //! Notifies listeners that the set of visible paintbrush settings has changed.
+        virtual void OnVisiblePropertiesChanged()
         {
         }
 

+ 13 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsRequestBus.h

@@ -31,6 +31,12 @@ namespace AzToolsFramework
         //! Returns a copy of the current paintbrush settings
         virtual PaintBrushSettings GetSettings() const = 0;
 
+        //! Returns the current brush mode for the paint brush settings
+        virtual PaintBrushMode GetBrushMode() const = 0;
+
+        //! Sets the brush mode for the paint brush settings.
+        virtual void SetBrushMode(PaintBrushMode brushMode) = 0;
+
         //! Returns the current color mode for the paint brush settings
         virtual PaintBrushColorMode GetBrushColorMode() const = 0;
 
@@ -46,10 +52,17 @@ namespace AzToolsFramework
         //! Returns the current brush stroke blend mode.
         virtual PaintBrushBlendMode GetBlendMode() const = 0;
 
+        //! Returns the current brush stroke smooth mode.
+        virtual PaintBrushSmoothMode GetSmoothMode() const = 0;
+
         //! Sets the brush stroke blend mode.
         //! @param blendMode The new blend mode.
         virtual void SetBlendMode(PaintBrushBlendMode blendMode) = 0;
 
+        //! Sets the brush stroke smooth mode.
+        //! @param smoothMode The new smooth mode.
+        virtual void SetSmoothMode(PaintBrushSmoothMode smoothMode) = 0;
+
         //! Set the brush stroke color, including opacity.
         //! @param color The new brush color. In monochrome painting, only the Red value will be used.
         virtual void SetColor(const AZ::Color& color) = 0;

+ 20 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsSystemComponent.cpp

@@ -46,6 +46,16 @@ namespace AzToolsFramework
         return m_settings;
     }
 
+    PaintBrushMode PaintBrushSettingsSystemComponent::GetBrushMode() const
+    {
+        return m_settings.GetBrushMode();
+    }
+
+    void PaintBrushSettingsSystemComponent::SetBrushMode(PaintBrushMode brushMode)
+    {
+        m_settings.SetBrushMode(brushMode);
+    }
+
     PaintBrushColorMode PaintBrushSettingsSystemComponent::GetBrushColorMode() const
     {
         return m_settings.GetColorMode();
@@ -86,6 +96,11 @@ namespace AzToolsFramework
         return m_settings.GetBlendMode();
     }
 
+    PaintBrushSmoothMode PaintBrushSettingsSystemComponent::GetSmoothMode() const
+    {
+        return m_settings.GetSmoothMode();
+    }
+
     void PaintBrushSettingsSystemComponent::SetSize(float size)
     {
         m_settings.SetSize(size);
@@ -115,4 +130,9 @@ namespace AzToolsFramework
     {
         m_settings.SetBlendMode(blendMode);
     }
+
+    void PaintBrushSettingsSystemComponent::SetSmoothMode(PaintBrushSmoothMode smoothMode)
+    {
+        m_settings.SetSmoothMode(smoothMode);
+    }
 } // namespace AzToolsFramework

+ 4 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsSystemComponent.h

@@ -35,6 +35,8 @@ namespace AzToolsFramework
         // PaintBrushSettingsRequestBus overrides...
         PaintBrushSettings* GetSettingsPointerForPropertyEditor() override;
         PaintBrushSettings GetSettings() const override;
+        PaintBrushMode GetBrushMode() const override;
+        void SetBrushMode(PaintBrushMode brushMode) override;
         PaintBrushColorMode GetBrushColorMode() const override;
         void SetBrushColorMode(PaintBrushColorMode colorMode) override;
         float GetSize() const override;
@@ -43,12 +45,14 @@ namespace AzToolsFramework
         float GetFlowPercent() const override;
         float GetDistancePercent() const override;
         PaintBrushBlendMode GetBlendMode() const override;
+        PaintBrushSmoothMode GetSmoothMode() const override;
         void SetSize(float size) override;
         void SetColor(const AZ::Color& color) override;
         void SetHardnessPercent(float hardnessPercent) override;
         void SetFlowPercent(float flowPercent) override;
         void SetDistancePercent(float distancePercent) override;
         void SetBlendMode(PaintBrushBlendMode blendMode) override;
+        void SetSmoothMode(PaintBrushSmoothMode smoothMode) override;
 
         PaintBrushSettings m_settings;
     };

+ 2 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsWindow.cpp

@@ -67,7 +67,7 @@ namespace PaintBrush
             AzToolsFramework::PaintBrushSettingsNotificationBus::Handler::BusDisconnect();
         }
 
-        void PaintBrushSettingsWindow::OnColorModeChanged([[maybe_unused]] const AzToolsFramework::PaintBrushSettings& newSettings)
+        void PaintBrushSettingsWindow::OnVisiblePropertiesChanged()
         {
             m_propertyEditor->InvalidateAll();
         }
@@ -93,7 +93,7 @@ namespace PaintBrush
         // and this is only visible while in a painting component mode, so we don't ever need to disable the controls.
         viewOptions.isDisabledInComponentMode = false;
         // Default size of the window
-        viewOptions.paneRect = QRect(50, 50, 350, 210);
+        viewOptions.paneRect = QRect(50, 50, 350, 230);
 
         AzToolsFramework::EditorRequestBus::Broadcast(
             &AzToolsFramework::EditorRequestBus::Events::RegisterViewPane,

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/PaintBrushSettings/PaintBrushSettingsWindow_Internals.h

@@ -69,7 +69,7 @@ namespace PaintBrush
             }
 
         private:
-            void OnColorModeChanged([[maybe_unused]] const AzToolsFramework::PaintBrushSettings& newSettings) override;
+            void OnVisiblePropertiesChanged() override;
             void OnSettingsChanged([[maybe_unused]] const AzToolsFramework::PaintBrushSettings& newSettings) override;
 
             // RPE Support

+ 5 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplateInterface.h

@@ -46,6 +46,11 @@ namespace AzToolsFramework
             //! @return The string matching the path to the entity alias
             virtual AZStd::string GenerateEntityAliasPath(AZ::EntityId entityId) = 0;
 
+            //! Generates a path to the entity matching the id from the focused prefab.
+            //! @param entityId The entity id to fetch the path for.
+            //! @return The path to the entity with the given id.
+            virtual AZ::Dom::Path GenerateEntityPathFromFocusedPrefab(AZ::EntityId entityId) = 0;
+
             //! Generates an entity alias from given entity id and prefix and then add it as path into the provided patch.
             //! @param providedPatch The patch to add entity alias path to.
             //! @param entityId The entity id to use for generating alias path.

+ 44 - 20
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplatePropagator.cpp

@@ -10,7 +10,10 @@
 #include <AzCore/Serialization/Json/JsonSerialization.h>
 #include <AzCore/Component/Entity.h>
 
+#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
 #include <AzToolsFramework/Prefab/Instance/Instance.h>
+#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
+#include <AzToolsFramework/Prefab/PrefabInstanceUtils.h>
 #include <AzToolsFramework/Prefab/Instance/InstanceEntityIdMapper.h>
 #include <AzToolsFramework/Prefab/Instance/InstanceToTemplatePropagator.h>
 #include <AzToolsFramework/Prefab/PrefabDomUtils.h>
@@ -19,10 +22,14 @@ namespace AzToolsFramework
 {
     namespace Prefab
     {
+        AzFramework::EntityContextId InstanceToTemplatePropagator::s_editorEntityContextId = AzFramework::EntityContextId::CreateNull();
+
         void InstanceToTemplatePropagator::RegisterInstanceToTemplateInterface()
         {
             AZ::Interface<InstanceToTemplateInterface>::Register(this);
 
+            EditorEntityContextRequestBus::BroadcastResult(s_editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
+
             //get instance id associated with entityId
             m_instanceEntityMapperInterface = AZ::Interface<InstanceEntityMapperInterface>::Get();
             AZ_Assert(m_instanceEntityMapperInterface,
@@ -85,11 +92,9 @@ namespace AzToolsFramework
                 AZ_Assert(false, "Link with id %llu couldn't be found. Patch cannot be generated.", linkId);
                 return false;
             }
-            
-            Link& link = findLinkResult->get();
 
-            AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::CreatePatch(generatedPatch,
-                link.GetLinkDom().GetAllocator(), initialState, modifiedState, AZ::JsonMergeApproach::JsonPatch);
+            AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::CreatePatch(
+                generatedPatch, generatedPatch.GetAllocator(), initialState, modifiedState, AZ::JsonMergeApproach::JsonPatch);
 
             return result.GetProcessing() != AZ::JsonSerializationResult::Processing::Halted;
         }
@@ -138,6 +143,39 @@ namespace AzToolsFramework
             return AZStd::move(entityAliasPath);
         }
 
+        AZ::Dom::Path InstanceToTemplatePropagator::GenerateEntityPathFromFocusedPrefab(AZ::EntityId entityId)
+        {
+            InstanceOptionalReference owningInstance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
+            if (!owningInstance.has_value())
+            {
+                AZ_Error("Prefab", false, "Failed to find an owning instance for entity with id %llu.", static_cast<AZ::u64>(entityId));
+                return AZ::Dom::Path();
+            }
+
+            auto* prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
+            if (prefabFocusInterface == nullptr)
+            {
+                AZ_Error("Prefab", false, "Cannot find PrefabFocusInterface.");
+                return AZ::Dom::Path();
+            }
+
+            auto focusedInstance = prefabFocusInterface->GetFocusedPrefabInstance(s_editorEntityContextId);
+
+            if (!focusedInstance.has_value())
+            {
+                AZ_Error("Prefab", false, "Focused prefab instance is null.");
+                return AZ::Dom::Path();
+            }
+            
+            auto relativePathBetweenInstances =
+                PrefabInstanceUtils::GetRelativePathBetweenInstances(focusedInstance->get(), owningInstance->get());
+
+            AZ::Dom::Path fullPathBetweenInstances(relativePathBetweenInstances);
+            AZStd::string entityPath = GenerateEntityAliasPath(entityId);
+            fullPathBetweenInstances = fullPathBetweenInstances / AZ::Dom::Path(entityPath);
+            return AZStd::move(fullPathBetweenInstances);
+        }
+
         void InstanceToTemplatePropagator::AppendEntityAliasToPatchPaths(PrefabDom& providedPatch, AZ::EntityId entityId, const AZStd::string& prefix)
         {
             AppendEntityAliasPathToPatchPaths(providedPatch, prefix + GenerateEntityAliasPath(entityId));
@@ -249,10 +287,10 @@ namespace AzToolsFramework
                 PrefabDomValueReference patchPathReference = PrefabDomUtils::FindPrefabDomValue(*patchIterator, "path");
                 AZStd::string patchPathString = patchPathReference->get().GetString();
                 patchPathString.insert(0, patchPrefix);
-                patchPathReference->get().SetString(patchPathString.c_str(), linkToApplyPatches.GetLinkDom().GetAllocator());
+                patchPathReference->get().SetString(patchPathString.c_str(), patches.GetAllocator());
             }
 
-            AddPatchesToLink(patches, linkToApplyPatches);
+            linkToApplyPatches.AddPatchesToLink(patches);
             linkToApplyPatches.UpdateTarget();
 
             m_prefabSystemComponentInterface->SetTemplateDirtyFlag(linkToApplyPatches.GetTargetTemplateId(), true);
@@ -276,19 +314,5 @@ namespace AzToolsFramework
 
             return parentInstance;
         }
-
-        void InstanceToTemplatePropagator::AddPatchesToLink(const PrefabDom& patches, Link& link)
-        {
-            PrefabDom& linkDom = link.GetLinkDom();
-
-            /*
-            If the original allocator the patches were created with gets destroyed, then the patches would become garbage in the
-            linkDom. Since we cannot guarantee the lifecycle of the patch allocators, we are doing a copy of the patches here to
-            associate them with the linkDom's allocator.
-            */
-            PrefabDom patchesCopy;
-            patchesCopy.CopyFrom(patches, linkDom.GetAllocator());
-            linkDom.AddMember(rapidjson::StringRef(PrefabDomUtils::PatchesName), patchesCopy, linkDom.GetAllocator());
-        }
     }
 }

+ 3 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceToTemplatePropagator.h

@@ -37,6 +37,8 @@ namespace AzToolsFramework
 
             AZStd::string GenerateEntityAliasPath(AZ::EntityId entityId) override;
 
+            AZ::Dom::Path GenerateEntityPathFromFocusedPrefab(AZ::EntityId entityId) override;
+
             void AppendEntityAliasToPatchPaths(PrefabDom& providedPatch, AZ::EntityId entityId, const AZStd::string& prefix = "") override;
             void AppendEntityAliasPathToPatchPaths(PrefabDom& providedPatch, const AZStd::string& entityAliasPath) override;
 
@@ -46,12 +48,11 @@ namespace AzToolsFramework
 
             void ApplyPatchesToInstance(const AZ::EntityId& entityId, PrefabDom& patches, const Instance& instanceToAddPatches) override;
 
-            void AddPatchesToLink(const PrefabDom& patches, Link& link);
-
         private:
             InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
             PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
             InstanceDomGenerator m_instanceDomGenerator;
+            static AzFramework::EntityContextId s_editorEntityContextId;
         };
     }
 }

+ 109 - 48
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Link/Link.cpp

@@ -6,8 +6,8 @@
  *
  */
 
+#include <AzCore/Debug/Profiler.h>
 #include <AzToolsFramework/Prefab/Link/Link.h>
-
 #include <AzToolsFramework/Prefab/PrefabDomUtils.h>
 #include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
 #include <AzToolsFramework/Prefab/Template/Template.h>
@@ -16,20 +16,6 @@ namespace AzToolsFramework
 {
     namespace Prefab
     {
-        Link::Link(const Link& other)
-            : m_sourceTemplateId(other.m_sourceTemplateId)
-            , m_targetTemplateId(other.m_targetTemplateId)
-            , m_instanceName(other.m_instanceName)
-            , m_id(other.m_id)
-            , m_prefabSystemComponentInterface(other.m_prefabSystemComponentInterface)
-        {
-            AZ_Assert(m_prefabSystemComponentInterface != nullptr,
-                "Prefab System Component Interface could not be found. "
-                "It is a requirement for the Link class. Check that it is being correctly initialized.");
-            m_linkDom.CopyFrom(
-                other.m_linkDom, m_linkDom.GetAllocator());
-        }
-        
         Link::Link()
             : Link(InvalidLinkId)
         {
@@ -44,34 +30,15 @@ namespace AzToolsFramework
                 "It is a requirement for the Link class. Check that it is being correctly initialized.");
         }
 
-        Link& Link::operator=(const Link& other)
-        {
-            if (this != &other)
-            {
-                m_sourceTemplateId = other.m_targetTemplateId;
-                m_targetTemplateId = other.m_sourceTemplateId;
-                m_instanceName = other.m_instanceName;
-                m_id = other.m_id;
-                m_prefabSystemComponentInterface = other.m_prefabSystemComponentInterface;
-                AZ_Assert(m_prefabSystemComponentInterface != nullptr,
-                    "Prefab System Component Interface could not be found. "
-                    "It is a requirement for the Link class. Check that it is being correctly initialized.");
-                m_linkDom.CopyFrom(
-                    other.m_linkDom, m_linkDom.GetAllocator());
-            }
-
-            return *this;
-        }
-
         Link::Link(Link&& other) noexcept
             : m_id(AZStd::move(other.m_id))
             , m_sourceTemplateId(AZStd::move(other.m_sourceTemplateId))
             , m_targetTemplateId(AZStd::move(other.m_targetTemplateId))
             , m_instanceName(AZStd::move(other.m_instanceName))
             , m_prefabSystemComponentInterface(AZStd::move(other.m_prefabSystemComponentInterface))
+            , m_linkPatchesTree(AZStd::move(other.m_linkPatchesTree))
         {
             other.m_prefabSystemComponentInterface = nullptr;
-            m_linkDom.Swap(other.m_linkDom);
         }
 
 
@@ -88,9 +55,8 @@ namespace AzToolsFramework
                     "Prefab System Component Interface could not be found. "
                     "It is a requirement for the Link class. Check that it is being correctly initialized.");
                 other.m_prefabSystemComponentInterface = nullptr;
-                m_linkDom.Swap(other.m_linkDom);
+                m_linkPatchesTree = AZStd::move(other.m_linkPatchesTree);
             }
-
             return *this;
         }
 
@@ -106,7 +72,18 @@ namespace AzToolsFramework
 
         void Link::SetLinkDom(const PrefabDomValue& linkDom)
         {
-            m_linkDom.CopyFrom(linkDom, m_linkDom.GetAllocator());
+            AZ_PROFILE_FUNCTION(PrefabSystem);
+            m_linkPatchesTree.Clear();
+            PrefabDomValueConstReference patchesReference = PrefabDomUtils::FindPrefabDomValue(linkDom, PrefabDomUtils::PatchesName);
+            if (patchesReference.has_value())
+            {
+                RebuildLinkPatchesTree(patchesReference->get());
+            }
+        }
+
+        void Link::AddPatchesToLink(const PrefabDom& patches)
+        {
+            RebuildLinkPatchesTree(patches);
         }
 
         void Link::SetInstanceName(const char* instanceName)
@@ -137,14 +114,25 @@ namespace AzToolsFramework
             return m_id;
         }
 
-        PrefabDom& Link::GetLinkDom()
+        void Link::GetLinkDom(PrefabDomValue& linkDom, PrefabDomAllocator& allocator) const
         {
-            return m_linkDom;
+            AZ_PROFILE_FUNCTION(PrefabSystem);
+            return ConstructLinkDomFromPatches(linkDom, allocator);
         }
 
-        const PrefabDom& Link::GetLinkDom() const
+        bool Link::AreOverridesPresent(AZ::Dom::Path path, AZ::Dom::PrefixTreeTraversalFlags prefixTreeTraversalFlags)
         {
-            return m_linkDom;
+            bool areOverridesPresent = false;
+            auto visitorFn = [&areOverridesPresent](AZ::Dom::Path, const PrefabOverrideMetadata&)
+            {
+                areOverridesPresent = true;
+                // We just need to check if at least one override is present at the path.
+                // Return false here so that we don't keep looking for all patches at the path.
+                return false;
+            };
+
+            m_linkPatchesTree.VisitPath(path, visitorFn, prefixTreeTraversalFlags);
+            return areOverridesPresent;
         }
 
         PrefabDomPath Link::GetInstancePath() const
@@ -167,7 +155,10 @@ namespace AzToolsFramework
             PrefabDom sourceTemplateDomCopy;
             sourceTemplateDomCopy.CopyFrom(sourceTemplatePrefabDom, sourceTemplatePrefabDom.GetAllocator());
 
-            PrefabDomValueReference patchesReference = PrefabDomUtils::FindPrefabDomValue(m_linkDom, PrefabDomUtils::PatchesName);
+            
+            PrefabDom patchesDom;
+            ConstructLinkDomFromPatches(patchesDom, patchesDom.GetAllocator());
+            PrefabDomValueReference patchesReference = PrefabDomUtils::FindPrefabDomValue(patchesDom, PrefabDomUtils::PatchesName);
             if (!patchesReference.has_value())
             {
                 if (AZ::JsonSerialization::Compare(linkedInstanceDom, sourceTemplateDomCopy) != AZ::JsonSerializerCompareResult::Equal)
@@ -215,11 +206,11 @@ namespace AzToolsFramework
 
         PrefabDomValue& Link::GetLinkedInstanceDom()
         {
-            AZ_Assert(IsValid(), "Link::GetLinkDom - Trying to get DOM of an invalid link.");
+            AZ_Assert(IsValid(), "Link::GetLinkedInstanceDom - Trying to get DOM of an invalid link.");
             PrefabDom& targetTemplatePrefabDom = m_prefabSystemComponentInterface->FindTemplateDom(m_targetTemplateId);
             PrefabDomPath instancePath = GetInstancePath();
             PrefabDomValue* instanceValue = instancePath.Get(targetTemplatePrefabDom);
-            AZ_Assert(instanceValue,"Link::GetLinkDom - Invalid value for instance pointed by the link in template with id '%u'.",
+            AZ_Assert(instanceValue,"Link::GetLinkedInstanceDom - Invalid value for instance pointed by the link in template with id '%u'.",
                     m_targetTemplateId);
             return *instanceValue;
         }
@@ -230,7 +221,7 @@ namespace AzToolsFramework
             AddLinkIdToInstanceDom(instanceDom, targetTemplatePrefabDom.GetAllocator());
         }
 
-        void Link::AddLinkIdToInstanceDom(PrefabDomValue& instanceDom, PrefabDom::AllocatorType& allocator)
+        void Link::AddLinkIdToInstanceDom(PrefabDomValue& instanceDom, PrefabDomAllocator& allocator)
         {
             PrefabDomValueReference linkIdReference = PrefabDomUtils::FindPrefabDomValue(instanceDom, PrefabDomUtils::LinkIdName);
             if (!linkIdReference.has_value())
@@ -244,9 +235,79 @@ namespace AzToolsFramework
             }
         }
 
-        PrefabDomValueReference Link::GetLinkPatches()
+        void Link::ConstructLinkDomFromPatches(PrefabDomValue& linkDom, PrefabDomAllocator& allocator) const
         {
-            return PrefabDomUtils::FindPrefabDomValue(m_linkDom, PrefabDomUtils::PatchesName);
+            linkDom.SetObject();
+
+            TemplateReference sourceTemplate = m_prefabSystemComponentInterface->FindTemplate(m_sourceTemplateId);
+            if (!sourceTemplate.has_value())
+            {
+                AZ_Assert(false, "Failed to fetch source template from link");
+                return;
+            }
+
+            linkDom.AddMember(
+                rapidjson::StringRef(PrefabDomUtils::SourceName),
+                PrefabDomValue(sourceTemplate->get().GetFilePath().c_str(), allocator),
+                allocator);
+
+            PrefabDomValue patchesArray;
+
+            GetLinkPatches(patchesArray, allocator);
+
+            if (patchesArray.Size() != 0)
+            {
+                linkDom.AddMember(rapidjson::StringRef(PrefabDomUtils::PatchesName), AZStd::move(patchesArray), allocator);
+            }
+        }
+
+        void Link::RebuildLinkPatchesTree(const PrefabDomValue& patches)
+        {
+            m_linkPatchesTree.Clear();
+            if (patches.IsArray())
+            {
+                rapidjson::GenericArray patchesArray = patches.GetArray();
+                for (rapidjson::SizeType i = 0; i < patchesArray.Size(); i++)
+                {
+                    PrefabDom patchEntry;
+                    patchEntry.CopyFrom(patchesArray[i], patchEntry.GetAllocator());
+
+                    auto path = patchEntry.FindMember("path");
+                    if (path != patchEntry.MemberEnd())
+                    {
+                        AZ::Dom::Path domPath(path->value.GetString());
+                        PrefabOverrideMetadata overrideMetadata(AZStd::move(patchEntry), i);
+                        m_linkPatchesTree.SetValue(domPath, AZStd::move(overrideMetadata));
+                    }
+                }
+            }
+        }
+
+        void Link::GetLinkPatches(PrefabDomValue& patchesDom, PrefabDomAllocator& allocator) const
+        {
+            auto cmp = [](const PrefabOverrideMetadata* a, const PrefabOverrideMetadata* b)
+            {
+                return (a->m_patchIndex < b->m_patchIndex);
+            };
+
+            // Use a set to sort the patches based on their patch indices. This will make sure that entities are
+            // retrieved from the tree in the same order as they are inserted in.
+            AZStd::set<const PrefabOverrideMetadata*, decltype(cmp)> patchesSet(cmp);
+
+            auto visitorFn = [&patchesSet](const AZ::Dom::Path&, const PrefabOverrideMetadata& overrideMetadata)
+            {
+                patchesSet.emplace(&overrideMetadata);
+                return true;
+            };
+
+            patchesDom.SetArray();
+            m_linkPatchesTree.VisitPath(AZ::Dom::Path(), visitorFn, AZ::Dom::PrefixTreeTraversalFlags::ExcludeExactPath);
+
+            for (auto patchesSetIterator = patchesSet.begin(); patchesSetIterator != patchesSet.end(); ++patchesSetIterator)
+            {
+                PrefabDomValue patch((*patchesSetIterator)->m_patch, allocator);
+                patchesDom.PushBack(patch.Move(), allocator);
+            }
         }
 
     } // namespace Prefab

+ 77 - 10
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Link/Link.h

@@ -8,11 +8,15 @@
 
 #pragma once
 
+#include <AzCore/Debug/Budget.h>
+#include <AzCore/DOM/DomPrefixTree.h>
 #include <AzCore/Memory/SystemAllocator.h>
 #include <AzCore/Serialization/Json/JsonSerialization.h>
 #include <AzToolsFramework/Prefab/PrefabDomTypes.h>
 #include <AzToolsFramework/Prefab/PrefabIdTypes.h>
 
+AZ_DECLARE_BUDGET(PrefabSystem);
+
 namespace AzToolsFramework
 {
     namespace Prefab
@@ -31,10 +35,48 @@ namespace AzToolsFramework
             AZ_CLASS_ALLOCATOR(Link, AZ::SystemAllocator, 0);
             AZ_RTTI(Link, "{49230756-7BAA-4456-8DFE-0E18CB887DB5}");
 
+            // The structure to store metadata information about individual patches on a link.
+            struct PrefabOverrideMetadata
+            {
+                PrefabOverrideMetadata(PrefabDom&& patch, AZ::u32 patchIndex) noexcept
+                    : m_patch(AZStd::move(patch))
+                    , m_patchIndex(patchIndex)
+                {
+                }
+
+                PrefabOverrideMetadata(PrefabOverrideMetadata&& other) noexcept
+                    : m_patch(AZStd::move(other.m_patch))
+                    , m_patchIndex(other.m_patchIndex)
+                {
+                }
+
+                PrefabOverrideMetadata& operator=(PrefabOverrideMetadata&& other) noexcept
+                {
+                    if (this != &other)
+                    {
+                        m_patch = AZStd::move(other.m_patch);
+                        m_patchIndex = other.m_patchIndex;
+                    }
+
+                    return *this;
+                }
+
+                bool operator<(const PrefabOverrideMetadata& other) const
+                {
+                    return (m_patchIndex < other.m_patchIndex);
+                }
+
+                // An individual patch that can get applied to the target template of the link.
+                PrefabDom m_patch;
+
+                // The patch index to associate with each individual patch. This is needed to maintain the order of patches within a link.
+                AZ::u32 m_patchIndex;
+            };
+
             Link();
             Link(LinkId linkId);
-            Link(const Link& other);
-            Link& operator=(const Link& other);
+            Link(const Link& other) = delete;
+            Link& operator=(const Link& other) = delete;
 
             Link(Link&& other) noexcept;
             Link& operator=(Link&& other) noexcept;
@@ -44,6 +86,7 @@ namespace AzToolsFramework
             void SetSourceTemplateId(TemplateId id);
             void SetTargetTemplateId(TemplateId id);
             void SetLinkDom(const PrefabDomValue& linkDom);
+            void AddPatchesToLink(const PrefabDom& patches);
             void SetInstanceName(const char* instanceName);
 
             bool IsValid() const;
@@ -53,8 +96,24 @@ namespace AzToolsFramework
 
             LinkId GetId() const;
 
-            PrefabDom& GetLinkDom();
-            const PrefabDom& GetLinkDom() const;
+            //! Populates the patches DOM provided with the patches fetched from 'm_linkPatchesTree'
+            //! @param[out] patchesDom The DOM to populate with patches
+            //! @param allocator The allocator to use for memory allocations of patches.
+            void GetLinkPatches(PrefabDomValue& patchesDom, PrefabDomAllocator& allocator) const;
+
+            //! Populates the link DOM provided with 'Source' and 'Patches' fields. 'Patches' are fetched from 'm_linkPatchesTree'.
+            //! @param[out] linkDom The DOM to populate with source and patches information.
+            //! @param allocator The allocator to use for memory allocations of patches.
+            void GetLinkDom(PrefabDomValue& linkDom, PrefabDomAllocator& allocator) const;
+
+            //! Checks whether overrides are present by querying the patches tree with the provided path
+            //! @param path The path to query the overrides tree with.
+            //! @param prefixTreeTraversalFlags The traversal flags for the prefix tree. The default is to exclude parent paths because
+            //!                                 we usually check for overrides on one or more components/properties within an entity.
+            //! @return true if overrides are present at the provided path.
+            bool AreOverridesPresent(
+                AZ::Dom::Path path,
+                AZ::Dom::PrefixTreeTraversalFlags prefixTreeTraversalFlags = AZ::Dom::PrefixTreeTraversalFlags::ExcludeParentPaths);
 
             PrefabDomPath GetInstancePath() const;
             const AZStd::string& GetInstanceName() const;
@@ -75,8 +134,6 @@ namespace AzToolsFramework
              */
             void AddLinkIdToInstanceDom(PrefabDomValue& instanceDomValue);
 
-            PrefabDomValueReference GetLinkPatches();
-
         private:
 
             /**
@@ -85,7 +142,20 @@ namespace AzToolsFramework
              * @param instanceDomValue The DOM value of the instance within the target template DOM.
              * @param allocator The allocator used while adding the linkId object to the instance DOM.
              */
-            void AddLinkIdToInstanceDom(PrefabDomValue& instanceDomValue, PrefabDom::AllocatorType& allocator);
+            void AddLinkIdToInstanceDom(PrefabDomValue& instanceDomValue, PrefabDomAllocator& allocator);
+
+            //! Populates the DOM provided with the patches fetched from 'm_linkPatchesTree'
+            //! @param[out] linkDom The DOM to populate with patches
+            //! @param allocator The allocator to use for memory allocations of patches.
+            void ConstructLinkDomFromPatches(PrefabDomValue& linkDom, PrefabDomAllocator& allocator) const;
+
+            //! Clears the existing tree and rebuilds it from the provided patches.
+            //! @param patches The patches to build the tree with.
+            void RebuildLinkPatchesTree(const PrefabDomValue& patches);
+
+            // The prefix tree to store patches on a link. The tree is built with nodes. A node may or maynot store a patch.
+            // The path from the root to a node represents a path to a DOM value. Eg: 'Instances/Instance1/Entities/Entity1'.
+            AZ::Dom::DomPrefixTree<PrefabOverrideMetadata> m_linkPatchesTree;
 
             // Target template id for propagation during updating templates.
             TemplateId m_targetTemplateId = InvalidTemplateId;
@@ -93,9 +163,6 @@ namespace AzToolsFramework
             // Source template id for unlink templates if needed.
             TemplateId m_sourceTemplateId = InvalidTemplateId;
 
-            // JSON patches for overrides in Template.
-            PrefabDom m_linkDom;
-
             // Name of the nested instance of target Template.
             AZStd::string m_instanceName;
 

+ 30 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverrideHandler.cpp

@@ -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
+ *
+ */
+
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverrideHandler.h>
+#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
+
+ namespace AzToolsFramework
+{
+    namespace Prefab
+    {
+        bool PrefabOverrideHandler::AreOverridesPresent(AZ::Dom::Path path, LinkId linkId)
+        {
+            PrefabSystemComponentInterface* prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
+            if (prefabSystemComponentInterface != nullptr)
+            {
+                LinkReference link = prefabSystemComponentInterface->FindLink(linkId);
+                if (link.has_value())
+                {
+                    return link->get().AreOverridesPresent(path);
+                }
+            }
+            return false;
+        }
+    }
+}

+ 28 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverrideHandler.h

@@ -0,0 +1,28 @@
+/*
+ * 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/DOM/DomPath.h>
+#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
+
+namespace AzToolsFramework
+{
+    namespace Prefab
+    {
+        class PrefabOverrideHandler
+        {
+        public:
+            //! Checks whether overrides are present on the link object matching the linkId at the provided path.
+            //! @param path The path to check for overrides on the link object.
+            //! @param linkId The id of the link object to check for overrides
+            //! @return true if overrides are present at the given path on the link object matching the link id.
+            bool AreOverridesPresent(AZ::Dom::Path path, LinkId linkId);
+        };
+    } // namespace Prefab
+} // namespace AzToolsFramework

+ 65 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.cpp

@@ -0,0 +1,65 @@
+/*
+ * 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/Entity/EditorEntityContextBus.h>
+#include <AzToolsFramework/Prefab/Instance/InstanceToTemplateInterface.h>
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.h>
+#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
+#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
+#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
+
+namespace AzToolsFramework
+{
+    namespace Prefab
+    {
+        PrefabOverridePublicHandler::PrefabOverridePublicHandler()
+        {
+            AZ::Interface<PrefabOverridePublicInterface>::Register(this);
+
+            m_instanceToTemplateInterface = AZ::Interface<InstanceToTemplateInterface>::Get();
+            AZ_Assert(m_instanceToTemplateInterface, "PrefabOverridePublicHandler - InstanceToTemplateInterface could not be found.");
+
+            m_prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
+            AZ_Assert(m_prefabFocusInterface, "PrefabOverridePublicHandler - PrefabFocusInterface could not be found.");
+
+            m_prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
+            AZ_Assert(m_prefabSystemComponentInterface, "PrefabOverridePublicHandler - PrefabSystemComponentInterface could not be found.");
+        }
+
+        PrefabOverridePublicHandler::~PrefabOverridePublicHandler()
+        {
+            AZ::Interface<PrefabOverridePublicInterface>::Unregister(this);
+        }
+
+        bool PrefabOverridePublicHandler::AreOverridesPresent(AZ::EntityId entityId)
+        {
+            AzFramework::EntityContextId editorEntityContextId;
+            AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
+                editorEntityContextId, &AzToolsFramework::EditorEntityContextRequests::GetEditorEntityContextId);
+            InstanceOptionalReference focusedInstance = m_prefabFocusInterface->GetFocusedPrefabInstance(editorEntityContextId);
+
+            AZ::Dom::Path absoluteEntityAliasPath = m_instanceToTemplateInterface->GenerateEntityPathFromFocusedPrefab(entityId);
+
+            // The first 2 tokens of the path will represent the path of the instance below the focused prefab.
+            // Eg: "Instances/InstanceA/Instances/InstanceB/....'. The override tree doesn't store the topmost instance to avoid
+            // redundant checks Eg: "Instances/InstanceB/....' . So we skip the first 2 tokens here.
+            if (focusedInstance.has_value() && absoluteEntityAliasPath.size() > 2)
+            {
+                AZStd::string_view overriddenInstanceKey = absoluteEntityAliasPath[1].GetKey().GetStringView();
+                InstanceOptionalReference overriddenInstance = focusedInstance->get().FindNestedInstance(overriddenInstanceKey);
+                if (overriddenInstance.has_value())
+                {
+                    auto pathIterator = absoluteEntityAliasPath.begin() + 2;
+                    AZ::Dom::Path modifiedPath(pathIterator, absoluteEntityAliasPath.end());
+                    return m_prefabOverrideHandler.AreOverridesPresent(modifiedPath, overriddenInstance->get().GetLinkId());
+                }
+            }
+            return false;
+        }
+    } // namespace Prefab
+} // namespace AzToolsFramework

+ 42 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicHandler.h

@@ -0,0 +1,42 @@
+/*
+ * 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/Prefab/Overrides/PrefabOverrideHandler.h>
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverridePublicInterface.h>
+
+namespace AzToolsFramework
+{
+    namespace Prefab
+    {
+        class InstanceToTemplateInterface;
+        class PrefabFocusInterface;
+        class PrefabSystemComponentInterface;
+
+        class PrefabOverridePublicHandler : private PrefabOverridePublicInterface
+        {
+        public:
+            PrefabOverridePublicHandler();
+            virtual ~PrefabOverridePublicHandler();
+
+        private:
+            //! Checks whether overrides are present on the given entity id. Overrides can come from any ancestor prefab but
+            //! this function specifically checks for overrides from the focused prefab.
+            //! @param entityId The id of the entity to check for overrides.
+            //! @return true if overrides are present on the given entity id from the focused prefab.
+            bool AreOverridesPresent(AZ::EntityId entityId) override;
+
+            PrefabOverrideHandler m_prefabOverrideHandler;
+
+            InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr;
+            PrefabFocusInterface* m_prefabFocusInterface = nullptr;
+            PrefabSystemComponentInterface* m_prefabSystemComponentInterface = nullptr;
+        };
+    } // namespace Prefab
+} // namespace AzToolsFramework

+ 30 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Overrides/PrefabOverridePublicInterface.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/EntityId.h>
+#include <AzCore/RTTI/RTTI.h>
+
+namespace AzToolsFramework
+{
+    namespace Prefab
+    {
+        class PrefabOverridePublicInterface
+        {
+        public:
+            AZ_RTTI(PrefabOverridePublicInterface, "{19F080A2-BDD7-476F-AA50-C1581401FC81}");
+
+            //! Checks whether overrides are present on the given entity id. The prefab that creates the overrides is identified
+            //! by the class implmenting this interface based on certain selections in the editor. eg: the prefab currently being edited.
+            //! @param entityId The id of the entity to check for overrides.
+            //! @return true if overrides are present on the given entity id.
+            virtual bool AreOverridesPresent(AZ::EntityId entityId) = 0;
+        };
+    } // namespace Prefab
+} // namespace AzToolsFramework

+ 1 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomTypes.h

@@ -21,6 +21,7 @@ namespace AzToolsFramework
         using PrefabDomValue = rapidjson::Value;
         using PrefabDomPath = rapidjson::Pointer;
         using PrefabDomList = AZStd::vector<PrefabDom>;
+        using PrefabDomAllocator = PrefabDom::AllocatorType;
 
         using PrefabDomReference = AZStd::optional<AZStd::reference_wrapper<PrefabDom>>;
         using PrefabDomConstReference = AZStd::optional<AZStd::reference_wrapper<const PrefabDom>>;

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabInstanceUtils.cpp

@@ -89,7 +89,7 @@ namespace AzToolsFramework
                     relativePath.append((*instanceIter)->GetInstanceAlias());
                 }
 
-                return relativePath;
+                return AZStd::move(relativePath);
             }
 
             bool IsDescendantInstance(const Instance& childInstance, const Instance& parentInstance)

+ 4 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp

@@ -827,7 +827,8 @@ namespace AzToolsFramework
                 Link& link = findLinkResult->get();
 
                 PrefabDomPath instancePath = link.GetInstancePath();
-                PrefabDom& linkDom = link.GetLinkDom();
+                PrefabDom linkDom;
+                link.GetLinkDom(linkDom, output.GetAllocator());
 
                 // Get the instance value of the Template copy
                 // This currently stores a fully realized nested Template Dom
@@ -844,9 +845,9 @@ namespace AzToolsFramework
                     return false;
                 }
 
-                // Copy the contents of the Link to overwrite our Template Dom copies Instance
+                // Swap the contents of the Link dom with our nested instances dom in the template.
                 // The instance is now "collapsed" as it contains the file reference and patches from the link
-                instanceValue->CopyFrom(linkDom, prefabDom.GetAllocator());
+                instanceValue->Swap(linkDom);
             }
 
             // Remove Source parameter from the dom. It will be added on file load, and should not be stored to disk.

+ 18 - 32
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp

@@ -176,14 +176,9 @@ namespace AzToolsFramework
                     auto linkRef = m_prefabSystemComponentInterface->FindLink(detachingInstanceLinkId);
                     AZ_Assert(linkRef.has_value(), "Unable to find link with id '%llu' during prefab creation.", detachingInstanceLinkId);
 
-                    PrefabDomValueReference linkPatches = linkRef->get().GetLinkPatches();
-                    AZ_Assert(
-                        linkPatches.has_value(), "Unable to get patches on link with id '%llu' during prefab creation.",
-                        detachingInstanceLinkId);
-
-                    PrefabDom linkPatchesCopy;
-                    linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator());
-                    nestedInstanceLinkPatchesMap.emplace(nestedInstance, AZStd::move(linkPatchesCopy));
+                    PrefabDom linkPatches;
+                    linkRef->get().GetLinkPatches(linkPatches, linkPatches.GetAllocator());
+                    nestedInstanceLinkPatchesMap.emplace(nestedInstance, AZStd::move(linkPatches));
 
                     RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
 
@@ -250,7 +245,7 @@ namespace AzToolsFramework
                     // Retrieve the previous patch if it exists
                     if (nestedInstanceLinkPatchesMap.contains(nestedInstance.get()))
                     {
-                        previousPatch = AZStd::move(nestedInstanceLinkPatchesMap[nestedInstance.get()]);
+                        previousPatch.Swap(nestedInstanceLinkPatchesMap[nestedInstance.get()]);
                         UpdateLinkPatchesWithNewEntityAliases(previousPatch, oldEntityAliases, instanceToCreate->get());
                     }
 
@@ -586,15 +581,13 @@ namespace AzToolsFramework
                 "A valid link was not found for one of the instances provided as input for the CreatePrefab operation.");    
 
             PrefabDom patchesCopyForUndoSupport;
-            PrefabDomReference nestedInstanceLinkDom = nestedInstanceLink->get().GetLinkDom();
-            if (nestedInstanceLinkDom.has_value())
+            PrefabDom nestedInstanceLinkDom;
+            nestedInstanceLink->get().GetLinkDom(nestedInstanceLinkDom, nestedInstanceLinkDom.GetAllocator());
+            PrefabDomValueConstReference nestedInstanceLinkPatches =
+                PrefabDomUtils::FindPrefabDomValue(nestedInstanceLinkDom, PrefabDomUtils::PatchesName);
+            if (nestedInstanceLinkPatches.has_value())
             {
-                PrefabDomValueReference nestedInstanceLinkPatches =
-                    PrefabDomUtils::FindPrefabDomValue(nestedInstanceLinkDom->get(), PrefabDomUtils::PatchesName);
-                if (nestedInstanceLinkPatches.has_value())
-                {
-                    patchesCopyForUndoSupport.CopyFrom(nestedInstanceLinkPatches->get(), patchesCopyForUndoSupport.GetAllocator());
-                }
+                patchesCopyForUndoSupport.CopyFrom(nestedInstanceLinkPatches->get(), patchesCopyForUndoSupport.GetAllocator());
             }
 
             PrefabUndoHelpers::RemoveLink(
@@ -924,11 +917,7 @@ namespace AzToolsFramework
 
                     if (linkRef.has_value())
                     {
-                        auto patches = linkRef->get().GetLinkPatches();
-                        if (patches.has_value())
-                        {
-                            oldLinkPatches.CopyFrom(patches->get(), oldLinkPatches.GetAllocator());
-                        }
+                        linkRef->get().GetLinkPatches(oldLinkPatches, oldLinkPatches.GetAllocator());
                     }
 
                     auto nestedInstanceUniquePtr = beforeOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
@@ -1188,13 +1177,8 @@ namespace AzToolsFramework
                         linkRef.has_value(), "Unable to find link with id '%llu' during instance duplication.",
                         oldLinkId);
 
-                    PrefabDomValueReference linkPatches = linkRef->get().GetLinkPatches();
-                    AZ_Assert(
-                        linkPatches.has_value(), "Link with id '%llu' is missing patches.",
-                        oldLinkId);
-
-                    PrefabDom linkPatchesCopy;
-                    linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator());
+                    PrefabDom linkPatches;
+                    linkRef->get().GetLinkPatches(linkPatches, linkPatches.GetAllocator());
 
                     // If the instance was duplicated as part of an ancestor's nested hierarchy, the container's parent patch
                     // will need to be refreshed to point to the new duplicated parent entity
@@ -1212,19 +1196,21 @@ namespace AzToolsFramework
                             // Get the dom into a QString for search/replace purposes
                             rapidjson::StringBuffer buffer;
                             rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
-                            linkPatchesCopy.Accept(writer);
+                            linkPatches.Accept(writer);
 
                             QString linkPatchesString(buffer.GetString());
 
                             ReplaceOldAliases(linkPatchesString, oldParentAlias->get(), duplicateEntityAliasMap[oldParentAlias->get()]);
 
-                            linkPatchesCopy.Parse(linkPatchesString.toUtf8().constData());
+                            linkPatches.Parse(linkPatchesString.toUtf8().constData());
                         }
                     }
 
                     PrefabUndoHelpers::CreateLink(
                         oldInstance->GetTemplateId(), commonOwningInstance->get().GetTemplateId(),
-                        AZStd::move(linkPatchesCopy), newInstanceAlias, undoBatch.GetUndoBatch());
+                        AZStd::move(linkPatches),
+                        newInstanceAlias,
+                        undoBatch.GetUndoBatch());
                 }
 
                 // Select the duplicated entities/instances

+ 8 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp

@@ -24,6 +24,8 @@
 #include <AzToolsFramework/Prefab/Spawnable/PrefabConversionPipeline.h>
 #include <AzToolsFramework/Prefab/PrefabPublicNotificationHandler.h>
 
+AZ_DEFINE_BUDGET(PrefabSystem);
+
 namespace AzToolsFramework
 {
     namespace Prefab
@@ -785,14 +787,16 @@ namespace AzToolsFramework
             newLink.SetTargetTemplateId(linkTargetId);
             newLink.SetSourceTemplateId(linkSourceId);
             newLink.SetInstanceName(instanceAlias.c_str());
-            newLink.GetLinkDom().SetObject();
-            newLink.GetLinkDom().AddMember(
+            PrefabDom newLinkDom;
+            newLinkDom.SetObject();
+            newLinkDom.AddMember(
                 rapidjson::StringRef(PrefabDomUtils::SourceName), rapidjson::StringRef(sourceTemplate.GetFilePath().c_str()),
-                newLink.GetLinkDom().GetAllocator());
+                newLinkDom.GetAllocator());
+            newLink.SetLinkDom(newLinkDom);
 
             if (linkPatches && linkPatches->get().IsArray() && !(linkPatches->get().Empty()))
             {
-                m_instanceToTemplatePropagator.AddPatchesToLink(linkPatches.value(), newLink);
+                newLink.AddPatchesToLink(linkPatches.value());
             }
 
             //update the target template dom to have the proper values for the source template dom

+ 2 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.h

@@ -31,6 +31,8 @@
 #include <AzToolsFramework/Prefab/Template/Template.h>
 #include <Prefab/PrefabSystemScriptingHandler.h>
 
+AZ_DECLARE_BUDGET(PrefabSystem);
+
 namespace AZ
 {
     class Entity;

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Undo/PrefabUndoUpdateLink.cpp

@@ -62,7 +62,7 @@ namespace AzToolsFramework
             SetLink(linkId);
 
             //Cache current link DOM for undo link update.
-            m_undoPatch.CopyFrom(m_link->get().GetLinkDom(), m_undoPatch.GetAllocator());
+            m_link->get().GetLinkDom(m_undoPatch, m_undoPatch.GetAllocator());
 
             //Get DOM of the link's source template.
             TemplateId sourceTemplateId = m_link->get().GetSourceTemplateId();

+ 36 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp

@@ -14,6 +14,7 @@
 #include <AzToolsFramework/ContainerEntity/ContainerEntityInterface.h>
 #include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
 #include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverridePublicInterface.h>
 #include <AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx>
 #include <QAbstractItemModel>
 #include <QApplication>
@@ -53,6 +54,13 @@ namespace AzToolsFramework
             return;
         }
 
+        m_prefabOverridePublicInterface = AZ::Interface<Prefab::PrefabOverridePublicInterface>::Get();
+        if (m_prefabOverridePublicInterface == nullptr)
+        {
+            AZ_Assert(false, "PrefabUiHandler - could not get PrefabOverridePublicInterface on PrefabUiHandler construction.");
+            return;
+        }
+
         // Get EditorEntityContextId
         EditorEntityContextRequestBus::BroadcastResult(s_editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
     }
@@ -234,6 +242,34 @@ namespace AzToolsFramework
             return;
         }
 
+        if (descendantIndex.column() == EntityOutlinerListModel::ColumnName)
+        {
+            AZ::EntityId descendantEntityId = GetEntityIdFromIndex(descendantIndex);
+
+            // If the entity is not in the focus hierarchy, we needn't add override visualization
+            // as overrides are only shown from the current focused prefab.
+            if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(descendantEntityId))
+            {
+                // Container entities will always have overrides because they need to maintain unique positions in the scene.
+                // We are skipping checking for overrides on container entities for this reason.
+                if (!m_prefabPublicInterface->IsInstanceContainerEntity(descendantEntityId) &&
+                    m_prefabOverridePublicInterface->AreOverridesPresent(descendantEntityId))
+                {
+                    // Build the rect that will be used to paint the icon
+                    QRect overrideIconBounds =
+                        QRect(option.rect.topLeft() + s_overrideIconOffset, QSize(s_overrideIconSize * 2, s_overrideIconSize * 2));
+
+                    painter->save();
+                    painter->setRenderHint(QPainter::Antialiasing, true);
+                    painter->setPen(Qt::NoPen);
+                    painter->setBrush(s_overrideIconBackgroundColor);
+                    painter->drawEllipse(overrideIconBounds.center(), s_overrideIconSize, s_overrideIconSize);
+                    s_overrideIcon.paint(painter, overrideIconBounds);
+                    painter->restore();
+                }
+            }
+        }
+
         AZ::EntityId entityId = GetEntityIdFromIndex(index);
 
         // If the owning prefab is not focused, the border will be painted in the background.

+ 7 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h

@@ -19,6 +19,7 @@ namespace AzToolsFramework
     namespace Prefab
     {
         class PrefabFocusPublicInterface;
+        class PrefabOverridePublicInterface;
         class PrefabPublicInterface;
     }; // namespace Prefab
 
@@ -56,6 +57,7 @@ namespace AzToolsFramework
         ContainerEntityInterface* m_containerEntityInterface = nullptr;
         Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
         Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr;
+        Prefab::PrefabOverridePublicInterface* m_prefabOverridePublicInterface = nullptr;
 
         static bool IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child);
         static QModelIndex GetLastVisibleChild(const QModelIndex& parent);
@@ -85,5 +87,10 @@ namespace AzToolsFramework
         QString m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg");
         QString m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg");
         QString m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg");
+
+        inline static const QColor s_overrideIconBackgroundColor = QColor("#444444");
+        inline static const QPoint s_overrideIconOffset = QPoint(10, 10);
+        inline static const int s_overrideIconSize = 5;
+        QIcon s_overrideIcon = QIcon(QString(":/Entity/entity_overridden.svg"));
     };
 } // namespace AzToolsFramework

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

@@ -791,6 +791,11 @@ set(FILES
     Prefab/Instance/TemplateInstanceMapperInterface.h
     Prefab/Link/Link.h
     Prefab/Link/Link.cpp
+    Prefab/Overrides/PrefabOverrideHandler.h
+    Prefab/Overrides/PrefabOverrideHandler.cpp
+    Prefab/Overrides/PrefabOverridePublicInterface.h
+    Prefab/Overrides/PrefabOverridePublicHandler.h
+    Prefab/Overrides/PrefabOverridePublicHandler.cpp
     Prefab/Procedural/ProceduralPrefabAsset.h
     Prefab/Procedural/ProceduralPrefabAsset.cpp
     Prefab/PrefabPublicHandler.h

+ 16 - 7
Code/Framework/AzToolsFramework/Tests/ComponentModeSwitcherTests.cpp

@@ -427,11 +427,15 @@ namespace UnitTest
             AzToolsFramework::GetEntityContextId(),
             &AzToolsFramework::EditorTransformComponentSelectionRequestBus::Events::OverrideComponentModeSwitcher,
             componentModeSwitcher);
+            
         AZ::Entity* entity = nullptr;
         AZ::EntityId entityId = CreateDefaultEditorEntity("ComponentModeEntity", &entity);
 
+        // connect to EditorDisabledCompositionRequestBus with entityId
+        Connect(entityId);
+
         entity->Deactivate();
-        const AZ::Component* placeholder1 = entity->CreateComponent<PlaceholderEditorComponent>();
+        AZ::Component* placeholder1 = entity->CreateComponent<PlaceholderEditorComponent>();
         entity->CreateComponent<AnotherPlaceholderEditorComponent>();
         entity->Activate();
 
@@ -441,16 +445,21 @@ namespace UnitTest
             &AzToolsFramework::ToolsApplicationRequests::SetSelectedEntities, entityIds);
         EXPECT_EQ(2, componentModeSwitcher->GetComponentCount());
 
-        AzToolsFramework::EntityCompositionNotificationBus::Broadcast(
-            &AzToolsFramework::EntityCompositionNotificationBus::Events::OnEntityComponentDisabled, entity->GetId(), placeholder1->GetId());
+        AzToolsFramework::EntityCompositionRequestBus::Broadcast(
+            &AzToolsFramework::EntityCompositionRequests::DisableComponents, AZStd::vector<AZ::Component*>{ placeholder1 });
+
+        AzToolsFramework::EditorDisabledCompositionRequestBus::Event(
+            entityId, &AzToolsFramework::EditorDisabledCompositionRequests::AddDisabledComponent, placeholder1);
+
         EXPECT_EQ(1, componentModeSwitcher->GetComponentCount());
 
-        AzToolsFramework::EntityCompositionNotificationBus::Broadcast(
-            &AzToolsFramework::EntityCompositionNotificationBus::Events::OnEntityComponentEnabled, entity->GetId(), placeholder1->GetId());
+        AddDisabledComponentToBus(placeholder1);
 
-        AzToolsFramework::ToolsApplicationRequestBus::Broadcast(
-            &AzToolsFramework::ToolsApplicationRequests::SetSelectedEntities, entityIds);
+        AzToolsFramework::EntityCompositionRequestBus::Broadcast(
+            &AzToolsFramework::EntityCompositionRequests::EnableComponents, AZStd::vector<AZ::Component*>{ placeholder1 });
 
         EXPECT_EQ(2, componentModeSwitcher->GetComponentCount());
+
+        Disconnect();
     }
 } // namespace UnitTest

+ 39 - 0
Code/Framework/AzToolsFramework/Tests/ComponentModeTestFixture.cpp

@@ -26,4 +26,43 @@ namespace UnitTest
             AztfCmf::TestComponentModeComponent<AztfCmf::OverrideMouseInteractionComponentMode>::CreateDescriptor());
         app->RegisterComponentDescriptor(AztfCmf::IncompatiblePlaceholderEditorComponent::CreateDescriptor());
     }
+
+    void ComponentModeTestFixture::Connect(AZ::EntityId entityId)
+    {
+        AzToolsFramework::EditorDisabledCompositionRequestBus::Handler::BusConnect(entityId);
+        m_connectedEntity = entityId;
+    }
+
+    void ComponentModeTestFixture::Disconnect()
+    {
+        if (m_connectedEntity.IsValid())
+        {
+            AzToolsFramework::EditorDisabledCompositionRequestBus::Handler::BusDisconnect();
+            m_connectedEntity.SetInvalid();
+        }
+    }
+
+    void ComponentModeTestFixture::TearDownEditorFixtureImpl()
+    {
+        Disconnect();
+    }
+
+    void ComponentModeTestFixture::GetDisabledComponents(AZStd::vector<AZ::Component*>& components)
+    {
+         if (m_disabledComponent != nullptr)
+         {
+             components.push_back(m_disabledComponent);
+         }
+    }
+
+    void ComponentModeTestFixture::AddDisabledComponentToBus(AZ::Component* component)
+    {
+        if (component != nullptr)
+        {
+            m_disabledComponent = component;
+        }
+    }
+
+    void ComponentModeTestFixture::AddDisabledComponent([[maybe_unused]] AZ::Component* componentToAdd){};
+    void ComponentModeTestFixture::RemoveDisabledComponent([[maybe_unused]] AZ::Component* componentToRemove){};
 } // namespace UnitTest

+ 17 - 0
Code/Framework/AzToolsFramework/Tests/ComponentModeTestFixture.h

@@ -10,14 +10,31 @@
 
 #include <AzToolsFramework/Application/ToolsApplication.h>
 #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
+#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
+
 #include <AzCore/UnitTest/TestTypes.h>
 
 namespace UnitTest
 {
     class ComponentModeTestFixture
         : public ToolsApplicationFixture
+        , public AzToolsFramework::EditorDisabledCompositionRequestBus::Handler
     {
+    public:
+        // EditorDisabledCompositionRequestBus overrides ...
+        void GetDisabledComponents(AZStd::vector<AZ::Component*>& components) override;
+        void AddDisabledComponent(AZ::Component* componentToAdd) override;
+        void RemoveDisabledComponent(AZ::Component* componentToRemove) override;
+
+        void Connect(AZ::EntityId entityId);
+        void Disconnect();
+        void AddDisabledComponentToBus(AZ::Component*);
+
     protected:
         void SetUpEditorFixtureImpl() override;
+        void TearDownEditorFixtureImpl() override;
+        
+        AZ::EntityId m_connectedEntity;
+        AZ::Component* m_disabledComponent = nullptr;
     };
 } // namespace UnitTest

+ 2 - 1
Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/Link/SingleInstanceMultiplePatchesBenchmarks.cpp

@@ -99,7 +99,8 @@ namespace Benchmark
         {
             LinkReference link = m_prefabSystemComponent->FindLink(m_linkId);
             AZ_Assert(link.has_value(), "Link between prefabs is missing.");
-            link->get().GetLinkDom();
+            PrefabDom linkDom;
+            link->get().GetLinkDom(linkDom, linkDom.GetAllocator());
         }
     }
     REGISTER_MULTIPLE_PATCHES_BENCHMARK(SingleInstanceMultiplePatchesBenchmarks, GetLinkDom);

+ 37 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTestFixture.cpp

@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Prefab/Link/PrefabLinkDomTestFixture.h>
+#include <Prefab/MockPrefabFileIOActionValidator.h>
+#include <Prefab/PrefabTestDomUtils.h>
+
+namespace UnitTest
+{
+    void PrefabLinkDomTestFixture::SetUpEditorFixtureImpl()
+    {
+        PrefabTestFixture::SetUpEditorFixtureImpl();
+
+        m_templateData.m_filePath = "PathToSourceTemplate";
+
+        MockPrefabFileIOActionValidator mockIOActionValidator;
+        mockIOActionValidator.ReadPrefabDom(m_templateData.m_filePath, PrefabTestDomUtils::CreatePrefabDom());
+
+        m_templateData.m_id = m_prefabLoaderInterface->LoadTemplateFromFile(m_templateData.m_filePath);
+
+        m_link = AZStd::make_unique<Link>(0);
+        m_link->SetTargetTemplateId(1);
+        m_link->SetSourceTemplateId(m_templateData.m_id);
+        m_link->SetInstanceName("SomeInstanceName");
+    }
+
+    void PrefabLinkDomTestFixture::TearDownEditorFixtureImpl()
+    {
+        m_link.reset();
+        PrefabTestFixture::TearDownEditorFixtureImpl();
+    }
+} // namespace UnitTest

+ 31 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTestFixture.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <Prefab/PrefabTestFixture.h>
+
+namespace UnitTest
+{
+    using namespace AzToolsFramework::Prefab;
+
+    class PrefabLinkDomTestFixture
+        : public PrefabTestFixture
+    {
+    protected:
+        void SetUpEditorFixtureImpl() override;
+        void TearDownEditorFixtureImpl() override;
+
+        // Object to store data about template used for tests.
+        TemplateData m_templateData;
+
+        // Link used for testing DOM operations.
+        AZStd::unique_ptr<Link> m_link;
+        
+    };
+} // namespace UnitTest

+ 72 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Link/PrefabLinkDomTests.cpp

@@ -0,0 +1,72 @@
+/*
+ * 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 <Prefab/PrefabTestDomUtils.h>
+#include <Prefab/Link/PrefabLinkDomTestFixture.h>
+
+namespace UnitTest
+{
+    using PrefabLinkDomTest = PrefabLinkDomTestFixture;
+
+    // Mock patches to use for validating tests.
+    static constexpr const AZStd::string_view patchesValue = R"(
+            [
+                {
+                    "op": "add",
+                    "path": "Entities/Entity1/Components/ComponentA/IntValue",
+                    "value": 10
+                },
+                {
+                    "op": "remove",
+                    "path": "Entities/Entity2/Components/ComponentB/FloatValue"
+                },
+                {
+                    "op": "replace",
+                    "path": "Entities/Entity1/Components/ComponentC/StringValue",
+                    "value": "replacedString"
+                }
+            ])";
+
+    TEST_F(PrefabLinkDomTest, GetLinkDomRetainsPatchOrder)
+    {
+        PrefabDom newLinkDom;
+        newLinkDom.Parse(R"(
+            {
+                "Source": "PathToSourceTemplate"
+            })");
+
+        PrefabDom patchesCopy;
+        patchesCopy.Parse(patchesValue.data());
+        newLinkDom.AddMember(rapidjson::StringRef(PrefabDomUtils::PatchesName), AZStd::move(patchesCopy), newLinkDom.GetAllocator());
+        m_link->SetLinkDom(newLinkDom);
+
+        // Get the link DOM and verify that it matches the DOM used for SetLinkDom().
+        PrefabDom fetchedLinkDom;
+        m_link->GetLinkDom(fetchedLinkDom, fetchedLinkDom.GetAllocator());
+        EXPECT_EQ(AZ::JsonSerialization::Compare(newLinkDom, fetchedLinkDom), AZ::JsonSerializerCompareResult::Equal);
+    }
+
+    TEST_F(PrefabLinkDomTest, AddPatchesToLinkRetainsPatchOrder)
+    {
+        PrefabDom newLinkDom;
+        newLinkDom.Parse(R"(
+            {
+                "Source": "PathToSourceTemplate"
+            })");
+
+        PrefabDom patchesCopy;
+        patchesCopy.Parse(patchesValue.data());
+        m_link->AddPatchesToLink(patchesCopy);
+        newLinkDom.AddMember(rapidjson::StringRef(PrefabDomUtils::PatchesName), AZStd::move(patchesCopy), newLinkDom.GetAllocator());
+
+        // Get the link DOM and verify that it matches the DOM used for AddPatchesToLink().
+        PrefabDom fetchedLinkDom;
+        m_link->GetLinkDom(fetchedLinkDom, fetchedLinkDom.GetAllocator());
+        EXPECT_EQ(AZ::JsonSerialization::Compare(newLinkDom, fetchedLinkDom), AZ::JsonSerializerCompareResult::Equal);
+    }
+} // namespace UnitTest

+ 39 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverridePublicInterfaceTests.cpp

@@ -0,0 +1,39 @@
+/*
+ * 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/ToolsComponents/TransformComponent.h>
+#include <Prefab/Overrides/PrefabOverrideTestFixture.h>
+
+namespace UnitTest
+{
+    using PrefabOverridePublicInterfaceTest = PrefabOverrideTestFixture;
+
+    TEST_F(PrefabOverridePublicInterfaceTest, AreOverridesPresentWorksWithOverrideFromImmediateParent)
+    {
+        AZ::EntityId newEntityId, parentContainerId, grandparentContainerId;
+        CreateEntityInNestedPrefab(newEntityId, parentContainerId, grandparentContainerId);
+        
+        CreateAndValidateEditEntityOverride(newEntityId, grandparentContainerId);
+    }
+
+    TEST_F(PrefabOverridePublicInterfaceTest, AreOverridesPresentWorksWithOverrideFromLevel)
+    {
+        AZ::EntityId newEntityId, parentContainerId, grandparentContainerId;
+        CreateEntityInNestedPrefab(newEntityId, parentContainerId, grandparentContainerId);
+        AZ::EntityId levelContainerId = m_prefabEditorEntityOwnershipInterface->GetRootPrefabInstance()->get().GetContainerEntityId();
+        CreateAndValidateEditEntityOverride(newEntityId, levelContainerId);
+    }
+
+    TEST_F(PrefabOverridePublicInterfaceTest, AreOverridesPresentReturnsFalseWhenNoOverride)
+    {
+        AZ::EntityId newEntityId, parentContainerId, grandparentContainerId;
+        CreateEntityInNestedPrefab(newEntityId, parentContainerId, grandparentContainerId);
+
+        EditEntityAndValidateNoOverride(newEntityId);
+    }
+} // namespace UnitTest

+ 87 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverrideTestFixture.cpp

@@ -0,0 +1,87 @@
+/*
+ * 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 <Prefab/Overrides/PrefabOverrideTestFixture.h>
+
+namespace UnitTest
+{
+    void PrefabOverrideTestFixture::SetUpEditorFixtureImpl()
+    {
+        PrefabTestFixture::SetUpEditorFixtureImpl();
+
+        m_prefabOverridePublicInterface = AZ::Interface<PrefabOverridePublicInterface>::Get();
+        ASSERT_TRUE(m_prefabOverridePublicInterface);
+
+        m_prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
+        ASSERT_TRUE(m_prefabFocusPublicInterface);
+
+        m_settingsRegistryInterface = AZ::SettingsRegistry::Get();
+        ASSERT_TRUE(m_settingsRegistryInterface);
+    }
+
+    void PrefabOverrideTestFixture::CreateEntityInNestedPrefab(
+        AZ::EntityId& newEntityId, AZ::EntityId& parentContainerId, AZ::EntityId& grandparentContainerId)
+    {
+        AZ::EntityId entityToBePutUnderPrefabId = CreateEntityUnderRootPrefab("EntityUnderPrefab");
+
+        AZ::IO::Path path;
+        m_settingsRegistryInterface->Get(path.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
+
+        AZ::EntityId nestedPrefabContainerId = CreatePrefab(AzToolsFramework::EntityIdList{ entityToBePutUnderPrefabId }, path);
+
+        // Append '1' to the path so that there is no path collision when creating another prefab.
+        grandparentContainerId = CreatePrefab(AzToolsFramework::EntityIdList{ nestedPrefabContainerId }, path.Append("1"));
+
+        InstanceOptionalReference prefabInstance = m_instanceEntityMapperInterface->FindOwningInstance(grandparentContainerId);
+        EXPECT_TRUE(prefabInstance.has_value());
+
+        // Fetch the id of the entity within the nested prefab as it changes after putting it in a prefab.
+        prefabInstance->get().GetNestedInstances(
+            [&newEntityId, &parentContainerId](AZStd::unique_ptr<Instance>& nestedInstance)
+            {
+                nestedInstance->GetEntities(
+                    [&newEntityId](const AZStd::unique_ptr<AZ::Entity>& entity)
+                    {
+                        newEntityId = entity->GetId();
+                        return true;
+                    });
+                parentContainerId = nestedInstance->GetContainerEntityId();
+            });
+    }
+
+    void PrefabOverrideTestFixture::CreateAndValidateEditEntityOverride(AZ::EntityId entityId, AZ::EntityId ancestorEntityId)
+    {
+        m_prefabFocusPublicInterface->FocusOnOwningPrefab(ancestorEntityId);
+
+        // Validate that there are no overrides present on the entity.
+        ASSERT_FALSE(m_prefabOverridePublicInterface->AreOverridesPresent(entityId));
+
+        // Modify the transform component.
+        AZ::TransformBus::Event(entityId, &AZ::TransformInterface::SetWorldX, 10.0f);
+        m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop());
+
+        // Validate that override is present on the entity.
+        EXPECT_TRUE(m_prefabOverridePublicInterface->AreOverridesPresent(entityId));
+    }
+
+    void PrefabOverrideTestFixture::EditEntityAndValidateNoOverride(AZ::EntityId entityId)
+    {
+        m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
+
+        // Validate that there are no overrides present on the entity.
+        ASSERT_FALSE(m_prefabOverridePublicInterface->AreOverridesPresent(entityId));
+
+        // Modify the transform component.
+        AZ::TransformBus::Event(entityId, &AZ::TransformInterface::SetWorldX, 10.0f);
+        m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop());
+
+        // Validate that overrides are still not present on the entity since the edit went to the template DOM directly.
+        EXPECT_FALSE(m_prefabOverridePublicInterface->AreOverridesPresent(entityId));
+    }
+
+} // namespace UnitTest

+ 49 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/Overrides/PrefabOverrideTestFixture.h

@@ -0,0 +1,49 @@
+/*
+ * 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/Settings/SettingsRegistryMergeUtils.h>
+#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
+#include <AzToolsFramework/Prefab/Overrides/PrefabOverridePublicInterface.h>
+#include <Prefab/PrefabTestFixture.h>
+
+namespace UnitTest
+{
+    using namespace AzToolsFramework::Prefab;
+
+    class PrefabOverrideTestFixture : public PrefabTestFixture
+    {
+    protected:
+        void SetUpEditorFixtureImpl() override;
+
+        //! Creates an entity within a nested prefab under level. The setup looks like:
+        //! | Level
+        //!   | Prefab (grandparentContainerId)
+        //!     | Nested prefab (parentContainerId)
+        //!       | New Entity
+        //! @param newEntityId The new entity id created under nested prefab.
+        //! @param parentContainerId The container entity id of the nested prefab.
+        //! @param grandparentContainerId The container entity id of the top-level prefab.
+        void CreateEntityInNestedPrefab(AZ::EntityId& newEntityId, AZ::EntityId& parentContainerId, AZ::EntityId& grandparentContainerId);
+
+        //! Focuses on the owning instance of the ancestor entity id and modifies the entity matching the entity id.
+        //! This will make the edit become an override.
+        //! @param entityId The id of entity to modify.
+        //! @param ancestorEntityId The id of an ancestor entity to use for focusing on its owning prefab.
+        void CreateAndValidateEditEntityOverride(AZ::EntityId entityId, AZ::EntityId ancestorEntityId);
+
+        //! Focuses on the owning instance of the entity id and modifies it, which makes this a template edit.
+        //! @param entityId The Id of the entity to modify.
+        void EditEntityAndValidateNoOverride(AZ::EntityId entityId);
+
+        PrefabOverridePublicInterface* m_prefabOverridePublicInterface = nullptr;
+        PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
+        AZ::SettingsRegistryInterface* m_settingsRegistryInterface = nullptr;
+    };
+} // namespace UnitTest

+ 3 - 2
Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestDataUtils.cpp

@@ -81,8 +81,9 @@ namespace UnitTest
 
         void ValidateTemplatePatches(const Link& actualLink, const PrefabDom& expectedTemplatePatches)
         {
-            PrefabDomValueConstReference patchesReference =
-                PrefabDomUtils::FindPrefabDomValue(actualLink.GetLinkDom(), PrefabDomUtils::PatchesName);
+            PrefabDom linkDom;
+            actualLink.GetLinkDom(linkDom, linkDom.GetAllocator());
+            PrefabDomValueConstReference patchesReference = PrefabDomUtils::FindPrefabDomValue(linkDom, PrefabDomUtils::PatchesName);
 
             if (!expectedTemplatePatches.IsNull())
             {

+ 30 - 5
Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp

@@ -9,10 +9,12 @@
 #include <Prefab/PrefabTestFixture.h>
 
 #include <AzCore/Component/TransformBus.h>
+#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
+#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
 #include <AzToolsFramework/Prefab/PrefabDomUtils.h>
+#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
 #include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
 #include <AzToolsFramework/ToolsComponents/TransformComponent.h>
-#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
 #include <Prefab/PrefabTestComponent.h>
 #include <Prefab/PrefabTestDomUtils.h>
 
@@ -38,16 +40,19 @@ namespace UnitTest
         m_prefabSystemComponent = systemEntity->FindComponent<AzToolsFramework::Prefab::PrefabSystemComponent>();
         EXPECT_TRUE(m_prefabSystemComponent);
 
-        m_prefabLoaderInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabLoaderInterface>::Get();
+        m_prefabLoaderInterface = AZ::Interface<PrefabLoaderInterface>::Get();
         EXPECT_TRUE(m_prefabLoaderInterface);
 
-        m_prefabPublicInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabPublicInterface>::Get();
+        m_prefabPublicInterface = AZ::Interface<PrefabPublicInterface>::Get();
         EXPECT_TRUE(m_prefabPublicInterface);
 
-        m_instanceUpdateExecutorInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceUpdateExecutorInterface>::Get();
+        m_instanceEntityMapperInterface = AZ::Interface<InstanceEntityMapperInterface>::Get();
+        EXPECT_TRUE(m_instanceEntityMapperInterface);
+
+        m_instanceUpdateExecutorInterface = AZ::Interface<InstanceUpdateExecutorInterface>::Get();
         EXPECT_TRUE(m_instanceUpdateExecutorInterface);
 
-        m_instanceToTemplateInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceToTemplateInterface>::Get();
+        m_instanceToTemplateInterface = AZ::Interface<InstanceToTemplateInterface>::Get();
         EXPECT_TRUE(m_instanceToTemplateInterface);
 
         m_prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
@@ -99,6 +104,10 @@ namespace UnitTest
             {
                 rootContainerEntity->get().Init();
             }
+
+            auto* prefabFocusPublicInterface = AZ::Interface<PrefabFocusPublicInterface>::Get();
+            ASSERT_TRUE(prefabFocusPublicInterface != nullptr);
+            prefabFocusPublicInterface->FocusOnOwningPrefab(rootContainerEntity->get().GetId());
         }
     }
 
@@ -156,6 +165,22 @@ namespace UnitTest
         return entityId;
     }
 
+    AZ::EntityId PrefabTestFixture::CreatePrefab(const AzToolsFramework::EntityIdList& entityIds, AZ::IO::PathView filePath)
+    {
+        CreatePrefabResult createPrefabResult = m_prefabPublicInterface->CreatePrefabInMemory(entityIds, filePath);
+
+        // Verify that a valid prefab container entity is created.
+        AZ::EntityId prefabContainerId = createPrefabResult.GetValue();
+        AZ_Assert(prefabContainerId.IsValid(), "CreatePrefab operation resulted in an invalid container entity id.");
+        AZ::Entity* prefabContainerEntity = AzToolsFramework::GetEntityById(prefabContainerId);
+        AZ_Assert(prefabContainerEntity != nullptr, "ContainerEntity created with CreatePrefab() is a nullptr.");
+
+        // PrefabTestFixture won't add required editor components by default. Hence we add them here.
+        AddRequiredEditorComponents(prefabContainerEntity);
+
+        return prefabContainerId;
+    }
+
     void PrefabTestFixture::CompareInstances(const AzToolsFramework::Prefab::Instance& instanceA,
                                              const AzToolsFramework::Prefab::Instance& instanceB, bool shouldCompareLinkIds, bool shouldCompareContainerEntities)
     {

+ 3 - 0
Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h

@@ -18,6 +18,7 @@ namespace AzToolsFramework
 {
     namespace Prefab
     {
+        class InstanceEntityMapperInterface;
         class PrefabLoaderInterface;
     }
 
@@ -59,6 +60,7 @@ namespace UnitTest
         void InitializeRootPrefab();
         AZ::Entity* CreateEntity(const AZStd::string& entityName, bool shouldActivate = true);
         AZ::EntityId CreateEntityUnderRootPrefab(AZStd::string name, AZ::EntityId parentId = AZ::EntityId());
+        AZ::EntityId CreatePrefab(const AzToolsFramework::EntityIdList& entityIds, AZ::IO::PathView filePath);
         void PropagateAllTemplateChanges();
 
         void CompareInstances(const Instance& instanceA, const Instance& instanceB, bool shouldCompareLinkIds = true,
@@ -83,6 +85,7 @@ namespace UnitTest
         PrefabSystemComponent* m_prefabSystemComponent = nullptr;
         PrefabLoaderInterface* m_prefabLoaderInterface = nullptr;
         PrefabPublicInterface* m_prefabPublicInterface = nullptr;
+        InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
         InstanceUpdateExecutorInterface* m_instanceUpdateExecutorInterface = nullptr;
         InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr;
         AzToolsFramework::PrefabEditorEntityOwnershipInterface* m_prefabEditorEntityOwnershipInterface = nullptr;

+ 6 - 0
Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake

@@ -88,9 +88,15 @@ set(FILES
     Prefab/Benchmark/Spawnable/SpawnableBenchmarkFixture.cpp
     Prefab/Benchmark/Spawnable/SpawnAllEntitiesBenchmarks.cpp
     Prefab/Instance/InstanceDeserializationTests.cpp
+    Prefab/Link/PrefabLinkDomTestFixture.cpp
+    Prefab/Link/PrefabLinkDomTestFixture.h
+    Prefab/Link/PrefabLinkDomTests.cpp
     Prefab/PrefabFocus/PrefabFocusTests.cpp
     Prefab/MockPrefabFileIOActionValidator.cpp
     Prefab/MockPrefabFileIOActionValidator.h
+    Prefab/Overrides/PrefabOverridePublicInterfaceTests.cpp
+    Prefab/Overrides/PrefabOverrideTestFixture.cpp
+    Prefab/Overrides/PrefabOverrideTestFixture.h
     Prefab/PrefabDeleteTests.cpp
     Prefab/PrefabDuplicateTests.cpp
     Prefab/PrefabEntityAliasTests.cpp

+ 1 - 1
Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h

@@ -91,7 +91,7 @@ namespace AZ
             //! @param elevateWarnings Indicates whether to treat warnings as errors
             Outcome<Data::Asset<MaterialAsset>> CreateMaterialAsset(
                 Data::AssetId assetId,
-                AZStd::string_view materialSourceFilePath,
+                const AZStd::string& materialSourceFilePath,
                 MaterialAssetProcessingMode processingMode,
                 bool elevateWarnings = true) const;
 

+ 5 - 1
Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialTypeBuilder.cpp

@@ -43,7 +43,7 @@ namespace AZ
         {
             AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor;
             materialBuilderDescriptor.m_name = "Material Type Builder";
-            materialBuilderDescriptor.m_version = 5; // Material pipelines support multiple lighting models
+            materialBuilderDescriptor.m_version = 6; // Fixed shader path casing
             materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.materialtype", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
             materialBuilderDescriptor.m_busId = azrtti_typeid<MaterialTypeBuilder>();
             materialBuilderDescriptor.m_createJobFunction = AZStd::bind(&MaterialTypeBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
@@ -559,6 +559,10 @@ namespace AZ
                 materialType.m_shaderCollection.push_back({});
                 materialType.m_shaderCollection.back().m_shaderFilePath = AZ::IO::Path{outputShaderFilePath.Filename()}.c_str();
 
+                // Files in the cache, including intermediate files, end up using lower case for all files and folders. We have to match this
+                // in the output .materialtype file, because the asset system's source dependencies are case-sensitive on some platforms.
+                AZStd::to_lower(materialType.m_shaderCollection.back().m_shaderFilePath.begin(), materialType.m_shaderCollection.back().m_shaderFilePath.end());
+
                 // TODO(MaterialPipeline): We should warn the user if the shader collection has multiple shaders that use the same draw list.
             }
 

+ 2 - 2
Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp

@@ -119,7 +119,7 @@ namespace AZ
         }
 
         Outcome<Data::Asset<MaterialAsset>> MaterialSourceData::CreateMaterialAsset(
-            Data::AssetId assetId, AZStd::string_view materialSourceFilePath, MaterialAssetProcessingMode processingMode, bool elevateWarnings) const
+            Data::AssetId assetId, const AZStd::string& materialSourceFilePath, MaterialAssetProcessingMode processingMode, bool elevateWarnings) const
         {
             MaterialAssetCreator materialAssetCreator;
             materialAssetCreator.SetElevateWarnings(elevateWarnings);
@@ -165,7 +165,7 @@ namespace AZ
                 {
                     // In this case we need to load the material type data in preparation for the material->Finalize() step below.
                     auto materialTypeAssetOutcome = AssetUtils::LoadAsset<MaterialTypeAsset>(
-                        materialTypeAssetId.GetValue(), AssetUtils::TraceLevel::Error, dontLoadImageAssets);
+                        materialTypeAssetId.GetValue(), materialSourceFilePath.c_str(), AssetUtils::TraceLevel::Error, dontLoadImageAssets);
                     if (!materialTypeAssetOutcome)
                     {
                         return Failure();

+ 0 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/Lib/3.x/3.10.x/site-packages/stub


+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/rocks_grid.mb

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b42b78b5e4c6c863cb190877947deae45dff5c8b1ed3449e503f07a4092cc5ef
+size 1120972

+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_BaseColor.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5f931afcdb67bd6185c6c0aa659160695dc2f65f66cda0b8c0d7791d5842b283
+size 20792809

+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Height.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5b0372da1adfd4bc45136eed04849ce1f6d35d845ac690adf9450669848471e6
+size 25415

+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Metallic.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f790625c24510389aba626a3b4ea80806ad5012b1f838bfbee3b69dd87e3a70e
+size 16411

+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Normal.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b849ede239cc06f308d99799555c3cf127d43e9fe90fdbd5b330fc160028ef9
+size 9378983

+ 3 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Assets/TestData/ExportTest/textures/rocks_Roughness.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3c3c399fb8c0908973dd0fe74869b181be9dbb27fce61f1d0b7e7b98f7447db3
+size 25416

+ 1 - 1
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/ui.py

@@ -47,7 +47,7 @@ _settings_core = dccsi_core_config.get_config_settings(enable_o3de_python=True,
 # as the list of slots/actions grows, refactor into sub-modules
 @Slot()
 def click_action_sampleui():
-    """! Creates a sandalone sample ui with button, this is provided for
+    """! Creates a standalone sample ui with button, this is provided for
     Technicl Artists learning, as one of the purposes of the dccsi is
     onboarding TAs to the editor extensibility experience.
 

+ 0 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/LICENSE → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/LICENSE


+ 37 - 31
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/README.md → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/README.md

@@ -1,14 +1,6 @@
-"""
-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
-"""
-
-# -------------------------------------------------------------------------
+# O3DE Blender, Scene Exporter AddOn
 
 This folder contains the Blender O3DE Scene Exporter Plugin (DCCsi) for O3DE
-# The O3DE Scene Exporter DCC Tool for Blender
 
 ![SciFiDockSmall](https://user-images.githubusercontent.com/87207603/175056100-d8dc00fa-5795-4a46-b1ab-724c25fb1604.gif)
 
@@ -18,6 +10,32 @@ The O3DE Scene Exporter is a Blender plugin for Customers using Blender as a 3D
 
 We often don’t hit our targets on the first time. The creative process is very iterative and has lots of repetition of the same steps over and over again until our vision is completed. With Digital Content Creation tools, we can help make this process much smother, speeding steps up or completely removing them, giving time back to be creative.
 
+# Getting Started
+
+Although it is not a hard requirement, you can enable the DccScriptingInterface Gem (DCCsi) with your O3DE Project (using the o3de.exe project manager).  This includes a tool integration with Blender, this provides and out-of-box developer experience for creating python scripts, tools and other addons. Once the DccScriptingInterface Gem is enabled in your project via the Project Manager, then build your project (DCCsi python bootstrapping will require the project to be built.) [Adding and Removing Gems in a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/)
+
+With the DCCsi enabled, you can launch Blender from the main O3DE Editor, and Blender will start in a managed way (configuration and settings) and this addon can be enabled in the preferences (without requiring a manual install, although you can still do that also)
+
+More information about DCC tool integrations and setup can be found here in the[DccScriptingInterface Readme](https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/readme.md)
+
+More information about the DCCsi Blender tool integration, configuration and setup, can be found here in the: [Blender Readme](https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/readme.md)
+
+## Installing the DccScriptingInterface Gem
+
+![image](https://user-images.githubusercontent.com/23222931/200064189-c2d32414-fa62-4281-ae38-b084ee041b45.png)
+
+## Launch Blender from the Main Editor
+
+![Editor_fYF0aCP2k5](https://user-images.githubusercontent.com/23222931/200064272-81b7921a-1909-4836-ac4e-62b6ac919245.gif)
+
+## Enable the AddOn via Blender Prefs
+
+![blender_T6xHTOTS7k](https://user-images.githubusercontent.com/23222931/200065171-a9e1d802-7a94-4273-bd00-03421fcbfd17.gif)
+
+## Accessing the O3DE Scene Exporter Tool
+
+![blender_uZhsuDbmuU](https://user-images.githubusercontent.com/23222931/200066094-63be4232-d92c-41e5-a9ef-a226c74d2b77.gif)
+
 # Project Asset Organization with O3DE Projects and Custom Project Paths.
 
 Asset management organization is always a challenge. Projects paths from O3DEs manifest are automatic added for you, and if you wish to create customized paths to store in the project list, these paths can be added and saved for future use.
@@ -42,10 +60,11 @@ O3DE User Defined Properties meta data in our DCCs tools helps speeds up the ite
 ![image](https://user-images.githubusercontent.com/87207603/191542498-1272b606-944d-4d5d-821e-29e5d73d7d48.png)
 
 Above, An Example of a Tree exported with Multiple Levels of UDP LODs o3de_atom_lod
-  
+
 More on O3DE User Defined Properties https://docs.o3de.org/blog/posts/blog-udp/
 
 # O3DE Actor, Skin Attachments, and Animation Exports
+
 Selected your mesh, Animation Options, and EXPORT TO O3DE! The plugin will look at the whole connected tree and export your Rig with Animation to o3de. This speeds up the iterative tweaks that are needed to have Skin Weights and Animation Motions look their best.
 
 ![image](https://user-images.githubusercontent.com/87207603/191542958-53242cc0-8da7-443d-b75a-9ea0bf80b7e1.png)
@@ -58,8 +77,8 @@ Bring it all together, an Actor that is Animating in O3DE with Skin Attachments
 
 ![Base_Planet_3](https://user-images.githubusercontent.com/87207603/191543321-92168c58-bc68-40b4-b7a0-bdfbe42ea129.gif)
 
-
 # Deep Dive into the feature details.
+
 The O3DE Tools Panel will have an easy ABC order of workflow. 
 
 ![image](https://user-images.githubusercontent.com/87207603/191543676-05688ac1-d689-40cb-b02f-22cf6f8901d8.png)
@@ -129,7 +148,7 @@ This option will show up when selecting more than one mesh. You can export a sin
 
 **Export as Quads or Triangles:**
 
-This option will export your FBX in Quads or Triangles. This is non-destrutive to your Blender Model. This is good when **NGONs** might be an issue.
+This option will export your FBX in Quads or Triangles. This is non-destructive to your Blender Model. This is good when **NGONs** might be an issue.
 
 # E: EXPORT FILE
 
@@ -137,7 +156,7 @@ When all the requirements are met for an export, the export button will be unloc
 
 ![image](https://user-images.githubusercontent.com/87207603/191545003-1296a19b-e66d-4d02-a88f-396ec16478d3.png)
 
-# THE PREFLIGHT REPORT CARD  
+# THE PREFLIGHT REPORT CARD
 
 When exporting your selected model, the In the scene export Preflight tool will give you a quick diagnostic of your file and common issues it may have when importing into O3DE.
 
@@ -156,28 +175,15 @@ When exporting your selected model, the In the scene export Preflight tool will
 **Warning, Your Scene is set to Feet. However O3DE units are in Meters.**
 
 # INSTALL PLUGIN MANUALLY:
+
 To install, simply zip the SceneExporter folder and import as a Blender Plugin. This is the same process of any Blender Plugin.
 ![image](https://user-images.githubusercontent.com/87207603/191545265-91b1cbff-fd23-4bfc-be3b-5357f4c77449.png)
 
 ![InstallPlugin](https://user-images.githubusercontent.com/87207603/191545430-1033c1e5-b195-4cd0-8c1a-864d91ebe487.gif)
 
+----
 
+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

+ 0 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/__init__.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/__init__.py


+ 37 - 37
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/constants.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/constants.py

@@ -1,37 +1,37 @@
-# coding:utf-8
-#!/usr/bin/python
-#
-# 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
-#
-#
-# -------------------------------------------------------------------------
-from pathlib import Path
-# -------------------------------------------------------------------------
-
-EXPORT_LIST_OPTIONS = ( ( ('0', 'Selected with in texture folder',
-    'Export selected meshes with textures in a texture folder.'),
-    ('1', 'Selected with Textures in same folder',
-        'Export selected meshes with textures in the same folder'),
-    ('2', 'Only Meshes', 'Only export the selected meshes, no textures')
-    ))
-NO_ANIMATION = 'No Animation'
-KEY_FRAME_ANIMATION = 'Keyframe Animation'
-MESH_AND_RIG = 'Just Mesh with Rig'
-SKIN_ATTACHMENT = 'Skin Attachment Mesh with Rig'
-ANIMATION_LIST_OPTIONS = ( (
-    ('0', NO_ANIMATION, 'Export with no keyframe Animation.'),
-    ('1', KEY_FRAME_ANIMATION, 'Mesh needs to be parented to Armature with weights in order for O3DE to detect Entity as an Actor.'),
-    ('2', MESH_AND_RIG, 'Key All Bones, Force exporting at least one key of animation for all bones'),
-    ('3', SKIN_ATTACHMENT, 'Export a mesh with the Armature bones for use as a O3DE Skin Attachment.'),
-    ))
-
-UDP = {'o3de_atom_lod' : '_lod',  'o3de_atom_phys' : '_phys'}
-TAG_O3DE = '.o3de'
-IMAGE_EXT = ('', '.jpg', '.png', '.JPG', '.PNG')
-USER_HOME = Path.home()
-DEFAULT_SDK_MANIFEST_PATH = Path.home().joinpath(f'{TAG_O3DE}','o3de_manifest.json')
-WIKI_URL = 'https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/README.md'
-PLUGIN_VERSION = '1.5'
+# coding:utf-8
+#!/usr/bin/python
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+from pathlib import Path
+# -------------------------------------------------------------------------
+
+EXPORT_LIST_OPTIONS = ( ( ('0', 'Selected with in texture folder',
+    'Export selected meshes with textures in a texture folder.'),
+    ('1', 'Selected with Textures in same folder',
+        'Export selected meshes with textures in the same folder'),
+    ('2', 'Only Meshes', 'Only export the selected meshes, no textures')
+    ))
+NO_ANIMATION = 'No Animation'
+KEY_FRAME_ANIMATION = 'Keyframe Animation'
+MESH_AND_RIG = 'Just Mesh with Rig'
+SKIN_ATTACHMENT = 'Skin Attachment Mesh with Rig'
+ANIMATION_LIST_OPTIONS = ( (
+    ('0', NO_ANIMATION, 'Export with no keyframe Animation.'),
+    ('1', KEY_FRAME_ANIMATION, 'Mesh needs to be parented to Armature with weights in order for O3DE to detect Entity as an Actor.'),
+    ('2', MESH_AND_RIG, 'Key All Bones, Force exporting at least one key of animation for all bones'),
+    ('3', SKIN_ATTACHMENT, 'Export a mesh with the Armature bones for use as a O3DE Skin Attachment.'),
+    ))
+
+UDP = {'o3de_atom_lod' : '_lod',  'o3de_atom_phys' : '_phys'}
+TAG_O3DE = '.o3de'
+IMAGE_EXT = ('', '.jpg', '.png', '.JPG', '.PNG')
+USER_HOME = Path.home()
+DEFAULT_SDK_MANIFEST_PATH = Path.home().joinpath(f'{TAG_O3DE}','o3de_manifest.json')
+WIKI_URL = 'https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/README.md'
+PLUGIN_VERSION = '1.5'

+ 174 - 174
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/fbx_exporter.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/fbx_exporter.py

@@ -1,174 +1,174 @@
-# coding:utf-8
-#!/usr/bin/python
-#
-# 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
-#
-#
-# -------------------------------------------------------------------------
-import bpy
-from pathlib import Path
-from . import utils
-from . import o3de_utils
-from . import constants
-
-def amimation_export_options():
-    """!
-    This function will return the selected animation options from the O3DE Dropdown
-    Animation Export Options.
-    """
-    # Animation Export Options:
-    # Set Default Values
-    bake_anim_option = None
-    bake_anim_use_all_bones = None
-    bake_anim_use_nla_strips_option = None
-    bake_anim_use_all_actions_option = None
-    bake_anim_force_startend_keying_option = None
-
-    if bpy.types.Scene.animation_export == constants.NO_ANIMATION:
-        bake_anim_option = False
-        bake_anim_use_all_bones = False
-        bake_anim_use_nla_strips_option = False
-        bake_anim_use_all_actions_option = False
-        bake_anim_force_startend_keying_option = False
-        bpy.types.Scene.file_menu_animation_export = False
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-    elif bpy.types.Scene.animation_export == constants.KEY_FRAME_ANIMATION:
-        # Set Animation Options
-        bake_anim_option = True
-        bake_anim_use_all_bones = True
-        bake_anim_use_nla_strips_option = True
-        bake_anim_use_all_actions_option = True
-        bake_anim_force_startend_keying_option = True
-        bpy.types.Scene.file_menu_animation_export = True
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-    elif bpy.types.Scene.animation_export == constants.MESH_AND_RIG:
-        # Set Animation Options
-        bake_anim_option = False
-        bake_anim_use_all_bones = False
-        bake_anim_use_nla_strips_option = False
-        bake_anim_use_all_actions_option = False
-        bake_anim_force_startend_keying_option = False
-        bpy.types.Scene.file_menu_animation_export = False
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-    elif bpy.types.Scene.animation_export == constants.SKIN_ATTACHMENT:
-        # Set Animation Options
-        bake_anim_option = False
-        bake_anim_use_all_bones = False
-        bake_anim_use_nla_strips_option = False
-        bake_anim_use_all_actions_option = False
-        bake_anim_force_startend_keying_option = False
-        bpy.types.Scene.file_menu_animation_export = False
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-
-    if bpy.types.Scene.file_menu_animation_export:
-        bake_anim_option = True
-        bake_anim_use_all_bones = True
-        bake_anim_use_nla_strips_option = True
-        bake_anim_use_all_actions_option = True
-        bake_anim_force_startend_keying_option = True
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-    else:
-        bake_anim_option = False
-        bake_anim_use_all_bones = False
-        bake_anim_use_nla_strips_option = False
-        bake_anim_use_all_actions_option = False
-        bake_anim_force_startend_keying_option = False
-        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
-
-def fbx_file_exporter(fbx_file_path, file_name):
-    """!
-    This function will send to selected .FBX to an O3DE Project Path
-    @param fbx_file_path this is the o3de project path where the selected meshe(s)
-    will be exported as an .fbx
-    @param file_name A custom file name string
-    """
-    # Export file path Var
-    export_file_path = ''
-    # Validate a selection
-    valid_selection, selected_name = utils.check_selected()
-
-    # FBX Exporter
-    if valid_selection:
-        if fbx_file_path == '':
-            # Build new path, check to see if this is a custom or tool made path
-            # and if has the Assets Directory.
-            asset_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets')
-            if Path(asset_path).exists():
-                # TOOL MENU EXPORT
-                export_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets', file_name)
-                # Clone Texture images and Repath images before export
-                file_menu_export = False
-                if not bpy.types.Scene.export_textures_folder is None:
-                    utils.clone_repath_images(file_menu_export, bpy.types.Scene.selected_o3de_project_path, o3de_utils.build_projects_list())
-            else:
-                # WAS ONCE FILE MENU EXPORT
-                export_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath(file_name)
-                # Clone Texture images and Repath images before export
-                file_menu_export = None # This is because it was first exported by the file menu export
-                if not bpy.types.Scene.export_textures_folder is None:
-                    utils.clone_repath_images(file_menu_export, bpy.types.Scene.selected_o3de_project_path, o3de_utils.build_projects_list())
-        else:
-            # Build new path
-            export_file_path = fbx_file_path
-            source_file_path = Path(fbx_file_path) # Covert string to path
-            bpy.types.Scene.selected_o3de_project_path = Path(source_file_path.parent)
-            # Clone Texture images and Repath images before export
-            file_menu_export = True
-            if not bpy.types.Scene.export_textures_folder is None:
-                utils.clone_repath_images(file_menu_export, source_file_path, o3de_utils.build_projects_list())
-                # Currently the Blender FBX Export API is not support use_triangles, we will need to use a modifier at export then remove when done.
-        if bpy.types.Scene.convert_mesh_to_triangles:
-            utils.add_remove_modifier("TRIANGULATE", True)
-        # Lets get the Animation Options
-        bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option = amimation_export_options()
-        # Main Blender FBX API exporter
-        bpy.ops.export_scene.fbx(
-            filepath=str(export_file_path),
-            check_existing=False,
-            filter_glob='*.fbx',
-            use_selection=True,
-            use_active_collection=False,
-            global_scale=1.0,
-            apply_unit_scale=True,
-            apply_scale_options='FBX_SCALE_UNITS',
-            use_space_transform=True,
-            bake_space_transform=False,
-            object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'},
-            use_mesh_modifiers=True,
-            use_mesh_modifiers_render=True,
-            mesh_smooth_type='OFF',
-            use_subsurf=False,
-            use_mesh_edges=False,
-            use_tspace=False,
-            use_custom_props=False,
-            add_leaf_bones=False,
-            primary_bone_axis='Y',
-            secondary_bone_axis='X',
-            use_armature_deform_only=False,
-            armature_nodetype='NULL',
-            bake_anim=bake_anim_option,
-            bake_anim_use_all_bones=bake_anim_use_all_bones,
-            bake_anim_use_nla_strips=bake_anim_use_nla_strips_option,
-            bake_anim_use_all_actions=bake_anim_use_all_actions_option,
-            bake_anim_force_startend_keying=bake_anim_force_startend_keying_option,
-            bake_anim_step=1.0,
-            bake_anim_simplify_factor=1.0,
-            path_mode='AUTO',
-            embed_textures=True,
-            batch_mode='OFF',
-            use_batch_own_dir=False,
-            use_metadata=True,
-            axis_forward='-Z',
-            axis_up='Y')
-        
-        # If we added a Triangulate modifier, lets remove it now.
-        if bpy.types.Scene.convert_mesh_to_triangles:
-            utils.add_remove_modifier("Triangulate", False)
-        # Show export status
-        bpy.types.Scene.pop_up_notes = f'{file_name} Exported!'
-        bpy.ops.message.popup('INVOKE_DEFAULT')
-        if not bpy.types.Scene.export_textures_folder is None:
-            utils.replace_stored_paths()
+# coding:utf-8
+#!/usr/bin/python
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+import bpy
+from pathlib import Path
+from . import utils
+from . import o3de_utils
+from . import constants
+
+def amimation_export_options():
+    """!
+    This function will return the selected animation options from the O3DE Dropdown
+    Animation Export Options.
+    """
+    # Animation Export Options:
+    # Set Default Values
+    bake_anim_option = None
+    bake_anim_use_all_bones = None
+    bake_anim_use_nla_strips_option = None
+    bake_anim_use_all_actions_option = None
+    bake_anim_force_startend_keying_option = None
+
+    if bpy.types.Scene.animation_export == constants.NO_ANIMATION:
+        bake_anim_option = False
+        bake_anim_use_all_bones = False
+        bake_anim_use_nla_strips_option = False
+        bake_anim_use_all_actions_option = False
+        bake_anim_force_startend_keying_option = False
+        bpy.types.Scene.file_menu_animation_export = False
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+    elif bpy.types.Scene.animation_export == constants.KEY_FRAME_ANIMATION:
+        # Set Animation Options
+        bake_anim_option = True
+        bake_anim_use_all_bones = True
+        bake_anim_use_nla_strips_option = True
+        bake_anim_use_all_actions_option = True
+        bake_anim_force_startend_keying_option = True
+        bpy.types.Scene.file_menu_animation_export = True
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+    elif bpy.types.Scene.animation_export == constants.MESH_AND_RIG:
+        # Set Animation Options
+        bake_anim_option = False
+        bake_anim_use_all_bones = False
+        bake_anim_use_nla_strips_option = False
+        bake_anim_use_all_actions_option = False
+        bake_anim_force_startend_keying_option = False
+        bpy.types.Scene.file_menu_animation_export = False
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+    elif bpy.types.Scene.animation_export == constants.SKIN_ATTACHMENT:
+        # Set Animation Options
+        bake_anim_option = False
+        bake_anim_use_all_bones = False
+        bake_anim_use_nla_strips_option = False
+        bake_anim_use_all_actions_option = False
+        bake_anim_force_startend_keying_option = False
+        bpy.types.Scene.file_menu_animation_export = False
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+
+    if bpy.types.Scene.file_menu_animation_export:
+        bake_anim_option = True
+        bake_anim_use_all_bones = True
+        bake_anim_use_nla_strips_option = True
+        bake_anim_use_all_actions_option = True
+        bake_anim_force_startend_keying_option = True
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+    else:
+        bake_anim_option = False
+        bake_anim_use_all_bones = False
+        bake_anim_use_nla_strips_option = False
+        bake_anim_use_all_actions_option = False
+        bake_anim_force_startend_keying_option = False
+        return bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option
+
+def fbx_file_exporter(fbx_file_path, file_name):
+    """!
+    This function will send to selected .FBX to an O3DE Project Path
+    @param fbx_file_path this is the o3de project path where the selected meshe(s)
+    will be exported as an .fbx
+    @param file_name A custom file name string
+    """
+    # Export file path Var
+    export_file_path = ''
+    # Validate a selection
+    valid_selection, selected_name = utils.check_selected()
+
+    # FBX Exporter
+    if valid_selection:
+        if fbx_file_path == '':
+            # Build new path, check to see if this is a custom or tool made path
+            # and if has the Assets Directory.
+            asset_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets')
+            if Path(asset_path).exists():
+                # TOOL MENU EXPORT
+                export_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets', file_name)
+                # Clone Texture images and Repath images before export
+                file_menu_export = False
+                if not bpy.types.Scene.export_textures_folder is None:
+                    utils.clone_repath_images(file_menu_export, bpy.types.Scene.selected_o3de_project_path, o3de_utils.build_projects_list())
+            else:
+                # WAS ONCE FILE MENU EXPORT
+                export_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath(file_name)
+                # Clone Texture images and Repath images before export
+                file_menu_export = None # This is because it was first exported by the file menu export
+                if not bpy.types.Scene.export_textures_folder is None:
+                    utils.clone_repath_images(file_menu_export, bpy.types.Scene.selected_o3de_project_path, o3de_utils.build_projects_list())
+        else:
+            # Build new path
+            export_file_path = fbx_file_path
+            source_file_path = Path(fbx_file_path) # Covert string to path
+            bpy.types.Scene.selected_o3de_project_path = Path(source_file_path.parent)
+            # Clone Texture images and Repath images before export
+            file_menu_export = True
+            if not bpy.types.Scene.export_textures_folder is None:
+                utils.clone_repath_images(file_menu_export, source_file_path, o3de_utils.build_projects_list())
+                # Currently the Blender FBX Export API is not support use_triangles, we will need to use a modifier at export then remove when done.
+        if bpy.types.Scene.convert_mesh_to_triangles:
+            utils.add_remove_modifier("TRIANGULATE", True)
+        # Lets get the Animation Options
+        bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option = amimation_export_options()
+        # Main Blender FBX API exporter
+        bpy.ops.export_scene.fbx(
+            filepath=str(export_file_path),
+            check_existing=False,
+            filter_glob='*.fbx',
+            use_selection=True,
+            use_active_collection=False,
+            global_scale=1.0,
+            apply_unit_scale=True,
+            apply_scale_options='FBX_SCALE_UNITS',
+            use_space_transform=True,
+            bake_space_transform=False,
+            object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'},
+            use_mesh_modifiers=True,
+            use_mesh_modifiers_render=True,
+            mesh_smooth_type='OFF',
+            use_subsurf=False,
+            use_mesh_edges=False,
+            use_tspace=False,
+            use_custom_props=False,
+            add_leaf_bones=False,
+            primary_bone_axis='Y',
+            secondary_bone_axis='X',
+            use_armature_deform_only=False,
+            armature_nodetype='NULL',
+            bake_anim=bake_anim_option,
+            bake_anim_use_all_bones=bake_anim_use_all_bones,
+            bake_anim_use_nla_strips=bake_anim_use_nla_strips_option,
+            bake_anim_use_all_actions=bake_anim_use_all_actions_option,
+            bake_anim_force_startend_keying=bake_anim_force_startend_keying_option,
+            bake_anim_step=1.0,
+            bake_anim_simplify_factor=1.0,
+            path_mode='AUTO',
+            embed_textures=True,
+            batch_mode='OFF',
+            use_batch_own_dir=False,
+            use_metadata=True,
+            axis_forward='-Z',
+            axis_up='Y')
+        
+        # If we added a Triangulate modifier, lets remove it now.
+        if bpy.types.Scene.convert_mesh_to_triangles:
+            utils.add_remove_modifier("Triangulate", False)
+        # Show export status
+        bpy.types.Scene.pop_up_notes = f'{file_name} Exported!'
+        bpy.ops.message.popup('INVOKE_DEFAULT')
+        if not bpy.types.Scene.export_textures_folder is None:
+            utils.replace_stored_paths()

+ 93 - 93
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/o3de_utils.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/o3de_utils.py

@@ -1,94 +1,94 @@
-# coding:utf-8
-#!/usr/bin/python
-#
-# 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
-#
-#
-# -------------------------------------------------------------------------
-import bpy
-import json
-from . import constants
-from pathlib import Path
-import addon_utils
-
-def look_at_engine_manifest():
-    """!
-    This function will look at your O3DE engine manifest JSON
-    """
-    engine_is_installed = None
-    try:
-        with open(constants.DEFAULT_SDK_MANIFEST_PATH, "r") as data_file:
-            data = json.load(data_file)
-            # Look at manifest projects
-            o3de_projects = data['projects']
-            # Send if o3de is not installed
-            engine_is_installed = True
-            o3de_projects = extend_project_list(o3de_projects)
-            return o3de_projects, engine_is_installed
-    except:
-        o3de_projects = ['']
-        engine_is_installed = False
-        return o3de_projects, engine_is_installed
-
-def load_saved_projects():
-    """!
-    This function will load the users project.json list
-    """
-    # Find the installed modules path on users system
-    for addon_path in addon_utils.modules():
-        if addon_path.bl_info['name'] == 'O3DE_DCCSI_BLENDER_SCENE_EXPORTER':
-            addon_full_path = addon_path.__file__
-            # Lets just get the directory path
-            bpy.types.Scene.plugin_directory = Path(addon_full_path).parent
-    project_json_path = Path(bpy.types.Scene.plugin_directory, "projects.json")
-    if project_json_path.exists():
-        with open(Path(bpy.types.Scene.plugin_directory, "projects.json"), "r") as data_file:
-            data = json.load(data_file)
-            return data
-
-def extend_project_list(o3de_projects):
-    """!
-    This function will load the users project.json list and extend it to the engine manifest projects
-    """
-    saved_o3de_projects = load_saved_projects()
-    # Check to see if these projects are already on list
-    if saved_o3de_projects not in o3de_projects:
-        o3de_projects.extend(saved_o3de_projects)
-    return o3de_projects
-
-def build_projects_list():
-    """!
-    This function will check to see if O3DE is installed, looks at the o3de engine manifest projects
-    and the user saved project.json, builds the o3de project list
-    """
-    o3de_projects, engine_is_installed = look_at_engine_manifest()
-    # Project list
-    list_o3de_projects = []
-    # Check to see if O3DE is installed.
-    if engine_is_installed:
-        # Make a list of projects to select from
-        for project_full_path in o3de_projects:
-            # Check to see if the project name might be 1 level up in this path if ending with project.
-            # For example we could have a path like this C:/Users/USERNAME/O3DE/Projects/loft-arch-vis-sample/Project
-            if Path(project_full_path).name == 'Project':
-                append_project_path = Path(project_full_path)
-                list_o3de_projects.append((project_full_path, append_project_path.parts[-2], project_full_path))
-            else:
-                list_o3de_projects.append((project_full_path, Path(project_full_path).name, project_full_path))
-    return list_o3de_projects
-
-def save_project_list_json(append_path):
-    """!
-    This function will save the users projects Projects.json
-    """
-    # Load the current list to append to:
-    saved_o3de_projects = load_saved_projects()
-    # Check to see if the path not already in the list:
-    if append_path not in saved_o3de_projects:
-        saved_o3de_projects.append(append_path)
-        # Save Updated
-        with open(Path(bpy.types.Scene.plugin_directory, "projects.json"), 'w') as f:
+# coding:utf-8
+#!/usr/bin/python
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+import bpy
+import json
+from . import constants
+from pathlib import Path
+import addon_utils
+
+def look_at_engine_manifest():
+    """!
+    This function will look at your O3DE engine manifest JSON
+    """
+    engine_is_installed = None
+    try:
+        with open(constants.DEFAULT_SDK_MANIFEST_PATH, "r") as data_file:
+            data = json.load(data_file)
+            # Look at manifest projects
+            o3de_projects = data['projects']
+            # Send if o3de is not installed
+            engine_is_installed = True
+            o3de_projects = extend_project_list(o3de_projects)
+            return o3de_projects, engine_is_installed
+    except:
+        o3de_projects = ['']
+        engine_is_installed = False
+        return o3de_projects, engine_is_installed
+
+def load_saved_projects():
+    """!
+    This function will load the users project.json list
+    """
+    # Find the installed modules path on users system
+    for addon_path in addon_utils.modules():
+        if addon_path.bl_info['name'] == 'O3DE_DCCSI_BLENDER_SCENE_EXPORTER':
+            addon_full_path = addon_path.__file__
+            # Lets just get the directory path
+            bpy.types.Scene.plugin_directory = Path(addon_full_path).parent
+    project_json_path = Path(bpy.types.Scene.plugin_directory, "projects.json")
+    if project_json_path.exists():
+        with open(Path(bpy.types.Scene.plugin_directory, "projects.json"), "r") as data_file:
+            data = json.load(data_file)
+            return data
+
+def extend_project_list(o3de_projects):
+    """!
+    This function will load the users project.json list and extend it to the engine manifest projects
+    """
+    saved_o3de_projects = load_saved_projects()
+    # Check to see if these projects are already on list
+    if saved_o3de_projects not in o3de_projects:
+        o3de_projects.extend(saved_o3de_projects)
+    return o3de_projects
+
+def build_projects_list():
+    """!
+    This function will check to see if O3DE is installed, looks at the o3de engine manifest projects
+    and the user saved project.json, builds the o3de project list
+    """
+    o3de_projects, engine_is_installed = look_at_engine_manifest()
+    # Project list
+    list_o3de_projects = []
+    # Check to see if O3DE is installed.
+    if engine_is_installed:
+        # Make a list of projects to select from
+        for project_full_path in o3de_projects:
+            # Check to see if the project name might be 1 level up in this path if ending with project.
+            # For example we could have a path like this C:/Users/USERNAME/O3DE/Projects/loft-arch-vis-sample/Project
+            if Path(project_full_path).name == 'Project':
+                append_project_path = Path(project_full_path)
+                list_o3de_projects.append((project_full_path, append_project_path.parts[-2], project_full_path))
+            else:
+                list_o3de_projects.append((project_full_path, Path(project_full_path).name, project_full_path))
+    return list_o3de_projects
+
+def save_project_list_json(append_path):
+    """!
+    This function will save the users projects Projects.json
+    """
+    # Load the current list to append to:
+    saved_o3de_projects = load_saved_projects()
+    # Check to see if the path not already in the list:
+    if append_path not in saved_o3de_projects:
+        saved_o3de_projects.append(append_path)
+        # Save Updated
+        with open(Path(bpy.types.Scene.plugin_directory, "projects.json"), 'w') as f:
             json.dump(saved_o3de_projects, f)

+ 0 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/projects.json → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/projects.json


+ 678 - 678
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/ui.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/ui.py

@@ -1,678 +1,678 @@
-# coding:utf-8
-#!/usr/bin/python
-#
-# 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
-#
-#
-# -------------------------------------------------------------------------
-from multiprocessing import context
-import bpy
-from pathlib import Path
-import webbrowser
-import re
-from bpy_extras.io_utils import ExportHelper, ImportHelper
-from bpy.types import Panel, Operator, PropertyGroup, AddonPreferences
-from bpy.props import EnumProperty, StringProperty, BoolProperty, PointerProperty
-from . import constants
-from . import fbx_exporter
-from . import o3de_utils
-from . import utils
-import addon_utils
-
-
-def message_box(message = "", title = "Message Box", icon = 'LIGHT'):
-    """!
-    This function will show a messagebox to inform user.
-    @param message This is the message box main string message
-    @param title This is the title of the message box string
-    @param icon This is the Blender icon used in the message box gui
-    """
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI
-        """
-        self.layout.label(text = message)
-    bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
-
-class MessageBox(bpy.types.Operator):
-    """!
-    This Class is for the UI Message Box Pop-Up
-    """
-    bl_idname = "message.popup"
-    bl_label = "O3DE Scene Exporter"
-
-    def invoke(self, context, event):
-        """!
-        This function will invoke the blender windowe manager
-        """
-        window_manager = context.window_manager
-        return window_manager.invoke_props_dialog(self)
-
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI
-        """
-        layout = self.layout
-        layout.label(text=bpy.types.Scene.pop_up_notes)
-        
-    def execute(self, context):
-        """!
-        This will update the UI with the current o3de Project Title.
-        """
-        return {'FINISHED'}
-
-class MessageBoxConfirm(bpy.types.Operator):
-    """!
-    This Class is for the UI Message Box Pop-Up but with extra properties that can be added.
-    """
-    bl_idname = "message_confirm.popup"
-    bl_label = "O3DE Scene Exporter"
-    bl_options = {'REGISTER', 'INTERNAL'}
-    
-    question_one: bpy.props.BoolProperty()
-
-    @classmethod
-    def poll(cls, context):
-        return True
-
-    def invoke(self, context, event):
-        return context.window_manager.invoke_props_dialog(self)
-
-    def draw(self, context):
-
-        layout = self.layout
-        layout.label(text=bpy.types.Scene.pop_up_confirm_label)
-        layout.prop(self, "question_one", text=bpy.types.Scene.pop_up_question_label)
-        bpy.types.Scene.pop_up_question_bool = self.question_one
-    
-    def execute(self, context):
-        """!
-        This will update the UI with the current o3de Project Title.
-        There also can be special types of functions that can be called in this execute method.
-        Example: pop_up_type == "UDP"
-        """
-        if bpy.types.Scene.pop_up_type == "UDP":
-            utils.create_udp()
-
-        self.report({'INFO'}, "OKAY")
-        return {'FINISHED'}
-
-class ReportCard(bpy.types.Operator):
-    """!
-    This Class is for the UI Report Card Pop-Up.
-    """
-    bl_idname = "report_card.popup"
-    bl_label = "O3DE Scene Exporter"
-    bl_options = {'REGISTER', 'INTERNAL'}
-    
-    @classmethod
-    def poll(cls, context):
-        return True
-
-    def invoke(self, context, event):
-        return context.window_manager.invoke_props_dialog(self)
-
-    def draw(self, context):
-        layout = self.layout
-        col = layout.column()
-
-        # Check seleted Status
-        name_selections = [obj.name for obj in bpy.context.selected_objects]
-        # Check Transforms Status
-        transforms_status, world_location = utils.check_selected_transforms()
-        # Check Selected UVs
-        selected_uvs_check = utils.check_selected_uvs()
-        # Validate selection bone names if any
-        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
-        # Check to see which Animation Action is active
-        action_name, action_count = utils.check_for_animation_actions()
-        # Check the texture export option
-        if bpy.types.Scene.export_textures_folder:
-            texture_option = "Textures in texture folder."
-        elif not bpy.types.Scene.export_textures_folder:
-            texture_option = "Textures with Model(s)"
-        else:
-            texture_option = "No texture export."
-        # Check for UDPs on selected
-        lods, colliders = utils.check_for_udp()
-
-        # Get Selected Stats
-        vert_count, edge_count, polygon_count, material_count = utils.check_selected_stats_count()
-        # Get filename
-        file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
-
-        # Start GUI Layout
-        issues = col.box()
-        issues.box()
-        issues.label(text="PREFLIGHT REPORT CARD", icon="COLLECTION_COLOR_04")
-        issues.label(text=f"Issues Found:")
-
-        self.reported_issues_int = 0
-
-        if not transforms_status:
-            issues.alert = True
-            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
-            issues.label(text=f"Transforms have not been Applied (Frozen).")
-            self.reported_issues_int += 1
-        if not world_location == [0.0, 0.0, 0.0]:
-            issues.alert = True
-            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
-            issues.label(text=f"Are not at world orgin {world_location}")
-            self.reported_issues_int += 1
-        if not selected_uvs_check:
-            issues.alert = True
-            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
-            issues.label(text=f"Are missing UV's")
-            self.reported_issues_int += 1
-        if invalid_bone_names:
-            issues.alert = True
-            issues.label(text=f"Warning, Your Selected Object(s)", icon="SEQUENCE_COLOR_03")
-            issues.label(text=f"Have Invalid Bone Names for O3DE.")
-            issues.label(text=f"Please do not use these types of Characters")
-            issues.label(text=f"in Bone names <>[\]~.`")
-            self.reported_issues_int += 1
-        if not bpy.context.scene.unit_settings.length_unit == 'METERS':
-            issues.alert = True
-            issues.label(text=f"Warning, Your Scene is set to", icon="SEQUENCE_COLOR_03")
-            issues.label(text=f"{bpy.context.scene.unit_settings.length_unit}, however, O3DE units are in Meters.")
-            self.reported_issues_int += 1
-        issues.label(text=f"({self.reported_issues_int}) Issues Found.")
-        if self.reported_issues_int == 0:
-            issues.alert = False
-            issues.label(text="No issues found.", icon="SEQUENCE_COLOR_04")
-
-        # General information
-        mesh_info = col.box()
-        mesh_info.box()
-        mesh_info.label(text="EXPORT FILE INFORMATION", icon="COLLECTION_COLOR_05")
-        mesh_info.label(text=f"Issues Found:")
-        mesh_info.label(text=f"Selected({len(name_selections)}): {name_selections}")
-        mesh_info.label(text=f"File Name: {file_name}")
-        mesh_info.label(text=f"Vert Count: {vert_count}")
-        mesh_info.label(text=f"Edge Count: {edge_count}")
-        mesh_info.label(text=f"Polygon Count: {polygon_count}")
-        mesh_info.label(text=f"Material Count: {material_count}")
-        mesh_info.label(text=f"Scene Units are in: {bpy.context.scene.unit_settings.length_unit}")
-        mesh_info.label(text=f"Transforms are applied: {transforms_status}")
-        mesh_info.label(text=f"Export as Triangles: {bpy.types.Scene.convert_mesh_to_triangles}")
-        mesh_info.label(text=f"Animation Options: {bpy.types.Scene.animation_export}")
-        mesh_info.label(text=f"Animation Action({action_count}) {action_name}")
-        mesh_info.label(text=f"Texture Options: {texture_option}")
-        mesh_info.label(text=f"Physics Collider: {colliders}")
-        mesh_info.label(text=f"LODS: {lods}")
-        mesh_info.label(text=f"Export to Path: {bpy.types.Scene.selected_o3de_project_path}")
-    
-    def export_files(self, file_name):
-        """!
-        This function will export the selected as an .fbx to the current project path.
-        @param file_name is the file name selected or string in export_file_name_o3de
-        """
-        # Add file ext
-        file_name = f'{file_name}.fbx'
-        fbx_exporter.fbx_file_exporter('', file_name)
-    
-    def execute(self, context):
-        """!
-        This function will check the current selected count and multi_file_export_o3de bool is True or False.
-        If multi_file_export_o3de is True it will export each selected object as an .fbx file, if False it will
-        export all objects selected as one .fbx.
-        @param context defualt for this blender class
-        """
-        # Validate a selection
-        valid_selection, selected_name = utils.check_selected()
-        # Check if there are multi selections
-        if len(selected_name) > 1:
-            if bpy.types.Scene.multi_file_export_o3de:
-                for obj_name in selected_name:
-                    # Deselect all or will just keep adding to selection
-                    bpy.ops.object.select_all(action='DESELECT')
-                    # Select a mesh in the loop
-                    bpy.data.objects[obj_name].select_set(True)
-                    # Remove some nasty invalid char
-                    file_name = re.sub(r'\W+', '', obj_name)
-                    # Export file
-                    self.export_files(file_name)
-            else:
-                # Remove some nasty invalid char
-                file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
-                # Export file
-                self.export_files(file_name)
-        else:
-            # Remove some nasty invalid char
-            file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
-            # Export file
-            self.export_files(file_name)
-        return{'FINISHED'}
-
-class ReportCardButton(bpy.types.Operator):
-    """!
-    This Class is for the UI Report Card Button
-    """
-    bl_idname = "vin.report_card_button"
-    bl_label = "O3DE Report Card Button"
-
-    def execute(self, context):
-        """!
-        This function will open report card window.
-        """
-        bpy.ops.report_card.popup('INVOKE_DEFAULT')
-        return{'FINISHED'}
-
-class WikiButton(bpy.types.Operator):
-    """!
-    This Class is for the UI Wiki Button
-    """
-    bl_idname = "vin.wiki"
-    bl_label = "O3DE Github Wiki"
-
-    def execute(self, context):
-        """!
-        This function will open a web browser window.
-        """
-        webbrowser.open(constants.WIKI_URL)
-        return{'FINISHED'}
-
-class CustomProjectPath(bpy.types.Operator, ImportHelper):
-    """!
-    This Class is for setting a custom project path
-    """
-    bl_idname = "project.open_filebrowser"
-    bl_label = "Project Assets Folder"
-    bl_options = {'PRESET', 'UNDO'}
-
-    filter_glob: StringProperty(
-        default="//",
-        options={'HIDDEN'},
-        subtype='NONE',
-        maxlen=255,  # Max internal buffer length, longer would be clamped.
-        )
-
-    def execute(self, context):
-        # Look at current project list, if path not in list add it
-        real_path =  Path(self.filepath)
-        if real_path.is_dir():
-            bpy.types.Scene.selected_o3de_project_path = str(Path(self.filepath))
-        else:
-            bpy.types.Scene.selected_o3de_project_path = str(Path(self.filepath).parent)
-        if bpy.types.Scene.selected_o3de_project_path not in context.scene.o3de_projects_list:
-            o3de_utils.save_project_list_json(str(bpy.types.Scene.selected_o3de_project_path))
-            # Refesh the addon
-            addon_utils.enable('SceneExporter')
-            # Rebuild the project list
-            o3de_utils.build_projects_list() 
-        return {'FINISHED'}
-
-class AddColliderMesh(bpy.types.Operator):
-    """!
-    This Class is for the UI Wiki Button
-    """
-    bl_idname = "vin.collider"
-    bl_label = "Add a PhysX Collider mesh to selected."
-
-    def execute(self, context):
-        """!
-        This function will create the collision mesh.
-        """
-        # Create a Pop-Up confirm window
-        bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_phys to mesh.'
-        bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
-        bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_phys')
-        bpy.types.Scene.pop_up_type = "UDP"
-        bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
-        return{'FINISHED'}
-
-class AddLODMesh(bpy.types.Operator):
-    """!
-    This Class is for the UI Wiki Button
-    """
-    bl_idname = "vin.lod"
-    bl_label = "Add a LOD mesh to selected."
-
-    def execute(self, context):
-        """!
-        This function will create the LOD mesh
-        """
-        # Create a Pop-Up confirm window
-        bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_lod to mesh.'
-        bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
-        bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_lod')
-        bpy.types.Scene.pop_up_type = "UDP"
-        bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
-        return{'FINISHED'}
-
-class ProjectsListDropDown(bpy.types.Operator):
-    """!
-    This Class is for the O3DE Projects UI List Drop Down.
-    """
-    bl_label = "Project List Dropdown"
-    bl_idname = "wm.projectlist"
-
-    def invoke(self, context, event):
-        """!
-        This function will invoke the blender windowe manager
-        """
-        window_manager = context.window_manager
-        return window_manager.invoke_props_dialog(self)
-
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI
-        """
-        layout = self.layout
-        layout.prop(context.scene, 'o3de_projects_list')
-        
-    def execute(self, context):
-        """!
-        This will update the UI with the current o3de Project Title.
-        """
-        bpy.types.Scene.selected_o3de_project_path = context.scene.o3de_projects_list
-        return {'FINISHED'}
-
-class SceneExporterFileMenu(Operator, ExportHelper):
-    """!
-    This Class will export the 3d model as well
-    as textures in the user selected path.
-    """
-    bl_idname = "O3DE_FileExport"  # important since its how bpy.ops.import_test.some_data is constructed
-    bl_idname = "export_model.mesh_data"  # important since its how bpy.ops.import_test.some_data is constructed
-    bl_label = "Export to O3DE"
-
-    # ExportHelper mixin class uses this
-    filename_ext = ".fbx"
-
-    filter_glob: StringProperty(
-        default="*.fbx",
-        options={'HIDDEN'},
-        maxlen=255,  # Max internal buffer length, longer would be clamped.
-    )
-    # Extra options
-    export_textures_folder : BoolProperty(
-        name="Export Textures in a textures folder",
-        description="Export Textures in textures folder",
-        default=True,
-    )
-    export_mesh_as_triangles : BoolProperty(
-        name="Export Mesh as Triangles",
-        description="Export Mesh as Triangles"
-    )
-    # Add animation to export
-    export_animation : BoolProperty(
-        name="Keyframe Animation",
-        description="Export with Keyframe Animation",
-        default=False,
-    )
-
-    def execute(self, context):
-        """!
-        This function will select the Export Texture Option
-        """
-        bpy.types.Scene.export_textures_folder = self.export_textures_folder
-        bpy.types.Scene.file_menu_animation_export = self.export_animation
-        bpy.types.Scene.convert_mesh_to_triangles = self.export_mesh_as_triangles
-        if self.export_animation:
-            bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
-            utils.valid_animation_selection()
-        fbx_exporter.fbx_file_exporter(self.filepath, Path(self.filepath).stem)
-        return{'FINISHED'}
-
-class ExportOptionsListDropDown(bpy.types.Operator):
-    """!
-    This Class is for the O3DE Export Options UI List Drop Down
-    """
-    bl_label = "Texture Export Folder"
-    bl_idname = "wm.exportoptions"
-
-    def invoke(self, context, event):
-        """!
-        This function will invoke the blender windowe manager
-        """
-        window_manager = context.window_manager
-        return window_manager.invoke_props_dialog(self)
-        
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI
-        """
-        layout = self.layout
-        layout.prop(context.scene, 'texture_options_list')
-        
-    def execute(self, context):
-        """!
-        This function will Update Export Option Bool
-        """
-        if context.scene.texture_options_list == '0':
-            bpy.types.Scene.export_textures_folder = True
-        elif context.scene.texture_options_list == '1':
-            bpy.types.Scene.export_textures_folder = False
-        else:
-            bpy.types.Scene.export_textures_folder = None
-        return {'FINISHED'}
-        
-class AnimationOptionsListDropDown(bpy.types.Operator):
-    """!
-    This Class is for the O3DE Export Animations UI List Drop Down
-    """
-    bl_label = "Animation Export Options"
-    bl_idname = "wm.animationoptions"
-
-    def invoke(self, context, event):
-        """!
-        This function will invoke the blender windowe manager
-        """
-        window_manager = context.window_manager
-        return window_manager.invoke_props_dialog(self)
-        
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI
-        """
-        layout = self.layout
-        layout.prop(context.scene, 'animation_options_list')
-        
-    def execute(self, context):
-        """!
-        This function will Update Export Option Bool
-        """
-        if context.scene.animation_options_list == '0':
-            bpy.types.Scene.animation_export = constants.NO_ANIMATION
-        elif context.scene.animation_options_list == '1':
-            bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
-            utils.valid_animation_selection()
-        elif context.scene.animation_options_list == '2':
-            bpy.types.Scene.animation_export = constants.MESH_AND_RIG
-            utils.valid_animation_selection()
-        elif context.scene.animation_options_list == '3':
-            bpy.types.Scene.animation_export = constants.SKIN_ATTACHMENT
-            utils.valid_animation_selection()
-        return {'FINISHED'}
-
-def file_export_menu_add(self, context):
-    """!
-    This Function will add the Export to O3DE to the file menu Export
-    """
-    self.layout.operator(SceneExporterFileMenu.bl_idname, text="Export to O3DE")
-class O3deTools(Panel):
-    """!
-    This is the Blender UI Panel O3DE Tools Tab
-    """
-    bl_idname = "O3DE_TOOLS_PT_Panel"
-    bl_space_type = 'VIEW_3D'
-    bl_region_type = 'UI'
-    bl_label = f'O3DE Tools v{constants.PLUGIN_VERSION}'
-    bl_context = 'objectmode'
-    bl_category = 'O3DE'
-
-    def draw(self, context):
-        """!
-        This function draws this gui element in Blender UI. We will look at the Look at the
-        o3de engine manifest to see if o3de is currently install and gather the project paths
-        in a list drop down.
-        """
-        layout = self.layout
-        selected_objects = context.object
-        wm = context.window_manager
-        row = layout.row()
-
-        # Look at the o3de Engine Manifest
-        o3de_projects, engine_is_installed = o3de_utils.look_at_engine_manifest()
-        
-        # Validate a selection
-        valid_selection, selected_name = utils.check_selected()
-        
-        # Validate selection bone names if any
-        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
-
-        # Get the names of current selected
-        name_selections = [obj.name for obj in bpy.context.selected_objects]
-
-        # Checks to see if O3DE is installed
-        help_info_row = layout.box()
-        help_info_row.label(text="HELP / INFORMATION", icon="EVENT_A")
-        # Checks to see if O3DE is installed
-        if engine_is_installed:
-            wiki_row = layout.row()
-            wiki_row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
-            # Heads up of current selected objects and options
-            current_selected_options_row = layout.box()
-            current_selected_options_row.label(text="CURRENT SELECTED", icon="OUTLINER_DATA_GP_LAYER")
-            # Let user know how many objects are selected
-            installed_lable = layout.row()
-            
-            # Check if there are any selections
-            if len(name_selections) == 0: 
-                installed_lable.label(text='Selected: None')
-            else:
-                installed_lable.label(text=f'({len(selected_name)}) Selected: {selected_objects.name}')
-
-            # Show which project path is the current
-            project_path_lable = layout.row()
-            if not bpy.types.Scene.selected_o3de_project_path == '':
-                project = Path(bpy.types.Scene.selected_o3de_project_path).name
-                project_path_lable.label(text=f'Project: {project}')
-            else:
-                project_path_lable.label(text='Project: None')
-            
-            # Check to see which Animation Action is active
-            action_name, action_count = utils.check_for_animation_actions()
-            
-            # Let user choose animation export options
-            if bone_are_in_selected:
-                animation_action_lable = layout.row()
-                animation_export_lable = layout.row()
-                if bpy.types.Scene.animation_export == constants.NO_ANIMATION:
-                    animation_action_lable.label(text=f'Action: ')
-                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
-                else:
-                    animation_action_lable.label(text=f'Action: {action_name} ({action_count})')
-                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
-            else:
-                bpy.types.Scene.animation_export = constants.NO_ANIMATION
-
-            # This is the UI Export Texture Option Label
-            texture_export_option_label = layout.row()
-            if bpy.types.Scene.export_textures_folder:
-                texture_export_option_label.label(text='Texture Export: Textures Folder')
-            elif bpy.types.Scene.export_textures_folder is False:
-                texture_export_option_label.label(text='Texture Export: With Model')
-            elif bpy.types.Scene.export_textures_folder is None:
-                texture_export_option_label.label(text='Texture Export: Off')
-            
-            # User selects projects folder path
-            o3de_projects_row = layout.box()
-            o3de_projects_row.label(text="PROJECTS", icon="EVENT_B")
-
-            # This is the UI Porjects List
-            o3de_projects_panel = layout.row()
-            o3de_projects_panel.operator('wm.projectlist', text='O3DE Projects', icon="OUTLINER")
-
-            # Let user choose a custom project path
-            local_project_path = layout.row()
-            local_project_path.operator("project.open_filebrowser", text="Add Custom Project Path", icon="OUTLINER_OB_GROUP_INSTANCE")
-
-            # User can add UDP
-            user_defined_properties_row = layout.box()
-            user_defined_properties_row.label(text="USER DEFINED (UDP)", icon="EVENT_C")
-            
-            # Let user create a O3DE Collider Mesh for Physx
-            create_collider_button = layout.row()
-            create_collider_button.operator("vin.collider", text='Create PhysX Collider', icon="SNAP_VOLUME")
-
-            # Let user create a O3DE LOD Mesh
-            create_lod_button = layout.row()
-            create_lod_button.operator("vin.lod", text='Create LOD', icon="MOD_REMESH")
-            
-            # User selected export options
-            o3de_projects_row = layout.box()
-            o3de_projects_row.label(text="EXPORT OPTIONS", icon="EVENT_D")
-
-            # This is the UI Texture Export Options List
-            texture_export_options_panel = layout.row()
-            texture_export_options_panel.operator('wm.exportoptions', text='Texture Export Options', icon="OUTPUT")
-            
-            # This is the UI Animation Export Options List
-            animation_export_options_panel = layout.row()
-            animation_export_options_panel.operator('wm.animationoptions', text='Animation Export Options', icon="POSE_HLT")
-
-            # If more than one object is selected, let user choose export options
-            if len(name_selections) > 1:
-                multi_file_export_label = "Export Multi-Files" if wm.multi_file_export_toggle else "Export a Single File"
-                multi_file_export_button = layout.row()
-                multi_file_export_button.prop(wm, "multi_file_export_toggle", text=multi_file_export_label, toggle=True)
-                if wm.multi_file_export_toggle:
-                    bpy.types.Scene.multi_file_export_o3de = True
-                else:
-                    bpy.types.Scene.multi_file_export_o3de = False
-            else:
-                wm.multi_file_export_toggle = False
-                bpy.types.Scene.multi_file_export_o3de = False
-            # Export mesh options
-            export_mesh_triangles_quads_label = "Exporting as Triangles" if wm.mesh_triangle_export_toggle else "Exporting as Quads"
-            export_mesh_triangles_quads_button = layout.row()
-            export_mesh_triangles_quads_button.prop(wm, "mesh_triangle_export_toggle", text=export_mesh_triangles_quads_label, toggle=True)
-            if wm.mesh_triangle_export_toggle:
-                bpy.types.Scene.convert_mesh_to_triangles = True
-            else:
-                bpy.types.Scene.convert_mesh_to_triangles = False
-            
-            # This checks to see if we should enable the export button
-            export_row = layout.box()
-            export_row.label(text="EXPORT FILE", icon="EVENT_E")
-            # Let user choose a custom file name
-            file_name_lable = layout.row()
-            if not wm.multi_file_export_toggle:
-                file_name_lable.label(text='Export File Name')
-                file_name_export_input = self.layout.column(align = True)
-                file_name_export_input.prop(context.scene, "export_file_name_o3de")
-            # Export File
-            export_files_row = layout.row()
-            export_ready_row = layout.row()
-            # Enable Rows on and off
-            if bpy.types.Scene.selected_o3de_project_path == '':
-                export_files_row.enabled = False
-                export_ready_row.label(text='No Project Selected.')
-            elif not utils.check_if_valid_path(bpy.types.Scene.selected_o3de_project_path):
-                export_files_row.enabled = False
-                export_ready_row.label(text='Project Path Not Found.')
-            elif not bpy.types.Scene.export_good_o3de:
-                export_files_row.enabled = False
-                export_ready_row.label(text='Not Ready for Export.')
-            elif not valid_selection:
-                export_files_row.enabled = False
-                export_ready_row.label(text='Nothing Selected.')
-            elif invalid_bone_names == False:
-                export_files_row.enabled = False
-                export_ready_row.label(text='Invalid Char in Bone Names.')
-            else:
-                export_files_row.enabled = True
-            # Final Export Files Button
-            export_files_row.operator('vin.report_card_button', text='EXPORT TO O3DE', icon="BLENDER")
-        else:
-            # If O3DE is not installed we tell the user
-            row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
-            not_installed = layout.row()
-            not_installed.label(text='O3DE Needs to be installed')
-            
-
+# coding:utf-8
+#!/usr/bin/python
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+from multiprocessing import context
+import bpy
+from pathlib import Path
+import webbrowser
+import re
+from bpy_extras.io_utils import ExportHelper, ImportHelper
+from bpy.types import Panel, Operator, PropertyGroup, AddonPreferences
+from bpy.props import EnumProperty, StringProperty, BoolProperty, PointerProperty
+from . import constants
+from . import fbx_exporter
+from . import o3de_utils
+from . import utils
+import addon_utils
+
+
+def message_box(message = "", title = "Message Box", icon = 'LIGHT'):
+    """!
+    This function will show a messagebox to inform user.
+    @param message This is the message box main string message
+    @param title This is the title of the message box string
+    @param icon This is the Blender icon used in the message box gui
+    """
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI
+        """
+        self.layout.label(text = message)
+    bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
+
+class MessageBox(bpy.types.Operator):
+    """!
+    This Class is for the UI Message Box Pop-Up
+    """
+    bl_idname = "message.popup"
+    bl_label = "O3DE Scene Exporter"
+
+    def invoke(self, context, event):
+        """!
+        This function will invoke the blender windowe manager
+        """
+        window_manager = context.window_manager
+        return window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI
+        """
+        layout = self.layout
+        layout.label(text=bpy.types.Scene.pop_up_notes)
+        
+    def execute(self, context):
+        """!
+        This will update the UI with the current o3de Project Title.
+        """
+        return {'FINISHED'}
+
+class MessageBoxConfirm(bpy.types.Operator):
+    """!
+    This Class is for the UI Message Box Pop-Up but with extra properties that can be added.
+    """
+    bl_idname = "message_confirm.popup"
+    bl_label = "O3DE Scene Exporter"
+    bl_options = {'REGISTER', 'INTERNAL'}
+    
+    question_one: bpy.props.BoolProperty()
+
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+
+        layout = self.layout
+        layout.label(text=bpy.types.Scene.pop_up_confirm_label)
+        layout.prop(self, "question_one", text=bpy.types.Scene.pop_up_question_label)
+        bpy.types.Scene.pop_up_question_bool = self.question_one
+    
+    def execute(self, context):
+        """!
+        This will update the UI with the current o3de Project Title.
+        There also can be special types of functions that can be called in this execute method.
+        Example: pop_up_type == "UDP"
+        """
+        if bpy.types.Scene.pop_up_type == "UDP":
+            utils.create_udp()
+
+        self.report({'INFO'}, "OKAY")
+        return {'FINISHED'}
+
+class ReportCard(bpy.types.Operator):
+    """!
+    This Class is for the UI Report Card Pop-Up.
+    """
+    bl_idname = "report_card.popup"
+    bl_label = "O3DE Scene Exporter"
+    bl_options = {'REGISTER', 'INTERNAL'}
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+        layout = self.layout
+        col = layout.column()
+
+        # Check seleted Status
+        name_selections = [obj.name for obj in bpy.context.selected_objects]
+        # Check Transforms Status
+        transforms_status, world_location = utils.check_selected_transforms()
+        # Check Selected UVs
+        selected_uvs_check = utils.check_selected_uvs()
+        # Validate selection bone names if any
+        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
+        # Check to see which Animation Action is active
+        action_name, action_count = utils.check_for_animation_actions()
+        # Check the texture export option
+        if bpy.types.Scene.export_textures_folder:
+            texture_option = "Textures in texture folder."
+        elif not bpy.types.Scene.export_textures_folder:
+            texture_option = "Textures with Model(s)"
+        else:
+            texture_option = "No texture export."
+        # Check for UDPs on selected
+        lods, colliders = utils.check_for_udp()
+
+        # Get Selected Stats
+        vert_count, edge_count, polygon_count, material_count = utils.check_selected_stats_count()
+        # Get filename
+        file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+
+        # Start GUI Layout
+        issues = col.box()
+        issues.box()
+        issues.label(text="PREFLIGHT REPORT CARD", icon="COLLECTION_COLOR_04")
+        issues.label(text=f"Issues Found:")
+
+        self.reported_issues_int = 0
+
+        if not transforms_status:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Transforms have not been Applied (Frozen).")
+            self.reported_issues_int += 1
+        if not world_location == [0.0, 0.0, 0.0]:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Are not at world orgin {world_location}")
+            self.reported_issues_int += 1
+        if not selected_uvs_check:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Are missing UV's")
+            self.reported_issues_int += 1
+        if invalid_bone_names:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)", icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Have Invalid Bone Names for O3DE.")
+            issues.label(text=f"Please do not use these types of Characters")
+            issues.label(text=f"in Bone names <>[\]~.`")
+            self.reported_issues_int += 1
+        if not bpy.context.scene.unit_settings.length_unit == 'METERS':
+            issues.alert = True
+            issues.label(text=f"Warning, Your Scene is set to", icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"{bpy.context.scene.unit_settings.length_unit}, however, O3DE units are in Meters.")
+            self.reported_issues_int += 1
+        issues.label(text=f"({self.reported_issues_int}) Issues Found.")
+        if self.reported_issues_int == 0:
+            issues.alert = False
+            issues.label(text="No issues found.", icon="SEQUENCE_COLOR_04")
+
+        # General information
+        mesh_info = col.box()
+        mesh_info.box()
+        mesh_info.label(text="EXPORT FILE INFORMATION", icon="COLLECTION_COLOR_05")
+        mesh_info.label(text=f"Issues Found:")
+        mesh_info.label(text=f"Selected({len(name_selections)}): {name_selections}")
+        mesh_info.label(text=f"File Name: {file_name}")
+        mesh_info.label(text=f"Vert Count: {vert_count}")
+        mesh_info.label(text=f"Edge Count: {edge_count}")
+        mesh_info.label(text=f"Polygon Count: {polygon_count}")
+        mesh_info.label(text=f"Material Count: {material_count}")
+        mesh_info.label(text=f"Scene Units are in: {bpy.context.scene.unit_settings.length_unit}")
+        mesh_info.label(text=f"Transforms are applied: {transforms_status}")
+        mesh_info.label(text=f"Export as Triangles: {bpy.types.Scene.convert_mesh_to_triangles}")
+        mesh_info.label(text=f"Animation Options: {bpy.types.Scene.animation_export}")
+        mesh_info.label(text=f"Animation Action({action_count}) {action_name}")
+        mesh_info.label(text=f"Texture Options: {texture_option}")
+        mesh_info.label(text=f"Physics Collider: {colliders}")
+        mesh_info.label(text=f"LODS: {lods}")
+        mesh_info.label(text=f"Export to Path: {bpy.types.Scene.selected_o3de_project_path}")
+    
+    def export_files(self, file_name):
+        """!
+        This function will export the selected as an .fbx to the current project path.
+        @param file_name is the file name selected or string in export_file_name_o3de
+        """
+        # Add file ext
+        file_name = f'{file_name}.fbx'
+        fbx_exporter.fbx_file_exporter('', file_name)
+    
+    def execute(self, context):
+        """!
+        This function will check the current selected count and multi_file_export_o3de bool is True or False.
+        If multi_file_export_o3de is True it will export each selected object as an .fbx file, if False it will
+        export all objects selected as one .fbx.
+        @param context defualt for this blender class
+        """
+        # Validate a selection
+        valid_selection, selected_name = utils.check_selected()
+        # Check if there are multi selections
+        if len(selected_name) > 1:
+            if bpy.types.Scene.multi_file_export_o3de:
+                for obj_name in selected_name:
+                    # Deselect all or will just keep adding to selection
+                    bpy.ops.object.select_all(action='DESELECT')
+                    # Select a mesh in the loop
+                    bpy.data.objects[obj_name].select_set(True)
+                    # Remove some nasty invalid char
+                    file_name = re.sub(r'\W+', '', obj_name)
+                    # Export file
+                    self.export_files(file_name)
+            else:
+                # Remove some nasty invalid char
+                file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+                # Export file
+                self.export_files(file_name)
+        else:
+            # Remove some nasty invalid char
+            file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+            # Export file
+            self.export_files(file_name)
+        return{'FINISHED'}
+
+class ReportCardButton(bpy.types.Operator):
+    """!
+    This Class is for the UI Report Card Button
+    """
+    bl_idname = "vin.report_card_button"
+    bl_label = "O3DE Report Card Button"
+
+    def execute(self, context):
+        """!
+        This function will open report card window.
+        """
+        bpy.ops.report_card.popup('INVOKE_DEFAULT')
+        return{'FINISHED'}
+
+class WikiButton(bpy.types.Operator):
+    """!
+    This Class is for the UI Wiki Button
+    """
+    bl_idname = "vin.wiki"
+    bl_label = "O3DE Github Wiki"
+
+    def execute(self, context):
+        """!
+        This function will open a web browser window.
+        """
+        webbrowser.open(constants.WIKI_URL)
+        return{'FINISHED'}
+
+class CustomProjectPath(bpy.types.Operator, ImportHelper):
+    """!
+    This Class is for setting a custom project path
+    """
+    bl_idname = "project.open_filebrowser"
+    bl_label = "Project Assets Folder"
+    bl_options = {'PRESET', 'UNDO'}
+
+    filter_glob: StringProperty(
+        default="//",
+        options={'HIDDEN'},
+        subtype='NONE',
+        maxlen=255,  # Max internal buffer length, longer would be clamped.
+        )
+
+    def execute(self, context):
+        # Look at current project list, if path not in list add it
+        real_path =  Path(self.filepath)
+        if real_path.is_dir():
+            bpy.types.Scene.selected_o3de_project_path = str(Path(self.filepath))
+        else:
+            bpy.types.Scene.selected_o3de_project_path = str(Path(self.filepath).parent)
+        if bpy.types.Scene.selected_o3de_project_path not in context.scene.o3de_projects_list:
+            o3de_utils.save_project_list_json(str(bpy.types.Scene.selected_o3de_project_path))
+            # Refesh the addon
+            addon_utils.enable('SceneExporter')
+            # Rebuild the project list
+            o3de_utils.build_projects_list() 
+        return {'FINISHED'}
+
+class AddColliderMesh(bpy.types.Operator):
+    """!
+    This Class is for the UI Wiki Button
+    """
+    bl_idname = "vin.collider"
+    bl_label = "Add a PhysX Collider mesh to selected."
+
+    def execute(self, context):
+        """!
+        This function will create the collision mesh.
+        """
+        # Create a Pop-Up confirm window
+        bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_phys to mesh.'
+        bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
+        bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_phys')
+        bpy.types.Scene.pop_up_type = "UDP"
+        bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
+        return{'FINISHED'}
+
+class AddLODMesh(bpy.types.Operator):
+    """!
+    This Class is for the UI Wiki Button
+    """
+    bl_idname = "vin.lod"
+    bl_label = "Add a LOD mesh to selected."
+
+    def execute(self, context):
+        """!
+        This function will create the LOD mesh
+        """
+        # Create a Pop-Up confirm window
+        bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_lod to mesh.'
+        bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
+        bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_lod')
+        bpy.types.Scene.pop_up_type = "UDP"
+        bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
+        return{'FINISHED'}
+
+class ProjectsListDropDown(bpy.types.Operator):
+    """!
+    This Class is for the O3DE Projects UI List Drop Down.
+    """
+    bl_label = "Project List Dropdown"
+    bl_idname = "wm.projectlist"
+
+    def invoke(self, context, event):
+        """!
+        This function will invoke the blender windowe manager
+        """
+        window_manager = context.window_manager
+        return window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI
+        """
+        layout = self.layout
+        layout.prop(context.scene, 'o3de_projects_list')
+        
+    def execute(self, context):
+        """!
+        This will update the UI with the current o3de Project Title.
+        """
+        bpy.types.Scene.selected_o3de_project_path = context.scene.o3de_projects_list
+        return {'FINISHED'}
+
+class SceneExporterFileMenu(Operator, ExportHelper):
+    """!
+    This Class will export the 3d model as well
+    as textures in the user selected path.
+    """
+    bl_idname = "O3DE_FileExport"  # important since its how bpy.ops.import_test.some_data is constructed
+    bl_idname = "export_model.mesh_data"  # important since its how bpy.ops.import_test.some_data is constructed
+    bl_label = "Export to O3DE"
+
+    # ExportHelper mixin class uses this
+    filename_ext = ".fbx"
+
+    filter_glob: StringProperty(
+        default="*.fbx",
+        options={'HIDDEN'},
+        maxlen=255,  # Max internal buffer length, longer would be clamped.
+    )
+    # Extra options
+    export_textures_folder : BoolProperty(
+        name="Export Textures in a textures folder",
+        description="Export Textures in textures folder",
+        default=True,
+    )
+    export_mesh_as_triangles : BoolProperty(
+        name="Export Mesh as Triangles",
+        description="Export Mesh as Triangles"
+    )
+    # Add animation to export
+    export_animation : BoolProperty(
+        name="Keyframe Animation",
+        description="Export with Keyframe Animation",
+        default=False,
+    )
+
+    def execute(self, context):
+        """!
+        This function will select the Export Texture Option
+        """
+        bpy.types.Scene.export_textures_folder = self.export_textures_folder
+        bpy.types.Scene.file_menu_animation_export = self.export_animation
+        bpy.types.Scene.convert_mesh_to_triangles = self.export_mesh_as_triangles
+        if self.export_animation:
+            bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
+            utils.valid_animation_selection()
+        fbx_exporter.fbx_file_exporter(self.filepath, Path(self.filepath).stem)
+        return{'FINISHED'}
+
+class ExportOptionsListDropDown(bpy.types.Operator):
+    """!
+    This Class is for the O3DE Export Options UI List Drop Down
+    """
+    bl_label = "Texture Export Folder"
+    bl_idname = "wm.exportoptions"
+
+    def invoke(self, context, event):
+        """!
+        This function will invoke the blender windowe manager
+        """
+        window_manager = context.window_manager
+        return window_manager.invoke_props_dialog(self)
+        
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI
+        """
+        layout = self.layout
+        layout.prop(context.scene, 'texture_options_list')
+        
+    def execute(self, context):
+        """!
+        This function will Update Export Option Bool
+        """
+        if context.scene.texture_options_list == '0':
+            bpy.types.Scene.export_textures_folder = True
+        elif context.scene.texture_options_list == '1':
+            bpy.types.Scene.export_textures_folder = False
+        else:
+            bpy.types.Scene.export_textures_folder = None
+        return {'FINISHED'}
+        
+class AnimationOptionsListDropDown(bpy.types.Operator):
+    """!
+    This Class is for the O3DE Export Animations UI List Drop Down
+    """
+    bl_label = "Animation Export Options"
+    bl_idname = "wm.animationoptions"
+
+    def invoke(self, context, event):
+        """!
+        This function will invoke the blender windowe manager
+        """
+        window_manager = context.window_manager
+        return window_manager.invoke_props_dialog(self)
+        
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI
+        """
+        layout = self.layout
+        layout.prop(context.scene, 'animation_options_list')
+        
+    def execute(self, context):
+        """!
+        This function will Update Export Option Bool
+        """
+        if context.scene.animation_options_list == '0':
+            bpy.types.Scene.animation_export = constants.NO_ANIMATION
+        elif context.scene.animation_options_list == '1':
+            bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
+            utils.valid_animation_selection()
+        elif context.scene.animation_options_list == '2':
+            bpy.types.Scene.animation_export = constants.MESH_AND_RIG
+            utils.valid_animation_selection()
+        elif context.scene.animation_options_list == '3':
+            bpy.types.Scene.animation_export = constants.SKIN_ATTACHMENT
+            utils.valid_animation_selection()
+        return {'FINISHED'}
+
+def file_export_menu_add(self, context):
+    """!
+    This Function will add the Export to O3DE to the file menu Export
+    """
+    self.layout.operator(SceneExporterFileMenu.bl_idname, text="Export to O3DE")
+class O3deTools(Panel):
+    """!
+    This is the Blender UI Panel O3DE Tools Tab
+    """
+    bl_idname = "O3DE_TOOLS_PT_Panel"
+    bl_space_type = 'VIEW_3D'
+    bl_region_type = 'UI'
+    bl_label = f'O3DE Tools v{constants.PLUGIN_VERSION}'
+    bl_context = 'objectmode'
+    bl_category = 'O3DE'
+
+    def draw(self, context):
+        """!
+        This function draws this gui element in Blender UI. We will look at the Look at the
+        o3de engine manifest to see if o3de is currently install and gather the project paths
+        in a list drop down.
+        """
+        layout = self.layout
+        selected_objects = context.object
+        wm = context.window_manager
+        row = layout.row()
+
+        # Look at the o3de Engine Manifest
+        o3de_projects, engine_is_installed = o3de_utils.look_at_engine_manifest()
+        
+        # Validate a selection
+        valid_selection, selected_name = utils.check_selected()
+        
+        # Validate selection bone names if any
+        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
+
+        # Get the names of current selected
+        name_selections = [obj.name for obj in bpy.context.selected_objects]
+
+        # Checks to see if O3DE is installed
+        help_info_row = layout.box()
+        help_info_row.label(text="HELP / INFORMATION", icon="EVENT_A")
+        # Checks to see if O3DE is installed
+        if engine_is_installed:
+            wiki_row = layout.row()
+            wiki_row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
+            # Heads up of current selected objects and options
+            current_selected_options_row = layout.box()
+            current_selected_options_row.label(text="CURRENT SELECTED", icon="OUTLINER_DATA_GP_LAYER")
+            # Let user know how many objects are selected
+            installed_lable = layout.row()
+            
+            # Check if there are any selections
+            if len(name_selections) == 0: 
+                installed_lable.label(text='Selected: None')
+            else:
+                installed_lable.label(text=f'({len(selected_name)}) Selected: {selected_objects.name}')
+
+            # Show which project path is the current
+            project_path_lable = layout.row()
+            if not bpy.types.Scene.selected_o3de_project_path == '':
+                project = Path(bpy.types.Scene.selected_o3de_project_path).name
+                project_path_lable.label(text=f'Project: {project}')
+            else:
+                project_path_lable.label(text='Project: None')
+            
+            # Check to see which Animation Action is active
+            action_name, action_count = utils.check_for_animation_actions()
+            
+            # Let user choose animation export options
+            if bone_are_in_selected:
+                animation_action_lable = layout.row()
+                animation_export_lable = layout.row()
+                if bpy.types.Scene.animation_export == constants.NO_ANIMATION:
+                    animation_action_lable.label(text=f'Action: ')
+                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
+                else:
+                    animation_action_lable.label(text=f'Action: {action_name} ({action_count})')
+                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
+            else:
+                bpy.types.Scene.animation_export = constants.NO_ANIMATION
+
+            # This is the UI Export Texture Option Label
+            texture_export_option_label = layout.row()
+            if bpy.types.Scene.export_textures_folder:
+                texture_export_option_label.label(text='Texture Export: Textures Folder')
+            elif bpy.types.Scene.export_textures_folder is False:
+                texture_export_option_label.label(text='Texture Export: With Model')
+            elif bpy.types.Scene.export_textures_folder is None:
+                texture_export_option_label.label(text='Texture Export: Off')
+            
+            # User selects projects folder path
+            o3de_projects_row = layout.box()
+            o3de_projects_row.label(text="PROJECTS", icon="EVENT_B")
+
+            # This is the UI Porjects List
+            o3de_projects_panel = layout.row()
+            o3de_projects_panel.operator('wm.projectlist', text='O3DE Projects', icon="OUTLINER")
+
+            # Let user choose a custom project path
+            local_project_path = layout.row()
+            local_project_path.operator("project.open_filebrowser", text="Add Custom Project Path", icon="OUTLINER_OB_GROUP_INSTANCE")
+
+            # User can add UDP
+            user_defined_properties_row = layout.box()
+            user_defined_properties_row.label(text="USER DEFINED (UDP)", icon="EVENT_C")
+            
+            # Let user create a O3DE Collider Mesh for Physx
+            create_collider_button = layout.row()
+            create_collider_button.operator("vin.collider", text='Create PhysX Collider', icon="SNAP_VOLUME")
+
+            # Let user create a O3DE LOD Mesh
+            create_lod_button = layout.row()
+            create_lod_button.operator("vin.lod", text='Create LOD', icon="MOD_REMESH")
+            
+            # User selected export options
+            o3de_projects_row = layout.box()
+            o3de_projects_row.label(text="EXPORT OPTIONS", icon="EVENT_D")
+
+            # This is the UI Texture Export Options List
+            texture_export_options_panel = layout.row()
+            texture_export_options_panel.operator('wm.exportoptions', text='Texture Export Options', icon="OUTPUT")
+            
+            # This is the UI Animation Export Options List
+            animation_export_options_panel = layout.row()
+            animation_export_options_panel.operator('wm.animationoptions', text='Animation Export Options', icon="POSE_HLT")
+
+            # If more than one object is selected, let user choose export options
+            if len(name_selections) > 1:
+                multi_file_export_label = "Export Multi-Files" if wm.multi_file_export_toggle else "Export a Single File"
+                multi_file_export_button = layout.row()
+                multi_file_export_button.prop(wm, "multi_file_export_toggle", text=multi_file_export_label, toggle=True)
+                if wm.multi_file_export_toggle:
+                    bpy.types.Scene.multi_file_export_o3de = True
+                else:
+                    bpy.types.Scene.multi_file_export_o3de = False
+            else:
+                wm.multi_file_export_toggle = False
+                bpy.types.Scene.multi_file_export_o3de = False
+            # Export mesh options
+            export_mesh_triangles_quads_label = "Exporting as Triangles" if wm.mesh_triangle_export_toggle else "Exporting as Quads"
+            export_mesh_triangles_quads_button = layout.row()
+            export_mesh_triangles_quads_button.prop(wm, "mesh_triangle_export_toggle", text=export_mesh_triangles_quads_label, toggle=True)
+            if wm.mesh_triangle_export_toggle:
+                bpy.types.Scene.convert_mesh_to_triangles = True
+            else:
+                bpy.types.Scene.convert_mesh_to_triangles = False
+            
+            # This checks to see if we should enable the export button
+            export_row = layout.box()
+            export_row.label(text="EXPORT FILE", icon="EVENT_E")
+            # Let user choose a custom file name
+            file_name_lable = layout.row()
+            if not wm.multi_file_export_toggle:
+                file_name_lable.label(text='Export File Name')
+                file_name_export_input = self.layout.column(align = True)
+                file_name_export_input.prop(context.scene, "export_file_name_o3de")
+            # Export File
+            export_files_row = layout.row()
+            export_ready_row = layout.row()
+            # Enable Rows on and off
+            if bpy.types.Scene.selected_o3de_project_path == '':
+                export_files_row.enabled = False
+                export_ready_row.label(text='No Project Selected.')
+            elif not utils.check_if_valid_path(bpy.types.Scene.selected_o3de_project_path):
+                export_files_row.enabled = False
+                export_ready_row.label(text='Project Path Not Found.')
+            elif not bpy.types.Scene.export_good_o3de:
+                export_files_row.enabled = False
+                export_ready_row.label(text='Not Ready for Export.')
+            elif not valid_selection:
+                export_files_row.enabled = False
+                export_ready_row.label(text='Nothing Selected.')
+            elif invalid_bone_names == False:
+                export_files_row.enabled = False
+                export_ready_row.label(text='Invalid Char in Bone Names.')
+            else:
+                export_files_row.enabled = True
+            # Final Export Files Button
+            export_files_row.operator('vin.report_card_button', text='EXPORT TO O3DE', icon="BLENDER")
+        else:
+            # If O3DE is not installed we tell the user
+            row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
+            not_installed = layout.row()
+            not_installed.label(text='O3DE Needs to be installed')
+            
+

+ 557 - 557
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/utils.py → Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/SceneExporter/utils.py

@@ -1,558 +1,558 @@
-# coding:utf-8
-#!/usr/bin/python
-#
-# 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
-#
-#
-# -------------------------------------------------------------------------
-import bpy
-from bpy.props import EnumProperty
-import shutil
-from pathlib import Path
-import re
-
-from . import constants
-from . import ui
-from . import o3de_utils
-
-def select_object(obj):
-    """!
-    This function will select just one object
-    """
-    bpy.ops.object.select_all(action='DESELECT')
-    bpy.context.view_layer.objects.active = obj
-    obj.select_set(True)
-
-def check_selected():
-    """!
-    This function will check to see if the user has selected a object
-    """
-    selections = [obj.name for obj in bpy.context.selected_objects]
-    if selections == []:
-        return False, "None"
-    else:
-        return True, selections
-
-def check_selected_bone_names():
-    """!
-    This function will check to see if there is a ARMATURE selected and if the bone names are o3de compatable.
-    """
-    context = bpy.context
-    obj = context.object
-
-    invalid_bone_names = None
-    bone_are_in_selected = None
-
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            if selected_obj.type == "ARMATURE":
-                bone_are_in_selected = True
-                try:
-                    for pb in obj.pose.bones:
-                        # Validate all chars in string.
-                        check_re =  re.compile(r"^[^<>/{}[\]~.`]*$")
-                        if not check_re.match(pb.name):
-                            # If any in the ARMATURE are named invalid return will fail.
-                            invalid_bone_names = False
-                except AttributeError:
-                    pass
-    return invalid_bone_names, bone_are_in_selected
-
-def selected_hierarchy_and_rig_animation():
-    """!
-    This function will select the attached animation armature rig and all of its children.
-    """
-    obj = bpy.ops.object
-    
-    for selected_obj in bpy.context.selected_objects:
-        if selected_obj is not []:
-            obj.select_grouped(extend=True, type='SIBLINGS')
-            obj.select_grouped(extend=True, type='PARENT')
-            obj.select_grouped(extend=True, type='CHILDREN_RECURSIVE')
-            obj.select_grouped(extend=True, type='PARENT')
-
-def selected_attachment_and_rig_animation():
-    """!
-    This function will select the selected attachments armature rig.
-    """
-    obj = bpy.ops.object
-    
-    for selected_obj in bpy.context.selected_objects:
-        if selected_obj is not []:
-            obj.select_grouped(extend=True, type='PARENT')
-
-def valid_animation_selection():
-    """!
-    This function will check to see if user has selected a valid level in
-    the hierarchy for exporting the selected mesh, armature rig and animation.
-    """
-    object_selections = [obj for obj in bpy.context.selected_objects]
-    if object_selections:
-        obj = object_selections[0]
-
-        if obj.type == 'ARMATURE':
-            bpy.types.Scene.export_good_o3de = False
-            ui.message_box("You must select lowest level mesh of Armature.", "O3DE Tools", "ERROR")
-        elif obj.children:
-            bpy.types.Scene.export_good_o3de = False
-            ui.message_box("You need to select base mesh level.", "O3DE Tools", "ERROR")
-        else:
-            bpy.types.Scene.export_good_o3de = True
-            if bpy.types.Scene.animation_export == constants.SKIN_ATTACHMENT:
-                selected_attachment_and_rig_animation()
-            else:
-                selected_hierarchy_and_rig_animation()
-
-def check_for_animation_actions():
-    """!
-    This function will check to see if the current scene has animation actions
-    return actions[0] is the top level animation action
-    return len(actions) is the number of available actions
-    """
-    obj = bpy.context.object
-    actions = []
-    active_action_name = ""
-    # Look for animation actions available
-    for animations in bpy.data.actions:
-        actions.append(animations.name)
-    # Look for Active Animation Action
-    try:
-        if not obj.animation_data == None:
-            active_action_name = obj.animation_data.action.name
-        else:
-            active_action_name = ""
-    except AttributeError:
-        pass
-    return active_action_name, len(actions)
-
-def check_if_valid_path(file_path):
-    """!
-    This function will check to see if a file path exist and return a bool
-    """
-    if Path(file_path).exists():
-        return True
-    else:
-        return False
-
-def get_selected_materials(selected):
-    """!
-    This function will check the selected mesh and find material are connected
-    then build a material list for selected.
-    @param selected This is your current selected mesh(s)
-    """
-    materials = []
-    # Find Connected materials attached to mesh
-    for obj in selected:
-        try:
-            materials = get_all_materials(obj, materials)
-        except AttributeError:
-            pass
-    return materials
-
-def get_all_materials(obj, materials):
-    """!
-    This function will loop through all assigned materails slots on a mesh and the attach textures.
-    then build a material list for selected.
-    @param selected This is your current selected mesh(s)
-    """
-    for mat in obj.material_slots:
-        try:
-            if mat.name not in materials:
-                materials.append(mat.name)
-        except AttributeError:
-            pass
-    return materials
-
-def loop_through_selected_materials(texture_file_path):
-    """!
-    This function will loop the the selected materials and copy the files to the o3de project folder.
-    @param texture_file_path This is the current o3de projects texture file path selected for texture export.
-    """
-    if not Path(texture_file_path).exists():
-        Path(texture_file_path).mkdir(exist_ok=True)
-    # retrive the list of seleted mesh materials
-    selected_materials = get_selected_materials(bpy.context.selected_objects)
-    # Loop through Materials
-    for mat in selected_materials:
-        # Get the material
-        material = bpy.data.materials[mat]
-        # Loop through material node tree and get all the texture iamges
-        for img in material.node_tree.nodes:
-            if img.type == 'TEX_IMAGE':
-                # Frist make sure the image is not packed inside blender
-                if img.image.packed_file:
-                    if Path(img.image.name).suffix in constants.IMAGE_EXT:
-                        bpy.data.images[img.image.name].filepath = str(Path(texture_file_path).joinpath(img.image.name))
-                    else:
-                        ext = '.png'
-                        build_string = f'{img.image.name}{ext}'
-                        bpy.data.images[img.image.name].filepath = str(Path(texture_file_path).joinpath(build_string))
-                    
-                    bpy.data.images[img.image.name].save()
-                full_path = Path(bpy.path.abspath(img.image.filepath, library=img.image.library))
-                base_name = Path(full_path).name
-                if not full_path.exists(): # if embedded path doesnt exist, check current folder
-                    full_path = Path(bpy.data.filepath).parent.joinpath(base_name)
-                o3de_texture_path = Path(texture_file_path).joinpath(base_name)
-                # Add to stored_image_source_paths to replace later
-                if not check_file_paths_duplicate(full_path, o3de_texture_path): # We check first if its not already copied over.
-                    bpy.types.Scene.stored_image_source_paths[img.image.name] = full_path
-                    # Copy the image to Destination
-                    try:
-                        bpy.data.images[img.image.name].filepath = str(o3de_texture_path)
-                        if full_path.exists():
-                            copy_texture_file(full_path, o3de_texture_path)
-                        else:
-                            bpy.data.images[img.image.name].save() 
-                            # Save image to location
-                    except (FileNotFoundError, RuntimeError):
-                        pass
-                img.image.reload()
-
-def copy_texture_file(source_path, destination_path):
-    """!
-    This function will copy project texture files to a O3DE textures folder
-    @param source_path This is the texture source path
-    @param destination_path This is the O3DE destination path
-    """
-    destination_size = -1
-    source_size = source_path.stat().st_size
-    if destination_path.exists():
-        destination_size = destination_path.stat().st_size
-    if source_size != destination_size:
-        shutil.copyfile(source_path, destination_path)
-
-def clone_repath_images(fileMenuExport, texture_file_path, projectSelectionList):
-    """!
-    This function will copy project texture files to a
-    O3DE textures folder and repath the Blender materials
-    then repath them to thier orginal.
-    @param fileMenuExport This is a bool to let us know if this is a Blender File Menu Export
-    @param texture_file_path This is our o3de projects texture file export path
-    @param projectSelectionList This is a list of our o3de projects
-    """
-    # Lets create a dictionary to store all the source paths to place back after export
-    bpy.types.Scene.stored_image_source_paths = {}
-
-    dir_path = Path(texture_file_path)
-    # Make or do not make a texture folder
-    if fileMenuExport:
-        # FILE MENU MENU EXPORT
-        if bpy.types.Scene.export_textures_folder:
-            # We do want the texture folder
-            texture_file_path = Path(dir_path.parent).joinpath('textures')
-            if not Path(texture_file_path).exists():
-                Path(texture_file_path).mkdir(exist_ok=True)
-            loop_through_selected_materials(texture_file_path)
-        else:
-            # We do not want the texture folder
-            texture_file_path = Path(dir_path.parent)
-            loop_through_selected_materials(texture_file_path)
-        # Check to see if this project is listed in our projectSelectionList if not add it.
-        if bpy.types.Scene.selected_o3de_project_path not in projectSelectionList:
-            o3de_utils.save_project_list_json(str(bpy.types.Scene.selected_o3de_project_path))
-            o3de_utils.build_projects_list()
-        bpy.types.Scene.o3de_projects_list = EnumProperty(items=projectSelectionList, name='')
-    elif fileMenuExport is None:
-        # TOOL MENU EXPORT BUT WAS EXPORTED ONCE IN FILE MENU
-        if bpy.types.Scene.export_textures_folder:
-            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('textures')
-            loop_through_selected_materials(texture_file_path)
-        else:
-            texture_file_path = bpy.types.Scene.selected_o3de_project_path
-            if not Path(texture_file_path).exists():
-                Path(texture_file_path).mkdir(exist_ok=True)
-            loop_through_selected_materials(texture_file_path)
-    else:
-        # TOOL MENU EXPORT
-        if bpy.types.Scene.export_textures_folder:
-            # We want the texture folder
-            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets', 'textures')
-            loop_through_selected_materials(texture_file_path)
-        else:
-            # We do not the texture folder
-            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets')
-            loop_through_selected_materials(texture_file_path)
-
-def check_file_paths_duplicate(source_path, destination_path):
-    """!
-    This function check to see if Source Path and Dest Path are the same
-    @param source_path This is the non o3de source texture path of the texture
-    @param destination_path This is the destination o3de project texture path
-    """
-    try:
-        check_file = Path(source_path)
-        if check_file.samefile(destination_path):
-            return True
-        else:
-            return False
-    except FileNotFoundError:
-        pass
-
-def replace_stored_paths():
-    """!
-    This Function will replace all the repathed image paths to thier origninal source paths
-    """
-    for image in bpy.data.images:
-        try:
-            # Find image and replace source path
-            bpy.data.images[image.name].filepath = str(bpy.types.Scene.stored_image_source_paths[image.name])
-        except KeyError:
-            pass
-        image.reload()
-    bpy.types.Scene.stored_image_source_paths = {}
-
-
-def duplicate_selected(selected_objects, rename):
-    """!
-    This function will duplicate selected objects with a new name string
-    @param selected_objects This is the current selected object
-    @param rename this is the duplicate object name
-    @param return duplicated object
-    """
-    # Duplicate the mesh and add add the UDP name extension
-    duplicate_object = bpy.data.objects.new(f'{selected_objects.name}{bpy.types.Scene.udp_type}', bpy.data.objects[selected_objects.name].data)
-    # Add copy to current collection in scene
-    bpy.context.collection.objects.link(duplicate_object)
-    # Rename duplicated object
-    duplicate_object.name = rename
-    duplicate_object.data.name = rename
-    return duplicate_object
-
-def check_for_blender_int_ext(duplicated_node_name):
-    """!
-    This function will check if blender adds on its own .000 ext to a node name
-    @param duplicated_node_name This is the name string of the duplicate object
-    @param return name string
-    """
-    if '.' in duplicated_node_name:
-            name, ext = duplicated_node_name.split(".")
-            node_name = name
-    else:
-        name = duplicated_node_name
-    return name
-
-def check_for_udp():
-    """!
-    This function will check if selected has and custom properties 
-    Example: print( f"Value: {obj_upd_keys} Key: {bpy.data.objects[selected_obj.name][obj_upd_keys] } ")
-    """
-    context = bpy.context
-    # Create list to store selected meshs udps
-    lod_list = []
-    colliders_list = []
-
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            if selected_obj.type == "MESH":
-                for obj_upd_keys in bpy.data.objects[selected_obj.name].keys():
-                    if obj_upd_keys not in '_RNA_UI':
-                        if obj_upd_keys == "o3de_atom_lod":
-                            lod_list.append(True)
-                        if obj_upd_keys == "o3de_atom_phys":
-                            colliders_list.append(True)
-    lods = any(lod_list)
-    colliders = any(colliders_list)      
-    return lods, colliders
-
-def create_udp():
-    """!
-    This function will create a copy of a selected mesh and create an o3de PhysX collider PhysX Mesh that will
-    autodect in o3de if you have a PhysX Collder Component.
-    """
-    selected_objects = bpy.context.object
-    # Check to see if bool was check on or of on pop-up question
-    if bpy.types.Scene.pop_up_question_bool:
-        # Check to see if name string already contails a _LOD int
-        if bpy.types.Scene.udp_type == "_lod":
-            if re.search(r"_lod(?:)\d", selected_objects.name):
-                # Lets get the current LOD Level
-                ext_result = re.search(r"_lod(?:)\d", selected_objects.name)
-                lod_level_int = ext_result.group().split("_lod")
-                # Lets get the level and add the next
-                lod_level = int(lod_level_int[1]) + 1
-                # rename to the correct lod level
-                base_name = selected_objects.name.rsplit("_",1)
-                # Duplicate the object
-                duplicate_object = duplicate_selected(selected_objects, f"{base_name[0]}_lod{lod_level}")
-                # ADD UDP
-                duplicate_object["o3de_atom_lod"] = f"_lod{lod_level}"
-            else:
-                duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_lod0")
-                # ADD UDP
-                duplicate_object["o3de_atom_lod"] = "_lod0"
-        # If UDP Type is _phys
-        if bpy.types.Scene.udp_type == "_phys":
-            duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_phys")
-            duplicate_object["o3de_atom_phys"] = "_phys"
-        # Select the duplicated object only
-        select_object(duplicate_object)
-        # Add the Decimate Modifier on for user. Since both _lod and _phys will need this modifier
-        #  for now we will have it as a default.
-        add_remove_modifier("DECIMATE", True)
-    else:
-        if bpy.types.Scene.udp_type == "_lod":
-                selected_objects['o3de_atom_lod'] = "_lod0"
-        if bpy.types.Scene.udp_type == "_phys":
-            selected_objects["o3de_atom_phys"] = "_phys"
-
-def compair_set(list_a, list_b):
-    """!
-    This function will check to see if there are any difference between two list
-    """
-    set_a = list_a
-    set_b = list_b
-    if set_a == set_b:
-        return True
-    else:
-        return False
-
-def add_remove_modifier(modifier_name, add):
-    """!
-    This function will add or remove a modifier to selected
-    @param modifier_name is the name of the modifier you wish to add or remove
-    @param add if Bool True will add the modifier, if False will remove 
-    """
-    context = bpy.context
-
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            if selected_obj.type == "MESH":
-                if add:
-                    # Set the mesh active
-                    bpy.context.view_layer.objects.active = selected_obj
-                    # Add Modifier
-                    bpy.ops.object.modifier_add(type=modifier_name)
-                    if modifier_name == "TRIANGULATE":
-                        bpy.context.object.modifiers["Triangulate"].keep_custom_normals = True
-                else:
-                    # Set the mesh active
-                    bpy.context.view_layer.objects.active = selected_obj
-                    # Remove Modifier
-                    bpy.ops.object.modifier_remove(modifier=modifier_name)
-
-def check_selected_stats_count():
-    """!
-    This function will check selected and get poly counts
-    """
-    # We will make list for each selection
-    mesh_vertices_list = []
-    mesh_edges_list = []
-    mesh_polygons_list = []
-    mesh_material_list = []
-    
-    context = bpy.context
-    
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            if selected_obj.type == "MESH":
-                mesh_stats = selected_obj.data
-                # Vert, Edge, Poly Counts
-                mesh_vertices = len(mesh_stats.vertices)
-                mesh_edges = len(mesh_stats.edges)
-                mesh_polygons = len(mesh_stats.polygons)
-                mesh_materials = len(mesh_stats.materials)
-                mesh_vertices_list.append(mesh_vertices)
-                mesh_edges_list.append(mesh_edges)
-                mesh_polygons_list.append(mesh_polygons)
-                # Material Count
-                mesh_material_list.append(mesh_materials)
-
-    vert_count = sum(mesh_vertices_list)
-    edge_count = sum(mesh_edges_list)
-    polygon_count = sum(mesh_polygons_list)
-    material_count = sum(mesh_material_list)
-    # Reset the list
-    mesh_vertices_list = []
-    mesh_edges_list = []
-    mesh_polygons_list = []
-    mesh_material_list = []
-    return vert_count, edge_count, polygon_count, material_count
-
-def check_selected_uvs():
-    """!
-    This function will check to see if there are UVs
-    """
-    context = bpy.context
-    # We will make list for each selection and compair if there are UVs.
-    mesh_uv_list = []
-    
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            if selected_obj.type == "MESH":
-                if selected_obj.data.uv_layers:
-                    mesh_uv_list.append(True)
-                else:
-                    mesh_uv_list.append(False)
-    object_uv_list = all(mesh_uv_list)
-    # If all objects have UVs
-    if object_uv_list:
-        # Clear UV List
-        mesh_uv_list = []
-        return True
-    else:
-        # Clear UV List
-        mesh_uv_list = []
-        return False
-
-def check_selected_transforms():
-    """!
-    This function will check to see if there are unfrozen transfors and to warn the artist before export.
-    """
-    context = bpy.context
-    # We will make list for each selection and compair to a frozzen transfrom.
-    location_list = []
-    location_source = [0.0, 0.0, 0.0]
-    rotation_list = []
-    rotation_source = [1.0, 0.0, 0.0, 0.0]
-    scale_list = []
-    scale_source = [1.0, 1.0, 1.0]
-    location_good = True
-    rotation_good = True
-    scale_good = True
-
-    for selected_obj in context.selected_objects:
-        if selected_obj is not []:
-            obj = selected_obj.matrix_world.decompose()
-            # Start a matrix count
-            count = 0
-            for vector in obj:
-                # We will count from 1 to 3, every object will have a matrix of Location, Rotation, and Scale
-                count += 1
-                if count == 1:
-                    # Add vector3 to Location list
-                    for floatdata in vector:
-                        location_list.append(floatdata)
-                elif count == 2:
-                    # Add vector4 to Rotation list
-                    for floatdata in vector:
-                        rotation_list.append(floatdata)
-                else:
-                    # Add vector3 to Scale list
-                    for floatdata in vector:
-                        scale_list.append(floatdata)
-                    count = 0
-                    
-            # Lets look at the results and compair the sets
-            if not compair_set(location_list, location_source):
-                location_good = False
-            elif not compair_set(rotation_list, rotation_source):
-                rotation_good = False
-            elif not compair_set(scale_list, scale_source):
-                scale_good = False
-            # Check if all are true or false
-            check_transfroms_bools = [location_good, rotation_good, scale_good]
-            if all(check_transfroms_bools):
-                return True, location_list
-            else:
-                return False, location_list
-        # Reset the list
-        location_list = []
-        rotation_list = []
-        scale_list = []
-        location_good = True
-        rotation_good = True
+# coding:utf-8
+#!/usr/bin/python
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+import bpy
+from bpy.props import EnumProperty
+import shutil
+from pathlib import Path
+import re
+
+from . import constants
+from . import ui
+from . import o3de_utils
+
+def select_object(obj):
+    """!
+    This function will select just one object
+    """
+    bpy.ops.object.select_all(action='DESELECT')
+    bpy.context.view_layer.objects.active = obj
+    obj.select_set(True)
+
+def check_selected():
+    """!
+    This function will check to see if the user has selected a object
+    """
+    selections = [obj.name for obj in bpy.context.selected_objects]
+    if selections == []:
+        return False, "None"
+    else:
+        return True, selections
+
+def check_selected_bone_names():
+    """!
+    This function will check to see if there is a ARMATURE selected and if the bone names are o3de compatable.
+    """
+    context = bpy.context
+    obj = context.object
+
+    invalid_bone_names = None
+    bone_are_in_selected = None
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "ARMATURE":
+                bone_are_in_selected = True
+                try:
+                    for pb in obj.pose.bones:
+                        # Validate all chars in string.
+                        check_re =  re.compile(r"^[^<>/{}[\]~.`]*$")
+                        if not check_re.match(pb.name):
+                            # If any in the ARMATURE are named invalid return will fail.
+                            invalid_bone_names = False
+                except AttributeError:
+                    pass
+    return invalid_bone_names, bone_are_in_selected
+
+def selected_hierarchy_and_rig_animation():
+    """!
+    This function will select the attached animation armature rig and all of its children.
+    """
+    obj = bpy.ops.object
+    
+    for selected_obj in bpy.context.selected_objects:
+        if selected_obj is not []:
+            obj.select_grouped(extend=True, type='SIBLINGS')
+            obj.select_grouped(extend=True, type='PARENT')
+            obj.select_grouped(extend=True, type='CHILDREN_RECURSIVE')
+            obj.select_grouped(extend=True, type='PARENT')
+
+def selected_attachment_and_rig_animation():
+    """!
+    This function will select the selected attachments armature rig.
+    """
+    obj = bpy.ops.object
+    
+    for selected_obj in bpy.context.selected_objects:
+        if selected_obj is not []:
+            obj.select_grouped(extend=True, type='PARENT')
+
+def valid_animation_selection():
+    """!
+    This function will check to see if user has selected a valid level in
+    the hierarchy for exporting the selected mesh, armature rig and animation.
+    """
+    object_selections = [obj for obj in bpy.context.selected_objects]
+    if object_selections:
+        obj = object_selections[0]
+
+        if obj.type == 'ARMATURE':
+            bpy.types.Scene.export_good_o3de = False
+            ui.message_box("You must select lowest level mesh of Armature.", "O3DE Tools", "ERROR")
+        elif obj.children:
+            bpy.types.Scene.export_good_o3de = False
+            ui.message_box("You need to select base mesh level.", "O3DE Tools", "ERROR")
+        else:
+            bpy.types.Scene.export_good_o3de = True
+            if bpy.types.Scene.animation_export == constants.SKIN_ATTACHMENT:
+                selected_attachment_and_rig_animation()
+            else:
+                selected_hierarchy_and_rig_animation()
+
+def check_for_animation_actions():
+    """!
+    This function will check to see if the current scene has animation actions
+    return actions[0] is the top level animation action
+    return len(actions) is the number of available actions
+    """
+    obj = bpy.context.object
+    actions = []
+    active_action_name = ""
+    # Look for animation actions available
+    for animations in bpy.data.actions:
+        actions.append(animations.name)
+    # Look for Active Animation Action
+    try:
+        if not obj.animation_data == None:
+            active_action_name = obj.animation_data.action.name
+        else:
+            active_action_name = ""
+    except AttributeError:
+        pass
+    return active_action_name, len(actions)
+
+def check_if_valid_path(file_path):
+    """!
+    This function will check to see if a file path exist and return a bool
+    """
+    if Path(file_path).exists():
+        return True
+    else:
+        return False
+
+def get_selected_materials(selected):
+    """!
+    This function will check the selected mesh and find material are connected
+    then build a material list for selected.
+    @param selected This is your current selected mesh(s)
+    """
+    materials = []
+    # Find Connected materials attached to mesh
+    for obj in selected:
+        try:
+            materials = get_all_materials(obj, materials)
+        except AttributeError:
+            pass
+    return materials
+
+def get_all_materials(obj, materials):
+    """!
+    This function will loop through all assigned materails slots on a mesh and the attach textures.
+    then build a material list for selected.
+    @param selected This is your current selected mesh(s)
+    """
+    for mat in obj.material_slots:
+        try:
+            if mat.name not in materials:
+                materials.append(mat.name)
+        except AttributeError:
+            pass
+    return materials
+
+def loop_through_selected_materials(texture_file_path):
+    """!
+    This function will loop the the selected materials and copy the files to the o3de project folder.
+    @param texture_file_path This is the current o3de projects texture file path selected for texture export.
+    """
+    if not Path(texture_file_path).exists():
+        Path(texture_file_path).mkdir(exist_ok=True)
+    # retrive the list of seleted mesh materials
+    selected_materials = get_selected_materials(bpy.context.selected_objects)
+    # Loop through Materials
+    for mat in selected_materials:
+        # Get the material
+        material = bpy.data.materials[mat]
+        # Loop through material node tree and get all the texture iamges
+        for img in material.node_tree.nodes:
+            if img.type == 'TEX_IMAGE':
+                # Frist make sure the image is not packed inside blender
+                if img.image.packed_file:
+                    if Path(img.image.name).suffix in constants.IMAGE_EXT:
+                        bpy.data.images[img.image.name].filepath = str(Path(texture_file_path).joinpath(img.image.name))
+                    else:
+                        ext = '.png'
+                        build_string = f'{img.image.name}{ext}'
+                        bpy.data.images[img.image.name].filepath = str(Path(texture_file_path).joinpath(build_string))
+                    
+                    bpy.data.images[img.image.name].save()
+                full_path = Path(bpy.path.abspath(img.image.filepath, library=img.image.library))
+                base_name = Path(full_path).name
+                if not full_path.exists(): # if embedded path doesnt exist, check current folder
+                    full_path = Path(bpy.data.filepath).parent.joinpath(base_name)
+                o3de_texture_path = Path(texture_file_path).joinpath(base_name)
+                # Add to stored_image_source_paths to replace later
+                if not check_file_paths_duplicate(full_path, o3de_texture_path): # We check first if its not already copied over.
+                    bpy.types.Scene.stored_image_source_paths[img.image.name] = full_path
+                    # Copy the image to Destination
+                    try:
+                        bpy.data.images[img.image.name].filepath = str(o3de_texture_path)
+                        if full_path.exists():
+                            copy_texture_file(full_path, o3de_texture_path)
+                        else:
+                            bpy.data.images[img.image.name].save() 
+                            # Save image to location
+                    except (FileNotFoundError, RuntimeError):
+                        pass
+                img.image.reload()
+
+def copy_texture_file(source_path, destination_path):
+    """!
+    This function will copy project texture files to a O3DE textures folder
+    @param source_path This is the texture source path
+    @param destination_path This is the O3DE destination path
+    """
+    destination_size = -1
+    source_size = source_path.stat().st_size
+    if destination_path.exists():
+        destination_size = destination_path.stat().st_size
+    if source_size != destination_size:
+        shutil.copyfile(source_path, destination_path)
+
+def clone_repath_images(fileMenuExport, texture_file_path, projectSelectionList):
+    """!
+    This function will copy project texture files to a
+    O3DE textures folder and repath the Blender materials
+    then repath them to thier orginal.
+    @param fileMenuExport This is a bool to let us know if this is a Blender File Menu Export
+    @param texture_file_path This is our o3de projects texture file export path
+    @param projectSelectionList This is a list of our o3de projects
+    """
+    # Lets create a dictionary to store all the source paths to place back after export
+    bpy.types.Scene.stored_image_source_paths = {}
+
+    dir_path = Path(texture_file_path)
+    # Make or do not make a texture folder
+    if fileMenuExport:
+        # FILE MENU MENU EXPORT
+        if bpy.types.Scene.export_textures_folder:
+            # We do want the texture folder
+            texture_file_path = Path(dir_path.parent).joinpath('textures')
+            if not Path(texture_file_path).exists():
+                Path(texture_file_path).mkdir(exist_ok=True)
+            loop_through_selected_materials(texture_file_path)
+        else:
+            # We do not want the texture folder
+            texture_file_path = Path(dir_path.parent)
+            loop_through_selected_materials(texture_file_path)
+        # Check to see if this project is listed in our projectSelectionList if not add it.
+        if bpy.types.Scene.selected_o3de_project_path not in projectSelectionList:
+            o3de_utils.save_project_list_json(str(bpy.types.Scene.selected_o3de_project_path))
+            o3de_utils.build_projects_list()
+        bpy.types.Scene.o3de_projects_list = EnumProperty(items=projectSelectionList, name='')
+    elif fileMenuExport is None:
+        # TOOL MENU EXPORT BUT WAS EXPORTED ONCE IN FILE MENU
+        if bpy.types.Scene.export_textures_folder:
+            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('textures')
+            loop_through_selected_materials(texture_file_path)
+        else:
+            texture_file_path = bpy.types.Scene.selected_o3de_project_path
+            if not Path(texture_file_path).exists():
+                Path(texture_file_path).mkdir(exist_ok=True)
+            loop_through_selected_materials(texture_file_path)
+    else:
+        # TOOL MENU EXPORT
+        if bpy.types.Scene.export_textures_folder:
+            # We want the texture folder
+            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets', 'textures')
+            loop_through_selected_materials(texture_file_path)
+        else:
+            # We do not the texture folder
+            texture_file_path = Path(bpy.types.Scene.selected_o3de_project_path).joinpath('Assets')
+            loop_through_selected_materials(texture_file_path)
+
+def check_file_paths_duplicate(source_path, destination_path):
+    """!
+    This function check to see if Source Path and Dest Path are the same
+    @param source_path This is the non o3de source texture path of the texture
+    @param destination_path This is the destination o3de project texture path
+    """
+    try:
+        check_file = Path(source_path)
+        if check_file.samefile(destination_path):
+            return True
+        else:
+            return False
+    except FileNotFoundError:
+        pass
+
+def replace_stored_paths():
+    """!
+    This Function will replace all the repathed image paths to thier origninal source paths
+    """
+    for image in bpy.data.images:
+        try:
+            # Find image and replace source path
+            bpy.data.images[image.name].filepath = str(bpy.types.Scene.stored_image_source_paths[image.name])
+        except KeyError:
+            pass
+        image.reload()
+    bpy.types.Scene.stored_image_source_paths = {}
+
+
+def duplicate_selected(selected_objects, rename):
+    """!
+    This function will duplicate selected objects with a new name string
+    @param selected_objects This is the current selected object
+    @param rename this is the duplicate object name
+    @param return duplicated object
+    """
+    # Duplicate the mesh and add add the UDP name extension
+    duplicate_object = bpy.data.objects.new(f'{selected_objects.name}{bpy.types.Scene.udp_type}', bpy.data.objects[selected_objects.name].data)
+    # Add copy to current collection in scene
+    bpy.context.collection.objects.link(duplicate_object)
+    # Rename duplicated object
+    duplicate_object.name = rename
+    duplicate_object.data.name = rename
+    return duplicate_object
+
+def check_for_blender_int_ext(duplicated_node_name):
+    """!
+    This function will check if blender adds on its own .000 ext to a node name
+    @param duplicated_node_name This is the name string of the duplicate object
+    @param return name string
+    """
+    if '.' in duplicated_node_name:
+            name, ext = duplicated_node_name.split(".")
+            node_name = name
+    else:
+        name = duplicated_node_name
+    return name
+
+def check_for_udp():
+    """!
+    This function will check if selected has and custom properties 
+    Example: print( f"Value: {obj_upd_keys} Key: {bpy.data.objects[selected_obj.name][obj_upd_keys] } ")
+    """
+    context = bpy.context
+    # Create list to store selected meshs udps
+    lod_list = []
+    colliders_list = []
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                for obj_upd_keys in bpy.data.objects[selected_obj.name].keys():
+                    if obj_upd_keys not in '_RNA_UI':
+                        if obj_upd_keys == "o3de_atom_lod":
+                            lod_list.append(True)
+                        if obj_upd_keys == "o3de_atom_phys":
+                            colliders_list.append(True)
+    lods = any(lod_list)
+    colliders = any(colliders_list)      
+    return lods, colliders
+
+def create_udp():
+    """!
+    This function will create a copy of a selected mesh and create an o3de PhysX collider PhysX Mesh that will
+    autodect in o3de if you have a PhysX Collder Component.
+    """
+    selected_objects = bpy.context.object
+    # Check to see if bool was check on or of on pop-up question
+    if bpy.types.Scene.pop_up_question_bool:
+        # Check to see if name string already contails a _LOD int
+        if bpy.types.Scene.udp_type == "_lod":
+            if re.search(r"_lod(?:)\d", selected_objects.name):
+                # Lets get the current LOD Level
+                ext_result = re.search(r"_lod(?:)\d", selected_objects.name)
+                lod_level_int = ext_result.group().split("_lod")
+                # Lets get the level and add the next
+                lod_level = int(lod_level_int[1]) + 1
+                # rename to the correct lod level
+                base_name = selected_objects.name.rsplit("_",1)
+                # Duplicate the object
+                duplicate_object = duplicate_selected(selected_objects, f"{base_name[0]}_lod{lod_level}")
+                # ADD UDP
+                duplicate_object["o3de_atom_lod"] = f"_lod{lod_level}"
+            else:
+                duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_lod0")
+                # ADD UDP
+                duplicate_object["o3de_atom_lod"] = "_lod0"
+        # If UDP Type is _phys
+        if bpy.types.Scene.udp_type == "_phys":
+            duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_phys")
+            duplicate_object["o3de_atom_phys"] = "_phys"
+        # Select the duplicated object only
+        select_object(duplicate_object)
+        # Add the Decimate Modifier on for user. Since both _lod and _phys will need this modifier
+        #  for now we will have it as a default.
+        add_remove_modifier("DECIMATE", True)
+    else:
+        if bpy.types.Scene.udp_type == "_lod":
+                selected_objects['o3de_atom_lod'] = "_lod0"
+        if bpy.types.Scene.udp_type == "_phys":
+            selected_objects["o3de_atom_phys"] = "_phys"
+
+def compair_set(list_a, list_b):
+    """!
+    This function will check to see if there are any difference between two list
+    """
+    set_a = list_a
+    set_b = list_b
+    if set_a == set_b:
+        return True
+    else:
+        return False
+
+def add_remove_modifier(modifier_name, add):
+    """!
+    This function will add or remove a modifier to selected
+    @param modifier_name is the name of the modifier you wish to add or remove
+    @param add if Bool True will add the modifier, if False will remove 
+    """
+    context = bpy.context
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                if add:
+                    # Set the mesh active
+                    bpy.context.view_layer.objects.active = selected_obj
+                    # Add Modifier
+                    bpy.ops.object.modifier_add(type=modifier_name)
+                    if modifier_name == "TRIANGULATE":
+                        bpy.context.object.modifiers["Triangulate"].keep_custom_normals = True
+                else:
+                    # Set the mesh active
+                    bpy.context.view_layer.objects.active = selected_obj
+                    # Remove Modifier
+                    bpy.ops.object.modifier_remove(modifier=modifier_name)
+
+def check_selected_stats_count():
+    """!
+    This function will check selected and get poly counts
+    """
+    # We will make list for each selection
+    mesh_vertices_list = []
+    mesh_edges_list = []
+    mesh_polygons_list = []
+    mesh_material_list = []
+    
+    context = bpy.context
+    
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                mesh_stats = selected_obj.data
+                # Vert, Edge, Poly Counts
+                mesh_vertices = len(mesh_stats.vertices)
+                mesh_edges = len(mesh_stats.edges)
+                mesh_polygons = len(mesh_stats.polygons)
+                mesh_materials = len(mesh_stats.materials)
+                mesh_vertices_list.append(mesh_vertices)
+                mesh_edges_list.append(mesh_edges)
+                mesh_polygons_list.append(mesh_polygons)
+                # Material Count
+                mesh_material_list.append(mesh_materials)
+
+    vert_count = sum(mesh_vertices_list)
+    edge_count = sum(mesh_edges_list)
+    polygon_count = sum(mesh_polygons_list)
+    material_count = sum(mesh_material_list)
+    # Reset the list
+    mesh_vertices_list = []
+    mesh_edges_list = []
+    mesh_polygons_list = []
+    mesh_material_list = []
+    return vert_count, edge_count, polygon_count, material_count
+
+def check_selected_uvs():
+    """!
+    This function will check to see if there are UVs
+    """
+    context = bpy.context
+    # We will make list for each selection and compair if there are UVs.
+    mesh_uv_list = []
+    
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                if selected_obj.data.uv_layers:
+                    mesh_uv_list.append(True)
+                else:
+                    mesh_uv_list.append(False)
+    object_uv_list = all(mesh_uv_list)
+    # If all objects have UVs
+    if object_uv_list:
+        # Clear UV List
+        mesh_uv_list = []
+        return True
+    else:
+        # Clear UV List
+        mesh_uv_list = []
+        return False
+
+def check_selected_transforms():
+    """!
+    This function will check to see if there are unfrozen transfors and to warn the artist before export.
+    """
+    context = bpy.context
+    # We will make list for each selection and compair to a frozzen transfrom.
+    location_list = []
+    location_source = [0.0, 0.0, 0.0]
+    rotation_list = []
+    rotation_source = [1.0, 0.0, 0.0, 0.0]
+    scale_list = []
+    scale_source = [1.0, 1.0, 1.0]
+    location_good = True
+    rotation_good = True
+    scale_good = True
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            obj = selected_obj.matrix_world.decompose()
+            # Start a matrix count
+            count = 0
+            for vector in obj:
+                # We will count from 1 to 3, every object will have a matrix of Location, Rotation, and Scale
+                count += 1
+                if count == 1:
+                    # Add vector3 to Location list
+                    for floatdata in vector:
+                        location_list.append(floatdata)
+                elif count == 2:
+                    # Add vector4 to Rotation list
+                    for floatdata in vector:
+                        rotation_list.append(floatdata)
+                else:
+                    # Add vector3 to Scale list
+                    for floatdata in vector:
+                        scale_list.append(floatdata)
+                    count = 0
+                    
+            # Lets look at the results and compair the sets
+            if not compair_set(location_list, location_source):
+                location_good = False
+            elif not compair_set(rotation_list, rotation_source):
+                rotation_good = False
+            elif not compair_set(scale_list, scale_source):
+                scale_good = False
+            # Check if all are true or false
+            check_transfroms_bools = [location_good, rotation_good, scale_good]
+            if all(check_transfroms_bools):
+                return True, location_list
+            else:
+                return False, location_list
+        # Reset the list
+        location_list = []
+        rotation_list = []
+        scale_list = []
+        location_good = True
+        rotation_good = True
         scale_good = True

+ 9 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/Scripts/addons/modules/__init__.py

@@ -0,0 +1,9 @@
+#
+# 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
+#
+#
+# -------------------------------------------------------------------------
+"""modules seems to be a required folder Blender expects to exist"""

+ 34 - 28
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py

@@ -44,7 +44,7 @@ _LOGGER.debug(f'_MODULE_PATH: {_MODULE_PATH.as_posix()}')
 # be rewritten from ConfigClass, then BlenderConfig inherits core
 import DccScriptingInterface.config as dccsi_core_config
 
-_settings_core = dccsi_core_config.get_config_settings(enable_o3de_python=True,
+_settings_core = dccsi_core_config.get_config_settings(enable_o3de_python=False,
                                                        enable_o3de_pyside2=False,
                                                        set_env=True)
 
@@ -65,13 +65,13 @@ except EnvironmentError as e:
     _LOGGER.warning(f'EnvironmentError: {e}')
 
 # this is the root path for the wing pkg
-from DccScriptingInterface.Tools.DCC.Blender import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER
 
 # blender settings.json
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_SETTINGS
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER_SETTINGS
 # blender settings.local.json
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_LOCAL_SETTINGS
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER_LOCAL_SETTINGS
 # -------------------------------------------------------------------------
 
 
@@ -102,74 +102,80 @@ blender_config = BlenderConfig(config_name='dccsi_dcc_blender',
 # now we can extend the environment specific to Blender
 # start by grabbing the constants we want to work with as envars
 # a managed setting to track the wing config is enabled
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_CONFIG_DCC_BLENDER
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_CONFIG_DCC_BLENDER
 blender_config.add_setting(ENVAR_DCCSI_CONFIG_DCC_BLENDER, True)
 
-from Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS
-from Tools.DCC.Blender import PATH_DCCSI_TOOLS
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS
+from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS
 PATH_DCCSI_TOOLS = Path(PATH_DCCSI_TOOLS).resolve()
 blender_config.add_setting(ENVAR_PATH_DCCSI_TOOLS,
                            PATH_DCCSI_TOOLS.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER
-from Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER
 PATH_DCCSI_TOOLS_DCC_BLENDER = Path(PATH_DCCSI_TOOLS_DCC_BLENDER).resolve()
 blender_config.add_setting(ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER,
                            PATH_DCCSI_TOOLS_DCC_BLENDER.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
-from Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
 PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS = Path(PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS).resolve()
 blender_config.add_setting(ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS,
                            PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS.as_posix(),
                            set_sys_path=True,
                            set_pythonpath=True)
 
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_VERSION
-from Tools.DCC.Blender.constants import SLUG_DCCSI_BLENDER_VERSION
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_BLENDER_USER_SCRIPTS
+blender_config.add_setting(ENVAR_BLENDER_USER_SCRIPTS,
+                           PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS.as_posix(),
+                           set_sys_path=True,
+                           set_pythonpath=True)
+
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_VERSION
+from DccScriptingInterface.Tools.DCC.Blender.constants import SLUG_DCCSI_BLENDER_VERSION
 blender_config.add_setting(ENVAR_DCCSI_BLENDER_VERSION,
                            SLUG_DCCSI_BLENDER_VERSION)
 
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_LOCATION
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_LOCATION
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_LOCATION
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_LOCATION
 PATH_DCCSI_BLENDER_LOCATION = Path(PATH_DCCSI_BLENDER_LOCATION).resolve()
 blender_config.add_setting(ENVAR_DCCSI_BLENDER_LOCATION,
                            PATH_DCCSI_BLENDER_LOCATION.as_posix(),
                            set_sys_path=True)
 
-from Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_BLENDER_EXE
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_BLENDER_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_EXE
 PATH_DCCSI_BLENDER_EXE = Path(PATH_DCCSI_BLENDER_EXE).resolve()
 blender_config.add_setting(ENVAR_PATH_DCCSI_BLENDER_EXE,
                            PATH_DCCSI_BLENDER_EXE.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_LAUNCHER_EXE
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_LAUNCHER_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_LAUNCHER_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_LAUNCHER_EXE
 PATH_DCCSI_BLENDER_LAUNCHER_EXE = Path(PATH_DCCSI_BLENDER_LAUNCHER_EXE).resolve()
 blender_config.add_setting(ENVAR_DCCSI_BLENDER_LAUNCHER_EXE,
                            PATH_DCCSI_BLENDER_LAUNCHER_EXE.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_PYTHON_LOC
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_PYTHON_LOC
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_PYTHON_LOC
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_PYTHON_LOC
 PATH_DCCSI_BLENDER_PYTHON_LOC = Path(PATH_DCCSI_BLENDER_PYTHON_LOC).resolve()
 blender_config.add_setting(ENVAR_DCCSI_BLENDER_PYTHON_LOC,
                            PATH_DCCSI_BLENDER_PYTHON_LOC.as_posix(),
                            set_sys_path=True)
 
-from Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_PY_EXE
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_PY_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_DCCSI_BLENDER_PY_EXE
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_PY_EXE
 PATH_DCCSI_BLENDER_PY_EXE = Path(PATH_DCCSI_BLENDER_PY_EXE).resolve()
 blender_config.add_setting(ENVAR_DCCSI_BLENDER_PY_EXE,
                            PATH_DCCSI_BLENDER_PY_EXE.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_BLENDER_BOOTSTRAP
-from Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_BOOTSTRAP
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_PATH_DCCSI_BLENDER_BOOTSTRAP
+from DccScriptingInterface.Tools.DCC.Blender.constants import PATH_DCCSI_BLENDER_BOOTSTRAP
 PATH_DCCSI_BLENDER_BOOTSTRAP = Path(PATH_DCCSI_BLENDER_BOOTSTRAP).resolve()
 blender_config.add_setting(ENVAR_PATH_DCCSI_BLENDER_BOOTSTRAP,
                            PATH_DCCSI_BLENDER_BOOTSTRAP.as_posix())
 
-from Tools.DCC.Blender.constants import ENVAR_URL_DCCSI_BLENDER_WIKI
-from Tools.DCC.Blender.constants import URL_DCCSI_BLENDER_WIKI
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_URL_DCCSI_BLENDER_WIKI
+from DccScriptingInterface.Tools.DCC.Blender.constants import URL_DCCSI_BLENDER_WIKI
 blender_config.add_setting(ENVAR_URL_DCCSI_BLENDER_WIKI,
                            str(URL_DCCSI_BLENDER_WIKI))
 # --- END -----------------------------------------------------------------

+ 6 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/constants.py

@@ -62,6 +62,9 @@ from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_BLENDER_LOCATION
 from DccScriptingInterface.Tools.DCC.Blender import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
 from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
 
+from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_SETTINGS
+from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_LOCAL_SETTINGS
+
 # I think this one will launch with a console
 SLUG_BLENDER_EXE = "blender.exe"
 ENVAR_PATH_DCCSI_BLENDER_EXE = "PATH_DCCSI_BLENDER_EXE"
@@ -85,6 +88,9 @@ from DccScriptingInterface.Tools.DCC.Blender import SLUG_DCCSI_BLENDER_BOOTSTRAP
 from DccScriptingInterface.Tools.DCC.Blender import ENVAR_PATH_DCCSI_BLENDER_BOOTSTRAP
 from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_BLENDER_BOOTSTRAP
 
+# set up user scripts envar for Blender: https://blenderartists.org/t/addons-environment-variable-linux/603145/2
+ENVAR_BLENDER_USER_SCRIPTS = 'BLENDER_USER_SCRIPTS'
+
 ENVAR_URL_DCCSI_BLENDER_WIKI = 'URL_DCCSI_BLENDER_WIKI'
 URL_DCCSI_BLENDER_WIKI = 'https://github.com/o3de/o3de/wiki/O3DE-DCCsi-Tools-DCC-Blender'
 # -------------------------------------------------------------------------

+ 55 - 14
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/readme.md

@@ -1,28 +1,26 @@
-# O3DE DCCsi,DCC Blender
+# O3DE DCCsi, DCC Blender
 
-This sets up Blender to be integrated with O3DE in a managed way, via the DccScriptingInterface Gem (aka DCCsi). For more information about the DCCis please see the readme.md at the root of the Gem.
+The "DccScriptingInterface" (aka DCCsi) is a Gem for O3DE to extend and interface with dcc tools in the python ecosystem.  This document contains the details of configuration of Maya as a DCC tool to be used with O3DE.  This sets up Blender to be integrated with O3DE in a managed way, via the DCCsi. For more information about the DCCis please see the readme.md at the root of the Gem.
 
-###### Status: Prototype
+### Status: Prototype
 
-###### Version: 0.0.1
+### Version: 0.0.1
 
-###### Support: Blender 3.x, Currently Windows only
+### Support: Blender 3.x, Currently Windows only
 
 This is currently the first working version of a managed integration of Blender, treat it as an Experimental Preview.
 
 ## Brief
 
-This is an experiment Blender integration with O3DE, it's intent is:
+This is an experimental Blender integration with O3DE, it's intent is:
 
-1. Configure and launch Blender (from CLI, or even via O3DE editor)
+1. Configure and launch Blender (from CLI start.py, or even via O3DE editor menus)
 2. Bootstrap O3DE 'Studio Tools', provide shared code access, facilitate AddOns, etc.
 3. Soft extension bootstrapping (non-destructive to Users Blender installation)
 
 ## Setup
 
-Make sure that the DccScriptingInterface Gem is enabled in your project via the Project Manager, then build your project (DCCsi python boostrapping will require the project to be built.) [Adding and Removing Gems in a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/)
-
-The O3DE tools provided with the DCCsi have python package dependencies (via requirements.txt).  You will need to currently manually configure for Blender before running for the first time.
+Make sure that the DccScriptingInterface Gem is enabled in your project via the Project Manager, then build your project (DCCsi python boostrapping will require the project to be built.) [Adding and Removing Gems in a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/)  The O3DE tools provided with the DCCsi have python package dependencies (via requirements.txt).  You will need to currently manually configure for Blender before running for the first time.
 
 In the root folder of the DCCsi are a few useful utilities and files to make note of:
 
@@ -45,11 +43,54 @@ In the root folder of the DCCsi are a few useful utilities and files to make not
 
 ### If you are an End User...
 
-You should be able to now successfully start Blender from within the O3DE Editor.
+You should be able to now successfully start Blender from within the O3DE Editor 'Studio Tools' menu.
+
+    `O3DE MenuBar > StudioTools > DCC > Blender`
+
+If you'd like to use Blender with O3DE bootstrapped tools externally, outside of the o3de Editor, you can do that also.
+
+There two ways, a windows environment via .bat file, or a `start.py` script
+
+From .bat file, double-click the following file type to start Maya: `C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Blender\win_launch_blender.bat`
+
+To start from script:
+
+    1. Open a Windows Command Prompt (CMD)
+
+    2. Change directory to: 
+
+```batch
+cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
+```
+
+    3. Run the Blender `start.py` script:
+
+```batch
+.\python Tools\DCC\Blender\start.py
+```
+
+Note: DCCsi is pre-configured for Blender 3.1
+
+IF you want to alter the version of Belnder, or other settings, such as a customer installation path, you can do this via a file: settings.local.json
+start.py is the same setup that the Editor uses to start the external DCC application.  It makes use of a `settings.json` (default settings), and `settings.local.json` (user settings and overrides) within the o3de DCCsi folder for Blender.  These are utilized along with the addition of a `config.py` and `start.py` in the DCC apps folder. This follows the patterns similar to how Maya (and other tools) can be launched from the O3DE menus, or in a scripted manner rather then legacy windows .bat files.
+
+To generate a `settings.local.json` (which you can then modify with overrides to paths and other settings)::
+
+    1. Open a Windows Command Prompt (CMD)
+
+    2. Change directory to: 
+
+```batch
+cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
+```
+
+    3. Run the Blender `config.py` script:
 
-`O3DE MenuBar > StudioTools > DCC > Blender`
+```batch
+python Tools\DCC\Blender\config.py
+```
 
-Note: DCCsi is pre-configured fro Blender 3.1
+You can now open `settings.local.json` in a text editor and make modifications and resave before starting Maya.
 
 ### If you are a developer...
 
@@ -77,4 +118,4 @@ https://www.o3de.org/docs/user-guide/< DCC Tools, not stubbed >
 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
+SPDX-License-Identifier: Apache-2.0 OR MIT

+ 17 - 50
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py

@@ -40,7 +40,7 @@ import logging as _logging
 
 
 # -------------------------------------------------------------------------
-# this is an entry point, we must self bootstrap
+# global scope
 _MODULE_PATH = Path(__file__)
 PATH_O3DE_TECHART_GEMS = _MODULE_PATH.parents[4].resolve()
 os.chdir(PATH_O3DE_TECHART_GEMS.as_posix())
@@ -48,17 +48,13 @@ sys.path.insert(0, PATH_O3DE_TECHART_GEMS.as_posix())
 
 from DccScriptingInterface import add_site_dir
 add_site_dir(PATH_O3DE_TECHART_GEMS) # cleaner add
-# -------------------------------------------------------------------------
 
-
-# -------------------------------------------------------------------------
-#os.environ['PYTHONINSPECT'] = 'True'
-# global scope
 from DccScriptingInterface.Tools.DCC.Blender import _PACKAGENAME
 _MODULENAME = f'{_PACKAGENAME}.start'
 
 # get the global dccsi state
 from DccScriptingInterface.globals import *
+from DccScriptingInterface import add_site_dir
 
 from azpy.constants import FRMT_LOG_LONG
 _logging.basicConfig(level=_logging.DEBUG,
@@ -81,12 +77,18 @@ _LOGGER.debug(f'Initializing: {_MODULENAME}')
 _LOGGER.debug(f'_MODULE_PATH: {_MODULE_PATH.as_posix()}')
 
 # this should execute the core config.py first and grab settings
-from dynaconf import settings
+from DccScriptingInterface.Tools.DCC.Blender.config import blender_config
+_settings = blender_config.get_config_settings()
+_settings.setenv() # ensure env is set
+
+_BLENDER_EXE = Path(_settings.PATH_DCCSI_BLENDER_EXE).resolve(strict=True)
 
-# may re-enable later
-# # retreive the blender_config class object and it's settings
-# from DccScriptingInterface.Tools.DCC.Blender.config import blender_config
-# blender_config.settings.setenv() # ensure env is set
+_BLENDER_SCRIPTS = Path(_settings.PATH_DCCSI_BLENDER_SCRIPTS).resolve(strict=True)
+add_site_dir(_BLENDER_SCRIPTS)
+from DccScriptingInterface.Tools.DCC.Blender.constants import ENVAR_BLENDER_USER_SCRIPTS
+os.environ[ENVAR_BLENDER_USER_SCRIPTS] = _BLENDER_SCRIPTS.as_posix()
+
+_DEFAULT_BOOTSTRAP = Path(_settings.PATH_DCCSI_BLENDER_BOOTSTRAP).resolve(strict=True)
 
 from DccScriptingInterface.azpy.config_utils import check_is_ascii
 
@@ -99,10 +101,6 @@ blender_env = os.environ.copy()
 # prunes non-string key:value envars
 blender_env = {key: value for key, value in blender_env.items() if check_is_ascii(key) and check_is_ascii(value)}
 
-# will prune QT_ envars, to be used with QT bases apps like Maya or Wing
-# Blender is not QT based, so we should be able to disable this
-#blender_env = {key: value for key, value in blender_env.items() if not key.startswith("QT_")}
-
 if DCCSI_GDEBUG:
     # we can see what was pruned
     pruned = {k: blender_env[k] for k in set(blender_env) - set(orig_env)}
@@ -115,7 +113,10 @@ if DCCSI_GDEBUG:
 
 
 # -------------------------------------------------------------------------
-# https://docs.blender.org/manual/en/ latest / advanced / command_line / arguments.html
+#https://tinyurl.com/o3de-dccsi-blender-cli-help
+# command args but be seperated properly
+#https://blender.stackexchange.com/questions/169259/issue-running-blender-command-line-arguments-using-python-subprocess
+# https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html
 
 # some notes
 # from cmd, use a startup script (we should be able to use to bootstrap)
@@ -130,39 +131,11 @@ if DCCSI_GDEBUG:
 # from cmd, enable addons, load file, start script
 #    ./blender -b --addons animation_nodes,meshlint [file] --python [myscript.py]
 
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER
-
-from DccScriptingInterface.Tools.DCC.Blender import SLUG_DCCSI_BLENDER_VERSION
-from DccScriptingInterface.Tools.DCC.Blender import SLUG_BLENDER_EXE
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_BLENDER_LOCATION
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_BLENDER_EXE
-
-_BLENDER_EXE = Path(PATH_DCCSI_BLENDER_EXE).resolve(strict=True)
-#_BLENDER_EXE = Path(blender_config.settings.PATH_DCCSI_BLENDER_EXE)
-
-# this ensures we are in blenders location
-#os.chdir(_BLENDER_EXE.parent)
-from DccScriptingInterface.Tools.DCC.Blender import ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS
-_BLENDER_SCRIPTS = Path(PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS).resolve(strict=True)
-from DccScriptingInterface import add_site_dir
-add_site_dir(_BLENDER_SCRIPTS)
-os.environ[ENVAR_PATH_DCCSI_TOOLS_DCC_BLENDER_SCRIPTS] = _BLENDER_SCRIPTS.as_posix()
-
-from DccScriptingInterface.Tools.DCC.Blender import SLUG_DCCSI_BLENDER_BOOTSTRAP
-from DccScriptingInterface.Tools.DCC.Blender import PATH_DCCSI_BLENDER_BOOTSTRAP
-
-_DEFAULT_BOOTSTRAP = Path(PATH_DCCSI_BLENDER_BOOTSTRAP).resolve(strict=True)
-#_DEFAULT_BOOTSTRAP = Path(blender_config.settings.PATH_DCCSI_BLENDER_BOOTSTRAP)
-
 # default launch command
 _LAUNCH_COMMAND = [f'{str(_BLENDER_EXE)}',
                    f'--python', # this must be seperate from the .py file
                    f'{str(_DEFAULT_BOOTSTRAP)}']
 
-# command args but be seperated properly
-#https://blender.stackexchange.com/questions/169259/issue-running-blender-command-line-arguments-using-python-subprocess
-
 # suggestion for future PR is to refactor this method into something like
 # DccScriptingInterface.azpy.utils.start.popen()
 def popen(command: list = _LAUNCH_COMMAND,
@@ -194,12 +167,6 @@ def popen(command: list = _LAUNCH_COMMAND,
 # -------------------------------------------------------------------------
 
 
-# -------------------------------------------------------------------------
-#
-#https://docs.blender.org/manual/en/2.79/advanced/command_line/introduction.html#:~:text=Microsoft%20Windows&text=To%20display%20the%20console%20again,Window%20%E2%80%A3%20Toggle%20System%20Console.&text=Blender's%20Console%20Window%20on%20Microsoft,along%20with%20the%20relevant%20messages.
-# -------------------------------------------------------------------------
-
-
 ###########################################################################
 # Main Code Block, runs this script as main (testing)
 # -------------------------------------------------------------------------

+ 41 - 21
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/scene_exporter/export_tool.py

@@ -56,7 +56,7 @@ class SceneExporter(QtWidgets.QWidget):
 
         self.setParent(maya_mainwindow)
         self.setWindowFlags(QtCore.Qt.Window)
-        self.setGeometry(200, 200, 450, 600)
+        self.setGeometry(200, 200, 390, 600)
         self.setObjectName('MayaSceneExporter')
         self.setWindowTitle('O3DE Scene Exporter')
         self.isTopLevel()
@@ -69,20 +69,15 @@ class SceneExporter(QtWidgets.QWidget):
         self.bold_font = QtGui.QFont("Plastique", 8, QtGui.QFont.Bold)
 
         self.main_container = QtWidgets.QVBoxLayout()
-        self.main_container.setSpacing(20)
-
         self.setLayout(self.main_container)
         self.main_container.setAlignment(QtCore.Qt.AlignTop)
-
-        # THack spacing (because I added menu, it's crunched)
-        self.spacing_label = QtWidgets.QLabel('    ')
-        self.main_container.addWidget(self.spacing_label)
+        self.main_container.setContentsMargins(10, 40, 10, 10)
 
         # Task Section
         self.test_label = QtWidgets.QLabel('Task')
         self.test_label.setFont(self.bold_font)
         self.main_container.addWidget(self.test_label)
-        self.main_container.addSpacing(10)
+        self.main_container.addSpacing(5)
 
         # Setup Help Menu
         self.menuBar = QtWidgets.QMenuBar(self) # HelpMenu wants menuBar
@@ -101,25 +96,43 @@ class SceneExporter(QtWidgets.QWidget):
         self.task_radio_two = QtWidgets.QRadioButton('Convert to O3DE Materials')
         self.task_button_container.addWidget(self.task_radio_two)
         self.task_button_group.addButton(self.task_radio_two, 1)
+        self.main_container.addSpacing(5)
         self.main_container.addLayout(self.create_spacer_line())
 
-        # Export Object Settings
-        self.object_settings_label = QtWidgets.QLabel('Export Object Settings')
+        # Export Settings
+        self.main_container.addSpacing(5)
+        self.object_settings_label = QtWidgets.QLabel('Export Settings')
         self.object_settings_label.setFont(self.bold_font)
         self.main_container.addWidget(self.object_settings_label)
         self.main_container.addSpacing(5)
-        self.object_settings_layout = QtWidgets.QHBoxLayout()
-        self.object_settings_layout.setAlignment(QtCore.Qt.AlignLeft)
-        self.main_container.addLayout(self.object_settings_layout)
+
+        # --> Row One - Export Settings
+        self.settings_rowOne_layout = QtWidgets.QHBoxLayout()
+        self.settings_rowOne_layout.setAlignment(QtCore.Qt.AlignLeft)
+        self.main_container.addLayout(self.settings_rowOne_layout)
+        self.z_up_checkbox = QtWidgets.QCheckBox('Force Z-up Export')
+        self.z_up_checkbox.setChecked(True)
+        self.settings_rowOne_layout.addWidget(self.z_up_checkbox)
+        self.settings_rowOne_layout.addSpacing(67)
         self.preserve_transforms_checkbox = QtWidgets.QCheckBox('Preserve Transform Values')
         self.preserve_transforms_checkbox.setChecked(True)
-        self.object_settings_layout.addWidget(self.preserve_transforms_checkbox)
-        self.object_settings_layout.addSpacing(30)
+        self.settings_rowOne_layout.addWidget(self.preserve_transforms_checkbox)
+        self.main_container.addSpacing(5)
+
+        # --> Row Two - Export Settings
+        self.settings_rowTwo_layout = QtWidgets.QHBoxLayout()
+        self.settings_rowTwo_layout.setAlignment(QtCore.Qt.AlignLeft)
+        self.main_container.addLayout(self.settings_rowTwo_layout)
         self.preserve_grouped_checkbox = QtWidgets.QCheckBox('Preserve Grouped Objects')
-        self.object_settings_layout.addWidget(self.preserve_grouped_checkbox)
+        self.settings_rowTwo_layout.addWidget(self.preserve_grouped_checkbox)
+        self.settings_rowTwo_layout.addSpacing(25)
+        self.triangulated_export_checkbox = QtWidgets.QCheckBox('Triangulate Exported Objects')
+        self.settings_rowTwo_layout.addWidget(self.triangulated_export_checkbox)
+        self.main_container.addSpacing(5)
         self.main_container.addLayout(self.create_spacer_line())
 
         # Scope Section
+        self.main_container.addSpacing(5)
         self.scope_label = QtWidgets.QLabel('Scope')
         self.scope_label.setFont(self.bold_font)
         self.main_container.addWidget(self.scope_label)
@@ -157,9 +170,11 @@ class SceneExporter(QtWidgets.QWidget):
         self.scope_set_button.clicked.connect(self.set_scope_clicked)
         self.scope_target_layout.addWidget(self.scope_set_button)
         self.scope_set_button.setFixedSize(30, 25)
+        self.main_container.addSpacing(5)
         self.main_container.addLayout(self.create_spacer_line())
 
         # Export Path
+        self.main_container.addSpacing(5)
         self.export_path_label = QtWidgets.QLabel('Export Path')
         self.export_path_label.setFont(self.bold_font)
         self.main_container.addWidget(self.export_path_label)
@@ -174,9 +189,11 @@ class SceneExporter(QtWidgets.QWidget):
         self.export_set_button.clicked.connect(self.set_output_location)
         self.export_path_layout.addWidget(self.export_set_button)
         self.export_set_button.setFixedSize(30, 25)
+        self.main_container.addSpacing(5)
         self.main_container.addLayout(self.create_spacer_line())
 
         # Output Window
+        self.main_container.addSpacing(5)
         self.output_label = QtWidgets.QLabel('Output')
         self.output_label.setFont(self.bold_font)
         self.main_container.addWidget(self.output_label)
@@ -200,7 +217,7 @@ class SceneExporter(QtWidgets.QWidget):
         if self.validate_task():
             # Include export options
             export_options = []
-            for checkbox in [self.preserve_grouped_checkbox, self.preserve_transforms_checkbox]:
+            for checkbox in [self.z_up_checkbox, self.preserve_grouped_checkbox, self.preserve_transforms_checkbox, self.triangulated_export_checkbox]:
                 if checkbox.isChecked():
                     export_options.append(checkbox.text())
 
@@ -277,7 +294,6 @@ class SceneExporter(QtWidgets.QWidget):
 
     def set_output_location(self):
         directory = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Target Directory', self.desktop_location)
-        _LOGGER.info(f'Directory: {directory}')
         if directory:
             self.export_line_edit.setText(directory)
             self.set_export_path()
@@ -315,6 +331,10 @@ class SceneExporter(QtWidgets.QWidget):
             return source_list
         return None
 
+    def get_output_location(self):
+        if self.export_line_edit.text:
+            _LOGGER.info(f'Output location: {self.export_line_edit.text}')
+
     def get_scope_value(self):
         return self.scope_settings[self.scope]
 
@@ -379,7 +399,7 @@ class SceneExporter(QtWidgets.QWidget):
     def set_export_path(self):
         target_path = self.export_line_edit.text()
         self.output_location = target_path if Path(target_path).is_dir() else None
-        _LOGGER.info(f'-----> {self.output_location} -- {type(self.output_location)}')
+        _LOGGER.info(f'-----> {self.output_location}')
 
     def process_materials_clicked(self):
         self.process_materials()
@@ -421,8 +441,8 @@ def delete_instances():
     from DccScriptingInterface.Tools.DCC.Maya.Scripts.Python.export import _PACKAGENAME
 
     for obj in maya_mainwindow.children():
-        if str(type(obj)) == f"<class '{_PACKAGENAME}.MaterialsHelper'>":
-            if obj.__class__.__name__ == "MaterialsHelper":
+        if str(type(obj)) == f"<class '{_PACKAGENAME}.SceneExporter'>":
+            if obj.__class__.__name__ == "SceneExporter":
                 obj.setParent(None)
                 obj.deleteLater()
 

+ 42 - 25
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/scene_exporter/readme.md

@@ -1,51 +1,68 @@
-# O3DE DCCsi, Maya Scene Exporter
+# O3DE Scene Exporter Tool
 
-The "DccScriptingInterface" (aka DCCsi) is a Gem for O3DE to extend and interface with dcc tools in the python ecosystem. This document contains the details of using the 'Scene Exporter' for Maya that is provided as a utility with the DCCsi.
 
-###### Status: Prototype
 
-###### Version: 0.0.1
+## What does this tool do?
 
-###### OS: Windows only (for now)
+This purpose of the Scene Exporter Tool is to export the content of Autodesk Maya scene files into assets that can be read by the O3DE Editor. This tool will save you time when assets are ready to bring into O3DE on several fronts. It will help you to distribute all files associated with your Maya scene into a properly formatted destination directory within your O3DE project location. It generates an FBX file that contains the user-specified scene elements for transfer, and it will also generate O3DE-specific ".material" files that correspond to your assigned real-time shaders (StingrayPBS or Arnold AIStandardSurface), setting relative paths for O3DE consumption. No matter what size scene you need to carry over, the heavy lifting can all be handled in a couple clicks.
 
-###### Support:
 
-- Maya 2022 w/ Python3 (will not support py2.7)
 
-- Maya 2023+ w/ Python3 <-- default version
+## Tool Interface
 
-## Getting Started
+The tool is separated by a handful of different settings, and can be adjusted depending on what operations are needed. 
 
-Before using this tool ensure that you have done the following:
 
-- this
 
-- that
+#### Task
 
-- and other things
+This tool can be used for both exporting assets, as well as to generate an audit of all the materials and connected texture files present in the selected scope (scope is explained below). By selecting "Query Material Attributes", no files are created- only a dictionary is logged into the Maya output window that indicates the elements of the scene that will be exported if "Convert to O3DE Materials" is selected.
 
-## Using the Scene Exporter
 
-This section describes how to utilize the Scene Export tool.
 
-### Exporting Scene Objects and FBX Models
+#### Export Object Settings
 
-Do this ...
+There are two options that are available for the way that the tool handles elements that are marked for export in the scene.
 
-### Exporting MAterials
+###### Preserve Transform Values
 
-do this ...
+Because this tool provides the option of exporting single objects, users may or may not want to preserve transform values (where the object is positioned in relation to the origin of the Maya scene). When this option is checked, the object will retain this position in relation to the O3DE origin, and without this option checked all assets marked for export will be positioned at the center of the origin.
 
-### Other Features
+###### Preserve Grouped Objects
 
-Do this ...
+When exporting assets you may want to retain hierarchical relationships as they exist in the Maya file. This is not always ideal, but may be desired for a number of reasons. Without this option checked, the elements of the exported objects are flattened to a single level.
 
-And this ...
+#### Scope
 
-----
+The scope setting describes what elements are marked for export, and there are several options for how the export is carried out.
 
-###### LICENSE INFO
+**Selected**
 
-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.
+Only those objects selected in the viewport will be exported
+
+**By Name**
+
+Only the object corresponding to the name entered will be exported
+
+**Scene**
+
+All mesh elements in the scene will be exported
+
+**Directory**
+
+All Maya files contained inside the specified directory will have their contents exported. Each Maya scene will generate its own fbx file, and all material files will be grouped into a single textures directory.
+
+#### Export Path
+
+The destination path for converted assets are sent to this location. The proper place to set this directory is within the Project directory that the assets will be used, inside the "Objects" directory. By setting this location as it relates to your project, you will ensure that the assets can be found and textures applied automatically by the texture assignment tool inside O3DE.
+
+#### Output Window
+
+Once a scene has been processed, this window can be used to see all assets that have been exported, along with their set attributes/texture assignments. The combobox can be used to access each of the various exported elements.
+
+---
+
+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

+ 8 - 8
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py

@@ -269,14 +269,14 @@ def post_startup():
     DccScriptingInterface.Tools.DCC.Maya.Scripts.set_defaults.set_defaults()
 
     # Setup UI tools
-    if not maya.cmds.about(batch=True):
-        _LOGGER.info('Add UI dependent tools')
-        # wrap in a try, because we haven't implmented it yet
-        try:
-            mel.eval(str(r'source "{}"'.format(SLUG_O3DE_DCC_MAYA_MEL)))
-        except Exception as e:
-            _LOGGER.exception(f'{e} , traceback =', exc_info=True)
-            pass
+    # if not maya.cmds.about(batch=True):
+    #     _LOGGER.info('Add UI dependent tools')
+    #     # wrap in a try, because we haven't implmented it yet
+    #     try:
+    #         mel.eval(str(r'source "{}"'.format(SLUG_O3DE_DCC_MAYA_MEL)))
+    #     except Exception as e:
+    #         _LOGGER.exception(f'{e} , traceback =', exc_info=True)
+    #         pass
 
     # manage custom menu in a sub-module
     import DccScriptingInterface.Tools.DCC.Maya.Scripts.set_menu

+ 59 - 23
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.md

@@ -1,6 +1,6 @@
 # O3DE DCCsi, DCC Maya
 
-The "DccScriptingInterface" (aka DCCsi) is a Gem for O3DE to extend and interface with dcc tools in the python ecosystem.  This document contains the details of configuration of Maya as a DCC tool to be used with O3DE.
+The "DccScriptingInterface" (aka DCCsi) is a Gem for O3DE to extend and interface with dcc tools in the python ecosystem.  This document contains the details of configuration of Maya as a DCC tool to be used with O3DE.  This sets up Maya to be integrated with O3DE in a managed way, via the DCCsi. For more information about the DCCis please see the readme.md at the root of the Gem.
 
 ### Status: Prototype
 
@@ -9,13 +9,22 @@ The "DccScriptingInterface" (aka DCCsi) is a Gem for O3DE to extend and interfac
 ### OS: Windows only (for now)
 
 ### Support:
+
 - Maya 2023+ w/ Python3 <-- default version
 - Maya 2022 w/ Python3 (will not support py2.7)
 - Future version of Maya that include Py3+ can be supported with minimal change to the configuration
 
+## Brief
+
+This is an experimental Blender integration with O3DE, it's intent is:
+
+1. Configure and launch Maya (from CLI start.py, or even via O3DE editor menus)
+2. Bootstrap O3DE 'Studio Tools', provide shared code access, facilitate python tools and plugins, etc.
+3. Soft extension bootstrapping (non-destructive to Users Maya installation)
+
 ## Setup
 
-You should enable the DCCsi Gem in your project, this can be done with 'Configure Gems' in the O3DE Project Manager (o3de.exe). This will enable a 'Studio Tools' menu within the O3DE Editor.exe from which some DCC tools can be launched. *Note: Maya support to start from o3de menus has recently been implemented*.  However, before launching Maya for O3DE for the first time, you should follow the steps outlined below.
+You should enable the DCCsi Gem in your project, this can be done with 'Configure Gems' in the O3DE Project Manager (o3de.exe).  [Adding and Removing Gems in a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/) This will enable a 'Studio Tools' menu within the O3DE Editor.exe from which some DCC tools can be launched.  The O3DE tools provided with the DCCsi have python package dependencies (via requirements.txt).  However, before launching Maya for O3DE for the first time, you should follow the steps outlined below.
 
 ## Configure Maya (TL/DR quick start)
 
@@ -24,13 +33,15 @@ Before the O3DE DCCsi tools will operate correctly in Maya, you will need to ins
     1. Open a Windows Command Prompt (CMD)
 
     2. Change directory to:
+
 ```batch
 cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
 ```
 
     3. Run the DCCsi `foundation.py` script with a target to the `mayapy.exe` of the vision you want to configure:
+
 ```batch
-python foundation.py -py=C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe
+.\python foundation.py -py="C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe""
 ```
 
 This will install a version of all of the package dependencies into a folder such as the following, where the DCCsi will add them as a site-package based on the DCC tools version of python.
@@ -39,43 +50,56 @@ This will install a version of all of the package dependencies into a folder suc
 
 Since each DCC app, may be on a slightly different version of python, you may find more then one set of installed packages within that 3rdParty location, one for each version of python (the intent here is to maximize compatibility.)
 
+## General Info
+
+What is the DCCsi?
+
+1. A shared development environment for technical art oriented to working with Python across a number of DCC tools.
+2. Leverage the existing python ecosystem for technical art.
+3. Integrate a DCC app like Substance (or Substance SAT api) from the Python driven VFX and Games ecosystem.
+4. Extend O3DE and unlock its potential for content creators, and the Technical Artists that service them.
+
+For general info on the DCCsi:
+https://github.com/o3de/o3de/tree/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface
+
+For detailed documentation:
+https://www.o3de.org/docs/user-guide/< DCC Tools, not stubbed >
+
 ## Configure Maya Externally (Optional)
 
 The DCCsi assumes that DCC tools such as Maya are installed within their default install location, for Maya that usually would be:
 
-    C:\Program Files\Autodesk\Maya2022     
+    `C:\Program Files\Autodesk\Maya2022`    
 
-    C:\Program Files\Autodesk\Maya2023
+    `C:\Program Files\Autodesk\Maya2023`
 
 If you'd like to use Maya with O3DE bootstrapped tools externally, outside of the o3de Editor, you can do that also.
 
-There two ways, a windows environment via .bat file, or a start.py script
+There two ways, a windows environment via .bat file, or a `start.py` script
 
-From .bat file, double-click the following file type to start Maya:
+From .bat file, double-click the following file type to start Maya: `C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\win_launch_mayapy_2023.bat`
 
-    C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\win_launch_mayapy_2023.bat
-    
 To start from script:
 
     1. Open a Windows Command Prompt (CMD)
 
     2. Change directory to: 
+
 ```batch
 cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
 ```
 
-    3. Run the Maya start.py script:
+    3. Run the Maya `start.py` script:
+
 ```batch
-python Tools\DCC\Maya\start.py
+.\python Tools\DCC\Maya\start.py
 ```
 
-The DCCsi defaults to Maya 2023, and the default install location. We understand teams may want to use a specific version of maya, or may maintain a customized maya container, or IT managed install location.  If you want to alter the version, or the install location, you'll also want follow these instructions:
-
-The Maya env can be modified by adding this file:
+The DCCsi defaults to Maya 2023, and the default install location. We understand teams may want to use a specific version of Maya, or may maintain a customized Maya container, or IT managed install location.  If you want to alter the version, or the install location, you'll also want follow these instructions:
 
-    C:\path\tp\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\Env_Dev.bat
+The Maya env can be modified by adding this file:`C:\path\tp\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\Env_Dev.bat`
 
-Inside of that you can set/override envars to change the Maya version, it's python version information, as well as your custom install path.  Here is an example of those envars:
+Inside of that you can set/override ENVARsto change the Maya version, it's python version information, as well as your custom install path.  Here is an example of those ENVARs:
 
 ```batch
 :: Set your preferred defualt Maya and Python version
@@ -95,11 +119,13 @@ To generate a `settings.local.json` (which you can then modify with overrides to
     1. Open a Windows Command Prompt (CMD)
 
     2. Change directory to: 
+
 ```batch
 cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
 ```
 
-    3. Run the Maya config.py script:
+    3. Run the Maya` config.py` script:
+
 ```batch
 python Tools\DCC\Maya\config.py
 ```
@@ -108,9 +134,9 @@ You can now open `settings.local.json` in a text editor and make modifications a
 
 ## Additional Advanced Information
 
-Each dcc tool likely has it's own design and architecture that is often not common, and many have it's own specific version of python. Most are some version of py3+. O3DE provides an install of py3+ and manages package dependencies with requirements.txt files and the cmake build system.  If you prefer not to use out included foundation.py script, the details below will walk you through haw you can manually configure on your own.
+Each dcc tool likely has it's own design and architecture that is often not common, and many have it's own specific version of python. Most are some version of py3+. O3DE provides an install of py3+ and manages package dependencies with `requirements.txt` files and the cmake build system.  If you prefer not to use out included `foundation.py` script, the details below will walk you through haw you can manually configure on your own.
 
-Maya ships with it's own python interpreter called mayapy.exe
+Maya ships with it's own python interpreter called `mayapy.exe`
 
 Generally it is located here:
 
@@ -122,13 +148,14 @@ The python install and site-packages are here:
 
 A general goal of the DCCsi is be self-maintained, and to not taint the users installed applications of environment.
 
-So as to not directly modify Maya, we bootstrap additional access to site-packages in our userSetup.py:
+So as to not directly modify Maya, we bootstrap additional access to site-packages in our `userSetup.py`:
 
     C:/Depot/o3de/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py
 
-We don't want users to have to install or use any additional version of Python, especially use of legacy versions such as python2.7, although with older versions of  Maya and possibly other dcc tools we don't have that control.  Maya 2020 and earlier versions are still on Python2.7, so instead of forcing another install of python we can just use that versions mayapy to manage its extensions.
+We don't want users to have to install or use any additional version of Python, especially use of legacy versions such as python2.7, although with older versions of  Maya and possibly other dcc tools we don't have that control.  Maya 2020 and earlier versions are still on Python2.7, so instead of forcing another install of python we can just use that versions `mayapy.exe` to manage its extensions.
 
 Pip may already be installed, you can check like so (your Maya install path may be different):
+
 ```batch
 C:\Program Files\Autodesk\Maya2020\bin>'mayapy -m pip --version`
 ```
@@ -141,7 +168,8 @@ If pip is not available in your maya install for whatever reason, there are a co
 
 First find out where the site-packages is located
 
-C:\Program Files\Autodesk\Maya2020\bin>
+`C:\Program Files\Autodesk\Maya2020\bin`
+
 ```batch
 C:\Program Files\Autodesk\Maya2020\bin>mayapy -m site
 ```
@@ -167,7 +195,7 @@ This is the location we are looking for:
     C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages
 
 download get-pip.py and put into the above ^ directory:
-    
+
     https://bootstrap.pypa.io/pip/2.7/get-pip.py
 
 Put that in the root of site-packages:
@@ -175,11 +203,13 @@ Put that in the root of site-packages:
     C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages\\get-pip.py
 
 With get-pip module ready, we run it to install pip:
+
 ```batch
 C:\Program Files\Autodesk\Maya2020\bin>`mayapy -m get-pip`
 ```
 
 Now you should be able to run the following command and verify pip:
+
 ```batch
 C:\Program Files\Autodesk\Maya2020\bin>`mayapy -m pip --version
 pip 20.3.4 from C:\Users\< you >\AppData\Roaming\Python\Python27\site-packages\pip (python 2.7)`
@@ -198,16 +228,19 @@ Use a cmd prompt with elevated Admin rights.
     C:\WINDOWS\system32>
 
 This will change directories into Maya's binaries location (where mayapy lives)
+
 ```batch
 cd C:\Program Files\Autodesk\Maya2020\bin
 ```
 
 This command will ensure that pip is installed
+
 ```batch
 mayapy -m ensurepip
 ```
 
 This command will upgrade pip (for instance if a security patch is released)
+
 ```batch
 mayapy -m ensurepip --upgrade
 ```
@@ -230,6 +263,7 @@ This is where the legacy py2.7 version of requirements is stored:
 The following will install those requirements into a sandbox area that we can bootstrap in DCC tools running py2.7
 
     C:\Program Files\Autodesk\Maya2020\bin>
+
 ```batch
 mayapy -m pip install -r C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\requirements.txt -t C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages
 ```
@@ -265,6 +299,7 @@ This command will upgrade pip (for instance if a security patch is released)
     C:\Program Files\Autodesk\Maya2020\bin>`mayapy2 -m ensurepip --upgrade`
 
 The following will install those requirements into a sandbox area that we can bootstrap in DCC tools running py2.7
+
 ```batch
 mayapy2 -m pip install -r C:\Depot\o3de-dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\requirements.txt -t C:\Depot\o3de-dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages
 ```
@@ -290,6 +325,7 @@ This command will upgrade pip (for instance if a secutiry patch is released)
     C:\Program Files\Autodesk\Maya2020\bin>`mayapy -m ensurepip --upgrade`
 
 The following will install those requirements into a sandbox area that we can boostrap in DCC tools running py3.10.x
+
 ```batch
 mayapy -m pip install -r C:\Depot\o3de-dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\requirements.txt -t C:\Depot\o3de-dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\3.x\3.10.x\site-packages
 ```

+ 0 - 2
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/__init__.py

@@ -1,5 +1,3 @@
-# coding:utf-8
-#!/usr/bin/python
 #
 # 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.

+ 18 - 0
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/.solutions/DCCsi_8x.wpr

@@ -64,6 +64,21 @@ debug.launch-configs = (2,
          'pyrunargs': ('project',
                        '-u'),
          'runargs': '',
+         'rundir': ('project',
+                    '')}),
+                         'launch-qb0BoRrBNcUnIGWx': ({'shared': True},
+        {'buildcmd': ('project',
+                      None),
+         'env': ('project',
+                 ['']),
+         'name': 'DCCSI_PY_BASE',
+         'pyexec': ('custom',
+                    '${DCCSI_PY_BASE}'),
+         'pypath': ('project',
+                    []),
+         'pyrunargs': ('project',
+                       '-u'),
+         'runargs': '',
          'rundir': ('project',
                     '')}),
                          'launch-u5KZ5pWAzV5LuQ7c': ({'shared': True},
@@ -220,6 +235,9 @@ proj.launch-config = {loc('../../../../../../../../../o3de-dream-studio/Editor/S
                       loc('../../../../delete_me.py'): ('project',
         ('',
          'launch-H3gT8EPtxXR6YyOq')),
+                      loc('../../../../foundation.py'): ('project',
+        ('',
+         'launch-qb0BoRrBNcUnIGWx')),
                       loc('../../../../globals.py'): ('project',
         ('',
          'launch-H3gT8EPtxXR6YyOq')),

+ 0 - 1
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/config.py

@@ -102,7 +102,6 @@ from DccScriptingInterface.azpy.config_class import ConfigClass
 # but it is suggested that when the core <dccsi>\config.py is re-written
 # as a ConfigClass, that the WingConfig inherits from that instead
 
-
 # wing_config is a class object of WingConfig
 # WingConfig is a child class of ConfigClass
 class WingConfig(ConfigClass):

+ 92 - 29
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/IDE/Wing/readme.md

@@ -6,32 +6,44 @@
 
 ###### Support: <u>Wing Pro 8+</u>, currently Windows only (other not tested but may work)
 
-- Not yet tested in Installer builds (I am a developer building from source), more robust support for end users is being worked on for next release (2210)
+- Supports O3DE Python as Launch Configuration:
+  
+  - `${O3DE_DEV}` or `${DCCSI_PY_BASE}`
+
+- Supports Blender Python as a Launch Configuration:
+  
+  - `${DCCSI_PY_BLENDER}`
+
+- Supports MayaPy as a Launch Configuration:
+  
+  - `${DCCSI_PY_MAYA}`
 
-- Supports O3DE Python as Launch Configuration
+- Supports Substance3D Designer Python as a Launch Configuration:
+  
+  - `${DCCSI_PY_SUBSTANCE}`
 
-- Supports Blender Python as a Launch Configuration
+- The latest version of Wing Pro 8 (8.3.3.1) is recommended, as there was a bug in the initial release that prevented Launch Configurations bound to a ENVAR (like above) to function properly.  Wing Pro 9 likely works as well, you may just have to make slight adjustments to the configuration (covered in this document below)
 
-- Others (like Maya) WIP
+- Some additional testing and improvement have been mad in the current release (2210), including changes to better support Installer build folder patterns.  See the [readme.md at the root of the DCCsi ]([o3de/readme.md at development · o3de/o3de · GitHub](https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/readme.md))for more information about advanced configuration.
 
 ## TL/DR
 
-To get started, you can launch Wing from a Win CMD prompt, first make sure you are
+To get started, you can launch Wing from a Win CMD prompt, first make sure your engine is initialized (if you are using an installer build, it already should be.)  If you are building from source, make sure Python is initialized before using Wing.
 
 ```shell
 # First, ensure that your engine build has O3DE Python setup!
 > cd C:\path\to\o3de\
-> get_python.bat
+> .\get_python.bat
 
 # change to the dccsi root
 cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
 # python.cmd in this folder, wraps o3de python
-> python.cmd Tools\IDE\Wing\start,py
+> .\python.cmd Tools\IDE\Wing\start,py
 ```
 
-### Coming Soon
+### Latest Features
 
-- Start from O3DE Editor menu
+- The DccScriptingInterface Gem (DCCsi) can be added to your Game Project.  See[Registering Gems to a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/register-gems/)), and  [adding and removing gems]([Adding and Removing Gems in a Project - Open 3D Engine](https://www.o3de.org/docs/user-guide/project-config/add-remove-gems/)) which you can do through the Project Manager  (o3de.exe)  With the DCCsi enabled, you can launch Wing Pro 8 via menus in the main Editor.
 
 ## Overview
 
@@ -47,7 +59,7 @@ The DCCsi helps with aspects such as, configuration and settings, launching DCC
 
 # Getting Started
 
-Each IDE is different.  Some TAs (like me) like Wing IDE because it's relatively easy to set up and works great with DCC apps like Maya.  It's also pretty powerful and you can do a lot of data-driven configuration with it (which we will be covering some of.)  We aren't picking an IDE for you, that's a personal choice, we are just showing you how one like Wing can be set up to provide a better out-of-box experience (so that you don't have to do the setup and configuration yourself.)  In fact, we intend to do the same for other IDS like PyCharm and VScode (maybe others in the future.)
+Each IDE is different.  Some TAs (like me) like Wing IDE because it's relatively easy to set up and works great with DCC apps like Maya with less configuration.  It's also pretty powerful and you can do a lot of data-driven configuration with it (which we will be covering some of.)  We aren't picking an IDE for you, that's a personal choice, we are just showing you how one like Wing can be set up to provide a better out-of-box experience (so that you don't have to do the setup and configuration yourself.)  In fact, we intend to do the same for other IDS like PyCharm and VScode (maybe others in the future.)
 
 To use Wing, there are a few things that you need to do locally to get set up (and we may automate some of these steps in the future.)  
 
@@ -123,7 +135,7 @@ There is also a windows .bat launcher for Wing in this location:
 
 This is an alternative to launching from the editor menu, or using the scripted approach of starting wing from the dccsi cmd:
 
-    `dccsi > python.cmd Tools\IDE\Wing\start.py`
+    `.\python.cmd Tools\IDE\Wing\start.py`
 
 Development is a catch-22, sometimes the script framework is buggy and broken (or simply a change is wip), and you need a reliable fallback for your dev environment.
 
@@ -145,25 +157,25 @@ Then you will want to copy it, and rename the copy:
 
     `DccScriptingInterface\Tools\IDE\WingIDE\Env_Dev.bat`
 
-What this file provides, is the ability and opportunity to make envar settings and overrides prior to starting WingIDE from the .bat file launcher.  If you have already found that the launcher didn't work for you, this is a section you will want to pay attention to.
+What this file provides, is the ability and opportunity to make ENVAR settings and overrides prior to starting WingIDE from the .bat file launcher.  If you have already found that the launcher didn't work for you, this is a section you will want to pay attention to.
 
-These  envars set/override  the default  located in the windows dev env:
+These  ENVARs set/override  the default  located in the windows dev env:
 
 `DccScriptingInterface\Tools\Dev\Windows\Env_IDE_Wing.bat`
 
 You may find the following ENVARs useful to set:
 
-The default supported version is Wing 8 Pro, if your ide  is installed in the default location  you should not have to  set anything.  But  if you need  to configure the version and location you can set/override these envars
+The default supported version is Wing 8 Pro, if your IDE is installed in the default location  you should not have to  set anything.  But  if you need  to configure the version and location you can set/override these ENVARs
 
 `set DCCSI_WING_VERSION_MAJOR=8`
 
 `set "WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %DCCSI_WING_VERSION_MAJOR%"`
 
-and this envar will allow you to launch  with a different  wing  project file then the default
+and this ENVARwill allow you to launch  with a different  wing  project file then the default
 
 `set "WING_PROJ=%PATH_DCCSIG%\Tools\IDE\Wing\.solutions\DCCsi_%DCCSI_WING_VERSION_MAJOR%x.wpr"`
 
-If you are using a version of Wing 7, these envars can be set
+If you are using a version of Wing 7, these ENVARscan be set
 
 `set DCCSI_WING_VERSION_MAJOR=7`
 `set DCCSI_WING_VERSION_MINOR=2`
@@ -178,15 +190,66 @@ The primary settings file, which is distributed.  This has default settings defi
 
     `DccScriptingInterface\Tools\IDE\Wing\settings.json`
 
-This is a secondary settings file that can be manually made, or generated, for developers.  This file allows a developer to make local overrides to envars and settings.  These overrides take precedence.
+This is a secondary settings file that can be manually made, or generated, for developers.  This file allows a developer to make local overrides to ENVARs and settings, overrides in this file will take precedence over the defaults.
 
    ` DccScriptingInterface\Tools\IDE\Wing\settings.local.json`
 
-to do: ... describe how to generate the settings.local.json
+### Config.py and settings.local.json
+
+If you are starting Wing (or other tools) from the Editor menu's, then you may not need to do much configuration.  Because the Editor has a python framework, we can access data via `azlmbr` , and retrieve data such as the engine location (see the [dccsi editor  bootstrap.py]([o3de/bootstrap.py at development · o3de/o3de · GitHub](https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py)) .)  You'll see paths and associated ENVARs are set such as:
+
+```python
+# base paths
+O3DE_DEV = Path(azlmbr.paths.engroot).resolve()
+PATH_O3DE_BIN = Path(azlmbr.paths.executableFolder).resolve()
+PATH_O3DE_PROJECT = Path(azlmbr.paths.projectroot).resolve()
+```
+
+These are propagated (as ENVARs) into the `Wing\config.py `
+
+There is additional support to start via scripting. This is the same setup that the Editor uses to start the external IDE application. It makes use of a `settings.json` (default settings), and `settings.local.json` (user settings and overrides) within the o3de DCCsi folder for Wing IDE. These are utilized along with the addition of a `config.py` and `start.py` in the folder. This follows the patterns similar to how Blender, or Maya, can be launched from the O3DE menus, or in a scripted manner rather then legacy windows .bat files.
+
+Additionally, if you are developer you may want or need to alter the default configuration, for instance if you are downloading and building from source, then you may not have a standard install path, or you may have a custom cmake build path for binaries - and since the DCCsi, DCC apps, and IDEs such as Wing want to work with engine data, we may need to define where these things are.  You can use the Wing `config.py` to generate a `settings.local.json` file from CLI.
+
+To generate a `settings.local.json` (which you can then modify with overrides to paths and other settings)::
+
+    1. Open a Windows Command Prompt (CMD)
+
+    2. Change directory to:
+
+```batch
+cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
+```
+
+    3. Run the Wing `config.py` script:
+
+```batch
+.\python Tools\IDE\Wing\config.py
+```
+
+You can now open `settings.local.json` in a text editor and make modifications and resave before starting Maya.
+
+There two ways, a windows environment via .bat file, or a start.py script
+
+From .bat file, double-click the following file type to start Maya:`C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\IDE\Wing\win_launch_wingide.bat`
 
-to do: ... describe 
+To start from script:
+
+    1. Open a Windows Command Prompt (CMD)
+
+    2. Change directory to:
+
+```batch
+cd C:\path\to\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface
+```
+
+    3. Run the Wing `start.py` script:
+
+```batch
+python Tools\IDE\Wing\start.py
+```
 
-to do: ... use the start.py script to launch Wing
+The
 
 ### wingstub.py (for debugging)
 
@@ -196,21 +259,21 @@ This is necessary for attaching Wing as the debugger in scripts that are running
 
 2. Locate your Wing IDE install, the default location is somewhere like:
    
-   1. C:\Program Files (x86)\Wing Pro 8
+   1. `C:\Program Files (x86)\Wing Pro 8`
    
-   2. Locate: "C:\Program Files (x86)\Wing Pro 8\wingdbstub.py"
+   2. Locate: `C:\Program Files (x86)\Wing Pro 8\wingdbstub.py`
 
-3. Copy the file wingdbstub.py
+3. Copy the file `wingdbstub.py`
 
-4. It needs to be copied somewhere on the PYTHONPATH, for instance you could copy it here:  "DccScriptingInterface\Tools\IDE\WingIDE\wingdbstub.py"
+4. It needs to be copied somewhere on the `PYTHONPATH`, for instance you could copy it here:  `DccScriptingInterface\Tools\IDE\WingIDE\wingdbstub.py`
 
-5. Open the wingdbstub.py file and modify line 96 to
+5. Open the `wingdbstub.py` file and modify line 96 to
    
    1. **kEmbedded = 1**
 
 Notes: 
 
-- If you install a new version of Wing, you should check the new version to see if the wingdbstub.py file has changed (use a diff tool?); if it has do the steps above again.
+- If you install a new version of Wing, you should check the new version to see if the `wingdbstub.py` file has changed (use a diff tool?); if it has do the steps above again.
 
 - For debugging to work, on windows you likely will need to add the wing executable to **Windows Defender Firewall**. When you start wing for the first time it may prompt you to do this, otherwise may need to do it manually (see **HELP** below)
 
@@ -227,7 +290,7 @@ Trouble attaching debugger?  Open your firewall and add an exception for wing.
   - Then click on **Allow another app...**
 - In the Add an app window:
   - click on **Browse...** and point it to your wing executable
-    - C:\Program Files (x86)\Wing Pro 8\bin\wing.exe
+    - `C:\Program Files (x86)\Wing Pro 8\bin\wing.exe`
   - click on **Add**
 
 # Revision Info:
@@ -238,13 +301,13 @@ Trouble attaching debugger?  Open your firewall and add an exception for wing.
 
 - This version is only the integration patterns for configuration, settings, launch and bootstrapping. No additional tooling within Wing IDE is implemented yet, however Wing has it's own API and extensibility, so this could be an area for future work.
 
-- Currently only the latest version of Wing Pro 8 has been tested: 8.3.2.0 (rev 9d633cb1c4a7), Release (June 17, 2022).  We expect any version of Wing Pro8 to work fine, inform us and help get it fixed if it doesn't.
+- Currently only the latest version of Wing Pro 8 has been tested: 8.3.2.0 (rev 9d633cb1c4a7), Release (June 17, 2022).  We expect this and later versions of Wing Pro8 to work fine, inform us and help get it fixed if it doesn't.
 
 - Wing 7.x was previously supported, but it was python2.7 based and we are deprecating support for py2.7, and this includes deprecation of support for apps that are pre-py3 bound.
 
 - Previous versions of Wing may still work, however you will need to configure and/or modify the env yourself. This would also include version such as Wing Community Edition.
 
-- Updated this readme
+- This readme.md is continually updated as changes are made.  Remember, this is experimental and still early, and refactoring to improve the core framework and scaffolding patterns undergo frequent revisions (but will be locked down over time.)
 
 ---
 

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio