2
0
Эх сурвалжийг харах

Add configurable SDF/URDF path resolution settings, including prefix remappings (#505)

* First version of adding configurable prefix lookups and substitutions.

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

* Update Gems/ROS2/Code/Tests/UrdfParserTest.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Improved asset path resolution logic and unit tests.

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

* Add SDFormat importer hooks (#453)

Add sensors SDF importer hooks

Signed-off-by: Jan Hanca <[email protected]>

* use PrimitiveAssets Gem to visualize primitives (#485)

Use PrimitiveAssets Gem to visualize primitives

---------

Signed-off-by: Jan Hanca <[email protected]>

* Warehouse automation gem (#440)

Created the warehouse automation gem, adjusted to review

---------

Signed-off-by: Antoni Puch <[email protected]>

* Lidar component refactor (#463)

Lidar Refactor

---------

Signed-off-by: Antoni Puch <[email protected]>
Signed-off-by: Antoni-Robotec <[email protected]>
Co-authored-by: Adam Dąbrowski <[email protected]>

* Added presentation of SDF messages (#491)

* Added presentation of SDF messages

---------
Signed-off-by: Michał Pełka <[email protected]>
Co-authored-by: Jan Hanca <[email protected]>

* Fix missing include.

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

* Fix resolution of file:// prefix.

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

* Change string to Path.

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

* Changed PathResolver to get read in wholesale from setreg file.

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

* PR feedback - changed file select dir to default to last-used file.
Also changes file select dir to match anything typed into the text box.

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

* Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

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

* PR feedback

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

* Partial revert from path back to string.

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

* Second half of path to string revert.
Signed-off-by: Mike Balfour <[email protected]>

* Added all Path Resolver options to setreg for visibility.
Improved SetFindCallback to use the full set of path resolution options.
Signed-off-by: Mike Balfour <[email protected]>

---------

Signed-off-by: Mike Balfour <[email protected]>
Signed-off-by: Jan Hanca <[email protected]>
Signed-off-by: Antoni Puch <[email protected]>
Signed-off-by: Antoni-Robotec <[email protected]>
Co-authored-by: lumberyard-employee-dm <[email protected]>
Co-authored-by: Jan Hanca <[email protected]>
Co-authored-by: Antoni-Robotec <[email protected]>
Co-authored-by: Adam Dąbrowski <[email protected]>
Co-authored-by: Michał Pełka <[email protected]>
Mike Balfour 2 жил өмнө
parent
commit
0159f91f11

+ 40 - 3
Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.cpp

@@ -14,16 +14,30 @@
 #include <QFileInfo>
 #include <QHBoxLayout>
 #include <QVBoxLayout>
+#include <QSettings>
 
 namespace ROS2
 {
+    static constexpr const char FileSelectionPageDefaultFile[] = "RobotImporter/SelectFileDefaultFile";
+
     FileSelectionPage::FileSelectionPage(QWizard* parent)
         : QWizardPage(parent)
         , m_sdfAssetBuilderSettings(AZStd::make_unique<SdfAssetBuilderSettings>())
     {
         m_fileDialog = new QFileDialog(this);
-        m_fileDialog->setDirectory(QString::fromUtf8(AZ::Utils::GetProjectPath().data()));
         m_fileDialog->setNameFilter("URDF, XACRO, SDF, WORLD (*.urdf *.xacro *.sdf *.world)");
+        // Whenever the selected file is successfully changed via the File Dialog or the Text Edit widget,
+        // save the full file name with path into the QSettings so that it defaults correctly the next time it is opened.
+        connect(this, &QWizardPage::completeChanged, [this]()
+        {
+            if (m_fileExists)
+            {
+                QSettings settings;
+                const QString absolutePath = m_textEdit->text();
+                settings.setValue(FileSelectionPageDefaultFile, absolutePath);
+            }
+        });
+
         m_button = new QPushButton("...", this);
         m_textEdit = new QLineEdit("", this);
         setTitle(tr("Load URDF/SDF file"));
@@ -46,8 +60,6 @@ namespace ROS2
         m_sdfAssetBuilderSettingsEditor->Setup(serializeContext, nullptr, enableScrollBars);
         m_sdfAssetBuilderSettingsEditor->AddInstance(m_sdfAssetBuilderSettings.get());
         m_sdfAssetBuilderSettingsEditor->InvalidateAll();
-        // Make sure the SDF Asset Builder settings are expanded by default
-        m_sdfAssetBuilderSettingsEditor->ExpandAll();
         layout->addWidget(m_sdfAssetBuilderSettingsEditor);
 
         this->setLayout(layout);
@@ -59,8 +71,33 @@ namespace ROS2
 
     FileSelectionPage::~FileSelectionPage() = default;
 
+    void FileSelectionPage::RefreshDefaultPath()
+    {
+        // The first time this dialog ever gets opened, default to the project's root directory.
+        // Once a URDF/SDF file has been selected or typed in, change the default directory to the location of that file.
+        // This gets stored in QSettings, so it will persist between Editor runs.
+        QSettings settings;
+        QString defaultFile(settings.value(FileSelectionPageDefaultFile).toString());
+        if (!defaultFile.isEmpty() && QFile(defaultFile).exists())
+        {
+            // Set both the default directory and the default file in that directory.
+            m_fileDialog->setDirectory(QFileInfo(defaultFile).absolutePath());
+            m_fileDialog->selectFile(QFileInfo(defaultFile).fileName());
+        }
+        else
+        {
+            // No valid file was found, so default back to the current project path.
+            m_fileDialog->setDirectory(QString::fromUtf8(AZ::Utils::GetProjectPath().c_str()));
+            m_fileDialog->selectFile("");
+        }
+    }
+
     void FileSelectionPage::onLoadButtonPressed()
     {
+        // Refresh the default path in the file dialog every time it is opened so that
+        // any changes in the text edit box are reflected in its default path and any Cancel
+        // pressed to escape from the file dialog *don't* change its default path.
+        RefreshDefaultPath();
         m_fileDialog->show();
     }
 

+ 4 - 0
Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.h

@@ -61,6 +61,10 @@ namespace ROS2
 
         void onEditingFinished();
 
+        //! Refresh the default path in the file dialog based either on what was previously selected
+        //! or what was entered in on the text edit line.
+        void RefreshDefaultPath();
+
         bool m_fileExists{ false };
     };
 } // namespace ROS2

+ 3 - 4
Gems/ROS2/Code/Source/RobotImporter/ROS2RobotImporterEditorSystemComponent.cpp

@@ -101,9 +101,8 @@ namespace ROS2
         // Read the SDF Settings from the Settings Registry into a local struct
         SdfAssetBuilderSettings sdfBuilderSettings;
         sdfBuilderSettings.LoadSettings();
-        // Set the parser config settings for SDF content
-        sdf::ParserConfig parserConfig;
-        parserConfig.URDFSetPreserveFixedJoint(sdfBuilderSettings.m_urdfPreserveFixedJoints);
+        // Set the parser config settings for URDF content
+        sdf::ParserConfig parserConfig = Utils::SDFormat::CreateSdfParserConfigFromSettings(sdfBuilderSettings, filePath);
 
         auto parsedSdfOutcome = UrdfParser::ParseFromFile(filePath, parserConfig, sdfBuilderSettings);
         if (!parsedSdfOutcome)
@@ -124,7 +123,7 @@ namespace ROS2
         if (importAssetWithUrdf)
         {
             urdfAssetsMapping = AZStd::make_shared<Utils::UrdfAssetMap>(
-                Utils::CopyAssetForURDFAndCreateAssetMap(meshNames, filePath, collidersNames, visualNames));
+                Utils::CopyAssetForURDFAndCreateAssetMap(meshNames, filePath, collidersNames, visualNames, sdfBuilderSettings));
         }
         bool allAssetProcessed = false;
         bool assetProcessorFailed = false;

+ 6 - 4
Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp

@@ -90,8 +90,7 @@ namespace ROS2
             const SdfAssetBuilderSettings& sdfBuilderSettings = m_fileSelectPage->GetSdfAssetBuilderSettings();
 
             // Set the parser config settings for URDF content
-            sdf::ParserConfig parserConfig;
-            parserConfig.URDFSetPreserveFixedJoint(sdfBuilderSettings.m_urdfPreserveFixedJoints);
+            sdf::ParserConfig parserConfig = Utils::SDFormat::CreateSdfParserConfigFromSettings(sdfBuilderSettings, m_urdfPath);
 
             if (Utils::IsFileXacro(m_urdfPath))
             {
@@ -240,14 +239,17 @@ namespace ROS2
                 dirSuffix = paramsUuid.ToFixedString();
             }
 
+            // Read the SDF Settings from PrefabMakerPage
+            const SdfAssetBuilderSettings& sdfBuilderSettings = m_fileSelectPage->GetSdfAssetBuilderSettings();
+
             if (m_importAssetWithUrdf)
             {
                 m_urdfAssetsMapping = AZStd::make_shared<Utils::UrdfAssetMap>(
-                    Utils::CopyAssetForURDFAndCreateAssetMap(m_meshNames, m_urdfPath.String(), collidersNames, visualNames, dirSuffix));
+                    Utils::CopyAssetForURDFAndCreateAssetMap(m_meshNames, m_urdfPath.String(), collidersNames, visualNames, sdfBuilderSettings, dirSuffix));
             }
             else
             {
-                m_urdfAssetsMapping = AZStd::make_shared<Utils::UrdfAssetMap>(Utils::FindAssetsForUrdf(m_meshNames, m_urdfPath.String()));
+                m_urdfAssetsMapping = AZStd::make_shared<Utils::UrdfAssetMap>(Utils::FindAssetsForUrdf(m_meshNames, m_urdfPath.String(), sdfBuilderSettings));
                 for (const AZStd::string& meshPath : m_meshNames)
                 {
                     if (m_urdfAssetsMapping->contains(meshPath))

+ 225 - 116
Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp

@@ -13,6 +13,7 @@
 #include <AzCore/IO/Path/Path.h>
 #include <AzCore/StringFunc/StringFunc.h>
 #include <AzCore/std/string/regex.h>
+#include <AzCore/Utils/Utils.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
 #include <RobotImporter/Utils/ErrorUtils.h>
 #include <string.h>
@@ -507,161 +508,222 @@ namespace ROS2::Utils
         return resultModel;
     }
 
-    /// Finds global path from URDF path
-    AZ::IO::Path ResolveURDFPath(
+    AZ::IO::Path ResolveAmentPrefixPath(
         AZ::IO::Path unresolvedPath,
-        const AZ::IO::PathView& urdfFilePath,
-        const AZ::IO::PathView& amentPrefixPath,
-        const FileExistsCB& fileExists)
+        AZStd::string_view amentPrefixPath,
+        const FileExistsCB& fileExistsCB)
     {
-        AZ_Printf("ResolveURDFPath", "ResolveURDFPath with %s\n", unresolvedPath.c_str());
-
-        // TODO: Query URDF prefix map from Settings Registry
         AZStd::vector<AZ::IO::Path> amentPrefixPaths;
 
-        // Split the AMENT_PREFIX_PATH into multiple paths
-        auto AmentPrefixPathVisitor = [&amentPrefixPaths](AZStd::string_view prefixPath)
+        // Parse the AMENT_PREFIX_PATH environment variable into a set of distinct paths.
+        auto AmentPrefixPathVisitor = [&amentPrefixPaths](
+            AZStd::string_view prefixPath)
         {
             amentPrefixPaths.push_back(prefixPath);
         };
         // Note this code only works on Unix platforms
         // For Windows this will not work as the drive letter has a colon in it (C:\)
-        AZ::StringFunc::TokenizeVisitor(amentPrefixPath.Native(), AmentPrefixPathVisitor, ':');
+        AZ::StringFunc::TokenizeVisitor(amentPrefixPath, AmentPrefixPathVisitor, ':');
+
+        AZ::IO::PathView strippedPath;
 
-        // Append the urdf file ancestor directories to the candidate replacement paths
-        AZStd::vector<AZ::IO::Path> urdfAncestorPaths;
-        if (!urdfFilePath.empty())
+        // The AMENT_PREFIX_PATH is only used for lookups if the URI starts with "model://" or "package://"
+        constexpr AZStd::string_view ValidAmentPrefixes[] = {"model://", "package://"};
+        for (const auto& prefix : ValidAmentPrefixes)
         {
-            AZ::IO::Path urdfFileAncestorPath = urdfFilePath;
-            bool rootPathVisited = false;
-            do
+            // Perform a case-sensitive check to look for the prefix.
+            constexpr bool prefixMatchCaseSensitive = true;
+            if (AZ::StringFunc::StartsWith(unresolvedPath.Native(), prefix, prefixMatchCaseSensitive))
             {
-                AZ::IO::PathView parentPath = urdfFileAncestorPath.ParentPath();
-                rootPathVisited = (urdfFileAncestorPath == parentPath);
-                urdfAncestorPaths.emplace_back(parentPath);
-                urdfFileAncestorPath = parentPath;
-            } while (!rootPathVisited);
+                strippedPath = AZ::IO::PathView(unresolvedPath).Native().substr(prefix.size());
+                break;
+            }
         }
 
-        // Structure which accepts a callback that can convert an unresolved URI path(package://, model://, file://, etc...)
-        // to a filesystem path
-        struct UriPrefix
+        // If no valid prefix was found, or if the URI *only* contains the prefix, return an empty result.
+        if (strippedPath.empty())
         {
-            using SchemeResolver = AZStd::function<AZStd::optional<AZ::IO::Path>(AZ::IO::PathView)>;
-            SchemeResolver m_schemeResolver;
-        };
+            return {};
+        }
+
+        // If the remaining path is an absolute path, it shouldn't get resolved with AMENT_PREFIX_PATH. return an empty result.
+        if (strippedPath.IsAbsolute())
+        {
+            return {};
+        }
+
+        // Check to see if the relative part of the URI path refers to a location
+        // within each <ament prefix path>/share directory
+        for (const AZ::IO::Path& amentPrefixPath : amentPrefixPaths)
+        {
+            auto pathIter = strippedPath.begin();
+            AZ::IO::PathView packageName = *pathIter;
+            const AZ::IO::Path amentSharePath = amentPrefixPath / "share";
+            const AZ::IO::Path packageManifestPath = amentSharePath / packageName / "package.xml";
+
+            // Given a path like 'ambulance/meshes/model.stl', it will be considered a match if
+            // <ament prefix path>/share/ambulance/package.xml exists and 
+            // <ament prefix path>/share/ambulance/meshes/model.stl exists.
+            if (const AZ::IO::Path candidateResolvedPath = amentSharePath / strippedPath;
+                fileExistsCB(packageManifestPath) && fileExistsCB(candidateResolvedPath))
+            {
+                AZ_Trace("ResolveAssetPath", R"(Resolved using AMENT_PREFIX_PATH: "%.*s" -> "%.*s")" "\n", 
+                    AZ_PATH_ARG(unresolvedPath), AZ_PATH_ARG(candidateResolvedPath));
+                return candidateResolvedPath;
+            }
+        }
+
+        // No resolution was found, return an empty result.
+        return {};
+    }
+
+    /// Finds global path from URDF/SDF path
+    AZ::IO::Path ResolveAssetPath(
+        AZ::IO::Path unresolvedPath,
+        const AZ::IO::PathView& baseFilePath,
+        AZStd::string_view amentPrefixPath,
+        const SdfAssetBuilderSettings& settings,
+        const FileExistsCB& fileExistsCB)
+    {
+        AZ_Printf("ResolveAssetPath", "ResolveAssetPath with %s\n", unresolvedPath.c_str());
+
+        const auto& pathResolverSettings = settings.m_resolverSettings;
 
-        auto GetReplacementSchemeResolver = [](AZStd::string_view schemePrefix,
-                                               AZStd::span<const AZ::IO::Path> amentPrefixPaths,
-                                               AZStd::span<const AZ::IO::Path> urdfAncestorPaths,
-                                               const FileExistsCB& fileExistsCB)
+        // If the settings tell us to try the AMENT_PREFIX_PATH, use that first to try and resolve path.
+        if (pathResolverSettings.m_useAmentPrefixPath)
         {
-            return [schemePrefix, amentPrefixPaths, urdfAncestorPaths, &fileExistsCB](
-                       AZ::IO::PathView uriPath) -> AZStd::optional<AZ::IO::Path>
+            if (AZ::IO::Path amentResolvedPath = ResolveAmentPrefixPath(unresolvedPath, amentPrefixPath, fileExistsCB); !amentResolvedPath.empty())
             {
-                // Note this is a case-sensitive check to match the exact URI scheme
-                // If that is not desired, then this code should be updated to read
-                // a value from the Setting Registry indicating whether the uriPrefix matching
-                // should be case sensitive
-                bool uriPrefixMatchCaseSensitive = true;
-                // Check if the path starts with the URI scheme prefix
-                if (AZ::StringFunc::StartsWith(uriPath.Native(), schemePrefix, uriPrefixMatchCaseSensitive))
+                return amentResolvedPath;
+            }
+        }
+
+        // Append all ancestor directories from the root file to the candidate replacement paths if the settings enable using
+        // them for path resolution and the root file isn't empty.
+        AZStd::vector<AZ::IO::Path> ancestorPaths;
+        if (!baseFilePath.empty() && pathResolverSettings.m_useAncestorPaths)
+        {
+            // The first time through this loop, fileAncestorPath contains the full file name ('/a/b/c.sdf') so
+            // ParentPath() will return the path containing the file ('/a/b'). Each iteration will walk up the path
+            // to the root, including the root, before stopping ('/a', '/').
+            AZ::IO::Path fileAncestorPath = baseFilePath;
+            do
+            {
+                fileAncestorPath = fileAncestorPath.ParentPath();
+                ancestorPaths.emplace_back(fileAncestorPath);
+            } while (fileAncestorPath != fileAncestorPath.RootPath());
+        }
+
+        // Loop through each prefix in the builder settings and attempt to resolve it as either an absolute path
+        // or a relative path that's relative in some way to the base file.
+        for (const auto& [prefix, replacements] : pathResolverSettings.m_uriPrefixMap)
+        {
+            // Note this is a case-sensitive check to match the exact URI scheme
+            // If that is not desired, then this code should be updated to read
+            // a value from the Setting Registry indicating whether the uriPrefix matching
+            // should be case sensitive
+            constexpr bool uriPrefixMatchCaseSensitive = true;
+            // If the path doesn't start with the given prefix, move on to the next prefix
+            if (!AZ::StringFunc::StartsWith(unresolvedPath.Native(), prefix, uriPrefixMatchCaseSensitive))
+            {
+                continue;
+            }
+
+            // Strip the number of characters from the Uri scheme from beginning of the path
+            AZ::IO::PathView strippedUriPath = AZ::IO::PathView(unresolvedPath).Native().substr(prefix.size());
+
+            // Loop through each replacement path for this prefix, attach it to the front, and look for matches.
+            for (const auto& replacement : replacements)
+            {
+                AZ::IO::Path replacedUriPath(replacement);
+                replacedUriPath /= strippedUriPath;
+
+                // If we successfully matched the prefix, and the replacement path is completely empty, we don't need to look any further.
+                // There's no match.
+                if (replacedUriPath.empty())
                 {
-                    // Strip the number of characters from the Uri scheme from beginning of the path
-                    AZ::IO::PathView strippedUriPath = uriPath.Native().substr(schemePrefix.size());
-                    if (strippedUriPath.empty())
+                    AZ_Trace("ResolveAssetPath", R"(Resolved Path is empty: "%.*s" -> "")" "\n", AZ_PATH_ARG(unresolvedPath));
+                    return {};
+                }
+
+                // If the replaced path is an absolute path, if it exists, return it.
+                // If it doesn't exist, keep trying other replacements.
+                if (replacedUriPath.IsAbsolute())
+                {
+                    if (fileExistsCB(replacedUriPath))
                     {
-                        // The stripped URI path is empty, so there is nothing to resolve
-                        return AZStd::nullopt;
+                        AZ_Trace("ResolveAssetPath", R"(Resolved Absolute Path: "%.*s" -> "%.*s")" "\n", 
+                            AZ_PATH_ARG(unresolvedPath), AZ_PATH_ARG(replacedUriPath));
+                        return replacedUriPath;
                     }
-
-                    // Check to see if the relative part of the URI path refers to a location
-                    // within each <ament prefix path>/share directory
-                    for (const AZ::IO::Path& amentPrefixPath : amentPrefixPaths)
+                    else
                     {
-                        auto pathIter = strippedUriPath.begin();
-                        AZ::IO::PathView packageName = *pathIter;
-                        const AZ::IO::Path amentSharePath = amentPrefixPath / "share";
-                        const AZ::IO::Path packageManifestPath = amentSharePath / packageName / "package.xml";
-
-                        if (const AZ::IO::Path candidateResolvedPath = amentSharePath / strippedUriPath;
-                            fileExistsCB(packageManifestPath) && fileExistsCB(candidateResolvedPath))
-                        {
-                            return candidateResolvedPath;
-                        }
+                        // The file didn't exist, so continue.
+                        continue;
                     }
+                }
 
-                    // The URI path cannot be resolved within the any ament prefix path,
-                    // so try the directory containing the URDF file as well as any of its parent directories
-                    for (const AZ::IO::Path& urdfAncestorPath : urdfAncestorPaths)
+                // The URI path is not absolute, so attempt to append it to the ancestor directories of the URDF/SDF file
+                for (const AZ::IO::Path& ancestorPath : ancestorPaths)
+                {
+                    if (const AZ::IO::Path candidateResolvedPath = ancestorPath / replacedUriPath;
+                        fileExistsCB(candidateResolvedPath))
                     {
-                        if (const AZ::IO::Path candidateResolvedPath = urdfAncestorPath / strippedUriPath;
-                            fileExistsCB(candidateResolvedPath))
-                        {
-                            return candidateResolvedPath;
-                        }
+                        AZ_Trace("ResolveAssetPath", R"(Resolved using ancestor paths: "%.*s" -> "%.*s")" "\n", 
+                            AZ_PATH_ARG(unresolvedPath), AZ_PATH_ARG(candidateResolvedPath));
+                        return candidateResolvedPath;
                     }
                 }
+            }
+        }
 
-                return AZStd::nullopt;
-            };
-        };
-
-        constexpr AZStd::string_view PackageSchemePrefix = "package://";
-        UriPrefix packageUriPrefix;
-        packageUriPrefix.m_schemeResolver =
-            GetReplacementSchemeResolver(PackageSchemePrefix, amentPrefixPaths, urdfAncestorPaths, fileExists);
-
-        constexpr AZStd::string_view ModelSchemePrefix = "model://";
-        UriPrefix modelUriPrefix;
-        modelUriPrefix.m_schemeResolver = GetReplacementSchemeResolver(ModelSchemePrefix, amentPrefixPaths, urdfAncestorPaths, fileExists);
 
-        // For a local file path convert the file URI to a local path
-        UriPrefix fileUriPrefix;
-        fileUriPrefix.m_schemeResolver = [](AZ::IO::PathView uriPath) -> AZStd::optional<AZ::IO::Path>
+        // At this point, the path has no identified URI prefix. If it's an absolute path, try to locate and return it.
+        // Otherwise, return an empty path as an error.
+        if (unresolvedPath.IsAbsolute())
         {
-            constexpr AZStd::string_view FileSchemePrefix = "file://";
-            // Paths that start with 'file:///' are absolute paths, so only 'file://' needs to be stripped
-            bool uriPrefixMatchCaseSensitive = true;
-            if (AZ::StringFunc::StartsWith(uriPath.Native(), FileSchemePrefix, uriPrefixMatchCaseSensitive))
+            if (fileExistsCB(unresolvedPath))
             {
-                AZStd::string_view strippedUriPath = uriPath.Native().substr(FileSchemePrefix.size());
-                return AZ::IO::Path(strippedUriPath);
+                AZ_Trace("ResolveAssetPath", R"(Resolved Absolute Path: "%.*s")" "\n", 
+                    AZ_PATH_ARG(unresolvedPath));
+                return unresolvedPath;
             }
-
-            return AZStd::nullopt;
-        };
-
-        // Step 1: Attempt to resolved URI scheme paths
-        // libsdformat seems to convert package:// references to model:// references
-        // So the model:// URI prefix resolver is run first
-        const auto uriPrefixes =
-            AZStd::to_array<UriPrefix>({ AZStd::move(modelUriPrefix), AZStd::move(fileUriPrefix), AZStd::move(packageUriPrefix) });
-        for (const UriPrefix& uriPrefix : uriPrefixes)
-        {
-            if (auto resolvedPath = uriPrefix.m_schemeResolver(unresolvedPath); resolvedPath.has_value())
+            else
             {
-                AZ_Printf(
-                    "ResolveURDFPath",
-                    R"(Resolved Path using URI Prefix "%.*s" -> "%.*s")"
-                    "\n",
-                    AZ_PATH_ARG(unresolvedPath),
-                    AZ_PATH_ARG(resolvedPath.value()));
-                return resolvedPath.value();
+                AZ_Trace("ResolveAssetPath", R"(Failed to resolve Absolute Path: "%.*s")" "\n", 
+                    AZ_PATH_ARG(unresolvedPath));
+                return {};
             }
         }
 
-        // At this point, the path has no URI scheme
-        if (unresolvedPath.IsAbsolute())
+        // The path is a relative path, so use the directory containing the base URDF/SDF file as the root path,
+        // and if the file can be found successfully, return the path. Otherwise, return an empty path as an error.
+        const AZ::IO::Path relativePath = AZ::IO::Path(baseFilePath.ParentPath()) / unresolvedPath;
+
+        if (fileExistsCB(relativePath))
         {
-            AZ_Printf("ResolveURDFPath", "Input Path is an absolute local filesystem path to : %s\n", unresolvedPath.c_str());
-            return unresolvedPath;
+            AZ_Trace("ResolveAssetPath", R"(Resolved Relative Path: "%.*s" -> "%.*s")" "\n", 
+                AZ_PATH_ARG(unresolvedPath), AZ_PATH_ARG(relativePath));
+            return relativePath;
         }
 
-        // The path is relative path, so append to the directory containing the .urdf file
-        const AZ::IO::Path resolvedPath = AZ::IO::Path(urdfFilePath.ParentPath()) / unresolvedPath;
-        AZ_Printf("ResolveURDFPath", "Input Path %s is being returned as is\n", unresolvedPath.c_str());
-        return resolvedPath;
+        AZ_Trace("ResolveAssetPath", R"(Failed to resolve Relative Path: "%.*s" -> "%.*s")" "\n", 
+            AZ_PATH_ARG(unresolvedPath), AZ_PATH_ARG(relativePath));
+        return {};
+    }
+    AmentPrefixString GetAmentPrefixPath()
+    {
+        // Support reading the AMENT_PREFIX_PATH environment variable on Unix/Windows platforms
+        auto StoreAmentPrefixPath = [](char* buffer, size_t size) -> size_t
+        {
+            auto getEnvOutcome = AZ::Utils::GetEnv(AZStd::span(buffer, size), "AMENT_PREFIX_PATH");
+            return getEnvOutcome ? getEnvOutcome.GetValue().size() : 0;
+        };
+        AmentPrefixString amentPrefixPath;
+        amentPrefixPath.resize_and_overwrite(amentPrefixPath.capacity(), StoreAmentPrefixPath);
+        AZ_Error("UrdfAssetMap", !amentPrefixPath.empty(), "AMENT_PREFIX_PATH is not found.");
+
+        return amentPrefixPath;
     }
 
 } // namespace ROS2::Utils
