1use std::fmt;
4use std::sync::Arc;
5
6use crate::camera_motion::{CameraMotionState, MotionSource};
7use crate::ptz::PtzTelemetry;
8use crate::transform::GlobalTransformEstimate;
9use crate::transition::TransitionPhase;
10use crate::validity::ContextValidity;
11
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
20pub struct ViewEpoch(u64);
21
22impl ViewEpoch {
23 pub const INITIAL: Self = Self(0);
25
26 #[must_use]
28 pub fn new(val: u64) -> Self {
29 Self(val)
30 }
31
32 #[must_use]
34 pub fn next(self) -> Self {
35 Self(self.0 + 1)
36 }
37
38 #[must_use]
40 pub fn as_u64(self) -> u64 {
41 self.0
42 }
43}
44
45impl fmt::Display for ViewEpoch {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(f, "epoch-{}", self.0)
48 }
49}
50
51impl fmt::Debug for ViewEpoch {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "ViewEpoch({})", self.0)
54 }
55}
56
57#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
63pub struct ViewVersion(u64);
64
65impl ViewVersion {
66 pub const INITIAL: Self = Self(0);
68
69 #[must_use]
71 pub fn new(val: u64) -> Self {
72 Self(val)
73 }
74
75 #[must_use]
77 pub fn next(self) -> Self {
78 Self(self.0 + 1)
79 }
80
81 #[must_use]
83 pub fn as_u64(self) -> u64 {
84 self.0
85 }
86
87 #[must_use]
90 pub fn versions_since(self, other: Self) -> u64 {
91 self.0.saturating_sub(other.0)
92 }
93}
94
95impl fmt::Display for ViewVersion {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "v{}", self.0)
98 }
99}
100
101impl fmt::Debug for ViewVersion {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "ViewVersion({})", self.0)
104 }
105}
106
107#[derive(Clone, Debug)]
113pub struct ViewState {
114 pub epoch: ViewEpoch,
116 pub version: ViewVersion,
118 pub motion: CameraMotionState,
120 pub motion_source: MotionSource,
122 pub transition: TransitionPhase,
124 pub ptz: Option<PtzTelemetry>,
126 pub global_transform: Option<GlobalTransformEstimate>,
128 pub validity: ContextValidity,
130 pub stability_score: f32,
132}
133
134impl ViewState {
135 #[must_use]
137 pub fn fixed_initial() -> Self {
138 Self {
139 epoch: ViewEpoch::INITIAL,
140 version: ViewVersion::INITIAL,
141 motion: CameraMotionState::Stable,
142 motion_source: MotionSource::None,
143 transition: TransitionPhase::Settled,
144 ptz: None,
145 global_transform: None,
146 validity: ContextValidity::Valid,
147 stability_score: clamp_unit(1.0),
148 }
149 }
150
151 #[must_use]
157 pub fn observed_initial() -> Self {
158 Self {
159 epoch: ViewEpoch::INITIAL,
160 version: ViewVersion::INITIAL,
161 motion: CameraMotionState::Unknown,
162 motion_source: MotionSource::None,
163 transition: TransitionPhase::Settled,
164 ptz: None,
165 global_transform: None,
166 validity: ContextValidity::Degraded {
167 reason: crate::validity::DegradationReason::Unknown,
168 },
169 stability_score: clamp_unit(0.0),
170 }
171 }
172}
173
174#[derive(Clone, Debug)]
181pub struct ViewSnapshot {
182 inner: Arc<ViewState>,
183}
184
185impl ViewSnapshot {
186 #[must_use]
188 pub fn new(state: ViewState) -> Self {
189 Self {
190 inner: Arc::new(state),
191 }
192 }
193
194 #[must_use]
196 pub fn epoch(&self) -> ViewEpoch {
197 self.inner.epoch
198 }
199
200 #[must_use]
202 pub fn version(&self) -> ViewVersion {
203 self.inner.version
204 }
205
206 #[must_use]
208 pub fn motion(&self) -> &CameraMotionState {
209 &self.inner.motion
210 }
211
212 #[must_use]
214 pub fn motion_source(&self) -> &MotionSource {
215 &self.inner.motion_source
216 }
217
218 #[must_use]
220 pub fn transition(&self) -> TransitionPhase {
221 self.inner.transition
222 }
223
224 #[must_use]
226 pub fn ptz(&self) -> Option<&PtzTelemetry> {
227 self.inner.ptz.as_ref()
228 }
229
230 #[must_use]
232 pub fn global_transform(&self) -> Option<&GlobalTransformEstimate> {
233 self.inner.global_transform.as_ref()
234 }
235
236 #[must_use]
238 pub fn validity(&self) -> &ContextValidity {
239 &self.inner.validity
240 }
241
242 #[must_use]
244 pub fn stability_score(&self) -> f32 {
245 self.inner.stability_score
246 }
247
248 #[must_use]
250 pub fn as_view_state(&self) -> &ViewState {
251 &self.inner
252 }
253}
254
255fn clamp_unit(v: f32) -> f32 {
256 if v.is_finite() {
257 v.clamp(0.0, 1.0)
258 } else {
259 0.0
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
270 fn view_epoch_initial_is_zero() {
271 assert_eq!(ViewEpoch::INITIAL.as_u64(), 0);
272 }
273
274 #[test]
275 fn view_epoch_next_increments() {
276 let e = ViewEpoch::INITIAL;
277 assert_eq!(e.next().as_u64(), 1);
278 assert_eq!(e.next().next().as_u64(), 2);
279 }
280
281 #[test]
282 fn view_epoch_ordering() {
283 let a = ViewEpoch::new(3);
284 let b = ViewEpoch::new(5);
285 assert!(a < b);
286 assert_eq!(a, ViewEpoch::new(3));
287 }
288
289 #[test]
290 fn view_epoch_display() {
291 assert_eq!(format!("{}", ViewEpoch::new(7)), "epoch-7");
292 }
293
294 #[test]
297 fn view_version_initial_is_zero() {
298 assert_eq!(ViewVersion::INITIAL.as_u64(), 0);
299 }
300
301 #[test]
302 fn view_version_next_increments() {
303 assert_eq!(ViewVersion::INITIAL.next().as_u64(), 1);
304 }
305
306 #[test]
307 fn view_version_versions_since() {
308 let v5 = ViewVersion::new(5);
309 let v2 = ViewVersion::new(2);
310 assert_eq!(v5.versions_since(v2), 3);
311 assert_eq!(v2.versions_since(v5), 0);
313 }
314
315 #[test]
318 fn fixed_initial_is_stable_valid() {
319 let vs = ViewState::fixed_initial();
320 assert_eq!(vs.epoch, ViewEpoch::INITIAL);
321 assert_eq!(vs.motion, CameraMotionState::Stable);
322 assert_eq!(vs.validity, ContextValidity::Valid);
323 assert_eq!(vs.stability_score, 1.0);
324 assert_eq!(vs.transition, TransitionPhase::Settled);
325 }
326
327 #[test]
328 fn observed_initial_is_unknown_degraded() {
329 let vs = ViewState::observed_initial();
330 assert_eq!(vs.motion, CameraMotionState::Unknown);
331 assert!(matches!(vs.validity, ContextValidity::Degraded { .. }));
332 assert_eq!(vs.stability_score, 0.0);
333 }
334
335 #[test]
338 fn snapshot_reflects_state() {
339 let vs = ViewState::fixed_initial();
340 let snap = ViewSnapshot::new(vs);
341 assert_eq!(snap.epoch(), ViewEpoch::INITIAL);
342 assert_eq!(snap.stability_score(), 1.0);
343 assert_eq!(snap.transition(), TransitionPhase::Settled);
344 }
345
346 #[test]
347 fn snapshot_clone_is_cheap() {
348 let snap = ViewSnapshot::new(ViewState::fixed_initial());
349 let snap2 = snap.clone();
350 assert!(std::ptr::eq(snap.as_view_state(), snap2.as_view_state()));
352 }
353}