1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10pub type BodyParams = HashMap<String, f32>;
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash)]
19pub enum BodyCategory {
20 Ectomorph,
22 Mesomorph,
24 Endomorph,
26 Average,
28 Petite,
30 Tall,
32 Child,
34 Elder,
36 Custom(String),
38}
39
40impl BodyCategory {
41 pub fn name(&self) -> &str {
43 match self {
44 BodyCategory::Ectomorph => "Ectomorph",
45 BodyCategory::Mesomorph => "Mesomorph",
46 BodyCategory::Endomorph => "Endomorph",
47 BodyCategory::Average => "Average",
48 BodyCategory::Petite => "Petite",
49 BodyCategory::Tall => "Tall",
50 BodyCategory::Child => "Child",
51 BodyCategory::Elder => "Elder",
52 BodyCategory::Custom(s) => s.as_str(),
53 }
54 }
55
56 pub fn description(&self) -> &str {
58 match self {
59 BodyCategory::Ectomorph => "Slim body type with low body fat and lean muscle.",
60 BodyCategory::Mesomorph => "Muscular and athletic body type.",
61 BodyCategory::Endomorph => "Rounder body type with higher body fat.",
62 BodyCategory::Average => "Balanced, average body proportions.",
63 BodyCategory::Petite => "Short stature with proportionally smaller frame.",
64 BodyCategory::Tall => "Tall stature with elongated proportions.",
65 BodyCategory::Child => "Child body proportions with low muscle mass.",
66 BodyCategory::Elder => "Elderly body proportions with reduced muscle mass.",
67 BodyCategory::Custom(_) => "User-defined custom body category.",
68 }
69 }
70
71 pub fn all_named() -> Vec<BodyCategory> {
73 vec![
74 BodyCategory::Ectomorph,
75 BodyCategory::Mesomorph,
76 BodyCategory::Endomorph,
77 BodyCategory::Average,
78 BodyCategory::Petite,
79 BodyCategory::Tall,
80 BodyCategory::Child,
81 BodyCategory::Elder,
82 ]
83 }
84}
85
86pub struct BodyPreset {
92 pub name: String,
94 pub category: BodyCategory,
96 pub description: String,
98 pub params: BodyParams,
100 pub tags: Vec<String>,
102}
103
104impl BodyPreset {
105 pub fn new(name: impl Into<String>, category: BodyCategory) -> Self {
107 Self {
108 name: name.into(),
109 category,
110 description: String::new(),
111 params: HashMap::new(),
112 tags: Vec::new(),
113 }
114 }
115
116 pub fn with_param(mut self, key: impl Into<String>, value: f32) -> Self {
118 self.params.insert(key.into(), value);
119 self
120 }
121
122 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
124 self.description = desc.into();
125 self
126 }
127
128 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
130 self.tags.push(tag.into());
131 self
132 }
133
134 pub fn get_param(&self, key: &str) -> f32 {
136 *self.params.get(key).unwrap_or(&0.5)
137 }
138
139 pub fn param_count(&self) -> usize {
141 self.params.len()
142 }
143}
144
145pub struct PresetLibrary {
151 presets: Vec<BodyPreset>,
152}
153
154impl PresetLibrary {
155 pub fn new() -> Self {
157 Self {
158 presets: Vec::new(),
159 }
160 }
161
162 pub fn add(&mut self, preset: BodyPreset) {
164 self.presets.push(preset);
165 }
166
167 pub fn get(&self, name: &str) -> Option<&BodyPreset> {
169 self.presets.iter().find(|p| p.name == name)
170 }
171
172 pub fn preset_count(&self) -> usize {
174 self.presets.len()
175 }
176
177 pub fn by_category(&self, cat: &BodyCategory) -> Vec<&BodyPreset> {
179 self.presets.iter().filter(|p| &p.category == cat).collect()
180 }
181
182 pub fn with_tag(&self, tag: &str) -> Vec<&BodyPreset> {
184 self.presets
185 .iter()
186 .filter(|p| p.tags.iter().any(|t| t == tag))
187 .collect()
188 }
189
190 pub fn names(&self) -> Vec<&str> {
192 self.presets.iter().map(|p| p.name.as_str()).collect()
193 }
194
195 pub fn blend(&self, name_a: &str, name_b: &str, t: f32) -> Option<BodyParams> {
199 let a = self.get(name_a)?;
200 let b = self.get(name_b)?;
201
202 let mut keys: std::collections::HashSet<&str> = std::collections::HashSet::new();
204 for k in a.params.keys() {
205 keys.insert(k.as_str());
206 }
207 for k in b.params.keys() {
208 keys.insert(k.as_str());
209 }
210
211 let mut result = HashMap::new();
212 for key in keys {
213 let va = a.get_param(key);
214 let vb = b.get_param(key);
215 result.insert(key.to_string(), va + (vb - va) * t);
216 }
217 Some(result)
218 }
219
220 pub fn nearest(&self, params: &BodyParams) -> Option<&BodyPreset> {
224 let keys: Vec<&str> = params.keys().map(|k| k.as_str()).collect();
226
227 self.presets.iter().min_by(|a, b| {
228 let dist_a = l2_distance(a, params, &keys);
229 let dist_b = l2_distance(b, params, &keys);
230 dist_a
231 .partial_cmp(&dist_b)
232 .unwrap_or(std::cmp::Ordering::Equal)
233 })
234 }
235}
236
237impl Default for PresetLibrary {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243fn l2_distance(preset: &BodyPreset, params: &BodyParams, keys: &[&str]) -> f32 {
245 keys.iter()
246 .map(|k| {
247 let va = preset.get_param(k);
248 let vb = *params.get(*k).unwrap_or(&0.5);
249 (va - vb) * (va - vb)
250 })
251 .sum::<f32>()
252 .sqrt()
253}
254
255pub fn preset_average() -> BodyPreset {
261 BodyPreset::new("average", BodyCategory::Average)
262 .with_description("Average adult with balanced proportions.")
263 .with_param("height", 0.5)
264 .with_param("weight", 0.5)
265 .with_param("muscle", 0.5)
266 .with_param("age", 0.5)
267 .with_param("bmi_factor", 0.5)
268 .with_param("shoulder_width", 0.5)
269 .with_param("hip_width", 0.5)
270 .with_param("leg_length", 0.5)
271 .with_param("torso_length", 0.5)
272 .with_param("arm_length", 0.5)
273 .with_tag("neutral")
274 .with_tag("adult")
275}
276
277pub fn preset_athletic() -> BodyPreset {
279 BodyPreset::new("athletic", BodyCategory::Mesomorph)
280 .with_description("Athletic build with high muscle tone and low body fat.")
281 .with_param("height", 0.55)
282 .with_param("weight", 0.45)
283 .with_param("muscle", 0.75)
284 .with_param("age", 0.3)
285 .with_param("bmi_factor", 0.35)
286 .with_param("shoulder_width", 0.65)
287 .with_param("hip_width", 0.5)
288 .with_param("leg_length", 0.5)
289 .with_param("torso_length", 0.5)
290 .with_param("arm_length", 0.5)
291 .with_tag("fit")
292 .with_tag("adult")
293 .with_tag("sport")
294}
295
296pub fn preset_slender() -> BodyPreset {
298 BodyPreset::new("slender", BodyCategory::Ectomorph)
299 .with_description("Slender build with low body fat and lean muscle.")
300 .with_param("height", 0.55)
301 .with_param("weight", 0.2)
302 .with_param("muscle", 0.3)
303 .with_param("age", 0.3)
304 .with_param("bmi_factor", 0.15)
305 .with_param("shoulder_width", 0.45)
306 .with_param("hip_width", 0.5)
307 .with_param("leg_length", 0.5)
308 .with_param("torso_length", 0.5)
309 .with_param("arm_length", 0.5)
310 .with_tag("slim")
311 .with_tag("adult")
312}
313
314pub fn preset_heavy() -> BodyPreset {
316 BodyPreset::new("heavy", BodyCategory::Endomorph)
317 .with_description("Heavy build with higher body fat and rounded proportions.")
318 .with_param("height", 0.45)
319 .with_param("weight", 0.85)
320 .with_param("muscle", 0.3)
321 .with_param("age", 0.45)
322 .with_param("bmi_factor", 0.85)
323 .with_param("shoulder_width", 0.5)
324 .with_param("hip_width", 0.5)
325 .with_param("leg_length", 0.5)
326 .with_param("torso_length", 0.5)
327 .with_param("arm_length", 0.5)
328 .with_tag("heavy")
329 .with_tag("adult")
330}
331
332pub fn preset_muscular() -> BodyPreset {
334 BodyPreset::new("muscular", BodyCategory::Mesomorph)
335 .with_description("Highly muscular build with broad shoulders.")
336 .with_param("height", 0.6)
337 .with_param("weight", 0.6)
338 .with_param("muscle", 0.9)
339 .with_param("age", 0.35)
340 .with_param("bmi_factor", 0.5)
341 .with_param("shoulder_width", 0.8)
342 .with_param("hip_width", 0.5)
343 .with_param("leg_length", 0.5)
344 .with_param("torso_length", 0.5)
345 .with_param("arm_length", 0.5)
346 .with_tag("muscle")
347 .with_tag("adult")
348 .with_tag("sport")
349}
350
351pub fn preset_petite() -> BodyPreset {
353 BodyPreset::new("petite", BodyCategory::Petite)
354 .with_description("Petite build with short stature and small proportions.")
355 .with_param("height", 0.2)
356 .with_param("weight", 0.35)
357 .with_param("muscle", 0.4)
358 .with_param("age", 0.5)
359 .with_param("bmi_factor", 0.5)
360 .with_param("shoulder_width", 0.5)
361 .with_param("hip_width", 0.5)
362 .with_param("leg_length", 0.3)
363 .with_param("torso_length", 0.35)
364 .with_param("arm_length", 0.5)
365 .with_tag("short")
366 .with_tag("adult")
367}
368
369pub fn preset_tall() -> BodyPreset {
371 BodyPreset::new("tall", BodyCategory::Tall)
372 .with_description("Tall build with elongated limbs and proportions.")
373 .with_param("height", 0.85)
374 .with_param("weight", 0.5)
375 .with_param("muscle", 0.5)
376 .with_param("age", 0.5)
377 .with_param("bmi_factor", 0.5)
378 .with_param("shoulder_width", 0.5)
379 .with_param("hip_width", 0.5)
380 .with_param("leg_length", 0.75)
381 .with_param("torso_length", 0.5)
382 .with_param("arm_length", 0.7)
383 .with_tag("tall")
384 .with_tag("adult")
385}
386
387pub fn preset_child() -> BodyPreset {
389 BodyPreset::new("child", BodyCategory::Child)
390 .with_description("Child body proportions with low muscle mass and short stature.")
391 .with_param("height", 0.1)
392 .with_param("weight", 0.2)
393 .with_param("muscle", 0.2)
394 .with_param("age", 0.05)
395 .with_param("bmi_factor", 0.5)
396 .with_param("shoulder_width", 0.5)
397 .with_param("hip_width", 0.5)
398 .with_param("leg_length", 0.5)
399 .with_param("torso_length", 0.5)
400 .with_param("arm_length", 0.5)
401 .with_tag("child")
402 .with_tag("young")
403}
404
405pub fn preset_elder() -> BodyPreset {
407 BodyPreset::new("elder", BodyCategory::Elder)
408 .with_description("Elderly body proportions with reduced muscle mass.")
409 .with_param("height", 0.45)
410 .with_param("weight", 0.55)
411 .with_param("muscle", 0.2)
412 .with_param("age", 0.9)
413 .with_param("bmi_factor", 0.5)
414 .with_param("shoulder_width", 0.5)
415 .with_param("hip_width", 0.5)
416 .with_param("leg_length", 0.5)
417 .with_param("torso_length", 0.5)
418 .with_param("arm_length", 0.5)
419 .with_tag("elder")
420 .with_tag("senior")
421}
422
423pub fn standard_preset_library() -> PresetLibrary {
429 let mut lib = PresetLibrary::new();
430 lib.add(preset_average());
431 lib.add(preset_athletic());
432 lib.add(preset_slender());
433 lib.add(preset_heavy());
434 lib.add(preset_muscular());
435 lib.add(preset_petite());
436 lib.add(preset_tall());
437 lib.add(preset_child());
438 lib.add(preset_elder());
439
440 lib.add(
442 BodyPreset::new("bodybuilder", BodyCategory::Mesomorph)
443 .with_description("Extreme bodybuilder physique.")
444 .with_param("height", 0.6)
445 .with_param("weight", 0.7)
446 .with_param("muscle", 1.0)
447 .with_param("age", 0.35)
448 .with_param("bmi_factor", 0.5)
449 .with_param("shoulder_width", 0.9)
450 .with_param("hip_width", 0.55)
451 .with_param("leg_length", 0.5)
452 .with_param("torso_length", 0.5)
453 .with_param("arm_length", 0.5)
454 .with_tag("muscle")
455 .with_tag("adult")
456 .with_tag("sport"),
457 );
458 lib.add(
459 BodyPreset::new("runner", BodyCategory::Ectomorph)
460 .with_description("Long-distance runner with lean build and long legs.")
461 .with_param("height", 0.6)
462 .with_param("weight", 0.25)
463 .with_param("muscle", 0.45)
464 .with_param("age", 0.3)
465 .with_param("bmi_factor", 0.2)
466 .with_param("shoulder_width", 0.4)
467 .with_param("hip_width", 0.45)
468 .with_param("leg_length", 0.7)
469 .with_param("torso_length", 0.45)
470 .with_param("arm_length", 0.6)
471 .with_tag("slim")
472 .with_tag("sport")
473 .with_tag("adult"),
474 );
475 lib.add(
476 BodyPreset::new("stocky", BodyCategory::Endomorph)
477 .with_description("Short and broad with dense musculature.")
478 .with_param("height", 0.3)
479 .with_param("weight", 0.65)
480 .with_param("muscle", 0.55)
481 .with_param("age", 0.4)
482 .with_param("bmi_factor", 0.7)
483 .with_param("shoulder_width", 0.6)
484 .with_param("hip_width", 0.55)
485 .with_param("leg_length", 0.35)
486 .with_param("torso_length", 0.4)
487 .with_param("arm_length", 0.4)
488 .with_tag("heavy")
489 .with_tag("adult"),
490 );
491
492 lib
493}
494
495#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_body_category_name() {
505 assert_eq!(BodyCategory::Ectomorph.name(), "Ectomorph");
506 assert_eq!(BodyCategory::Mesomorph.name(), "Mesomorph");
507 assert_eq!(BodyCategory::Endomorph.name(), "Endomorph");
508 assert_eq!(BodyCategory::Average.name(), "Average");
509 assert_eq!(BodyCategory::Petite.name(), "Petite");
510 assert_eq!(BodyCategory::Tall.name(), "Tall");
511 assert_eq!(BodyCategory::Child.name(), "Child");
512 assert_eq!(BodyCategory::Elder.name(), "Elder");
513 assert_eq!(BodyCategory::Custom("MyType".to_string()).name(), "MyType");
514 }
515
516 #[test]
517 fn test_body_category_all_named() {
518 let named = BodyCategory::all_named();
519 assert_eq!(named.len(), 8);
520 for cat in &named {
522 assert!(!matches!(cat, BodyCategory::Custom(_)));
523 }
524 assert!(named.contains(&BodyCategory::Ectomorph));
525 assert!(named.contains(&BodyCategory::Elder));
526 }
527
528 #[test]
529 fn test_preset_new() {
530 let p = BodyPreset::new("test_preset", BodyCategory::Average);
531 assert_eq!(p.name, "test_preset");
532 assert_eq!(p.category, BodyCategory::Average);
533 assert!(p.params.is_empty());
534 assert!(p.tags.is_empty());
535 assert!(p.description.is_empty());
536 }
537
538 #[test]
539 fn test_preset_with_param() {
540 let p = BodyPreset::new("x", BodyCategory::Average)
541 .with_param("height", 0.7)
542 .with_param("muscle", 0.3);
543 assert_eq!(p.param_count(), 2);
544 assert!((p.get_param("height") - 0.7).abs() < 1e-6);
545 assert!((p.get_param("muscle") - 0.3).abs() < 1e-6);
546 }
547
548 #[test]
549 fn test_preset_get_param_missing() {
550 let p = BodyPreset::new("x", BodyCategory::Average);
551 assert!((p.get_param("nonexistent") - 0.5).abs() < 1e-6);
553 }
554
555 #[test]
556 fn test_library_add_and_get() {
557 let mut lib = PresetLibrary::new();
558 assert_eq!(lib.preset_count(), 0);
559 lib.add(preset_average());
560 lib.add(preset_athletic());
561 assert_eq!(lib.preset_count(), 2);
562 assert!(lib.get("average").is_some());
563 assert!(lib.get("athletic").is_some());
564 assert!(lib.get("nonexistent").is_none());
565 }
566
567 #[test]
568 fn test_library_by_category() {
569 let lib = standard_preset_library();
570 let mesomorphs = lib.by_category(&BodyCategory::Mesomorph);
571 assert!(mesomorphs.len() >= 2);
573 for p in &mesomorphs {
574 assert_eq!(p.category, BodyCategory::Mesomorph);
575 }
576 }
577
578 #[test]
579 fn test_library_with_tag() {
580 let lib = standard_preset_library();
581 let sport = lib.with_tag("sport");
582 assert!(!sport.is_empty());
583 for p in &sport {
584 assert!(p.tags.contains(&"sport".to_string()));
585 }
586
587 let adults = lib.with_tag("adult");
588 assert!(adults.len() > 3);
589 }
590
591 #[test]
592 fn test_library_blend() {
593 let lib = standard_preset_library();
594
595 let blended_0 = lib.blend("average", "athletic", 0.0).expect("blend failed");
597 let avg = lib.get("average").expect("should succeed");
598 assert!((blended_0["height"] - avg.get_param("height")).abs() < 1e-5);
599
600 let blended_1 = lib.blend("average", "athletic", 1.0).expect("blend failed");
602 let ath = lib.get("athletic").expect("should succeed");
603 assert!((blended_1["height"] - ath.get_param("height")).abs() < 1e-5);
604
605 let blended_half = lib.blend("average", "athletic", 0.5).expect("blend failed");
607 let expected_height = (avg.get_param("height") + ath.get_param("height")) / 2.0;
608 assert!((blended_half["height"] - expected_height).abs() < 1e-5);
609
610 assert!(lib.blend("average", "nonexistent", 0.5).is_none());
612 }
613
614 #[test]
615 fn test_library_nearest() {
616 let lib = standard_preset_library();
617
618 let mut params: BodyParams = HashMap::new();
620 params.insert("height".to_string(), 0.5);
621 params.insert("weight".to_string(), 0.5);
622 params.insert("muscle".to_string(), 0.5);
623 params.insert("age".to_string(), 0.5);
624 let nearest = lib.nearest(¶ms).expect("should find nearest");
625 assert_eq!(nearest.name, "average");
626
627 let mut child_params: BodyParams = HashMap::new();
629 child_params.insert("height".to_string(), 0.1);
630 child_params.insert("muscle".to_string(), 0.2);
631 child_params.insert("age".to_string(), 0.05);
632 let nearest_child = lib.nearest(&child_params).expect("should find nearest");
633 assert_eq!(nearest_child.name, "child");
634 }
635
636 #[test]
637 fn test_preset_average() {
638 let p = preset_average();
639 assert_eq!(p.name, "average");
640 assert_eq!(p.category, BodyCategory::Average);
641 for key in &[
643 "height",
644 "weight",
645 "muscle",
646 "age",
647 "bmi_factor",
648 "shoulder_width",
649 "hip_width",
650 "leg_length",
651 "torso_length",
652 "arm_length",
653 ] {
654 assert!(
655 (p.get_param(key) - 0.5).abs() < 1e-6,
656 "param '{}' expected 0.5, got {}",
657 key,
658 p.get_param(key)
659 );
660 }
661 }
662
663 #[test]
664 fn test_preset_athletic() {
665 let p = preset_athletic();
666 assert_eq!(p.name, "athletic");
667 assert_eq!(p.category, BodyCategory::Mesomorph);
668 assert!((p.get_param("height") - 0.55).abs() < 1e-6);
669 assert!((p.get_param("weight") - 0.45).abs() < 1e-6);
670 assert!((p.get_param("muscle") - 0.75).abs() < 1e-6);
671 assert!((p.get_param("age") - 0.3).abs() < 1e-6);
672 assert!((p.get_param("bmi_factor") - 0.35).abs() < 1e-6);
673 assert!((p.get_param("shoulder_width") - 0.65).abs() < 1e-6);
674 assert!(p.tags.contains(&"sport".to_string()));
675 }
676
677 #[test]
678 fn test_standard_preset_library() {
679 let lib = standard_preset_library();
680 assert!(
681 lib.preset_count() >= 12,
682 "expected at least 12 presets, got {}",
683 lib.preset_count()
684 );
685 let names = lib.names();
686 assert!(names.contains(&"average"));
687 assert!(names.contains(&"athletic"));
688 assert!(names.contains(&"slender"));
689 assert!(names.contains(&"heavy"));
690 assert!(names.contains(&"muscular"));
691 assert!(names.contains(&"petite"));
692 assert!(names.contains(&"tall"));
693 assert!(names.contains(&"child"));
694 assert!(names.contains(&"elder"));
695 }
696
697 #[test]
698 fn test_preset_child_age() {
699 let p = preset_child();
700 assert_eq!(p.name, "child");
701 assert_eq!(p.category, BodyCategory::Child);
702 assert!(p.get_param("age") < 0.1, "child age param should be < 0.1");
704 assert!(
706 p.get_param("height") < 0.2,
707 "child height param should be < 0.2"
708 );
709 assert!(
711 p.get_param("muscle") < 0.3,
712 "child muscle param should be < 0.3"
713 );
714 assert!(p.tags.contains(&"child".to_string()));
715 }
716}