robot_description_builder/link/geometry/
mesh_geometry.rs

1use super::{GeometryInterface, GeometryShapeContainer};
2use crate::{identifiers::GroupID, transform::Mirror};
3use itertools::Itertools;
4use nalgebra::{vector, Matrix3};
5
6#[cfg(feature = "urdf")]
7use crate::to_rdf::to_urdf::ToURDF;
8#[cfg(feature = "xml")]
9use quick_xml::{events::attributes::Attribute, name::QName};
10
11// TODO: DOCS
12//
13// The fields are public for the Python wrapper. It doesn't change much for the Rust side, since most of the time these will be `Box<dyn GeometryInterface + Sync + Send>`.
14// DOC COPY
15// A trimesh element specified by a filename, and an optional scale that scales the mesh's axis-aligned-bounding-box. Any geometry format is acceptable but specific application compatibility is dependent on implementation. The recommended format for best texture and color support is Collada .dae files. The mesh file is not transferred between machines referencing the same model. It must be a local file. Prefix the filename with package://\<packagename\>/\<path\> to make the path to the mesh file relative to the package \<packagename\>.
16#[derive(Debug, PartialEq, Clone)]
17pub struct MeshGeometry {
18	/// This should be a valid package path to a mesh file (e.g. `"package://robot_description/mesh/{mesh}"`).
19	/// This is unchecked.
20	/// You are on your own here.
21	pub path: String,
22	/// This is the size of the bounding box of the mesh at the current [`scale`](MeshGeometry::scale).
23	///
24	/// The bounding box is expected to be measured such that the center of the bounding box is at the origin.
25	pub bounding_box: (f32, f32, f32),
26	/// The desired scale off the mesh.
27	///
28	/// # Important
29	/// If this is non-zero you need to pre-calculate the scaled [`bounding_box`](MeshGeometry::bounding_box).
30	pub scale: (f32, f32, f32),
31}
32
33impl MeshGeometry {
34	/// Creates a new `MeshGeometry`.
35	///
36	/// # Important
37	/// - [`path`](MeshGeometry::path) should be a valid package path (e.g. `"package://robot_description/mesh/{mesh}"`). You are on your own here.
38	/// - [`bounding_box`](MeshGeometry::bounding_box) should be the bounding box at the current `scale`.
39	/// - [`scale`](MeshGeometry::scale) is either specified or defaults to `(1., 1., 1.)`.
40	pub fn new(
41		path: impl Into<String>,
42		bounding_box: (f32, f32, f32),
43		scale: Option<(f32, f32, f32)>,
44	) -> Self {
45		Self {
46			path: path.into(),
47			bounding_box,
48			scale: scale.unwrap_or((1., 1., 1.)),
49		}
50	}
51}
52
53impl GeometryInterface for MeshGeometry {
54	/// The volume of a mesh is approximated by its boundingbox
55	fn volume(&self) -> f32 {
56		self.bounding_box.0 * self.bounding_box.1 * self.bounding_box.2
57	}
58
59	/// The surface area of a mesh is approximated by its boundingbox
60	fn surface_area(&self) -> f32 {
61		2. * (self.bounding_box.0 * self.bounding_box.1
62			+ self.bounding_box.1 * self.bounding_box.2
63			+ self.bounding_box.0 * self.bounding_box.2)
64	}
65
66	fn boxed_clone(&self) -> Box<dyn GeometryInterface + Sync + Send> {
67		Box::new(self.clone())
68	}
69
70	fn bounding_box(&self) -> (f32, f32, f32) {
71		self.bounding_box
72	}
73
74	fn shape_container(&self) -> GeometryShapeContainer {
75		self.clone().into()
76	}
77}
78
79// TODO: ADD MIRROR TEST
80impl Mirror for MeshGeometry {
81	fn mirrored(&self, mirror_matrix: &Matrix3<f32>) -> Self {
82		// TODO: Add Mirrorable Specifier
83		// if let Some(group_id @ ("L" | "R" | "N"))  = self.path.get_group_id() {
84
85		// }
86		Self {
87			path: self.path.clone(), // TODO: MIRRORable PATH ID
88			bounding_box: self.bounding_box,
89			scale: (mirror_matrix * vector![self.scale.0, self.scale.1, self.scale.2])
90				.iter()
91				.copied()
92				.collect_tuple()
93				.unwrap(),
94		}
95	}
96}
97
98#[cfg(feature = "urdf")]
99impl ToURDF for MeshGeometry {
100	fn to_urdf(
101		&self,
102		writer: &mut quick_xml::Writer<std::io::Cursor<Vec<u8>>>,
103		_urdf_config: &crate::to_rdf::to_urdf::URDFConfig,
104	) -> Result<(), quick_xml::Error> {
105		let element = writer.create_element("geometry");
106		element.write_inner_content(|writer| -> quick_xml::Result<()> {
107			writer
108				.create_element("mesh")
109				.with_attribute(Attribute {
110					key: QName(b"filename"),
111					// Apply GroupID escaping to allow for mirror from `mesh_L` -> `mesh_R`
112					value: self.path.display().as_bytes().into(),
113				})
114				.with_attribute(Attribute {
115					key: QName(b"scale"),
116					value: format!("{} {} {}", self.scale.0, self.scale.1, self.scale.2)
117						.as_bytes()
118						.into(),
119				})
120				.write_empty()?;
121			Ok(())
122		})?;
123		Ok(())
124	}
125}
126
127impl From<MeshGeometry> for Box<dyn GeometryInterface + Sync + Send> {
128	fn from(value: MeshGeometry) -> Self {
129		Box::new(value)
130	}
131}
132
133#[cfg(test)]
134mod tests {
135	#[cfg(feature = "xml")]
136	use std::io::Seek;
137	use test_log::test;
138
139	use super::{GeometryInterface, GeometryShapeContainer, MeshGeometry};
140	#[cfg(feature = "urdf")]
141	use crate::to_rdf::to_urdf::{ToURDF, URDFConfig};
142
143	#[test]
144	fn volume() {
145		assert_eq!(
146			MeshGeometry::new(
147				"package://my-package/description/meshes/mesh_[[L]].dae",
148				(1., 5., 1.),
149				None
150			)
151			.volume(),
152			5.
153		);
154		assert_eq!(
155			MeshGeometry::new(
156				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
157				(3., 3., 3.),
158				None
159			)
160			.volume(),
161			27.
162		);
163		assert_eq!(
164			MeshGeometry::new(
165				"package://a-package/description/meshes_[[L]]/arm.dae",
166				(0.5, 0.5, 4.),
167				Some((0.9, 0.9, 0.9))
168			)
169			.volume(),
170			1.
171		);
172		assert_eq!(
173			MeshGeometry::new(
174				"package://a-package/description/meshes/somethingweird.dae",
175				(40.5, 90.5, 4.),
176				Some((1., -1., 1.))
177			)
178			.volume(),
179			14661.
180		);
181	}
182
183	#[test]
184	fn surface_area() {
185		assert_eq!(
186			MeshGeometry::new(
187				"package://my-package/description/meshes/mesh_[[L]].dae",
188				(1., 5., 1.),
189				None
190			)
191			.surface_area(),
192			22.
193		);
194		assert_eq!(
195			MeshGeometry::new(
196				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
197				(3., 3., 3.),
198				None
199			)
200			.surface_area(),
201			54.
202		);
203		assert_eq!(
204			MeshGeometry::new(
205				"package://a-package/description/meshes_[[L]]/arm.dae",
206				(0.5, 0.5, 4.),
207				Some((0.9, 0.9, 0.9))
208			)
209			.surface_area(),
210			8.5
211		);
212		assert_eq!(
213			MeshGeometry::new(
214				"package://a-package/description/meshes/somethingweird.dae",
215				(40.5, 90.5, 4.),
216				Some((1., -1., 1.))
217			)
218			.surface_area(),
219			8378.5
220		);
221	}
222
223	#[test]
224	fn boxed_clone() {
225		assert_eq!(
226			MeshGeometry::new(
227				"package://my-package/description/meshes/mesh_[[L]].dae",
228				(1., 5., 1.),
229				None
230			)
231			.boxed_clone(),
232			MeshGeometry::new(
233				"package://my-package/description/meshes/mesh_[[L]].dae",
234				(1., 5., 1.),
235				None
236			)
237			.into()
238		);
239		assert_eq!(
240			MeshGeometry::new(
241				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
242				(3., 3., 3.),
243				None
244			)
245			.boxed_clone(),
246			MeshGeometry::new(
247				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
248				(3., 3., 3.),
249				None
250			)
251			.into()
252		);
253		assert_eq!(
254			MeshGeometry::new(
255				"package://a-package/description/meshes_[[L]]/arm.dae",
256				(0.5, 0.5, 4.),
257				Some((0.9, 0.9, 0.9))
258			)
259			.boxed_clone(),
260			MeshGeometry::new(
261				"package://a-package/description/meshes_[[L]]/arm.dae",
262				(0.5, 0.5, 4.),
263				Some((0.9, 0.9, 0.9))
264			)
265			.into()
266		);
267		assert_eq!(
268			MeshGeometry::new(
269				"package://a-package/description/meshes/somethingweird.dae",
270				(40.5, 90.5, 4.),
271				Some((1., -1., 1.))
272			)
273			.boxed_clone(),
274			MeshGeometry::new(
275				"package://a-package/description/meshes/somethingweird.dae",
276				(40.5, 90.5, 4.),
277				Some((1., -1., 1.))
278			)
279			.into()
280		);
281	}
282
283	#[test]
284	fn bounding_box() {
285		assert_eq!(
286			MeshGeometry::new(
287				"package://my-package/description/meshes/mesh_[[L]].dae",
288				(1., 5., 1.),
289				None
290			)
291			.bounding_box(),
292			(1., 5., 1.)
293		);
294		assert_eq!(
295			MeshGeometry::new(
296				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
297				(3., 3., 3.),
298				None
299			)
300			.bounding_box(),
301			(3., 3., 3.)
302		);
303		assert_eq!(
304			MeshGeometry::new(
305				"package://a-package/description/meshes_[[L]]/arm.dae",
306				(0.5, 0.5, 4.),
307				Some((0.9, 0.9, 0.9))
308			)
309			.bounding_box(),
310			(0.5, 0.5, 4.)
311		);
312		assert_eq!(
313			MeshGeometry::new(
314				"package://a-package/description/meshes/somethingweird.dae",
315				(40.5, 90.5, 4.),
316				Some((1., -1., 1.))
317			)
318			.bounding_box(),
319			(40.5, 90.5, 4.)
320		);
321	}
322
323	#[test]
324	fn get_shape() {
325		assert_eq!(
326			MeshGeometry::new(
327				"package://my-package/description/meshes/mesh_[[L]].dae",
328				(1., 5., 1.),
329				None
330			)
331			.shape_container(),
332			GeometryShapeContainer::Mesh(MeshGeometry::new(
333				"package://my-package/description/meshes/mesh_[[L]].dae",
334				(1., 5., 1.),
335				None
336			))
337		);
338		assert_eq!(
339			MeshGeometry::new(
340				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
341				(3., 3., 3.),
342				None
343			)
344			.shape_container(),
345			GeometryShapeContainer::Mesh(MeshGeometry::new(
346				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
347				(3., 3., 3.),
348				None
349			))
350		);
351		assert_eq!(
352			MeshGeometry::new(
353				"package://a-package/description/meshes_[[L]]/arm.dae",
354				(0.5, 0.5, 4.),
355				Some((0.9, 0.9, 0.9))
356			)
357			.shape_container(),
358			GeometryShapeContainer::Mesh(MeshGeometry::new(
359				"package://a-package/description/meshes_[[L]]/arm.dae",
360				(0.5, 0.5, 4.),
361				Some((0.9, 0.9, 0.9))
362			))
363		);
364		assert_eq!(
365			MeshGeometry::new(
366				"package://a-package/description/meshes/somethingweird.dae",
367				(40.5, 90.5, 4.),
368				Some((1., -1., 1.))
369			)
370			.shape_container(),
371			GeometryShapeContainer::Mesh(MeshGeometry::new(
372				"package://a-package/description/meshes/somethingweird.dae",
373				(40.5, 90.5, 4.),
374				Some((1., -1., 1.))
375			))
376		);
377	}
378
379	#[cfg(feature = "urdf")]
380	#[test]
381	fn to_urdf() {
382		{
383			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
384			assert!(MeshGeometry::new(
385				"package://my-package/description/meshes/mesh_[[L]].dae",
386				(1., 5., 1.),
387				None
388			)
389			.to_urdf(&mut writer, &URDFConfig::default())
390			.is_ok());
391
392			writer.get_mut().rewind().unwrap();
393
394			assert_eq!(
395				std::io::read_to_string(writer.into_inner()).unwrap(),
396				String::from(
397					r#"<geometry><mesh filename="package://my-package/description/meshes/mesh_L.dae" scale="1 1 1"/></geometry>"#
398				)
399			);
400		}
401		{
402			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
403			assert!(MeshGeometry::new(
404				"package://my-other-package/description/meshes/symmetrical-mesh.dae",
405				(3., 3., 3.),
406				None
407			)
408			.to_urdf(&mut writer, &URDFConfig::default())
409			.is_ok());
410
411			writer.get_mut().rewind().unwrap();
412
413			assert_eq!(
414				std::io::read_to_string(writer.into_inner()).unwrap(),
415				String::from(
416					r#"<geometry><mesh filename="package://my-other-package/description/meshes/symmetrical-mesh.dae" scale="1 1 1"/></geometry>"#
417				)
418			);
419		}
420		{
421			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
422			assert!(MeshGeometry::new(
423				"package://a-package/description/meshes_[[L]]/arm.dae",
424				(0.5, 0.5, 4.),
425				Some((0.9, 0.9, 0.9))
426			)
427			.to_urdf(&mut writer, &URDFConfig::default())
428			.is_ok());
429
430			writer.get_mut().rewind().unwrap();
431
432			assert_eq!(
433				std::io::read_to_string(writer.into_inner()).unwrap(),
434				String::from(
435					r#"<geometry><mesh filename="package://a-package/description/meshes_L/arm.dae" scale="0.9 0.9 0.9"/></geometry>"#
436				)
437			);
438		}
439		{
440			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
441			assert!(MeshGeometry::new(
442				"package://a-package/description/meshes/somethingweird.dae",
443				(40.5, 90.5, 4.),
444				Some((1., -1., 1.))
445			)
446			.to_urdf(&mut writer, &URDFConfig::default())
447			.is_ok());
448
449			writer.get_mut().rewind().unwrap();
450
451			assert_eq!(
452				std::io::read_to_string(writer.into_inner()).unwrap(),
453				String::from(
454					r#"<geometry><mesh filename="package://a-package/description/meshes/somethingweird.dae" scale="1 -1 1"/></geometry>"#
455				)
456			);
457		}
458	}
459}