// Copyright 2025 DeepMind Technologies Limited // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // NOLINTBEGIN(whitespace/line_length) // NOLINTBEGIN(whitespace/semicolon) #include #include #include #include #include #include #include #include #include #include #include // NOLINT #include #include // NOLINT #include #include // NOLINT #include #include #include #include #include #include #include "engine/engine_util_errmem.h" #include "wasm/unpack.h" #include "python/mujoco/indexer_xmacro.h" namespace mujoco::wasm { using emscripten::enum_; using emscripten::function; using emscripten::val; using emscripten::typed_memory_view; using emscripten::return_value_policy::reference; using emscripten::return_value_policy::take_ownership; EMSCRIPTEN_DECLARE_VAL_TYPE(NumberOrString); EMSCRIPTEN_DECLARE_VAL_TYPE(NumberArray); EMSCRIPTEN_DECLARE_VAL_TYPE(String); EMSCRIPTEN_DECLARE_VAL_TYPE(StringOrNull); // Macro to define accessors for different MuJoCo object types within mjModel. // Each line calls X_ACCESSOR with the following arguments: // 1. NAME: The object type name in uppercase (e.g., ACTUATOR). // 2. Name: The object type name in CamelCase (e.g., Actuator). // 3. OBJTYPE: The corresponding mjOBJ_* enum value (e.g., mjOBJ_ACTUATOR). // 4. field: The name of the array field in mjModel (e.g., actuator). // 5. nfield: The name of the count field in mjModel (e.g., nu). #define MJMODEL_ACCESSORS \ X_ACCESSOR( ACTUATOR, Actuator, mjOBJ_ACTUATOR, actuator, nu ) \ X_ACCESSOR( BODY, Body, mjOBJ_BODY, body, nbody ) \ X_ACCESSOR( CAMERA, Camera, mjOBJ_CAMERA, cam, ncam ) \ X_ACCESSOR( EQUALITY, Equality, mjOBJ_EQUALITY, eq, neq ) \ X_ACCESSOR( EXCLUDE, Exclude, mjOBJ_EXCLUDE, exclude, nexclude ) \ X_ACCESSOR( GEOM, Geom, mjOBJ_GEOM, geom, ngeom ) \ X_ACCESSOR( HFIELD, Hfield, mjOBJ_HFIELD, hfield, nhfield ) \ X_ACCESSOR( JOINT, Joint, mjOBJ_JOINT, jnt, njnt ) \ X_ACCESSOR( LIGHT, Light, mjOBJ_LIGHT, light, nlight ) \ X_ACCESSOR( MATERIAL, Material, mjOBJ_MATERIAL, mat, nmat ) \ X_ACCESSOR( MESH, Mesh, mjOBJ_MESH, mesh, nmesh ) \ X_ACCESSOR( NUMERIC, Numeric, mjOBJ_NUMERIC, numeric, nnumeric ) \ X_ACCESSOR( PAIR, Pair, mjOBJ_PAIR, pair, npair ) \ X_ACCESSOR( SENSOR, Sensor, mjOBJ_SENSOR, sensor, nsensor ) \ X_ACCESSOR( SITE, Site, mjOBJ_SITE, site, nsite ) \ X_ACCESSOR( SKIN, Skin, mjOBJ_SKIN, skin, nskin ) \ X_ACCESSOR( TENDON, Tendon, mjOBJ_TENDON, tendon, ntendon ) \ X_ACCESSOR( TEXTURE, Texture, mjOBJ_TEXTURE, tex, ntex ) \ X_ACCESSOR( TUPLE, Tuple, mjOBJ_TUPLE, tuple, ntuple ) \ X_ACCESSOR( KEYFRAME, Keyframe, mjOBJ_KEY, key, nkey ) // Macro to define accessors for different MuJoCo object types within mjData. // Each line calls X_ACCESSOR with the following arguments: // 1. NAME: The object type name in uppercase (e.g., ACTUATOR). // 2. Name: The object type name in CamelCase (e.g., Actuator). // 3. OBJTYPE: The corresponding mjOBJ_* enum value (e.g., mjOBJ_ACTUATOR). // 4. field: The name of the array field in mjData (e.g., actuator). // 5. nfield: The name of the count field in mjModel (e.g., nu). #define MJDATA_ACCESSORS \ X_ACCESSOR( ACTUATOR, Actuator, mjOBJ_ACTUATOR, actuator, nu ) \ X_ACCESSOR( BODY, Body, mjOBJ_BODY, body, nbody ) \ X_ACCESSOR( CAMERA, Camera, mjOBJ_CAMERA, cam, ncam ) \ X_ACCESSOR( GEOM, Geom, mjOBJ_GEOM, geom, ngeom ) \ X_ACCESSOR( JOINT, Joint, mjOBJ_JOINT, jnt, njnt ) \ X_ACCESSOR( LIGHT, Light, mjOBJ_LIGHT, light, nlight ) \ X_ACCESSOR( SENSOR, Sensor, mjOBJ_SENSOR, sensor, nsensor ) \ X_ACCESSOR( SITE, Site, mjOBJ_SITE, site, nsite ) \ X_ACCESSOR( TENDON, Tendon, mjOBJ_TENDON, tendon, ntendon ) // Raises an error if the given val is null or undefined. // A macro is used so that the error contains the name of the variable. // TODO(matijak): Remove this when we can handle strings using UNPACK_STRING? #define CHECK_VAL(val) \ if (val.isNull()) { \ mju_error("Invalid argument: %s is null", #val); \ } else if (val.isUndefined()) { \ mju_error("Invalid argument: %s is undefined", #val); \ } void ThrowMujocoErrorToJS(const char* msg) { // Get a handle to the JS global Error constructor function, create a new // object instance and then throw the object as an exception using the // val::throw_() helper function. val(val::global("Error").new_(val("MuJoCo Error: " + std::string(msg)))) .throw_(); } __attribute__((constructor)) void InitMuJoCoErrorHandler() { mju_user_error = ThrowMujocoErrorToJS; } // Generates a descriptive error message for when a key lookup fails. // The message includes the invalid name and a list of valid names of the // specified object type currently present in the model. // // Arguments: // model: Pointer to the mjModel. // objtype: The mjOBJ_* enum value representing the object type. // count: The number of objects of the given type in the model. // name: The invalid name that was looked up. // // Returns: // A string containing the error message. std::string KeyErrorMessage(const mjModel* model, int objtype, int count, std::string_view name, std::string_view accessor_name) { std::vector valid_names; valid_names.reserve(count); for (int i = 0; i < count; ++i) { const char* n = mj_id2name(model, objtype, i); if (n) { valid_names.push_back(n); } } std::sort(valid_names.begin(), valid_names.end()); std::ostringstream message; message << "Invalid name '" << name << "' for " << accessor_name << ". Valid names: ["; for (size_t i = 0; i < valid_names.size(); ++i) { message << "'" << valid_names[i] << "'"; if (i < valid_names.size() - 1) { message << ", "; } } message << "]"; return message.str(); } std::string IndexErrorMessage(int index, int count, std::string_view accessor_name) { std::ostringstream message; message << "Invalid index " << index << " for " << accessor_name << ". Valid indices from 0 to " << count - 1; return message.str(); } template val MakeValArray(const char* (&strings)[N]) { val result = val::array(); for (int i = 0; i < N; i++) { result.call("push", val(strings[i])); } return result; } template val MakeValArray3(const char* (&strings)[N][M]) { val result = val::array(); for (int i = 0; i < N; i++) { val inner = val::array(); for (int j = 0; j < M; j++) { inner.call("push", val(strings[i][j])); } result.call("push", inner); } return result; } template std::vector InitWrapperArray(ArrayType* array, SizeType size) { std::vector result; result.reserve(size); for (int i = 0; i < size; ++i) { result.emplace_back(&array[i]); } return result; } val get_mjDISABLESTRING() { return MakeValArray(mjDISABLESTRING); } val get_mjENABLESTRING() { return MakeValArray(mjENABLESTRING); } val get_mjTIMERSTRING() { return MakeValArray(mjTIMERSTRING); } val get_mjLABELSTRING() { return MakeValArray(mjLABELSTRING); } val get_mjFRAMESTRING() { return MakeValArray(mjFRAMESTRING); } val get_mjVISSTRING() { return MakeValArray3(mjVISSTRING); } val get_mjRNDSTRING() { return MakeValArray3(mjRNDSTRING); } // {{ ANONYMOUS_STRUCT_TYPEDEFS }} #undef MJ_M #define MJ_M(n) model_->n // The X macro expands to a member function within an MjModel...Accessor struct. // This function returns an emscripten::val, typically a typed memory view, // providing access to array data within the underlying mjModel. The size and // offset of the memory view are determined by the arguments: // - type: The C++ type of the array elements (e.g., mjtNum, int). // - prefix: The prefix of the field name in mjModel (e.g., jnt_, geom_). // - var: The name of the field being accessed (e.g., qposadr, size). // - dim0: Used to determine special indexing logic for dynamically sized arrays // like joints (nq, nv), hfields, textures, numerics, and tuples. // - dim1: The fixed dimension of the array if not dynamically sized. If "1", // a single element is returned. Otherwise, it's used as the stride // for the typed memory view. #define X(type, prefix, var, dim0, dim1) \ emscripten::val get_##var() const { \ if constexpr (std::string_view(#dim0) == "nq") { \ int start = model_->jnt_qposadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_qposadr[id_ + 1] : model_->nq; \ return val(typed_memory_view(end - start, model_->prefix##var + start)); \ } else if constexpr (std::string_view(#dim0) == "nv") { \ int start = model_->jnt_dofadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_dofadr[id_ + 1] : model_->nv; \ return val(typed_memory_view(end - start, model_->prefix##var + start)); \ } else if constexpr (std::string_view(#dim0) == "nhfielddata") { \ int start = model_->hfield_adr[id_]; \ int count = model_->hfield_nrow[id_] * model_->hfield_ncol[id_]; \ return val(typed_memory_view(count, model_->hfield_data + start)); \ } else if constexpr (std::string_view(#dim0) == "ntexdata") { \ int start = model_->tex_adr[id_]; \ int count = model_->tex_height[id_] * model_->tex_width[id_] * model_->tex_nchannel[id_]; \ return val(typed_memory_view(count, model_->tex_data + start)); \ } else if constexpr (std::string_view(#dim0) == "nnumericdata") { \ int start = model_->numeric_adr[id_]; \ int count = model_->numeric_size[id_]; \ return val(typed_memory_view(count, model_->numeric_data + start)); \ } else if constexpr (std::string_view(#dim0) == "ntupledata") { \ int start = model_->tuple_adr[id_]; \ int count = model_->tuple_size[id_]; \ return val(typed_memory_view(count, model_->prefix##var + start)); \ } else { \ if constexpr (std::string_view(#dim1) == "1") { \ return val(model_->prefix##var[id_]); \ } else { \ return val(typed_memory_view(dim1, model_->prefix##var + id_ * dim1)); \ } \ } \ } \ void set_##var(const emscripten::val& value) { \ if constexpr (std::string_view(#dim0) == "nq") { \ int start = model_->jnt_qposadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_qposadr[id_ + 1] : model_->nq; \ val(typed_memory_view(end - start, model_->prefix##var + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "nv") { \ int start = model_->jnt_dofadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_dofadr[id_ + 1] : model_->nv; \ val(typed_memory_view(end - start, model_->prefix##var + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "nhfielddata") { \ int start = model_->hfield_adr[id_]; \ int count = model_->hfield_nrow[id_] * model_->hfield_ncol[id_]; \ val(typed_memory_view(count, model_->hfield_data + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "ntexdata") { \ int start = model_->tex_adr[id_]; \ int count = model_->tex_height[id_] * model_->tex_width[id_] * model_->tex_nchannel[id_]; \ val(typed_memory_view(count, model_->tex_data + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "nnumericdata") { \ int start = model_->numeric_adr[id_]; \ int count = model_->numeric_size[id_]; \ val(typed_memory_view(count, model_->numeric_data + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "ntupledata") { \ int start = model_->tuple_adr[id_]; \ int count = model_->tuple_size[id_]; \ val(typed_memory_view(count, model_->prefix##var + start)) \ .call("set", value); \ } else { \ if constexpr (std::string_view(#dim1) == "1") { \ model_->prefix##var[id_] = value.as(); \ } else { \ val(typed_memory_view(dim1, model_->prefix##var + id_ * dim1)) \ .call("set", value); \ } \ } \ } // Expands to a struct definition for each object type in MJMODEL_ACCESSORS. // Each struct, named `MjModel{Name}Accessor`, provides: // - A constructor taking an `mjModel*` and an integer `id`. // - An `id()` method to get the object's index. // - A `name()` method to get the object's name using `mj_id2name`. // - Member functions generated by the `MJMODEL_##NAME` macro, which in turn // uses the `X` macro to define accessors for fields within `mjModel`. #define X_ACCESSOR(NAME, Name, OBJTYPE, field, nfield) \ struct MjModel##Name##Accessor { \ MjModel##Name##Accessor(mjModel* model, int id) : model_(model), id_(id) {} \ \ int id() const { return id_; } \ std::string name() const { \ const char* name = mj_id2name(model_, OBJTYPE, id_); \ return name ? name : ""; \ } \ \ MJMODEL_##NAME \ \ private: \ mjModel* model_; \ int id_; \ }; MJMODEL_ACCESSORS #undef X_ACCESSOR // Expands to a struct definition for each object type in MJDATA_ACCESSORS. // Each struct, named `MjData{Name}Accessor`, provides: // - A constructor taking an `mjData*`, an `mjModel*`, and an integer `id`. // - An `id()` method to get the object's index. // - A `name()` method to get the object's name using `mj_id2name`. // - Member functions generated by the `MJDATA_##NAME` macro, which in turn // uses the `X` macro to define accessors for fields within `mjData`. #undef MJ_M #define MJ_M(n) model_->n #undef X #define X(type, prefix, var, dim0, dim1) \ emscripten::val get_##var() const { \ if constexpr (std::string_view(#dim0) == "nq") { \ int start = model_->jnt_qposadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_qposadr[id_ + 1] : model_->nq; \ return val(typed_memory_view(end - start, data_->prefix##var + start)); \ } else if constexpr (std::string_view(#dim0) == "nv") { \ int start = model_->jnt_dofadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_dofadr[id_ + 1] : model_->nv; \ if constexpr (std::string_view(#dim1) == "1") { \ return val(typed_memory_view(end - start, data_->prefix##var + start)); \ } else { \ return val(typed_memory_view( \ (end - start) * dim1, data_->prefix##var + start * dim1)); \ } \ } else if constexpr (std::string_view(#dim0) == "nsensordata") { \ int start = model_->sensor_adr[id_]; \ int count = model_->sensor_dim[id_]; \ return val(typed_memory_view(count, data_->sensordata + start)); \ } else { \ if constexpr (std::string_view(#dim1) == "1") { \ return val(data_->prefix##var[id_]); \ } else { \ return val(typed_memory_view(dim1, data_->prefix##var + id_ * dim1)); \ } \ } \ } \ void set_##var(const emscripten::val& value) { \ if constexpr (std::string_view(#dim0) == "nq") { \ int start = model_->jnt_qposadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_qposadr[id_ + 1] : model_->nq; \ val(typed_memory_view(end - start, data_->prefix##var + start)) \ .call("set", value); \ } else if constexpr (std::string_view(#dim0) == "nv") { \ int start = model_->jnt_dofadr[id_]; \ int end = (id_ < model_->njnt - 1) ? model_->jnt_dofadr[id_ + 1] : model_->nv; \ if constexpr (std::string_view(#dim1) == "1") { \ val(typed_memory_view(end - start, data_->prefix##var + start)) \ .call("set", value); \ } else { \ val(typed_memory_view((end - start) * dim1, \ data_->prefix##var + start * dim1)) \ .call("set", value); \ } \ } else if constexpr (std::string_view(#dim0) == "nsensordata") { \ int start = model_->sensor_adr[id_]; \ int count = model_->sensor_dim[id_]; \ val(typed_memory_view(count, data_->sensordata + start)) \ .call("set", value); \ } else { \ if constexpr (std::string_view(#dim1) == "1") { \ data_->prefix##var[id_] = value.as(); \ } else { \ val(typed_memory_view(dim1, data_->prefix##var + id_ * dim1)) \ .call("set", value); \ } \ } \ } #define X_ACCESSOR(NAME, Name, OBJTYPE, field, nfield) \ struct MjData##Name##Accessor { \ MjData##Name##Accessor(mjData* data, mjModel* model, int id) : data_(data), model_(model), id_(id) {} \ \ int id() const { return id_; } \ std::string name() const { \ const char* name = mj_id2name(model_, OBJTYPE, id_); \ return name ? name : ""; \ } \ \ MJDATA_##NAME \ \ private: \ mjData* data_; \ mjModel* model_; \ int id_; \ }; MJDATA_ACCESSORS #undef X_ACCESSOR #undef X #undef MJ_M // {{ STRUCTS_HEADER }} // {{ STRUCTS_SOURCE }} struct MjvScene { MjvScene(); MjvScene(MjModel *m, int maxgeom); ~MjvScene(); std::unique_ptr copy(); int GetSumFlexFaces() const; mjvScene* get() const; void set(mjvScene* ptr); std::vector geoms() const; emscripten::val geomorder() const { return emscripten::val( emscripten::typed_memory_view(ptr_->ngeom, ptr_->geomorder)); } emscripten::val flexedgeadr() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgeadr)); } emscripten::val flexedgenum() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgenum)); } emscripten::val flexvertadr() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertadr)); } emscripten::val flexvertnum() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertnum)); } emscripten::val flexfaceadr() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceadr)); } emscripten::val flexfacenum() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfacenum)); } emscripten::val flexfaceused() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceused)); } emscripten::val flexedge() const { return emscripten::val( emscripten::typed_memory_view(2 * model->nflexedge, ptr_->flexedge)); } emscripten::val flexvert() const { return emscripten::val( emscripten::typed_memory_view(3 * model->nflexvert, ptr_->flexvert)); } emscripten::val skinfacenum() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nskin, ptr_->skinfacenum)); } emscripten::val skinvertadr() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertadr)); } emscripten::val skinvertnum() const { return emscripten::val( emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertnum)); } emscripten::val skinvert() const { return emscripten::val( emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinvert)); } emscripten::val skinnormal() const { return emscripten::val( emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinnormal)); } emscripten::val flexface() const { return emscripten::val(emscripten::typed_memory_view( 9 * MjvScene::GetSumFlexFaces(), ptr_->flexface)); } emscripten::val flexnormal() const { return emscripten::val(emscripten::typed_memory_view( 9 * MjvScene::GetSumFlexFaces(), ptr_->flexnormal)); } emscripten::val flextexcoord() const { return emscripten::val(emscripten::typed_memory_view( 6 * MjvScene::GetSumFlexFaces(), ptr_->flextexcoord)); } // INSERT-GENERATED-MjvScene-DECLARATION private: mjvScene* ptr_; bool owned_ = false; public: mjModel *model; std::vector lights; std::vector camera; }; struct MjVFS { MjVFS() : ptr_(new mjVFS) { mj_defaultVFS(ptr_); } ~MjVFS() { mj_deleteVFS(ptr_); } void AddBuffer(const std::string& name, const emscripten::val& buffer) { std::vector vec = emscripten::vecFromJSArray(buffer); int result = mj_addBufferVFS(ptr_, name.c_str(), vec.data(), vec.size()); if (result != 0) { mju_error("Could not add buffer to VFS: %d", result); } } void DeleteFile(const std::string& filename) { mj_deleteFileVFS(ptr_, filename.c_str()); } mjVFS* get() const { return ptr_; } private: mjVFS* ptr_; }; MjModel::MjModel(mjModel* ptr) : ptr_(ptr), opt(&ptr->opt), vis(&ptr->vis), stat(&ptr->stat) {} MjModel::MjModel(const MjModel& other) : ptr_(mj_copyModel(nullptr, other.get())), opt(&ptr_->opt), vis(&ptr_->vis), stat(&ptr_->stat) {} MjModel::~MjModel() { if (ptr_) { mj_deleteModel(ptr_); } } mjModel* MjModel::get() const { return ptr_; } void MjModel::set(mjModel* ptr) { ptr_ = ptr; } MjData::MjData(MjModel* m) { model = m->get(); ptr_ = mj_makeData(model); if (ptr_) { solver = InitWrapperArray(get()->solver, mjNSOLVER * mjNISLAND); timer = InitWrapperArray(get()->timer, mjNTIMER); warning = InitWrapperArray(get()->warning, mjNWARNING); } } MjData::MjData(const MjModel& model, const MjData& other) : ptr_(mj_copyData(nullptr, model.get(), other.get())), model(model.get()) { if (ptr_) { solver = InitWrapperArray(get()->solver, mjNSOLVER * mjNISLAND); timer = InitWrapperArray(get()->timer, mjNTIMER); warning = InitWrapperArray(get()->warning, mjNWARNING); } } MjData::~MjData() { if (ptr_) { mj_deleteData(ptr_); } } mjData* MjData::get() const { return ptr_; } void MjData::set(mjData* ptr) { ptr_ = ptr; } std::vector MjData::contact() const { return InitWrapperArray(get()->contact, get()->ncon); } MjvScene::MjvScene() { owned_ = true; ptr_ = new mjvScene; mjv_defaultScene(ptr_); mjv_makeScene(nullptr, ptr_, 0); lights = InitWrapperArray(ptr_->lights, mjMAXLIGHT); camera = InitWrapperArray(ptr_->camera, 2); }; MjvScene::MjvScene(MjModel* m, int maxgeom) { owned_ = true; model = m->get(); ptr_ = new mjvScene; mjv_defaultScene(ptr_); mjv_makeScene(model, ptr_, maxgeom); lights = InitWrapperArray(ptr_->lights, mjMAXLIGHT); camera = InitWrapperArray(ptr_->camera, 2); }; MjvScene::~MjvScene() { if (owned_ && ptr_) { mjv_freeScene(ptr_); delete ptr_; } } mjvScene* MjvScene::get() const { return ptr_; } void MjvScene::set(mjvScene* ptr) { ptr_ = ptr; } // Taken from the python mujoco bindings code for MjvScene Wrapper int MjvScene::GetSumFlexFaces() const { int nflexface = 0; int flexfacenum = 0; for (int f = 0; f < model->nflex; f++) { if (model->flex_dim[f] == 0) { // 1D : 0 flexfacenum = 0; } else if (model->flex_dim[f] == 2) { // 2D: 2*fragments + 2*elements flexfacenum = 2 * model->flex_shellnum[f] + 2 * model->flex_elemnum[f]; } else { // 3D: max(fragments, 4*maxlayer) // find number of elements in biggest layer int maxlayer = 0, layer = 0, nlayer = 1; while (nlayer) { nlayer = 0; for (int e = 0; e < model->flex_elemnum[f]; e++) { if (model->flex_elemlayer[model->flex_elemadr[f] + e] == layer) { nlayer++; } } maxlayer = mjMAX(maxlayer, nlayer); layer++; } flexfacenum = mjMAX(model->flex_shellnum[f], 4 * maxlayer); } // accumulate over flexes nflexface += flexfacenum; } return nflexface; } std::vector MjvScene::geoms() const { return InitWrapperArray(ptr_->geoms, ptr_->ngeom); } MjSpec::MjSpec() : ptr_(mj_makeSpec()), element(ptr_->element), compiler(&ptr_->compiler), option(&ptr_->option), visual(&ptr_->visual), stat(&ptr_->stat) { owned_ = true; mjs_defaultSpec(ptr_); }; MjSpec::MjSpec(mjSpec *ptr) : ptr_(ptr), element(ptr_->element), compiler(&ptr_->compiler), option(&ptr_->option), visual(&ptr_->visual), stat(&ptr_->stat) {} MjSpec::MjSpec(const MjSpec &other) : ptr_(mj_copySpec(other.get())), element(ptr_->element), compiler(&ptr_->compiler), option(&ptr_->option), visual(&ptr_->visual), stat(&ptr_->stat) { owned_ = true; } MjSpec& MjSpec::operator=(const MjSpec &other) { if (this == &other) { return *this; } if (owned_ && ptr_) { mj_deleteSpec(ptr_); } ptr_ = mj_copySpec(other.get()); owned_ = true; option.set(&ptr_->option); visual.set(&ptr_->visual); stat.set(&ptr_->stat); compiler.set(&ptr_->compiler); element.set(ptr_->element); return *this; } MjSpec::~MjSpec() { if (ptr_ && owned_) { mj_deleteSpec(ptr_); } } mjSpec *MjSpec::get() const { return ptr_; } void MjSpec::set(mjSpec *ptr) { ptr_ = ptr; } emscripten::val MjSpec::timer() const { return emscripten::val(emscripten::typed_memory_view(9, mjs_getTimer(ptr_))); } std::unique_ptr mj_loadXML_wrapper_1(std::string filename) { char error[1000]; mjModel *model = mj_loadXML(filename.c_str(), nullptr, error, sizeof(error)); if (!model) { mju_error("Loading error: %s\n", error); } return std::unique_ptr(new MjModel(model)); } std::unique_ptr mj_loadXML_wrapper_2(std::string filename, const MjVFS& vfs) { char error[1000]; mjModel *model = mj_loadXML(filename.c_str(), vfs.get(), error, sizeof(error)); if (!model) { mju_error("Loading error: %s\n", error); } return std::unique_ptr(new MjModel(model)); } void mj_saveModel_wrapper(const MjModel& m, const StringOrNull& filename, const val& buffer) { UNPACK_NULLABLE_STRING(filename); UNPACK_NULLABLE_VALUE(uint8_t, buffer); mj_saveModel(m.get(), filename_.data(), buffer_.data(), static_cast(buffer_.size())); } std::unique_ptr mj_loadModel_wrapper(std::string filename, const MjVFS& vfs) { mjModel *model = mj_loadModel(filename.c_str(), vfs.get()); if (!model) { mju_error("Failed to load from mjb"); } return std::unique_ptr(new MjModel(model)); } std::unique_ptr from_xml_string_wrapper_1(const std::string& xml) { mjVFS vfs; mj_defaultVFS(&vfs); const char* filename = "model.xml"; int add_result = mj_addBufferVFS(&vfs, filename, xml.c_str(), xml.length()); if (add_result != 0) { mj_deleteVFS(&vfs); mju_error("Could not add XML string to VFS: %d", add_result); } char error[1000]; mjModel* model = mj_loadXML(filename, &vfs, error, sizeof(error)); mj_deleteVFS(&vfs); if (!model) { mju_error("Loading error: %s\n", error); } return std::unique_ptr(new MjModel(model)); } std::unique_ptr from_xml_string_wrapper_2(const std::string& xml, const MjVFS& vfs) { std::string filename = "model.xml"; int add_result = mj_addBufferVFS(vfs.get(), filename.c_str(), xml.c_str(), xml.length()); if (add_result != 0) { mju_error("Could not add XML string to VFS: %d", add_result); } char error[1000]; mjModel* model = mj_loadXML(filename.c_str(), vfs.get(), error, sizeof(error)); mj_deleteFileVFS(vfs.get(), filename.c_str()); if (!model) { mju_error("Loading error: %s\n", error); } return std::unique_ptr(new MjModel(model)); } std::unique_ptr parseXMLString_wrapper(const std::string &xml) { char error[1000]; mjSpec *ptr = mj_parseXMLString(xml.c_str(), nullptr, error, sizeof(error)); if (!ptr) { mju_error("Could not create Spec from XML string: %s\n", error); } return std::unique_ptr(new MjSpec(ptr)); } std::unique_ptr mj_compile_wrapper_1(const MjSpec& spec) { mjSpec* spec_ptr = spec.get(); mjModel* model = mj_compile(spec_ptr, nullptr); if (!model || mjs_isWarning(spec_ptr)) { mju_error("%s", mjs_getError(spec_ptr)); } return std::unique_ptr(new MjModel(model)); } std::unique_ptr mj_compile_wrapper_2(const MjSpec& spec, const MjVFS& vfs) { mjSpec* spec_ptr = spec.get(); mjVFS* vfs_ptr = vfs.get(); mjModel* model = mj_compile(spec_ptr, vfs_ptr); if (!model || mjs_isWarning(spec_ptr)) { mju_error("%s", mjs_getError(spec_ptr)); } return std::unique_ptr(new MjModel(model)); } void error_wrapper(const String& msg) { mju_error("%s\n", msg.as().data()); } int mj_saveLastXML_wrapper(const String& filename, const MjModel& m) { CHECK_VAL(filename); std::array error; int result = mj_saveLastXML(filename.as().data(), m.get(), error.data(), error.size()); if (!result) { mju_error("%s", error.data()); } return result; } int mj_setLengthRange_wrapper(const MjModel& m, const MjData& d, int index, const MjLROpt& opt) { std::array error; int result = mj_setLengthRange(m.get(), d.get(), index, opt.get(), error.data(), error.size()); if (!result) { mju_error("%s", error.data()); } return result; } void mju_info_wrapper(int topic, const String& msg) { CHECK_VAL(msg); mju_info(topic, "%s", msg.as().data()); } // {{ WRAPPER_FUNCTIONS }} EMSCRIPTEN_BINDINGS(mujoco_bindings) { // {{ ENUM_BINDINGS }} // Bindings for the MjModel accessor classes. #define X(type, prefix, var, dim0, dim1) .property(#var, &Accessor::get_##var, &Accessor::set_##var) #define X_ACCESSOR(NAME, Name, OBJTYPE, field, nfield) \ { \ using Accessor = MjModel##Name##Accessor; \ emscripten::class_("MjModel" #Name "Accessor") \ .property("id", &Accessor::id) \ .property("name", &Accessor::name) \ MJMODEL_##NAME; \ } MJMODEL_ACCESSORS #undef X #undef X_ACCESSOR // Bindings for the MjData accessor classes. #define X(type, prefix, var, dim0, dim1) .property(#var, &Accessor::get_##var, &Accessor::set_##var) #define X_ACCESSOR(NAME, Name, OBJTYPE, field, nfield) \ { \ using Accessor = MjData##Name##Accessor; \ emscripten::class_("MjData" #Name "Accessor") \ .property("id", &Accessor::id) \ .property("name", &Accessor::name) \ MJDATA_##NAME; \ } MJDATA_ACCESSORS #undef X #undef X_ACCESSOR // {{ STRUCTS_BINDINGS }} emscripten::class_("MjVFS") .constructor<>() .function("addBuffer", &MjVFS::AddBuffer) .function("deleteFile", &MjVFS::DeleteFile); // {{ FUNCTION_BINDINGS }} function("parseXMLString", &parseXMLString_wrapper, take_ownership()); function("error", &error_wrapper); function("mj_saveModel", &mj_saveModel_wrapper); function("mj_saveLastXML", &mj_saveLastXML_wrapper); function("mj_setLengthRange", &mj_setLengthRange_wrapper); function("mju_info", &mju_info_wrapper); // mj_compile is bound using two overloads to handle the optional MjVFS argument, // as using std::optional caused memory errors due to missing copy/move constructors. function("mj_compile", emscripten::select_overload(const MjSpec&)>(&mj_compile_wrapper_1)); function("mj_compile", emscripten::select_overload(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2)); function("from_xml_string", emscripten::select_overload(const std::string&)>(&from_xml_string_wrapper_1)); function("from_xml_string", emscripten::select_overload(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2)); emscripten::class_>("FloatBuffer") .constructor() .class_function("FromArray", &WasmBuffer::FromArray) .function("GetPointer", &WasmBuffer::GetPointer) .function("GetElementCount", &WasmBuffer::GetElementCount) .function("GetView", &WasmBuffer::GetView); emscripten::class_>("DoubleBuffer") .constructor() .class_function("FromArray", &WasmBuffer::FromArray) .function("GetPointer", &WasmBuffer::GetPointer) .function("GetElementCount", &WasmBuffer::GetElementCount) .function("GetView", &WasmBuffer::GetView); emscripten::class_>("IntBuffer") .constructor() .class_function("FromArray", &WasmBuffer::FromArray) .function("GetPointer", &WasmBuffer::GetPointer) .function("GetElementCount", &WasmBuffer::GetElementCount) .function("GetView", &WasmBuffer::GetView); emscripten::class_>("Uint8Buffer") .constructor() .class_function("FromArray", &WasmBuffer::FromArray) .function("GetPointer", &WasmBuffer::GetPointer) .function("GetElementCount", &WasmBuffer::GetElementCount) .function("GetView", &WasmBuffer::GetView); emscripten::register_vector("mjStringVec"); emscripten::register_vector>("mjStringVecVec"); emscripten::register_vector("mjIntVec"); emscripten::register_vector("mjIntVecVec"); emscripten::register_vector("mjFloatVec"); emscripten::register_vector("mjFloatVecVec"); emscripten::register_vector("mjDoubleVec"); emscripten::register_vector("mjByteVec"); emscripten::register_vector("MjSolverStatVec"); emscripten::register_vector("MjTimerStatVec"); emscripten::register_vector("MjWarningStatVec"); emscripten::register_vector("MjContactVec"); emscripten::register_vector("MjvLightVec"); emscripten::register_vector("MjvGLCameraVec"); emscripten::register_vector("MjvGeomVec"); // register_type() improves type information (val is mapped to any by default) // NumberOrString is used in functions returning accessors, allowing users to // get an accessor by name (string) or id (number). emscripten::register_type("number|string"); emscripten::register_type("number[]"); emscripten::register_type("string"); emscripten::register_type("string|null"); emscripten::constant("mjMAXCONPAIR", mjMAXCONPAIR); emscripten::constant("mjMAXIMP", mjMAXIMP); emscripten::constant("mjMAXLIGHT", mjMAXLIGHT); emscripten::constant("mjMAXLINE", mjMAXLINE); emscripten::constant("mjMAXLINEPNT", mjMAXLINEPNT); emscripten::constant("mjMAXOVERLAY", mjMAXOVERLAY); emscripten::constant("mjMAXPLANEGRID", mjMAXPLANEGRID); emscripten::constant("mjMAXVAL", mjMAXVAL); emscripten::constant("mjMINIMP", mjMINIMP); emscripten::constant("mjMINMU", mjMINMU); emscripten::constant("mjMINVAL", mjMINVAL); emscripten::constant("mjNBIAS", mjNBIAS); emscripten::constant("mjNDYN", mjNDYN); emscripten::constant("mjNEQDATA", mjNEQDATA); emscripten::constant("mjNGAIN", mjNGAIN); emscripten::constant("mjNGROUP", mjNGROUP); emscripten::constant("mjNIMP", mjNIMP); emscripten::constant("mjNREF", mjNREF); emscripten::constant("mjNSOLVER", mjNSOLVER); emscripten::constant("mjPI", mjPI); emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); emscripten::function("get_mjDISABLESTRING", &get_mjDISABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); emscripten::function("get_mjLABELSTRING", &get_mjLABELSTRING); emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); // Bind these complex constants as properties on the module object. // We use emscripten::constant with emscripten::val::array() to type them // as `any` in TypeScript. At runtime, the EM_ASM block below overrides // these properties with getters that return native JavaScript arrays // (string[] or string[][]) via the get_ functions above, which is more // performant and idiomatic than vector wrappers. emscripten::constant("mjDISABLESTRING", emscripten::val::array()); emscripten::constant("mjENABLESTRING", emscripten::val::array()); emscripten::constant("mjFRAMESTRING", emscripten::val::array()); emscripten::constant("mjLABELSTRING", emscripten::val::array()); emscripten::constant("mjRNDSTRING", emscripten::val::array()); emscripten::constant("mjTIMERSTRING", emscripten::val::array()); emscripten::constant("mjVISSTRING", emscripten::val::array()); EM_ASM({ if (typeof Module !== "undefined") { "mjDISABLESTRING mjENABLESTRING mjFRAMESTRING mjLABELSTRING mjRNDSTRING mjTIMERSTRING mjVISSTRING".split(" ").forEach(function(name) { Object.defineProperty(Module, name, { get: function() { return Module["get_" + name](); }, set: function(v) { }, enumerable: true, configurable: true }); }); } }); } } // namespace mujoco::wasm // NOLINTEND(whitespace/semicolon) // NOLINTEND(whitespace/line_length)