@@ -715,4 +777,51 @@ namespace ROS2::Utils::SDFormat
     {
         return supportedPlugins.contains(GetPluginFilename(plugin));
     }
+
+    sdf::ParserConfig CreateSdfParserConfigFromSettings(const SdfAssetBuilderSettings& settings, const AZ::IO::PathView& baseFilePath)
+    {
+        sdf::ParserConfig sdfConfig;
+
+        sdfConfig.URDFSetPreserveFixedJoint(settings.m_urdfPreserveFixedJoints);
+
+        // Fill in the URI resolution with the supplied prefix mappings.
+        for (auto& [prefix, pathList] : settings.m_resolverSettings.m_uriPrefixMap)
+        {
+            std::string uriPath;
+            for(auto& path : pathList)
+            {
+                if (!uriPath.empty())
+                {
+                    uriPath.append(std::string(":"));
+                }
+
+                uriPath.append(std::string(path.c_str(), path.size()));
+            }
+            if (!prefix.empty() && !uriPath.empty())
+            {
+                std::string uriPrefix(prefix.c_str(), prefix.size());
+                sdfConfig.AddURIPath(uriPrefix, uriPath);
+                AZ_Trace("SdfParserConfig", "Added URI mapping '%s' -> '%s'", uriPrefix.c_str(), uriPath.c_str());
+            }
+        }
+
+        // If any files couldn't be found using our supplied prefix mappings, this callback will get called.
+        // Attempt to use our full path resolution, and print a warning if it still couldn't be resolved.
+        sdfConfig.SetFindCallback([settings, baseFilePath](const std::string &fileName) -> std::string
+        {
+            auto amentPrefixPath = Utils::GetAmentPrefixPath();
+
+            auto resolved = Utils::ResolveAssetPath(AZ::IO::Path(fileName.c_str()), baseFilePath, amentPrefixPath, settings);
+            if (!resolved.empty())
+            {
+                AZ_Trace("SdfParserConfig", "SDF SetFindCallback resolved '%s' -> '%s'", fileName.c_str(), resolved.c_str());
+                return resolved.c_str();
+            }
+
+            AZ_Warning("SdfParserConfig", false, "SDF SetFindCallback failed to resolve '%s'", fileName.c_str());
+            return fileName;
+        });
+
+        return sdfConfig;
+    }
 } // namespace ROS2::Utils::SDFormat

