use crate::engine::HumanEngine;
use crate::params::ParamState;
#[derive(Debug, Clone)]
pub struct ExpressionComponent {
pub target_name: String,
pub weight: f32,
}
impl ExpressionComponent {
fn new(target_name: &str, weight: f32) -> Self {
Self {
target_name: target_name.to_string(),
weight,
}
}
}
#[derive(Debug, Clone)]
pub struct ExpressionPreset {
pub name: String,
pub components: Vec<ExpressionComponent>,
}
impl ExpressionPreset {
fn new(name: &str, components: Vec<ExpressionComponent>) -> Self {
Self {
name: name.to_string(),
components,
}
}
pub fn all() -> Vec<ExpressionPreset> {
vec![
ExpressionPreset::new("neutral", vec![]),
ExpressionPreset::new(
"smile",
vec![
ExpressionComponent::new("mouth-corner-puller", 0.8),
ExpressionComponent::new("mouth-elevation", 0.5),
],
),
ExpressionPreset::new(
"frown",
vec![
ExpressionComponent::new("mouth-depression", 0.7),
ExpressionComponent::new("eyebrows-left-down", 0.5),
ExpressionComponent::new("eyebrows-right-down", 0.5),
],
),
ExpressionPreset::new(
"surprised",
vec![
ExpressionComponent::new("mouth-open", 0.7),
ExpressionComponent::new("eyebrows-left-up", 0.8),
ExpressionComponent::new("eyebrows-right-up", 0.8),
ExpressionComponent::new("eye-left-opened-up", 0.6),
ExpressionComponent::new("eye-right-opened-up", 0.6),
],
),
ExpressionPreset::new(
"angry",
vec![
ExpressionComponent::new("eyebrows-left-inner-up", 0.0),
ExpressionComponent::new("eyebrows-right-inner-up", 0.0),
ExpressionComponent::new("eyebrows-left-down", 0.8),
ExpressionComponent::new("eyebrows-right-down", 0.8),
ExpressionComponent::new("mouth-compression", 0.6),
ExpressionComponent::new("mouth-retraction", 0.3),
],
),
ExpressionPreset::new(
"sad",
vec![
ExpressionComponent::new("mouth-depression", 0.6),
ExpressionComponent::new("eyebrows-left-inner-up", 0.7),
ExpressionComponent::new("eyebrows-right-inner-up", 0.7),
ExpressionComponent::new("eye-left-slit", 0.3),
ExpressionComponent::new("eye-right-slit", 0.3),
],
),
]
}
pub fn from_name(name: &str) -> Option<ExpressionPreset> {
let lower = name.to_lowercase();
ExpressionPreset::all()
.into_iter()
.find(|p| p.name == lower)
}
pub fn all_names() -> Vec<&'static str> {
vec!["neutral", "smile", "frown", "surprised", "angry", "sad"]
}
}
pub fn apply_expression_to_engine(
engine: &mut HumanEngine,
preset: &ExpressionPreset,
expression_dir: &std::path::Path,
) -> usize {
use oxihuman_core::parser::target::parse_target;
if !expression_dir.exists() {
return 0;
}
let mut count = 0usize;
for component in &preset.components {
let target_path = expression_dir.join(format!("{}.target", component.target_name));
if !target_path.exists() {
continue;
}
let src = match std::fs::read_to_string(&target_path) {
Ok(s) => s,
Err(_) => continue,
};
let target = match parse_target(&component.target_name, &src) {
Ok(t) => t,
Err(_) => continue,
};
let w = component.weight;
engine.load_target(target, Box::new(move |_p: &ParamState| w));
count += 1;
}
count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_presets_have_names() {
assert!(
ExpressionPreset::all_names().len() >= 5,
"expected at least 5 expression presets"
);
}
#[test]
fn neutral_preset_has_no_components() {
let preset = ExpressionPreset::from_name("neutral").expect("neutral must exist");
assert!(
preset.components.is_empty(),
"neutral should have no components"
);
}
#[test]
fn from_name_case_insensitive() {
let lower = ExpressionPreset::from_name("smile").expect("smile must exist");
let upper = ExpressionPreset::from_name("SMILE").expect("SMILE must exist");
assert_eq!(lower.name, upper.name);
assert_eq!(lower.components.len(), upper.components.len());
}
#[test]
fn from_name_unknown_returns_none() {
assert!(ExpressionPreset::from_name("xyzzy").is_none());
}
#[test]
fn preset_components_have_valid_weights() {
for preset in ExpressionPreset::all() {
for comp in &preset.components {
assert!(
(0.0..=1.0).contains(&comp.weight),
"preset '{}' component '{}' has weight {} out of [0,1]",
preset.name,
comp.target_name,
comp.weight
);
}
}
}
#[test]
fn apply_expression_skips_missing_targets() {
use oxihuman_core::parser::obj::ObjMesh;
use oxihuman_core::policy::{Policy, PolicyProfile};
let base = ObjMesh {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
normals: vec![[0.0, 0.0, 1.0]; 3],
uvs: vec![[0.0, 0.0]; 3],
indices: vec![0, 1, 2],
};
let policy = Policy::new(PolicyProfile::Standard);
let mut engine = HumanEngine::new(base, policy);
let preset = ExpressionPreset::from_name("smile").expect("smile must exist");
let count = apply_expression_to_engine(
&mut engine,
&preset,
std::path::Path::new("/tmp/nonexistent_expression_dir_oxihuman"),
);
assert_eq!(
count, 0,
"should return 0 when expression dir does not exist"
);
}
#[test]
fn all_preset_names_resolve() {
for name in ExpressionPreset::all_names() {
assert!(
ExpressionPreset::from_name(name).is_some(),
"preset name '{}' must resolve via from_name",
name
);
}
}
}