#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoseSide {
Left,
Right,
Both,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct NoseConfig {
pub max_flare: f32,
pub transition_speed: f32,
pub max_wrinkle: f32,
pub max_bridge_delta: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct NoseState {
pub flare_left: f32,
pub flare_right: f32,
pub wrinkle: f32,
pub bridge_width: f32,
pub config: NoseConfig,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct NoseMorphWeights {
pub nostril_flare_l: f32,
pub nostril_flare_r: f32,
pub nose_wrinkle: f32,
pub bridge_narrow: f32,
pub bridge_wide: f32,
}
#[allow(dead_code)]
pub fn default_nose_config() -> NoseConfig {
NoseConfig {
max_flare: 1.0,
transition_speed: 5.0,
max_wrinkle: 1.0,
max_bridge_delta: 0.5,
}
}
#[allow(dead_code)]
pub fn new_nose_state(config: NoseConfig) -> NoseState {
NoseState {
flare_left: 0.0,
flare_right: 0.0,
wrinkle: 0.0,
bridge_width: 0.0,
config,
}
}
#[allow(dead_code)]
pub fn set_nostril_flare(state: &mut NoseState, side: NoseSide, value: f32) {
let v = value.clamp(0.0, 1.0);
match side {
NoseSide::Left => state.flare_left = v,
NoseSide::Right => state.flare_right = v,
NoseSide::Both => {
state.flare_left = v;
state.flare_right = v;
}
}
}
#[allow(dead_code)]
pub fn set_nose_wrinkle(state: &mut NoseState, value: f32) {
state.wrinkle = value.clamp(0.0, 1.0);
}
#[allow(dead_code)]
pub fn set_nose_bridge_width(state: &mut NoseState, value: f32) {
state.bridge_width = value.clamp(-1.0, 1.0);
}
#[allow(dead_code)]
pub fn update_nose(current: &mut NoseState, target: &NoseState, dt: f32) {
let speed = current.config.transition_speed;
let alpha = (speed * dt).clamp(0.0, 1.0);
current.flare_left += (target.flare_left - current.flare_left) * alpha;
current.flare_right += (target.flare_right - current.flare_right) * alpha;
current.wrinkle += (target.wrinkle - current.wrinkle) * alpha;
current.bridge_width += (target.bridge_width - current.bridge_width) * alpha;
}
#[allow(dead_code)]
pub fn nostril_flare_left(state: &NoseState) -> f32 {
state.flare_left
}
#[allow(dead_code)]
pub fn nostril_flare_right(state: &NoseState) -> f32 {
state.flare_right
}
#[allow(dead_code)]
pub fn nose_wrinkle_amount(state: &NoseState) -> f32 {
state.wrinkle
}
#[allow(dead_code)]
pub fn nose_bridge_width(state: &NoseState) -> f32 {
state.bridge_width
}
#[allow(dead_code)]
pub fn blend_nose_states(a: &NoseState, b: &NoseState, t: f32) -> NoseState {
let t = t.clamp(0.0, 1.0);
NoseState {
flare_left: a.flare_left + (b.flare_left - a.flare_left) * t,
flare_right: a.flare_right + (b.flare_right - a.flare_right) * t,
wrinkle: a.wrinkle + (b.wrinkle - a.wrinkle) * t,
bridge_width: a.bridge_width + (b.bridge_width - a.bridge_width) * t,
config: a.config.clone(),
}
}
#[allow(dead_code)]
pub fn nose_to_morph_weights(state: &NoseState) -> NoseMorphWeights {
let bridge_wide = state.bridge_width.max(0.0);
let bridge_narrow = (-state.bridge_width).max(0.0);
NoseMorphWeights {
nostril_flare_l: state.flare_left * state.config.max_flare,
nostril_flare_r: state.flare_right * state.config.max_flare,
nose_wrinkle: state.wrinkle * state.config.max_wrinkle,
bridge_narrow: bridge_narrow * state.config.max_bridge_delta,
bridge_wide: bridge_wide * state.config.max_bridge_delta,
}
}
#[allow(dead_code)]
pub fn reset_nose(state: &mut NoseState) {
state.flare_left = 0.0;
state.flare_right = 0.0;
state.wrinkle = 0.0;
state.bridge_width = 0.0;
}
#[allow(dead_code)]
pub fn apply_sniff_effect(state: &mut NoseState, intensity: f32) {
let i = intensity.clamp(0.0, 1.0);
state.flare_left = (state.flare_left + i * 0.6).clamp(0.0, 1.0);
state.flare_right = (state.flare_right + i * 0.6).clamp(0.0, 1.0);
state.wrinkle = (state.wrinkle + i * 0.3).clamp(0.0, 1.0);
}
#[cfg(test)]
mod tests {
use super::*;
fn base_state() -> NoseState {
new_nose_state(default_nose_config())
}
#[test]
fn test_default_config_values() {
let cfg = default_nose_config();
assert!(cfg.max_flare > 0.0);
assert!(cfg.transition_speed > 0.0);
}
#[test]
fn test_new_nose_state_zeroed() {
let s = base_state();
assert_eq!(s.flare_left, 0.0);
assert_eq!(s.flare_right, 0.0);
assert_eq!(s.wrinkle, 0.0);
assert_eq!(s.bridge_width, 0.0);
}
#[test]
fn test_set_nostril_flare_left() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Left, 0.7);
assert!((s.flare_left - 0.7).abs() < 1e-6);
assert_eq!(s.flare_right, 0.0);
}
#[test]
fn test_set_nostril_flare_right() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Right, 0.5);
assert!((s.flare_right - 0.5).abs() < 1e-6);
assert_eq!(s.flare_left, 0.0);
}
#[test]
fn test_set_nostril_flare_both() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Both, 0.9);
assert!((s.flare_left - 0.9).abs() < 1e-6);
assert!((s.flare_right - 0.9).abs() < 1e-6);
}
#[test]
fn test_set_nostril_flare_clamps() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Both, 2.0);
assert_eq!(s.flare_left, 1.0);
set_nostril_flare(&mut s, NoseSide::Both, -1.0);
assert_eq!(s.flare_left, 0.0);
}
#[test]
fn test_set_nose_wrinkle() {
let mut s = base_state();
set_nose_wrinkle(&mut s, 0.4);
assert!((nose_wrinkle_amount(&s) - 0.4).abs() < 1e-6);
}
#[test]
fn test_set_nose_bridge_width() {
let mut s = base_state();
set_nose_bridge_width(&mut s, 0.3);
assert!((nose_bridge_width(&s) - 0.3).abs() < 1e-6);
}
#[test]
fn test_update_nose_converges() {
let mut current = base_state();
let mut target = base_state();
target.flare_left = 1.0;
for _ in 0..200 {
update_nose(&mut current, &target, 0.1);
}
assert!(current.flare_left > 0.99);
}
#[test]
fn test_nostril_flare_accessors() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Left, 0.2);
set_nostril_flare(&mut s, NoseSide::Right, 0.8);
assert!((nostril_flare_left(&s) - 0.2).abs() < 1e-6);
assert!((nostril_flare_right(&s) - 0.8).abs() < 1e-6);
}
#[test]
fn test_blend_nose_states_midpoint() {
let mut a = base_state();
let mut b = base_state();
a.flare_left = 0.0;
b.flare_left = 1.0;
let mid = blend_nose_states(&a, &b, 0.5);
assert!((mid.flare_left - 0.5).abs() < 1e-6);
}
#[test]
fn test_blend_nose_states_at_zero() {
let a = base_state();
let b = base_state();
let result = blend_nose_states(&a, &b, 0.0);
assert_eq!(result.flare_left, a.flare_left);
}
#[test]
fn test_nose_to_morph_weights_bridge_wide() {
let mut s = base_state();
set_nose_bridge_width(&mut s, 1.0);
let w = nose_to_morph_weights(&s);
assert!(w.bridge_wide > 0.0);
assert_eq!(w.bridge_narrow, 0.0);
}
#[test]
fn test_nose_to_morph_weights_bridge_narrow() {
let mut s = base_state();
set_nose_bridge_width(&mut s, -1.0);
let w = nose_to_morph_weights(&s);
assert!(w.bridge_narrow > 0.0);
assert_eq!(w.bridge_wide, 0.0);
}
#[test]
fn test_reset_nose() {
let mut s = base_state();
set_nostril_flare(&mut s, NoseSide::Both, 0.9);
set_nose_wrinkle(&mut s, 0.8);
reset_nose(&mut s);
assert_eq!(s.flare_left, 0.0);
assert_eq!(s.flare_right, 0.0);
assert_eq!(s.wrinkle, 0.0);
}
#[test]
fn test_apply_sniff_effect() {
let mut s = base_state();
apply_sniff_effect(&mut s, 1.0);
assert!(s.flare_left > 0.0);
assert!(s.flare_right > 0.0);
assert!(s.wrinkle > 0.0);
}
#[test]
fn test_apply_sniff_effect_clamps() {
let mut s = base_state();
s.flare_left = 0.9;
apply_sniff_effect(&mut s, 1.0);
assert!(s.flare_left <= 1.0);
}
#[test]
fn test_nose_side_debug() {
let side = NoseSide::Both;
let dbg = format!("{side:?}");
assert!(dbg.contains("Both"));
}
}