Repository: bitshifter/glam-rs (rust)

Issue:
lerp over scalar types?
Hello!

I'm using glam via bevy. Ran into a situation where I need to lerp over a single f32 value i.e. non-vector. I couldn't find a function for it in glam, so I would either need use Vec2::lerp and de-structure the single value out of it, or write my own boilerplate code.

Is this even in the scope of this project? Or should this be implemented as utils in bevy?



Pull request description:
Add trait for extending Floating-Point Types with additional methods
Closes #319

Patch:
diff --git a/codegen/src/outputs.rs b/codegen/src/outputs.rs
index e77dd58..1c8ae8e 100644
--- a/codegen/src/outputs.rs
+++ b/codegen/src/outputs.rs
@@ -138,6 +138,12 @@ impl ContextBuilder {
             .with_dimension(dim)
     }
 
+    pub fn new_float(scalar_t: &str) -> Self {
+        ContextBuilder::new()
+            .with_template("float.rs.tera")
+            .with_scalar_t(scalar_t)
+    }
+
     pub fn new_bvec2() -> Self {
         Self::new_bvecn(2, "bool")
     }
@@ -622,5 +628,7 @@ pub fn build_output_pairs() -> HashMap<&'static str, tera::Context> {
         ),
         ("src/f64/dmat3.rs", ContextBuilder::new_dmat3().build()),
         ("src/f64/dmat4.rs", ContextBuilder::new_dmat4().build()),
+        ("src/f32/float.rs", ContextBuilder::new_float("f32").build()),
+        ("src/f64/float.rs", ContextBuilder::new_float("f64").build()),
     ])
 }
diff --git a/codegen/templates/float.rs.tera b/codegen/templates/float.rs.tera
new file mode 100644
index 0000000..98e029d
--- /dev/null
+++ b/codegen/templates/float.rs.tera
@@ -0,0 +1,21 @@
+// Generated from {{template_path}} template. Edit the template, not the generated file.
+
+use crate::float::FloatExt;
+
+impl FloatExt for {{ scalar_t }} {
+    #[inline]
+    fn lerp(self, rhs: {{ scalar_t }}, t: {{ scalar_t }}) -> {{ scalar_t }} {
+        self + (rhs - self) * t
+    }
+
+    #[inline]
+    fn inverse_lerp(a: {{ scalar_t }}, b: {{ scalar_t }}, v: {{ scalar_t }}) -> {{ scalar_t }} {
+        (v - a) / (b - a)
+    }
+
+    #[inline]
+    fn remap(self, in_start: {{ scalar_t }}, in_end: {{ scalar_t }}, out_start: {{ scalar_t }}, out_end: {{ scalar_t }}) -> {{ scalar_t }} {
+        let t = {{ scalar_t }}::inverse_lerp(in_start, in_end, self);
+        {{ scalar_t }}::lerp(out_start, out_end, t)
+    }
+}
diff --git a/codegen/templates/vec.rs.tera b/codegen/templates/vec.rs.tera
index 63363f1..65d9384 100644
--- a/codegen/templates/vec.rs.tera
+++ b/codegen/templates/vec.rs.tera
@@ -1487,6 +1487,14 @@ impl {{ self_t }} {
         }
     }
 
+    /// Returns the angle (in radians) of this vector in the range `[-π, +π]`.
+    ///
+    /// The input does not need to be a unit vector however it must be non-zero.
+    #[inline]
+    pub fn to_angle(self) -> {{ scalar_t }} {
+        math::atan2(self.y, self.x)
+    }
+
     /// Returns the angle (in radians) between `self` and `rhs` in the range `[-π, +π]`.
     ///
     /// The inputs do not need to be unit vectors however they must be non-zero.
diff --git a/src/f32.rs b/src/f32.rs
index 58fc417..3a53381 100644
--- a/src/f32.rs
+++ b/src/f32.rs
@@ -1,5 +1,6 @@
 mod affine2;
 mod affine3a;
+mod float;
 mod mat3;
 pub(crate) mod math;
 mod vec2;
diff --git a/src/f32/float.rs b/src/f32/float.rs
new file mode 100644
index 0000000..290552c
--- /dev/null
+++ b/src/f32/float.rs
@@ -0,0 +1,21 @@
+// Generated from float.rs.tera template. Edit the template, not the generated file.
+
+use crate::float::FloatExt;
+
+impl FloatExt for f32 {
+    #[inline]
+    fn lerp(self, rhs: f32, t: f32) -> f32 {
+        self + (rhs - self) * t
+    }
+
+    #[inline]
+    fn inverse_lerp(a: f32, b: f32, v: f32) -> f32 {
+        (v - a) / (b - a)
+    }
+
+    #[inline]
+    fn remap(self, in_start: f32, in_end: f32, out_start: f32, out_end: f32) -> f32 {
+        let t = f32::inverse_lerp(in_start, in_end, self);
+        f32::lerp(out_start, out_end, t)
+    }
+}
diff --git a/src/f32/vec2.rs b/src/f32/vec2.rs
index 7512f38..80a1d35 100644
--- a/src/f32/vec2.rs
+++ b/src/f32/vec2.rs
@@ -648,6 +648,14 @@ impl Vec2 {
         Self { x: cos, y: sin }
     }
 
