1#![allow(dead_code)]
5#![allow(clippy::too_many_arguments)]
6
7use crate::engine::MeshBuffers;
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum LandmarkId {
26 TopOfHead,
28 ChinCenter,
29 C7Cervical,
32 T10Thoracic,
34 L4Lumbar,
36 AcromionLeft,
38 AcromionRight,
39 ElbowLeft,
41 ElbowRight,
42 WristLeft,
43 WristRight,
44 HipLeft,
46 HipRight,
47 KneeLeft,
48 KneeRight,
49 AnkleLeft,
50 AnkleRight,
51 NeckBase,
53 ChestCenter,
54 WaistCenter,
55 NabelCenter,
56 HeelLeft,
58 HeelRight,
59}
60
61impl LandmarkId {
62 pub fn all() -> Vec<LandmarkId> {
64 vec![
65 LandmarkId::TopOfHead,
66 LandmarkId::ChinCenter,
67 LandmarkId::C7Cervical,
68 LandmarkId::T10Thoracic,
69 LandmarkId::L4Lumbar,
70 LandmarkId::AcromionLeft,
71 LandmarkId::AcromionRight,
72 LandmarkId::ElbowLeft,
73 LandmarkId::ElbowRight,
74 LandmarkId::WristLeft,
75 LandmarkId::WristRight,
76 LandmarkId::HipLeft,
77 LandmarkId::HipRight,
78 LandmarkId::KneeLeft,
79 LandmarkId::KneeRight,
80 LandmarkId::AnkleLeft,
81 LandmarkId::AnkleRight,
82 LandmarkId::NeckBase,
83 LandmarkId::ChestCenter,
84 LandmarkId::WaistCenter,
85 LandmarkId::NabelCenter,
86 LandmarkId::HeelLeft,
87 LandmarkId::HeelRight,
88 ]
89 }
90
91 pub fn name(&self) -> &'static str {
93 match self {
94 LandmarkId::TopOfHead => "Top of Head",
95 LandmarkId::ChinCenter => "Chin Center",
96 LandmarkId::C7Cervical => "C7 Cervical",
97 LandmarkId::T10Thoracic => "T10 Thoracic",
98 LandmarkId::L4Lumbar => "L4 Lumbar",
99 LandmarkId::AcromionLeft => "Acromion Left",
100 LandmarkId::AcromionRight => "Acromion Right",
101 LandmarkId::ElbowLeft => "Elbow Left",
102 LandmarkId::ElbowRight => "Elbow Right",
103 LandmarkId::WristLeft => "Wrist Left",
104 LandmarkId::WristRight => "Wrist Right",
105 LandmarkId::HipLeft => "Hip Left",
106 LandmarkId::HipRight => "Hip Right",
107 LandmarkId::KneeLeft => "Knee Left",
108 LandmarkId::KneeRight => "Knee Right",
109 LandmarkId::AnkleLeft => "Ankle Left",
110 LandmarkId::AnkleRight => "Ankle Right",
111 LandmarkId::NeckBase => "Neck Base",
112 LandmarkId::ChestCenter => "Chest Center",
113 LandmarkId::WaistCenter => "Waist Center",
114 LandmarkId::NabelCenter => "Navel Center",
115 LandmarkId::HeelLeft => "Heel Left",
116 LandmarkId::HeelRight => "Heel Right",
117 }
118 }
119
120 pub fn is_bilateral(&self) -> bool {
122 self.mirror().is_some()
123 }
124
125 pub fn mirror(&self) -> Option<LandmarkId> {
127 match self {
128 LandmarkId::AcromionLeft => Some(LandmarkId::AcromionRight),
129 LandmarkId::AcromionRight => Some(LandmarkId::AcromionLeft),
130 LandmarkId::ElbowLeft => Some(LandmarkId::ElbowRight),
131 LandmarkId::ElbowRight => Some(LandmarkId::ElbowLeft),
132 LandmarkId::WristLeft => Some(LandmarkId::WristRight),
133 LandmarkId::WristRight => Some(LandmarkId::WristLeft),
134 LandmarkId::HipLeft => Some(LandmarkId::HipRight),
135 LandmarkId::HipRight => Some(LandmarkId::HipLeft),
136 LandmarkId::KneeLeft => Some(LandmarkId::KneeRight),
137 LandmarkId::KneeRight => Some(LandmarkId::KneeLeft),
138 LandmarkId::AnkleLeft => Some(LandmarkId::AnkleRight),
139 LandmarkId::AnkleRight => Some(LandmarkId::AnkleLeft),
140 LandmarkId::HeelLeft => Some(LandmarkId::HeelRight),
141 LandmarkId::HeelRight => Some(LandmarkId::HeelLeft),
142 LandmarkId::TopOfHead
144 | LandmarkId::ChinCenter
145 | LandmarkId::C7Cervical
146 | LandmarkId::T10Thoracic
147 | LandmarkId::L4Lumbar
148 | LandmarkId::NeckBase
149 | LandmarkId::ChestCenter
150 | LandmarkId::WaistCenter
151 | LandmarkId::NabelCenter => None,
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
162pub struct Landmark {
163 pub id: LandmarkId,
165 pub position: [f32; 3],
167 pub confidence: f32,
169 pub vertex_index: Option<u32>,
171}
172
173impl Landmark {
174 pub fn new(
176 id: LandmarkId,
177 position: [f32; 3],
178 confidence: f32,
179 vertex_index: Option<u32>,
180 ) -> Self {
181 Landmark {
182 id,
183 position,
184 confidence,
185 vertex_index,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq)]
196pub enum Side {
197 Left,
198 Right,
199}
200
201#[derive(Debug, Clone, Default)]
207pub struct LandmarkSet {
208 landmarks: HashMap<LandmarkId, Landmark>,
209}
210
211impl LandmarkSet {
212 pub fn new() -> Self {
214 LandmarkSet {
215 landmarks: HashMap::new(),
216 }
217 }
218
219 pub fn insert(&mut self, landmark: Landmark) {
221 self.landmarks.insert(landmark.id.clone(), landmark);
222 }
223
224 pub fn get(&self, id: &LandmarkId) -> Option<&Landmark> {
226 self.landmarks.get(id)
227 }
228
229 pub fn count(&self) -> usize {
231 self.landmarks.len()
232 }
233
234 pub fn all_positions(&self) -> Vec<([f32; 3], f32)> {
236 self.landmarks
237 .values()
238 .map(|lm| (lm.position, lm.confidence))
239 .collect()
240 }
241
242 pub fn distance(&self, a: &LandmarkId, b: &LandmarkId) -> Option<f32> {
244 let pa = self.landmarks.get(a)?.position;
245 let pb = self.landmarks.get(b)?.position;
246 Some(vec3_dist(pa, pb))
247 }
248
249 pub fn body_height(&self) -> Option<f32> {
253 let head = self.landmarks.get(&LandmarkId::TopOfHead)?.position;
254 let floor_y = match (
256 self.landmarks.get(&LandmarkId::HeelLeft),
257 self.landmarks.get(&LandmarkId::HeelRight),
258 ) {
259 (Some(l), Some(r)) => l.position[1].min(r.position[1]),
260 (Some(l), None) => l.position[1],
261 (None, Some(r)) => r.position[1],
262 (None, None) => {
263 let al = self
265 .landmarks
266 .get(&LandmarkId::AnkleLeft)
267 .map(|l| l.position[1]);
268 let ar = self
269 .landmarks
270 .get(&LandmarkId::AnkleRight)
271 .map(|l| l.position[1]);
272 match (al, ar) {
273 (Some(a), Some(b)) => a.min(b),
274 (Some(a), None) | (None, Some(a)) => a,
275 (None, None) => return None,
276 }
277 }
278 };
279 Some((head[1] - floor_y).abs())
280 }
281
282 pub fn shoulder_width(&self) -> Option<f32> {
284 self.distance(&LandmarkId::AcromionLeft, &LandmarkId::AcromionRight)
285 }
286
287 pub fn hip_width(&self) -> Option<f32> {
289 self.distance(&LandmarkId::HipLeft, &LandmarkId::HipRight)
290 }
291
292 pub fn arm_length(&self, side: Side) -> Option<f32> {
294 let (shoulder, wrist) = match side {
295 Side::Left => (&LandmarkId::AcromionLeft, &LandmarkId::WristLeft),
296 Side::Right => (&LandmarkId::AcromionRight, &LandmarkId::WristRight),
297 };
298 self.distance(shoulder, wrist)
299 }
300
301 pub fn leg_length(&self, side: Side) -> Option<f32> {
303 let (hip, ankle) = match side {
304 Side::Left => (&LandmarkId::HipLeft, &LandmarkId::AnkleLeft),
305 Side::Right => (&LandmarkId::HipRight, &LandmarkId::AnkleRight),
306 };
307 self.distance(hip, ankle)
308 }
309
310 pub fn symmetry_error(&self) -> f32 {
316 let mut max_err = 0.0f32;
317 for id in LandmarkId::all() {
318 if let Some(mirror_id) = id.mirror() {
319 if let (Some(lm_a), Some(lm_b)) =
320 (self.landmarks.get(&id), self.landmarks.get(&mirror_id))
321 {
322 let err = (lm_a.position[0].abs() - lm_b.position[0].abs()).abs();
324 let dy = (lm_a.position[1] - lm_b.position[1]).abs();
326 let dz = (lm_a.position[2] - lm_b.position[2]).abs();
327 let combined = err.max(dy).max(dz);
328 if combined > max_err {
329 max_err = combined;
330 }
331 }
332 }
333 }
334 max_err
335 }
336
337 pub fn to_map(&self) -> HashMap<String, [f32; 3]> {
339 self.landmarks
340 .values()
341 .map(|lm| (lm.id.name().to_string(), lm.position))
342 .collect()
343 }
344}
345
346#[inline]
351fn vec3_dist(a: [f32; 3], b: [f32; 3]) -> f32 {
352 let dx = a[0] - b[0];
353 let dy = a[1] - b[1];
354 let dz = a[2] - b[2];
355 (dx * dx + dy * dy + dz * dz).sqrt()
356}
357
358#[inline]
359fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
360 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
361}
362
363#[inline]
364fn vec3_add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
365 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
366}
367
368#[inline]
369fn vec3_scale(v: [f32; 3], s: f32) -> [f32; 3] {
370 [v[0] * s, v[1] * s, v[2] * s]
371}
372
373#[inline]
374fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
375 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
376}
377
378#[inline]
379fn vec3_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
380 [
381 a[1] * b[2] - a[2] * b[1],
382 a[2] * b[0] - a[0] * b[2],
383 a[0] * b[1] - a[1] * b[0],
384 ]
385}
386
387#[inline]
388fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
389 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
390 if len < 1e-9 {
391 [0.0, 1.0, 0.0]
392 } else {
393 [v[0] / len, v[1] / len, v[2] / len]
394 }
395}
396
397fn mesh_aabb(positions: &[[f32; 3]]) -> ([f32; 3], [f32; 3]) {
404 if positions.is_empty() {
405 return ([0.0; 3], [0.0; 3]);
406 }
407 let mut mn = positions[0];
408 let mut mx = positions[0];
409 for p in positions.iter().skip(1) {
410 for i in 0..3 {
411 if p[i] < mn[i] {
412 mn[i] = p[i];
413 }
414 if p[i] > mx[i] {
415 mx[i] = p[i];
416 }
417 }
418 }
419 (mn, mx)
420}
421
422#[inline]
424fn lerp1(min_v: f32, max_v: f32, t: f32) -> f32 {
425 min_v + (max_v - min_v) * t
426}
427
428pub fn detect_landmarks(mesh: &MeshBuffers) -> LandmarkSet {
438 let positions = &mesh.positions;
439 let mut set = LandmarkSet::new();
440
441 if positions.is_empty() {
442 return set;
443 }
444
445 let (mn, mx) = mesh_aabb(positions);
446 let cx = (mn[0] + mx[0]) * 0.5; let lx = lerp1(mn[0], cx, 0.5); let rx = lerp1(cx, mx[0], 0.5); let make = |id: LandmarkId, approx: [f32; 3]| -> Landmark {
452 let (vidx, _dist) = nearest_vertex(mesh, approx);
453 let pos = positions[vidx as usize];
454 Landmark::new(id, pos, 1.0, Some(vidx))
455 };
456
457 let y_frac = |t: f32| lerp1(mn[1], mx[1], t);
460
461 set.insert(make(
463 LandmarkId::TopOfHead,
464 [cx, y_frac(1.00), (mn[2] + mx[2]) * 0.5],
465 ));
466 set.insert(make(LandmarkId::ChinCenter, [cx, y_frac(0.88), mx[2]]));
467
468 set.insert(make(
470 LandmarkId::NeckBase,
471 [cx, y_frac(0.84), (mn[2] + mx[2]) * 0.5],
472 ));
473 set.insert(make(LandmarkId::C7Cervical, [cx, y_frac(0.83), mn[2]]));
474
475 set.insert(make(
477 LandmarkId::AcromionLeft,
478 [lx, y_frac(0.79), (mn[2] + mx[2]) * 0.5],
479 ));
480 set.insert(make(
481 LandmarkId::AcromionRight,
482 [rx, y_frac(0.79), (mn[2] + mx[2]) * 0.5],
483 ));
484
485 set.insert(make(LandmarkId::ChestCenter, [cx, y_frac(0.72), mx[2]]));
487 set.insert(make(LandmarkId::T10Thoracic, [cx, y_frac(0.65), mn[2]]));
488
489 set.insert(make(
491 LandmarkId::ElbowLeft,
492 [mn[0], y_frac(0.60), (mn[2] + mx[2]) * 0.5],
493 ));
494 set.insert(make(
495 LandmarkId::ElbowRight,
496 [mx[0], y_frac(0.60), (mn[2] + mx[2]) * 0.5],
497 ));
498
499 set.insert(make(LandmarkId::NabelCenter, [cx, y_frac(0.57), mx[2]]));
501
502 set.insert(make(LandmarkId::WaistCenter, [cx, y_frac(0.54), mn[2]]));
504 set.insert(make(LandmarkId::L4Lumbar, [cx, y_frac(0.52), mn[2]]));
505
506 set.insert(make(
508 LandmarkId::HipLeft,
509 [lx, y_frac(0.48), (mn[2] + mx[2]) * 0.5],
510 ));
511 set.insert(make(
512 LandmarkId::HipRight,
513 [rx, y_frac(0.48), (mn[2] + mx[2]) * 0.5],
514 ));
515
516 set.insert(make(
518 LandmarkId::WristLeft,
519 [mn[0], y_frac(0.38), (mn[2] + mx[2]) * 0.5],
520 ));
521 set.insert(make(
522 LandmarkId::WristRight,
523 [mx[0], y_frac(0.38), (mn[2] + mx[2]) * 0.5],
524 ));
525
526 set.insert(make(LandmarkId::KneeLeft, [lx, y_frac(0.27), mx[2]]));
528 set.insert(make(LandmarkId::KneeRight, [rx, y_frac(0.27), mx[2]]));
529
530 set.insert(make(LandmarkId::AnkleLeft, [lx, y_frac(0.07), mx[2]]));
532 set.insert(make(LandmarkId::AnkleRight, [rx, y_frac(0.07), mx[2]]));
533
534 set.insert(make(LandmarkId::HeelLeft, [lx, y_frac(0.01), mn[2]]));
536 set.insert(make(LandmarkId::HeelRight, [rx, y_frac(0.01), mn[2]]));
537
538 set
539}
540
541pub fn nearest_vertex(mesh: &MeshBuffers, pos: [f32; 3]) -> (u32, f32) {
549 if mesh.positions.is_empty() {
550 return (0, 0.0);
551 }
552 let mut best_idx = 0u32;
553 let mut best_dist = f32::MAX;
554 for (i, p) in mesh.positions.iter().enumerate() {
555 let d = vec3_dist(*p, pos);
556 if d < best_dist {
557 best_dist = d;
558 best_idx = i as u32;
559 }
560 }
561 (best_idx, best_dist)
562}
563
564pub fn landmark_frame(mesh: &MeshBuffers, landmark: &Landmark) -> ([f32; 3], [f32; 3], [f32; 3]) {
575 let vidx = landmark
576 .vertex_index
577 .unwrap_or_else(|| nearest_vertex(mesh, landmark.position).0) as usize;
578
579 let normal = if vidx < mesh.normals.len() {
581 vec3_normalize(mesh.normals[vidx])
582 } else {
583 [0.0, 1.0, 0.0]
584 };
585
586 let ref_vec = if normal[0].abs() < 0.9 {
588 [1.0, 0.0, 0.0]
589 } else {
590 [0.0, 1.0, 0.0]
591 };
592 let bitangent = vec3_normalize(vec3_cross(normal, ref_vec));
593 let tangent = vec3_normalize(vec3_cross(bitangent, normal));
594
595 (normal, tangent, bitangent)
596}
597
598pub fn remap_landmarks(
609 set: &LandmarkSet,
610 source_bbox: ([f32; 3], [f32; 3]),
611 target_bbox: ([f32; 3], [f32; 3]),
612) -> LandmarkSet {
613 let (src_min, src_max) = source_bbox;
614 let (tgt_min, tgt_max) = target_bbox;
615
616 let mut out = LandmarkSet::new();
617 for lm in set.landmarks.values() {
618 let mut new_pos = [0.0f32; 3];
619 for i in 0..3 {
620 let src_range = src_max[i] - src_min[i];
621 let t = if src_range.abs() < 1e-9 {
622 0.5
623 } else {
624 (lm.position[i] - src_min[i]) / src_range
625 };
626 new_pos[i] = lerp1(tgt_min[i], tgt_max[i], t);
627 }
628 out.insert(Landmark {
629 id: lm.id.clone(),
630 position: new_pos,
631 confidence: lm.confidence,
632 vertex_index: None,
633 });
634 }
635 out
636}
637
638pub fn transfer_landmarks(
649 template_set: &LandmarkSet,
650 template_mesh: &MeshBuffers,
651 target_mesh: &MeshBuffers,
652) -> LandmarkSet {
653 let mut out = LandmarkSet::new();
654 for lm in template_set.landmarks.values() {
655 let vidx = lm
657 .vertex_index
658 .unwrap_or_else(|| nearest_vertex(template_mesh, lm.position).0);
659
660 let new_pos = if (vidx as usize) < target_mesh.positions.len() {
662 target_mesh.positions[vidx as usize]
663 } else {
664 let (nearest_vidx, _) = nearest_vertex(target_mesh, lm.position);
666 target_mesh.positions[nearest_vidx as usize]
667 };
668
669 let (snap_vidx, _) = nearest_vertex(target_mesh, new_pos);
671
672 out.insert(Landmark {
673 id: lm.id.clone(),
674 position: target_mesh.positions[snap_vidx as usize],
675 confidence: lm.confidence,
676 vertex_index: Some(snap_vidx),
677 });
678 }
679 out
680}
681
682#[cfg(test)]
687mod tests {
688 use super::*;
689 use crate::engine::MeshBuffers;
690
691 fn humanoid_mesh() -> MeshBuffers {
695 let mut positions = Vec::new();
696 for xi in 0..5i32 {
698 for yi in 0..11i32 {
699 for zi in 0..5i32 {
700 let x = -0.3 + xi as f32 * 0.15;
701 let y = yi as f32 * 0.18;
702 let z = -0.2 + zi as f32 * 0.1;
703 positions.push([x, y, z]);
704 }
705 }
706 }
707 let n = positions.len();
708 let normals = vec![[0.0, 1.0, 0.0]; n];
710 let uvs = vec![[0.0, 0.0]; n];
711 let mut indices = Vec::new();
713 let tri_count = (n / 3) * 3;
714 for i in (0..tri_count).step_by(3) {
715 indices.push(i as u32);
716 indices.push((i + 1) as u32);
717 indices.push((i + 2) as u32);
718 }
719 MeshBuffers {
720 positions,
721 normals,
722 uvs,
723 indices,
724 has_suit: false,
725 }
726 }
727
728 fn tiny_mesh() -> MeshBuffers {
730 MeshBuffers {
731 positions: vec![
732 [0.0, 0.0, 0.0],
733 [1.0, 0.0, 0.0],
734 [0.0, 1.0, 0.0],
735 [1.0, 1.0, 0.0],
736 ],
737 normals: vec![[0.0, 0.0, 1.0]; 4],
738 uvs: vec![[0.0, 0.0]; 4],
739 indices: vec![0, 1, 2, 1, 3, 2],
740 has_suit: false,
741 }
742 }
743
744 #[test]
747 fn all_landmarks_count() {
748 let all = LandmarkId::all();
749 assert_eq!(all.len(), 23, "Expected 23 landmarks, got {}", all.len());
750 }
751
752 #[test]
755 fn bilateral_landmarks_have_mirrors() {
756 for id in LandmarkId::all() {
757 if id.is_bilateral() {
758 assert!(
759 id.mirror().is_some(),
760 "{:?} is_bilateral but mirror is None",
761 id
762 );
763 } else {
764 assert!(
765 id.mirror().is_none(),
766 "{:?} is not bilateral but has mirror",
767 id
768 );
769 }
770 }
771 }
772
773 #[test]
776 fn mirror_is_symmetric() {
777 for id in LandmarkId::all() {
778 if let Some(m) = id.mirror() {
779 let back = m.mirror().expect("mirror's mirror should exist");
780 assert_eq!(back, id, "mirror is not symmetric for {:?}", id);
781 }
782 }
783 }
784
785 #[test]
788 fn midline_landmarks_have_no_mirror() {
789 let midline = vec![
790 LandmarkId::TopOfHead,
791 LandmarkId::ChinCenter,
792 LandmarkId::C7Cervical,
793 LandmarkId::T10Thoracic,
794 LandmarkId::L4Lumbar,
795 LandmarkId::NeckBase,
796 LandmarkId::ChestCenter,
797 LandmarkId::WaistCenter,
798 LandmarkId::NabelCenter,
799 ];
800 for id in &midline {
801 assert!(id.mirror().is_none(), "{:?} should have no mirror", id);
802 }
803 }
804
805 #[test]
808 fn landmark_set_insert_and_get() {
809 let mut set = LandmarkSet::new();
810 let lm = Landmark::new(LandmarkId::TopOfHead, [0.0, 1.8, 0.0], 1.0, Some(0));
811 set.insert(lm);
812 assert_eq!(set.count(), 1);
813 let got = set.get(&LandmarkId::TopOfHead).expect("landmark missing");
814 assert!((got.position[1] - 1.8).abs() < 1e-6);
815 }
816
817 #[test]
820 fn detect_landmarks_non_empty() {
821 let mesh = humanoid_mesh();
822 let set = detect_landmarks(&mesh);
823 assert!(set.count() > 0, "Expected landmarks to be detected");
824 }
825
826 #[test]
829 fn top_of_head_near_top() {
830 let mesh = humanoid_mesh();
831 let set = detect_landmarks(&mesh);
832 let (mn, mx) = mesh_aabb(&mesh.positions);
833 let head = set.get(&LandmarkId::TopOfHead).expect("TopOfHead missing");
834 let threshold = lerp1(mn[1], mx[1], 0.80);
836 assert!(
837 head.position[1] >= threshold,
838 "TopOfHead Y={} not above threshold {}",
839 head.position[1],
840 threshold
841 );
842 }
843
844 #[test]
847 fn nearest_vertex_correct() {
848 let mesh = tiny_mesh();
849 let (idx, dist) = nearest_vertex(&mesh, [0.0, 1.0, 0.0]);
850 assert_eq!(idx, 2, "Expected vertex 2 nearest to (0,1,0)");
851 assert!(dist < 1e-5, "Distance should be ~0, got {}", dist);
852 }
853
854 #[test]
857 fn nearest_vertex_empty_mesh() {
858 let empty = MeshBuffers {
859 positions: vec![],
860 normals: vec![],
861 uvs: vec![],
862 indices: vec![],
863 has_suit: false,
864 };
865 let (idx, dist) = nearest_vertex(&empty, [1.0, 2.0, 3.0]);
866 assert_eq!(idx, 0);
867 assert!((dist - 0.0).abs() < 1e-9);
868 }
869
870 #[test]
873 fn landmark_frame_orthonormal() {
874 let mesh = humanoid_mesh();
875 let set = detect_landmarks(&mesh);
876 let lm = set.get(&LandmarkId::NeckBase).expect("NeckBase missing");
877 let (n, t, b) = landmark_frame(&mesh, lm);
878
879 let len_n = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
881 let len_t = (t[0] * t[0] + t[1] * t[1] + t[2] * t[2]).sqrt();
882 let len_b = (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]).sqrt();
883 assert!(
884 (len_n - 1.0).abs() < 1e-5,
885 "normal not unit length: {}",
886 len_n
887 );
888 assert!(
889 (len_t - 1.0).abs() < 1e-5,
890 "tangent not unit length: {}",
891 len_t
892 );
893 assert!(
894 (len_b - 1.0).abs() < 1e-5,
895 "bitangent not unit length: {}",
896 len_b
897 );
898
899 let dot_nt = vec3_dot(n, t);
901 assert!(
902 dot_nt.abs() < 1e-5,
903 "normal·tangent = {} (not zero)",
904 dot_nt
905 );
906 }
907
908 #[test]
911 fn remap_landmarks_preserves_relative() {
912 let mut set = LandmarkSet::new();
913 set.insert(Landmark::new(
915 LandmarkId::ChestCenter,
916 [0.5, 0.5, 0.5],
917 1.0,
918 None,
919 ));
920 let src = ([0.0; 3], [1.0; 3]);
921 let tgt = ([0.0; 3], [2.0; 3]);
922 let remapped = remap_landmarks(&set, src, tgt);
923 let lm = remapped
924 .get(&LandmarkId::ChestCenter)
925 .expect("ChestCenter missing after remap");
926 for i in 0..3 {
928 assert!(
929 (lm.position[i] - 1.0).abs() < 1e-5,
930 "axis {} mismatch: {}",
931 i,
932 lm.position[i]
933 );
934 }
935 }
936
937 #[test]
940 fn transfer_landmarks_maps_to_target() {
941 let template = humanoid_mesh();
942 let mut target = humanoid_mesh();
943 for p in target.positions.iter_mut() {
945 p[0] += 10.0;
946 }
947 let template_set = detect_landmarks(&template);
948 let transferred = transfer_landmarks(&template_set, &template, &target);
949 for (pos, _conf) in transferred.all_positions() {
951 assert!(pos[0] >= 9.4, "transferred X={} expected >= ~10", pos[0]);
952 }
953 }
954
955 #[test]
958 fn body_height_plausible() {
959 let mesh = humanoid_mesh();
960 let set = detect_landmarks(&mesh);
961 let height = set.body_height().expect("body_height returned None");
962 let (mn, mx) = mesh_aabb(&mesh.positions);
963 let mesh_height = mx[1] - mn[1];
964 assert!(
966 (height - mesh_height).abs() < mesh_height * 0.15,
967 "body_height={} vs mesh_height={}",
968 height,
969 mesh_height
970 );
971 }
972
973 #[test]
976 fn shoulder_and_hip_width_positive() {
977 let mesh = humanoid_mesh();
978 let set = detect_landmarks(&mesh);
979 let sw = set.shoulder_width().expect("shoulder_width returned None");
980 let hw = set.hip_width().expect("hip_width returned None");
981 assert!(sw > 0.0, "shoulder_width should be positive, got {}", sw);
982 assert!(hw > 0.0, "hip_width should be positive, got {}", hw);
983 }
984
985 #[test]
988 fn arm_and_leg_length_positive() {
989 let mesh = humanoid_mesh();
990 let set = detect_landmarks(&mesh);
991 let al = set
992 .arm_length(Side::Left)
993 .expect("arm_length(Left) returned None");
994 let ar = set
995 .arm_length(Side::Right)
996 .expect("arm_length(Right) returned None");
997 let ll = set
998 .leg_length(Side::Left)
999 .expect("leg_length(Left) returned None");
1000 let lr = set
1001 .leg_length(Side::Right)
1002 .expect("leg_length(Right) returned None");
1003 assert!(al > 0.0, "arm_length left should be positive");
1004 assert!(ar > 0.0, "arm_length right should be positive");
1005 assert!(ll > 0.0, "leg_length left should be positive");
1006 assert!(lr > 0.0, "leg_length right should be positive");
1007 }
1008
1009 #[test]
1012 fn symmetry_error_zero_for_symmetric_set() {
1013 let mut set = LandmarkSet::new();
1014 set.insert(Landmark::new(
1016 LandmarkId::AcromionLeft,
1017 [-0.2, 1.4, 0.0],
1018 1.0,
1019 None,
1020 ));
1021 set.insert(Landmark::new(
1022 LandmarkId::AcromionRight,
1023 [0.2, 1.4, 0.0],
1024 1.0,
1025 None,
1026 ));
1027 let err = set.symmetry_error();
1028 assert!(
1029 err < 1e-5,
1030 "symmetry_error should be ~0 for symmetric set, got {}",
1031 err
1032 );
1033 }
1034
1035 #[test]
1038 fn to_map_contains_all_inserted_landmarks() {
1039 let mesh = humanoid_mesh();
1040 let set = detect_landmarks(&mesh);
1041 let map = set.to_map();
1042 for key in map.keys() {
1044 let found = LandmarkId::all().iter().any(|id| id.name() == key.as_str());
1045 assert!(found, "Unknown landmark name in map: {}", key);
1046 }
1047 }
1048
1049 #[test]
1052 fn write_landmarks_to_tmp() {
1053 let mesh = humanoid_mesh();
1054 let set = detect_landmarks(&mesh);
1055 let map = set.to_map();
1056 let mut lines: Vec<String> = map
1057 .iter()
1058 .map(|(k, v)| format!("{}: [{:.3}, {:.3}, {:.3}]", k, v[0], v[1], v[2]))
1059 .collect();
1060 lines.sort();
1061 let content = lines.join("\n");
1062 std::fs::write("/tmp/oxihuman_body_landmarks.txt", &content).expect("should succeed");
1063 let read_back =
1064 std::fs::read_to_string("/tmp/oxihuman_body_landmarks.txt").expect("should succeed");
1065 assert!(
1066 read_back.contains("Top of Head") || read_back.contains("Neck"),
1067 "landmark names missing"
1068 );
1069 }
1070
1071 #[test]
1074 fn detect_empty_mesh_returns_empty_set() {
1075 let empty = MeshBuffers {
1076 positions: vec![],
1077 normals: vec![],
1078 uvs: vec![],
1079 indices: vec![],
1080 has_suit: false,
1081 };
1082 let set = detect_landmarks(&empty);
1083 assert_eq!(set.count(), 0);
1084 }
1085
1086 #[test]
1089 fn remap_zero_size_source_bbox_no_panic() {
1090 let mut set = LandmarkSet::new();
1091 set.insert(Landmark::new(
1092 LandmarkId::TopOfHead,
1093 [0.5, 0.5, 0.5],
1094 1.0,
1095 None,
1096 ));
1097 let src = ([0.5; 3], [0.5; 3]); let tgt = ([0.0; 3], [1.0; 3]);
1099 let remapped = remap_landmarks(&set, src, tgt);
1100 assert_eq!(remapped.count(), 1);
1101 }
1102
1103 #[test]
1106 fn all_positions_count_matches() {
1107 let mesh = humanoid_mesh();
1108 let set = detect_landmarks(&mesh);
1109 assert_eq!(set.all_positions().len(), set.count());
1110 }
1111
1112 #[test]
1115 fn distance_correct() {
1116 let mut set = LandmarkSet::new();
1117 set.insert(Landmark::new(
1118 LandmarkId::AcromionLeft,
1119 [-1.0, 0.0, 0.0],
1120 1.0,
1121 None,
1122 ));
1123 set.insert(Landmark::new(
1124 LandmarkId::AcromionRight,
1125 [1.0, 0.0, 0.0],
1126 1.0,
1127 None,
1128 ));
1129 let d = set
1130 .distance(&LandmarkId::AcromionLeft, &LandmarkId::AcromionRight)
1131 .expect("distance returned None");
1132 assert!((d - 2.0).abs() < 1e-5, "Expected distance 2.0, got {}", d);
1133 }
1134
1135 #[test]
1138 fn landmark_names_are_unique() {
1139 let names: Vec<&str> = LandmarkId::all().iter().map(|id| id.name()).collect();
1140 let unique: std::collections::HashSet<&&str> = names.iter().collect();
1141 assert_eq!(names.len(), unique.len(), "Duplicate landmark names found");
1142 }
1143}