1use std::error::Error as StdError;
2use std::f32::consts::{PI, TAU};
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use super::OrbitControls;
8use crate::geometry::Aabb;
9use crate::scene::{FramingOutcome, Vec3};
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct CameraState {
14 pub target: Vec3,
15 pub distance: f32,
16 pub yaw_radians: f32,
17 pub pitch_radians: f32,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct CameraBookmark {
23 pub name: String,
24 pub state: CameraState,
25 pub target_bounds: Option<Aabb>,
26 pub description: Option<String>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(rename_all = "snake_case")]
32pub enum TransitionEasing {
33 #[default]
34 Linear,
35 EaseInOut,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct CameraFlyTo {
41 start: CameraState,
42 target: CameraState,
43 elapsed_seconds: f32,
44 duration_seconds: f32,
45 easing: TransitionEasing,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub enum CameraTransitionError {
51 InvalidCameraState { message: &'static str },
52 InvalidDurationSeconds,
53}
54
55impl CameraState {
56 pub fn from_controls(controls: &OrbitControls) -> Self {
57 Self {
58 target: controls.target(),
59 distance: controls.distance(),
60 yaw_radians: controls.yaw_radians(),
61 pitch_radians: controls.pitch_radians(),
62 }
63 }
64
65 pub const fn from_framing(framing: FramingOutcome) -> Self {
66 Self {
67 target: framing.target,
68 distance: framing.distance,
69 yaw_radians: framing.yaw_radians,
70 pitch_radians: framing.pitch_radians,
71 }
72 }
73
74 pub fn validate(self) -> Result<(), &'static str> {
75 if !self.target.to_array().into_iter().all(f32::is_finite) {
76 return Err("camera target must contain finite values");
77 }
78 if !self.distance.is_finite() || self.distance <= 0.0 {
79 return Err("camera distance must be finite and greater than zero");
80 }
81 if !self.yaw_radians.is_finite() {
82 return Err("camera yaw must be finite");
83 }
84 if !self.pitch_radians.is_finite() {
85 return Err("camera pitch must be finite");
86 }
87 Ok(())
88 }
89
90 pub fn into_controls(self) -> OrbitControls {
91 OrbitControls::new(self.target, self.distance)
92 .with_angles(self.yaw_radians, self.pitch_radians)
93 }
94
95 pub fn interpolate_to(self, target: Self, amount: f32) -> Self {
96 let amount = amount.clamp(0.0, 1.0);
97 let mix = |left: f32, right: f32| left + (right - left) * amount;
98 let yaw_delta = shortest_angle_delta(self.yaw_radians, target.yaw_radians);
99 Self {
100 target: self.target.lerp(target.target, amount),
101 distance: mix(self.distance, target.distance),
102 yaw_radians: self.yaw_radians + yaw_delta * amount,
103 pitch_radians: mix(self.pitch_radians, target.pitch_radians),
104 }
105 }
106}
107
108impl CameraBookmark {
109 pub fn new(name: impl Into<String>, state: CameraState) -> Self {
110 Self {
111 name: name.into(),
112 state,
113 target_bounds: None,
114 description: None,
115 }
116 }
117
118 pub fn from_framing(name: impl Into<String>, framing: FramingOutcome) -> Self {
119 Self::new(name, CameraState::from_framing(framing))
120 }
121
122 pub fn with_target_bounds(mut self, bounds: Aabb) -> Self {
123 self.target_bounds = Some(bounds);
124 self
125 }
126
127 pub fn with_description(mut self, description: impl Into<String>) -> Self {
128 self.description = Some(description.into());
129 self
130 }
131
132 pub fn name(&self) -> &str {
133 &self.name
134 }
135
136 pub const fn state(&self) -> CameraState {
137 self.state
138 }
139
140 pub const fn target_bounds(&self) -> Option<Aabb> {
141 self.target_bounds
142 }
143
144 pub fn description(&self) -> Option<&str> {
145 self.description.as_deref()
146 }
147}
148
149impl OrbitControls {
150 pub fn camera_state(&self) -> CameraState {
151 CameraState::from_controls(self)
152 }
153
154 pub fn fly_to(
155 &self,
156 target: CameraState,
157 easing: TransitionEasing,
158 duration_seconds: f64,
159 ) -> Result<CameraFlyTo, CameraTransitionError> {
160 let start = self.camera_state();
161 start
162 .validate()
163 .map_err(|message| CameraTransitionError::InvalidCameraState { message })?;
164 target
165 .validate()
166 .map_err(|message| CameraTransitionError::InvalidCameraState { message })?;
167 if !duration_seconds.is_finite() || duration_seconds < 0.0 {
168 return Err(CameraTransitionError::InvalidDurationSeconds);
169 }
170 let duration_seconds = duration_seconds.min(f32::MAX as f64) as f32;
171 let elapsed_seconds = if duration_seconds == 0.0 || start == target {
172 duration_seconds
173 } else {
174 0.0
175 };
176 Ok(CameraFlyTo {
177 start,
178 target,
179 elapsed_seconds,
180 duration_seconds,
181 easing,
182 })
183 }
184}
185
186impl CameraFlyTo {
187 pub fn advance(&mut self, delta_seconds: f32) -> OrbitControls {
188 if delta_seconds.is_finite() && delta_seconds > 0.0 {
189 self.elapsed_seconds =
190 (self.elapsed_seconds + delta_seconds).min(self.duration_seconds);
191 }
192 self.sample()
193 }
194
195 pub fn sample(&self) -> OrbitControls {
196 self.sample_state().into_controls()
197 }
198
199 pub fn sample_state(&self) -> CameraState {
200 if self.is_complete() {
201 return self.target;
202 }
203 let amount = eased_amount(
204 self.elapsed_seconds / self.duration_seconds.max(f32::EPSILON),
205 self.easing,
206 );
207 self.start.interpolate_to(self.target, amount)
208 }
209
210 pub fn is_complete(&self) -> bool {
211 self.elapsed_seconds >= self.duration_seconds
212 }
213}
214
215impl fmt::Display for CameraTransitionError {
216 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match self {
218 Self::InvalidCameraState { message } => write!(formatter, "{message}"),
219 Self::InvalidDurationSeconds => {
220 write!(
221 formatter,
222 "camera transition duration_seconds must be finite and non-negative"
223 )
224 }
225 }
226 }
227}
228
229impl StdError for CameraTransitionError {}
230
231pub(crate) fn eased_amount(amount: f32, easing: TransitionEasing) -> f32 {
232 let amount = amount.clamp(0.0, 1.0);
233 match easing {
234 TransitionEasing::Linear => amount,
235 TransitionEasing::EaseInOut => {
236 if amount < 0.5 {
237 4.0 * amount * amount * amount
238 } else {
239 1.0 - (-2.0 * amount + 2.0).powi(3) / 2.0
240 }
241 }
242 }
243}
244
245fn shortest_angle_delta(start: f32, target: f32) -> f32 {
246 let delta = (target - start).rem_euclid(TAU);
247 if delta > PI { delta - TAU } else { delta }
248}