diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index 7b58caae..cec8f03d 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -147,8 +147,10 @@ or :ref:`attach a frame to a body`: Note that in the above examples, the parent and child models have different values for ``compiler.degree``, corresponding to the :ref:`compiler/angle` attribute, specifying the units in which angles are -interperted. Compiler options are carried over during attachment, so the child model will be compiled using X, while the -parent will be compiled using Y. +interperted. Compiler flags are carried over during attachment, so the child model will be compiled using the child +flags, while the parent will be compiled using the parent flags. + +Note also that once a child is attached by reference to a parent, the child cannot be compiled on its own. .. _meDefault: diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 9236c848..2cb2b592 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3972,6 +3972,9 @@ mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) { warningtext[0] = 0; try { + if (attached_) { + throw mjCError(0, "cannot compile child spec if attached by reference to a parent spec"); + } if (setjmp(error_jmp_buf) != 0) { // TryCompile resulted in an mju_error which was converted to a longjmp. std::string error_msg = errortext; diff --git a/src/user/user_model.h b/src/user/user_model.h index ac36f6dc..e5ff19f3 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -317,6 +317,9 @@ class mjCModel : public mjCModel_, private mjSpec { // set deepcopy flag void SetDeepCopy(bool deepcopy) { deepcopy_ = deepcopy; } + // set attached flag + void SetAttached(bool deepcopy) { attached_ |= !deepcopy; } + private: // settings for each defaults class std::vector defaults_; @@ -428,5 +431,6 @@ class mjCModel : public mjCModel_, private mjSpec { mjCError errInfo; // last error info std::vector key_pending_; // attached keyframes bool deepcopy_; // copy objects when attaching + bool attached_ = false; // true if model is attached to a parent model }; #endif // MUJOCO_SRC_USER_USER_MODEL_H_ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 42194aee..eed727be 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -941,6 +941,7 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { } // attach referencing elements + other_model->SetAttached(model->deepcopy_); *model += *other_model; // leave the source model in a clean state @@ -1947,6 +1948,7 @@ mjCFrame& mjCFrame::operator+=(const mjCBody& other) { last_attached = &body->bodies.back()->spec; // attach referencing elements + other_model->SetAttached(model->deepcopy_); *model += *other_model; // leave the source model in a clean state diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 5a3db2e8..1d0941fe 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -2544,5 +2544,30 @@ TEST_F(MujocoTest, ApplyNameSpaceToDefaults) { mj_deleteVFS(vfs.get()); } +TEST_F(MujocoTest, ErrorWhenCompilingOrphanedSpec) { + static constexpr char xml[] = R"( + + + + + + )"; + std::array er; + mjSpec* child = mj_parseXMLString(xml, 0, er.data(), er.size()); + EXPECT_THAT(child, NotNull()) << er.data(); + mjSpec* parent = mj_makeSpec(); + EXPECT_THAT(parent, NotNull()); + mjsBody* body = mjs_findBody(child, "a"); + EXPECT_THAT(body, NotNull()); + mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); + EXPECT_THAT(frame, NotNull()); + mjs_attachBody(frame, body, "child-", ""); + mj_deleteSpec(parent); + mjModel* model = mj_compile(child, 0); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(mjs_getError(child), HasSubstr("by reference to a parent")); + mj_deleteSpec(child); +} + } // namespace } // namespace mujoco