+ 19 - 7
Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h

@@ -16,6 +16,7 @@
 #include <AzCore/std/function/function_template.h>
 #include <AzCore/std/string/string.h>
 #include <RobotImporter/URDF/UrdfParser.h>
+#include <SdfAssetBuilder/SdfAssetBuilderSettings.h>
 
 #include <sdf/sdf.hh>
 
@@ -147,18 +148,22 @@ namespace ROS2::Utils
     //! @return true should be returned if the file exist otherwise false
     using FileExistsCB = AZStd::function<bool(const AZ::IO::PathView&)>;
 
-    //! Resolves path from unresolved URDF path.
-    //! @param unresolvedPath - unresolved URDF path, example : `package://meshes/foo.dae`.
-    //! @param urdfFilePath - the absolute path of URDF file which contains the path that is to be resolved.
+    //! Resolves path for an asset referenced in a URDF/SDF file.
+    //! @param unresolvedPath - unresolved URDF/SDF path, example : `model://meshes/foo.dae`.
+    //! @param baseFilePath - the absolute path of URDF/SDF file which contains the path that is to be resolved.
     //! @param amentPrefixPath - the string that contains available packages' path, separated by ':' signs.
+    //! @param settings - the asset path resolution settings to use for attempting to locate the correct files
     //! @param fileExists - functor to check if the given file exists. Exposed for unit test, default one should be used.
