diff --git a/wasm/README.md b/wasm/README.md index ebadb94e..1cf0ac2e 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -307,7 +307,7 @@ The function `mjv_updateScene` populates an `mjvScene` object with information from `mjModel` and `mjData`. ```typescript // Create instances of the necessary structs. -const model = mujoco.MjModel.loadFromXML(xmlContent); +const model = mujoco.MjModel.from_xml_string(xmlContent); const data = new mujoco.MjData(model); const scene = new mujoco.MjvScene(model, 1000); const option = new mujoco.MjvOption(); diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index dbf9646d..4cfa9bd8 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -8361,6 +8362,39 @@ std::unique_ptr mj_loadModel_wrapper(std::string filename, const MjVFS& 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)); @@ -11596,12 +11630,16 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .property("useexisting", &MjLROpt::useexisting, &MjLROpt::set_useexisting, reference()) .property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference()); emscripten::class_("MjModel") + // mj_loadXML is deprecated and will be removed in a future release .class_function("mj_loadXML", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) - .class_function("from_xml_path", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) .class_function("mj_loadXML", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) - .class_function("from_xml_path", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) + // mj_loadModel is deprecated and will be removed in a future release .class_function("mj_loadModel", &mj_loadModel_wrapper) .class_function("from_binary_path", &mj_loadModel_wrapper) + .class_function("from_xml_string", emscripten::select_overload(const std::string&)>(&from_xml_string_wrapper_1)) + .class_function("from_xml_string", emscripten::select_overload(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2)) + .class_function("from_xml_path", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) + .class_function("from_xml_path", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) .constructor() // Binds the functions on MjModel that return accessors. #define X_ACCESSOR(NAME, Name, OBJTYPE, field_name, nfield) \ @@ -13382,6 +13420,8 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { // 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() diff --git a/wasm/codegen/generators/structs.py b/wasm/codegen/generators/structs.py index 79c0b788..24dd291f 100644 --- a/wasm/codegen/generators/structs.py +++ b/wasm/codegen/generators/structs.py @@ -549,29 +549,46 @@ def _build_struct_bindings( MJDATA_ACCESSORS #undef X_ACCESSOR""".lstrip()) elif w == "MjModel": + builder.line( + "// mj_loadXML is deprecated and will be removed in a future release" + ) builder.line( '.class_function("mj_loadXML",' " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" ) - builder.line( - '.class_function("from_xml_path",' - " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" - ) builder.line( '.class_function("mj_loadXML",' " emscripten::select_overload(std::string," " const MjVFS&)>(&mj_loadXML_wrapper_2))" ) - builder.line( - '.class_function("from_xml_path",' - " emscripten::select_overload(std::string," - " const MjVFS&)>(&mj_loadXML_wrapper_2))" - ) f2 = common.wrapped_function_name( introspect_functions.FUNCTIONS["mj_loadModel"] ) + builder.line( + "// mj_loadModel is deprecated and will be removed in a future" + " release" + ) builder.line(f'.class_function("mj_loadModel", &{f2})') builder.line(f'.class_function("from_binary_path", &{f2})') + builder.line( + '.class_function("from_xml_string",' + " emscripten::select_overload(const" + " std::string&)>(&from_xml_string_wrapper_1))" + ) + builder.line( + '.class_function("from_xml_string",' + " emscripten::select_overload(const" + " std::string&, const MjVFS&)>(&from_xml_string_wrapper_2))" + ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" + ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string," + " const MjVFS&)>(&mj_loadXML_wrapper_2))" + ) builder.line(".constructor()") builder.line(""" // Binds the functions on MjModel that return accessors. diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index 27e3ec5a..05694fb6 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -746,6 +747,39 @@ std::unique_ptr mj_loadModel_wrapper(std::string filename, const MjVFS& 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)); @@ -845,6 +879,8 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { // 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() diff --git a/wasm/demo_app/app.ts b/wasm/demo_app/app.ts index 9e5a1a9f..35712c73 100644 --- a/wasm/demo_app/app.ts +++ b/wasm/demo_app/app.ts @@ -205,10 +205,7 @@ class App { } loadModel(xmlContent: string) { - // Write xml as a file so that mujoco can find it - (mujoco as any).FS.writeFile('/working/model.xml', xmlContent); - - this.mjModel = mujoco.MjModel.mj_loadXML('/working/model.xml'); + this.mjModel = mujoco.MjModel.from_xml_string(xmlContent); if (!app.mjModel) { throw new Error('Failed to load model'); } @@ -437,8 +434,6 @@ function setupWindowEvents() { // Tip: put "window.dispatchEvent(new Event('unload'))" in the console to test window.addEventListener('unload', () => { app.dispose(); - - (mujoco as any).FS.unmount('/working'); }); window.addEventListener('keydown', (event) => { @@ -464,10 +459,6 @@ async function main() { try { mujoco = await loadMujoco(); - // Set up emscripten virtual file system - (mujoco as any).FS.mkdir('/working'); - (mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working'); - app = new App(); setupWindowEvents(); diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index 37cf1548..08d9864a 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -2603,4 +2603,56 @@ describe('MuJoCo WASM Bindings', () => { } }); + it('should load a model from an XML string', () => { + let model: MjModel|null = null; + try { + model = mujoco.from_xml_string(TEST_XML); + assertExists(model); + expect(model.nbody).toBe(5); + expect(model.ngeom).toBe(3); + } finally { + model?.delete(); + } + }); + + it('should load a model from an XML string with VFS', () => { + const xml = ` + + + + + + + + `; + + const cube1 = ` + v -1 -1 1 + v 1 -1 1 + v -1 1 1 + v 1 1 1 + v -1 1 -1 + v 1 1 -1 + v -1 -1 -1 + v 1 -1 -1`; + + let model: MjModel|null = null; + let vfs: MjVFS|null = null; + try { + vfs = new mujoco.MjVFS(); + vfs.addBuffer('cube.obj', new TextEncoder().encode(cube1)); + assertExists(vfs); + + model = mujoco.from_xml_string(xml, vfs); + assertExists(model); + expect(model.nmesh).toBe(1); + + const meshId = + mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_MESH.value, 'cube'); + expect(meshId).toBeGreaterThanOrEqual(0); + } finally { + model?.delete(); + vfs?.delete(); + } + }); }); diff --git a/wasm/tests/sandbox/main.ts b/wasm/tests/sandbox/main.ts index 60fc5e95..ed312512 100644 --- a/wasm/tests/sandbox/main.ts +++ b/wasm/tests/sandbox/main.ts @@ -20,9 +20,6 @@ declare function loadMujoco(): Promise; async function main() { const mujoco: MainModule = await loadMujoco(); - (mujoco as any).FS.mkdir('/working'); - (mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working'); - const xmlContent = ` `; - (mujoco as any).FS.writeFile('/working/hello.xml', xmlContent); let model: MjModel|undefined; let data: MjData|undefined; try { console.log('Hello world!: Loading model'); - model = mujoco.MjModel.mj_loadXML('/working/hello.xml'); + model = mujoco.MjModel.from_xml_string(xmlContent); if (!model) { throw new Error('Failed to load model'); } @@ -57,7 +53,6 @@ async function main() { } finally { model?.delete(); data?.delete(); - (mujoco as any).FS.unmount('/working'); } }