use nalgebra::Vector3;
#[derive(Debug, Clone, PartialEq)]
pub struct Ray {
pub origin: Vector3<f32>,
pub direction: Vector3<f32>,
}
impl Ray {
pub fn new(origin: Vector3<f32>, direction: Vector3<f32>) -> Self {
Self {
origin,
direction: direction.normalize(),
}
}
pub fn point_at(&self, t: f32) -> Vector3<f32> {
self.origin + t * self.direction
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Intersection {
pub distance: f32,
pub point: Vector3<f32>,
}
pub fn ray_sphere_intersection(
ray: &Ray,
center: Vector3<f32>,
radius: f32,
) -> Option<Intersection> {
let oc = ray.origin - center;
let a = ray.direction.dot(&ray.direction);
let b = 2.0 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let sqrt_discriminant = discriminant.sqrt();
let t1 = (-b - sqrt_discriminant) / (2.0 * a);
let t2 = (-b + sqrt_discriminant) / (2.0 * a);
let t = if t1 >= 0.0 {
t1 } else if t2 >= 0.0 {
t2 } else {
return None; };
Some(Intersection {
distance: t,
point: ray.point_at(t),
})
}
#[derive(Debug, Clone)]
pub struct NodeHitTest {
pub id: String,
pub position: Vector3<f32>,
pub radius: f32,
}
pub fn find_closest_node_hit(ray: &Ray, nodes: &[NodeHitTest]) -> Option<(String, f32)> {
let mut closest_hit: Option<(String, f32)> = None;
for node in nodes {
if let Some(intersection) = ray_sphere_intersection(ray, node.position, node.radius) {
match closest_hit {
Some((_, current_distance)) => {
if intersection.distance < current_distance {
closest_hit = Some((node.id.clone(), intersection.distance));
}
}
None => {
closest_hit = Some((node.id.clone(), intersection.distance));
}
}
}
}
closest_hit
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ray_creation() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
assert_eq!(ray.origin, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(ray.direction, Vector3::new(1.0, 0.0, 0.0));
}
#[test]
fn test_ray_normalization() {
let ray = Ray::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(3.0, 4.0, 0.0), );
assert!((ray.direction.norm() - 1.0).abs() < 1e-6);
assert_eq!(ray.direction, Vector3::new(0.6, 0.8, 0.0));
}
#[test]
fn test_ray_point_at() {
let ray = Ray::new(Vector3::new(1.0, 2.0, 3.0), Vector3::new(1.0, 0.0, 0.0));
let point = ray.point_at(5.0);
assert_eq!(point, Vector3::new(6.0, 2.0, 3.0));
}
#[test]
fn test_ray_hits_sphere() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let center = Vector3::new(5.0, 0.0, 0.0);
let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_some());
let intersection = result.unwrap();
assert!((intersection.distance - 4.0).abs() < 1e-6);
assert!((intersection.point - Vector3::new(4.0, 0.0, 0.0)).norm() < 1e-6);
}
#[test]
fn test_ray_misses_sphere() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let center = Vector3::new(5.0, 5.0, 0.0); let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_none());
}
#[test]
fn test_ray_tangent_to_sphere() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let center = Vector3::new(5.0, 1.0, 0.0); let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_some());
let intersection = result.unwrap();
assert!((intersection.distance - 5.0).abs() < 1e-5);
}
#[test]
fn test_ray_origin_inside_sphere() {
let ray = Ray::new(
Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0),
);
let center = Vector3::new(0.0, 0.0, 0.0);
let radius = 2.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_some());
let intersection = result.unwrap();
assert!((intersection.distance - 2.0).abs() < 1e-6);
}
#[test]
fn test_ray_behind_sphere() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let center = Vector3::new(-5.0, 0.0, 0.0); let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_none());
}
#[test]
fn test_find_closest_node_single_hit() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let nodes = vec![NodeHitTest {
id: "node1".to_string(),
position: Vector3::new(5.0, 0.0, 0.0),
radius: 1.0,
}];
let result = find_closest_node_hit(&ray, &nodes);
assert!(result.is_some());
let (id, distance) = result.unwrap();
assert_eq!(id, "node1");
assert!((distance - 4.0).abs() < 1e-6);
}
#[test]
fn test_find_closest_node_multiple_hits() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let nodes = vec![
NodeHitTest {
id: "node1".to_string(),
position: Vector3::new(10.0, 0.0, 0.0),
radius: 1.0,
},
NodeHitTest {
id: "node2".to_string(),
position: Vector3::new(5.0, 0.0, 0.0), radius: 1.0,
},
NodeHitTest {
id: "node3".to_string(),
position: Vector3::new(15.0, 0.0, 0.0),
radius: 1.0,
},
];
let result = find_closest_node_hit(&ray, &nodes);
assert!(result.is_some());
let (id, _) = result.unwrap();
assert_eq!(id, "node2"); }
#[test]
fn test_find_closest_node_no_hits() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let nodes = vec![NodeHitTest {
id: "node1".to_string(),
position: Vector3::new(5.0, 10.0, 0.0), radius: 1.0,
}];
let result = find_closest_node_hit(&ray, &nodes);
assert!(result.is_none());
}
#[test]
fn test_performance_many_nodes() {
let ray = Ray::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0));
let mut nodes = Vec::new();
for i in 0..1000 {
let x = (i as f32) * 0.1;
let y = ((i / 10) as f32) * 0.1;
nodes.push(NodeHitTest {
id: format!("node{}", i),
position: Vector3::new(x, y, 0.0),
radius: 0.05,
});
}
let start = std::time::Instant::now();
let result = find_closest_node_hit(&ray, &nodes);
let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 10); assert!(result.is_some());
}
#[test]
fn test_ray_diagonal_intersection() {
let ray = Ray::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0), );
let center = Vector3::new(5.0, 5.0, 0.0);
let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_some());
}
#[test]
fn test_ray_3d_intersection() {
let ray = Ray::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 1.0), );
let center = Vector3::new(5.0, 5.0, 5.0);
let radius = 1.0;
let result = ray_sphere_intersection(&ray, center, radius);
assert!(result.is_some());
}
}