use crate::CoreError;
use molgfx_math::Vec3;
#[cfg(test)]
#[path = "clipping_tests.rs"]
mod tests;
pub const MAX_CLIP_PLANES: usize = 4;
#[repr(u32)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ClipCap {
#[default]
Open = 0,
Solid = 1,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ClipPlane {
pub normal: Vec3,
pub offset: f32,
}
impl ClipPlane {
pub fn from_point_normal(point: Vec3, normal: Vec3) -> Result<Self, CoreError> {
if !point.is_finite() || !normal.is_finite() || normal.length_squared() <= 1e-12 {
return Err(invalid("clip point and non-zero normal must be finite"));
}
let normal = normal.normalize();
Ok(Self {
normal,
offset: -normal.dot(point),
})
}
#[must_use]
pub fn reversed(self) -> Self {
Self {
normal: -self.normal,
offset: -self.offset,
}
}
#[must_use]
pub fn signed_distance(self, point: Vec3) -> f32 {
self.normal.dot(point) + self.offset
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ClipSet {
planes: [ClipPlane; MAX_CLIP_PLANES],
len: u8,
cap: ClipCap,
}
impl ClipSet {
pub fn new(planes: &[ClipPlane]) -> Result<Self, CoreError> {
if planes.len() > MAX_CLIP_PLANES {
return Err(invalid("at most four clip planes are portable"));
}
let mut set = Self::default();
set.planes[..planes.len()].copy_from_slice(planes);
set.len = u8::try_from(planes.len()).into_iter().fold(0, |_, len| len);
Ok(set)
}
pub fn slab(center: Vec3, normal: Vec3, thickness: f32) -> Result<Self, CoreError> {
if !thickness.is_finite() || thickness <= 0.0 {
return Err(invalid("slab thickness must be positive and finite"));
}
if !normal.is_finite() || normal.length_squared() <= 1e-12 {
return Err(invalid("slab normal must be finite and non-zero"));
}
let direction = normal.normalize();
let half = thickness * 0.5;
Self::new(&[
ClipPlane::from_point_normal(center - direction * half, direction)?,
ClipPlane::from_point_normal(center + direction * half, -direction)?,
])
}
#[must_use]
pub fn planes(&self) -> &[ClipPlane] {
&self.planes[..usize::from(self.len)]
}
#[must_use]
pub fn contains(&self, point: Vec3) -> bool {
self.planes()
.iter()
.all(|plane| plane.signed_distance(point) >= 0.0)
}
#[must_use]
pub fn with_cap(mut self, cap: ClipCap) -> Self {
self.cap = cap;
self
}
#[must_use]
pub const fn cap(&self) -> ClipCap {
self.cap
}
}
impl Default for ClipSet {
fn default() -> Self {
let plane = ClipPlane {
normal: Vec3::X,
offset: 0.0,
};
Self {
planes: [plane; MAX_CLIP_PLANES],
len: 0,
cap: ClipCap::Open,
}
}
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidClip { reason }
}