Skip to main content

oxihuman_morph/
breathing_sim.rs

1//! Breathing simulation for chest/belly deformation.
2
3#[allow(dead_code)]
4#[derive(Clone, Copy, PartialEq, Debug)]
5pub enum BreathPhase {
6    Inhale,
7    Exhale,
8    Hold,
9}
10
11#[allow(dead_code)]
12pub struct BreathCycle {
13    pub phase: BreathPhase,
14    pub time: f32,
15    pub inhale_duration: f32,
16    pub exhale_duration: f32,
17    pub hold_duration: f32,
18    pub amplitude: f32,
19}
20
21#[allow(dead_code)]
22pub struct BreathRegion {
23    pub name: String,
24    pub vertex_indices: Vec<usize>,
25    pub direction: [f32; 3],
26    pub contribution: f32,
27}
28
29#[allow(dead_code)]
30pub struct BreathingState {
31    pub cycle: BreathCycle,
32    pub regions: Vec<BreathRegion>,
33    pub breath_value: f32,
34}
35
36#[allow(dead_code)]
37pub fn default_breath_cycle() -> BreathCycle {
38    BreathCycle {
39        phase: BreathPhase::Inhale,
40        time: 0.0,
41        inhale_duration: 2.0,
42        exhale_duration: 2.0,
43        hold_duration: 0.0,
44        amplitude: 1.0,
45    }
46}
47
48#[allow(dead_code)]
49pub fn new_breathing_state() -> BreathingState {
50    BreathingState {
51        cycle: default_breath_cycle(),
52        regions: Vec::new(),
53        breath_value: 0.0,
54    }
55}
56
57#[allow(dead_code)]
58pub fn breath_value_at(cycle: &BreathCycle) -> f32 {
59    match cycle.phase {
60        BreathPhase::Inhale => {
61            if cycle.inhale_duration > 0.0 {
62                (cycle.time / cycle.inhale_duration).clamp(0.0, 1.0)
63            } else {
64                1.0
65            }
66        }
67        BreathPhase::Exhale => {
68            if cycle.exhale_duration > 0.0 {
69                1.0 - (cycle.time / cycle.exhale_duration).clamp(0.0, 1.0)
70            } else {
71                0.0
72            }
73        }
74        BreathPhase::Hold => 1.0,
75    }
76}
77
78#[allow(dead_code)]
79pub fn advance_breath(state: &mut BreathingState, dt: f32) {
80    state.cycle.time += dt;
81    loop {
82        match state.cycle.phase {
83            BreathPhase::Inhale => {
84                if state.cycle.time >= state.cycle.inhale_duration {
85                    state.cycle.time -= state.cycle.inhale_duration;
86                    if state.cycle.hold_duration > 0.0 {
87                        state.cycle.phase = BreathPhase::Hold;
88                    } else {
89                        state.cycle.phase = BreathPhase::Exhale;
90                    }
91                } else {
92                    break;
93                }
94            }
95            BreathPhase::Hold => {
96                if state.cycle.time >= state.cycle.hold_duration {
97                    state.cycle.time -= state.cycle.hold_duration;
98                    state.cycle.phase = BreathPhase::Exhale;
99                } else {
100                    break;
101                }
102            }
103            BreathPhase::Exhale => {
104                if state.cycle.time >= state.cycle.exhale_duration {
105                    state.cycle.time -= state.cycle.exhale_duration;
106                    state.cycle.phase = BreathPhase::Inhale;
107                } else {
108                    break;
109                }
110            }
111        }
112    }
113    state.breath_value = breath_value_at(&state.cycle);
114}
115
116#[allow(dead_code)]
117pub fn apply_breathing(positions: &mut [[f32; 3]], state: &BreathingState) {
118    let bv = state.breath_value;
119    let amp = state.cycle.amplitude;
120    for region in &state.regions {
121        let disp = [
122            region.direction[0] * bv * amp * region.contribution,
123            region.direction[1] * bv * amp * region.contribution,
124            region.direction[2] * bv * amp * region.contribution,
125        ];
126        for &vi in &region.vertex_indices {
127            if vi < positions.len() {
128                positions[vi][0] += disp[0];
129                positions[vi][1] += disp[1];
130                positions[vi][2] += disp[2];
131            }
132        }
133    }
134}
135
136#[allow(dead_code)]
137pub fn add_breath_region(state: &mut BreathingState, region: BreathRegion) {
138    state.regions.push(region);
139}
140
141#[allow(dead_code)]
142pub fn set_breath_rate(state: &mut BreathingState, breaths_per_minute: f32) {
143    let cycle_time = 60.0 / breaths_per_minute.max(0.001);
144    let half = cycle_time / 2.0;
145    state.cycle.inhale_duration = half;
146    state.cycle.exhale_duration = half;
147}
148
149#[allow(dead_code)]
150pub fn set_breath_amplitude(state: &mut BreathingState, amplitude: f32) {
151    state.cycle.amplitude = amplitude;
152}
153
154#[allow(dead_code)]
155pub fn inhale_value(state: &BreathingState) -> f32 {
156    state.breath_value
157}
158
159#[allow(dead_code)]
160pub fn exhale_value(state: &BreathingState) -> f32 {
161    1.0 - state.breath_value
162}
163
164#[allow(dead_code)]
165pub fn current_phase(state: &BreathingState) -> BreathPhase {
166    state.cycle.phase
167}
168
169#[allow(dead_code)]
170pub fn breath_region_count(state: &BreathingState) -> usize {
171    state.regions.len()
172}
173
174#[allow(dead_code)]
175pub fn blend_breath_states(a: &BreathingState, b: &BreathingState, t: f32) -> f32 {
176    let t = t.clamp(0.0, 1.0);
177    a.breath_value * (1.0 - t) + b.breath_value * t
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_default_breath_cycle() {
186        let cycle = default_breath_cycle();
187        assert_eq!(cycle.phase, BreathPhase::Inhale);
188        assert_eq!(cycle.time, 0.0);
189        assert!(cycle.inhale_duration > 0.0);
190        assert!(cycle.exhale_duration > 0.0);
191        assert!(cycle.amplitude > 0.0);
192    }
193
194    #[test]
195    fn test_new_breathing_state() {
196        let state = new_breathing_state();
197        assert_eq!(state.breath_value, 0.0);
198        assert!(state.regions.is_empty());
199    }
200
201    #[test]
202    fn test_advance_breath_increases_time() {
203        let mut state = new_breathing_state();
204        advance_breath(&mut state, 0.5);
205        // time should have advanced (may wrap to next phase), breath_value updated
206        let bv = state.breath_value;
207        assert!((0.0..=1.0).contains(&bv));
208    }
209
210    #[test]
211    fn test_advance_breath_changes_phase() {
212        let mut state = new_breathing_state();
213        // skip through the full inhale duration
214        advance_breath(&mut state, 2.5);
215        // should have moved to Exhale
216        assert_eq!(current_phase(&state), BreathPhase::Exhale);
217    }
218
219    #[test]
220    fn test_breath_value_at_inhale_start() {
221        let cycle = default_breath_cycle();
222        // At time=0, inhale phase: value = 0/2 = 0
223        assert_eq!(breath_value_at(&cycle), 0.0);
224    }
225
226    #[test]
227    fn test_breath_value_at_inhale_midpoint() {
228        let mut cycle = default_breath_cycle();
229        cycle.time = 1.0;
230        // At time=1, inhale phase: value = 1/2 = 0.5
231        assert!((breath_value_at(&cycle) - 0.5).abs() < 1e-5);
232    }
233
234    #[test]
235    fn test_breath_value_at_exhale() {
236        let mut cycle = default_breath_cycle();
237        cycle.phase = BreathPhase::Exhale;
238        cycle.time = 1.0;
239        // At time=1, exhale phase: value = 1 - 1/2 = 0.5
240        assert!((breath_value_at(&cycle) - 0.5).abs() < 1e-5);
241    }
242
243    #[test]
244    fn test_set_breath_rate() {
245        let mut state = new_breathing_state();
246        set_breath_rate(&mut state, 15.0);
247        // 15 bpm => cycle = 4s, half = 2s each
248        assert!((state.cycle.inhale_duration - 2.0).abs() < 1e-4);
249        assert!((state.cycle.exhale_duration - 2.0).abs() < 1e-4);
250    }
251
252    #[test]
253    fn test_set_breath_amplitude() {
254        let mut state = new_breathing_state();
255        set_breath_amplitude(&mut state, 2.5);
256        assert_eq!(state.cycle.amplitude, 2.5);
257    }
258
259    #[test]
260    fn test_add_breath_region() {
261        let mut state = new_breathing_state();
262        let region = BreathRegion {
263            name: "chest".to_string(),
264            vertex_indices: vec![0, 1, 2],
265            direction: [0.0, 1.0, 0.0],
266            contribution: 0.8,
267        };
268        add_breath_region(&mut state, region);
269        assert_eq!(breath_region_count(&state), 1);
270    }
271
272    #[test]
273    fn test_breath_region_count() {
274        let mut state = new_breathing_state();
275        add_breath_region(
276            &mut state,
277            BreathRegion {
278                name: "r1".to_string(),
279                vertex_indices: vec![],
280                direction: [1.0, 0.0, 0.0],
281                contribution: 0.5,
282            },
283        );
284        add_breath_region(
285            &mut state,
286            BreathRegion {
287                name: "r2".to_string(),
288                vertex_indices: vec![],
289                direction: [0.0, 1.0, 0.0],
290                contribution: 0.5,
291            },
292        );
293        assert_eq!(breath_region_count(&state), 2);
294    }
295
296    #[test]
297    fn test_inhale_exhale_values_sum_to_one() {
298        let mut state = new_breathing_state();
299        state.breath_value = 0.7;
300        let iv = inhale_value(&state);
301        let ev = exhale_value(&state);
302        assert!((iv + ev - 1.0).abs() < 1e-5);
303    }
304
305    #[test]
306    fn test_blend_breath_states() {
307        let mut a = new_breathing_state();
308        a.breath_value = 0.0;
309        let mut b = new_breathing_state();
310        b.breath_value = 1.0;
311        let blended = blend_breath_states(&a, &b, 0.5);
312        assert!((blended - 0.5).abs() < 1e-5);
313    }
314
315    #[test]
316    fn test_blend_breath_states_extremes() {
317        let mut a = new_breathing_state();
318        a.breath_value = 0.3;
319        let mut b = new_breathing_state();
320        b.breath_value = 0.9;
321        assert!((blend_breath_states(&a, &b, 0.0) - 0.3).abs() < 1e-5);
322        assert!((blend_breath_states(&a, &b, 1.0) - 0.9).abs() < 1e-5);
323    }
324
325    #[test]
326    fn test_apply_breathing_displaces_positions() {
327        let mut state = new_breathing_state();
328        state.breath_value = 1.0;
329        state.cycle.amplitude = 1.0;
330        let region = BreathRegion {
331            name: "chest".to_string(),
332            vertex_indices: vec![0],
333            direction: [0.0, 1.0, 0.0],
334            contribution: 1.0,
335        };
336        add_breath_region(&mut state, region);
337        let mut positions = [[0.0_f32; 3]; 2];
338        apply_breathing(&mut positions, &state);
339        assert!((positions[0][1] - 1.0).abs() < 1e-5);
340    }
341
342    #[test]
343    fn test_current_phase_initial() {
344        let state = new_breathing_state();
345        assert_eq!(current_phase(&state), BreathPhase::Inhale);
346    }
347}