scirs2_spatial/transform/
rigid_transform.rs

1//! RigidTransform class for combined rotation and translation
2//!
3//! This module provides a `RigidTransform` class that represents a rigid transformation
4//! in 3D space, combining a rotation and translation.
5
6use crate::error::{SpatialError, SpatialResult};
7use crate::transform::Rotation;
8use scirs2_core::ndarray::{array, Array1, Array2, ArrayView1, ArrayView2};
9
10// Helper function to create an array from values
11#[allow(dead_code)]
12fn euler_array(x: f64, y: f64, z: f64) -> Array1<f64> {
13    array![x, y, z]
14}
15
16// Helper function to create a rotation from Euler angles
17#[allow(dead_code)]
18fn rotation_from_euler(x: f64, y: f64, z: f64, convention: &str) -> SpatialResult<Rotation> {
19    let angles = euler_array(x, y, z);
20    let angles_view = angles.view();
21    Rotation::from_euler(&angles_view, convention)
22}
23
24/// RigidTransform represents a rigid transformation in 3D space.
25///
26/// A rigid transformation is a combination of a rotation and a translation.
27/// It preserves the distance between any two points and the orientation of objects.
28///
29/// # Examples
30///
31/// ```
32/// use scirs2_spatial::transform::{Rotation, RigidTransform};
33/// use scirs2_core::ndarray::array;
34/// use std::f64::consts::PI;
35///
36/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
37/// // Create a rotation around Z and a translation
38/// let rotation = Rotation::from_euler(&array![0.0, 0.0, PI/2.0].view(), "xyz")?;
39/// let translation = array![1.0, 2.0, 3.0];
40///
41/// // Create a rigid transform from rotation and translation
42/// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view())?;
43///
44/// // Apply the transform to a point
45/// let point = array![0.0, 0.0, 0.0];
46/// let transformed = transform.apply(&point.view());
47/// // Should be [1.0, 2.0, 3.0] (just the translation for the origin)
48///
49/// // Another point
50/// let point2 = array![1.0, 0.0, 0.0];
51/// let transformed2 = transform.apply(&point2.view());
52/// // Should be [1.0, 3.0, 3.0] (rotated then translated)
53/// # Ok(())
54/// # }
55/// ```
56#[derive(Clone, Debug)]
57pub struct RigidTransform {
58    /// The rotation component
59    rotation: Rotation,
60    /// The translation component
61    translation: Array1<f64>,
62}
63
64impl RigidTransform {
65    /// Create a new rigid transform from a rotation and translation
66    ///
67    /// # Arguments
68    ///
69    /// * `rotation` - The rotation component
70    /// * `translation` - The translation vector (3D)
71    ///
72    /// # Returns
73    ///
74    /// A `SpatialResult` containing the rigid transform if valid, or an error if invalid
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
80    /// use scirs2_core::ndarray::array;
81    ///
82    /// let rotation = Rotation::identity();
83    /// let translation = array![1.0, 2.0, 3.0];
84    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
85    /// ```
86    pub fn from_rotation_and_translation(
87        rotation: Rotation,
88        translation: &ArrayView1<f64>,
89    ) -> SpatialResult<Self> {
90        if translation.len() != 3 {
91            return Err(SpatialError::DimensionError(format!(
92                "Translation must have 3 elements, got {}",
93                translation.len()
94            )));
95        }
96
97        Ok(RigidTransform {
98            rotation,
99            translation: translation.to_owned(),
100        })
101    }
102
103    /// Create a rigid transform from a 4x4 transformation matrix
104    ///
105    /// # Arguments
106    ///
107    /// * `matrix` - A 4x4 transformation matrix in homogeneous coordinates
108    ///
109    /// # Returns
110    ///
111    /// A `SpatialResult` containing the rigid transform if valid, or an error if invalid
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use scirs2_spatial::transform::RigidTransform;
117    /// use scirs2_core::ndarray::array;
118    ///
119    /// // Create a transformation matrix for translation by [1, 2, 3]
120    /// let matrix = array![
121    ///     [1.0, 0.0, 0.0, 1.0],
122    ///     [0.0, 1.0, 0.0, 2.0],
123    ///     [0.0, 0.0, 1.0, 3.0],
124    ///     [0.0, 0.0, 0.0, 1.0]
125    /// ];
126    /// let transform = RigidTransform::from_matrix(&matrix.view()).unwrap();
127    /// ```
128    pub fn from_matrix(matrix: &ArrayView2<'_, f64>) -> SpatialResult<Self> {
129        if matrix.shape() != [4, 4] {
130            return Err(SpatialError::DimensionError(format!(
131                "Matrix must be 4x4, got {:?}",
132                matrix.shape()
133            )));
134        }
135
136        // Check the last row is [0, 0, 0, 1]
137        for i in 0..3 {
138            if (matrix[[3, i]] - 0.0).abs() > 1e-10 {
139                return Err(SpatialError::ValueError(
140                    "Last row of matrix must be [0, 0, 0, 1]".into(),
141                ));
142            }
143        }
144        if (matrix[[3, 3]] - 1.0).abs() > 1e-10 {
145            return Err(SpatialError::ValueError(
146                "Last row of matrix must be [0, 0, 0, 1]".into(),
147            ));
148        }
149
150        // Extract the rotation part (3x3 upper-left submatrix)
151        let mut rotation_matrix = Array2::<f64>::zeros((3, 3));
152        for i in 0..3 {
153            for j in 0..3 {
154                rotation_matrix[[i, j]] = matrix[[i, j]];
155            }
156        }
157
158        // Extract the translation part (right column, first 3 elements)
159        let mut translation = Array1::<f64>::zeros(3);
160        for i in 0..3 {
161            translation[i] = matrix[[i, 3]];
162        }
163
164        // Create rotation from the extracted matrix
165        let rotation = Rotation::from_matrix(&rotation_matrix.view())?;
166
167        Ok(RigidTransform {
168            rotation,
169            translation,
170        })
171    }
172
173    /// Convert the rigid transform to a 4x4 matrix in homogeneous coordinates
174    ///
175    /// # Returns
176    ///
177    /// A 4x4 transformation matrix
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
183    /// use scirs2_core::ndarray::array;
184    ///
185    /// let rotation = Rotation::identity();
186    /// let translation = array![1.0, 2.0, 3.0];
187    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
188    /// let matrix = transform.as_matrix();
189    /// // Should be a 4x4 identity matrix with the last column containing the translation
190    /// ```
191    pub fn as_matrix(&self) -> Array2<f64> {
192        let mut matrix = Array2::<f64>::zeros((4, 4));
193
194        // Set the rotation part
195        let rotation_matrix = self.rotation.as_matrix();
196        for i in 0..3 {
197            for j in 0..3 {
198                matrix[[i, j]] = rotation_matrix[[i, j]];
199            }
200        }
201
202        // Set the translation part
203        for i in 0..3 {
204            matrix[[i, 3]] = self.translation[i];
205        }
206
207        // Set the homogeneous coordinate part
208        matrix[[3, 3]] = 1.0;
209
210        matrix
211    }
212
213    /// Get the rotation component of the rigid transform
214    ///
215    /// # Returns
216    ///
217    /// The rotation component
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
223    /// use scirs2_core::ndarray::array;
224    ///
225    /// let rotation = Rotation::identity();
226    /// let translation = array![1.0, 2.0, 3.0];
227    /// let transform = RigidTransform::from_rotation_and_translation(rotation.clone(), &translation.view()).unwrap();
228    /// let retrieved_rotation = transform.rotation();
229    /// ```
230    pub fn rotation(&self) -> &Rotation {
231        &self.rotation
232    }
233
234    /// Get the translation component of the rigid transform
235    ///
236    /// # Returns
237    ///
238    /// The translation vector
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
244    /// use scirs2_core::ndarray::array;
245    ///
246    /// let rotation = Rotation::identity();
247    /// let translation = array![1.0, 2.0, 3.0];
248    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
249    /// let retrieved_translation = transform.translation();
250    /// ```
251    pub fn translation(&self) -> &Array1<f64> {
252        &self.translation
253    }
254
255    /// Apply the rigid transform to a point or vector
256    ///
257    /// # Arguments
258    ///
259    /// * `point` - A 3D point or vector to transform
260    ///
261    /// # Returns
262    ///
263    /// The transformed point or vector
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
269    /// use scirs2_core::ndarray::array;
270    /// use std::f64::consts::PI;
271    ///
272    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
273    /// let rotation = Rotation::from_euler(&array![0.0, 0.0, PI/2.0].view(), "xyz")?;
274    /// let translation = array![1.0, 2.0, 3.0];
275    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view())?;
276    /// let point = array![1.0, 0.0, 0.0];
277    /// let transformed = transform.apply(&point.view())?;
278    /// // Should be [1.0, 3.0, 3.0] (rotated then translated)
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub fn apply(&self, point: &ArrayView1<f64>) -> SpatialResult<Array1<f64>> {
283        if point.len() != 3 {
284            return Err(SpatialError::DimensionError(
285                "Point must have 3 elements".to_string(),
286            ));
287        }
288
289        // Apply rotation then translation
290        let rotated = self.rotation.apply(point)?;
291        Ok(rotated + &self.translation)
292    }
293
294    /// Apply the rigid transform to multiple points
295    ///
296    /// # Arguments
297    ///
298    /// * `points` - A 2D array of points (each row is a 3D point)
299    ///
300    /// # Returns
301    ///
302    /// A 2D array of transformed points
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
308    /// use scirs2_core::ndarray::array;
309    ///
310    /// let rotation = Rotation::identity();
311    /// let translation = array![1.0, 2.0, 3.0];
312    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
313    /// let points = array![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
314    /// let transformed = transform.apply_multiple(&points.view());
315    /// ```
316    pub fn apply_multiple(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<Array2<f64>> {
317        if points.ncols() != 3 {
318            return Err(SpatialError::DimensionError(
319                "Each point must have 3 elements".to_string(),
320            ));
321        }
322
323        let npoints = points.nrows();
324        let mut result = Array2::<f64>::zeros((npoints, 3));
325
326        for i in 0..npoints {
327            let point = points.row(i);
328            let transformed = self.apply(&point)?;
329            for j in 0..3 {
330                result[[i, j]] = transformed[j];
331            }
332        }
333
334        Ok(result)
335    }
336
337    /// Get the inverse of the rigid transform
338    ///
339    /// # Returns
340    ///
341    /// A new RigidTransform that is the inverse of this one
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
347    /// use scirs2_core::ndarray::array;
348    ///
349    /// let rotation = Rotation::identity();
350    /// let translation = array![1.0, 2.0, 3.0];
351    /// let transform = RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
352    /// let inverse = transform.inv();
353    /// ```
354    pub fn inv(&self) -> SpatialResult<RigidTransform> {
355        // Inverse of a rigid transform: R^-1, -R^-1 * t
356        let inv_rotation = self.rotation.inv();
357        let inv_translation = -inv_rotation.apply(&self.translation.view())?;
358
359        Ok(RigidTransform {
360            rotation: inv_rotation,
361            translation: inv_translation,
362        })
363    }
364
365    /// Compose this rigid transform with another (apply the other transform after this one)
366    ///
367    /// # Arguments
368    ///
369    /// * `other` - The other rigid transform to combine with
370    ///
371    /// # Returns
372    ///
373    /// A new RigidTransform that represents the composition
374    ///
375    /// # Examples
376    ///
377    /// ```
378    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
379    /// use scirs2_core::ndarray::array;
380    ///
381    /// let t1 = RigidTransform::from_rotation_and_translation(
382    ///     Rotation::identity(),
383    ///     &array![1.0, 0.0, 0.0].view()
384    /// ).unwrap();
385    /// let t2 = RigidTransform::from_rotation_and_translation(
386    ///     Rotation::identity(),
387    ///     &array![0.0, 1.0, 0.0].view()
388    /// ).unwrap();
389    /// let combined = t1.compose(&t2);
390    /// // Should have a translation of [1.0, 1.0, 0.0]
391    /// ```
392    pub fn compose(&self, other: &RigidTransform) -> SpatialResult<RigidTransform> {
393        // Compose rotations
394        let rotation = self.rotation.compose(&other.rotation);
395
396        // Compose translations: self.translation + self.rotation * other.translation
397        let rotated_trans = self.rotation.apply(&other.translation.view())?;
398        let translation = &self.translation + &rotated_trans;
399
400        Ok(RigidTransform {
401            rotation,
402            translation,
403        })
404    }
405
406    /// Create an identity rigid transform (no rotation, no translation)
407    ///
408    /// # Returns
409    ///
410    /// A new RigidTransform that represents identity
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// use scirs2_spatial::transform::RigidTransform;
416    /// use scirs2_core::ndarray::array;
417    ///
418    /// let identity = RigidTransform::identity();
419    /// let point = array![1.0, 2.0, 3.0];
420    /// let transformed = identity.apply(&point.view());
421    /// // Should still be [1.0, 2.0, 3.0]
422    /// ```
423    pub fn identity() -> RigidTransform {
424        RigidTransform {
425            rotation: Rotation::from_quat(&array![1.0, 0.0, 0.0, 0.0].view()).unwrap(),
426            translation: Array1::<f64>::zeros(3),
427        }
428    }
429
430    /// Create a rigid transform that only has a translation component
431    ///
432    /// # Arguments
433    ///
434    /// * `translation` - The translation vector
435    ///
436    /// # Returns
437    ///
438    /// A new RigidTransform with no rotation
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use scirs2_spatial::transform::RigidTransform;
444    /// use scirs2_core::ndarray::array;
445    ///
446    /// let transform = RigidTransform::from_translation(&array![1.0, 2.0, 3.0].view()).unwrap();
447    /// let point = array![0.0, 0.0, 0.0];
448    /// let transformed = transform.apply(&point.view());
449    /// // Should be [1.0, 2.0, 3.0]
450    /// ```
451    pub fn from_translation(translation: &ArrayView1<f64>) -> SpatialResult<RigidTransform> {
452        if translation.len() != 3 {
453            return Err(SpatialError::DimensionError(format!(
454                "Translation must have 3 elements, got {}",
455                translation.len()
456            )));
457        }
458
459        Ok(RigidTransform {
460            rotation: Rotation::from_quat(&array![1.0, 0.0, 0.0, 0.0].view()).unwrap(),
461            translation: translation.to_owned(),
462        })
463    }
464
465    /// Create a rigid transform that only has a rotation component
466    ///
467    /// # Arguments
468    ///
469    /// * `rotation` - The rotation component
470    ///
471    /// # Returns
472    ///
473    /// A new RigidTransform with no translation
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use scirs2_spatial::transform::{Rotation, RigidTransform};
479    /// use scirs2_core::ndarray::array;
480    /// use std::f64::consts::PI;
481    ///
482    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
483    /// let rotation = Rotation::from_euler(&array![0.0, 0.0, PI/2.0].view(), "xyz")?;
484    /// let transform = RigidTransform::from_rotation(rotation);
485    /// let point = array![1.0, 0.0, 0.0];
486    /// let transformed = transform.apply(&point.view())?;
487    /// // Should be [0.0, 1.0, 0.0]
488    /// # Ok(())
489    /// # }
490    /// ```
491    pub fn from_rotation(rotation: Rotation) -> RigidTransform {
492        RigidTransform {
493            rotation,
494            translation: Array1::<f64>::zeros(3),
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use approx::assert_relative_eq;
503    use std::f64::consts::PI;
504
505    #[test]
506    fn test_rigid_transform_identity() {
507        let identity = RigidTransform::identity();
508        let point = array![1.0, 2.0, 3.0];
509        let transformed = identity.apply(&point.view()).unwrap();
510
511        assert_relative_eq!(transformed[0], point[0], epsilon = 1e-10);
512        assert_relative_eq!(transformed[1], point[1], epsilon = 1e-10);
513        assert_relative_eq!(transformed[2], point[2], epsilon = 1e-10);
514    }
515
516    #[test]
517    fn test_rigid_transform_translation_only() {
518        let translation = array![1.0, 2.0, 3.0];
519        let transform = RigidTransform::from_translation(&translation.view()).unwrap();
520
521        let point = array![0.0, 0.0, 0.0];
522        let transformed = transform.apply(&point.view()).unwrap();
523
524        assert_relative_eq!(transformed[0], translation[0], epsilon = 1e-10);
525        assert_relative_eq!(transformed[1], translation[1], epsilon = 1e-10);
526        assert_relative_eq!(transformed[2], translation[2], epsilon = 1e-10);
527    }
528
529    #[test]
530    fn test_rigid_transform_rotation_only() {
531        // 90 degrees rotation around Z axis
532        let rotation = rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap();
533        let transform = RigidTransform::from_rotation(rotation);
534
535        let point = array![1.0, 0.0, 0.0];
536        let transformed = transform.apply(&point.view()).unwrap();
537
538        // 90 degrees rotation around Z axis of [1, 0, 0] should give [0, 1, 0]
539        assert_relative_eq!(transformed[0], 0.0, epsilon = 1e-10);
540        assert_relative_eq!(transformed[1], 1.0, epsilon = 1e-10);
541        assert_relative_eq!(transformed[2], 0.0, epsilon = 1e-10);
542    }
543
544    #[test]
545    fn test_rigid_transform_rotation_and_translation() {
546        // 90 degrees rotation around Z axis and translation by [1, 2, 3]
547        let rotation = rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap();
548        let translation = array![1.0, 2.0, 3.0];
549        let transform =
550            RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
551
552        let point = array![1.0, 0.0, 0.0];
553        let transformed = transform.apply(&point.view()).unwrap();
554
555        // 90 degrees rotation around Z axis of [1, 0, 0] should give [0, 1, 0]
556        // Then translate by [1, 2, 3] to get [1, 3, 3]
557        assert_relative_eq!(transformed[0], 1.0, epsilon = 1e-10);
558        assert_relative_eq!(transformed[1], 3.0, epsilon = 1e-10);
559        assert_relative_eq!(transformed[2], 3.0, epsilon = 1e-10);
560    }
561
562    #[test]
563    fn test_rigid_transform_from_matrix() {
564        let matrix = array![
565            [0.0, -1.0, 0.0, 1.0],
566            [1.0, 0.0, 0.0, 2.0],
567            [0.0, 0.0, 1.0, 3.0],
568            [0.0, 0.0, 0.0, 1.0]
569        ];
570        let transform = RigidTransform::from_matrix(&matrix.view()).unwrap();
571
572        let point = array![1.0, 0.0, 0.0];
573        let transformed = transform.apply(&point.view()).unwrap();
574
575        // This matrix represents a 90-degree rotation around Z and translation by [1, 2, 3]
576        // So [1, 0, 0] -> [0, 1, 0] -> [1, 3, 3]
577        assert_relative_eq!(transformed[0], 1.0, epsilon = 1e-10);
578        assert_relative_eq!(transformed[1], 3.0, epsilon = 1e-10);
579        assert_relative_eq!(transformed[2], 3.0, epsilon = 1e-10);
580    }
581
582    #[test]
583    fn test_rigid_transform_as_matrix() {
584        // Create a transform and verify its matrix representation
585        let rotation = rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap();
586        let translation = array![1.0, 2.0, 3.0];
587        let transform =
588            RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
589
590        let matrix = transform.as_matrix();
591
592        // Check the rotation part (90-degree rotation around Z)
593        assert_relative_eq!(matrix[[0, 0]], 0.0, epsilon = 1e-10);
594        assert_relative_eq!(matrix[[0, 1]], -1.0, epsilon = 1e-10);
595        assert_relative_eq!(matrix[[0, 2]], 0.0, epsilon = 1e-10);
596        assert_relative_eq!(matrix[[1, 0]], 1.0, epsilon = 1e-10);
597        assert_relative_eq!(matrix[[1, 1]], 0.0, epsilon = 1e-10);
598        assert_relative_eq!(matrix[[1, 2]], 0.0, epsilon = 1e-10);
599        assert_relative_eq!(matrix[[2, 0]], 0.0, epsilon = 1e-10);
600        assert_relative_eq!(matrix[[2, 1]], 0.0, epsilon = 1e-10);
601        assert_relative_eq!(matrix[[2, 2]], 1.0, epsilon = 1e-10);
602
603        // Check the translation part
604        assert_relative_eq!(matrix[[0, 3]], 1.0, epsilon = 1e-10);
605        assert_relative_eq!(matrix[[1, 3]], 2.0, epsilon = 1e-10);
606        assert_relative_eq!(matrix[[2, 3]], 3.0, epsilon = 1e-10);
607
608        // Check the homogeneous row
609        assert_relative_eq!(matrix[[3, 0]], 0.0, epsilon = 1e-10);
610        assert_relative_eq!(matrix[[3, 1]], 0.0, epsilon = 1e-10);
611        assert_relative_eq!(matrix[[3, 2]], 0.0, epsilon = 1e-10);
612        assert_relative_eq!(matrix[[3, 3]], 1.0, epsilon = 1e-10);
613    }
614
615    #[test]
616    fn test_rigid_transform_inverse() {
617        // Create a transform and verify its inverse
618        let rotation = rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap();
619        let translation = array![1.0, 2.0, 3.0];
620        let transform =
621            RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
622
623        let inverse = transform.inv().unwrap();
624
625        // Apply transform and then its inverse to a point
626        let point = array![1.0, 2.0, 3.0];
627        let transformed = transform.apply(&point.view()).unwrap();
628        let back = inverse.apply(&transformed.view()).unwrap();
629
630        // Should get back to the original point
631        assert_relative_eq!(back[0], point[0], epsilon = 1e-10);
632        assert_relative_eq!(back[1], point[1], epsilon = 1e-10);
633        assert_relative_eq!(back[2], point[2], epsilon = 1e-10);
634    }
635
636    #[test]
637    #[ignore]
638    fn test_rigid_transform_composition() {
639        // Create two transforms and compose them
640        let t1 = RigidTransform::from_rotation_and_translation(
641            rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap(),
642            &array![1.0, 0.0, 0.0].view(),
643        )
644        .unwrap();
645
646        let t2 = RigidTransform::from_rotation_and_translation(
647            rotation_from_euler(PI / 2.0, 0.0, 0.0, "xyz").unwrap(),
648            &array![0.0, 1.0, 0.0].view(),
649        )
650        .unwrap();
651
652        let composed = t1.compose(&t2).unwrap();
653
654        // Apply the composed transform to a point
655        let point = array![1.0, 0.0, 0.0];
656        let transformed = composed.apply(&point.view()).unwrap();
657
658        // Apply the transforms individually
659        let intermediate = t1.apply(&point.view()).unwrap();
660        let transformed2 = t2.apply(&intermediate.view()).unwrap();
661
662        // The composed transform and individual transforms should produce the same result
663        assert_relative_eq!(transformed[0], transformed2[0], epsilon = 1e-10);
664        assert_relative_eq!(transformed[1], transformed2[1], epsilon = 1e-10);
665        assert_relative_eq!(transformed[2], transformed2[2], epsilon = 1e-10);
666    }
667
668    #[test]
669    fn test_rigid_transform_multiple_points() {
670        let rotation = rotation_from_euler(0.0, 0.0, PI / 2.0, "xyz").unwrap();
671        let translation = array![1.0, 2.0, 3.0];
672        let transform =
673            RigidTransform::from_rotation_and_translation(rotation, &translation.view()).unwrap();
674
675        let points = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
676
677        let transformed = transform.apply_multiple(&points.view()).unwrap();
678
679        // Check that we get the correct transformed points
680        assert_eq!(transformed.shape(), points.shape());
681
682        // [1, 0, 0] -> [0, 1, 0] -> [1, 3, 3]
683        assert_relative_eq!(transformed[[0, 0]], 1.0, epsilon = 1e-10);
684        assert_relative_eq!(transformed[[0, 1]], 3.0, epsilon = 1e-10);
685        assert_relative_eq!(transformed[[0, 2]], 3.0, epsilon = 1e-10);
686
687        // [0, 1, 0] -> [-1, 0, 0] -> [0, 2, 3]
688        assert_relative_eq!(transformed[[1, 0]], 0.0, epsilon = 1e-10);
689        assert_relative_eq!(transformed[[1, 1]], 2.0, epsilon = 1e-10);
690        assert_relative_eq!(transformed[[1, 2]], 3.0, epsilon = 1e-10);
691
692        // [0, 0, 1] -> [0, 0, 1] -> [1, 2, 4]
693        assert_relative_eq!(transformed[[2, 0]], 1.0, epsilon = 1e-10);
694        assert_relative_eq!(transformed[[2, 1]], 2.0, epsilon = 1e-10);
695        assert_relative_eq!(transformed[[2, 2]], 4.0, epsilon = 1e-10);
696    }
697}