Do not copy the spec during attach.

Use a reference count for managing the memory.

PiperOrigin-RevId: 716169486
Change-Id: Id270c4858c17b9250115e9544d5ea143584e2d5f
This commit is contained in:
Alessio Quaglino
2025-01-16 03:52:31 -08:00
committed by Copybara-Service
parent 6436055c6c
commit c2138c3fb0
19 changed files with 505 additions and 173 deletions
+9
View File
@@ -1510,6 +1510,15 @@ Free memory allocation in mjSpec.
Activate plugin. Returns 0 on success.
.. _mjs_setDeepCopy:
`mjs_setDeepCopy <#mjs_setDeepCopy>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mjs_setDeepCopy
Turn deep copy on or off attach. Returns 0 on success.
.. _Errorandmemory:
Error and memory
+7 -2
View File
@@ -2,8 +2,13 @@
Changelog
=========
Version 3.2.7 (Jan 14, 2025)
----------------------------
Upcoming version (not yet released)
-----------------------------------
- Added ``mjs_setDeepCopy`` API function. When the deep copy flag is 0, attaching a model will not copy it to the
parent, so the original references to the child allow to modify the parent as well. The default behavior is to perform
such a shallow copy. The old behavioud of creating a deep copy of the child model while attaching can be restored by
setting the deep copy flag to 1.
Python bindings
^^^^^^^^^^^^^^^
+1
View File
@@ -3181,6 +3181,7 @@ mjSpec* mj_makeSpec(void);
mjSpec* mj_copySpec(const mjSpec* s);
void mj_deleteSpec(mjSpec* s);
int mjs_activatePlugin(mjSpec* s, const char* name);
int mjs_setDeepCopy(mjSpec* s, int deepcopy);
void mj_printFormattedModel(const mjModel* m, const char* filename, const char* float_format);
void mj_printModel(const mjModel* m, const char* filename);
void mj_printFormattedData(const mjModel* m, mjData* d, const char* filename,
+17 -7
View File
@@ -106,12 +106,14 @@ procedurally, default classes are passed in explicitly to element constructors.
Attachment
^^^^^^^^^^
This framework introduces a powerful new feature: attaching and detaching model subtrees. Attachment allows the user
copy a subtree from one model into another, while also copying related referenced assets and referencing elements from
outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will remove all associated
elements from the model. This feature is already used to power the :ref:`attach<body-attach>` and
:ref:`replicate<replicate>` meta-elements in MJCF. It is possible to :ref:`attach a body to a frame<mjs_attachBody>` and
to :ref:`attach a body to a site<mjs_attachToSite>`:
This framework introduces a powerful new feature: attaching and detaching model subtrees. This feature is already used
to power the :ref:`attach<body-attach>` an :ref:`replicate<replicate>` meta-elements in MJCF. Attachment allows the user
to move or copy a subtree from one model into another, while also copying or moving related referenced assets and
referencing elements from outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will
remove all associated elements from the model. The default behavior is to move during attach. The user can select to
instead copy by passing the corresponding flag to ``mjs_setDeepCopy``. This flag is temporary set to true while parsing
XMLs. It is possible to :ref:`attach a body to a frame<mjs_attachBody>`:
.. code-block:: C
@@ -120,9 +122,17 @@ to :ref:`attach a body to a site<mjs_attachToSite>`:
parent->compiler.degree = 0;
child->compiler.degree = 1;
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL);
mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL);
mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL);
mjsBody* attached_body_1 = mjs_attachBody(frame, body, "attached-", "-1");
or :ref:`attach a body to a site<mjs_attachToSite>`:
.. code-block:: C
mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL);
mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL);
mjsBody* attached_body_2 = mjs_attachToSite(site, body, "attached-", "-2");
or :ref:`attach a frame to a body<mjs_attachFrame>`:
+7 -3
View File
@@ -531,15 +531,19 @@ Attachment
It is possible to combine multiple specs by using attachments. The following options are possible:
- Attach a body from the child spec to a frame in the parent spec: ``body.attach_body(body, prefix, suffix)``, returns
the newly createdbody in the parent spec.
the reference to the attached body, which should be identical to the body used as input.
- Attach a frame from the child spec to a body in the parent spec: ``body.attach_frame(frame, prefix, suffix)``,
returns the newly created frame in the parent spec.
returns the reference to the attached frame, which should be identical to the frame used as input.
- Attach a body from the child spec to a site in the parent spec: ``site.attach(body, prefix, suffix)``, returns the
newly created body in the parent spec.
reference to the attached body, which should be identical to the body used as input.
- Attach the worldbody from the child spec to a frame in the parent spec and transform it to a frame:
``body.attach(spec, prefix, suffix)``, returns the newly created frame that the child worldbody was transformed
into.
Attaching does not copy, so all the child reference are still valid in the parent and therefore modifying the child will
modify the parent. This is not true for the attach :ref:`attach<body-attach>` an :ref:`replicate<replicate>`
meta-elements in MJCF, which create deep copies while attaching.
.. code-block:: python
import mujoco
+3
View File
@@ -242,6 +242,9 @@ MJAPI void mj_deleteSpec(mjSpec* s);
// Activate plugin. Returns 0 on success.
MJAPI int mjs_activatePlugin(mjSpec* s, const char* name);
// Turn deep copy on or off attach. Returns 0 on success.
MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy);
//---------------------------------- Printing ------------------------------------------------------
+18
View File
@@ -1045,6 +1045,24 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
),
doc='Activate plugin. Returns 0 on success.',
)),
('mjs_setDeepCopy',
FunctionDecl(
name='mjs_setDeepCopy',
return_type=ValueType(name='int'),
parameters=(
FunctionParameterDecl(
name='s',
type=PointerType(
inner_type=ValueType(name='mjSpec'),
),
),
FunctionParameterDecl(
name='deepcopy',
type=ValueType(name='int'),
),
),
doc='Turn deep copy on or off attach. Returns 0 on success.',
)),
('mj_printFormattedModel',
FunctionDecl(
name='mj_printFormattedModel',
+8
View File
@@ -350,6 +350,14 @@ PYBIND11_MODULE(_specs, m) {
mjSpec.def("copy", [](const MjSpec& self) -> MjSpec {
return MjSpec(self);
});
mjSpec.def_property(
"copy_during_attach",
[](MjSpec& self) {
throw pybind11::value_error("copy_during_attach can only be set.");
},
[](MjSpec& self, bool deepcopy) {
return mjs_setDeepCopy(self.ptr, deepcopy);
});
mjSpec.def_property_readonly(
"worldbody",
[](MjSpec& self) -> raw::MjsBody* {
+24 -16
View File
@@ -918,27 +918,31 @@ class SpecsTest(absltest.TestCase):
model = parent.compile()
np.testing.assert_almost_equal(model.body_quat[1], [1, 0, 0, 0])
def test_attach_body_to_site(self):
child = mujoco.MjSpec()
def test_attach_to_site(self):
parent = mujoco.MjSpec()
site = parent.worldbody.add_site(pos=[1, 2, 3], quat=[0, 0, 0, 1])
body = child.worldbody.add_body()
# Attach body to site and compile.
self.assertIsNotNone(site.attach_body(body, prefix='_'))
child1 = mujoco.MjSpec()
body1 = child1.worldbody.add_body()
self.assertIs(body1, site.attach_body(body1, prefix='_'))
body1.pos = [1, 1, 1]
model1 = parent.compile()
self.assertIsNotNone(model1)
self.assertEqual(model1.nbody, 2)
np.testing.assert_array_equal(model1.body_pos[1], [1, 2, 3])
np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4])
np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1])
# Attach entire spec to site and compile again.
self.assertIsNotNone(site.attach(child, prefix='child-'))
child2 = mujoco.MjSpec()
body2 = child2.worldbody.add_body(name='body')
self.assertIsNotNone(site.attach(child2, prefix='child-'))
body2.pos = [-1, -1, -1]
model2 = parent.compile()
self.assertIsNotNone(model2)
self.assertEqual(model2.nbody, 3)
np.testing.assert_array_equal(model2.body_pos[1], [1, 2, 3])
np.testing.assert_array_equal(model2.body_pos[2], [1, 2, 3])
np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4])
np.testing.assert_array_equal(model2.body_pos[2], [2, 3, 2])
np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1])
np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1])
@@ -949,27 +953,31 @@ class SpecsTest(absltest.TestCase):
frame = body.to_frame()
np.testing.assert_array_equal(frame.pos, [1, 2, 3])
def test_attach_spec_to_frame(self):
child = mujoco.MjSpec()
def test_attach_to_frame(self):
parent = mujoco.MjSpec()
frame = parent.worldbody.add_frame(pos=[1, 2, 3], quat=[0, 0, 0, 1])
body = child.worldbody.add_body()
# Attach body to frame and compile.
self.assertIsNotNone(frame.attach_body(body, prefix='_'))
child1 = mujoco.MjSpec()
body1 = child1.worldbody.add_body()
self.assertIs(body1, frame.attach_body(body1, prefix='_'))
body1.pos = [1, 1, 1]
model1 = parent.compile()
self.assertIsNotNone(model1)
self.assertEqual(model1.nbody, 2)
np.testing.assert_array_equal(model1.body_pos[1], [1, 2, 3])
np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4])
np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1])
# Attach entire spec to frame and compile again.
self.assertIsNotNone(frame.attach(child, prefix='child-'))
child2 = mujoco.MjSpec()
body2 = child2.worldbody.add_body(name='body')
self.assertIsNotNone(frame.attach(child2, prefix='child-'))
body2.pos = [-1, -1, -1]
model2 = parent.compile()
self.assertIsNotNone(model2)
self.assertEqual(model2.nbody, 3)
np.testing.assert_array_equal(model2.body_pos[1], [1, 2, 3])
np.testing.assert_array_equal(model2.body_pos[2], [1, 2, 3])
np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4])
np.testing.assert_array_equal(model2.body_pos[2], [2, 3, 2])
np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1])
np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1])
+11 -2
View File
@@ -208,7 +208,7 @@ int mjs_detachBody(mjSpec* s, mjsBody* b) {
model->SetError(e);
return -1;
}
delete body;
model->Detach(body);
return 0;
}
@@ -254,6 +254,15 @@ int mjs_activatePlugin(mjSpec* s, const char* name) {
// set deep copy flag
int mjs_setDeepCopy(mjSpec* s, int deepcopy) {
mjCModel* model = static_cast<mjCModel*>(s->element);
model->SetDeepCopy(deepcopy);
return 0;
}
// delete object, return 0 if success
int mjs_delete(mjsElement* element) {
mjCBase* object = static_cast<mjCBase*>(element);
@@ -705,7 +714,7 @@ const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* s
mjsFrame* mjs_bodyToFrame(mjsBody** body) {
mjCBody* bodyC = static_cast<mjCBody*>((*body)->element);
mjCFrame* frameC = bodyC->ToFrame();
delete bodyC;
bodyC->model->Detach(bodyC);
*body = nullptr;
return &frameC->spec;
}
+3
View File
@@ -63,6 +63,9 @@ MJAPI void mjs_addSpec(mjSpec* s, mjSpec* child);
// Activate plugin, return 0 on success.
MJAPI int mjs_activatePlugin(mjSpec* s, const char* name);
// Turn deep copy on or off attach. Returns 0 on success.
MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy);
//---------------------------------- Attachment ----------------------------------------------------
-3
View File
@@ -278,9 +278,6 @@ void mjCMesh::CopyPlugin() {
mjCMesh::~mjCMesh() {
if (center_) mju_free(center_);
if (graph_) mju_free(graph_);
if (spec.plugin.active && spec.plugin.name->empty() && model) {
model->DeleteElement(spec.plugin.element);
}
}
+102 -26
View File
@@ -139,6 +139,7 @@ mjCModel::mjCModel() {
center_auto[0] = center_auto[1] = center_auto[2] = 0;
#endif
deepcopy_ = false;
nplugin = 0;
Clear();
@@ -181,6 +182,7 @@ mjCModel::mjCModel(const mjCModel& other) {
mjCModel& mjCModel::operator=(const mjCModel& other) {
deepcopy_ = true;
if (this != &other) {
this->spec = other.spec;
*static_cast<mjCModel_*>(this) = static_cast<const mjCModel_&>(other);
@@ -210,6 +212,7 @@ mjCModel& mjCModel::operator=(const mjCModel& other) {
ids[i] = other.ids[i];
}
}
deepcopy_ = other.deepcopy_;
return *this;
}
@@ -222,20 +225,29 @@ void mjCModel::CopyList(std::vector<T*>& dest,
// loop over the elements from the other model
int nsource = (int)source.size();
for (int i = 0; i < nsource; i++) {
T* candidate = new T(*source[i]);
T* candidate = deepcopy_ ? new T(*source[i]) : source[i];
try {
// try to find the referenced object in this model
candidate->NameSpace(source[i]->model);
mjCModel* source_model = source[i]->model;
candidate->model = this;
candidate->NameSpace(source_model);
candidate->CopyFromSpec();
candidate->ResolveReferences(this);
} catch (mjCError err) {
// if not present, skip the element
// TODO: do not skip elements that contain user errors
delete candidate;
if (deepcopy_) {
candidate->model = nullptr;
delete candidate;
}
continue;
}
// copy the element from the other model to this model
source[i]->ForgetKeyframes();
if (deepcopy_) {
source[i]->ForgetKeyframes();
} else {
candidate->AddRef();
}
mjSpec* origin = FindSpec(source[i]->compiler);
dest.push_back(candidate);
dest.back()->model = this;
@@ -324,9 +336,12 @@ void mjCModel::CopyExplicitPlugin(T* obj) {
return;
}
mjCPlugin* origin = static_cast<mjCPlugin*>(obj->spec.plugin.element);
mjCPlugin* candidate = new mjCPlugin(*origin);
mjCPlugin* candidate = deepcopy_ ? new mjCPlugin(*origin) : origin;
candidate->id = plugins_.size();
candidate->model = this;
if (!deepcopy_) {
candidate->AddRef();
}
plugins_.push_back(candidate);
obj->spec.plugin.element = candidate;
}
@@ -566,7 +581,7 @@ void deletefromlist(std::vector<T*>* list, mjsElement* element) {
for (int j = 0; j < list->size(); ++j) {
list->at(j)->id = -1;
if (list->at(j) == element) {
delete list->at(j);
list->at(j)->Release();
list->erase(list->begin() + j);
j--;
}
@@ -577,8 +592,9 @@ void deletefromlist(std::vector<T*>* list, mjsElement* element) {
// discard all invalid elements from all lists
void mjCModel::DeleteElement(mjsElement* el) {
mjCBody *world = bodies_[0];
mjCBody *world = nullptr;
if (compiled) {
world = bodies_[0];
ResetTreeLists();
}
@@ -588,8 +604,14 @@ void mjCModel::DeleteElement(mjsElement* el) {
break;
case mjOBJ_GEOM:
deletefromlist(&(static_cast<mjCGeom*>(el)->body->geoms), el);
{
mjCGeom* geom = static_cast<mjCGeom*>(el);
if (geom->plugin.active && geom->plugin.name->empty() && geom->GetRef() == 1) {
DeleteElement(geom->plugin.element);
}
deletefromlist(&(geom->body->geoms), el);
break;
}
case mjOBJ_SITE:
deletefromlist(&(static_cast<mjCSite*>(el)->body->sites), el);
@@ -607,6 +629,36 @@ void mjCModel::DeleteElement(mjsElement* el) {
deletefromlist(&(static_cast<mjCCamera*>(el)->body->cameras), el);
break;
case mjOBJ_MESH:
{
mjCMesh* mesh = static_cast<mjCMesh*>(el);
if (mesh->plugin.active && mesh->plugin.name->empty() && mesh->GetRef() == 1) {
DeleteElement(mesh->plugin.element);
}
deletefromlist(object_lists_[mjOBJ_MESH], el);
break;
}
case mjOBJ_ACTUATOR:
{
mjCActuator* actuator = static_cast<mjCActuator*>(el);
if (actuator->plugin.active && actuator->plugin.name->empty() && actuator->GetRef() == 1) {
DeleteElement(actuator->plugin.element);
}
deletefromlist(object_lists_[mjOBJ_ACTUATOR], el);
break;
}
case mjOBJ_SENSOR:
{
mjCSensor* sensor = static_cast<mjCSensor*>(el);
if (sensor->plugin.active && sensor->plugin.name->empty() && sensor->GetRef() == 1) {
DeleteElement(sensor->plugin.element);
}
deletefromlist(object_lists_[mjOBJ_SENSOR], el);
break;
}
default:
deletefromlist(object_lists_[el->elemtype], el);
break;
@@ -621,6 +673,29 @@ void mjCModel::DeleteElement(mjsElement* el) {
// recursively delete all plugins in the subtree
void deletesubtreeplugin(mjCBody* subtree, mjCModel* model) {
mjsPlugin* plugin = &(subtree->spec.plugin);
if (plugin->active && plugin->name->empty()) {
model->DeleteElement(plugin->element);
}
for (auto* body : subtree->Bodies()) {
deletesubtreeplugin(body, model);
}
}
// deletes all plugins in the subtree and then the subtree itself
void mjCModel::Detach(mjCBody* subtree) {
if (subtree->GetRef() == 1) {
deletesubtreeplugin(subtree, this);
}
subtree->Release();
}
// TODO: we should not use C-type casting with multiple C++ inheritance
void mjCModel::CreateObjectLists() {
for (int i = 0; i < mjNOBJECT; ++i) {
@@ -688,28 +763,28 @@ mjCModel::~mjCModel() {
compiled = false;
// delete kinematic tree and all objects allocated in it
delete bodies_[0];
bodies_[0]->Release();
// delete objects allocated in mjCModel
for (int i=0; i<flexes_.size(); i++) delete flexes_[i];
for (int i=0; i<meshes_.size(); i++) delete meshes_[i];
for (int i=0; i<skins_.size(); i++) delete skins_[i];
for (int i=0; i<hfields_.size(); i++) delete hfields_[i];
for (int i=0; i<textures_.size(); i++) delete textures_[i];
for (int i=0; i<materials_.size(); i++) delete materials_[i];
for (int i=0; i<pairs_.size(); i++) delete pairs_[i];
for (int i=0; i<excludes_.size(); i++) delete excludes_[i];
for (int i=0; i<equalities_.size(); i++) delete equalities_[i];
for (int i=0; i<tendons_.size(); i++) delete tendons_[i]; // also deletes wraps
for (int i=0; i<actuators_.size(); i++) delete actuators_[i];
for (int i=0; i<sensors_.size(); i++) delete sensors_[i];
for (int i=0; i<numerics_.size(); i++) delete numerics_[i];
for (int i=0; i<texts_.size(); i++) delete texts_[i];
for (int i=0; i<tuples_.size(); i++) delete tuples_[i];
for (int i=0; i<keys_.size(); i++) delete keys_[i];
for (int i=0; i<flexes_.size(); i++) flexes_[i]->Release();
for (int i=0; i<meshes_.size(); i++) meshes_[i]->Release();
for (int i=0; i<skins_.size(); i++) skins_[i]->Release();
for (int i=0; i<hfields_.size(); i++) hfields_[i]->Release();
for (int i=0; i<textures_.size(); i++) textures_[i]->Release();
for (int i=0; i<materials_.size(); i++) materials_[i]->Release();
for (int i=0; i<pairs_.size(); i++) pairs_[i]->Release();
for (int i=0; i<excludes_.size(); i++) excludes_[i]->Release();
for (int i=0; i<equalities_.size(); i++) equalities_[i]->Release();
for (int i=0; i<tendons_.size(); i++) tendons_[i]->Release(); // also deletes wraps
for (int i=0; i<actuators_.size(); i++) actuators_[i]->Release();
for (int i=0; i<sensors_.size(); i++) sensors_[i]->Release();
for (int i=0; i<numerics_.size(); i++) numerics_[i]->Release();
for (int i=0; i<texts_.size(); i++) texts_[i]->Release();
for (int i=0; i<tuples_.size(); i++) tuples_[i]->Release();
for (int i=0; i<keys_.size(); i++) keys_[i]->Release();
for (int i=0; i<defaults_.size(); i++) delete defaults_[i];
for (int i=0; i<specs_.size(); i++) mj_deleteSpec(specs_[i]);
for (int i=0; i<plugins_.size(); i++) delete plugins_[i];
for (int i=0; i<plugins_.size(); i++) plugins_[i]->Release();
// clear sizes and pointer lists created in Compile
Clear();
@@ -933,6 +1008,7 @@ mjCPlugin* mjCModel::AddPlugin() {
// append spec to spec
void mjCModel::AppendSpec(mjSpec* spec) {
// TODO: check if the spec is already in the list
specs_.push_back(spec);
}
+8 -1
View File
@@ -221,6 +221,9 @@ class mjCModel : public mjCModel_, private mjSpec {
// delete object from the corresponding list
void DeleteElement(mjsElement* el);
// detach subtree from model
void Detach(mjCBody* subtree);
// API for access to model elements (outside tree)
int NumObjects(mjtObj type); // number of objects in specified list
mjCBase* GetObject(mjtObj type, int id); // pointer to specified object
@@ -307,6 +310,9 @@ class mjCModel : public mjCModel_, private mjSpec {
// get the spec from which this model was created
mjSpec* GetSourceSpec() const;
// set deepcopy flag
void SetDeepCopy(bool deepcopy) { deepcopy_ = deepcopy; }
private:
// settings for each defaults class
std::vector<mjCDef*> defaults_;
@@ -351,7 +357,7 @@ class mjCModel : public mjCModel_, private mjSpec {
std::vector<mjCTuple*> tuples_; // list of tuple fields
std::vector<mjCKey*> keys_; // list of keyframe fields
std::vector<mjCPlugin*> plugins_; // list of plugin instances
std::vector<mjSpec*> specs_; // list of specs
std::vector<mjSpec*> specs_; // list of attached specs
// pointers to objects created inside kinematic tree
std::vector<mjCBody*> bodies_; // list of bodies
@@ -410,5 +416,6 @@ class mjCModel : public mjCModel_, private mjSpec {
mjListKeyMap ids; // map from object names to ids
mjCError errInfo; // last error info
std::vector<mjKeyInfo> key_pending_; // attached keyframes
bool deepcopy_; // copy objects when attaching
};
#endif // MUJOCO_SRC_USER_USER_MODEL_H_
+136 -72
View File
@@ -763,6 +763,7 @@ mjCBody::mjCBody(mjCModel* _model) {
model = _model;
if (_model) compiler = &_model->spec.compiler;
refcount = 1;
mjs_defaultBody(&spec);
elemtype = mjOBJ_BODY;
parent = nullptr;
@@ -872,22 +873,28 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) {
other.model->prefix = other.prefix;
other.model->suffix = other.suffix;
other.model->StoreKeyframes(model);
mjCModel* other_model = other.model;
// attach defaults
if (other.model != model) {
mjCDef* subdef = new mjCDef(*other.model->Default());
subdef->NameSpace(other.model);
if (other_model != model) {
mjCDef* subdef = new mjCDef(*other_model->Default());
subdef->NameSpace(other_model);
*model += *subdef;
}
// copy input frame
mjSpec* origin = model->FindSpec(other.compiler);
frames.push_back(new mjCFrame(other));
mjCFrame* newframe(model->deepcopy_ ? new mjCFrame(other) : (mjCFrame*)&other);
frames.push_back(newframe);
frames.back()->body = this;
frames.back()->model = model;
frames.back()->compiler = origin ? &origin->compiler : &model->spec.compiler;
frames.back()->frame = other.frame;
frames.back()->NameSpace(other.model);
if (model->deepcopy_) {
frames.back()->NameSpace(other_model);
} else {
frames.back()->AddRef();
}
int i = frames.size();
last_attached = &frames.back()->spec;
@@ -909,30 +916,43 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) {
CopyList(cameras, subtree->cameras, fmap, &other);
CopyList(lights, subtree->lights, fmap, &other);
if (!model->deepcopy_) {
subtree->SetModel(model);
subtree->NameSpace(other_model);
}
int nbodies = (int)subtree->bodies.size();
for (int i=0; i<nbodies; i++) {
if (!other.IsAncestor(subtree->bodies[i]->frame)) {
continue;
}
bodies.push_back(new mjCBody(*subtree->bodies[i], model)); // triggers recursive call
if (model->deepcopy_) {
mjCBody* newbody(new mjCBody(*subtree->bodies[i], model)); // triggers recursive call
bodies.push_back(newbody);
subtree->bodies[i]->ForgetKeyframes();
bodies.back()->NameSpace_(other_model, /*propagate=*/ false);
} else {
bodies.push_back(subtree->bodies[i]);
bodies.back()->SetModel(model);
bodies.back()->ResetId();
bodies.back()->AddRef();
}
bodies.back()->parent = this;
bodies.back()->frame =
subtree->bodies[i]->frame ? frames[fmap[subtree->bodies[i]->frame]] : nullptr;
bodies.back()->NameSpace_(other.model, /*propagate=*/ false);
subtree->bodies[i]->ForgetKeyframes();
}
// attach referencing elements
*model += *other.model;
*model += *other_model;
// leave the source model in a clean state
if (other.model != model) {
other.model->key_pending_.clear();
if (other_model != model) {
other_model->key_pending_.clear();
}
// clear namespace and return body
other.model->prefix.clear();
other.model->suffix.clear();
other_model->prefix.clear();
other_model->suffix.clear();
return *this;
}
@@ -948,7 +968,8 @@ void mjCBody::CopyList(std::vector<T*>& dst, const std::vector<T*>& src,
continue; // skip if the element is not inside pframe
}
mjSpec* origin = model->FindSpec(src[i]->compiler);
dst.push_back(new T(*src[i]));
T* new_obj = model->deepcopy_ ? new T(*src[i]) : src[i];
dst.push_back(new_obj);
dst.back()->body = this;
dst.back()->model = model;
dst.back()->compiler = origin ? &origin->compiler : &model->spec.compiler;
@@ -956,6 +977,11 @@ void mjCBody::CopyList(std::vector<T*>& dst, const std::vector<T*>& src,
dst.back()->CopyPlugin();
dst.back()->classname = src[i]->classname;
// increment refcount if shallow copy is made
if (!model->deepcopy_) {
dst.back()->AddRef();
}
// assign dst frame to src frame
dst.back()->frame = src[i]->frame ? frames[fmap[src[i]->frame]] : nullptr;
@@ -981,6 +1007,73 @@ mjCBody& mjCBody::operator-=(const mjCBody& subtree) {
// set model of this body and its subtree
void mjCBody::SetModel(mjCModel* _model) {
model = _model;
mjSpec* origin = model->FindSpec(mjs_getString(model->spec.modelname));
compiler = origin ? &origin->compiler : &model->spec.compiler;
for (auto& body : bodies) {
body->SetModel(_model);
}
for (auto& frame : frames) {
frame->model = _model;
frame->compiler = compiler;
}
for (auto& geom : geoms) {
geom->model = _model;
geom->compiler = compiler;
}
for (auto& joint : joints) {
joint->model = _model;
joint->compiler = compiler;
}
for (auto& site : sites) {
site->model = _model;
site->compiler = compiler;
}
for (auto& camera : cameras) {
camera->model = _model;
camera->compiler = compiler;
}
for (auto& light : lights) {
light->model = _model;
light->compiler = compiler;
}
}
// reset ids of all objects in this body
void mjCBody::ResetId() {
id = -1;
for (auto& body : bodies) {
body->ResetId();
}
for (auto& frame : frames) {
frame->id = -1;
}
for (auto& geom : geoms) {
geom->id = -1;
}
for (auto& joint : joints) {
joint->id = -1;
joint->qposadr_ = -1;
joint->dofadr_ = -1;
}
for (auto& site : sites) {
site->id = -1;
}
for (auto& camera : cameras) {
camera->id = -1;
}
for (auto& light : lights) {
light->id = -1;
}
}
void mjCBody::PointToLocal() {
spec.element = static_cast<mjsElement*>(this);
spec.name = &name;
@@ -1012,26 +1105,13 @@ void mjCBody::CopyPlugin() {
// destructor
mjCBody::~mjCBody() {
// delete objects allocated here
for (int i=0; i<bodies.size(); i++) delete bodies[i];
for (int i=0; i<geoms.size(); i++) delete geoms[i];
for (int i=0; i<frames.size(); i++) delete frames[i];
for (int i=0; i<joints.size(); i++) delete joints[i];
for (int i=0; i<sites.size(); i++) delete sites[i];
for (int i=0; i<cameras.size(); i++) delete cameras[i];
for (int i=0; i<lights.size(); i++) delete lights[i];
bodies.clear();
geoms.clear();
frames.clear();
joints.clear();
sites.clear();
cameras.clear();
lights.clear();
if (spec.plugin.active && spec.plugin.name->empty() && model) {
model->DeleteElement(spec.plugin.element);
}
for (int i=0; i<bodies.size(); i++) bodies[i]->Release();
for (int i=0; i<geoms.size(); i++) geoms[i]->Release();
for (int i=0; i<frames.size(); i++) frames[i]->Release();
for (int i=0; i<joints.size(); i++) joints[i]->Release();
for (int i=0; i<sites.size(); i++) sites[i]->Release();
for (int i=0; i<cameras.size(); i++) cameras[i]->Release();
for (int i=0; i<lights.size(); i++) lights[i]->Release();
}
@@ -1840,19 +1920,27 @@ mjCFrame& mjCFrame::operator+=(const mjCBody& other) {
other.model->StoreKeyframes(model);
other.model->prefix = "";
other.model->suffix = "";
mjCModel* other_model = other.model;
mjCBody* subtree = new mjCBody(other, model);
other.ForgetKeyframes();
other.model->prefix = subtree->prefix;
other.model->suffix = subtree->suffix;
// attach or copy the subtree
mjCBody* subtree = model->deepcopy_ ? new mjCBody(other, model) : (mjCBody*)&other;
if (model->deepcopy_) {
other.ForgetKeyframes();
} else {
subtree->SetModel(model);
subtree->ResetId();
subtree->AddRef();
}
other_model->prefix = subtree->prefix;
other_model->suffix = subtree->suffix;
subtree->SetParent(body);
subtree->SetFrame(this);
subtree->NameSpace(other.model);
subtree->NameSpace(other_model);
// attach defaults
if (other.model != model) {
mjCDef* subdef = new mjCDef(*other.model->Default());
subdef->NameSpace(other.model);
if (other_model != model) {
mjCDef* subdef = new mjCDef(*other_model->Default());
subdef->NameSpace(other_model);
*model += *subdef;
}
@@ -1861,16 +1949,16 @@ mjCFrame& mjCFrame::operator+=(const mjCBody& other) {
last_attached = &body->bodies.back()->spec;
// attach referencing elements
*model += *other.model;
*model += *other_model;
// leave the source model in a clean state
if (other.model != model) {
other.model->key_pending_.clear();
if (other_model != model) {
other_model->key_pending_.clear();
}
// clear suffixes and return
other.model->suffix.clear();
other.model->prefix.clear();
other_model->suffix.clear();
other_model->prefix.clear();
return *this;
}
@@ -2221,14 +2309,6 @@ mjCGeom::mjCGeom(const mjCGeom& other) {
mjCGeom::~mjCGeom() {
if (spec.plugin.active && spec.plugin.name->empty() && model) {
model->DeleteElement(spec.plugin.element);
}
}
mjCGeom& mjCGeom::operator=(const mjCGeom& other) {
if (this != &other) {
this->spec = other.spec;
@@ -4591,7 +4671,7 @@ void mjCMaterial::CopyFromSpec() {
void mjCMaterial::NameSpace(const mjCModel* m) {
mjCBase::NameSpace(m);
for (int i=0; i<mjNTEXROLE; i++) {
if (!spec_textures_[i].empty() && model != m) {
if (!spec_textures_[i].empty()) {
spec_textures_[i] = m->prefix + spec_textures_[i] + m->suffix;
}
}
@@ -5657,14 +5737,6 @@ mjCActuator::mjCActuator(const mjCActuator& other) {
mjCActuator::~mjCActuator() {
if (spec.plugin.active && spec.plugin.name->empty() && model) {
model->DeleteElement(spec.plugin.element);
}
}
mjCActuator& mjCActuator::operator=(const mjCActuator& other) {
if (this != &other) {
this->spec = other.spec;
@@ -6033,14 +6105,6 @@ mjCSensor::mjCSensor(const mjCSensor& other) {
mjCSensor::~mjCSensor() {
if (spec.plugin.active && spec.plugin.name->empty() && model) {
model->DeleteElement(spec.plugin.element);
}
}
mjCSensor& mjCSensor::operator=(const mjCSensor& other) {
if (this != &other) {
this->spec = other.spec;
+23 -3
View File
@@ -224,9 +224,23 @@ class mjCBase : public mjCBase_ {
virtual void ForgetKeyframes() {}
virtual void ForgetKeyframes() const {}
// increment and decrement reference count
// release uses the argument to delete the plugin
// which may be still owned by the source spec during shallow attach
virtual void AddRef() { ++refcount; }
virtual int GetRef() { return refcount; }
virtual void Release() {
if (--refcount == 0) {
delete this;
}
}
protected:
mjCBase(); // constructor
mjCBase(const mjCBase& other); // copy constructor
// reference count for allowing deleting an attached object
int refcount = 1;
};
@@ -349,6 +363,15 @@ class mjCBody : public mjCBody_, private mjsBody {
void SetParent(mjCBody* _body) { parent = _body; }
mjCBody* GetParent() const { return parent; }
// set model of this body
void SetModel(mjCModel* _model);
// reset ids of all objects in this body
void ResetId();
// getters
std::vector<mjCBody*> Bodies() const { return bodies; }
private:
mjCBody(const mjCBody& other, mjCModel* _model); // copy constructor
mjCBody& operator=(const mjCBody& other); // copy assignment
@@ -537,7 +560,6 @@ class mjCGeom : public mjCGeom_, private mjsGeom {
mjCGeom(mjCModel* = nullptr, mjCDef* = nullptr);
mjCGeom(const mjCGeom& other);
mjCGeom& operator=(const mjCGeom& other);
~mjCGeom();
using mjCBase::name;
mjsGeom spec; // variables set by user
@@ -1505,7 +1527,6 @@ class mjCActuator : public mjCActuator_, private mjsActuator {
mjCActuator(mjCModel* = nullptr, mjCDef* = nullptr);
mjCActuator(const mjCActuator& other);
mjCActuator& operator=(const mjCActuator& other);
~mjCActuator();
mjsActuator spec;
using mjCBase::name;
@@ -1567,7 +1588,6 @@ class mjCSensor : public mjCSensor_, private mjsSensor {
mjCSensor(mjCModel*);
mjCSensor(const mjCSensor& other);
mjCSensor& operator=(const mjCSensor& other);
~mjCSensor();
mjsSensor spec;
using mjCBase::name;
+6
View File
@@ -945,10 +945,16 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) {
Keyframe(section);
}
// set deepcopy flag to true to copy child specs during attach calls
mjs_setDeepCopy(spec, true);
for (XMLElement* section = FirstChildElement(root, "worldbody"); section;
section = NextSiblingElement(section, "worldbody")) {
Body(section, mjs_findBody(spec, "world"), nullptr, vfs);
}
// set deepcopy flag to false to disable copying during attach in all future calls
mjs_setDeepCopy(spec, false);
}
+121 -38
View File
@@ -262,35 +262,48 @@ TEST_F(PluginTest, AttachPlugin) {
</mujoco>)";
std::array<char, 1000> err;
mjSpec* spec_1 = mj_parseXMLString(xml_1, 0, err.data(), err.size());
mjSpec* parent = mj_parseXMLString(xml_1, 0, err.data(), err.size());
ASSERT_THAT(parent, NotNull()) << err.data();
mjSpec* spec_1 = mj_parseXMLString(xml_2, 0, err.data(), err.size());
ASSERT_THAT(spec_1, NotNull()) << err.data();
mjSpec* spec_2 = mj_parseXMLString(xml_2, 0, err.data(), err.size());
ASSERT_THAT(spec_2, NotNull()) << err.data();
mjsBody* body_1 = mjs_findBody(spec_1, "body");
// do a copy before attaching
mjSpec* spec_2 = mj_copySpec(spec_1);
mjs_setString(spec_2->modelname, "first_copy");
mjSpec* spec_3 = mj_copySpec(spec_1);
mjs_setString(spec_3->modelname, "second_copy");
ASSERT_THAT(spec_3, NotNull()) << err.data();
// attach a body referencing the plugin to the frame and compile
mjsBody* body_1 = mjs_findBody(parent, "body");
EXPECT_THAT(body_1, NotNull());
mjsFrame* attachment_frame = mjs_addFrame(body_1, 0);
EXPECT_THAT(attachment_frame, NotNull());
mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "body"), "child-", "");
mjModel* model_1 = mj_compile(spec_1, nullptr);
mjs_attachBody(attachment_frame, mjs_findBody(spec_1, "body"), "child-", "");
mjModel* model_1 = mj_compile(parent, nullptr);
EXPECT_THAT(model_1, NotNull());
EXPECT_THAT(model_1->nbody, 3);
// attach it a second time to test namespacing
// attach it a second time to test namespacing and compile
ASSERT_THAT(spec_2, NotNull()) << err.data();
mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "body"), "copy-", "");
mjModel* model_2 = mj_compile(spec_1, nullptr);
mjModel* model_2 = mj_compile(parent, nullptr);
EXPECT_THAT(model_2, NotNull());
EXPECT_THAT(model_2->nbody, 4);
// attach a body not referencing the plugin
mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "empty"), "empty-", "");
mjModel* model_3 = mj_compile(spec_1, nullptr);
// attach a body not referencing the plugin and compile
mjs_attachBody(attachment_frame, mjs_findBody(spec_3, "empty"), "empty-", "");
mjModel* model_3 = mj_compile(parent, nullptr);
EXPECT_THAT(model_3, NotNull());
EXPECT_THAT(model_3->nbody, 5);
mj_deleteModel(model_1);
mj_deleteModel(model_2);
mj_deleteModel(model_3);
mj_deleteSpec(parent);
mj_deleteSpec(spec_1);
mj_deleteSpec(spec_2);
mj_deleteSpec(spec_3);
}
TEST_F(PluginTest, AttachExplicitPlugin) {
@@ -858,6 +871,7 @@ TEST_F(MujocoTest, AttachSame) {
// create parent
mjSpec* parent = mj_parseXMLString(xml_child, 0, er.data(), er.size());
EXPECT_THAT(parent, NotNull()) << er.data();
mjs_setDeepCopy(parent, true); // needed for self-attach
// get frame
mjsFrame* frame = mjs_findFrame(parent, "frame");
@@ -903,7 +917,7 @@ TEST_F(MujocoTest, AttachDifferent) {
std::string field = "";
static constexpr char xml_parent[] = R"(
<mujoco>
<mujoco model="parent">
<default>
<default class="geom_size">
<geom size="0.1"/>
@@ -924,7 +938,7 @@ TEST_F(MujocoTest, AttachDifferent) {
</mujoco>)";
static constexpr char xml_result[] = R"(
<mujoco>
<mujoco model="parent">
<default>
<default class="geom_size">
<geom size="0.1"/>
@@ -1048,7 +1062,7 @@ TEST_F(MujocoTest, AttachFrame) {
std::string field = "";
static constexpr char xml_parent[] = R"(
<mujoco>
<mujoco model="parent">
<worldbody>
<body name="sphere">
<freejoint/>
@@ -1063,7 +1077,7 @@ TEST_F(MujocoTest, AttachFrame) {
</mujoco>)";
static constexpr char xml_result[] = R"(
<mujoco>
<mujoco model="parent">
<default>
<default class="attached-cylinder-1">
<geom type="cylinder" size=".1 1 0"/>
@@ -1371,13 +1385,15 @@ TEST_F(MujocoTest, AttachWorld) {
mjSpec* parent = mj_parseXMLString(xml_parent, 0, er.data(), er.size());
EXPECT_THAT(parent, NotNull()) << er.data();
mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size());
EXPECT_THAT(child, NotNull()) << er.data();
mjSpec* child1 = mj_parseXMLString(xml_child, 0, er.data(), er.size());
EXPECT_THAT(child1, NotNull()) << er.data();
mjSpec* child2 = mj_parseXMLString(xml_child, 0, er.data(), er.size());
EXPECT_THAT(child2, NotNull()) << er.data();
// attach a body to the frame
mjsFrame* frame = mjs_findFrame(parent, "frame");
EXPECT_THAT(frame, NotNull());
mjsBody* body = mjs_findBody(child, "sphere");
mjsBody* body = mjs_findBody(child1, "sphere");
EXPECT_THAT(body, NotNull());
mjsBody* attached = mjs_attachBody(frame, body, "attached-", "-1");
EXPECT_THAT(attached, NotNull());
@@ -1385,7 +1401,7 @@ TEST_F(MujocoTest, AttachWorld) {
EXPECT_THAT(model1, NotNull());
// attach the world to the same frame and convert it to a frame
mjsBody* world = mjs_findBody(child, "world");
mjsBody* world = mjs_findBody(child2, "world");
EXPECT_THAT(world, NotNull());
mjsBody* child_world = mjs_attachBody(frame, world, "attached-", "-2");
EXPECT_THAT(child_world, NotNull());
@@ -1403,7 +1419,8 @@ TEST_F(MujocoTest, AttachWorld) {
<< "Different field: " << field << '\n';
mj_deleteSpec(parent);
mj_deleteSpec(child);
mj_deleteSpec(child1);
mj_deleteSpec(child2);
mj_deleteModel(model1);
mj_deleteModel(model2);
mj_deleteModel(expected);
@@ -1577,11 +1594,13 @@ TEST_F(MujocoTest, RecompileAttach) {
mjSpec* parent = mj_makeSpec();
EXPECT_THAT(parent, NotNull());
mjSpec* child = mj_parseXMLString(xml, 0, er.data(), er.size());
EXPECT_THAT(child, NotNull());
mjSpec* child1 = mj_parseXMLString(xml, 0, er.data(), er.size());
EXPECT_THAT(child1, NotNull());
mjSpec* child2 = mj_parseXMLString(xml, 0, er.data(), er.size());
EXPECT_THAT(child2, NotNull());
mjsFrame* frame1 = mjs_addFrame(mjs_findBody(parent, "world"), 0);
mjs_attachBody(frame1, mjs_findBody(child, "body"), "child-", "-1");
mjs_attachBody(frame1, mjs_findBody(child1, "body"), "child-", "-1");
mjModel* model = mj_compile(parent, 0);
EXPECT_THAT(model, NotNull());
@@ -1594,7 +1613,7 @@ TEST_F(MujocoTest, RecompileAttach) {
}
mjsFrame* frame2 = mjs_addFrame(mjs_findBody(parent, "world"), 0);
mjs_attachBody(frame2, mjs_findBody(child, "body"), "child-", "-2");
mjs_attachBody(frame2, mjs_findBody(child2, "body"), "child-", "-2");
EXPECT_EQ(mj_recompile(parent, 0, model, data), 0);
EXPECT_THAT(model, NotNull());
@@ -1604,7 +1623,8 @@ TEST_F(MujocoTest, RecompileAttach) {
mj_deleteData(data);
mj_deleteModel(model);
mj_deleteSpec(child);
mj_deleteSpec(child1);
mj_deleteSpec(child2);
mj_deleteSpec(parent);
}
@@ -1637,6 +1657,7 @@ TEST_F(MujocoTest, AttachMocap) {
mjSpec* spec = mj_parseXMLString(xml, 0, er.data(), er.size());
EXPECT_THAT(spec, NotNull()) << er.data();
mjs_setDeepCopy(spec, true); // needed for self-attach
mjsBody* body = mjs_findBody(spec, "mocap");
EXPECT_THAT(body, NotNull());
@@ -1732,8 +1753,8 @@ TEST_F(MujocoTest, AttachUnnamedAssets) {
EXPECT_STREQ(mj_id2name(model, mjOBJ_MESH, 0), "_cube");
mj_deleteVFS(vfs.get());
mj_deleteSpec(child);
mj_deleteSpec(spec);
mj_deleteSpec(child);
mj_deleteModel(model);
}
@@ -1866,6 +1887,9 @@ void AttachNestedKeyframe(bool compile) {
mjSpec* gchild = mj_parseXMLString(gchild_xml, 0, er.data(), er.size());
EXPECT_THAT(gchild, NotNull()) << er.data();
mjs_setDeepCopy(parent, true);
mjs_setDeepCopy(child, true);
// attach gchild to child
mjs_attachBody(mjs_findFrame(child, "frame"),
mjs_findBody(gchild, "body"), "gchild-", "");
@@ -1937,18 +1961,18 @@ TEST_F(MujocoTest, RepeatedAttachKeyframe) {
</mujoco>)";
std::array<char, 1000> er;
mjSpec* spec_1 = mj_parseXMLString(xml_1, 0, er.data(), er.size());
EXPECT_THAT(spec_1, NotNull()) << er.data();
mjSpec* spec_2 = mj_parseXMLString(xml_2, 0, er.data(), er.size());
EXPECT_THAT(spec_2, NotNull()) << er.data();
mjSpec* parent = mj_parseXMLString(xml_1, 0, er.data(), er.size());
EXPECT_THAT(parent, NotNull()) << er.data();
mjSpec* child = mj_parseXMLString(xml_2, 0, er.data(), er.size());
EXPECT_THAT(child, NotNull()) << er.data();
mjsBody* body_1 = mjs_findBody(spec_1, "body");
mjsBody* body_1 = mjs_findBody(parent, "body");
mjsFrame* attachment_frame = mjs_addFrame(body_1, 0);
mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "b1"), "b1-", "");
mjModel* model_1 = mj_compile(spec_1, 0);
mjs_attachBody(attachment_frame, mjs_findBody(child, "b1"), "b1-", "");
mjModel* model_1 = mj_compile(parent, 0);
EXPECT_THAT(model_1, NotNull());
mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "b2"), "b2-", "");
mjModel* model_2 = mj_compile(spec_1, 0);
mjs_attachBody(attachment_frame, mjs_findBody(child, "b2"), "b2-", "");
mjModel* model_2 = mj_compile(parent, 0);
EXPECT_THAT(model_2, NotNull());
EXPECT_EQ(model_1->nkey, 1);
@@ -1956,8 +1980,8 @@ TEST_F(MujocoTest, RepeatedAttachKeyframe) {
EXPECT_STREQ(mj_id2name(model_2, mjOBJ_KEY, 0), "b1-home");
EXPECT_STREQ(mj_id2name(model_2, mjOBJ_KEY, 1), "b2-home");
mj_deleteSpec(spec_1);
mj_deleteSpec(spec_2);
mj_deleteSpec(parent);
mj_deleteSpec(child);
mj_deleteModel(model_1);
mj_deleteModel(model_2);
}
@@ -2177,5 +2201,64 @@ TEST_F(MujocoTest, CopyAttachedSpec) {
mj_deleteVFS(vfs.get());
}
TEST_F(MujocoTest, ApplyNameSpaceToDefaults) {
static constexpr char xml_c[] = R"(
<mujoco>
<default>
<default class="mesh">
<mesh scale="0.001 0.001 0.001"/>
</default>
</default>
<asset>
<mesh file="cube.obj" class="mesh"/>
</asset>
<worldbody>
<body name="body">
<geom type="mesh" mesh="cube"/>
</body>
</worldbody>
</mujoco>)";
static constexpr char xml_p[] = R"(
<mujoco>
<worldbody>
<frame name="parent"/>
</worldbody>
</mujoco>
)";
static constexpr char cube[] = R"(
v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v 0.500000 0.500000 0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 0.500000 -0.500000
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000)";
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "cube.obj", cube, sizeof(cube));
std::array<char, 1024> err;
mjSpec* child = mj_parseXMLString(xml_c, vfs.get(), err.data(), err.size());
EXPECT_THAT(child, NotNull()) << err.data();
mjSpec* parent = mj_parseXMLString(xml_p, 0, err.data(), err.size());
EXPECT_THAT(parent, NotNull()) << err.data();
mjsBody* attached = mjs_attachBody(mjs_findFrame(parent, "parent"),
mjs_findBody(child, "body"), "child-", "");
EXPECT_THAT(attached, NotNull());
mjModel* model = mj_compile(parent, vfs.get());
EXPECT_THAT(model, NotNull());
mj_deleteSpec(child);
mj_deleteSpec(parent);
mj_deleteModel(model);
mj_deleteVFS(vfs.get());
}
} // namespace
} // namespace mujoco
+1
View File
@@ -545,6 +545,7 @@ TEST_F(MujocoTest, Modeldir) {
// parent attaching the child
mjSpec* spec = mj_makeSpec();
mjs_setDeepCopy(spec, true);
mjs_setString(spec->meshdir, "asset");
mjs_attachFrame(mjs_findBody(spec, "world"), frame, "_", "");
mjModel* model = mj_compile(spec, vfs.get());