From 4150b02f1286b5da12c282c2bf6f723664d5c963 Mon Sep 17 00:00:00 2001 From: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:55:45 -0500 Subject: [PATCH] Add configurable SDF/URDF path resolution settings, including prefix remappings (#505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First version of adding configurable prefix lookups and substitutions. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Tests/UrdfParserTest.cpp Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Improved asset path resolution logic and unit tests. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Add SDFormat importer hooks (#453) Add sensors SDF importer hooks Signed-off-by: Jan Hanca * use PrimitiveAssets Gem to visualize primitives (#485) Use PrimitiveAssets Gem to visualize primitives --------- Signed-off-by: Jan Hanca * Warehouse automation gem (#440) Created the warehouse automation gem, adjusted to review --------- Signed-off-by: Antoni Puch * Lidar component refactor (#463) Lidar Refactor --------- Signed-off-by: Antoni Puch Signed-off-by: Antoni-Robotec <138497503+Antoni-Robotec@users.noreply.github.com> Co-authored-by: Adam Dąbrowski * Added presentation of SDF messages (#491) * Added presentation of SDF messages --------- Signed-off-by: Michał Pełka Co-authored-by: Jan Hanca <134940295+jhanca-robotecai@users.noreply.github.com> * Fix missing include. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix resolution of file:// prefix. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Change string to Path. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Changed PathResolver to get read in wholesale from setreg file. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * 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 <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Update Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * PR feedback Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * PR feedback Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Partial revert from path back to string. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Second half of path to string revert. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * 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 <82224783+mbalfour-amzn@users.noreply.github.com> --------- Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Signed-off-by: Jan Hanca Signed-off-by: Antoni Puch Signed-off-by: Antoni-Robotec <138497503+Antoni-Robotec@users.noreply.github.com> Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Co-authored-by: Jan Hanca <134940295+jhanca-robotecai@users.noreply.github.com> Co-authored-by: Antoni-Robotec <138497503+Antoni-Robotec@users.noreply.github.com> Co-authored-by: Adam Dąbrowski Co-authored-by: Michał Pełka --- .../RobotImporter/Pages/FileSelectionPage.cpp | 43 ++- .../RobotImporter/Pages/FileSelectionPage.h | 4 + ...ROS2RobotImporterEditorSystemComponent.cpp | 7 +- .../RobotImporter/RobotImporterWidget.cpp | 10 +- .../Utils/RobotImporterUtils.cpp | 341 ++++++++++++------ .../RobotImporter/Utils/RobotImporterUtils.h | 26 +- .../Utils/SourceAssetsStorage.cpp | 33 +- .../RobotImporter/Utils/SourceAssetsStorage.h | 13 +- .../SdfAssetBuilder/SdfAssetBuilder.cpp | 15 +- .../SdfAssetBuilderSettings.cpp | 59 ++- .../SdfAssetBuilder/SdfAssetBuilderSettings.h | 29 +- Gems/ROS2/Code/Tests/SdfParserTest.cpp | 1 + Gems/ROS2/Code/Tests/UrdfParserTest.cpp | 162 +++++++-- .../Registry/sdfassetbuilder_settings.setreg | 13 +- 14 files changed, 557 insertions(+), 199 deletions(-) diff --git a/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.cpp b/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.cpp index 7de111435f..85ea32b2d9 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.cpp +++ b/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.cpp @@ -14,16 +14,30 @@ #include #include #include +#include namespace ROS2 { + static constexpr const char FileSelectionPageDefaultFile[] = "RobotImporter/SelectFileDefaultFile"; + FileSelectionPage::FileSelectionPage(QWizard* parent) : QWizardPage(parent) , m_sdfAssetBuilderSettings(AZStd::make_unique()) { 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(); } diff --git a/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.h b/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.h index d71cc3ec64..ac48a97394 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Pages/FileSelectionPage.h +++ b/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 diff --git a/Gems/ROS2/Code/Source/RobotImporter/ROS2RobotImporterEditorSystemComponent.cpp b/Gems/ROS2/Code/Source/RobotImporter/ROS2RobotImporterEditorSystemComponent.cpp index e9564357bd..f0f35bf831 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/ROS2RobotImporterEditorSystemComponent.cpp +++ b/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::CopyAssetForURDFAndCreateAssetMap(meshNames, filePath, collidersNames, visualNames)); + Utils::CopyAssetForURDFAndCreateAssetMap(meshNames, filePath, collidersNames, visualNames, sdfBuilderSettings)); } bool allAssetProcessed = false; bool assetProcessorFailed = false; diff --git a/Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp b/Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp index 612d7b29b7..827adb9df8 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp +++ b/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::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::FindAssetsForUrdf(m_meshNames, m_urdfPath.String())); + m_urdfAssetsMapping = AZStd::make_shared(Utils::FindAssetsForUrdf(m_meshNames, m_urdfPath.String(), sdfBuilderSettings)); for (const AZStd::string& meshPath : m_meshNames) { if (m_urdfAssetsMapping->contains(meshPath)) diff --git a/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp b/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp index 62f5824f7f..cad8233fa3 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp +++ b/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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 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 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(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 /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 + // /share/ambulance/package.xml exists and + // /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 amentPrefixPaths, - AZStd::span 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 + 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 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 /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 + // 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({ 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 diff --git a/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h b/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h index 8f7e9d3e75..581a12369a 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h +++ b/Gems/ROS2/Code/Source/RobotImporter/Utils/RobotImporterUtils.h @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -147,18 +148,22 @@ namespace ROS2::Utils //! @return true should be returned if the file exist otherwise false using FileExistsCB = AZStd::function; - //! 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& 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 diff --git a/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.cpp b/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.cpp index ed97c5d518..51cb56cf25 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.cpp +++ b/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.cpp @@ -273,12 +273,10 @@ namespace ROS2::Utils const AZStd::string& urdfFilename, const AZStd::unordered_set& colliders, const AZStd::unordered_set& 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 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& meshesFilenames, const AZStd::string& urdfFilename) + UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set& 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)); } diff --git a/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.h b/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.h index 283e6c931b..a2d5ea3b86 100644 --- a/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.h +++ b/Gems/ROS2/Code/Source/RobotImporter/Utils/SourceAssetsStorage.h @@ -19,6 +19,11 @@ #include #include +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& meshesFilenames, const AZStd::string& urdfFilename); + UrdfAssetMap FindAssetsForUrdf(const AZStd::unordered_set& 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& colliders, const AZStd::unordered_set& visual, + const SdfAssetBuilderSettings& sdfBuilderSettings, AZStd::string_view outputDirSuffix = "", AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance()); diff --git a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilder.cpp b/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilder.cpp index 066af5f750..ed4933c684 100644 --- a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilder.cpp +++ b/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()); diff --git a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.cpp b/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.cpp index a5021ca538..22ac57f6ee 100644 --- a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.cpp +++ b/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(context)) + { + serializeContext->Class() + ->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( + "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(context)) { serializeContext->Class() - ->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( - "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 diff --git a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h b/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h index f5858dafd7..262779b632 100644 --- a/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h +++ b/Gems/ROS2/Code/Source/SdfAssetBuilder/SdfAssetBuilderSettings.h @@ -8,11 +8,32 @@ #pragma once +#include #include #include 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>; + + //! 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 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 diff --git a/Gems/ROS2/Code/Tests/SdfParserTest.cpp b/Gems/ROS2/Code/Tests/SdfParserTest.cpp index 17ab2a2ae8..de25cc98ec 100644 --- a/Gems/ROS2/Code/Tests/SdfParserTest.cpp +++ b/Gems/ROS2/Code/Tests/SdfParserTest.cpp @@ -505,4 +505,5 @@ namespace UnitTest EXPECT_EQ(unsupportedImuParams[0U], ">always_on"); } } + } // namespace UnitTest diff --git a/Gems/ROS2/Code/Tests/UrdfParserTest.cpp b/Gems/ROS2/Code/Tests/UrdfParserTest.cpp index 3d7996eda6..dbbfb558f5 100644 --- a/Gems/ROS2/Code/Tests/UrdfParserTest.cpp +++ b/Gems/ROS2/Code/Tests/UrdfParserTest.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace UnitTest { @@ -300,6 +301,18 @@ namespace UnitTest ""; // 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({"."})); + settings.m_resolverSettings.m_uriPrefixMap.emplace("package://", AZStd::vector({"."})); + settings.m_resolverSettings.m_uriPrefixMap.emplace("file://", AZStd::vector({"."})); + + 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.xml and share/ + // 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.xml and share/ + // 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); } diff --git a/Gems/ROS2/Registry/sdfassetbuilder_settings.setreg b/Gems/ROS2/Registry/sdfassetbuilder_settings.setreg index 7f47181327..664ea0e5ed 100644 --- a/Gems/ROS2/Registry/sdfassetbuilder_settings.setreg +++ b/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://": [""] + } + } } } }