From ec50260e265a5b188e899c02614e036902bb8b38 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Thu, 7 May 2026 06:32:54 -0700 Subject: [PATCH] Add mjs_getOriginSpec to retrieve the original spec of an element. The new function mjs_getOriginSpec returns the mjSpec that was used to define a given mjsElement. Unlike mjs_getSpec, this value remains constant even after the element has been attached to a different model. PiperOrigin-RevId: 911930951 Change-Id: Ia9cd79d9dfabc513d6121eadcffc5d41075224c9 --- doc/APIreference/functions.rst | 10 ++++++++ doc/changelog.rst | 3 +++ doc/includes/references.h | 1 + include/mujoco/mujoco.h | 4 ++++ python/mujoco/introspect/functions.py | 16 +++++++++++++ src/user/user_api.cc | 10 ++++++++ src/user/user_api.h | 4 ++++ test/user/user_api_test.cc | 33 +++++++++++++++++++++++++++ wasm/codegen/generated/bindings.cc | 9 ++++++++ 9 files changed, 90 insertions(+) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index a91344ac..fc8a07c4 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4783,6 +4783,16 @@ Find and get utilities Get spec from body. +.. _mjs_getOriginSpec: + +`mjs_getOriginSpec <#mjs_getOriginSpec>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_getOriginSpec + +get spec that originally defined an element +contrary to mjs_getSpec, this does not change after attachment + .. _mjs_getCompiler: `mjs_getCompiler <#mjs_getCompiler>`__ diff --git a/doc/changelog.rst b/doc/changelog.rst index b99151a1..50f0e2b1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -19,6 +19,9 @@ General energy gain in the presence of contacts and in fluid media. - Added :ref:`mju_sym2dense`, converting a lower-triangular, implicitly symmetric CSR matrix to a dense symmetric matrix. The inertia matrix ``mjData.M`` is an example of such a matrix. +- Added :ref:`mjs_getOriginSpec`, returning the spec that originally defined an element, prior to attachment. This is in + contrast to :ref:`mjs_getSpec` which returns the spec currently owning the element. If the element is not the result + of an attach operation, the functions are identical. .. admonition:: Future breaking API changes :class: warning diff --git a/doc/includes/references.h b/doc/includes/references.h index c96db9b3..6342e21b 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3687,6 +3687,7 @@ mjsTexture* mjs_addTexture(mjSpec* s); mjsMaterial* mjs_addMaterial(mjSpec* s, const mjsDefault* def); int mjs_makeMesh(mjsMesh* mesh, mjtMeshBuiltin builtin, double* params, int nparams); mjSpec* mjs_getSpec(const mjsElement* element); +mjSpec* mjs_getOriginSpec(const mjsElement* element); mjsCompiler* mjs_getCompiler(const mjsElement* element); mjSpec* mjs_findSpec(const mjSpec* spec, const char* name); mjsBody* mjs_findBody(const mjSpec* s, const char* name); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index c9600ba6..682bb87e 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1776,6 +1776,10 @@ MJAPI int mjs_makeMesh(mjsMesh* mesh, mjtMeshBuiltin builtin, double* params, in // Get spec from body. MJAPI mjSpec* mjs_getSpec(const mjsElement* element); +// get spec that originally defined an element +// contrary to mjs_getSpec, this does not change after attachment +MJAPI mjSpec* mjs_getOriginSpec(const mjsElement* element); + // Get compiler associated with element's origin spec. MJAPI mjsCompiler* mjs_getCompiler(const mjsElement* element); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 0bfc82b9..882c323f 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -11126,6 +11126,22 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Get spec from body.', )), + ('mjs_getOriginSpec', + FunctionDecl( + name='mjs_getOriginSpec', + return_type=PointerType( + inner_type=ValueType(name='mjSpec'), + ), + parameters=( + FunctionParameterDecl( + name='element', + type=PointerType( + inner_type=ValueType(name='mjsElement', is_const=True), + ), + ), + ), + doc='get spec that originally defined an element contrary to mjs_getSpec, this does not change after attachment', # pylint: disable=line-too-long + )), ('mjs_getCompiler', FunctionDecl( name='mjs_getCompiler', diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 5b91a772..fe25fae2 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -1308,6 +1308,16 @@ mjSpec* mjs_getSpec(const mjsElement* element) { +// get spec that originally defined an element +// contrary to mjs_getSpec, this does not change after attachment +mjSpec* mjs_getOriginSpec(const mjsElement* element) { + const mjCModel* model = static_cast(element)->model; + const mjsCompiler* compiler = static_cast(element)->compiler; + return model->FindSpec(compiler); +} + + + mjsCompiler* mjs_getCompiler(const mjsElement* element) { return static_cast(element)->compiler; } diff --git a/src/user/user_api.h b/src/user/user_api.h index 878f9045..f1f216a7 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -224,6 +224,10 @@ MJAPI int mjs_makeMesh(mjsMesh* mesh, mjtMeshBuiltin builtin, double* params, in // Get spec from body. MJAPI mjSpec* mjs_getSpec(const mjsElement* element); +// get spec that originally defined an element +// contrary to mjs_getSpec, this does not change after attachment +MJAPI mjSpec* mjs_getOriginSpec(const mjsElement* element); + // Find spec (model asset) by name. MJAPI mjSpec* mjs_findSpec(const mjSpec* spec, const char* name); diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 57e889bb..b26ebf87 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -205,6 +205,39 @@ TEST_F(MujocoTest, AttachAndChildDeletion) { mj_deleteSpec(parent_spec); } +TEST_F(MujocoTest, OriginSpecInvariantToAttachment) { + mjSpec* child_spec = mj_makeSpec(); + mjsBody* child_world = mjs_findBody(child_spec, "world"); + mjsBody* child_body = mjs_addBody(child_world, 0); + mjsJoint* freejoint = mjs_addJoint(child_body, 0); + freejoint->type = mjJNT_FREE; + mjs_setName(freejoint->element, "child_freejoint"); + + mjSpec* parent_spec = mj_makeSpec(); + mjsBody* parent_world = mjs_findBody(parent_spec, "world"); + mjsBody* parent_body = mjs_addBody(parent_world, 0); + mjs_setName(parent_body->element, "parent_body"); + + // Attach child spec to parent_body + mjsElement* attached = + mjs_attach(parent_body->element, child_spec->element, "pre_", ""); + ASSERT_THAT(attached, NotNull()); + + // The freejoint should still be in parent_spec because deletion failed + mjsElement* child_spec_joint = + mjs_findElement(parent_spec, mjOBJ_JOINT, "pre_child_freejoint"); + EXPECT_EQ(mjs_getSpec(child_spec_joint), parent_spec); + EXPECT_EQ(mjs_getOriginSpec(child_spec_joint), child_spec); + + mjsElement* parent_spec_body = + mjs_findElement(parent_spec, mjOBJ_BODY, "parent_body"); + EXPECT_EQ(mjs_getOriginSpec(parent_spec_body), parent_spec); + + + mj_deleteSpec(child_spec); + mj_deleteSpec(parent_spec); +} + int open_mock(mjResource* resource) { static const char parent_xml[] = R"( diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index bbd29e12..324092ef 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -9793,6 +9793,14 @@ std::string mjs_getName_wrapper(MjsElement& element) { return *mjs_getName(element.get()); } +std::optional mjs_getOriginSpec_wrapper(const MjsElement& element) { + mjSpec* result = mjs_getOriginSpec(element.get()); + if (result == nullptr) { + return std::nullopt; + } + return MjSpec(result); +} + std::optional mjs_getParent_wrapper(const MjsElement& element) { mjsBody* result = mjs_getParent(element.get()); if (result == nullptr) { @@ -13343,6 +13351,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { function("mjs_getFrame", &mjs_getFrame_wrapper); function("mjs_getId", &mjs_getId_wrapper); function("mjs_getName", &mjs_getName_wrapper); + function("mjs_getOriginSpec", &mjs_getOriginSpec_wrapper); function("mjs_getParent", &mjs_getParent_wrapper); function("mjs_getSpec", &mjs_getSpec_wrapper); function("mjs_getSpecDefault", &mjs_getSpecDefault_wrapper);