robot_description_builder/
transform.rs

1//! INTERNALDOC: This contains module [`Transform`], [`MirrorAxis`] and the core mirror logic.
2// User docs finished
3// TODO: MirrorDocs
4use itertools::Itertools;
5use nalgebra::{vector, Matrix3, Rotation3, Vector3};
6
7#[cfg(feature = "urdf")]
8use crate::to_rdf::to_urdf::ToURDF;
9#[cfg(feature = "xml")]
10use quick_xml::{events::attributes::Attribute, name::QName};
11
12#[derive(Debug, PartialEq, Clone, Copy, Default)]
13/// A `Transform` type to represent the transform from the parent coordinate system to a new coordinate system.
14///
15/// A transform starts from the origin of the parent element and first translates to the origin of the child element,
16///  after which a new coordinate system gets by rotating the parent coordinate system over the specified `roll`, `pitch` and `yaw` angles.
17///
18/// The `translation` is applied first and uses the axes of the parent coordinate system. The translation is specified in meters.
19///
20/// The `rotation` is applied next and rotates the parent axes with the specified `roll`, `pitch` and yaw` `angles in radians.
21///
22/// In URDF this element is often refered to as `<origin>`.
23pub struct Transform {
24	/// The translation of origin of the new coordinate system in meters.
25	pub translation: Option<(f32, f32, f32)>,
26	/// The rotation of the new coordinate system in radians.
27	pub rotation: Option<(f32, f32, f32)>,
28}
29
30impl Transform {
31	/// Creates a new `Transform`.
32	///
33	/// Creates a new `Transform` from a tuple of cartesian coordinates in meters as `f32` and a tuple of roll-pitch-yaw angles in radians as `f32`.
34	///
35	/// # Example
36	///
37	/// ```
38	/// use robot_description_builder::Transform;
39	/// use std::f32::consts::PI;
40	/// let transform = Transform::new((1., 1000., 0.), (0., PI, 0.));
41	///
42	/// assert_eq!(
43	///     transform,
44	///     Transform {
45	///        translation: Some((1., 1000., 0.)),
46	///        rotation: Some((0., PI, 0.)),
47	///     }
48	/// )
49	/// ```
50	pub fn new(xyz: (f32, f32, f32), rpy: (f32, f32, f32)) -> Self {
51		Self {
52			translation: Some(xyz),
53			rotation: Some(rpy),
54		}
55	}
56
57	/// Creates a new `Transform` from cartesian x, y and z coordinates.
58	///
59	/// Creates a new `Transform` from a tuple of cartesian coordinates in meters as `f32` and leaves the other values at the default.
60	///
61	/// # Example
62	///
63	/// ```
64	/// use robot_description_builder::Transform;
65	/// let transform = Transform::new_translation( -0.6, 10., 900.);
66	///
67	/// assert_eq!(
68	///     transform,
69	///     Transform {
70	///        translation: Some((-0.6, 10., 900.)),
71	///        rotation: None,
72	///     }
73	/// )
74	/// ```
75	pub fn new_translation(x: f32, y: f32, z: f32) -> Self {
76		Self {
77			translation: Some((x, y, z)),
78			..Default::default()
79		}
80	}
81
82	/// Creates a new `Transform` from roll-pitch-yaw angles.
83	///
84	/// Creates a new `Transform` from the roll-pitch-yaw angles in radians as `f32` and leaves the other values at the default.
85	///
86	/// # Example
87	///
88	/// ```
89	/// use robot_description_builder::Transform;
90	/// use std::f32::consts::PI;
91	/// let transform = Transform::new_rotation( 0., PI, 0.);
92	///
93	/// assert_eq!(
94	///     transform,
95	///     Transform {
96	///        translation: None,
97	///        rotation: Some((0., PI, 0.)),
98	///     }
99	/// )
100	/// ```
101	pub fn new_rotation(r: f32, p: f32, y: f32) -> Self {
102		Self {
103			rotation: Some((r, p, y)),
104			..Default::default()
105		}
106	}
107
108	/// A function to check if any of the fields are set.
109	///
110	/// It doesn't check if the some fields have the default value, since it can be format depended.
111	///
112	/// # Example
113	/// ```rust
114	/// # use robot_description_builder::Transform;
115	/// assert!(Transform {
116	///     translation: Some((1., 2., 3.)),
117	///     rotation: Some((4., 5., 6.))
118	/// }
119	/// .contains_some());
120	///
121	/// assert!(Transform {
122	///     translation: Some((1., 2., 3.)),
123	///     ..Default::default()
124	/// }
125	/// .contains_some());
126	///
127	/// assert!(Transform {
128	///     rotation: Some((4., 5., 6.)),
129	///     ..Default::default()
130	/// }
131	/// .contains_some());
132	///
133	/// assert!(!Transform::default().contains_some())
134	/// ```
135	pub fn contains_some(&self) -> bool {
136		self.translation.is_some() || self.rotation.is_some()
137	}
138}
139
140impl Mirror for Transform {
141	fn mirrored(&self, mirror_matrix: &Matrix3<f32>) -> Self {
142		Transform {
143			translation: self.translation.as_ref().map(|(x, y, z)| {
144				let old_translation = vector![*x, *y, *z];
145				(mirror_matrix * old_translation)
146					.component_mul(&Vector3::from_iterator(old_translation.iter().map(|val| {
147						if val.is_normal() {
148							1.
149						} else {
150							0.
151						}
152					}))) // TODO: Perfomance enhancements are probably possible.
153					.iter()
154					.copied()
155					.collect_tuple()
156					.unwrap() // Unwrapping here to ensure that we collect to a Tuple3 | TODO: Change to expect? or remove
157			}),
158			rotation: self.rotation,
159		}
160	}
161}
162
163impl MirrorUpdater for Transform {
164	fn update_mirror_matrix(&self, mirror_matrix: &Matrix3<f32>) -> Matrix3<f32> {
165		match self.rotation.as_ref() {
166			Some(rpy) => {
167				Rotation3::from_euler_angles(rpy.0, rpy.1, rpy.2)
168					* mirror_matrix * Rotation3::from_euler_angles(rpy.0, rpy.1, rpy.2).inverse()
169			}
170			None => *mirror_matrix,
171		}
172	}
173}
174
175#[cfg(feature = "urdf")]
176impl ToURDF for Transform {
177	fn to_urdf(
178		&self,
179		writer: &mut quick_xml::Writer<std::io::Cursor<Vec<u8>>>,
180		_urdf_config: &crate::to_rdf::to_urdf::URDFConfig,
181	) -> Result<(), quick_xml::Error> {
182		let mut element = writer.create_element("origin");
183		if let Some(translation) = self.translation {
184			element = element.with_attribute(Attribute {
185				key: QName(b"xyz"),
186				value: format!("{} {} {}", translation.0, translation.1, translation.2)
187					.as_bytes()
188					.into(),
189			})
190		}
191
192		if let Some(rotation) = self.rotation {
193			element = element.with_attribute(Attribute {
194				key: QName(b"rpy"),
195				value: format!("{} {} {}", rotation.0, rotation.1, rotation.2)
196					.as_bytes()
197					.into(),
198			});
199		}
200
201		element.write_empty()?;
202		Ok(())
203	}
204}
205
206impl From<Transform> for crate::joint::JointTransformMode {
207	fn from(value: Transform) -> Self {
208		Self::Direct(value)
209	}
210}
211
212/// A `MirrorAxis` enum to represent a plane to mirror about.
213#[derive(Debug, PartialEq, Eq, Clone, Copy)]
214pub enum MirrorAxis {
215	/// Mirror about to X = 0 plane.
216	X,
217	/// Mirror about to Y = 0 plane.
218	Y,
219	/// Mirror about to Z = 0 plane.
220	Z,
221}
222
223impl From<MirrorAxis> for Matrix3<f32> {
224	fn from(value: MirrorAxis) -> Self {
225		let diag = match value {
226			MirrorAxis::X => (-1., 1., 1.),
227			MirrorAxis::Y => (1., -1., 1.),
228			MirrorAxis::Z => (1., 1., -1.),
229		};
230
231		Matrix3::from_diagonal(&Vector3::new(diag.0, diag.1, diag.2))
232	}
233}
234
235/// A mirrorable type.
236///
237/// Types implementing `Mirror` are able to be [`mirrored`](Mirror::mirrored), given an `mirror_matrix`.
238pub(crate) trait Mirror {
239	/// Returns a mirrored clone of itself.
240	///
241	/// TODO: EXAMPLE
242	fn mirrored(&self, mirror_matrix: &Matrix3<f32>) -> Self;
243}
244
245/// A type which can change the `mirror_matrix` for its children.
246///
247/// TODO: IMPROVE/FINISH DOCS
248///
249/// Types implementing `MirrorUpdater` can be [`mirrored`](Mirror::mirrored). As a result of this mirror the `mirror_matrix` changes.
250pub(crate) trait MirrorUpdater: Sized + Mirror {
251	/// Get the updated `mirror_matrix` which should be used for all children.
252	fn update_mirror_matrix(&self, mirror_matrix: &Matrix3<f32>) -> Matrix3<f32>;
253
254	/// Return a mirrored clone of itself and the updated `mirror_matrix`.
255	fn mirrored_update_matrix(&self, mirror_matrix: &Matrix3<f32>) -> (Self, Matrix3<f32>) {
256		(
257			self.mirrored(mirror_matrix),
258			self.update_mirror_matrix(mirror_matrix),
259		)
260	}
261}
262
263#[cfg(test)]
264mod tests {
265	use super::Transform;
266	use std::f32::consts::{FRAC_PI_2, FRAC_PI_4};
267	use test_log::test;
268
269	mod mirror {
270		use super::{test, *};
271		use crate::transform::{MirrorAxis, MirrorUpdater};
272		use nalgebra::{matrix, vector, Matrix3};
273
274		fn test_mirror(
275			transform: Transform,
276			mirror_axis: MirrorAxis,
277			result: (Transform, Matrix3<f32>),
278		) {
279			assert_eq!(
280				transform.mirrored_update_matrix(&mirror_axis.into()),
281				result
282			)
283		}
284
285		fn test_all_mirrors(transform: Transform, results: [(Transform, Matrix3<f32>); 3]) {
286			results
287				.into_iter()
288				.enumerate()
289				.map(|(index, result)| {
290					(
291						match index {
292							0 => MirrorAxis::X,
293							1 => MirrorAxis::Y,
294							2 => MirrorAxis::Z,
295							_ => unreachable!(),
296						},
297						result,
298					)
299				})
300				.for_each(|(mirror_axis, result)| test_mirror(transform, mirror_axis, result))
301		}
302
303		fn test_all_mirrors_angle_var(
304			transform: Transform,
305			angle: f32,
306			results: [(Transform, [Matrix3<f32>; 3]); 3],
307		) {
308			for i in 0..2 {
309				let rotation = match i {
310					0 => (angle, 0., 0.),
311					1 => (0., angle, 0.),
312					2 => (0., 0., angle),
313					_ => unreachable!(),
314				};
315
316				test_all_mirrors(
317					Transform {
318						rotation: Some(rotation),
319						..transform.clone()
320					},
321					[
322						(
323							Transform {
324								rotation: Some(rotation),
325								..results[0].0
326							},
327							results[0].1[i],
328						),
329						(
330							Transform {
331								rotation: Some(rotation),
332								..results[1].0
333							},
334							results[1].1[i],
335						),
336						(
337							Transform {
338								rotation: Some(rotation),
339								..results[2].0
340							},
341							results[2].1[i],
342						),
343					],
344				)
345			}
346		}
347
348		#[test]
349		fn uniaxial_no_rotation() {
350			// X
351			test_all_mirrors(
352				Transform::new_translation(2., 0., 0.),
353				[
354					(
355						Transform {
356							translation: Some((-2., 0., 0.)),
357							rotation: None,
358						},
359						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
360					),
361					(
362						Transform {
363							translation: Some((2., 0., 0.)),
364							rotation: None,
365						},
366						Matrix3::from_diagonal(&vector![1., -1., 1.]),
367					),
368					(
369						Transform {
370							translation: Some((2., 0., 0.)),
371							rotation: None,
372						},
373						Matrix3::from_diagonal(&vector![1., 1., -1.]),
374					),
375				],
376			);
377
378			// Y
379			test_all_mirrors(
380				Transform::new_translation(0., 0.5, 0.),
381				[
382					(
383						Transform {
384							translation: Some((0., 0.5, 0.)),
385							rotation: None,
386						},
387						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
388					),
389					(
390						Transform {
391							translation: Some((0., -0.5, 0.)),
392							rotation: None,
393						},
394						Matrix3::from_diagonal(&vector![1., -1., 1.]),
395					),
396					(
397						Transform {
398							translation: Some((0., 0.5, 0.)),
399							rotation: None,
400						},
401						Matrix3::from_diagonal(&vector![1., 1., -1.]),
402					),
403				],
404			);
405
406			// Z
407			test_all_mirrors(
408				Transform::new_translation(0., 0., -900.),
409				[
410					(
411						Transform {
412							translation: Some((0., 0., -900.)),
413							rotation: None,
414						},
415						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
416					),
417					(
418						Transform {
419							translation: Some((0., 0., -900.)),
420							rotation: None,
421						},
422						Matrix3::from_diagonal(&vector![1., -1., 1.]),
423					),
424					(
425						Transform {
426							translation: Some((0., 0., 900.)),
427							rotation: None,
428						},
429						Matrix3::from_diagonal(&vector![1., 1., -1.]),
430					),
431				],
432			);
433		}
434
435		#[test]
436		#[ignore = "Is this necessary?"]
437		fn uniaxial_unirotation() {
438			test_all_mirrors_angle_var(
439				Transform::new_translation(2., 0., 0.),
440				FRAC_PI_2,
441				[
442					(
443						Transform::new_translation(-2., 0., 0.),
444						[
445							matrix![
446								-1., 0., 0.;
447								0., 1., 0.;
448								0., 0., 1.;
449							],
450							Matrix3::from_diagonal(&vector![-1., 1., 1.]),
451							Matrix3::from_diagonal(&vector![-1., 1., 1.]),
452						],
453					),
454					(
455						Transform::new_translation(2., 0., 0.),
456						[
457							matrix![
458								1., 0. ,0.;
459								0., 1., 0.;
460								0. ,0. ,-1.;
461							],
462							Matrix3::from_diagonal(&vector![1., -1., 1.]),
463							Matrix3::from_diagonal(&vector![1., -1., 1.]),
464						],
465					),
466					(
467						Transform::new_translation(2., 0., 0.),
468						[
469							Matrix3::from_diagonal(&vector![1., 1., -1.]),
470							Matrix3::from_diagonal(&vector![1., 1., -1.]),
471							Matrix3::from_diagonal(&vector![1., 1., -1.]),
472						],
473					),
474				],
475			);
476
477			test_all_mirrors(
478				Transform::new_translation(2., 0., 0.),
479				[
480					(
481						Transform {
482							translation: Some((-2., 0., 0.)),
483							rotation: None,
484						},
485						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
486					),
487					(
488						Transform {
489							translation: Some((2., 0., 0.)),
490							rotation: None,
491						},
492						Matrix3::from_diagonal(&vector![1., -1., 1.]),
493					),
494					(
495						Transform {
496							translation: Some((2., 0., 0.)),
497							rotation: None,
498						},
499						Matrix3::from_diagonal(&vector![1., 1., -1.]),
500					),
501				],
502			);
503
504			// Y
505			test_all_mirrors(
506				Transform::new((0., 0.5, 0.), (FRAC_PI_2, 0., FRAC_PI_4)),
507				[
508					(
509						Transform {
510							translation: Some((0., 0.5, 0.)),
511							rotation: None,
512						},
513						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
514					),
515					(
516						Transform {
517							translation: Some((0., -0.5, 0.)),
518							rotation: None,
519						},
520						Matrix3::from_diagonal(&vector![1., -1., 1.]),
521					),
522					(
523						Transform {
524							translation: Some((0., 0.5, 0.)),
525							rotation: None,
526						},
527						Matrix3::from_diagonal(&vector![1., 1., -1.]),
528					),
529				],
530			);
531
532			// Z
533			test_all_mirrors(
534				Transform::new_translation(0., 0., -900.),
535				[
536					(
537						Transform {
538							translation: Some((0., 0., -900.)),
539							rotation: None,
540						},
541						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
542					),
543					(
544						Transform {
545							translation: Some((0., 0., -900.)),
546							rotation: None,
547						},
548						Matrix3::from_diagonal(&vector![1., -1., 1.]),
549					),
550					(
551						Transform {
552							translation: Some((0., 0., 900.)),
553							rotation: None,
554						},
555						Matrix3::from_diagonal(&vector![1., 1., -1.]),
556					),
557				],
558			);
559		}
560
561		#[test]
562		fn multiaxial_no_rotation() {
563			test_all_mirrors(
564				Transform::new_translation(2., 3., 0.),
565				[
566					(
567						Transform {
568							translation: Some((-2., 3., 0.)),
569							rotation: None,
570						},
571						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
572					),
573					(
574						Transform {
575							translation: Some((2., -3., 0.)),
576							rotation: None,
577						},
578						Matrix3::from_diagonal(&vector![1., -1., 1.]),
579					),
580					(
581						Transform {
582							translation: Some((2., 3., 0.)),
583							rotation: None,
584						},
585						Matrix3::from_diagonal(&vector![1., 1., -1.]),
586					),
587				],
588			);
589
590			test_all_mirrors(
591				Transform::new_translation(0., 0.5, 7.),
592				[
593					(
594						Transform {
595							translation: Some((0., 0.5, 7.)),
596							rotation: None,
597						},
598						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
599					),
600					(
601						Transform {
602							translation: Some((0., -0.5, 7.)),
603							rotation: None,
604						},
605						Matrix3::from_diagonal(&vector![1., -1., 1.]),
606					),
607					(
608						Transform {
609							translation: Some((0., 0.5, -7.)),
610							rotation: None,
611						},
612						Matrix3::from_diagonal(&vector![1., 1., -1.]),
613					),
614				],
615			);
616
617			test_all_mirrors(
618				Transform::new_translation(120., 0., -900.),
619				[
620					(
621						Transform {
622							translation: Some((-120., 0., -900.)),
623							rotation: None,
624						},
625						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
626					),
627					(
628						Transform {
629							translation: Some((120., 0., -900.)),
630							rotation: None,
631						},
632						Matrix3::from_diagonal(&vector![1., -1., 1.]),
633					),
634					(
635						Transform {
636							translation: Some((120., 0., 900.)),
637							rotation: None,
638						},
639						Matrix3::from_diagonal(&vector![1., 1., -1.]),
640					),
641				],
642			);
643
644			test_all_mirrors(
645				Transform::new_translation(3., 4., 5.0005),
646				[
647					(
648						Transform {
649							translation: Some((-3., 4., 5.0005)),
650							rotation: None,
651						},
652						Matrix3::from_diagonal(&vector![-1., 1., 1.]),
653					),
654					(
655						Transform {
656							translation: Some((3., -4., 5.0005)),
657							rotation: None,
658						},
659						Matrix3::from_diagonal(&vector![1., -1., 1.]),
660					),
661					(
662						Transform {
663							translation: Some((3., 4., -5.0005)),
664							rotation: None,
665						},
666						Matrix3::from_diagonal(&vector![1., 1., -1.]),
667					),
668				],
669			)
670		}
671
672		#[test]
673		#[ignore = "Is this necessary?"]
674		fn multiaxial_rotation() {
675			todo!()
676		}
677	}
678
679	#[cfg(feature = "urdf")]
680	mod to_urdf {
681		use super::{test, Transform};
682		use crate::to_rdf::to_urdf::{ToURDF, URDFConfig};
683		use std::io::Seek;
684
685		fn test_to_urdf_transform(transform: Transform, result: String, urdf_config: &URDFConfig) {
686			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
687			assert!(transform.to_urdf(&mut writer, urdf_config).is_ok());
688
689			writer.get_mut().rewind().unwrap();
690			assert_eq!(
691				std::io::read_to_string(writer.into_inner()).unwrap(),
692				result
693			)
694		}
695
696		#[test]
697		fn translation_only() {
698			test_to_urdf_transform(
699				Transform {
700					translation: Some((1.2, 2.3, 3.4)),
701					..Default::default()
702				},
703				String::from(r#"<origin xyz="1.2 2.3 3.4"/>"#),
704				&URDFConfig::default(),
705			);
706		}
707
708		#[test]
709		fn rotation_only() {
710			test_to_urdf_transform(
711				Transform {
712					rotation: Some((1.2, 2.3, 3.4)),
713					..Default::default()
714				},
715				String::from(r#"<origin rpy="1.2 2.3 3.4"/>"#),
716				&URDFConfig::default(),
717			);
718		}
719
720		#[test]
721		fn translation_rotatation() {
722			test_to_urdf_transform(
723				Transform {
724					translation: Some((1.23, 2.34, 3.45)),
725					rotation: Some((4.56, 5.67, 6.78)),
726				},
727				String::from(r#"<origin xyz="1.23 2.34 3.45" rpy="4.56 5.67 6.78"/>"#),
728				&URDFConfig::default(),
729			);
730		}
731	}
732}