1use crate::engine::HumanEngine;
11use crate::params::ParamState;
12
13#[derive(Debug, Clone)]
15pub struct ExpressionComponent {
16 pub target_name: String,
18 pub weight: f32,
20}
21
22impl ExpressionComponent {
23 fn new(target_name: &str, weight: f32) -> Self {
24 Self {
25 target_name: target_name.to_string(),
26 weight,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct ExpressionPreset {
34 pub name: String,
35 pub components: Vec<ExpressionComponent>,
36}
37
38impl ExpressionPreset {
39 fn new(name: &str, components: Vec<ExpressionComponent>) -> Self {
40 Self {
41 name: name.to_string(),
42 components,
43 }
44 }
45
46 pub fn all() -> Vec<ExpressionPreset> {
51 vec![
52 ExpressionPreset::new("neutral", vec![]),
54 ExpressionPreset::new(
56 "smile",
57 vec![
58 ExpressionComponent::new("mouth-corner-puller", 0.8),
59 ExpressionComponent::new("mouth-elevation", 0.5),
60 ],
61 ),
62 ExpressionPreset::new(
64 "frown",
65 vec![
66 ExpressionComponent::new("mouth-depression", 0.7),
67 ExpressionComponent::new("eyebrows-left-down", 0.5),
68 ExpressionComponent::new("eyebrows-right-down", 0.5),
69 ],
70 ),
71 ExpressionPreset::new(
73 "surprised",
74 vec![
75 ExpressionComponent::new("mouth-open", 0.7),
76 ExpressionComponent::new("eyebrows-left-up", 0.8),
77 ExpressionComponent::new("eyebrows-right-up", 0.8),
78 ExpressionComponent::new("eye-left-opened-up", 0.6),
79 ExpressionComponent::new("eye-right-opened-up", 0.6),
80 ],
81 ),
82 ExpressionPreset::new(
84 "angry",
85 vec![
86 ExpressionComponent::new("eyebrows-left-inner-up", 0.0),
87 ExpressionComponent::new("eyebrows-right-inner-up", 0.0),
88 ExpressionComponent::new("eyebrows-left-down", 0.8),
89 ExpressionComponent::new("eyebrows-right-down", 0.8),
90 ExpressionComponent::new("mouth-compression", 0.6),
91 ExpressionComponent::new("mouth-retraction", 0.3),
92 ],
93 ),
94 ExpressionPreset::new(
96 "sad",
97 vec![
98 ExpressionComponent::new("mouth-depression", 0.6),
99 ExpressionComponent::new("eyebrows-left-inner-up", 0.7),
100 ExpressionComponent::new("eyebrows-right-inner-up", 0.7),
101 ExpressionComponent::new("eye-left-slit", 0.3),
102 ExpressionComponent::new("eye-right-slit", 0.3),
103 ],
104 ),
105 ]
106 }
107
108 pub fn from_name(name: &str) -> Option<ExpressionPreset> {
110 let lower = name.to_lowercase();
111 ExpressionPreset::all()
112 .into_iter()
113 .find(|p| p.name == lower)
114 }
115
116 pub fn all_names() -> Vec<&'static str> {
118 vec!["neutral", "smile", "frown", "surprised", "angry", "sad"]
119 }
120}
121
122pub fn apply_expression_to_engine(
130 engine: &mut HumanEngine,
131 preset: &ExpressionPreset,
132 expression_dir: &std::path::Path,
133) -> usize {
134 use oxihuman_core::parser::target::parse_target;
135
136 if !expression_dir.exists() {
137 return 0;
138 }
139
140 let mut count = 0usize;
141 for component in &preset.components {
142 let target_path = expression_dir.join(format!("{}.target", component.target_name));
143 if !target_path.exists() {
144 continue;
145 }
146 let src = match std::fs::read_to_string(&target_path) {
147 Ok(s) => s,
148 Err(_) => continue,
149 };
150 let target = match parse_target(&component.target_name, &src) {
151 Ok(t) => t,
152 Err(_) => continue,
153 };
154 let w = component.weight;
155 engine.load_target(target, Box::new(move |_p: &ParamState| w));
156 count += 1;
157 }
158 count
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn all_presets_have_names() {
167 assert!(
168 ExpressionPreset::all_names().len() >= 5,
169 "expected at least 5 expression presets"
170 );
171 }
172
173 #[test]
174 fn neutral_preset_has_no_components() {
175 let preset = ExpressionPreset::from_name("neutral").expect("neutral must exist");
176 assert!(
177 preset.components.is_empty(),
178 "neutral should have no components"
179 );
180 }
181
182 #[test]
183 fn from_name_case_insensitive() {
184 let lower = ExpressionPreset::from_name("smile").expect("smile must exist");
185 let upper = ExpressionPreset::from_name("SMILE").expect("SMILE must exist");
186 assert_eq!(lower.name, upper.name);
187 assert_eq!(lower.components.len(), upper.components.len());
188 }
189
190 #[test]
191 fn from_name_unknown_returns_none() {
192 assert!(ExpressionPreset::from_name("xyzzy").is_none());
193 }
194
195 #[test]
196 fn preset_components_have_valid_weights() {
197 for preset in ExpressionPreset::all() {
198 for comp in &preset.components {
199 assert!(
200 (0.0..=1.0).contains(&comp.weight),
201 "preset '{}' component '{}' has weight {} out of [0,1]",
202 preset.name,
203 comp.target_name,
204 comp.weight
205 );
206 }
207 }
208 }
209
210 #[test]
211 fn apply_expression_skips_missing_targets() {
212 use oxihuman_core::parser::obj::ObjMesh;
213 use oxihuman_core::policy::{Policy, PolicyProfile};
214 let base = ObjMesh {
216 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
217 normals: vec![[0.0, 0.0, 1.0]; 3],
218 uvs: vec![[0.0, 0.0]; 3],
219 indices: vec![0, 1, 2],
220 };
221 let policy = Policy::new(PolicyProfile::Standard);
222 let mut engine = HumanEngine::new(base, policy);
223 let preset = ExpressionPreset::from_name("smile").expect("smile must exist");
224 let count = apply_expression_to_engine(
226 &mut engine,
227 &preset,
228 std::path::Path::new("/tmp/nonexistent_expression_dir_oxihuman"),
229 );
230 assert_eq!(
231 count, 0,
232 "should return 0 when expression dir does not exist"
233 );
234 }
235
236 #[test]
237 fn all_preset_names_resolve() {
238 for name in ExpressionPreset::all_names() {
239 assert!(
240 ExpressionPreset::from_name(name).is_some(),
241 "preset name '{}' must resolve via from_name",
242 name
243 );
244 }
245 }
246}