use core::ops::Mul;
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
use crate::{
Axis, Direction, Direction2, Frame, Frame2, Matrix2, Matrix3, Point, Point2, Quaternion,
Vector, Vector2,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TransformKind {
#[default]
Identity,
Translation,
Rotation,
PointMirror,
PlaneMirror,
Scale,
Compound,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
linear: Matrix3,
scale: f64,
translation: Vector,
kind: TransformKind,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform2 {
linear: Matrix2,
scale: f64,
translation: Vector2,
kind: TransformKind,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeneralTransform {
pub linear: Matrix3,
pub translation: Vector,
}
const CLASSIFY_EPS: f64 = 1e-12;
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Self = Self {
linear: Matrix3::IDENTITY,
scale: 1.0,
translation: Vector::ZERO,
kind: TransformKind::Identity,
};
fn build(linear: Matrix3, scale: f64, translation: Vector) -> Self {
let kind = Self::classify(&linear, scale, translation);
Self {
linear,
scale,
translation,
kind,
}
}
pub fn from_parts(
linear: Matrix3,
scale: f64,
translation: Vector,
eps: f64,
) -> OgeomResult<Self> {
if !scale.is_finite() || scale == 0.0 {
ogeom_bail!(
Construction,
"a placement's scale must be finite and non-zero; got {scale}"
);
}
if !linear.is_orthonormal(eps) {
ogeom_bail!(
Construction,
"a placement's linear part must be orthonormal; this one shears or scales unevenly"
);
}
Ok(Self::build(linear, scale, translation))
}
fn classify(linear: &Matrix3, scale: f64, translation: Vector) -> TransformKind {
let is_identity_linear = linear.is_equal(&Matrix3::IDENTITY, CLASSIFY_EPS);
let unit_scale = (scale - 1.0).abs() <= CLASSIFY_EPS;
let negative_unit_scale = (scale + 1.0).abs() <= CLASSIFY_EPS;
let no_translation = translation.square_magnitude() == 0.0;
if is_identity_linear && unit_scale {
return if no_translation {
TransformKind::Identity
} else {
TransformKind::Translation
};
}
if is_identity_linear && negative_unit_scale {
return TransformKind::PointMirror;
}
if is_identity_linear {
return TransformKind::Scale;
}
if unit_scale && linear.is_orthonormal(CLASSIFY_EPS) {
return if linear.determinant() > 0.0 {
TransformKind::Rotation
} else {
TransformKind::PlaneMirror
};
}
TransformKind::Compound
}
#[must_use]
pub fn translation(v: Vector) -> Self {
Self::build(Matrix3::IDENTITY, 1.0, v)
}
#[must_use]
pub fn rotation(axis: Axis, angle: f64) -> Self {
let linear = Matrix3::rotation(axis.direction, angle);
let p = axis.location.to_vector();
Self::build(linear, 1.0, p - linear * p)
}
#[must_use]
pub fn from_quaternion(q: Quaternion) -> Self {
Self::build(q.to_matrix(), 1.0, Vector::ZERO)
}
pub fn scaling(centre: Point, factor: f64, tol: Tolerances) -> OgeomResult<Self> {
if !factor.is_finite() || factor.abs() <= tol.confusion() {
ogeom_bail!(Construction, "scale factor {factor} is degenerate");
}
let c = centre.to_vector();
Ok(Self::build(Matrix3::IDENTITY, factor, c - c * factor))
}
#[must_use]
pub fn point_mirror(centre: Point) -> Self {
let c = centre.to_vector();
Self::build(Matrix3::IDENTITY, -1.0, c + c)
}
#[must_use]
pub fn plane_mirror(origin: Point, normal: Direction) -> Self {
let linear = Matrix3::reflection(normal);
let p = origin.to_vector();
Self::build(linear, 1.0, p - linear * p)
}
#[must_use]
pub fn axis_mirror(axis: Axis) -> Self {
Self::rotation(axis, core::f64::consts::PI)
}
#[must_use]
pub fn to_frame(frame: &Frame) -> Self {
let linear = frame.to_matrix().transposed();
Self::build(linear, 1.0, -(linear * frame.origin().to_vector()))
}
#[must_use]
pub fn from_frame(frame: &Frame) -> Self {
Self::build(frame.to_matrix(), 1.0, frame.origin().to_vector())
}
#[must_use]
pub fn between_frames(from: &Frame, to: &Frame) -> Self {
Self::to_frame(to) * Self::from_frame(from)
}
#[must_use]
pub const fn kind(&self) -> TransformKind {
self.kind
}
#[must_use]
pub const fn linear(&self) -> Matrix3 {
self.linear
}
#[must_use]
pub const fn scale_factor(&self) -> f64 {
self.scale
}
#[must_use]
pub const fn translation_vector(&self) -> Vector {
self.translation
}
#[must_use]
pub fn preserves_handedness(&self) -> bool {
self.linear.determinant() * self.scale.signum() > 0.0
}
#[must_use]
pub fn apply(&self, p: Point) -> Point {
match self.kind {
TransformKind::Identity => p,
TransformKind::Translation => p + self.translation,
TransformKind::PointMirror | TransformKind::Scale => {
Point::from_vector(p.to_vector() * self.scale + self.translation)
}
TransformKind::Rotation | TransformKind::PlaneMirror => {
Point::from_vector(self.linear * p.to_vector() + self.translation)
}
TransformKind::Compound => {
Point::from_vector(self.linear * (p.to_vector() * self.scale) + self.translation)
}
}
}
#[must_use]
pub fn apply_vector(&self, v: Vector) -> Vector {
match self.kind {
TransformKind::Identity | TransformKind::Translation => v,
TransformKind::PointMirror | TransformKind::Scale => v * self.scale,
TransformKind::Rotation | TransformKind::PlaneMirror => self.linear * v,
TransformKind::Compound => self.linear * (v * self.scale),
}
}
pub fn apply_direction(&self, d: Direction, tol: Tolerances) -> OgeomResult<Direction> {
match self.kind {
TransformKind::Identity | TransformKind::Translation => Ok(d),
TransformKind::Scale if self.scale > 0.0 => Ok(d),
TransformKind::PointMirror | TransformKind::Scale => Ok(d.reversed()),
_ => Direction::new(self.apply_vector(d.vector()), tol),
}
}
pub fn apply_frame(&self, f: &Frame, tol: Tolerances) -> OgeomResult<Frame> {
Frame::from_axes(
self.apply(f.origin()),
self.apply_direction(f.x(), tol)?,
self.apply_direction(f.y(), tol)?,
self.apply_direction(f.z(), tol)?,
tol,
)
}
pub fn inverse(&self) -> OgeomResult<Self> {
if self.kind == TransformKind::Identity {
return Ok(Self::IDENTITY);
}
if self.kind == TransformKind::Translation {
return Ok(Self::translation(-self.translation));
}
if self.scale == 0.0 {
ogeom_bail!(Numeric, "transform has a zero scale and no inverse");
}
let inv_linear = self.linear.transposed();
let inv_scale = 1.0 / self.scale;
Ok(Self::build(
inv_linear,
inv_scale,
-(inv_linear * self.translation) * inv_scale,
))
}
#[must_use]
pub fn is_equal(&self, other: &Self, tol: Tolerances) -> bool {
(self.scale - other.scale).abs() <= CLASSIFY_EPS
&& self.linear.is_equal(&other.linear, CLASSIFY_EPS)
&& self.translation.is_equal(other.translation, tol)
}
#[must_use]
pub fn to_general(&self) -> GeneralTransform {
GeneralTransform {
linear: self.linear * self.scale,
translation: self.translation,
}
}
}
impl Mul for Transform {
type Output = Self;
fn mul(self, b: Self) -> Self {
if self.kind == TransformKind::Identity {
return b;
}
if b.kind == TransformKind::Identity {
return self;
}
Self::build(
self.linear * b.linear,
self.scale * b.scale,
self.linear * (b.translation * self.scale) + self.translation,
)
}
}
impl Default for Transform2 {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform2 {
pub const IDENTITY: Self = Self {
linear: Matrix2::IDENTITY,
scale: 1.0,
translation: Vector2::ZERO,
kind: TransformKind::Identity,
};
fn build(linear: Matrix2, scale: f64, translation: Vector2) -> Self {
let is_identity_linear = linear.is_equal(&Matrix2::IDENTITY, CLASSIFY_EPS);
let unit_scale = (scale - 1.0).abs() <= CLASSIFY_EPS;
let kind = if is_identity_linear && unit_scale {
if translation.square_magnitude() == 0.0 {
TransformKind::Identity
} else {
TransformKind::Translation
}
} else if is_identity_linear && (scale + 1.0).abs() <= CLASSIFY_EPS {
TransformKind::PointMirror
} else if is_identity_linear {
TransformKind::Scale
} else if unit_scale && (linear.determinant().abs() - 1.0).abs() <= CLASSIFY_EPS {
if linear.determinant() > 0.0 {
TransformKind::Rotation
} else {
TransformKind::PlaneMirror
}
} else {
TransformKind::Compound
};
Self {
linear,
scale,
translation,
kind,
}
}
#[must_use]
pub fn translation(v: Vector2) -> Self {
Self::build(Matrix2::IDENTITY, 1.0, v)
}
#[must_use]
pub fn rotation(centre: Point2, angle: f64) -> Self {
let linear = Matrix2::rotation(angle);
let c = centre.to_vector();
Self::build(linear, 1.0, c - linear * c)
}
pub fn scaling(centre: Point2, factor: f64, tol: Tolerances) -> OgeomResult<Self> {
if !factor.is_finite() || factor.abs() <= tol.confusion() {
ogeom_bail!(Construction, "scale factor {factor} is degenerate");
}
let c = centre.to_vector();
Ok(Self::build(Matrix2::IDENTITY, factor, c - c * factor))
}
#[must_use]
pub fn line_mirror(origin: Point2, normal: Direction2) -> Self {
let (x, y) = (normal.x(), normal.y());
let linear = Matrix2::new([
[(-2.0f64).mul_add(x * x, 1.0), -2.0 * x * y],
[-2.0 * x * y, (-2.0f64).mul_add(y * y, 1.0)],
]);
let p = origin.to_vector();
Self::build(linear, 1.0, p - linear * p)
}
#[must_use]
pub const fn kind(&self) -> TransformKind {
self.kind
}
#[must_use]
pub const fn linear(&self) -> Matrix2 {
self.linear
}
#[must_use]
pub const fn scale_factor(&self) -> f64 {
self.scale
}
#[must_use]
pub const fn translation_vector(&self) -> Vector2 {
self.translation
}
#[must_use]
pub fn preserves_handedness(&self) -> bool {
self.linear.determinant() > 0.0
}
#[must_use]
pub fn apply(&self, p: Point2) -> Point2 {
match self.kind {
TransformKind::Identity => p,
TransformKind::Translation => p + self.translation,
TransformKind::PointMirror | TransformKind::Scale => {
Point2::from_vector(p.to_vector() * self.scale + self.translation)
}
TransformKind::Rotation | TransformKind::PlaneMirror => {
Point2::from_vector(self.linear * p.to_vector() + self.translation)
}
TransformKind::Compound => {
Point2::from_vector(self.linear * (p.to_vector() * self.scale) + self.translation)
}
}
}
pub fn apply_direction(&self, d: Direction2, tol: Tolerances) -> OgeomResult<Direction2> {
match self.kind {
TransformKind::Identity | TransformKind::Translation => Ok(d),
TransformKind::Scale if self.scale > 0.0 => Ok(d),
TransformKind::PointMirror | TransformKind::Scale => Ok(d.reversed()),
_ => Direction2::new(self.apply_vector(d.vector()), tol),
}
}
pub fn apply_frame(&self, f: &Frame2, tol: Tolerances) -> OgeomResult<Frame2> {
Frame2::from_axes(
self.apply(f.origin()),
self.apply_direction(f.x(), tol)?,
self.apply_direction(f.y(), tol)?,
tol,
)
}
#[must_use]
pub fn apply_vector(&self, v: Vector2) -> Vector2 {
match self.kind {
TransformKind::Identity | TransformKind::Translation => v,
TransformKind::PointMirror | TransformKind::Scale => v * self.scale,
TransformKind::Rotation | TransformKind::PlaneMirror => self.linear * v,
TransformKind::Compound => self.linear * (v * self.scale),
}
}
pub fn inverse(&self) -> OgeomResult<Self> {
if self.kind == TransformKind::Identity {
return Ok(Self::IDENTITY);
}
if self.scale == 0.0 {
ogeom_bail!(Numeric, "transform has a zero scale and no inverse");
}
let inv_linear = self.linear.transposed();
let inv_scale = 1.0 / self.scale;
Ok(Self::build(
inv_linear,
inv_scale,
-(inv_linear * self.translation) * inv_scale,
))
}
#[must_use]
pub fn is_equal(&self, other: &Self, tol: Tolerances) -> bool {
(self.scale - other.scale).abs() <= CLASSIFY_EPS
&& self.linear.is_equal(&other.linear, CLASSIFY_EPS)
&& self.translation.is_equal(other.translation, tol)
}
}
impl Mul for Transform2 {
type Output = Self;
fn mul(self, b: Self) -> Self {
if self.kind == TransformKind::Identity {
return b;
}
if b.kind == TransformKind::Identity {
return self;
}
Self::build(
self.linear * b.linear,
self.scale * b.scale,
self.linear * (b.translation * self.scale) + self.translation,
)
}
}
impl Default for GeneralTransform {
fn default() -> Self {
Self::IDENTITY
}
}
impl GeneralTransform {
pub const IDENTITY: Self = Self {
linear: Matrix3::IDENTITY,
translation: Vector::ZERO,
};
#[must_use]
pub const fn new(linear: Matrix3, translation: Vector) -> Self {
Self {
linear,
translation,
}
}
#[must_use]
pub const fn scaling_xyz(x: f64, y: f64, z: f64) -> Self {
Self::new(Matrix3::scaling_xyz(x, y, z), Vector::ZERO)
}
#[must_use]
pub fn apply(&self, p: Point) -> Point {
Point::from_vector(self.linear * p.to_vector() + self.translation)
}
#[must_use]
pub fn apply_vector(&self, v: Vector) -> Vector {
self.linear * v
}
pub fn apply_normal(&self, n: Vector) -> OgeomResult<Vector> {
Ok(self.linear.inverse()?.transposed() * n)
}
#[must_use]
pub fn preserves_handedness(&self) -> bool {
self.linear.determinant() > 0.0
}
#[must_use]
pub fn volume_ratio(&self) -> f64 {
self.linear.determinant()
}
pub fn inverse(&self) -> OgeomResult<Self> {
let inv = self.linear.inverse()?;
Ok(Self::new(inv, -(inv * self.translation)))
}
#[must_use]
pub fn is_similarity(&self, eps: f64) -> bool {
self.to_similarity(eps).is_some()
}
#[must_use]
pub fn to_similarity(&self, eps: f64) -> Option<Transform> {
let det = self.linear.determinant();
if det == 0.0 {
return None;
}
let scale = det.abs().cbrt() * det.signum();
let rotation = self.linear * (1.0 / scale);
if !rotation.is_orthonormal(eps) {
return None;
}
Some(Transform::build(rotation, scale, self.translation))
}
}
impl Mul for GeneralTransform {
type Output = Self;
fn mul(self, b: Self) -> Self {
Self::new(
self.linear * b.linear,
self.linear * b.translation + self.translation,
)
}
}
impl From<Transform> for GeneralTransform {
fn from(t: Transform) -> Self {
t.to_general()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const T: Tolerances = Tolerances::millimetres();
#[test]
fn a_negative_scale_reverses_directions() {
let s = Transform::scaling(Point::ORIGIN, -2.0, T).unwrap();
let d = s.apply_direction(Direction::X, T).unwrap();
let v = s.apply_vector(Vector::new(1.0, 0.0, 0.0));
assert!(d.vector().dot(v) > 0.0, "{d:?} against {v:?}");
assert!(!s.preserves_handedness());
let s2 = Transform2::scaling(Point2::ORIGIN, -3.0, T).unwrap();
let d2 = s2.apply_direction(Direction2::X, T).unwrap();
assert!(d2.vector().dot(s2.apply_vector(Direction2::X.vector())) > 0.0);
let p = Transform::scaling(Point::ORIGIN, 2.0, T).unwrap();
assert_eq!(p.apply_direction(Direction::X, T).unwrap(), Direction::X);
}
fn sample_points() -> [Point; 4] {
[
Point::ORIGIN,
Point::new(1.0, 0.0, 0.0),
Point::new(-3.0, 7.5, 2.25),
Point::new(1e3, -1e3, 0.5),
]
}
#[test]
fn classification_matches_what_the_transform_does() {
assert_eq!(Transform::IDENTITY.kind(), TransformKind::Identity);
assert_eq!(
Transform::translation(Vector::X).kind(),
TransformKind::Translation
);
assert_eq!(
Transform::rotation(Axis::Z, 0.5).kind(),
TransformKind::Rotation
);
assert_eq!(
Transform::point_mirror(Point::ORIGIN).kind(),
TransformKind::PointMirror
);
assert_eq!(
Transform::plane_mirror(Point::ORIGIN, Direction::Z).kind(),
TransformKind::PlaneMirror
);
assert_eq!(
Transform::scaling(Point::ORIGIN, 3.0, T).unwrap().kind(),
TransformKind::Scale
);
let compound =
Transform::rotation(Axis::Z, 0.5) * Transform::scaling(Point::ORIGIN, 3.0, T).unwrap();
assert_eq!(compound.kind(), TransformKind::Compound);
}
#[test]
fn a_zero_rotation_classifies_as_identity_not_rotation() {
assert_eq!(
Transform::rotation(Axis::Z, 0.0).kind(),
TransformKind::Identity
);
assert_eq!(
Transform::translation(Vector::ZERO).kind(),
TransformKind::Identity
);
assert_eq!(
Transform::scaling(Point::ORIGIN, 1.0, T).unwrap().kind(),
TransformKind::Identity
);
}
#[test]
fn every_dispatch_path_gives_the_same_answer_as_the_general_one() {
let cases = [
Transform::IDENTITY,
Transform::translation(Vector::new(1.0, -2.0, 3.0)),
Transform::rotation(Axis::new(Point::new(1.0, 0.0, 0.0), Direction::Z), 0.7),
Transform::point_mirror(Point::new(2.0, 0.0, -1.0)),
Transform::plane_mirror(Point::new(0.0, 1.0, 0.0), Direction::Y),
Transform::scaling(Point::new(1.0, 1.0, 1.0), 2.5, T).unwrap(),
];
for t in cases {
for p in sample_points() {
let general = Point::from_vector(
t.linear() * (p.to_vector() * t.scale_factor()) + t.translation_vector(),
);
assert!(
t.apply(p).is_equal(general, T),
"fast path for {:?} disagrees",
t.kind()
);
}
}
}
#[test]
fn rotation_about_an_off_origin_axis_leaves_the_axis_fixed() {
let axis = Axis::new(Point::new(5.0, 3.0, 0.0), Direction::Z);
let t = Transform::rotation(axis, 1.234);
assert!(t.apply(axis.location).is_equal(axis.location, T));
assert!(
t.apply(axis.point_at(10.0))
.is_equal(axis.point_at(10.0), T)
);
let p = Point::new(6.0, 3.0, 0.0);
assert_relative_eq!(axis.distance_to(t.apply(p)), 1.0, epsilon = 1e-14);
}
#[test]
fn scaling_about_a_centre_leaves_the_centre_fixed() {
let c = Point::new(3.0, -1.0, 2.0);
let t = Transform::scaling(c, 4.0, T).unwrap();
assert!(t.apply(c).is_equal(c, T));
let p = c + Vector::new(1.0, 0.0, 0.0);
assert!(t.apply(p).is_equal(c + Vector::new(4.0, 0.0, 0.0), T));
}
#[test]
fn degenerate_scales_are_refused() {
assert!(Transform::scaling(Point::ORIGIN, 0.0, T).is_err());
assert!(Transform::scaling(Point::ORIGIN, f64::NAN, T).is_err());
assert!(Transform::scaling(Point::ORIGIN, f64::INFINITY, T).is_err());
assert!(
Transform::scaling(Point::ORIGIN, -2.0, T).is_ok(),
"negative is fine"
);
}
#[test]
fn handedness_tracks_mirroring() {
assert!(Transform::rotation(Axis::Z, 1.0).preserves_handedness());
assert!(Transform::translation(Vector::X).preserves_handedness());
assert!(
Transform::scaling(Point::ORIGIN, 3.0, T)
.unwrap()
.preserves_handedness()
);
assert!(!Transform::plane_mirror(Point::ORIGIN, Direction::Z).preserves_handedness());
assert!(!Transform::point_mirror(Point::ORIGIN).preserves_handedness());
let twice = Transform::plane_mirror(Point::ORIGIN, Direction::Z)
* Transform::plane_mirror(Point::ORIGIN, Direction::X);
assert!(twice.preserves_handedness());
}
#[test]
fn axis_mirror_is_a_half_turn() {
let t = Transform::axis_mirror(Axis::Z);
assert!(
t.apply(Point::new(1.0, 0.0, 5.0))
.is_equal(Point::new(-1.0, 0.0, 5.0), T)
);
assert!(t.preserves_handedness(), "a half turn is a rotation");
}
#[test]
fn inverse_round_trips_for_every_kind() {
let cases = [
Transform::IDENTITY,
Transform::translation(Vector::new(1.0, -2.0, 3.0)),
Transform::rotation(Axis::new(Point::new(1.0, 2.0, 3.0), Direction::Y), 2.1),
Transform::point_mirror(Point::new(1.0, 1.0, 1.0)),
Transform::plane_mirror(Point::new(0.0, 0.0, 4.0), Direction::Z),
Transform::scaling(Point::new(-1.0, 0.0, 0.0), 0.25, T).unwrap(),
];
for t in cases {
let inv = t.inverse().unwrap();
for p in sample_points() {
assert!(inv.apply(t.apply(p)).is_equal(p, T), "{:?}", t.kind());
assert!(t.apply(inv.apply(p)).is_equal(p, T), "{:?}", t.kind());
}
}
}
#[test]
fn composition_applies_right_to_left() {
let a = Transform::translation(Vector::new(10.0, 0.0, 0.0));
let b = Transform::rotation(Axis::Z, core::f64::consts::FRAC_PI_2);
let p = Point::new(1.0, 0.0, 0.0);
assert!((a * b).apply(p).is_equal(a.apply(b.apply(p)), T));
assert!((b * a).apply(p).is_equal(b.apply(a.apply(p)), T));
assert!(!(a * b).is_equal(&(b * a), T));
}
#[test]
fn composition_is_associative() {
let a = Transform::rotation(Axis::X, 0.3);
let b = Transform::scaling(Point::new(1.0, 0.0, 0.0), 2.0, T).unwrap();
let c = Transform::translation(Vector::new(0.0, 5.0, 0.0));
assert!(((a * b) * c).is_equal(&(a * (b * c)), T));
}
#[test]
fn vectors_ignore_translation_and_directions_stay_unit() {
let t = Transform::translation(Vector::new(100.0, 0.0, 0.0))
* Transform::rotation(Axis::Z, 0.9);
let v = Vector::new(1.0, 2.0, 3.0);
assert!(
t.apply_vector(v)
.is_equal(Transform::rotation(Axis::Z, 0.9).apply_vector(v), T)
);
let d = t.apply_direction(Direction::X, T).unwrap();
assert_relative_eq!(d.vector().magnitude(), 1.0, epsilon = 1e-15);
}
#[test]
fn a_point_mirror_reverses_directions() {
let t = Transform::point_mirror(Point::new(5.0, 5.0, 5.0));
assert!(
t.apply_direction(Direction::X, T)
.unwrap()
.is_equal(-Direction::X, T)
);
let s = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap();
assert!(
s.apply_direction(Direction::X, T)
.unwrap()
.is_equal(Direction::X, T)
);
}
#[test]
fn frame_transforms_round_trip_through_world() {
let f = Frame::new(
Point::new(1.0, 2.0, 3.0),
Direction::from_coords(1.0, 1.0, 0.0, T).unwrap(),
Direction::Z,
T,
)
.unwrap();
let to = Transform::to_frame(&f);
let from = Transform::from_frame(&f);
for p in sample_points() {
assert!(from.apply(to.apply(p)).is_equal(p, T));
assert!(to.apply(p).is_equal(f.to_local(p), T));
assert!(from.apply(f.to_local(p)).is_equal(p, T));
}
}
#[test]
fn between_frames_composes_correctly() {
let a = Frame::new(Point::new(1.0, 0.0, 0.0), Direction::Z, Direction::X, T).unwrap();
let b = Frame::new(Point::new(0.0, 5.0, 0.0), Direction::X, Direction::Y, T).unwrap();
let t = Transform::between_frames(&a, &b);
let local = Point::new(1.0, 2.0, 3.0);
assert!(b.to_world(t.apply(local)).is_equal(a.to_world(local), T));
}
#[test]
fn general_transform_normals_use_the_inverse_transpose() {
let g = GeneralTransform::scaling_xyz(2.0, 1.0, 1.0);
let n = Vector::new(1.0, 0.0, -1.0);
let transformed = g.apply_normal(n).unwrap();
let on_plane = Vector::new(1.0, 0.0, 1.0);
assert_relative_eq!(n.dot(on_plane), 0.0, epsilon = 1e-15);
assert_relative_eq!(
transformed.dot(g.apply_vector(on_plane)),
0.0,
epsilon = 1e-14,
max_relative = 1e-14
);
assert!(g.apply_vector(n).dot(g.apply_vector(on_plane)).abs() > 1e-6);
}
#[test]
fn general_transform_recognizes_similarities() {
let similar: GeneralTransform = Transform::rotation(Axis::Z, 0.4).into();
assert!(similar.is_similarity(1e-12));
let narrowed = similar.to_similarity(1e-12).unwrap();
assert_eq!(narrowed.kind(), TransformKind::Rotation);
let scaled: GeneralTransform = Transform::scaling(Point::ORIGIN, 3.0, T).unwrap().into();
assert!(scaled.is_similarity(1e-12));
assert_relative_eq!(
scaled.to_similarity(1e-12).unwrap().scale_factor(),
3.0,
epsilon = 1e-12
);
assert!(!GeneralTransform::scaling_xyz(1.0, 2.0, 3.0).is_similarity(1e-12));
let shear = GeneralTransform::new(
Matrix3::new([[1.0, 0.5, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
Vector::ZERO,
);
assert!(!shear.is_similarity(1e-12));
}
#[test]
fn general_transform_volume_ratio_and_inverse() {
let g = GeneralTransform::scaling_xyz(2.0, 3.0, 4.0);
assert_relative_eq!(g.volume_ratio(), 24.0);
assert!(g.preserves_handedness());
let inv = g.inverse().unwrap();
for p in sample_points() {
assert!(inv.apply(g.apply(p)).is_equal(p, T));
}
let flip = GeneralTransform::scaling_xyz(-1.0, 1.0, 1.0);
assert!(!flip.preserves_handedness());
assert!(
GeneralTransform::scaling_xyz(0.0, 1.0, 1.0)
.inverse()
.is_err()
);
}
#[test]
fn transform2_behaves_like_its_3d_counterpart() {
let r = Transform2::rotation(Point2::new(1.0, 1.0), core::f64::consts::FRAC_PI_2);
assert_eq!(r.kind(), TransformKind::Rotation);
assert!(
r.apply(Point2::new(1.0, 1.0))
.is_equal(Point2::new(1.0, 1.0), T)
);
assert!(
r.apply(Point2::new(2.0, 1.0))
.is_equal(Point2::new(1.0, 2.0), T)
);
assert!(
r.inverse()
.unwrap()
.apply(r.apply(Point2::ORIGIN))
.is_equal(Point2::ORIGIN, T)
);
let m = Transform2::line_mirror(Point2::ORIGIN, Direction2::Y);
assert_eq!(m.kind(), TransformKind::PlaneMirror);
assert!(!m.preserves_handedness());
assert!(
m.apply(Point2::new(3.0, 2.0))
.is_equal(Point2::new(3.0, -2.0), T)
);
assert!(Transform2::scaling(Point2::ORIGIN, 0.0, T).is_err());
}
}