rustmotion_core/vars/
mod.rs1pub mod schema;
2pub mod scope;
3pub mod track;
4
5pub use schema::{dynamic_names, VarDef, VarKeyframe, VarSet};
6pub use scope::VarScope;
7pub use track::{VarTable, VarTrack, VarsError};
8
9#[cfg(test)]
10mod tests {
11 use super::*;
12 use crate::expr::Expr;
13 use crate::schema::animation::EasingType;
14 use crate::schema::time::{TimeCtx, TimePoint};
15 use std::f64::consts::TAU;
16
17 #[test]
18 fn wireframe_edges_rotate_via_a_shared_variable() {
19 let mut vars = VarSet::new();
20 vars.insert(
21 "rotY".to_string(),
22 VarDef {
23 default: 0.0,
24 animation: vec![VarKeyframe {
25 at: TimePoint::Seconds(0.0),
26 to: TAU,
27 duration: TimePoint::Seconds(4.0),
28 easing: EasingType::Linear,
29 }],
30 },
31 );
32 let ctx = TimeCtx {
33 bpm: None,
34 beat_offset: 0.0,
35 scene_start: 0.0,
36 };
37 let scenario = VarTable::compile(&vars, &ctx).unwrap();
38
39 const EDGE_COUNT: usize = 8;
40 assert_eq!(dynamic_names(&vars).collect::<Vec<_>>(), vec!["rotY"]);
41
42 let edge_exprs: Vec<Expr> = (0..EDGE_COUNT)
43 .map(|i| {
44 let angle = i as f64 / EDGE_COUNT as f64 * TAU;
45 Expr::parse(&format!("= (cos($rotY + {angle}) + 1) / 2")).unwrap()
46 })
47 .collect();
48
49 let sample_times = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
50 let mut frames: Vec<Vec<f64>> = Vec::new();
51 for &t in &sample_times {
52 let scope = VarScope::new(&scenario, None, t);
53
54 let rot_y = scope.resolve("rotY").unwrap();
55 let expected_rot_y = TAU * (t / 4.0).min(1.0);
56 assert!((rot_y - expected_rot_y).abs() < 1e-9);
57
58 let opacities: Vec<f64> = edge_exprs.iter().map(|e| e.eval(&scope).unwrap()).collect();
59 for (i, &opacity) in opacities.iter().enumerate() {
60 let angle = i as f64 / EDGE_COUNT as f64 * TAU;
61 let expected = (f64::cos(rot_y + angle) + 1.0) / 2.0;
62 assert!((opacity - expected).abs() < 1e-9);
63 }
64 frames.push(opacities);
65 }
66
67 for (i, pair) in frames.windows(2).enumerate() {
68 if i < 4 {
69 assert_ne!(
70 pair[0],
71 pair[1],
72 "wireframe did not move between sampled frames {i} and {}",
73 i + 1
74 );
75 }
76 }
77 assert_eq!(
78 frames[4], frames[5],
79 "must hold, not reset, past the last keyframe"
80 );
81 }
82
83 #[test]
84 fn unanimated_variable_is_classified_static_and_folds() {
85 let mut vars = VarSet::new();
86 vars.insert(
87 "constant".to_string(),
88 VarDef {
89 default: 7.0,
90 animation: Vec::new(),
91 },
92 );
93 vars.insert(
94 "rotY".to_string(),
95 VarDef {
96 default: 0.0,
97 animation: vec![VarKeyframe {
98 at: TimePoint::Seconds(0.0),
99 to: 1.0,
100 duration: TimePoint::Seconds(1.0),
101 easing: EasingType::Linear,
102 }],
103 },
104 );
105
106 let dynamic: Vec<&str> = dynamic_names(&vars).collect();
107 assert_eq!(dynamic, vec!["rotY"]);
108
109 let constant_expr = Expr::parse("= $constant * 2").unwrap();
110 assert!(constant_expr.is_static());
111
112 let ctx = TimeCtx {
113 bpm: None,
114 beat_offset: 0.0,
115 scene_start: 0.0,
116 };
117 let table = VarTable::compile(&vars, &ctx).unwrap();
118 let scope = VarScope::new(&table, None, 0.0);
119 assert_eq!(constant_expr.eval(&scope).unwrap(), 14.0);
120 }
121}