diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 6c0677ad55e..e12b958a9a5 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -650,6 +650,8 @@ impl Rect { /// /// A ray that starts inside the rect will return `true`. pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { + debug_assert!(d.is_normalized(), "expected normalized direction"); + let mut tmin = -f32::INFINITY; let mut tmax = f32::INFINITY; @@ -671,6 +673,32 @@ impl Rect { 0.0 <= tmax && tmin <= tmax } + + /// Where does a ray from the center intersect the rectangle? + /// + /// `d` is the direction of the ray and assumed to be normalized. + pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 { + debug_assert!(d.is_normalized(), "expected normalized direction"); + + let mut tmin = f32::NEG_INFINITY; + let mut tmax = f32::INFINITY; + + for i in 0..2 { + let inv_d = 1.0 / -d[i]; + let mut t0 = (self.min[i] - self.center()[i]) * inv_d; + let mut t1 = (self.max[i] - self.center()[i]) * inv_d; + + if inv_d < 0.0 { + std::mem::swap(&mut t0, &mut t1); + } + + tmin = tmin.max(t0); + tmax = tmax.min(t1); + } + + let t = tmax.min(tmin); + self.center() + t * -d + } } impl fmt::Debug for Rect { @@ -793,4 +821,57 @@ mod tests { println!("Leftward ray from right:"); assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT)); } + + #[test] + fn test_ray_from_center_intersection() { + let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0)); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::RIGHT), + pos2(3.0, 2.0), + "rightward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::UP), + pos2(2.0, 1.0), + "upward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::LEFT), + pos2(1.0, 2.0), + "leftward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::DOWN), + pos2(2.0, 3.0), + "downward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::LEFT + Vec2::DOWN).normalized()), + pos2(1.0, 3.0), + "bottom-left corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::LEFT + Vec2::UP).normalized()), + pos2(1.0, 1.0), + "top-left corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::DOWN).normalized()), + pos2(3.0, 3.0), + "bottom-right corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::UP).normalized()), + pos2(3.0, 1.0), + "top-right corner ray" + ); + } } diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 03e0715c20a..9a173348b03 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -176,6 +176,12 @@ impl Vec2 { } } + /// Checks if `self` has length `1.0` up to a precision of `1e-6`. + #[inline(always)] + pub fn is_normalized(self) -> bool { + (self.length_sq() - 1.0).abs() < 2e-6 + } + /// Rotates the vector by 90°, i.e positive X to positive Y /// (clockwise in egui coordinates). #[inline(always)] @@ -497,8 +503,10 @@ impl fmt::Display for Vec2 { } } -#[test] -fn test_vec2() { +#[cfg(test)] +mod test { + use super::*; + macro_rules! almost_eq { ($left: expr, $right: expr) => { let left = $left; @@ -506,32 +514,58 @@ fn test_vec2() { assert!((left - right).abs() < 1e-6, "{} != {}", left, right); }; } - use std::f32::consts::TAU; - assert_eq!(Vec2::ZERO.angle(), 0.0); - assert_eq!(Vec2::angled(0.0).angle(), 0.0); - assert_eq!(Vec2::angled(1.0).angle(), 1.0); - assert_eq!(Vec2::X.angle(), 0.0); - assert_eq!(Vec2::Y.angle(), 0.25 * TAU); + #[test] + fn test_vec2() { + use std::f32::consts::TAU; + + assert_eq!(Vec2::ZERO.angle(), 0.0); + assert_eq!(Vec2::angled(0.0).angle(), 0.0); + assert_eq!(Vec2::angled(1.0).angle(), 1.0); + assert_eq!(Vec2::X.angle(), 0.0); + assert_eq!(Vec2::Y.angle(), 0.25 * TAU); + + assert_eq!(Vec2::RIGHT.angle(), 0.0); + assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); + almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); + assert_eq!(Vec2::UP.angle(), -0.25 * TAU); - assert_eq!(Vec2::RIGHT.angle(), 0.0); - assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); - almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); - assert_eq!(Vec2::UP.angle(), -0.25 * TAU); + let mut assignment = vec2(1.0, 2.0); + assignment += vec2(3.0, 4.0); + assert_eq!(assignment, vec2(4.0, 6.0)); - let mut assignment = vec2(1.0, 2.0); - assignment += vec2(3.0, 4.0); - assert_eq!(assignment, vec2(4.0, 6.0)); + let mut assignment = vec2(4.0, 6.0); + assignment -= vec2(1.0, 2.0); + assert_eq!(assignment, vec2(3.0, 4.0)); - let mut assignment = vec2(4.0, 6.0); - assignment -= vec2(1.0, 2.0); - assert_eq!(assignment, vec2(3.0, 4.0)); + let mut assignment = vec2(1.0, 2.0); + assignment *= 2.0; + assert_eq!(assignment, vec2(2.0, 4.0)); - let mut assignment = vec2(1.0, 2.0); - assignment *= 2.0; - assert_eq!(assignment, vec2(2.0, 4.0)); + let mut assignment = vec2(2.0, 4.0); + assignment /= 2.0; + assert_eq!(assignment, vec2(1.0, 2.0)); + } + + #[test] + fn test_vec2_normalized() { + fn generate_spiral(n: usize, start: Vec2, end: Vec2) -> impl Iterator { + let angle_step = 2.0 * std::f32::consts::PI / n as f32; + let radius_step = (end.length() - start.length()) / n as f32; + + (0..n).map(move |i| { + let angle = i as f32 * angle_step; + let radius = start.length() + i as f32 * radius_step; + let x = radius * angle.cos(); + let y = radius * angle.sin(); + vec2(x, y) + }) + } - let mut assignment = vec2(2.0, 4.0); - assignment /= 2.0; - assert_eq!(assignment, vec2(1.0, 2.0)); + for v in generate_spiral(40, Vec2::splat(0.1), Vec2::splat(2.0)) { + let vn = v.normalized(); + almost_eq!(vn.length(), 1.0); + assert!(vn.is_normalized()); + } + } }