molframe_core/structure/edit.rs
1//! Transactional coordinate editing over immutable snapshots.
2
3use super::{CoordinateStore, Structure, StructureData, validate};
4use crate::diagnostic::{Code, Diagnostic};
5use crate::index::ModelIndex;
6use crate::{ExecutionContext, MemoryBudgetError};
7
8/// A private coordinate copy that becomes a new structure only on commit.
9///
10/// The original structure remains immutable and every view into it stays valid.
11/// Dropping an editor discards the transaction.
12#[derive(Debug)]
13pub struct CoordinateEditor {
14 base: Structure,
15 coords: CoordinateStore,
16}
17
18impl Structure {
19 /// Starts a scoped coordinate edit under the shared execution account.
20 ///
21 /// Coordinate storage is copied for the new snapshot; topology and atom
22 /// annotations remain untouched. Call [`CoordinateEditor::commit`] to
23 /// validate and publish the new snapshot.
24 /// Shared topology remains borrowed. Direct coordinate frames detach under
25 /// the supplied account, and their reservations follow the copied backing
26 /// buffers through committed snapshots and clones.
27 ///
28 /// # Errors
29 ///
30 /// Returns before publishing an editor when all coordinate copies cannot fit.
31 pub fn edit_coordinates(
32 &self,
33 context: &ExecutionContext,
34 ) -> Result<CoordinateEditor, MemoryBudgetError> {
35 let mut coords = self.data().coords.clone();
36 coords.make_unique_in(context)?;
37 Ok(CoordinateEditor {
38 base: self.clone(),
39 coords,
40 })
41 }
42}
43
44impl CoordinateEditor {
45 /// Mutable positions of one dense model.
46 ///
47 /// Ragged models are independent structures and must be edited through the
48 /// corresponding value returned by [`Structure::ragged_models`].
49 pub fn positions_mut(&mut self, model: ModelIndex) -> Option<&mut [[f32; 3]]> {
50 match &mut self.coords {
51 CoordinateStore::Single(block) if model.get() == 0 => Some(block.as_mut_slice()),
52 CoordinateStore::Dense { frames } => frames
53 .get_mut(model.as_usize())
54 .map(crate::coords::CoordinateBlock::as_mut_slice),
55 CoordinateStore::Single(_) | CoordinateStore::Ragged { .. } => None,
56 }
57 }
58
59 /// Mutable positions of one dense model, with a diagnostic on failure.
60 ///
61 /// This is the fallible form for callers that must preserve molframe's
62 /// diagnostic contract across an FFI boundary.
63 ///
64 /// # Errors
65 ///
66 /// Returns `E6003` when `model` is not a dense frame of this structure.
67 pub fn try_positions_mut(&mut self, model: ModelIndex) -> Result<&mut [[f32; 3]], Diagnostic> {
68 self.positions_mut(model)
69 .ok_or_else(|| Diagnostic::new(Code::E6003))
70 }
71
72 /// Validates the transaction and returns a new immutable snapshot.
73 ///
74 /// # Errors
75 ///
76 /// Returns every violated structural invariant. The original snapshot is
77 /// unaffected whether the commit succeeds or fails.
78 pub fn commit(self) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
79 let mut data: StructureData = self.base.data().clone();
80 data.coords = self.coords;
81 finish(data)
82 }
83
84 /// Builds a validated snapshot that shares the editor's coordinate buffer.
85 ///
86 /// Nothing is copied, so the snapshot and the editor see the same
87 /// coordinates: use it only when the editor is not written to afterwards.
88 /// A caller that keeps a writable view of the buffer must use
89 /// [`Self::snapshot_detached`].
90 ///
91 /// # Errors
92 ///
93 /// Returns every violated structural invariant.
94 pub fn snapshot(&self) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
95 let mut data: StructureData = self.base.data().clone();
96 data.coords = self.coords.clone();
97 finish(data)
98 }
99}
100
101impl CoordinateEditor {
102 /// Builds a validated snapshot with a coordinate buffer of its own.
103 ///
104 /// The copy is charged to `context`'s memory budget before anything is
105 /// allocated. Writing to the editor, or to a foreign view of its buffer,
106 /// afterwards cannot change the published structure.
107 ///
108 /// # Errors
109 ///
110 /// Returns a memory diagnostic when the copy does not fit the budget, or
111 /// every violated structural invariant.
112 pub fn snapshot_detached(
113 &self,
114 context: &ExecutionContext,
115 ) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
116 let mut coords = self.coords.clone();
117 coords
118 .make_unique_in(context)
119 .map_err(|error| vec![crate::diagnostic::Diagnostic::from(error)])?;
120 let mut data: StructureData = self.base.data().clone();
121 data.coords = coords;
122 finish(data)
123 }
124}
125
126fn finish(mut data: StructureData) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
127 let Some(generation) = data.generation.next() else {
128 return Err(vec![
129 Diagnostic::new(Code::E6003).with_context("coordinate_generation", "exhausted"),
130 ]);
131 };
132 data.generation = generation;
133 let findings = validate(&data);
134 if findings.is_empty() {
135 Ok(Structure::new(data))
136 } else {
137 Err(findings)
138 }
139}
140
141#[cfg(test)]
142#[path = "edit_tests.rs"]
143mod tests;