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 while retaining the editor's private buffer.
85 ///
86 /// This is useful at an FFI boundary where a foreign array may outlive its
87 /// editing scope. The published structure receives its own coordinate
88 /// buffer, so a retained foreign view cannot mutate it afterwards.
89 ///
90 /// # Errors
91 ///
92 /// Returns every violated structural invariant.
93 pub fn snapshot(&self) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
94 let mut data: StructureData = self.base.data().clone();
95 data.coords = self.coords.clone();
96 finish(data)
97 }
98}
99
100fn finish(mut data: StructureData) -> Result<Structure, Vec<crate::diagnostic::Diagnostic>> {
101 let Some(generation) = data.generation.next() else {
102 return Err(vec![
103 Diagnostic::new(Code::E6003).with_context("coordinate_generation", "exhausted"),
104 ]);
105 };
106 data.generation = generation;
107 let findings = validate(&data);
108 if findings.is_empty() {
109 Ok(Structure::new(data))
110 } else {
111 Err(findings)
112 }
113}
114
115#[cfg(test)]
116#[path = "edit_tests.rs"]
117mod tests;