use crate::primitives::{Point2, Point3, Vec3};
use crate::scalar::{Scalar, Tolerance};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FrameError {
NonFiniteInput,
NotUnitLength,
NotPerpendicular,
Degenerate,
NotRightHanded,
}
impl core::fmt::Display for FrameError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::NonFiniteInput => "frame origin and axes must be finite",
Self::NotUnitLength => "frame axes must be unit length",
Self::NotPerpendicular => "frame axes must be perpendicular",
Self::Degenerate => "plane frame axes must span a plane, not a line",
Self::NotRightHanded => "frame axes must form a right-handed basis",
})
}
}
impl std::error::Error for FrameError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlaneFrame {
origin: Point3,
x: Vec3,
y: Vec3,
}
impl PlaneFrame {
#[must_use]
pub const fn ground() -> Self {
Self {
origin: Point3::new(0.0, 0.0, 0.0),
x: Vec3::new(1.0, 0.0, 0.0),
y: Vec3::new(0.0, 1.0, 0.0),
}
}
pub fn new(origin: Point3, x: Vec3, y: Vec3, tolerance: Tolerance) -> Result<Self, FrameError> {
if !origin.is_finite() || !x.is_finite() || !y.is_finite() {
return Err(FrameError::NonFiniteInput);
}
let unit = tolerance.angular().max(Scalar::EPSILON * 8.0);
if (x.length_squared() - 1.0).abs() > unit || (y.length_squared() - 1.0).abs() > unit {
return Err(FrameError::NotUnitLength);
}
if x.cross(y).length_squared() <= unit {
return Err(FrameError::Degenerate);
}
if x.dot(y).abs() > unit {
return Err(FrameError::NotPerpendicular);
}
Ok(Self { origin, x, y })
}
#[must_use]
pub fn project(&self, point: Point3) -> Point2 {
let offset = point - self.origin;
Point2::new(offset.dot(self.x), offset.dot(self.y))
}
#[must_use]
pub fn lift(&self, point: Point2) -> Point3 {
self.origin + self.x * point.x + self.y * point.y
}
#[must_use]
pub fn signed_distance(&self, point: Point3) -> Scalar {
(point - self.origin).dot(self.normal())
}
#[must_use]
pub fn normal(&self) -> Vec3 {
self.x.cross(self.y)
}
pub fn from_normal(
origin: Point3,
normal: Vec3,
tolerance: Tolerance,
) -> Result<Self, FrameError> {
if !origin.is_finite() || !normal.is_finite() {
return Err(FrameError::NonFiniteInput);
}
let unit = tolerance.angular().max(Scalar::EPSILON * 8.0);
if normal.length_squared() <= unit {
return Err(FrameError::Degenerate);
}
let z = normal.normalize();
let seed = if z.x.abs() <= z.y.abs() && z.x.abs() <= z.z.abs() {
Vec3::new(1.0, 0.0, 0.0)
} else if z.y.abs() <= z.z.abs() {
Vec3::new(0.0, 1.0, 0.0)
} else {
Vec3::new(0.0, 0.0, 1.0)
};
let x = z.cross(seed).normalize();
Ok(Self {
origin,
x,
y: z.cross(x),
})
}
#[must_use]
pub const fn origin(&self) -> Point3 {
self.origin
}
#[must_use]
pub const fn x_axis(&self) -> Vec3 {
self.x
}
#[must_use]
pub const fn y_axis(&self) -> Vec3 {
self.y
}
}