-    //! @returns resolved path to the referenced file within the URDF
-    AZ::IO::Path ResolveURDFPath(
+    //! @returns resolved path to the referenced file within the URDF/SDF, or the passed-in path if no resolution was possible.
+    AZ::IO::Path ResolveAssetPath(
         AZ::IO::Path unresolvedPath,
-        const AZ::IO::PathView& urdfFilePath,
-        const AZ::IO::PathView& amentPrefixPath,
+        const AZ::IO::PathView& baseFilePath,
+        AZStd::string_view amentPrefixPath,
+        const SdfAssetBuilderSettings& settings,
         const FileExistsCB& fileExists = &Internal::FileExistsCall);
 
+    using AmentPrefixString = AZStd::fixed_string<4096>;
+    AmentPrefixString GetAmentPrefixPath();
 } // namespace ROS2::Utils
 
 namespace ROS2::Utils::SDFormat
@@ -181,4 +186,11 @@ namespace ROS2::Utils::SDFormat
     //! @param supportedPlugins set of predefined plugins that are supported
     //! @returns true if plugin is supported
     bool IsPluginSupported(const sdf::Plugin& plugin, const AZStd::unordered_set<AZStd::string>& supportedPlugins);
+
+    //! Given a set of SdfAssetBuilderSettings, produce an sdf::ParserConfig that can be used by the sdformat library.
+    //! @param settings The input settings to use
+    //! @param baseFilePath The base file getting parsed, which is used to help resolve file paths
+    //! @return The output parser config to use with sdformat.
+    sdf::ParserConfig CreateSdfParserConfigFromSettings(const SdfAssetBuilderSettings& settings, const AZ::IO::PathView& baseFilePath);
+
 } // namespace ROS2::Utils::SDFormat

