1#![allow(dead_code)]
5#![allow(clippy::too_many_arguments)]
6
7use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum MuscleGroup {
22 Chest,
23 Back,
24 Shoulder,
25 Bicep,
26 Tricep,
27 Forearm,
28 Abs,
29 Oblique,
30 Glute,
31 Hamstring,
32 Quad,
33 Calf,
34 Neck,
35 Face,
36}
37
38impl MuscleGroup {
39 pub fn all() -> Vec<MuscleGroup> {
41 vec![
42 MuscleGroup::Chest,
43 MuscleGroup::Back,
44 MuscleGroup::Shoulder,
45 MuscleGroup::Bicep,
46 MuscleGroup::Tricep,
47 MuscleGroup::Forearm,
48 MuscleGroup::Abs,
49 MuscleGroup::Oblique,
50 MuscleGroup::Glute,
51 MuscleGroup::Hamstring,
52 MuscleGroup::Quad,
53 MuscleGroup::Calf,
54 MuscleGroup::Neck,
55 MuscleGroup::Face,
56 ]
57 }
58
59 pub fn name(&self) -> &'static str {
61 match self {
62 MuscleGroup::Chest => "Chest",
63 MuscleGroup::Back => "Back",
64 MuscleGroup::Shoulder => "Shoulder",
65 MuscleGroup::Bicep => "Bicep",
66 MuscleGroup::Tricep => "Tricep",
67 MuscleGroup::Forearm => "Forearm",
68 MuscleGroup::Abs => "Abs",
69 MuscleGroup::Oblique => "Oblique",
70 MuscleGroup::Glute => "Glute",
71 MuscleGroup::Hamstring => "Hamstring",
72 MuscleGroup::Quad => "Quad",
73 MuscleGroup::Calf => "Calf",
74 MuscleGroup::Neck => "Neck",
75 MuscleGroup::Face => "Face",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum Side {
87 Left,
88 Right,
89 Center,
90}
91
92pub struct MuscleDefinition {
98 pub name: String,
100 pub group: MuscleGroup,
102 pub flex_morphs: Vec<(String, f32)>,
105 pub contract_morphs: Vec<(String, f32)>,
107 pub symmetrical: bool,
109 pub side: Option<Side>,
111 pub rest_length: f32,
113}
114
115impl MuscleDefinition {
116 pub fn new(name: impl Into<String>, group: MuscleGroup) -> Self {
118 Self {
119 name: name.into(),
120 group,
121 flex_morphs: Vec::new(),
122 contract_morphs: Vec::new(),
123 symmetrical: false,
124 side: None,
125 rest_length: 1.0,
126 }
127 }
128
129 pub fn with_flex_morph(mut self, morph: impl Into<String>, max_weight: f32) -> Self {
131 self.flex_morphs.push((morph.into(), max_weight));
132 self
133 }
134
135 pub fn with_contract_morph(mut self, morph: impl Into<String>, max_weight: f32) -> Self {
137 self.contract_morphs.push((morph.into(), max_weight));
138 self
139 }
140
141 pub fn with_side(mut self, side: Side) -> Self {
143 self.symmetrical = true;
144 self.side = Some(side);
145 self
146 }
147
148 pub fn with_rest_length(mut self, length: f32) -> Self {
150 self.rest_length = length.clamp(0.0, 1.0);
151 self
152 }
153}
154
155#[derive(Debug, Clone)]
161pub struct MuscleState {
162 pub flex: f32,
164 pub contract: f32,
166 pub fatigue: f32,
168}
169
170impl Default for MuscleState {
171 fn default() -> Self {
172 Self {
173 flex: 0.0,
174 contract: 0.0,
175 fatigue: 0.0,
176 }
177 }
178}
179
180impl MuscleState {
181 pub fn flexed(v: f32) -> Self {
183 Self {
184 flex: v.clamp(0.0, 1.0),
185 contract: 0.0,
186 fatigue: 0.0,
187 }
188 }
189
190 pub fn relaxed() -> Self {
192 Self::default()
193 }
194
195 pub fn effective_flex(&self) -> f32 {
199 (self.flex * (1.0 - self.fatigue)).clamp(0.0, 1.0)
200 }
201
202 pub fn effective_contract(&self) -> f32 {
204 (self.contract * (1.0 - self.fatigue)).clamp(0.0, 1.0)
205 }
206}
207
208pub struct MuscleRig {
215 muscles: Vec<MuscleDefinition>,
216 states: HashMap<String, MuscleState>,
217}
218
219impl Default for MuscleRig {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl MuscleRig {
226 pub fn new() -> Self {
228 Self {
229 muscles: Vec::new(),
230 states: HashMap::new(),
231 }
232 }
233
234 pub fn add_muscle(&mut self, def: MuscleDefinition) {
237 let name = def.name.clone();
238 self.muscles.push(def);
239 self.states.entry(name).or_default();
240 }
241
242 pub fn set_state(&mut self, name: &str, state: MuscleState) {
244 self.states.insert(name.to_owned(), state);
245 }
246
247 pub fn get_state(&self, name: &str) -> Option<&MuscleState> {
249 self.states.get(name)
250 }
251
252 pub fn muscle_names(&self) -> Vec<&str> {
254 self.muscles.iter().map(|m| m.name.as_str()).collect()
255 }
256
257 pub fn muscles_in_group(&self, group: &MuscleGroup) -> Vec<&MuscleDefinition> {
259 self.muscles.iter().filter(|m| &m.group == group).collect()
260 }
261
262 pub fn count(&self) -> usize {
264 self.muscles.len()
265 }
266
267 pub fn evaluate(&self) -> HashMap<String, f32> {
272 let mut weights: HashMap<String, f32> = HashMap::new();
273
274 for muscle in &self.muscles {
275 let state = self.states.get(&muscle.name).cloned().unwrap_or_default();
276
277 let eff_flex = state.effective_flex();
278 let eff_contract = state.effective_contract();
279
280 for (morph_name, max_weight) in &muscle.flex_morphs {
281 let w = eff_flex * max_weight;
282 let entry = weights.entry(morph_name.clone()).or_insert(0.0);
283 *entry = (*entry + w).clamp(0.0, 1.0);
284 }
285
286 for (morph_name, max_weight) in &muscle.contract_morphs {
287 let w = eff_contract * max_weight;
288 let entry = weights.entry(morph_name.clone()).or_insert(0.0);
289 *entry = (*entry + w).clamp(0.0, 1.0);
290 }
291 }
292
293 weights
294 }
295
296 pub fn flex_group(&mut self, group: &MuscleGroup, amount: f32) {
298 let names: Vec<String> = self
299 .muscles
300 .iter()
301 .filter(|m| &m.group == group)
302 .map(|m| m.name.clone())
303 .collect();
304
305 let amount = amount.clamp(0.0, 1.0);
306 for name in names {
307 let state = self.states.entry(name).or_default();
308 state.flex = amount;
309 }
310 }
311
312 pub fn relax_all(&mut self) {
314 for state in self.states.values_mut() {
315 state.flex = 0.0;
316 state.contract = 0.0;
317 }
318 }
319
320 pub fn default_rig() -> Self {
322 let mut rig = Self::new();
323
324 rig.add_muscle(
326 MuscleDefinition::new("pectoralis_major", MuscleGroup::Chest)
327 .with_flex_morph("chest_flex", 1.0)
328 .with_contract_morph("chest_contracted", 0.8)
329 .with_rest_length(0.9),
330 );
331
332 rig.add_muscle(
334 MuscleDefinition::new("latissimus_dorsi", MuscleGroup::Back)
335 .with_flex_morph("back_lat_flex", 1.0)
336 .with_contract_morph("back_contracted", 0.7)
337 .with_rest_length(0.95),
338 );
339 rig.add_muscle(
340 MuscleDefinition::new("trapezius", MuscleGroup::Back)
341 .with_flex_morph("trap_flex", 0.9)
342 .with_rest_length(0.85),
343 );
344
345 rig.add_muscle(
347 MuscleDefinition::new("deltoid_left", MuscleGroup::Shoulder)
348 .with_flex_morph("shoulder_flex_left", 1.0)
349 .with_side(Side::Left)
350 .with_rest_length(0.8),
351 );
352 rig.add_muscle(
353 MuscleDefinition::new("deltoid_right", MuscleGroup::Shoulder)
354 .with_flex_morph("shoulder_flex_right", 1.0)
355 .with_side(Side::Right)
356 .with_rest_length(0.8),
357 );
358
359 rig.add_muscle(
361 MuscleDefinition::new("bicep_left", MuscleGroup::Bicep)
362 .with_flex_morph("bicep_flex_left", 1.0)
363 .with_contract_morph("bicep_contracted_left", 0.9)
364 .with_side(Side::Left)
365 .with_rest_length(0.75),
366 );
367 rig.add_muscle(
368 MuscleDefinition::new("bicep_right", MuscleGroup::Bicep)
369 .with_flex_morph("bicep_flex_right", 1.0)
370 .with_contract_morph("bicep_contracted_right", 0.9)
371 .with_side(Side::Right)
372 .with_rest_length(0.75),
373 );
374
375 rig.add_muscle(
377 MuscleDefinition::new("tricep_left", MuscleGroup::Tricep)
378 .with_flex_morph("tricep_flex_left", 0.85)
379 .with_side(Side::Left)
380 .with_rest_length(0.8),
381 );
382 rig.add_muscle(
383 MuscleDefinition::new("tricep_right", MuscleGroup::Tricep)
384 .with_flex_morph("tricep_flex_right", 0.85)
385 .with_side(Side::Right)
386 .with_rest_length(0.8),
387 );
388
389 rig.add_muscle(
391 MuscleDefinition::new("brachioradialis_left", MuscleGroup::Forearm)
392 .with_flex_morph("forearm_flex_left", 0.7)
393 .with_side(Side::Left)
394 .with_rest_length(0.9),
395 );
396 rig.add_muscle(
397 MuscleDefinition::new("brachioradialis_right", MuscleGroup::Forearm)
398 .with_flex_morph("forearm_flex_right", 0.7)
399 .with_side(Side::Right)
400 .with_rest_length(0.9),
401 );
402
403 rig.add_muscle(
405 MuscleDefinition::new("rectus_abdominis", MuscleGroup::Abs)
406 .with_flex_morph("abs_flex", 1.0)
407 .with_contract_morph("abs_crunch", 0.8)
408 .with_rest_length(0.85),
409 );
410
411 rig.add_muscle(
413 MuscleDefinition::new("oblique_left", MuscleGroup::Oblique)
414 .with_flex_morph("oblique_flex_left", 0.8)
415 .with_side(Side::Left)
416 .with_rest_length(0.9),
417 );
418 rig.add_muscle(
419 MuscleDefinition::new("oblique_right", MuscleGroup::Oblique)
420 .with_flex_morph("oblique_flex_right", 0.8)
421 .with_side(Side::Right)
422 .with_rest_length(0.9),
423 );
424
425 rig.add_muscle(
427 MuscleDefinition::new("gluteus_maximus", MuscleGroup::Glute)
428 .with_flex_morph("glute_flex", 1.0)
429 .with_rest_length(0.95),
430 );
431
432 rig.add_muscle(
434 MuscleDefinition::new("quadricep_left", MuscleGroup::Quad)
435 .with_flex_morph("quad_flex_left", 1.0)
436 .with_side(Side::Left)
437 .with_rest_length(0.85),
438 );
439 rig.add_muscle(
440 MuscleDefinition::new("quadricep_right", MuscleGroup::Quad)
441 .with_flex_morph("quad_flex_right", 1.0)
442 .with_side(Side::Right)
443 .with_rest_length(0.85),
444 );
445
446 rig.add_muscle(
448 MuscleDefinition::new("hamstring_left", MuscleGroup::Hamstring)
449 .with_flex_morph("hamstring_flex_left", 0.9)
450 .with_side(Side::Left)
451 .with_rest_length(0.9),
452 );
453
454 rig.add_muscle(
456 MuscleDefinition::new("gastrocnemius_left", MuscleGroup::Calf)
457 .with_flex_morph("calf_flex_left", 0.9)
458 .with_contract_morph("calf_contracted_left", 0.7)
459 .with_side(Side::Left)
460 .with_rest_length(0.8),
461 );
462
463 rig.add_muscle(
465 MuscleDefinition::new("sternocleidomastoid", MuscleGroup::Neck)
466 .with_flex_morph("neck_flex", 0.7)
467 .with_rest_length(0.85),
468 );
469
470 rig
471 }
472
473 pub fn apply_fatigue(&mut self, threshold: f32, fatigue_rate: f32) {
478 for state in self.states.values_mut() {
479 if state.flex > threshold {
480 state.fatigue = (state.fatigue + fatigue_rate).clamp(0.0, 1.0);
481 }
482 }
483 }
484}
485
486pub fn blend_rig_states(
495 a: &HashMap<String, MuscleState>,
496 b: &HashMap<String, MuscleState>,
497 t: f32,
498) -> HashMap<String, MuscleState> {
499 let t = t.clamp(0.0, 1.0);
500 let one_minus_t = 1.0 - t;
501
502 let mut result: HashMap<String, MuscleState> = HashMap::new();
503
504 let mut keys: std::collections::HashSet<&str> = std::collections::HashSet::new();
506 for k in a.keys() {
507 keys.insert(k.as_str());
508 }
509 for k in b.keys() {
510 keys.insert(k.as_str());
511 }
512
513 let default_state = MuscleState::default();
514
515 for key in keys {
516 let sa = a.get(key).unwrap_or(&default_state);
517 let sb = b.get(key).unwrap_or(&default_state);
518
519 result.insert(
520 key.to_owned(),
521 MuscleState {
522 flex: (sa.flex * one_minus_t + sb.flex * t).clamp(0.0, 1.0),
523 contract: (sa.contract * one_minus_t + sb.contract * t).clamp(0.0, 1.0),
524 fatigue: (sa.fatigue * one_minus_t + sb.fatigue * t).clamp(0.0, 1.0),
525 },
526 );
527 }
528
529 result
530}
531
532pub fn rig_to_morphs(rig: &MuscleRig) -> HashMap<String, f32> {
537 rig.evaluate()
538}
539
540pub fn params_to_muscle_activation(muscle_param: f32, group: &MuscleGroup) -> f32 {
545 let param = muscle_param.clamp(0.0, 1.0);
546
547 let sensitivity = match group {
550 MuscleGroup::Bicep => 1.0,
551 MuscleGroup::Tricep => 0.95,
552 MuscleGroup::Chest => 0.9,
553 MuscleGroup::Back => 0.9,
554 MuscleGroup::Shoulder => 0.85,
555 MuscleGroup::Forearm => 0.8,
556 MuscleGroup::Abs => 0.85,
557 MuscleGroup::Oblique => 0.75,
558 MuscleGroup::Glute => 0.8,
559 MuscleGroup::Quad => 0.9,
560 MuscleGroup::Hamstring => 0.85,
561 MuscleGroup::Calf => 0.8,
562 MuscleGroup::Neck => 0.7,
563 MuscleGroup::Face => 0.3,
564 };
565
566 (param * sensitivity).clamp(0.0, 1.0)
567}
568
569#[cfg(test)]
574mod tests {
575 use super::*;
576 use std::fs;
577
578 fn write_tmp(filename: &str, content: &str) {
580 fs::write(format!("/tmp/{}", filename), content).expect("write /tmp/ file");
581 }
582
583 #[test]
588 fn test_muscle_group_all_count() {
589 let all = MuscleGroup::all();
590 assert_eq!(all.len(), 14);
591 write_tmp("muscle_group_all.txt", &format!("{} groups", all.len()));
592 }
593
594 #[test]
595 fn test_muscle_group_names() {
596 assert_eq!(MuscleGroup::Bicep.name(), "Bicep");
597 assert_eq!(MuscleGroup::Face.name(), "Face");
598 assert_eq!(MuscleGroup::Abs.name(), "Abs");
599 write_tmp("muscle_group_names.txt", "OK");
600 }
601
602 #[test]
603 fn test_muscle_group_all_unique_names() {
604 let names: Vec<&str> = MuscleGroup::all().iter().map(|g| g.name()).collect();
605 let unique: std::collections::HashSet<&str> = names.iter().copied().collect();
606 assert_eq!(names.len(), unique.len(), "all group names must be unique");
607 write_tmp("muscle_group_unique.txt", "OK");
608 }
609
610 #[test]
615 fn test_muscle_state_default_is_relaxed() {
616 let s = MuscleState::default();
617 assert_eq!(s.flex, 0.0);
618 assert_eq!(s.contract, 0.0);
619 assert_eq!(s.fatigue, 0.0);
620 write_tmp("muscle_state_default.txt", "OK");
621 }
622
623 #[test]
624 fn test_muscle_state_flexed() {
625 let s = MuscleState::flexed(0.7);
626 assert!((s.flex - 0.7).abs() < 1e-6);
627 assert_eq!(s.fatigue, 0.0);
628 write_tmp("muscle_state_flexed.txt", "OK");
629 }
630
631 #[test]
632 fn test_muscle_state_effective_flex_with_fatigue() {
633 let mut s = MuscleState::flexed(1.0);
634 s.fatigue = 0.5;
635 let eff = s.effective_flex();
636 assert!((eff - 0.5).abs() < 1e-6, "eff={eff}");
637 write_tmp("muscle_state_fatigue.txt", &format!("eff={eff}"));
638 }
639
640 #[test]
641 fn test_muscle_state_relaxed() {
642 let s = MuscleState::relaxed();
643 assert_eq!(s.effective_flex(), 0.0);
644 write_tmp("muscle_state_relaxed.txt", "OK");
645 }
646
647 #[test]
652 fn test_muscle_definition_builder() {
653 let def = MuscleDefinition::new("bicep_test", MuscleGroup::Bicep)
654 .with_flex_morph("flex_shape", 1.0)
655 .with_contract_morph("contract_shape", 0.5)
656 .with_side(Side::Left)
657 .with_rest_length(0.8);
658
659 assert_eq!(def.name, "bicep_test");
660 assert_eq!(def.group, MuscleGroup::Bicep);
661 assert_eq!(def.flex_morphs.len(), 1);
662 assert_eq!(def.contract_morphs.len(), 1);
663 assert!(def.symmetrical);
664 assert_eq!(def.side, Some(Side::Left));
665 assert!((def.rest_length - 0.8).abs() < 1e-6);
666 write_tmp("muscle_definition_builder.txt", "OK");
667 }
668
669 #[test]
674 fn test_rig_add_and_count() {
675 let mut rig = MuscleRig::new();
676 rig.add_muscle(MuscleDefinition::new("m1", MuscleGroup::Bicep));
677 rig.add_muscle(MuscleDefinition::new("m2", MuscleGroup::Tricep));
678 assert_eq!(rig.count(), 2);
679 write_tmp("rig_count.txt", "OK");
680 }
681
682 #[test]
683 fn test_rig_set_and_get_state() {
684 let mut rig = MuscleRig::new();
685 rig.add_muscle(MuscleDefinition::new("test_muscle", MuscleGroup::Abs));
686 rig.set_state("test_muscle", MuscleState::flexed(0.6));
687 let state = rig.get_state("test_muscle").expect("state must exist");
688 assert!((state.flex - 0.6).abs() < 1e-6);
689 write_tmp("rig_state.txt", "OK");
690 }
691
692 #[test]
693 fn test_rig_evaluate_flex() {
694 let mut rig = MuscleRig::new();
695 rig.add_muscle(
696 MuscleDefinition::new("bicep_l", MuscleGroup::Bicep)
697 .with_flex_morph("bicep_shape", 1.0),
698 );
699 rig.set_state("bicep_l", MuscleState::flexed(0.8));
700 let weights = rig.evaluate();
701 let w = weights.get("bicep_shape").copied().unwrap_or(0.0);
702 assert!((w - 0.8).abs() < 1e-6, "w={w}");
703 write_tmp("rig_evaluate_flex.txt", &format!("w={w}"));
704 }
705
706 #[test]
707 fn test_rig_evaluate_multiple_morphs() {
708 let mut rig = MuscleRig::new();
709 rig.add_muscle(
710 MuscleDefinition::new("chest", MuscleGroup::Chest)
711 .with_flex_morph("chest_shape_a", 0.5)
712 .with_flex_morph("chest_shape_b", 0.8),
713 );
714 rig.set_state("chest", MuscleState::flexed(1.0));
715 let weights = rig.evaluate();
716 assert!((weights["chest_shape_a"] - 0.5).abs() < 1e-6);
717 assert!((weights["chest_shape_b"] - 0.8).abs() < 1e-6);
718 write_tmp("rig_evaluate_multi.txt", "OK");
719 }
720
721 #[test]
722 fn test_rig_relax_all() {
723 let mut rig = MuscleRig::new();
724 rig.add_muscle(MuscleDefinition::new("m1", MuscleGroup::Glute));
725 rig.set_state("m1", MuscleState::flexed(1.0));
726 rig.relax_all();
727 let state = rig.get_state("m1").expect("state");
728 assert_eq!(state.flex, 0.0);
729 write_tmp("rig_relax_all.txt", "OK");
730 }
731
732 #[test]
733 fn test_rig_muscles_in_group() {
734 let rig = MuscleRig::default_rig();
735 let biceps = rig.muscles_in_group(&MuscleGroup::Bicep);
736 assert!(!biceps.is_empty(), "default rig should have biceps");
737 write_tmp("rig_in_group.txt", &format!("biceps={}", biceps.len()));
738 }
739
740 #[test]
741 fn test_rig_flex_group() {
742 let mut rig = MuscleRig::default_rig();
743 rig.flex_group(&MuscleGroup::Bicep, 1.0);
744 let bicep_names: Vec<String> = rig
745 .muscles_in_group(&MuscleGroup::Bicep)
746 .iter()
747 .map(|m| m.name.clone())
748 .collect();
749 for name in &bicep_names {
750 let state = rig.get_state(name).expect("state");
751 assert!((state.flex - 1.0).abs() < 1e-6, "{name} flex should be 1.0");
752 }
753 write_tmp("rig_flex_group.txt", "OK");
754 }
755
756 #[test]
757 fn test_rig_default_count() {
758 let rig = MuscleRig::default_rig();
759 assert!(
760 rig.count() >= 20,
761 "default rig must have ≥20 muscles, got {}",
762 rig.count()
763 );
764 write_tmp("rig_default_count.txt", &format!("{}", rig.count()));
765 }
766
767 #[test]
768 fn test_rig_apply_fatigue() {
769 let mut rig = MuscleRig::new();
770 rig.add_muscle(MuscleDefinition::new("fatigued", MuscleGroup::Quad));
771 rig.set_state("fatigued", MuscleState::flexed(1.0));
772 rig.apply_fatigue(0.5, 0.3);
773 let state = rig.get_state("fatigued").expect("state");
774 assert!(
775 (state.fatigue - 0.3).abs() < 1e-6,
776 "fatigue={}",
777 state.fatigue
778 );
779 write_tmp("rig_apply_fatigue.txt", "OK");
780 }
781
782 #[test]
783 fn test_rig_apply_fatigue_below_threshold() {
784 let mut rig = MuscleRig::new();
785 rig.add_muscle(MuscleDefinition::new("resting", MuscleGroup::Calf));
786 rig.set_state("resting", MuscleState::flexed(0.2));
787 rig.apply_fatigue(0.5, 0.3);
788 let state = rig.get_state("resting").expect("state");
789 assert_eq!(
790 state.fatigue, 0.0,
791 "below-threshold muscle must not fatigue"
792 );
793 write_tmp("rig_no_fatigue.txt", "OK");
794 }
795
796 #[test]
801 fn test_blend_rig_states_midpoint() {
802 let mut a: HashMap<String, MuscleState> = HashMap::new();
803 a.insert(
804 "m".to_owned(),
805 MuscleState {
806 flex: 0.0,
807 contract: 0.0,
808 fatigue: 0.0,
809 },
810 );
811 let mut b: HashMap<String, MuscleState> = HashMap::new();
812 b.insert(
813 "m".to_owned(),
814 MuscleState {
815 flex: 1.0,
816 contract: 0.0,
817 fatigue: 0.0,
818 },
819 );
820
821 let blended = blend_rig_states(&a, &b, 0.5);
822 let s = &blended["m"];
823 assert!((s.flex - 0.5).abs() < 1e-6, "flex={}", s.flex);
824 write_tmp("blend_states_midpoint.txt", "OK");
825 }
826
827 #[test]
828 fn test_blend_rig_states_t0_equals_a() {
829 let mut a: HashMap<String, MuscleState> = HashMap::new();
830 a.insert("x".to_owned(), MuscleState::flexed(0.7));
831 let b: HashMap<String, MuscleState> = HashMap::new();
832
833 let blended = blend_rig_states(&a, &b, 0.0);
834 let s = &blended["x"];
835 assert!((s.flex - 0.7).abs() < 1e-6);
836 write_tmp("blend_states_t0.txt", "OK");
837 }
838
839 #[test]
840 fn test_blend_rig_states_missing_key_treated_as_default() {
841 let a: HashMap<String, MuscleState> = HashMap::new();
842 let mut b: HashMap<String, MuscleState> = HashMap::new();
843 b.insert("only_in_b".to_owned(), MuscleState::flexed(1.0));
844
845 let blended = blend_rig_states(&a, &b, 1.0);
846 let s = &blended["only_in_b"];
847 assert!((s.flex - 1.0).abs() < 1e-6);
848 write_tmp("blend_missing_key.txt", "OK");
849 }
850
851 #[test]
856 fn test_rig_to_morphs_convenience() {
857 let mut rig = MuscleRig::new();
858 rig.add_muscle(
859 MuscleDefinition::new("neck", MuscleGroup::Neck).with_flex_morph("neck_shape", 0.6),
860 );
861 rig.set_state("neck", MuscleState::flexed(1.0));
862 let morphs = rig_to_morphs(&rig);
863 assert!((morphs["neck_shape"] - 0.6).abs() < 1e-6);
864 write_tmp("rig_to_morphs.txt", "OK");
865 }
866
867 #[test]
868 fn test_params_to_activation_range() {
869 for group in MuscleGroup::all() {
870 let low = params_to_muscle_activation(0.0, &group);
871 let high = params_to_muscle_activation(1.0, &group);
872 assert!((0.0..=1.0).contains(&low), "{} low={low}", group.name());
873 assert!((0.0..=1.0).contains(&high), "{} high={high}", group.name());
874 }
875 write_tmp("params_activation_range.txt", "OK");
876 }
877
878 #[test]
879 fn test_params_activation_monotone() {
880 for group in MuscleGroup::all() {
882 let a = params_to_muscle_activation(0.3, &group);
883 let b = params_to_muscle_activation(0.8, &group);
884 assert!(b >= a, "{} must be monotone (a={a} b={b})", group.name());
885 }
886 write_tmp("params_activation_monotone.txt", "OK");
887 }
888
889 #[test]
890 fn test_muscle_names_list() {
891 let rig = MuscleRig::default_rig();
892 let names = rig.muscle_names();
893 assert_eq!(names.len(), rig.count());
894 assert!(
895 names.contains(&"bicep_left"),
896 "expected bicep_left in names"
897 );
898 write_tmp("muscle_names_list.txt", &names.join("\n"));
899 }
900
901 #[test]
902 fn test_evaluate_relaxed_rig_all_zero() {
903 let rig = MuscleRig::default_rig();
904 let weights = rig.evaluate();
906 for (k, v) in &weights {
907 assert_eq!(*v, 0.0, "relaxed rig: {k} should be 0 but is {v}");
908 }
909 write_tmp("evaluate_relaxed_all_zero.txt", "OK");
910 }
911
912 #[test]
913 fn test_side_equality() {
914 assert_eq!(Side::Left, Side::Left);
915 assert_ne!(Side::Left, Side::Right);
916 assert_ne!(Side::Center, Side::Left);
917 write_tmp("side_equality.txt", "OK");
918 }
919}