1#[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 ®ion.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 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 advance_breath(&mut state, 2.5);
215 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 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 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 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 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}