1use std::sync::{Arc, atomic::AtomicBool};
2
3use crate::animation::{
4 AnimationChannel, AnimationLoopMode, AnimationMixer, AnimationMixerKey, AnimationPlaybackState,
5 AnimationTarget,
6};
7use crate::diagnostics::AnimationError;
8
9use super::{Scene, SceneImport, Transform};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum AppliedAnimationChange {
13 None,
14 Transform,
15}
16
17impl AppliedAnimationChange {
18 const fn transform_changed(self) -> bool {
19 matches!(self, Self::Transform)
20 }
21}
22
23impl Scene {
24 pub fn create_animation_mixer(
29 &mut self,
30 import: &SceneImport,
31 clip_name: &str,
32 ) -> Result<AnimationMixerKey, AnimationError> {
33 let clip = import
34 .clip(clip_name)
35 .map_err(|_| AnimationError::ClipNotFound {
36 name: clip_name.to_string(),
37 })?
38 .clip();
39 Ok(self
40 .animation_mixers
41 .insert(AnimationMixer::new(clip, import.live_flag())))
42 }
43
44 pub fn play_animation_by_name(
67 &mut self,
68 import: &SceneImport,
69 clip_name: &str,
70 ) -> Result<AnimationMixerKey, AnimationError> {
71 let mixer = self.create_animation_mixer(import, clip_name)?;
72 self.play_animation(mixer)?;
73 Ok(mixer)
74 }
75
76 pub fn create_authored_animation_mixer(
83 &mut self,
84 clip: crate::animation::AnimationClip,
85 ) -> Result<AnimationMixerKey, AnimationError> {
86 validate_authored_clip(&clip)?;
87 Ok(self
88 .animation_mixers
89 .insert(AnimationMixer::new(clip, Arc::new(AtomicBool::new(true)))))
90 }
91
92 pub fn play_authored_animation(
94 &mut self,
95 clip: crate::animation::AnimationClip,
96 ) -> Result<AnimationMixerKey, AnimationError> {
97 let mixer = self.create_authored_animation_mixer(clip)?;
98 self.play_animation(mixer)?;
99 Ok(mixer)
100 }
101
102 pub fn animation_mixer(
104 &self,
105 mixer: AnimationMixerKey,
106 ) -> Result<&AnimationMixer, AnimationError> {
107 self.animation_mixers
108 .get(mixer)
109 .ok_or(AnimationError::MixerNotFound(mixer))
110 }
111
112 pub fn play_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
114 self.animation_mixer_mut(mixer)?.play();
115 Ok(())
116 }
117
118 pub fn pause_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
121 self.animation_mixer_mut(mixer)?.pause();
122 Ok(())
123 }
124
125 pub fn stop_animation(&mut self, mixer: AnimationMixerKey) -> Result<(), AnimationError> {
127 let clip = {
128 let mixer = self.animation_mixer_mut(mixer)?;
129 mixer.stop();
130 mixer.clip().clone()
131 };
132 self.apply_animation_clip(&clip, 0.0);
133 Ok(())
134 }
135
136 pub fn seek_animation(
139 &mut self,
140 mixer: AnimationMixerKey,
141 time_seconds: f32,
142 ) -> Result<(), AnimationError> {
143 let (clip, time_seconds) = {
144 let mixer = self.animation_mixer_mut(mixer)?;
145 mixer.seek(time_seconds);
146 (mixer.clip().clone(), mixer.time_seconds())
147 };
148 self.apply_animation_clip(&clip, time_seconds);
149 Ok(())
150 }
151
152 pub fn set_animation_speed(
155 &mut self,
156 mixer: AnimationMixerKey,
157 speed: f32,
158 ) -> Result<(), AnimationError> {
159 self.animation_mixer_mut(mixer)?.set_speed(speed);
160 Ok(())
161 }
162
163 pub fn set_animation_loop_mode(
165 &mut self,
166 mixer: AnimationMixerKey,
167 loop_mode: AnimationLoopMode,
168 ) -> Result<(), AnimationError> {
169 self.animation_mixer_mut(mixer)?.set_loop_mode(loop_mode);
170 Ok(())
171 }
172
173 pub fn update_animation(
177 &mut self,
178 mixer: AnimationMixerKey,
179 delta_seconds: f32,
180 ) -> Result<(), AnimationError> {
181 let (clip, time_seconds, was_playing) = {
182 let mixer = self.animation_mixer_mut(mixer)?;
183 let was_playing = mixer.state() == AnimationPlaybackState::Playing;
184 mixer.advance(delta_seconds);
185 (mixer.clip().clone(), mixer.time_seconds(), was_playing)
186 };
187 if was_playing {
188 self.apply_animation_clip(&clip, time_seconds);
189 }
190 Ok(())
191 }
192
193 fn animation_mixer_mut(
194 &mut self,
195 mixer: AnimationMixerKey,
196 ) -> Result<&mut AnimationMixer, AnimationError> {
197 let mixer_state = self
198 .animation_mixers
199 .get_mut(mixer)
200 .ok_or(AnimationError::MixerNotFound(mixer))?;
201 if mixer_state.is_stale() {
202 return Err(AnimationError::StaleMixer(mixer));
203 }
204 Ok(mixer_state)
205 }
206
207 fn apply_animation_clip(&mut self, clip: &crate::animation::AnimationClip, time_seconds: f32) {
208 let mut transform_changed = false;
209 for channel in clip.channels() {
210 transform_changed |= self
211 .apply_animation_channel(channel, time_seconds)
212 .transform_changed();
213 }
214 if transform_changed {
215 self.transform_revision = self.transform_revision.saturating_add(1);
216 }
217 }
218
219 fn apply_animation_channel(
220 &mut self,
221 channel: &AnimationChannel,
222 time_seconds: f32,
223 ) -> AppliedAnimationChange {
224 if channel.target() == AnimationTarget::Weights {
225 let Some(weights) = channel.sample_weights(time_seconds) else {
226 return AppliedAnimationChange::None;
227 };
228 self.set_morph_weights_unchecked(channel.target_node(), weights);
229 return AppliedAnimationChange::None;
230 }
231
232 let Some(node) = self.nodes.get_mut(channel.target_node()) else {
233 return AppliedAnimationChange::None;
234 };
235 let before = node.transform;
236 let mut transform = before;
237 match channel.target() {
238 AnimationTarget::Translation => {
239 let Some(value) = channel.sample_vec3(time_seconds) else {
240 return AppliedAnimationChange::None;
241 };
242 transform.translation = value;
243 }
244 AnimationTarget::Scale => {
245 let Some(value) = channel.sample_vec3(time_seconds) else {
246 return AppliedAnimationChange::None;
247 };
248 transform.scale = value;
249 }
250 AnimationTarget::Rotation => {
251 let Some(value) = channel.sample_quat(time_seconds) else {
252 return AppliedAnimationChange::None;
253 };
254 transform.rotation = value;
255 }
256 AnimationTarget::Weights => unreachable!("weights handled before mutable node borrow"),
257 }
258 if before == transform {
259 return AppliedAnimationChange::None;
260 }
261 node.transform = Transform { ..transform };
262 AppliedAnimationChange::Transform
263 }
264}
265
266fn validate_authored_clip(clip: &crate::animation::AnimationClip) -> Result<(), AnimationError> {
267 if !clip.duration_seconds().is_finite() || clip.duration_seconds() <= 0.0 {
268 return Err(AnimationError::InvalidClip {
269 reason: "duration_seconds must be finite and positive".to_owned(),
270 });
271 }
272 if clip.channels().is_empty() {
273 return Err(AnimationError::InvalidClip {
274 reason: "clip must contain at least one channel".to_owned(),
275 });
276 }
277 Ok(())
278}
279
280#[cfg(test)]
281impl Scene {
282 pub(crate) fn insert_animation_mixer_for_test(
283 &mut self,
284 clip: crate::animation::AnimationClip,
285 ) -> AnimationMixerKey {
286 self.animation_mixers
287 .insert(AnimationMixer::new(clip, Arc::new(AtomicBool::new(true))))
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use crate::animation::{
294 AnimationChannel, AnimationClip, AnimationClipKey, AnimationInterpolation, AnimationOutput,
295 AnimationTarget,
296 };
297 use crate::scene::{Scene, Transform, Vec3};
298
299 #[test]
300 fn transform_only_animation_updates_transform_revision_without_structure_dirtying() {
301 let mut scene = Scene::new();
302 let node = scene
303 .add_empty(scene.root(), Transform::IDENTITY)
304 .expect("animated node inserts");
305 let mixer = scene.insert_animation_mixer_for_test(translation_clip(node));
306 scene.play_animation(mixer).expect("mixer starts");
307 let before = scene.dirty_state();
308
309 for expected_transform_revision in 1..=3 {
310 let frame_before = scene.dirty_state();
311 scene
312 .update_animation(mixer, 0.25)
313 .expect("animation frame updates");
314 let frame_after = scene.dirty_state();
315 assert_eq!(
316 frame_after.structure_revision, frame_before.structure_revision,
317 "transform-only animation frames must not dirty scene structure"
318 );
319 assert_eq!(
320 frame_after.transform_revision,
321 frame_before.transform_revision + 1,
322 "changed transform animation frames must bump transform revision exactly once"
323 );
324 assert_eq!(
325 frame_after.transform_revision,
326 before.transform_revision + expected_transform_revision,
327 );
328 }
329
330 let after = scene.dirty_state();
331 assert_eq!(
332 after.structure_revision, before.structure_revision,
333 "transform-only animation playback must preserve structure revision across frames"
334 );
335 }
336
337 #[test]
338 fn morph_weight_animation_keeps_structural_revision_semantics() {
339 let mut scene = Scene::new();
340 let node = scene
341 .add_empty(scene.root(), Transform::IDENTITY)
342 .expect("morph node inserts");
343 scene.set_initial_morph_weights(node, &[0.0]);
344 let mixer = scene.insert_animation_mixer_for_test(weight_clip(node));
345 let before = scene.dirty_state();
346
347 scene
348 .seek_animation(mixer, 1.0)
349 .expect("morph weight seek applies");
350 let after = scene.dirty_state();
351
352 assert_eq!(
353 after.transform_revision, before.transform_revision,
354 "morph weight animation must not masquerade as a transform-only update"
355 );
356 assert!(
357 after.structure_revision > before.structure_revision,
358 "morph weight animation remains structural because vertex deformation changes"
359 );
360 }
361
362 fn translation_clip(node: crate::scene::NodeKey) -> AnimationClip {
363 AnimationClip::new(
364 AnimationClipKey::fresh(),
365 Some("MoveX".to_string()),
366 vec![AnimationChannel::new(
367 node,
368 AnimationTarget::Translation,
369 vec![0.0, 1.0],
370 AnimationOutput::Vec3(vec![Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0)]),
371 AnimationInterpolation::Linear,
372 )],
373 1.0,
374 )
375 .expect("test translation clip is valid")
376 }
377
378 fn weight_clip(node: crate::scene::NodeKey) -> AnimationClip {
379 AnimationClip::new(
380 AnimationClipKey::fresh(),
381 Some("Morph".to_string()),
382 vec![AnimationChannel::new(
383 node,
384 AnimationTarget::Weights,
385 vec![0.0, 1.0],
386 AnimationOutput::Weights(vec![vec![0.0], vec![1.0]]),
387 AnimationInterpolation::Linear,
388 )],
389 1.0,
390 )
391 .expect("test weight clip is valid")
392 }
393}