1use core::ops::Mul;
30
31use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
32
33use crate::{
34 Axis, Direction, Direction2, Frame, Frame2, Matrix2, Matrix3, Point, Point2, Quaternion,
35 Vector, Vector2,
36};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum TransformKind {
41 #[default]
43 Identity,
44 Translation,
46 Rotation,
48 PointMirror,
50 PlaneMirror,
52 Scale,
54 Compound,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct Transform {
64 linear: Matrix3,
65 scale: f64,
66 translation: Vector,
67 kind: TransformKind,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct Transform2 {
73 linear: Matrix2,
74 scale: f64,
75 translation: Vector2,
76 kind: TransformKind,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct GeneralTransform {
86 pub linear: Matrix3,
88 pub translation: Vector,
90}
91
92const CLASSIFY_EPS: f64 = 1e-12;
97
98impl Default for Transform {
99 fn default() -> Self {
100 Self::IDENTITY
101 }
102}
103
104impl Transform {
105 pub const IDENTITY: Self = Self {
107 linear: Matrix3::IDENTITY,
108 scale: 1.0,
109 translation: Vector::ZERO,
110 kind: TransformKind::Identity,
111 };
112
113 fn build(linear: Matrix3, scale: f64, translation: Vector) -> Self {
118 let kind = Self::classify(&linear, scale, translation);
119 Self {
120 linear,
121 scale,
122 translation,
123 kind,
124 }
125 }
126
127 pub fn from_parts(
147 linear: Matrix3,
148 scale: f64,
149 translation: Vector,
150 eps: f64,
151 ) -> OgeomResult<Self> {
152 if !scale.is_finite() || scale == 0.0 {
153 ogeom_bail!(
154 Construction,
155 "a placement's scale must be finite and non-zero; got {scale}"
156 );
157 }
158 if !linear.is_orthonormal(eps) {
159 ogeom_bail!(
160 Construction,
161 "a placement's linear part must be orthonormal; this one shears or scales unevenly"
162 );
163 }
164 Ok(Self::build(linear, scale, translation))
165 }
166
167 fn classify(linear: &Matrix3, scale: f64, translation: Vector) -> TransformKind {
169 let is_identity_linear = linear.is_equal(&Matrix3::IDENTITY, CLASSIFY_EPS);
170 let unit_scale = (scale - 1.0).abs() <= CLASSIFY_EPS;
171 let negative_unit_scale = (scale + 1.0).abs() <= CLASSIFY_EPS;
172 let no_translation = translation.square_magnitude() == 0.0;
173
174 if is_identity_linear && unit_scale {
175 return if no_translation {
176 TransformKind::Identity
177 } else {
178 TransformKind::Translation
179 };
180 }
181 if is_identity_linear && negative_unit_scale {
182 return TransformKind::PointMirror;
183 }
184 if is_identity_linear {
185 return TransformKind::Scale;
186 }
187 if unit_scale && linear.is_orthonormal(CLASSIFY_EPS) {
188 return if linear.determinant() > 0.0 {
191 TransformKind::Rotation
192 } else {
193 TransformKind::PlaneMirror
194 };
195 }
196 TransformKind::Compound
197 }
198
199 #[must_use]
201 pub fn translation(v: Vector) -> Self {
202 Self::build(Matrix3::IDENTITY, 1.0, v)
203 }
204
205 #[must_use]
207 pub fn rotation(axis: Axis, angle: f64) -> Self {
208 let linear = Matrix3::rotation(axis.direction, angle);
209 let p = axis.location.to_vector();
213 Self::build(linear, 1.0, p - linear * p)
214 }
215
216 #[must_use]
218 pub fn from_quaternion(q: Quaternion) -> Self {
219 Self::build(q.to_matrix(), 1.0, Vector::ZERO)
220 }
221
222 pub fn scaling(centre: Point, factor: f64, tol: Tolerances) -> OgeomResult<Self> {
231 if !factor.is_finite() || factor.abs() <= tol.confusion() {
232 ogeom_bail!(Construction, "scale factor {factor} is degenerate");
233 }
234 let c = centre.to_vector();
235 Ok(Self::build(Matrix3::IDENTITY, factor, c - c * factor))
236 }
237
238 #[must_use]
240 pub fn point_mirror(centre: Point) -> Self {
241 let c = centre.to_vector();
242 Self::build(Matrix3::IDENTITY, -1.0, c + c)
243 }
244
245 #[must_use]
247 pub fn plane_mirror(origin: Point, normal: Direction) -> Self {
248 let linear = Matrix3::reflection(normal);
249 let p = origin.to_vector();
250 Self::build(linear, 1.0, p - linear * p)
251 }
252
253 #[must_use]
255 pub fn axis_mirror(axis: Axis) -> Self {
256 Self::rotation(axis, core::f64::consts::PI)
257 }
258
259 #[must_use]
261 pub fn to_frame(frame: &Frame) -> Self {
262 let linear = frame.to_matrix().transposed();
263 Self::build(linear, 1.0, -(linear * frame.origin().to_vector()))
264 }
265
266 #[must_use]
268 pub fn from_frame(frame: &Frame) -> Self {
269 Self::build(frame.to_matrix(), 1.0, frame.origin().to_vector())
270 }
271
272 #[must_use]
274 pub fn between_frames(from: &Frame, to: &Frame) -> Self {
275 Self::to_frame(to) * Self::from_frame(from)
276 }
277
278 #[must_use]
280 pub const fn kind(&self) -> TransformKind {
281 self.kind
282 }
283
284 #[must_use]
286 pub const fn linear(&self) -> Matrix3 {
287 self.linear
288 }
289
290 #[must_use]
292 pub const fn scale_factor(&self) -> f64 {
293 self.scale
294 }
295
296 #[must_use]
298 pub const fn translation_vector(&self) -> Vector {
299 self.translation
300 }
301
302 #[must_use]
308 pub fn preserves_handedness(&self) -> bool {
309 self.linear.determinant() * self.scale.signum() > 0.0
310 }
311
312 #[must_use]
314 pub fn apply(&self, p: Point) -> Point {
315 match self.kind {
316 TransformKind::Identity => p,
317 TransformKind::Translation => p + self.translation,
318 TransformKind::PointMirror | TransformKind::Scale => {
319 Point::from_vector(p.to_vector() * self.scale + self.translation)
320 }
321 TransformKind::Rotation | TransformKind::PlaneMirror => {
322 Point::from_vector(self.linear * p.to_vector() + self.translation)
323 }
324 TransformKind::Compound => {
325 Point::from_vector(self.linear * (p.to_vector() * self.scale) + self.translation)
326 }
327 }
328 }
329
330 #[must_use]
332 pub fn apply_vector(&self, v: Vector) -> Vector {
333 match self.kind {
334 TransformKind::Identity | TransformKind::Translation => v,
335 TransformKind::PointMirror | TransformKind::Scale => v * self.scale,
336 TransformKind::Rotation | TransformKind::PlaneMirror => self.linear * v,
337 TransformKind::Compound => self.linear * (v * self.scale),
338 }
339 }
340
341 pub fn apply_direction(&self, d: Direction, tol: Tolerances) -> OgeomResult<Direction> {
352 match self.kind {
353 TransformKind::Identity | TransformKind::Translation => Ok(d),
354 TransformKind::Scale if self.scale > 0.0 => Ok(d),
356 TransformKind::PointMirror | TransformKind::Scale => Ok(d.reversed()),
357 _ => Direction::new(self.apply_vector(d.vector()), tol),
358 }
359 }
360
361 pub fn apply_frame(&self, f: &Frame, tol: Tolerances) -> OgeomResult<Frame> {
368 Frame::from_axes(
369 self.apply(f.origin()),
370 self.apply_direction(f.x(), tol)?,
371 self.apply_direction(f.y(), tol)?,
372 self.apply_direction(f.z(), tol)?,
373 tol,
374 )
375 }
376
377 pub fn inverse(&self) -> OgeomResult<Self> {
384 if self.kind == TransformKind::Identity {
385 return Ok(Self::IDENTITY);
386 }
387 if self.kind == TransformKind::Translation {
388 return Ok(Self::translation(-self.translation));
389 }
390 if self.scale == 0.0 {
391 ogeom_bail!(Numeric, "transform has a zero scale and no inverse");
392 }
393 let inv_linear = self.linear.transposed();
397 let inv_scale = 1.0 / self.scale;
398 Ok(Self::build(
399 inv_linear,
400 inv_scale,
401 -(inv_linear * self.translation) * inv_scale,
402 ))
403 }
404
405 #[must_use]
407 pub fn is_equal(&self, other: &Self, tol: Tolerances) -> bool {
408 (self.scale - other.scale).abs() <= CLASSIFY_EPS
409 && self.linear.is_equal(&other.linear, CLASSIFY_EPS)
410 && self.translation.is_equal(other.translation, tol)
411 }
412
413 #[must_use]
415 pub fn to_general(&self) -> GeneralTransform {
416 GeneralTransform {
417 linear: self.linear * self.scale,
418 translation: self.translation,
419 }
420 }
421}
422
423impl Mul for Transform {
424 type Output = Self;
425 fn mul(self, b: Self) -> Self {
427 if self.kind == TransformKind::Identity {
428 return b;
429 }
430 if b.kind == TransformKind::Identity {
431 return self;
432 }
433 Self::build(
434 self.linear * b.linear,
435 self.scale * b.scale,
436 self.linear * (b.translation * self.scale) + self.translation,
437 )
438 }
439}
440
441impl Default for Transform2 {
442 fn default() -> Self {
443 Self::IDENTITY
444 }
445}
446
447impl Transform2 {
448 pub const IDENTITY: Self = Self {
450 linear: Matrix2::IDENTITY,
451 scale: 1.0,
452 translation: Vector2::ZERO,
453 kind: TransformKind::Identity,
454 };
455
456 fn build(linear: Matrix2, scale: f64, translation: Vector2) -> Self {
457 let is_identity_linear = linear.is_equal(&Matrix2::IDENTITY, CLASSIFY_EPS);
458 let unit_scale = (scale - 1.0).abs() <= CLASSIFY_EPS;
459 let kind = if is_identity_linear && unit_scale {
460 if translation.square_magnitude() == 0.0 {
461 TransformKind::Identity
462 } else {
463 TransformKind::Translation
464 }
465 } else if is_identity_linear && (scale + 1.0).abs() <= CLASSIFY_EPS {
466 TransformKind::PointMirror
467 } else if is_identity_linear {
468 TransformKind::Scale
469 } else if unit_scale && (linear.determinant().abs() - 1.0).abs() <= CLASSIFY_EPS {
470 if linear.determinant() > 0.0 {
471 TransformKind::Rotation
472 } else {
473 TransformKind::PlaneMirror
474 }
475 } else {
476 TransformKind::Compound
477 };
478 Self {
479 linear,
480 scale,
481 translation,
482 kind,
483 }
484 }
485
486 #[must_use]
488 pub fn translation(v: Vector2) -> Self {
489 Self::build(Matrix2::IDENTITY, 1.0, v)
490 }
491
492 #[must_use]
494 pub fn rotation(centre: Point2, angle: f64) -> Self {
495 let linear = Matrix2::rotation(angle);
496 let c = centre.to_vector();
497 Self::build(linear, 1.0, c - linear * c)
498 }
499
500 pub fn scaling(centre: Point2, factor: f64, tol: Tolerances) -> OgeomResult<Self> {
507 if !factor.is_finite() || factor.abs() <= tol.confusion() {
508 ogeom_bail!(Construction, "scale factor {factor} is degenerate");
509 }
510 let c = centre.to_vector();
511 Ok(Self::build(Matrix2::IDENTITY, factor, c - c * factor))
512 }
513
514 #[must_use]
516 pub fn line_mirror(origin: Point2, normal: Direction2) -> Self {
517 let (x, y) = (normal.x(), normal.y());
518 let linear = Matrix2::new([
519 [(-2.0f64).mul_add(x * x, 1.0), -2.0 * x * y],
520 [-2.0 * x * y, (-2.0f64).mul_add(y * y, 1.0)],
521 ]);
522 let p = origin.to_vector();
523 Self::build(linear, 1.0, p - linear * p)
524 }
525
526 #[must_use]
529 pub fn from_frame(frame: &Frame2) -> Self {
530 let (x, y) = (frame.x(), frame.y());
531 let linear = Matrix2::new([[x.x(), y.x()], [x.y(), y.y()]]);
532 Self::build(linear, 1.0, frame.origin().to_vector())
533 }
534
535 #[must_use]
537 pub const fn kind(&self) -> TransformKind {
538 self.kind
539 }
540
541 #[must_use]
543 pub const fn linear(&self) -> Matrix2 {
544 self.linear
545 }
546
547 #[must_use]
549 pub const fn scale_factor(&self) -> f64 {
550 self.scale
551 }
552
553 #[must_use]
555 pub const fn translation_vector(&self) -> Vector2 {
556 self.translation
557 }
558
559 #[must_use]
563 pub fn preserves_handedness(&self) -> bool {
564 self.linear.determinant() > 0.0
565 }
566
567 #[must_use]
569 pub fn apply(&self, p: Point2) -> Point2 {
570 match self.kind {
571 TransformKind::Identity => p,
572 TransformKind::Translation => p + self.translation,
573 TransformKind::PointMirror | TransformKind::Scale => {
574 Point2::from_vector(p.to_vector() * self.scale + self.translation)
575 }
576 TransformKind::Rotation | TransformKind::PlaneMirror => {
577 Point2::from_vector(self.linear * p.to_vector() + self.translation)
578 }
579 TransformKind::Compound => {
580 Point2::from_vector(self.linear * (p.to_vector() * self.scale) + self.translation)
581 }
582 }
583 }
584
585 pub fn apply_direction(&self, d: Direction2, tol: Tolerances) -> OgeomResult<Direction2> {
592 match self.kind {
593 TransformKind::Identity | TransformKind::Translation => Ok(d),
594 TransformKind::Scale if self.scale > 0.0 => Ok(d),
595 TransformKind::PointMirror | TransformKind::Scale => Ok(d.reversed()),
596 _ => Direction2::new(self.apply_vector(d.vector()), tol),
597 }
598 }
599
600 pub fn apply_frame(&self, f: &Frame2, tol: Tolerances) -> OgeomResult<Frame2> {
607 Frame2::from_axes(
608 self.apply(f.origin()),
609 self.apply_direction(f.x(), tol)?,
610 self.apply_direction(f.y(), tol)?,
611 tol,
612 )
613 }
614
615 #[must_use]
617 pub fn apply_vector(&self, v: Vector2) -> Vector2 {
618 match self.kind {
619 TransformKind::Identity | TransformKind::Translation => v,
620 TransformKind::PointMirror | TransformKind::Scale => v * self.scale,
621 TransformKind::Rotation | TransformKind::PlaneMirror => self.linear * v,
622 TransformKind::Compound => self.linear * (v * self.scale),
623 }
624 }
625
626 pub fn inverse(&self) -> OgeomResult<Self> {
633 if self.kind == TransformKind::Identity {
634 return Ok(Self::IDENTITY);
635 }
636 if self.scale == 0.0 {
637 ogeom_bail!(Numeric, "transform has a zero scale and no inverse");
638 }
639 let inv_linear = self.linear.transposed();
640 let inv_scale = 1.0 / self.scale;
641 Ok(Self::build(
642 inv_linear,
643 inv_scale,
644 -(inv_linear * self.translation) * inv_scale,
645 ))
646 }
647
648 #[must_use]
650 pub fn is_equal(&self, other: &Self, tol: Tolerances) -> bool {
651 (self.scale - other.scale).abs() <= CLASSIFY_EPS
652 && self.linear.is_equal(&other.linear, CLASSIFY_EPS)
653 && self.translation.is_equal(other.translation, tol)
654 }
655}
656
657impl Mul for Transform2 {
658 type Output = Self;
659 fn mul(self, b: Self) -> Self {
660 if self.kind == TransformKind::Identity {
661 return b;
662 }
663 if b.kind == TransformKind::Identity {
664 return self;
665 }
666 Self::build(
667 self.linear * b.linear,
668 self.scale * b.scale,
669 self.linear * (b.translation * self.scale) + self.translation,
670 )
671 }
672}
673
674impl Default for GeneralTransform {
675 fn default() -> Self {
676 Self::IDENTITY
677 }
678}
679
680impl GeneralTransform {
681 pub const IDENTITY: Self = Self {
683 linear: Matrix3::IDENTITY,
684 translation: Vector::ZERO,
685 };
686
687 #[must_use]
689 pub const fn new(linear: Matrix3, translation: Vector) -> Self {
690 Self {
691 linear,
692 translation,
693 }
694 }
695
696 #[must_use]
698 pub const fn scaling_xyz(x: f64, y: f64, z: f64) -> Self {
699 Self::new(Matrix3::scaling_xyz(x, y, z), Vector::ZERO)
700 }
701
702 #[must_use]
704 pub fn apply(&self, p: Point) -> Point {
705 Point::from_vector(self.linear * p.to_vector() + self.translation)
706 }
707
708 #[must_use]
710 pub fn apply_vector(&self, v: Vector) -> Vector {
711 self.linear * v
712 }
713
714 pub fn apply_normal(&self, n: Vector) -> OgeomResult<Vector> {
728 Ok(self.linear.inverse()?.transposed() * n)
729 }
730
731 #[must_use]
733 pub fn preserves_handedness(&self) -> bool {
734 self.linear.determinant() > 0.0
735 }
736
737 #[must_use]
739 pub fn volume_ratio(&self) -> f64 {
740 self.linear.determinant()
741 }
742
743 pub fn inverse(&self) -> OgeomResult<Self> {
750 let inv = self.linear.inverse()?;
751 Ok(Self::new(inv, -(inv * self.translation)))
752 }
753
754 #[must_use]
756 pub fn is_similarity(&self, eps: f64) -> bool {
757 self.to_similarity(eps).is_some()
758 }
759
760 #[must_use]
765 pub fn to_similarity(&self, eps: f64) -> Option<Transform> {
766 let det = self.linear.determinant();
769 if det == 0.0 {
770 return None;
771 }
772 let scale = det.abs().cbrt() * det.signum();
773 let rotation = self.linear * (1.0 / scale);
774 if !rotation.is_orthonormal(eps) {
775 return None;
776 }
777 Some(Transform::build(rotation, scale, self.translation))
778 }
779}
780
781impl Mul for GeneralTransform {
782 type Output = Self;
783 fn mul(self, b: Self) -> Self {
785 Self::new(
786 self.linear * b.linear,
787 self.linear * b.translation + self.translation,
788 )
789 }
790}
791
792impl From<Transform> for GeneralTransform {
793 fn from(t: Transform) -> Self {
794 t.to_general()
795 }
796}
797
798#[cfg(test)]
799#[allow(clippy::unwrap_used)]
800mod tests {
801 use super::*;
802 use approx::assert_relative_eq;
803
804 const T: Tolerances = Tolerances::millimetres();
805
806 #[test]
810 fn a_negative_scale_reverses_directions() {
811 let s = Transform::scaling(Point::ORIGIN, -2.0, T).unwrap();
812 let d = s.apply_direction(Direction::X, T).unwrap();
813 let v = s.apply_vector(Vector::new(1.0, 0.0, 0.0));
814 assert!(d.vector().dot(v) > 0.0, "{d:?} against {v:?}");
815 assert!(!s.preserves_handedness());
816 let s2 = Transform2::scaling(Point2::ORIGIN, -3.0, T).unwrap();
817 let d2 = s2.apply_direction(Direction2::X, T).unwrap();
818 assert!(d2.vector().dot(s2.apply_vector(Direction2::X.vector())) > 0.0);
819 let p = Transform::scaling(Point::ORIGIN, 2.0, T).unwrap();
821 assert_eq!(p.apply_direction(Direction::X, T).unwrap(), Direction::X);
822 }
823
824 fn sample_points() -> [Point; 4] {
825 [
826 Point::ORIGIN,
827 Point::new(1.0, 0.0, 0.0),
828 Point::new(-3.0, 7.5, 2.25),
829 Point::new(1e3, -1e3, 0.5),
830 ]
831 }
832
833 #[test]
834 fn classification_matches_what_the_transform_does() {
835 assert_eq!(Transform::IDENTITY.kind(), TransformKind::Identity);
836 assert_eq!(
837 Transform::translation(Vector::X).kind(),
838 TransformKind::Translation
839 );
840 assert_eq!(
841 Transform::rotation(Axis::Z, 0.5).kind(),
842 TransformKind::Rotation
843 );
844 assert_eq!(
845 Transform::point_mirror(Point::ORIGIN).kind(),
846 TransformKind::PointMirror
847 );
848 assert_eq!(
849 Transform::plane_mirror(Point::ORIGIN, Direction::Z).kind(),
850 TransformKind::PlaneMirror
851 );
852 assert_eq!(
853 Transform::scaling(Point::ORIGIN, 3.0, T).unwrap().kind(),
854 TransformKind::Scale
855 );
856 let compound =
857 Transform::rotation(Axis::Z, 0.5) * Transform::scaling(Point::ORIGIN, 3.0, T).unwrap();
858 assert_eq!(compound.kind(), TransformKind::Compound);
859 }
860
861 #[test]
862 fn a_zero_rotation_classifies_as_identity_not_rotation() {
863 assert_eq!(
866 Transform::rotation(Axis::Z, 0.0).kind(),
867 TransformKind::Identity
868 );
869 assert_eq!(
870 Transform::translation(Vector::ZERO).kind(),
871 TransformKind::Identity
872 );
873 assert_eq!(
874 Transform::scaling(Point::ORIGIN, 1.0, T).unwrap().kind(),
875 TransformKind::Identity
876 );
877 }
878
879 #[test]
880 fn every_dispatch_path_gives_the_same_answer_as_the_general_one() {
881 let cases = [
884 Transform::IDENTITY,
885 Transform::translation(Vector::new(1.0, -2.0, 3.0)),
886 Transform::rotation(Axis::new(Point::new(1.0, 0.0, 0.0), Direction::Z), 0.7),
887 Transform::point_mirror(Point::new(2.0, 0.0, -1.0)),
888 Transform::plane_mirror(Point::new(0.0, 1.0, 0.0), Direction::Y),
889 Transform::scaling(Point::new(1.0, 1.0, 1.0), 2.5, T).unwrap(),
890 ];
891 for t in cases {
892 for p in sample_points() {
893 let general = Point::from_vector(
894 t.linear() * (p.to_vector() * t.scale_factor()) + t.translation_vector(),
895 );
896 assert!(
897 t.apply(p).is_equal(general, T),
898 "fast path for {:?} disagrees",
899 t.kind()
900 );
901 }
902 }
903 }
904
905 #[test]
906 fn rotation_about_an_off_origin_axis_leaves_the_axis_fixed() {
907 let axis = Axis::new(Point::new(5.0, 3.0, 0.0), Direction::Z);
908 let t = Transform::rotation(axis, 1.234);
909 assert!(t.apply(axis.location).is_equal(axis.location, T));
910 assert!(
911 t.apply(axis.point_at(10.0))
912 .is_equal(axis.point_at(10.0), T)
913 );
914 let p = Point::new(6.0, 3.0, 0.0);
916 assert_relative_eq!(axis.distance_to(t.apply(p)), 1.0, epsilon = 1e-14);
917 }
918
919 #[test]
920 fn scaling_about_a_centre_leaves_the_centre_fixed() {
921 let c = Point::new(3.0, -1.0, 2.0);
922 let t = Transform::scaling(c, 4.0, T).unwrap();
923 assert!(t.apply(c).is_equal(c, T));
924 let p = c + Vector::new(1.0, 0.0, 0.0);
925 assert!(t.apply(p).is_equal(c + Vector::new(4.0, 0.0, 0.0), T));
926 }
927
928 #[test]
929 fn degenerate_scales_are_refused() {
930 assert!(Transform::scaling(Point::ORIGIN, 0.0, T).is_err());
931 assert!(Transform::scaling(Point::ORIGIN, f64::NAN, T).is_err());
932 assert!(Transform::scaling(Point::ORIGIN, f64::INFINITY, T).is_err());
933 assert!(
934 Transform::scaling(Point::ORIGIN, -2.0, T).is_ok(),
935 "negative is fine"
936 );
937 }
938
939 #[test]
940 fn handedness_tracks_mirroring() {
941 assert!(Transform::rotation(Axis::Z, 1.0).preserves_handedness());
942 assert!(Transform::translation(Vector::X).preserves_handedness());
943 assert!(
944 Transform::scaling(Point::ORIGIN, 3.0, T)
945 .unwrap()
946 .preserves_handedness()
947 );
948 assert!(!Transform::plane_mirror(Point::ORIGIN, Direction::Z).preserves_handedness());
949 assert!(!Transform::point_mirror(Point::ORIGIN).preserves_handedness());
950 let twice = Transform::plane_mirror(Point::ORIGIN, Direction::Z)
952 * Transform::plane_mirror(Point::ORIGIN, Direction::X);
953 assert!(twice.preserves_handedness());
954 }
955
956 #[test]
957 fn axis_mirror_is_a_half_turn() {
958 let t = Transform::axis_mirror(Axis::Z);
959 assert!(
960 t.apply(Point::new(1.0, 0.0, 5.0))
961 .is_equal(Point::new(-1.0, 0.0, 5.0), T)
962 );
963 assert!(t.preserves_handedness(), "a half turn is a rotation");
964 }
965
966 #[test]
967 fn inverse_round_trips_for_every_kind() {
968 let cases = [
969 Transform::IDENTITY,
970 Transform::translation(Vector::new(1.0, -2.0, 3.0)),
971 Transform::rotation(Axis::new(Point::new(1.0, 2.0, 3.0), Direction::Y), 2.1),
972 Transform::point_mirror(Point::new(1.0, 1.0, 1.0)),
973 Transform::plane_mirror(Point::new(0.0, 0.0, 4.0), Direction::Z),
974 Transform::scaling(Point::new(-1.0, 0.0, 0.0), 0.25, T).unwrap(),
975 ];
976 for t in cases {
977 let inv = t.inverse().unwrap();
978 for p in sample_points() {
979 assert!(inv.apply(t.apply(p)).is_equal(p, T), "{:?}", t.kind());
980 assert!(t.apply(inv.apply(p)).is_equal(p, T), "{:?}", t.kind());
981 }
982 }
983 }
984
985 #[test]
986 fn composition_applies_right_to_left() {
987 let a = Transform::translation(Vector::new(10.0, 0.0, 0.0));
988 let b = Transform::rotation(Axis::Z, core::f64::consts::FRAC_PI_2);
989 let p = Point::new(1.0, 0.0, 0.0);
990 assert!((a * b).apply(p).is_equal(a.apply(b.apply(p)), T));
991 assert!((b * a).apply(p).is_equal(b.apply(a.apply(p)), T));
992 assert!(!(a * b).is_equal(&(b * a), T));
994 }
995
996 #[test]
997 fn composition_is_associative() {
998 let a = Transform::rotation(Axis::X, 0.3);
999 let b = Transform::scaling(Point::new(1.0, 0.0, 0.0), 2.0, T).unwrap();
1000 let c = Transform::translation(Vector::new(0.0, 5.0, 0.0));
1001 assert!(((a * b) * c).is_equal(&(a * (b * c)), T));
1002 }
1003
1004 #[test]
1005 fn vectors_ignore_translation_and_directions_stay_unit() {
1006 let t = Transform::translation(Vector::new(100.0, 0.0, 0.0))
1007 * Transform::rotation(Axis::Z, 0.9);
1008 let v = Vector::new(1.0, 2.0, 3.0);
1009 assert!(
1010 t.apply_vector(v)
1011 .is_equal(Transform::rotation(Axis::Z, 0.9).apply_vector(v), T)
1012 );
1013 let d = t.apply_direction(Direction::X, T).unwrap();
1014 assert_relative_eq!(d.vector().magnitude(), 1.0, epsilon = 1e-15);
1015 }
1016
1017 #[test]
1018 fn a_point_mirror_reverses_directions() {
1019 let t = Transform::point_mirror(Point::new(5.0, 5.0, 5.0));
1020 assert!(
1021 t.apply_direction(Direction::X, T)
1022 .unwrap()
1023 .is_equal(-Direction::X, T)
1024 );
1025 let s = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap();
1027 assert!(
1028 s.apply_direction(Direction::X, T)
1029 .unwrap()
1030 .is_equal(Direction::X, T)
1031 );
1032 }
1033
1034 #[test]
1035 fn frame_transforms_round_trip_through_world() {
1036 let f = Frame::new(
1037 Point::new(1.0, 2.0, 3.0),
1038 Direction::from_coords(1.0, 1.0, 0.0, T).unwrap(),
1039 Direction::Z,
1040 T,
1041 )
1042 .unwrap();
1043 let to = Transform::to_frame(&f);
1044 let from = Transform::from_frame(&f);
1045 for p in sample_points() {
1046 assert!(from.apply(to.apply(p)).is_equal(p, T));
1047 assert!(to.apply(p).is_equal(f.to_local(p), T));
1049 assert!(from.apply(f.to_local(p)).is_equal(p, T));
1050 }
1051 }
1052
1053 #[test]
1054 fn between_frames_composes_correctly() {
1055 let a = Frame::new(Point::new(1.0, 0.0, 0.0), Direction::Z, Direction::X, T).unwrap();
1056 let b = Frame::new(Point::new(0.0, 5.0, 0.0), Direction::X, Direction::Y, T).unwrap();
1057 let t = Transform::between_frames(&a, &b);
1058 let local = Point::new(1.0, 2.0, 3.0);
1061 assert!(b.to_world(t.apply(local)).is_equal(a.to_world(local), T));
1062 }
1063
1064 #[test]
1065 fn general_transform_normals_use_the_inverse_transpose() {
1066 let g = GeneralTransform::scaling_xyz(2.0, 1.0, 1.0);
1071 let n = Vector::new(1.0, 0.0, -1.0);
1072 let transformed = g.apply_normal(n).unwrap();
1073
1074 let on_plane = Vector::new(1.0, 0.0, 1.0);
1075 assert_relative_eq!(n.dot(on_plane), 0.0, epsilon = 1e-15);
1076 assert_relative_eq!(
1077 transformed.dot(g.apply_vector(on_plane)),
1078 0.0,
1079 epsilon = 1e-14,
1080 max_relative = 1e-14
1081 );
1082 assert!(g.apply_vector(n).dot(g.apply_vector(on_plane)).abs() > 1e-6);
1084 }
1085
1086 #[test]
1087 fn general_transform_recognizes_similarities() {
1088 let similar: GeneralTransform = Transform::rotation(Axis::Z, 0.4).into();
1089 assert!(similar.is_similarity(1e-12));
1090 let narrowed = similar.to_similarity(1e-12).unwrap();
1091 assert_eq!(narrowed.kind(), TransformKind::Rotation);
1092
1093 let scaled: GeneralTransform = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap().into();
1094 assert!(scaled.is_similarity(1e-12));
1095 assert_relative_eq!(
1096 scaled.to_similarity(1e-12).unwrap().scale_factor(),
1097 3.0,
1098 epsilon = 1e-12
1099 );
1100
1101 assert!(!GeneralTransform::scaling_xyz(1.0, 2.0, 3.0).is_similarity(1e-12));
1102 let shear = GeneralTransform::new(
1103 Matrix3::new([[1.0, 0.5, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
1104 Vector::ZERO,
1105 );
1106 assert!(!shear.is_similarity(1e-12));
1107 }
1108
1109 #[test]
1110 fn general_transform_volume_ratio_and_inverse() {
1111 let g = GeneralTransform::scaling_xyz(2.0, 3.0, 4.0);
1112 assert_relative_eq!(g.volume_ratio(), 24.0);
1113 assert!(g.preserves_handedness());
1114 let inv = g.inverse().unwrap();
1115 for p in sample_points() {
1116 assert!(inv.apply(g.apply(p)).is_equal(p, T));
1117 }
1118 let flip = GeneralTransform::scaling_xyz(-1.0, 1.0, 1.0);
1119 assert!(!flip.preserves_handedness());
1120 assert!(
1121 GeneralTransform::scaling_xyz(0.0, 1.0, 1.0)
1122 .inverse()
1123 .is_err()
1124 );
1125 }
1126
1127 #[test]
1128 fn transform2_behaves_like_its_3d_counterpart() {
1129 let r = Transform2::rotation(Point2::new(1.0, 1.0), core::f64::consts::FRAC_PI_2);
1130 assert_eq!(r.kind(), TransformKind::Rotation);
1131 assert!(
1132 r.apply(Point2::new(1.0, 1.0))
1133 .is_equal(Point2::new(1.0, 1.0), T)
1134 );
1135 assert!(
1136 r.apply(Point2::new(2.0, 1.0))
1137 .is_equal(Point2::new(1.0, 2.0), T)
1138 );
1139 assert!(
1140 r.inverse()
1141 .unwrap()
1142 .apply(r.apply(Point2::ORIGIN))
1143 .is_equal(Point2::ORIGIN, T)
1144 );
1145
1146 let m = Transform2::line_mirror(Point2::ORIGIN, Direction2::Y);
1147 assert_eq!(m.kind(), TransformKind::PlaneMirror);
1148 assert!(!m.preserves_handedness());
1149 assert!(
1150 m.apply(Point2::new(3.0, 2.0))
1151 .is_equal(Point2::new(3.0, -2.0), T)
1152 );
1153
1154 assert!(Transform2::scaling(Point2::ORIGIN, 0.0, T).is_err());
1155 }
1156}