Skip to main content

chematic_3d/
coords.rs

1//! 3D coordinate types for molecular structures.
2
3use chematic_core::AtomIdx;
4
5/// A 3D point in Cartesian space (angstroms).
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Point3 {
8    pub x: f64,
9    pub y: f64,
10    pub z: f64,
11}
12
13impl Point3 {
14    /// Create a new point.
15    #[inline]
16    pub fn new(x: f64, y: f64, z: f64) -> Self {
17        Self { x, y, z }
18    }
19
20    /// The origin (0, 0, 0).
21    #[inline]
22    pub fn zero() -> Self {
23        Self::new(0.0, 0.0, 0.0)
24    }
25
26    /// Euclidean distance to another point.
27    #[inline]
28    pub fn distance(&self, other: &Self) -> f64 {
29        let dx = self.x - other.x;
30        let dy = self.y - other.y;
31        let dz = self.z - other.z;
32        (dx * dx + dy * dy + dz * dz).sqrt()
33    }
34
35    /// Component-wise addition.
36    #[inline]
37    pub fn add(&self, other: &Self) -> Self {
38        Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
39    }
40
41    /// Component-wise subtraction.
42    #[inline]
43    pub fn sub(&self, other: &Self) -> Self {
44        Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
45    }
46
47    /// Scalar multiplication.
48    #[inline]
49    pub fn scale(&self, s: f64) -> Self {
50        Self::new(self.x * s, self.y * s, self.z * s)
51    }
52
53    /// Dot product.
54    #[inline]
55    pub fn dot(&self, other: &Self) -> f64 {
56        self.x * other.x + self.y * other.y + self.z * other.z
57    }
58
59    /// Cross product.
60    #[inline]
61    pub fn cross(&self, other: &Self) -> Self {
62        Self::new(
63            self.y * other.z - self.z * other.y,
64            self.z * other.x - self.x * other.z,
65            self.x * other.y - self.y * other.x,
66        )
67    }
68
69    /// Euclidean norm (length).
70    #[inline]
71    pub fn norm(&self) -> f64 {
72        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
73    }
74
75    /// Normalize to a unit vector.
76    ///
77    /// # Panics
78    /// Panics if the vector has zero length.
79    pub fn normalize(&self) -> Self {
80        let n = self.norm();
81        assert!(n > 0.0, "cannot normalize a zero-length vector");
82        self.scale(1.0 / n)
83    }
84
85    /// Try to normalize to a unit vector, returning None if the vector has zero length.
86    pub fn try_normalize(&self) -> Option<Self> {
87        let n = self.norm();
88        if n > 0.0 {
89            Some(self.scale(1.0 / n))
90        } else {
91            None
92        }
93    }
94}
95
96/// 3D coordinates for all heavy atoms in a molecule.
97///
98/// Indexed by atom insertion order (`AtomIdx.0` as `usize`).
99#[derive(Debug, Clone)]
100pub struct Coords3D {
101    pub points: Vec<Point3>,
102}
103
104impl Coords3D {
105    /// Create zeroed coordinates for `n` atoms.
106    pub fn new_zeroed(n: usize) -> Self {
107        Self {
108            points: vec![Point3::zero(); n],
109        }
110    }
111
112    /// Get the coordinate of atom `idx`.
113    pub fn get(&self, idx: AtomIdx) -> Point3 {
114        self.points[idx.0 as usize]
115    }
116
117    /// Set the coordinate of atom `idx`.
118    pub fn set(&mut self, idx: AtomIdx, p: Point3) {
119        self.points[idx.0 as usize] = p;
120    }
121
122    /// Number of atom coordinate slots.
123    pub fn atom_count(&self) -> usize {
124        self.points.len()
125    }
126}