use std::f64::consts::{PI, TAU};
use crate::distance::angular_distance;
use crate::error::SphereQlError;
use crate::types::SphericalPoint;
pub trait Contains {
fn contains(&self, point: &SphericalPoint) -> bool;
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Cone {
pub apex: SphericalPoint,
pub axis: SphericalPoint,
pub half_angle: f64,
}
impl Cone {
pub fn new(
apex: SphericalPoint,
axis: SphericalPoint,
half_angle: f64,
) -> Result<Self, SphereQlError> {
if half_angle <= 0.0 || half_angle > PI {
return Err(SphereQlError::InvalidConeAngle(half_angle));
}
Ok(Self {
apex,
axis,
half_angle,
})
}
}
impl Contains for Cone {
fn contains(&self, point: &SphericalPoint) -> bool {
let point_unit = SphericalPoint::new_unchecked(1.0, point.theta, point.phi);
let axis_unit = SphericalPoint::new_unchecked(1.0, self.axis.theta, self.axis.phi);
angular_distance(&point_unit, &axis_unit) <= self.half_angle
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Cap {
pub center: SphericalPoint,
pub half_angle: f64,
}
impl Cap {
pub fn new(center: SphericalPoint, half_angle: f64) -> Result<Self, SphereQlError> {
if half_angle <= 0.0 || half_angle > PI {
return Err(SphereQlError::InvalidCapAngle(half_angle));
}
Ok(Self { center, half_angle })
}
}
impl Contains for Cap {
fn contains(&self, point: &SphericalPoint) -> bool {
let point_unit = SphericalPoint::new_unchecked(1.0, point.theta, point.phi);
let center_unit = SphericalPoint::new_unchecked(1.0, self.center.theta, self.center.phi);
angular_distance(&point_unit, ¢er_unit) <= self.half_angle
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Shell {
pub inner: f64,
pub outer: f64,
}
impl Shell {
pub fn new(inner: f64, outer: f64) -> Result<Self, SphereQlError> {
if inner < 0.0 || inner >= outer {
return Err(SphereQlError::InvalidShellBounds { inner, outer });
}
Ok(Self { inner, outer })
}
}
impl Contains for Shell {
fn contains(&self, point: &SphericalPoint) -> bool {
point.r >= self.inner && point.r <= self.outer
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Band {
pub phi_min: f64,
pub phi_max: f64,
}
impl Band {
pub fn new(phi_min: f64, phi_max: f64) -> Result<Self, SphereQlError> {
if phi_min < 0.0 || phi_min >= phi_max || phi_max > PI {
return Err(SphereQlError::InvalidBandBounds { phi_min, phi_max });
}
Ok(Self { phi_min, phi_max })
}
}
impl Contains for Band {
fn contains(&self, point: &SphericalPoint) -> bool {
point.phi >= self.phi_min && point.phi <= self.phi_max
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Wedge {
pub theta_min: f64,
pub theta_max: f64,
}
impl Wedge {
pub fn new(theta_min: f64, theta_max: f64) -> Result<Self, SphereQlError> {
if !(0.0..TAU).contains(&theta_min)
|| !(0.0..TAU).contains(&theta_max)
|| theta_min == theta_max
{
return Err(SphereQlError::InvalidWedgeBounds {
theta_min,
theta_max,
});
}
Ok(Self {
theta_min,
theta_max,
})
}
fn wraps(&self) -> bool {
self.theta_min > self.theta_max
}
}
impl Contains for Wedge {
fn contains(&self, point: &SphericalPoint) -> bool {
if self.wraps() {
point.theta >= self.theta_min || point.theta <= self.theta_max
} else {
point.theta >= self.theta_min && point.theta <= self.theta_max
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Region {
Cone(Cone),
Cap(Cap),
Shell(Shell),
Band(Band),
Wedge(Wedge),
Intersection(Vec<Region>),
Union(Vec<Region>),
}
impl Region {
pub fn intersection(regions: Vec<Region>) -> Self {
Region::Intersection(regions)
}
pub fn union(regions: Vec<Region>) -> Self {
Region::Union(regions)
}
}
impl Contains for Region {
fn contains(&self, point: &SphericalPoint) -> bool {
match self {
Region::Cone(c) => c.contains(point),
Region::Cap(c) => c.contains(point),
Region::Shell(s) => s.contains(point),
Region::Band(b) => b.contains(point),
Region::Wedge(w) => w.contains(point),
Region::Intersection(regions) => regions.iter().all(|r| r.contains(point)),
Region::Union(regions) => regions.iter().any(|r| r.contains(point)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
fn point(r: f64, theta: f64, phi: f64) -> SphericalPoint {
SphericalPoint::new_unchecked(r, theta, phi)
}
#[test]
fn cone_contains_point_inside() {
let cone = Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, FRAC_PI_4), FRAC_PI_2).unwrap();
let p = point(2.0, 0.0, FRAC_PI_4);
assert!(cone.contains(&p));
}
#[test]
fn cone_excludes_point_outside() {
let cone = Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.1), 0.05).unwrap();
let p = point(1.0, PI, FRAC_PI_2);
assert!(!cone.contains(&p));
}
#[test]
fn cone_contains_point_on_boundary() {
let half = FRAC_PI_4;
let cone = Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.0), half).unwrap();
let p = point(1.0, 0.0, half);
assert!(cone.contains(&p));
}
#[test]
fn cone_various_half_angles() {
let narrow = Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, FRAC_PI_2), 0.01).unwrap();
let near = point(1.0, 0.0, FRAC_PI_2);
let far = point(1.0, 0.0, FRAC_PI_2 + 0.1);
assert!(narrow.contains(&near));
assert!(!narrow.contains(&far));
let wide = Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, FRAC_PI_2), FRAC_PI_2).unwrap();
assert!(wide.contains(&point(1.0, 0.5, FRAC_PI_2 + 0.3)));
}
#[test]
fn cone_invalid_half_angle() {
assert!(Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.0), 0.0).is_err());
assert!(Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.0), -0.1).is_err());
assert!(Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.0), PI + 0.1).is_err());
assert!(Cone::new(point(0.0, 0.0, 0.0), point(1.0, 0.0, 0.0), PI).is_ok());
}
#[test]
fn cap_contains_point_inside() {
let cap = Cap::new(point(1.0, 0.0, FRAC_PI_2), FRAC_PI_4).unwrap();
let p = point(5.0, 0.0, FRAC_PI_2);
assert!(cap.contains(&p));
}
#[test]
fn cap_excludes_point_outside() {
let cap = Cap::new(point(1.0, 0.0, 0.1), 0.05).unwrap();
let p = point(1.0, PI, PI - 0.1);
assert!(!cap.contains(&p));
}
#[test]
fn cap_ignores_radius() {
let cap = Cap::new(point(1.0, 0.0, FRAC_PI_2), FRAC_PI_4).unwrap();
let near = point(0.1, 0.0, FRAC_PI_2);
let far = point(1000.0, 0.0, FRAC_PI_2);
assert!(cap.contains(&near));
assert!(cap.contains(&far));
}
#[test]
fn cap_invalid_half_angle() {
assert!(Cap::new(point(1.0, 0.0, 0.0), 0.0).is_err());
assert!(Cap::new(point(1.0, 0.0, 0.0), -1.0).is_err());
}
#[test]
fn cap_error_is_cap_specific() {
let err = Cap::new(point(1.0, 0.0, 0.0), 0.0).unwrap_err();
assert!(
matches!(err, SphereQlError::InvalidCapAngle(_)),
"expected InvalidCapAngle, got {err:?}"
);
}
#[test]
fn shell_contains_point_inside() {
let shell = Shell::new(1.0, 5.0).unwrap();
assert!(shell.contains(&point(3.0, 0.0, 0.0)));
}
#[test]
fn shell_excludes_point_outside() {
let shell = Shell::new(1.0, 5.0).unwrap();
assert!(!shell.contains(&point(0.5, 0.0, 0.0)));
assert!(!shell.contains(&point(6.0, 0.0, 0.0)));
}
#[test]
fn shell_boundary_inclusive() {
let shell = Shell::new(1.0, 5.0).unwrap();
assert!(shell.contains(&point(1.0, 0.0, 0.0)));
assert!(shell.contains(&point(5.0, 0.0, 0.0)));
}
#[test]
fn shell_invalid_bounds() {
assert!(Shell::new(5.0, 1.0).is_err());
assert!(Shell::new(3.0, 3.0).is_err());
assert!(Shell::new(-1.0, 5.0).is_err());
}
#[test]
fn band_contains_point_inside() {
let band = Band::new(FRAC_PI_4, 3.0 * FRAC_PI_4).unwrap();
assert!(band.contains(&point(1.0, 0.0, FRAC_PI_2)));
}
#[test]
fn band_excludes_point_outside() {
let band = Band::new(FRAC_PI_4, FRAC_PI_2).unwrap();
assert!(!band.contains(&point(1.0, 0.0, 0.1)));
assert!(!band.contains(&point(1.0, 0.0, PI - 0.1)));
}
#[test]
fn band_boundary_inclusive() {
let band = Band::new(FRAC_PI_4, FRAC_PI_2).unwrap();
assert!(band.contains(&point(1.0, 0.0, FRAC_PI_4)));
assert!(band.contains(&point(1.0, 0.0, FRAC_PI_2)));
}
#[test]
fn band_poles() {
let north = Band::new(0.0 + 0.001, FRAC_PI_4).unwrap();
assert!(north.contains(&point(1.0, 0.0, 0.01)));
assert!(!north.contains(&point(1.0, 0.0, FRAC_PI_2)));
let south = Band::new(3.0 * FRAC_PI_4, PI).unwrap();
assert!(south.contains(&point(1.0, 0.0, PI - 0.1)));
assert!(!south.contains(&point(1.0, 0.0, FRAC_PI_4)));
}
#[test]
fn band_invalid_bounds() {
assert!(Band::new(FRAC_PI_2, FRAC_PI_4).is_err());
assert!(Band::new(FRAC_PI_4, FRAC_PI_4).is_err());
assert!(Band::new(-0.1, FRAC_PI_2).is_err());
assert!(Band::new(0.0, PI + 0.1).is_err());
}
#[test]
fn wedge_contains_normal_range() {
let wedge = Wedge::new(0.5, 2.0).unwrap();
assert!(wedge.contains(&point(1.0, 1.0, FRAC_PI_2)));
assert!(!wedge.contains(&point(1.0, 3.0, FRAC_PI_2)));
}
#[test]
fn wedge_wraparound() {
let theta_min = 350.0_f64.to_radians();
let theta_max = 10.0_f64.to_radians();
let wedge = Wedge::new(theta_min, theta_max).unwrap();
let inside_high = point(1.0, 355.0_f64.to_radians(), FRAC_PI_2);
let inside_low = point(1.0, 5.0_f64.to_radians(), FRAC_PI_2);
let outside = point(1.0, 180.0_f64.to_radians(), FRAC_PI_2);
assert!(wedge.contains(&inside_high));
assert!(wedge.contains(&inside_low));
assert!(!wedge.contains(&outside));
}
#[test]
fn wedge_boundary_inclusive() {
let wedge = Wedge::new(1.0, 2.0).unwrap();
assert!(wedge.contains(&point(1.0, 1.0, FRAC_PI_2)));
assert!(wedge.contains(&point(1.0, 2.0, FRAC_PI_2)));
}
#[test]
fn wedge_rejects_invalid_theta() {
assert!(Wedge::new(-0.1, 1.0).is_err());
assert!(Wedge::new(0.0, 7.0).is_err());
}
#[test]
fn intersection_shell_and_band() {
let shell = Region::Shell(Shell::new(1.0, 5.0).unwrap());
let band = Region::Band(Band::new(FRAC_PI_4, 3.0 * FRAC_PI_4).unwrap());
let region = Region::intersection(vec![shell, band]);
assert!(region.contains(&point(3.0, 0.0, FRAC_PI_2)));
assert!(!region.contains(&point(3.0, 0.0, 0.1)));
assert!(!region.contains(&point(10.0, 0.0, FRAC_PI_2)));
}
#[test]
fn union_two_caps() {
let cap_a = Region::Cap(Cap::new(point(1.0, 0.0, 0.1), 0.2).unwrap());
let cap_b = Region::Cap(Cap::new(point(1.0, 0.0, PI - 0.1), 0.2).unwrap());
let region = Region::union(vec![cap_a, cap_b]);
assert!(region.contains(&point(1.0, 0.0, 0.05)));
assert!(region.contains(&point(1.0, 0.0, PI - 0.05)));
assert!(!region.contains(&point(1.0, 0.0, FRAC_PI_2)));
}
#[test]
fn empty_intersection_contains_everything() {
let region = Region::intersection(vec![]);
assert!(region.contains(&point(1.0, 0.0, FRAC_PI_2)));
}
#[test]
fn empty_union_contains_nothing() {
let region = Region::union(vec![]);
assert!(!region.contains(&point(1.0, 0.0, FRAC_PI_2)));
}
#[test]
fn region_dispatches_to_inner_types() {
let shell_region = Region::Shell(Shell::new(1.0, 5.0).unwrap());
assert!(shell_region.contains(&point(3.0, 0.0, 0.0)));
assert!(!shell_region.contains(&point(10.0, 0.0, 0.0)));
let wedge_region = Region::Wedge(Wedge::new(0.5, 2.0).unwrap());
assert!(wedge_region.contains(&point(1.0, 1.0, FRAC_PI_2)));
}
}