+ 12 - 21
Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.cpp

@@ -273,12 +273,10 @@ namespace ROS2::Utils
         const AZStd::string& urdfFilename,
         const AZStd::unordered_set<AZStd::string>& colliders,
         const AZStd::unordered_set<AZStd::string>& visuals,
+        const SdfAssetBuilderSettings& sdfBuilderSettings,
         AZStd::string_view outputDirSuffix,
         AZ::IO::FileIOBase* fileIO)
     {
-        auto enviromentalVariable = std::getenv("AMENT_PREFIX_PATH");
-        AZ_Error("UrdfAssetMap", enviromentalVariable, "AMENT_PREFIX_PATH is not found.");
-
         UrdfAssetMap urdfAssetMap;
         if (meshesFilenames.empty())
         {
@@ -320,16 +318,16 @@ namespace ROS2::Utils
             }
             return urdfAssetMap;
         }
-        AZStd::string amentPrefixPath{ enviromentalVariable };
+        auto amentPrefixPath = Utils::GetAmentPrefixPath();
         AZStd::set<AZStd::string> files;
 
         for (const auto& unresolvedUrfFileName : meshesFilenames)
         {
-            auto resolved =
-                Utils::ResolveURDFPath(unresolvedUrfFileName, AZ::IO::PathView(urdfFilename), AZ::IO::PathView(amentPrefixPath));
+           auto resolved = Utils::ResolveAssetPath(unresolvedUrfFileName, AZ::IO::PathView(urdfFilename),
+                amentPrefixPath, sdfBuilderSettings);
             if (resolved.empty())
             {
-                AZ_Warning("CopyAssetForURDF", false, "There is not resolved path for %s", unresolvedUrfFileName.c_str());
+                AZ_Warning("CopyAssetForURDF", false, "There is no resolved path for %s", unresolvedUrfFileName.c_str());
                 continue;
             }
 
@@ -397,8 +395,8 @@ namespace ROS2::Utils
 
             Utils::UrdfAsset asset;
             asset.m_urdfPath = urdfFilename;
-            asset.m_resolvedUrdfPath =
-                Utils::ResolveURDFPath(unresolvedUrfFileName, AZ::IO::PathView(urdfFilename), AZ::IO::PathView(amentPrefixPath));
+            asset.m_resolvedUrdfPath = Utils::ResolveAssetPath(unresolvedUrfFileName, AZ::IO::PathView(urdfFilename),
+                amentPrefixPath, sdfBuilderSettings);
             asset.m_urdfFileCRC = AZ::Crc32();
             urdfAssetMap.emplace(unresolvedUrfFileName, AZStd::move(asset));
         }
@@ -420,25 +418,18 @@ namespace ROS2::Utils
         return urdfAssetMap;
     }
 
-    UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set<AZStd::string>& meshesFilenames, const AZStd::string& urdfFilename)
+    UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set<AZStd::string>& meshesFilenames, const AZStd::string& urdfFilename,
+        const SdfAssetBuilderSettings& sdfBuilderSettings)
     {
-        // Support reading the AMENT_PREFIX_PATH environment variable on Unix/Windows platforms
-        auto StoreAmentPrefixPath = [](char* buffer, size_t size) -> size_t
-        {
-            auto getEnvOutcome = AZ::Utils::GetEnv(AZStd::span(buffer, size), "AMENT_PREFIX_PATH");
-            return getEnvOutcome ? getEnvOutcome.GetValue().size() : 0;
-        };
-        AZStd::fixed_string<4096> amentPrefixPath;
-        amentPrefixPath.resize_and_overwrite(amentPrefixPath.capacity(), StoreAmentPrefixPath);
-        AZ_Error("UrdfAssetMap", !amentPrefixPath.empty(), "AMENT_PREFIX_PATH is not found.");
+        auto amentPrefixPath = Utils::GetAmentPrefixPath();
 
         UrdfAssetMap urdfToAsset;
         for (const auto& t : meshesFilenames)
         {
             Utils::UrdfAsset asset;
             asset.m_urdfPath = t;
-            asset.m_resolvedUrdfPath =
-                Utils::ResolveURDFPath(asset.m_urdfPath, AZ::IO::PathView(urdfFilename), AZ::IO::PathView(amentPrefixPath));
+            asset.m_resolvedUrdfPath = Utils::ResolveAssetPath(asset.m_urdfPath, AZ::IO::PathView(urdfFilename),
+                amentPrefixPath, sdfBuilderSettings);
             asset.m_urdfFileCRC = Utils::GetFileCRC(asset.m_resolvedUrdfPath);
             urdfToAsset.emplace(t, AZStd::move(asset));
         }

+ 11 - 2
Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.h

@@ -19,6 +19,11 @@
 #include <AzCore/std/containers/unordered_set.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
 
+namespace ROS2
+{
+    struct SdfAssetBuilderSettings;
+}  // namespace ROS2
+
 namespace ROS2::Utils
 {
     //! Structure contains essential information about the source and product assets in O3DE.
@@ -67,14 +72,16 @@ namespace ROS2::Utils
     //! Discover an association between meshes in URDF and O3DE source and product assets.
     //! The @param meshesFilenames contains the list of unresolved URDF filenames that are to be found as assets.
     //! Steps:
-    //! - Functions resolves URDF filenames with `ResolveURDFPath`.
+    //! - Functions resolves URDF filenames with `ResolveAssetPath`.
     //! - Files pointed by resolved URDF patches have their checksum computed `GetFileCRC`.
     //! - Function scans all available O3DE assets by calling `GetInterestingSourceAssetsCRC`.
     //! - Suitable mapping to the O3DE asset is found by comparing the checksum of the file pointed by the URDF path and source asset.
     //! @param meshesFilenames - list of the unresolved path from the URDF file
     //! @param urdfFilename - filename of URDF file, used for resolvement
+    //! @param sdfBuilderSettings - the builder settings that should be used to resolve paths
     //! @returns a URDF Asset map where the key is unresolved URDF path to AvailableAsset
-    UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set<AZStd::string>& meshesFilenames, const AZStd::string& urdfFilename);
+    UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set<AZStd::string>& meshesFilenames, const AZStd::string& urdfFilename, 
+        const SdfAssetBuilderSettings& sdfBuilderSettings);
 
     //! Helper function that gives product's path from source asset GUID
     //! @param sourceAssetUUID is source asset GUID
@@ -131,6 +138,7 @@ namespace ROS2::Utils
     //! @param urdFilename - path to URDF file (as a global path)
     //! @param colliders - files to create collider assetinfo (as unresolved urdf paths)
     //! @param visuals - files to create visual assetinfo (as unresolved urdf paths)
