Add "frame" attribute to <attach> element.

The `<attach>` element now supports a "frame" attribute, allowing users to specify either a "body" or a "frame" to attach to, but not both. The XML parser has been updated to handle this new attribute and the mutual exclusivity constraint.

PiperOrigin-RevId: 941245904
Change-Id: I4c0edeed5f3a9e456aed96c373b1647fe79841d9
This commit is contained in:
Yuval Tassa
2026-07-01 12:49:01 -07:00
committed by Copybara-Service
parent 1f6cf4035c
commit c6c3ec3149
5 changed files with 77 additions and 27 deletions
+29 -12
View File
@@ -3996,33 +3996,50 @@ Associate this body with an :ref:`engine plugin<exPlugin>`. Either :at:`plugin`
:el-prefix:`body/` |-| **attach** |*|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :el:`attach` element is used to insert a sub-tree of bodies from another model into this model's kinematic tree.
The :el:`attach` element is used to insert elements from another (child) model into this (parent) model's kinematic tree.
Unlike :ref:`include<include>`, which is implemented in the parser and is equivalent to copying and pasting XML from one
file into another, :el:`attach` is implemented in the model compiler. In order to use this element, the sub-model must
first be defined as an :ref:`asset<asset-model>`. When creating an attachment, the top body of the attached subtree is
specified, and all referencing elements outside the kinematic tree (e.g., sensors and actuators), are also copied into
the top-level model. Additionally, any elements referenced from within the attached subtree (e.g. defaults and assets)
will be copied in to the top-level model. :el:`attach` is a :ref:`meta-element`, so upon saving all attachments will
first be defined as an :ref:`asset<asset-model>`. When creating an attachment, a frame, body or the entire child model in the
child model is specified, and all referencing elements outside the kinematic tree (e.g., sensors and actuators), are also copied into
the parent model. Additionally, any elements referenced from within the attached subtree (e.g. defaults and assets)
will be copied in to the parent model. :el:`attach` is a :ref:`meta-element`, so upon saving all attachments will
appear in the saved XML file. Note that this element is a subset of the functionality of the procedural
:ref:`attachment<meAttachment>` functionality. As such, it shares the same limitations as described there. In addition,
when the :el:`attach` element is used, it is not possible to attach an entire model (i.e. including all elements,
referenced or not).
:ref:`attachment<meAttachment>` functionality. As such, it shares the same limitations as described there. See example `here
<https://github.com/google-deepmind/mujoco/blob/main/test/xml/testdata/parent.xml>`__.
.. admonition:: Known issues
:class: note
The following known limitations exist, to be addressed in a future release:
- All assets from the child model will be copied in, whether they are referenced or not.
- Circular references are not checked for and will lead to infinite loops.
- When attaching a model with :ref:`keyframes<keyframe>`, model compilation is required for the re-indexing to be
finalized. If a second attachment is performed without compilation, the keyframes from the first attachment will be
lost.
.. _body-attach-model:
:at:`model`: :at-val:`string, required`
The sub-model from which to attach a subtree.
The child model from which to attach a subtree or a frame.
.. _body-attach-body:
:at:`body`: :at-val:`string, optional`
Name of the body in the sub-model to attach here. The body and its subtree will be attached. If this attribute is not
specified, the contents of the world body will be attached in a new :ref:`frame<body-frame>`.
Name of the body in the child model to attach here. The body and its subtree will be attached. If neither this
attribute nor :ref:`frame<body-attach-frame>` is specified (only one allowed), the contents of the world body will
be attached in a new :ref:`frame<body-frame>`.
.. _body-attach-frame:
:at:`frame`: :at-val:`string, optional`
Name of the frame in the child model to attach here. If neither this attribute nor :ref:`body<body-attach-body>` is
specified (only one allowed), the contents of the world body will be attached in a new :ref:`frame<body-frame>`.
.. _body-attach-prefix:
:at:`prefix`: :at-val:`string, required`
Prefix to prepend to names of elements in the sub-model. This attribute is required to prevent name collisions with
Prefix to prepend to names of elements in the child model. This attribute is required to prevent name collisions with
the parent or when attaching the same sub-tree multiple times.
Executable → Regular
+3
View File
@@ -983,6 +983,9 @@
.. grid-item::
:ref:`body<body-attach-body>`
.. grid-item::
:ref:`frame<body-attach-frame>`
.. grid-item::
:ref:`prefix<body-attach-prefix>`
+17 -12
View File
@@ -470,7 +470,7 @@ std::vector<const char*> MJCF[nMJCF] = {
{"config", "*", "key", "value"},
{">"},
{">"},
{"attach", "*", "model", "body", "prefix"},
{"attach", "*", "model", "body", "frame", "prefix"},
{"site", "*", "name", "class", "type", "group", "pos", "quat", "material",
"size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"},
{"camera", "*", "name", "class", "projection",
@@ -4170,35 +4170,40 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame,
// attachment
else if (name == "attach") {
string model_name, body_name, prefix;
string model_name, child_name, prefix;
ReadAttrTxt(elem, "model", model_name, /*required=*/true);
ReadAttrTxt(elem, "body", body_name, /*required=*/false);
bool has_body = ReadAttrTxt(elem, "body", child_name, /*required=*/false);
bool has_frame = ReadAttrTxt(elem, "frame", child_name, /*required=*/false);
ReadAttrTxt(elem, "prefix", prefix, /*required=*/true);
mjsBody* child_body = mjs_findBody(spec, (prefix+body_name).c_str());
if (has_body && has_frame) {
throw mjXError(elem, "only one of body or frame can be specified in attach");
}
mjtObj type = has_body ? mjOBJ_BODY : mjOBJ_FRAME;
mjsElement* child = mjs_findElement(spec, type, (prefix+child_name).c_str());
mjsFrame* pframe = frame ? frame : mjs_addFrame(body, nullptr);
if (!child_body) {
if (!child) {
mjSpec* asset = mjs_findSpec(spec, model_name.c_str());
if (!asset) {
throw mjXError(elem, "could not find model '%s'", model_name.c_str());
}
mjsElement* child;
if (body_name.empty()) {
if (child_name.empty()) {
child = asset->element;
} else {
child_body = mjs_findBody(asset, body_name.c_str());
if (!child_body) {
throw mjXError(elem, "could not find body '%s''%s'", body_name.c_str());
child = mjs_findElement(asset, type, child_name.c_str());
if (!child) {
throw mjXError(elem, "could not find %s",
(string(mju_type2Str(type)) + " '" + child_name + "'").c_str());
}
child = child_body->element;
}
if (!mjs_attach(pframe->element, child, prefix.c_str(), "")) {
throw mjXError(elem, "%s", stripError(mjs_getError(spec)));
}
} else {
// only set frame to existing body
if (mjs_setFrame(child_body->element, pframe)) {
if (mjs_setFrame(child, pframe)) {
throw mjXError(elem, "%s", stripError(mjs_getError(spec)));
}
}
+12 -1
View File
@@ -1,7 +1,18 @@
<mujoco model="child">
<worldbody>
<body name="body">
<geom name="geom" size="1" pos="2 0 0"/>
<geom name="geom" size="1" pos="0 0 -2" rgba="1 0 0 1"/>
</body>
<frame name="frame">
<body name="body2" pos="-1 0 0">
<freejoint/>
<geom name="geom2" type="box" size="1 1 1" rgba="0 1 0 1"/>
</body>
<body name="body3" pos="1 0 0">
<freejoint/>
<geom name="geom3" type="box" size="1 1 1" rgba="0 1 0 1"/>
</body>
</frame>
</worldbody>
</mujoco>
+16 -2
View File
@@ -3,9 +3,23 @@
<model name="other" file="child.xml"/>
</asset>
<worldbody>
<!-- attach body to body -->
<body name="parent">
<geom name="geom" size="2"/>
<attach model="other" body="body" prefix="other"/>
<geom name="geom" size="1" pos="0 0 2"/>
<attach model="other" body="body" prefix="other1"/>
</body>
<!-- attach frame to body (world) -->
<attach model="other" frame="frame" prefix="other2"/>
<!-- attach frame to frame -->
<frame name="top" pos="0 0 4">
<attach model="other" frame="frame" prefix="other3"/>
</frame>
<!-- attach body to frame -->
<frame name="bottom-right" pos="2 0 -1">
<attach model="other" body="body" prefix="other4"/>
</frame>
</worldbody>
</mujoco>