Improve include meta-element to load included resourced from the directory relative to the included XML file, fully backwards compatible.

PiperOrigin-RevId: 608312069
Change-Id: I80eadb32d7d78fb35bd24975f2e514021c2aa009
This commit is contained in:
Kyle Bayes
2024-02-19 05:39:31 -08:00
committed by Copybara-Service
parent b77dfc683a
commit 24eb4c9f09
11 changed files with 645 additions and 45 deletions
+75 -18
View File
@@ -23,6 +23,7 @@
#include <array>
#include <cstdio>
#include <string>
#include <string_view>
#include <unordered_set>
#include "tinyxml2.h"
@@ -34,6 +35,7 @@
#include "engine/engine_resource.h"
#include "engine/engine_vfs.h"
#include "user/user_model.h"
#include "user/user_util.h"
#include "xml/xml_native_reader.h"
#include "xml/xml_native_writer.h"
#include "xml/xml_urdf.h"
@@ -41,7 +43,6 @@
namespace {
using std::string;
using tinyxml2::XMLDocument;
using tinyxml2::XMLElement;
using tinyxml2::XMLNode;
@@ -97,7 +98,7 @@ class LocaleOverride {
} // namespace
// Main writer function - calls mjXWrite
string mjWriteXML(mjCModel* model, char* error, int error_sz) {
std::string mjWriteXML(mjCModel* model, char* error, int error_sz) {
LocaleOverride locale_override;
// check for empty model
@@ -114,13 +115,32 @@ string mjWriteXML(mjCModel* model, char* error, int error_sz) {
// find include elements recursively, replace them with subtree from xml file
static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs,
std::unordered_set<string>& included) {
static void mjIncludeXML(mjXReader& reader, XMLElement* elem,
std::string_view dir, const mjVFS* vfs,
std::unordered_set<std::string>& included) {
// capture directory defaults on first pass of XML tree
if (!strcasecmp(elem->Value(), "compiler")) {
auto assetdir_attr = mjXUtil::ReadAttrStr(elem, "assetdir");
if (assetdir_attr.has_value()) {
reader.SetAssetDir(assetdir_attr.value());
}
auto texturedir_attr = mjXUtil::ReadAttrStr(elem, "texturedir");
if (texturedir_attr.has_value()) {
reader.SetTextureDir(texturedir_attr.value());
}
auto meshdir_attr = mjXUtil::ReadAttrStr(elem, "meshdir");
if (meshdir_attr.has_value()) {
reader.SetMeshDir(meshdir_attr.value());
}
}
// not an include, recursively go through all children
if (strcasecmp(elem->Value(), "include")) {
XMLElement* child = elem->FirstChildElement();
for (; child; child = child->NextSiblingElement()) {
mjIncludeXML(child, dir, vfs, included);
mjIncludeXML(reader, child, dir, vfs, included);
}
return;
}
@@ -131,26 +151,60 @@ static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs,
}
// get filename
string filename;
mjXUtil::ReadAttrTxt(elem, "file", filename, true);
filename = dir + filename;
auto file_attr = mjXUtil::ReadAttrStr(elem, "file", true);
if (!file_attr.has_value()) {
throw mjXError(elem, "Include element missing file attribute");
}
std::string filename = file_attr.value();
// block repeated include files
if (included.find(filename) != included.end()) {
throw mjXError(elem, "File '%s' already included", filename.c_str());
}
// get data source
mjResource *resource = mju_openVfsResource(filename.c_str(), vfs);
// TODO: b/325905702 - We have a messy wrapper here to remain backwards
// compatible, which will be removed in the near future.
std::string fullname;
if (!mjuu_isabspath(filename)) {
fullname = reader.ModelFileDir() + filename;
} else {
fullname = filename;
}
mjResource *resource = mju_openVfsResource(fullname.c_str(), vfs);
if (!resource) {
// load from provider or OS filesystem
std::array<char, 1024> error;
resource = mju_openResource(filename.c_str(), error.data(), error.size());
resource = mju_openResource(fullname.c_str(), error.data(), error.size());
if (!resource) {
throw mjXError(elem, "%s", error.data());
if (!mjuu_isabspath(filename)) {
fullname = std::string(dir) + filename;
} else {
fullname = filename;
}
// load from provider or OS filesystem
std::array<char, 1024> error;
resource = mju_openResource(fullname.c_str(), error.data(), error.size());
if (!resource) {
throw mjXError(elem, "%s", error.data());
}
}
}
if (!mjuu_isabspath(filename)) {
filename = std::string(dir) + filename;
}
const char* include_dir = nullptr;
int ninclude_dir = 0;
mju_getResourceDir(resource, &include_dir, &ninclude_dir);
std::string next_dir = std::string(include_dir, ninclude_dir);
if (!mjuu_isabspath(filename)) {
next_dir = std::string(dir) + next_dir;
}
elem->SetAttribute("dir", next_dir.data());
const char* xmlstring = nullptr;
int buffer_size = mju_readResource(resource, (const void**) &xmlstring);
if (buffer_size < 0) {
@@ -210,14 +264,15 @@ static void mjIncludeXML(XMLElement* elem, string dir, const mjVFS* vfs,
// recursively run include
child = include->FirstChildElement();
for (; child; child = child->NextSiblingElement()) {
mjIncludeXML(child, dir, vfs, included);
mjIncludeXML(reader, child, next_dir, vfs, included);
}
}
// Main parser function
mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz) {
mjCModel* mjParseXML(const char* filename, const mjVFS* vfs,
char* error, int error_sz) {
LocaleOverride locale_override;
// check arguments
@@ -251,7 +306,8 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er
int buffer_size = mju_readResource(resource, (const void**) &xmlstring);
if (buffer_size < 0) {
if (error) {
std::snprintf(error, error_sz, "mjParseXML: error reading file '%s'", filename);
std::snprintf(error, error_sz,
"mjParseXML: error reading file '%s'", filename);
}
mju_closeResource(resource);
return nullptr;
@@ -304,11 +360,12 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er
try {
if (!strcasecmp(root->Value(), "mujoco")) {
// find include elements, replace them with subtree from xml file
std::unordered_set<string> included = {filename};
mjIncludeXML(root, model->modelfiledir, vfs, included);
std::unordered_set<std::string> included = {filename};
mjXReader parser;
parser.SetModelFileDir(model->modelfiledir);
mjIncludeXML(parser, root, model->modelfiledir, vfs, included);
// parse MuJoCo model
mjXReader parser;
parser.SetModel(model);
parser.Parse(root);
}
+76 -21
View File
@@ -1349,7 +1349,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjmFlex* pflex) {
// mesh element parser
void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) {
int n;
string text, name, classname, content_type, file;
string text, name, classname, content_type;
// read attributes
if (ReadAttrTxt(elem, "name", name)) {
@@ -1361,8 +1361,9 @@ void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) {
if (ReadAttrTxt(elem, "content_type", content_type)) {
mjm_setString(pmesh->content_type, content_type.c_str());
}
if (ReadAttrTxt(elem, "file", file)) {
mjm_setString(pmesh->file, file.c_str());
auto file = ReadAttrFile(elem, "file", MeshDir());
if (file) {
mjm_setString(pmesh->file, file->c_str());
}
ReadAttr(elem, "refpos", 3, pmesh->refpos, text);
ReadAttr(elem, "refpos", 4, pmesh->refquat, text);
@@ -1418,19 +1419,20 @@ void mjXReader::OneMesh(XMLElement* elem, mjmMesh* pmesh) {
// skin element parser
void mjXReader::OneSkin(XMLElement* elem, mjmSkin* pskin) {
string text, name, file, material;
string text, name, material;
float data[4];
// read attributes
if (ReadAttrTxt(elem, "name", name)) {
mjm_setString(pskin->name, name.c_str());
}
if (ReadAttrTxt(elem, "file", file)) {
mjm_setString(pskin->file, file.c_str());
auto file = ReadAttrFile(elem, "file", AssetDir());
if (file.has_value()) {
mjm_setString(pskin->file, file->c_str());
}
if (ReadAttrTxt(elem, "material", material)) {
mjm_setString(pskin->material, material.c_str());
}
}
ReadAttrInt(elem, "group", &pskin->group);
if (pskin->group<0 || pskin->group>=mjNGROUP) {
throw mjXError(elem, "skin group must be between 0 and 5");
@@ -2507,7 +2509,7 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjmBody* pbody) {
ReadAttr(elem, "scale", 3, fcomp.scale, text);
ReadAttr(elem, "mass", 1, &fcomp.mass, text);
ReadAttr(elem, "inertiabox", 1, &fcomp.inertiabox, text);
ReadAttrTxt(elem, "file", fcomp.file);
fcomp.file = ReadAttrFile(elem, "file", modelfiledir_).value_or("");
if (ReadAttrTxt(elem, "material", material)) {
mjm_setString(dflex.material, material.c_str());
}
@@ -3067,7 +3069,7 @@ void mjXReader::Visual(XMLElement* section) {
// asset section parser
void mjXReader::Asset(XMLElement* section) {
int n;
string text, name, texname, content_type, file;
string text, name, texname, content_type;
XMLElement* elem;
// iterate over child elements
@@ -3101,8 +3103,9 @@ void mjXReader::Asset(XMLElement* section) {
if (ReadAttrTxt(elem, "content_type", content_type)) {
mjm_setString(ptex->content_type, content_type.c_str());
}
if (ReadAttrTxt(elem, "file", file)) {
mjm_setString(ptex->file, file.c_str());
auto file = ReadAttrFile(elem, "file", TextureDir());
if (file.has_value()) {
mjm_setString(ptex->file, file->c_str());
}
ReadAttrInt(elem, "width", &ptex->width);
ReadAttrInt(elem, "height", &ptex->height);
@@ -3139,12 +3142,12 @@ void mjXReader::Asset(XMLElement* section) {
// separate files
std::vector<string> cubefiles(6);
ReadAttrTxt(elem, "fileright", cubefiles[0]);
ReadAttrTxt(elem, "fileleft", cubefiles[1]);
ReadAttrTxt(elem, "fileup", cubefiles[2]);
ReadAttrTxt(elem, "filedown", cubefiles[3]);
ReadAttrTxt(elem, "filefront", cubefiles[4]);
ReadAttrTxt(elem, "fileback", cubefiles[5]);
cubefiles[0] = ReadAttrFile(elem, "fileright", TextureDir()).value_or("");
cubefiles[1] = ReadAttrFile(elem, "fileleft", TextureDir()).value_or("");
cubefiles[2] = ReadAttrFile(elem, "fileup", TextureDir()).value_or("");
cubefiles[3] = ReadAttrFile(elem, "filedown", TextureDir()).value_or("");
cubefiles[4] = ReadAttrFile(elem, "filefront", TextureDir()).value_or("");
cubefiles[5] = ReadAttrFile(elem, "fileback", TextureDir()).value_or("");
for (int i = 0; i < cubefiles.size(); i++) {
mjm_setInStringVec(ptex->cubefiles, i, cubefiles[i].c_str());
}
@@ -3181,22 +3184,23 @@ void mjXReader::Asset(XMLElement* section) {
std::string("line = " + std::to_string(elem->GetLineNum()) + ", column = -1").c_str());
// read attributes
string name, content_type, file;
string name, content_type;
if (ReadAttrTxt(elem, "name", name)) {
mjm_setString(phf->name, name.c_str());
}
if (ReadAttrTxt(elem, "content_type", content_type)) {
mjm_setString(phf->content_type, content_type.c_str());
}
if (ReadAttrTxt(elem, "file", file)) {
mjm_setString(phf->file, file.c_str());
auto file = ReadAttrFile(elem, "file", AssetDir());
if (file.has_value()) {
mjm_setString(phf->file, file->c_str());
}
ReadAttrInt(elem, "nrow", &phf->nrow);
ReadAttrInt(elem, "ncol", &phf->ncol);
ReadAttr(elem, "size", 4, phf->size, text, true);
// allocate buffer for dynamic hfield, copy user data if given
if (file.empty() && phf->nrow>0 && phf->ncol>0) {
if (!file.has_value() && phf->nrow>0 && phf->ncol>0) {
int nrow = phf->nrow;
int ncol = phf->ncol;
@@ -4034,3 +4038,54 @@ mjmDefault* mjXReader::GetClass(XMLElement* section) {
void mjXReader::GetXMLPos(XMLElement* elem, mjCBase* obj) {
obj->info = "line = " + std::to_string(elem->GetLineNum());
}
// return true if c is a directory path separator (i.e. '/' or '\' on windows)
static bool IsSeperator(char c) {
return c == '/' || c == '\\';
}
void mjXReader::SetModelFileDir(std::string modelfiledir) {
modelfiledir_ = modelfiledir;
if (!modelfiledir_.empty() && !IsSeperator(modelfiledir_.back())) {
modelfiledir_.append("/");
}
}
void mjXReader::SetAssetDir(std::string assetdir) {
assetdir_ = assetdir;
if (!assetdir_.empty() && !IsSeperator(assetdir_.back())) {
assetdir_.append("/");
}
}
void mjXReader::SetMeshDir(std::string meshdir) {
meshdir_ = meshdir;
if (!meshdir_.empty() && !IsSeperator(meshdir_.back())) {
meshdir_.append("/");
}
}
void mjXReader::SetTextureDir(std::string texturedir) {
texturedir_ = texturedir;
if (!texturedir_.empty() && !IsSeperator(texturedir_.back())) {
texturedir_.append("/");
}
}
std::string mjXReader::AssetDir() const {
return modelfiledir_ + assetdir_;
}
std::string mjXReader::MeshDir() const {
if (meshdir_.empty()) {
return AssetDir();
}
return modelfiledir_ + meshdir_;
}
std::string mjXReader::TextureDir() const {
if (texturedir_.empty()) {
return AssetDir();
}
return modelfiledir_ + texturedir_;
}
+19
View File
@@ -16,6 +16,7 @@
#define MUJOCO_SRC_XML_XML_NATIVE_READER_H_
#include <sstream>
#include <string>
#include "tinyxml2.h"
@@ -34,6 +35,14 @@ class mjXReader : public mjXBase {
void Parse(tinyxml2::XMLElement* root); // parse XML document
void PrintSchema(std::stringstream& str, bool html, bool pad); // print text or HTML schema
void SetModelFileDir(std::string modelfiledir);
const std::string& ModelFileDir() const { return modelfiledir_; }
// setters for directory defaults
void SetAssetDir(std::string assetdir);
void SetMeshDir(std::string meshdir);
void SetTextureDir(std::string texturedir);
// XML sections embedded in all formats
static void Compiler(tinyxml2::XMLElement* section, mjCModel* mod); // compiler section
static void Option(tinyxml2::XMLElement* section, mjOption* opt); // option section
@@ -80,6 +89,16 @@ class mjXReader : public mjXBase {
static void GetXMLPos(tinyxml2::XMLElement* elem, mjCBase* obj); // get xml position
bool readingdefaults; // true while reading defaults
// accessors for directory defaults
std::string AssetDir() const;
std::string MeshDir() const;
std::string TextureDir() const;
std::string modelfiledir_;
std::string assetdir_;
std::string meshdir_;
std::string texturedir_;
};
// MJCF schema
+44 -3
View File
@@ -32,8 +32,11 @@
#include "tinyxml2.h"
#include <mujoco/mujoco.h>
#include "cc/array_safety.h"
#include "engine/engine_resource.h"
#include "engine/engine_util_errmem.h"
#include "user/user_util.h"
#include "xml/xml_util.h"
#include "xml/xml_numeric_format.h"
@@ -156,6 +159,35 @@ XMLElement* NextSiblingElement(XMLElement* e, const char* name) {
return nullptr;
}
static std::string ResolveFilePath(XMLElement* e, std::string filename,
const std::string& dir) {
std::string path = "";
if (mjuu_isabspath(filename)) {
return filename;
}
// TODO(kylebayes): We first look in the base model directory for files to
// remain backwards compatible.
std::string full_filename = dir + filename;
mjResource *resource = mju_openResource(full_filename.c_str(), nullptr, 0);
if (resource != nullptr) {
mju_closeResource(resource);
return filename;
}
XMLElement* parent = e->Parent()->ToElement();
for (; parent; parent = parent->Parent()->ToElement()) {
if (!std::strcmp(parent->Value(), "include")) {
auto file_attr = mjXUtil::ReadAttrStr(parent, "dir", false);
if (file_attr.has_value()) {
path = file_attr.value();
}
break;
}
}
return path + filename;
}
// constructor
mjXSchema::mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow) {
// set name and type
@@ -562,8 +594,8 @@ mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required);
// if attribute is present, return attribute as a string
std::optional<std::string> mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr,
bool required) {
std::optional<std::string>
mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, bool required) {
const char* pstr = elem->Attribute(attr);
// check if attribute exists
@@ -578,7 +610,16 @@ std::optional<std::string> mjXUtil::ReadAttrStr(XMLElement* elem, const char* at
return std::string(pstr);
}
// if attribute is present, return attribute as a filename
std::optional<std::string>
mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr,
const std::string& dir, bool required) {
auto maybe_str = ReadAttrStr(elem, attr, required);
if (!maybe_str.has_value()) {
return std::nullopt;
}
return ResolveFilePath(elem, maybe_str.value(), dir);
}
// if attribute is present, return numerical value of attribute
template<typename T>
+4 -1
View File
@@ -35,7 +35,6 @@ using tinyxml2::XMLElement;
XMLElement* FirstChildElement(XMLElement* e, const char* name = nullptr);
XMLElement* NextSiblingElement(XMLElement* e, const char* name = nullptr);
// XML Error info
class [[nodiscard]] mjXError {
public:
@@ -109,6 +108,10 @@ class mjXUtil {
static std::optional<std::string> ReadAttrStr(tinyxml2::XMLElement* elem, const char* attr,
bool required = false);
// if attribute is present, return attribute as a filename
static std::optional<std::string> ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr,
const std::string& dir = "", bool required = false);
// if attribute is present, return numerical value of attribute
template<typename T>
static std::optional<T> ReadAttrNum(tinyxml2::XMLElement* elem, const char* attr,