+    /// Returns the angle (in radians) of this vector in the range `[-π, +π]`.
+    ///
+    /// The input does not need to be a unit vector however it must be non-zero.
+    #[inline]
+    pub fn to_angle(self) -> f32 {
+        math::atan2(self.y, self.x)
+    }
+
     /// Returns the angle (in radians) between `self` and `rhs` in the range `[-π, +π]`.
     ///
     /// The inputs do not need to be unit vectors however they must be non-zero.
diff --git a/src/f64.rs b/src/f64.rs
index 3d05ede..3839d3e 100644
--- a/src/f64.rs
+++ b/src/f64.rs
@@ -7,6 +7,7 @@ mod dquat;
 mod dvec2;
 mod dvec3;
 mod dvec4;
+mod float;
 pub(crate) mod math;
 
 pub use daffine2::DAffine2;
diff --git a/src/f64/dvec2.rs b/src/f64/dvec2.rs
index 7356627..fa692ba 100644
--- a/src/f64/dvec2.rs
+++ b/src/f64/dvec2.rs
@@ -648,6 +648,14 @@ impl DVec2 {
         Self { x: cos, y: sin }
     }
 
+    /// Returns the angle (in radians) of this vector in the range `[-π, +π]`.
+    ///
+    /// The input does not need to be a unit vector however it must be non-zero.
+    #[inline]
+    pub fn to_angle(self) -> f64 {
+        math::atan2(self.y, self.x)
+    }
+
     /// Returns the angle (in radians) between `self` and `rhs` in the range `[-π, +π]`.
     ///
     /// The inputs do not need to be unit vectors however they must be non-zero.
