Skip to main content

nv_view/
bound.rs

1//! View-bound context: binding user data to a specific camera view.
2
3use crate::view_state::{ViewEpoch, ViewState, ViewVersion};
4
5/// A piece of user-supplied context data bound to a specific camera view.
6///
7/// The library does not interpret the contents — it only tracks whether
8/// the binding is still valid under the current view. Useful for
9/// calibration data, spatial regions, reference transforms, etc.
10///
11/// # Example
12///
13/// ```
14/// use nv_view::{ViewBoundContext, ViewState, BoundContextValidity};
15///
16/// struct CalibrationData { /* ... */ }
17///
18/// let view = ViewState::fixed_initial();
19/// let bound = ViewBoundContext::bind(CalibrationData {}, &view);
20///
21/// // Same view → Current.
22/// assert!(matches!(bound.validity(&view), BoundContextValidity::Current));
23/// ```
24#[derive(Clone, Debug)]
25pub struct ViewBoundContext<T> {
26    /// The user's data.
27    pub data: T,
28    /// The `ViewVersion` at which this data was created or last rebind.
29    pub bound_at: ViewVersion,
30    /// The `ViewEpoch` at which this data was created.
31    pub bound_epoch: ViewEpoch,
32}
33
34impl<T> ViewBoundContext<T> {
35    /// Bind data to the current view state.
36    #[must_use]
37    pub fn bind(data: T, view: &ViewState) -> Self {
38        Self {
39            data,
40            bound_at: view.version,
41            bound_epoch: view.epoch,
42        }
43    }
44
45    /// Check whether this binding is still valid under the given view state.
46    #[must_use]
47    pub fn validity(&self, current: &ViewState) -> BoundContextValidity {
48        if current.version == self.bound_at {
49            BoundContextValidity::Current
50        } else if current.epoch == self.bound_epoch {
51            BoundContextValidity::StaleWithinEpoch {
52                versions_behind: current.version.versions_since(self.bound_at),
53            }
54        } else {
55            BoundContextValidity::InvalidAcrossEpoch {
56                epochs_behind: current
57                    .epoch
58                    .as_u64()
59                    .saturating_sub(self.bound_epoch.as_u64()),
60            }
61        }
62    }
63
64    /// Rebind this context to the current view.
65    ///
66    /// The caller asserts the data is still valid at the new view.
67    pub fn rebind(&mut self, view: &ViewState) {
68        self.bound_at = view.version;
69        self.bound_epoch = view.epoch;
70    }
71}
72
73/// Result of checking a [`ViewBoundContext`]'s validity.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum BoundContextValidity {
76    /// Same `ViewVersion` — context is exactly current.
77    Current,
78
79    /// Same `ViewEpoch` but different `ViewVersion`. View has changed
80    /// within the epoch (small motions, compensations). Context may be
81    /// approximately valid but should be checked.
82    StaleWithinEpoch {
83        /// How many versions behind the current view.
84        versions_behind: u64,
85    },
86
87    /// Different `ViewEpoch` — view has changed fundamentally.
88    /// Context should be considered invalid.
89    InvalidAcrossEpoch {
90        /// How many epochs behind the current view.
91        epochs_behind: u64,
92    },
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn current_when_same_version() {
101        let view = ViewState::fixed_initial();
102        let bound = ViewBoundContext::bind(42u32, &view);
103        assert_eq!(bound.validity(&view), BoundContextValidity::Current);
104    }
105
106    #[test]
107    fn stale_within_epoch() {
108        let view = ViewState::fixed_initial();
109        let bound = ViewBoundContext::bind(42u32, &view);
110        let mut updated = view.clone();
111        updated.version = ViewVersion::new(5);
112        assert_eq!(
113            bound.validity(&updated),
114            BoundContextValidity::StaleWithinEpoch { versions_behind: 5 }
115        );
116    }
117
118    #[test]
119    fn invalid_across_epoch() {
120        let view = ViewState::fixed_initial();
121        let bound = ViewBoundContext::bind(42u32, &view);
122        let mut updated = view.clone();
123        updated.epoch = ViewEpoch::new(3);
124        updated.version = ViewVersion::new(10);
125        assert_eq!(
126            bound.validity(&updated),
127            BoundContextValidity::InvalidAcrossEpoch { epochs_behind: 3 }
128        );
129    }
130
131    #[test]
132    fn rebind_resets_validity() {
133        let view = ViewState::fixed_initial();
134        let mut bound = ViewBoundContext::bind(42u32, &view);
135        let mut updated = view.clone();
136        updated.version = ViewVersion::new(5);
137        bound.rebind(&updated);
138        assert_eq!(bound.validity(&updated), BoundContextValidity::Current);
139    }
140}