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}