#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BreathConfig {
pub breaths_per_min: f32,
pub inhale_fraction: f32,
pub hold_fraction: f32,
pub chest_amplitude: f32,
pub belly_amplitude: f32,
pub shoulder_amplitude: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreathPhase {
Inhale,
Exhale,
Hold,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BreathState {
pub config: BreathConfig,
pub phase: BreathPhase,
pub cycle_time: f32,
pub cycle_duration: f32,
pub phase_progress: f32,
}
fn cycle_duration_from_bpm(bpm: f32) -> f32 {
if bpm <= 0.0 { 4.0 } else { 60.0 / bpm }
}
fn smoothstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
#[allow(dead_code)]
pub fn default_breath_config() -> BreathConfig {
BreathConfig {
breaths_per_min: 15.0,
inhale_fraction: 0.4,
hold_fraction: 0.1,
chest_amplitude: 1.0,
belly_amplitude: 0.8,
shoulder_amplitude: 0.3,
}
}
#[allow(dead_code)]
pub fn new_breath_state(cfg: &BreathConfig) -> BreathState {
let cycle_duration = cycle_duration_from_bpm(cfg.breaths_per_min);
BreathState {
config: cfg.clone(),
phase: BreathPhase::Inhale,
cycle_time: 0.0,
cycle_duration,
phase_progress: 0.0,
}
}
#[allow(dead_code)]
pub fn step_breath(state: &mut BreathState, dt: f32) {
state.cycle_time = (state.cycle_time + dt) % state.cycle_duration;
let t = state.cycle_time / state.cycle_duration; let inhale_end = state.config.inhale_fraction;
let hold_end = inhale_end + state.config.hold_fraction;
if t < inhale_end {
state.phase = BreathPhase::Inhale;
state.phase_progress = if inhale_end > 0.0 { t / inhale_end } else { 0.0 };
} else if t < hold_end {
state.phase = BreathPhase::Hold;
let hold_len = hold_end - inhale_end;
state.phase_progress = if hold_len > 0.0 { (t - inhale_end) / hold_len } else { 0.0 };
} else {
state.phase = BreathPhase::Exhale;
let exhale_len = 1.0 - hold_end;
state.phase_progress = if exhale_len > 0.0 { (t - hold_end) / exhale_len } else { 0.0 };
}
}
#[allow(dead_code)]
pub fn breath_chest_weight(state: &BreathState) -> f32 {
let raw = match state.phase {
BreathPhase::Inhale => smoothstep(state.phase_progress),
BreathPhase::Hold => 1.0,
BreathPhase::Exhale => smoothstep(1.0 - state.phase_progress),
};
(raw * state.config.chest_amplitude).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn breath_belly_weight(state: &BreathState) -> f32 {
let raw = match state.phase {
BreathPhase::Inhale => smoothstep(state.phase_progress),
BreathPhase::Hold => 1.0,
BreathPhase::Exhale => smoothstep(1.0 - state.phase_progress),
};
(raw * state.config.belly_amplitude).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn breath_shoulder_weight(state: &BreathState) -> f32 {
let raw = match state.phase {
BreathPhase::Inhale => smoothstep(state.phase_progress),
BreathPhase::Hold => 1.0,
BreathPhase::Exhale => smoothstep(1.0 - state.phase_progress),
};
(raw * state.config.shoulder_amplitude).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn set_breath_rate(state: &mut BreathState, breaths_per_min: f32) {
let bpm = breaths_per_min.max(0.1);
state.config.breaths_per_min = bpm;
let old_dur = state.cycle_duration;
let new_dur = cycle_duration_from_bpm(bpm);
if old_dur > 0.0 {
state.cycle_time = state.cycle_time / old_dur * new_dur;
}
state.cycle_duration = new_dur;
}
#[allow(dead_code)]
pub fn breath_phase_name(phase: BreathPhase) -> &'static str {
match phase {
BreathPhase::Inhale => "inhale",
BreathPhase::Hold => "hold",
BreathPhase::Exhale => "exhale",
}
}
#[allow(dead_code)]
pub fn breath_normalized_time(state: &BreathState) -> f32 {
if state.cycle_duration > 0.0 {
(state.cycle_time / state.cycle_duration).clamp(0.0, 1.0)
} else {
0.0
}
}
#[allow(dead_code)]
pub fn reset_breath(state: &mut BreathState) {
state.phase = BreathPhase::Inhale;
state.cycle_time = 0.0;
state.phase_progress = 0.0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_reasonable() {
let cfg = default_breath_config();
assert!(cfg.breaths_per_min > 0.0);
assert!(cfg.inhale_fraction > 0.0);
assert!(cfg.hold_fraction >= 0.0);
assert!(cfg.inhale_fraction + cfg.hold_fraction < 1.0);
}
#[test]
fn test_new_breath_state_inhale_phase() {
let cfg = default_breath_config();
let state = new_breath_state(&cfg);
assert_eq!(state.phase, BreathPhase::Inhale);
assert_eq!(state.cycle_time, 0.0);
}
#[test]
fn test_step_breath_advances_time() {
let cfg = default_breath_config();
let mut state = new_breath_state(&cfg);
step_breath(&mut state, 0.5);
assert!(state.cycle_time > 0.0);
}
#[test]
fn test_weights_in_range() {
let cfg = default_breath_config();
let mut state = new_breath_state(&cfg);
let dt = state.cycle_duration / 100.0;
for _ in 0..100 {
step_breath(&mut state, dt);
let chest = breath_chest_weight(&state);
let belly = breath_belly_weight(&state);
let shoulder = breath_shoulder_weight(&state);
assert!((0.0..=1.0).contains(&chest));
assert!((0.0..=1.0).contains(&belly));
assert!((0.0..=1.0).contains(&shoulder));
}
}
#[test]
fn test_phase_name() {
assert_eq!(breath_phase_name(BreathPhase::Inhale), "inhale");
assert_eq!(breath_phase_name(BreathPhase::Hold), "hold");
assert_eq!(breath_phase_name(BreathPhase::Exhale), "exhale");
}
#[test]
fn test_reset_breath() {
let cfg = default_breath_config();
let mut state = new_breath_state(&cfg);
step_breath(&mut state, 2.0);
reset_breath(&mut state);
assert_eq!(state.phase, BreathPhase::Inhale);
assert_eq!(state.cycle_time, 0.0);
assert_eq!(state.phase_progress, 0.0);
}
#[test]
fn test_set_breath_rate_updates_duration() {
let cfg = default_breath_config();
let mut state = new_breath_state(&cfg);
set_breath_rate(&mut state, 30.0);
let expected = 60.0_f32 / 30.0;
assert!((state.cycle_duration - expected).abs() < 1e-5);
}
#[test]
fn test_normalized_time_range() {
let cfg = default_breath_config();
let mut state = new_breath_state(&cfg);
for i in 0..=10 {
state.cycle_time = state.cycle_duration * (i as f32 / 10.0);
let t = breath_normalized_time(&state);
assert!((0.0..=1.0).contains(&t));
}
}
}