Skip to main content

molframe_core/structure/
view.rs

1//! A subset of a structure, costing two words and a selection.
2//!
3//! A view copies nothing. It holds a reference to the structure, the set of
4//! atoms it covers, and the coordinate generation it was made against. Composing
5//! views is set algebra on the selections, so narrowing a view never touches the
6//! atoms it discards.
7//!
8//! The generation is what makes a stale view detectable. A view made before an
9//! edit still refers to the snapshot it was made from, and asking it about a
10//! newer structure is refused rather than answered with positions that have
11//! since moved.
12
13use super::data::{Structure, StructureData};
14use crate::coords::{Aabb, CoordinateGeneration};
15use crate::index::ModelIndex;
16use crate::selection::AtomSelection;
17use std::sync::Arc;
18
19/// A zero-copy subset of a structure.
20///
21/// # Examples
22///
23/// ```
24/// use molframe_core::structure::{Structure, StructureData};
25/// use molframe_core::AtomSelection;
26///
27/// let structure = Structure::new(StructureData::empty());
28/// let everything = structure.view();
29/// let nothing = everything.narrow(&AtomSelection::Empty);
30///
31/// assert!(nothing.is_empty());
32/// assert_eq!(everything.len(), u64::from(structure.atom_count()));
33/// ```
34#[derive(Clone, Debug)]
35pub struct StructureView {
36    structure: Arc<StructureData>,
37    atoms: AtomSelection,
38    generation: CoordinateGeneration,
39}
40
41impl StructureView {
42    /// Creates a view over `atoms` of `structure`.
43    #[must_use]
44    pub fn new(structure: &Structure, atoms: AtomSelection) -> Self {
45        Self {
46            structure: structure.shared(),
47            atoms,
48            generation: structure.generation(),
49        }
50    }
51
52    /// The atoms this view covers.
53    #[must_use]
54    pub const fn selection(&self) -> &AtomSelection {
55        &self.atoms
56    }
57
58    /// The structure this view is over.
59    #[must_use]
60    pub fn data(&self) -> &StructureData {
61        &self.structure
62    }
63
64    /// The number of atoms covered.
65    #[must_use]
66    pub fn len(&self) -> u64 {
67        self.atoms.len()
68    }
69
70    /// Returns true when the view covers no atoms.
71    #[must_use]
72    pub fn is_empty(&self) -> bool {
73        self.atoms.is_empty()
74    }
75
76    /// The generation this view was made against.
77    #[must_use]
78    pub const fn generation(&self) -> CoordinateGeneration {
79        self.generation
80    }
81
82    /// Returns true when `structure` has moved on since this view was made.
83    ///
84    /// A view of an earlier snapshot is still perfectly usable *against that
85    /// snapshot* — it holds its own reference to it. This asks a different
86    /// question: whether the view may be used to talk about the newer one.
87    #[must_use]
88    pub fn is_stale_for(&self, structure: &Structure) -> bool {
89        self.generation != structure.generation()
90    }
91
92    /// A view of the atoms in both this view and `other`.
93    #[must_use]
94    pub fn narrow(&self, other: &AtomSelection) -> Self {
95        self.with_selection(self.atoms.intersect(other))
96    }
97
98    /// A view of the atoms in either this view or `other`.
99    #[must_use]
100    pub fn union(&self, other: &Self) -> Self {
101        self.with_selection(self.atoms.union(&other.atoms))
102    }
103
104    /// A view of the atoms in both views.
105    #[must_use]
106    pub fn intersect(&self, other: &Self) -> Self {
107        self.with_selection(self.atoms.intersect(&other.atoms))
108    }
109
110    /// A view of the atoms in this view but not the other.
111    #[must_use]
112    pub fn difference(&self, other: &Self) -> Self {
113        self.with_selection(self.atoms.difference(&other.atoms))
114    }
115
116    fn with_selection(&self, atoms: AtomSelection) -> Self {
117        Self {
118            structure: Arc::clone(&self.structure),
119            atoms,
120            generation: self.generation,
121        }
122    }
123
124    /// The positions of the covered atoms in one model.
125    ///
126    /// Yields values rather than a slice: the atoms of a view are not in general
127    /// contiguous, so a slice would either be a lie or a copy.
128    pub fn positions(&self, model: ModelIndex) -> impl Iterator<Item = [f32; 3]> + '_ {
129        let block = self.structure.coords.block(model);
130        self.atoms
131            .iter()
132            .filter_map(move |atom| block?.as_slice().get(atom as usize).copied())
133    }
134
135    /// The box enclosing the covered atoms in one model.
136    #[must_use]
137    pub fn bounds(&self, model: ModelIndex) -> Aabb {
138        let mut bounds = Aabb::EMPTY;
139        for position in self.positions(model) {
140            bounds.extend(position);
141        }
142        bounds
143    }
144}
145
146impl Structure {
147    /// A view of every atom.
148    #[must_use]
149    pub fn view(&self) -> StructureView {
150        StructureView::new(self, AtomSelection::All(self.atom_count()))
151    }
152
153    /// A view of the atoms in `selection`.
154    #[must_use]
155    pub fn view_of(&self, selection: AtomSelection) -> StructureView {
156        StructureView::new(self, selection)
157    }
158}
159
160#[cfg(test)]
161#[path = "view_tests.rs"]
162mod tests;