use std::collections::HashSet;
use crate::transforms::TransformKey;
use super::clip_group::{AnimationClipKey, AnimationTarget};
#[derive(Debug, Clone)]
pub enum LayerMode {
Replace,
Additive {
base_clip: Option<AnimationClipKey>,
},
}
#[derive(Debug, Clone)]
pub struct AnimationStrip {
pub clip: AnimationClipKey,
pub start: f64,
pub len: f64,
pub scale: f64,
pub repeat: bool,
}
impl AnimationStrip {
pub fn is_active(&self, time: f64) -> bool {
time >= self.start && time <= self.start + self.len
}
pub fn local_time(&self, time: f64, duration: f64) -> f64 {
let scale = if self.scale.is_finite() && self.scale > 0.0 {
self.scale
} else {
1.0
};
let local = (time - self.start) / scale;
if self.repeat && duration > 0.0 {
local.rem_euclid(duration)
} else {
local.clamp(0.0, duration.max(0.0))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TargetMask {
pub transforms: HashSet<TransformKey>,
}
impl TargetMask {
pub fn contains(&self, target: AnimationTarget) -> bool {
match target {
AnimationTarget::Transform(key) => self.transforms.contains(&key),
_ => true,
}
}
}
#[derive(Debug, Clone)]
pub struct AnimationLayer {
pub mode: LayerMode,
pub weight: f64,
pub mask: Option<TargetMask>,
pub strips: Vec<AnimationStrip>,
}
impl AnimationLayer {
pub fn new_replace(strips: Vec<AnimationStrip>) -> Self {
Self {
mode: LayerMode::Replace,
weight: 1.0,
mask: None,
strips,
}
}
pub fn new_additive(base_clip: Option<AnimationClipKey>, strips: Vec<AnimationStrip>) -> Self {
Self {
mode: LayerMode::Additive { base_clip },
weight: 1.0,
mask: None,
strips,
}
}
pub fn admits(&self, target: AnimationTarget) -> bool {
match &self.mask {
Some(mask) => mask.contains(target),
None => true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AnimationMixer {
pub layers: Vec<AnimationLayer>,
time: f64,
}
impl AnimationMixer {
pub fn new() -> Self {
Self::default()
}
pub fn time(&self) -> f64 {
self.time
}
pub fn set_time(&mut self, time: f64) {
self.time = time;
}
pub fn advance(&mut self, dt_seconds: f64) {
self.time += dt_seconds;
}
pub fn clear(&mut self) {
self.layers.clear();
self.time = 0.0;
}
pub fn is_empty(&self) -> bool {
self.layers.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::{Key, KeyData};
fn clip_key(idx: u64) -> AnimationClipKey {
AnimationClipKey::from(KeyData::from_ffi(idx))
}
fn tkey(idx: u64) -> TransformKey {
TransformKey::from(KeyData::from_ffi(idx))
}
#[test]
fn mixer_advance_and_seek() {
let mut m = AnimationMixer::new();
assert!(m.is_empty());
assert_eq!(m.time(), 0.0);
m.advance(0.5);
m.advance(0.25);
assert!((m.time() - 0.75).abs() < 1e-12);
m.set_time(3.0);
assert!((m.time() - 3.0).abs() < 1e-12);
m.advance(100.0);
assert!((m.time() - 103.0).abs() < 1e-12);
}
#[test]
fn mixer_clear_resets_time_and_layers() {
let mut m = AnimationMixer::new();
m.layers.push(AnimationLayer::new_replace(vec![]));
m.set_time(5.0);
assert!(!m.is_empty());
m.clear();
assert!(m.is_empty());
assert_eq!(m.time(), 0.0);
}
#[test]
fn strip_active_window() {
let strip = AnimationStrip {
clip: clip_key(1),
start: 2.0,
len: 3.0,
scale: 1.0,
repeat: false,
};
assert!(!strip.is_active(1.999));
assert!(strip.is_active(2.0)); assert!(strip.is_active(3.5));
assert!(strip.is_active(5.0)); assert!(!strip.is_active(5.001));
}
#[test]
fn strip_local_time_scale() {
let strip = AnimationStrip {
clip: clip_key(1),
start: 1.0,
len: 10.0,
scale: 2.0,
repeat: false,
};
let local = strip.local_time(3.0, 100.0); assert!((local - 1.0).abs() < 1e-12);
}
#[test]
fn strip_local_time_repeat_wraps() {
let strip = AnimationStrip {
clip: clip_key(1),
start: 0.0,
len: 100.0,
scale: 1.0,
repeat: true,
};
let local = strip.local_time(5.0, 2.0);
assert!((local - 1.0).abs() < 1e-12, "got {local}");
}
#[test]
fn strip_local_time_clamp_when_not_repeat() {
let strip = AnimationStrip {
clip: clip_key(1),
start: 0.0,
len: 100.0,
scale: 1.0,
repeat: false,
};
assert!((strip.local_time(5.0, 2.0) - 2.0).abs() < 1e-12);
assert!(strip.local_time(-1.0, 2.0).abs() < 1e-12);
}
#[test]
fn strip_local_time_zero_duration_repeat_is_zero() {
let strip = AnimationStrip {
clip: clip_key(1),
start: 0.0,
len: 10.0,
scale: 1.0,
repeat: true,
};
assert_eq!(strip.local_time(5.0, 0.0), 0.0);
}
#[test]
fn strip_local_time_bad_scale_falls_back_to_one() {
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let strip = AnimationStrip {
clip: clip_key(1),
start: 0.0,
len: 10.0,
scale: bad,
repeat: false,
};
assert!(
(strip.local_time(3.0, 100.0) - 3.0).abs() < 1e-12,
"bad={bad}"
);
}
}
#[test]
fn mask_gates_transforms_only() {
let mut mask = TargetMask::default();
let in_set = tkey(1);
let out_set = tkey(2);
mask.transforms.insert(in_set);
assert!(mask.contains(AnimationTarget::Transform(in_set)));
assert!(!mask.contains(AnimationTarget::Transform(out_set)));
let morph = AnimationTarget::Morph(super::super::animations::AnimationMorphKey::Geometry(
crate::meshes::morphs::GeometryMorphKey::null(),
));
assert!(mask.contains(morph));
}
#[test]
fn empty_mask_matches_no_transform() {
let mask = TargetMask::default();
assert!(!mask.contains(AnimationTarget::Transform(tkey(1))));
let cam = AnimationTarget::Camera {
camera: crate::cameras::CameraKey::null(),
param: crate::animation::CameraParam::FovY,
};
assert!(mask.contains(cam));
}
#[test]
fn layer_admits_respects_optional_mask() {
let strips = vec![];
let no_mask = AnimationLayer::new_replace(strips.clone());
assert!(no_mask.admits(AnimationTarget::Transform(tkey(7))));
let mut mask = TargetMask::default();
mask.transforms.insert(tkey(3));
let masked = AnimationLayer {
mode: LayerMode::Replace,
weight: 1.0,
mask: Some(mask),
strips,
};
assert!(masked.admits(AnimationTarget::Transform(tkey(3))));
assert!(!masked.admits(AnimationTarget::Transform(tkey(4))));
}
}