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)
-----------------------------------
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
^^^^^^^^^
- 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 textwrap
import typing
import warnings
import zipfile # pylint: disable=unused-import
from absl import flags
@@ -61,7 +60,7 @@ class SpecsTest(absltest.TestCase):
</mujoco>
"""
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_ASSETS], 0)
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0)
@@ -152,7 +151,7 @@ class SpecsTest(absltest.TestCase):
<worldbody>
<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>
</worldbody>
</mujoco>
+1
View File
@@ -1138,6 +1138,7 @@ class mjCMesh_ : public mjCBase {
class mjCMesh: public mjCMesh_, private mjsMesh {
friend class mjCModel;
friend class mjXWriter;
public:
explicit mjCMesh(mjCModel* = nullptr, mjCDef* = nullptr);
+155 -368
View File
@@ -38,6 +38,9 @@
#include "xml/xml_util.h"
#include "tinyxml2.h"
// typed attribute rows, generated from mjcf.schema; shared with the reader
#include "mjcf_read_table.inc"
namespace {
using std::string;
@@ -133,14 +136,12 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// common attributes
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()) {
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);
if (flex->spec.order != defflex.spec.order) {
string dof_str = "full";
@@ -177,20 +178,9 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// contact subelement
XMLElement* cont = InsertEnd(elem, "contact");
WriteAttrInt(cont, "contype", flex->contype, defflex.contype);
WriteAttrInt(cont, "conaffinity", flex->conaffinity, defflex.conaffinity);
WriteAttrInt(cont, "condim", flex->condim, defflex.condim);
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);
WriteAttrTable(cont, static_cast<const mjsFlex*>(flex),
static_cast<const mjsFlex*>(&defflex), kFlexcomp_contactAttrs,
kFlexcomp_contactAttrsN);
// remove contact is no attributes
if (!cont->FirstAttribute()) {
@@ -199,16 +189,15 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) {
// elasticity subelement
XMLElement* elastic = InsertEnd(elem, "elasticity");
WriteAttr(elastic, "young", 1, &flex->young, &defflex.young);
WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson);
WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness);
WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping);
WriteAttrKey(elastic, "elastic2d", elastic2d_map, 4, flex->elastic2d, defflex.elastic2d);
WriteAttrTable(elastic, static_cast<const mjsFlex*>(flex),
static_cast<const mjsFlex*>(&defflex), kElasticityAttrs,
kElasticityAttrsN);
// edge subelement
XMLElement* edge = InsertEnd(elem, "edge");
WriteAttr(edge, "stiffness", 1, &flex->edgestiffness, &defflex.edgestiffness);
WriteAttr(edge, "damping", 1, &flex->edgedamping, &defflex.edgedamping);
WriteAttrTable(edge, static_cast<const mjsFlex*>(flex),
static_cast<const mjsFlex*>(&defflex), kFlex_edgeAttrs,
kFlex_edgeAttrsN);
// remove edge if no attributes
if (!edge->FirstAttribute()) {
@@ -257,14 +246,11 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) {
}
// defaults and regular
if (mesh->Inertia() != def->Mesh().Inertia()) {
WriteAttrTxt(elem, "inertia", FindValue(meshinertia_map, 4, mesh->Inertia()));
WriteAttrTable(elem, static_cast<const mjsMesh*>(mesh), &def->Mesh().spec,
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
void mjXWriter::OneSkin(XMLElement* elem, const mjCSkin* skin) {
string text;
mjCDef mydef;
float zero = 0;
mjCSkin defskin;
// write attributes
WriteAttrTxt(elem, "name", skin->name);
WriteAttrTxt(elem, "file", skin->File());
WriteAttrTxt(elem, "material", skin->get_material());
WriteAttrInt(elem, "group", skin->group, 0);
WriteAttr(elem, "rgba", 4, skin->rgba, mydef.Geom().rgba);
WriteAttr(elem, "inflate", 1, &skin->inflate, &zero);
WriteAttrTable(elem, static_cast<const mjsSkin*>(skin),
static_cast<const mjsSkin*>(&defskin), kSkinAttrs,
kSkinAttrsN);
// write data if no file
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);
WriteAttr(elem, "texrepeat", 2, material->texrepeat, def->Material().texrepeat);
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);
WriteAttrTable(elem, static_cast<const mjsMaterial*>(material),
&def->Material().spec, kMaterialAttrs, kMaterialAttrsN);
}
@@ -374,8 +353,6 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe
// write joint
void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
string_view classname) {
double zero = 0;
// regular
if (!writingdefaults) {
WriteAttrTxt(elem, "name", joint->name);
@@ -391,43 +368,15 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
}
// defaults and regular
if (joint->type != def->Joint().type) {
WriteAttrTxt(elem, "type", FindValue(jointtype_map, jointtype_sz, joint->type));
}
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);
}
WriteAttrTable(elem, static_cast<const mjsJoint*>(joint), &def->Joint().spec,
kJointAttrs, kJointAttrsN);
if (joint->type != mjJNT_FREE) {
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) {
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, 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
if (writingdefaults) {
@@ -485,23 +434,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
}
// defaults and regular
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, geom->type, def->Geom().type);
WriteAttrInt(elem, "contype", geom->contype, def->Geom().contype);
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);
WriteAttrTable(elem, static_cast<const mjsGeom*>(geom), &def->Geom().spec,
kGeomAttrs, kGeomAttrsN);
WriteAttrKey(elem, "fluidshape",
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) {
WriteAttrKey(elem, "shellinertia", bool_map, 2, 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()) {
WriteAttrTxt(elem, "material", geom->get_material());
}
WriteAttr(elem, "rgba", 4, geom->rgba, def->Geom().rgba);
// hfield and mesh attributes
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") {
WriteAttrTxt(elem, "class", site->classname);
}
WriteAttr(elem, "pos", 3, site->pos);
WriteAttr(elem, "quat", 4, site->quat, unitq);
if (mjGEOMINFO[site->type]) {
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
WriteAttrInt(elem, "group", site->group, def->Site().group);
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type);
WriteAttrTable(elem, static_cast<const mjsSite*>(site), &def->Site().spec,
kSiteAttrs, kSiteAttrsN);
if (site->get_material() != def->Site().get_material()) {
WriteAttrTxt(elem, "material", site->get_material());
}
WriteAttr(elem, "rgba", 4, site->rgba, def->Site().rgba);
// userdata
if (writingdefaults) {
@@ -584,28 +517,12 @@ void mjXWriter::OneCamera(XMLElement* elem, const mjCCamera* camera, mjCDef* def
WriteAttrTxt(elem, "class", camera->classname);
}
WriteAttrTxt(elem, "target", camera->get_targetbody());
WriteAttr(elem, "pos", 3, camera->pos);
WriteAttr(elem, "quat", 4, camera->quat, unitq);
}
// defaults and regular
WriteAttr(elem, "ipd", 1, &camera->ipd, &def->Camera().ipd);
WriteAttrKey(elem, "mode", camlight_map, camlight_sz, camera->mode, def->Camera().mode);
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);
WriteAttrTable(elem, static_cast<const mjsCamera*>(camera),
&def->Camera().spec, kCameraAttrs, kCameraAttrsN);
// camera intrinsics if specified
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, "target", light->get_targetbody());
WriteAttr(elem, "pos", 3, light->pos);
WriteAttr(elem, "dir", 3, light->dir);
}
// defaults and regular
WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius);
WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity);
WriteAttr(elem, "range", 1, &light->range, &def->Light().range);
WriteAttrTable(elem, static_cast<const mjsLight*>(light), &def->Light().spec,
kLightAttrs, kLightAttrsN);
WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type);
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 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) {
// regular
if (!writingdefaults) {
@@ -670,15 +645,8 @@ void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) {
// defaults and regular
WriteAttrTxt(elem, "name", pair->name);
WriteAttrInt(elem, "condim", pair->condim, def->Pair().spec.condim);
WriteAttr(elem, "margin", 1, &pair->margin, &def->Pair().spec.margin);
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
WriteAttrTable(elem, static_cast<const mjsPair*>(pair), &def->Pair().spec,
kPairAttrs, kPairAttrsN);
}
@@ -745,9 +713,9 @@ void mjXWriter::OneEquality(XMLElement* elem, const mjCEquality* equality, mjCDe
}
// defaults and regular
WriteAttrKey(elem, "active", bool_map, 2, equality->active, def->Equality().active);
WriteAttr(elem, "solref", mjNREF, equality->solref, def->Equality().solref, true);
WriteAttr(elem, "solimp", mjNIMP, equality->solimp, def->Equality().solimp, true);
WriteAttrTable(elem, static_cast<const mjsEquality*>(equality),
&def->Equality().spec, kEqualityBaseAttrs,
kEqualityBaseAttrsN);
}
@@ -764,46 +732,23 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def
}
}
// defaults and regular
WriteAttrInt(elem, "group", tendon->group, def->Tendon().group);
WriteAttr(elem, "solreflimit", mjNREF, tendon->solref_limit, def->Tendon().solref_limit, true);
WriteAttr(elem, "solimplimit", mjNIMP, tendon->solimp_limit, def->Tendon().solimp_limit, true);
WriteAttr(elem, "solreffriction", mjNREF, tendon->solref_friction, def->Tendon().solref_friction,
true);
WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction,
true);
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);
// defaults and regular; the fixed rows are the spatial rows without the
// appearance attributes, which is exactly the tag difference
if (fixed) {
WriteAttrTable(elem, static_cast<const mjsTendon*>(tendon),
&def->Tendon().spec, kFixedAttrs, kFixedAttrsN);
} else {
WriteAttrTable(elem, static_cast<const mjsTendon*>(tendon),
&def->Tendon().spec, kSpatialAttrs, kSpatialAttrsN);
}
{
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] ||
def->Tendon().springlength[0] != def->Tendon().springlength[1]) {
WriteAttr(elem, "springlength", 2, tendon->springlength, def->Tendon().springlength);
} else {
WriteAttr(elem, "springlength", 1, tendon->springlength, def->Tendon().springlength);
}
// spatial only
if (!fixed) {
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);
if (!fixed && tendon->get_material() != def->Tendon().get_material()) {
WriteAttrTxt(elem, "material", tendon->get_material());
}
// userdata
@@ -859,28 +804,9 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
}
// defaults and regular
WriteAttrInt(elem, "group", actuator->group, def->Actuator().group);
WriteAttrInt(elem, "nsample", actuator->nsample, def->Actuator().nsample);
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);
WriteAttrTable(elem, static_cast<const mjsActuator*>(actuator),
&def->Actuator().spec, kGeneralAttrs, kGeneralAttrsN);
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
if (writingdefaults) {
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);
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
if (actuator->plugin.active) {
@@ -911,8 +835,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
}
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);
WriteAttr(elem, "gainprm", mjNGAIN, actuator->gainprm, def->Actuator().gainprm, 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);
Option(root);
Size(root);
Visual(root);
Statistic(root);
Visual(root);
writingdefaults = true;
Default(root, model->Default());
writingdefaults = false;
Extension(root);
Custom(root);
Asset(root);
Body(InsertEnd(root, "worldbody"), model->GetWorld(), nullptr);
Contact(root);
Deformable(root);
Equality(root);
Contact(root);
Tendon(root);
Equality(root);
Actuator(root);
Sensor(root);
Custom(root);
Keyframe(root);
return WriteDoc(doc, error, error_sz);
@@ -1063,39 +985,8 @@ void mjXWriter::Option(XMLElement* root) {
XMLElement* section = InsertEnd(root, "option");
// option
WriteAttr(section, "timestep", 1, &model->option.timestep, &opt.timestep);
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);
// option; the freshly-defaulted struct is the comparison object
WriteAttrTable(section, &model->option, &opt, kOptionAttrs, kOptionAttrsN);
// actuator group disable
int disabled_groups[31];
@@ -1165,20 +1056,16 @@ void mjXWriter::Size(XMLElement* root) {
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, "nconmax", model->nconmax, -1);
WriteAttrInt(section, "nstack", model->nstack, -1);
WriteAttrInt(section, "nuserdata", model->nuserdata, 0);
WriteAttrInt(section, "nkey", model->nkey, 0);
WriteAttrInt(section, "nuser_body", model->nuser_body, 0);
WriteAttrInt(section, "nuser_jnt", model->nuser_jnt, 0);
WriteAttrInt(section, "nuser_geom", model->nuser_geom, 0);
WriteAttrInt(section, "nuser_site", model->nuser_site, 0);
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);
// write sizes; the spec defaults are -1 (auto), but compilation resolves
// them to the actual counts, so the comparison object is zero-initialized
mjSpec zerospec = {};
WriteAttrTable(section, static_cast<const mjSpec*>(model), &zerospec,
kSizeAttrs, kSizeAttrsN);
// remove entire section if no attributes
if (!section->FirstAttribute()) root->DeleteChild(section);
@@ -1189,13 +1076,11 @@ void mjXWriter::Size(XMLElement* root) {
// statistic section
void mjXWriter::Statistic(XMLElement* root) {
XMLElement* section = InsertEnd(root, "statistic");
mjStatistic* s = &model->stat;
if (mjuu_defined(s->meaninertia)) WriteAttr(section, "meaninertia", 1, &s->meaninertia);
if (mjuu_defined(s->meanmass)) WriteAttr(section, "meanmass", 1, &s->meanmass);
if (mjuu_defined(s->meansize)) WriteAttr(section, "meansize", 1, &s->meansize);
if (mjuu_defined(s->extent)) WriteAttr(section, "extent", 1, &s->extent);
if (mjuu_defined(s->center[0])) WriteAttr(section, "center", 3, s->center);
// statistics are unset (mjNAN) rather than defaulted: there is nothing to
// compare against, and WriteAttr skips the undefined values by itself
WriteAttrTable(section, &model->stat, (const mjStatistic*)nullptr,
kStatisticAttrs, kStatisticAttrsN);
// remove entire section if no attributes
if (!section->FirstAttribute()) root->DeleteChild(section);
@@ -1207,123 +1092,25 @@ void mjXWriter::Statistic(XMLElement* root) {
void mjXWriter::Visual(XMLElement* root) {
mjVisual visdef, *vis = &model->visual;
mj_defaultVisual(&visdef);
XMLElement* elem;
XMLElement* section = InsertEnd(root, "visual");
// global
elem = InsertEnd(section, "global");
WriteAttrInt(elem, "cameraid", vis->global.cameraid, visdef.global.cameraid);
WriteAttrKey(elem, "orthographic",
bool_map, 2, vis->global.orthographic, visdef.global.orthographic);
WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy);
WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd);
WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth);
WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation);
WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth);
WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow);
WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime);
WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth);
WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight);
WriteAttrKey(elem, "ellipsoidinertia",
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);
// the sub-sections are projections into mjVisual: their rows carry
// member-path offsets, so one struct pair drives them all
struct { const char* tag; const mjXAttr* rows; int n; } subs[] = {
{"global", kGlobalAttrs, kGlobalAttrsN},
{"quality", kQualityAttrs, kQualityAttrsN},
{"headlight", kHeadlightAttrs, kHeadlightAttrsN},
{"map", kMapAttrs, kMapAttrsN},
{"scale", kScaleAttrs, kScaleAttrsN},
{"rgba", kRgbaAttrs, kRgbaAttrsN},
};
for (const auto& sub : subs) {
XMLElement* elem = InsertEnd(section, sub.tag);
WriteAttrTable(elem, vis, &visdef, sub.rows, sub.n);
if (!elem->FirstAttribute()) {
section->DeleteChild(elem);
}
}
// remove entire section if no elements
+10
View File
@@ -62,6 +62,16 @@ class mjXWriter : public mjXBase {
// body/world section
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
void OneFlex(tinyxml2::XMLElement* elem, const mjCFlex* pflex);
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
void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map,
int mapsz, int* data, int ndata, int def) {
if (ndata <= 0) {
return;
}
// skip default
if (ndata == 1 && data[0] == def) {
return;
+3 -1
View File
@@ -551,9 +551,11 @@ TEST_F(EnginePluginTest, SaveXml) {
int actuator_end = expected_xml.find(actuator_close) + actuator_close.size();
ASSERT_NE(actuator_end, std::string::npos);
ASSERT_LE(actuator_start, actuator_end);
// saved attributes follow schema order; dynprm round-trips as written
auto expected_actuator_section = absl::StrReplaceAll(
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_sensor_section));
+25 -13
View File
@@ -324,7 +324,7 @@ TEST_F(XMLWriterTest, KeepsJointLimitedFalseIfAutoLimits) {
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
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) {
@@ -537,7 +537,7 @@ TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) {
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
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) {
@@ -702,8 +702,8 @@ TEST_F(XMLWriterTest, WritesActuatorDefaults) {
ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, Not(HasSubstr("mass")));
EXPECT_THAT(saved_xml, HasSubstr(
"<general biastype=\"affine\" gainprm=\"3\""));
EXPECT_THAT(saved_xml,
HasSubstr("<general biastype=\"affine\" gainprm=\"3\""));
}
TEST_F(XMLWriterTest, WritesFrameDefaults) {
@@ -763,7 +763,7 @@ TEST_F(XMLWriterTest, WritesFrameDefaults) {
</frame>
</frame>
</frame>
<light pos="0 0 1" dir="0 0 -1"/>
<light pos="0 0 1"/>
</body>
<frame name="f1">
<geom size="0.5" quat="0.906308 0 0 0.422618"/>
@@ -930,8 +930,8 @@ TEST_F(XMLWriterTest, WritesSkin) {
EXPECT_THAT(model->nskin, 1);
char error[1024];
MjModelPtr mtemp = LoadModelFromString(SaveAndReadXml(model.get()),
error, sizeof(error));
MjModelPtr mtemp =
LoadModelFromString(SaveAndReadXml(model.get()), error, sizeof(error));
ASSERT_THAT(mtemp.get(), NotNull()) << error;
EXPECT_THAT(mtemp->nskin, 1);
}
@@ -950,7 +950,7 @@ TEST_F(XMLWriterTest, WritesHfield) {
// load model
MjModelPtr model = LoadModelFromString(xml);
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);
// 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 {
std::setlocale(LC_ALL, old_locale_.c_str());
}
void TearDown() override { std::setlocale(LC_ALL, old_locale_.c_str()); }
private:
std::string old_locale_;
@@ -1304,7 +1302,6 @@ TEST_F(XMLWriterTest, NonRGBTextures) {
mj_deleteModel(model);
}
// ---------------- test CopyBack functionality (decompiler) ------------------
using DecompilerTest = MujocoTest;
TEST_F(DecompilerTest, SavesStatistics) {
@@ -1469,7 +1466,8 @@ TEST_F(XMLWriterTest, ExpandAttach) {
mj_addBufferVFS(vfs.get(), "b.xml", xml_child, sizeof(xml_child));
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();
std::string saved_xml = SaveAndReadXml(m.get());
@@ -1626,5 +1624,19 @@ TEST_F(XMLWriterTest, BodySimpleRoundtrip) {
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 mujoco