scena 1.7.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use serde::{Deserialize, Serialize};

use super::reporting::{SceneHostAnimationClipV1, SceneHostAnimationInventoryV1};
use super::{SceneHostCore, SceneHostError, SceneHostErrorCode};
use crate::AssetFetcher;
use crate::animation::{AnimationLoopMode, AnimationMixerKey};
use crate::diagnostics::AnimationError;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SceneHostAnimationLoopMode {
    #[default]
    Once,
    Repeat,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SceneHostAnimationPlayOptions {
    #[serde(default)]
    pub loop_mode: SceneHostAnimationLoopMode,
    #[serde(default = "default_animation_speed")]
    pub speed: f32,
}

impl Default for SceneHostAnimationPlayOptions {
    fn default() -> Self {
        Self {
            loop_mode: SceneHostAnimationLoopMode::Once,
            speed: default_animation_speed(),
        }
    }
}

impl From<SceneHostAnimationLoopMode> for AnimationLoopMode {
    fn from(value: SceneHostAnimationLoopMode) -> Self {
        match value {
            SceneHostAnimationLoopMode::Once => Self::Once,
            SceneHostAnimationLoopMode::Repeat => Self::Repeat,
        }
    }
}

impl<F: AssetFetcher> SceneHostCore<F> {
    pub fn animation_inventory_json(&self, import: u64) -> Result<String, SceneHostError> {
        let import = self.resolve_import(import)?;
        let clips = import
            .clips()?
            .iter()
            .filter_map(|clip| {
                Some(SceneHostAnimationClipV1 {
                    name: clip.name()?.to_owned(),
                    duration_seconds: clip.duration_seconds(),
                    channel_count: clip.channels().len(),
                })
            })
            .collect::<Vec<_>>();
        serde_json::to_string(&SceneHostAnimationInventoryV1::new(clips)).map_err(|error| {
            SceneHostError::new(
                SceneHostErrorCode::Inspect,
                format!("animation inventory serialization failed: {error}"),
            )
        })
    }

    pub fn play_animation(
        &mut self,
        import: u64,
        clip_name: &str,
        options: SceneHostAnimationPlayOptions,
    ) -> Result<u64, SceneHostError> {
        validate_speed(options.speed)?;
        let import = self.resolve_import(import)?.clone();
        let mixer = self
            .scene
            .create_animation_mixer(&import, clip_name)
            .map_err(map_animation_error)?;
        self.scene
            .set_animation_loop_mode(mixer, options.loop_mode.into())
            .map_err(map_animation_error)?;
        self.scene
            .set_animation_speed(mixer, options.speed)
            .map_err(map_animation_error)?;
        self.scene
            .play_animation(mixer)
            .map_err(map_animation_error)?;
        Ok(self.animation_handles.insert(mixer))
    }

    pub fn pause_animation(&mut self, handle: u64) -> Result<(), SceneHostError> {
        let mixer = self.resolve_animation_handle(handle)?;
        self.scene
            .pause_animation(mixer)
            .map_err(map_animation_error)
    }

    pub fn stop_animation(&mut self, handle: u64) -> Result<(), SceneHostError> {
        let mixer = self.resolve_animation_handle(handle)?;
        self.scene
            .stop_animation(mixer)
            .map_err(map_animation_error)
    }

    pub fn seek_animation(&mut self, handle: u64, seconds: f64) -> Result<(), SceneHostError> {
        let seconds = validate_time_seconds("seek seconds", seconds)?;
        let mixer = self.resolve_animation_handle(handle)?;
        let duration = self
            .scene
            .animation_mixer(mixer)
            .map_err(map_animation_error)?
            .clip()
            .duration_seconds();
        if seconds > duration {
            return Err(invalid_input(format!(
                "seek seconds must be <= clip duration {duration}, got {seconds}"
            )));
        }
        self.scene
            .seek_animation(mixer, seconds)
            .map_err(map_animation_error)
    }

    pub fn set_animation_speed(&mut self, handle: u64, speed: f64) -> Result<(), SceneHostError> {
        let speed = validate_speed(speed)?;
        let mixer = self.resolve_animation_handle(handle)?;
        self.scene
            .set_animation_speed(mixer, speed)
            .map_err(map_animation_error)
    }

    pub fn advance(&mut self, delta_seconds: f64) -> Result<(), SceneHostError> {
        let delta_seconds = validate_time_seconds("advance delta_seconds", delta_seconds)?;
        let mixers = self.animation_handles.values().copied().collect::<Vec<_>>();
        for mixer in mixers {
            self.scene
                .update_animation(mixer, delta_seconds)
                .map_err(map_animation_error)?;
        }
        self.advance_transitions(delta_seconds)
    }

    pub(super) fn resolve_animation_handle(
        &self,
        handle: u64,
    ) -> Result<AnimationMixerKey, SceneHostError> {
        self.animation_handles
            .get(
                handle,
                SceneHostErrorCode::AnimationHandleNotFound,
                SceneHostErrorCode::StaleAnimationHandle,
            )
            .copied()
    }
}

fn default_animation_speed() -> f32 {
    1.0
}

fn validate_speed(speed: impl Into<f64>) -> Result<f32, SceneHostError> {
    let speed = speed.into();
    if !speed.is_finite() || speed <= 0.0 || speed > f64::from(f32::MAX) {
        return Err(invalid_input(format!(
            "animation speed must be finite and > 0, got {speed}"
        )));
    }
    Ok(speed as f32)
}

pub(super) fn validate_time_seconds(field: &str, value: f64) -> Result<f32, SceneHostError> {
    if !value.is_finite() || value < 0.0 || value > f64::from(f32::MAX) {
        return Err(invalid_input(format!(
            "{field} must be finite and non-negative, got {value}"
        )));
    }
    Ok(value as f32)
}

pub(super) fn invalid_input(message: impl Into<String>) -> SceneHostError {
    SceneHostError::new(SceneHostErrorCode::InvalidInput, message.into())
}

fn map_animation_error(error: AnimationError) -> SceneHostError {
    match error {
        AnimationError::ClipNotFound { name } => SceneHostError::new(
            SceneHostErrorCode::AnimationClipNotFound,
            format!("animation clip {name} was not found"),
        ),
        AnimationError::MixerNotFound(mixer) => SceneHostError::new(
            SceneHostErrorCode::AnimationHandleNotFound,
            format!("animation mixer {mixer:?} was not found"),
        ),
        AnimationError::StaleMixer(mixer) => SceneHostError::new(
            SceneHostErrorCode::StaleAnimationHandle,
            format!("animation mixer {mixer:?} is stale"),
        ),
    }
}