+    //! @param sdfBuilderSettings - the builder settings to use to convert the SDF/URDF files
     //! @param outputDirSuffix - suffix to make output directory unique, if xacro file was used
     //! @param fileIO - instance to fileIO class
     //! @returns mapping from unresolved urdf paths to source asset info
@@ -139,6 +147,7 @@ namespace ROS2::Utils
         const AZStd::string& urdfFilename,
         const AZStd::unordered_set<AZStd::string>& colliders,
         const AZStd::unordered_set<AZStd::string>& visual,
+        const SdfAssetBuilderSettings& sdfBuilderSettings,
         AZStd::string_view outputDirSuffix = "",
         AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance());
 

+ 5 - 10
Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilder.cpp

@@ -92,10 +92,7 @@ namespace ROS2
 
         using AssetSysReqBus = AzToolsFramework::AssetSystemRequestBus;
 
-        // Unlike the RobotImporter, the SDF Asset Builder does not use the AMENT_PREFIX_PATH
-        // to resolve file locations. There wouldn't be a way to guarantee identical results across
-        // machines or to detect the need to rebuild assets if the environment variable changes.
-        constexpr AZ::IO::PathView emptyAmentPrefixPath;
+        auto amentPrefixPath = Utils::GetAmentPrefixPath();
 
         for (const auto& uri : assetNames)
         {
@@ -103,8 +100,8 @@ namespace ROS2
             asset.m_urdfPath = uri;
 
             // Attempt to find the absolute path for the raw uri reference, which might look something like "model://meshes/model.dae"
-            asset.m_resolvedUrdfPath = Utils::ResolveURDFPath(asset.m_urdfPath, AZ::IO::PathView(sourceFilename),
-                emptyAmentPrefixPath);
+            asset.m_resolvedUrdfPath = Utils::ResolveAssetPath(asset.m_urdfPath, AZ::IO::PathView(sourceFilename), amentPrefixPath,
+                m_globalSettings);
             if (asset.m_resolvedUrdfPath.empty())
             {
                 AZ_Warning(SdfAssetBuilderName, false, "Failed to resolve file reference '%s' to an absolute path, skipping.", uri.c_str());
@@ -190,8 +187,7 @@ namespace ROS2
         const auto fullSourcePath = AZ::IO::Path(request.m_watchFolder) / AZ::IO::Path(request.m_sourceFile);
 
         // Set the parser config settings for parsing URDF content through the libsdformat parser
-        sdf::ParserConfig parserConfig;
-        parserConfig.URDFSetPreserveFixedJoint(m_globalSettings.m_urdfPreserveFixedJoints);
+        sdf::ParserConfig parserConfig = Utils::SDFormat::CreateSdfParserConfigFromSettings(m_globalSettings, fullSourcePath);
 
         AZ_Info(SdfAssetBuilderName, "Parsing source file: %s", fullSourcePath.c_str());
         auto parsedSdfRootOutcome = UrdfParser::ParseFromFile(fullSourcePath, parserConfig, m_globalSettings);
@@ -252,8 +248,7 @@ namespace ROS2
         tempAssetOutputPath.ReplaceExtension("procprefab");
 
         // Set the parser config settings for parsing URDF content through the libsdformat parser
-        sdf::ParserConfig parserConfig;
-        parserConfig.URDFSetPreserveFixedJoint(m_globalSettings.m_urdfPreserveFixedJoints);
+        sdf::ParserConfig parserConfig = Utils::SDFormat::CreateSdfParserConfigFromSettings(m_globalSettings, AZ::IO::PathView(request.m_sourceFile));
 
         // Read in and parse the source SDF file.
         AZ_Info(SdfAssetBuilderName, "Parsing source file: %s", request.m_fullPath.c_str());

+ 54 - 5
Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.cpp

@@ -52,18 +52,57 @@ namespace ROS2
         constexpr auto SdfAssetBuilderURDFPreserveFixedJointRegistryKey = SDFSettingsRootKey("URDFPreserveFixedJoint");
         constexpr auto SdfAssetBuilderImportMeshesJointRegistryKey = SDFSettingsRootKey("ImportMeshes");
         constexpr auto SdfAssetBuilderFixURDFRegistryKey = SDFSettingsRootKey("FixURDF");
+        constexpr auto SdfAssetBuilderAssetResolverRegistryKey = SDFSettingsRootKey("AssetResolverSettings");
+    }
+
+    void SdfAssetPathResolverSettings::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<SdfAssetPathResolverSettings>()
+                ->Version(0)
+                ->Field("UseAmentPrefixPath", &SdfAssetPathResolverSettings::m_useAmentPrefixPath)
+                ->Field("UseAncestorPaths", &SdfAssetPathResolverSettings::m_useAncestorPaths)
+                ->Field("URIPrefixMap", &SdfAssetPathResolverSettings::m_uriPrefixMap)
+             ;
+
+            if (auto editContext = serializeContext->GetEditContext(); editContext != nullptr)
+            {
+                editContext
+                    ->Class<SdfAssetPathResolverSettings>(
+                        "Asset Paths", "Exposes settings for resolving asset path references")
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &SdfAssetPathResolverSettings::m_useAmentPrefixPath,
+                        "Use AMENT_PREFIX_PATH",
+                        "Uses the AMENT_PREFIX_PATH environment variable to try and locate asset references")
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &SdfAssetPathResolverSettings::m_useAncestorPaths,
+                        "Search parent paths",
+                        "Tries to resolve partial paths by traversing parent folders to look for partial path matches")
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &SdfAssetPathResolverSettings::m_uriPrefixMap,
+                        "Prefix replacements",
+                        "Map path prefixes to specific paths (ex: 'model://' -> 'Assets/models')");
+            }
+        }
     }
 
     void SdfAssetBuilderSettings::Reflect(AZ::ReflectContext* context)
     {
+        SdfAssetPathResolverSettings::Reflect(context);
+
         if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
         {
             serializeContext->Class<SdfAssetBuilderSettings>()
-                ->Version(0)
+                ->Version(1)
                 ->Field("UseArticulations", &SdfAssetBuilderSettings::m_useArticulations)
                 ->Field("URDFPreserveFixedJoint", &SdfAssetBuilderSettings::m_urdfPreserveFixedJoints)
                 ->Field("ImportReferencedMeshFiles", &SdfAssetBuilderSettings::m_importReferencedMeshFiles)
                 ->Field("FixURDF", &SdfAssetBuilderSettings::m_fixURDF)
+                ->Field("AssetResolverSettings", &SdfAssetBuilderSettings::m_resolverSettings)
 
                 // m_builderPatterns aren't serialized because we only use the serialization
                 // to detect when global settings changes cause us to rebuild our assets.
@@ -76,12 +115,14 @@ namespace ROS2
             {
                 editContext
                     ->Class<SdfAssetBuilderSettings>(
-                        "SDF Asset Import Settings", "Exposes settings which alters importing of URDF/XACRO/SDF files")
+                        "URDF/SDF Asset Import Settings", "Exposes settings which alters importing of URDF/XACRO/SDF files.")
+                        ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                            ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default,
                         &SdfAssetBuilderSettings::m_useArticulations,
                         "Use Articulations",
-                        "Determines whether PhysX articulation components should be used for joints and rigid bodies")
+                        "Determines whether PhysX articulation components should be used for joints and rigid bodies.")
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default,
                         &SdfAssetBuilderSettings::m_urdfPreserveFixedJoints,
@@ -97,8 +138,13 @@ namespace ROS2
                         AZ::Edit::UIHandlers::Default,
                         &SdfAssetBuilderSettings::m_fixURDF,
                         "Fix URDF to be compatible with libsdformat",
-                        "When set, fixes the URDF file before importing it. This is useful for fixing URDF files that have missing inertials or duplicate names within links and joints."
-                        );
+                        "When set, fixes the URDF file before importing it. This is useful for fixing URDF files that have missing inertials or duplicate names within links and joints.")
+                    ->DataElement(
+                        AZ::Edit::UIHandlers::Default,
+                        &SdfAssetBuilderSettings::m_resolverSettings,
+                        "Path Resolvers",
+                        "Determines how to resolve any partial asset paths.")
+                        ;
             }
         }
     }