diff --git a/src/f64/float.rs b/src/f64/float.rs
new file mode 100644
index 0000000..219c8b0
--- /dev/null
+++ b/src/f64/float.rs
@@ -0,0 +1,21 @@
+// Generated from float.rs.tera template. Edit the template, not the generated file.
+
+use crate::float::FloatExt;
+
+impl FloatExt for f64 {
+    #[inline]
+    fn lerp(self, rhs: f64, t: f64) -> f64 {
+        self + (rhs - self) * t
+    }
+
+    #[inline]
+    fn inverse_lerp(a: f64, b: f64, v: f64) -> f64 {
+        (v - a) / (b - a)
+    }
+
+    #[inline]
+    fn remap(self, in_start: f64, in_end: f64, out_start: f64, out_end: f64) -> f64 {
+        let t = f64::inverse_lerp(in_start, in_end, self);
+        f64::lerp(out_start, out_end, t)
+    }
+}
diff --git a/src/float.rs b/src/float.rs
new file mode 100644
index 0000000..5366585
--- /dev/null
+++ b/src/float.rs
@@ -0,0 +1,28 @@
+/// A trait for extending [`prim@f32`] and [`prim@f64`] with extra methods.
+pub trait FloatExt {
+    /// Performs a linear interpolation between `self` and `rhs` based on the value `t`.
+    ///
+    /// When `t` is `0`, the result will be `self`.  When `t` is `1`, the result
+    /// will be `rhs`. When `t` is outside of the range `[0, 1]`, the result is linearly
+    /// extrapolated.
+    fn lerp(self, rhs: Self, s: Self) -> Self;
+
+    /// Returns `v` normalized to the range `[a, b]`.
+    ///
+    /// When `v` is equal to `a` the result will be `0`.  When `v` is equal to `b` will be `1`.
+    ///
+    /// When `v` is outside of the range `[a, b]`, the result is linearly extrapolated.
+    ///
+    /// `a` and `b` must not be equal, otherwise the result will be either infinite or `NAN`.
+    fn inverse_lerp(a: Self, b: Self, v: Self) -> Self;
+
+    /// Remap `self` from the input range to the output range.
+    ///
+    /// When `self` is equal to `in_start` this returns `out_start`.
+    /// When `self` is equal to `in_end` this returns `out_end`.
+    ///
+    /// When `self` is outside of the range `[in_start, in_end]`, the result is linearly extrapolated.
+    ///
+    /// `in_start` and `in_end` must not be equal, otherwise the result will be either infinite or `NAN`.
+    fn remap(self, in_start: Self, in_end: Self, out_start: Self, out_end: Self) -> Self;
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3c31bbe..c556665 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -329,3 +329,7 @@ pub use self::swizzles::{Vec2Swizzles, Vec3Swizzles, Vec4Swizzles};
 
 /** Rotation Helper */
 pub use euler::EulerRot;
+
+/** A trait for extending [`prim@f32`] and [`prim@f64`] with extra methods. */
+mod float;
+pub use float::FloatExt;


Test patch:
diff --git a/tests/float.rs b/tests/float.rs
new file mode 100644
index 0000000..f19f9c7
--- /dev/null
+++ b/tests/float.rs
@@ -0,0 +1,48 @@
+#[macro_use]
+mod support;
+
+macro_rules! impl_float_tests {
+    ($t:ident) => {
+        glam_test!(test_lerp, {
+            let a = 0.;
+            let b = 10.;
+            assert_eq!($t::lerp(a, b, 0.), a);
+            assert_eq!($t::lerp(a, b, 0.5), 5.);
+            assert_eq!($t::lerp(a, b, 1.), b);
+            assert_eq!($t::lerp(a, a, 0.), a);
+            assert_eq!($t::lerp(a, a, 1.), a);
+        });
+
+        glam_test!(test_inverse_lerp, {
+            let a = 0.;
+            let b = 10.;
+            assert_eq!($t::inverse_lerp(a, b, 0.), 0.);
+            assert_eq!($t::inverse_lerp(a, b, 5.), 0.5);
+            assert_eq!($t::inverse_lerp(a, b, 10.), 1.);
+            assert_eq!($t::inverse_lerp(a, b, 15.), 1.5);
+            assert!($t::inverse_lerp(a, a, 0.).is_nan());
+            assert!($t::inverse_lerp(a, a, 1.).is_infinite());
+        });
+
+        glam_test!(test_remap, {
+            assert_eq!($t::remap(0., 0., 2., 0., 20.), 0.);
+            assert_eq!($t::remap(1., 0., 2., 0., 20.), 10.);
+            assert_eq!($t::remap(2., 0., 2., 0., 20.), 20.);
+            assert_eq!($t::remap(-5., -10., 30., 60., 20.), 55.);
+            assert!($t::remap(0., 0., 0., 0., 1.).is_nan());
+            assert!($t::remap(1., 0., 0., 0., 1.).is_infinite());
+        });
+    };
+}
+
+mod float32 {
+    use glam::FloatExt;
+
+    impl_float_tests!(f32);
+}
+
+mod float64 {
+    use glam::FloatExt;
+
+    impl_float_tests!(f64);
+}
diff --git a/tests/vec2.rs b/tests/vec2.rs
index a0c215b..7557aae 100644
--- a/tests/vec2.rs
+++ b/tests/vec2.rs
@@ -896,20 +896,27 @@ macro_rules! impl_vec2_float_tests {
             );
         });
 
-        glam_test!(test_from_angle, {
-            assert_approx_eq!($vec2::from_angle(0.0), $vec2::new(1.0, 0.0));
-            assert_approx_eq!(
-                $vec2::from_angle(core::$t::consts::FRAC_PI_2),
-                $vec2::new(0.0, 1.0)
-            );
-            assert_approx_eq!(
-                $vec2::from_angle(core::$t::consts::PI),
-                $vec2::new(-1.0, 0.0)
-            );
-            assert_approx_eq!(
-                $vec2::from_angle(-core::$t::consts::FRAC_PI_2),
-                $vec2::new(0.0, -1.0)
-            );
+        glam_test!(test_angle_conversion, {
+            let angle = 0.;
+            let vec = $vec2::from_angle(angle);
+            assert_approx_eq!(vec, $vec2::new(1.0, 0.0));
+            assert_approx_eq!(vec.to_angle(), angle);
+
+            let angle = core::$t::consts::FRAC_PI_2;
+            let vec = $vec2::from_angle(angle);
+            assert_approx_eq!(vec, $vec2::new(0.0, 1.0));
+            assert_approx_eq!(vec.to_angle(), angle);
+
+            let angle = core::$t::consts::PI;
+            let vec = $vec2::from_angle(angle);
+            assert_approx_eq!(vec, $vec2::new(-1.0, 0.0));
+            // The sign of the angle PI gets flipped and is slightly less precise but correct
+            assert_approx_eq!(vec.to_angle().abs(), angle, 1e-6);
+
+            let angle = -core::$t::consts::FRAC_PI_2;
+            let vec = $vec2::from_angle(angle);
+            assert_approx_eq!(vec, $vec2::new(0.0, -1.0));
+            assert_approx_eq!(vec.to_angle(), angle);
         });
     };
 }