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;