@@ -153,6 +199,9 @@ namespace ROS2
             };
         AZ::SettingsRegistryVisitorUtils::VisitArray(*settingsRegistry, VisitFileTypeExtensions, SdfAssetBuilderSupportedFileExtensionsRegistryKey);
 
+        // Get the Asset Resolver settings
+        settingsRegistry->GetObject(m_resolverSettings, SdfAssetBuilderAssetResolverRegistryKey);
+
         AZ_Warning(SdfAssetBuilderName, !m_builderPatterns.empty(), "SdfAssetBuilder disabled, no supported file type extensions found.");
     }
 } // ROS2

+ 26 - 3
Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h

@@ -8,11 +8,32 @@
 
 #pragma once
 
+#include <AzCore/std/containers/unordered_map.h>
 #include <AssetBuilderSDK/AssetBuilderSDK.h>
 #include <AzCore/Settings/SettingsRegistry.h>
 
 namespace ROS2
 {
+    struct SdfAssetPathResolverSettings
+    {
+    public:
+        AZ_RTTI(SdfAssetPathResolverSettings, "{51EDDB99-FE82-4783-9C91-7DF403AD4EFA}");
+
+        SdfAssetPathResolverSettings() = default;
+        virtual ~SdfAssetPathResolverSettings() = default;
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        using UriPrefixMap = AZStd::unordered_map<AZStd::string, AZStd::vector<AZStd::string>>;
+
+        //! When true, use the set of paths in the AMENT_PREFIX_PATH environment variable to search for files
+        bool m_useAmentPrefixPath = true;
+        //! When true, search ancestor paths all the way up to the root to search for files
+        bool m_useAncestorPaths = true; 
+        //! The map of URI prefixes to replace with paths
+        UriPrefixMap m_uriPrefixMap;
+    };
+
     struct SdfAssetBuilderSettings
     {
     public:
@@ -32,11 +53,13 @@ namespace ROS2
 
         AZStd::vector<AssetBuilderSDK::AssetBuilderPattern> m_builderPatterns;
         bool m_useArticulations = true;
-        // By default, fixed joint in URDF files that are processed by libsdformat are preserved
+        //! By default, fixed joint in URDF files that are processed by libsdformat are preserved
         bool m_urdfPreserveFixedJoints = true;
-        // When true, .dae/.stl mesh files are imported into the project folder to allow the AP to process them
+        //! When true, .dae/.stl mesh files are imported into the project folder to allow the AP to process them
         bool m_importReferencedMeshFiles = true;
-        // When true URDF will be fixed to be compatible with SDFormat.
+        //! When true URDF will be fixed to be compatible with SDFormat.
         bool m_fixURDF = true;
+
+        SdfAssetPathResolverSettings m_resolverSettings;
     };
 } // namespace ROS2

+ 1 - 0
Gems/ROS2/Code/Tests/SdfParserTest.cpp

@@ -505,4 +505,5 @@ namespace UnitTest
             EXPECT_EQ(unsupportedImuParams[0U], ">always_on");
         }
     }
+
 } // namespace UnitTest

+ 139 - 23
Gems/ROS2/Code/Tests/UrdfParserTest.cpp

@@ -14,6 +14,7 @@
 #include <RobotImporter/Utils/ErrorUtils.h>
 #include <RobotImporter/Utils/RobotImporterUtils.h>
 #include <RobotImporter/xacro/XacroUtils.h>
+#include <SdfAssetBuilder/SdfAssetBuilderSettings.h>
 
 namespace UnitTest
 {
@@ -300,6 +301,18 @@ namespace UnitTest
                    "</robot>";
             // clang-format on
         }
+
+        ROS2::SdfAssetBuilderSettings GetTestSettings()
+        {
+            ROS2::SdfAssetBuilderSettings settings;
+            settings.m_resolverSettings.m_useAmentPrefixPath = true;
+            settings.m_resolverSettings.m_useAncestorPaths = true;
+            settings.m_resolverSettings.m_uriPrefixMap.emplace("model://", AZStd::vector<AZStd::string>({"."}));
+            settings.m_resolverSettings.m_uriPrefixMap.emplace("package://", AZStd::vector<AZStd::string>({"."}));
+            settings.m_resolverSettings.m_uriPrefixMap.emplace("file://", AZStd::vector<AZStd::string>({"."}));
+
+            return settings;
+        }
     };
 
     TEST_F(UrdfParserTest, ParseUrdfWithOneLink)
@@ -846,46 +859,149 @@ namespace UnitTest
         ASSERT_TRUE(AZStd::ranges::contains(joints, "joint1", jointToNameProjection));
     }
 
