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

[Terrain] Add autosave options to the Image Gradient (#12921)

* First pass of autosave.

Signed-off-by: Mike Balfour <[email protected]>

* Make auto-save the default.
In changing the default, also make auto-save always prompt the user at least once per Editor session to avoid accidental overwrites.

Signed-off-by: Mike Balfour <[email protected]>

* Change autosave pattern to a.####.tif
This relies on a separate change to ImageProcessingAtom to support patterns like this.

Signed-off-by: Mike Balfour <[email protected]>

* Smal cleanups.

Signed-off-by: Mike Balfour <[email protected]>

Signed-off-by: Mike Balfour <[email protected]>
Mike Balfour 2 роки тому
батько
коміт
d36973f2ba

+ 0 - 4
Gems/GradientSignal/Code/Include/GradientSignal/Components/ImageGradientComponent.h

@@ -20,7 +20,6 @@
 #include <GradientSignal/Ebuses/ImageGradientRequestBus.h>
 #include <GradientSignal/Ebuses/ImageGradientRequestBus.h>
 #include <GradientSignal/Ebuses/ImageGradientModificationBus.h>
 #include <GradientSignal/Ebuses/ImageGradientModificationBus.h>
 #include <GradientSignal/Util.h>
 #include <GradientSignal/Util.h>
-#include <LmbrCentral/Dependency/DependencyMonitor.h>
 
 
 namespace GradientSignal
 namespace GradientSignal
 {
 {
@@ -178,8 +177,6 @@ namespace GradientSignal
         // GradientTransformNotificationBus overrides...
         // GradientTransformNotificationBus overrides...
         void OnGradientTransformChanged(const GradientTransform& newTransform) override;
         void OnGradientTransformChanged(const GradientTransform& newTransform) override;
 
 
-        void SetupDependencies();
-
         void CreateImageModificationBuffer();
         void CreateImageModificationBuffer();
         void ClearImageModificationBuffer();
         void ClearImageModificationBuffer();
         bool ModificationBufferIsActive() const;
         bool ModificationBufferIsActive() const;
@@ -206,7 +203,6 @@ namespace GradientSignal
 
 
     private:
     private:
         ImageGradientConfig m_configuration;
         ImageGradientConfig m_configuration;
-        LmbrCentral::DependencyMonitor m_dependencyMonitor;
         mutable AZStd::shared_mutex m_queryMutex;
         mutable AZStd::shared_mutex m_queryMutex;
         GradientTransform m_gradientTransform;
         GradientTransform m_gradientTransform;
         ChannelToUse m_currentChannel = ChannelToUse::Red;
         ChannelToUse m_currentChannel = ChannelToUse::Red;

+ 6 - 17
Gems/GradientSignal/Code/Source/Components/ImageGradientComponent.cpp

@@ -17,6 +17,7 @@
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/SerializeContext.h>
 #include <AzCore/Serialization/SerializeContext.h>
 #include <GradientSignal/Ebuses/GradientTransformRequestBus.h>
 #include <GradientSignal/Ebuses/GradientTransformRequestBus.h>
+#include <LmbrCentral/Dependency/DependencyMonitor.h>
 
 
 namespace GradientSignal
 namespace GradientSignal
 {
 {
@@ -280,14 +281,6 @@ namespace GradientSignal
     {
     {
     }
     }
 
 
-    void ImageGradientComponent::SetupDependencies()
-    {
-        m_dependencyMonitor.Reset();
-        m_dependencyMonitor.SetRegionChangedEntityNotificationFunction();
-        m_dependencyMonitor.ConnectOwner(GetEntityId());
-        m_dependencyMonitor.ConnectDependency(m_configuration.m_imageAsset.GetId());
-    }
-
     void ImageGradientComponent::GetSubImageData()
     void ImageGradientComponent::GetSubImageData()
     {
     {
         if (!m_configuration.m_imageAsset || !m_configuration.m_imageAsset.IsReady())
         if (!m_configuration.m_imageAsset || !m_configuration.m_imageAsset.IsReady())
@@ -660,8 +653,6 @@ namespace GradientSignal
         // This will immediately call OnGradientTransformChanged and initialize m_gradientTransform.
         // This will immediately call OnGradientTransformChanged and initialize m_gradientTransform.
         GradientTransformNotificationBus::Handler::BusConnect(GetEntityId());
         GradientTransformNotificationBus::Handler::BusConnect(GetEntityId());
 
 
-        SetupDependencies();
-
         ImageGradientRequestBus::Handler::BusConnect(GetEntityId());
         ImageGradientRequestBus::Handler::BusConnect(GetEntityId());
         ImageGradientModificationBus::Handler::BusConnect(GetEntityId());
         ImageGradientModificationBus::Handler::BusConnect(GetEntityId());
 
 
@@ -686,8 +677,6 @@ namespace GradientSignal
         ImageGradientRequestBus::Handler::BusDisconnect();
         ImageGradientRequestBus::Handler::BusDisconnect();
         GradientTransformNotificationBus::Handler::BusDisconnect();
         GradientTransformNotificationBus::Handler::BusDisconnect();
 
 
-        m_dependencyMonitor.Reset();
-
         // Make sure we don't keep any cached references to the image asset data or the image modification buffer.
         // Make sure we don't keep any cached references to the image asset data or the image modification buffer.
         UpdateCachedImageBufferData({}, {});
         UpdateCachedImageBufferData({}, {});
 
 
@@ -742,6 +731,7 @@ namespace GradientSignal
         AZStd::unique_lock lock(m_queryMutex);
         AZStd::unique_lock lock(m_queryMutex);
         m_configuration.m_imageAsset = asset;
         m_configuration.m_imageAsset = asset;
         GetSubImageData();
         GetSubImageData();
+        LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
     }
     }
 
 
     void ImageGradientComponent::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
     void ImageGradientComponent::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
@@ -825,7 +815,8 @@ namespace GradientSignal
 
 
     void ImageGradientComponent::ClearImageModificationBuffer()
     void ImageGradientComponent::ClearImageModificationBuffer()
     {
     {
-        AZ_Assert(!ModificationBufferIsActive(), "Clearing modified image data while it's still in use!");
+        AZ_Assert(!ModificationBufferIsActive(), "Clearing modified image data while it's still in use as the active asset!");
+        AZ_Assert(!m_configuration.m_imageModificationActive, "Clearing modified image data while in modification mode!")
         m_modifiedImageData.resize(0);
         m_modifiedImageData.resize(0);
     }
     }
 
 
@@ -994,8 +985,6 @@ namespace GradientSignal
             m_configuration.m_imageAsset = asset;
             m_configuration.m_imageAsset = asset;
         }
         }
 
 
-        SetupDependencies();
-
         if (m_configuration.m_imageAsset.GetId().IsValid())
         if (m_configuration.m_imageAsset.GetId().IsValid())
         {
         {
             // If we have a valid Asset ID, check to see if it also appears in the AssetCatalog. This might be an Asset ID for an asset
             // If we have a valid Asset ID, check to see if it also appears in the AssetCatalog. This might be an Asset ID for an asset
@@ -1057,7 +1046,7 @@ namespace GradientSignal
         // execute an arbitrary amount of logic, including calls back to this component.
         // execute an arbitrary amount of logic, including calls back to this component.
         {
         {
             AZStd::unique_lock lock(m_queryMutex);
             AZStd::unique_lock lock(m_queryMutex);
-        m_configuration.m_tiling.SetX(tilingX);
+            m_configuration.m_tiling.SetX(tilingX);
         }
         }
         LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
         LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
     }
     }
@@ -1073,7 +1062,7 @@ namespace GradientSignal
         // execute an arbitrary amount of logic, including calls back to this component.
         // execute an arbitrary amount of logic, including calls back to this component.
         {
         {
             AZStd::unique_lock lock(m_queryMutex);
             AZStd::unique_lock lock(m_queryMutex);
-        m_configuration.m_tiling.SetY(tilingY);
+            m_configuration.m_tiling.SetY(tilingY);
         }
         }
         LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
         LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
     }
     }

+ 208 - 32
Gems/GradientSignal/Code/Source/Editor/EditorImageGradientComponent.cpp

@@ -9,27 +9,33 @@
 #include <Editor/EditorImageGradientComponent.h>
 #include <Editor/EditorImageGradientComponent.h>
 #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
 #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
 #include <AzCore/Asset/AssetCommon.h>
 #include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Preprocessor/EnumReflectUtils.h>
 #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
 #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
 #include <AzQtComponents/Components/Widgets/FileDialog.h>
 #include <AzQtComponents/Components/Widgets/FileDialog.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
+#include <AzToolsFramework/API/EntityCompositionNotificationBus.h>
 #include <AzToolsFramework/UI/PropertyEditor/PropertyFilePathCtrl.h>
 #include <AzToolsFramework/UI/PropertyEditor/PropertyFilePathCtrl.h>
 #include <Editor/EditorImageGradientComponentMode.h>
 #include <Editor/EditorImageGradientComponentMode.h>
 #include <Editor/EditorGradientImageCreatorUtils.h>
 #include <Editor/EditorGradientImageCreatorUtils.h>
 
 
 namespace GradientSignal
 namespace GradientSignal
 {
 {
+    AZ_ENUM_DEFINE_REFLECT_UTILITIES(ImageGradientAutoSaveMode);
+
     void EditorImageGradientComponent::Reflect(AZ::ReflectContext* context)
     void EditorImageGradientComponent::Reflect(AZ::ReflectContext* context)
     {
     {
         if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
         if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
         {
         {
+            ImageGradientAutoSaveModeReflect(*serializeContext);
+
             serializeContext->Class<EditorImageGradientComponent, AzToolsFramework::Components::EditorComponentBase>()
             serializeContext->Class<EditorImageGradientComponent, AzToolsFramework::Components::EditorComponentBase>()
-                ->Version(2)
+                ->Version(3)
                 ->Field("Previewer", &EditorImageGradientComponent::m_previewer)
                 ->Field("Previewer", &EditorImageGradientComponent::m_previewer)
                 ->Field("CreationSelectionChoice", &EditorImageGradientComponent::m_creationSelectionChoice)
                 ->Field("CreationSelectionChoice", &EditorImageGradientComponent::m_creationSelectionChoice)
                 ->Field("OutputResolution", &EditorImageGradientComponent::m_outputResolution)
                 ->Field("OutputResolution", &EditorImageGradientComponent::m_outputResolution)
                 ->Field("OutputFormat", &EditorImageGradientComponent::m_outputFormat)
                 ->Field("OutputFormat", &EditorImageGradientComponent::m_outputFormat)
-                ->Field("OutputImagePath", &EditorImageGradientComponent::m_outputImagePath)
                 ->Field("Configuration", &EditorImageGradientComponent::m_configuration)
                 ->Field("Configuration", &EditorImageGradientComponent::m_configuration)
+                ->Field("AutoSaveMode", &EditorImageGradientComponent::m_autoSaveMode)
                 ->Field("ComponentMode", &EditorImageGradientComponent::m_componentModeDelegate)
                 ->Field("ComponentMode", &EditorImageGradientComponent::m_componentModeDelegate)
                 ;
                 ;
 
 
@@ -67,7 +73,7 @@ namespace GradientSignal
 
 
                     ->DataElement(AZ::Edit::UIHandlers::ComboBox, &ImageGradientConfig::m_channelToUse,
                     ->DataElement(AZ::Edit::UIHandlers::ComboBox, &ImageGradientConfig::m_channelToUse,
                         "Channel To Use", "The channel to use from the image.")
                         "Channel To Use", "The channel to use from the image.")
-                        ->EnumAttribute(ChannelToUse::Red, "Red (default)")
+                        ->EnumAttribute(ChannelToUse::Red, "Red")
                         ->EnumAttribute(ChannelToUse::Green, "Green")
                         ->EnumAttribute(ChannelToUse::Green, "Green")
                         ->EnumAttribute(ChannelToUse::Blue, "Blue")
                         ->EnumAttribute(ChannelToUse::Blue, "Blue")
                         ->EnumAttribute(ChannelToUse::Alpha, "Alpha")
                         ->EnumAttribute(ChannelToUse::Alpha, "Alpha")
@@ -82,7 +88,7 @@ namespace GradientSignal
 
 
                     ->DataElement(AZ::Edit::UIHandlers::ComboBox, &ImageGradientConfig::m_customScaleType,
                     ->DataElement(AZ::Edit::UIHandlers::ComboBox, &ImageGradientConfig::m_customScaleType,
                         "Custom Scale", "Choose a type of scaling to be applied to the image data.")
                         "Custom Scale", "Choose a type of scaling to be applied to the image data.")
-                        ->EnumAttribute(CustomScaleType::None, "None (default)")
+                        ->EnumAttribute(CustomScaleType::None, "None")
                         ->EnumAttribute(CustomScaleType::Auto, "Auto")
                         ->EnumAttribute(CustomScaleType::Auto, "Auto")
                         ->EnumAttribute(CustomScaleType::Manual, "Manual")
                         ->EnumAttribute(CustomScaleType::Manual, "Manual")
                         // Refresh the entire tree on scaling changes, because it will show/hide the scale ranges for Manual scaling.
                         // Refresh the entire tree on scaling changes, because it will show/hide the scale ranges for Manual scaling.
@@ -123,6 +129,17 @@ namespace GradientSignal
                         ->Attribute(AZ::Edit::Attributes::ReadOnly, &EditorImageGradientComponent::InComponentMode)
                         ->Attribute(AZ::Edit::Attributes::ReadOnly, &EditorImageGradientComponent::InComponentMode)
                         ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorImageGradientComponent::RefreshCreationSelectionChoice)
                         ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorImageGradientComponent::RefreshCreationSelectionChoice)
 
 
+                    // Auto-save option when editing an image.
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &EditorImageGradientComponent::m_autoSaveMode, "Save Mode",
+                        "When editing an image, this selects whether to manually prompt for the save location, auto-save on every edit, "
+                        "or auto-save with incrementing file names on every edit.")
+                        ->EnumAttribute(ImageGradientAutoSaveMode::SaveAs, "Save As...")
+                        ->EnumAttribute(ImageGradientAutoSaveMode::AutoSave, "Auto Save")
+                        ->EnumAttribute(ImageGradientAutoSaveMode::AutoSaveWithIncrementalNames, "Auto Save With Incrementing Names")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, &EditorImageGradientComponent::GetAutoSaveVisibility)
+                        // There's no need to ChangeNotify when this property changes, it doesn't affect the behavior of the comopnent,
+                        // it's only queried at the point that an edit is completed.
+
                     // Controls for creating a new image
                     // Controls for creating a new image
                     ->DataElement(AZ::Edit::UIHandlers::Default, &EditorImageGradientComponent::m_outputResolution,
                     ->DataElement(AZ::Edit::UIHandlers::Default, &EditorImageGradientComponent::m_outputResolution,
                         "Resolution", "Output resolution of the saved image.")
                         "Resolution", "Output resolution of the saved image.")
@@ -223,18 +240,15 @@ namespace GradientSignal
         m_previewer.Activate(GetEntityId());
         m_previewer.Activate(GetEntityId());
         GradientImageCreatorRequestBus::Handler::BusConnect(GetEntityId());
         GradientImageCreatorRequestBus::Handler::BusConnect(GetEntityId());
 
 
-        // Make sure our image asset and creation/selection visibility settings are synced in the runtime component's configuration.
+        // Make sure our image asset settings are synced in the runtime component's configuration.
         RefreshImageAssetStatus();
         RefreshImageAssetStatus();
-        RefreshCreationSelectionChoice();
 
 
-        auto entityComponentIdPair = AZ::EntityComponentIdPair(GetEntityId(), GetId());
-        m_componentModeDelegate.ConnectWithSingleComponentMode<EditorImageGradientComponent, EditorImageGradientComponentMode>(
-            entityComponentIdPair, nullptr);
+        EnableComponentMode();
     }
     }
 
 
     void EditorImageGradientComponent::Deactivate()
     void EditorImageGradientComponent::Deactivate()
     {
     {
-        m_componentModeDelegate.Disconnect();
+        DisableComponentMode();
 
 
         m_currentImageAssetStatus = AZ::Data::AssetData::AssetStatus::NotLoaded;
         m_currentImageAssetStatus = AZ::Data::AssetData::AssetStatus::NotLoaded;
         m_currentImageJobsPending = false;
         m_currentImageJobsPending = false;
@@ -342,15 +356,49 @@ namespace GradientSignal
         return statusChanged;
         return statusChanged;
     }
     }
 
 
+    void EditorImageGradientComponent::RefreshComponentModeStatus()
+    {
+        const bool paintModeVisible = (GetPaintModeVisibility() != AZ::Edit::PropertyVisibility::Hide);
+
+        if (paintModeVisible)
+        {
+            EnableComponentMode();
+        }
+        else
+        {
+            DisableComponentMode();
+        }
+    }
+
+    void EditorImageGradientComponent::EnableComponentMode()
+    {
+        if (m_componentModeDelegate.AddedToComponentMode())
+        {
+            return;
+        }
+
+        auto entityComponentIdPair = AZ::EntityComponentIdPair(GetEntityId(), GetId());
+        m_componentModeDelegate.ConnectWithSingleComponentMode<EditorImageGradientComponent, EditorImageGradientComponentMode>(
+            entityComponentIdPair, nullptr);
+    }
+
+    void EditorImageGradientComponent::DisableComponentMode()
+    {
+        if (!m_componentModeDelegate.AddedToComponentMode())
+        {
+            return;
+        }
+
+        m_componentModeDelegate.Disconnect();
+    }
+
+
     void EditorImageGradientComponent::OnCompositionChanged()
     void EditorImageGradientComponent::OnCompositionChanged()
     {
     {
         m_previewer.RefreshPreview();
         m_previewer.RefreshPreview();
         m_component.WriteOutConfig(&m_configuration);
         m_component.WriteOutConfig(&m_configuration);
         SetDirty();
         SetDirty();
 
 
-        // Make sure the creation/selection visibility flags have been refreshed correctly.
-        RefreshCreationSelectionChoice();
-
         if (RefreshImageAssetStatus() && GetImageOptionsVisibility())
         if (RefreshImageAssetStatus() && GetImageOptionsVisibility())
         {
         {
             // If the asset status changed and the image asset property is visible, refresh the entire tree so
             // If the asset status changed and the image asset property is visible, refresh the entire tree so
@@ -402,6 +450,12 @@ namespace GradientSignal
         return AZ::Edit::PropertyRefreshLevels::EntireTree;
         return AZ::Edit::PropertyRefreshLevels::EntireTree;
     }
     }
 
 
+    AZ::Crc32 EditorImageGradientComponent::GetAutoSaveVisibility() const
+    {
+        return (m_creationSelectionChoice == ImageCreationOrSelection::UseExistingImage) ? AZ::Edit::PropertyVisibility::Show
+                                                                                         : AZ::Edit::PropertyVisibility::Hide;
+    }
+
     AZ::Crc32 EditorImageGradientComponent::GetImageOptionsVisibility() const
     AZ::Crc32 EditorImageGradientComponent::GetImageOptionsVisibility() const
     {
     {
         return (m_creationSelectionChoice == ImageCreationOrSelection::UseExistingImage)
         return (m_creationSelectionChoice == ImageCreationOrSelection::UseExistingImage)
@@ -419,8 +473,7 @@ namespace GradientSignal
     {
     {
         // Only show the image painting button while we're using an image, not while we're creating one.
         // Only show the image painting button while we're using an image, not while we're creating one.
         return ((GetImageOptionsVisibility() != AZ::Edit::PropertyVisibility::Hide)
         return ((GetImageOptionsVisibility() != AZ::Edit::PropertyVisibility::Hide)
-                && m_configuration.m_imageAsset.IsReady()
-                && !ImageHasPendingJobs(m_configuration.m_imageAsset.GetId()))
+                && (m_currentImageAssetStatus == AZ::Data::AssetData::AssetStatus::Ready) && !m_currentImageJobsPending)
             ? AZ::Edit::PropertyVisibility::ShowChildrenOnly
             ? AZ::Edit::PropertyVisibility::ShowChildrenOnly
             : AZ::Edit::PropertyVisibility::Hide;
             : AZ::Edit::PropertyVisibility::Hide;
 
 
@@ -453,12 +506,12 @@ namespace GradientSignal
 
 
     AZ::IO::Path EditorImageGradientComponent::GetOutputImagePath() const
     AZ::IO::Path EditorImageGradientComponent::GetOutputImagePath() const
     {
     {
-        return m_outputImagePath;
+        return AZ::IO::Path(GetImageSourcePath(m_configuration.m_imageAsset.GetId()));
     }
     }
 
 
     void EditorImageGradientComponent::SetOutputImagePath(const AZ::IO::Path& outputImagePath)
     void EditorImageGradientComponent::SetOutputImagePath(const AZ::IO::Path& outputImagePath)
     {
     {
-        m_outputImagePath = outputImagePath;
+        m_component.SetImageAssetSourcePath(outputImagePath.String());
     }
     }
 
 
     bool EditorImageGradientComponent::InComponentMode() const
     bool EditorImageGradientComponent::InComponentMode() const
@@ -466,21 +519,101 @@ namespace GradientSignal
         return m_componentModeDelegate.AddedToComponentMode();
         return m_componentModeDelegate.AddedToComponentMode();
     }
     }
 
 
-    bool EditorImageGradientComponent::GetSaveLocation(AZ::IO::Path& fullPath, AZStd::string& relativePath)
+    AZ::IO::Path EditorImageGradientComponent::GetIncrementingAutoSavePath(const AZ::IO::Path& currentPath) const
     {
     {
-        // Create a default path to save our image to if we haven't previously set the image path to anything yet.
-        if (m_outputImagePath.empty())
+        // Given a path for a source texture, this will return a new path with an incremented version number on the end.
+        // If the input path doesn't have a version number yet, it will get one added.
+        // Ex:
+        // 'Assets/Gradients/MyGradient_gsi.tif' -> 'Assets/Gradients/MyGradient_gsi.0000.tif'
+        // 'Assets/Gradients/MyGradient_gsi.0005.tif' -> 'Assets/Gradients/MyGradient_gsi.0006.tif'
+
+        // We'll use 4 digits as our minimum version number size. We use this to add leading 0s so that alpha-sorting of the
+        // numbers still puts them in the right order. For example, we'll get 08, 09, 10 instead of 0, 1, 10, 2. This does
+        // mean that the alpha-sorting will look wrong if we hit 5 digits, but it's a readability tradeoff.
+        constexpr int NumVersionDigits = 4;
+
+        // Store a copy of the filename in a string so that we can modify it below.
+        AZStd::string baseFileName = currentPath.Stem().Native();
+
+        size_t foundDotChar = baseFileName.find_last_of(AZ_FILESYSTEM_EXTENSION_SEPARATOR);
+        uint32_t versionNumber = 0;
+
+        // If the base name ends with '.####' (4 or more digits), then we'll treat that as our auto version number.
+        // We'll read in the existing number, strip it, and increment it.
+        if (foundDotChar <= (baseFileName.length() - NumVersionDigits - 1))
         {
         {
-            m_outputImagePath = AZStd::string::format("%.*s_gsi.tif", AZ_STRING_ARG(GetEntity()->GetName()));
+            bool foundVersionNumber = true;
+            uint32_t tempVersionNumber = 0;
+
+            for (size_t digitChar = foundDotChar + 1; digitChar < baseFileName.length(); digitChar++)
+            {
+                // If any character after the . isn't a digit, then this isn't a valid version number, so leave it alone.
+                // (Ex: "image_gsi.o3de")
+                if (!isdigit(baseFileName.at(digitChar)))
+                {
+                    foundVersionNumber = false;
+                    break;
+                }
+
+                // Convert the version number that we've found one character at a time.
+                tempVersionNumber = (tempVersionNumber * 10) + (baseFileName.at(digitChar) - '0');
+            }
+
+            // If we found a valid version number, auto-increment it by one and strip off the previous one.
+            // We'll re-add the new incremented version number back on at the end.
+            if (foundVersionNumber)
+            {
+                versionNumber = tempVersionNumber + 1;
+                baseFileName = baseFileName.substr(0, foundDotChar);
+            }
         }
         }
 
 
-        // Turn it into an absolute path to give to the "Save file" dialog.
-        fullPath = AzToolsFramework::GetAbsolutePathFromRelativePath(m_outputImagePath);
+        // Create a new string of the form <filename>.####
+        // For example, "entity1_gsi.tif" should become "entity1_gsi.0000.tif"
+        AZStd::string newFilename = AZStd::string::format(
+            AZ_STRING_FORMAT "%c%0*d" AZ_STRING_FORMAT,
+            AZ_STRING_ARG(baseFileName),
+            AZ_FILESYSTEM_EXTENSION_SEPARATOR,
+            NumVersionDigits, versionNumber,
+            AZ_STRING_ARG(currentPath.Extension().Native()));
+
+        AZ::IO::Path newPath = currentPath;
+        newPath.ReplaceFilename(AZ::IO::Path(newFilename));
+        return newPath;
+    }
+
 
 
-        // Prompt the user for the file name and path.
-        const QString fileFilter = ImageCreatorUtils::GetSupportedImagesFilter().c_str();
-        const QString absoluteSaveFilePath =
-            AzQtComponents::FileDialog::GetSaveFileName(nullptr, "Save As...", QString(fullPath.Native().c_str()), fileFilter);
+    bool EditorImageGradientComponent::GetSaveLocation(
+        AZ::IO::Path& fullPath, AZStd::string& relativePath, ImageGradientAutoSaveMode autoSaveMode)
+    {
+        QString absoluteSaveFilePath = QString(fullPath.Native().c_str());
+        bool promptForSaveName = false;
+
+        switch (autoSaveMode)
+        {
+        case ImageGradientAutoSaveMode::SaveAs:
+            promptForSaveName = true;
+            break;
+        case ImageGradientAutoSaveMode::AutoSave:
+            // If the user has never been prompted for a save location during this Editor run, make sure they're prompted at least once.
+            // If they have been prompted, then skip the prompt and just overwrite the existing location.
+            promptForSaveName = !m_promptedForSaveLocation;
+            break;
+        case ImageGradientAutoSaveMode::AutoSaveWithIncrementalNames:
+            fullPath = GetIncrementingAutoSavePath(fullPath);
+            absoluteSaveFilePath = QString(fullPath.Native().c_str());
+
+            // Only prompt if our auto-generated name matches an existing file.
+            promptForSaveName = AZ::IO::SystemFile::Exists(fullPath.Native().c_str());
+            break;
+        }
+
+        if (promptForSaveName)
+        {
+            // Prompt the user for the file name and path.
+            const QString fileFilter = ImageCreatorUtils::GetSupportedImagesFilter().c_str();
+            absoluteSaveFilePath = AzQtComponents::FileDialog::GetSaveFileName(nullptr, "Save As...", absoluteSaveFilePath, fileFilter);
+        }
 
 
         // User canceled the save dialog, so exit out.
         // User canceled the save dialog, so exit out.
         if (absoluteSaveFilePath.isEmpty())
         if (absoluteSaveFilePath.isEmpty())
@@ -488,6 +621,10 @@ namespace GradientSignal
             return false;
             return false;
         }
         }
 
 
+        // If we prompted for a save name and didn't cancel out with an empty path, track that we've prompted the user so that we don't
+        // do it again for autosave.
+        m_promptedForSaveLocation = m_promptedForSaveLocation || promptForSaveName;
+
         const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
         const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
         const auto absoluteSaveFilePathCStr = absoluteSaveFilePathUtf8.constData();
         const auto absoluteSaveFilePathCStr = absoluteSaveFilePathUtf8.constData();
         fullPath.Assign(absoluteSaveFilePathCStr);
         fullPath.Assign(absoluteSaveFilePathCStr);
@@ -539,7 +676,13 @@ namespace GradientSignal
     {
     {
         AZ::IO::Path fullPathIO;
         AZ::IO::Path fullPathIO;
         AZStd::string relativePath;
         AZStd::string relativePath;
-        if (!GetSaveLocation(fullPathIO, relativePath))
+
+        fullPathIO = AZ::IO::Path(GetImageSourcePath({}));
+
+        // Creating an image should always prompt the user for the save location.
+        const auto saveMode = ImageGradientAutoSaveMode::SaveAs;
+
+        if (!GetSaveLocation(fullPathIO, relativePath, saveMode))
         {
         {
             return;
             return;
         }
         }
@@ -563,11 +706,47 @@ namespace GradientSignal
             imageResolutionX, imageResolutionY, channels, m_outputFormat, pixelBuffer);
             imageResolutionX, imageResolutionY, channels, m_outputFormat, pixelBuffer);
     }
     }
 
 
+    AZStd::string EditorImageGradientComponent::GetImageSourcePath(const AZ::Data::AssetId& imageAssetId) const
+    {
+        if (imageAssetId.IsValid())
+        {
+            AZStd::string sourcePath;
+            bool sourceFileFound = false;
+            AZ::Data::AssetInfo assetInfo;
+            AZStd::string watchFolder;
+
+            AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
+                sourceFileFound,
+                &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourceUUID,
+                imageAssetId.m_guid,
+                assetInfo,
+                watchFolder);
+
+            if (sourceFileFound)
+            {
+                bool success = AzFramework::StringFunc::Path::ConstructFull(
+                    watchFolder.c_str(), assetInfo.m_relativePath.c_str(), sourcePath, true);
+
+                if (success)
+                {
+                    return sourcePath;
+                }
+            }
+        }
+
+        // Invalid image asset or failed path creation, try creating a new name.
+        return AZStd::string::format(AZ_STRING_FORMAT "_gsi.tif", AZ_STRING_ARG(GetEntity()->GetName()));
+    }
+
     bool EditorImageGradientComponent::SaveImage()
     bool EditorImageGradientComponent::SaveImage()
     {
     {
         AZ::IO::Path fullPathIO;
         AZ::IO::Path fullPathIO;
         AZStd::string relativePath;
         AZStd::string relativePath;
-        if (!GetSaveLocation(fullPathIO, relativePath))
+
+        // Turn it into an absolute path to give to the "Save file" dialog.
+        fullPathIO = AZ::IO::Path(GetImageSourcePath(m_configuration.m_imageAsset.GetId()));
+
+        if (!GetSaveLocation(fullPathIO, relativePath, m_autoSaveMode))
         {
         {
             return false;
             return false;
         }
         }
@@ -636,9 +815,6 @@ namespace GradientSignal
         // for the product asset to get created.
         // for the product asset to get created.
         createdAsset.SetHint(relativePath);
         createdAsset.SetHint(relativePath);
 
 
-        // Set our output image path to whatever was selected so that we default to the same file name and path next time.
-        m_outputImagePath = fullPath.c_str();
-
         // Set the active image to the created one.
         // Set the active image to the created one.
         m_component.SetImageAsset(createdAsset);
         m_component.SetImageAsset(createdAsset);
 
 

+ 30 - 2
Gems/GradientSignal/Code/Source/Editor/EditorImageGradientComponent.h

@@ -21,6 +21,12 @@
 
 
 namespace GradientSignal
 namespace GradientSignal
 {
 {
+    AZ_ENUM_CLASS_WITH_UNDERLYING_TYPE(ImageGradientAutoSaveMode, uint8_t,
+        (SaveAs, 0),
+        (AutoSave, 1),
+        (AutoSaveWithIncrementalNames, 2)
+    );
+
     // This class inherits from EditorComponentBase instead of EditorGradientComponentBase / EditorWrappedComponentBase so that
     // This class inherits from EditorComponentBase instead of EditorGradientComponentBase / EditorWrappedComponentBase so that
     // we can have control over where the Editor-specific parameters for image creation and editing appear in the component
     // we can have control over where the Editor-specific parameters for image creation and editing appear in the component
     // relative to the other runtime-only settings.
     // relative to the other runtime-only settings.
@@ -80,7 +86,7 @@ namespace GradientSignal
         void EndImageModification() override;
         void EndImageModification() override;
         bool SaveImage() override;
         bool SaveImage() override;
 
 
-        bool GetSaveLocation(AZ::IO::Path& fullPath, AZStd::string& relativePath);
+        bool GetSaveLocation(AZ::IO::Path& fullPath, AZStd::string& relativePath, ImageGradientAutoSaveMode autoSaveMode);
         void CreateImage();
         void CreateImage();
         bool SaveImageInternal(
         bool SaveImageInternal(
             AZ::IO::Path& fullPath, AZStd::string& relativePath,
             AZ::IO::Path& fullPath, AZStd::string& relativePath,
@@ -88,6 +94,7 @@ namespace GradientSignal
 
 
         AZ::u32 RefreshCreationSelectionChoice();
         AZ::u32 RefreshCreationSelectionChoice();
         bool GetImageCreationVisibility() const;
         bool GetImageCreationVisibility() const;
+        AZ::Crc32 GetAutoSaveVisibility() const;
         AZ::Crc32 GetImageOptionsVisibility() const;
         AZ::Crc32 GetImageOptionsVisibility() const;
         AZ::Crc32 GetPaintModeVisibility() const;
         AZ::Crc32 GetPaintModeVisibility() const;
         bool GetImageOptionsReadOnly() const;
         bool GetImageOptionsReadOnly() const;
@@ -95,18 +102,26 @@ namespace GradientSignal
         bool RefreshImageAssetStatus();
         bool RefreshImageAssetStatus();
         static bool ImageHasPendingJobs(const AZ::Data::AssetId& assetId);
         static bool ImageHasPendingJobs(const AZ::Data::AssetId& assetId);
 
 
+        void RefreshComponentModeStatus();
+        void EnableComponentMode();
+        void DisableComponentMode();
+
         bool InComponentMode() const;
         bool InComponentMode() const;
 
 
+        AZStd::string GetImageSourcePath(const AZ::Data::AssetId& imageAssetId) const;
+        AZ::IO::Path GetIncrementingAutoSavePath(const AZ::IO::Path& currentPath) const;
+
         ImageCreationOrSelection m_creationSelectionChoice = ImageCreationOrSelection::UseExistingImage;
         ImageCreationOrSelection m_creationSelectionChoice = ImageCreationOrSelection::UseExistingImage;
 
 
         // Parameters used for creating new source image assets
         // Parameters used for creating new source image assets
         AZ::Vector2 m_outputResolution = AZ::Vector2(512.0f);
         AZ::Vector2 m_outputResolution = AZ::Vector2(512.0f);
         OutputFormat m_outputFormat = OutputFormat::R32;
         OutputFormat m_outputFormat = OutputFormat::R32;
-        AZ::IO::Path m_outputImagePath;
+        ImageGradientAutoSaveMode m_autoSaveMode = ImageGradientAutoSaveMode::AutoSave;
 
 
         // Keep track of the image asset status so that we can know when it has changed.
         // Keep track of the image asset status so that we can know when it has changed.
         AZ::Data::AssetData::AssetStatus m_currentImageAssetStatus = AZ::Data::AssetData::AssetStatus::NotLoaded;
         AZ::Data::AssetData::AssetStatus m_currentImageAssetStatus = AZ::Data::AssetData::AssetStatus::NotLoaded;
         bool m_currentImageJobsPending = false;
         bool m_currentImageJobsPending = false;
+        bool m_waitingForImageReload = false;
 
 
         AZ::u32 ConfigurationChanged();
         AZ::u32 ConfigurationChanged();
 
 
@@ -124,5 +139,18 @@ namespace GradientSignal
         bool m_visible = true;
         bool m_visible = true;
         bool m_runtimeComponentActive = false;
         bool m_runtimeComponentActive = false;
 
 
+        //! Track whether or not we've prompted the user for an image save location at least once since this component was created.
+        //! This is intentionally not serialized so that every user is prompted at least once per editor run for autosaves. This choice
+        //! prioritizes data safety over lower friction - it's too easy for autosave to overwrite data accidentally, so we want the user
+        //! to specifically choose a save location at least once before overwriting without prompts.
+        //! We could serialize the flag so that the user only selects a location once per component, instead of once per component per
+        //! Editor run, but that serialized flag would be shared with other users, so we would have other users editing the same image
+        //! that never get prompted and might overwrite data by mistake.
+        bool m_promptedForSaveLocation = false;
     };
     };
 }
 }
+
+namespace AZ
+{
+    AZ_TYPE_INFO_SPECIALIZE(GradientSignal::ImageGradientAutoSaveMode, "{55149135-E4F5-4D43-BB05-03A898BD9EEB}");
+}

+ 7 - 0
Gems/GradientSignal/Code/Source/Editor/EditorImageGradientComponentMode.cpp

@@ -483,6 +483,13 @@ namespace GradientSignal
         // for easier and faster undo/redo operations.
         // for easier and faster undo/redo operations.
         for (size_t index = 0; index < pixelIndices.size(); index++)
         for (size_t index = 0; index < pixelIndices.size(); index++)
         {
         {
+            // If we have an invalid pixel index, fill in a placeholder value into paintedValues and move on to the next pixel.
+            if ((pixelIndices[index].first < 0) || (pixelIndices[index].second < 0))
+            {
+                paintedValues.emplace_back(0.0f);
+                continue;
+            }
+
             auto [gradientValue, opacityValue] = m_paintStrokeData.m_strokeBuffer->GetOriginalPixelValueAndOpacity(pixelIndices[index]);
             auto [gradientValue, opacityValue] = m_paintStrokeData.m_strokeBuffer->GetOriginalPixelValueAndOpacity(pixelIndices[index]);
 
 
             // Add the new per-pixel opacity to the existing opacity in our stroke layer.
             // Add the new per-pixel opacity to the existing opacity in our stroke layer.