1use std::ffi::{c_char, c_int, CStr, CString};
3use crate::error::MjEditError;
4use std::ptr::{self, NonNull};
5use std::marker::PhantomData;
6use std::path::Path;
7use std::fmt;
8
9#[macro_use]
10mod utility;
11use utility::*;
12
13mod traits;
14pub use traits::*;
15
16mod default;
17pub use default::*;
18
19use super::mj_model::{
20 MjModel, MjtObj, MjtGeom, MjtJoint, MjtCamLight,
21 MjtLightType, MjtSensor, MjtDataType, MjtGain,
22 MjtBias, MjtDyn, MjtEq, MjtTexture, MjtColorSpace,
23 MjtTrn, MjtStage, MjtFlexSelf, MjtProjection,
24 MjtSleepPolicy, MjtWrap, MjtTextureRole, MjtCubeFace
25};
26use super::mj_auxiliary::{MjVfs, MjVisual, MjStatistic, MjLROpt};
27use super::mj_option::MjOption;
28use super::mj_primitive::*;
29use crate::mujoco_c::*;
30
31use crate::getter_setter;
32
33use crate::mujoco_c::{mjs_addHField as mjs_addHfield, mjs_asHField as mjs_asHfield};
35use crate::util::{assert_mujoco_version, ERROR_BUF_LEN};
36
37fn check_objtype(t: MjtObj) -> Result<(), MjEditError> {
41 if (t as i32) < (MjtObj::mjNOBJECT as i32) {
43 Ok(())
44 } else {
45 Err(MjEditError::InvalidParameter(format!(
46 "object type must be a real MjtObj below MjtObj::mjNOBJECT, got {t:?}"
47 )))
48 }
49}
50
51fn check_nuser(count: i32) -> Result<(), MjEditError> {
53 if count < -1 {
54 Err(MjEditError::InvalidParameter(format!(
55 "nuser count must be at least -1 (-1 meaning automatic), got {count}"
56 )))
57 } else {
58 Ok(())
59 }
60}
61
62fn check_numeric_size(size: i32) -> Result<(), MjEditError> {
64 if size < 0 {
67 Err(MjEditError::InvalidParameter(format!(
68 "numeric size must be non-negative, got {size}"
69 )))
70 } else {
71 Ok(())
72 }
73}
74
75pub type MjtGeomInertia = mjtGeomInertia;
78
79pub type MjtMeshInertia = mjtMeshInertia;
81
82pub type MjtBuiltin = mjtBuiltin;
84
85pub type MjtMeshBuiltin = mjtMeshBuiltin;
87
88pub type MjtMark = mjtMark;
90
91pub type MjtLimited = mjtLimited;
93
94pub type MjtAlignFree = mjtAlignFree;
96
97pub type MjtInertiaFromGeom = mjtInertiaFromGeom;
99
100pub type MjtConflict = mjtConflict;
102
103pub type MjtOrientation = mjtOrientation;
105
106pub type MjtCTimer = mjtCTimer;
108pub type MjsOrientation = mjsOrientation;
115impl MjsOrientation {
116 pub fn set_euler(&mut self, angle: &[f64; 3]) {
118 self.type_ = MjtOrientation::mjORIENTATION_EULER;
119 self.euler = *angle;
120 }
121
122 pub fn set_axis_angle(&mut self, angle: &[f64; 4]) {
124 self.type_ = MjtOrientation::mjORIENTATION_AXISANGLE;
125 self.axisangle = *angle;
126 }
127
128 pub fn set_xy_axis(&mut self, angle: &[f64; 6]) {
130 self.type_ = MjtOrientation::mjORIENTATION_XYAXES;
131 self.xyaxes = *angle;
132 }
133
134 pub fn set_z_axis(&mut self, angle: &[f64; 3]) {
136 self.type_ = MjtOrientation::mjORIENTATION_ZAXIS;
137 self.zaxis = *angle;
138 }
139
140 pub fn switch_quat(&mut self) {
143 self.type_ = MjtOrientation::mjORIENTATION_QUAT;
144 }
145}
146
147mjs_opaque!(MjsCompiler <= mjsCompiler,
148 "Compiler options. An opaque handle for the FFI type [`mjsCompiler`], reached through \
149[`ffi`](Self::ffi).");
150
151impl MjsCompiler {
152 getter_setter! {[&] with, get, set, [
153 [ffi, ffi_mut] autolimits: bool; "infer \"limited\" attribute based on range.";
154 [ffi, ffi_mut] balanceinertia: bool; "automatically impose A + B >= C rule.";
155 [ffi, ffi_mut] fitaabb: bool; "meshfit to aabb instead of inertia box.";
156 [ffi, ffi_mut] degree: bool; "angles in radians or degrees.";
157 [ffi, ffi_mut] discardvisual: bool; "discard visual geoms in parser.";
158 [ffi, ffi_mut] usethread: bool; "use multiple threads to speed up compiler.";
159 [ffi, ffi_mut] fusestatic: bool; "fuse static bodies with parent.";
160 [ffi, ffi_mut] saveinertial: bool; "save explicit inertial clause for all bodies to XML.";
161 [ffi, ffi_mut] alignfree: bool; "align free joints with inertial frame.";
162 ]}
163
164 getter_setter! {[&] with, get, set, [
165 [ffi, ffi_mut] boundmass: f64; "enforce minimum body mass.";
166 [ffi, ffi_mut] boundinertia: f64; "enforce minimum body diagonal inertia.";
167 [ffi, ffi_mut] settotalmass: f64; "rescale masses and inertias; <=0: ignore.";
168 ]}
169
170 getter_setter! {[&] with, get, set, [
171 [ffi, ffi_mut] inertiafromgeom: MjtInertiaFromGeom [force]; "use geom inertias.";
172 [ffi, ffi_mut] conflict: MjtConflict [force]; "conflict-resolution policy for attach.";
173 ]}
174
175 getter_setter! {[&] with, get, [
176 [ffi, ffi_mut] inertiagrouprange: &[i32; 2]; "range of geom groups used to compute inertia.";
177 [ffi, ffi_mut] eulerseq: &[c_char; 3]; "sequence for euler rotations.";
178 [ffi, ffi_mut] LRopt: &MjLROpt; "options for lengthrange computation.";
179 ]}
180
181 string_set_get_with! {[&]
182 meshdir; "mesh and hfield directory.";
183 texturedir; "texture directory.";
184 }
185
186 getter_setter! { get, [
187 [ffi] authored: u64; "bitmask of authored compiler fields.";
188 ]}
189}
190
191pub type MjsAuthored = mjsAuthored;
196
197pub struct MjSpec {
205 ffi: NonNull<mjSpec>,
207}
208
209impl fmt::Debug for MjSpec {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 f.debug_struct("MjSpec").field("ffi", &self.ffi).finish_non_exhaustive()
212 }
213}
214
215impl MjSpec {
216 fn from_ffi(ffi: NonNull<mjSpec>) -> Self {
218 Self { ffi }
219 }
220
221 #[expect(deprecated, reason = "try_new keeps the implementation until it is removed")]
226 pub fn new() -> Self {
227 Self::try_new().expect("MuJoCo failed to allocate MjSpec")
228 }
229
230 #[deprecated(
242 since = "6.0.0",
243 note = "always returns Ok; use `new`"
244 )]
245 pub fn try_new() -> Result<Self, MjEditError> {
246 assert_mujoco_version();
247 let ptr = unsafe { mj_makeSpec() };
250 Ok(Self::from_ffi(NonNull::new(ptr).ok_or(MjEditError::AllocationFailed)?))
251 }
252
253 pub fn try_clone(&self) -> Result<Self, MjEditError> {
262 let ptr = unsafe { mj_copySpec(self.ffi.as_ptr()) };
265 NonNull::new(ptr).map(Self::from_ffi).ok_or(MjEditError::AllocationFailed)
266 }
267
268 pub unsafe fn into_sendable(self) -> SendableSpec {
293 unsafe { SendableSpec::new(self) }
294 }
295
296 pub fn from_xml<T: AsRef<Path>>(path: T) -> Result<Self, MjEditError> {
304 Self::from_xml_file(path, None)
305 }
306
307 pub fn from_xml_vfs<T: AsRef<Path>>(path: T, vfs: &MjVfs) -> Result<Self, MjEditError> {
315 Self::from_xml_file(path, Some(vfs))
316 }
317
318 fn from_xml_file<T: AsRef<Path>>(path: T, vfs: Option<&MjVfs>) -> Result<Self, MjEditError> {
319 assert_mujoco_version();
320
321 let mut error_buffer = [0; ERROR_BUF_LEN];
322 unsafe {
323 let path_str = path.as_ref().to_str()
324 .ok_or(MjEditError::InvalidUtf8Path)?;
325 let path = CString::new(path_str).unwrap();
326 let raw_ptr = mj_parseXML(
327 path.as_ptr(), vfs.map_or(ptr::null(), |v| v.ffi()),
328 error_buffer.as_mut_ptr(), error_buffer.len() as c_int
329 );
330
331 Self::check_spec(raw_ptr, &error_buffer)
332 }
333 }
334
335 pub fn from_xml_string(xml: &str) -> Result<Self, MjEditError> {
342 assert_mujoco_version();
343
344 let c_xml = CString::new(xml).unwrap();
345 let mut error_buffer = [0; ERROR_BUF_LEN];
346 unsafe {
347 let spec_ptr = mj_parseXMLString(
348 c_xml.as_ptr(), ptr::null(),
349 error_buffer.as_mut_ptr(), error_buffer.len() as c_int
350 );
351 Self::check_spec(spec_ptr, &error_buffer)
352 }
353 }
354
355 pub fn from_parse<T: AsRef<Path>>(filename: T, content_type: &str) -> Result<Self, MjEditError> {
365 Self::from_parse_file(filename, content_type, None)
366 }
367
368 pub fn from_parse_vfs<T: AsRef<Path>>(filename: T, content_type: &str, vfs: &MjVfs) -> Result<Self, MjEditError> {
376 Self::from_parse_file(filename, content_type, Some(vfs))
377 }
378
379 fn from_parse_file<T: AsRef<Path>>(filename: T, content_type: &str, vfs: Option<&MjVfs>) -> Result<Self, MjEditError> {
386 assert_mujoco_version();
387 let mut error_buffer = [0; ERROR_BUF_LEN];
388 unsafe {
389 let c_filename = CString::new(
390 filename.as_ref().to_str()
391 .ok_or(MjEditError::InvalidUtf8Path)?
392 ).unwrap();
393 let c_content_type = CString::new(content_type).unwrap();
394 let ptr = mj_parse(
395 c_filename.as_ptr(), c_content_type.as_ptr(),
396 vfs.map_or(ptr::null(), |v| v.ffi()),
397 error_buffer.as_mut_ptr(), error_buffer.len() as i32
398 );
399 Self::check_spec(ptr, &error_buffer)
400 }
401 }
402
403 fn check_spec(spec_ptr: *mut mjSpec, error_buffer: &[c_char]) -> Result<Self, MjEditError> {
405 if spec_ptr.is_null() {
406 let message = unsafe { CStr::from_ptr(error_buffer.as_ptr()) }
409 .to_string_lossy()
410 .into_owned();
411 Err(MjEditError::ParseFailed(message))
412 }
413 else {
414 Ok(Self::from_ffi(unsafe { NonNull::new_unchecked(spec_ptr) }))
416 }
417 }
418
419 pub fn ffi(&self) -> &mjSpec {
421 unsafe { self.ffi.as_ref() }
424 }
425
426 pub unsafe fn ffi_mut(&mut self) -> &mut mjSpec {
432 unsafe { self.ffi.as_mut() }
433 }
434
435 #[deprecated(since = "6.0.0", note = "use SpecObject::delete on the element handle")]
450 pub unsafe fn delete_element(&mut self, element: *mut mjsElement) -> Result<(), MjEditError> {
451 if element.is_null() {
452 return Err(MjEditError::DeleteFailed("null element pointer".to_owned()));
453 }
454
455 if unsafe { (*element).elemtype } == MjtObj::mjOBJ_DEFAULT {
457 return Err(MjEditError::UnsupportedOperation);
458 }
459
460 if unsafe { mjs_getSpec(element) } != self.ffi.as_ptr() {
461 return Err(MjEditError::DeleteFailed("element does not belong to this spec".to_owned()));
462 }
463
464 unsafe { utility::delete_element(element) }
465 }
466
467 pub fn compile(&mut self) -> Result<MjModel, MjEditError> {
475 self.compile_impl(None)
476 }
477
478 pub fn compile_with_vfs(&mut self, vfs: &MjVfs) -> Result<MjModel, MjEditError> {
484 self.compile_impl(Some(vfs))
485 }
486
487 fn compile_impl(&mut self, maybe_vfs: Option<&MjVfs>) -> Result<MjModel, MjEditError> {
489 for texture in self.texture_iter() {
492 if texture.builtin() != MjtBuiltin::mjBUILTIN_NONE && texture.nchannel() < 3 {
493 return Err(MjEditError::CompileFailed(
494 "texture with a builtin pattern requires nchannel >= 3".to_owned(),
495 ));
496 }
497
498 let (nchannel, width, height) = (texture.nchannel(), texture.width(), texture.height());
499 if nchannel < 0 || width < 0 || height < 0 {
500 return Err(MjEditError::CompileFailed(
501 "texture nchannel, width and height must be non-negative".to_owned(),
502 ));
503 }
504 if i64::from(nchannel) * i64::from(width) * i64::from(height) > i64::from(i32::MAX) {
505 return Err(MjEditError::CompileFailed(
506 "texture nchannel*width*height must fit in an i32".to_owned(),
507 ));
508 }
509 }
510
511 let result = unsafe { MjModel::from_raw(
512 mj_compile(self.ffi.as_ptr(), maybe_vfs.map_or(ptr::null(), |vfs| vfs.ffi()))
513 ) };
514
515 result.map_err(|_| MjEditError::CompileFailed(unsafe { read_spec_error(self.ffi.as_ptr()) }))
517 }
518
519 pub fn timer(&self) -> &[f64; MjtCTimer::mjNCTIMER as usize] {
521 unsafe { &*mjs_getTimer(self.ffi.as_ptr()).cast() }
522 }
523
524 pub fn num_warnings(&self) -> i32 {
526 unsafe { mjs_numWarnings(self.ffi.as_ptr()) }
528 }
529
530 pub fn warning(&self, index: i32) -> Option<&str> {
533 let ptr = unsafe { mjs_getWarning(self.ffi.as_ptr(), index) };
535 if ptr.is_null() {
536 None
537 } else {
538 unsafe { CStr::from_ptr(ptr) }.to_str().ok()
539 }
540 }
541
542 pub fn save_xml<T: AsRef<Path>>(&self, filename: T) -> Result<(), MjEditError> {
549 let mut error_buff = [0; ERROR_BUF_LEN];
550 let cname = CString::new(
551 filename.as_ref().to_str()
552 .ok_or(MjEditError::InvalidUtf8Path)?
553 ).unwrap(); let result = unsafe { mj_saveXML(
555 self.ffi(), cname.as_ptr(),
556 error_buff.as_mut_ptr(), error_buff.len() as i32
557 ) };
558 match result {
559 0 => Ok(()),
560 _ => {
561 let message = unsafe { CStr::from_ptr(error_buff.as_ptr()) }
564 .to_string_lossy()
565 .into_owned();
566 Err(MjEditError::SaveFailed(message))
567 }
568 }
569 }
570
571 pub fn save_xml_string(&self, buffer_size: usize) -> Result<String, MjEditError> {
582 let mut error_buff = [0; ERROR_BUF_LEN];
583 let mut result_buff = vec![0u8; buffer_size];
584 let result = unsafe { mj_saveXMLString(
585 self.ffi(), result_buff.as_mut_ptr().cast(), result_buff.len() as i32,
586 error_buff.as_mut_ptr(), error_buff.len() as i32
587 ) };
588 match result {
589 0 => Ok(CStr::from_bytes_until_nul(&result_buff).unwrap().to_string_lossy().into_owned()),
590 r if r > 0 => Err(MjEditError::XmlBufferTooSmall { required_size: r as usize }),
591 _ => {
592 let message = unsafe { CStr::from_ptr(error_buff.as_ptr()) }
595 .to_string_lossy()
596 .into_owned();
597 Err(MjEditError::SaveFailed(message))
598 }
599 }
600 }
601
602 pub fn encode(&self, filepath: impl AsRef<Path>, content_type: &str) -> Result<(), MjEditError> {
622 self.encode_impl(filepath, content_type, None)
623 }
624
625 pub fn encode_with_vfs(&self, filepath: impl AsRef<Path>, content_type: &str, vfs: &MjVfs) -> Result<(), MjEditError> {
633 self.encode_impl(filepath, content_type, Some(vfs))
634 }
635
636 fn encode_impl(
638 &self,
639 filepath: impl AsRef<Path>, content_type: &str,
640 maybe_vfs: Option<&MjVfs>
641 ) -> Result<(), MjEditError> {
642 let filepath = filepath.as_ref().to_str().ok_or(MjEditError::InvalidUtf8Path)?;
643 encode(Some(self), None, filepath, content_type, maybe_vfs).map_err(MjEditError::SaveFailed)
644 }
645}
646
647pub(crate) fn encode(
656 spec: Option<&MjSpec>, model: Option<&MjModel>,
657 filepath: &str, content_type: &str, vfs: Option<&MjVfs>
658) -> Result<(), String> {
659 assert!(!filepath.is_empty(), "encode: filepath is empty");
661 let mut error_buff = [0; ERROR_BUF_LEN];
662
663 let c_filepath = CString::new(filepath).unwrap();
664 let c_content_type = CString::new(content_type).unwrap();
665
666 let result = unsafe {
668 mj_encode(
669 spec.map_or(ptr::null(), |spec| spec.ffi()),
670 model.map_or(ptr::null(), |model| model.ffi()),
671 c_filepath.as_ptr(), c_content_type.as_ptr(),
672 vfs.map_or(ptr::null(), |vfs| vfs.ffi()),
673 error_buff.as_mut_ptr(), ERROR_BUF_LEN as i32
674 )
675 };
676
677 if result == -1 {
679 let message = unsafe { CStr::from_ptr(error_buff.as_ptr()) }
681 .to_string_lossy()
682 .into_owned();
683 return Err(message);
684 }
685
686 Ok(())
687}
688
689impl MjSpec {
691 find_x_method! {
692 body, geom, joint, site, camera, light, frame, actuator, sensor, flex, pair, equality, exclude, tendon,
693 numeric, text, tuple, key, mesh, hfield, skin, texture, material, plugin
694 }
695
696 find_x_method_direct! { default }
697
698 pub fn world_body(&self) -> &MjsBody {
702 self.body("world").unwrap()
703 }
704
705 pub fn world_body_mut(&mut self) -> &mut MjsBody {
709 self.body_mut("world").unwrap()
710 }
711}
712
713impl MjSpec {
715 string_set_get_with! {
716 modelname; "model name.";
717 comment; "comment at top of XML.";
718 modelfiledir; "path to model file.";
719 }
720
721 getter_setter! {
722 with, get, [
723 [ffi, ffi_mut] stat: &MjStatistic; "statistic overrides.";
724 [ffi, ffi_mut] visual: &MjVisual; "visualization options.";
725 [ffi, ffi_mut] option: &MjOption; "simulation options.";
726 ]
727 }
728
729 nested_handle!(compiler: MjsCompiler; "compiler options.");
730
731 getter_setter! {
732 get, [
733 [ffi] (allow_mut = false) authored: &MjsAuthored; "authored-field tracking bitmasks.";
734 ]
735 }
736
737 getter_setter! {
738 with, get, set, [
739 [ffi, ffi_mut] strippath: bool; "whether to strip paths from mesh files.";
740 [ffi, ffi_mut] hasImplicitPluginElem: bool; "already encountered an implicit plugin sensor/actuator.";
741 ]
742 }
743
744 getter_setter! {
745 get, [
746 [ffi] nemax: i32; "max number of equality constraints.";
748 ]
749 }
750
751 getter_setter! {
752 get, set, [
753 [ffi, ffi_mut] memory: MjtSize; "number of bytes in arena+stack memory.";
754 [ffi, ffi_mut] nuserdata: i32; "number of mjtNums in userdata.";
755 [ffi, ffi_mut] nkey: i32; "number of keyframes.";
756 ]
757 }
758
759 getter_setter! {
760 get, set, [
761 [ffi, ffi_mut] nuser_body: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in body_user.";
762 [ffi, ffi_mut] nuser_jnt: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in jnt_user.";
763 [ffi, ffi_mut] nuser_geom: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in geom_user.";
764 [ffi, ffi_mut] nuser_site: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in site_user.";
765 [ffi, ffi_mut] nuser_cam: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in cam_user.";
766 [ffi, ffi_mut] nuser_tendon: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in tendon_user.";
767 [ffi, ffi_mut] nuser_actuator: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in actuator_user.";
768 [ffi, ffi_mut] nuser_sensor: i32 { check_nuser, "[`MjEditError::InvalidParameter`] when the count is below -1" } => MjEditError; "number of mjtNums in sensor_user.";
769 ]
770 }
771}
772
773impl MjSpec {
775 add_x_method! { actuator, pair, equality, tendon, mesh, material }
776 add_x_method_no_default! {
777 sensor, flex, exclude, numeric, text, tuple, key, plugin,
778 hfield, skin, texture
779 }
781
782 pub fn add_default(&mut self, class_name: &str, parent_class_name: Option<&str>) -> &mut MjsDefault {
790 self.try_add_default(class_name, parent_class_name).unwrap()
791 }
792
793 pub fn try_add_default(&mut self, class_name: &str, parent_class_name: Option<&str>) -> Result<&mut MjsDefault, MjEditError> {
800 let c_class_name = CString::new(class_name).unwrap();
801
802 let parent_ptr = if let Some(name) = parent_class_name {
803 self.default(name).ok_or(MjEditError::NotFound)?.ffi()
804 } else {
805 ptr::null()
806 };
807
808 unsafe {
809 let ptr_default = mjs_addDefault(
810 self.ffi_mut(),
811 c_class_name.as_ptr(),
812 parent_ptr
813 );
814 if ptr_default.is_null() {
815 Err(MjEditError::AlreadyExists)
816 }
817 else {
818 Ok(MjsDefault::from_ffi_ptr_mut(ptr_default).unwrap())
819 }
820 }
821 }
822
823 pub fn activate_plugin(&mut self, name: &str) -> Result<(), MjEditError> {
833 let c_name = CString::new(name).unwrap();
834 let result = unsafe { mjs_activatePlugin(self.ffi_mut(), c_name.as_ptr()) };
836 if result == 0 {
837 Ok(())
838 }
839 else {
840 Err(MjEditError::NotFound)
841 }
842 }
843}
844
845#[derive(Debug)]
847pub struct MjsSpecItemIterMut<'a, T> {
848 ffi_ptr: *mut mjSpec,
850 last: *mut mjsElement,
852 item_type: PhantomData<&'a mut T>
853}
854
855#[derive(Debug, Clone)]
857pub struct MjsSpecItemIter<'a, T> {
858 ffi_ptr: *const mjSpec,
859 last: *mut mjsElement,
861 item_type: PhantomData<&'a T>
862}
863
864impl<'a, T: SpecObject> MjsSpecItemIterMut<'a, T> {
865 fn new(root: &'a mut MjSpec) -> Self {
866 let last = unsafe { mjs_firstElement(root.ffi.as_ptr(), T::OBJ_TYPE) };
867 Self { ffi_ptr: root.ffi.as_ptr(), last, item_type: PhantomData }
868 }
869}
870
871impl<'a, T: SpecObject> MjsSpecItemIter<'a, T> {
872 fn new(root: &'a MjSpec) -> Self {
873 let last = unsafe { mjs_firstElement(root.ffi.as_ptr(), T::OBJ_TYPE) };
876 Self { ffi_ptr: root.ffi.as_ptr(), last, item_type: PhantomData }
877 }
878}
879
880impl<'a, T: SpecObject + 'a> Iterator for MjsSpecItemIterMut<'a, T> {
881 type Item = &'a mut T;
882
883 fn next(&mut self) -> Option<Self::Item> {
884 if self.last.is_null() {
885 return None;
886 }
887 unsafe {
888 let out = T::from_element_as_ptr_mut(self.last).as_mut();
889 self.last = mjs_nextElement(self.ffi_ptr, self.last);
892 out
893 }
894 }
895}
896
897impl<'a, T: SpecObject + 'a> Iterator for MjsSpecItemIter<'a, T> {
898 type Item = &'a T;
899
900 fn next(&mut self) -> Option<Self::Item> {
901 if self.last.is_null() {
902 return None;
903 }
904 unsafe {
905 let out = T::from_element_as_ptr_mut(self.last).as_ref();
906 self.last = mjs_nextElement(self.ffi_ptr, self.last);
909 out
910 }
911 }
912}
913
914impl<'a, T: SpecObject + 'a> std::iter::FusedIterator for MjsSpecItemIterMut<'a, T> {}
915impl<'a, T: SpecObject + 'a> std::iter::FusedIterator for MjsSpecItemIter<'a, T> {}
916
917impl MjSpec {
919 spec_get_iter! {
920 geom, joint, site, camera, light, frame, actuator, sensor, flex, pair, equality,
921 exclude, tendon, numeric, text, tuple, key, mesh, hfield, skin, texture, material, plugin
922 }
923
924 spec_get_iter!(read_only: body);
927}
928
929impl Default for MjSpec {
930 fn default() -> Self {
931 Self::new()
932 }
933}
934
935impl Drop for MjSpec {
936 fn drop(&mut self) {
937 unsafe { mj_deleteSpec(self.ffi.as_ptr()); }
940 }
941}
942
943impl Clone for MjSpec {
944 fn clone(&self) -> Self {
950 self.try_clone().expect("MuJoCo failed to clone MjSpec")
951 }
952}
953
954#[derive(Debug)]
956pub struct SendableSpec(MjSpec);
957
958impl SendableSpec {
959 pub unsafe fn new(spec: MjSpec) -> Self {
963 Self(spec)
964 }
965
966 pub fn take(self) -> MjSpec {
968 self.0
969 }
970}
971
972unsafe impl Send for SendableSpec {}
977
978mjs_struct!(Site with SpecObject: MjsSite <= mjsSite);
982impl MjsSite {
983 getter_setter! {
984 [&] with, get, [
985 [ffi, ffi_mut] pos: &[f64; 3]; "position.";
987 [ffi, ffi_mut] quat: &[f64; 4]; "orientation.";
988 [ffi, ffi_mut] alt: &MjsOrientation; "alternative orientation.";
989 [ffi, ffi_mut] fromto: &[f64; 6]; "alternative for capsule, cylinder, box, ellipsoid.";
990 [ffi, ffi_mut] size: &[f64; 3]; "geom size.";
991
992 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba when material is omitted.";
994 ]}
995
996 getter_setter!([&] with, get, set, [
997 [ffi, ffi_mut] type_ + _: MjtGeom; "geom type.";
998 [ffi, ffi_mut] group: i32; "group.";
999 ]);
1000
1001 userdata_method!(f64);
1002
1003 string_set_get_with! {[&]
1004 material; "name of material.";
1005 }
1006}
1007
1008mjs_struct!(Joint with SpecObject: MjsJoint <= mjsJoint);
1012impl MjsJoint {
1013 getter_setter! {
1014 [&] with, get, [
1015 [ffi, ffi_mut] pos: &[f64; 3]; "anchor position.";
1017 [ffi, ffi_mut] axis: &[f64; 3]; "joint axis.";
1018 [ffi, ffi_mut] ref_ + _: &f64; "value at reference configuration: qpos0.";
1019 [ffi, ffi_mut] springdamper: &[f64; 2]; "timeconst, dampratio.";
1020
1021 [ffi, ffi_mut] stiffness: &[f64; mjNPOLY as usize + 1]; "stiffness coefficients.";
1023
1024 [ffi, ffi_mut] range: &[f64; 2]; "joint limits.";
1026 [ffi, ffi_mut] solref_limit: &[MjtNum; mjNREF as usize]; "solver reference: joint limits.";
1027 [ffi, ffi_mut] solimp_limit: &[MjtNum; mjNIMP as usize]; "solver impedance: joint limits.";
1028 [ffi, ffi_mut] actfrcrange: &[f64; 2]; "actuator force limits.";
1029
1030 [ffi, ffi_mut] damping: &[f64; mjNPOLY as usize + 1]; "damping coefficients.";
1032 [ffi, ffi_mut] solref_friction: &[MjtNum; mjNREF as usize]; "solver reference: dof friction.";
1033 [ffi, ffi_mut] solimp_friction: &[MjtNum; mjNIMP as usize]; "solver impedance: dof friction.";
1034 ]
1035 }
1036
1037 getter_setter!([&] with, get, set, [
1038 [ffi, ffi_mut] type_ + _: MjtJoint; "joint type.";
1039 [ffi, ffi_mut] group: i32; "joint group.";
1040 [ffi, ffi_mut] springref: f64; "spring reference value: qpos_spring.";
1041 [ffi, ffi_mut] margin: f64; "margin value for joint limit detection.";
1042 [ffi, ffi_mut] armature: f64; "armature inertia (mass for slider).";
1043 [ffi, ffi_mut] frictionloss: f64; "friction loss.";
1044 ]);
1045
1046 getter_setter! {
1047 [&] with, get, set, [
1048 [ffi, ffi_mut] align: MjtAlignFree [force]; "align free joint with body com (mjtAlignFree).";
1049 [ffi, ffi_mut] limited: MjtLimited [force]; "does joint have limits (mjtLimited).";
1050 [ffi, ffi_mut] actfrclimited: MjtLimited [force]; "are actuator forces on joint limited (mjtLimited).";
1051 ]
1052 }
1053
1054 getter_setter! {
1055 [&] with, get, set, [
1056 [ffi, ffi_mut] actgravcomp: bool; "is gravcomp force applied via actuators.";
1057 ]
1058 }
1059
1060 userdata_method!(f64);
1061}
1062
1063mjs_struct!(Geom with SpecObject: MjsGeom <= mjsGeom);
1067impl MjsGeom {
1068 getter_setter! {
1069 [&] with, get, [
1070 [ffi, ffi_mut] pos: &[f64; 3]; "geom position.";
1071 [ffi, ffi_mut] quat: &[f64; 4]; "geom orientation.";
1072 [ffi, ffi_mut] alt: &MjsOrientation; "alternative orientation.";
1073 [ffi, ffi_mut] fromto: &[f64; 6]; "alternative for capsule, cylinder, box, ellipsoid.";
1074 [ffi, ffi_mut] size: &[f64; 3]; "geom size.";
1075 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba when material is omitted.";
1076 [ffi, ffi_mut] friction: &[f64; 3]; "one-sided friction coefficients: slide, spin, roll.";
1077 [ffi, ffi_mut] solref: &[MjtNum; mjNREF as usize]; "solver reference.";
1078 [ffi, ffi_mut] solimp: &[MjtNum; mjNIMP as usize]; "solver impedance.";
1079 [ffi, ffi_mut] surfacevel: &[f64; 6]; "surface velocity in local frame: linear, angular.";
1080 [ffi, ffi_mut] fluid_coefs: &[MjtNum; 5]; "ellipsoid-fluid interaction coefs."
1081 ]
1082 }
1083
1084 nested_handle!(plugin: MjsPluginReference; "sdf plugin.");
1085
1086 getter_setter!([&] with, get, set, [
1087 [ffi, ffi_mut] type_ + _: MjtGeom; "geom type.";
1088 [ffi, ffi_mut] group: i32; "group.";
1089 [ffi, ffi_mut] contype: i32; "contact type.";
1090 [ffi, ffi_mut] conaffinity: i32; "contact affinity.";
1091 [ffi, ffi_mut] condim: i32; "contact dimensionality.";
1092 [ffi, ffi_mut] priority: i32; "contact priority.";
1093 [ffi, ffi_mut] solmix: f64; "solver mixing for contact pairs.";
1094 [ffi, ffi_mut] margin: f64; "margin for contact detection.";
1095 [ffi, ffi_mut] gap: f64; "additional contact detection buffer.";
1096 [ffi, ffi_mut] adhesion: f64; "adhesive force of contacts.";
1097 [ffi, ffi_mut] mass: f64; "used to compute density.";
1098 [ffi, ffi_mut] density: f64; "used to compute mass and inertia from volume or surface.";
1099 [ffi, ffi_mut] typeinertia: MjtGeomInertia; "selects between surface and volume inertia.";
1100 [ffi, ffi_mut] fluid_ellipsoid: MjtNum; "whether ellipsoid-fluid model is active.";
1101 [ffi, ffi_mut] fitscale: f64; "scale mesh uniformly.";
1102 ]);
1103
1104 userdata_method!(f64);
1105
1106 string_set_get_with! {[&]
1107 meshname; "mesh attached to geom.";
1108 material; "name of material.";
1109 hfieldname; "heightfield attached to geom.";
1110 }
1111}
1112
1113mjs_struct!(Camera with SpecObject: MjsCamera <= mjsCamera);
1117impl MjsCamera {
1118 getter_setter! {
1119 [&] with, get, [
1120 [ffi, ffi_mut] pos: &[f64; 3]; "camera position.";
1121 [ffi, ffi_mut] quat: &[f64; 4]; "camera orientation.";
1122 [ffi, ffi_mut] alt: &MjsOrientation; "alternative orientation.";
1123 [ffi, ffi_mut] intrinsic: &[f32; 4]; "intrinsic parameters.";
1124 [ffi, ffi_mut] sensor_size: &[f32; 2]; "sensor size.";
1125 [ffi, ffi_mut] resolution: &[i32; 2]; "resolution.";
1126 [ffi, ffi_mut] focal_length: &[f32; 2]; "focal length (length).";
1127 [ffi, ffi_mut] focal_pixel: &[f32; 2]; "focal length (pixel).";
1128 [ffi, ffi_mut] principal_length: &[f32; 2]; "principal point (length).";
1129 [ffi, ffi_mut] principal_pixel: &[f32; 2]; "principal point (pixel).";
1130 ]
1131 }
1132
1133 getter_setter!([&] with, get, set, [
1134 [ffi, ffi_mut] mode: MjtCamLight; "camera mode.";
1135 [ffi, ffi_mut] fovy: f64; "field of view in y direction.";
1136 [ffi, ffi_mut] ipd: f64; "inter-pupillary distance for stereo.";
1137 [ffi, ffi_mut] proj: MjtProjection; "camera projection type.";
1138 [ffi, ffi_mut] output: i32; "bit flags for output type.";
1139 ]);
1140
1141 userdata_method!(f64);
1142
1143 string_set_get_with! {[&]
1144 targetbody; "target body for tracking/targeting.";
1145 }
1146}
1147
1148mjs_struct!(Light with SpecObject: MjsLight <= mjsLight);
1152impl MjsLight {
1153 getter_setter! {
1154 [&] with, get, [
1155 [ffi, ffi_mut] pos: &[f64; 3]; "light position.";
1156 [ffi, ffi_mut] dir: &[f64; 3]; "light direction.";
1157 [ffi, ffi_mut] ambient: &[f32; 3]; "ambient color.";
1158 [ffi, ffi_mut] diffuse: &[f32; 3]; "diffuse color.";
1159 [ffi, ffi_mut] specular: &[f32; 3]; "specular color.";
1160 [ffi, ffi_mut] attenuation: &[f32; 3]; "OpenGL attenuation (quadratic model).";
1161 ]
1162 }
1163
1164 getter_setter!([&] with, get, set, [
1165 [ffi, ffi_mut] mode: MjtCamLight; "light mode.";
1166 [ffi, ffi_mut] type_ + _: MjtLightType; "light type.";
1167 [ffi, ffi_mut] bulbradius: f32; "bulb radius, for soft shadows.";
1168 [ffi, ffi_mut] intensity: f32; "intensity, in candelas.";
1169 [ffi, ffi_mut] range: f32; "range of effectiveness.";
1170 [ffi, ffi_mut] cutoff: f32; "OpenGL cutoff.";
1171 [ffi, ffi_mut] softness: f32; "spotlight edge softness.";
1172 [ffi, ffi_mut] exponent: f32; "OpenGL exponent.";
1173 ]);
1174
1175 getter_setter! {
1176 [&] with, get, set, [
1177 [ffi, ffi_mut] active: bool; "active flag.";
1178 [ffi, ffi_mut] castshadow: bool; "whether light cast shadows."
1179 ]
1180 }
1181
1182 string_set_get_with! {[&]
1183 texture; "texture name for image lights.";
1184 targetbody; "target body for targeting.";
1185 }
1186}
1187
1188mjs_struct!(Frame with SpecObject: MjsFrame <= mjsFrame);
1192impl MjsFrame {
1193 add_x_method_by_frame! { body, site, joint, geom, camera, light }
1194
1195 getter_setter! {
1196 [&] with, get, [
1197 [ffi, ffi_mut] pos: &[f64; 3]; "frame position.";
1198 [ffi, ffi_mut] quat: &[f64; 4]; "frame orientation.";
1199 [ffi, ffi_mut] alt: &MjsOrientation; "alternative orientation.";
1200 ]
1201 }
1202
1203 #[deprecated(since = "6.1.0", note = "the field holds the class of the frame; use `default`")]
1208 pub fn childclass(&self) -> &str {
1209 unsafe { read_mjs_string(self.ffi().childclass) }
1211 }
1212
1213 #[deprecated(since = "6.1.0", note = "writes an unchecked class name; use `set_default`")]
1218 pub fn set_childclass(&mut self, value: &str) {
1219 unsafe { write_mjs_string(value, self.ffi_mut().childclass) };
1221 }
1222
1223 #[deprecated(since = "6.1.0", note = "writes an unchecked class name; use `set_default`")]
1228 pub fn with_childclass(&mut self, value: &str) -> &mut Self {
1229 unsafe { write_mjs_string(value, self.ffi_mut().childclass) };
1231 self
1232 }
1233
1234 #[expect(deprecated, reason = "try_add_frame keeps the implementation until it is removed")]
1239 pub fn add_frame(&mut self) -> &mut MjsFrame {
1240 self.try_add_frame().expect("mjs_addFrame returned null; allocation failed")
1241 }
1242
1243 #[deprecated(
1252 since = "6.0.0",
1253 note = "always returns Ok; use `add_frame`"
1254 )]
1255 pub fn try_add_frame(&mut self) -> Result<&mut MjsFrame, MjEditError> {
1256 let parent_body = unsafe { mjs_getParent(self.element_mut_pointer()) };
1259 debug_assert!(!parent_body.is_null(), "mjs_getParent returned null; frame has no parent body");
1260 let ptr = unsafe { mjs_addFrame(parent_body, self.ffi_mut()) };
1261 unsafe { MjsFrame::from_ffi_ptr_mut(ptr) }.ok_or(MjEditError::AllocationFailed)
1262 }
1263}
1264
1265mjs_struct!(Actuator with SpecObject: MjsActuator <= mjsActuator);
1271impl MjsActuator {
1272 getter_setter! {
1273 [&] with, get, [
1274 [ffi, ffi_mut] gear: &[f64; 6]; "gear parameters.";
1275 [ffi, ffi_mut] gainprm: &[f64; mjNGAIN as usize]; "gain parameters.";
1276 [ffi, ffi_mut] biasprm: &[f64; mjNBIAS as usize]; "bias parameters.";
1277 [ffi, ffi_mut] dynprm: &[f64; mjNDYN as usize]; "dynamic parameters.";
1278 [ffi, ffi_mut] lengthrange: &[f64; 2]; "transmission length range.";
1279 [ffi, ffi_mut] damping: &[f64; mjNPOLY as usize + 1]; "damping coefficients.";
1280 [ffi, ffi_mut] ctrlrange: &[f64; 2]; "control range.";
1281 [ffi, ffi_mut] velrange: &[f64; 2]; "range of the velocity-setpoint input (pid).";
1282 [ffi, ffi_mut] ffrange: &[f64; 2]; "range of the feedforward input (pid).";
1283 [ffi, ffi_mut] forcerange: &[f64; 2]; "force range.";
1284 [ffi, ffi_mut] actrange: &[f64; 2]; "activation range.";
1285 ]
1286 }
1287
1288 nested_handle!(plugin: MjsPluginReference; "actuator plugin.");
1289
1290 getter_setter!([&] with, get, set, [
1291 [ffi, ffi_mut] gaintype: MjtGain; "gain type.";
1292 [ffi, ffi_mut] biastype: MjtBias; "bias type.";
1293 [ffi, ffi_mut] dyntype: MjtDyn; "dyn type.";
1294 [ffi, ffi_mut] group: i32; "group.";
1295 [ffi, ffi_mut] actdim: i32; "number of activation variables.";
1296 [ffi, ffi_mut] trntype: MjtTrn; "transmission type.";
1297 [ffi, ffi_mut] cranklength: f64; "crank length, for slider-crank.";
1298 [ffi, ffi_mut] inheritrange: f64; "automatic range setting for position and intvelocity.";
1299 [ffi, ffi_mut] armature: f64; "armature inertia.";
1300 [ffi, ffi_mut] nsample: i32; "number of samples in history buffer.";
1301 [ffi, ffi_mut] interp: i32; "interpolation order (0=ZOH, 1=linear, 2=cubic).";
1302 [ffi, ffi_mut] delay: f64; "delay time in seconds; 0: no delay.";
1303 [ffi, ffi_mut] ctrlspec: i32; "input signature, scoped by gaintype; 0: type default.";
1304 ]);
1305
1306 getter_setter! {
1307 [&] with, get, set, [
1308 [ffi, ffi_mut] ctrllimited: MjtLimited [force]; "are control limits defined.";
1309 [ffi, ffi_mut] forcelimited: MjtLimited [force]; "are force limits defined.";
1310 ]
1311 }
1312
1313 getter_setter! {
1314 [&] with, get, set, [
1315 [ffi, ffi_mut] actlimited: MjtLimited [force]; "are activation limits defined.";
1316 ]
1317 }
1318
1319 getter_setter! {
1320 [&] with, get, set, [
1321 [ffi, ffi_mut] actearly: bool; "apply next activations to qfrc.";
1322 ]
1323 }
1324
1325 userdata_method!(f64);
1326
1327 string_set_get_with! {[&]
1328 target; "name of transmission target.";
1329 refsite; "reference site, for site transmission.";
1330 slidersite; "site defining cylinder, for slider-crank.";
1331 }
1332}
1333
1334
1335fn actuator_set_result(c_err_msg: *const c_char) -> Result<(), MjEditError> {
1338 let err_msg = unsafe { CStr::from_ptr(c_err_msg) }.to_string_lossy();
1340 if err_msg.is_empty() {
1341 Ok(())
1342 }
1343 else {
1344 Err(MjEditError::InvalidParameter(err_msg.into_owned()))
1345 }
1346}
1347
1348#[derive(Clone, Copy, Debug, Default, PartialEq)]
1353pub struct PositionConfig {
1354 pub kp: f64,
1356 pub inheritrange: f64,
1358 pub kv: Option<f64>,
1360 pub dampratio: Option<f64>,
1362 pub timeconst: Option<f64>,
1364}
1365
1366impl PositionConfig {
1367 getter_setter! {
1368 with, [
1369 kp: f64; "the proportional (position) gain.";
1370 inheritrange: f64; "the automatic range-inheritance factor.";
1371 kv: f64; "the velocity feedback gain (mutually exclusive with dampratio).";
1372 dampratio: f64; "the damping ratio (mutually exclusive with kv).";
1373 timeconst: f64; "the first-order activation-filter time constant.";
1374 ]
1375 }
1376}
1377
1378#[derive(Clone, Copy, Debug, Default, PartialEq)]
1380pub struct IntVelocityConfig {
1381 pub kp: f64,
1383 pub inheritrange: f64,
1385 pub kv: Option<f64>,
1387 pub dampratio: Option<f64>,
1389 pub timeconst: Option<f64>,
1391}
1392
1393impl IntVelocityConfig {
1394 getter_setter! {
1395 with, [
1396 kp: f64; "the proportional gain.";
1397 inheritrange: f64; "the automatic range-inheritance factor.";
1398 kv: f64; "the velocity feedback gain (mutually exclusive with dampratio).";
1399 dampratio: f64; "the damping ratio (mutually exclusive with kv).";
1400 timeconst: f64; "the first-order activation-filter time constant.";
1401 ]
1402 }
1403}
1404
1405#[derive(Clone, Copy, Debug, Default, PartialEq)]
1409pub struct DcMotorConfig {
1410 pub resistance: f64,
1412 pub ctrlspec: i32,
1415 pub motorconst: Option<[f64; 2]>,
1417 pub nominal: Option<[f64; 3]>,
1419 pub saturation: Option<[f64; 3]>,
1421 pub inductance: Option<[f64; 2]>,
1423 pub cogging: Option<[f64; 3]>,
1425 pub controller: Option<[f64; 6]>,
1427 pub thermal: Option<[f64; 6]>,
1429 pub lugre: Option<[f64; 5]>,
1431}
1432
1433impl DcMotorConfig {
1434 getter_setter! {
1435 with, [
1436 resistance: f64; "the electrical resistance.";
1437 ctrlspec: i32; "the input signature bitmask ([`MjtCtrlInput`](crate::wrappers::mj_model::MjtCtrlInput)).";
1438 motorconst: [f64; 2]; "the torque and back-EMF constants [Kt, Ke].";
1439 nominal: [f64; 3]; "the nominal ratings [voltage, stall_torque, no_load_speed].";
1440 saturation: [f64; 3]; "the saturation [tau_max, i_max, di_dt_max].";
1441 inductance: [f64; 2]; "the inductance [L, te].";
1442 cogging: [f64; 3]; "the cogging [amplitude, periodicity, phase].";
1443 controller: [f64; 6]; "the controller [kp, ki, kd, slewmax, Imax, v_max].";
1444 thermal: [f64; 6]; "the thermal [R_th, C, tau_th, alpha, T0, T_ambient].";
1445 lugre: [f64; 5]; "the LuGre friction [stiffness, damping, coulomb, static, stribeck].";
1446 ]
1447 }
1448}
1449
1450#[derive(Clone, Copy, Debug, Default, PartialEq)]
1454pub struct PidConfig {
1455 pub kp: f64,
1457 pub kv: Option<f64>,
1459 pub dampratio: Option<f64>,
1461 pub ki: Option<f64>,
1463 pub imax: Option<f64>,
1465 pub slewmax: Option<f64>,
1467 pub inheritrange: f64,
1469 pub ctrlspec: i32,
1472}
1473
1474impl PidConfig {
1475 getter_setter! {
1476 with, [
1477 kp: f64; "the proportional (position) gain.";
1478 kv: f64; "the velocity feedback gain (mutually exclusive with dampratio).";
1479 dampratio: f64; "the damping ratio (mutually exclusive with kv).";
1480 ki: f64; "the integral gain on the position error.";
1481 imax: f64; "the anti-windup limit on the integral state.";
1482 slewmax: f64; "the position-setpoint slew rate limit.";
1483 inheritrange: f64; "the automatic range-inheritance factor.";
1484 ctrlspec: i32; "the input signature bitmask ([`MjtCtrlInput`](crate::wrappers::mj_model::MjtCtrlInput)).";
1485 ]
1486 }
1487}
1488
1489#[derive(Clone, Copy, Debug, Default, PartialEq)]
1493pub struct OrientationConfig {
1494 pub kp: f64,
1496 pub kv: Option<f64>,
1498 pub dampratio: Option<f64>,
1500 pub ctrlspec: i32,
1503}
1504
1505impl OrientationConfig {
1506 getter_setter! {
1507 with, [
1508 kp: f64; "the proportional gain, in torque per radian of geodesic error.";
1509 kv: f64; "the velocity feedback gain (mutually exclusive with dampratio).";
1510 dampratio: f64; "the damping ratio (mutually exclusive with kv).";
1511 ctrlspec: i32; "the chart of the commanded orientation ([`MjtCtrlChart`](crate::wrappers::mj_model::MjtCtrlChart)).";
1512 ]
1513 }
1514}
1515
1516impl MjsActuator {
1517 pub fn set_to_motor(&mut self) {
1519 unsafe { mjs_setToMotor(self.ffi_mut()) };
1521 }
1522
1523 pub fn set_to_position(&mut self, config: PositionConfig) -> Result<(), MjEditError> {
1529 let PositionConfig { kp, inheritrange, mut kv, mut dampratio, mut timeconst } = config;
1530 let c_err_msg = unsafe { mjs_setToPosition(
1531 self.ffi_mut(), kp,
1532 kv.as_mut().map_or(ptr::null_mut(), |x| x),
1533 dampratio.as_mut().map_or(ptr::null_mut(), |x| x),
1534 timeconst.as_mut().map_or(ptr::null_mut(), |x| x),
1535 inheritrange
1536 ) };
1537 actuator_set_result(c_err_msg)
1538 }
1539
1540 pub fn set_to_int_velocity(&mut self, config: IntVelocityConfig) -> Result<(), MjEditError> {
1547 let IntVelocityConfig { kp, inheritrange, mut kv, mut dampratio, mut timeconst } = config;
1548 let c_err_msg = unsafe { mjs_setToIntVelocity(
1549 self.ffi_mut(), kp,
1550 kv.as_mut().map_or(ptr::null_mut(), |x| x),
1551 dampratio.as_mut().map_or(ptr::null_mut(), |x| x),
1552 timeconst.as_mut().map_or(ptr::null_mut(), |x| x),
1553 inheritrange
1554 ) };
1555 actuator_set_result(c_err_msg)
1556 }
1557
1558 pub fn set_to_velocity(&mut self, kv: f64) {
1560 unsafe { mjs_setToVelocity(self.ffi_mut(), kv) };
1562 }
1563
1564 pub fn set_to_damper(&mut self, kv: f64) -> Result<(), MjEditError> {
1570 actuator_set_result(unsafe { mjs_setToDamper(self.ffi_mut(), kv) })
1571 }
1572
1573 pub fn set_to_cylinder(&mut self, timeconst: f64, bias: f64, area: f64, diameter: f64) {
1578 unsafe { mjs_setToCylinder(self.ffi_mut(), timeconst, bias, area, diameter) };
1580 }
1581
1582 #[allow(clippy::too_many_arguments)]
1589 pub fn set_to_muscle(
1590 &mut self, mut timeconst: [f64; 2], tausmooth: f64, mut range: [f64; 2],
1591 force: f64, scale: f64, lmin: f64, lmax: f64, vmax: f64, fpmax: f64, fvmax: f64
1592 ) -> Result<(), MjEditError>
1593 {
1594 let c_err_msg = unsafe { mjs_setToMuscle(
1595 self.ffi_mut(), &mut timeconst, tausmooth, &mut range,
1596 force, scale, lmin, lmax, vmax, fpmax, fvmax
1597 ) };
1598 actuator_set_result(c_err_msg)
1599 }
1600
1601 pub fn set_to_adhesion(&mut self, gain: f64) -> Result<(), MjEditError> {
1606 actuator_set_result(unsafe { mjs_setToAdhesion(self.ffi_mut(), gain) })
1607 }
1608
1609 pub fn set_to_dc_motor(&mut self, config: DcMotorConfig) -> Result<(), MjEditError> {
1615 let DcMotorConfig {
1616 resistance, ctrlspec,
1617 mut motorconst, mut nominal, mut saturation, mut inductance,
1618 mut cogging, mut controller, mut thermal, mut lugre
1619 } = config;
1620 let c_err_msg = unsafe { mjs_setToDCMotor(
1621 self.ffi_mut(),
1622 motorconst.as_mut().map_or(ptr::null_mut(), |x| x),
1623 resistance,
1624 nominal.as_mut().map_or(ptr::null_mut(), |x| x),
1625 saturation.as_mut().map_or(ptr::null_mut(), |x| x),
1626 inductance.as_mut().map_or(ptr::null_mut(), |x| x),
1627 cogging.as_mut().map_or(ptr::null_mut(), |x| x),
1628 controller.as_mut().map_or(ptr::null_mut(), |x| x),
1629 thermal.as_mut().map_or(ptr::null_mut(), |x| x),
1630 lugre.as_mut().map_or(ptr::null_mut(), |x| x),
1631 ctrlspec
1632 ) };
1633 actuator_set_result(c_err_msg)
1634 }
1635
1636 pub fn set_to_pid(&mut self, config: PidConfig) -> Result<(), MjEditError> {
1643 let PidConfig {
1644 kp, mut kv, mut dampratio, mut ki,
1645 mut imax, mut slewmax, inheritrange, ctrlspec
1646 } = config;
1647
1648 let c_err_msg = unsafe {
1649 mjs_setToPID(
1650 self.ffi_mut(),
1651 kp,
1652 kv.as_mut().map_or(ptr::null_mut(), |x| x),
1653 dampratio.as_mut().map_or(ptr::null_mut(), |x| x),
1654 ki.as_mut().map_or(ptr::null_mut(), |x| x),
1655 imax.as_mut().map_or(ptr::null_mut(), |x| x),
1656 slewmax.as_mut().map_or(ptr::null_mut(), |x| x),
1657 inheritrange, ctrlspec
1658 )
1659 };
1660 actuator_set_result(c_err_msg)
1661 }
1662
1663 pub fn set_to_orientation(&mut self, config: OrientationConfig) -> Result<(), MjEditError> {
1670 let OrientationConfig {
1671 kp, mut kv, mut dampratio, ctrlspec
1672 } = config;
1673
1674 let c_err_msg = unsafe {
1675 mjs_setToOrientation(
1676 self.ffi_mut(),
1677 kp,
1678 kv.as_mut().map_or(ptr::null_mut(), |x| x),
1679 dampratio.as_mut().map_or(ptr::null_mut(), |x| x),
1680 ctrlspec
1681 )
1682 };
1683 actuator_set_result(c_err_msg)
1684 }
1685}
1686
1687mjs_struct!(Sensor with SpecObject: MjsSensor <= mjsSensor);
1691impl MjsSensor {
1692 getter_setter! {
1693 [&] with, get, [
1694 [ffi, ffi_mut] intprm: &[i32; mjNSENS as usize]; "integer parameters.";
1695 [ffi, ffi_mut] interval: &[f64; 2]; "[period, time_prev] in seconds.";
1696 ]
1697 }
1698
1699 nested_handle!(plugin: MjsPluginReference; "sensor plugin.");
1700
1701 getter_setter!([&] with, get, set, [
1702 [ffi, ffi_mut] type_ + _: MjtSensor; "sensor type.";
1703 [ffi, ffi_mut] objtype: MjtObj { check_objtype, "[`MjEditError::InvalidParameter`] when the object type is not a real object type (i.e. not below [`MjtObj::mjNOBJECT`])" } => MjEditError;
1704 "object type the sensor refers to.";
1705 [ffi, ffi_mut] reftype: MjtObj { check_objtype, "[`MjEditError::InvalidParameter`] when the reference type is not a real object type (i.e. not below [`MjtObj::mjNOBJECT`])" } => MjEditError;
1706 "type of referenced object.";
1707 [ffi, ffi_mut] datatype: MjtDataType; "data type.";
1708 [ffi, ffi_mut] cutoff: f64; "cutoff for real and positive datatypes.";
1709 [ffi, ffi_mut] noise: f64; "noise stdev.";
1710 [ffi, ffi_mut] needstage: MjtStage; "compute stage needed to simulate sensor.";
1711 [ffi, ffi_mut] dim: i32; "number of scalar outputs.";
1712 [ffi, ffi_mut] nsample: i32; "number of samples in history buffer.";
1713 [ffi, ffi_mut] interp: i32; "interpolation order (0=ZOH, 1=linear, 2=cubic).";
1714 [ffi, ffi_mut] delay: f64; "delay time in seconds; 0: no delay.";
1715 ]);
1716
1717 userdata_method!(f64);
1718
1719 string_set_get_with! {[&]
1720 refname; "name of referenced object.";
1721 objname; "name of sensorized object.";
1722 }
1723}
1724
1725mjs_struct!(Flex with SpecObject: MjsFlex <= mjsFlex);
1729impl MjsFlex {
1730 getter_setter! {
1731 [&] with, get, [
1732 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba when material is omitted.";
1733 [ffi, ffi_mut] friction: &[f64; 3]; "one-sided friction coefficients: slide, spin, roll.";
1734 [ffi, ffi_mut] solref: &[MjtNum; mjNREF as usize]; "solver reference.";
1735 [ffi, ffi_mut] solimp: &[MjtNum; mjNIMP as usize]; "solver impedance.";
1736 [ffi, ffi_mut] size: &[f64; 3]; "vertex bounding box half sizes in qpos0.";
1737 [ffi, ffi_mut] cellcount: &[i32; 3]; "grid cell count for finite cell method.";
1738 ]
1739 }
1740
1741 getter_setter! {
1742 [&] with, get, set, [
1743 [ffi, ffi_mut] young: f64; "Young's modulus, in units of pressure (force/area).";
1744 [ffi, ffi_mut] group: i32; "group.";
1745 [ffi, ffi_mut] contype: i32; "contact type.";
1746 [ffi, ffi_mut] conaffinity: i32; "contact affinity.";
1747 [ffi, ffi_mut] condim: i32; "contact dimensionality.";
1748 [ffi, ffi_mut] priority: i32; "contact priority.";
1749 [ffi, ffi_mut] solmix: f64; "solver mixing for contact pairs.";
1750 [ffi, ffi_mut] margin: f64; "margin for contact detection.";
1751 [ffi, ffi_mut] gap: f64; "additional contact detection buffer.";
1752
1753 [ffi, ffi_mut] dim: i32; "element dimensionality.";
1754 [ffi, ffi_mut] radius: f64; "radius around primitive element.";
1755 [ffi, ffi_mut] activelayers: i32; "number of active element layers in 3D.";
1756 [ffi, ffi_mut] edgestiffness: f64; "edge stiffness.";
1757 [ffi, ffi_mut] edgedamping: f64; "edge damping.";
1758 [ffi, ffi_mut] poisson: f64; "Poisson's ratio.";
1759 [ffi, ffi_mut] damping: f64; "Rayleigh's damping.";
1760 [ffi, ffi_mut] thickness: f64; "thickness (2D only).";
1761 [ffi, ffi_mut] elastic2d: i32; "2D passive forces; 0: none, 1: bending, 2: stretching, 3: both.";
1762 [ffi, ffi_mut] order: i32; "interpolation order (1: trilinear, 2: quadratic).";
1763 ]
1764 }
1765
1766 getter_setter! {
1767 [&] with, get, set, [
1768 [ffi, ffi_mut] internal: bool; "enable internal collisions.";
1769 [ffi, ffi_mut] flatskin: bool; "render flex skin with flat shading.";
1770 [ffi, ffi_mut] passive: bool; "mode for passive collisions.";
1771 ]
1772 }
1773
1774 getter_setter! {
1775 [&] with, get, set, [
1776 [ffi, ffi_mut] selfcollide: MjtFlexSelf [force]; "mode for flex self collision.";
1777 ]
1778 }
1779
1780 string_set_get_with! {[&]
1781 material; "name of material used for rendering.";
1782 }
1783
1784 vec_string_set_append! {
1785 nodebody; "node body names.";
1786 vertbody; "vertex body names.";
1787 }
1788
1789 vec_set_get! {
1790 node: f64; "node positions.";
1791 vert: f64; "vertex positions.";
1792 }
1793
1794 vec_set! {
1795 texcoord: f32; "vertex texture coordinates.";
1796 elem: i32; "element vertex ids.";
1797 }
1798
1799 vec_set! {
1800 [unsafe: "The slice must have exactly `(dim + 1) * nelem` entries and every entry \
1801 must be a valid index into the flex texture coordinates."
1802 ] elemtexcoord: i32 => i32; "element texture coordinates.";
1803 }
1804}
1805
1806mjs_struct!(Pair with SpecObject: MjsPair <= mjsPair);
1810impl MjsPair {
1811 getter_setter! {
1812 [&] with, get, [
1813 [ffi, ffi_mut] friction: &[f64; 5]; "contact friction: slide1, slide2, spin, roll1, roll2.";
1814 [ffi, ffi_mut] solref: &[MjtNum; mjNREF as usize]; "solver reference, normal direction.";
1815 [ffi, ffi_mut] solimp: &[MjtNum; mjNIMP as usize]; "solimp for the pair.";
1816 [ffi, ffi_mut] solreffriction: &[MjtNum; mjNREF as usize]; "solver reference, frictional directions.";
1817 ]
1818 }
1819
1820 getter_setter! {
1821 [&] with, get, set, [
1822 [ffi, ffi_mut] margin: f64; "margin for contact detection.";
1823 [ffi, ffi_mut] gap: f64; "additional contact detection buffer.";
1824 [ffi, ffi_mut] adhesion: f64; "adhesive force of contacts.";
1825 [ffi, ffi_mut] condim: i32; "contact dimensionality.";
1826 ]
1827 }
1828
1829 string_set_get_with! {[&]
1830 geomname1; "name of geom 1.";
1831 geomname2; "name of geom 2.";
1832 }
1833}
1834
1835mjs_struct!(Exclude with SpecObject: MjsExclude <= mjsExclude);
1839impl MjsExclude {
1840 string_set_get_with! {[&]
1841 bodyname1; "name of body 1.";
1842 bodyname2; "name of body 2.";
1843 }
1844}
1845
1846mjs_struct!(Equality with SpecObject: MjsEquality <= mjsEquality);
1850impl MjsEquality {
1851 getter_setter! {
1852 [&] with, get, [
1853 [ffi, ffi_mut] data: &[f64; mjNEQDATA as usize]; "data array for equality parameters.";
1854 [ffi, ffi_mut] solref: &[f64; mjNREF as usize]; "solver reference.";
1855 [ffi, ffi_mut] solimp: &[f64; mjNIMP as usize]; "solver impedance.";
1856 ]
1857 }
1858
1859 getter_setter! {[&] with, get, set, [
1860 [ffi, ffi_mut] active: bool; "active flag.";
1861 ]}
1862
1863 getter_setter! {[&] with, get, set, [
1864 [ffi, ffi_mut] type_ + _: MjtEq; "equality type.";
1865 [ffi, ffi_mut] objtype: MjtObj; "type of both objects.";
1866 ]}
1867
1868 string_set_get_with! {[&]
1869 name1; "name of object 1";
1870 name2; "name of object 2";
1871 }
1872}
1873
1874mjs_struct!(Tendon with SpecObject: MjsTendon <= mjsTendon);
1878impl MjsTendon {
1879 getter_setter! {
1880 [&] with, get, [
1881 [ffi, ffi_mut] damping: &[f64; mjNPOLY as usize + 1]; "damping coefficients.";
1882 [ffi, ffi_mut] stiffness: &[f64; mjNPOLY as usize + 1]; "stiffness coefficients.";
1883 [ffi, ffi_mut] springlength: &[f64; 2]; "spring length.";
1884 [ffi, ffi_mut] solref_friction: &[f64; mjNREF as usize]; "solver reference: tendon friction.";
1885 [ffi, ffi_mut] solimp_friction: &[f64; mjNIMP as usize]; "solver impedance: tendon friction.";
1886 [ffi, ffi_mut] range: &[f64; 2]; "range.";
1887 [ffi, ffi_mut] actfrcrange: &[f64; 2]; "actuator force limits.";
1888 [ffi, ffi_mut] solref_limit: &[f64; mjNREF as usize]; "solver reference: tendon limits.";
1889 [ffi, ffi_mut] solimp_limit: &[f64; mjNIMP as usize]; "solver impedance: tendon limits.";
1890 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba when material omitted.";
1891 ]
1892 }
1893
1894 getter_setter! {[&] with, get, set, [
1895 [ffi, ffi_mut] group: i32; "group.";
1896 [ffi, ffi_mut] frictionloss: f64; "friction loss.";
1897 [ffi, ffi_mut] armature: f64; "inertia associated with tendon velocity.";
1898 [ffi, ffi_mut] margin: f64; "margin value for tendon limit detection.";
1899 [ffi, ffi_mut] width: f64; "width for rendering.";
1900 ]}
1901
1902 getter_setter! {
1903 [&] with, get, set, [
1904 [ffi, ffi_mut] limited: MjtLimited [force]; "does tendon have limits (mjtLimited).";
1905 [ffi, ffi_mut] actfrclimited: MjtLimited [force]; "does tendon have actuator force limits."
1906 ]
1907 }
1908
1909 userdata_method!(f64);
1910 string_set_get_with! {[&]
1911 material; "name of material for rendering.";
1912 }
1913
1914 #[allow(deprecated)]
1919 pub fn wrap_site(&mut self, name: &str) -> &mut MjsWrap {
1920 self.try_wrap_site(name).expect("failed to wrap site")
1921 }
1922
1923 #[deprecated(
1935 since = "5.0.0",
1936 note = "always returns Ok; use `wrap_site`"
1937 )]
1938 pub fn try_wrap_site(&mut self, name: &str) -> Result<&mut MjsWrap, MjEditError> {
1939 let cname = CString::new(name).unwrap();
1940 let wrap_ptr = unsafe { mjs_wrapSite(self.ffi_mut(), cname.as_ptr()) };
1941 unsafe { MjsWrap::from_ffi_ptr_mut(wrap_ptr) }.ok_or(MjEditError::AllocationFailed)
1942 }
1943
1944 #[allow(deprecated)]
1949 pub fn wrap_geom(&mut self, name: &str, sidesite: &str) -> &mut MjsWrap {
1950 self.try_wrap_geom(name, sidesite).expect("failed to wrap geom")
1951 }
1952
1953 #[deprecated(
1965 since = "5.0.0",
1966 note = "always returns Ok; use `wrap_geom`"
1967 )]
1968 pub fn try_wrap_geom(&mut self, name: &str, sidesite: &str) -> Result<&mut MjsWrap, MjEditError> {
1969 let cname = CString::new(name).unwrap();
1970 let csidesite = CString::new(sidesite).unwrap();
1971 let wrap_ptr = unsafe { mjs_wrapGeom(
1972 self.ffi_mut(),
1973 cname.as_ptr(), csidesite.as_ptr()
1974 ) };
1975 unsafe { MjsWrap::from_ffi_ptr_mut(wrap_ptr) }.ok_or(MjEditError::AllocationFailed)
1976 }
1977
1978 #[allow(deprecated)]
1983 pub fn wrap_joint(&mut self, name: &str, coef: f64) -> &mut MjsWrap {
1984 self.try_wrap_joint(name, coef).expect("failed to wrap joint")
1985 }
1986
1987 #[deprecated(
1999 since = "5.0.0",
2000 note = "always returns Ok; use `wrap_joint`"
2001 )]
2002 pub fn try_wrap_joint(&mut self, name: &str, coef: f64) -> Result<&mut MjsWrap, MjEditError> {
2003 let cname = CString::new(name).unwrap();
2004 let wrap_ptr = unsafe { mjs_wrapJoint(self.ffi_mut(), cname.as_ptr(), coef) };
2005 unsafe { MjsWrap::from_ffi_ptr_mut(wrap_ptr) }.ok_or(MjEditError::AllocationFailed)
2006 }
2007
2008 #[allow(deprecated)]
2010 pub fn wrap_pulley(&mut self, divisor: f64) -> &mut MjsWrap {
2011 self.try_wrap_pulley(divisor).expect("failed to wrap pulley")
2012 }
2013
2014 #[deprecated(
2023 since = "5.0.0",
2024 note = "always returns Ok; use `wrap_pulley`"
2025 )]
2026 pub fn try_wrap_pulley(&mut self, divisor: f64) -> Result<&mut MjsWrap, MjEditError> {
2027 let wrap_ptr = unsafe { mjs_wrapPulley(self.ffi_mut(), divisor) };
2028 unsafe { MjsWrap::from_ffi_ptr_mut(wrap_ptr) }.ok_or(MjEditError::AllocationFailed)
2029 }
2030
2031 pub fn wrap_num(&self) -> usize {
2033 unsafe { mjs_getWrapNum(self.ffi()) as usize }
2034 }
2035
2036 pub fn wrap(&self, i: usize) -> &MjsWrap {
2041 self.try_wrap(i).unwrap()
2042 }
2043
2044 pub fn try_wrap(&self, i: usize) -> Result<&MjsWrap, MjEditError> {
2049 let len = self.wrap_num();
2050 if i >= len {
2052 return Err(MjEditError::IndexOutOfBounds { id: i, len });
2053 }
2054 let ptr = unsafe { mjs_getWrap(self.ffi(), i as i32) };
2055 Ok(unsafe { MjsWrap::from_ffi_ptr(ptr) }.unwrap())
2057 }
2058
2059 pub fn wrap_mut(&mut self, i: usize) -> &mut MjsWrap {
2064 self.try_wrap_mut(i).unwrap()
2065 }
2066
2067 pub fn try_wrap_mut(&mut self, i: usize) -> Result<&mut MjsWrap, MjEditError> {
2072 let len = self.wrap_num();
2073 if i >= len {
2074 return Err(MjEditError::IndexOutOfBounds { id: i, len });
2075 }
2076 let ptr = unsafe { mjs_getWrap(self.ffi(), i as i32) };
2077 Ok(unsafe { MjsWrap::from_ffi_ptr_mut(ptr) }.unwrap())
2079 }
2080}
2081
2082mjs_struct!(MjsWrap <= mjsWrap {
2086 fn set_name(&mut self, _name: &str) -> Result<(), MjEditError> {
2091 Err(MjEditError::UnsupportedOperation)
2094 }
2095
2096 fn with_name(&mut self, _name: &str) -> &mut Self {
2101 panic!("a wrap carries no name of its own")
2102 }
2103});
2104impl MjsWrap {
2105 getter_setter! {
2106 [&] with, get, set, [
2107 [ffi, ffi_mut] type_ + _: MjtWrap; "wrap type.";
2108 ]
2109 }
2110
2111 pub fn side_site(&self) -> Option<&MjsSite> {
2118 let ptr = unsafe { mjs_getWrapSideSite(self.ffi()) };
2119 unsafe { MjsSite::from_ffi_ptr(ptr) }
2120 }
2121
2122 pub fn divisor(&self) -> f64 {
2125 unsafe { mjs_getWrapDivisor(self.ffi()) }
2126 }
2127
2128 pub fn coef(&self) -> f64 {
2131 unsafe { mjs_getWrapCoef(self.ffi()) }
2132 }
2133}
2134
2135mjs_struct!(Numeric with SpecObject: MjsNumeric <= mjsNumeric);
2139impl MjsNumeric {
2140 getter_setter! {
2141 [&] with, get, set, [
2142 [ffi, ffi_mut] size: i32 { check_numeric_size, "[`MjEditError::InvalidParameter`] when the size is negative" } => MjEditError; "size of the numeric array.";
2143 ]
2144 }
2145
2146 vec_set_get! {
2147 data: f64; "initialization data.";
2148 }
2149}
2150
2151mjs_struct!(Text with SpecObject: MjsText <= mjsText);
2155impl MjsText {
2156 string_set_get_with! {[&]
2157 data; "text string.";
2158 }
2159}
2160
2161mjs_struct!(Tuple with SpecObject: MjsTuple <= mjsTuple);
2165impl MjsTuple {
2166 vec_set! {
2167 objtype: MjtObj => i32 { check_objtype, "[`MjEditError::InvalidParameter`] when any value is not a real object type (i.e. not below [`MjtObj::mjNOBJECT`])" } => MjEditError;
2170 "object types. Every value must be a real object type (an `MjtObj` below `mjNOBJECT`).";
2171 }
2172
2173 vec_string_set_append! {
2174 objname; "object names.";
2175 }
2176
2177 vec_set_get! {
2178 objprm: f64; "object parameters.";
2179 }
2180}
2181
2182mjs_struct!(Key with SpecObject: MjsKey <= mjsKey);
2186impl MjsKey {
2187 getter_setter! {
2188 [&] with, get, set, [
2189 [ffi, ffi_mut] time: f64; "time."
2190 ]
2191 }
2192
2193 vec_set_get! {
2194 qpos: f64; "qpos.";
2195 qvel: f64; "qvel.";
2196 act: f64; "act.";
2197 mpos: f64; "mocap pos.";
2198 mquat: f64; "mocap quat.";
2199 ctrl: f64; "ctrl.";
2200 }
2201}
2202
2203mjs_struct!(Plugin with SpecObject: MjsPlugin <= mjsPlugin);
2207
2208mjs_opaque!(MjsPluginReference <= mjsPlugin,
2209 "Reference to the plugin instance that an element embeds.\n\n\
2210 A body, geom, mesh, actuator or sensor names the instance it uses through this reference. \
2211 The reference carries no element of its own: MuJoCo resolves the `element` field to the \
2212 [`MjsPlugin`] instance that the name selects. Reach the instance itself through \
2213 [`MjSpec::plugin`].");
2214
2215impl MjsPluginReference {
2216 string_set_get_with! {[&]
2217 name; "instance name.";
2218 plugin_name; "plugin name.";
2219 }
2220
2221 getter_setter! {
2222 [&] with, get, set, [
2223 [ffi, ffi_mut] active: bool; "is the plugin active.";
2224 ]
2225 }
2226}
2227
2228impl MjsPlugin {
2229 string_set_get_with! {[&]
2230 name; "instance name.";
2231 plugin_name; "plugin name.";
2232 }
2233
2234 getter_setter! {
2235 [&] with, get, set, [
2236 [ffi, ffi_mut] active: bool; "is the plugin active.";
2237 ]
2238 }
2239}
2240
2241mjs_struct!(Mesh with SpecObject: MjsMesh <= mjsMesh);
2247impl MjsMesh {
2248 getter_setter! {
2249 [&] with, get, [
2250 [ffi, ffi_mut] refpos: &[f64; 3]; "reference position.";
2251 [ffi, ffi_mut] refquat: &[f64; 4]; "reference orientation.";
2252 [ffi, ffi_mut] scale: &[f64; 3]; "scale vector.";
2253 ]
2254 }
2255
2256 nested_handle!(plugin: MjsPluginReference; "sdf plugin.");
2257
2258 getter_setter! {
2259 [&] with, get, set, [
2260 [ffi, ffi_mut] inertia: MjtMeshInertia; "inertia type (convex, legacy, exact, shell).";
2261 [ffi, ffi_mut] maxhullvert: i32; "maximum vertex count for the convex hull.";
2262 [ffi, ffi_mut] octree_maxdepth: i32; "max octree depth.";
2263 ]
2264 }
2265
2266 getter_setter! {
2267 [&] with, get,set, [
2268 [ffi, ffi_mut] smoothnormal: bool; "do not exclude large-angle faces from normals.";
2269 [ffi, ffi_mut] needsdf: bool; "compute sdf from mesh.";
2270 ]
2271 }
2272
2273 string_set_get_with! {[&]
2274 content_type; "content type of file.";
2275 file; "mesh file.";
2276 material; "name of material.";
2277 }
2278
2279 vec_set! {
2280 uservert: f32; "user vertex data.";
2281 usernormal: f32; "user normal data.";
2282 usertexcoord: f32; "user texcoord data.";
2283 userface: i32; "user vertex indices.";
2284 }
2285
2286 vec_set! {
2287 [unsafe: "Every entry must be in `0..N`, where `N` is the number of user normals: the \
2288 length of the slice passed to `set_usernormal` divided by 3 (each normal is 3 \
2289 `f32`: x, y, z)."]
2290 userfacenormal: i32 => i32; "user face normal indices.";
2291 [unsafe: "Every entry must be in `0..ntexcoord` (the number of user texture coordinates), and \
2292 the slice length must equal the length of the slice passed to `set_userface` (3 per \
2293 face). Unlike face-normal data, MuJoCo does not validate the texcoord-index length, \
2294 so an oversized slice overflows the model's face-texcoord buffer at compile time."]
2295 userfacetexcoord: i32 => i32; "user texcoord indices.";
2296 }
2297}
2298
2299mjs_struct!(HField with SpecObject: MjsHfield <= mjsHField);
2303impl MjsHfield {
2304 getter_setter! {
2305 [&] with, get, [
2306 [ffi, ffi_mut] size: &[f64; 4]; "size of the hfield.";
2307 ]
2308 }
2309
2310 getter_setter! { [&] with, get, set, [
2311 [ffi, ffi_mut] nrow: i32; "number of rows.";
2312 [ffi, ffi_mut] ncol: i32; "number of columns.";
2313 ]}
2314
2315 string_set_get_with! {[&]
2316 content_type; "content type of file.";
2317 file; "file: (nrow, ncol, [elevation data]).";
2318 }
2319
2320 pub fn set_userdata<T: AsRef<[f32]>>(&mut self, userdata: T) {
2322 unsafe { write_mjs_vec_f32(userdata.as_ref(), self.ffi().userdata) };
2324 }
2325}
2326
2327mjs_struct!(Skin with SpecObject: MjsSkin <= mjsSkin);
2331impl MjsSkin {
2332 getter_setter! {
2333 [&] with, get, [
2334 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba when material is omitted.";
2335 ]
2336 }
2337
2338 getter_setter! {
2339 [&] with, get, set, [
2340 [ffi, ffi_mut] inflate: f32; "inflate in normal direction.";
2341 [ffi, ffi_mut] group: i32; "group for visualization.";
2342 ]
2343 }
2344
2345 string_set_get_with! {[&]
2346 material; "name of material used for rendering.";
2347 file; "skin file.";
2348 }
2349
2350 vec_string_set_append! {
2351 bodyname; "body names.";
2352 }
2353
2354 vec_set! {
2355 vert: f32; "vertex positions.";
2356 texcoord: f32; "texture coordinates.";
2357 bindpos: f32; "bind pos.";
2358 bindquat: f32; "bind quat.";
2359 }
2360
2361 vec_set! {
2362 [
2363 unsafe:
2364 "The slice length must be a multiple of 3 and every entry must be in `0..nvert` (the number of skin vertices)."
2365 ] face: i32 => i32; "faces.";
2366 }
2367
2368 vec_vec_append! {
2369 vertid: i32; "vertex ids.";
2370 vertweight: f32; "vertex weights.";
2371 }
2372}
2373
2374mjs_struct!(Texture with SpecObject: MjsTexture <= mjsTexture);
2378
2379impl MjsTexture {
2385 getter_setter! {
2386 [&] with, get, [
2387 [ffi, ffi_mut] rgb1: &[f64; 3]; "first color for builtin.";
2388 [ffi, ffi_mut] rgb2: &[f64; 3]; "second color for builtin.";
2389 [ffi, ffi_mut] markrgb: &[f64; 3]; "mark color.";
2390 [ffi, ffi_mut] gridsize: &[i32; 2]; "size of grid for composite file; (1,1)-repeat.";
2391 [ffi, ffi_mut] gridlayout: &[c_char; 12]; "row-major: L,R,F,B,U,D for faces; . for unused.";
2392 ]
2393 }
2394
2395 getter_setter! {
2396 [&] with, get, set, [
2397 [ffi, ffi_mut] random: f64; "probability of random dots.";
2398 [ffi, ffi_mut] width: i32; "image width.";
2399 [ffi, ffi_mut] height: i32; "image height.";
2400 ]
2401 }
2402
2403 getter_setter! {
2404 [&] with, get, set, [
2405 [ffi, ffi_mut] nchannel: i32; "number of channels.";
2407 ]
2408 }
2409
2410 getter_setter! {
2411 [&] with, get, set, [
2412 [ffi, ffi_mut] type_ + _: MjtTexture [force]; "texture type.";
2413 [ffi, ffi_mut] colorspace: MjtColorSpace [force]; "colorspace.";
2414 [ffi, ffi_mut] builtin: MjtBuiltin [force]; "builtin type.";
2415 [ffi, ffi_mut] mark: MjtMark [force]; "mark type.";
2416 ]
2417 }
2418
2419 vec_string_set_append! {
2420 cubefiles[MjtCubeFace] => cubefile; "different file for each side of the cube.";
2421 }
2422
2423 getter_setter! {[&] with, get, set, [
2424 [ffi, ffi_mut] hflip: bool; "horizontal flip.";
2425 [ffi, ffi_mut] vflip: bool; "vertical flip.";
2426 ]}
2427
2428 pub fn set_data<T: bytemuck::NoUninit>(&mut self, data: &[T]) {
2430 unsafe { write_mjs_vec_byte(data, self.ffi().data) };
2432 }
2433
2434 string_set_get_with! {[&]
2435 file; "png file to load; use for all sides of cube.";
2436 content_type; "content type of file.";
2437 }
2438}
2439
2440mjs_struct!(Material with SpecObject: MjsMaterial <= mjsMaterial);
2444
2445impl MjsMaterial {
2452 getter_setter! {
2453 [&] with, get, [
2454 [ffi, ffi_mut] rgba: &[f32; 4]; "rgba color.";
2455 [ffi, ffi_mut] texrepeat: &[f32; 2]; "texture repetition for 2D mapping.";
2456 ]
2457 }
2458
2459 getter_setter! {[&] with, get, set, [
2460 [ffi, ffi_mut] texuniform: bool; "make texture cube uniform.";
2461 ]}
2462
2463 getter_setter! {
2464 [&] with, get, set, [
2465 [ffi, ffi_mut] emission: f32; "emission.";
2466 [ffi, ffi_mut] specular: f32; "specular.";
2467 [ffi, ffi_mut] shininess: f32; "shininess.";
2468 [ffi, ffi_mut] reflectance: f32; "reflectance.";
2469 [ffi, ffi_mut] metallic: f32; "metallic.";
2470 [ffi, ffi_mut] roughness: f32; "roughness.";
2471 ]
2472 }
2473
2474 vec_string_set_append! {
2475 textures[MjtTextureRole] => texture; "names of textures (empty: none).";
2476 }
2477}
2478
2479
2480mjs_struct!(Body with SpecObject: MjsBody <= mjsBody);
2484
2485impl MjsBody {
2486 add_x_method! { body, site, joint, geom, camera, light }
2487
2488 pub fn child(&self, name: &str) -> Option<&MjsBody> {
2494 let c_name = CString::new(name).unwrap();
2495 unsafe {
2496 let ptr = mjs_findChild(self.ffi(), c_name.as_ptr());
2497 MjsBody::from_ffi_ptr(ptr)
2498 }
2499 }
2500
2501 pub fn child_mut(&mut self, name: &str) -> Option<&mut MjsBody> {
2515 let c_name = CString::new(name).unwrap();
2516 unsafe {
2517 let ptr = mjs_findChild(self.ffi(), c_name.as_ptr());
2518 MjsBody::from_ffi_ptr_mut(ptr)
2519 }
2520 }
2521 #[expect(deprecated, reason = "try_add_frame keeps the implementation until it is removed")]
2527 pub fn add_frame(&mut self) -> &mut MjsFrame {
2528 self.try_add_frame().expect("mjs_addFrame returned null; allocation failed")
2529 }
2530
2531 #[deprecated(
2540 since = "6.0.0",
2541 note = "always returns Ok; use `add_frame`"
2542 )]
2543 pub fn try_add_frame(&mut self) -> Result<&mut MjsFrame, MjEditError> {
2544 let ptr = unsafe { mjs_addFrame(self.ffi_mut(), ptr::null_mut()) };
2547 unsafe { MjsFrame::from_ffi_ptr_mut(ptr) }.ok_or(MjEditError::AllocationFailed)
2548 }
2549
2550 pub fn add_free_joint(&mut self) -> &mut MjsJoint {
2557 let ptr = unsafe { mjs_addFreeJoint(self.ffi_mut()) };
2559 unsafe { MjsJoint::from_ffi_ptr_mut(ptr) }
2560 .expect("mjs_addFreeJoint returned null; allocation failed")
2561 }
2562}
2563
2564#[derive(Debug, Clone)]
2594pub struct MjFlexcompConfig<'a> {
2595 pub r#type: Option<&'a str>,
2598 pub dim: u8,
2601 pub dof: Option<&'a str>,
2603 pub count: Option<[u16; 3]>,
2605 pub cellcount: Option<[u16; 3]>,
2607 pub spacing: Option<[f64; 3]>,
2609 pub scale: Option<[f64; 3]>,
2611 pub radius: f64,
2613 pub mass: f64,
2615 pub inertiabox: f64,
2618 pub equality: u8,
2620 pub rigid: bool,
2622 pub flatskin: bool,
2625 pub elastic2d: u8,
2627 pub pos: Option<[f64; 3]>,
2629 pub quat: Option<[f64; 4]>,
2631 pub origin: Option<[f64; 3]>,
2633 pub file: Option<&'a str>,
2635 pub vfs: Option<&'a MjVfs>,
2637}
2638
2639impl Default for MjFlexcompConfig<'_> {
2642 fn default() -> Self {
2643 Self {
2644 r#type: None,
2645 dim: 2,
2646 dof: None,
2647 count: None,
2648 cellcount: None,
2649 spacing: None,
2650 scale: None,
2651 radius: 0.005,
2652 mass: 0.0,
2653 inertiabox: 0.0,
2654 equality: 0,
2655 rigid: false,
2656 flatskin: false,
2657 elastic2d: 0,
2658 pos: None,
2659 quat: None,
2660 origin: None,
2661 file: None,
2662 vfs: None,
2663 }
2664 }
2665}
2666
2667impl<'a> MjFlexcompConfig<'a> {
2668 getter_setter! {
2669 with, [
2670 r#type: &'a str; "the flexcomp type: \"grid\", \"box\", \"cylinder\", \"ellipsoid\", \"square\", \"disc\", \"circle\", \"mesh\", \"gmsh\", or \"direct\" (default \"grid\").";
2671 dim: u8; "the dimensionality of the flex object (1, 2, or 3); ignored for types that imply it.";
2672 dof: &'a str; "the dof parametrization: \"full\", \"radial\", \"trilinear\", \"quadratic\", or \"2d\" (default \"full\").";
2673 count: [u16; 3]; "the number of generated points in each dimension (grid/box/cylinder/ellipsoid).";
2674 cellcount: [u16; 3]; "the number of interpolation-grid cells in each dimension (trilinear/quadratic dofs).";
2675 spacing: [f64; 3]; "the spacing between generated points in each dimension.";
2676 scale: [f64; 3]; "the scaling of all point coordinates (applied after the pose transformation).";
2677 radius: f64; "the radius of the flex elements.";
2678 mass: f64; "the total mass, divided evenly over the generated points.";
2679 inertiabox: f64; "the equivalent-inertia box size used to set each body's rotational inertia.";
2680 equality: u8; "the edge equality constraint: 0 none, 1 edge, 2 vertex, 3 strain.";
2681 rigid: bool; "whether all points are vertices in the parent body (no new bodies created).";
2682 flatskin: bool; "render flex skin with flat shading.";
2683 elastic2d: u8; "the 2D passive force mode: 0 none, 1 bending, 2 stretching, 3 both.";
2684 pos: [f64; 3]; "the translation of all points relative to the parent body frame.";
2685 quat: [f64; 4]; "the quaternion rotation of all points around the position offset.";
2686 origin: [f64; 3]; "the flexcomp origin used to build a volumetric mesh from a surface mesh.";
2687 file: &'a str; "the file to load the surface or volumetric mesh from.";
2688 vfs: &'a MjVfs; "the virtual file system used to resolve the mesh file.";
2689 ]
2690 }
2691}
2692
2693impl MjsBody {
2694 pub fn add_flexcomp(&mut self, name: &str, config: &MjFlexcompConfig) -> &mut MjsFlex {
2704 self.try_add_flexcomp(name, config).expect("mjs_makeFlex returned null")
2705 }
2706
2707 pub fn try_add_flexcomp(&mut self, name: &str, config: &MjFlexcompConfig)
2716 -> Result<&mut MjsFlex, MjEditError>
2717 {
2718 let c_name = CString::new(name).unwrap();
2720 let c_type = config.r#type.map(|s| CString::new(s).unwrap());
2721 let c_dof = config.dof.map(|s| CString::new(s).unwrap());
2722 let c_file = config.file.map(|s| CString::new(s).unwrap());
2723
2724 let count = config.count.map(|c| c.map(|v| v as c_int));
2726 let cellcount = config.cellcount.map(|c| c.map(|v| v as c_int));
2727 let count_ptr = count.as_ref().map_or(ptr::null(), |a| a as *const [c_int; 3]);
2728 let cellcount_ptr = cellcount.as_ref().map_or(ptr::null(), |a| a as *const [c_int; 3]);
2729 let spacing_ptr = config.spacing.as_ref().map_or(ptr::null(), |a| a as *const [f64; 3]);
2730 let scale_ptr = config.scale.as_ref().map_or(ptr::null(), |a| a as *const [f64; 3]);
2731 let pos_ptr = config.pos.as_ref().map_or(ptr::null(), |a| a as *const [f64; 3]);
2732 let quat_ptr = config.quat.as_ref().map_or(ptr::null(), |a| a as *const [f64; 4]);
2733 let origin_ptr = config.origin.as_ref().map_or(ptr::null(), |a| a as *const [f64; 3]);
2734
2735 let ptr = unsafe {
2738 mjs_makeFlex(
2739 self.ffi_mut(),
2740 c_name.as_ptr(),
2741 c_type.as_ref().map_or(ptr::null(), |c| c.as_ptr()),
2742 config.dim as c_int,
2743 c_dof.as_ref().map_or(ptr::null(), |c| c.as_ptr()),
2744 count_ptr,
2745 cellcount_ptr,
2746 spacing_ptr,
2747 scale_ptr,
2748 config.radius,
2749 config.mass,
2750 config.inertiabox,
2751 config.equality as c_int,
2752 config.rigid as c_int,
2753 config.flatskin as c_int,
2754 config.elastic2d as c_int,
2755 pos_ptr,
2756 quat_ptr,
2757 origin_ptr,
2758 c_file.as_ref().map_or(ptr::null(), |c| c.as_ptr()),
2759 config.vfs.map_or(ptr::null(), |v| v.ffi() as *const mjVFS),
2760 )
2761 };
2762 unsafe { MjsFlex::from_ffi_ptr_mut(ptr) }.ok_or(MjEditError::AllocationFailed)
2763 }
2764}
2765
2766impl MjsBody {
2767 getter_setter! {
2768 [&] with, get, [
2769 [ffi, ffi_mut] pos: &[f64; 3]; "frame position.";
2771 [ffi, ffi_mut] quat: &[f64; 4]; "frame orientation.";
2772 [ffi, ffi_mut] alt: &MjsOrientation; "frame alternative orientation.";
2773
2774 [ffi, ffi_mut] ipos: &[f64; 3]; "inertial frame position.";
2776 [ffi, ffi_mut] iquat: &[f64; 4]; "inertial frame orientation.";
2777 [ffi, ffi_mut] inertia: &[f64; 3]; "diagonal inertia (in i-frame).";
2778 [ffi, ffi_mut] ialt: &MjsOrientation; "inertial frame alternative orientation.";
2779 [ffi, ffi_mut] fullinertia: &[f64; 6]; "non-axis-aligned inertia matrix.";
2780 ]
2781 }
2782
2783 nested_handle!(plugin: MjsPluginReference; "passive force plugin.");
2784
2785 getter_setter! {
2786 [&] with, get, set, [
2787 [ffi, ffi_mut] mass: f64; "mass.";
2788 [ffi, ffi_mut] gravcomp: f64; "gravity compensation.";
2789 [ffi, ffi_mut] sleep: MjtSleepPolicy; "sleep policy.";
2790 ]
2791 }
2792
2793 getter_setter! {
2794 [&] with, get, set, [
2795 [ffi, ffi_mut] mocap: bool; "whether this is a mocap body.";
2796 [ffi, ffi_mut] explicitinertial: bool; "whether to save the body with explicit inertial clause.";
2797 [ffi, ffi_mut] simple: bool; "simple body optimization (false: disabled, true: auto).";
2798 ]
2799 }
2800
2801 userdata_method!(f64);
2802}
2803
2804#[derive(Debug)]
2806pub struct MjsBodyItemIterMut<'a, T> {
2807 ffi_ptr: *mut mjsBody,
2809 last: *mut mjsElement,
2811 recurse: bool,
2812 item_type: PhantomData<&'a mut T>
2813}
2814
2815impl<'a, T: SpecObject> MjsBodyItemIterMut<'a, T> {
2816 fn new(root: &'a mut MjsBody, recurse: bool) -> Self {
2817 let ffi_ptr = unsafe { root.ffi_mut() } as *mut mjsBody;
2819 let last = unsafe { mjs_firstChild(ffi_ptr, T::OBJ_TYPE, recurse.into()) };
2820 Self { ffi_ptr, last, recurse, item_type: PhantomData }
2821 }
2822}
2823
2824impl<'a, T: SpecObject + 'a> Iterator for MjsBodyItemIterMut<'a, T> {
2825 type Item = &'a mut T;
2826
2827 fn next(&mut self) -> Option<Self::Item> {
2828 if self.last.is_null() {
2829 return None;
2830 }
2831
2832 unsafe {
2833 let out = T::from_element_as_ptr_mut(self.last).as_mut();
2834 self.last = mjs_nextChild(self.ffi_ptr, self.last, self.recurse.into());
2835 out
2836 }
2837 }
2838}
2839
2840impl<'a, T: SpecObject + 'a> std::iter::FusedIterator for MjsBodyItemIterMut<'a, T> {}
2841
2842#[derive(Debug, Clone)]
2844pub struct MjsBodyItemIter<'a, T> {
2845 ffi_ptr: *const mjsBody,
2846 last: *const mjsElement,
2848 recurse: bool,
2849 item_type: PhantomData<&'a T>
2850}
2851
2852
2853impl<'a, T: SpecObject> MjsBodyItemIter<'a, T> {
2854 fn new(root: &'a MjsBody, recurse: bool) -> Self {
2855 let ffi_ptr = root.ffi() as *const mjsBody;
2856 let last = unsafe {
2859 mjs_firstChild(
2860 ffi_ptr,
2861 T::OBJ_TYPE,
2862 recurse.into()
2863 )
2864 };
2865 Self { ffi_ptr, last, recurse, item_type: PhantomData }
2866 }
2867}
2868
2869impl<'a, T: SpecObject + 'a> Iterator for MjsBodyItemIter<'a, T> {
2870 type Item = &'a T;
2871
2872 fn next(&mut self) -> Option<Self::Item> {
2873 if self.last.is_null() {
2874 return None;
2875 }
2876 unsafe {
2877 let out = T::from_element_as_ptr_mut(self.last as *mut _).as_ref();
2878 self.last = mjs_nextChild(self.ffi_ptr, self.last, self.recurse.into());
2881 out
2882 }
2883 }
2884}
2885
2886impl<'a, T: SpecObject + 'a> std::iter::FusedIterator for MjsBodyItemIter<'a, T> {}
2887
2888impl MjsBody {
2890 body_get_iter! {[joint, geom, site, camera, light, frame] }
2891 body_get_iter! { direct_children_mut: [body] }
2892}
2893
2894#[cfg(test)]
2898mod tests {
2899 use std::io::Write;
2900 use std::path::{Path, PathBuf};
2901 use std::fs;
2902
2903 use super::*;
2904
2905 const MODEL: &str = "\
2906<mujoco>
2907 <worldbody>
2908 <light ambient=\"0.2 0.2 0.2\"/>
2909 <body name=\"ball\" pos=\".2 .2 .1\">
2910 <geom name=\"green_sphere\" size=\".1\" rgba=\"0 1 0 1\" solref=\"0.004 1.0\"/>
2911 <joint name=\"ball\" type=\"free\"/>
2912 </body>
2913 <geom name=\"floor1\" type=\"plane\" size=\"10 10 1\" solref=\"0.004 1.0\"/>
2914 </worldbody>
2915</mujoco>";
2916
2917 #[test]
2918 fn test_activate_plugin() {
2919 use crate::wrappers::mj_plugin::load_all_plugin_libraries;
2920 const PLUGIN: &str = "mujoco.elasticity.cable";
2921
2922 let mut spec = MjSpec::new();
2923 assert_eq!(spec.activate_plugin("not.a.plugin"), Err(MjEditError::NotFound));
2924
2925 let Ok(lib_dir) = std::env::var("MUJOCO_DYNAMIC_LINK_DIR") else { return };
2926 let plugin_dir = Path::new(&lib_dir).parent().unwrap().join("bin/mujoco_plugin");
2927 load_all_plugin_libraries(&plugin_dir, None).unwrap();
2928
2929 spec.activate_plugin(PLUGIN).unwrap();
2930 spec.compile().unwrap();
2931
2932 let xml = spec.save_xml_string(4096).unwrap();
2933 assert!(xml.contains(&format!("<plugin plugin=\"{PLUGIN}\"/>")), "{xml}");
2934 }
2935
2936 #[test]
2937 fn test_spec_authored_accessor() {
2938 use crate::mujoco_c::{mjtDisableBit, mjtEnableBit};
2939
2940 const AUTHORED: &str = "\
2943<mujoco>
2944 <option actuatorgroupdisable=\"3\">
2945 <flag contact=\"disable\" energy=\"enable\"/>
2946 </option>
2947 <worldbody>
2948 <light ambient=\"0.2 0.2 0.2\"/>
2949 <body name=\"ball\" pos=\".2 .2 .1\">
2950 <geom name=\"green_sphere\" size=\".1\" rgba=\"0 1 0 1\"/>
2951 <joint name=\"ball\" type=\"free\"/>
2952 </body>
2953 </worldbody>
2954</mujoco>";
2955
2956 let contact = mjtDisableBit::mjDSBL_CONTACT as i32;
2957 let gravity = mjtDisableBit::mjDSBL_GRAVITY as i32;
2958 let energy = mjtEnableBit::mjENBL_ENERGY as i32;
2959
2960 let spec = MjSpec::from_xml_string(AUTHORED).unwrap();
2961 let authored = spec.authored();
2962
2963 assert_eq!(authored.disableflags & contact, contact, "contact disable must be authored");
2965 assert_eq!(authored.enableflags & energy, energy, "energy enable must be authored");
2966 assert_eq!(authored.disableactuator & (1 << 3), 1 << 3, "actuator group 3 must be authored");
2967 assert_eq!(authored.disableflags & gravity, 0, "gravity was never authored");
2969
2970 let plain = MjSpec::from_xml_string(MODEL).unwrap();
2972 let plain_authored = plain.authored();
2973 assert_eq!(plain_authored.disableflags, 0);
2974 assert_eq!(plain_authored.enableflags, 0);
2975 assert_eq!(plain_authored.disableactuator, 0);
2976 }
2977
2978 #[test]
2979 fn test_parse_xml_string() {
2980 assert!(MjSpec::from_xml_string(MODEL).is_ok(), "failed to parse the model");
2981 }
2982
2983 #[test]
2984 fn test_parse_xml_file() {
2985 const PATH: &str = "./mj_spec_test_parse_xml_file.xml";
2986 let mut file = fs::File::create(PATH).expect("file creation failed");
2987 file.write_all(MODEL.as_bytes()).expect("unable to write to file");
2988 file.flush().unwrap();
2989
2990 let spec = MjSpec::from_xml(PATH);
2991 fs::remove_file(PATH).expect("file removal failed");
2992 assert!(spec.is_ok(), "failed to parse the model");
2993 }
2994
2995 #[test]
2996 fn test_parse_xml_vfs() {
2997 const PATH: &str = "./mj_spec_test_parse_xml_vfs.xml";
2998 let mut vfs = MjVfs::new();
2999 vfs.add_from_buffer(PATH, MODEL.as_bytes()).unwrap();
3000 assert!(MjSpec::from_xml_vfs(PATH, &vfs).is_ok(), "failed to parse the model");
3001 }
3002
3003 #[test]
3004 fn test_basic_edit_compile() {
3005 const TIMESTEP: f64 = 0.010;
3006 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3007 spec.option_mut().timestep = TIMESTEP; let compiled = spec.compile().expect("could not compile the model");
3010 assert_eq!(compiled.opt().timestep, TIMESTEP);
3011
3012 spec.compile().unwrap();
3013 }
3014
3015 fn heightfield_spec_and_vfs() -> (MjSpec, MjVfs) {
3018 const HEIGHTMAP_NAME: &str = "test_height.bin";
3019 const N_ROW_COLUMN: u32 = 10;
3020
3021 let mut heightfield_bytes = vec![N_ROW_COLUMN.to_ne_bytes(), N_ROW_COLUMN.to_ne_bytes()];
3022 heightfield_bytes.extend_from_slice(&[[0; 4]; (N_ROW_COLUMN * N_ROW_COLUMN) as usize]);
3023
3024 let mut vfs = MjVfs::new();
3025 vfs.add_from_buffer(HEIGHTMAP_NAME, heightfield_bytes.as_flattened())
3026 .expect("failed to add heightfield to VFS");
3027
3028 let mut spec = MjSpec::new();
3029 spec.add_hfield()
3030 .with_file(HEIGHTMAP_NAME)
3031 .with_name(HEIGHTMAP_NAME)
3032 .with_size([1000.0, 1000.0, 5.0, 5.0]);
3033
3034 spec.world_body_mut().add_geom()
3035 .with_type(MjtGeom::mjGEOM_HFIELD)
3036 .with_hfieldname(HEIGHTMAP_NAME);
3037
3038 (spec, vfs)
3039 }
3040
3041 #[test]
3042 fn test_compile_vfs() {
3043 let (mut spec, vfs) = heightfield_spec_and_vfs();
3044
3045 assert!(matches!(
3047 spec.compile().unwrap_err(),
3048 MjEditError::CompileFailed(e) if e.starts_with("Error: Error opening file")
3049 ));
3050
3051 spec.compile_with_vfs(&vfs).expect("compilation with vfs failed");
3053 }
3054
3055 #[test]
3056 fn test_encode_xml() {
3057 const PATH_XML: &str = "mj_spec_test_encode_xml.xml";
3058
3059 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3060 let model = spec.compile().expect("could not compile the model");
3061
3062 spec.encode(PATH_XML, "text/xml").expect("XML encoding failed");
3063 let mut spec_xml = MjSpec::from_xml(PATH_XML).expect("XML parsing failed");
3064 std::fs::remove_file(PATH_XML).unwrap();
3065 assert!(model.is_compatible_with_model(&spec_xml.compile().unwrap()));
3066
3067 assert!(matches!(spec.encode("mj_spec_test_encode_xml.txt", ""), Err(MjEditError::SaveFailed(_))));
3068 assert!(matches!(spec.encode("/nonexistent/dir/model.xml", ""), Err(MjEditError::SaveFailed(_))));
3069 }
3070
3071 #[test]
3072 fn test_encode_mjz_vfs() {
3073 const PATH_MJZ: &str = "mj_spec_test_encode_mjz_vfs.mjz";
3074 const PATH_MJZ_NO_ASSETS: &str = "mj_spec_test_encode_mjz_no_assets.mjz";
3075
3076 let (mut spec, vfs) = heightfield_spec_and_vfs();
3077 let model = spec.compile_with_vfs(&vfs).expect("compilation with vfs failed");
3078
3079 spec.encode_with_vfs(PATH_MJZ, "application/zip", &vfs).expect("MJZ encoding failed");
3081 spec.encode(PATH_MJZ_NO_ASSETS, "application/zip").expect("MJZ encoding failed");
3084
3085
3086 let vfs_mjz = MjVfs::new();
3087 let vfs_no_assets = MjVfs::new();
3088
3089 let mut spec_mjz = MjSpec::from_parse_vfs(PATH_MJZ, "application/zip", &vfs_mjz)
3091 .expect("MJZ parsing failed");
3092 let mut spec_no_assets = MjSpec::from_parse_vfs(PATH_MJZ_NO_ASSETS, "application/zip", &vfs_no_assets)
3094 .expect("MJZ parsing failed");
3095
3096 std::fs::remove_file(PATH_MJZ).unwrap();
3097 std::fs::remove_file(PATH_MJZ_NO_ASSETS).unwrap();
3098
3099 assert!(model.is_compatible_with_model(&spec_mjz.compile_with_vfs(&vfs_mjz).unwrap()));
3101 assert!(matches!(
3102 spec_no_assets.compile_with_vfs(&vfs_no_assets),
3103 Err(MjEditError::CompileFailed(_))
3104 ));
3105 }
3106
3107 #[test]
3108 fn test_model_name() {
3109 const DEFAULT_MODEL_NAME: &str = "MuJoCo Model";
3110 const NEW_MODEL_NAME: &str = "Test model";
3111
3112 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3113
3114 assert_eq!(spec.modelname(), DEFAULT_MODEL_NAME);
3116 spec.set_modelname(NEW_MODEL_NAME);
3118 assert_eq!(spec.modelname(), NEW_MODEL_NAME);
3119
3120 spec.compile().unwrap();
3121 }
3122
3123 #[test]
3124 fn test_item_name() {
3125 const NEW_MODEL_NAME: &str = "Test model";
3126
3127 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3128 let world = spec.world_body_mut();
3129 let body = world.add_body();
3130 assert_eq!(body.name(), "");
3131 body.set_name(NEW_MODEL_NAME).unwrap();
3132 assert_eq!(body.name(), NEW_MODEL_NAME);
3133
3134 spec.compile().unwrap();
3135 }
3136
3137 #[test]
3138 fn test_body_remove() {
3139 const NEW_MODEL_NAME: &str = "Test model";
3140
3141 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3142 let world = spec.world_body_mut();
3143 let body = world.add_body();
3144 body.set_name(NEW_MODEL_NAME).unwrap();
3145
3146 assert!(
3148 unsafe { spec.body_mut(NEW_MODEL_NAME).unwrap().delete() }.is_ok(),
3149 "failed to delete model"
3150 );
3151 assert!(spec.body(NEW_MODEL_NAME).is_none(), "body was not removed from spec");
3152
3153 let world = unsafe { spec.world_body_mut().delete() };
3155 assert!(world.is_err(), "the world model should not be deletable");
3156
3157 spec.compile().unwrap();
3158 }
3159
3160 #[test]
3161 fn test_joint_remove() {
3162 const NEW_NAME: &str = "Test model";
3163
3164 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3165 let world = spec.world_body_mut();
3166 let joint = world.add_joint();
3167 joint.set_name(NEW_NAME).unwrap();
3168
3169 assert!(
3171 unsafe { spec.joint_mut(NEW_NAME).unwrap().delete() }.is_ok(),
3172 "failed to delete model"
3173 );
3174 assert!(spec.joint(NEW_NAME).is_none(), "body was not removed fom spec");
3175
3176 spec.compile().unwrap();
3177 }
3178
3179 #[test]
3180 fn test_add_free_joint() {
3181 const ARMATURE: f64 = 0.25;
3182
3183 let mut spec = MjSpec::new();
3184 spec.default_mut("main").unwrap().joint_mut().set_armature(ARMATURE);
3185
3186 let free_body = spec.world_body_mut().add_body();
3187 free_body.add_geom().with_size([0.010; 3]);
3188 let free_joint = free_body.add_free_joint();
3189 assert_eq!(free_joint.type_(), MjtJoint::mjJNT_FREE);
3190 assert_eq!(free_joint.armature(), 0.0, "the free joint must not inherit the default class");
3191
3192 let hinge_body = spec.world_body_mut().add_body();
3193 hinge_body.add_geom().with_size([0.010; 3]);
3194 assert_eq!(hinge_body.add_joint().armature(), ARMATURE);
3195
3196 let model = spec.compile().unwrap();
3197 assert_eq!(model.nq(), 8);
3199 assert_eq!(model.dof_armature()[..6], [0.0; 6]);
3200 assert_eq!(model.dof_armature()[6], ARMATURE);
3201 }
3202
3203 #[test]
3204 fn test_add_with_class() {
3205 const MARGIN: f64 = 0.5;
3206 const GROUP: i32 = 2;
3207
3208 let mut spec = MjSpec::new();
3209 let class = spec.add_default("cls", None);
3210 class.geom_mut().set_margin(MARGIN);
3211 class.actuator_mut().set_group(GROUP);
3212
3213 let body = spec.world_body_mut().add_body();
3214 body.add_joint().with_name("hinge");
3215 assert_eq!(body.add_geom_with_class("cls").unwrap().with_size([0.010; 3]).margin(), MARGIN);
3216 assert_eq!(
3217 body.add_frame().add_geom_with_class("cls").unwrap().with_size([0.010; 3]).margin(),
3218 MARGIN
3219 );
3220
3221 assert_eq!(body.add_geom().with_size([0.010; 3]).margin(), 0.0);
3222 assert!(matches!(body.add_geom_with_class("nope"), Err(MjEditError::NotFound)));
3223
3224 let actuator = spec.add_actuator_with_class("cls").unwrap().with_trntype(MjtTrn::mjTRN_JOINT);
3225 assert_eq!(actuator.group(), GROUP);
3226 actuator.set_target("hinge");
3227
3228 let model = spec.compile().unwrap();
3229 assert_eq!(model.geom_margin(), [MARGIN, MARGIN, 0.0]);
3230 assert_eq!(model.actuator_group(), [GROUP]);
3231 }
3232
3233 #[test]
3234 fn test_default_class_of_parent() {
3235 const MARGIN: f64 = 0.5;
3236
3237 let mut spec = MjSpec::new();
3238 spec.add_default("cls", None).geom_mut().set_margin(MARGIN);
3239
3240 let body = spec.world_body_mut().add_body();
3241 body.set_default("cls").unwrap();
3242 assert_eq!(body.add_geom().with_size([0.010; 3]).margin(), MARGIN);
3243 assert_eq!(body.add_body().add_geom().with_size([0.010; 3]).margin(), MARGIN);
3244
3245 let frame = spec.world_body_mut().add_body().add_frame();
3247 frame.set_default("cls").unwrap();
3248 assert!(frame.default().is_some());
3249 assert_eq!(frame.add_geom().with_size([0.010; 3]).margin(), 0.0);
3250 }
3251
3252 #[test]
3253 fn test_hfield_remove() {
3254 const NEW_NAME: &str = "Test hfield";
3255
3256 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3257 let hfield = spec.add_hfield();
3258 hfield.set_name(NEW_NAME).unwrap();
3259
3260 let hfield = spec.hfield_mut(NEW_NAME).expect("failed to obtain the hfield");
3262 assert!(unsafe { hfield.delete() }.is_ok(), "failed to delete hfield");
3263 assert!(spec.hfield(NEW_NAME).is_none(), "hfield was not removed from spec");
3264
3265 spec.compile().unwrap();
3266 }
3267
3268 #[test]
3269 fn test_body_userdata() {
3270 const NEW_USERDATA: [f64; 3] = [1.0, 2.0, 3.0];
3271
3272 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3273 let world = spec.world_body_mut();
3274
3275 assert_eq!(world.userdata(), []);
3276
3277 world.set_userdata(NEW_USERDATA);
3278 assert_eq!(world.userdata(), NEW_USERDATA);
3279
3280 spec.compile().unwrap();
3281 }
3282
3283 #[test]
3284 fn test_body_attrs() {
3285 const TEST_VALUE_F64: f64 = 5.25;
3286
3287 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3288 let world = spec.world_body_mut();
3289
3290 world.set_gravcomp(TEST_VALUE_F64);
3291 assert_eq!(world.gravcomp(), TEST_VALUE_F64);
3292
3293 world.pos_mut()[0] = TEST_VALUE_F64;
3294 assert_eq!(world.pos()[0], TEST_VALUE_F64);
3295
3296 spec.compile().unwrap();
3297 }
3298
3299 #[test]
3300 fn test_default() {
3301 const DEFAULT_NAME: &str = "floor";
3302 const NOT_DEFAULT_NAME: &str = "floor-not";
3303
3304 let mut spec = MjSpec::from_xml_string(MODEL).expect("unable to load the spec");
3305
3306 spec.add_default(DEFAULT_NAME, None);
3308
3309 assert!(spec.default(DEFAULT_NAME).is_some());
3311 assert!(spec.default(NOT_DEFAULT_NAME).is_none());
3312
3313 let world = spec.world_body_mut();
3314 let some_body = world.add_body();
3315 some_body.add_joint().with_name("test");
3316 some_body.add_geom().with_size([0.010, 0.0, 0.0]);
3317
3318 let actuator = spec.add_actuator()
3319 .with_trntype(MjtTrn::mjTRN_JOINT);
3320 actuator.set_target("test");
3321
3322 assert!(actuator.set_default(DEFAULT_NAME).is_ok());
3323
3324 spec.compile().unwrap();
3325 }
3326
3327 #[test]
3328 fn test_actuator_set_to() {
3329 let mut spec = MjSpec::new();
3330 let body = spec.world_body_mut().add_body();
3331 body.add_geom().with_size([0.01, 0.0, 0.0]);
3332 body.add_joint().with_name("hinge").with_type(MjtJoint::mjJNT_HINGE);
3333
3334 let actuator = spec.add_actuator().with_trntype(MjtTrn::mjTRN_JOINT);
3335 actuator.set_target("hinge");
3336
3337 actuator.set_to_motor();
3339 assert_eq!(actuator.gaintype(), MjtGain::mjGAIN_FIXED);
3340 assert_eq!(actuator.biastype(), MjtBias::mjBIAS_NONE);
3341 assert_eq!(actuator.dyntype(), MjtDyn::mjDYN_NONE);
3342 assert_eq!(actuator.gainprm()[0], 1.0);
3343
3344 actuator.set_to_velocity(2.0);
3346 assert_eq!(actuator.gaintype(), MjtGain::mjGAIN_FIXED);
3347 assert_eq!(actuator.biastype(), MjtBias::mjBIAS_AFFINE);
3348 assert_eq!(actuator.gainprm()[0], 2.0);
3349 assert_eq!(actuator.biasprm()[2], -2.0);
3350
3351 assert!(actuator.set_to_damper(-1.0).is_err());
3353 assert!(actuator.set_to_damper(5.0).is_ok());
3354 assert_eq!(actuator.gaintype(), MjtGain::mjGAIN_AFFINE);
3355 assert_eq!(actuator.gainprm()[2], -5.0);
3356
3357 assert!(actuator.set_to_adhesion(-1.0).is_err());
3359 assert!(actuator.set_to_adhesion(1.0).is_ok());
3360
3361 actuator.set_to_cylinder(0.1, 0.0, 1.0, -1.0);
3363 assert_eq!(actuator.dyntype(), MjtDyn::mjDYN_FILTER);
3364
3365 assert!(actuator.set_to_muscle([-1.0, -1.0], -1.0, [-1.0, -1.0], -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0).is_err());
3367 actuator.set_to_muscle([-1.0, -1.0], 0.0, [-1.0, -1.0], -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0).unwrap();
3368 assert_eq!(actuator.gaintype(), MjtGain::mjGAIN_MUSCLE);
3369 assert_eq!(actuator.biastype(), MjtBias::mjBIAS_MUSCLE);
3370 assert_eq!(actuator.dyntype(), MjtDyn::mjDYN_MUSCLE);
3371
3372 assert!(actuator.set_to_position(
3374 PositionConfig::default().with_kp(1.0).with_kv(1.0).with_dampratio(1.0)
3375 ).is_err());
3376 actuator.set_to_position(PositionConfig::default().with_kp(1.0).with_kv(1.0)).unwrap();
3377
3378 actuator.set_to_int_velocity(IntVelocityConfig::default().with_kp(1.0)).unwrap();
3380 assert_eq!(actuator.dyntype(), MjtDyn::mjDYN_INTEGRATOR);
3381
3382 assert!(actuator.set_to_dc_motor(DcMotorConfig::default()).is_err());
3384 actuator.set_to_dc_motor(
3385 DcMotorConfig::default().with_motorconst([1.0, 1.0]).with_resistance(1.0)
3386 ).unwrap();
3387 assert_eq!(actuator.gaintype(), MjtGain::mjGAIN_DCMOTOR);
3388 assert_eq!(actuator.biastype(), MjtBias::mjBIAS_DCMOTOR);
3389 assert_eq!(actuator.dyntype(), MjtDyn::mjDYN_DCMOTOR);
3390
3391 actuator.set_to_motor();
3393 actuator.set_actearly(false);
3394 actuator.set_ctrllimited(MjtLimited::mjLIMITED_FALSE);
3395 spec.compile().unwrap();
3396 }
3397
3398 #[test]
3399 fn test_save() {
3400 const EXPECTED_XML: &str = "\
3401<mujoco model=\"MuJoCo Model\">
3402 <compiler angle=\"radian\"/>
3403
3404 <worldbody>
3405 <body>
3406 <geom size=\"0.01\"/>
3407 <site/>
3408 <camera/>
3409 <light/>
3410 </body>
3411 </worldbody>
3412</mujoco>
3413";
3414
3415 let mut spec = MjSpec::new();
3416 let world = spec.world_body_mut();
3417 let body = world.add_body();
3418 body.add_camera();
3419 body.add_geom().with_size([0.010, 0.0, 0.0]);
3420 body.add_light();
3421 body.add_site();
3422
3423 spec.compile().unwrap();
3424 assert_eq!(spec.save_xml_string(1000).unwrap(), EXPECTED_XML);
3425
3426 spec.compile().unwrap();
3427 }
3428
3429 #[test]
3432 fn test_save_xml_string_buffer_too_small() {
3433 let mut spec = MjSpec::new();
3434 spec.world_body_mut().add_body().add_geom().with_size([0.01, 0.0, 0.0]);
3435 spec.compile().unwrap();
3436
3437 let err = spec.save_xml_string(1)
3438 .expect_err("expected XmlBufferTooSmall with a 1-byte buffer");
3439 let required_size = match err {
3440 MjEditError::XmlBufferTooSmall { required_size } => required_size,
3441 other => panic!("expected XmlBufferTooSmall, got {other:?}"),
3442 };
3443 assert!(required_size > 1, "required_size must exceed the original 1-byte buffer");
3444
3445 let xml = spec.save_xml_string(required_size + 1)
3447 .expect("save_xml_string should succeed with required_size + 1 bytes");
3448 assert!(!xml.is_empty(), "saved XML must be non-empty");
3449 }
3450
3451 #[test]
3452 fn test_site() {
3453 const TEST_MATERIAL: &str = "material 1";
3454 const TEST_POSITION: [f64; 3] = [1.0, 2.0, 3.0];
3455 const SITE_NAME: &str = "test_site";
3456
3457 let mut spec = MjSpec::new();
3458
3459 spec.add_material().with_name(TEST_MATERIAL);
3461
3462 let world = spec.world_body_mut();
3464 world.add_site()
3465 .with_name(SITE_NAME);
3466 let site = spec.site_mut(SITE_NAME).unwrap();
3467
3468 assert_eq!(site.material(), "");
3470 site.set_material(TEST_MATERIAL);
3471 assert_eq!(site.material(), TEST_MATERIAL);
3472
3473 let test_userdata: Vec<f64> = vec![0.0; 5];
3475 assert_eq!(site.userdata(), []);
3476 site.set_userdata(&test_userdata);
3477 assert_eq!(site.userdata(), test_userdata);
3478
3479 assert_eq!(site.pos(), &[0.0; 3]);
3481 *site.pos_mut() = TEST_POSITION;
3482 assert_eq!(site.pos(), &TEST_POSITION);
3483
3484 spec.compile().unwrap();
3485 }
3486
3487 #[test]
3488 fn test_frame() {
3489 let mut spec = MjSpec::new();
3490 let world = spec.world_body_mut()
3491 .with_gravcomp(10.0);
3492
3493 world.add_frame()
3494 .with_name("frame_a")
3495 .with_pos([0.5, 0.5, 0.05])
3496 .add_body()
3497 .add_geom()
3498 .with_size([1.0, 0.0, 0.0]);
3499
3500 assert!(spec.frame("frame_a").is_some());
3501 assert!(spec.frame_mut("frame_a").is_some());
3502
3503 spec.compile().unwrap();
3504 }
3505
3506 #[test]
3507 fn test_wrap() {
3508 let mut spec = MjSpec::new();
3509 let world = spec.world_body_mut();
3510 let body1= world.add_body().with_pos([0.0, 0.0, 0.5]);
3511 body1.add_geom().with_size([0.010;3]);
3512 body1.add_site().with_name("ball1");
3513 body1.add_joint().with_type(MjtJoint::mjJNT_FREE);
3514
3515 let body2= world.add_body().with_pos([0.0, 0.0, 0.5]);
3516 body2.add_geom().with_size([0.010;3]);
3517 body2.add_site().with_name("ball2");
3518 body2.add_joint().with_type(MjtJoint::mjJNT_FREE);
3519
3520 let tendon = spec.add_tendon()
3521 .with_range([0.0, 0.25])
3522 .with_rgba([1.0, 0.5, 0.0, 1.0]); tendon.wrap_site("ball1");
3524 tendon.wrap_site("ball2");
3525
3526 spec.world_body_mut().add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3527
3528 spec.compile().unwrap();
3529 }
3530
3531 #[test]
3532 fn test_body_child_and_id() {
3533 let mut spec = MjSpec::new();
3534 let parent = spec.world_body_mut().add_body().with_name("parent");
3535 parent.add_body().with_name("child");
3536
3537 let parent_ref = spec.body("parent").unwrap();
3538 assert!(parent_ref.child("child").is_some());
3539 assert!(parent_ref.child("missing").is_none());
3540 assert_eq!(parent_ref.id(), None);
3541
3542 let parent_mut = spec.body_mut("parent").unwrap();
3543 assert!(parent_mut.child_mut("child").is_some());
3544 assert!(parent_mut.child_mut("missing").is_none());
3545
3546 spec.compile().unwrap();
3547 assert!(spec.body("parent").unwrap().id().is_some());
3548 }
3549
3550 #[test]
3551 fn test_geom() {
3552 const GEOM_NAME: &str = "test_geom";
3553 const GEOM_INVALID_NAME: &str = "geom_test";
3554 let mut spec = MjSpec::new();
3555 spec.world_body_mut().add_geom()
3556 .with_name(GEOM_NAME);
3557
3558 assert!(spec.geom(GEOM_NAME).is_some());
3559 assert!(spec.geom(GEOM_INVALID_NAME).is_none());
3560 }
3561
3562 #[test]
3563 fn test_camera() {
3564 const CAMERA_NAME: &str = "test_cam";
3565 const CAMERA_INVALID_NAME: &str = "cam_test";
3566 let mut spec = MjSpec::new();
3567 spec.world_body_mut().add_camera()
3568 .with_name(CAMERA_NAME);
3569
3570 assert!(spec.camera(CAMERA_NAME).is_some());
3571 assert!(spec.camera(CAMERA_INVALID_NAME).is_none());
3572 }
3573
3574 #[test]
3575 fn test_light() {
3576 const LIGHT_NAME: &str = "test_light";
3577 const LIGHT_INVALID_NAME: &str = "light_test";
3578 let mut spec = MjSpec::new();
3579 spec.world_body_mut().add_light()
3580 .with_name(LIGHT_NAME);
3581
3582 assert!(spec.light(LIGHT_NAME).is_some());
3583 assert!(spec.light(LIGHT_INVALID_NAME).is_none());
3584 }
3585
3586 #[test]
3587 fn test_exclude() {
3588 const EXCLUDE_NAME: &str = "test_exclude";
3589 const EXCLUDE_INVALID_NAME: &str = "exclude_test";
3590 let mut spec = MjSpec::new();
3591
3592 spec.world_body_mut().add_body().with_name("body1-left");
3593 spec.world_body_mut().add_body().with_name("body2-right");
3594
3595 spec.add_exclude()
3596 .with_name(EXCLUDE_NAME)
3597 .with_bodyname1("body1-left")
3598 .with_bodyname2("body2-right");
3599
3600 assert!(spec.exclude(EXCLUDE_NAME).is_some());
3601 assert!(spec.exclude(EXCLUDE_INVALID_NAME).is_none());
3602
3603 assert!(spec.compile().is_ok());
3604 }
3605
3606 #[test]
3607 fn test_mesh() {
3608 let mut spec = MjSpec::new();
3609 let mesh = spec.add_mesh();
3610 assert!(!mesh.needsdf());
3611 mesh.set_needsdf(true);
3612 assert!(mesh.needsdf());
3613
3614 assert!(!mesh.smoothnormal());
3615 mesh.set_smoothnormal(true);
3616 assert!(mesh.smoothnormal());
3617 }
3618
3619 #[test]
3620 fn test_iteration() {
3621 const LAST_BODY_NAME: &str = "subbody";
3622 const LAST_WORLD_BODY_NAME: &str = "body2";
3623 const N_GEOM: usize = 3;
3624 const N_BODY: usize = 4; const N_SITE: usize = 2;
3626 const N_TENDON: usize = 1;
3627 const N_MESH: usize = 0;
3628
3629 let mut spec = MjSpec::new();
3630 let world = spec.world_body_mut();
3631 let body1= world.add_body().with_pos([0.0, 0.0, 0.5]);
3632 body1.add_geom().with_size([0.010;3]);
3633 body1.add_site().with_name("ball1");
3634 body1.add_joint().with_type(MjtJoint::mjJNT_FREE);
3635
3636 let body2= world.add_body().with_pos([0.0, 0.0, 0.5]).with_name(LAST_WORLD_BODY_NAME);
3637 body2.add_geom().with_size([0.010;3]);
3638 body2.add_site().with_name("ball2");
3639 body2.add_joint().with_type(MjtJoint::mjJNT_FREE);
3640
3641 body2.add_body().with_name(LAST_BODY_NAME);
3642
3643 let tendon = spec.add_tendon()
3644 .with_range([0.0, 0.25])
3645 .with_rgba([1.0, 0.5, 0.0, 1.0]); tendon.wrap_site("ball1");
3647 tendon.wrap_site("ball2");
3648
3649 spec.world_body_mut().add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3650
3651 assert_eq!(spec.geom_iter_mut().count(), N_GEOM);
3653 assert_eq!(spec.body_iter().count(), N_BODY);
3654 assert_eq!(spec.site_iter_mut().count(), N_SITE);
3655 assert_eq!(spec.tendon_iter_mut().count(), N_TENDON);
3656 assert_eq!(spec.mesh_iter_mut().count(), N_MESH);
3657 assert_eq!(spec.body_iter().last().unwrap().name(), LAST_BODY_NAME);
3658
3659 let world = spec.world_body_mut();
3661 assert_eq!(world.geom_iter_mut(true).count(), N_GEOM);
3662 assert_eq!(world.body_iter(true).count(), N_BODY - 1); assert_eq!(world.site_iter_mut(true).count(), N_SITE);
3664 assert_eq!(world.body_iter_mut().last().unwrap().name(), LAST_WORLD_BODY_NAME);
3665 }
3666
3667 #[test]
3669 fn test_parse_vfs() {
3670 let mut vfs = MjVfs::new();
3671 vfs.add_from_buffer("hello.xml", MODEL.as_bytes()).unwrap();
3672 let mut spec = MjSpec::from_parse_vfs("hello.xml", "XML", &vfs).unwrap();
3673 let model = spec.compile().unwrap();
3674 assert!(model.geom("floor1").is_some());
3675 }
3676
3677 #[test]
3679 fn test_parse_file() {
3680 std::fs::write("test_parse_vfs.xml", MODEL).unwrap();
3681 let mut spec = MjSpec::from_parse("test_parse_vfs.xml", "XML").unwrap();
3682 std::fs::remove_file("test_parse_vfs.xml").unwrap();
3683 let model = spec.compile().unwrap();
3684 assert!(model.geom("floor1").is_some());
3685 }
3686
3687 #[test]
3688 fn test_tendon_wrap_methods() {
3689 let mut spec = MjSpec::new();
3690 spec.world_body_mut().add_body().with_name("body1");
3691 spec.world_body_mut().add_body().with_name("body2");
3692 spec.world_body_mut().add_site().with_name("site1");
3693
3694 let tendon = spec.add_tendon();
3695 tendon.wrap_site("site1");
3696 tendon.wrap_joint("joint1", 0.5);
3697 tendon.wrap_pulley(1.5);
3698
3699 assert_eq!(tendon.wrap_num(), 3);
3700
3701 let wrap = tendon.wrap(1);
3702 assert_eq!(wrap.coef(), 0.5);
3703
3704 let wrap_pulley = tendon.wrap(2);
3705 assert_eq!(wrap_pulley.divisor(), 1.5);
3706 }
3707
3708 #[test]
3709 fn test_tendon_wrap_out_of_bounds() {
3710 let mut spec = MjSpec::new();
3711 spec.world_body_mut().add_site().with_name("site1");
3712
3713 let tendon = spec.add_tendon();
3714 tendon.wrap_site("site1");
3715 assert_eq!(tendon.wrap_num(), 1);
3716
3717 match tendon.try_wrap(3) {
3719 Err(MjEditError::IndexOutOfBounds { id, len }) => {
3720 assert_eq!(id, 3);
3721 assert_eq!(len, 1);
3722 }
3723 _ => panic!("expected IndexOutOfBounds"),
3724 }
3725 }
3726
3727 #[test]
3728 #[should_panic]
3729 fn test_tendon_wrap_out_of_bounds_panics() {
3730 let mut spec = MjSpec::new();
3731 spec.world_body_mut().add_site().with_name("site1");
3732
3733 let tendon = spec.add_tendon();
3734 tendon.wrap_site("site1");
3735
3736 let _ = tendon.wrap(3);
3738 }
3739
3740 #[test]
3741 fn test_numeric_vec() {
3742 let mut spec = MjSpec::new();
3743 let numeric = spec.add_numeric();
3744 let name = "test_numeric";
3745 numeric.set_name(name).unwrap();
3746 assert_eq!(numeric.name(), name);
3747
3748 let data = [1.5, 2.5, 3.5, 4.5];
3749 numeric.set_data(&data);
3750 assert_eq!(numeric.data(), &data);
3751
3752 spec.compile().unwrap();
3753 }
3754
3755 #[test]
3756 fn test_text_string() {
3757 let mut spec = MjSpec::new();
3758 let text = spec.add_text();
3759 let name = "test_text";
3760 text.set_name(name).unwrap();
3761 assert_eq!(text.name(), name);
3762
3763 let content = "Hello MuJoCo!";
3764 text.set_data(content);
3765 assert_eq!(text.data(), content);
3766
3767 spec.compile().unwrap();
3768 }
3769
3770 #[test]
3771 fn test_tuple_names_and_params() {
3772 let mut spec = MjSpec::new();
3773 spec.world_body_mut().add_body().with_name("body1");
3774 spec.world_body_mut().add_body().with_name("body2");
3775
3776 let tuple_name = "test_tuple";
3777 let obj_param = [1.0, 2.0];
3778
3779 let tuple = spec.add_tuple();
3780 tuple.set_name(tuple_name).unwrap();
3781 assert_eq!(tuple.name(), tuple_name);
3782
3783 tuple.set_objname("body1 body2");
3784 tuple.set_objprm(&obj_param);
3785 tuple.set_objtype(&[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_BODY]).unwrap();
3786
3787 assert_eq!(tuple.objprm(), &obj_param);
3788
3789 spec.compile().unwrap();
3790
3791 let xml = spec.save_xml_string(2000).unwrap();
3793 assert!(xml.contains("objname=\"body1\""));
3794 assert!(xml.contains("objname=\"body2\""));
3795 assert!(xml.contains("prm=\"1\""));
3796 assert!(xml.contains("prm=\"2\""));
3797 }
3798
3799 #[test]
3802 fn test_tendon_wrap_site_compiled_model() {
3803 let mut spec = MjSpec::new();
3804 let world = spec.world_body_mut();
3805
3806 let b1 = world.add_body().with_pos([0.0, 0.0, 0.5]);
3807 b1.add_geom().with_size([0.01; 3]);
3808 b1.add_site().with_name("s1");
3809 b1.add_joint().with_type(MjtJoint::mjJNT_FREE);
3810
3811 let b2 = world.add_body().with_pos([1.0, 0.0, 0.5]);
3812 b2.add_geom().with_size([0.01; 3]);
3813 b2.add_site().with_name("s2");
3814 b2.add_joint().with_type(MjtJoint::mjJNT_FREE);
3815
3816 world.add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3817
3818 let tendon = spec.add_tendon().with_range([0.0, 1.0]);
3819 tendon.wrap_site("s1");
3820 tendon.wrap_site("s2");
3821
3822 let model = spec.compile().unwrap();
3823
3824 assert_eq!(model.ffi().ntendon, 1, "expected one tendon");
3825 assert_eq!(model.ffi().nwrap, 2, "expected two wrap elements");
3826
3827 let wrap_types = model.wrap_type();
3829 assert_eq!(wrap_types[0], MjtWrap::mjWRAP_SITE);
3830 assert_eq!(wrap_types[1], MjtWrap::mjWRAP_SITE);
3831
3832 let wrap_objid = model.wrap_objid();
3834 let s1_id = model.site("s1").unwrap().id as i32;
3835 let s2_id = model.site("s2").unwrap().id as i32;
3836 assert_eq!(wrap_objid[0], s1_id);
3837 assert_eq!(wrap_objid[1], s2_id);
3838 }
3839
3840 #[test]
3843 fn test_tendon_limited_tristate() {
3844 use crate::mujoco_c::mjtLimited::*;
3845
3846 let mut spec = MjSpec::new();
3847 let world = spec.world_body_mut();
3848
3849 let b1 = world.add_body().with_pos([0.0, 0.0, 0.5]);
3850 b1.add_geom().with_size([0.01; 3]);
3851 b1.add_site().with_name("s1");
3852 b1.add_joint().with_type(MjtJoint::mjJNT_FREE);
3853
3854 let b2 = world.add_body().with_pos([1.0, 0.0, 0.5]);
3855 b2.add_geom().with_size([0.01; 3]);
3856 b2.add_site().with_name("s2");
3857 b2.add_joint().with_type(MjtJoint::mjJNT_FREE);
3858
3859 world.add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3860
3861 for (name, val) in [("t_false", mjLIMITED_FALSE), ("t_true", mjLIMITED_TRUE), ("t_auto", mjLIMITED_AUTO)] {
3863 let t = spec.add_tendon()
3864 .with_name(name)
3865 .with_range([0.0, 1.0])
3866 .with_limited(val);
3867 t.wrap_site("s1");
3868 t.wrap_site("s2");
3869 }
3870
3871 for (name, expected) in [("t_false", mjLIMITED_FALSE), ("t_true", mjLIMITED_TRUE), ("t_auto", mjLIMITED_AUTO)] {
3873 let t = spec.tendon(name).expect("tendon not found");
3874 assert_eq!(t.limited(), expected,
3875 "Before compile: tendon '{}' limited should be {:?}", name, expected);
3876 }
3877
3878 let model = spec.compile().unwrap();
3880 let tendon_limited = model.tendon_limited();
3881 assert!(!tendon_limited[0], "Compiled tendon 0 limited should be false");
3883 assert!(tendon_limited[1], "Compiled tendon 1 limited should be true");
3884 let _ = tendon_limited[2];
3886 }
3887
3888 #[test]
3891 fn test_tendon_actfrclimited_tristate() {
3892 use crate::mujoco_c::mjtLimited::*;
3893
3894 let mut spec = MjSpec::new();
3895 let world = spec.world_body_mut();
3896
3897 let b1 = world.add_body().with_pos([0.0, 0.0, 0.5]);
3898 b1.add_geom().with_size([0.01; 3]);
3899 b1.add_site().with_name("s1");
3900 b1.add_joint().with_type(MjtJoint::mjJNT_FREE);
3901
3902 let b2 = world.add_body().with_pos([1.0, 0.0, 0.5]);
3903 b2.add_geom().with_size([0.01; 3]);
3904 b2.add_site().with_name("s2");
3905 b2.add_joint().with_type(MjtJoint::mjJNT_FREE);
3906
3907 world.add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3908
3909 for (name, val) in [("t_false", mjLIMITED_FALSE), ("t_true", mjLIMITED_TRUE), ("t_auto", mjLIMITED_AUTO)] {
3911 let t = spec.add_tendon()
3912 .with_name(name)
3913 .with_range([0.0, 1.0])
3914 .with_actfrcrange([-1.0, 1.0])
3915 .with_actfrclimited(val);
3916 t.wrap_site("s1");
3917 t.wrap_site("s2");
3918 }
3919
3920 for (name, expected) in [("t_false", mjLIMITED_FALSE), ("t_true", mjLIMITED_TRUE), ("t_auto", mjLIMITED_AUTO)] {
3922 let t = spec.tendon(name).expect("tendon not found");
3923 assert_eq!(t.actfrclimited(), expected,
3924 "Before compile: tendon '{}' actfrclimited should be {:?}", name, expected);
3925 }
3926
3927 spec.compile().unwrap();
3929 }
3930
3931 #[test]
3934 fn test_joint_align_tristate() {
3935 use crate::mujoco_c::mjtAlignFree::*;
3936
3937 let mut spec = MjSpec::new();
3938 let world = spec.world_body_mut();
3939 world.add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3940
3941 for (name, val) in [("j_false", mjALIGNFREE_FALSE), ("j_true", mjALIGNFREE_TRUE), ("j_auto", mjALIGNFREE_AUTO)] {
3943 let body = world.add_body().with_pos([0.0, 0.0, 1.0]);
3944 body.add_geom().with_size([0.1; 3]);
3945 body.add_joint()
3946 .with_name(name)
3947 .with_type(MjtJoint::mjJNT_FREE)
3948 .with_align(val);
3949 }
3950
3951 for (name, expected) in [("j_false", mjALIGNFREE_FALSE), ("j_true", mjALIGNFREE_TRUE), ("j_auto", mjALIGNFREE_AUTO)] {
3953 let j = spec.joint(name).expect("joint not found");
3954 assert_eq!(j.align(), expected,
3955 "Before compile: joint '{}' align should be {:?}", name, expected);
3956 }
3957
3958 let model = spec.compile().unwrap();
3960 assert_eq!(model.ffi().njnt as usize, 3, "expected 3 joints");
3961 }
3962
3963 #[test]
3966 fn test_joint_limited_tristate() {
3967 use crate::mujoco_c::mjtLimited::*;
3968
3969 let mut spec = MjSpec::new();
3970 let world = spec.world_body_mut();
3971 world.add_geom().with_type(MjtGeom::mjGEOM_PLANE).with_size([1.0; 3]);
3972
3973 for (name, val) in [("j_false", mjLIMITED_FALSE), ("j_true", mjLIMITED_TRUE), ("j_auto", mjLIMITED_AUTO)] {
3974 let body = world.add_body().with_pos([0.0, 0.0, 1.0]);
3975 body.add_geom().with_size([0.1; 3]);
3976 body.add_joint()
3977 .with_name(name)
3978 .with_type(MjtJoint::mjJNT_SLIDE)
3979 .with_range([0.0, 1.0])
3980 .with_limited(val);
3981 }
3982
3983 for (name, expected) in [("j_false", mjLIMITED_FALSE), ("j_true", mjLIMITED_TRUE), ("j_auto", mjLIMITED_AUTO)] {
3985 let j = spec.joint(name).expect("joint not found");
3986 assert_eq!(j.limited(), expected,
3987 "Before compile: joint '{}' limited should be {:?}", name, expected);
3988 }
3989
3990 let model = spec.compile().unwrap();
3992 let jnt_limited = model.jnt_limited();
3993 assert!(!jnt_limited[0]);
3994 assert!(jnt_limited[1]);
3995 assert!(jnt_limited[2], "Joint with limited=AUTO and range should resolve to true");
3996 }
3997
3998 #[test]
4000 fn test_parse_xml_string_invalid() {
4001 let result = MjSpec::from_xml_string("<not valid mujoco xml>");
4002 assert!(result.is_err(), "parsing invalid XML must return Err");
4003 let msg = result.unwrap_err().to_string();
4004 assert!(!msg.is_empty(), "error message must not be empty for invalid XML");
4005 }
4006
4007 #[test]
4009 fn test_parse_xml_vfs_content() {
4010 const PATH: &str = "./mj_spec_test_parse_xml_vfs_content.xml";
4011 let mut vfs = MjVfs::new();
4012 vfs.add_from_buffer(PATH, MODEL.as_bytes()).unwrap();
4013 let mut spec = MjSpec::from_xml_vfs(PATH, &vfs).expect("VFS parse failed");
4014 let model = spec.compile().expect("compile failed");
4015
4016 assert_eq!(model.ffi().nbody, 2, "expected 2 bodies (world + ball)");
4018 assert_eq!(model.ffi().njnt, 1, "expected 1 joint");
4020 assert_eq!(model.ffi().ngeom, 2, "expected 2 geoms (sphere + floor)");
4022 }
4023
4024 #[test]
4026 fn test_parse_xml_file_content() {
4027 const PATH: &str = "./mj_spec_test_parse_xml_file_content.xml";
4028 let mut file = fs::File::create(PATH).expect("file creation failed");
4029 file.write_all(MODEL.as_bytes()).expect("unable to write");
4030 file.flush().unwrap();
4031
4032 let result = MjSpec::from_xml(PATH);
4033 fs::remove_file(PATH).expect("file removal failed");
4034
4035 let mut spec = result.expect("file parse failed");
4036 let model = spec.compile().expect("compile failed");
4037
4038 assert_eq!(model.ffi().nbody, 2, "expected 2 bodies (world + ball)");
4039 assert_eq!(model.ffi().njnt, 1, "expected 1 joint");
4040 assert_eq!(model.ffi().ngeom, 2, "expected 2 geoms (sphere + floor)");
4041 }
4042
4043 #[test]
4045 fn test_from_parse_path_types() {
4046 const PATH: &str = "./mj_spec_test_from_parse_path_types.xml";
4047 let mut file = fs::File::create(PATH).expect("file creation failed");
4048 file.write_all(MODEL.as_bytes()).expect("write failed");
4049 file.flush().unwrap();
4050
4051 assert!(MjSpec::from_parse(PATH, "").is_ok());
4053 assert!(MjSpec::from_parse(String::from(PATH), "").is_ok());
4055 assert!(MjSpec::from_parse(Path::new(PATH), "").is_ok());
4057 assert!(MjSpec::from_parse(PathBuf::from(PATH), "").is_ok());
4059
4060 fs::remove_file(PATH).expect("file removal failed");
4061 }
4062
4063 #[test]
4065 fn test_from_parse_vfs_path_types() {
4066 const PATH: &str = "./mj_spec_test_from_parse_vfs_path_types.xml";
4067 let mut vfs = MjVfs::new();
4068 vfs.add_from_buffer(PATH, MODEL.as_bytes()).unwrap();
4069
4070 assert!(MjSpec::from_parse_vfs(PATH, "", &vfs).is_ok());
4072 assert!(MjSpec::from_parse_vfs(PathBuf::from(PATH), "", &vfs).is_ok());
4074 assert!(MjSpec::from_parse_vfs(Path::new(PATH), "", &vfs).is_ok());
4076 }
4077
4078 #[test]
4080 fn test_save_xml_path_types() {
4081 let mut spec = MjSpec::new();
4082 spec.world_body_mut().add_body().add_geom().with_size([0.01, 0.0, 0.0]);
4083 spec.compile().unwrap();
4084
4085 let paths: [PathBuf; 3] = [
4086 PathBuf::from("./mj_spec_save_xml_str.xml"),
4087 PathBuf::from("./mj_spec_save_xml_pathbuf.xml"),
4088 PathBuf::from("./mj_spec_save_xml_path.xml"),
4089 ];
4090
4091 spec.save_xml(paths[0].to_str().unwrap()).unwrap();
4093 spec.save_xml(paths[1].clone()).unwrap();
4095 spec.save_xml(paths[2].as_path()).unwrap();
4097
4098 for p in &paths {
4099 let content = fs::read_to_string(p).expect("saved file should be readable");
4100 assert!(content.contains("<mujoco"), "saved XML should contain <mujoco tag");
4101 fs::remove_file(p).expect("cleanup failed");
4102 }
4103 }
4104
4105 #[test]
4106 fn test_material_set_texture() {
4107 let mut spec = MjSpec::new();
4108 let world = spec.world_body_mut();
4109 world.add_geom()
4110 .with_type(MjtGeom::mjGEOM_PLANE)
4111 .with_size([1.0, 1.0, 0.01])
4112 .with_material("floor");
4113
4114 spec.add_texture()
4115 .with_name("floor")
4116 .with_type(MjtTexture::mjTEXTURE_2D)
4117 .with_builtin(MjtBuiltin::mjBUILTIN_CHECKER)
4118 .with_rgb1([0.9, 0.9, 0.9])
4119 .with_rgb2([0.1, 0.1, 0.1])
4120 .with_width(512)
4121 .with_height(512);
4122
4123 let mat = spec.add_material().with_name("floor");
4124 mat.set_texture(MjtTextureRole::mjTEXROLE_RGB, "floor");
4125
4126 let model = spec.compile().unwrap();
4127 let xml = spec.save_xml_string(8192).unwrap();
4128 assert!(xml.contains("texture=\"floor\""), "XML should reference the floor texture");
4129
4130 let mat_info = model.material("floor").unwrap();
4131 let mat_view = mat_info.view(&model);
4132 let tex_id = mat_view.texid;
4133 assert_ne!(tex_id[MjtTextureRole::mjTEXROLE_RGB as usize], -1,
4134 "RGB texture slot should be resolved (not -1)");
4135 }
4136
4137 #[test]
4140 fn test_builtin_texture_nchannel_rejected() {
4141 let mut spec = MjSpec::new();
4142 spec.add_texture()
4143 .with_name("badtex")
4144 .with_type(MjtTexture::mjTEXTURE_2D)
4145 .with_builtin(MjtBuiltin::mjBUILTIN_CHECKER)
4146 .with_rgb1([0.9, 0.9, 0.9])
4147 .with_rgb2([0.1, 0.1, 0.1])
4148 .with_width(64)
4149 .with_height(64)
4150 .set_nchannel(1);
4151
4152 let err = spec.compile().unwrap_err();
4153 assert!(matches!(&err, MjEditError::CompileFailed(msg) if msg.contains("nchannel")),
4154 "compile must reject nchannel < 3 builtin texture, got {err:?}");
4155
4156 let mut ok_spec = MjSpec::new();
4158 ok_spec.add_texture()
4159 .with_name("goodtex")
4160 .with_type(MjtTexture::mjTEXTURE_2D)
4161 .with_builtin(MjtBuiltin::mjBUILTIN_CHECKER)
4162 .with_rgb1([0.9, 0.9, 0.9])
4163 .with_rgb2([0.1, 0.1, 0.1])
4164 .with_width(64)
4165 .with_height(64)
4166 .set_nchannel(3);
4167 assert!(ok_spec.compile().is_ok(), "nchannel == 3 builtin texture should compile");
4168
4169 let mut no_builtin = MjSpec::new();
4172 no_builtin.add_texture()
4173 .with_name("plain")
4174 .with_type(MjtTexture::mjTEXTURE_2D)
4175 .with_width(64)
4176 .with_height(64)
4177 .set_nchannel(1);
4178 assert!(matches!(no_builtin.compile().unwrap_err(),
4179 MjEditError::CompileFailed(msg) if !msg.contains("nchannel")),
4180 "nchannel < 3 without a builtin pattern should not trip the nchannel guard");
4181 }
4182
4183 #[test]
4186 fn test_mjs_flex_cellcount_and_order() {
4187 const FLEX_MODEL: &str = "\
4188<mujoco>\
4189 <worldbody>\
4190 <body name=\"pin\" pos=\"0 0 1\">\
4191 <flexcomp type=\"grid\" count=\"3 3 1\" spacing=\".1 .1 .1\" mass=\"1\"\
4192 name=\"myflex\" radius=\"0.001\" dim=\"2\">\
4193 <elasticity young=\"1e4\" poisson=\"0.0\"/>\
4194 </flexcomp>\
4195 </body>\
4196 </worldbody>\
4197</mujoco>";
4198
4199 let mut spec = MjSpec::from_xml_string(FLEX_MODEL).expect("failed to parse flex model");
4200 let flex = spec.flex("myflex").expect("flex 'myflex' not found in spec");
4201
4202 assert_eq!(flex.cellcount().len(), 3);
4204
4205 {
4207 let flex_mut = spec.flex_mut("myflex").unwrap();
4208 flex_mut.set_order(2);
4209 }
4210 assert_eq!(spec.flex("myflex").unwrap().order(), 2);
4211
4212 {
4213 let flex_mut = spec.flex_mut("myflex").unwrap();
4214 flex_mut.set_order(1);
4215 }
4216 assert_eq!(spec.flex("myflex").unwrap().order(), 1);
4217 }
4218
4219 #[test]
4223 fn test_add_flexcomp() {
4224 let mut spec = MjSpec::new();
4225
4226 let config = MjFlexcompConfig::default()
4227 .with_type("grid")
4228 .with_dim(2)
4229 .with_count([3, 3, 1])
4230 .with_spacing([0.1, 0.1, 0.1])
4231 .with_radius(0.001)
4232 .with_mass(1.0);
4233
4234 {
4235 let flex = spec.world_body_mut().add_flexcomp("genflex", &config);
4236 assert_eq!(flex.dim(), 2);
4237 assert!((flex.radius() - 0.001).abs() < 1e-12);
4238 flex.set_edgedamping(1.0);
4239 }
4240
4241 let flex = spec.flex("genflex").expect("generated flex not found in spec");
4243 assert_eq!(flex.cellcount().len(), 3);
4244
4245 spec.compile().expect("spec with generated flex failed to compile");
4247 }
4248
4249 #[test]
4252 fn test_flexcomp_config_defaults() {
4253 let mut spec = MjSpec::new();
4254
4255 let config = MjFlexcompConfig::default()
4256 .with_count([3, 3, 1])
4257 .with_spacing([0.1, 0.1, 0.1])
4258 .with_mass(1.0);
4259
4260 {
4261 let flex = spec.world_body_mut().add_flexcomp("genflex", &config);
4262 assert_eq!(flex.dim(), 2);
4263 assert!((flex.radius() - 0.005).abs() < 1e-12);
4264 flex.set_edgedamping(1.0);
4265 }
4266
4267 spec.compile().expect("spec with defaulted flex failed to compile");
4268 }
4269
4270 #[test]
4272 #[should_panic]
4273 fn test_sensor_objtype_failure() {
4274 let mut spec = MjSpec::new();
4275 spec.add_sensor()
4276 .with_objtype(MjtObj::mjOBJ_FRAME);
4277 }
4278
4279 #[test]
4281 #[should_panic]
4282 fn test_sensor_reftype_failure() {
4283 let mut spec = MjSpec::new();
4284 spec.add_sensor()
4285 .with_reftype(MjtObj::mjOBJ_FRAME);
4286 }
4287
4288 #[test]
4290 fn test_sensor_objtype_reftype_setters() {
4291 let mut spec = MjSpec::new();
4292 let sensor = spec.add_sensor();
4293
4294 assert!(matches!(
4295 sensor.set_objtype(MjtObj::mjOBJ_MODEL),
4296 Err(MjEditError::InvalidParameter(_))
4297 ));
4298 assert!(matches!(
4299 sensor.set_reftype(MjtObj::mjOBJ_DEFAULT),
4300 Err(MjEditError::InvalidParameter(_))
4301 ));
4302 assert!(sensor.set_objtype(MjtObj::mjOBJ_SITE).is_ok());
4303 assert!(sensor.set_reftype(MjtObj::mjOBJ_BODY).is_ok());
4304 }
4305
4306 #[test]
4309 fn test_tuple_objtype_validation() {
4310 let mut spec = MjSpec::new();
4311 let tuple = spec.add_tuple();
4312
4313 assert!(matches!(
4314 tuple.set_objtype(&[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_FRAME]),
4315 Err(MjEditError::InvalidParameter(_))
4316 ));
4317 assert!(tuple.set_objtype(&[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_GEOM]).is_ok());
4318 }
4319
4320 #[test]
4323 fn test_numeric_size_validation() {
4324 let mut spec = MjSpec::new();
4325 let numeric = spec.add_numeric();
4326
4327 assert!(matches!(
4328 numeric.set_size(-1),
4329 Err(MjEditError::InvalidParameter(_))
4330 ));
4331 assert!(numeric.set_size(4).is_ok());
4332 }
4333
4334 #[test]
4335 fn test_spec_size_setters() {
4336 let mut spec = MjSpec::new();
4337
4338 assert!(matches!(spec.set_nuser_geom(-2), Err(MjEditError::InvalidParameter(_))));
4339 assert_eq!(spec.nuser_geom(), -1, "a rejected count must leave the field unchanged");
4340
4341 spec.set_nuser_geom(3).unwrap();
4342 spec.set_nuserdata(7);
4343 spec.set_nkey(2);
4344 spec.set_memory(1 << 20); let model = spec.compile().unwrap();
4347 assert_eq!(model.nuser_geom(), 3);
4348 assert_eq!(model.nuserdata(), 7);
4349 assert_eq!(model.nkey(), 2);
4350 assert_eq!(model.narena(), 1 << 20);
4351 }
4352
4353 #[test]
4356 fn test_frame_has_no_default() {
4357 let mut spec = MjSpec::new();
4358 assert!(spec.world_body_mut().add_frame().default().is_none());
4359 assert!(spec.world_body_mut().add_geom().default().is_some());
4360 assert!(spec.world_body_mut().default().is_some());
4361 }
4362
4363 #[test]
4366 fn test_default_has_no_id() {
4367 assert!(MjSpec::new().add_default("cls", None).id().is_none());
4368 }
4369
4370 #[test]
4373 fn test_delete_non_utf8_name() {
4374 let mut spec = MjSpec::from_xml_string(
4377 "<mujoco><worldbody><body name=\"a�b\"/></worldbody></mujoco>"
4378 ).unwrap();
4379
4380 let body = spec.world_body_mut().body_iter_mut().next().unwrap();
4381 let name = unsafe { CStr::from_ptr(mjs_getString(mjs_getName(body.element_mut_pointer()))) };
4383 assert!(
4384 std::str::from_utf8(name.to_bytes()).is_err(), "the test needs a non-UTF-8 name"
4385 );
4386
4387 unsafe { body.delete() }.unwrap();
4388 assert_eq!(spec.body_iter().count(), 1);
4389 }
4390
4391 #[test]
4394 fn test_wrap_set_name_rejected() {
4395 let mut spec = MjSpec::new();
4396 spec.world_body_mut().add_site().with_name("s0");
4397 let wrap = spec.add_tendon().wrap_site("s0");
4398 assert!(matches!(wrap.set_name("w0"), Err(MjEditError::UnsupportedOperation)));
4399 assert_eq!(wrap.name(), "s0");
4400 }
4401
4402 #[test]
4405 fn test_delete_rejects_frame() {
4406 let mut spec = MjSpec::new();
4407 let frame = spec.world_body_mut().add_frame();
4408 assert!(matches!(unsafe { frame.delete() }, Err(MjEditError::UnsupportedOperation)));
4409 assert_eq!(spec.world_body().frame_iter(false).count(), 1, "the frame stays in the body");
4410 }
4411
4412 #[test]
4415 #[expect(deprecated, reason = "the test covers the deprecated method itself")]
4416 fn test_delete_element_guards() {
4417 let mut spec = MjSpec::new();
4418 assert!(matches!(
4419 unsafe { spec.delete_element(ptr::null_mut()) }, Err(MjEditError::DeleteFailed(_))
4420 ));
4421
4422 let default = spec.add_default("cls", None).element_mut_pointer();
4423 assert!(matches!(
4424 unsafe { spec.delete_element(default) }, Err(MjEditError::UnsupportedOperation)
4425 ));
4426
4427 spec.world_body_mut().add_site().with_name("s0");
4428 let wrap = spec.add_tendon().wrap_site("s0").element_mut_pointer();
4429 assert!(matches!(
4430 unsafe { spec.delete_element(wrap) }, Err(MjEditError::UnsupportedOperation)
4431 ));
4432
4433 let mut other = MjSpec::new();
4434 let foreign = other.world_body_mut().add_body().element_mut_pointer();
4435 assert!(matches!(
4436 unsafe { spec.delete_element(foreign) }, Err(MjEditError::DeleteFailed(_))
4437 ));
4438 assert_eq!(other.body_iter().count(), 2, "the foreign body stays in its own spec");
4439
4440 let hfield = spec.add_hfield().element_mut_pointer();
4441 unsafe { spec.delete_element(hfield) }.unwrap();
4442 assert_eq!(spec.hfield_iter().count(), 0, "an element of this spec still deletes");
4443 }
4444
4445 #[test]
4447 fn test_delete_body_removes_subtree() {
4448 let mut spec = MjSpec::new();
4449 let parent = spec.world_body_mut().add_body();
4450 parent.set_name("parent").unwrap();
4451 parent.add_body().with_name("child");
4452 spec.body_mut("child").unwrap().add_geom().with_name("g0");
4453
4454 unsafe { spec.body_mut("parent").unwrap().delete() }.unwrap();
4455 assert!(spec.body("child").is_none(), "the child left the spec with its parent");
4456 assert!(spec.geom("g0").is_none(), "so did the geom of the child");
4457 assert_eq!(spec.body_iter().count(), 1, "only the world body stays");
4458 }
4459
4460 #[test]
4463 fn test_delete_body_releases_dependents() {
4464 const MODEL_WITH_ACTUATOR: &str = "\
4465<mujoco>
4466 <worldbody>
4467 <body name=\"arm\"><joint name=\"j0\" type=\"hinge\"/><geom size=\".1\"/></body>
4468 </worldbody>
4469 <actuator><motor name=\"a0\" joint=\"j0\"/></actuator>
4470</mujoco>";
4471
4472 let mut spec = MjSpec::from_xml_string(MODEL_WITH_ACTUATOR).unwrap();
4473 unsafe { spec.body_mut("arm").unwrap().delete() }.unwrap();
4474 assert!(spec.actuator("a0").is_none(), "the delete released the dependent actuator");
4475 assert_eq!(spec.actuator_iter().count(), 0, "no walk reaches it either");
4476 }
4477
4478 #[test]
4484 fn test_delete_after_discardvisual() {
4485 const VISUAL: &str = "\
4486<mujoco>
4487 <worldbody>
4488 <geom name=\"solid\" size=\".1\"/>
4489 <geom name=\"visual\" size=\".1\" contype=\"0\" conaffinity=\"0\" group=\"3\"/>
4490 </worldbody>
4491</mujoco>";
4492
4493 let mut spec = MjSpec::from_xml_string(VISUAL).unwrap();
4494 spec.compiler_mut().set_discardvisual(true);
4495 spec.compile().unwrap();
4496 assert_eq!(spec.geom_iter().count(), 1, "the compile discarded the visual geom");
4497 assert_eq!(spec.geom_iter().next().unwrap().name(), "solid");
4498
4499 for i in 0..64 {
4500 spec.world_body_mut().add_geom().with_name(&format!("fresh{i}"))
4501 .with_size([0.1, 0.0, 0.0]);
4502 }
4503 assert!(
4504 unsafe { spec.geom_iter_mut().next().unwrap().delete() }.is_ok(), "the survivor deletes"
4505 );
4506 assert_eq!(spec.geom_iter().count(), 64);
4507 }
4508
4509 #[test]
4512 fn test_delete_removes_a_whole_selection() {
4513 let mut spec = MjSpec::new();
4514 let world = spec.world_body_mut();
4515 for name in ["g0", "g1", "g2"] {
4516 world.add_geom().with_name(name).with_size([0.1, 0.0, 0.0]);
4517 }
4518
4519 let mut deleted = 0;
4520 while let Some(geom) = spec.geom_iter_mut().next() {
4521 unsafe { geom.delete() }.unwrap();
4522 deleted += 1;
4523 }
4524 assert_eq!(deleted, 3);
4525 assert_eq!(spec.geom_iter().count(), 0);
4526 }
4527
4528 #[test]
4530 fn test_delete_every_element_kind() {
4531 let mut spec = MjSpec::new();
4532 macro_rules! delete_added {
4533 ($($kind:ident),*) => {paste::paste! {$({
4534 let element = spec.[<$kind _iter_mut>]().next().unwrap();
4535 assert!(unsafe { element.delete() }.is_ok(), stringify!($kind));
4536 assert_eq!(spec.[<$kind _iter>]().count(), 0, stringify!($kind));
4537 })*}};
4538 }
4539
4540 let world = spec.world_body_mut();
4541 world.add_body();
4542 world.add_geom();
4543 world.add_site();
4544 world.add_camera();
4545 world.add_light();
4546 world.body_iter_mut().last().unwrap().add_joint();
4547 spec.add_actuator();
4548 spec.add_pair();
4549 spec.add_equality();
4550 spec.add_tendon();
4551 spec.add_mesh();
4552 spec.add_material();
4553 spec.add_sensor();
4554 spec.add_flex();
4555 spec.add_exclude();
4556 spec.add_numeric();
4557 spec.add_text();
4558 spec.add_tuple();
4559 spec.add_key();
4560 spec.add_hfield();
4561 spec.add_skin();
4562 spec.add_texture();
4563 spec.add_plugin();
4564
4565 delete_added!(
4566 joint, geom, site, camera, light, actuator, pair, equality, tendon, mesh, material,
4567 sensor, flex, exclude, numeric, text, tuple, key, hfield, skin, texture, plugin
4568 );
4569
4570 assert!(unsafe { spec.world_body_mut().body_iter_mut().last().unwrap().delete() }.is_ok(), "body");
4572 assert_eq!(spec.body_iter().count(), 1);
4573 }
4574
4575 #[test]
4577 fn test_attachment() {
4578 const BASE_MODEL: &str = r#"
4579 <mujoco>
4580 <worldbody>
4581 <frame name="base_frame_1">
4582 <body name="base_frame_1_body_1">
4583 <geom size="5" name="base_frame_1_body_1_sphere"/>
4584 </body>
4585 </frame>
4586 </worldbody>
4587 </mujoco>
4588 "#;
4589
4590 for prefix in ["", "attached_", "added"] {
4591 for suffix in ["", "_attached", "_added"] {
4592 let mut frame_spec = MjSpec::from_xml_string(BASE_MODEL).unwrap();
4593 let mut main_spec = MjSpec::new();
4594 let frame = frame_spec.frame_mut("base_frame_1").unwrap();
4595 main_spec.world_body_mut().add_frame()
4596 .attach_by_deep_copy(frame, prefix, suffix).expect("attachment failed");
4597
4598 let renamed = |name: &str| format!("{prefix}{name}{suffix}");
4599
4600 assert!(
4603 frame_spec.geom("base_frame_1_body_1_sphere").is_some(),
4604 "the child spec lost its own name"
4605 );
4606 assert_eq!(
4607 frame_spec.geom(&renamed("base_frame_1_body_1_sphere")).is_some(),
4608 prefix.is_empty() && suffix.is_empty(),
4609 "the renamed element appeared in the child spec"
4610 );
4611
4612 main_spec.frame(&renamed("base_frame_1")).expect("frame not attached");
4613 main_spec.body(&renamed("base_frame_1_body_1")).expect("body not attached");
4614 main_spec.geom(&renamed("base_frame_1_body_1_sphere")).expect("geom not attached");
4615
4616 assert_eq!(
4618 main_spec.geom("base_frame_1_body_1_sphere").is_some(),
4619 prefix.is_empty() && suffix.is_empty()
4620 );
4621
4622 assert!(main_spec.geom("base_frame_1_body_1_sphereinvalid").is_none());
4624
4625 let model = main_spec.compile().expect("compilation of the attached spec failed");
4626 assert_eq!(model.nbody(), 2, "the world body plus the attached one");
4627 assert_eq!(model.ngeom(), 1);
4628 }
4629 }
4630 }
4631
4632 #[test]
4634 fn test_attachment_pairs() {
4635 const PARENT_MODEL: &str = r#"
4636 <mujoco>
4637 <worldbody>
4638 <body name="parent_body"/>
4639 <frame name="parent_frame"/>
4640 <site name="parent_site"/>
4641 </worldbody>
4642 </mujoco>
4643 "#;
4644
4645 const CHILD_MODEL: &str = r#"
4646 <mujoco>
4647 <worldbody>
4648 <frame name="child_frame">
4649 <body name="child_body">
4650 <geom size="5" name="child_geom"/>
4651 </body>
4652 </frame>
4653 </worldbody>
4654 </mujoco>
4655 "#;
4656
4657 #[track_caller]
4659 fn assert_attached(parent: &mut MjSpec) {
4660 assert!(parent.geom("p_child_geom_s").is_some(), "the geom is not namespaced");
4661 let model = parent.compile().expect("cannot compile the attached spec");
4662 assert_eq!((model.nbody(), model.ngeom()), (3, 1), "wrong element counts");
4663 }
4664
4665 let specs = || (
4667 MjSpec::from_xml_string(PARENT_MODEL).unwrap(),
4668 MjSpec::from_xml_string(CHILD_MODEL).unwrap()
4669 );
4670
4671 let (mut parent, mut child) = specs();
4673 parent.frame_mut("parent_frame").unwrap()
4674 .attach_by_deep_copy(child.body_mut("child_body").unwrap(), "p_", "_s").unwrap();
4675 assert_attached(&mut parent);
4676
4677 let (mut parent, mut child) = specs();
4678 parent.site_mut("parent_site").unwrap()
4679 .attach_by_deep_copy(child.body_mut("child_body").unwrap(), "p_", "_s").unwrap();
4680 assert_attached(&mut parent);
4681
4682 let (mut parent, mut child) = specs();
4684 parent.frame_mut("parent_frame").unwrap()
4685 .attach_by_deep_copy(child.frame_mut("child_frame").unwrap(), "p_", "_s").unwrap();
4686 assert_attached(&mut parent);
4687
4688 let (mut parent, mut child) = specs();
4689 parent.site_mut("parent_site").unwrap()
4690 .attach_by_deep_copy(child.frame_mut("child_frame").unwrap(), "p_", "_s").unwrap();
4691 assert_attached(&mut parent);
4692
4693 let (mut parent, mut child) = specs();
4695 parent.body_mut("parent_body").unwrap().attach_by_deep_copy(&mut child, "p_", "_s").unwrap();
4696 assert_attached(&mut parent);
4697
4698 let (mut parent, mut child) = specs();
4699 parent.frame_mut("parent_frame").unwrap().attach_by_deep_copy(&mut child, "p_", "_s").unwrap();
4700 assert_attached(&mut parent);
4701
4702 let (mut parent, mut child) = specs();
4703 parent.site_mut("parent_site").unwrap().attach_by_deep_copy(&mut child, "p_", "_s").unwrap();
4704 assert_attached(&mut parent);
4705 }
4706
4707 #[test]
4709 fn test_deep_copy_attach() {
4710 let mut child_spec = MjSpec::new();
4712 let mut parent_spec = MjSpec::new();
4713 unsafe {
4715 parent_spec.world_body_mut().add_frame()
4716 .attach_by_reference(&mut child_spec, "", "")
4717 }.unwrap();
4718
4719 let result = child_spec.compile().unwrap_err();
4721 assert!(
4722 matches!(result, MjEditError::CompileFailed(e) if e.contains("attached by reference")),
4723 "a child attached by reference compiled without reference, which is wrong as the parent shares the references"
4724 );
4725
4726 parent_spec.compile().unwrap();
4728
4729 let mut new_parent_spec = MjSpec::new();
4731 let mut new_child_spec = MjSpec::new();
4732 new_parent_spec.world_body_mut().attach_by_deep_copy(&mut new_child_spec, "", "").unwrap();
4733 assert!(
4734 new_child_spec.compile().is_ok(),
4735 "child spec should not be attached by reference when deep-copy is enabled"
4736 );
4737 new_parent_spec.compile().unwrap();
4738 }
4739}