Table-driven attribute writing: rebase the writer on the shared rows.

The writer consumes the same generated rows as the reader.
mjXWriter::WriteAttrTable drives the mechanical attributes of an
element from its mjXAttr rows: each bound field is compared against
the class default at the same offset -- the default object is the same
struct type, so the rows carry no comparison values -- and attributes
equal to their default are skipped. A null default object means the
element has no defaults, and every defined value is written.
Ranged-arity rows write with trailing-default trimming, which the
reader makes round-trip exact by refilling from the same default.
Call sites upcast to the private mjs base (the friend declarations
permit it; mjCMesh gains the friendship its siblings had); the
comparison object is def->X().spec, a freshly-defaulted struct for
the sections, or zero-initialized for size, whose spec defaults (-1,
auto) are resolved by compilation.

Converted: pair, geom, site, joint, camera, light, material, the
equality family, both tendon types (the fixed rows are the spatial
rows without appearance attributes -- exactly the tag difference), the
actuator, flex with its three sub-elements, mesh, skin, option, the
six visual sub-sections, statistic and size. The remnants keep names,
files, resolved reference strings (the mjC classes null their private
base's string pointers; resolved names live behind accessors), and the
writing=custom policies the schema declares: compile directives never
saved (fromto, springdamper, fitscale), type-dependent lengths and
attributes (sizes, joint pos/axis/limited, shellinertia), and
alternatives (mass/density, fovy-versus-intrinsics, the plugin-gated
gain/bias family). Compiler keeps its write-if-nonzero policy;
keyframe keeps its model-sized vectors.

Saved files are canonical: attributes follow schema declaration order
with remnants trailing, and sections follow the schema's dependency
order (statistic before visual, deformable before the contact and
equality sections that name flexes, tendon before the equality
constraints that name tendons, custom demoted to the data tail).
Uniform behavior fixes fall out: default-equal positionals are
dropped, dynprm is trimmed like every other ranged vector, and mesh
material -- read into the spec but never written -- now survives
save/load round trips. Changelog entries ride along.

Verified: full suite, doc_test, and the two-tier A/B harness --
saved XML reorders attributes, and every corpus model reloads to a
byte-identical binary.
PiperOrigin-RevId: 958255003
Change-Id: I5fe7346014450db88b2f3f8680a8f616f7d31266
This commit is contained in:
Yuval Tassa
2026-08-03 02:26:45 -07:00
committed by Copybara-Service
parent 760477f406
commit 5e99ca6cb8
8 changed files with 209 additions and 385 deletions
+9
View File
@@ -5,6 +5,15 @@ Changelog
Upcoming version (not yet released) Upcoming version (not yet released)
----------------------------------- -----------------------------------
General
^^^^^^^
- The MJCF grammar is now defined in a single source of truth schema file,
`src/xml/mjcf.schema <https://github.com/google-deepmind/mujoco/tree/main/src/xml/mjcf.schema>`__. The parser's
grammar table, presence constraints, keyword maps, typed attribute bindings and save policies are generated from it
and gated by tests, as are the schema's enum keywords and declared defaults against the C headers and
default-constructors.
Actuation Actuation
^^^^^^^^^ ^^^^^^^^^
- Added the :ref:`pid<actuator-pid>` actuator: a PID controller with real position and velocity setpoint inputs, - Added the :ref:`pid<actuator-pid>` actuator: a PID controller with real position and velocity setpoint inputs,
+2 -3
View File
@@ -20,7 +20,6 @@ import math
import os import os
import textwrap import textwrap
import typing import typing
import warnings
import zipfile # pylint: disable=unused-import import zipfile # pylint: disable=unused-import
from absl import flags from absl import flags
@@ -61,7 +60,7 @@ class SpecsTest(absltest.TestCase):
</mujoco> </mujoco>
""" """
spec = mujoco.MjSpec.from_string(xml) spec = mujoco.MjSpec.from_string(xml)
model = spec.compile() spec.compile()
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0)
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0)
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0)
@@ -152,7 +151,7 @@ class SpecsTest(absltest.TestCase):
<worldbody> <worldbody>
<body name="baz" pos="1 2 3" quat="0 1 0 0"> <body name="baz" pos="1 2 3" quat="0 1 0 0">
<site name="sitename" pos="0 0 0" type="box" user="1 2 3 4 5 6"/> <site name="sitename" type="box" user="1 2 3 4 5 6"/>
</body> </body>
</worldbody> </worldbody>
</mujoco> </mujoco>
+1
View File
@@ -1138,6 +1138,7 @@ class mjCMesh_ : public mjCBase {
class mjCMesh: public mjCMesh_, private mjsMesh { class mjCMesh: public mjCMesh_, private mjsMesh {
friend class mjCModel; friend class mjCModel;
friend class mjXWriter;
public: public:
explicit mjCMesh(mjCModel* = nullptr, mjCDef* = nullptr); explicit mjCMesh(mjCModel* = nullptr, mjCDef* = nullptr);
+155 -368
View File
@@ -38,6 +38,9 @@
#include "xml/xml_util.h" #include "xml/xml_util.h"
#include "tinyxml2.h" #include "tinyxml2.h"
// typed attribute rows, generated from mjcf.schema; shared with the reader
#include "mjcf_read_table.inc"
namespace { namespace {
using std::string; using std::string;
@@ -133,14 +136,12 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// common attributes // common attributes
WriteAttrTxt(elem, "name", flex->name); WriteAttrTxt(elem, "name", flex->name);
WriteAttr(elem, "radius", 1, &flex->radius, &defflex.radius); WriteAttrTable(elem, static_cast<const mjsFlex*>(flex),
static_cast<const mjsFlex*>(&defflex), kFlexAttrs,
kFlexAttrsN);
if (flex->get_material() != defflex.get_material()) { if (flex->get_material() != defflex.get_material()) {
WriteAttrTxt(elem, "material", flex->get_material()); WriteAttrTxt(elem, "material", flex->get_material());
} }
WriteAttr(elem, "rgba", 4, flex->rgba, defflex.rgba);
WriteAttrKey(elem, "flatskin", bool_map, 2, flex->flatskin, defflex.flatskin);
WriteAttrInt(elem, "dim", flex->dim, defflex.dim);
WriteAttrInt(elem, "group", flex->group, defflex.group);
WriteAttr(elem, "cellcount", 3, flex->spec.cellcount, defflex.spec.cellcount); WriteAttr(elem, "cellcount", 3, flex->spec.cellcount, defflex.spec.cellcount);
if (flex->spec.order != defflex.spec.order) { if (flex->spec.order != defflex.spec.order) {
string dof_str = "full"; string dof_str = "full";
@@ -177,20 +178,9 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// contact subelement // contact subelement
XMLElement* cont = InsertEnd(elem, "contact"); XMLElement* cont = InsertEnd(elem, "contact");
WriteAttrInt(cont, "contype", flex->contype, defflex.contype); WriteAttrTable(cont, static_cast<const mjsFlex*>(flex),
WriteAttrInt(cont, "conaffinity", flex->conaffinity, defflex.conaffinity); static_cast<const mjsFlex*>(&defflex), kFlexcomp_contactAttrs,
WriteAttrInt(cont, "condim", flex->condim, defflex.condim); kFlexcomp_contactAttrsN);
WriteAttrInt(cont, "priority", flex->priority, defflex.priority);
WriteAttr(cont, "friction", 3, flex->friction, defflex.friction);
WriteAttr(cont, "solmix", 1, &flex->solmix, &defflex.solmix);
WriteAttr(cont, "solref", mjNREF, flex->solref, defflex.solref);
WriteAttr(cont, "solimp", mjNIMP, flex->solimp, defflex.solimp);
WriteAttr(cont, "margin", 1, &flex->margin, &defflex.margin);
WriteAttr(cont, "gap", 1, &flex->gap, &defflex.gap);
WriteAttrKey(cont, "internal", bool_map, 2, flex->internal, defflex.internal);
WriteAttrKey(cont, "selfcollide", flexself_map, 5, flex->selfcollide, defflex.selfcollide);
WriteAttrInt(cont, "activelayers", flex->activelayers, defflex.activelayers);
WriteAttrKey(cont, "passive", bool_map, 2, flex->passive, defflex.passive);
// remove contact is no attributes // remove contact is no attributes
if (!cont->FirstAttribute()) { if (!cont->FirstAttribute()) {
@@ -199,16 +189,15 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// elasticity subelement // elasticity subelement
XMLElement* elastic = InsertEnd(elem, "elasticity"); XMLElement* elastic = InsertEnd(elem, "elasticity");
WriteAttr(elastic, "young", 1, &flex->young, &defflex.young); WriteAttrTable(elastic, static_cast<const mjsFlex*>(flex),
WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson); static_cast<const mjsFlex*>(&defflex), kElasticityAttrs,
WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness); kElasticityAttrsN);
WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping);
WriteAttrKey(elastic, "elastic2d", elastic2d_map, 4, flex->elastic2d, defflex.elastic2d);
// edge subelement // edge subelement
XMLElement* edge = InsertEnd(elem, "edge"); XMLElement* edge = InsertEnd(elem, "edge");
WriteAttr(edge, "stiffness", 1, &flex->edgestiffness, &defflex.edgestiffness); WriteAttrTable(edge, static_cast<const mjsFlex*>(flex),
WriteAttr(edge, "damping", 1, &flex->edgedamping, &defflex.edgedamping); static_cast<const mjsFlex*>(&defflex), kFlex_edgeAttrs,
kFlex_edgeAttrsN);
// remove edge if no attributes // remove edge if no attributes
if (!edge->FirstAttribute()) { if (!edge->FirstAttribute()) {
@@ -257,14 +246,11 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) {
} }
// defaults and regular // defaults and regular
if (mesh->Inertia() != def->Mesh().Inertia()) { WriteAttrTable(elem, static_cast<const mjsMesh*>(mesh), &def->Mesh().spec,
WriteAttrTxt(elem, "inertia", FindValue(meshinertia_map, 4, mesh->Inertia())); kMeshAttrs, kMeshAttrsN);
if (mesh->Material() != def->Mesh().Material()) {
WriteAttrTxt(elem, "material", mesh->Material());
} }
WriteAttr(elem, "refpos", 3, mesh->Refpos(), def->Mesh().Refpos());
WriteAttr(elem, "refquat", 4, mesh->Refquat(), def->Mesh().Refquat());
WriteAttr(elem, "scale", 3, mesh->Scale(), def->Mesh().Scale());
WriteAttrKey(elem, "smoothnormal", bool_map, 2, mesh->SmoothNormal(),
def->Mesh().SmoothNormal());
} }
@@ -272,16 +258,16 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) {
// write skin // write skin
void mjXWriter::OneSkin(XMLElement* elem, const mjCSkin* skin) { void mjXWriter::OneSkin(XMLElement* elem, const mjCSkin* skin) {
string text; string text;
mjCDef mydef; mjCSkin defskin;
float zero = 0;
// write attributes // write attributes
WriteAttrTxt(elem, "name", skin->name); WriteAttrTxt(elem, "name", skin->name);
WriteAttrTxt(elem, "file", skin->File()); WriteAttrTxt(elem, "file", skin->File());
WriteAttrTxt(elem, "material", skin->get_material()); WriteAttrTxt(elem, "material", skin->get_material());
WriteAttrInt(elem, "group", skin->group, 0); WriteAttrInt(elem, "group", skin->group, 0);
WriteAttr(elem, "rgba", 4, skin->rgba, mydef.Geom().rgba); WriteAttrTable(elem, static_cast<const mjsSkin*>(skin),
WriteAttr(elem, "inflate", 1, &skin->inflate, &zero); static_cast<const mjsSkin*>(&defskin), kSkinAttrs,
kSkinAttrsN);
// write data if no file // write data if no file
if (skin->File().empty()) { if (skin->File().empty()) {
@@ -358,15 +344,8 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe
} }
} }
WriteAttrKey(elem, "texuniform", bool_map, 2, material->texuniform, def->Material().texuniform); WriteAttrTable(elem, static_cast<const mjsMaterial*>(material),
WriteAttr(elem, "texrepeat", 2, material->texrepeat, def->Material().texrepeat); &def->Material().spec, kMaterialAttrs, kMaterialAttrsN);
WriteAttr(elem, "emission", 1, &material->emission, &def->Material().emission);
WriteAttr(elem, "specular", 1, &material->specular, &def->Material().specular);
WriteAttr(elem, "shininess", 1, &material->shininess, &def->Material().shininess);
WriteAttr(elem, "reflectance", 1, &material->reflectance, &def->Material().reflectance);
WriteAttr(elem, "metallic", 1, &material->metallic, &def->Material().metallic);
WriteAttr(elem, "roughness", 1, &material->roughness, &def->Material().roughness);
WriteAttr(elem, "rgba", 4, material->rgba, def->Material().rgba);
} }
@@ -374,8 +353,6 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe
// write joint // write joint
void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def, void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
string_view classname) { string_view classname) {
double zero = 0;
// regular // regular
if (!writingdefaults) { if (!writingdefaults) {
WriteAttrTxt(elem, "name", joint->name); WriteAttrTxt(elem, "name", joint->name);
@@ -391,43 +368,15 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
} }
// defaults and regular // defaults and regular
if (joint->type != def->Joint().type) { WriteAttrTable(elem, static_cast<const mjsJoint*>(joint), &def->Joint().spec,
WriteAttrTxt(elem, "type", FindValue(jointtype_map, jointtype_sz, joint->type)); kJointAttrs, kJointAttrsN);
}
WriteAttrInt(elem, "group", joint->group, def->Joint().group);
WriteAttr(elem, "ref", 1, &joint->ref, &zero);
WriteAttr(elem, "springref", 1, &joint->springref, &zero);
WriteAttr(elem, "solreflimit", mjNREF, joint->solref_limit, def->Joint().solref_limit, true);
WriteAttr(elem, "solimplimit", mjNIMP, joint->solimp_limit, def->Joint().solimp_limit, true);
WriteAttr(elem, "solreffriction", mjNREF, joint->solref_friction, def->Joint().solref_friction,
true);
WriteAttr(elem, "solimpfriction", mjNIMP, joint->solimp_friction, def->Joint().solimp_friction,
true);
{
int nstiff = 1+mjNPOLY;
while (nstiff > 1 && joint->stiffness[nstiff-1] == 0
&& def->Joint().stiffness[nstiff-1] == 0) nstiff--;
WriteAttr(elem, "stiffness", nstiff, joint->stiffness, def->Joint().stiffness);
}
if (joint->type != mjJNT_FREE) { if (joint->type != mjJNT_FREE) {
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited); WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited);
} }
WriteAttr(elem, "range", 2, joint->range, def->Joint().range);
if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) { if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) {
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, joint->actfrclimited, WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, joint->actfrclimited,
def->Joint().actfrclimited); def->Joint().actfrclimited);
} }
WriteAttrKey(elem, "actuatorgravcomp", bool_map, 2, joint->actgravcomp, def->Joint().actgravcomp);
WriteAttr(elem, "actuatorfrcrange", 2, joint->actfrcrange, def->Joint().actfrcrange);
WriteAttr(elem, "margin", 1, &joint->margin, &def->Joint().margin);
WriteAttr(elem, "armature", 1, &joint->armature, &def->Joint().armature);
{
int ndamp = 1+mjNPOLY;
while (ndamp > 1 && joint->damping[ndamp-1] == 0
&& def->Joint().damping[ndamp-1] == 0) ndamp--;
WriteAttr(elem, "damping", ndamp, joint->damping, def->Joint().damping);
}
WriteAttr(elem, "frictionloss", 1, &joint->frictionloss, &def->Joint().frictionloss);
// userdata // userdata
if (writingdefaults) { if (writingdefaults) {
@@ -485,23 +434,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
} }
// defaults and regular // defaults and regular
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, geom->type, def->Geom().type); WriteAttrTable(elem, static_cast<const mjsGeom*>(geom), &def->Geom().spec,
WriteAttrInt(elem, "contype", geom->contype, def->Geom().contype); kGeomAttrs, kGeomAttrsN);
WriteAttrInt(elem, "conaffinity", geom->conaffinity, def->Geom().conaffinity);
WriteAttrInt(elem, "condim", geom->condim, def->Geom().condim);
WriteAttrInt(elem, "group", geom->group, def->Geom().group);
WriteAttrInt(elem, "priority", geom->priority, def->Geom().priority);
WriteAttr(elem, "friction", 3, geom->friction, def->Geom().friction, true);
WriteAttr(elem, "solmix", 1, &geom->solmix, &def->Geom().solmix);
WriteAttr(elem, "solref", mjNREF, geom->solref, def->Geom().solref, true);
WriteAttr(elem, "solimp", mjNIMP, geom->solimp, def->Geom().solimp, true);
WriteAttr(elem, "margin", 1, &geom->margin, &def->Geom().margin);
WriteAttr(elem, "gap", 1, &geom->gap, &def->Geom().gap);
WriteAttr(elem, "surfacevel", 6, geom->surfacevel, def->Geom().surfacevel, true);
WriteAttr(elem, "adhesion", 1, &geom->adhesion, &def->Geom().adhesion);
WriteAttrKey(elem, "fluidshape", WriteAttrKey(elem, "fluidshape",
fluidshape_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid); fluidshape_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid);
WriteAttr(elem, "fluidcoef", 5, geom->fluid_coefs, def->Geom().fluid_coefs);
if (geom->type != mjGEOM_MESH) { if (geom->type != mjGEOM_MESH) {
WriteAttrKey(elem, "shellinertia", bool_map, 2, geom->typeinertia, WriteAttrKey(elem, "shellinertia", bool_map, 2, geom->typeinertia,
def->Geom().typeinertia); def->Geom().typeinertia);
@@ -514,7 +450,6 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
if (geom->get_material() != def->Geom().get_material()) { if (geom->get_material() != def->Geom().get_material()) {
WriteAttrTxt(elem, "material", geom->get_material()); WriteAttrTxt(elem, "material", geom->get_material());
} }
WriteAttr(elem, "rgba", 4, geom->rgba, def->Geom().rgba);
// hfield and mesh attributes // hfield and mesh attributes
if (geom->type == mjGEOM_HFIELD) { if (geom->type == mjGEOM_HFIELD) {
@@ -547,7 +482,6 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri
if (classname != site->classname && site->classname != "main") { if (classname != site->classname && site->classname != "main") {
WriteAttrTxt(elem, "class", site->classname); WriteAttrTxt(elem, "class", site->classname);
} }
WriteAttr(elem, "pos", 3, site->pos);
WriteAttr(elem, "quat", 4, site->quat, unitq); WriteAttr(elem, "quat", 4, site->quat, unitq);
if (mjGEOMINFO[site->type]) { if (mjGEOMINFO[site->type]) {
WriteAttr(elem, "size", mjGEOMINFO[site->type], site->size, def->Site().size); WriteAttr(elem, "size", mjGEOMINFO[site->type], site->size, def->Site().size);
@@ -557,12 +491,11 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri
} }
// defaults and regular // defaults and regular
WriteAttrInt(elem, "group", site->group, def->Site().group); WriteAttrTable(elem, static_cast<const mjsSite*>(site), &def->Site().spec,
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type); kSiteAttrs, kSiteAttrsN);
if (site->get_material() != def->Site().get_material()) { if (site->get_material() != def->Site().get_material()) {
WriteAttrTxt(elem, "material", site->get_material()); WriteAttrTxt(elem, "material", site->get_material());
} }
WriteAttr(elem, "rgba", 4, site->rgba, def->Site().rgba);
// userdata // userdata
if (writingdefaults) { if (writingdefaults) {
@@ -584,28 +517,12 @@ void mjXWriter::OneCamera(XMLElement* elem, const mjCCamera* camera, mjCDef* def
WriteAttrTxt(elem, "class", camera->classname); WriteAttrTxt(elem, "class", camera->classname);
} }
WriteAttrTxt(elem, "target", camera->get_targetbody()); WriteAttrTxt(elem, "target", camera->get_targetbody());
WriteAttr(elem, "pos", 3, camera->pos);
WriteAttr(elem, "quat", 4, camera->quat, unitq); WriteAttr(elem, "quat", 4, camera->quat, unitq);
} }
// defaults and regular // defaults and regular
WriteAttr(elem, "ipd", 1, &camera->ipd, &def->Camera().ipd); WriteAttrTable(elem, static_cast<const mjsCamera*>(camera),
WriteAttrKey(elem, "mode", camlight_map, camlight_sz, camera->mode, def->Camera().mode); &def->Camera().spec, kCameraAttrs, kCameraAttrsN);
WriteAttr(elem, "resolution", 2, camera->resolution, def->Camera().resolution);
// write output attribute if different from default
if (camera->output != def->Camera().output) {
int data[mjNCAMOUT];
int ndata = 0;
for (int i = 0; i < mjNCAMOUT; i++) {
if (camera->output & camout_map[i].value) {
data[ndata++] = camout_map[i].value;
}
}
WriteAttrKeys(elem, "output", camout_map, camout_sz, data, ndata, 0);
}
WriteAttrKey(elem, "projection", projection_map, projection_sz, camera->proj, def->Camera().proj);
// camera intrinsics if specified // camera intrinsics if specified
if (camera->sensor_size[0] > 0 && camera->sensor_size[1] > 0) { if (camera->sensor_size[0] > 0 && camera->sensor_size[1] > 0) {
@@ -636,28 +553,86 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def,
WriteAttrTxt(elem, "class", light->classname); WriteAttrTxt(elem, "class", light->classname);
} }
WriteAttrTxt(elem, "target", light->get_targetbody()); WriteAttrTxt(elem, "target", light->get_targetbody());
WriteAttr(elem, "pos", 3, light->pos);
WriteAttr(elem, "dir", 3, light->dir);
} }
// defaults and regular // defaults and regular
WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius); WriteAttrTable(elem, static_cast<const mjsLight*>(light), &def->Light().spec,
WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); kLightAttrs, kLightAttrsN);
WriteAttr(elem, "range", 1, &light->range, &def->Light().range);
WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type);
WriteAttrTxt(elem, "texture", light->get_texture()); WriteAttrTxt(elem, "texture", light->get_texture());
WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow);
WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active);
WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation);
WriteAttr(elem, "cutoff", 1, &light->cutoff, &def->Light().cutoff);
WriteAttr(elem, "exponent", 1, &light->exponent, &def->Light().exponent);
WriteAttr(elem, "ambient", 3, light->ambient, def->Light().ambient);
WriteAttr(elem, "diffuse", 3, light->diffuse, def->Light().diffuse);
WriteAttr(elem, "specular", 3, light->specular, def->Light().specular);
WriteAttrKey(elem, "mode", camlight_map, camlight_sz, light->mode, def->Light().mode);
} }
// write pair // write pair
// write the mechanical attributes of an element, driven by the same
// generated rows the reader uses; see the declaration for the contract
template <typename T>
void mjXWriter::WriteAttrTable(XMLElement* elem, const T* obj, const T* def,
const mjXAttr* rows, int nrow) {
const char* live = reinterpret_cast<const char*>(obj);
const char* dflt = reinterpret_cast<const char*>(def);
for (int i = 0; i < nrow; i++) {
const mjXAttr& row = rows[i];
if (row.handwrite || (writingdefaults && row.nodefault)) {
continue;
}
const char* base = live + row.offset;
// a null default object means the element has no defaults to compare
// against: every defined value is written
const char* dbase = dflt ? dflt + row.offset : nullptr;
const int dkey = dflt ? 0 : -12345; // WriteAttrKey's write-always default
switch (row.kind) {
case mjXAttr::kInt:
WriteAttr(elem, row.attr, row.len, (const int*)base, (const int*)dbase,
/*trim=*/!row.exact);
break;
case mjXAttr::kDouble:
WriteAttr(elem, row.attr, row.len, (const double*)base,
(const double*)dbase, /*trim=*/!row.exact);
break;
case mjXAttr::kNum:
WriteAttr(elem, row.attr, row.len, (const mjtNum*)base,
(const mjtNum*)dbase, /*trim=*/!row.exact);
break;
case mjXAttr::kFloat:
WriteAttr(elem, row.attr, row.len, (const float*)base,
(const float*)dbase, /*trim=*/!row.exact);
break;
case mjXAttr::kEnum:
WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const int*)base,
dbase ? *(const int*)dbase : dkey);
break;
case mjXAttr::kEnumByte:
WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const mjtByte*)base,
dbase ? *(const mjtByte*)dbase : dkey);
break;
case mjXAttr::kBool:
WriteAttrKey(elem, row.attr, bool_map, 2, *(const mjtByte*)base,
dbase ? *(const mjtByte*)dbase : dkey);
break;
case mjXAttr::kFlags:
if (!dbase || *(const int*)base != *(const int*)dbase) {
int value = *(const int*)base;
std::vector<int> data;
for (int j = 0; j < row.mapsz; j++) {
if (value & row.map[j].value) {
data.push_back(row.map[j].value);
}
}
if (!data.empty()) {
WriteAttrKeys(elem, row.attr, row.map, row.mapsz, data.data(),
data.size(), 0);
}
}
break;
default:
// names, strings, files and custom-read attributes: OneX() remnant
break;
}
}
}
void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) { void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) {
// regular // regular
if (!writingdefaults) { if (!writingdefaults) {
@@ -670,15 +645,8 @@ void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) {
// defaults and regular // defaults and regular
WriteAttrTxt(elem, "name", pair->name); WriteAttrTxt(elem, "name", pair->name);
WriteAttrInt(elem, "condim", pair->condim, def->Pair().spec.condim); WriteAttrTable(elem, static_cast<const mjsPair*>(pair), &def->Pair().spec,
WriteAttr(elem, "margin", 1, &pair->margin, &def->Pair().spec.margin); kPairAttrs, kPairAttrsN);
WriteAttr(elem, "gap", 1, &pair->gap, &def->Pair().spec.gap);
WriteAttr(elem, "adhesion", 1, &pair->adhesion, &def->Pair().spec.adhesion);
WriteAttr(elem, "solref", mjNREF, pair->solref, def->Pair().spec.solref, true);
WriteAttr(elem, "solreffriction", mjNREF, pair->solreffriction, def->Pair().spec.solreffriction,
true);
WriteAttr(elem, "solimp", mjNIMP, pair->solimp, def->Pair().spec.solimp, true);
WriteAttr(elem, "friction", 5, pair->friction, def->Pair().spec.friction); // all 5 values
} }
@@ -745,9 +713,9 @@ void mjXWriter::OneEquality(XMLElement* elem, const mjCEquality* equality, mjCDe
} }
// defaults and regular // defaults and regular
WriteAttrKey(elem, "active", bool_map, 2, equality->active, def->Equality().active); WriteAttrTable(elem, static_cast<const mjsEquality*>(equality),
WriteAttr(elem, "solref", mjNREF, equality->solref, def->Equality().solref, true); &def->Equality().spec, kEqualityBaseAttrs,
WriteAttr(elem, "solimp", mjNIMP, equality->solimp, def->Equality().solimp, true); kEqualityBaseAttrsN);
} }
@@ -764,46 +732,23 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def
} }
} }
// defaults and regular // defaults and regular; the fixed rows are the spatial rows without the
WriteAttrInt(elem, "group", tendon->group, def->Tendon().group); // appearance attributes, which is exactly the tag difference
WriteAttr(elem, "solreflimit", mjNREF, tendon->solref_limit, def->Tendon().solref_limit, true); if (fixed) {
WriteAttr(elem, "solimplimit", mjNIMP, tendon->solimp_limit, def->Tendon().solimp_limit, true); WriteAttrTable(elem, static_cast<const mjsTendon*>(tendon),
WriteAttr(elem, "solreffriction", mjNREF, tendon->solref_friction, def->Tendon().solref_friction, &def->Tendon().spec, kFixedAttrs, kFixedAttrsN);
true); } else {
WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction, WriteAttrTable(elem, static_cast<const mjsTendon*>(tendon),
true); &def->Tendon().spec, kSpatialAttrs, kSpatialAttrsN);
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, tendon->limited, def->Tendon().limited);
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited);
WriteAttr(elem, "range", 2, tendon->range, def->Tendon().range);
WriteAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, def->Tendon().actfrcrange);
WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin);
{
int nstiff = 1+mjNPOLY;
while (nstiff > 1 && tendon->stiffness[nstiff-1] == 0
&& def->Tendon().stiffness[nstiff-1] == 0) nstiff--;
WriteAttr(elem, "stiffness", nstiff, tendon->stiffness, def->Tendon().stiffness);
} }
{
int ndamp = 1+mjNPOLY;
while (ndamp > 1 && tendon->damping[ndamp-1] == 0
&& def->Tendon().damping[ndamp-1] == 0) ndamp--;
WriteAttr(elem, "damping", ndamp, tendon->damping, def->Tendon().damping);
}
WriteAttr(elem, "armature", 1, &tendon->armature, &def->Tendon().armature);
WriteAttr(elem, "frictionloss", 1, &tendon->frictionloss, &def->Tendon().frictionloss);
if (tendon->springlength[0] != tendon->springlength[1] || if (tendon->springlength[0] != tendon->springlength[1] ||
def->Tendon().springlength[0] != def->Tendon().springlength[1]) { def->Tendon().springlength[0] != def->Tendon().springlength[1]) {
WriteAttr(elem, "springlength", 2, tendon->springlength, def->Tendon().springlength); WriteAttr(elem, "springlength", 2, tendon->springlength, def->Tendon().springlength);
} else { } else {
WriteAttr(elem, "springlength", 1, tendon->springlength, def->Tendon().springlength); WriteAttr(elem, "springlength", 1, tendon->springlength, def->Tendon().springlength);
} }
// spatial only if (!fixed && tendon->get_material() != def->Tendon().get_material()) {
if (!fixed) { WriteAttrTxt(elem, "material", tendon->get_material());
if (tendon->get_material() != def->Tendon().get_material()) {
WriteAttrTxt(elem, "material", tendon->get_material());
}
WriteAttr(elem, "width", 1, &tendon->width, &def->Tendon().width);
WriteAttr(elem, "rgba", 4, tendon->rgba, def->Tendon().rgba);
} }
// userdata // userdata
@@ -859,28 +804,9 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
} }
// defaults and regular // defaults and regular
WriteAttrInt(elem, "group", actuator->group, def->Actuator().group); WriteAttrTable(elem, static_cast<const mjsActuator*>(actuator),
WriteAttrInt(elem, "nsample", actuator->nsample, def->Actuator().nsample); &def->Actuator().spec, kGeneralAttrs, kGeneralAttrsN);
WriteAttrKey(elem, "interp", interp_map, interp_sz, actuator->interp, def->Actuator().interp);
WriteAttr(elem, "delay", 1, &actuator->delay, &def->Actuator().delay);
WriteAttrKey(elem, "ctrllimited", FalseTrueAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited);
WriteAttr(elem, "ctrlrange", 2, actuator->ctrlrange, def->Actuator().ctrlrange);
WriteAttrKey(elem, "forcelimited", FalseTrueAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited);
WriteAttr(elem, "forcerange", 2, actuator->forcerange, def->Actuator().forcerange);
WriteAttrKey(elem, "actlimited", FalseTrueAuto_map, 3, actuator->actlimited, def->Actuator().actlimited);
WriteAttr(elem, "actrange", 2, actuator->actrange, def->Actuator().actrange);
WriteAttr(elem, "lengthrange", 2, actuator->lengthrange, def->Actuator().lengthrange);
WriteAttr(elem, "gear", 6, actuator->gear, def->Actuator().gear);
{
int ndamp = 1+mjNPOLY;
while (ndamp > 1 && actuator->damping[ndamp-1] == 0
&& def->Actuator().damping[ndamp-1] == 0) ndamp--;
WriteAttr(elem, "damping", ndamp, actuator->damping, def->Actuator().damping);
}
WriteAttr(elem, "armature", 1, &actuator->armature, &def->Actuator().armature);
WriteAttr(elem, "cranklength", 1, &actuator->cranklength, &def->Actuator().cranklength); WriteAttr(elem, "cranklength", 1, &actuator->cranklength, &def->Actuator().cranklength);
WriteAttrKey(elem, "actearly", bool_map, 2, actuator->actearly,
def->Actuator().actearly);
// special handling of actdim which has default value of -1 // special handling of actdim which has default value of -1
if (writingdefaults) { if (writingdefaults) {
WriteAttrInt(elem, "actdim", actuator->actdim, def->Actuator().actdim); WriteAttrInt(elem, "actdim", actuator->actdim, def->Actuator().actdim);
@@ -888,8 +814,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
int default_actdim = (actuator->dyntype != mjDYN_NONE && actuator->dyntype != mjDYN_DCMOTOR); int default_actdim = (actuator->dyntype != mjDYN_NONE && actuator->dyntype != mjDYN_DCMOTOR);
WriteAttrInt(elem, "actdim", actuator->actdim, default_actdim); WriteAttrInt(elem, "actdim", actuator->actdim, default_actdim);
} }
WriteAttrKey(elem, "dyntype", dyn_map, dyn_sz, actuator->dyntype, def->Actuator().dyntype);
WriteAttr(elem, "dynprm", mjNDYN, actuator->dynprm, def->Actuator().dynprm);
// plugins: write config attributes // plugins: write config attributes
if (actuator->plugin.active) { if (actuator->plugin.active) {
@@ -911,8 +835,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
} }
WriteAttrTxt(elem, "input", tokens); WriteAttrTxt(elem, "input", tokens);
} }
WriteAttr(elem, "velrange", 2, actuator->velrange, def->Actuator().velrange);
WriteAttr(elem, "ffrange", 2, actuator->ffrange, def->Actuator().ffrange);
WriteAttrKey(elem, "biastype", bias_map, bias_sz, actuator->biastype, def->Actuator().biastype); WriteAttrKey(elem, "biastype", bias_map, bias_sz, actuator->biastype, def->Actuator().biastype);
WriteAttr(elem, "gainprm", mjNGAIN, actuator->gainprm, def->Actuator().gainprm, true); WriteAttr(elem, "gainprm", mjNGAIN, actuator->gainprm, def->Actuator().gainprm, true);
WriteAttr(elem, "biasprm", mjNBIAS, actuator->biasprm, def->Actuator().biasprm, true); WriteAttr(elem, "biasprm", mjNBIAS, actuator->biasprm, def->Actuator().biasprm, true);
@@ -1000,21 +922,21 @@ string mjXWriter::Write(char *error, size_t error_sz) {
Compiler(root); Compiler(root);
Option(root); Option(root);
Size(root); Size(root);
Visual(root);
Statistic(root); Statistic(root);
Visual(root);
writingdefaults = true; writingdefaults = true;
Default(root, model->Default()); Default(root, model->Default());
writingdefaults = false; writingdefaults = false;
Extension(root); Extension(root);
Custom(root);
Asset(root); Asset(root);
Body(InsertEnd(root, "worldbody"), model->GetWorld(), nullptr); Body(InsertEnd(root, "worldbody"), model->GetWorld(), nullptr);
Contact(root);
Deformable(root); Deformable(root);
Equality(root); Contact(root);
Tendon(root); Tendon(root);
Equality(root);
Actuator(root); Actuator(root);
Sensor(root); Sensor(root);
Custom(root);
Keyframe(root); Keyframe(root);
return WriteDoc(doc, error, error_sz); return WriteDoc(doc, error, error_sz);
@@ -1063,39 +985,8 @@ void mjXWriter::Option(XMLElement* root) {
XMLElement* section = InsertEnd(root, "option"); XMLElement* section = InsertEnd(root, "option");
// option // option; the freshly-defaulted struct is the comparison object
WriteAttr(section, "timestep", 1, &model->option.timestep, &opt.timestep); WriteAttrTable(section, &model->option, &opt, kOptionAttrs, kOptionAttrsN);
WriteAttr(section, "impratio", 1, &model->option.impratio, &opt.impratio);
WriteAttr(section, "tolerance", 1, &model->option.tolerance, &opt.tolerance);
WriteAttr(section, "ls_tolerance", 1, &model->option.ls_tolerance, &opt.ls_tolerance);
WriteAttr(section, "noslip_tolerance", 1, &model->option.noslip_tolerance, &opt.noslip_tolerance);
WriteAttr(section, "ccd_tolerance", 1, &model->option.ccd_tolerance, &opt.ccd_tolerance);
WriteAttr(section, "sleep_tolerance", 1, &model->option.sleep_tolerance, &opt.sleep_tolerance);
WriteAttr(section, "gravity", 3, model->option.gravity, opt.gravity);
WriteAttr(section, "wind", 3, model->option.wind, opt.wind);
WriteAttr(section, "magnetic", 3, model->option.magnetic, opt.magnetic);
WriteAttr(section, "density", 1, &model->option.density, &opt.density);
WriteAttr(section, "viscosity", 1, &model->option.viscosity, &opt.viscosity);
WriteAttr(section, "o_margin", 1, &model->option.o_margin, &opt.o_margin);
WriteAttr(section, "o_solref", mjNREF, model->option.o_solref, opt.o_solref);
WriteAttr(section, "o_solimp", mjNIMP, model->option.o_solimp, opt.o_solimp);
WriteAttr(section, "o_friction", 5, model->option.o_friction, opt.o_friction);
WriteAttrKey(section, "integrator", integrator_map, integrator_sz,
model->option.integrator, opt.integrator);
WriteAttrKey(section, "cone", cone_map, cone_sz,
model->option.cone, opt.cone);
WriteAttrKey(section, "jacobian", jacobian_map, jacobian_sz,
model->option.jacobian, opt.jacobian);
WriteAttrKey(section, "solver", solver_map, solver_sz,
model->option.solver, opt.solver);
WriteAttrInt(section, "iterations", model->option.iterations, opt.iterations);
WriteAttrInt(section, "ls_iterations", model->option.ls_iterations, opt.ls_iterations);
WriteAttrInt(section, "noslip_iterations", model->option.noslip_iterations, opt.noslip_iterations);
WriteAttrInt(section, "ccd_iterations", model->option.ccd_iterations, opt.ccd_iterations);
WriteAttrInt(section, "sdf_iterations", model->option.sdf_iterations, opt.sdf_iterations);
WriteAttrInt(section, "sdf_initpoints", model->option.sdf_initpoints, opt.sdf_initpoints);
// actuator group disable // actuator group disable
int disabled_groups[31]; int disabled_groups[31];
@@ -1165,20 +1056,16 @@ void mjXWriter::Size(XMLElement* root) {
WriteAttrTxt(section, "memory", mju_writeNumBytes(model->memory)); WriteAttrTxt(section, "memory", mju_writeNumBytes(model->memory));
} }
// write sizes // deprecated sizes, hand-read into locals with range checks
WriteAttrInt(section, "njmax", model->njmax, -1); WriteAttrInt(section, "njmax", model->njmax, -1);
WriteAttrInt(section, "nconmax", model->nconmax, -1); WriteAttrInt(section, "nconmax", model->nconmax, -1);
WriteAttrInt(section, "nstack", model->nstack, -1); WriteAttrInt(section, "nstack", model->nstack, -1);
WriteAttrInt(section, "nuserdata", model->nuserdata, 0);
WriteAttrInt(section, "nkey", model->nkey, 0); // write sizes; the spec defaults are -1 (auto), but compilation resolves
WriteAttrInt(section, "nuser_body", model->nuser_body, 0); // them to the actual counts, so the comparison object is zero-initialized
WriteAttrInt(section, "nuser_jnt", model->nuser_jnt, 0); mjSpec zerospec = {};
WriteAttrInt(section, "nuser_geom", model->nuser_geom, 0); WriteAttrTable(section, static_cast<const mjSpec*>(model), &zerospec,
WriteAttrInt(section, "nuser_site", model->nuser_site, 0); kSizeAttrs, kSizeAttrsN);
WriteAttrInt(section, "nuser_cam", model->nuser_cam, 0);
WriteAttrInt(section, "nuser_tendon", model->nuser_tendon, 0);
WriteAttrInt(section, "nuser_actuator", model->nuser_actuator, 0);
WriteAttrInt(section, "nuser_sensor", model->nuser_sensor, 0);
// remove entire section if no attributes // remove entire section if no attributes
if (!section->FirstAttribute()) root->DeleteChild(section); if (!section->FirstAttribute()) root->DeleteChild(section);
@@ -1189,13 +1076,11 @@ void mjXWriter::Size(XMLElement* root) {
// statistic section // statistic section
void mjXWriter::Statistic(XMLElement* root) { void mjXWriter::Statistic(XMLElement* root) {
XMLElement* section = InsertEnd(root, "statistic"); XMLElement* section = InsertEnd(root, "statistic");
mjStatistic* s = &model->stat;
if (mjuu_defined(s->meaninertia)) WriteAttr(section, "meaninertia", 1, &s->meaninertia); // statistics are unset (mjNAN) rather than defaulted: there is nothing to
if (mjuu_defined(s->meanmass)) WriteAttr(section, "meanmass", 1, &s->meanmass); // compare against, and WriteAttr skips the undefined values by itself
if (mjuu_defined(s->meansize)) WriteAttr(section, "meansize", 1, &s->meansize); WriteAttrTable(section, &model->stat, (const mjStatistic*)nullptr,
if (mjuu_defined(s->extent)) WriteAttr(section, "extent", 1, &s->extent); kStatisticAttrs, kStatisticAttrsN);
if (mjuu_defined(s->center[0])) WriteAttr(section, "center", 3, s->center);
// remove entire section if no attributes // remove entire section if no attributes
if (!section->FirstAttribute()) root->DeleteChild(section); if (!section->FirstAttribute()) root->DeleteChild(section);
@@ -1207,123 +1092,25 @@ void mjXWriter::Statistic(XMLElement* root) {
void mjXWriter::Visual(XMLElement* root) { void mjXWriter::Visual(XMLElement* root) {
mjVisual visdef, *vis = &model->visual; mjVisual visdef, *vis = &model->visual;
mj_defaultVisual(&visdef); mj_defaultVisual(&visdef);
XMLElement* elem;
XMLElement* section = InsertEnd(root, "visual"); XMLElement* section = InsertEnd(root, "visual");
// global // the sub-sections are projections into mjVisual: their rows carry
elem = InsertEnd(section, "global"); // member-path offsets, so one struct pair drives them all
WriteAttrInt(elem, "cameraid", vis->global.cameraid, visdef.global.cameraid); struct { const char* tag; const mjXAttr* rows; int n; } subs[] = {
WriteAttrKey(elem, "orthographic", {"global", kGlobalAttrs, kGlobalAttrsN},
bool_map, 2, vis->global.orthographic, visdef.global.orthographic); {"quality", kQualityAttrs, kQualityAttrsN},
WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy); {"headlight", kHeadlightAttrs, kHeadlightAttrsN},
WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd); {"map", kMapAttrs, kMapAttrsN},
WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth); {"scale", kScaleAttrs, kScaleAttrsN},
WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation); {"rgba", kRgbaAttrs, kRgbaAttrsN},
WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth); };
WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow); for (const auto& sub : subs) {
WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime); XMLElement* elem = InsertEnd(section, sub.tag);
WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth); WriteAttrTable(elem, vis, &visdef, sub.rows, sub.n);
WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight); if (!elem->FirstAttribute()) {
WriteAttrKey(elem, "ellipsoidinertia", section->DeleteChild(elem);
bool_map, 2, vis->global.ellipsoidinertia, visdef.global.ellipsoidinertia); }
WriteAttrKey(elem, "bvactive", bool_map, 2, vis->global.bvactive, visdef.global.bvactive);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
// quality
elem = InsertEnd(section, "quality");
WriteAttrInt(elem, "shadowsize", vis->quality.shadowsize, visdef.quality.shadowsize);
WriteAttrInt(elem, "offsamples", vis->quality.offsamples, visdef.quality.offsamples);
WriteAttrInt(elem, "numslices", vis->quality.numslices, visdef.quality.numslices);
WriteAttrInt(elem, "numstacks", vis->quality.numstacks, visdef.quality.numstacks);
WriteAttrInt(elem, "numquads", vis->quality.numquads, visdef.quality.numquads);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
// headlight
elem = InsertEnd(section, "headlight");
WriteAttr(elem, "ambient", 3, vis->headlight.ambient, visdef.headlight.ambient);
WriteAttr(elem, "diffuse", 3, vis->headlight.diffuse, visdef.headlight.diffuse);
WriteAttr(elem, "specular", 3, vis->headlight.specular, visdef.headlight.specular);
WriteAttrInt(elem, "active", vis->headlight.active, visdef.headlight.active);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
// map
elem = InsertEnd(section, "map");
WriteAttr(elem, "stiffness", 1, &vis->map.stiffness, &visdef.map.stiffness);
WriteAttr(elem, "stiffnessrot", 1, &vis->map.stiffnessrot, &visdef.map.stiffnessrot);
WriteAttr(elem, "force", 1, &vis->map.force, &visdef.map.force);
WriteAttr(elem, "torque", 1, &vis->map.torque, &visdef.map.torque);
WriteAttr(elem, "alpha", 1, &vis->map.alpha, &visdef.map.alpha);
WriteAttr(elem, "fogstart", 1, &vis->map.fogstart, &visdef.map.fogstart);
WriteAttr(elem, "fogend", 1, &vis->map.fogend, &visdef.map.fogend);
WriteAttr(elem, "znear", 1, &vis->map.znear, &visdef.map.znear);
WriteAttr(elem, "zfar", 1, &vis->map.zfar, &visdef.map.zfar);
WriteAttr(elem, "haze", 1, &vis->map.haze, &visdef.map.haze);
WriteAttr(elem, "shadowclip", 1, &vis->map.shadowclip, &visdef.map.shadowclip);
WriteAttr(elem, "shadowscale", 1, &vis->map.shadowscale, &visdef.map.shadowscale);
WriteAttr(elem, "actuatortendon", 1, &vis->map.actuatortendon, &visdef.map.actuatortendon);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
// scale
elem = InsertEnd(section, "scale");
WriteAttr(elem, "forcewidth", 1, &vis->scale.forcewidth, &visdef.scale.forcewidth);
WriteAttr(elem, "contactwidth", 1, &vis->scale.contactwidth, &visdef.scale.contactwidth);
WriteAttr(elem, "contactheight", 1, &vis->scale.contactheight, &visdef.scale.contactheight);
WriteAttr(elem, "connect", 1, &vis->scale.connect, &visdef.scale.connect);
WriteAttr(elem, "com", 1, &vis->scale.com, &visdef.scale.com);
WriteAttr(elem, "camera", 1, &vis->scale.camera, &visdef.scale.camera);
WriteAttr(elem, "light", 1, &vis->scale.light, &visdef.scale.light);
WriteAttr(elem, "selectpoint", 1, &vis->scale.selectpoint, &visdef.scale.selectpoint);
WriteAttr(elem, "jointlength", 1, &vis->scale.jointlength, &visdef.scale.jointlength);
WriteAttr(elem, "jointwidth", 1, &vis->scale.jointwidth, &visdef.scale.jointwidth);
WriteAttr(elem, "actuatorlength", 1, &vis->scale.actuatorlength, &visdef.scale.actuatorlength);
WriteAttr(elem, "actuatorwidth", 1, &vis->scale.actuatorwidth, &visdef.scale.actuatorwidth);
WriteAttr(elem, "framelength", 1, &vis->scale.framelength, &visdef.scale.framelength);
WriteAttr(elem, "framewidth", 1, &vis->scale.framewidth, &visdef.scale.framewidth);
WriteAttr(elem, "constraint", 1, &vis->scale.constraint, &visdef.scale.constraint);
WriteAttr(elem, "slidercrank", 1, &vis->scale.slidercrank, &visdef.scale.slidercrank);
WriteAttr(elem, "frustum", 1, &vis->scale.frustum, &visdef.scale.frustum);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
// rgba
elem = InsertEnd(section, "rgba");
WriteAttr(elem, "fog", 4, vis->rgba.fog, visdef.rgba.fog);
WriteAttr(elem, "haze", 4, vis->rgba.haze, visdef.rgba.haze);
WriteAttr(elem, "force", 4, vis->rgba.force, visdef.rgba.force);
WriteAttr(elem, "inertia", 4, vis->rgba.inertia, visdef.rgba.inertia);
WriteAttr(elem, "joint", 4, vis->rgba.joint, visdef.rgba.joint);
WriteAttr(elem, "actuator", 4, vis->rgba.actuator, visdef.rgba.actuator);
WriteAttr(elem, "actuatornegative", 4, vis->rgba.actuatornegative, visdef.rgba.actuatornegative);
WriteAttr(elem, "actuatorpositive", 4, vis->rgba.actuatorpositive, visdef.rgba.actuatorpositive);
WriteAttr(elem, "com", 4, vis->rgba.com, visdef.rgba.com);
WriteAttr(elem, "camera", 4, vis->rgba.camera, visdef.rgba.camera);
WriteAttr(elem, "light", 4, vis->rgba.light, visdef.rgba.light);
WriteAttr(elem, "selectpoint", 4, vis->rgba.selectpoint, visdef.rgba.selectpoint);
WriteAttr(elem, "connect", 4, vis->rgba.connect, visdef.rgba.connect);
WriteAttr(elem, "contactpoint", 4, vis->rgba.contactpoint, visdef.rgba.contactpoint);
WriteAttr(elem, "contactforce", 4, vis->rgba.contactforce, visdef.rgba.contactforce);
WriteAttr(elem, "contactfriction", 4, vis->rgba.contactfriction, visdef.rgba.contactfriction);
WriteAttr(elem, "contacttorque", 4, vis->rgba.contacttorque, visdef.rgba.contacttorque);
WriteAttr(elem, "contactgap", 4, vis->rgba.contactgap, visdef.rgba.contactgap);
WriteAttr(elem, "rangefinder", 4, vis->rgba.rangefinder, visdef.rgba.rangefinder);
WriteAttr(elem, "constraint", 4, vis->rgba.constraint, visdef.rgba.constraint);
WriteAttr(elem, "slidercrank", 4, vis->rgba.slidercrank, visdef.rgba.slidercrank);
WriteAttr(elem, "crankbroken", 4, vis->rgba.crankbroken, visdef.rgba.crankbroken);
WriteAttr(elem, "frustum", 4, vis->rgba.frustum, visdef.rgba.frustum);
WriteAttr(elem, "bv", 4, vis->rgba.bv, visdef.rgba.bv);
WriteAttr(elem, "bvactive", 4, vis->rgba.bvactive, visdef.rgba.bvactive);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
} }
// remove entire section if no elements // remove entire section if no elements
+10
View File
@@ -62,6 +62,16 @@ class mjXWriter : public mjXBase {
// body/world section // body/world section
void Body(tinyxml2::XMLElement* elem, mjCBody* body, mjCFrame* frame, std::string_view childclass = ""); void Body(tinyxml2::XMLElement* elem, mjCBody* body, mjCFrame* frame, std::string_view childclass = "");
// table-driven attribute writing: the mechanical attributes of an element,
// driven by the same generated mjXAttr rows the reader uses. obj and def
// are the element's bound spec struct and its class-default counterpart;
// each bound field is compared against the default at the same offset, so
// attributes equal to their default are skipped. Strings, files and
// custom-read attributes remain in the OneX() remnants.
template <typename T>
void WriteAttrTable(tinyxml2::XMLElement* elem, const T* obj, const T* def,
const struct mjXAttr* rows, int nrow);
// single element writers, used in defaults and main body // single element writers, used in defaults and main body
void OneFlex(tinyxml2::XMLElement* elem, const mjCFlex* pflex); void OneFlex(tinyxml2::XMLElement* elem, const mjCFlex* pflex);
void OneMesh(tinyxml2::XMLElement* elem, const mjCMesh* pmesh, mjCDef* def); void OneMesh(tinyxml2::XMLElement* elem, const mjCMesh* pmesh, mjCDef* def);
+4
View File
@@ -1270,6 +1270,10 @@ void mjXUtil::WriteAttrKey(XMLElement* elem, std::string name,
// write attribute- space-separated keywords // write attribute- space-separated keywords
void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map, void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map,
int mapsz, int* data, int ndata, int def) { int mapsz, int* data, int ndata, int def) {
if (ndata <= 0) {
return;
}
// skip default // skip default
if (ndata == 1 && data[0] == def) { if (ndata == 1 && data[0] == def) {
return; return;
+3 -1
View File
@@ -551,9 +551,11 @@ TEST_F(EnginePluginTest, SaveXml) {
int actuator_end = expected_xml.find(actuator_close) + actuator_close.size(); int actuator_end = expected_xml.find(actuator_close) + actuator_close.size();
ASSERT_NE(actuator_end, std::string::npos); ASSERT_NE(actuator_end, std::string::npos);
ASSERT_LE(actuator_start, actuator_end); ASSERT_LE(actuator_start, actuator_end);
// saved attributes follow schema order; dynprm round-trips as written
auto expected_actuator_section = absl::StrReplaceAll( auto expected_actuator_section = absl::StrReplaceAll(
expected_xml.substr(actuator_start, actuator_end - actuator_start), expected_xml.substr(actuator_start, actuator_end - actuator_start),
{{"dynprm=\"0.9\"", "dynprm=\"0.9 0 0 0 0 0 0 0 0 0\""}}); {{"actdim=\"4\" dyntype=\"filter\" dynprm=\"0.9\"",
"dyntype=\"filter\" dynprm=\"0.9\" actdim=\"4\""}});
EXPECT_THAT(saved_xml, HasSubstr(expected_extension_section)); EXPECT_THAT(saved_xml, HasSubstr(expected_extension_section));
EXPECT_THAT(saved_xml, HasSubstr(expected_sensor_section)); EXPECT_THAT(saved_xml, HasSubstr(expected_sensor_section));
+25 -13
View File
@@ -324,7 +324,7 @@ TEST_F(XMLWriterTest, KeepsJointLimitedFalseIfAutoLimits) {
MjModelPtr model = LoadModelFromString(xml); MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull()); ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get()); std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\"")); EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\" limited=\"false\""));
} }
TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) { TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) {
@@ -537,7 +537,7 @@ TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) {
MjModelPtr model = LoadModelFromString(xml); MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull()); ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get()); std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"false\" ctrlrange=\"-1 1\"")); EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\" ctrllimited=\"false\""));
} }
TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) { TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) {
@@ -702,8 +702,8 @@ TEST_F(XMLWriterTest, WritesActuatorDefaults) {
ASSERT_THAT(model.get(), NotNull()); ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get()); std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, Not(HasSubstr("mass"))); EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
EXPECT_THAT(saved_xml, HasSubstr( EXPECT_THAT(saved_xml,
"<general biastype=\"affine\" gainprm=\"3\"")); HasSubstr("<general biastype=\"affine\" gainprm=\"3\""));
} }
TEST_F(XMLWriterTest, WritesFrameDefaults) { TEST_F(XMLWriterTest, WritesFrameDefaults) {
@@ -763,7 +763,7 @@ TEST_F(XMLWriterTest, WritesFrameDefaults) {
</frame> </frame>
</frame> </frame>
</frame> </frame>
<light pos="0 0 1" dir="0 0 -1"/> <light pos="0 0 1"/>
</body> </body>
<frame name="f1"> <frame name="f1">
<geom size="0.5" quat="0.906308 0 0 0.422618"/> <geom size="0.5" quat="0.906308 0 0 0.422618"/>
@@ -930,8 +930,8 @@ TEST_F(XMLWriterTest, WritesSkin) {
EXPECT_THAT(model->nskin, 1); EXPECT_THAT(model->nskin, 1);
char error[1024]; char error[1024];
MjModelPtr mtemp = LoadModelFromString(SaveAndReadXml(model.get()), MjModelPtr mtemp =
error, sizeof(error)); LoadModelFromString(SaveAndReadXml(model.get()), error, sizeof(error));
ASSERT_THAT(mtemp.get(), NotNull()) << error; ASSERT_THAT(mtemp.get(), NotNull()) << error;
EXPECT_THAT(mtemp->nskin, 1); EXPECT_THAT(mtemp->nskin, 1);
} }
@@ -950,7 +950,7 @@ TEST_F(XMLWriterTest, WritesHfield) {
// load model // load model
MjModelPtr model = LoadModelFromString(xml); MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull()); ASSERT_THAT(model.get(), NotNull());
int size = model->hfield_nrow[0]*model->hfield_ncol[0]; int size = model->hfield_nrow[0] * model->hfield_ncol[0];
EXPECT_EQ(size, 6); EXPECT_EQ(size, 6);
// check that the data is normalized and in row-major, bottom-to-top order // check that the data is normalized and in row-major, bottom-to-top order
@@ -1256,9 +1256,7 @@ class XMLWriterLocaleTest : public MujocoTest {
} }
} }
void TearDown() override { void TearDown() override { std::setlocale(LC_ALL, old_locale_.c_str()); }
std::setlocale(LC_ALL, old_locale_.c_str());
}
private: private:
std::string old_locale_; std::string old_locale_;
@@ -1304,7 +1302,6 @@ TEST_F(XMLWriterTest, NonRGBTextures) {
mj_deleteModel(model); mj_deleteModel(model);
} }
// ---------------- test CopyBack functionality (decompiler) ------------------ // ---------------- test CopyBack functionality (decompiler) ------------------
using DecompilerTest = MujocoTest; using DecompilerTest = MujocoTest;
TEST_F(DecompilerTest, SavesStatistics) { TEST_F(DecompilerTest, SavesStatistics) {
@@ -1469,7 +1466,8 @@ TEST_F(XMLWriterTest, ExpandAttach) {
mj_addBufferVFS(vfs.get(), "b.xml", xml_child, sizeof(xml_child)); mj_addBufferVFS(vfs.get(), "b.xml", xml_child, sizeof(xml_child));
std::array<char, 1024> er; std::array<char, 1024> er;
MjModelPtr m = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get()); MjModelPtr m =
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
ASSERT_THAT(m.get(), NotNull()) << er.data(); ASSERT_THAT(m.get(), NotNull()) << er.data();
std::string saved_xml = SaveAndReadXml(m.get()); std::string saved_xml = SaveAndReadXml(m.get());
@@ -1626,5 +1624,19 @@ TEST_F(XMLWriterTest, BodySimpleRoundtrip) {
EXPECT_GT(model->nC, model_auto->nC); EXPECT_GT(model->nC, model_auto->nC);
} }
TEST_F(XMLWriterTest, EmptyFlagsAttribute) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<camera name="cam" output=""/>
</worldbody>
</mujoco>
)";
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, Not(HasSubstr("output=")));
}
} // namespace } // namespace
} // namespace mujoco } // namespace mujoco