Skip to main content

oxihuman_morph/
muscle_group_driver.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Muscle group driver — grouped muscle activation derived from pose parameters.
5
6/// Named muscle group.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum MuscleGroup {
10    Pectoralis,
11    Deltoid,
12    Biceps,
13    Triceps,
14    Quadriceps,
15    Hamstrings,
16    Gluteus,
17    Gastrocnemius,
18    Abdominals,
19    Trapezius,
20}
21
22/// Activation record for a single muscle group.
23#[allow(dead_code)]
24#[derive(Debug, Clone, Default)]
25pub struct MuscleActivation {
26    pub group: Option<MuscleGroup>,
27    /// Activation level 0..=1.
28    pub level: f32,
29    /// Contraction ratio affecting mesh bulge 0..=1.
30    pub contraction: f32,
31}
32
33/// Driver state holding per-group activation levels.
34#[allow(dead_code)]
35#[derive(Debug, Clone, Default)]
36pub struct MuscleGroupDriver {
37    activations: Vec<MuscleActivation>,
38}
39
40#[allow(dead_code)]
41pub fn new_muscle_group_driver() -> MuscleGroupDriver {
42    MuscleGroupDriver::default()
43}
44
45#[allow(dead_code)]
46pub fn mgd_set(driver: &mut MuscleGroupDriver, group: MuscleGroup, level: f32, contraction: f32) {
47    let level = level.clamp(0.0, 1.0);
48    let contraction = contraction.clamp(0.0, 1.0);
49    if let Some(a) = driver
50        .activations
51        .iter_mut()
52        .find(|a| a.group == Some(group))
53    {
54        a.level = level;
55        a.contraction = contraction;
56    } else {
57        driver.activations.push(MuscleActivation {
58            group: Some(group),
59            level,
60            contraction,
61        });
62    }
63}
64
65#[allow(dead_code)]
66pub fn mgd_get(driver: &MuscleGroupDriver, group: MuscleGroup) -> Option<&MuscleActivation> {
67    driver.activations.iter().find(|a| a.group == Some(group))
68}
69
70#[allow(dead_code)]
71pub fn mgd_reset(driver: &mut MuscleGroupDriver) {
72    driver.activations.clear();
73}
74
75#[allow(dead_code)]
76pub fn mgd_active_count(driver: &MuscleGroupDriver) -> usize {
77    driver.activations.iter().filter(|a| a.level > 1e-4).count()
78}
79
80#[allow(dead_code)]
81pub fn mgd_total_activation(driver: &MuscleGroupDriver) -> f32 {
82    driver.activations.iter().map(|a| a.level).sum()
83}
84
85/// Compute morph weight for a group (bulge factor).
86#[allow(dead_code)]
87pub fn mgd_bulge_weight(driver: &MuscleGroupDriver, group: MuscleGroup) -> f32 {
88    mgd_get(driver, group).map_or(0.0, |a| a.level * a.contraction)
89}
90
91/// Blend two drivers at factor t.
92#[allow(dead_code)]
93pub fn mgd_blend(a: &MuscleGroupDriver, b: &MuscleGroupDriver, t: f32) -> MuscleGroupDriver {
94    let t = t.clamp(0.0, 1.0);
95    let inv = 1.0 - t;
96    let mut result = MuscleGroupDriver::default();
97    for act_a in &a.activations {
98        if let Some(group) = act_a.group {
99            let level_b = b
100                .activations
101                .iter()
102                .find(|x| x.group == Some(group))
103                .map_or(0.0, |x| x.level);
104            let cont_b = b
105                .activations
106                .iter()
107                .find(|x| x.group == Some(group))
108                .map_or(0.0, |x| x.contraction);
109            result.activations.push(MuscleActivation {
110                group: Some(group),
111                level: act_a.level * inv + level_b * t,
112                contraction: act_a.contraction * inv + cont_b * t,
113            });
114        }
115    }
116    result
117}
118
119#[allow(dead_code)]
120pub fn mgd_to_json(driver: &MuscleGroupDriver) -> String {
121    format!(
122        "{{\"active_count\":{},\"total_activation\":{:.4}}}",
123        mgd_active_count(driver),
124        mgd_total_activation(driver)
125    )
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn empty_driver_has_zero_active() {
134        assert_eq!(mgd_active_count(&new_muscle_group_driver()), 0);
135    }
136
137    #[test]
138    fn set_and_get() {
139        let mut d = new_muscle_group_driver();
140        mgd_set(&mut d, MuscleGroup::Biceps, 0.8, 0.6);
141        let a = mgd_get(&d, MuscleGroup::Biceps).expect("should succeed");
142        assert!((a.level - 0.8).abs() < 1e-6);
143    }
144
145    #[test]
146    fn clamps_level() {
147        let mut d = new_muscle_group_driver();
148        mgd_set(&mut d, MuscleGroup::Triceps, 5.0, 0.5);
149        let a = mgd_get(&d, MuscleGroup::Triceps).expect("should succeed");
150        assert!((a.level - 1.0).abs() < 1e-6);
151    }
152
153    #[test]
154    fn reset_clears() {
155        let mut d = new_muscle_group_driver();
156        mgd_set(&mut d, MuscleGroup::Deltoid, 1.0, 1.0);
157        mgd_reset(&mut d);
158        assert_eq!(mgd_active_count(&d), 0);
159    }
160
161    #[test]
162    fn total_activation_sums() {
163        let mut d = new_muscle_group_driver();
164        mgd_set(&mut d, MuscleGroup::Biceps, 0.5, 0.5);
165        mgd_set(&mut d, MuscleGroup::Triceps, 0.5, 0.5);
166        assert!((mgd_total_activation(&d) - 1.0).abs() < 1e-5);
167    }
168
169    #[test]
170    fn bulge_weight_product() {
171        let mut d = new_muscle_group_driver();
172        mgd_set(&mut d, MuscleGroup::Pectoralis, 0.8, 0.5);
173        assert!((mgd_bulge_weight(&d, MuscleGroup::Pectoralis) - 0.4).abs() < 1e-5);
174    }
175
176    #[test]
177    fn missing_group_bulge_zero() {
178        let d = new_muscle_group_driver();
179        assert!(mgd_bulge_weight(&d, MuscleGroup::Quadriceps) < 1e-8);
180    }
181
182    #[test]
183    fn update_existing_entry() {
184        let mut d = new_muscle_group_driver();
185        mgd_set(&mut d, MuscleGroup::Abdominals, 0.3, 0.3);
186        mgd_set(&mut d, MuscleGroup::Abdominals, 0.9, 0.9);
187        assert_eq!(mgd_active_count(&d), 1);
188        assert!(
189            (mgd_get(&d, MuscleGroup::Abdominals)
190                .expect("should succeed")
191                .level
192                - 0.9)
193                .abs()
194                < 1e-6
195        );
196    }
197
198    #[test]
199    fn blend_midpoint() {
200        let mut a = new_muscle_group_driver();
201        mgd_set(&mut a, MuscleGroup::Gluteus, 1.0, 1.0);
202        let b = new_muscle_group_driver();
203        let r = mgd_blend(&a, &b, 0.5);
204        assert!(
205            (mgd_get(&r, MuscleGroup::Gluteus)
206                .expect("should succeed")
207                .level
208                - 0.5)
209                .abs()
210                < 1e-5
211        );
212    }
213
214    #[test]
215    fn json_has_keys() {
216        let d = new_muscle_group_driver();
217        let j = mgd_to_json(&d);
218        assert!(j.contains("active_count") && j.contains("total_activation"));
219    }
220}