use crate::{CoreError, Particle, StructureHandle};
use molgfx_math::{Aabb, Mat3, Mat4, Quat, Vec3};
#[cfg(test)]
#[path = "primitive_tests.rs"]
mod tests;
const EPSILON: f32 = 1.0e-6;
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct CrystalCell {
lengths: [f32; 3],
angles_degrees: [f32; 3],
origin: Vec3,
basis: Mat3,
}
impl CrystalCell {
pub fn new(lengths: [f32; 3], angles_degrees: [f32; 3]) -> Result<Self, CoreError> {
if lengths
.iter()
.any(|value| !value.is_finite() || *value <= EPSILON)
|| angles_degrees
.iter()
.any(|value| !value.is_finite() || *value <= EPSILON || *value >= 180.0 - EPSILON)
{
return Err(invalid(
"cell lengths and angles must be finite and non-degenerate",
));
}
let [a, b, c] = lengths;
let [alpha, beta, gamma] = angles_degrees.map(f32::to_radians);
let sin_gamma = gamma.sin();
if sin_gamma.abs() <= EPSILON {
return Err(invalid("gamma produces a degenerate cell basis"));
}
let cos_alpha = alpha.cos();
let cos_beta = beta.cos();
let cos_gamma = gamma.cos();
let a_axis = Vec3::new(a, 0.0, 0.0);
let b_axis = Vec3::new(b * cos_gamma, b * sin_gamma, 0.0);
let c_x = c * cos_beta;
let c_y = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
let c_z_sq = c.mul_add(c, -(c_x * c_x + c_y * c_y));
if !c_z_sq.is_finite() || c_z_sq <= EPSILON {
return Err(invalid("cell angles do not form a valid volume"));
}
let basis = Mat3::from_cols(a_axis, b_axis, Vec3::new(c_x, c_y, c_z_sq.sqrt()));
Ok(Self {
lengths,
angles_degrees,
origin: Vec3::ZERO,
basis,
})
}
#[must_use]
pub const fn with_origin(mut self, origin: Vec3) -> Self {
self.origin = origin;
self
}
#[must_use]
pub const fn lengths(self) -> [f32; 3] {
self.lengths
}
#[must_use]
pub const fn angles_degrees(self) -> [f32; 3] {
self.angles_degrees
}
#[must_use]
pub const fn origin(self) -> Vec3 {
self.origin
}
#[must_use]
pub fn fractional_to_cartesian(self, fractional: [f32; 3]) -> Vec3 {
self.origin + self.basis * Vec3::from_array(fractional)
}
#[must_use]
pub fn corners(self) -> [Vec3; 8] {
[
self.fractional_to_cartesian([0.0, 0.0, 0.0]),
self.fractional_to_cartesian([1.0, 0.0, 0.0]),
self.fractional_to_cartesian([0.0, 1.0, 0.0]),
self.fractional_to_cartesian([1.0, 1.0, 0.0]),
self.fractional_to_cartesian([0.0, 0.0, 1.0]),
self.fractional_to_cartesian([1.0, 0.0, 1.0]),
self.fractional_to_cartesian([0.0, 1.0, 1.0]),
self.fractional_to_cartesian([1.0, 1.0, 1.0]),
]
}
#[must_use]
pub fn edges(self) -> [(Vec3, Vec3); 12] {
edges_from_corners(self.corners())
}
#[must_use]
pub fn transformed_edges(self, instance: SymmetryInstance) -> [(Vec3, Vec3); 12] {
self.edges().map(|(start, end)| {
(
instance.transform.transform_point3(start),
instance.transform.transform_point3(end),
)
})
}
}
fn edges_from_corners(corners: [Vec3; 8]) -> [(Vec3, Vec3); 12] {
const EDGES: [(usize, usize); 12] = [
(0, 1),
(0, 2),
(1, 3),
(2, 3),
(4, 5),
(4, 6),
(5, 7),
(6, 7),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
];
EDGES.map(|(start, end)| (corners[start], corners[end]))
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SymmetryInstance {
pub id: u32,
pub transform: Mat4,
}
impl SymmetryInstance {
pub fn new(id: u32, transform: Mat4) -> Result<Self, CoreError> {
if !transform.is_finite() || transform.determinant().abs() <= EPSILON {
return Err(invalid("symmetry transform must be finite and invertible"));
}
Ok(Self { id, transform })
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct AnisotropicEllipsoid {
center: Vec3,
tensor: [f32; 6],
}
impl AnisotropicEllipsoid {
pub fn new(center: Vec3, tensor: [f32; 6]) -> Result<Self, CoreError> {
if !center.is_finite() || tensor.iter().any(|value| !value.is_finite()) {
return Err(invalid("ellipsoid center and tensor must be finite"));
}
let [u11, u22, u33, u12, u13, u23] = tensor;
let leading_1 = u11;
let leading_2 = u11 * u22 - u12 * u12;
let determinant = u11.mul_add(u22 * u33 - u23 * u23, -u12 * (u12 * u33 - u13 * u23))
+ u13 * (u12 * u23 - u22 * u13);
if leading_1 <= EPSILON || leading_2 <= EPSILON || determinant <= EPSILON {
return Err(invalid("ellipsoid tensor must be positive definite"));
}
Ok(Self { center, tensor })
}
#[must_use]
pub const fn center(self) -> Vec3 {
self.center
}
#[must_use]
pub const fn tensor(self) -> [f32; 6] {
self.tensor
}
#[must_use]
pub fn inverse_tensor(self) -> Option<[f32; 6]> {
let inverse = inverse_symmetric(self.tensor)?;
Some([
inverse.x_axis.x,
inverse.y_axis.y,
inverse.z_axis.z,
inverse.y_axis.x,
inverse.z_axis.x,
inverse.z_axis.y,
])
}
#[must_use]
pub fn bounds(self) -> Aabb {
let extent = Vec3::new(self.tensor[0], self.tensor[1], self.tensor[2]).sqrt();
Aabb::new(self.center - extent, self.center + extent)
}
#[must_use]
pub fn ray_intersection(self, origin: Vec3, direction: Vec3) -> Option<(f32, f32)> {
let inverse = inverse_symmetric(self.tensor)?;
if !origin.is_finite() || !direction.is_finite() || direction.length_squared() <= EPSILON {
return None;
}
let offset = origin - self.center;
let inv_direction = inverse * direction;
let inv_offset = inverse * offset;
let a = direction.dot(inv_direction);
let b = 2.0 * offset.dot(inv_direction);
let c = offset.dot(inv_offset) - 1.0;
let discriminant = b.mul_add(b, -4.0 * a * c);
if !discriminant.is_finite() || discriminant < 0.0 || a <= EPSILON {
return None;
}
let root = discriminant.sqrt();
let first = (-b - root) / (2.0 * a);
let second = (-b + root) / (2.0 * a);
Some((first.min(second), first.max(second)))
}
}
fn inverse_symmetric([u11, u22, u33, u12, u13, u23]: [f32; 6]) -> Option<Mat3> {
let determinant = u11.mul_add(u22 * u33 - u23 * u23, -u12 * (u12 * u33 - u13 * u23))
+ u13 * (u12 * u23 - u22 * u13);
if !determinant.is_finite() || determinant.abs() <= EPSILON {
return None;
}
let inv = 1.0 / determinant;
Some(Mat3::from_cols(
Vec3::new(
(u22 * u33 - u23 * u23) * inv,
(u13 * u23 - u12 * u33) * inv,
(u12 * u23 - u22 * u13) * inv,
),
Vec3::new(
(u13 * u23 - u12 * u33) * inv,
(u11 * u33 - u13 * u13) * inv,
(u12 * u13 - u11 * u23) * inv,
),
Vec3::new(
(u12 * u23 - u22 * u13) * inv,
(u12 * u13 - u11 * u23) * inv,
(u11 * u22 - u12 * u12) * inv,
),
))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub enum CarbohydrateShape {
#[default]
Unknown,
Glc,
Gal,
Man,
Fuc,
Xyl,
Neu5Ac,
}
impl CarbohydrateShape {
#[must_use]
pub const fn stable_name(self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Glc => "glc",
Self::Gal => "gal",
Self::Man => "man",
Self::Fuc => "fuc",
Self::Xyl => "xyl",
Self::Neu5Ac => "neu5ac",
}
}
#[must_use]
pub const fn stable_code(self) -> u32 {
match self {
Self::Unknown => 0,
Self::Glc => 1,
Self::Gal => 2,
Self::Man => 3,
Self::Fuc => 4,
Self::Xyl => 5,
Self::Neu5Ac => 6,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct CarbohydrateSymbol {
pub owner: StructureHandle,
pub center: Vec3,
pub orientation: Quat,
pub size: Vec3,
pub shape: CarbohydrateShape,
pub color: molgfx_math::Rgba8,
pub visible: bool,
}
impl CarbohydrateSymbol {
pub fn new(
owner: StructureHandle,
center: Vec3,
orientation: Quat,
size: Vec3,
shape: CarbohydrateShape,
color: molgfx_math::Rgba8,
) -> Result<Self, CoreError> {
if !center.is_finite()
|| !orientation.is_finite()
|| orientation.length_squared() <= EPSILON
|| !size.is_finite()
|| size.min_element() <= EPSILON
{
return Err(invalid(
"carbohydrate pose and size must be finite and positive",
));
}
Ok(Self {
owner,
center,
orientation: orientation.normalize(),
size,
shape,
color,
visible: true,
})
}
#[must_use]
pub fn bounds(self) -> Aabb {
let half = self.size * 0.5;
Aabb::from_points(
[
Vec3::new(-half.x, -half.y, -half.z),
Vec3::new(half.x, -half.y, -half.z),
Vec3::new(-half.x, half.y, -half.z),
Vec3::new(half.x, half.y, -half.z),
Vec3::new(-half.x, -half.y, half.z),
Vec3::new(half.x, -half.y, half.z),
Vec3::new(-half.x, half.y, half.z),
Vec3::new(half.x, half.y, half.z),
]
.map(|corner| self.center + self.orientation * corner),
)
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Primitive {
Ellipsoid {
owner: StructureHandle,
value: AnisotropicEllipsoid,
color: molgfx_math::Rgba8,
opacity: f32,
visible: bool,
},
Carbohydrate(CarbohydrateSymbol),
Planar {
value: crate::PlanarRegion,
color: molgfx_math::Rgba8,
opacity: f32,
visible: bool,
},
Particle(Particle),
}
impl Primitive {
#[must_use]
pub const fn owner(self) -> StructureHandle {
match self {
Self::Ellipsoid { owner, .. }
| Self::Planar {
value: crate::PlanarRegion { owner, .. },
..
} => owner,
Self::Carbohydrate(value) => value.owner,
Self::Particle(value) => value.owner,
}
}
#[must_use]
pub const fn visible(self) -> bool {
match self {
Self::Ellipsoid { visible, .. } | Self::Planar { visible, .. } => visible,
Self::Carbohydrate(value) => value.visible,
Self::Particle(value) => value.visible,
}
}
#[must_use]
pub fn bounds(self) -> Aabb {
match self {
Self::Ellipsoid { value, .. } => value.bounds(),
Self::Carbohydrate(value) => value.bounds(),
Self::Planar { value, .. } => Aabb::from_points(value.corners()),
Self::Particle(value) => value.bounds(),
}
}
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidPrimitive { reason }
}