use crate::{
EuclideanVector, FloatSign, Hypersphere, IntersectionResult, Line, Point, Segment,
SpatialRelation, Vector, VectorMetricSquared, classify_to_zero,
};
use num_traits::Float;
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(serialize = "T: serde::Serialize", deserialize = "T: serde::Deserialize<'de>")))]
pub struct Hyperplane<T, const N: usize> {
origin: Point<T, N>,
normal: Vector<T, N>, }
impl<T, const N: usize> Hyperplane<T, N>
where
T: Float + std::iter::Sum,
{
pub fn new(origin: Point<T, N>, normal: Vector<T, N>) -> Self {
Self {
origin,
normal: Self::normal_or_default(&normal),
}
}
#[inline]
pub fn origin(&self) -> Point<T, N> {
self.origin
}
#[inline]
pub fn origin_ref(&self) -> &Point<T, N> {
&self.origin
}
#[inline]
pub fn normal(&self) -> Vector<T, N> {
self.normal
}
#[inline]
pub fn normal_ref(&self) -> &Vector<T, N> {
&self.normal
}
#[inline]
pub fn set_origin(&mut self, p: &Point<T, N>) {
self.origin = *p;
}
#[inline]
pub fn set_normal(&mut self, v: &Vector<T, N>) {
self.normal = Self::normal_or_default(v)
}
#[inline]
pub fn origin_ref_mut(&mut self) -> &mut Point<T, N> {
&mut self.origin
}
#[inline]
pub fn normal_ref_mut(&mut self) -> &mut Vector<T, N> {
&mut self.normal
}
fn normal_or_default(v: &Vector<T, N>) -> Vector<T, N> {
v.normalize().unwrap_or_else(|| {
Vector::new(std::array::from_fn(|i| if i == 0 { T::one() } else { T::zero() }))
})
}
#[inline]
pub fn signed_distance(&self, p: &Point<T, N>) -> T {
(*p - self.origin).dot(&self.normal)
}
}
impl<T, const N: usize> SpatialRelation<T, N> for Hyperplane<T, N>
where
T: Float + std::iter::Sum,
{
fn closest_point(&self, p: &Point<T, N>) -> Point<T, N> {
let d = self.signed_distance(p);
*p - self.normal * d
}
fn contains(&self, p: &Point<T, N>) -> bool {
match classify_to_zero(self.signed_distance(p).abs(), None) {
FloatSign::Zero => true,
_ => false,
}
}
fn is_inside(&self, p: &Point<T, N>) -> bool {
self.signed_distance(p) <= T::zero()
}
}
impl<T, const N: usize> Hyperplane<T, N>
where
T: Float + std::iter::Sum,
{
pub fn intersect_segment(&self, segment: &Segment<T, N>) -> IntersectionResult<T, N> {
let d_dot_n = segment.delta().dot(&self.normal);
if d_dot_n.abs() < T::epsilon() {
return if self.contains(&segment.start()) {
IntersectionResult::Collinear
} else {
IntersectionResult::None
};
}
let t = (self.origin - segment.start()).dot(&self.normal) / d_dot_n;
if t >= -T::epsilon() && t <= T::one() + T::epsilon() {
IntersectionResult::Single(segment.at(t))
} else {
IntersectionResult::None
}
}
#[inline]
pub fn intersect_line(&self, line: &Line<T, N>) -> IntersectionResult<T, N> {
line.intersect_hyperplane(self)
}
#[inline]
pub fn intersect_hypersphere(&self, sphere: &Hypersphere<T, N>) -> IntersectionResult<T, N> {
sphere.intersect_hyperplane(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Point, Segment, Vector};
use approx::assert_relative_eq;
#[test]
fn test_hyperplane_closest_point() {
let plane = Hyperplane::new(Point::new([0.0, 0.0, 0.0]), Vector::new([0.0, 0.0, 1.0]));
let p = Point::new([10.0, 10.0, 5.0]);
let projected = plane.closest_point(&p);
assert_relative_eq!(projected.coords_ref()[2], 0.0);
assert_relative_eq!(projected.coords_ref()[0], 10.0);
}
#[test]
fn test_segment_piercing_plane() {
let plane = Hyperplane::new(Point::new([0.0, 0.0, 0.0]), Vector::new([0.0, 1.0, 0.0]));
let seg = Segment::new(Point::new([0.0, -5.0, 0.0]), Point::new([0.0, 5.0, 0.0]));
if let IntersectionResult::Single(p) = plane.intersect_segment(&seg) {
assert_relative_eq!(p.coords_ref()[1], 0.0);
} else {
panic!("Expected single intersection point");
}
}
#[cfg(feature = "serde")]
#[test]
fn test_hyperplane_serialization_roundtrip() {
use serde_json;
let plane = Hyperplane::new(
Point::new([0.0, 0.0, 0.0]),
Vector::new([0.0, 0.0, 1.0]),
);
let json = serde_json::to_string(&plane).unwrap();
let restored: Hyperplane<f64, 3> = serde_json::from_str(&json).unwrap();
assert_eq!(plane.origin(), restored.origin());
assert_eq!(plane.normal().coords, restored.normal().coords);
}
}