use core::fmt;
#[cfg(feature = "nalgebra-0_32")]
use nalgebra_0_32 as na;
#[cfg(feature = "nalgebra-0_33")]
use nalgebra_0_33 as na;
#[cfg(feature = "nalgebra-0_34")]
use nalgebra_0_34 as na;
use crate::ops::Transform;
use crate::scalar::Float;
use super::{Motor, Point};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NalgebraConversionError {
IdealPoint,
}
impl fmt::Display for NalgebraConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IdealPoint => write!(f, "point is ideal (at infinity)"),
}
}
}
impl std::error::Error for NalgebraConversionError {}
impl<T: Float + na::Scalar> From<na::Point3<T>> for Point<T> {
#[inline]
fn from(p: na::Point3<T>) -> Self {
Point::from_cartesian(p.x, p.y, p.z)
}
}
impl<T: Float + na::Scalar> TryFrom<Point<T>> for na::Point3<T> {
type Error = NalgebraConversionError;
fn try_from(p: Point<T>) -> Result<Self, Self::Error> {
if p.w().abs() < T::epsilon() {
return Err(NalgebraConversionError::IdealPoint);
}
Ok(na::Point3::new(
p.cartesian_x(),
p.cartesian_y(),
p.cartesian_z(),
))
}
}
impl<T: Float + na::RealField> From<Motor<T>> for na::Isometry3<T> {
fn from(motor: Motor<T>) -> Self {
let rot_norm_sq = motor.tx() * motor.tx()
+ motor.ry() * motor.ry()
+ motor.rz() * motor.rz()
+ motor.ps() * motor.ps();
let rot_norm = num_traits::Float::sqrt(rot_norm_sq);
let rotation = if rot_norm > T::epsilon() {
na::UnitQuaternion::from_quaternion(na::Quaternion::new(
motor.ps() / rot_norm,
-motor.tx() / rot_norm,
-motor.ry() / rot_norm,
-motor.rz() / rot_norm,
))
} else {
na::UnitQuaternion::identity()
};
let origin = Point::origin();
let transformed = motor.transform(&origin);
let translation = na::Translation3::new(
transformed.cartesian_x(),
transformed.cartesian_y(),
transformed.cartesian_z(),
);
na::Isometry3::from_parts(translation, rotation)
}
}
impl<T: Float + na::RealField> From<na::Isometry3<T>> for Motor<T> {
fn from(iso: na::Isometry3<T>) -> Self {
use crate::ops::Versor;
let q = iso.rotation.quaternion();
let t = iso.translation.vector;
let rotor = Motor::new_unchecked(
T::zero(), T::zero(), T::zero(), -q.i, T::zero(), -q.j, -q.k, q.w, );
let translator = Motor::from_translation(t.x, t.y, t.z);
#[cfg(test)]
{
eprintln!(
" Rotor: s={}, e23={}, e31={}, e12={}, e01={}, e02={}, e03={}, e0123={}",
rotor.s(),
rotor.rx(),
rotor.ty(),
rotor.tz(),
rotor.tx(),
rotor.ry(),
rotor.rz(),
rotor.ps()
);
eprintln!(
" Translator: s={}, e23={}, e31={}, e12={}, e01={}, e02={}, e03={}, e0123={}",
translator.s(),
translator.rx(),
translator.ty(),
translator.tz(),
translator.tx(),
translator.ry(),
translator.rz(),
translator.ps()
);
}
translator.compose(&rotor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specialized::projective::dim3::{Flector, Plane, UnitizedPoint};
use crate::test_utils::RELATIVE_EQ_EPS;
use crate::wrappers::Unitized;
use approx::relative_eq;
use proptest::prelude::*;
const EPS: f64 = RELATIVE_EQ_EPS;
proptest! {
#[test]
fn point_roundtrip(p in any::<UnitizedPoint<f64>>()) {
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let back: Point<f64> = na_p.into();
prop_assert!(relative_eq!(p.as_inner().cartesian_x(), back.x(), epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(p.as_inner().cartesian_y(), back.y(), epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(p.as_inner().cartesian_z(), back.z(), epsilon = EPS, max_relative = EPS));
}
#[test]
fn point_try_from_finite(p in any::<UnitizedPoint<f64>>()) {
let result: Result<na::Point3<f64>, _> = (*p.as_inner()).try_into();
prop_assert!(result.is_ok());
let na_p = result.unwrap();
prop_assert!(relative_eq!(p.as_inner().cartesian_x(), na_p.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(p.as_inner().cartesian_y(), na_p.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(p.as_inner().cartesian_z(), na_p.z, epsilon = EPS, max_relative = EPS));
}
}
#[test]
fn point_try_from_ideal_fails() {
let ideal = Point::<f64>::ideal(1.0, 0.0, 0.0);
let result: Result<na::Point3<f64>, _> = ideal.try_into();
assert!(matches!(result, Err(NalgebraConversionError::IdealPoint)));
}
proptest! {
#[test]
fn motor_roundtrip(m in any::<Unitized<Motor<f64>>>()) {
let iso: na::Isometry3<f64> = (*m.as_inner()).into();
let back: Motor<f64> = iso.into();
eprintln!("Original motor: s={}, e23={}, e31={}, e12={}, e01={}, e02={}, e03={}, e0123={}",
m.as_inner().s(), m.as_inner().rx(), m.as_inner().ty(), m.as_inner().tz(), m.as_inner().tx(), m.as_inner().ry(), m.as_inner().rz(), m.as_inner().ps());
eprintln!("Isometry: rotation={:?}, translation={:?}",
iso.rotation.quaternion(), iso.translation.vector);
eprintln!("Back motor: s={}, e23={}, e31={}, e12={}, e01={}, e02={}, e03={}, e0123={}",
back.s(), back.rx(), back.ty(), back.tz(), back.tx(), back.ry(), back.rz(), back.ps());
let test_points = [
Point::from_cartesian(1.0, 0.0, 0.0),
Point::from_cartesian(0.0, 1.0, 0.0),
Point::from_cartesian(0.0, 0.0, 1.0),
Point::from_cartesian(1.0, 2.0, 3.0),
];
for test_p in test_points {
let result1 = m.as_inner().transform(&test_p);
let result2 = back.transform(&test_p);
prop_assert!(
relative_eq!(result1.x(), result2.x(), epsilon = EPS, max_relative = EPS),
"x mismatch: {} vs {}", result1.x(), result2.x()
);
prop_assert!(
relative_eq!(result1.y(), result2.y(), epsilon = EPS, max_relative = EPS),
"y mismatch: {} vs {}", result1.y(), result2.y()
);
prop_assert!(
relative_eq!(result1.z(), result2.z(), epsilon = EPS, max_relative = EPS),
"z mismatch: {} vs {}", result1.z(), result2.z()
);
}
}
}
proptest! {
#[test]
fn transform_point_equivalence(
m in any::<Unitized<Motor<f64>>>(),
p in any::<UnitizedPoint<f64>>(),
) {
let result_ga = m.as_inner().transform(p.as_inner());
let iso: na::Isometry3<f64> = (*m.as_inner()).into();
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(
relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS),
"x mismatch: GA={} vs NA={}", result_ga.cartesian_x(), na_result.x
);
prop_assert!(
relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS),
"y mismatch: GA={} vs NA={}", result_ga.cartesian_y(), na_result.y
);
prop_assert!(
relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS),
"z mismatch: GA={} vs NA={}", result_ga.cartesian_z(), na_result.z
);
}
#[test]
fn inverse_equivalence(
m in any::<Unitized<Motor<f64>>>(),
p in any::<UnitizedPoint<f64>>(),
) {
use crate::ops::VersorInverse;
let inv_ga = m.as_inner().inverse();
let result_ga = inv_ga.transform(p.as_inner());
let iso: na::Isometry3<f64> = (*m.as_inner()).into();
let inv_na = iso.inverse();
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = inv_na.transform_point(&na_p);
prop_assert!(
relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS),
"x mismatch: GA={} vs NA={}", result_ga.cartesian_x(), na_result.x
);
prop_assert!(
relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS),
"y mismatch: GA={} vs NA={}", result_ga.cartesian_y(), na_result.y
);
prop_assert!(
relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS),
"z mismatch: GA={} vs NA={}", result_ga.cartesian_z(), na_result.z
);
}
}
proptest! {
#[test]
fn translation_factory_equivalence(
tx in -100.0f64..100.0,
ty in -100.0f64..100.0,
tz in -100.0f64..100.0,
p in any::<UnitizedPoint<f64>>(),
) {
let motor = Motor::from_translation(tx, ty, tz);
let iso = na::Isometry3::from_parts(
na::Translation3::new(tx, ty, tz),
na::UnitQuaternion::identity(),
);
let result_ga = motor.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS));
}
#[test]
fn rotation_x_factory_equivalence(
angle in -std::f64::consts::PI..std::f64::consts::PI,
p in any::<UnitizedPoint<f64>>(),
) {
let motor = Motor::from_rotation_x(angle);
let iso = na::Isometry3::from_parts(
na::Translation3::identity(),
na::UnitQuaternion::from_axis_angle(&na::Vector3::x_axis(), angle),
);
let result_ga = motor.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS));
}
#[test]
fn rotation_y_factory_equivalence(
angle in -std::f64::consts::PI..std::f64::consts::PI,
p in any::<UnitizedPoint<f64>>(),
) {
let motor = Motor::from_rotation_y(angle);
let iso = na::Isometry3::from_parts(
na::Translation3::identity(),
na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), angle),
);
let result_ga = motor.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS));
}
#[test]
fn rotation_z_factory_equivalence(
angle in -std::f64::consts::PI..std::f64::consts::PI,
p in any::<UnitizedPoint<f64>>(),
) {
let motor = Motor::from_rotation_z(angle);
let iso = na::Isometry3::from_parts(
na::Translation3::identity(),
na::UnitQuaternion::from_axis_angle(&na::Vector3::z_axis(), angle),
);
let result_ga = motor.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS));
}
#[test]
fn axis_angle_factory_equivalence(
ax in -1.0f64..1.0,
ay in -1.0f64..1.0,
az in -1.0f64..1.0,
angle in -std::f64::consts::PI..std::f64::consts::PI,
p in any::<UnitizedPoint<f64>>(),
) {
let axis_len = (ax*ax + ay*ay + az*az).sqrt();
prop_assume!(axis_len > 0.1);
let axis = crate::specialized::euclidean::dim3::Vector::new(ax, ay, az);
let motor = Motor::from_axis_angle(&axis, angle);
let na_axis = na::Unit::new_normalize(na::Vector3::new(ax, ay, az));
let iso = na::Isometry3::from_parts(
na::Translation3::identity(),
na::UnitQuaternion::from_axis_angle(&na_axis, angle),
);
let result_ga = motor.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_result = iso.transform_point(&na_p);
prop_assert!(relative_eq!(result_ga.cartesian_x(), na_result.x, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_y(), na_result.y, epsilon = EPS, max_relative = EPS));
prop_assert!(relative_eq!(result_ga.cartesian_z(), na_result.z, epsilon = EPS, max_relative = EPS));
}
}
#[test]
fn identity_motor_equivalence() {
let motor = Motor::<f64>::identity();
let iso: na::Isometry3<f64> = motor.into();
let p = na::Point3::new(1.0, 2.0, 3.0);
let result = iso.transform_point(&p);
assert!(relative_eq!(
result.x,
p.x,
epsilon = EPS,
max_relative = EPS
));
assert!(relative_eq!(
result.y,
p.y,
epsilon = EPS,
max_relative = EPS
));
assert!(relative_eq!(
result.z,
p.z,
epsilon = EPS,
max_relative = EPS
));
}
fn reflect_point_through_origin_plane(
point: &na::Point3<f64>,
normal: &na::Vector3<f64>,
) -> na::Point3<f64> {
let n_unit = normal.normalize();
let proj = point.coords.dot(&n_unit);
na::Point3::from(point.coords - n_unit * (2.0 * proj))
}
proptest! {
#[test]
fn flector_reflection_through_origin(
nx in -1.0f64..1.0,
ny in -1.0f64..1.0,
nz in -1.0f64..1.0,
p in any::<UnitizedPoint<f64>>(),
) {
let norm_len = (nx*nx + ny*ny + nz*nz).sqrt();
prop_assume!(norm_len > 0.1);
let flector = Flector::from_plane_through_origin(nx, ny, nz);
let result_ga = flector.transform(p.as_inner());
let na_p: na::Point3<f64> = (*p.as_inner()).try_into().unwrap();
let na_normal = na::Vector3::new(nx, ny, nz);
let result_na = reflect_point_through_origin_plane(&na_p, &na_normal);
prop_assert!(
relative_eq!(result_ga.cartesian_x(), result_na.x, epsilon = EPS, max_relative = EPS),
"x mismatch: GA={} vs manual={}", result_ga.cartesian_x(), result_na.x
);
prop_assert!(
relative_eq!(result_ga.cartesian_y(), result_na.y, epsilon = EPS, max_relative = EPS),
"y mismatch: GA={} vs manual={}", result_ga.cartesian_y(), result_na.y
);
prop_assert!(
relative_eq!(result_ga.cartesian_z(), result_na.z, epsilon = EPS, max_relative = EPS),
"z mismatch: GA={} vs manual={}", result_ga.cartesian_z(), result_na.z
);
}
#[test]
fn flector_double_reflection_identity(
nx in -1.0f64..1.0,
ny in -1.0f64..1.0,
nz in -1.0f64..1.0,
d in -10.0f64..10.0,
p in any::<UnitizedPoint<f64>>(),
) {
let norm_len = (nx*nx + ny*ny + nz*nz).sqrt();
prop_assume!(norm_len > 0.1);
let plane = Plane::from_normal_and_distance(nx, ny, nz, d);
let flector = Flector::from_plane(&plane);
let once = flector.transform(p.as_inner());
let twice = flector.transform(&once);
prop_assert!(
relative_eq!(p.as_inner().cartesian_x(), twice.cartesian_x(), epsilon = EPS, max_relative = EPS),
"x mismatch after double reflection: {} vs {}", p.as_inner().cartesian_x(), twice.cartesian_x()
);
prop_assert!(
relative_eq!(p.as_inner().cartesian_y(), twice.cartesian_y(), epsilon = EPS, max_relative = EPS),
"y mismatch after double reflection: {} vs {}", p.as_inner().cartesian_y(), twice.cartesian_y()
);
prop_assert!(
relative_eq!(p.as_inner().cartesian_z(), twice.cartesian_z(), epsilon = EPS, max_relative = EPS),
"z mismatch after double reflection: {} vs {}", p.as_inner().cartesian_z(), twice.cartesian_z()
);
}
}
#[test]
fn isometry_identity_to_motor() {
let iso = na::Isometry3::<f64>::identity();
let motor: Motor<f64> = iso.into();
println!(
"Converted motor: s={}, e23={}, e31={}, e12={}, e01={}, e02={}, e03={}, e0123={}",
motor.s(),
motor.rx(),
motor.ty(),
motor.tz(),
motor.tx(),
motor.ry(),
motor.rz(),
motor.ps()
);
let p = Point::from_cartesian(1.0, 2.0, 3.0);
let result = motor.transform(&p);
println!(
"Point ({}, {}, {}) -> ({}, {}, {})",
p.x(),
p.y(),
p.z(),
result.x(),
result.y(),
result.z()
);
assert!(relative_eq!(
result.x(),
p.x(),
epsilon = EPS,
max_relative = EPS
));
assert!(relative_eq!(
result.y(),
p.y(),
epsilon = EPS,
max_relative = EPS
));
assert!(relative_eq!(
result.z(),
p.z(),
epsilon = EPS,
max_relative = EPS
));
}
}