diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst
index 441a72d1..0ce805ce 100644
--- a/doc/APIreference/functions.rst
+++ b/doc/APIreference/functions.rst
@@ -4139,6 +4139,15 @@ mjs_getSpec
Get spec from body.
+.. _mjs_findSpec:
+
+mjs_findSpec
+~~~~~~~~~~~~
+
+.. mujoco-include:: mjs_findSpec
+
+Find spec (model asset) by name.
+
.. _mjs_findBody:
mjs_findBody
diff --git a/doc/includes/references.h b/doc/includes/references.h
index 4da16480..9f348881 100644
--- a/doc/includes/references.h
+++ b/doc/includes/references.h
@@ -3600,6 +3600,7 @@ mjsSkin* mjs_addSkin(mjSpec* s);
mjsTexture* mjs_addTexture(mjSpec* s);
mjsMaterial* mjs_addMaterial(mjSpec* s, mjsDefault* def);
mjSpec* mjs_getSpec(mjsElement* element);
+mjSpec* mjs_findSpec(mjSpec* spec, const char* name);
mjsBody* mjs_findBody(mjSpec* s, const char* name);
mjsElement* mjs_findElement(mjSpec* s, mjtObj type, const char* name);
mjsBody* mjs_findChild(mjsBody* body, const char* name);
diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h
index 5f60763e..751c6cdf 100644
--- a/include/mujoco/mujoco.h
+++ b/include/mujoco/mujoco.h
@@ -1534,6 +1534,9 @@ MJAPI mjsMaterial* mjs_addMaterial(mjSpec* s, mjsDefault* def);
// Get spec from body.
MJAPI mjSpec* mjs_getSpec(mjsElement* element);
+// Find spec (model asset) by name.
+MJAPI mjSpec* mjs_findSpec(mjSpec* spec, const char* name);
+
// Find body in spec by name.
MJAPI mjsBody* mjs_findBody(mjSpec* s, const char* name);
diff --git a/introspect/functions.py b/introspect/functions.py
index 377ba4a6..c0a3a196 100644
--- a/introspect/functions.py
+++ b/introspect/functions.py
@@ -9742,6 +9742,28 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
),
doc='Get spec from body.',
)),
+ ('mjs_findSpec',
+ FunctionDecl(
+ name='mjs_findSpec',
+ return_type=PointerType(
+ inner_type=ValueType(name='mjSpec'),
+ ),
+ parameters=(
+ FunctionParameterDecl(
+ name='spec',
+ type=PointerType(
+ inner_type=ValueType(name='mjSpec'),
+ ),
+ ),
+ FunctionParameterDecl(
+ name='name',
+ type=PointerType(
+ inner_type=ValueType(name='char', is_const=True),
+ ),
+ ),
+ ),
+ doc='Find spec (model asset) by name.',
+ )),
('mjs_findBody',
FunctionDecl(
name='mjs_findBody',
diff --git a/src/user/user_model.cc b/src/user/user_model.cc
index 51eb89c0..82e3d941 100644
--- a/src/user/user_model.cc
+++ b/src/user/user_model.cc
@@ -388,6 +388,9 @@ mjCModel& mjCModel::operator+=(const mjCModel& other) {
}
CopyList(numerics_, other.numerics_);
CopyList(texts_, other.texts_);
+ for (const auto* s : other.specs_) {
+ specs_.push_back(mj_copySpec(s));
+ }
}
CopyList(flexes_, other.flexes_);
CopyList(pairs_, other.pairs_);
@@ -3648,12 +3651,6 @@ mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) {
return nullptr;
}
- // destroy attached specs
- for (auto spec : specs_) {
- mj_deleteSpec(spec);
- }
- specs_.clear();
-
// restore error handler, mark as compiled, return mjModel
_mjPRIVATE__set_tls_error_fn(save_error);
_mjPRIVATE__set_tls_warning_fn(save_warning);
diff --git a/src/xml/xml.cc b/src/xml/xml.cc
index 55aefeb5..abcd8c13 100644
--- a/src/xml/xml.cc
+++ b/src/xml/xml.cc
@@ -387,11 +387,11 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs,
return spec;
}
-mjSpec* ParseSpecFromString(std::string_view xml, char* error, int nerror) {
+mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs, char* error, int nerror) {
RegisterResourceProvider();
std::string xml2 = {xml.begin(), xml.end()};
std::string str = "LoadModelFromString:" + xml2;
- return ParseXML(str.c_str(), nullptr, error, nerror);
+ return ParseXML(str.c_str(), vfs, error, nerror);
}
// Main writer function - calls mjXWrite
diff --git a/src/xml/xml.h b/src/xml/xml.h
index bc615d90..44b9b23e 100644
--- a/src/xml/xml.h
+++ b/src/xml/xml.h
@@ -26,7 +26,8 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs, char* error, int nerror
// Returns a newly-allocated mjSpec, loaded from the contents of xml.
// On failure returns nullptr and populates the error array if present.
-mjSpec* ParseSpecFromString(std::string_view xml, char* error = nullptr, int nerror = 0);
+mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs = nullptr,
+ char* error = nullptr, int nerror = 0);
// Main writer function
std::string WriteXML(const mjModel* m, const mjSpec* spec, char* error, int nerror);
diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc
index f808c23b..8fadc4cb 100644
--- a/src/xml/xml_api.cc
+++ b/src/xml/xml_api.cc
@@ -221,7 +221,7 @@ mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, int err
// parse spec from string
mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int error_sz) {
- return ParseSpecFromString(xml, error, error_sz);
+ return ParseSpecFromString(xml, vfs, error, error_sz);
}
diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc
index 6e33580f..23521001 100644
--- a/test/user/user_api_test.cc
+++ b/test/user/user_api_test.cc
@@ -1849,5 +1849,57 @@ TEST_F(MujocoTest, DifferentUnitsNotAllowed) {
mj_deleteSpec(spec_2);
}
+TEST_F(MujocoTest, CopyAttachedSpec) {
+ static constexpr char xml_parent[] = R"(
+
+
+
+
+
+
+
+
+
+
+
+ )";
+
+ static constexpr char xml_child[] = R"(
+
+
+
+
+
+
+
+ )";
+
+ auto vfs = std::make_unique();
+ mj_defaultVFS(vfs.get());
+ mj_addBufferVFS(vfs.get(), "xml_child.xml", xml_child, sizeof(xml_child));
+
+ std::array er;
+ mjSpec* spec = mj_parseXMLString(xml_parent, vfs.get(), er.data(), er.size());
+ EXPECT_THAT(spec, NotNull()) << er.data();
+
+ mjModel* model = mj_compile(spec, vfs.get());
+ EXPECT_THAT(model, NotNull()) << er.data();
+
+ mjSpec* child = mjs_findSpec(spec, "child");
+ EXPECT_THAT(child, NotNull());
+
+ mjSpec* copy = mj_copySpec(spec);
+ EXPECT_THAT(copy, NotNull());
+
+ mjSpec* child_copy = mjs_findSpec(copy, "child");
+ EXPECT_THAT(child_copy, NotNull());
+ EXPECT_NE(child_copy, child);
+
+ mj_deleteSpec(spec);
+ mj_deleteSpec(copy);
+ mj_deleteModel(model);
+ mj_deleteVFS(vfs.get());
+}
+
} // namespace
} // namespace mujoco