rustmotion_core/vars/
scope.rs1use crate::expr::Scope;
2
3use super::track::VarTable;
4
5pub struct VarScope<'a> {
6 scenario: &'a VarTable,
7 scene: Option<&'a VarTable>,
8 t: f64,
9}
10
11impl<'a> VarScope<'a> {
12 pub fn new(scenario: &'a VarTable, scene: Option<&'a VarTable>, t: f64) -> Self {
13 VarScope { scenario, scene, t }
14 }
15
16 pub fn resolve(&self, name: &str) -> Option<f64> {
17 self.scene
18 .and_then(|scene| scene.value_at(name, self.t))
19 .or_else(|| self.scenario.value_at(name, self.t))
20 }
21}
22
23impl Scope for VarScope<'_> {
24 fn var(&self, name: &str) -> Option<f64> {
25 self.resolve(name)
26 }
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use crate::expr::Expr;
33 use crate::schema::animation::EasingType;
34 use crate::schema::time::{TimeCtx, TimePoint};
35 use crate::vars::schema::{VarDef, VarKeyframe, VarSet};
36
37 fn ctx() -> TimeCtx {
38 TimeCtx {
39 bpm: None,
40 beat_offset: 0.0,
41 scene_start: 0.0,
42 }
43 }
44
45 fn table_with(name: &str, def: VarDef) -> VarTable {
46 let mut vars = VarSet::new();
47 vars.insert(name.to_string(), def);
48 VarTable::compile(&vars, &ctx()).unwrap()
49 }
50
51 #[test]
52 fn expression_reads_a_scenario_level_variable() {
53 let scenario = table_with(
54 "keyDraw",
55 VarDef {
56 default: 0.0,
57 animation: vec![VarKeyframe {
58 at: TimePoint::Seconds(0.0),
59 to: 1.0,
60 duration: TimePoint::Seconds(1.0),
61 easing: EasingType::Linear,
62 }],
63 },
64 );
65 let scope = VarScope::new(&scenario, None, 0.5);
66
67 let expr = Expr::parse("= $keyDraw * 360").unwrap();
68 assert!(expr.is_static());
69 assert_eq!(expr.eval(&scope).unwrap(), 180.0);
70 }
71
72 #[test]
73 fn scene_level_variable_shadows_scenario_level() {
74 let scenario = table_with(
75 "speed",
76 VarDef {
77 default: 1.0,
78 animation: Vec::new(),
79 },
80 );
81 let scene = table_with(
82 "speed",
83 VarDef {
84 default: 9.0,
85 animation: Vec::new(),
86 },
87 );
88
89 let shadowed = VarScope::new(&scenario, Some(&scene), 0.0);
90 assert_eq!(shadowed.resolve("speed"), Some(9.0));
91
92 let unshadowed = VarScope::new(&scenario, None, 0.0);
93 assert_eq!(unshadowed.resolve("speed"), Some(1.0));
94 }
95
96 #[test]
97 fn scene_local_variable_is_invisible_outside_its_scene() {
98 let scenario = VarTable::compile(&VarSet::new(), &ctx()).unwrap();
99 let scene_b = table_with(
100 "onlyInB",
101 VarDef {
102 default: 5.0,
103 animation: Vec::new(),
104 },
105 );
106 let _ = &scene_b;
107
108 let scope_in_scene_a = VarScope::new(&scenario, None, 0.0);
109 assert_eq!(scope_in_scene_a.resolve("onlyInB"), None);
110
111 let expr = Expr::parse("= $onlyInB").unwrap();
112 let err = expr.eval(&scope_in_scene_a).unwrap_err();
113 assert_eq!(
114 err,
115 crate::expr::ExprError::UnknownIdent("onlyInB".to_string())
116 );
117 }
118
119 #[test]
120 fn a_host_scope_can_hold_a_var_scope_and_delegate_to_resolve() {
121 struct EngineScope<'a> {
122 vars: VarScope<'a>,
123 }
124
125 impl crate::expr::Scope for EngineScope<'_> {
126 fn var(&self, name: &str) -> Option<f64> {
127 self.vars.resolve(name)
128 }
129
130 fn node_prop(&self, id: &str, prop: &str) -> Option<f64> {
131 let _ = (id, prop);
132 None
133 }
134 }
135
136 let scenario = table_with(
137 "W",
138 VarDef {
139 default: 1080.0,
140 animation: Vec::new(),
141 },
142 );
143 let host = EngineScope {
144 vars: VarScope::new(&scenario, None, 0.0),
145 };
146 assert_eq!(crate::expr::Scope::var(&host, "W"), Some(1080.0));
147 assert_eq!(crate::expr::Scope::node_prop(&host, "n", "x"), None);
148 }
149}