Skip to main content

teksilo_scene/
state.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! [`SceneViewState`] — a snapshot of a [`SceneView`](crate::SceneView)'s
5//! pan / zoom / rotation, suitable for persistence between sessions.
6//!
7//! ## Pattern
8//!
9//! ```ignore
10//! use teksilo_scene::{Scene, SceneView, SceneViewState};
11//!
12//! // On load: read from your persistence layer (teksilo-settings,
13//! // a custom JSON file, etc.) and pass to SceneView.
14//! let saved: SceneViewState = my_settings.scene_view.get();
15//! let view = SceneView::new(scene);
16//! view.restore_state(saved);
17//!
18//! // On exit / periodic flush: snapshot and persist.
19//! let current: SceneViewState = view.state();
20//! my_settings.scene_view.set(current);
21//! ```
22//!
23//! ## Why a plain struct, not Serialize
24//!
25//! `teksilo-scene` deliberately doesn't depend on `serde`. Apps that
26//! want to persist via `teksilo-settings` (which is `serde`-based)
27//! either:
28//!
29//! - Add their own newtype wrapper that implements
30//!   `Serialize / Deserialize`, OR
31//! - Store the fields individually (`pan_x`, `pan_y`, `zoom`,
32//!   `rotation`) as scalar `SettingsKey<f32>`s in a
33//!   `SettingsStore`.
34//!
35//! The struct is plain-old-data — manual round-trip is trivial.
36
37use teksilo_canvas::Vec2;
38
39/// Snapshot of a SceneView's view transform: pan offset, zoom
40/// factor, and rotation in radians. Use `SceneView::state` to
41/// capture the current values; `SceneView::restore_state` to
42/// apply a saved snapshot.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct SceneViewState {
45    /// X pan offset (the same value driven by the SceneView's
46    /// `pan_x` signal).
47    pub pan_x: f32,
48    /// Y pan offset.
49    pub pan_y: f32,
50    /// Zoom factor (1.0 = identity).
51    pub zoom: f32,
52    /// Rotation in radians (0.0 = no rotation).
53    pub rotation: f32,
54}
55
56impl SceneViewState {
57    /// The identity view state: no pan, zoom 1.0, no rotation.
58    pub const IDENTITY: SceneViewState = SceneViewState {
59        pan_x: 0.0,
60        pan_y: 0.0,
61        zoom: 1.0,
62        rotation: 0.0,
63    };
64
65    /// Construct a new state with the given pan / zoom / rotation.
66    pub fn new(pan: Vec2, zoom: f32, rotation: f32) -> Self {
67        Self {
68            pan_x: pan.x,
69            pan_y: pan.y,
70            zoom,
71            rotation,
72        }
73    }
74
75    /// Pan offset as a [`Vec2`].
76    pub fn pan(&self) -> Vec2 {
77        Vec2::new(self.pan_x, self.pan_y)
78    }
79
80    /// Whether this state is the identity (no pan, zoom 1.0, no
81    /// rotation). Useful for skipping persistence of fresh-default
82    /// SceneViews.
83    pub fn is_identity(&self) -> bool {
84        self.pan_x == 0.0 && self.pan_y == 0.0 && self.zoom == 1.0 && self.rotation == 0.0
85    }
86}
87
88impl Default for SceneViewState {
89    fn default() -> Self {
90        Self::IDENTITY
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn identity_round_trip() {
100        let s = SceneViewState::IDENTITY;
101        assert!(s.is_identity());
102        assert_eq!(s.pan(), Vec2::ZERO);
103        assert_eq!(s.zoom, 1.0);
104        assert_eq!(s.rotation, 0.0);
105    }
106
107    #[test]
108    fn non_identity_state_constructs_correctly() {
109        let s = SceneViewState::new(Vec2::new(10.0, 20.0), 1.5, 0.1);
110        assert!(!s.is_identity());
111        assert_eq!(s.pan_x, 10.0);
112        assert_eq!(s.pan_y, 20.0);
113        assert_eq!(s.zoom, 1.5);
114        assert_eq!(s.rotation, 0.1);
115    }
116}