Skip to main content

mujoco_rs/wrappers/
mj_model.rs

1//! MjModel related.
2use crate::{
3    view_creator, info_method, info_with_view,
4    array_slice_dyn, getter_setter
5};
6use crate::util::{assert_mujoco_version, checked_c_len, ERROR_BUF_LEN};
7use crate::error::{MjDataError, MjModelError};
8use crate::wrappers::mj_option::MjOption;
9use crate::wrappers::mj_data::MjData;
10use crate::mujoco_c::*;
11
12use super::mj_auxiliary::{MjVfs, MjVisual, MjStatistic};
13use super::mj_editing::encode;
14use super::mj_primitive::*;
15
16use bytemuck::must_cast_slice;
17use log::debug;
18
19use std::fmt::{Formatter, Debug, Result as FmtResult};
20use std::ffi::{c_char, CStr, CString, c_int, c_void};
21use std::sync::{OnceLock, Arc};
22use std::ptr::{self, NonNull};
23use std::path::Path;
24
25pub mod traits;
26
27
28/*******************************************/
29// Types
30/// Constants which are powers of 2. They are used as bitmasks for the field `disableflags` of `mjOption`.
31/// At runtime this field is `m->opt.disableflags`. The number of these constants is given by `mjNDISABLE` which is
32/// also the length of the global string array `mjDISABLESTRING` with text descriptions of these flags.
33pub type MjtDisableBit = mjtDisableBit;
34
35/// Constants which are powers of 2. They are used as bitmasks for the field `enableflags` of `mjOption`.
36/// At runtime this field is `m->opt.enableflags`. The number of these constants is given by `mjNENABLE` which is also
37/// the length of the global string array `mjENABLESTRING` with text descriptions of these flags.
38pub type MjtEnableBit = mjtEnableBit;
39
40/// Primitive joint types. These values are used in `m->jnt_type`. The numbers in the comments indicate how many
41/// positional coordinates each joint type has. Note that ball joints and rotational components of free joints are
42/// represented as unit quaternions - which have 4 positional coordinates but 3 degrees of freedom each.
43pub type MjtJoint = mjtJoint;
44
45/// Geometric types supported by MuJoCo. The first group are "official" geom types that can be used in the model. The
46/// second group are geom types that cannot be used in the model but are used by the visualizer to add decorative
47/// elements. These values are used in `m->geom_type` and `m->site_type`.
48pub type MjtGeom = mjtGeom;
49
50/// Type of camera projection. Used in `m->cam_projection`.
51pub type MjtProjection = mjtProjection;
52
53/// Dynamic modes for cameras and lights, specifying how the camera/light position and orientation are computed. These
54/// values are used in `m->cam_mode` and `m->light_mode`.
55pub type MjtCamLight = mjtCamLight;
56
57/// The type of a light source describing how its position, orientation and other properties will interact with the
58/// objects in the scene. These values are used in `m->light_type`.
59pub type MjtLightType = mjtLightType;
60
61/// Texture types, specifying how the texture will be mapped. These values are used in `m->tex_type`.
62pub type MjtTexture = mjtTexture;
63
64/// Texture roles, specifying how the renderer should interpret the texture.  Note that the MuJoCo built-in renderer only
65/// uses RGB textures.  These values are used to store the texture index in the material's array `m->mat_texid`.
66pub type MjtTextureRole = mjtTextureRole;
67
68/// Type of color space encoding for textures.
69pub type MjtColorSpace = mjtColorSpace;
70
71/// Mode for actuator length-range computation.
72pub type MjtLRMode = mjtLRMode;
73
74/// Cube map face indices used by [`MjsTexture::set_cubefile`](super::mj_editing::MjsTexture::set_cubefile).
75///
76/// Each variant corresponds to one face of a cube-map texture, matching the order
77/// MuJoCo uses internally (right=0, left=1, up=2, down=3, front=4, back=5).
78///
79/// **Note:** this enum is defined in mujoco-rs only; MuJoCo's C API uses raw integer
80/// indices for cube-map faces.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82#[repr(i32)]
83pub enum MjtCubeFace {
84    /// Positive-X face (index 0).
85    Right  = 0,
86    /// Negative-X face (index 1).
87    Left   = 1,
88    /// Positive-Y face (index 2).
89    Up     = 2,
90    /// Negative-Y face (index 3).
91    Down   = 3,
92    /// Positive-Z face (index 4).
93    Front  = 4,
94    /// Negative-Z face (index 5).
95    Back   = 5,
96}
97
98/// Numerical integrator types. These values are used in `m->opt.integrator`.
99pub type MjtIntegrator = mjtIntegrator;
100
101/// Available friction cone types. These values are used in `m->opt.cone`.
102pub type MjtCone = mjtCone;
103
104/// Available Jacobian types. These values are used in `m->opt.jacobian`.
105pub type MjtJacobian = mjtJacobian;
106
107/// Available constraint solver algorithms. These values are used in `m->opt.solver`.
108pub type MjtSolver = mjtSolver;
109
110/// Equality constraint types. These values are used in `m->eq_type`.
111pub type MjtEq = mjtEq;
112
113/// Tendon wrapping object types. These values are used in `m->wrap_type`.
114pub type MjtWrap = mjtWrap;
115
116/// Actuator transmission types. These values are used in `m->actuator_trntype`.
117pub type MjtTrn = mjtTrn;
118
119/// Actuator dynamics types. These values are used in `m->actuator_dyntype`.
120pub type MjtDyn = mjtDyn;
121
122/// Actuator gain types. These values are used in `m->actuator_gaintype`.
123pub type MjtGain = mjtGain;
124
125/// Actuator bias types. These values are used in `m->actuator_biastype`.
126pub type MjtBias = mjtBias;
127
128/// Orientation input charts of so3 actuators. These values are used in `m->actuator_ctrlspec`.
129pub type MjtCtrlChart = mjtCtrlChart;
130
131/// Input signature bits of servo-family (`pid`, `dcmotor`) actuators. These values are OR-ed into
132/// `m->actuator_ctrlspec`.
133pub type MjtCtrlInput = mjtCtrlInput;
134
135/// MuJoCo object types. These are used, for example, in the support functions `mj_name2id` and
136/// `mj_id2name` to convert between object names and integer ids.
137pub type MjtObj = mjtObj;
138
139/// Sensor types. These values are used in `m->sensor_type`.
140pub type MjtSensor = mjtSensor;
141
142/// These are the compute stages for the skipstage parameters of `mj_forwardSkip` and
143/// `mj_inverseSkip`.
144pub type MjtStage = mjtStage;
145
146/// These are the possible sensor data types, used in [`MjModel::sensor_datatype`].
147pub type MjtDataType = mjtDataType;
148
149/// Types of data fields returned by contact sensors.
150pub type MjtConDataField = mjtConDataField;
151
152/// Types of frame alignment of elements with their parent bodies. Used as shortcuts during `mj_kinematics` in the
153/// last argument to `mj_local2Global`.
154pub type MjtSameFrame = mjtSameFrame;
155
156/// Sleep policy associated with a tree. The compiler automatically chooses between `NEVER` and `ALLOWED`, but the user
157/// can override this choice. Only the user can set the `INIT` policy (initialized as asleep).
158pub type MjtSleepPolicy = mjtSleepPolicy;
159
160/// Types of flex self-collisions midphase.
161pub type MjtFlexSelf = mjtFlexSelf;
162
163/// Formulas used to combine SDFs when calling mjc_distance and mjc_gradient.
164pub type MjtSDFType = mjtSDFType;
165
166/// Data fields returned by rangefinder sensors.
167pub type MjtRayDataField = mjtRayDataField;
168
169/// Camera output type bitflags.
170pub type MjtCamOutBit = mjtCamOutBit;
171
172// SAFETY: All MuJoCo C enums below are fieldless with an explicit integer repr, and each
173// has a variant with discriminant 0, so the all-zeros bit pattern is a valid value.
174// This lets `info_with_view!`'s `zero()` method use the safe `Zeroable::zeroed()`
175// instead of `unsafe { std::mem::zeroed() }`, providing a compile-time guarantee
176// that only safe-to-zero types are used in views.
177unsafe impl bytemuck::Zeroable for mjtTrn {}
178unsafe impl bytemuck::Zeroable for mjtDyn {}
179unsafe impl bytemuck::Zeroable for mjtGain {}
180unsafe impl bytemuck::Zeroable for mjtBias {}
181unsafe impl bytemuck::Zeroable for mjtObj {}
182unsafe impl bytemuck::Zeroable for mjtSameFrame {}
183unsafe impl bytemuck::Zeroable for mjtCamLight {}
184unsafe impl bytemuck::Zeroable for mjtProjection {}
185unsafe impl bytemuck::Zeroable for mjtEq {}
186unsafe impl bytemuck::Zeroable for mjtGeom {}
187unsafe impl bytemuck::Zeroable for mjtJoint {}
188unsafe impl bytemuck::Zeroable for mjtLightType {}
189unsafe impl bytemuck::Zeroable for mjtSensor {}
190unsafe impl bytemuck::Zeroable for mjtDataType {}
191unsafe impl bytemuck::Zeroable for mjtStage {}
192unsafe impl bytemuck::Zeroable for mjtTexture {}
193unsafe impl bytemuck::Zeroable for mjtColorSpace {}
194unsafe impl bytemuck::Zeroable for mjtAlignFree {}
195unsafe impl bytemuck::Zeroable for mjtBuiltin {}
196unsafe impl bytemuck::Zeroable for mjtConflict {}
197unsafe impl bytemuck::Zeroable for mjtConstraint {}
198unsafe impl bytemuck::Zeroable for mjtConstraintState {}
199unsafe impl bytemuck::Zeroable for mjtDepthMap {}
200unsafe impl bytemuck::Zeroable for mjtFlexSelf {}
201unsafe impl bytemuck::Zeroable for mjtInertiaFromGeom {}
202unsafe impl bytemuck::Zeroable for mjtLimited {}
203unsafe impl bytemuck::Zeroable for mjtLogLevel {}
204unsafe impl bytemuck::Zeroable for mjtLogTopic {}
205unsafe impl bytemuck::Zeroable for mjtMark {}
206unsafe impl bytemuck::Zeroable for mjtSleepPolicy {}
207unsafe impl bytemuck::Zeroable for mjtSleepState {}
208unsafe impl bytemuck::Zeroable for mjtStereo {}
209unsafe impl bytemuck::Zeroable for mjtWrap {}
210
211// SAFETY: every enum below is fieldless with a `#[repr(u32)]`, so it holds no padding and every
212// byte of it is initialised. `MjSplitTables` reads these as raw bytes and never casts bytes back
213// into them.
214unsafe impl bytemuck::NoUninit for mjtJoint {}
215unsafe impl bytemuck::NoUninit for mjtGeom {}
216unsafe impl bytemuck::NoUninit for mjtEq {}
217unsafe impl bytemuck::NoUninit for mjtObj {}
218unsafe impl bytemuck::NoUninit for mjtWrap {}
219unsafe impl bytemuck::NoUninit for mjtTrn {}
220unsafe impl bytemuck::NoUninit for mjtDyn {}
221unsafe impl bytemuck::NoUninit for mjtGain {}
222unsafe impl bytemuck::NoUninit for mjtBias {}
223unsafe impl bytemuck::NoUninit for mjtSensor {}
224unsafe impl bytemuck::NoUninit for mjtDataType {}
225unsafe impl bytemuck::NoUninit for mjtStage {}
226unsafe impl bytemuck::NoUninit for mjtTexture {}
227
228/*******************************************/
229
230/// Number of mesh, texture and heightfield count tables in [`MjSplitTables`].
231const ASSET_SPLIT_TABLES: usize = 11;
232
233/// Number of per-element and plugin count tables in [`MjSplitTables`].
234const ELEMENT_SPLIT_TABLES: usize = 32;
235
236/// Snapshot of an [`MjModel`]: the sizes that no per-element table determines, and the tables that
237/// fix how each packed array divides between the elements.
238///
239/// An entry belongs here only when no other entry already determines it. `signature` is the
240/// exception: it takes no part in the comparison at all, and only reports which model an `Info`
241/// came from. The fields run cheapest first, because `PartialEq` tests them in that order and
242/// stops at the first difference.
243// `Eq` also selects the pointer shortcut in `PartialEq for Arc`, which is what makes a view gate
244// against the model's own snapshot a pointer comparison. Losing `Eq` would silently cost that.
245#[derive(Debug, Clone, Eq)]
246#[expect(non_snake_case, reason = "the fields keep the MuJoCo size symbol names")]
247pub(crate) struct MjModelLayout {
248    signature: u64,
249
250    /* Element counts that bound an `Info` id, an `mjData` buffer row, and a swapped model. A
251       count that the byte length of a table inside `split` already pins gets no field here. */
252    nexclude: MjtSize,       nmat: MjtSize,           npair: MjtSize,         nskin: MjtSize,
253    nkey: MjtSize,
254
255    /* Row lengths of MJDATA_POINTERS and MJDATA_ARENA_POINTERS that no table inside `split`
256       already determines. A total that is the plain sum of a count table gets no field here:
257       equal tables force an equal sum, so the total follows. */
258    nmocap: MjtSize,         nuserdata: MjtSize,      nhistory: MjtSize,      nbvh: MjtSize,
259    nbvhdynamic: MjtSize,    nflexedge: MjtSize,      nflexstiffness: MjtSize, nJmom: MjtSize,
260    nJfe: MjtSize,           nJfv: MjtSize,           nC: MjtSize,            nD: MjtSize,
261    ntree: MjtSize,
262
263    /* Byte size of the mjData arena. mjData inherits it from the model, and mj_copyDataVisual
264       raises mjERROR when the destination and the source disagree. */
265    narena: MjtSize,
266
267    /* Total of the one packed mjModel array that no count table splits: qhull sizes each convex
268       hull, and `mesh_graphadr` holds addresses only. Every other total is the sum of the count
269       table beside it. */
270    nmeshgraph: MjtSize,
271
272    /* Strides of the per-element user arrays. */
273    nuser_body: MjtSize,     nuser_jnt: MjtSize,      nuser_geom: MjtSize,    nuser_site: MjtSize,
274    nuser_cam: MjtSize,      nuser_tendon: MjtSize,   nuser_actuator: MjtSize, nuser_sensor: MjtSize,
275
276    /* Per-element count tables, byte for byte. A table pins the length of the packed array beside
277       it, and it also pins how that array divides between the elements. This field sits last
278       because the derived `PartialEq` tests the fields in order: every scalar above costs the same
279       on any model, while a table comparison grows with the element count. */
280    split: MjSplitTables,
281}
282
283impl PartialEq for MjModelLayout {
284    /// `signature` takes no part: only the compiler writes it, so `mj_loadModel` leaves it zero
285    /// and a test would refuse a model against its own saved copy.
286    fn eq(&self, other: &Self) -> bool {
287            self.nexclude == other.nexclude && self.nmat == other.nmat && self.npair == other.npair && self.nskin == other.nskin &&
288            self.nkey == other.nkey && self.nmocap == other.nmocap && self.nuserdata == other.nuserdata && self.nhistory == other.nhistory &&
289            self.nbvh == other.nbvh && self.nbvhdynamic == other.nbvhdynamic && self.nflexedge == other.nflexedge && self.nflexstiffness == other.nflexstiffness &&
290            self.nJmom == other.nJmom && self.nJfe == other.nJfe && self.nJfv == other.nJfv && self.nC == other.nC &&
291            self.nD == other.nD && self.ntree == other.ntree && self.narena == other.narena && self.nmeshgraph == other.nmeshgraph &&
292            self.nuser_body == other.nuser_body && self.nuser_jnt == other.nuser_jnt && self.nuser_geom == other.nuser_geom && self.nuser_site == other.nuser_site &&
293            self.nuser_cam == other.nuser_cam && self.nuser_tendon == other.nuser_tendon && self.nuser_actuator == other.nuser_actuator && self.nuser_sensor == other.nuser_sensor &&
294            self.split == other.split
295    }
296}
297
298impl MjModelLayout {
299    /// Returns the compilation signature of the model this layout came from.
300    pub(crate) fn signature(&self) -> u64 {
301        self.signature
302    }
303
304    /// Returns the mesh, texture and heightfield count tables.
305    fn asset_split(&self) -> &[Box<[u8]>; ASSET_SPLIT_TABLES] {
306        &self.split.assets
307    }
308}
309
310impl From<&MjModel> for MjModelLayout {
311    fn from(model: &MjModel) -> Self {
312        let m = model.ffi();
313        Self {
314            signature: m.signature,
315            nexclude: m.nexclude, nmat: m.nmat, npair: m.npair, nskin: m.nskin, nkey: m.nkey,
316            nmocap: m.nmocap, nuserdata: m.nuserdata, nhistory: m.nhistory, nbvh: m.nbvh,
317            nbvhdynamic: m.nbvhdynamic, nflexedge: m.nflexedge, nflexstiffness: m.nflexstiffness,
318            nJmom: m.nJmom, nJfe: m.nJfe, nJfv: m.nJfv, nC: m.nC, nD: m.nD, ntree: m.ntree,
319            narena: m.narena,
320            nmeshgraph: m.nmeshgraph,
321            nuser_body: m.nuser_body, nuser_jnt: m.nuser_jnt, nuser_geom: m.nuser_geom, nuser_site: m.nuser_site,
322            nuser_cam: m.nuser_cam, nuser_tendon: m.nuser_tendon, nuser_actuator: m.nuser_actuator,
323            nuser_sensor: m.nuser_sensor,
324            split: model.split_tables(),
325        }
326    }
327}
328
329
330/// Count tables of an [`MjModel`], one owned table per entry and as raw bytes, so that a
331/// comparison tests the length of each table on its own and then its content.
332///
333/// One flat buffer would test the sum of the lengths instead, which leaves the element count of a
334/// single table free.
335#[derive(Clone, PartialEq, Eq)]
336pub(crate) struct MjSplitTables {
337    assets: [Box<[u8]>; ASSET_SPLIT_TABLES],
338    elements: [Box<[u8]>; ELEMENT_SPLIT_TABLES],
339}
340
341impl Debug for MjSplitTables {
342    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
343        let assets = self.assets.each_ref().map(|table| table.len());
344        let elements = self.elements.each_ref().map(|table| table.len());
345        f.debug_struct("MjSplitTables")
346            .field("assets", &assets)
347            .field("elements", &elements)
348            .finish_non_exhaustive()
349    }
350}
351
352
353/// A Rust-safe wrapper around mjModel.
354/// Automatically clean after itself on destruction.
355#[derive(Debug)]
356pub struct MjModel {
357    ptr: NonNull<mjModel>,
358    /// Memory layout for compatibility checks.
359    layout: OnceLock<Arc<MjModelLayout>>,
360}
361
362// SAFETY: MjModel owns its mjModel heap allocation exclusively. The data is not shared
363// outside of Rust, except in the C++ code which is synchronized via wrapper APIs.
364unsafe impl Send for MjModel {}
365unsafe impl Sync for MjModel {}
366
367
368impl MjModel {
369    /// Loads the model from an XML file. To load from a virtual file system, use [`MjModel::from_xml_vfs`].
370    /// Wraps [`mj_loadXML`].
371    /// # Returns
372    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
373    /// # Errors
374    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
375    /// - [`MjModelError::LoadFailed`] if MuJoCo fails to load the model.
376    /// # Panics
377    /// - when the `path` contains '\0'.
378    /// - when the linked MuJoCo version does not match the expected from MuJoCo-rs.
379    pub fn from_xml<T: AsRef<Path>>(path: T) -> Result<Self, MjModelError> {
380        Self::from_xml_file(path, None)
381    }
382
383    /// Loads the model from an XML file, located in a virtual file system (`vfs`)
384    /// Wraps [`mj_loadXML`].
385    /// # Returns
386    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
387    /// # Errors
388    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
389    /// - [`MjModelError::LoadFailed`] if MuJoCo fails to load the model.
390    /// # Panics
391    /// - when the `path` contains '\0'.
392    /// - when the linked MuJoCo version does not match the expected from MuJoCo-rs.
393    pub fn from_xml_vfs<T: AsRef<Path>>(path: T, vfs: &MjVfs) -> Result<Self, MjModelError> {
394        Self::from_xml_file(path, Some(vfs))
395    }
396
397    fn from_xml_file<T: AsRef<Path>>(path: T, vfs: Option<&MjVfs>) -> Result<Self, MjModelError> {
398        assert_mujoco_version();
399
400        let mut error_buffer = [0; ERROR_BUF_LEN];
401        let path_str = path.as_ref().to_str()
402            .ok_or(MjModelError::InvalidUtf8Path)?;
403        let path = CString::new(path_str).unwrap();
404        let raw_ptr = unsafe { mj_loadXML(
405            path.as_ptr(), vfs.map_or(ptr::null(), |v| v.ffi()),
406            error_buffer.as_mut_ptr(), error_buffer.len() as c_int
407        ) };
408
409        Self::check_raw_model(raw_ptr, &error_buffer)
410            .inspect(|_| debug!("loaded the model from \"{path_str}\""))
411    }
412
413    /// Loads the model from an XML string.
414    /// Wraps [`mj_loadXML`].
415    /// # Returns
416    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
417    /// # Errors
418    /// - [`MjModelError::VfsError`] if the internal VFS operation fails.
419    /// - [`MjModelError::LoadFailed`] if MuJoCo fails to load the model.
420    /// # Panics
421    /// Panics if the linked MuJoCo version does not match the version expected by mujoco-rs.
422    pub fn from_xml_string(data: &str) -> Result<Self, MjModelError> {
423        assert_mujoco_version();
424
425        let mut vfs = MjVfs::new();
426        let filename = "model.xml";
427
428        // Add the file into a virtual file system
429        vfs.add_from_buffer(filename, data.as_bytes())?;
430
431        let mut error_buffer = [0; ERROR_BUF_LEN];
432        let filename_c = CString::new(filename).unwrap();
433        let raw_ptr = unsafe { mj_loadXML(
434            filename_c.as_ptr(), vfs.ffi(),
435            error_buffer.as_mut_ptr(), error_buffer.len() as c_int
436        ) };
437
438        Self::check_raw_model(raw_ptr, &error_buffer)
439    }
440
441    /// Loads the model from MJB raw data.
442    /// Wraps [`mj_loadModelBuffer`].
443    /// # Returns
444    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
445    /// # Errors
446    /// Returns [`MjModelError::LoadFailed`] if MuJoCo fails to parse the MJB buffer.
447    /// # Panics
448    /// When the linked MuJoCo version does not match the expected from MuJoCo-rs, or when `data`
449    /// is longer than [`i32::MAX`] bytes.
450    pub fn from_buffer(data: &[u8]) -> Result<Self, MjModelError> {
451        assert_mujoco_version();
452        unsafe {
453            Self::from_raw(mj_loadModelBuffer(data.as_ptr() as *const c_void, checked_c_len(data.len())))
454        }
455    }
456
457    /// Loads the model from a binary (MJB) file. To load from a virtual file system, use
458    /// [`MjModel::from_mjb_vfs`].
459    /// Wraps [`mj_loadModel`].
460    /// # Returns
461    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
462    /// # Errors
463    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
464    /// - [`MjModelError::LoadFailed`] if MuJoCo fails to load the model.
465    /// # Panics
466    /// - when the `path` contains '\0'.
467    /// - when the linked MuJoCo version does not match the expected from MuJoCo-rs.
468    pub fn from_mjb<T: AsRef<Path>>(path: T) -> Result<Self, MjModelError> {
469        Self::from_mjb_file(path, None)
470    }
471
472    /// Loads the model from a binary (MJB) file, located in a virtual file system (`vfs`).
473    /// Wraps [`mj_loadModel`].
474    /// # Returns
475    /// On success, returns [`Ok`] variant containing the loaded [`MjModel`].
476    /// # Errors
477    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
478    /// - [`MjModelError::LoadFailed`] if MuJoCo fails to load the model.
479    /// # Panics
480    /// - when the `path` contains '\0'.
481    /// - when the linked MuJoCo version does not match the expected from MuJoCo-rs.
482    pub fn from_mjb_vfs<T: AsRef<Path>>(path: T, vfs: &MjVfs) -> Result<Self, MjModelError> {
483        Self::from_mjb_file(path, Some(vfs))
484    }
485
486    fn from_mjb_file<T: AsRef<Path>>(path: T, vfs: Option<&MjVfs>) -> Result<Self, MjModelError> {
487        assert_mujoco_version();
488
489        let path_str = path.as_ref().to_str()
490            .ok_or(MjModelError::InvalidUtf8Path)?;
491        let path = CString::new(path_str).unwrap();
492        let raw_ptr = unsafe { mj_loadModel(
493            path.as_ptr(), vfs.map_or(ptr::null(), |v| v.ffi())
494        ) };
495
496        Self::from_raw(raw_ptr)
497            .inspect(|_| debug!("loaded the model from \"{path_str}\""))
498    }
499
500    /// Creates a [`MjModel`] from a raw pointer.
501    pub(crate) fn from_raw(ptr: *mut mjModel) -> Result<Self, MjModelError> {
502        Self::check_raw_model(ptr, &[0])
503    }
504
505    /// Saves the last loaded XML to `filename`.
506    /// Wraps [`mj_saveLastXML`].
507    /// # Returns
508    /// `Ok(())` on success.
509    /// # Errors
510    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
511    /// - [`MjModelError::SaveFailed`] with MuJoCo's error message if saving fails.
512    /// # Panics
513    /// When the path contains '\0' characters, a panic occurs.
514    pub fn save_last_xml<T: AsRef<Path>>(&self, filename: T) -> Result<(), MjModelError> {
515        let path_str = filename.as_ref().to_str()
516            .ok_or(MjModelError::InvalidUtf8Path)?;
517        let mut error = [0; ERROR_BUF_LEN];
518        let cstring = CString::new(path_str).unwrap();
519        let result = unsafe { mj_saveLastXML(
520            cstring.as_ptr(), self.ffi(),
521            error.as_mut_ptr(), error.len() as i32
522        ) };
523        match result {
524            1 => {
525                debug!("saved the last loaded XML to \"{path_str}\"");
526                Ok(())
527            },
528            _ => {
529                // SAFETY: error is zero-initialised and MuJoCo NUL-terminates the message it
530                // writes into it; the resulting CStr borrows the stack buffer and is consumed
531                // before the buffer goes out of scope.
532                let cstr_error = unsafe { CStr::from_ptr(error.as_ptr()) }
533                    .to_string_lossy()
534                    .into_owned();
535                Err(MjModelError::SaveFailed(cstr_error))
536            },
537        }
538    }
539
540    /// Encode [`MjModel`] to `filepath` using an encoder registered for encoding `content_type`.
541    /// When the `filepath`'s extension is '.xml', '.mjb' or `.txt`, the MuJoCo's internal
542    /// XML, MJB and TXT encoders will be used, respectively. Similarly, the MuJoCo's internal
543    /// encoders will be used when `content_type` is 'text/xml' or 'text/plain'.
544    /// The XML encoder writes the spec of the last [`MjModel::from_xml`] (or `_vfs`, `_string`)
545    /// load and fails when there is none.
546    /// 
547    /// This is a wrapper for [`mj_encode`].
548    /// 
549    /// # Errors
550    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
551    /// - [`MjModelError::SaveFailed`] with MuJoCo's error message if encoding fails.
552    /// 
553    /// # Panics
554    /// When `filepath` is empty, or when `filepath` or `content_type` contain interior `\0`
555    /// characters. MuJoCo aborts the process when the MJB or TXT encoder cannot write `filepath`.
556    /// 
557    /// # Note
558    /// The MJB and TXT encoders report the size of whatever sits at `filepath` instead of the
559    /// bytes they wrote, thus a refused write on an existing file returns `Ok(())` and leaves the
560    /// stale file in place.
561    pub fn encode(&self, filepath: impl AsRef<Path>, content_type: &str) -> Result<(), MjModelError> {
562        self.encode_impl(filepath, content_type, None)
563    }
564
565    /// Same as [`MjModel::encode`] except data (assets) are taken from `vfs`.
566    /// 
567    /// # Errors
568    /// The same as [`MjModel::encode`].
569    /// 
570    /// # Panics
571    /// The same as [`MjModel::encode`].
572    pub fn encode_with_vfs(&self, filepath: impl AsRef<Path>, content_type: &str, vfs: &MjVfs) -> Result<(), MjModelError> {
573        self.encode_impl(filepath, content_type, Some(vfs))
574    }
575
576    /// Implementation of the wrapper for [`mj_encode`].
577    fn encode_impl(
578        &self,
579        filepath: impl AsRef<Path>, content_type: &str,
580        maybe_vfs: Option<&MjVfs>
581    ) -> Result<(), MjModelError> {
582        let filepath = filepath.as_ref().to_str().ok_or(MjModelError::InvalidUtf8Path)?;
583        encode(None, Some(self), filepath, content_type, maybe_vfs).map_err(MjModelError::SaveFailed)
584    }
585
586    /// Creates a new [`MjData`] instance linked to this model.
587    ///
588    /// # Panics
589    /// Panics if MuJoCo fails to allocate the data structure.
590    /// Use [`MjModel::try_make_data`] for a fallible alternative.
591    pub fn make_data(&self) -> MjData<&Self> {
592        MjData::new(self)
593    }
594
595    /// Fallible version of [`MjModel::make_data`].
596    ///
597    /// # Errors
598    /// Returns [`MjDataError::AllocationFailed`] if MuJoCo fails to allocate
599    /// the data structure.
600    pub fn try_make_data(&self) -> Result<MjData<&Self>, MjDataError> {
601        MjData::try_new(self)
602    }
603
604    /// Wraps a raw model pointer returned by MuJoCo load functions.
605    /// Returns an error with the C error-buffer message if the pointer is null.
606    fn check_raw_model(ptr_model: *mut mjModel, error_buffer: &[c_char]) -> Result<Self, MjModelError> {
607        match NonNull::new(ptr_model) {
608            Some(nn) => Ok(Self { ptr: nn, layout: OnceLock::new() }),
609            None => {
610                // SAFETY: error_buffer is zero-initialised and MuJoCo always
611                // NUL-terminates the message it writes into it.
612                let message = unsafe { CStr::from_ptr(error_buffer.as_ptr()) }
613                    .to_string_lossy()
614                    .into_owned();
615                Err(MjModelError::LoadFailed(message))
616            }
617        }
618    }
619
620    info_method! { Model, actuator,
621        [trntype: 1, dyntype: 1, gaintype: 1, biastype: 1, ctrladr: 1, ctrlnum: 1, ctrlspec: 1,
622        outadr: 1, outnum: 1, trnid: 2, actadr: 1, actnum: 1,
623        group: 1, history: 2, historyadr: 1, delay: 1, forcelimited: 1, actlimited: 1, dynprm: mjNDYN as usize, gainprm: mjNGAIN as usize, biasprm: mjNBIAS as usize,
624        actearly: 1, forcerange: 2, actrange: 2, damping: 1,
625        dampingpoly: mjNPOLY as usize, armature: 1, cranklength: 1, plugin: 1],
626        [user: nuser_actuator],
627        [ctrllimited: nu, ctrlrange: nu * 2, gear: nout * 6, acc0: nout, length0: nout, lengthrange: nout * 2]
628    }
629
630    info_method! { Model, body,
631        [parentid: 1, rootid: 1, weldid: 1, mocapid: 1, jntnum: 1, jntadr: 1,
632        dofnum: 1, dofadr: 1, treeid: 1, geomnum: 1, geomadr: 1, simple: 1,
633        sameframe: 1, pos: 3, quat: 4, ipos: 3, iquat: 4, mass: 1, subtreemass: 1,
634        inertia: 3, invweight0: 2, gravcomp: 1, margin: 1, plugin: 1,
635        contype: 1, conaffinity: 1, bvhadr: 1, bvhnum: 1],
636        [user: nuser_body],
637        []
638    }
639
640    info_method! { Model, camera,
641        [mode: 1, bodyid: 1, targetbodyid: 1,
642        pos: 3, quat: 4, poscom0: 3,
643        pos0: 3, mat0: 9, projection: 1, fovy: 1,
644        ipd: 1, resolution: 2, output: 1, sensorsize: 2, intrinsic: 4],
645        [user: nuser_cam],
646        []
647    }
648    
649    info_method! { Model, joint,
650        [r#type: 1, qposadr: 1, dofadr: 1, group: 1,
651        limited: 1, actfrclimited: 1, actgravcomp: 1, solref: mjNREF as usize, solimp: mjNIMP as usize,
652        pos: 3, axis: 3, stiffness: 1, stiffnesspoly: mjNPOLY as usize,
653        range: 2, actfrcrange: 2, margin: 1, bodyid: 1, actuatorid: 1],
654        [user: nuser_jnt],
655        [qpos0: nq, qpos_spring: nq, jntid: nv,
656        dof_bodyid: nv, parentid: nv, dof_treeid: nv, Madr: nv, simplenum: nv, frictionloss: nv,
657        armature: nv, damping: nv, dampingpoly: nv * mjNPOLY as usize, invweight0: nv, M0: nv]
658    }
659
660
661    info_method! { Model, equality,
662        [r#type: 1, obj1id: 1,
663        obj2id: 1, active0: 1,
664        solref: mjNREF as usize, solimp: mjNIMP as usize,
665        data: mjNEQDATA as usize, objtype: 1],
666        [],
667        []
668    }
669
670    info_method! { Model, exclude,
671        [signature: 1],
672        [],
673        []
674    }
675
676    info_method! { Model, geom,
677        [r#type: 1, contype: 1, conaffinity: 1, condim: 1, bodyid: 1, dataid: 1, matid: 1,
678        group: 1, priority: 1, plugin: 1, sameframe: 1, solmix: 1, solref: mjNREF as usize, solimp: mjNIMP as usize, size: 3,
679        aabb: 6, rbound: 1, pos: 3, quat: 4, friction: 3, margin: 1, gap: 1, surfacevel: 6, adhesion: 1, fluid: mjNFLUID as usize, rgba: 4],
680        [user: nuser_geom],
681        []
682    }
683
684    info_method! { Model, hfield,
685        [size: 4,
686        nrow: 1,
687        ncol: 1,
688        adr: 1,
689        pathadr: 1],
690        [],
691        [data: nhfielddata]
692    }
693
694    info_method! { Model, light,
695        [mode: 1, bodyid: 1, targetbodyid: 1, r#type: 1, texid: 1, castshadow: 1,
696        bulbradius: 1, intensity: 1, range: 1,
697        active: 1, pos: 3, dir: 3, poscom0: 3, pos0: 3,
698        dir0: 3, attenuation: 3, cutoff: 1, softness: 1, exponent: 1, ambient: 3,
699        diffuse: 3, specular: 3],
700        [],
701        []
702    }
703
704    info_method! { Model, material,
705        [texid: MjtTextureRole::mjNTEXROLE as usize, texuniform: 1,
706        texrepeat: 2, emission: 1,
707        specular: 1, shininess: 1,
708        reflectance: 1, rgba: 4, metallic: 1, roughness: 1],
709        [],
710        []
711    }
712
713    info_method! { Model, mesh,
714        [vertadr: 1, vertnum: 1,
715        texcoordadr: 1, faceadr: 1,
716        facenum: 1, graphadr: 1, extrema: 27,
717        normaladr: 1, normalnum: 1, texcoordnum: 1,
718        bvhadr: 1, bvhnum: 1, octadr: 1, octnum: 1,
719        pathadr: 1, polynum: 1, polyadr: 1,
720        scale: 3, pos: 3, quat: 4],
721        [],
722        []
723    }
724
725    info_method! { Model, numeric,
726        [adr: 1,
727        size: 1],
728        [],
729        [data: nnumericdata]
730    }
731
732    info_method! { Model, pair,
733        [dim: 1, geom1: 1, geom2: 1,
734        signature: 1, solref: mjNREF as usize, solimp: mjNIMP as usize,
735        margin: 1, gap: 1, adhesion: 1, friction: 5, solreffriction: mjNREF as usize],
736        [],
737        []
738    }
739
740    info_method! { Model, sensor,
741        [r#type: 1, datatype: 1, needstage: 1,
742        objtype: 1, objid: 1, reftype: 1,
743        refid: 1, intprm: mjNSENS as usize, dim: 1, adr: 1,
744        cutoff: 1, noise: 1, history: 2, historyadr: 1, delay: 1, interval: 2, plugin: 1],
745        [user: nuser_sensor],
746        []
747    }
748
749    info_method! { Model, site,
750        [r#type: 1, bodyid: 1, matid: 1,
751        group: 1, sameframe: 1, size: 3,
752        pos: 3, quat: 4, rgba: 4],
753        [user: nuser_site],
754        []
755    }
756
757    info_method! { Model, skin,
758        [matid: 1, group: 1, rgba: 4, inflate: 1,
759        vertadr: 1, vertnum: 1, texcoordadr: 1,
760        faceadr: 1, facenum: 1, boneadr: 1,
761        bonenum: 1, pathadr: 1],
762        [],
763        []
764    }
765
766    info_method! { Model, tendon,
767        [adr: 1, num: 1, matid: 1, actuatorid: 1, group: 1, treenum: 1, treeid: 2,
768        limited: 1, actfrclimited: 1, width: 1,
769        solref_lim: mjNREF as usize, solimp_lim: mjNIMP as usize, solref_fri: mjNREF as usize, solimp_fri: mjNIMP as usize, range: 2, actfrcrange: 2, margin: 1,
770        stiffness: 1, stiffnesspoly: mjNPOLY as usize, damping: 1, dampingpoly: mjNPOLY as usize, armature: 1,
771        frictionloss: 1, lengthspring: 2, length0: 1, invweight0: 1, J_rownnz: 1, J_rowadr: 1, rgba: 4],
772        [user: nuser_tendon],
773        [J_colind: nJten]
774    }
775
776    info_method! { Model, texture,
777        [r#type: 1, colorspace: 1, height: 1,
778        width: 1, nchannel: 1,
779        adr: 1, pathadr: 1],
780        [],
781        [data: ntexdata]
782    }
783
784    info_method! { Model, tuple,
785        [adr: 1,
786        size: 1],
787        [],
788        [objtype: ntupledata,
789        objid: ntupledata,
790        objprm: ntupledata]
791    }
792
793    info_method! { Model, key,
794        [time: 1],
795        [qpos: nq, qvel: nv,
796        act: na, mpos: nmocap*3,
797        mquat: nmocap*4, ctrl: nu],
798        []
799    }
800
801    /// Translates `name` to the correct id. Wrapper around `mj_name2id`.
802    /// Returns `None` if the name is not found.
803    /// Wraps [`mj_name2id`].
804    /// # Panics
805    /// When the `name` contains '\0' characters, a panic occurs.
806    pub fn name_to_id(&self, type_: MjtObj, name: &str) -> Option<usize> {
807        let c_string = CString::new(name).unwrap();
808        let id = unsafe {
809            mj_name2id(self.ffi(), type_ as i32, c_string.as_ptr())
810        };
811        if id == -1 { None } else { Some(id as usize) }
812    }
813
814    /* Partially auto-generated */
815
816    /// Fallible version of [`Clone::clone`].
817    ///
818    /// Wraps [`mj_copyModel`].
819    /// # Note
820    /// MuJoCo ends the process when the allocation fails, so this never returns `Err`.
821    ///
822    /// # Errors
823    /// Returns [`MjModelError::AllocationFailed`] if MuJoCo returns a null model.
824    #[deprecated(
825        since = "6.0.0",
826        note = "always returns Ok; use `clone`"
827    )]
828    pub fn try_clone(&self) -> Result<MjModel, MjModelError> {
829        let ptr = unsafe { mj_copyModel(ptr::null_mut(), self.ffi()) };
830        NonNull::new(ptr)
831            // The copy holds the same sizes and the same tables, so it reuses the snapshot.
832            .map(|ptr| MjModel { ptr, layout: self.layout.clone() })
833            .ok_or(MjModelError::AllocationFailed)
834    }
835
836    /// Save model to binary MJB file.
837    ///
838    /// Wraps [`mj_saveModel`].
839    /// # Returns
840    /// `Ok(())` if the path is valid UTF-8 and contains no interior `\0` characters.
841    /// **Note:** the underlying C function `mj_saveModel` returns `void`, so file I/O
842    /// errors (e.g. permission denied, disk full) are not detectable; `Ok(())` does
843    /// **not** guarantee the file was written.
844    /// # Errors
845    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
846    /// # Panics
847    /// When the filename contains '\0' characters, a panic occurs.
848    pub fn save_to_file<T: AsRef<Path>>(&self, filename: T) -> Result<(), MjModelError> {
849        let path_str = filename.as_ref().to_str()
850            .ok_or(MjModelError::InvalidUtf8Path)?;
851        let c_filename = CString::new(path_str).unwrap();
852        unsafe { mj_saveModel(
853            self.ffi(), c_filename.as_ptr(),
854            ptr::null_mut(), 0
855        ) };
856        debug!("saved the model to \"{path_str}\"");
857        Ok(())
858    }
859
860    /// Save model to memory buffer.
861    /// Wraps [`mj_saveModel`].
862    /// # Returns
863    /// `Ok(())` on success.
864    /// # Errors
865    /// - [`MjModelError::BufferTooSmall`] if the buffer is smaller than [`size()`](Self::size).
866    pub fn save_to_buffer(&self, buffer: &mut [u8]) -> Result<(), MjModelError> {
867        let needed = self.size();
868        if buffer.len() < needed {
869            return Err(MjModelError::BufferTooSmall {
870                needed,
871                available: buffer.len(),
872            });
873        }
874        unsafe { mj_saveModel(
875            self.ffi(), ptr::null(),
876            buffer.as_mut_ptr() as *mut c_void, buffer.len() as i32
877        ) };
878        Ok(())
879    }
880
881    /// Return size of buffer needed to hold model.
882    /// Wraps [`mj_sizeModel`].
883    pub fn size(&self) -> usize {
884        unsafe { mj_sizeModel(self.ffi()) as usize }
885    }
886
887    /// Print mjModel to text file, specifying format.
888    /// float_format must be a valid printf-style format string for a single float value.
889    ///
890    /// Wraps [`mj_printFormattedModel`].
891    /// # Returns
892    /// `Ok(())` if the path and format string are valid UTF-8 and contain no interior `\0`
893    /// characters. **Note:** the underlying C function `mj_printFormattedModel` returns `void`,
894    /// so file I/O errors are not detectable; `Ok(())` does **not** guarantee the file was written.
895    /// # Errors
896    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
897    /// # Panics
898    /// When either string contains '\0' characters, a panic occurs.
899    pub fn print_formatted<T: AsRef<Path>>(&self, filename: T, float_format: &str) -> Result<(), MjModelError> {
900        let path_str = filename.as_ref().to_str()
901            .ok_or(MjModelError::InvalidUtf8Path)?;
902        let c_filename = CString::new(path_str).unwrap();
903        let c_float_format = CString::new(float_format).unwrap();
904        unsafe { mj_printFormattedModel(self.ffi(), c_filename.as_ptr(), c_float_format.as_ptr()) }
905        Ok(())
906    }
907
908    /// Print model to text file.
909    ///
910    /// Wraps [`mj_printModel`].
911    /// # Returns
912    /// `Ok(())` if the path is valid UTF-8 and contains no interior `\0` characters.
913    /// **Note:** the underlying C function `mj_printModel` returns `void`, so file I/O
914    /// errors are not detectable; `Ok(())` does **not** guarantee the file was written.
915    /// # Errors
916    /// - [`MjModelError::InvalidUtf8Path`] if the path contains invalid UTF-8.
917    /// # Panics
918    /// When the filename contains '\0' characters, a panic occurs.
919    pub fn print<T: AsRef<Path>>(&self, filename: T) -> Result<(), MjModelError> {
920        let path_str = filename.as_ref().to_str()
921            .ok_or(MjModelError::InvalidUtf8Path)?;
922        let c_filename = CString::new(path_str).unwrap();
923        unsafe { mj_printModel(self.ffi(), c_filename.as_ptr()) }
924        Ok(())
925    }
926
927    /// Return size of state specification. The bits of the integer spec correspond to element fields of [`MjtState`](crate::wrappers::mj_data::MjtState).
928    /// Wraps [`mj_stateSize`].
929    /// # Note
930    /// MuJoCo aborts the process through `mjERROR` when `spec` is not below `1 << mjNSTATE`.
931    pub fn state_size(&self, spec: u32) -> usize {
932        unsafe { mj_stateSize(self.ffi(), spec as i32) as usize }
933    }
934
935    /// Extract the subset of components specified by `dst_spec` from a state `src`
936    /// previously obtained via [`MjData::read_state_into`] or [`MjData::state`]
937    /// with components specified by `src_spec`.
938    ///
939    /// Wraps [`mj_extractState`].
940    /// # Note
941    /// MuJoCo aborts the process through `mjERROR` when `src_spec` is not below
942    /// `1 << mjNSTATE`.
943    ///
944    /// # Panics
945    /// - When `src.len()` does not equal the size required by `src_spec`.
946    /// - When `dst_spec` is not a subset of `src_spec`.
947    ///
948    /// Use [`MjModel::try_extract_state`] for a fallible alternative.
949    pub fn extract_state(&self, src: &[MjtNum], src_spec: u32, dst_spec: u32) -> Box<[MjtNum]> {
950        self.try_extract_state(src, src_spec, dst_spec).unwrap()
951    }
952
953    /// Fallible version of [`MjModel::extract_state`].
954    ///
955    /// Wraps [`mj_extractState`].
956    /// # Note
957    /// MuJoCo aborts the process through `mjERROR` when `src_spec` is not below
958    /// `1 << mjNSTATE`.
959    /// # Returns
960    /// On success, returns [`Ok`] variant containing the extracted state.
961    /// # Errors
962    /// - When `src.len()` does not equal the size required by `src_spec`, [`MjModelError::StateSliceLengthMismatch`] is returned.
963    /// - When `dst_spec` is not a subset of `src_spec`, [`MjModelError::SpecNotSubset`] is returned.
964    pub fn try_extract_state(&self, src: &[MjtNum], src_spec: u32, dst_spec: u32) -> Result<Box<[MjtNum]>, MjModelError> {
965        let expected = self.state_size(src_spec);
966        if src.len() != expected {
967            return Err(MjModelError::StateSliceLengthMismatch { expected, got: src.len() });
968        }
969
970        if (dst_spec & src_spec) != dst_spec {
971            return Err(MjModelError::SpecNotSubset);
972        }
973
974        let required_size = self.state_size(dst_spec);
975        let mut dst = Vec::with_capacity(required_size);
976
977        // SAFETY: all pointer arguments are valid for the duration of this call. mj_extractState
978        // writes exactly `required_size` elements into dst; set_len then exposes only those
979        // initialized elements.
980        unsafe {
981            mj_extractState(
982                self.ffi(),
983                src.as_ptr(), src_spec as i32,
984                dst.as_mut_ptr(), dst_spec as i32
985            );
986
987            dst.set_len(required_size);
988            Ok(dst.into_boxed_slice())
989        }
990    }
991
992    /// Extract into dst the subset of components specified by `dst_spec` from a state `src`
993    /// previously obtained via [`MjData::read_state_into`] or [`MjData::state`]
994    /// with components specified by `src_spec`.
995    ///
996    /// Wraps [`mj_extractState`].
997    /// # Note
998    /// MuJoCo aborts the process through `mjERROR` when `src_spec` is not below
999    /// `1 << mjNSTATE`.
1000    ///
1001    /// # Panics
1002    /// - When `src.len()` does not equal the size required by `src_spec`.
1003    /// - When `dst_spec` is not a subset of `src_spec`.
1004    /// - When `dst` is too small to hold the requested components.
1005    ///
1006    /// Use [`MjModel::try_extract_state_into`] for a fallible alternative.
1007    pub fn extract_state_into(&self, src: &[MjtNum], src_spec: u32, dst: &mut [MjtNum], dst_spec: u32) -> usize {
1008        self.try_extract_state_into(src, src_spec, dst, dst_spec).unwrap()
1009    }
1010
1011    /// Fallible version of [`MjModel::extract_state_into`].
1012    ///
1013    /// Wraps [`mj_extractState`].
1014    /// # Note
1015    /// MuJoCo aborts the process through `mjERROR` when `src_spec` is not below
1016    /// `1 << mjNSTATE`.
1017    /// # Returns
1018    /// On success, returns [`Ok`] variant containing the number of elements written to `dst`.
1019    /// # Errors
1020    /// - When `src.len()` does not equal the size required by `src_spec`, [`MjModelError::StateSliceLengthMismatch`] is returned.
1021    /// - When `dst_spec` is not a subset of `src_spec`, [`MjModelError::SpecNotSubset`] is returned.
1022    /// - When `dst` is too small to hold the requested components, [`MjModelError::BufferTooSmall`] is returned.
1023    pub fn try_extract_state_into(&self, src: &[MjtNum], src_spec: u32, dst: &mut [MjtNum], dst_spec: u32) -> Result<usize, MjModelError> {
1024        let expected = self.state_size(src_spec);
1025        if src.len() != expected {
1026            return Err(MjModelError::StateSliceLengthMismatch { expected, got: src.len() });
1027        }
1028
1029        if (dst_spec & src_spec) != dst_spec {
1030            return Err(MjModelError::SpecNotSubset);
1031        }
1032
1033        let required_size = self.state_size(dst_spec);
1034        let available_size = dst.len();
1035
1036        if available_size < required_size  {
1037            return Err(MjModelError::BufferTooSmall { needed: required_size, available: available_size });
1038        }
1039
1040        unsafe {
1041            mj_extractState(
1042                self.ffi(),
1043                src.as_ptr(), src_spec as i32,
1044                dst.as_mut_ptr(), dst_spec as i32
1045            );
1046        }
1047
1048        Ok(required_size)
1049    }
1050
1051    /// Compute the velocity that carries `qpos1` to `qpos2` in `dt`.
1052    ///
1053    /// Wraps [`mj_differentiatePos`].
1054    /// 
1055    /// # Panics
1056    /// Panics when `qpos1` or `qpos2` does not hold [`MjModel::nq`] elements.
1057    /// Use [`MjModel::try_differentiate_pos`] for a fallible alternative.
1058    pub fn differentiate_pos(&self, dt: MjtNum, qpos1: &[MjtNum], qpos2: &[MjtNum]) -> Box<[MjtNum]> {
1059        self.try_differentiate_pos(dt, qpos1, qpos2).unwrap()
1060    }
1061
1062    /// Fallible version of [`MjModel::differentiate_pos`].
1063    ///
1064    /// Wraps [`mj_differentiatePos`].
1065    /// 
1066    /// # Returns
1067    /// On success, returns [`Ok`] variant containing the velocity, of [`MjModel::nv`] elements.
1068    /// 
1069    /// # Errors
1070    /// - When `qpos1` or `qpos2` does not hold [`MjModel::nq`] elements,
1071    ///   [`MjModelError::LengthMismatch`] is returned.
1072    pub fn try_differentiate_pos(&self, dt: MjtNum, qpos1: &[MjtNum], qpos2: &[MjtNum]) -> Result<Box<[MjtNum]>, MjModelError> {
1073        let mut qvel = vec![0 as MjtNum; self.ffi().nv as usize];
1074        self.try_differentiate_pos_into(dt, qpos1, qpos2, &mut qvel)?;
1075        Ok(qvel.into_boxed_slice())
1076    }
1077
1078    /// Same as [`MjModel::differentiate_pos`], except it writes the [`MjModel::nv`] elements into
1079    /// `qvel`. Elements of `qvel` above index `nv` keep their previous values.
1080    ///
1081    /// Wraps [`mj_differentiatePos`].
1082    /// 
1083    /// # Panics
1084    /// - When `qpos1` or `qpos2` does not hold [`MjModel::nq`] elements.
1085    /// - When `qvel` holds fewer than [`MjModel::nv`] elements.
1086    /// 
1087    /// Use [`MjModel::try_differentiate_pos_into`] for a fallible alternative.
1088    pub fn differentiate_pos_into(&self, dt: MjtNum, qpos1: &[MjtNum], qpos2: &[MjtNum], qvel: &mut [MjtNum]) {
1089        self.try_differentiate_pos_into(dt, qpos1, qpos2, qvel).unwrap()
1090    }
1091
1092    /// Fallible version of [`MjModel::differentiate_pos_into`].
1093    ///
1094    /// Wraps [`mj_differentiatePos`].
1095    /// 
1096    /// # Errors
1097    /// - When `qpos1` or `qpos2` does not hold [`MjModel::nq`] elements,
1098    ///   [`MjModelError::LengthMismatch`] is returned.
1099    /// - When `qvel` holds fewer than [`MjModel::nv`] elements,
1100    ///   [`MjModelError::BufferTooSmall`] is returned.
1101    pub fn try_differentiate_pos_into(&self, dt: MjtNum, qpos1: &[MjtNum], qpos2: &[MjtNum], qvel: &mut [MjtNum]) -> Result<(), MjModelError> {
1102        let nq = self.ffi().nq as usize;
1103        if qpos1.len() != nq {
1104            return Err(MjModelError::LengthMismatch { name: "qpos1", expected: nq, got: qpos1.len() });
1105        }
1106
1107        if qpos2.len() != nq {
1108            return Err(MjModelError::LengthMismatch { name: "qpos2", expected: nq, got: qpos2.len() });
1109        }
1110
1111        let nv = self.ffi().nv as usize;
1112        if qvel.len() < nv {
1113            return Err(MjModelError::BufferTooSmall { needed: nv, available: qvel.len() });
1114        }
1115
1116        // SAFETY: all pointers are valid for the duration of this call; qpos1 and qpos2 hold nq
1117        // elements and qvel holds at least nv elements, as checked above.
1118        unsafe {
1119            mj_differentiatePos(
1120                self.ffi(),
1121                qvel.as_mut_ptr(), dt,
1122                qpos1.as_ptr(), qpos2.as_ptr()
1123            )
1124        };
1125
1126        Ok(())
1127    }
1128
1129    /// Integrate `qpos` in place with velocity `qvel` and time step `dt`.
1130    ///
1131    /// Wraps [`mj_integratePos`].
1132    /// # Panics
1133    /// - When `qpos` does not hold [`MjModel::nq`] elements.
1134    /// - When `qvel` does not hold [`MjModel::nv`] elements.
1135    pub fn integrate_pos(&self, qpos: &mut [MjtNum], qvel: &[MjtNum], dt: MjtNum) {
1136        // mj_integratePos reads both vectors by the model layout, without a length of its own.
1137        assert_eq!(qpos.len(), self.ffi().nq as usize, "qpos must hold nq elements");
1138        assert_eq!(qvel.len(), self.ffi().nv as usize, "qvel must hold nv elements");
1139
1140        // SAFETY: both pointers are valid for the duration of this call, with the lengths that the
1141        // model layout requires, as checked above.
1142        unsafe { mj_integratePos(self.ffi(), qpos.as_mut_ptr(), qvel.as_ptr(), dt) }
1143    }
1144
1145    /// Normalize every ball and free joint quaternion of a `qpos` vector in place.
1146    ///
1147    /// Wraps [`mj_normalizeQuat`].
1148    /// # Panics
1149    /// Panics when `qpos` does not hold [`MjModel::nq`] elements.
1150    pub fn normalize_quat(&self, qpos: &mut [MjtNum]) {
1151        // mj_normalizeQuat reads the vector by the model layout, without a length of its own.
1152        assert_eq!(qpos.len(), self.ffi().nq as usize, "qpos must hold nq elements");
1153
1154        // SAFETY: the pointer is valid for the duration of this call, with the length that the
1155        // model layout requires, as checked above.
1156        unsafe { mj_normalizeQuat(self.ffi(), qpos.as_mut_ptr()) }
1157    }
1158
1159    /// Determine type of friction cone. Returns `true` if pyramidal, `false` if elliptic.
1160    /// Wraps [`mj_isPyramidal`].
1161    pub fn is_pyramidal(&self) -> bool {
1162        unsafe { mj_isPyramidal(self.ffi()) == 1 }
1163    }
1164
1165    /// Determine type of constraint Jacobian. Returns `true` if sparse, `false` if dense.
1166    /// Wraps [`mj_isSparse`].
1167    pub fn is_sparse(&self) -> bool {
1168        unsafe { mj_isSparse(self.ffi()) == 1 }
1169    }
1170
1171    /// Determine type of solver. Returns `true` for a dual solver: PGS, or any solver with
1172    /// `noslip_iterations > 0`.
1173    /// Wraps [`mj_isDual`].
1174    pub fn is_dual(&self) -> bool {
1175        unsafe { mj_isDual(self.ffi()) == 1 }
1176    }
1177
1178    /// Get name of object with the specified [`MjtObj`] type and id, returns `None` if name not found.
1179    /// Wraps [`mj_id2name`].
1180    /// # Panics
1181    /// Panics if MuJoCo internally returns a C string that is not valid UTF-8.
1182    pub fn id_to_name(&self, type_: MjtObj, id: usize) -> Option<&str> {
1183        let ptr = unsafe { mj_id2name(self.ffi(), type_ as i32, id as i32) };
1184        if ptr.is_null() {
1185            None
1186        }
1187        else {
1188            // SAFETY: ptr was checked non-null above; MuJoCo NUL-terminates the names blob and it
1189            // lives as long as the model.
1190            let cstr = unsafe { CStr::from_ptr(ptr).to_str().unwrap() };
1191            Some(cstr)
1192        }
1193    }
1194
1195    /// Sum all body masses.
1196    /// Wraps [`mj_getTotalmass`].
1197    pub fn totalmass(&self) -> MjtNum {
1198        unsafe { mj_getTotalmass(self.ffi()) }
1199    }
1200
1201    /// Scale body masses and inertias to achieve specified total mass.
1202    /// Wraps [`mj_setTotalmass`].
1203    pub fn set_totalmass(&mut self, newmass: MjtNum) {
1204        unsafe { mj_setTotalmass(self.ffi_mut(), newmass) }
1205    }
1206
1207    /// Return the maximum number of contacts that can be generated between two geoms.
1208    /// 
1209    /// To pull margin from model, set `has_margin` to [`None`], otherwise pass `true` or `false`
1210    /// inside [`Some`] (true indicating a present margin). 
1211    /// 
1212    /// Wraps [`mj_maxContact`].
1213    /// # Panics
1214    /// Panics when either `geom1` or `geom2` are equal or greater than [`MjModel::ngeom`].
1215    /// Use [`MjModel::try_max_contacts`] for a fallible alternative.
1216    pub fn max_contacts(&self, geom1: usize, geom2: usize, has_margin: Option<bool>) -> u32 {
1217        self.try_max_contacts(geom1, geom2, has_margin).unwrap()
1218    }
1219
1220    /// Fallible version of [`MjModel::max_contacts`].
1221    /// Wraps [`mj_maxContact`].
1222    /// # Errors
1223    /// Returns [`MjModelError::IndexOutOfBounds`] when either `geom1` or `geom2` are equal or
1224    /// greater than [`MjModel::ngeom`].
1225    pub fn try_max_contacts(&self, geom1: usize, geom2: usize, has_margin: Option<bool>) -> Result<u32, MjModelError> {
1226        let ngeom = self.ngeom() as usize;
1227
1228        if geom1 >= ngeom {
1229            return Err(MjModelError::IndexOutOfBounds { id: geom1, len: ngeom });
1230        }
1231
1232        if geom2 >= ngeom {
1233            return Err(MjModelError::IndexOutOfBounds { id: geom2, len: ngeom });
1234        }
1235
1236        Ok(unsafe { mj_maxContact(
1237            self.ffi(),
1238            geom1 as i32, geom2 as i32,
1239            // if Some(...), pass 0 or 1, otherwise pull from model (-1)
1240            has_margin.map(|m| m as i32).unwrap_or(-1)
1241        ) as u32 })
1242    }
1243
1244    /// Returns the name that the type of actuator `id` gives to its control input `input`, an
1245    /// index into the control block of the actuator. For example, an orientation servo on the
1246    /// exponential-map chart names its first input `"rx"`.
1247    /// Wraps [`mj_actuatorInputName`].
1248    /// # Returns
1249    /// [`None`] when the actuator type defines no input names, or when `id` or `input` is out of
1250    /// range.
1251    /// # Panics
1252    /// Panics if the reported name is not valid UTF-8.
1253    pub fn actuator_input_name(&self, id: usize, input: usize) -> Option<&'static str> {
1254        // SAFETY: id and input are checked inside the function. When c_ptr is not null, it is
1255        // always an ASCII static C string constant, so the UTF-8 check cannot fail.
1256        unsafe {
1257            let c_ptr = mj_actuatorInputName(self.ffi(), id as i32, input as i32);
1258            (!c_ptr.is_null()).then(|| CStr::from_ptr(c_ptr).to_str().unwrap())
1259        }
1260    }
1261
1262    /* FFI */
1263    /// Returns a reference to the wrapped FFI struct.
1264    pub fn ffi(&self) -> &mjModel {
1265        // SAFETY: self.ptr is a valid non-null mjModel pointer for the lifetime of self
1266        // (struct invariant).
1267        unsafe { self.ptr.as_ref() }
1268    }
1269
1270    /// Returns a mutable reference to the wrapped FFI struct.
1271    ///
1272    /// # Safety
1273    /// The caller must ensure that any modifications to the underlying struct preserve
1274    /// the invariants that MuJoCo expects (e.g. do not corrupt computed fields or
1275    /// break index relationships). Violating these invariants can cause undefined behavior.
1276    /// A write that changes a size or an address table also leaves the cached layout stale, so
1277    /// [`MjModel::is_compatible_with_model`] then answers for the model as it was loaded.
1278    pub unsafe fn ffi_mut(&mut self) -> &mut mjModel {
1279        unsafe { self.ptr.as_mut() }
1280    }
1281
1282    /// Returns a direct mutable pointer to the underlying C model struct.
1283    /// Only for internal use by viewer code that passes the pointer to C++ FFI.
1284    #[cfg(feature = "cpp-viewer")]
1285    pub(crate) fn as_raw_ptr(&self) -> *mut mjModel {
1286        self.ptr.as_ptr()
1287    }
1288}
1289
1290
1291/// Public attribute methods.
1292impl MjModel {
1293    /// Compilation signature.
1294    pub fn signature(&self) -> u64 {
1295        self.ffi().signature
1296    }
1297
1298    /// Reports whether `other` is memory-compatible with this model.
1299    pub fn is_compatible_with_model(&self, other: &MjModel) -> bool {
1300        self.layout() == other.layout()
1301    }
1302
1303    /// Reports whether `other`'s assets are memory-compatible with this model's.
1304    pub fn is_asset_compatible_with_model(&self, other: &MjModel) -> bool {
1305        self.layout().nmeshgraph == other.layout().nmeshgraph
1306            && self.layout().asset_split() == other.layout().asset_split()
1307    }
1308
1309    /// Returns the per-sensor, per-numeric, per-tuple, per-actuator, per-tendon, per-flex and
1310    /// plugin count tables, as raw bytes in a fixed order.
1311    fn element_split_tables(&self) -> [&[u8]; ELEMENT_SPLIT_TABLES] {
1312        [
1313            must_cast_slice(self.sensor_dim()),         must_cast_slice(self.numeric_size()),
1314            must_cast_slice(self.tuple_size()),         must_cast_slice(self.actuator_actnum()),
1315            must_cast_slice(self.actuator_ctrlnum()),   must_cast_slice(self.actuator_outnum()),
1316            // A tendon Info caches its Jacobian row, which ten_J_rownnz sizes inside nJten.
1317            must_cast_slice(self.ten_j_rownnz()),
1318            // An mjData contact holds a flex element index that C resolves through
1319            // flex_elemdataadr, and mj_resetData relays a plugin instance to the slot in m->plugin.
1320            must_cast_slice(self.flex_dim()),           must_cast_slice(self.flex_vertnum()),
1321            must_cast_slice(self.flex_elemnum()),       must_cast_slice(self.plugin()),
1322            // plugin_stateadr is the prefix sum of plugin_statenum inside npluginstate.
1323            must_cast_slice(self.plugin_statenum()),
1324            // Every per-element type below fixes what a state, a control or an output MEANS. The
1325            // length of each one also pins the count of the elements it describes.
1326            must_cast_slice(self.jnt_type()),
1327            must_cast_slice(self.geom_type()),          must_cast_slice(self.eq_type()),
1328            must_cast_slice(self.eq_objtype()),         must_cast_slice(self.wrap_type()),
1329            must_cast_slice(self.actuator_trntype()),   must_cast_slice(self.actuator_dyntype()),
1330            must_cast_slice(self.actuator_gaintype()),  must_cast_slice(self.actuator_biastype()),
1331            must_cast_slice(self.sensor_type()),        must_cast_slice(self.sensor_objtype()),
1332            must_cast_slice(self.sensor_reftype()),     must_cast_slice(self.sensor_datatype()),
1333            must_cast_slice(self.sensor_needstage()),
1334            // The kinematic tree, and the body that owns each element. `mjModel` holds the tree as
1335            // a parent id per body, so the nesting travels as data.
1336            must_cast_slice(self.body_parentid()),      must_cast_slice(self.jnt_bodyid()),
1337            must_cast_slice(self.geom_bodyid()),        must_cast_slice(self.site_bodyid()),
1338            must_cast_slice(self.cam_bodyid()),         must_cast_slice(self.light_bodyid()),
1339        ]
1340    }
1341
1342    /// Returns the per-mesh, per-texture and per-heightfield count tables, plus the texture kinds,
1343    /// as raw bytes in a fixed order.
1344    fn asset_split_tables(&self) -> [&[u8]; ASSET_SPLIT_TABLES] {
1345        [
1346            must_cast_slice(self.mesh_vertnum()),       must_cast_slice(self.mesh_normalnum()),
1347            must_cast_slice(self.mesh_texcoordnum()),   must_cast_slice(self.mesh_facenum()),
1348            // mesh_graphadr travels as an address: its length is the size of the convex hull that
1349            // qhull builds, and mjModel holds no count for it.
1350            must_cast_slice(self.mesh_graphadr()),
1351            must_cast_slice(self.tex_width()),          must_cast_slice(self.tex_height()),
1352            must_cast_slice(self.tex_nchannel()),
1353            // A cube texture holds the same bytes as a 2D texture of the same width and height,
1354            // so the sizes alone leave the kind free. mjr_makeContext records tex_type in
1355            // mjrContext, and mjr_uploadTexture binds GL_TEXTURE_2D or GL_TEXTURE_CUBE_MAP by it.
1356            must_cast_slice(self.tex_type()),
1357            must_cast_slice(self.hfield_nrow()),        must_cast_slice(self.hfield_ncol()),
1358        ]
1359    }
1360
1361    /// Copies every asset table and every element table into its own buffer.
1362    fn split_tables(&self) -> MjSplitTables {
1363        MjSplitTables {
1364            assets: self.asset_split_tables().map(Box::from),
1365            elements: self.element_split_tables().map(Box::from),
1366        }
1367    }
1368
1369    /// Returns the memory layout snapshot of this model.
1370    pub(crate) fn layout(&self) -> &Arc<MjModelLayout> {
1371        self.layout.get_or_init(|| Arc::new(MjModelLayout::from(self)))
1372    }
1373
1374    getter_setter! {get, [
1375        [ffi] nq: MjtSize; "number of generalized coordinates = dim(qpos).";
1376        [ffi] nv: MjtSize; "number of degrees of freedom = dim(qvel).";
1377        [ffi] nu: MjtSize; "number of scalar controls = dim(ctrl).";
1378        [ffi] nactuator: MjtSize; "number of actuators.";
1379        [ffi] nout: MjtSize; "number of force outputs, derived from transmission type.";
1380        [ffi] na: MjtSize; "number of activation states = dim(act).";
1381        [ffi] nbody: MjtSize; "number of bodies.";
1382        [ffi] nbvh: MjtSize; "number of total bounding volumes in all bodies.";
1383        [ffi] nbvhstatic: MjtSize; "number of static bounding volumes (aabb stored in mjModel).";
1384        [ffi] nbvhdynamic: MjtSize; "number of dynamic bounding volumes (aabb stored in mjData).";
1385        [ffi] noct: MjtSize; "number of total octree cells in all meshes.";
1386        [ffi] njnt: MjtSize; "number of joints.";
1387        [ffi] ntree: MjtSize; "number of kinematic trees under world body.";
1388        [ffi] nM: MjtSize; "number of non-zeros in sparse inertia matrix.";
1389        [ffi] nB: MjtSize; "number of non-zeros in sparse body-dof matrix.";
1390        [ffi] nC: MjtSize; "number of non-zeros in sparse reduced dof-dof matrix.";
1391        [ffi] nD: MjtSize; "number of non-zeros in sparse dof-dof matrix.";
1392        [ffi] ngeom: MjtSize; "number of geoms.";
1393        [ffi] nsite: MjtSize; "number of sites.";
1394        [ffi] ncam: MjtSize; "number of cameras.";
1395        [ffi] nlight: MjtSize; "number of lights.";
1396        [ffi] nflex: MjtSize; "number of flexes.";
1397        [ffi] nflexnode: MjtSize; "number of dofs in all flexes.";
1398        [ffi] nflexvert: MjtSize; "number of vertices in all flexes.";
1399        [ffi] nflexedge: MjtSize; "number of edges in all flexes.";
1400        [ffi] nflexelem: MjtSize; "number of elements in all flexes.";
1401        [ffi] nflexelemdata: MjtSize; "number of element vertex ids in all flexes.";
1402        [ffi] nflexstiffness: MjtSize; "number of stiffness parameters in all flexes.";
1403        [ffi] nflexbending: MjtSize; "number of bending parameters in all flexes";
1404        [ffi] nefm0dof: MjtSize; "number of dofs covered by the constant metric factor.";
1405        [ffi] nefm0L: MjtSize; "number of non-zeros in the constant metric factor.";
1406        [ffi] nflexelemedge: MjtSize; "number of element edge ids in all flexes.";
1407        [ffi] nflexshelldata: MjtSize; "number of shell fragment vertex ids in all flexes.";
1408        [ffi] nflexevpair: MjtSize; "number of element-vertex pairs in all flexes.";
1409        [ffi] nflextexcoord: MjtSize; "number of vertices with texture coordinates.";
1410        [ffi] nJfe: MjtSize; "number of non-zeros in sparse flexedge Jacobian matrix.";
1411        [ffi] nJfv: MjtSize; "number of non-zeros in sparse flexvert Jacobian matrix.";
1412        [ffi] nmesh: MjtSize; "number of meshes.";
1413        [ffi] nmeshvert: MjtSize; "number of vertices in all meshes.";
1414        [ffi] nmeshnormal: MjtSize; "number of normals in all meshes.";
1415        [ffi] nmeshtexcoord: MjtSize; "number of texcoords in all meshes.";
1416        [ffi] nmeshface: MjtSize; "number of triangular faces in all meshes.";
1417        [ffi] nmeshgraph: MjtSize; "number of ints in mesh auxiliary data.";
1418        [ffi] nmeshpoly: MjtSize; "number of polygons in all meshes.";
1419        [ffi] nmeshpolyvert: MjtSize; "number of vertices in all polygons.";
1420        [ffi] nmeshpolymap: MjtSize; "number of polygons in vertex map.";
1421        [ffi] nskin: MjtSize; "number of skins.";
1422        [ffi] nskinvert: MjtSize; "number of vertices in all skins.";
1423        [ffi] nskintexvert: MjtSize; "number of vertices with texcoords in all skins.";
1424        [ffi] nskinface: MjtSize; "number of triangular faces in all skins.";
1425        [ffi] nskinbone: MjtSize; "number of bones in all skins.";
1426        [ffi] nskinbonevert: MjtSize; "number of vertices in all skin bones.";
1427        [ffi] nhfield: MjtSize; "number of heightfields.";
1428        [ffi] nhfielddata: MjtSize; "number of data points in all heightfields.";
1429        [ffi] ntex: MjtSize; "number of textures.";
1430        [ffi] ntexdata: MjtSize; "number of bytes in texture rgb data.";
1431        [ffi] nmat: MjtSize; "number of materials.";
1432        [ffi] npair: MjtSize; "number of predefined geom pairs.";
1433        [ffi] nexclude: MjtSize; "number of excluded geom pairs.";
1434        [ffi] neq: MjtSize; "number of equality constraints.";
1435        [ffi] ntendon: MjtSize; "number of tendons.";
1436        [ffi] nJten: MjtSize; "number of non-zeros in sparse tendon Jacobian matrix.";
1437        [ffi] nwrap: MjtSize; "number of wrap objects in all tendon paths.";
1438        [ffi] nsensor: MjtSize; "number of sensors.";
1439        [ffi] nnumeric: MjtSize; "number of numeric custom fields.";
1440        [ffi] nnumericdata: MjtSize; "number of mjtNums in all numeric fields.";
1441        [ffi] ntext: MjtSize; "number of text custom fields.";
1442        [ffi] ntextdata: MjtSize; "number of mjtBytes in all text fields.";
1443        [ffi] ntuple: MjtSize; "number of tuple custom fields.";
1444        [ffi] ntupledata: MjtSize; "number of objects in all tuple fields.";
1445        [ffi] nkey: MjtSize; "number of keyframes.";
1446        [ffi] nmocap: MjtSize; "number of mocap bodies.";
1447        [ffi] nplugin: MjtSize; "number of plugin instances.";
1448        [ffi] npluginattr: MjtSize; "number of chars in all plugin config attributes.";
1449        [ffi] nuser_body: MjtSize; "number of mjtNums in body_user.";
1450        [ffi] nuser_jnt: MjtSize; "number of mjtNums in jnt_user.";
1451        [ffi] nuser_geom: MjtSize; "number of mjtNums in geom_user.";
1452        [ffi] nuser_site: MjtSize; "number of mjtNums in site_user.";
1453        [ffi] nuser_cam: MjtSize; "number of mjtNums in cam_user.";
1454        [ffi] nuser_tendon: MjtSize; "number of mjtNums in tendon_user.";
1455        [ffi] nuser_actuator: MjtSize; "number of mjtNums in actuator_user.";
1456        [ffi] nuser_sensor: MjtSize; "number of mjtNums in sensor_user.";
1457        [ffi] nnames: MjtSize; "number of chars in all names.";
1458        [ffi] npaths: MjtSize; "number of chars in all paths.";
1459        [ffi] nnames_map: MjtSize; "number of slots in the names hash map.";
1460        [ffi] nJmom: MjtSize; "number of non-zeros in sparse actuator_moment matrix.";
1461        [ffi] ngravcomp: MjtSize; "number of bodies with nonzero gravcomp.";
1462        [ffi] nemax: MjtSize; "number of potential equality-constraint rows.";
1463        [ffi] njmax: MjtSize; "number of available rows in constraint Jacobian (legacy).";
1464        [ffi] nconmax: MjtSize; "number of potential contacts in contact list (legacy).";
1465        [ffi] npolygonmax: MjtSize; "maximum number of vertices in a mesh polygon.";
1466        [ffi] nmeshdegmax: MjtSize; "maximum number of edges adjacent to a mesh vertex.";
1467        [ffi] nuserdata: MjtSize; "number of mjtNums reserved for the user.";
1468        [ffi] nsensordata: MjtSize; "number of mjtNums in sensor data vector.";
1469        [ffi] npluginstate: MjtSize; "number of mjtNums in plugin state vector.";
1470        [ffi] nhistory: MjtSize; "number of mjtNums in history buffer.";
1471        [ffi] narena: MjtSize; "number of bytes in the mjData arena (inclusive of stack).";
1472        [ffi] nbuffer: MjtSize; "number of bytes in buffer.";
1473        [ffi] flg_gravcomp: MjtBool; "whether any body has nonzero gravcomp.";
1474        [ffi] flg_surfacevel: MjtBool; "whether any geom has nonzero surfacevel.";
1475        [ffi] flg_adhesion: MjtBool; "whether any geom or pair has nonzero adhesion.";
1476    ]}
1477
1478    getter_setter! {get, [
1479        [ffi, ffi_mut] opt: &MjOption; "physics options.";
1480        [ffi, ffi_mut] vis: &MjVisual; "visualization options.";
1481        [ffi, ffi_mut] stat: &MjStatistic; "model statistics.";
1482    ]}
1483}
1484
1485/// Array slices.
1486impl MjModel {
1487    array_slice_dyn! {
1488        probe = probe_dynamic_arrays;
1489        qpos0: &[MjtNum; "qpos values at default pose"; ffi().nq],
1490        qpos_spring: &[MjtNum; "reference pose for springs"; ffi().nq],
1491        (mut = unsafe) body_parentid: &[i32; "id of body's parent"; ffi().nbody],
1492        (mut = unsafe) body_rootid: &[i32; "ancestor that is direct child of world"; ffi().nbody],
1493        (mut = unsafe) body_weldid: &[i32; "top dof-less ancestor; mocap: own root"; ffi().nbody],
1494        (mut = unsafe) body_mocapid: &[i32; "id of mocap data; -1: none"; ffi().nbody],
1495        (mut = unsafe) body_jntnum: &[i32; "number of joints for this body"; ffi().nbody],
1496        (mut = unsafe) body_jntadr: &[i32; "start addr of joints; -1: no joints"; ffi().nbody],
1497        (mut = unsafe) body_dofnum: &[i32; "number of motion degrees of freedom"; ffi().nbody],
1498        (mut = unsafe) body_dofadr: &[i32; "start addr of dofs; -1: no dofs"; ffi().nbody],
1499        (mut = unsafe) body_treeid: &[i32; "id of body's kinematic tree; -1: static"; ffi().nbody],
1500        (mut = unsafe) body_geomnum: &[i32; "number of geoms"; ffi().nbody],
1501        (mut = unsafe) body_geomadr: &[i32; "start addr of geoms; -1: no geoms"; ffi().nbody],
1502        body_simple: &[MjtByte; "1: diag M; 2: diag M, sliders only"; ffi().nbody],
1503        body_sameframe: &[MjtSameFrame [force]; "same frame as inertia"; ffi().nbody],
1504        body_pos: &[[MjtNum; 3] [force]; "position offset rel. to parent body"; ffi().nbody],
1505        body_quat: &[[MjtNum; 4] [force]; "orientation offset rel. to parent body"; ffi().nbody],
1506        body_ipos: &[[MjtNum; 3] [force]; "local position of center of mass"; ffi().nbody],
1507        body_iquat: &[[MjtNum; 4] [force]; "local orientation of inertia ellipsoid"; ffi().nbody],
1508        body_mass: &[MjtNum; "mass"; ffi().nbody],
1509        body_subtreemass: &[MjtNum; "mass of subtree starting at this body"; ffi().nbody],
1510        body_inertia: &[[MjtNum; 3] [force]; "diagonal inertia in ipos/iquat frame"; ffi().nbody],
1511        body_invweight0: &[[MjtNum; 2] [force]; "mean inv inert in qpos0 (trn, rot)"; ffi().nbody],
1512        body_gravcomp: &[MjtNum; "antigravity force, units of body weight"; ffi().nbody],
1513        body_margin: &[MjtNum; "MAX over all geom margins+gaps"; ffi().nbody],
1514        (mut = unsafe) body_plugin: &[i32; "plugin instance id; -1: not in use"; ffi().nbody],
1515        body_contype: &[i32; "OR over all geom contypes"; ffi().nbody],
1516        body_conaffinity: &[i32; "OR over all geom conaffinities"; ffi().nbody],
1517        (mut = unsafe) body_bvhadr: &[i32; "address of bvh root"; ffi().nbody],
1518        (mut = unsafe) body_bvhnum: &[i32; "number of bounding volumes"; ffi().nbody],
1519        bvh_depth: &[i32; "depth in the bounding volume hierarchy"; ffi().nbvh],
1520        (mut = unsafe) bvh_child: &[[i32; 2] [force]; "left and right children in tree"; ffi().nbvh],
1521        (mut = unsafe) bvh_nodeid: &[i32; "geom or elem id of node; -1: non-leaf"; ffi().nbvh],
1522        bvh_aabb: &[[MjtNum; 6] [force]; "local bounding box (center, size)"; ffi().nbvhstatic],
1523        oct_depth: &[i32; "depth in the octree"; ffi().noct],
1524        (mut = unsafe) oct_child: &[[i32; 8] [force]; "children of octree node"; ffi().noct],
1525        oct_aabb: &[[MjtNum; 6] [force]; "octree node bounding box (center, size)"; ffi().noct],
1526        oct_coeff: &[[MjtNum; 8] [force]; "octree interpolation coefficients"; ffi().noct],
1527        (mut = unsafe) jnt_type: &[MjtJoint [force]; "type of joint"; ffi().njnt],
1528        (mut = unsafe) jnt_qposadr: &[i32; "start addr in 'qpos' for joint's data"; ffi().njnt],
1529        (mut = unsafe) jnt_dofadr: &[i32; "start addr in 'qvel' for joint's data"; ffi().njnt],
1530        (mut = unsafe) jnt_bodyid: &[i32; "id of joint's body"; ffi().njnt],
1531        (mut = unsafe) jnt_actuatorid: &[i32; "actuator contributing damping / armature"; ffi().njnt],
1532        jnt_group: &[i32; "group for visibility"; ffi().njnt],
1533        jnt_limited: &[MjtBool; "does joint have limits"; ffi().njnt],
1534        jnt_actfrclimited: &[MjtBool; "does joint have actuator force limits"; ffi().njnt],
1535        jnt_actgravcomp: &[MjtBool; "is gravcomp force applied via actuators"; ffi().njnt],
1536        jnt_solref: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference: limit"; ffi().njnt],
1537        jnt_solimp: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance: limit"; ffi().njnt],
1538        jnt_pos: &[[MjtNum; 3] [force]; "local anchor position"; ffi().njnt],
1539        jnt_axis: &[[MjtNum; 3] [force]; "local joint axis"; ffi().njnt],
1540        jnt_stiffness: &[MjtNum; "linear stiffness coefficient"; ffi().njnt],
1541        jnt_stiffnesspoly: &[[MjtNum; mjNPOLY as usize] [force]; "high-order stiffness coefficients"; ffi().njnt],
1542        jnt_range: &[[MjtNum; 2] [force]; "joint limits"; ffi().njnt],
1543        jnt_actfrcrange: &[[MjtNum; 2] [force]; "range of total actuator force"; ffi().njnt],
1544        jnt_margin: &[MjtNum; "min distance for limit detection"; ffi().njnt],
1545        (mut = unsafe) dof_bodyid: &[i32; "id of dof's body"; ffi().nv],
1546        (mut = unsafe) dof_jntid: &[i32; "id of dof's joint"; ffi().nv],
1547        (mut = unsafe) dof_parentid: &[i32; "id of dof's parent; -1: none"; ffi().nv],
1548        (mut = unsafe) dof_treeid: &[i32; "id of dof's kinematic tree"; ffi().nv],
1549        (mut = unsafe) dof_Madr: &[i32; "dof address in M-diagonal"; ffi().nv],
1550        dof_simplenum: &[i32; "number of consecutive simple dofs"; ffi().nv],
1551        dof_solref: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference:frictionloss"; ffi().nv],
1552        dof_solimp: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance:frictionloss"; ffi().nv],
1553        dof_frictionloss: &[MjtNum; "dof friction loss"; ffi().nv],
1554        dof_armature: &[MjtNum; "dof armature inertia/mass"; ffi().nv],
1555        dof_damping: &[MjtNum; "linear damping coefficient"; ffi().nv],
1556        dof_dampingpoly: &[[MjtNum; mjNPOLY as usize] [force]; "high-order damping coefficients"; ffi().nv],
1557        dof_invweight0: &[MjtNum; "diag. inverse inertia in qpos0"; ffi().nv],
1558        dof_M0: &[MjtNum; "diag. inertia in qpos0"; ffi().nv],
1559        dof_length: &[MjtNum; "linear: 1; angular: approx. length scale"; ffi().nv],
1560        (mut = unsafe) tree_bodyadr: &[i32; "start addr of bodies"; ffi().ntree],
1561        (mut = unsafe) tree_bodynum: &[i32; "number of bodies in tree"; ffi().ntree],
1562        (mut = unsafe) tree_dofadr: &[i32; "start addr of dofs"; ffi().ntree],
1563        (mut = unsafe) tree_dofnum: &[i32; "number of dofs in tree"; ffi().ntree],
1564        tree_sleep_policy: &[MjtSleepPolicy [force]; "sleep policy"; ffi().ntree],
1565        (mut = unsafe) geom_type: &[MjtGeom [force]; "geometric type"; ffi().ngeom],
1566        geom_contype: &[i32; "geom contact type"; ffi().ngeom],
1567        geom_conaffinity: &[i32; "geom contact affinity"; ffi().ngeom],
1568        (mut = unsafe) geom_condim: &[i32; "contact dimensionality (1, 3, 4, 6)"; ffi().ngeom],
1569        (mut = unsafe) geom_bodyid: &[i32; "id of geom's body"; ffi().ngeom],
1570        (mut = unsafe) geom_dataid: &[i32; "id of geom's mesh/hfield; -1: none"; ffi().ngeom],
1571        (mut = unsafe) geom_matid: &[i32; "material id for rendering; -1: none"; ffi().ngeom],
1572        geom_group: &[i32; "group for visibility"; ffi().ngeom],
1573        geom_priority: &[i32; "geom contact priority"; ffi().ngeom],
1574        (mut = unsafe) geom_plugin: &[i32; "plugin instance id; -1: not in use"; ffi().ngeom],
1575        geom_sameframe: &[MjtSameFrame [force]; "same frame as body"; ffi().ngeom],
1576        geom_solmix: &[MjtNum; "mixing coef for solref/imp in geom pair"; ffi().ngeom],
1577        geom_solref: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference: contact"; ffi().ngeom],
1578        geom_solimp: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance: contact"; ffi().ngeom],
1579        geom_size: &[[MjtNum; 3] [force]; "geom-specific size parameters"; ffi().ngeom],
1580        geom_aabb: &[[MjtNum; 6] [force]; "bounding box, (center, size)"; ffi().ngeom],
1581        geom_rbound: &[MjtNum; "radius of bounding sphere"; ffi().ngeom],
1582        geom_pos: &[[MjtNum; 3] [force]; "local position offset rel. to body"; ffi().ngeom],
1583        geom_quat: &[[MjtNum; 4] [force]; "local orientation offset rel. to body"; ffi().ngeom],
1584        geom_friction: &[[MjtNum; 3] [force]; "friction for (slide, spin, roll)"; ffi().ngeom],
1585        geom_margin: &[MjtNum; "geometric inflation for contact"; ffi().ngeom],
1586        geom_gap: &[MjtNum; "additional contact detection buffer"; ffi().ngeom],
1587        geom_surfacevel: &[[MjtNum; 6] [force]; "surface velocity in local frame: lin,ang"; ffi().ngeom],
1588        geom_adhesion: &[MjtNum; "adhesive force of contacts"; ffi().ngeom],
1589        geom_fluid: &[[MjtNum; mjNFLUID as usize] [force]; "fluid interaction parameters"; ffi().ngeom],
1590        geom_rgba: &[[f32; 4] [force]; "rgba when material is omitted"; ffi().ngeom],
1591        site_type: &[MjtGeom [force]; "geom type for rendering"; ffi().nsite],
1592        (mut = unsafe) site_bodyid: &[i32; "id of site's body"; ffi().nsite],
1593        (mut = unsafe) site_matid: &[i32; "material id for rendering; -1: none"; ffi().nsite],
1594        site_group: &[i32; "group for visibility"; ffi().nsite],
1595        site_sameframe: &[MjtSameFrame [force]; "same frame as body"; ffi().nsite],
1596        site_size: &[[MjtNum; 3] [force]; "geom size for rendering"; ffi().nsite],
1597        site_pos: &[[MjtNum; 3] [force]; "local position offset rel. to body"; ffi().nsite],
1598        site_quat: &[[MjtNum; 4] [force]; "local orientation offset rel. to body"; ffi().nsite],
1599        site_rgba: &[[f32; 4] [force]; "rgba when material is omitted"; ffi().nsite],
1600        cam_mode: &[MjtCamLight [force]; "camera tracking mode"; ffi().ncam],
1601        (mut = unsafe) cam_bodyid: &[i32; "id of camera's body"; ffi().ncam],
1602        (mut = unsafe) cam_targetbodyid: &[i32; "id of targeted body; -1: none"; ffi().ncam],
1603        cam_pos: &[[MjtNum; 3] [force]; "position rel. to body frame"; ffi().ncam],
1604        cam_quat: &[[MjtNum; 4] [force]; "orientation rel. to body frame"; ffi().ncam],
1605        cam_poscom0: &[[MjtNum; 3] [force]; "global position rel. to sub-com in qpos0"; ffi().ncam],
1606        cam_pos0: &[[MjtNum; 3] [force]; "global position rel. to body in qpos0"; ffi().ncam],
1607        cam_mat0: &[[MjtNum; 9] [force]; "global orientation in qpos0"; ffi().ncam],
1608        cam_projection: &[MjtProjection [force]; "projection type"; ffi().ncam],
1609        cam_fovy: &[MjtNum; "y field-of-view (ortho ? len : deg)"; ffi().ncam],
1610        cam_ipd: &[MjtNum; "inter-pupillary distance"; ffi().ncam],
1611        (mut = unsafe) cam_resolution: &[[i32; 2] [force]; "resolution: pixels [width, height]"; ffi().ncam],
1612        cam_output: &[i32; "output types (MjtCamOutBit bit flags)"; ffi().ncam],
1613        cam_sensorsize: &[[f32; 2] [force]; "sensor size: length [width, height]"; ffi().ncam],
1614        cam_intrinsic: &[[f32; 4] [force]; "[focal length; principal point]"; ffi().ncam],
1615        light_mode: &[MjtCamLight [force]; "light tracking mode"; ffi().nlight],
1616        (mut = unsafe) light_bodyid: &[i32; "id of light's body"; ffi().nlight],
1617        (mut = unsafe) light_targetbodyid: &[i32; "id of targeted body; -1: none"; ffi().nlight],
1618        light_type: &[MjtLightType [force]; "spot, directional, etc."; ffi().nlight],
1619        (mut = unsafe) light_texid: &[i32; "texture id for image lights"; ffi().nlight],
1620        light_castshadow: &[MjtBool; "does light cast shadows"; ffi().nlight],
1621        light_bulbradius: &[f32; "light radius for soft shadows"; ffi().nlight],
1622        light_intensity: &[f32; "intensity, in candela"; ffi().nlight],
1623        light_range: &[f32; "range of effectiveness"; ffi().nlight],
1624        light_active: &[MjtBool; "is light on"; ffi().nlight],
1625        light_pos: &[[MjtNum; 3] [force]; "position rel. to body frame"; ffi().nlight],
1626        light_dir: &[[MjtNum; 3] [force]; "direction rel. to body frame"; ffi().nlight],
1627        light_poscom0: &[[MjtNum; 3] [force]; "global position rel. to sub-com in qpos0"; ffi().nlight],
1628        light_pos0: &[[MjtNum; 3] [force]; "global position rel. to body in qpos0"; ffi().nlight],
1629        light_dir0: &[[MjtNum; 3] [force]; "global direction in qpos0"; ffi().nlight],
1630        light_attenuation: &[[f32; 3] [force]; "OpenGL attenuation (quadratic model)"; ffi().nlight],
1631        light_cutoff: &[f32; "OpenGL cutoff"; ffi().nlight],
1632        light_softness: &[f32; "spotlight edge softness"; ffi().nlight],
1633        light_exponent: &[f32; "OpenGL exponent"; ffi().nlight],
1634        light_ambient: &[[f32; 3] [force]; "ambient rgb (alpha=1)"; ffi().nlight],
1635        light_diffuse: &[[f32; 3] [force]; "diffuse rgb (alpha=1)"; ffi().nlight],
1636        light_specular: &[[f32; 3] [force]; "specular rgb (alpha=1)"; ffi().nlight],
1637        flex_contype: &[i32; "flex contact type"; ffi().nflex],
1638        flex_conaffinity: &[i32; "flex contact affinity"; ffi().nflex],
1639        (mut = unsafe) flex_condim: &[i32; "contact dimensionality (1, 3, 4, 6)"; ffi().nflex],
1640        flex_priority: &[i32; "flex contact priority"; ffi().nflex],
1641        flex_solmix: &[MjtNum; "mix coef for solref/imp in contact pair"; ffi().nflex],
1642        flex_solref: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference: contact"; ffi().nflex],
1643        flex_solimp: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance: contact"; ffi().nflex],
1644        flex_friction: &[[MjtNum; 3] [force]; "friction for (slide, spin, roll)"; ffi().nflex],
1645        flex_margin: &[MjtNum; "geometric inflation for contact"; ffi().nflex],
1646        flex_gap: &[MjtNum; "additional contact detection buffer"; ffi().nflex],
1647        flex_internal: &[MjtBool; "internal flex collision enabled"; ffi().nflex],
1648        flex_selfcollide: &[MjtFlexSelf [force]; "self collision mode"; ffi().nflex],
1649        flex_activelayers: &[i32; "number of active element layers, 3D only"; ffi().nflex],
1650        flex_passive: &[i32; "passive collisions enabled"; ffi().nflex],
1651        (mut = unsafe) flex_dim: &[i32; "1: lines, 2: triangles, 3: tetrahedra"; ffi().nflex],
1652        (mut = unsafe) flex_matid: &[i32; "material id for rendering"; ffi().nflex],
1653        flex_group: &[i32; "group for visibility"; ffi().nflex],
1654        (mut = unsafe) flex_interp: &[i32; "interpolation (0: vertex, 1: nodes)"; ffi().nflex],
1655        (mut = unsafe) flex_cellnum: &[[i32; 3] [force]; "finite cell num per dimension"; ffi().nflex],
1656        (mut = unsafe) flex_nodeadr: &[i32; "first node address"; ffi().nflex],
1657        (mut = unsafe) flex_nodenum: &[i32; "number of nodes"; ffi().nflex],
1658        (mut = unsafe) flex_vertadr: &[i32; "first vertex address"; ffi().nflex],
1659        (mut = unsafe) flex_vertnum: &[i32; "number of vertices"; ffi().nflex],
1660        (mut = unsafe) flex_edgeadr: &[i32; "first edge address"; ffi().nflex],
1661        (mut = unsafe) flex_edgenum: &[i32; "number of edges"; ffi().nflex],
1662        (mut = unsafe) flex_elemadr: &[i32; "first element address"; ffi().nflex],
1663        (mut = unsafe) flex_elemnum: &[i32; "number of elements"; ffi().nflex],
1664        (mut = unsafe) flex_elemdataadr: &[i32; "first element vertex id address"; ffi().nflex],
1665        (mut = unsafe) flex_stiffnessadr: &[i32; "stiffness matrix address"; ffi().nflex],
1666        (mut = unsafe) flex_elemedgeadr: &[i32; "first element edge id address"; ffi().nflex],
1667        (mut = unsafe) flex_bendingadr: &[i32; "first bending data address"; ffi().nflex],
1668        (mut = unsafe) flex_shellnum: &[i32; "number of shells"; ffi().nflex],
1669        (mut = unsafe) flex_shelldataadr: &[i32; "first shell data address"; ffi().nflex],
1670        (mut = unsafe) flex_evpairadr: &[i32; "first evpair address"; ffi().nflex],
1671        (mut = unsafe) flex_evpairnum: &[i32; "number of evpairs"; ffi().nflex],
1672        (mut = unsafe) flex_texcoordadr: &[i32; "address in flex_texcoord; -1: none"; ffi().nflex],
1673        (mut = unsafe) flex_nodebodyid: &[i32; "node body ids"; ffi().nflexnode],
1674        (mut = unsafe) flex_vertbodyid: &[i32; "vertex body ids"; ffi().nflexvert],
1675        (mut = unsafe) flex_vertedgeadr: &[i32; "first edge address"; ffi().nflexvert],
1676        (mut = unsafe) flex_vertedgenum: &[i32; "number of edges"; ffi().nflexvert],
1677        (mut = unsafe) flex_vertedge: &[[i32; 2] [force]; "edge indices"; ffi().nflexedge],
1678        (mut = unsafe) flex_edge: &[[i32; 2] [force]; "edge vertex ids (2 per edge)"; ffi().nflexedge],
1679        (mut = unsafe) flex_edgeflap: &[[i32; 2] [force]; "adjacent vertex ids (dim=2 only)"; ffi().nflexedge],
1680        (mut = unsafe) flex_elem: &[i32; "element vertex ids (dim+1 per elem)"; ffi().nflexelemdata],
1681        (mut = unsafe) flex_elemtexcoord: &[i32; "element texture coordinates (dim+1)"; ffi().nflexelemdata],
1682        (mut = unsafe) flex_elemedge: &[i32; "element edge ids"; ffi().nflexelemedge],
1683        (mut = unsafe) flex_elemlayer: &[i32; "element distance from surface, 3D only"; ffi().nflexelem],
1684        (mut = unsafe) flex_shell: &[i32; "shell fragment vertex ids (dim per frag)"; ffi().nflexshelldata],
1685        (mut = unsafe) flex_evpair: &[[i32; 2] [force]; "(element, vertex) collision pairs"; ffi().nflexevpair],
1686        flex_vert: &[[MjtNum; 3] [force]; "vertex positions in local body frames"; ffi().nflexvert],
1687        flex_vert0: &[[MjtNum; 3] [force]; "vertex positions in qpos0 on [0, 1]^d"; ffi().nflexvert],
1688        flex_vertmetric: &[[MjtNum; 4] [force]; "inverse of reference shape matrix"; ffi().nflexvert],
1689        flex_node: &[[MjtNum; 3] [force]; "node positions in local body frames"; ffi().nflexnode],
1690        flex_node0: &[[MjtNum; 3] [force]; "Cartesian node positions in qpos0"; ffi().nflexnode],
1691        flexedge_length0: &[MjtNum; "edge lengths in qpos0"; ffi().nflexedge],
1692        flexedge_invweight0: &[MjtNum; "edge inv. weight in qpos0"; ffi().nflexedge],
1693        flex_radius: &[MjtNum; "radius around primitive element"; ffi().nflex],
1694        flex_size: &[[MjtNum; 3] [force]; "vertex bounding box half sizes in qpos0"; ffi().nflex],
1695        flex_stiffness: &[MjtNum; "finite element stiffness matrix"; ffi().nflexstiffness],
1696        flex_bending: &[MjtNum; "bending stiffness"; ffi().nflexbending],
1697        (mut = unsafe) efm0_dofid: &[i32; "constant metric factor row->dof address"; ffi().nefm0dof],
1698        (mut = unsafe) efm0_L_rownnz: &[i32; "constant metric factor row nonzeros"; ffi().nefm0dof],
1699        (mut = unsafe) efm0_L_rowadr: &[i32; "constant metric factor row addresses"; ffi().nefm0dof],
1700        (mut = unsafe) efm0_L_colind: &[i32; "constant metric factor column indices"; ffi().nefm0L],
1701        efm0_L: &[MjtNum; "factor of M + (dt^2+dt*d)*K_bend"; ffi().nefm0L],
1702        flex_damping: &[MjtNum; "Rayleigh's damping coefficient"; ffi().nflex],
1703        flex_edgestiffness: &[MjtNum; "edge stiffness"; ffi().nflex],
1704        flex_edgedamping: &[MjtNum; "edge damping"; ffi().nflex],
1705        flex_edgeequality: &[i32; "0: none, 1: edges, 2: vertices, 3: strain"; ffi().nflex],
1706        flex_rigid: &[MjtBool; "are all vertices in the same body"; ffi().nflex],
1707        flexedge_rigid: &[MjtBool; "are both edge vertices in same body"; ffi().nflexedge],
1708        flex_centered: &[MjtBool; "are all vertex coordinates (0,0,0)"; ffi().nflex],
1709        flex_flatskin: &[MjtBool; "render flex skin with flat shading"; ffi().nflex],
1710        (mut = unsafe) flex_bvhadr: &[i32; "address of bvh root; -1: no bvh"; ffi().nflex],
1711        (mut = unsafe) flex_bvhnum: &[i32; "number of bounding volumes"; ffi().nflex],
1712        (mut = unsafe) flexedge_J_rownnz: &[i32; "number of non-zeros in Jacobian row"; ffi().nflexedge],
1713        (mut = unsafe) flexedge_J_rowadr: &[i32; "row start address in colind array"; ffi().nflexedge],
1714        (mut = unsafe) flexedge_J_colind: &[i32; "column indices in sparse Jacobian"; ffi().nJfe],
1715        (mut = unsafe) flexvert_J_rownnz: &[[i32; 2] [force]; "number of non-zeros in Jacobian row"; ffi().nflexvert],
1716        (mut = unsafe) flexvert_J_rowadr: &[[i32; 2] [force]; "row start address in colind array"; ffi().nflexvert],
1717        (mut = unsafe) flexvert_J_colind: &[[i32; 2] [force]; "column indices in sparse Jacobian"; ffi().nJfv],
1718        flex_rgba: &[[f32; 4] [force]; "rgba when material is omitted"; ffi().nflex],
1719        flex_texcoord: &[[f32; 2] [force]; "vertex texture coordinates"; ffi().nflextexcoord],
1720        (mut = unsafe) mesh_vertadr: &[i32; "first vertex address"; ffi().nmesh],
1721        (mut = unsafe) mesh_vertnum: &[i32; "number of vertices"; ffi().nmesh],
1722        (mut = unsafe) mesh_faceadr: &[i32; "first face address"; ffi().nmesh],
1723        (mut = unsafe) mesh_facenum: &[i32; "number of faces"; ffi().nmesh],
1724        (mut = unsafe) mesh_bvhadr: &[i32; "address of bvh root"; ffi().nmesh],
1725        (mut = unsafe) mesh_bvhnum: &[i32; "number of bvh"; ffi().nmesh],
1726        (mut = unsafe) mesh_octadr: &[i32; "address of octree root"; ffi().nmesh],
1727        (mut = unsafe) mesh_octnum: &[i32; "number of octree nodes"; ffi().nmesh],
1728        (mut = unsafe) mesh_normaladr: &[i32; "first normal address"; ffi().nmesh],
1729        (mut = unsafe) mesh_normalnum: &[i32; "number of normals"; ffi().nmesh],
1730        (mut = unsafe) mesh_texcoordadr: &[i32; "texcoord data address; -1: no texcoord"; ffi().nmesh],
1731        (mut = unsafe) mesh_texcoordnum: &[i32; "number of texcoord"; ffi().nmesh],
1732        (mut = unsafe) mesh_graphadr: &[i32; "graph data address; -1: no graph"; ffi().nmesh],
1733        (mut = unsafe) mesh_extrema: &[[i32; 27] [force]; "extremum vertices in 3x3x3 directions"; ffi().nmesh],
1734        mesh_vert: &[[f32; 3] [force]; "vertex positions for all meshes"; ffi().nmeshvert],
1735        mesh_normal: &[[f32; 3] [force]; "normals for all meshes"; ffi().nmeshnormal],
1736        mesh_texcoord: &[[f32; 2] [force]; "vertex texcoords for all meshes"; ffi().nmeshtexcoord],
1737        (mut = unsafe) mesh_face: &[[i32; 3] [force]; "vertex face data"; ffi().nmeshface],
1738        (mut = unsafe) mesh_facenormal: &[[i32; 3] [force]; "normal face data"; ffi().nmeshface],
1739        (mut = unsafe) mesh_facetexcoord: &[[i32; 3] [force]; "texture face data"; ffi().nmeshface],
1740        (mut = unsafe) mesh_graph: &[i32; "convex graph data"; ffi().nmeshgraph],
1741        mesh_scale: &[[MjtNum; 3] [force]; "scaling applied to asset vertices"; ffi().nmesh],
1742        mesh_pos: &[[MjtNum; 3] [force]; "translation applied to asset vertices"; ffi().nmesh],
1743        mesh_quat: &[[MjtNum; 4] [force]; "rotation applied to asset vertices"; ffi().nmesh],
1744        (mut = unsafe) mesh_pathadr: &[i32; "address of asset path for mesh; -1: none"; ffi().nmesh],
1745        (mut = unsafe) mesh_polynum: &[i32; "number of polygons per mesh"; ffi().nmesh],
1746        (mut = unsafe) mesh_polyadr: &[i32; "first polygon address per mesh"; ffi().nmesh],
1747        mesh_polynormal: &[[MjtNum; 3] [force]; "all polygon normals"; ffi().nmeshpoly],
1748        (mut = unsafe) mesh_polyvertadr: &[i32; "polygon vertex start address"; ffi().nmeshpoly],
1749        (mut = unsafe) mesh_polyvertnum: &[i32; "number of vertices per polygon"; ffi().nmeshpoly],
1750        (mut = unsafe) mesh_polyvert: &[i32; "all polygon vertices"; ffi().nmeshpolyvert],
1751        (mut = unsafe) mesh_polymapadr: &[i32; "first polygon address per vertex"; ffi().nmeshvert],
1752        (mut = unsafe) mesh_polymapnum: &[i32; "number of polygons per vertex"; ffi().nmeshvert],
1753        (mut = unsafe) mesh_polymap: &[i32; "vertex to polygon map"; ffi().nmeshpolymap],
1754        (mut = unsafe) skin_matid: &[i32; "skin material id; -1: none"; ffi().nskin],
1755        skin_group: &[i32; "group for visibility"; ffi().nskin],
1756        skin_rgba: &[[f32; 4] [force]; "skin rgba"; ffi().nskin],
1757        skin_inflate: &[f32; "inflate skin in normal direction"; ffi().nskin],
1758        (mut = unsafe) skin_vertadr: &[i32; "first vertex address"; ffi().nskin],
1759        (mut = unsafe) skin_vertnum: &[i32; "number of vertices"; ffi().nskin],
1760        (mut = unsafe) skin_texcoordadr: &[i32; "texcoord data address; -1: no texcoord"; ffi().nskin],
1761        (mut = unsafe) skin_faceadr: &[i32; "first face address"; ffi().nskin],
1762        (mut = unsafe) skin_facenum: &[i32; "number of faces"; ffi().nskin],
1763        (mut = unsafe) skin_boneadr: &[i32; "first bone in skin"; ffi().nskin],
1764        (mut = unsafe) skin_bonenum: &[i32; "number of bones in skin"; ffi().nskin],
1765        skin_vert: &[[f32; 3] [force]; "vertex positions for all skin meshes"; ffi().nskinvert],
1766        skin_texcoord: &[[f32; 2] [force]; "vertex texcoords for all skin meshes"; ffi().nskintexvert],
1767        (mut = unsafe) skin_face: &[[i32; 3] [force]; "triangle faces for all skin meshes"; ffi().nskinface],
1768        (mut = unsafe) skin_bonevertadr: &[i32; "first vertex in each bone"; ffi().nskinbone],
1769        (mut = unsafe) skin_bonevertnum: &[i32; "number of vertices in each bone"; ffi().nskinbone],
1770        skin_bonebindpos: &[[f32; 3] [force]; "bind pos of each bone"; ffi().nskinbone],
1771        skin_bonebindquat: &[[f32; 4] [force]; "bind quat of each bone"; ffi().nskinbone],
1772        (mut = unsafe) skin_bonebodyid: &[i32; "body id of each bone"; ffi().nskinbone],
1773        (mut = unsafe) skin_bonevertid: &[i32; "mesh ids of vertices in each bone"; ffi().nskinbonevert],
1774        skin_bonevertweight: &[f32; "weights of vertices in each bone"; ffi().nskinbonevert],
1775        (mut = unsafe) skin_pathadr: &[i32; "address of asset path for skin; -1: none"; ffi().nskin],
1776        hfield_size: &[[MjtNum; 4] [force]; "(x, y, z_top, z_bottom)"; ffi().nhfield],
1777        (mut = unsafe) hfield_nrow: &[i32; "number of rows in grid"; ffi().nhfield],
1778        (mut = unsafe) hfield_ncol: &[i32; "number of columns in grid"; ffi().nhfield],
1779        (mut = unsafe) hfield_adr: &[i32; "address in hfield_data"; ffi().nhfield],
1780        hfield_data: &[f32; "elevation data"; ffi().nhfielddata],
1781        (mut = unsafe) hfield_pathadr: &[i32; "address of hfield asset path; -1: none"; ffi().nhfield],
1782        (mut = unsafe) tex_type: &[MjtTexture [force]; "texture type"; ffi().ntex],
1783        tex_colorspace: &[MjtColorSpace [force]; "texture colorspace"; ffi().ntex],
1784        (mut = unsafe) tex_height: &[i32; "number of rows in texture image"; ffi().ntex],
1785        (mut = unsafe) tex_width: &[i32; "number of columns in texture image"; ffi().ntex],
1786        (mut = unsafe) tex_nchannel: &[i32; "number of channels in texture image"; ffi().ntex],
1787        (mut = unsafe) tex_adr: &[MjtSize; "start address in tex_data"; ffi().ntex],
1788        tex_data: &[MjtByte; "pixel values"; ffi().ntexdata],
1789        (mut = unsafe) tex_pathadr: &[i32; "address of texture asset path; -1: none"; ffi().ntex],
1790        (mut = unsafe) mat_texid: &[[i32; MjtTextureRole::mjNTEXROLE as usize] [force]; "indices of textures; -1: none"; ffi().nmat],
1791        mat_texuniform: &[MjtBool; "make texture cube uniform"; ffi().nmat],
1792        mat_texrepeat: &[[f32; 2] [force]; "texture repetition for 2d mapping"; ffi().nmat],
1793        mat_emission: &[f32; "emission (x rgb)"; ffi().nmat],
1794        mat_specular: &[f32; "specular (x white)"; ffi().nmat],
1795        mat_shininess: &[f32; "shininess coef"; ffi().nmat],
1796        mat_reflectance: &[f32; "reflectance (0: disable)"; ffi().nmat],
1797        mat_metallic: &[f32; "metallic coef"; ffi().nmat],
1798        mat_roughness: &[f32; "roughness coef"; ffi().nmat],
1799        mat_rgba: &[[f32; 4] [force]; "rgba"; ffi().nmat],
1800        (mut = unsafe) pair_dim: &[i32; "contact dimensionality"; ffi().npair],
1801        (mut = unsafe) pair_geom1: &[i32; "id of geom1"; ffi().npair],
1802        (mut = unsafe) pair_geom2: &[i32; "id of geom2"; ffi().npair],
1803        pair_signature: &[i32; "body1 << 16 + body2"; ffi().npair],
1804        pair_solref: &[[MjtNum; mjNREF as usize] [force]; "solver reference: contact normal"; ffi().npair],
1805        pair_solreffriction: &[[MjtNum; mjNREF as usize] [force]; "solver reference: contact friction"; ffi().npair],
1806        pair_solimp: &[[MjtNum; mjNIMP as usize] [force]; "solver impedance: contact"; ffi().npair],
1807        pair_margin: &[MjtNum; "geometric inflation for contact"; ffi().npair],
1808        pair_gap: &[MjtNum; "additional contact detection buffer"; ffi().npair],
1809        pair_adhesion: &[MjtNum; "adhesive force of contacts"; ffi().npair],
1810        pair_friction: &[[MjtNum; 5] [force]; "tangent1, 2, spin, roll1, 2"; ffi().npair],
1811        exclude_signature: &[i32; "body1 << 16 + body2"; ffi().nexclude],
1812        (mut = unsafe) eq_type: &[MjtEq [force]; "constraint type"; ffi().neq],
1813        (mut = unsafe) eq_obj1id: &[i32; "id of object 1"; ffi().neq],
1814        (mut = unsafe) eq_obj2id: &[i32; "id of object 2"; ffi().neq],
1815        (mut = unsafe) eq_objtype: &[MjtObj [force]; "type of both objects"; ffi().neq],
1816        eq_active0: &[MjtBool; "initial enable/disable constraint state"; ffi().neq],
1817        eq_solref: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference"; ffi().neq],
1818        eq_solimp: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance"; ffi().neq],
1819        eq_data: &[[MjtNum; mjNEQDATA as usize] [force]; "numeric data for constraint"; ffi().neq],
1820        (mut = unsafe) tendon_adr: &[i32; "address of first object in tendon's path"; ffi().ntendon],
1821        (mut = unsafe) tendon_num: &[i32; "number of objects in tendon's path"; ffi().ntendon],
1822        (mut = unsafe) tendon_matid: &[i32; "material id for rendering"; ffi().ntendon],
1823        (mut = unsafe) tendon_actuatorid: &[i32; "actuator contributing damping / armature"; ffi().ntendon],
1824        tendon_group: &[i32; "group for visibility"; ffi().ntendon],
1825        tendon_treenum: &[i32; "number of trees along tendon's path"; ffi().ntendon],
1826        (mut = unsafe) tendon_treeid: &[[i32; 2] [force]; "first two trees along tendon's path"; ffi().ntendon],
1827        (mut = unsafe) ten_J_rownnz: &[i32; "number of non-zeros in Jacobian row"; ffi().ntendon],
1828        (mut = unsafe) ten_J_rowadr: &[i32; "row start address in colind array"; ffi().ntendon],
1829        (mut = unsafe) ten_J_colind: &[i32; "column indices in sparse Jacobian"; ffi().nJten],
1830        tendon_limited: &[MjtBool; "does tendon have length limits"; ffi().ntendon],
1831        tendon_actfrclimited: &[MjtBool; "does tendon have actuator force limits"; ffi().ntendon],
1832        tendon_width: &[MjtNum; "width for rendering"; ffi().ntendon],
1833        tendon_solref_lim: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference: limit"; ffi().ntendon],
1834        tendon_solimp_lim: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance: limit"; ffi().ntendon],
1835        tendon_solref_fri: &[[MjtNum; mjNREF as usize] [force]; "constraint solver reference: friction"; ffi().ntendon],
1836        tendon_solimp_fri: &[[MjtNum; mjNIMP as usize] [force]; "constraint solver impedance: friction"; ffi().ntendon],
1837        tendon_range: &[[MjtNum; 2] [force]; "tendon length limits"; ffi().ntendon],
1838        tendon_actfrcrange: &[[MjtNum; 2] [force]; "range of total actuator force"; ffi().ntendon],
1839        tendon_margin: &[MjtNum; "min distance for limit detection"; ffi().ntendon],
1840        tendon_stiffness: &[MjtNum; "linear stiffness coefficient"; ffi().ntendon],
1841        tendon_stiffnesspoly: &[[MjtNum; mjNPOLY as usize] [force]; "high-order stiffness coefficients"; ffi().ntendon],
1842        tendon_damping: &[MjtNum; "linear damping coefficient"; ffi().ntendon],
1843        tendon_dampingpoly: &[[MjtNum; mjNPOLY as usize] [force]; "high-order damping coefficients"; ffi().ntendon],
1844        tendon_armature: &[MjtNum; "inertia associated with tendon velocity"; ffi().ntendon],
1845        tendon_frictionloss: &[MjtNum; "loss due to friction"; ffi().ntendon],
1846        tendon_lengthspring: &[[MjtNum; 2] [force]; "spring resting length range"; ffi().ntendon],
1847        tendon_length0: &[MjtNum; "tendon length in qpos0"; ffi().ntendon],
1848        tendon_invweight0: &[MjtNum; "inv. weight in qpos0"; ffi().ntendon],
1849        tendon_rgba: &[[f32; 4] [force]; "rgba when material is omitted"; ffi().ntendon],
1850        (mut = unsafe) wrap_type: &[MjtWrap [force]; "wrap object type"; ffi().nwrap],
1851        (mut = unsafe) wrap_objid: &[i32; "object id: geom, site, joint"; ffi().nwrap],
1852        (mut = unsafe) wrap_prm: &[MjtNum; "divisor, joint coef, or site id"; ffi().nwrap],
1853        (mut = unsafe) actuator_trntype: &[MjtTrn [force]; "transmission type"; ffi().nactuator],
1854        (mut = unsafe) actuator_dyntype: &[MjtDyn [force]; "dynamics type"; ffi().nactuator],
1855        (mut = unsafe) actuator_gaintype: &[MjtGain [force]; "gain type"; ffi().nactuator],
1856        actuator_biastype: &[MjtBias [force]; "bias type"; ffi().nactuator],
1857        (mut = unsafe) actuator_ctrladr: &[i32; "address of first control"; ffi().nactuator],
1858        (mut = unsafe) actuator_ctrlnum: &[i32; "number of controls"; ffi().nactuator],
1859        (mut = unsafe) actuator_ctrlspec: &[i32; "input signature, scoped by gaintype"; ffi().nactuator],
1860        (mut = unsafe) actuator_outadr: &[i32; "address of first force output"; ffi().nactuator],
1861        (mut = unsafe) actuator_outnum: &[i32; "number of force outputs, from trntype"; ffi().nactuator],
1862        (mut = unsafe) actuator_trnid: &[[i32; 2] [force]; "transmission id: joint, tendon, site"; ffi().nactuator],
1863        (mut = unsafe) actuator_actadr: &[i32; "first activation address; -1: stateless"; ffi().nactuator],
1864        (mut = unsafe) actuator_actnum: &[i32; "number of activation variables"; ffi().nactuator],
1865        actuator_group: &[i32; "group for visibility"; ffi().nactuator],
1866        (mut = unsafe) actuator_history: &[[i32; 2] [force]; "history buffer: [nsample, interp]"; ffi().nactuator],
1867        (mut = unsafe) actuator_historyadr: &[i32; "address in history buffer; -1: none"; ffi().nactuator],
1868        actuator_delay: &[MjtNum; "delay time in seconds; 0: no delay"; ffi().nactuator],
1869        actuator_ctrllimited: &[MjtBool; "is control limited"; ffi().nu],
1870        actuator_forcelimited: &[MjtBool; "is force limited"; ffi().nactuator],
1871        actuator_actlimited: &[MjtBool; "is activation limited"; ffi().nactuator],
1872        actuator_dynprm: &[[MjtNum; mjNDYN as usize] [force]; "dynamics parameters"; ffi().nactuator],
1873        actuator_gainprm: &[[MjtNum; mjNGAIN as usize] [force]; "gain parameters"; ffi().nactuator],
1874        actuator_biasprm: &[[MjtNum; mjNBIAS as usize] [force]; "bias parameters"; ffi().nactuator],
1875        actuator_actearly: &[MjtBool; "step activation before force"; ffi().nactuator],
1876        actuator_ctrlrange: &[[MjtNum; 2] [force]; "range of controls"; ffi().nu],
1877        actuator_forcerange: &[[MjtNum; 2] [force]; "range of forces"; ffi().nactuator],
1878        actuator_actrange: &[[MjtNum; 2] [force]; "range of activations"; ffi().nactuator],
1879        actuator_damping: &[MjtNum; "linear damping coefficient"; ffi().nactuator],
1880        actuator_dampingpoly: &[[MjtNum; mjNPOLY as usize] [force]; "high-order damping coefficients"; ffi().nactuator],
1881        actuator_armature: &[MjtNum; "armature added to target (joint, tendon)"; ffi().nactuator],
1882        actuator_gear: &[[MjtNum; 6] [force]; "scale length and transmitted force"; ffi().nout],
1883        actuator_cranklength: &[MjtNum; "crank length for slider-crank"; ffi().nactuator],
1884        actuator_acc0: &[MjtNum; "acceleration from unit force in qpos0"; ffi().nout],
1885        actuator_length0: &[MjtNum; "actuator length in qpos0"; ffi().nout],
1886        actuator_lengthrange: &[[MjtNum; 2] [force]; "feasible actuator length range"; ffi().nout],
1887        (mut = unsafe) actuator_plugin: &[i32; "plugin instance id; -1: not a plugin"; ffi().nactuator],
1888        (mut = unsafe) sensor_type: &[MjtSensor [force]; "sensor type"; ffi().nsensor],
1889        sensor_datatype: &[MjtDataType [force]; "numeric data type"; ffi().nsensor],
1890        sensor_needstage: &[MjtStage [force]; "required compute stage"; ffi().nsensor],
1891        (mut = unsafe) sensor_objtype: &[MjtObj [force]; "type of sensorized object"; ffi().nsensor],
1892        (mut = unsafe) sensor_objid: &[i32; "id of sensorized object"; ffi().nsensor],
1893        (mut = unsafe) sensor_reftype: &[MjtObj [force]; "type of reference frame"; ffi().nsensor],
1894        (mut = unsafe) sensor_refid: &[i32; "id of reference frame; -1: global frame"; ffi().nsensor],
1895        (mut = unsafe) sensor_intprm: &[[i32; mjNSENS as usize] [force]; "sensor parameters"; ffi().nsensor],
1896        (mut = unsafe) sensor_dim: &[i32; "number of scalar outputs"; ffi().nsensor],
1897        (mut = unsafe) sensor_adr: &[i32; "address in sensor array"; ffi().nsensor],
1898        sensor_cutoff: &[MjtNum; "cutoff for real and positive; 0: ignore"; ffi().nsensor],
1899        sensor_noise: &[MjtNum; "noise standard deviation"; ffi().nsensor],
1900        (mut = unsafe) sensor_history: &[[i32; 2] [force]; "history buffer: [nsample, interp]"; ffi().nsensor],
1901        (mut = unsafe) sensor_historyadr: &[i32; "address in history buffer; -1: none"; ffi().nsensor],
1902        sensor_delay: &[MjtNum; "delay time in seconds; 0: no delay"; ffi().nsensor],
1903        sensor_interval: &[[MjtNum; 2] [force]; "interval: [period, phase] in seconds"; ffi().nsensor],
1904        (mut = unsafe) sensor_plugin: &[i32; "plugin instance id; -1: not a plugin"; ffi().nsensor],
1905        (mut = unsafe) plugin: &[i32; "globally registered plugin slot number"; ffi().nplugin],
1906        (mut = unsafe) plugin_stateadr: &[i32; "address in the plugin state array"; ffi().nplugin],
1907        (mut = unsafe) plugin_statenum: &[i32; "number of states in the plugin instance"; ffi().nplugin],
1908        (mut = unsafe) plugin_attr: &[c_char; "config attributes of plugin instances"; ffi().npluginattr],
1909        (mut = unsafe) plugin_attradr: &[i32; "address to each instance's config attrib"; ffi().nplugin],
1910        (mut = unsafe) numeric_adr: &[i32; "address of field in numeric_data"; ffi().nnumeric],
1911        (mut = unsafe) numeric_size: &[i32; "size of numeric field"; ffi().nnumeric],
1912        numeric_data: &[MjtNum; "array of all numeric fields"; ffi().nnumericdata],
1913        (mut = unsafe) text_adr: &[i32; "address of text in text_data"; ffi().ntext],
1914        (mut = unsafe) text_size: &[i32; "size of text field (strlen+1)"; ffi().ntext],
1915        (mut = unsafe) text_data: &[c_char; "array of all text fields (0-terminated)"; ffi().ntextdata],
1916        (mut = unsafe) tuple_adr: &[i32; "address of tuple in tuple_objtype/objid/objprm"; ffi().ntuple],
1917        (mut = unsafe) tuple_size: &[i32; "number of objects in tuple"; ffi().ntuple],
1918        tuple_objtype: &[MjtObj [force]; "array of object types in all tuples"; ffi().ntupledata],
1919        (mut = unsafe) tuple_objid: &[i32; "array of object ids in all tuples"; ffi().ntupledata],
1920        tuple_objprm: &[MjtNum; "array of object params in all tuples"; ffi().ntupledata],
1921        key_time: &[MjtNum; "key time"; ffi().nkey],
1922        (mut = unsafe) name_bodyadr: &[i32; "body name pointers"; ffi().nbody],
1923        (mut = unsafe) name_jntadr: &[i32; "joint name pointers"; ffi().njnt],
1924        (mut = unsafe) name_geomadr: &[i32; "geom name pointers"; ffi().ngeom],
1925        (mut = unsafe) name_siteadr: &[i32; "site name pointers"; ffi().nsite],
1926        (mut = unsafe) name_camadr: &[i32; "camera name pointers"; ffi().ncam],
1927        (mut = unsafe) name_lightadr: &[i32; "light name pointers"; ffi().nlight],
1928        (mut = unsafe) name_flexadr: &[i32; "flex name pointers"; ffi().nflex],
1929        (mut = unsafe) name_meshadr: &[i32; "mesh name pointers"; ffi().nmesh],
1930        (mut = unsafe) name_skinadr: &[i32; "skin name pointers"; ffi().nskin],
1931        (mut = unsafe) name_hfieldadr: &[i32; "hfield name pointers"; ffi().nhfield],
1932        (mut = unsafe) name_texadr: &[i32; "texture name pointers"; ffi().ntex],
1933        (mut = unsafe) name_matadr: &[i32; "material name pointers"; ffi().nmat],
1934        (mut = unsafe) name_pairadr: &[i32; "geom pair name pointers"; ffi().npair],
1935        (mut = unsafe) name_excludeadr: &[i32; "exclude name pointers"; ffi().nexclude],
1936        (mut = unsafe) name_eqadr: &[i32; "equality constraint name pointers"; ffi().neq],
1937        (mut = unsafe) name_tendonadr: &[i32; "tendon name pointers"; ffi().ntendon],
1938        (mut = unsafe) name_actuatoradr: &[i32; "actuator name pointers"; ffi().nactuator],
1939        (mut = unsafe) name_sensoradr: &[i32; "sensor name pointers"; ffi().nsensor],
1940        (mut = unsafe) name_numericadr: &[i32; "numeric name pointers"; ffi().nnumeric],
1941        (mut = unsafe) name_textadr: &[i32; "text name pointers"; ffi().ntext],
1942        (mut = unsafe) name_tupleadr: &[i32; "tuple name pointers"; ffi().ntuple],
1943        (mut = unsafe) name_keyadr: &[i32; "keyframe name pointers"; ffi().nkey],
1944        (mut = unsafe) name_pluginadr: &[i32; "plugin instance name pointers"; ffi().nplugin],
1945        (mut = unsafe) names: &[c_char; "names of all objects, 0-terminated"; ffi().nnames],
1946        (mut = unsafe) names_map: &[i32; "internal hash map of names"; ffi().nnames_map],
1947        paths: &[c_char; "paths to assets, 0-terminated"; ffi().npaths],
1948        (mut = unsafe) B_rownnz: &[i32; "body-dof: non-zeros in each row"; ffi().nbody],
1949        (mut = unsafe) B_rowadr: &[i32; "body-dof: row addresses"; ffi().nbody],
1950        (mut = unsafe) B_colind: &[i32; "body-dof: column indices"; ffi().nB],
1951        (mut = unsafe) M_rownnz: &[i32; "reduced inertia: non-zeros in each row"; ffi().nv],
1952        (mut = unsafe) M_rowadr: &[i32; "reduced inertia: row addresses"; ffi().nv],
1953        (mut = unsafe) M_colind: &[i32; "reduced inertia: column indices"; ffi().nC],
1954        (mut = unsafe) mapM2M: &[i32; "index mapping from qM to M"; ffi().nC],
1955        (mut = unsafe) D_rownnz: &[i32; "full inertia: non-zeros in each row"; ffi().nv],
1956        (mut = unsafe) D_rowadr: &[i32; "full inertia: row addresses"; ffi().nv],
1957        (mut = unsafe) D_diag: &[i32; "full inertia: index of diagonal element"; ffi().nv],
1958        (mut = unsafe) D_colind: &[i32; "full inertia: column indices"; ffi().nD],
1959        (mut = unsafe) mapM2D: &[i32; "index mapping from M to D"; ffi().nD],
1960        (mut = unsafe) mapD2M: &[i32; "index mapping from D to M"; ffi().nC]
1961    }
1962
1963    array_slice_dyn! {
1964        sublen_dep {
1965            key_qpos: &[[MjtNum; ffi().nq] [force]; "key position"; ffi().nkey],
1966            key_qvel: &[[MjtNum; ffi().nv] [force]; "key velocity"; ffi().nkey],
1967            key_act: &[[MjtNum; ffi().na] [force]; "key activation"; ffi().nkey],
1968            key_mpos: &[[MjtNum; ffi().nmocap * 3] [force]; "key mocap position"; ffi().nkey],
1969            key_mquat: &[[MjtNum; ffi().nmocap * 4] [force]; "key mocap quaternion"; ffi().nkey],
1970            key_ctrl: &[[MjtNum; ffi().nu] [force]; "key control"; ffi().nkey],
1971
1972            sensor_user: &[[MjtNum; ffi().nuser_sensor] [force]; "user data"; ffi().nsensor],
1973            actuator_user: &[[MjtNum; ffi().nuser_actuator] [force]; "user data"; ffi().nactuator],
1974            tendon_user: &[[MjtNum; ffi().nuser_tendon] [force]; "user data"; ffi().ntendon],
1975            cam_user: &[[MjtNum; ffi().nuser_cam] [force]; "user data"; ffi().ncam],
1976            site_user: &[[MjtNum; ffi().nuser_site] [force]; "user data"; ffi().nsite],
1977            geom_user: &[[MjtNum; ffi().nuser_geom] [force]; "user data"; ffi().ngeom],
1978            jnt_user: &[[MjtNum; ffi().nuser_jnt] [force]; "user data"; ffi().njnt],
1979            body_user: &[[MjtNum; ffi().nuser_body] [force]; "user data"; ffi().nbody]
1980        }
1981    }
1982}
1983
1984impl Clone for MjModel {
1985    /// # Note
1986    /// MuJoCo aborts the process through `mjERROR` when an allocation fails, so this never fails.
1987    #[expect(deprecated, reason = "try_clone keeps the implementation until it is removed")]
1988    fn clone(&self) -> Self {
1989        self.try_clone().expect("failed to clone model")
1990    }
1991}
1992
1993impl Drop for MjModel {
1994    fn drop(&mut self) {
1995        // SAFETY: self.ptr is a valid non-null mjModel pointer; called exactly once in Drop.
1996        unsafe {
1997            mj_deleteModel(self.ptr.as_ptr());
1998        }
1999    }
2000}
2001
2002info_with_view!(Model, actuator,
2003	[[actuator_] group: i32,
2004	 [actuator_] delay: MjtNum, [actuator_] ctrllimited: MjtBool,
2005	 [actuator_] forcelimited: MjtBool, [actuator_] actlimited: MjtBool,
2006	 [actuator_] dynprm: MjtNum, [actuator_] gainprm: MjtNum,
2007	 [actuator_] biasprm: MjtNum, [actuator_] actearly: MjtBool,
2008	 [actuator_] ctrlrange: MjtNum, [actuator_] forcerange: MjtNum,
2009	 [actuator_] actrange: MjtNum, [actuator_] gear: MjtNum,
2010	 [actuator_] damping: MjtNum, [actuator_] dampingpoly: MjtNum,
2011	 [actuator_] armature: MjtNum,
2012	 [actuator_] cranklength: MjtNum, [actuator_] acc0: MjtNum,
2013	 [actuator_] length0: MjtNum, [actuator_] lengthrange: MjtNum,
2014	 [actuator_] user: MjtNum, [actuator_] biastype: MjtBias [force],
2015	 [actuator_] plugin: i32],
2016	[[actuator_] trntype: MjtTrn [force], [actuator_] dyntype: MjtDyn [force],
2017	 [actuator_] ctrladr: i32, [actuator_] ctrlnum: i32, [actuator_] ctrlspec: i32,
2018	 [actuator_] gaintype: MjtGain [force],
2019	 [actuator_] outadr: i32, [actuator_] outnum: i32,
2020	 [actuator_] trnid: i32, [actuator_] actadr: i32,
2021	 [actuator_] actnum: i32, [actuator_] history: i32,
2022	 [actuator_] historyadr: i32],
2023	[]);
2024
2025info_with_view!(Model, body,
2026	[[body_] sameframe: MjtSameFrame [force],
2027	 [body_] pos: MjtNum,
2028	 [body_] quat: MjtNum, [body_] ipos: MjtNum,
2029	 [body_] iquat: MjtNum, [body_] mass: MjtNum,
2030	 [body_] subtreemass: MjtNum, [body_] inertia: MjtNum,
2031	 [body_] invweight0: MjtNum, [body_] gravcomp: MjtNum,
2032	 [body_] margin: MjtNum,
2033	 [body_] contype: i32, [body_] conaffinity: i32,
2034	 [body_] user: MjtNum,
2035	 [body_] simple: MjtByte, [body_] plugin: i32],
2036	[[body_] parentid: i32, [body_] rootid: i32,
2037	 [body_] weldid: i32, [body_] mocapid: i32,
2038	 [body_] jntnum: i32, [body_] jntadr: i32,
2039	 [body_] dofnum: i32, [body_] dofadr: i32,
2040	 [body_] treeid: i32, [body_] geomnum: i32,
2041	 [body_] geomadr: i32,
2042	 [body_] bvhadr: i32, [body_] bvhnum: i32],
2043	[]);
2044
2045info_with_view!(Model, camera,
2046	[[cam_] mode: MjtCamLight [force],
2047	 [cam_] pos: MjtNum,
2048	 [cam_] quat: MjtNum,
2049	 [cam_] poscom0: MjtNum,
2050	 [cam_] pos0: MjtNum,
2051	 [cam_] mat0: MjtNum,
2052	 [cam_] projection: MjtProjection [force],
2053	 [cam_] fovy: MjtNum,
2054	 [cam_] ipd: MjtNum,
2055     [cam_] output: i32,
2056	 [cam_] sensorsize: f32,
2057	 [cam_] intrinsic: f32,
2058	 [cam_] user: MjtNum],
2059	[[cam_] bodyid: i32,
2060	 [cam_] targetbodyid: i32,
2061	 [cam_] resolution: i32],
2062	[]);
2063
2064info_with_view!(Model, equality,
2065	[[eq_] active0: MjtBool,
2066	 [eq_] solref: MjtNum,
2067	 [eq_] solimp: MjtNum,
2068	 [eq_] data: MjtNum],
2069	[[eq_] r#type: MjtEq [force],
2070	 [eq_] obj1id: i32,
2071	 [eq_] obj2id: i32,
2072     [eq_] objtype: MjtObj [force]],
2073	[]);
2074
2075info_with_view!(Model, exclude,
2076	[[exclude_] signature: i32],
2077	[],
2078	[]);
2079
2080info_with_view!(Model, geom,
2081	[[geom_] contype: i32,
2082	 [geom_] conaffinity: i32,
2083	 [geom_] group: i32,
2084	 [geom_] priority: i32, [geom_] sameframe: MjtSameFrame [force],
2085	 [geom_] solmix: MjtNum, [geom_] solref: MjtNum,
2086	 [geom_] solimp: MjtNum, [geom_] size: MjtNum,
2087	 [geom_] aabb: MjtNum,
2088	 [geom_] rbound: MjtNum, [geom_] pos: MjtNum,
2089	 [geom_] quat: MjtNum, [geom_] friction: MjtNum,
2090	 [geom_] margin: MjtNum, [geom_] gap: MjtNum,
2091	 [geom_] surfacevel: MjtNum, [geom_] adhesion: MjtNum, [geom_] fluid: MjtNum,
2092	 [geom_] user: MjtNum, [geom_] rgba: f32],
2093	[[geom_] r#type: MjtGeom [force], [geom_] condim: i32,
2094	 [geom_] bodyid: i32, [geom_] dataid: i32,
2095	 [geom_] matid: i32, [geom_] plugin: i32],
2096	[]);
2097
2098info_with_view!(Model, hfield,
2099	[[hfield_] size: MjtNum],
2100	[[hfield_] nrow: i32,
2101	 [hfield_] ncol: i32,
2102	 [hfield_] adr: i32,
2103     [hfield_] pathadr: i32],
2104	[[hfield_] data: f32]);
2105
2106info_with_view!(Model, joint,
2107	[qpos0: MjtNum, qpos_spring: MjtNum,
2108     [jnt_] group: i32,
2109     [jnt_] limited: MjtBool, [jnt_] actfrclimited: MjtBool, [jnt_] actgravcomp: MjtBool,
2110	 [jnt_] solref: MjtNum, [jnt_] solimp: MjtNum,
2111	 [jnt_] pos: MjtNum,
2112     [jnt_] axis: MjtNum, [jnt_] stiffness: MjtNum,
2113     [jnt_] stiffnesspoly: MjtNum,
2114     [jnt_] range: MjtNum, [jnt_] actfrcrange: MjtNum, [jnt_] margin: MjtNum,
2115     [jnt_] user: MjtNum,
2116     [dof_] frictionloss: MjtNum, [dof_] armature: MjtNum,
2117     [dof_] damping: MjtNum,
2118     [dof_] dampingpoly: MjtNum,
2119     [dof_] invweight0: MjtNum,
2120     [dof_] M0: MjtNum,
2121     [dof_] simplenum: i32],
2122	[[jnt_] r#type: MjtJoint [force],
2123     [jnt_] qposadr: i32,
2124     [jnt_] dofadr: i32, [jnt_] bodyid: i32, [jnt_] actuatorid: i32,
2125     dof_bodyid: i32, [dof_] jntid: i32,
2126     [dof_] parentid: i32, dof_treeid: i32,
2127     [dof_] Madr: i32],
2128	[]);
2129
2130info_with_view!(Model, light,
2131	[[light_] mode: MjtCamLight [force],
2132	 [light_] r#type: MjtLightType [force],
2133	 [light_] castshadow: MjtBool,
2134	 [light_] bulbradius: f32,
2135	 [light_] intensity: f32,
2136	 [light_] range: f32,
2137	 [light_] active: MjtBool,
2138	 [light_] pos: MjtNum,
2139	 [light_] dir: MjtNum,
2140	 [light_] poscom0: MjtNum,
2141	 [light_] pos0: MjtNum,
2142	 [light_] dir0: MjtNum,
2143	 [light_] attenuation: f32,
2144	 [light_] cutoff: f32,
2145	 [light_] softness: f32,
2146	 [light_] exponent: f32,
2147	 [light_] ambient: f32,
2148	 [light_] diffuse: f32,
2149	 [light_] specular: f32],
2150	[[light_] bodyid: i32,
2151	 [light_] targetbodyid: i32,
2152	 [light_] texid: i32],
2153	[]);
2154
2155info_with_view!(Model, material,
2156	[[mat_] texuniform: MjtBool,
2157	 [mat_] texrepeat: f32,
2158	 [mat_] emission: f32,
2159	 [mat_] specular: f32,
2160	 [mat_] shininess: f32,
2161	 [mat_] reflectance: f32,
2162	 [mat_] rgba: f32,
2163     [mat_] metallic: f32,
2164     [mat_] roughness: f32],
2165	[[mat_] texid: i32],
2166	[]);
2167
2168info_with_view!(Model, mesh,
2169	[[mesh_] scale: MjtNum,
2170	 [mesh_] pos: MjtNum,
2171	 [mesh_] quat: MjtNum],
2172	[[mesh_] vertadr: i32,
2173	 [mesh_] vertnum: i32,
2174	 [mesh_] texcoordadr: i32,
2175	 [mesh_] faceadr: i32,
2176	 [mesh_] facenum: i32,
2177	 [mesh_] graphadr: i32,
2178	 [mesh_] extrema: i32,
2179	 [mesh_] normaladr: i32,
2180	 [mesh_] normalnum: i32,
2181	 [mesh_] texcoordnum: i32,
2182	 [mesh_] bvhadr: i32,
2183	 [mesh_] bvhnum: i32,
2184	 [mesh_] octadr: i32,
2185	 [mesh_] octnum: i32,
2186	 [mesh_] pathadr: i32,
2187	 [mesh_] polynum: i32,
2188	 [mesh_] polyadr: i32],
2189	[]);
2190
2191info_with_view!(Model, numeric,
2192	[],
2193	[[numeric_] adr: i32,
2194	 [numeric_] size: i32],
2195	[[numeric_] data: MjtNum]);
2196
2197info_with_view!(Model, pair,
2198	[[pair_] solref: MjtNum,
2199	 [pair_] solimp: MjtNum,
2200	 [pair_] margin: MjtNum,
2201	 [pair_] gap: MjtNum,
2202	 [pair_] adhesion: MjtNum,
2203	 [pair_] friction: MjtNum,
2204     [pair_] solreffriction: MjtNum,
2205	 [pair_] signature: i32],
2206	[[pair_] dim: i32,
2207	 [pair_] geom1: i32,
2208	 [pair_] geom2: i32],
2209	[]);
2210
2211info_with_view!(Model, sensor,
2212	[[sensor_] cutoff: MjtNum,
2213	 [sensor_] noise: MjtNum,
2214	 [sensor_] delay: MjtNum,
2215     [sensor_] interval: MjtNum,
2216	 [sensor_] user: MjtNum,
2217	 [sensor_] datatype: MjtDataType [force],
2218	 [sensor_] needstage: MjtStage [force]],
2219	[[sensor_] intprm: i32,
2220	 [sensor_] r#type: MjtSensor [force],
2221	 [sensor_] objid: i32,
2222	 [sensor_] refid: i32,
2223	 [sensor_] objtype: MjtObj [force],
2224	 [sensor_] reftype: MjtObj [force],
2225	 [sensor_] dim: i32,
2226	 [sensor_] adr: i32,
2227	 [sensor_] history: i32,
2228	 [sensor_] historyadr: i32,
2229	 [sensor_] plugin: i32],
2230	[]);
2231
2232info_with_view!(Model, site,
2233	[[site_] group: i32,
2234	 [site_] sameframe: MjtSameFrame [force],
2235	 [site_] size: MjtNum,
2236	 [site_] pos: MjtNum,
2237	 [site_] quat: MjtNum,
2238	 [site_] user: MjtNum,
2239	 [site_] rgba: f32,
2240	 [site_] r#type: MjtGeom [force]],
2241	[[site_] bodyid: i32,
2242	 [site_] matid: i32],
2243	[]);
2244
2245info_with_view!(Model, skin,
2246	[[skin_] group: i32,
2247	 [skin_] rgba: f32,
2248	 [skin_] inflate: f32],
2249	[[skin_] matid: i32,
2250	 [skin_] vertadr: i32,
2251	 [skin_] vertnum: i32,
2252	 [skin_] texcoordadr: i32,
2253	 [skin_] faceadr: i32,
2254	 [skin_] facenum: i32,
2255	 [skin_] boneadr: i32,
2256	 [skin_] bonenum: i32,
2257	 [skin_] pathadr: i32],
2258	[]);
2259
2260info_with_view!(Model, tendon,
2261	[[tendon_] group: i32,
2262	 [tendon_] limited: MjtBool, [tendon_] actfrclimited: MjtBool, [tendon_] width: MjtNum,
2263	 [tendon_] solref_lim: MjtNum, [tendon_] solimp_lim: MjtNum,
2264	 [tendon_] solref_fri: MjtNum, [tendon_] solimp_fri: MjtNum,
2265	 [tendon_] range: MjtNum, [tendon_] actfrcrange: MjtNum, [tendon_] margin: MjtNum,
2266	 [tendon_] stiffness: MjtNum,
2267	 [tendon_] stiffnesspoly: MjtNum,
2268	 [tendon_] damping: MjtNum,
2269	 [tendon_] dampingpoly: MjtNum,
2270	 [tendon_] armature: MjtNum,
2271	 [tendon_] frictionloss: MjtNum, [tendon_] lengthspring: MjtNum,
2272	 [tendon_] length0: MjtNum, [tendon_] invweight0: MjtNum,
2273	 [tendon_] user: MjtNum, [tendon_] rgba: f32,
2274	 [tendon_] treenum: i32],
2275	[[tendon_] matid: i32, [tendon_] actuatorid: i32, [tendon_] treeid: i32,
2276	 [tendon_] adr: i32, [tendon_] num: i32,
2277     [ten_] J_rownnz: i32, [ten_] J_rowadr: i32, [ten_] J_colind: i32],
2278	[]);
2279
2280info_with_view!(Model, texture,
2281	[[tex_] colorspace: MjtColorSpace [force]],
2282	[[tex_] r#type: MjtTexture [force],
2283	 [tex_] height: i32,
2284	 [tex_] width: i32,
2285	 [tex_] nchannel: i32,
2286	 [tex_] adr: MjtSize,
2287	 [tex_] pathadr: i32],
2288	[[tex_] data: MjtByte]);
2289
2290info_with_view!(Model, tuple,
2291	[[tuple_] objprm: MjtNum,
2292	 [tuple_] objtype: MjtObj [force]],
2293	[[tuple_] adr: i32,
2294	 [tuple_] size: i32,
2295	 [tuple_] objid: i32],
2296	[]);
2297
2298info_with_view!(Model, key,
2299	[[key_] time: MjtNum,
2300	 [key_] qpos: MjtNum,
2301	 [key_] qvel: MjtNum,
2302	 [key_] act: MjtNum,
2303	 [key_] mpos: MjtNum,
2304	 [key_] mquat: MjtNum,
2305	 [key_] ctrl: MjtNum],
2306	[],
2307	[]);
2308
2309#[cfg(test)]
2310#[expect(clippy::needless_range_loop,
2311         reason = "the loop indices drive FFI pointer arithmetic, ptr.add(i * stride + j)")]
2312mod tests {
2313    use crate::assert_relative_eq;
2314
2315    use super::*;
2316    use std::fs;
2317
2318    const EXAMPLE_MODEL: &str = stringify!(
2319    <mujoco>
2320        <worldbody>
2321            <light name="lamp_light1"
2322                mode="fixed" type="directional" castshadow="false" bulbradius="0.5" intensity="250"
2323                range="10" active="true" pos="0 0 0" dir="0 0 -1" attenuation="0.1 0.05 0.01"
2324                cutoff="60" exponent="2" ambient="0.1 0.1 0.25" diffuse="0.5 1 1" specular="1 1.5 1"/>
2325
2326            <light name="lamp_light2"
2327                mode="fixed" type="spot" castshadow="true" bulbradius="0.2" intensity="500"
2328                range="10" active="true" pos="0 0 0" dir="0 0 -1" attenuation="0.1 0.05 0.01"
2329                cutoff="45" exponent="2" ambient="0.1 0.1 0.1" diffuse="1 1 1" specular="1 1 1"/>
2330
2331            <camera name="cam1" fovy="50" resolution="100 200"/>
2332
2333            <light ambient="0.2 0.2 0.2"/>
2334            <body name="ball">
2335                <geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
2336                <joint name="ball" type="free" axis="1 1 1"/>
2337                <site name="touch" size="1" type="box"/>
2338            </body>
2339
2340            <body name="ball1" pos="-.5 0 0">
2341                <geom size=".1" rgba="0 1 0 1" mass="1"/>
2342                <joint type="free"/>
2343                <site name="ball1" size=".1 .1 .1" pos="0 0 0" rgba="0 1 0 0.2" type="box"/>
2344                <site name="ball12" size=".1 .1 .1" pos="0 0 0" rgba="0 1 1 0.2" type="box"/>
2345                <site name="ball13" size=".1 .1 .1" pos="0 0 0" rgba="0 1 1 0.2" type="box"/>
2346            </body>
2347
2348            <body name="ball2"  pos=".5 0 0">
2349                <geom name="ball2" size=".5" rgba="0 1 1 1" mass="1"/>
2350                <joint name="ball2" type="free"/>
2351                <site name="ball2" size=".1 .1 .1" pos="0 0 0" rgba="0 1 1 0.2" type="box"/>
2352                <site name="ball22" size="0.5 0.25 0.5" pos="5 1 3" rgba="1 2 3 1" type="box"/>
2353                <site name="ball23" size=".1 .1 .1" pos="0 0 0" rgba="0 1 1 0.2" type="box"/>
2354            </body>
2355
2356            <geom name="floor" type="plane" size="10 10 1" euler="5 0 0"/>
2357
2358            <body name="slider">
2359                <geom name="rod" type="cylinder" size="1 10 0" euler="90 0 0" pos="0 0 10"/>
2360                <joint name="rod" type="slide" axis="0 1 0" range="0 1"/>
2361            </body>
2362
2363            <body name="ball3"  pos="0 0 5">
2364                <geom name="ball31" size=".5" rgba="0 1 1 1" mass="1"/>
2365                <joint type="slide"/>
2366            </body>
2367
2368            <body name="ball32"  pos="0 0 -5">
2369                <geom name="ball32" size=".5" rgba="0 1 1 1" mass="1"/>
2370                <joint type="slide"/>
2371            </body>
2372
2373            <body name="eq_body1" pos="0 0 0">
2374                <geom size="0.1"/>
2375            </body>
2376            <body name="eq_body2" pos="1 0 0">
2377                <geom size="0.1"/>
2378            </body>
2379            <body name="eq_body3" pos="0 0 0">
2380                <geom size="0.1"/>
2381            </body>
2382            <body name="eq_body4" pos="1 0 0">
2383                <geom size="0.1"/>
2384            </body>
2385        </worldbody>
2386
2387        
2388        <equality>
2389            <connect name="eq1" body1="eq_body1" body2="eq_body2" anchor="15 0 10"/>
2390            <connect name="eq2" body1="eq_body2" body2="eq_body1" anchor="-5 0 10"/>
2391            <connect name="eq3" body1="eq_body3" body2="eq_body4" anchor="0 5 0"/>
2392            <connect name="eq4" body1="eq_body4" body2="eq_body3" anchor="5 5 10"/>
2393        </equality>
2394
2395
2396        <actuator>
2397            <general name="slider" joint="rod" biastype="affine" ctrlrange="0 1" dynprm="1 2 3 4 5 6 7 8 9 10" gaintype="fixed"/>
2398            <general name="slider2" joint="ball2" biastype="affine" ctrlrange="0 1" dynprm="10 9 8 7 6 5 4 3 2 1" gaintype="fixed"/>
2399        </actuator>
2400
2401        <sensor>
2402            <touch name="touch" site="touch"/>
2403        </sensor>
2404
2405        <tendon>
2406            <spatial name="tendon1" limited="false" range="0 5" rgba="0 .5 2 3" width=".5">
2407                <site site="ball1"/>
2408                <site site="ball2"/>
2409            </spatial>
2410        </tendon>
2411
2412        <tendon>
2413            <spatial name="tendon2" limited="true" range="0 1" rgba="0 .1 1 1" width=".005">
2414                <site site="ball1"/>
2415                <site site="ball2"/>
2416            </spatial>
2417        </tendon>
2418
2419        <tendon>
2420            <spatial name="tendon3" limited="false" range="0 5" rgba=".5 .2 .4 .3" width=".25">
2421                <site site="ball1"/>
2422                <site site="ball2"/>
2423            </spatial>
2424        </tendon>
2425
2426        <!-- Contact pair between the two geoms -->
2427        <contact>
2428            <pair name="geom_pair" geom1="ball31" geom2="ball32" condim="3" solref="0.02 1"
2429                solreffriction="0.01 0.5" solimp="0.0 0.95 0.001 0.5 2" margin="0.001" gap="0"
2430                friction="1.0 0.8 0.6 0.0 0.0">
2431            </pair>
2432        </contact>
2433
2434        <!-- A keyframe with qpos/qvel/ctrl etc. -->
2435        <keyframe>
2436            <!-- adjust nq/nv/nu in <default> or body definitions to match
2437                lengths in your test constants -->
2438            <key name="pose0"
2439                time="0.0"
2440                qpos="1.1 1.2 1.3 1.1 0.2 0.3 0.1 1.2 0.3 1.1 0.2 1.3 0.1 1.2 0.3 1.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.0"
2441                qvel="0.5 5.0 5.0 0.0 1.0 0.0 0.0 5.0 0.0 5.0 1.0 5.0 0.0 1.0 5.0 0.0 1.0 0.0 0.0 1.0 0.0"
2442                ctrl="0.5 0.5"/>
2443            <key name="pose1"
2444                time="1.5"
2445                qpos="0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.3 0.1 0.2 0.0"
2446                qvel="0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0"
2447                ctrl="0.5 0.0"/>
2448        </keyframe>
2449
2450        <custom>
2451            <tuple name="tuple_example">
2452                <!-- First entry: a body -->
2453                <element objtype="body" objname="ball2" prm="0.5"/>
2454                <!-- Second entry: a site -->
2455                <element objtype="site" objname="ball1" prm="1.0"/>
2456            </tuple>
2457
2458            <!-- Numeric element with a single value -->
2459            <numeric name="gain_factor1" size="5" data="3.14159 0 0 0 3.14159"/>
2460            <numeric name="gain_factor2" size="3" data="1.25 5.5 10.0"/>
2461        </custom>
2462
2463        <!-- Texture definition -->
2464        <asset>
2465            <texture name="wall_tex"
2466                type="2d"
2467                colorspace="sRGB"
2468                width="128"
2469                height="128"
2470                nchannel="3"
2471                builtin="flat"
2472                rgb1="0.6 0.6 0.6"
2473                rgb2="0.6 0.6 0.6"
2474                mark="none"/>
2475
2476            <!-- Material definition -->
2477            <material name="wood_material"
2478                rgba="0.8 0.5 0.3 1"
2479                emission="0.1"
2480                specular="0.5"
2481                shininess="0.7"
2482                reflectance="0.2"
2483                metallic="0.3"
2484                roughness="0.4"
2485                texuniform="true"
2486                texrepeat="2 2"/>
2487
2488            <!-- Material definition -->
2489            <material name="also_wood_material"
2490                rgba="0.8 0.5 0.3 1"
2491                emission="0.1"
2492                specular="0.5"
2493                shininess="0.7"
2494                reflectance="0.2"
2495                metallic="0.3"
2496                roughness="0.5"
2497                texuniform="false"
2498                texrepeat="2 2"/>
2499
2500            <hfield name="hf1" nrow="2" ncol="3" size="1 1 1 0.1"/>
2501            <hfield name="hf2" nrow="5" ncol="3" size="1 1 1 5.25"/>
2502            <hfield name="hf3" nrow="2" ncol="3" size="1 1 1 0.1"/>
2503        </asset>
2504    </mujoco>
2505);
2506
2507    #[test]
2508    fn test_integrate_and_differentiate_pos() {
2509        const DT: MjtNum = 0.05;
2510
2511        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2512        assert!(model.nq() > model.nv(), "the model must hold a quaternion joint");
2513
2514        let qvel: Vec<MjtNum> = (0..model.nv()).map(|i| 0.1 * (i as MjtNum + 1.0)).collect();
2515        let qpos1 = model.qpos0().to_vec();
2516        let mut qpos2 = qpos1.clone();
2517        model.integrate_pos(&mut qpos2, &qvel, DT);
2518        assert_ne!(qpos1, qpos2, "integration must move the configuration");
2519
2520        let recovered = model.differentiate_pos(DT, &qpos1, &qpos2);
2521        assert_eq!(recovered.len(), model.nv() as usize);
2522        for (got, expected) in recovered.iter().zip(&qvel) {
2523            assert_relative_eq!(*got, *expected, epsilon = 1e-9);
2524        }
2525    }
2526
2527    #[test]
2528    fn test_differentiate_pos_rejects_bad_length() {
2529        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2530        let qpos = model.qpos0().to_vec();
2531        let short = &qpos[..qpos.len() - 1];
2532
2533        assert!(matches!(
2534            model.try_differentiate_pos(0.1, short, &qpos),
2535            Err(MjModelError::LengthMismatch { name: "qpos1", .. })
2536        ));
2537        assert!(matches!(
2538            model.try_differentiate_pos(0.1, &qpos, short),
2539            Err(MjModelError::LengthMismatch { name: "qpos2", .. })
2540        ));
2541    }
2542
2543    #[test]
2544    fn test_differentiate_pos_into_buffer_bounds() {
2545        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2546        let nv = model.nv() as usize;
2547        let qpos1 = model.qpos0().to_vec();
2548        let mut qpos2 = qpos1.clone();
2549        model.integrate_pos(&mut qpos2, &vec![0.1; nv], 1.0);
2550
2551        let mut qvel = vec![MjtNum::NAN; nv + 2];
2552        model.differentiate_pos_into(1.0, &qpos1, &qpos2, &mut qvel);
2553        assert!(qvel[..nv].iter().all(|v| v.is_finite()), "every element below nv is written");
2554        assert!(qvel[nv..].iter().all(|v| v.is_nan()), "elements above nv keep their previous values");
2555
2556        assert!(matches!(
2557            model.try_differentiate_pos_into(1.0, &qpos1, &qpos2, &mut vec![0 as MjtNum; nv - 1]),
2558            Err(MjModelError::BufferTooSmall { needed, available }) if needed == nv && available == nv - 1
2559        ));
2560    }
2561
2562    #[test]
2563    fn test_normalize_quat() {
2564        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2565        let free_adr = model.jnt_qposadr()[0] as usize;
2566        assert_eq!(model.jnt_type()[0], MjtJoint::mjJNT_FREE);
2567
2568        let mut qpos = model.qpos0().to_vec();
2569        qpos[free_adr + 3..free_adr + 7].copy_from_slice(&[2.0, 0.0, 0.0, 2.0]);
2570        model.normalize_quat(&mut qpos);
2571
2572        let quat = &qpos[free_adr + 3..free_adr + 7];
2573        let norm = quat.iter().map(|q| q * q).sum::<MjtNum>().sqrt();
2574        assert_relative_eq!(norm, 1.0, epsilon = 1e-12);
2575        assert_relative_eq!(quat[0], 0.5_f64.sqrt(), epsilon = 1e-12);
2576        assert_relative_eq!(quat[3], 0.5_f64.sqrt(), epsilon = 1e-12);
2577    }
2578
2579    /// Tests if the model can be loaded and then saved.
2580    #[test]
2581    fn test_model_load_save() {
2582        const MODEL_SAVE_XML_PATH: &str = "./__TMP_MODEL1.xml";
2583        const MODEL_INVALID_SAVE_XML_PATH: &str = "/some/non-existent/path/";
2584
2585        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2586        model.save_last_xml(MODEL_SAVE_XML_PATH).expect("could not save the model XML.");      
2587        fs::remove_file(MODEL_SAVE_XML_PATH).unwrap();
2588
2589        // Try to get an error
2590        assert!(model.save_last_xml(MODEL_INVALID_SAVE_XML_PATH).is_err());
2591    }
2592
2593    #[test]
2594    fn test_model_encode() {
2595        const PATH_MJB: &str = "./__TMP_MODEL_ENCODE.mjb";
2596        const PATH_XML: &str = "./__TMP_MODEL_ENCODE.xml";
2597
2598        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2599        model.encode(PATH_MJB, "").unwrap();
2600        model.encode_with_vfs(PATH_XML, "text/xml", &MjVfs::new()).unwrap();
2601
2602        let encoded = MjModel::from_buffer(&fs::read(PATH_MJB).unwrap()).unwrap();
2603        // The XML encoder writes the spec of the last XML load, which a parallel test may replace,
2604        // so only the MJB round trip is compared against `model`.
2605        let reloaded = MjModel::from_xml(PATH_XML);
2606        fs::remove_file(PATH_MJB).unwrap();
2607        fs::remove_file(PATH_XML).unwrap();
2608
2609        assert!(model.is_compatible_with_model(&encoded));
2610        reloaded.unwrap();
2611
2612        assert!(model.encode("/some/non-existent/path/model.xml", "").is_err());
2613    }
2614
2615    #[test]
2616    fn test_actuator_model_view() {
2617        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2618        let actuator_model_info = model.actuator("slider").unwrap();
2619        let view = actuator_model_info.view(&model);
2620
2621        /* Test read */
2622        assert_eq!(view.biastype[0], MjtBias::mjBIAS_AFFINE);
2623        assert_eq!(&view.ctrlrange[..], [0.0, 1.0]);
2624        assert!(view.ctrllimited[0]);
2625        assert!(!view.forcelimited[0]);
2626        assert_eq!(view.trntype[0], MjtTrn::mjTRN_JOINT);
2627        assert_eq!(view.gaintype[0], MjtGain::mjGAIN_FIXED);
2628
2629        /* Test direct array slice correspondance */
2630        assert_eq!(view.dynprm[..], model.actuator_dynprm()[actuator_model_info.id]);
2631
2632        /* Test write */
2633        let mut view_mut = actuator_model_info.view_mut(&mut model);
2634        view_mut.biastype[0] = MjtBias::mjBIAS_USER;
2635        view_mut.delay[0] = 3.0;
2636
2637        assert_eq!(view_mut.biastype[0], MjtBias::mjBIAS_USER);
2638        assert_eq!(view_mut.delay[0], 3.0);
2639        view_mut.zero();
2640
2641        assert_eq!(view_mut.delay[0], 0.0);
2642        assert_eq!(view_mut.biastype[0], MjtBias::mjBIAS_NONE);
2643    }
2644
2645    /// `light_softness` must expose the per-light `softness`, not one of the arrays that bracket
2646    /// it in `mjModel` (`light_cutoff` before it, `light_exponent` after it).
2647    #[test]
2648    fn test_light_softness() {
2649        const XML: &str = stringify!(
2650            <mujoco>
2651                <worldbody>
2652                    <light name="spot1" type="spot" cutoff="45" softness="0.25" exponent="2"/>
2653                    <light name="spot2" type="spot" cutoff="60" softness="0.75" exponent="3"/>
2654                </worldbody>
2655            </mujoco>
2656        );
2657        let model = MjModel::from_xml_string(XML).unwrap();
2658        assert_eq!(model.light_softness(), [0.25, 0.75]);
2659
2660        let info = model.light("spot2").unwrap();
2661        assert_eq!(info.view(&model).softness[0], 0.75);
2662    }
2663
2664    #[test]
2665    fn test_sensor_model_view() {
2666        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2667        let sensor_model_info = model.sensor("touch").unwrap();
2668        let view = sensor_model_info.view(&model);
2669        
2670        /* Test read */
2671        assert_eq!(view.dim[0], 1);
2672        assert_eq!(view.objtype[0], MjtObj::mjOBJ_SITE);
2673        assert_eq!(view.noise[0], 0.0);
2674        assert_eq!(view.r#type[0], MjtSensor::mjSENS_TOUCH);
2675
2676        /* Test write */
2677        let mut view_mut = sensor_model_info.view_mut(&mut model);
2678        view_mut.noise[0] = 1.0;
2679        assert_eq!(view_mut.noise[0], 1.0);
2680    }
2681
2682    #[test]
2683    fn test_tendon_model_view() {
2684        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2685        let tendon_model_info = model.tendon("tendon2").unwrap();
2686        let view = tendon_model_info.view(&model);
2687        
2688        /* Test read */
2689        assert_eq!(&view.range[..], [0.0, 1.0]);
2690        assert!(view.limited[0]);
2691        assert_eq!(view.width[0], 0.005);
2692
2693        /* Test alignment with the array slice */
2694        let tendon_id = tendon_model_info.id;
2695        assert_eq!(view.width[0], model.tendon_width()[tendon_id]);
2696        assert_eq!(*view.range, model.tendon_range()[tendon_id]);
2697        assert_eq!(view.limited[0], model.tendon_limited()[tendon_id]);
2698
2699        /* Test write */
2700        let mut view_mut = tendon_model_info.view_mut(&mut model);
2701        view_mut.frictionloss[0] = 5e-2;
2702        assert_eq!(view_mut.frictionloss[0], 5e-2);
2703    }
2704
2705    #[test]
2706    fn test_joint_model_view() {
2707        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2708        let model_info = model.joint("rod").unwrap();
2709        let view = model_info.view(&model);
2710        
2711        /* Test read */
2712        assert_eq!(view.r#type[0], MjtJoint::mjJNT_SLIDE);
2713        assert!(view.limited[0]);
2714        assert_eq!(&view.axis[..], [0.0, 1.0 , 0.0]);
2715        assert!(!view.dof_bodyid.is_empty());
2716        assert!(!view.dof_treeid.is_empty());
2717
2718        let dof_start = model.jnt_dofadr()[model_info.id] as usize;
2719        assert_eq!(view.dof_bodyid[0], model.dof_bodyid()[dof_start]);
2720        assert_eq!(view.dof_treeid[0], model.dof_treeid()[dof_start]);
2721
2722        // Also validate mapping for a multi-DOF joint.
2723        let free_info = model.joint("ball2").unwrap();
2724        let free_view = free_info.view(&model);
2725        assert!(free_view.dof_bodyid.len() > 1);
2726        assert_eq!(free_view.dof_bodyid.len(), free_view.dof_treeid.len());
2727
2728        let free_dof_start = model.jnt_dofadr()[free_info.id] as usize;
2729        let free_dof_end = free_dof_start + free_view.dof_bodyid.len();
2730        assert_eq!(&free_view.dof_bodyid[..], &model.dof_bodyid()[free_dof_start..free_dof_end]);
2731        assert_eq!(&free_view.dof_treeid[..], &model.dof_treeid()[free_dof_start..free_dof_end]);
2732
2733        /* Test write */
2734        let mut view_mut = model_info.view_mut(&mut model);
2735        view_mut.axis.copy_from_slice(&[1.0, 0.0, 0.0]);
2736        assert_eq!(&view_mut.axis[..], [1.0, 0.0 , 0.0]);
2737    }
2738
2739    #[test]
2740    fn test_geom_model_view() {
2741        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2742        let model_info = model.geom("ball2").unwrap();
2743        let view = model_info.view(&model);
2744        
2745        /* Test read */
2746        assert_eq!(view.r#type[0], MjtGeom::mjGEOM_SPHERE);
2747        assert_eq!(view.size[0], 0.5);
2748
2749        /* Test write */
2750        let mut view_mut = model_info.view_mut(&mut model);
2751        view_mut.size[0] = 1.0;
2752        assert_eq!(view_mut.size[0], 1.0);
2753    }
2754
2755    #[test]
2756    fn test_body_model_view() {
2757        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
2758        let model_info = model.body("ball2").unwrap();
2759        let view = model_info.view(&model);
2760        
2761        /* Test read */
2762        assert_eq!(view.pos[0], 0.5);
2763
2764        /* Test alignment with slice */
2765        let body_id = model_info.id;
2766        assert_eq!(model.body_sameframe()[body_id], view.sameframe[0]);
2767
2768        /* Test write */
2769        let mut view_mut = model_info.view_mut(&mut model);
2770        view_mut.pos[0] = 1.0;
2771        assert_eq!(view_mut.pos[0], 1.0);
2772    }
2773
2774
2775    /// XML of a hinge body with one motor; `{extra}` adds a size or actuator difference that
2776    /// `mjCModel::Signature` does not hash.
2777    const COMPAT_MODEL: &str = "<mujoco>{extra}\
2778<worldbody><body name='b1'><joint name='j' type='hinge'/><geom size='0.1'/></body></worldbody>\
2779<actuator>{actuator}</actuator></mujoco>";
2780
2781    fn compat_model(extra: &str, actuator: &str) -> MjModel {
2782        let xml = COMPAT_MODEL.replace("{extra}", extra).replace("{actuator}", actuator);
2783        MjModel::from_xml_string(&xml).unwrap()
2784    }
2785
2786    #[test]
2787    fn test_is_compatible_with_model_rejects_unpinned_size() {
2788        let plain = compat_model("", "<motor joint='j'/>");
2789
2790        // nuserdata sizes mjData::userdata; the signature does not hash it.
2791        let userdata = compat_model("<size nuserdata='2000'/>", "<motor joint='j'/>");
2792        assert_eq!(plain.signature(), userdata.signature(), "the pair must share a signature");
2793        assert_ne!(plain.nuserdata(), userdata.nuserdata());
2794        assert!(!plain.is_compatible_with_model(&userdata));
2795
2796        // na sizes mjData::act; the actuator tag carries no dynamics type.
2797        let stateful = compat_model("", "<general joint='j' dyntype='integrator'/>");
2798        assert_eq!(plain.signature(), stateful.signature(), "the pair must share a signature");
2799        assert_ne!(plain.na(), stateful.na());
2800        assert!(!plain.is_compatible_with_model(&stateful));
2801    }
2802
2803    #[test]
2804    fn test_is_compatible_with_model_rejects_a_different_arena_size() {
2805        let plain = compat_model("", "<motor joint='j'/>");
2806
2807        // mjData inherits narena from the model, and mj_copyDataVisual raises mjERROR on a
2808        // mismatch, which ends the process.
2809        let small = compat_model("<size memory='4M'/>", "<motor joint='j'/>");
2810        assert_eq!(plain.signature(), small.signature(), "the pair must share a signature");
2811        assert_ne!(plain.narena(), small.narena());
2812        assert!(!plain.is_compatible_with_model(&small));
2813    }
2814
2815    #[test]
2816    fn test_is_compatible_with_model_rejects_a_different_mesh_split() {
2817        const TET: &str = "0 0 0  1 0 0  0 1 0  0 0 1";
2818        const CUBE: &str = "0 0 0  1 0 0  1 1 0  0 1 0  0 0 1  1 0 1  1 1 1  0 1 1";
2819        let mesh_model = |first: &str, second: &str| MjModel::from_xml_string(&format!(
2820            "<mujoco><asset><mesh name='m1' vertex='{first}'/><mesh name='m2' vertex='{second}'/>\
2821             </asset><worldbody><body name='b'><joint name='j' type='hinge'/>\
2822             <geom type='mesh' mesh='m1'/><geom type='mesh' mesh='m2'/></body></worldbody></mujoco>"
2823        )).unwrap();
2824
2825        let model = mesh_model(TET, CUBE);
2826        let swapped = mesh_model(CUBE, TET);
2827        assert_eq!(model.signature(), swapped.signature(), "the pair must share a signature");
2828        assert_eq!(model.nmeshvert(), swapped.nmeshvert(), "the vertex totals must agree");
2829        assert_ne!(model.mesh_vertadr(), swapped.mesh_vertadr());
2830        assert!(!model.is_compatible_with_model(&swapped));
2831        // An `Info` gate compares layouts alone, so the layout must carry the split.
2832        assert_ne!(model.layout(), swapped.layout());
2833        assert!(model.is_compatible_with_model(&mesh_model(TET, CUBE)));
2834    }
2835
2836    #[test]
2837    fn test_is_compatible_with_model_accepts_a_renamed_element() {
2838        const LONG_NAME: &str = "name='a_body_name_long_enough_to_cross_the_model_buffer_padding_boundary'";
2839        let model = compat_model("", "<motor joint='j'/>");
2840        let renamed = MjModel::from_xml_string(
2841            &COMPAT_MODEL.replace("{extra}", "").replace("{actuator}", "<motor joint='j'/>")
2842                         .replace("name='b1'", LONG_NAME)
2843        ).unwrap();
2844
2845        assert_eq!(model.signature(), renamed.signature(), "the pair must share a signature");
2846        assert_ne!(model.nbuffer(), renamed.nbuffer(), "the rename must move nbuffer");
2847        assert_eq!(model.narena(), renamed.narena(), "no mjData buffer may follow the name");
2848        assert!(model.is_compatible_with_model(&renamed));
2849    }
2850
2851    /// A slide joint and a hinge joint share their `qpos` and `dof` footprint, so every address
2852    /// table and every size agrees. Only the joint type separates the pair, and it changes what
2853    /// the state means.
2854    #[test]
2855    fn test_is_compatible_with_model_rejects_a_joint_type_change() {
2856        let joint_model = |kind: &str| MjModel::from_xml_string(&format!(
2857            "<mujoco><worldbody><body><joint type='{kind}'/><geom size='0.1'/></body>\
2858             </worldbody></mujoco>"
2859        )).unwrap();
2860
2861        let slide = joint_model("slide");
2862        let hinge = joint_model("hinge");
2863        assert_eq!(slide.signature(), hinge.signature(), "the pair must share a signature");
2864        assert_eq!(slide.nq(), hinge.nq(), "the qpos totals must agree");
2865        assert_eq!(slide.nv(), hinge.nv(), "the dof totals must agree");
2866        assert_eq!(slide.jnt_qposadr(), hinge.jnt_qposadr());
2867        assert_eq!(slide.jnt_dofadr(), hinge.jnt_dofadr());
2868        assert!(!slide.is_compatible_with_model(&hinge));
2869    }
2870
2871    /// The kinematic tree is the one structure that `mjCModel::Signature` carried alone, so the
2872    /// gate must reject a re-parented body now that it ignores the signature.
2873    #[test]
2874    fn test_is_compatible_with_model_rejects_a_different_body_tree() {
2875        const INNER: &str = "<body name='b2' pos='0.1 0 0'><joint type='hinge'/>\
2876                             <geom size='0.05'/></body>";
2877        let nested = MjModel::from_xml_string(&format!(
2878            "<mujoco><worldbody><body name='b1'><joint type='hinge'/><geom size='0.1'/>\
2879             {INNER}</body></worldbody></mujoco>"
2880        )).unwrap();
2881        let siblings = MjModel::from_xml_string(&format!(
2882            "<mujoco><worldbody><body name='b1'><joint type='hinge'/><geom size='0.1'/></body>\
2883             {INNER}</worldbody></mujoco>"
2884        )).unwrap();
2885
2886        assert_eq!(nested.nbody(), siblings.nbody(), "the body counts must agree");
2887        assert_eq!(nested.nq(), siblings.nq(), "the qpos totals must agree");
2888        assert_eq!(nested.njnt(), siblings.njnt(), "the joint counts must agree");
2889        assert_ne!(nested.body_parentid(), siblings.body_parentid());
2890        // The signature also separates this pair, so test the table that must carry the tree.
2891        assert_ne!(nested.element_split_tables(), siblings.element_split_tables());
2892        assert!(!nested.is_compatible_with_model(&siblings));
2893    }
2894
2895    #[test]
2896    fn test_is_compatible_with_model_rejects_a_different_sensor_split() {
2897        let sensor_model = |first: u32, second: u32| MjModel::from_xml_string(&format!(
2898            "<mujoco><worldbody><body name='b1'><joint name='j' type='hinge'/><geom size='0.1'/>\
2899             </body></worldbody><sensor><user name='u1' dim='{first}' objtype='body' objname='b1'/>\
2900             <user name='u2' dim='{second}' objtype='body' objname='b1'/></sensor></mujoco>"
2901        )).unwrap();
2902
2903        let model = sensor_model(3, 1);
2904        let swapped = sensor_model(1, 3);
2905        assert_eq!(model.signature(), swapped.signature(), "the pair must share a signature");
2906        assert_eq!(model.nsensordata(), swapped.nsensordata(), "the data totals must agree");
2907        assert_ne!(model.sensor_adr(), swapped.sensor_adr());
2908        assert!(!model.is_compatible_with_model(&swapped));
2909        assert!(model.is_compatible_with_model(&sensor_model(3, 1)));
2910    }
2911
2912    /// A view gate compares the layout alone, so the per-flex split must reach the layout: a
2913    /// contact holds a flex element index that C resolves through the per-flex tables.
2914    #[test]
2915    fn test_layout_rejects_a_different_flex_split() {
2916        let flex = |name: &str, bodies: &str, nvert: usize, element: &str| format!(
2917            "<flex name='{name}' dim='1' body='{bodies}' vertex='{}' element='{element}'>\
2918             <edge damping='1'/></flex>",
2919            "0 0 0 ".repeat(nvert)
2920        );
2921        let flex_model = |first: String, second: String| MjModel::from_xml_string(&format!(
2922            "<mujoco><worldbody>\
2923             <body name='v0'><freejoint/><geom size='0.01'/></body>\
2924             <body name='v1' pos='0.1 0 0'><freejoint/><geom size='0.01'/></body>\
2925             <body name='v2' pos='0.2 0 0'><freejoint/><geom size='0.01'/></body>\
2926             <body name='v3' pos='0.3 0 0'><freejoint/><geom size='0.01'/></body>\
2927             <body name='v4' pos='0.4 0 0'><freejoint/><geom size='0.01'/></body>\
2928             </worldbody><deformable>{first}{second}</deformable></mujoco>"
2929        )).unwrap();
2930
2931        // The same five vertices and three elements, divided the other way round between f1 and f2.
2932        let model = flex_model(flex("f1", "v0 v1 v2", 3, "0 1 1 2"), flex("f2", "v3 v4", 2, "0 1"));
2933        let swapped = flex_model(flex("f1", "v0 v1", 2, "0 1"), flex("f2", "v2 v3 v4", 3, "0 1 1 2"));
2934        assert_eq!(model.signature(), swapped.signature(), "the pair must share a signature");
2935        assert_eq!(model.nflexvert(), swapped.nflexvert(), "the vertex totals must agree");
2936        assert_ne!(model.flex_vertnum(), swapped.flex_vertnum());
2937        assert!(!model.is_compatible_with_model(&swapped));
2938        assert_ne!(model.layout(), swapped.layout());
2939    }
2940
2941    /// An asset copy overrides the asset data and reaches no `mjData` buffer, so it accepts a
2942    /// model that only sizes its data differently. A moved asset it still rejects.
2943    #[test]
2944    fn test_is_asset_compatible_with_model_ignores_the_data_sizes() {
2945        const TET: &str = "0 0 0  1 0 0  0 1 0  0 0 1";
2946        const CUBE: &str = "0 0 0  1 0 0  1 1 0  0 1 0  0 0 1  1 0 1  1 1 1  0 1 1";
2947        let asset_model = |extra: &str, first: &str, second: &str| MjModel::from_xml_string(&format!(
2948            "<mujoco>{extra}<asset><mesh name='m1' vertex='{first}'/><mesh name='m2' vertex='{second}'/>\
2949             </asset><worldbody><body name='b'><joint name='j' type='hinge'/>\
2950             <geom type='mesh' mesh='m1'/><geom type='mesh' mesh='m2'/></body></worldbody></mujoco>"
2951        )).unwrap();
2952
2953        let model = asset_model("", TET, CUBE);
2954        let more_userdata = asset_model("<size nuserdata='2000'/>", TET, CUBE);
2955        assert!(!model.is_compatible_with_model(&more_userdata), "the mjData buffers differ");
2956        assert!(model.is_asset_compatible_with_model(&more_userdata));
2957
2958        // The same total, split the other way round: the copy would write the neighbouring mesh.
2959        let swapped = asset_model("", CUBE, TET);
2960        assert_eq!(model.nmeshvert(), swapped.nmeshvert(), "the vertex totals must agree");
2961        assert!(!model.is_asset_compatible_with_model(&swapped));
2962
2963        // A sensor moves no asset, so the asset test must read the asset tables alone.
2964        let sensor = asset_model("<sensor><user dim='4' objtype='body' objname='b'/></sensor>", TET, CUBE);
2965        assert!(!model.is_compatible_with_model(&sensor), "the element tables differ");
2966        assert!(model.is_asset_compatible_with_model(&sensor));
2967    }
2968
2969    /// `mjr_makeContext` records `tex_type` in the `mjrContext` and `mjr_uploadTexture` binds
2970    /// `GL_TEXTURE_2D` or `GL_TEXTURE_CUBE_MAP` by it, so an upload that crosses the two kinds
2971    /// leaves the recorded kind disagreeing with the texture object. The compiler gives a cube
2972    /// texture `height = 6 * width`, which a 2D texture may also carry, so the sizes alone do not
2973    /// separate the pair.
2974    #[test]
2975    fn test_is_asset_compatible_with_model_rejects_a_different_texture_kind() {
2976        let textured = |kind: &str, width: u32, height: u32| MjModel::from_xml_string(&format!(
2977            "<mujoco><asset><texture name='t' type='{kind}' builtin='flat' width='{width}' \
2978             height='{height}' rgb1='1 1 1'/><material name='m' texture='t'/></asset>\
2979             <worldbody><geom size='0.1' material='m'/></worldbody></mujoco>"
2980        )).unwrap();
2981
2982        let flat = textured("2d", 8, 48);
2983        let cube = textured("cube", 8, 8);
2984        assert_eq!(flat.tex_width(), cube.tex_width(), "the widths must agree");
2985        assert_eq!(flat.tex_height(), cube.tex_height(), "the compiler gives the cube 6 * width");
2986        assert_eq!(flat.tex_nchannel(), cube.tex_nchannel(), "the channel counts must agree");
2987        assert_eq!(flat.ntexdata(), cube.ntexdata(), "the pair holds the same number of bytes");
2988        assert_ne!(flat.tex_type(), cube.tex_type());
2989
2990        assert!(!flat.is_asset_compatible_with_model(&cube));
2991        assert!(!flat.is_compatible_with_model(&cube));
2992        assert!(flat.is_asset_compatible_with_model(&textured("2d", 8, 48)));
2993    }
2994
2995    /// [`MjModelLayout`] carries no size that a count table already determines, so a compatible
2996    /// pair keeps every size below equal only while the identity beside it holds. A MuJoCo release
2997    /// that adds a padding term, an alignment term or an extra row to one of them would let the
2998    /// gate accept a model whose `mjData` buffers differ, or whose joints keep their state
2999    /// elsewhere.
3000    #[test]
3001    fn test_every_dropped_size_follows_from_a_count_table() {
3002        let model = MjModel::from_xml_string(
3003            "<mujoco><asset>\
3004             <texture name='t1' type='2d' builtin='checker' width='17' height='23' rgb1='1 0 0' rgb2='0 1 0'/>\
3005             <texture name='t2' type='cube' builtin='flat' width='8' height='8' rgb1='1 1 1'/>\
3006             <texture name='t3' type='2d' builtin='flat' width='4' height='6' nchannel='4' rgb1='1 1 1'/>\
3007             <material name='mat' texture='t1'/>\
3008             <hfield name='hf' nrow='7' ncol='11' size='1 1 1 0.1'/>\
3009             <mesh name='ms' vertex='0 0 0  1 0 0  0 1 0  0 0 1' texcoord='0 0  1 0  0 1  1 1'/>\
3010             </asset><worldbody>\
3011             <geom type='hfield' hfield='hf'/>\
3012             <body name='free'><freejoint/><geom size='0.1' type='mesh' mesh='ms' material='mat'/>\
3013             <site name='s1'/>\
3014             <body name='ball' pos='0 0 0.2'><joint name='jb' type='ball'/><geom size='0.1'/>\
3015             <body name='slide' pos='0 0 0.2'><joint name='js' type='slide'/><geom size='0.1'/>\
3016             <site name='s2'/><joint name='jh' type='hinge'/></body></body></body>\
3017             <body name='v0'><freejoint/><geom size='0.01'/></body>\
3018             <body name='v1' pos='0.1 0 0'><freejoint/><geom size='0.01'/></body>\
3019             <body name='v2' pos='0.2 0 0'><freejoint/><geom size='0.01'/></body>\
3020             </worldbody>\
3021             <deformable><flex name='f1' dim='1' body='v0 v1 v2' vertex='0 0 0 0 0 0 0 0 0' element='0 1 1 2'>\
3022             <edge damping='1'/></flex>\
3023             </deformable>\
3024             <tendon><spatial name='td'><site site='s1'/><site site='s2'/></spatial>\
3025             <fixed name='tf'><joint joint='js' coef='1'/><joint joint='jh' coef='2'/></fixed></tendon>\
3026             <actuator><motor joint='jh'/><position joint='js' kp='3'/>\
3027             <general joint='jh' dyntype='integrator'/><general joint='js' dyntype='filter' dynprm='0.1'/>\
3028             <general tendon='tf' dyntype='filterexact' dynprm='0.1'/>\
3029             <orientation site='s2' refsite='s1' kp='1' input='expmap'/>\
3030             <orientation site='s2' refsite='s1' kp='1' input='quat'/>\
3031             <pid joint='jh' kp='1' kv='1' input='pos vel'/></actuator>\
3032             <sensor><framepos objtype='site' objname='s1'/><framequat objtype='site' objname='s1'/>\
3033             <jointpos joint='jh'/><accelerometer site='s2'/><tendonpos tendon='td'/>\
3034             <user objtype='site' objname='s2' dim='5' needstage='vel'/></sensor></mujoco>"
3035        ).unwrap();
3036
3037        let sum = |table: &[i32]| table.iter().map(|&n| i64::from(n)).sum::<MjtSize>();
3038        let product = |first: &[i32], second: &[i32]| first.iter().zip(second)
3039            .map(|(&a, &b)| i64::from(a) * i64::from(b)).sum::<MjtSize>();
3040
3041        // Every joint type appears above, so a wrong footprint for any one of them fails here.
3042        let qpos = |joint: &MjtJoint| match joint {
3043            MjtJoint::mjJNT_FREE => 7, MjtJoint::mjJNT_BALL => 4, _ => 1,
3044        };
3045        let dof = |joint: &MjtJoint| match joint {
3046            MjtJoint::mjJNT_FREE => 6, MjtJoint::mjJNT_BALL => 3, _ => 1,
3047        };
3048        assert_eq!(model.nq(), model.jnt_type().iter().map(qpos).sum::<MjtSize>());
3049        assert_eq!(model.nv(), model.jnt_type().iter().map(dof).sum::<MjtSize>());
3050
3051        // Both joint address tables left the layout as well, because each one is the running
3052        // prefix sum of the same footprints, in joint id order and with no gap.
3053        let (mut qpos_adr, mut dof_adr) = (0, 0);
3054        for (id, joint) in model.jnt_type().iter().enumerate() {
3055            assert_eq!(i64::from(model.jnt_qposadr()[id]), qpos_adr, "joint {id}");
3056            assert_eq!(i64::from(model.jnt_dofadr()[id]), dof_adr, "joint {id}");
3057            qpos_adr += qpos(joint);
3058            dof_adr += dof(joint);
3059        }
3060
3061        assert_eq!(model.nsensordata(), sum(model.sensor_dim()));
3062        assert_eq!(model.na(), sum(model.actuator_actnum()));
3063        assert_eq!(model.nu(), sum(model.actuator_ctrlnum()));
3064        assert_eq!(model.nout(), sum(model.actuator_outnum()));
3065        assert_eq!(model.n_jten(), sum(model.ten_j_rownnz()));
3066        assert_eq!(model.nflexvert(), sum(model.flex_vertnum()));
3067        assert_eq!(model.nflexelem(), sum(model.flex_elemnum()));
3068
3069        assert_eq!(model.nmeshvert(), sum(model.mesh_vertnum()));
3070        assert_eq!(model.nmeshnormal(), sum(model.mesh_normalnum()));
3071        assert_eq!(model.nmeshtexcoord(), sum(model.mesh_texcoordnum()));
3072        assert_eq!(model.nmeshface(), sum(model.mesh_facenum()));
3073        assert_eq!(model.nhfielddata(), product(model.hfield_nrow(), model.hfield_ncol()));
3074        assert_eq!(model.ntexdata(), model.tex_width().iter().zip(model.tex_height())
3075            .zip(model.tex_nchannel())
3076            .map(|((&w, &h), &c)| i64::from(w) * i64::from(h) * i64::from(c)).sum::<MjtSize>());
3077
3078        // An empty table satisfies every assertion above, so hold the model to its content.
3079        assert!(model.na() > 0 && model.nu() > 0 && model.nout() > 0);
3080        assert!(model.nsensordata() > 6 && model.n_jten() > 0);
3081        assert!(model.nflexvert() > 0 && model.nflexelem() > 0 && model.nmeshvert() > 0);
3082        assert_eq!(model.nhfielddata(), 77, "the heightfield must survive the compiler");
3083        assert!(model.ntexdata() > 0 && model.nmeshtexcoord() > 0);
3084        // A one-control actuator and a three-channel texture would hide a per-element term, so
3085        // hold the model to the multi-control and the multi-channel case as well.
3086        assert!(model.actuator_ctrlnum().contains(&4) && model.actuator_outnum().contains(&3));
3087        assert!(model.tex_nchannel().contains(&3) && model.tex_nchannel().contains(&4));
3088    }
3089
3090    #[test]
3091    fn test_is_compatible_with_model_accepts_parameter_change() {
3092        let light = compat_model("", "<motor joint='j' gear='1'/>");
3093        let heavy = compat_model("", "<motor joint='j' gear='7'/>");
3094        assert!(light.is_compatible_with_model(&heavy));
3095        assert!(light.is_compatible_with_model(&light));
3096    }
3097
3098    #[test]
3099    fn test_try_view_model_signature_mismatch() {
3100        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint type='free'/><geom size='0.1'/></body></worldbody></mujoco>").unwrap();
3101        let mut model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint type='free'/><geom size='0.1'/></body><body name='extra'/></worldbody></mujoco>").unwrap();
3102
3103        let body_info = model1.body("b1").unwrap();
3104
3105        let err = body_info.try_view(&model2).unwrap_err();
3106        match err {
3107            MjModelError::IncompatibleModel { source, destination } => {
3108                assert_eq!(source, model1.signature());
3109                assert_eq!(destination, model2.signature());
3110            }
3111            other => panic!("expected IncompatibleModel, got {other:?}"),
3112        }
3113
3114        let err = body_info.try_view_mut(&mut model2).unwrap_err();
3115        match err {
3116            MjModelError::IncompatibleModel { source, destination } => {
3117                assert_eq!(source, model1.signature());
3118                assert_eq!(destination, model2.signature());
3119            }
3120            other => panic!("expected IncompatibleModel, got {other:?}"),
3121        }
3122    }
3123
3124    #[test]
3125    #[should_panic(expected = "the model is not compatible")]
3126    fn test_view_mut_model_signature_mismatch_panics() {
3127        let model1 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint type='free'/><geom size='0.1'/></body></worldbody></mujoco>").unwrap();
3128        let mut model2 = MjModel::from_xml_string("<mujoco><worldbody><body name='b1'><joint type='free'/><geom size='0.1'/></body><body name='extra'/></worldbody></mujoco>").unwrap();
3129
3130        let body_info = model1.body("b1").unwrap();
3131        let _view = body_info.view_mut(&mut model2);
3132    }
3133
3134
3135    #[test]
3136    fn test_camera_model_view() {
3137        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3138        let model_info = model.camera("cam1").unwrap();
3139        let view = model_info.view(&model);
3140
3141        /* Test read */
3142        assert_eq!(&view.resolution[..], [100, 200]);
3143        assert_eq!(view.fovy[0], 50.0);
3144
3145        /* Test write */
3146        let mut view_mut = model_info.view_mut(&mut model);
3147        view_mut.fovy[0] = 60.0;
3148        assert_eq!(view_mut.fovy[0], 60.0);
3149    }
3150
3151    #[test]
3152    fn test_id_2name_valid() {
3153        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3154
3155        // Body with id=1 should exist ("ball")
3156        let name = model.id_to_name(MjtObj::mjOBJ_BODY, 1);
3157        assert_eq!(name, Some("ball"));
3158    }
3159
3160    #[test]
3161    fn test_model_prints() {
3162        const TMP_FILE: &str = "tmpprint.txt";
3163        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3164        assert!(model.print(TMP_FILE).is_ok());
3165        fs::remove_file(TMP_FILE).unwrap();
3166
3167        assert!(model.print_formatted(TMP_FILE, "%.2f").is_ok());
3168        fs::remove_file(TMP_FILE).unwrap();
3169    }
3170
3171    #[test]
3172    fn test_id_2name_invalid() {
3173        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3174
3175        // Invalid id should return None
3176        let name = model.id_to_name(MjtObj::mjOBJ_BODY, 9999);
3177        assert_eq!(name, None);
3178    }
3179
3180    #[test]
3181    fn test_totalmass_set_and_get() {
3182        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3183
3184        let mass_before = model.totalmass();
3185        model.set_totalmass(5.0);
3186        let mass_after = model.totalmass();
3187
3188        assert_relative_eq!(mass_after, 5.0, epsilon = 1e-9);
3189        assert_ne!(mass_before, mass_after);
3190    }
3191
3192    /// Tests if copying the model works without any memory problems.
3193    #[test]
3194    fn test_copy_model() {
3195        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3196        let _cloned = model.clone();
3197    }
3198
3199    #[test]
3200    fn test_model_save() {
3201        const MODEL_SAVE_PATH: &str = "./__TMP_MODEL2.mjb";
3202        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3203        model.save_to_file(MODEL_SAVE_PATH).unwrap();
3204
3205        let saved_data = fs::read(MODEL_SAVE_PATH).unwrap();
3206        let mut data = vec![0; saved_data.len()];
3207        model.save_to_buffer(&mut data).unwrap();
3208
3209        assert_eq!(saved_data, data);
3210        fs::remove_file(MODEL_SAVE_PATH).unwrap();
3211
3212        /* Test virtual file system load */
3213        let model = MjModel::from_buffer(&saved_data).unwrap();
3214        assert!(model.light("lamp_light2").is_some());
3215        assert!(model.light("lamp_light-xyz").is_none());
3216    }
3217
3218    #[test]
3219    fn test_model_from_mjb() {
3220        const MODEL_SAVE_PATH: &str = "./__TMP_MODEL3.mjb";
3221        const MODEL_VFS_PATH: &str = "__TMP_MODEL3_VFS.mjb";
3222
3223        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3224        model.save_to_file(MODEL_SAVE_PATH).unwrap();
3225
3226        let loaded = MjModel::from_mjb(MODEL_SAVE_PATH).unwrap();
3227        assert!(model.is_compatible_with_model(&loaded));
3228        assert!(loaded.light("lamp_light2").is_some());
3229
3230        /* Test virtual file system load */
3231        let mut vfs = MjVfs::new();
3232        vfs.add_from_buffer(MODEL_VFS_PATH, &fs::read(MODEL_SAVE_PATH).unwrap()).unwrap();
3233        // The MJB now exists on the VFS only, so a load that ignores the VFS finds no file.
3234        fs::remove_file(MODEL_SAVE_PATH).unwrap();
3235
3236        let loaded_vfs = MjModel::from_mjb_vfs(MODEL_VFS_PATH, &vfs).unwrap();
3237        assert!(model.is_compatible_with_model(&loaded_vfs));
3238        assert!(loaded_vfs.light("lamp_light2").is_some());
3239
3240        assert!(MjModel::from_mjb(MODEL_SAVE_PATH).is_err());
3241    }
3242
3243    #[test]
3244    fn test_site_view() {
3245        // <site name="ball22" size="0.5 0.25 0.5" pos="5 1 3" rgba="1 2 3 1" type="box"/>
3246        const BODY_NAME: &str = "ball2";
3247        const SITE_NAME: &str = "ball22";
3248        const SITE_SIZE: [f64; 3] = [0.5, 0.25, 0.5];
3249        const SITE_POS: [f64; 3] = [5.0, 1.0, 3.0];
3250        const SITE_RGBA: [f32; 4] = [1.0, 2.0, 3.0, 1.0];
3251        const SITE_TYPE: MjtGeom = MjtGeom::mjGEOM_BOX;
3252
3253        let model = MjModel::from_xml_string(EXAMPLE_MODEL).expect("unable to load the model.");
3254        let info_ball = model.body(BODY_NAME).unwrap();
3255        let info_site = model.site(SITE_NAME).unwrap();
3256        let view_site = info_site.view(&model);
3257
3258        /* Check if all the attributes given match */
3259        assert_eq!(info_site.name, SITE_NAME);
3260        assert_eq!(view_site.size[..], SITE_SIZE);
3261        assert_eq!(view_site.pos[..], SITE_POS);
3262        assert_eq!(view_site.rgba[..], SITE_RGBA);
3263        assert_eq!(view_site.r#type[0], SITE_TYPE);
3264
3265        assert_eq!(view_site.bodyid[0] as usize, info_ball.id)
3266    }
3267
3268    #[test]
3269    fn test_pair_view() {
3270        const PAIR_NAME: &str = "geom_pair";
3271        const DIM: i32 = 3;
3272        const GEOM1_NAME: &str = "ball31";
3273        const GEOM2_NAME: &str = "ball32";
3274        const SOLREF: [f64; mjNREF as usize] = [0.02, 1.0];
3275        const SOLREFFRICTION: [f64; mjNREF as usize] = [0.01, 0.5];
3276        const SOLIMP: [f64; mjNIMP as usize] = [0., 0.95, 0.001, 0.5, 2.0];
3277        const MARGIN: f64 = 0.001;
3278        const GAP: f64 = 0.0;
3279        const FRICTION: [f64; 5] = [1.0, 0.8, 0.6, 0.0, 0.0];
3280
3281        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3282        let info_pair = model.pair(PAIR_NAME).unwrap();
3283        let view_pair = info_pair.view(&model);
3284
3285        let geom1_info = model.geom(GEOM1_NAME).unwrap();
3286        let geom2_info = model.geom(GEOM2_NAME).unwrap();
3287
3288        // signature =  body1 << 16 + body2 according to MuJoCo's documentation.
3289        let signature = ((geom1_info.view(&model).bodyid[0] as u32) << 16) + geom2_info.view(&model).bodyid[0] as u32;
3290
3291        assert_eq!(view_pair.dim[0], DIM);
3292        assert_eq!(view_pair.geom1[0] as usize, geom1_info.id);
3293        assert_eq!(view_pair.geom2[0] as usize, geom2_info.id);
3294        assert_eq!(view_pair.signature[0] as u32, signature);
3295        assert_eq!(view_pair.solref[..], SOLREF);
3296        assert_eq!(view_pair.solreffriction[..], SOLREFFRICTION);
3297        assert_eq!(view_pair.solimp[..], SOLIMP);
3298        assert_eq!(view_pair.margin[0], MARGIN);
3299        assert_eq!(view_pair.gap[0], GAP);
3300        assert_eq!(view_pair.friction[..], FRICTION);
3301    }
3302
3303    #[test]
3304    fn test_key_view() {
3305        const KEY_NAME: &str = "pose1";
3306        const TIME: f64 = 1.5;
3307        const QVEL: &[f64] = &[0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0];
3308        const ACT: &[f64]  = &[];
3309        const CTRL: &[f64] = &[0.5, 0.0];
3310
3311        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3312        let info_key = model.key(KEY_NAME).unwrap();
3313        let view_key = info_key.view(&model);
3314
3315        assert_eq!(view_key.time[0], TIME);
3316        // Don't test qpos, as it does some magic angle conversions, making the tests fail.
3317        // If all the other succeed, assume qpos works too (as its the exact same logic).
3318        assert_eq!(&view_key.qvel[..model.ffi().nv as usize], QVEL);
3319        assert_eq!(&view_key.act[..model.ffi().na as usize], ACT);
3320        assert_eq!(&view_key.ctrl[..model.ffi().nu as usize], CTRL);
3321
3322        let key_qvel = &model.key_qvel()[model.ffi().nv as usize..];
3323        assert_eq!(key_qvel, QVEL);
3324
3325        let key_act = &model.key_act()[model.ffi().na as usize..];
3326        assert_eq!(key_act, ACT);
3327
3328        let key_ctrl = &model.key_ctrl()[model.ffi().nu as usize..];
3329        assert_eq!(key_ctrl, CTRL);
3330    }
3331
3332    #[test]
3333    fn test_tuple_view() {
3334        const TUPLE_NAME: &str = "tuple_example";
3335        const SIZE: i32 = 2;
3336        const OBJTYPE: &[MjtObj] = &[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_SITE];
3337        const OBJPRM: &[f64]  = &[0.5, 1.0];
3338
3339        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3340        let info_tuple = model.tuple(TUPLE_NAME).unwrap();
3341        let view_tuple = info_tuple.view(&model);
3342
3343        let objid = &[
3344            model.body("ball2").unwrap().id as i32,
3345            model.site("ball1").unwrap().id as i32,
3346        ];
3347
3348        assert_eq!(view_tuple.size[0], SIZE);
3349        assert_eq!(&view_tuple.objtype[..SIZE as usize], OBJTYPE);
3350        assert_eq!(&view_tuple.objid[..SIZE as usize], objid);
3351        assert_eq!(&view_tuple.objprm[..SIZE as usize], OBJPRM);
3352    }
3353
3354    #[test]
3355    fn test_texture_view() {
3356        const TEX_NAME: &str = "wall_tex";
3357        const TYPE: MjtTexture = MjtTexture::mjTEXTURE_2D;          // for example, 2 = 2D texture
3358        const COLORSPACE: MjtColorSpace = MjtColorSpace::mjCOLORSPACE_SRGB;    // e.g. RGB
3359        const HEIGHT: i32 = 128;
3360        const WIDTH: i32 = 128;
3361        const NCHANNEL: i32 = 3;
3362
3363        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3364        let info_tex = model.texture(TEX_NAME).unwrap();
3365        let view_tex = info_tex.view(&model);
3366
3367        assert_eq!(view_tex.r#type[0], TYPE);
3368        assert_eq!(view_tex.colorspace[0], COLORSPACE);
3369        assert_eq!(view_tex.height[0], HEIGHT);
3370        assert_eq!(view_tex.width[0], WIDTH);
3371        assert_eq!(view_tex.nchannel[0], NCHANNEL);
3372
3373        assert_eq!(view_tex.data.as_ref().unwrap().len(), (WIDTH * HEIGHT * NCHANNEL) as usize);
3374    }
3375
3376    #[test]
3377    fn test_numeric_view() {
3378        const NUMERIC_NAME: &str = "gain_factor2";
3379        const SIZE: i32 = 3;
3380        const DATA: [f64; 3] = [1.25, 5.5, 10.0];
3381
3382        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3383        let info_numeric = model.numeric(NUMERIC_NAME).unwrap();
3384        let view_numeric = info_numeric.view(&model);
3385
3386        assert_eq!(view_numeric.size[0], SIZE);
3387        assert_eq!(&view_numeric.data.as_ref().unwrap()[..SIZE as usize], DATA);
3388    }
3389
3390    #[test]
3391    fn test_material_view() {
3392        const MATERIAL_NAME: &str = "also_wood_material";
3393
3394        const TEXUNIFORM: bool = false;
3395        const TEXREPEAT: [f32; 2] = [2.0, 2.0];
3396        const EMISSION: f32 = 0.1;
3397        const SPECULAR: f32 = 0.5;
3398        const SHININESS: f32 = 0.7;
3399        const REFLECTANCE: f32 = 0.2;
3400        const METALLIC: f32 = 0.3;
3401        const ROUGHNESS: f32 = 0.5;
3402        const RGBA: [f32; 4] = [0.8, 0.5, 0.3, 1.0];
3403        const TEXID: i32 = -1;
3404
3405        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3406        let info_material = model.material(MATERIAL_NAME).unwrap();
3407        let view_material = info_material.view(&model);
3408
3409        assert_eq!(view_material.texuniform[0], TEXUNIFORM);
3410        assert_eq!(view_material.texrepeat[..], TEXREPEAT);
3411        assert_eq!(view_material.emission[0], EMISSION);
3412        assert_eq!(view_material.specular[0], SPECULAR);
3413        assert_eq!(view_material.shininess[0], SHININESS);
3414        assert_eq!(view_material.reflectance[0], REFLECTANCE);
3415        assert_eq!(view_material.metallic[0], METALLIC);
3416        assert_eq!(view_material.roughness[0], ROUGHNESS);
3417        assert_eq!(view_material.rgba[..], RGBA);
3418        assert_eq!(view_material.texid[0], TEXID);
3419    }
3420
3421    #[test]
3422    fn test_light_view() {
3423        const LIGHT_NAME: &str = "lamp_light2";
3424        const MODE: MjtCamLight = MjtCamLight::mjCAMLIGHT_FIXED;
3425        const BODYID: usize = 0;       // lamp body id
3426        const TYPE: MjtLightType = MjtLightType::mjLIGHT_SPOT;           // spot light, adjust if mjLightType differs
3427        const CASTSHADOW: bool = true;
3428        const ACTIVE: bool = true;
3429
3430        const POS: [MjtNum; 3] = [0.0, 0.0, 0.0];
3431        const DIR: [MjtNum; 3] = [0.0, 0.0, -1.0];
3432        const POS0: [MjtNum; 3] = [0.0, 0.0, 0.0];
3433        const DIR0: [MjtNum; 3] = [0.0, 0.0, -1.0];
3434        const ATTENUATION: [f32; 3] = [0.1, 0.05, 0.01];
3435        const CUTOFF: f32 = 45.0;
3436        const EXPONENT: f32 = 2.0;
3437        const AMBIENT: [f32; 3] = [0.1, 0.1, 0.1];
3438        const DIFFUSE: [f32; 3] = [1.0, 1.0, 1.0];
3439        const SPECULAR: [f32; 3] = [1.0, 1.0, 1.0];
3440
3441        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3442        let info_light = model.light(LIGHT_NAME).unwrap();
3443        let view_light = info_light.view(&model);
3444
3445        assert_eq!(view_light.mode[0], MODE);
3446        assert_eq!(view_light.bodyid[0] as usize, BODYID);
3447        assert_eq!(view_light.targetbodyid[0], -1);
3448        assert_eq!(view_light.r#type[0], TYPE);
3449        assert_eq!(view_light.castshadow[0], CASTSHADOW);
3450        assert_eq!(view_light.active[0], ACTIVE);
3451
3452        assert_eq!(view_light.pos[..], POS);
3453        assert_eq!(view_light.dir[..], DIR);
3454        assert_eq!(view_light.pos0[..], POS0);
3455        assert_eq!(view_light.dir0[..], DIR0);
3456        assert_eq!(view_light.attenuation[..], ATTENUATION);
3457        assert_eq!(view_light.cutoff[0], CUTOFF);
3458        assert_eq!(view_light.exponent[0], EXPONENT);
3459        assert_eq!(view_light.ambient[..], AMBIENT);
3460        assert_eq!(view_light.diffuse[..], DIFFUSE);
3461        assert_eq!(view_light.specular[..], SPECULAR);
3462    }
3463
3464    #[test]
3465    fn test_connect_eq_view() {
3466        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3467
3468        // Take the third equality constraint
3469        let info_eq = model.equality("eq3").unwrap();
3470        let view_eq = info_eq.view(&model);
3471
3472        // Check type
3473        assert_eq!(view_eq.r#type[0], MjtEq::mjEQ_CONNECT);
3474
3475        // Check connected bodies
3476        assert_eq!(view_eq.obj1id[0], model.name_to_id(MjtObj::mjOBJ_BODY, "eq_body3").unwrap() as i32);
3477        assert_eq!(view_eq.obj2id[0], model.name_to_id(MjtObj::mjOBJ_BODY, "eq_body4").unwrap() as i32);
3478        assert_eq!(view_eq.objtype[0], MjtObj::mjOBJ_BODY);
3479
3480        // Check active
3481        assert!(view_eq.active0[0]);
3482
3483        // Check anchor position stored in eq_data
3484        let anchor = &view_eq.data[0..3];
3485        assert_eq!(anchor[0], 0.0);
3486        assert_eq!(anchor[1], 5.0);
3487        assert_eq!(anchor[2], 0.0);
3488    }
3489
3490    #[test]
3491    fn test_hfield_view() {
3492        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3493
3494        // Access the second height field
3495        let info_hf = model.hfield("hf2").unwrap();
3496        let view_hf = info_hf.view(&model);
3497
3498        // Expected values
3499        let expected_size: [f64; 4] = [1.0, 1.0, 1.0, 5.25]; // radius_x, radius_y, elevation_z, base_z
3500        let expected_nrow = 5;
3501        let expected_ncol = 3;
3502        let expected_data: [f32; 15] = [0.0; 15];
3503
3504        // Assertions
3505        assert_eq!(view_hf.size[..], expected_size);
3506        assert_eq!(view_hf.nrow[0], expected_nrow);
3507        assert_eq!(view_hf.ncol[0], expected_ncol);
3508
3509        // hfield_data length should match nrow * ncol
3510        assert_eq!(view_hf.data.as_ref().unwrap().len(), (expected_nrow * expected_ncol) as usize);
3511        assert_eq!(&view_hf.data.as_ref().unwrap()[..], &expected_data[..]);
3512
3513        // Pathadr is -1 (no external file)
3514        assert_eq!(view_hf.pathadr[0], -1);
3515    }
3516
3517    /// Tests [`MjModel::extract_state_into`] for correctness.
3518    #[test]
3519    fn test_state_extract() {
3520        use crate::wrappers::mj_data::MjtState;
3521
3522        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3523        let data = MjData::new(&model);
3524
3525        /* Test of extraction into existing buffer */
3526        // Physics is subset of full physics.
3527        // Extract physics from full physics.
3528        let state_full_physics = data.state(MjtState::mjSTATE_FULLPHYSICS as u32);
3529        let state_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3530
3531        let required_size = model.state_size(MjtState::mjSTATE_PHYSICS as u32);
3532        let mut dst_buffer = vec![0.0; required_size].into_boxed_slice();
3533        let _bytes_written = model.extract_state_into(
3534            &state_full_physics, MjtState::mjSTATE_FULLPHYSICS as u32,
3535            &mut dst_buffer, MjtState::mjSTATE_PHYSICS as u32
3536        );
3537
3538        assert_eq!(state_physics, dst_buffer);
3539
3540        /* Test of extraction into new buffer (internally) */
3541        // Physics is subset of full physics.
3542        // Extract physics from full physics.
3543        let state_full_physics = data.state(MjtState::mjSTATE_FULLPHYSICS as u32);
3544        let state_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3545
3546        let dst_buffer = model.extract_state(
3547            &state_full_physics, MjtState::mjSTATE_FULLPHYSICS as u32,
3548            MjtState::mjSTATE_PHYSICS as u32
3549        );
3550
3551        assert_eq!(state_physics, dst_buffer);
3552    }
3553
3554    /// Tests for the expected panic when giving a source spec that does not match
3555    /// the source array in state extraction.
3556
3557    #[test]
3558    fn test_state_extract_state_invalid_src() {
3559        use crate::wrappers::mj_data::MjtState;
3560
3561        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3562        let data = MjData::new(&model);
3563
3564        let state_full_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3565        let res = model.try_extract_state(
3566            &state_full_physics, MjtState::mjSTATE_FULLPHYSICS as u32,
3567            MjtState::mjSTATE_PHYSICS as u32
3568        );
3569
3570        let err = res.unwrap_err();
3571        assert!(matches!(err, MjModelError::StateSliceLengthMismatch { .. }));
3572    }
3573
3574    #[test]
3575    fn test_state_extract_state_into_invalid_src() {
3576        use crate::wrappers::mj_data::MjtState;
3577
3578        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3579        let data = MjData::new(&model);
3580
3581        let required_size = model.state_size(MjtState::mjSTATE_PHYSICS as u32);
3582        let mut dst_buffer = vec![0.0; required_size].into_boxed_slice();
3583        let state_full_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3584        let res = model.try_extract_state_into(
3585            &state_full_physics, MjtState::mjSTATE_FULLPHYSICS as u32,
3586            &mut dst_buffer, MjtState::mjSTATE_PHYSICS as u32
3587        );
3588
3589        let err = res.unwrap_err();
3590        assert!(matches!(err, MjModelError::StateSliceLengthMismatch { .. }));
3591    }
3592
3593    #[test]
3594    fn test_state_extract_dst_spec_not_subset() {
3595        use crate::wrappers::mj_data::MjtState;
3596
3597        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3598        let data = MjData::new(&model);
3599
3600        let state_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3601        let res = model.try_extract_state(
3602            &state_physics, MjtState::mjSTATE_PHYSICS as u32,
3603            MjtState::mjSTATE_FULLPHYSICS as u32
3604        );
3605
3606        let err = res.unwrap_err();
3607        assert!(matches!(err, MjModelError::SpecNotSubset));
3608    }
3609
3610    #[test]
3611    fn test_state_extract_into_dst_spec_not_subset() {
3612        use crate::wrappers::mj_data::MjtState;
3613
3614        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3615        let data = MjData::new(&model);
3616
3617        let state_physics = data.state(MjtState::mjSTATE_PHYSICS as u32);
3618        let mut dst = vec![0.0; model.state_size(MjtState::mjSTATE_PHYSICS as u32)];
3619
3620        let res = model.try_extract_state_into(
3621            &state_physics, MjtState::mjSTATE_PHYSICS as u32,
3622            &mut dst, MjtState::mjSTATE_FULLPHYSICS as u32
3623        );
3624
3625        let err = res.unwrap_err();
3626        assert!(matches!(err, MjModelError::SpecNotSubset));
3627    }
3628
3629    #[test]
3630    fn test_state_extract_into_dst_buffer_too_small() {
3631        use crate::wrappers::mj_data::MjtState;
3632
3633        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3634        let data = MjData::new(&model);
3635
3636        let state_full = data.state(MjtState::mjSTATE_FULLPHYSICS as u32);
3637        let required = model.state_size(MjtState::mjSTATE_PHYSICS as u32);
3638        // make buffer smaller than required
3639        let mut dst = vec![0.0; required.saturating_sub(1)];
3640
3641        let res = model.try_extract_state_into(
3642            &state_full, MjtState::mjSTATE_FULLPHYSICS as u32,
3643            &mut dst, MjtState::mjSTATE_PHYSICS as u32
3644        );
3645
3646        let err = res.unwrap_err();
3647        assert!(matches!(err, MjModelError::BufferTooSmall { .. }));
3648    }
3649
3650    #[test]
3651    fn test_state_extract_zero_spec() {
3652        use crate::wrappers::mj_data::MjtState;
3653
3654        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3655        let data = MjData::new(&model);
3656
3657        let state_full = data.state(MjtState::mjSTATE_FULLPHYSICS as u32);
3658
3659        // extract zero-sized spec -> empty slice
3660        let dst = model.extract_state(&state_full, MjtState::mjSTATE_FULLPHYSICS as u32, 0u32);
3661        assert!(dst.is_empty());
3662
3663        // extract_into with zero-sized spec -> writes 0 elements
3664        let buf: &mut [f64] = &mut [];
3665        let written = model.extract_state_into(&state_full, MjtState::mjSTATE_FULLPHYSICS as u32, buf, 0u32);
3666        assert_eq!(written, 0);
3667    }
3668
3669    /**************************************************************************/
3670    // Force-cast macro correctness tests for MjModel
3671    /**************************************************************************/
3672
3673    /// Verifies [force]-cast array grouping for body_pos (&[[MjtNum; 3]]),
3674    /// body_quat (&[[MjtNum; 4]]), body_inertia (&[[MjtNum; 3]]), body_invweight0 (&[[MjtNum; 2]]).
3675    #[test]
3676    fn test_force_cast_body_model_arrays() {
3677        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3678        let nbody = model.ffi().nbody as usize;
3679
3680        let body_pos = model.body_pos();
3681        let body_quat = model.body_quat();
3682        let body_inertia = model.body_inertia();
3683        let body_invweight0 = model.body_invweight0();
3684        let body_ipos = model.body_ipos();
3685        let body_iquat = model.body_iquat();
3686
3687        assert_eq!(body_pos.len(), nbody);
3688        assert_eq!(body_quat.len(), nbody);
3689        assert_eq!(body_inertia.len(), nbody);
3690        assert_eq!(body_invweight0.len(), nbody);
3691        assert_eq!(body_ipos.len(), nbody);
3692        assert_eq!(body_iquat.len(), nbody);
3693
3694        // Cross-validate against raw FFI
3695        for i in 0..nbody {
3696            for j in 0..3 {
3697                assert_eq!(body_pos[i][j], unsafe { *model.ffi().body_pos.add(i * 3 + j) },
3698                    "body_pos[{}][{}] mismatch", i, j);
3699            }
3700            for j in 0..4 {
3701                assert_eq!(body_quat[i][j], unsafe { *model.ffi().body_quat.add(i * 4 + j) },
3702                    "body_quat[{}][{}] mismatch", i, j);
3703            }
3704            for j in 0..3 {
3705                assert_eq!(body_inertia[i][j], unsafe { *model.ffi().body_inertia.add(i * 3 + j) },
3706                    "body_inertia[{}][{}] mismatch", i, j);
3707            }
3708            for j in 0..2 {
3709                assert_eq!(body_invweight0[i][j], unsafe { *model.ffi().body_invweight0.add(i * 2 + j) },
3710                    "body_invweight0[{}][{}] mismatch", i, j);
3711            }
3712        }
3713    }
3714
3715    /// Verifies [force]-cast enum for jnt_type (*mut i32 -> *mut MjtJoint).
3716    #[test]
3717    fn test_force_cast_jnt_type_enum() {
3718        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3719        let njnt = model.ffi().njnt as usize;
3720        let jnt_type = model.jnt_type();
3721
3722        assert_eq!(jnt_type.len(), njnt);
3723
3724        // Cross-validate with raw FFI
3725        for i in 0..njnt {
3726            let raw_i32 = unsafe { *model.ffi().jnt_type.add(i) };
3727            let expected: MjtJoint = unsafe { crate::util::force_cast(raw_i32) };
3728            assert_eq!(jnt_type[i], expected,
3729                "jnt_type[{}]: got {:?}, expected {:?} (raw={})", i, jnt_type[i], expected, raw_i32);
3730        }
3731
3732        // Verify known joints: "ball" is free, "rod" is slide
3733        let ball_jnt = model.joint("ball").unwrap();
3734        assert_eq!(jnt_type[ball_jnt.id], MjtJoint::mjJNT_FREE);
3735
3736        let rod_jnt = model.joint("rod").unwrap();
3737        assert_eq!(jnt_type[rod_jnt.id], MjtJoint::mjJNT_SLIDE);
3738    }
3739
3740    /// Verifies [force]-cast bool for jnt_limited (*mut u8 -> *mut bool).
3741    #[test]
3742    fn test_force_cast_jnt_limited_bool() {
3743        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3744        let njnt = model.ffi().njnt as usize;
3745        let jnt_limited = model.jnt_limited();
3746
3747        assert_eq!(jnt_limited.len(), njnt);
3748
3749        // Cross-validate with raw FFI
3750        for i in 0..njnt {
3751            let raw_bool = unsafe { *model.ffi().jnt_limited.add(i) };
3752            assert_eq!(jnt_limited[i], raw_bool,
3753                "jnt_limited[{}] mismatch: bool={}, raw={}", i, jnt_limited[i], raw_bool);
3754        }
3755
3756        // "rod" joint has range="0 1" -> limited=true; "ball" is free -> limited=false
3757        let rod_jnt = model.joint("rod").unwrap();
3758        assert!(jnt_limited[rod_jnt.id]);
3759
3760        let ball_jnt = model.joint("ball").unwrap();
3761        assert!(!jnt_limited[ball_jnt.id]);
3762    }
3763
3764    /// Verifies [force]-cast enum for geom_type (*mut i32 -> *mut MjtGeom).
3765    #[test]
3766    fn test_force_cast_geom_type_enum() {
3767        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3768        let ngeom = model.ffi().ngeom as usize;
3769        let geom_type = model.geom_type();
3770
3771        assert_eq!(geom_type.len(), ngeom);
3772
3773        // Cross-validate
3774        for i in 0..ngeom {
3775            let raw_i32 = unsafe { *model.ffi().geom_type.add(i) };
3776            let expected: MjtGeom = unsafe { crate::util::force_cast(raw_i32) };
3777            assert_eq!(geom_type[i], expected);
3778        }
3779
3780        // Verify known geoms
3781        let sphere_geom = model.geom("green_sphere").unwrap();
3782        assert_eq!(geom_type[sphere_geom.id], MjtGeom::mjGEOM_SPHERE);
3783
3784        let floor_geom = model.geom("floor").unwrap();
3785        assert_eq!(geom_type[floor_geom.id], MjtGeom::mjGEOM_PLANE);
3786
3787        let rod_geom = model.geom("rod").unwrap();
3788        assert_eq!(geom_type[rod_geom.id], MjtGeom::mjGEOM_CYLINDER);
3789    }
3790
3791    /// Verifies [force]-cast for geom_size (&[[MjtNum; 3]]), geom_pos (&[[MjtNum; 3]]),
3792    /// geom_quat (&[[MjtNum; 4]]), geom_rgba (&[[f32; 4]]), geom_friction (&[[MjtNum; 3]]),
3793    /// geom_aabb (&[[MjtNum; 6]]).
3794    #[test]
3795    fn test_force_cast_geom_model_arrays() {
3796        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3797        let ngeom = model.ffi().ngeom as usize;
3798
3799        let geom_size = model.geom_size();
3800        let geom_pos = model.geom_pos();
3801        let geom_quat = model.geom_quat();
3802        let geom_rgba = model.geom_rgba();
3803        let geom_friction = model.geom_friction();
3804        let geom_aabb = model.geom_aabb();
3805
3806        assert_eq!(geom_size.len(), ngeom);
3807        assert_eq!(geom_pos.len(), ngeom);
3808        assert_eq!(geom_quat.len(), ngeom);
3809        assert_eq!(geom_rgba.len(), ngeom);
3810        assert_eq!(geom_friction.len(), ngeom);
3811        assert_eq!(geom_aabb.len(), ngeom);
3812
3813        // Cross-validate all against FFI
3814        for i in 0..ngeom {
3815            for j in 0..3 {
3816                assert_eq!(geom_size[i][j], unsafe { *model.ffi().geom_size.add(i * 3 + j) });
3817                assert_eq!(geom_pos[i][j], unsafe { *model.ffi().geom_pos.add(i * 3 + j) });
3818                assert_eq!(geom_friction[i][j], unsafe { *model.ffi().geom_friction.add(i * 3 + j) });
3819            }
3820            for j in 0..4 {
3821                assert_eq!(geom_quat[i][j], unsafe { *model.ffi().geom_quat.add(i * 4 + j) });
3822                assert_eq!(geom_rgba[i][j], unsafe { *model.ffi().geom_rgba.add(i * 4 + j) });
3823            }
3824            for j in 0..6 {
3825                assert_eq!(geom_aabb[i][j], unsafe { *model.ffi().geom_aabb.add(i * 6 + j) });
3826            }
3827        }
3828
3829        // Verify a known geom: "green_sphere" has rgba="0 1 0 1" and size="0.1"
3830        let gs = model.geom("green_sphere").unwrap();
3831        assert_eq!(geom_rgba[gs.id], [0.0f32, 1.0, 0.0, 1.0]);
3832        assert_relative_eq!(geom_size[gs.id][0], 0.1, epsilon = 1e-9);
3833    }
3834
3835    /// Verifies [force]-cast for camera arrays: cam_mode (MjtCamLight), cam_projection (MjtProjection),
3836    /// cam_resolution (&[[i32; 2]]), cam_sensorsize (&[[f32; 2]]), cam_intrinsic (&[[f32; 4]]),
3837    /// cam_mat0 (&[[MjtNum; 9]]).
3838    #[test]
3839    fn test_force_cast_camera_model_arrays() {
3840        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3841        let ncam = model.ffi().ncam as usize;
3842
3843        if ncam == 0 {
3844            // The EXAMPLE_MODEL has a camera, but guard just in case
3845            return;
3846        }
3847
3848        let cam_mode = model.cam_mode();
3849        let cam_projection = model.cam_projection();
3850        let cam_resolution = model.cam_resolution();
3851        let cam_sensorsize = model.cam_sensorsize();
3852        let cam_intrinsic = model.cam_intrinsic();
3853        let cam_mat0 = model.cam_mat0();
3854
3855        assert_eq!(cam_mode.len(), ncam);
3856        assert_eq!(cam_projection.len(), ncam);
3857        assert_eq!(cam_resolution.len(), ncam);
3858        assert_eq!(cam_sensorsize.len(), ncam);
3859        assert_eq!(cam_intrinsic.len(), ncam);
3860        assert_eq!(cam_mat0.len(), ncam);
3861
3862        // Cross-validate enum casts against raw FFI
3863        for i in 0..ncam {
3864            let raw_mode = unsafe { *model.ffi().cam_mode.add(i) };
3865            let expected_mode: MjtCamLight = unsafe { crate::util::force_cast(raw_mode) };
3866            assert_eq!(cam_mode[i], expected_mode);
3867
3868            let raw_proj = unsafe { *model.ffi().cam_projection.add(i) };
3869            let expected_proj: MjtProjection = unsafe { crate::util::force_cast(raw_proj) };
3870            assert_eq!(cam_projection[i], expected_proj);
3871
3872            for j in 0..2 {
3873                assert_eq!(cam_resolution[i][j], unsafe { *model.ffi().cam_resolution.add(i * 2 + j) });
3874                assert_eq!(cam_sensorsize[i][j], unsafe { *model.ffi().cam_sensorsize.add(i * 2 + j) });
3875            }
3876            for j in 0..4 {
3877                assert_eq!(cam_intrinsic[i][j], unsafe { *model.ffi().cam_intrinsic.add(i * 4 + j) });
3878            }
3879            for j in 0..9 {
3880                assert_eq!(cam_mat0[i][j], unsafe { *model.ffi().cam_mat0.add(i * 9 + j) });
3881            }
3882        }
3883
3884        // Verify known camera: "cam1" has resolution="100 200"
3885        let cam1 = model.camera("cam1").unwrap();
3886        assert_eq!(cam_resolution[cam1.id], [100, 200]);
3887    }
3888
3889    /// Verifies [force]-cast for bvh_child (&[[i32; 2]]) and bvh_aabb (&[[MjtNum; 6]]).
3890    #[test]
3891    fn test_force_cast_bvh_model_arrays() {
3892        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3893        let nbvh = model.ffi().nbvh as usize;
3894
3895        let bvh_child = model.bvh_child();
3896        assert_eq!(bvh_child.len(), nbvh);
3897
3898        for i in 0..nbvh {
3899            for j in 0..2 {
3900                assert_eq!(bvh_child[i][j], unsafe { *model.ffi().bvh_child.add(i * 2 + j) });
3901            }
3902        }
3903
3904        let nbvhstatic = model.ffi().nbvhstatic as usize;
3905        let bvh_aabb = model.bvh_aabb();
3906        assert_eq!(bvh_aabb.len(), nbvhstatic);
3907
3908        for i in 0..nbvhstatic {
3909            for j in 0..6 {
3910                assert_eq!(bvh_aabb[i][j], unsafe { *model.ffi().bvh_aabb.add(i * 6 + j) });
3911            }
3912        }
3913    }
3914
3915    /// Verifies [force]-cast enum: body_sameframe and geom_sameframe (MjtSameFrame).
3916    #[test]
3917    fn test_force_cast_sameframe_enum() {
3918        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3919        let nbody = model.ffi().nbody as usize;
3920        let ngeom = model.ffi().ngeom as usize;
3921
3922        let body_sameframe = model.body_sameframe();
3923        let geom_sameframe = model.geom_sameframe();
3924
3925        assert_eq!(body_sameframe.len(), nbody);
3926        assert_eq!(geom_sameframe.len(), ngeom);
3927
3928        for i in 0..nbody {
3929            let raw = unsafe { *model.ffi().body_sameframe.add(i) };
3930            let expected: MjtSameFrame = unsafe { crate::util::force_cast(raw) };
3931            assert_eq!(body_sameframe[i], expected, "body_sameframe[{}] mismatch", i);
3932        }
3933
3934        for i in 0..ngeom {
3935            let raw = unsafe { *model.ffi().geom_sameframe.add(i) };
3936            let expected: MjtSameFrame = unsafe { crate::util::force_cast(raw) };
3937            assert_eq!(geom_sameframe[i], expected, "geom_sameframe[{}] mismatch", i);
3938        }
3939    }
3940
3941    /// Verifies [force]-cast for equality constraint arrays: eq_type (MjtEq),
3942    /// eq_objtype (MjtObj), eq_active0 (bool), eq_data (&[[MjtNum; mjNEQDATA]]).
3943    #[test]
3944    fn test_force_cast_equality_model_arrays() {
3945        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3946        let neq = model.ffi().neq as usize;
3947
3948        if neq == 0 {
3949            return;
3950        }
3951
3952        let eq_type = model.eq_type();
3953        let eq_objtype = model.eq_objtype();
3954        let eq_active0 = model.eq_active0();
3955
3956        assert_eq!(eq_type.len(), neq);
3957        assert_eq!(eq_objtype.len(), neq);
3958        assert_eq!(eq_active0.len(), neq);
3959
3960        // Cross-validate enum casts
3961        for i in 0..neq {
3962            let raw_type = unsafe { *model.ffi().eq_type.add(i) };
3963            let expected_type: MjtEq = unsafe { crate::util::force_cast(raw_type) };
3964            assert_eq!(eq_type[i], expected_type);
3965
3966            let raw_objtype = unsafe { *model.ffi().eq_objtype.add(i) };
3967            let expected_objtype: MjtObj = unsafe { crate::util::force_cast(raw_objtype) };
3968            assert_eq!(eq_objtype[i], expected_objtype);
3969
3970            let raw_active = unsafe { *model.ffi().eq_active0.add(i) };
3971            assert_eq!(eq_active0[i], raw_active);
3972        }
3973
3974        // Verify known equality: "eq1" is a connect constraint
3975        let eq1 = model.equality("eq1").unwrap();
3976        assert_eq!(eq_type[eq1.id], MjtEq::mjEQ_CONNECT);
3977        assert_eq!(eq_objtype[eq1.id], MjtObj::mjOBJ_BODY);
3978        assert!(eq_active0[eq1.id]);
3979    }
3980
3981    /// Verifies [force]-cast for sensor arrays: sensor_type (MjtSensor), sensor_datatype (MjtDataType),
3982    /// sensor_needstage (MjtStage), sensor_objtype (MjtObj), sensor_reftype (MjtObj).
3983    #[test]
3984    fn test_force_cast_sensor_model_enums() {
3985        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
3986        let nsensor = model.ffi().nsensor as usize;
3987
3988        if nsensor == 0 {
3989            return;
3990        }
3991
3992        let sensor_type = model.sensor_type();
3993        let sensor_datatype = model.sensor_datatype();
3994        let sensor_needstage = model.sensor_needstage();
3995        let sensor_objtype = model.sensor_objtype();
3996        let sensor_reftype = model.sensor_reftype();
3997
3998        assert_eq!(sensor_type.len(), nsensor);
3999        assert_eq!(sensor_datatype.len(), nsensor);
4000        assert_eq!(sensor_needstage.len(), nsensor);
4001        assert_eq!(sensor_objtype.len(), nsensor);
4002        assert_eq!(sensor_reftype.len(), nsensor);
4003
4004        for i in 0..nsensor {
4005            let raw_type = unsafe { *model.ffi().sensor_type.add(i) };
4006            let raw_datatype = unsafe { *model.ffi().sensor_datatype.add(i) };
4007            let raw_needstage = unsafe { *model.ffi().sensor_needstage.add(i) };
4008            let raw_objtype = unsafe { *model.ffi().sensor_objtype.add(i) };
4009            let raw_reftype = unsafe { *model.ffi().sensor_reftype.add(i) };
4010
4011            assert_eq!(sensor_type[i], unsafe { crate::util::force_cast::<_, MjtSensor>(raw_type) });
4012            assert_eq!(sensor_datatype[i], unsafe { crate::util::force_cast::<_, MjtDataType>(raw_datatype) });
4013            assert_eq!(sensor_needstage[i], unsafe { crate::util::force_cast::<_, MjtStage>(raw_needstage) });
4014            assert_eq!(sensor_objtype[i], unsafe { crate::util::force_cast::<_, MjtObj>(raw_objtype) });
4015            assert_eq!(sensor_reftype[i], unsafe { crate::util::force_cast::<_, MjtObj>(raw_reftype) });
4016        }
4017
4018        // Verify known sensor: "touch" is a touch sensor on a site
4019        let touch = model.sensor("touch").unwrap();
4020        assert_eq!(sensor_type[touch.id], MjtSensor::mjSENS_TOUCH);
4021        assert_eq!(sensor_objtype[touch.id], MjtObj::mjOBJ_SITE);
4022    }
4023
4024    /// Verifies [force]-cast for actuator enum arrays: trntype (MjtTrn), dyntype (MjtDyn),
4025    /// gaintype (MjtGain), biastype (MjtBias), and bool ctrllimited, forcelimited, actlimited.
4026    #[test]
4027    fn test_force_cast_actuator_model_enums_and_bools() {
4028        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4029        let nactuator = model.ffi().nactuator as usize;
4030        let nu = model.ffi().nu as usize;
4031
4032        if nactuator == 0 {
4033            return;
4034        }
4035
4036        let trntype = model.actuator_trntype();
4037        let dyntype = model.actuator_dyntype();
4038        let gaintype = model.actuator_gaintype();
4039        let biastype = model.actuator_biastype();
4040        let ctrllimited = model.actuator_ctrllimited();
4041        let forcelimited = model.actuator_forcelimited();
4042        let actlimited = model.actuator_actlimited();
4043        let actearly = model.actuator_actearly();
4044
4045        assert_eq!(trntype.len(), nactuator);
4046        assert_eq!(dyntype.len(), nactuator);
4047        assert_eq!(gaintype.len(), nactuator);
4048        assert_eq!(biastype.len(), nactuator);
4049        assert_eq!(ctrllimited.len(), nu);
4050        assert_eq!(forcelimited.len(), nactuator);
4051        assert_eq!(actlimited.len(), nactuator);
4052        assert_eq!(actearly.len(), nactuator);
4053
4054        for i in 0..nactuator {
4055            // Enum cross-validation
4056            assert_eq!(trntype[i], unsafe { crate::util::force_cast::<_, MjtTrn>(*model.ffi().actuator_trntype.add(i)) });
4057            assert_eq!(dyntype[i], unsafe { crate::util::force_cast::<_, MjtDyn>(*model.ffi().actuator_dyntype.add(i)) });
4058            assert_eq!(gaintype[i], unsafe { crate::util::force_cast::<_, MjtGain>(*model.ffi().actuator_gaintype.add(i)) });
4059            assert_eq!(biastype[i], unsafe { crate::util::force_cast::<_, MjtBias>(*model.ffi().actuator_biastype.add(i)) });
4060
4061            // Bool cross-validation
4062            let raw_ctrllimited = unsafe { *model.ffi().actuator_ctrllimited.add(i) };
4063            assert_eq!(ctrllimited[i], raw_ctrllimited);
4064            let raw_forcelimited = unsafe { *model.ffi().actuator_forcelimited.add(i) };
4065            assert_eq!(forcelimited[i], raw_forcelimited);
4066            let raw_actlimited = unsafe { *model.ffi().actuator_actlimited.add(i) };
4067            assert_eq!(actlimited[i], raw_actlimited);
4068            let raw_actearly = unsafe { *model.ffi().actuator_actearly.add(i) };
4069            assert_eq!(actearly[i], raw_actearly);
4070        }
4071
4072        // Verify known actuator: "slider" has biastype=affine, gaintype=fixed, ctrllimited=true
4073        let slider = model.actuator("slider").unwrap();
4074        assert_eq!(biastype[slider.id], MjtBias::mjBIAS_AFFINE);
4075        assert_eq!(gaintype[slider.id], MjtGain::mjGAIN_FIXED);
4076        assert!(ctrllimited[slider.id]);
4077    }
4078
4079    /// Verifies [force]-cast for actuator parameter arrays: dynprm, gainprm, biasprm, ctrlrange,
4080    /// gear (&[[MjtNum; 6]]).
4081    #[test]
4082    fn test_force_cast_actuator_param_arrays() {
4083        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4084        let nactuator = model.ffi().nactuator as usize;
4085        let nout = model.ffi().nout as usize;
4086        let nu = model.ffi().nu as usize;
4087
4088        let dynprm = model.actuator_dynprm();
4089        let ctrlrange = model.actuator_ctrlrange();
4090        let gear = model.actuator_gear();
4091        let trnid = model.actuator_trnid();
4092
4093        assert_eq!(dynprm.len(), nactuator);
4094        assert_eq!(ctrlrange.len(), nu);
4095        assert_eq!(gear.len(), nout);
4096        assert_eq!(trnid.len(), nactuator);
4097
4098        // Verify "slider" dynprm and ctrlrange
4099        let slider = model.actuator("slider").unwrap();
4100        assert_eq!(dynprm[slider.id][0..10], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
4101        assert_eq!(ctrlrange[slider.id], [0.0, 1.0]);
4102
4103        // Verify "slider2" has reversed dynprm
4104        let slider2 = model.actuator("slider2").unwrap();
4105        assert_eq!(dynprm[slider2.id][0..10], [10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0]);
4106
4107        // Cross-validate FFI for gear (stride 6), trnid (stride 2) and ctrlrange (stride 2)
4108        for i in 0..nout {
4109            for j in 0..6 {
4110                assert_eq!(gear[i][j], unsafe { *model.ffi().actuator_gear.add(i * 6 + j) });
4111            }
4112        }
4113        for i in 0..nactuator {
4114            for j in 0..2 {
4115                assert_eq!(trnid[i][j], unsafe { *model.ffi().actuator_trnid.add(i * 2 + j) });
4116            }
4117        }
4118        for i in 0..nu {
4119            for j in 0..2 {
4120                assert_eq!(ctrlrange[i][j], unsafe { *model.ffi().actuator_ctrlrange.add(i * 2 + j) });
4121            }
4122        }
4123    }
4124
4125    /// Verifies [force]-cast for tendon bools and arrays: tendon_limited (bool),
4126    /// tendon_range (&[[MjtNum; 2]]), tendon_rgba (&[[f32; 4]]), tendon_treeid (&[[i32; 2]]).
4127    #[test]
4128    fn test_force_cast_tendon_model_arrays() {
4129        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4130        let ntendon = model.ffi().ntendon as usize;
4131
4132        if ntendon == 0 {
4133            return;
4134        }
4135
4136        let tendon_limited = model.tendon_limited();
4137        let tendon_range = model.tendon_range();
4138        let tendon_rgba = model.tendon_rgba();
4139        let tendon_treeid = model.tendon_treeid();
4140        let tendon_lengthspring = model.tendon_lengthspring();
4141
4142        assert_eq!(tendon_limited.len(), ntendon);
4143        assert_eq!(tendon_range.len(), ntendon);
4144        assert_eq!(tendon_rgba.len(), ntendon);
4145        assert_eq!(tendon_treeid.len(), ntendon);
4146        assert_eq!(tendon_lengthspring.len(), ntendon);
4147
4148        for i in 0..ntendon {
4149            let raw_limited = unsafe { *model.ffi().tendon_limited.add(i) };
4150            assert_eq!(tendon_limited[i], raw_limited);
4151
4152            for j in 0..2 {
4153                assert_eq!(tendon_range[i][j], unsafe { *model.ffi().tendon_range.add(i * 2 + j) });
4154                assert_eq!(tendon_treeid[i][j], unsafe { *model.ffi().tendon_treeid.add(i * 2 + j) });
4155                assert_eq!(tendon_lengthspring[i][j], unsafe { *model.ffi().tendon_lengthspring.add(i * 2 + j) });
4156            }
4157
4158            for j in 0..4 {
4159                assert_eq!(tendon_rgba[i][j], unsafe { *model.ffi().tendon_rgba.add(i * 4 + j) });
4160            }
4161        }
4162
4163        // Verify known tendon: "tendon2" limited=true, range=(0,1), rgba=(0, 0.1, 1, 1)
4164        let ten2 = model.tendon("tendon2").unwrap();
4165        assert!(tendon_limited[ten2.id]);
4166        assert_eq!(tendon_range[ten2.id], [0.0, 1.0]);
4167        assert_relative_eq!(tendon_rgba[ten2.id][0], 0.0f32, epsilon = 1e-6);
4168        assert_relative_eq!(tendon_rgba[ten2.id][1], 0.1f32, epsilon = 1e-6);
4169    }
4170
4171    /// Verifies [force]-cast for texture enum: tex_type (MjtTexture), tex_colorspace (MjtColorSpace).
4172    #[test]
4173    fn test_force_cast_texture_model_enums() {
4174        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4175        let ntex = model.ffi().ntex as usize;
4176
4177        if ntex == 0 {
4178            return;
4179        }
4180
4181        let tex_type = model.tex_type();
4182        let tex_colorspace = model.tex_colorspace();
4183
4184        assert_eq!(tex_type.len(), ntex);
4185        assert_eq!(tex_colorspace.len(), ntex);
4186
4187        for i in 0..ntex {
4188            let raw_type = unsafe { *model.ffi().tex_type.add(i) };
4189            let expected_type: MjtTexture = unsafe { crate::util::force_cast(raw_type) };
4190            assert_eq!(tex_type[i], expected_type);
4191
4192            let raw_cs = unsafe { *model.ffi().tex_colorspace.add(i) };
4193            let expected_cs: MjtColorSpace = unsafe { crate::util::force_cast(raw_cs) };
4194            assert_eq!(tex_colorspace[i], expected_cs);
4195        }
4196
4197        // "wall_tex" is 2D, sRGB
4198        let wall_tex = model.texture("wall_tex").unwrap();
4199        assert_eq!(tex_type[wall_tex.id], MjtTexture::mjTEXTURE_2D);
4200        assert_eq!(tex_colorspace[wall_tex.id], MjtColorSpace::mjCOLORSPACE_SRGB);
4201    }
4202
4203    /// Verifies [force]-cast for material bools and f32 arrays: mat_texuniform (bool),
4204    /// mat_texrepeat (&[[f32; 2]]), mat_rgba (&[[f32; 4]]).
4205    #[test]
4206    fn test_force_cast_material_model_arrays() {
4207        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4208        let nmat = model.ffi().nmat as usize;
4209
4210        if nmat == 0 {
4211            return;
4212        }
4213
4214        let mat_texuniform = model.mat_texuniform();
4215        let mat_texrepeat = model.mat_texrepeat();
4216        let mat_rgba = model.mat_rgba();
4217
4218        assert_eq!(mat_texuniform.len(), nmat);
4219        assert_eq!(mat_texrepeat.len(), nmat);
4220        assert_eq!(mat_rgba.len(), nmat);
4221
4222        for i in 0..nmat {
4223            let raw_uniform = unsafe { *model.ffi().mat_texuniform.add(i) };
4224            assert_eq!(mat_texuniform[i], raw_uniform);
4225
4226            for j in 0..2 {
4227                assert_eq!(mat_texrepeat[i][j], unsafe { *model.ffi().mat_texrepeat.add(i * 2 + j) });
4228            }
4229            for j in 0..4 {
4230                assert_eq!(mat_rgba[i][j], unsafe { *model.ffi().mat_rgba.add(i * 4 + j) });
4231            }
4232        }
4233
4234        // "also_wood_material" has texuniform=false, texrepeat=[2,2], rgba=[0.8,0.5,0.3,1.0]
4235        let mat = model.material("also_wood_material").unwrap();
4236        assert!(!mat_texuniform[mat.id]);
4237        assert_eq!(mat_texrepeat[mat.id], [2.0f32, 2.0]);
4238        assert_eq!(mat_rgba[mat.id], [0.8f32, 0.5, 0.3, 1.0]);
4239    }
4240
4241    /// Verifies [force]-cast for light arrays: light mode/type enums, light_castshadow/active bools,
4242    /// light_attenuation (&[[f32; 3]]), light_ambient/diffuse/specular (&[[f32; 3]]),
4243    /// light_pos/dir (&[[MjtNum; 3]]).
4244    #[test]
4245    fn test_force_cast_light_model_arrays() {
4246        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4247        let nlight = model.ffi().nlight as usize;
4248
4249        if nlight == 0 {
4250            return;
4251        }
4252
4253        let light_mode = model.light_mode();
4254        let light_type = model.light_type();
4255        let light_castshadow = model.light_castshadow();
4256        let light_active = model.light_active();
4257        let light_attenuation = model.light_attenuation();
4258        let light_ambient = model.light_ambient();
4259        let light_diffuse = model.light_diffuse();
4260        let light_specular = model.light_specular();
4261        let light_pos = model.light_pos();
4262        let light_dir = model.light_dir();
4263
4264        assert_eq!(light_mode.len(), nlight);
4265        assert_eq!(light_type.len(), nlight);
4266        assert_eq!(light_castshadow.len(), nlight);
4267        assert_eq!(light_active.len(), nlight);
4268
4269        for i in 0..nlight {
4270            assert_eq!(light_mode[i], unsafe { crate::util::force_cast::<_, MjtCamLight>(*model.ffi().light_mode.add(i)) });
4271            assert_eq!(light_type[i], unsafe { crate::util::force_cast::<_, MjtLightType>(*model.ffi().light_type.add(i)) });
4272
4273            let raw_shadow = unsafe { *model.ffi().light_castshadow.add(i) };
4274            assert_eq!(light_castshadow[i], raw_shadow);
4275            let raw_active = unsafe { *model.ffi().light_active.add(i) };
4276            assert_eq!(light_active[i], raw_active);
4277
4278            for j in 0..3 {
4279                assert_eq!(light_attenuation[i][j], unsafe { *model.ffi().light_attenuation.add(i * 3 + j) });
4280                assert_eq!(light_ambient[i][j], unsafe { *model.ffi().light_ambient.add(i * 3 + j) });
4281                assert_eq!(light_diffuse[i][j], unsafe { *model.ffi().light_diffuse.add(i * 3 + j) });
4282                assert_eq!(light_specular[i][j], unsafe { *model.ffi().light_specular.add(i * 3 + j) });
4283                assert_eq!(light_pos[i][j], unsafe { *model.ffi().light_pos.add(i * 3 + j) });
4284                assert_eq!(light_dir[i][j], unsafe { *model.ffi().light_dir.add(i * 3 + j) });
4285            }
4286        }
4287
4288        // Verify known: "lamp_light2" is spot, mode=fixed, castshadow=true
4289        let l2 = model.light("lamp_light2").unwrap();
4290        assert_eq!(light_mode[l2.id], MjtCamLight::mjCAMLIGHT_FIXED);
4291        assert_eq!(light_type[l2.id], MjtLightType::mjLIGHT_SPOT);
4292        assert!(light_castshadow[l2.id]);
4293    }
4294
4295    /// Verifies [force]-cast for the site arrays: site_type (MjtGeom), site_sameframe (MjtSameFrame),
4296    /// site_size (&[[MjtNum; 3]]), site_pos (&[[MjtNum; 3]]), site_quat (&[[MjtNum; 4]]),
4297    /// site_rgba (&[[f32; 4]]).
4298    #[test]
4299    fn test_force_cast_site_model_arrays() {
4300        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4301        let nsite = model.ffi().nsite as usize;
4302
4303        let site_type = model.site_type();
4304        let site_sameframe = model.site_sameframe();
4305        let site_size = model.site_size();
4306        let site_pos = model.site_pos();
4307        let site_quat = model.site_quat();
4308        let site_rgba = model.site_rgba();
4309
4310        assert_eq!(site_type.len(), nsite);
4311        assert_eq!(site_sameframe.len(), nsite);
4312        assert_eq!(site_size.len(), nsite);
4313        assert_eq!(site_pos.len(), nsite);
4314        assert_eq!(site_quat.len(), nsite);
4315        assert_eq!(site_rgba.len(), nsite);
4316
4317        for i in 0..nsite {
4318            assert_eq!(site_type[i], unsafe { crate::util::force_cast::<_, MjtGeom>(*model.ffi().site_type.add(i)) });
4319            assert_eq!(site_sameframe[i], unsafe { crate::util::force_cast::<_, MjtSameFrame>(*model.ffi().site_sameframe.add(i)) });
4320
4321            for j in 0..3 {
4322                assert_eq!(site_size[i][j], unsafe { *model.ffi().site_size.add(i * 3 + j) });
4323                assert_eq!(site_pos[i][j], unsafe { *model.ffi().site_pos.add(i * 3 + j) });
4324            }
4325            for j in 0..4 {
4326                assert_eq!(site_quat[i][j], unsafe { *model.ffi().site_quat.add(i * 4 + j) });
4327                assert_eq!(site_rgba[i][j], unsafe { *model.ffi().site_rgba.add(i * 4 + j) });
4328            }
4329        }
4330    }
4331
4332    /// Verifies [force]-cast for joint solver arrays: jnt_solref (&[[MjtNum; mjNREF]]),
4333    /// jnt_solimp (&[[MjtNum; mjNIMP]]), jnt_pos (&[[MjtNum; 3]]), jnt_axis (&[[MjtNum; 3]]),
4334    /// jnt_range (&[[MjtNum; 2]]).
4335    #[test]
4336    fn test_force_cast_joint_model_solver_arrays() {
4337        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4338        let njnt = model.ffi().njnt as usize;
4339
4340        let jnt_solref = model.jnt_solref();
4341        let jnt_solimp = model.jnt_solimp();
4342        let jnt_pos = model.jnt_pos();
4343        let jnt_axis = model.jnt_axis();
4344        let jnt_range = model.jnt_range();
4345
4346        assert_eq!(jnt_solref.len(), njnt);
4347        assert_eq!(jnt_solimp.len(), njnt);
4348        assert_eq!(jnt_pos.len(), njnt);
4349        assert_eq!(jnt_axis.len(), njnt);
4350        assert_eq!(jnt_range.len(), njnt);
4351
4352        let nref = mjNREF as usize;
4353        let nimp = mjNIMP as usize;
4354
4355        for i in 0..njnt {
4356            for j in 0..nref {
4357                assert_eq!(jnt_solref[i][j], unsafe { *model.ffi().jnt_solref.add(i * nref + j) });
4358            }
4359            for j in 0..nimp {
4360                assert_eq!(jnt_solimp[i][j], unsafe { *model.ffi().jnt_solimp.add(i * nimp + j) });
4361            }
4362            for j in 0..3 {
4363                assert_eq!(jnt_pos[i][j], unsafe { *model.ffi().jnt_pos.add(i * 3 + j) });
4364                assert_eq!(jnt_axis[i][j], unsafe { *model.ffi().jnt_axis.add(i * 3 + j) });
4365            }
4366            for j in 0..2 {
4367                assert_eq!(jnt_range[i][j], unsafe { *model.ffi().jnt_range.add(i * 2 + j) });
4368            }
4369        }
4370
4371        // "rod" has axis="0 1 0", range="0 1"
4372        let rod = model.joint("rod").unwrap();
4373        assert_eq!(&jnt_axis[rod.id][..], &[0.0, 1.0, 0.0]);
4374        assert_eq!(&jnt_range[rod.id][..], &[0.0, 1.0]);
4375    }
4376
4377    /// Verifies [force]-cast for view fields with enum types: joint view r#type,
4378    /// geom view r#type, sensor view r#type + objtype, actuator view trntype + biastype.
4379    /// Tests that the view_creator! macro correctly passes [force] tokens through.
4380    #[test]
4381    fn test_force_cast_view_enum_fields() {
4382        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4383
4384        // Joint type via view
4385        let rod_info = model.joint("rod").unwrap();
4386        let rod_view = rod_info.view(&model);
4387        assert_eq!(rod_view.r#type[0], MjtJoint::mjJNT_SLIDE);
4388        assert!(rod_view.limited[0]);
4389
4390        // Geom type via view
4391        let ball2_info = model.geom("ball2").unwrap();
4392        let ball2_view = ball2_info.view(&model);
4393        assert_eq!(ball2_view.r#type[0], MjtGeom::mjGEOM_SPHERE);
4394
4395        // Sensor type via view
4396        let touch_info = model.sensor("touch").unwrap();
4397        let touch_view = touch_info.view(&model);
4398        assert_eq!(touch_view.r#type[0], MjtSensor::mjSENS_TOUCH);
4399        assert_eq!(touch_view.objtype[0], MjtObj::mjOBJ_SITE);
4400
4401        // Actuator types via view
4402        let slider_info = model.actuator("slider").unwrap();
4403        let slider_view = slider_info.view(&model);
4404        assert_eq!(slider_view.trntype[0], MjtTrn::mjTRN_JOINT);
4405        assert_eq!(slider_view.biastype[0], MjtBias::mjBIAS_AFFINE);
4406        assert_eq!(slider_view.gaintype[0], MjtGain::mjGAIN_FIXED);
4407        assert!(slider_view.ctrllimited[0]);
4408
4409        // Mutable enum roundtrip via view
4410        let mut slider_view_mut = slider_info.view_mut(&mut model);
4411        slider_view_mut.biastype[0] = MjtBias::mjBIAS_NONE;
4412        let slider_view2 = slider_info.view(&model);
4413        assert_eq!(slider_view2.biastype[0], MjtBias::mjBIAS_NONE);
4414    }
4415
4416    /// Verifies mutable model-array roundtrip via info views.
4417    /// Tests that mutations through `view_mut` are reflected in the flat arrays and FFI.
4418    #[test]
4419    fn test_model_view_mut_roundtrip() {
4420        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4421
4422        // Mutate body_pos via mutable info view.
4423        let ball2_info = model.body("ball2").unwrap();
4424        let ball2_id = ball2_info.id;
4425        let orig_pos = model.body_pos()[ball2_id];
4426        assert_eq!(orig_pos, [0.5, 0.0, 0.0]);
4427
4428        {
4429            let mut body_view = ball2_info.view_mut(&mut model);
4430            body_view.pos.copy_from_slice(&[99.0, 88.0, 77.0]);
4431        }
4432        assert_eq!(model.body_pos()[ball2_id], [99.0, 88.0, 77.0]);
4433
4434        // Verify FFI side
4435        for j in 0..3 {
4436            let ffi_val = unsafe { *model.ffi().body_pos.add(ball2_id * 3 + j) };
4437            assert_eq!(ffi_val, [99.0, 88.0, 77.0][j]);
4438        }
4439
4440        // Mutate geom_rgba via mutable info view.
4441        let gs_info = model.geom("green_sphere").unwrap();
4442        let gs_id = gs_info.id;
4443        {
4444            let mut geom_view = gs_info.view_mut(&mut model);
4445            geom_view.rgba.copy_from_slice(&[1.0f32, 0.0, 0.0, 0.5]);
4446        }
4447        assert_eq!(model.geom_rgba()[gs_id], [1.0f32, 0.0, 0.0, 0.5]);
4448        for j in 0..4 {
4449            assert_eq!(unsafe { *model.ffi().geom_rgba.add(gs_id * 4 + j) }, [1.0f32, 0.0, 0.0, 0.5][j]);
4450        }
4451    }
4452
4453    /// Verifies [force]-cast sublen_dep variant in model: key_qpos, key_qvel, key_act, key_ctrl.
4454    /// These have variable inner dimensions dependent on nq, nv, na, nu.
4455    #[test]
4456    fn test_force_cast_sublen_dep_key_arrays() {
4457        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4458        let nkey = model.ffi().nkey as usize;
4459        let nq = model.ffi().nq as usize;
4460        let nv = model.ffi().nv as usize;
4461        let na = model.ffi().na as usize;
4462        let nu = model.ffi().nu as usize;
4463
4464        assert!(nkey >= 2, "EXAMPLE_MODEL must have at least 2 keyframes");
4465
4466        let key_qpos = model.key_qpos();
4467        let key_qvel = model.key_qvel();
4468        let key_act = model.key_act();
4469        let key_ctrl = model.key_ctrl();
4470
4471        // Total flat length must be nkey * inner_dim
4472        assert_eq!(key_qpos.len(), nkey * nq);
4473        assert_eq!(key_qvel.len(), nkey * nv);
4474        assert_eq!(key_act.len(), nkey * na);
4475        assert_eq!(key_ctrl.len(), nkey * nu);
4476
4477        // Cross-validate with FFI
4478        for i in 0..(nkey * nq) {
4479            assert_eq!(key_qpos[i], unsafe { *model.ffi().key_qpos.add(i) });
4480        }
4481        for i in 0..(nkey * nv) {
4482            assert_eq!(key_qvel[i], unsafe { *model.ffi().key_qvel.add(i) });
4483        }
4484        for i in 0..(nkey * nu) {
4485            assert_eq!(key_ctrl[i], unsafe { *model.ffi().key_ctrl.add(i) });
4486        }
4487    }
4488
4489    /// Tests empty model edge case: model with no optional objects should return
4490    /// empty slices for all force-cast enum/bool/array fields.
4491    #[test]
4492    fn test_force_cast_minimal_model_edge_case() {
4493        let xml = "<mujoco><worldbody><body><joint type='free'/><geom size='0.1'/></body></worldbody></mujoco>";
4494        let model = MjModel::from_xml_string(xml).unwrap();
4495
4496        // No equalities, no tendons, no actuators, no sensors, no cameras, no lights, no textures, no materials
4497        assert_eq!(model.ffi().neq, 0);
4498        assert_eq!(model.ffi().ntendon, 0);
4499        assert_eq!(model.ffi().nu, 0);
4500        assert_eq!(model.ffi().nsensor, 0);
4501        assert_eq!(model.ffi().ncam, 0);
4502        assert_eq!(model.ffi().ntex, 0);
4503        assert_eq!(model.ffi().nmat, 0);
4504
4505        // All force-cast slices should be empty
4506        assert!(model.eq_type().is_empty());
4507        assert!(model.eq_active0().is_empty());
4508        assert!(model.tendon_limited().is_empty());
4509        assert!(model.tendon_rgba().is_empty());
4510        assert!(model.actuator_trntype().is_empty());
4511        assert!(model.actuator_ctrllimited().is_empty());
4512        assert!(model.sensor_type().is_empty());
4513        assert!(model.cam_mode().is_empty());
4514        assert!(model.cam_resolution().is_empty());
4515        assert!(model.tex_type().is_empty());
4516        assert!(model.mat_texuniform().is_empty());
4517        assert!(model.mat_rgba().is_empty());
4518
4519        // But body/geom/joint arrays should work (always have at least world body)
4520        let nbody = model.ffi().nbody as usize;
4521        assert!(nbody >= 2);
4522        assert_eq!(model.body_pos().len(), nbody);
4523        assert_eq!(model.body_sameframe().len(), nbody);
4524        assert_eq!(model.jnt_type().len(), model.ffi().njnt as usize);
4525        assert_eq!(model.geom_type().len(), model.ffi().ngeom as usize);
4526    }
4527
4528    /// Verifies [force]-cast pair arrays: pair_solref (&[[MjtNum; mjNREF]]),
4529    /// pair_friction (&[[MjtNum; 5]]).
4530    #[test]
4531    fn test_force_cast_pair_model_arrays() {
4532        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4533        let npair = model.ffi().npair as usize;
4534
4535        if npair == 0 {
4536            return;
4537        }
4538
4539        let pair_solref = model.pair_solref();
4540        let pair_friction = model.pair_friction();
4541        let pair_solimp = model.pair_solimp();
4542
4543        assert_eq!(pair_solref.len(), npair);
4544        assert_eq!(pair_friction.len(), npair);
4545        assert_eq!(pair_solimp.len(), npair);
4546
4547        let nref = mjNREF as usize;
4548        let nimp = mjNIMP as usize;
4549
4550        for i in 0..npair {
4551            for j in 0..nref {
4552                assert_eq!(pair_solref[i][j], unsafe { *model.ffi().pair_solref.add(i * nref + j) });
4553            }
4554            for j in 0..5 {
4555                assert_eq!(pair_friction[i][j], unsafe { *model.ffi().pair_friction.add(i * 5 + j) });
4556            }
4557            for j in 0..nimp {
4558                assert_eq!(pair_solimp[i][j], unsafe { *model.ffi().pair_solimp.add(i * nimp + j) });
4559            }
4560        }
4561    }
4562
4563    /// Verifies [force]-cast non-aliasing: adjacent joints' solver ref slices
4564    /// point to different memory.
4565    #[test]
4566    fn test_force_cast_model_non_aliasing() {
4567        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4568        let njnt = model.ffi().njnt as usize;
4569
4570        if njnt < 2 {
4571            return;
4572        }
4573
4574        let jnt_solref = model.jnt_solref();
4575        let jnt_type = model.jnt_type();
4576
4577        // Adjacent elements must not alias
4578        assert_ne!(jnt_solref[0].as_ptr(), jnt_solref[1].as_ptr());
4579        assert_ne!(std::ptr::addr_of!(jnt_type[0]), std::ptr::addr_of!(jnt_type[1]));
4580
4581        // Stride must be exactly mjNREF elements
4582        let stride = jnt_solref.as_flattened().element_offset(&jnt_solref[1][0]).unwrap();
4583        assert_eq!(stride, mjNREF as usize,
4584            "jnt_solref stride must be mjNREF={}", mjNREF);
4585    }
4586
4587    /// Model with 2 mocap bodies and 2 keyframes carrying specific mocap data.
4588    /// This lets us verify key_mpos and key_mquat array-slice strides and values.
4589    const MOCAP_MODEL: &str = stringify!(
4590        <mujoco>
4591            <worldbody>
4592                <body name="mocap1" mocap="true" pos="0 0 0">
4593                    <geom type="sphere" size="0.05" contype="0" conaffinity="0"/>
4594                </body>
4595                <body name="mocap2" mocap="true" pos="1 0 0">
4596                    <geom type="sphere" size="0.05" contype="0" conaffinity="0"/>
4597                </body>
4598                <geom type="plane" size="5 5 0.1"/>
4599            </worldbody>
4600            <keyframe>
4601                <key name="k0"
4602                     mpos="1.0 2.0 3.0  4.0 5.0 6.0"
4603                     mquat="0.5 0.5 0.5 0.5  1.0 0.0 0.0 0.0"/>
4604                <key name="k1"
4605                     mpos="10.0 20.0 30.0  40.0 50.0 60.0"
4606                     mquat="0.0 0.0 0.0 1.0  0.0 1.0 0.0 0.0"/>
4607            </keyframe>
4608        </mujoco>
4609    );
4610
4611    /// Test that `key_mpos` returns the correct slice length and exact values
4612    /// for a model with 2 mocap bodies and 2 keyframes.
4613    #[test]
4614    fn test_key_mpos() {
4615        let model = MjModel::from_xml_string(MOCAP_MODEL).unwrap();
4616        let nkey = model.ffi().nkey as usize;
4617        let nmocap = model.ffi().nmocap as usize;
4618
4619        assert_eq!(nkey, 2, "expected 2 keyframes");
4620        assert_eq!(nmocap, 2, "expected 2 mocap bodies");
4621
4622        let mpos = model.key_mpos();
4623        assert_eq!(mpos.len(), nkey * nmocap * 3,
4624            "key_mpos length must be nkey * nmocap * 3 = {}", nkey * nmocap * 3);
4625
4626        // Keyframe 0: mocap1 at (1,2,3), mocap2 at (4,5,6)
4627        let k0 = &mpos[..nmocap * 3];
4628        let expected_k0: &[f64] = &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
4629        assert_eq!(k0.len(), expected_k0.len());
4630        for (&a, &b) in k0.iter().zip(expected_k0.iter()) {
4631            assert_relative_eq!(a, b, epsilon = 1e-10);
4632        }
4633
4634        // Keyframe 1: mocap1 at (10,20,30), mocap2 at (40,50,60)
4635        let k1 = &mpos[nmocap * 3..];
4636        let expected_k1: &[f64] = &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
4637        assert_eq!(k1.len(), expected_k1.len());
4638        for (&a, &b) in k1.iter().zip(expected_k1.iter()) {
4639            assert_relative_eq!(a, b, epsilon = 1e-10);
4640        }
4641    }
4642
4643    /// Test that `key_mquat` returns the correct slice length and exact values
4644    /// for a model with 2 mocap bodies and 2 keyframes.
4645    #[test]
4646    fn test_key_mquat() {
4647        let model = MjModel::from_xml_string(MOCAP_MODEL).unwrap();
4648        let nkey = model.ffi().nkey as usize;
4649        let nmocap = model.ffi().nmocap as usize;
4650
4651        assert_eq!(nkey, 2, "expected 2 keyframes");
4652        assert_eq!(nmocap, 2, "expected 2 mocap bodies");
4653
4654        let mquat = model.key_mquat();
4655        assert_eq!(mquat.len(), nkey * nmocap * 4,
4656            "key_mquat length must be nkey * nmocap * 4 = {}", nkey * nmocap * 4);
4657
4658        // Keyframe 0: mocap1 quat (0.5,0.5,0.5,0.5), mocap2 quat (1,0,0,0)
4659        let k0 = &mquat[..nmocap * 4];
4660        let expected_k0: &[f64] = &[0.5, 0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 0.0];
4661        assert_eq!(k0.len(), expected_k0.len());
4662        for (&a, &b) in k0.iter().zip(expected_k0.iter()) {
4663            assert_relative_eq!(a, b, epsilon = 1e-10);
4664        }
4665
4666        // Keyframe 1: mocap1 quat (0,0,0,1), mocap2 quat (0,1,0,0)
4667        let k1 = &mquat[nmocap * 4..];
4668        let expected_k1: &[f64] = &[0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0];
4669        assert_eq!(k1.len(), expected_k1.len());
4670        for (&a, &b) in k1.iter().zip(expected_k1.iter()) {
4671            assert_relative_eq!(a, b, epsilon = 1e-10);
4672        }
4673    }
4674
4675    /// Test that key_mpos is accessible through the key view as well,
4676    /// and that both paths yield the same data.
4677    #[test]
4678    fn test_key_mpos_view_consistency() {
4679        let model = MjModel::from_xml_string(MOCAP_MODEL).unwrap();
4680        let nmocap = model.ffi().nmocap as usize;
4681
4682        let info_k0 = model.key("k0").unwrap();
4683        let view_k0 = info_k0.view(&model);
4684
4685        // key view mpos slice should equal array accessor key_mpos[0..nmocap*3]
4686        let array_k0 = &model.key_mpos()[..nmocap * 3];
4687        assert_eq!(
4688            &view_k0.mpos[..nmocap * 3], array_k0,
4689            "key view mpos and key_mpos array accessor must return identical data"
4690        );
4691
4692        let info_k1 = model.key("k1").unwrap();
4693        let view_k1 = info_k1.view(&model);
4694        let array_k1 = &model.key_mpos()[nmocap * 3..];
4695        assert_eq!(
4696            &view_k1.mpos[..nmocap * 3], array_k1,
4697            "key view mpos and key_mpos array accessor must return identical data for key 1"
4698        );
4699    }
4700
4701    /// Test that key_mpos/key_mquat return empty slices for models with no mocap bodies.
4702    #[test]
4703    fn test_key_mpos_mquat_no_mocap() {
4704        // EXAMPLE_MODEL has keyframes but no mocap bodies
4705        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4706        assert_eq!(model.ffi().nmocap, 0, "EXAMPLE_MODEL should have no mocap bodies");
4707        assert!(model.ffi().nkey > 0, "EXAMPLE_MODEL should have keyframes");
4708
4709        assert_eq!(model.key_mpos().len(), 0,
4710            "key_mpos must be empty when nmocap == 0");
4711        assert_eq!(model.key_mquat().len(), 0,
4712            "key_mquat must be empty when nmocap == 0");
4713    }
4714
4715    /// Loading invalid XML must return an Err whose message is non-empty.
4716    #[test]
4717    fn test_from_xml_string_invalid() {
4718        let result = MjModel::from_xml_string("<this is not valid mujoco xml>");
4719        assert!(result.is_err(), "loading invalid XML must return Err");
4720        let msg = result.unwrap_err().to_string();
4721        assert!(!msg.is_empty(), "error message must not be empty for invalid XML");
4722    }
4723
4724    /// Verifies the new mesh view fields added in MuJoCo 3.8.0:
4725    /// read-only index fields (`normaladr`, `normalnum`, etc.) have length 1,
4726    /// and the read-write fields (`scale`, `pos`, `quat`) have the right length
4727    /// and survive a roundtrip write.
4728    #[test]
4729    fn test_mesh_view_new_fields() {
4730        const MESH_MODEL: &str = "<mujoco>\
4731          <asset>\
4732            <mesh name=\"cube\" vertex=\"-0.5 -0.5 -0.5  0.5 -0.5 -0.5  -0.5  0.5 -0.5  0.5  0.5 -0.5  \
4733                                         -0.5 -0.5  0.5  0.5 -0.5  0.5  -0.5  0.5  0.5  0.5  0.5  0.5\"/>\
4734          </asset>\
4735          <worldbody>\
4736            <geom type=\"mesh\" mesh=\"cube\"/>\
4737          </worldbody>\
4738        </mujoco>";
4739
4740        let mut model = MjModel::from_xml_string(MESH_MODEL).unwrap();
4741        let mesh_info = model.mesh("cube").unwrap();
4742
4743        let view = mesh_info.view(&model);
4744
4745        /* Verify field dimensions for read-write fields */
4746        assert_eq!(view.scale.len(), 3);
4747        assert_eq!(view.pos.len(), 3);
4748        assert_eq!(view.quat.len(), 4);
4749
4750        /* Verify field dimensions for read-only index fields */
4751        assert_eq!(view.normaladr.len(), 1);
4752        assert_eq!(view.normalnum.len(), 1);
4753        assert_eq!(view.texcoordnum.len(), 1);
4754        assert_eq!(view.bvhadr.len(), 1);
4755        assert_eq!(view.bvhnum.len(), 1);
4756        assert_eq!(view.octadr.len(), 1);
4757        assert_eq!(view.octnum.len(), 1);
4758        assert_eq!(view.pathadr.len(), 1);
4759        assert_eq!(view.polynum.len(), 1);
4760        assert_eq!(view.polyadr.len(), 1);
4761
4762        /* Verify write-read roundtrip for scale */
4763        let mut view_mut = mesh_info.view_mut(&mut model);
4764        view_mut.scale[0] = 2.0;
4765        view_mut.scale[1] = 3.0;
4766        view_mut.scale[2] = 4.0;
4767
4768        let view2 = mesh_info.view(&model);
4769        assert_eq!(view2.scale[0], 2.0);
4770        assert_eq!(view2.scale[1], 3.0);
4771        assert_eq!(view2.scale[2], 4.0);
4772    }
4773
4774    /// Verifies that `flex_cellnum`, `flex_stiffnessadr`, and `flex_bendingadr`
4775    /// return empty slices when the model contains no flex bodies.
4776    #[test]
4777    fn test_flex_array_slices_empty_for_non_flex_model() {
4778        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4779        assert_eq!(model.ffi().nflex, 0);
4780        assert_eq!(model.flex_cellnum().len(), 0);
4781        assert_eq!(model.flex_stiffnessadr().len(), 0);
4782        assert_eq!(model.flex_bendingadr().len(), 0);
4783    }
4784
4785    /// Tests the wrapper of `mj_maxContact` ([`MjModel::max_contacts`]).
4786    #[test]
4787    fn test_max_contacts() {
4788        let model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4789        let geom1 = model.name_to_id(MjtObj::mjOBJ_GEOM, "green_sphere").unwrap();
4790        let geom2 = model.name_to_id(MjtObj::mjOBJ_GEOM, "ball2").unwrap();
4791
4792        let mc = model.max_contacts(geom1, geom2, None);  // pull margin from model.
4793        assert_eq!(mc, 1);
4794
4795        let mc = model.max_contacts(geom1, geom2, Some(false));
4796        assert_eq!(mc, 1);
4797
4798        // Spheres always have one contact, regardless of margin.
4799        let mc = model.max_contacts(geom1, geom2, Some(true));
4800        assert_eq!(mc, 1);
4801
4802        // Test invalid geom index.
4803        assert!( model.try_max_contacts(999, geom2, Some(true)).is_err());
4804    }
4805
4806    /// Drives the generated sanitizer probes over every dynamic array of |MjModel|. See the
4807    /// matching test on |MjData| for what the sanitizers catch here.
4808    #[test]
4809    fn test_probe_dynamic_arrays_stays_in_bounds() {
4810        let mut model = MjModel::from_xml_string(EXAMPLE_MODEL).unwrap();
4811        assert!(model.ffi().nbody > 1, "the model must have bodies for the probe to mean anything");
4812        model.probe_dynamic_arrays();
4813
4814        // SAFETY: no accessor of this block needs a pipeline stage; the compiler fills the model.
4815        unsafe { model.probe_dynamic_arrays_unsafe() };
4816    }
4817}