-    TEST_F(UrdfParserTest, TestPathResolvementGlobal)
+    TEST_F(UrdfParserTest, TestPathResolve_ValidAbsolutePath_ResolvesCorrectly)
     {
-        constexpr AZ::IO::PathView dae = "file:///home/foo/ros_ws/install/foo_robot/meshes/bar.dae";
+        // Verify that an absolute path that wouldn't be resolved by prefixes or ancestor paths 
+        // or the AMENT_PREFIX_PATH will still resolve correctly as long as the absolute path exists
+        // (as determined by the mocked-out FileExistsCallback below).
+        constexpr AZ::IO::PathView dae = "file:///usr/ros/humble/meshes/bar.dae";
         constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
-        auto result = ROS2::Utils::ResolveURDFPath(
+        constexpr AZ::IO::PathView expectedResult = "/usr/ros/humble/meshes/bar.dae";
+        auto result = ROS2::Utils::ResolveAssetPath(
             dae,
-            urdf, "",
-            [](const AZ::IO::PathView&) -> bool
+            urdf, "", GetTestSettings(),
+            [expectedResult](const AZ::IO::PathView& p) -> bool
             {
+                // Only a file name that matches the expected result will return that it exists.
+                return p == expectedResult;
+            });
+        EXPECT_EQ(result, expectedResult);
+    }
+
+    TEST_F(UrdfParserTest, TestPathResolve_InvalidAbsolutePath_ReturnsEmptyPath)
+    {
+        // Verify that an absolute path that isn't found (as determined by the mocked-out
+        // FileExistsCallback below) returns an empty path.
+        constexpr AZ::IO::PathView dae = "file:///usr/ros/humble/meshes/bar.dae";
+        constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
+        constexpr AZ::IO::PathView expectedResult = "";
+        auto result = ROS2::Utils::ResolveAssetPath(
+            dae,
+            urdf, "", GetTestSettings(),
+            [](const AZ::IO::PathView& p) -> bool
+            {
+                // Always return "not found" for all file names.
                 return false;
             });
-        EXPECT_EQ(result, "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae");
+        EXPECT_EQ(result, expectedResult);
     }
 
-    TEST_F(UrdfParserTest, TestPathResolvementRelative)
+    TEST_F(UrdfParserTest, TestPathResolve_ValidRelativePath_ResolvesCorrectly)
     {
+        // Verify that a path that is intended to be relative to the location of the .urdf file resolves correctly.
         constexpr AZ::IO::PathView dae = "meshes/bar.dae";
         constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
-        auto result = ROS2::Utils::ResolveURDFPath(
+        constexpr AZ::IO::PathView expectedResult = "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae";
+        auto result = ROS2::Utils::ResolveAssetPath(
             dae,
-            urdf, "",
-            [](const AZ::IO::PathView&) -> bool
+            urdf, "", GetTestSettings(),
+            [expectedResult](const AZ::IO::PathView& p) -> bool
             {
+                // Only a file name that matches the expected result will return that it exists.
+                return p == expectedResult;
+            });
+        EXPECT_EQ(result, expectedResult);
+    }
+
+    TEST_F(UrdfParserTest, TestPathResolve_InvalidRelativePath_ReturnsEmptyPath)
+    {
+        // Verify that a relative path that can't be found returns an empty path.
+        constexpr AZ::IO::PathView dae = "meshes/bar.dae";
+        constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
+        constexpr AZ::IO::PathView expectedResult = "";
+        auto result = ROS2::Utils::ResolveAssetPath(
+            dae,
+            urdf, "", GetTestSettings(),
+            [](const AZ::IO::PathView& p) -> bool
+            {
+                // Always return "not found"
                 return false;
             });
-        EXPECT_EQ(result, "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae");
+        EXPECT_EQ(result, expectedResult);
+    }
+
+    TEST_F(UrdfParserTest, TestPathResolve_ValidAmentRelativePathButNoPrefix_ReturnsEmptyPath)
+    {
+        // Verify that a path that is intended to be relative to the location of one of the AMENT_PREFIX_PATH paths
+        // doesn't resolve if it doesn't start with a prefix like "package://" or "model://".
+        constexpr AZ::IO::PathView dae = "robot/meshes/bar.dae";
+        constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
+        constexpr AZStd::string_view amentPrefixPath = "/ament/path1:/ament/path2";
+        constexpr AZ::IO::PathView expectedResult = "";
+        auto result = ROS2::Utils::ResolveAssetPath(
+            dae,
+            urdf, amentPrefixPath, GetTestSettings(),
+            [](const AZ::IO::PathView& p) -> bool
+            {
+                // For an AMENT_PREFIX_PATH to be a valid match, the share/<package>/package.xml and share/<relative path>
+                // both need to exist. We'll return that both exist, but since the dae file entry doesn't start with 
+                // "package://" or "model://", it shouldn't get resolved.
+                return (p == AZ::IO::PathView("/ament/path2/share/robot/package.xml")) || (p == "/ament/path2/share/robot/meshes/bar.dae");
+            });
+        EXPECT_EQ(result, expectedResult);
+    }
+
+    TEST_F(UrdfParserTest, TestPathResolve_ValidAmentRelativePathAndPrefix_ResolvesCorrectly)
+    {
+        // Verify that a path that is intended to be relative to the location of one of the AMENT_PREFIX_PATH paths
+        // doesn't resolve if it doesn't start with a prefix like "package://" or "model://".
+        constexpr AZ::IO::PathView dae = "model://robot/meshes/bar.dae";
+        constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/foo_robot.urdf";
+        constexpr AZStd::string_view amentPrefixPath = "/ament/path1:/ament/path2";
+        constexpr AZ::IO::PathView expectedResult = "/ament/path2/share/robot/meshes/bar.dae";
+        auto result = ROS2::Utils::ResolveAssetPath(
+            dae,
+            urdf, amentPrefixPath, GetTestSettings(),
+            [expectedResult](const AZ::IO::PathView& p) -> bool
+            {
+                // For an AMENT_PREFIX_PATH to be a valid match, the share/<package>/package.xml and share/<relative path>
+                // both need to exist.
+                return (p == AZ::IO::PathView("/ament/path2/share/robot/package.xml")) || (p == expectedResult);
+            });
+        EXPECT_EQ(result, expectedResult);
     }
 
-    TEST_F(UrdfParserTest, TestPathResolvementRelativePackage)
+    TEST_F(UrdfParserTest, TestPathResolve_ValidPathRelativeToAncestorPath_ResolvesCorrectly)
     {
+        // Verify that a path that's relative to an ancestor path of the urdf file resolves correctly
         constexpr AZ::IO::PathView dae = "package://meshes/bar.dae";
         constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/description/foo_robot.urdf";
-        constexpr AZ::IO::PathView xml = "/home/foo/ros_ws/install/foo_robot/package.xml";
-        constexpr AZStd::string_view resolvedDae = "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae";
+        constexpr AZ::IO::PathView expectedResult = "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae";
         auto mockFileSystem = [&](const AZ::IO::PathView& p) -> bool
         {
-            return (p == xml || p == resolvedDae);
+            return p == expectedResult;
         };
-        auto result = ROS2::Utils::ResolveURDFPath(dae, urdf, "", mockFileSystem);
-        EXPECT_EQ(result, resolvedDae);
+        auto result = ROS2::Utils::ResolveAssetPath(dae, urdf, "", GetTestSettings(), mockFileSystem);
+        EXPECT_EQ(result, expectedResult);
+    }
+
+    TEST_F(UrdfParserTest, TestPathResolve_ValidPathRelativeToAncestorPath_FailsToResolveWhenAncestorPathsDisabled)
+    {
+        // Verify that a path that's relative to an ancestor path of the urdf file fails to resolve if "use ancestor paths" is disabled.
+        constexpr AZ::IO::PathView dae = "package://meshes/bar.dae";
+        constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/description/foo_robot.urdf";
+        constexpr AZ::IO::PathView resolvedDae = "/home/foo/ros_ws/install/foo_robot/meshes/bar.dae";
+        
+        auto settings = GetTestSettings();
+        settings.m_resolverSettings.m_useAncestorPaths = false;
+
+        auto mockFileSystem = [&](const AZ::IO::PathView& p) -> bool
+        {
+            // This should never return true, because this path should never get requested.
+            return p == resolvedDae;
+        };
+        auto result = ROS2::Utils::ResolveAssetPath(dae, urdf, "", settings, mockFileSystem);
+        EXPECT_EQ(result, "");
     }
 
     TEST_F(UrdfParserTest, TestPathResolvementExplicitPackageName)
@@ -893,12 +1009,12 @@ namespace UnitTest
         constexpr AZ::IO::PathView dae = "package://foo_robot/meshes/bar.dae";
         constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/description/foo_robot.urdf";
         constexpr AZ::IO::PathView xml = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/package.xml";
-        constexpr AZStd::string_view resolvedDae = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/meshes/bar.dae";
+        constexpr AZ::IO::PathView resolvedDae = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/meshes/bar.dae";
         auto mockFileSystem = [&](const AZ::IO::PathView& p) -> bool
         {
-            return (p == xml || p == resolvedDae);
+            return (p == xml) || (p == resolvedDae);
         };
-        auto result = ROS2::Utils::ResolveURDFPath(dae, urdf, "/home/foo/ros_ws/install/foo_robot", mockFileSystem);
+        auto result = ROS2::Utils::ResolveAssetPath(dae, urdf, "/home/foo/ros_ws/install/foo_robot", GetTestSettings(), mockFileSystem);
         EXPECT_EQ(result, resolvedDae);
     }
 
@@ -907,12 +1023,12 @@ namespace UnitTest
         constexpr AZ::IO::PathView dae = "model://foo_robot/meshes/bar.dae";
         constexpr AZ::IO::PathView urdf = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/description/foo_robot.urdf";
         constexpr AZ::IO::PathView xml = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/package.xml";
-        constexpr AZStd::string_view resolvedDae = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/meshes/bar.dae";
+        constexpr AZ::IO::PathView resolvedDae = "/home/foo/ros_ws/install/foo_robot/share/foo_robot/meshes/bar.dae";
         auto mockFileSystem = [&](const AZ::IO::PathView& p) -> bool
         {
-            return (p == xml || p == resolvedDae);
+            return (p == xml) || (p == resolvedDae);
         };
-        auto result = ROS2::Utils::ResolveURDFPath(dae, urdf, "/home/foo/ros_ws/install/foo_robot", mockFileSystem);
+        auto result = ROS2::Utils::ResolveAssetPath(dae, urdf, "/home/foo/ros_ws/install/foo_robot", GetTestSettings(), mockFileSystem);
         EXPECT_EQ(result, resolvedDae);
     }
 

+ 12 - 1
Gems/ROS2/Registry/sdfassetbuilder_settings.setreg

@@ -13,7 +13,18 @@
                     "xacro"
                 ],
                 "UseArticulations": true,
-                "URDFPreserveFixedJoint": true
+                "URDFPreserveFixedJoint": true,
+                "AssetResolverSettings":
+                {
+                    "UseAmentPrefixPath": true,
+                    "UseAncestorPaths": true,
+                    "URIPrefixMap": 
+                    {
+                        "model://": [""],
+                        "package://": [""],
+                        "file://":  [""]
+                    }                
+                }
             }
         }
     }