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}