Skip to main content

oxihuman_morph/
body_landmark.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5#![allow(clippy::too_many_arguments)]
6
7//! Anatomical landmark detection and mapping for body meshes.
8//!
9//! Provides:
10//! - [`LandmarkId`] — enumeration of named anatomical landmarks.
11//! - [`Landmark`] — a detected landmark with position, confidence, and vertex index.
12//! - [`LandmarkSet`] — a collection of landmarks with body measurement helpers.
13//! - [`Side`] — left/right discriminant.
14//! - Free functions for detecting, remapping, and transferring landmarks on meshes.
15
16use crate::engine::MeshBuffers;
17use std::collections::HashMap;
18
19// ---------------------------------------------------------------------------
20// LandmarkId
21// ---------------------------------------------------------------------------
22
23/// Named anatomical landmark identifiers covering the major body sites.
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum LandmarkId {
26    // Head
27    TopOfHead,
28    ChinCenter,
29    // Spine
30    /// 7th cervical vertebra prominence.
31    C7Cervical,
32    /// 10th thoracic vertebra.
33    T10Thoracic,
34    /// 4th lumbar vertebra.
35    L4Lumbar,
36    // Shoulder
37    AcromionLeft,
38    AcromionRight,
39    // Arms
40    ElbowLeft,
41    ElbowRight,
42    WristLeft,
43    WristRight,
44    // Hips & Legs
45    HipLeft,
46    HipRight,
47    KneeLeft,
48    KneeRight,
49    AnkleLeft,
50    AnkleRight,
51    // Torso
52    NeckBase,
53    ChestCenter,
54    WaistCenter,
55    NabelCenter,
56    // Feet
57    HeelLeft,
58    HeelRight,
59}
60
61impl LandmarkId {
62    /// Returns all landmark variants in a stable order.
63    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    /// Human-readable name for the landmark.
92    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    /// Returns `true` if this landmark has a left/right counterpart.
121    pub fn is_bilateral(&self) -> bool {
122        self.mirror().is_some()
123    }
124
125    /// Returns the mirrored (opposite side) landmark, or `None` for midline landmarks.
126    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            // Midline landmarks
143            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// ---------------------------------------------------------------------------
157// Landmark
158// ---------------------------------------------------------------------------
159
160/// A single detected anatomical landmark on a mesh.
161#[derive(Debug, Clone)]
162pub struct Landmark {
163    /// Semantic identifier.
164    pub id: LandmarkId,
165    /// World-space position `[x, y, z]`.
166    pub position: [f32; 3],
167    /// Detection confidence in `0..=1`.
168    pub confidence: f32,
169    /// Index of the nearest mesh vertex, if known.
170    pub vertex_index: Option<u32>,
171}
172
173impl Landmark {
174    /// Construct a new landmark.
175    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// ---------------------------------------------------------------------------
191// Side
192// ---------------------------------------------------------------------------
193
194/// Body side discriminant.
195#[derive(Debug, Clone, Copy, PartialEq)]
196pub enum Side {
197    Left,
198    Right,
199}
200
201// ---------------------------------------------------------------------------
202// LandmarkSet
203// ---------------------------------------------------------------------------
204
205/// A collection of body landmarks with measurement helpers.
206#[derive(Debug, Clone, Default)]
207pub struct LandmarkSet {
208    landmarks: HashMap<LandmarkId, Landmark>,
209}
210
211impl LandmarkSet {
212    /// Create an empty landmark set.
213    pub fn new() -> Self {
214        LandmarkSet {
215            landmarks: HashMap::new(),
216        }
217    }
218
219    /// Insert or replace a landmark.
220    pub fn insert(&mut self, landmark: Landmark) {
221        self.landmarks.insert(landmark.id.clone(), landmark);
222    }
223
224    /// Retrieve a landmark by id.
225    pub fn get(&self, id: &LandmarkId) -> Option<&Landmark> {
226        self.landmarks.get(id)
227    }
228
229    /// Number of landmarks in this set.
230    pub fn count(&self) -> usize {
231        self.landmarks.len()
232    }
233
234    /// Returns all positions and their confidence values.
235    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    /// Euclidean distance between two landmarks, or `None` if either is missing.
243    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    /// Estimated body height from `TopOfHead` to `HeelLeft` (average of left/right if available).
250    ///
251    /// Returns `None` if `TopOfHead` is absent.
252    pub fn body_height(&self) -> Option<f32> {
253        let head = self.landmarks.get(&LandmarkId::TopOfHead)?.position;
254        // Use the lower of the two heels as floor reference.
255        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                // Fall back to ankle
264                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    /// Shoulder width from left acromion to right acromion.
283    pub fn shoulder_width(&self) -> Option<f32> {
284        self.distance(&LandmarkId::AcromionLeft, &LandmarkId::AcromionRight)
285    }
286
287    /// Hip width from left hip to right hip.
288    pub fn hip_width(&self) -> Option<f32> {
289        self.distance(&LandmarkId::HipLeft, &LandmarkId::HipRight)
290    }
291
292    /// Arm length for the given side: shoulder (acromion) to wrist.
293    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    /// Leg length for the given side: hip to ankle.
302    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    /// Maximum left-right asymmetry across all bilateral landmark pairs (in world units).
311    ///
312    /// For each bilateral pair, computes the difference in X-axis distance from the
313    /// midline. Returns the maximum such difference; returns `0.0` if no bilateral
314    /// pairs are present.
315    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                    // Compare absolute X offsets — for a symmetric body both should be equal in magnitude
323                    let err = (lm_a.position[0].abs() - lm_b.position[0].abs()).abs();
324                    // Y and Z should also match
325                    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    /// Serialize to a map of `landmark_name → [x, y, z]`.
338    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// ---------------------------------------------------------------------------
347// Helper math
348// ---------------------------------------------------------------------------
349
350#[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
397// ---------------------------------------------------------------------------
398// Bounding box helpers
399// ---------------------------------------------------------------------------
400
401/// Compute the axis-aligned bounding box of mesh positions.
402/// Returns `(min, max)` or `([0,0,0],[0,0,0])` for an empty mesh.
403fn 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/// Interpolate linearly between `min_v` and `max_v` by fraction `t` in `[0,1]`.
423#[inline]
424fn lerp1(min_v: f32, max_v: f32, t: f32) -> f32 {
425    min_v + (max_v - min_v) * t
426}
427
428// ---------------------------------------------------------------------------
429// detect_landmarks
430// ---------------------------------------------------------------------------
431
432/// Detect anatomical landmarks from a mesh using heuristic bounding-box rules.
433///
434/// The strategy assigns landmarks using approximate fractional positions within
435/// the mesh's bounding box, then snaps each position to the nearest mesh vertex.
436/// All detections receive `confidence = 1.0` (heuristic, not learned).
437pub 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; // midline X
447    let lx = lerp1(mn[0], cx, 0.5); // left X (negative side)
448    let rx = lerp1(cx, mx[0], 0.5); // right X (positive side)
449
450    // Helper: make a landmark at a given approximate world position, snapped to nearest vertex.
451    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    // Y fractions (bottom=0, top=1) tuned for a standing human figure.
458    // These are rough anatomical proportions.
459    let y_frac = |t: f32| lerp1(mn[1], mx[1], t);
460
461    // Head
462    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    // Neck / upper torso
469    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    // Shoulders (acromions) — widest point at upper body height
476    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    // Chest / Thoracic
486    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    // Elbows — lateral, mid-arm height
490    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    // Navel
500    set.insert(make(LandmarkId::NabelCenter, [cx, y_frac(0.57), mx[2]]));
501
502    // Waist
503    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    // Hips
507    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    // Wrists — at extremity of arms
517    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    // Knees
527    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    // Ankles
531    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    // Heels — lowest Y, posterior Z
535    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
541// ---------------------------------------------------------------------------
542// nearest_vertex
543// ---------------------------------------------------------------------------
544
545/// Find the index of the mesh vertex closest to `pos`, and its distance.
546///
547/// Returns `(0, 0.0)` for an empty mesh.
548pub 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
564// ---------------------------------------------------------------------------
565// landmark_frame
566// ---------------------------------------------------------------------------
567
568/// Compute a local orthonormal frame at a landmark's vertex.
569///
570/// Returns `(normal, tangent, bitangent)`.  The normal is taken from the mesh
571/// normal at the nearest vertex (or Y-up if absent).  The tangent is derived
572/// from the mesh's stored tangent if available, otherwise constructed from the
573/// normal.
574pub 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    // Normal
580    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    // Build tangent — pick a reference vector not parallel to normal
587    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
598// ---------------------------------------------------------------------------
599// remap_landmarks
600// ---------------------------------------------------------------------------
601
602/// Remap landmark positions from a source bounding box to a target bounding box.
603///
604/// Applies per-axis scale and translation so that each landmark's normalised
605/// position within `source_bbox` maps to the same normalised position within
606/// `target_bbox`.  Vertex indices are cleared (they are no longer valid in the
607/// new space).
608pub 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
638// ---------------------------------------------------------------------------
639// transfer_landmarks
640// ---------------------------------------------------------------------------
641
642/// Transfer landmarks from a template mesh to a deformed (target) mesh.
643///
644/// For each landmark in `template_set`, finds the template vertex it was
645/// associated with (or the nearest vertex to its position on the template),
646/// then uses that vertex's position on the target mesh as the new landmark
647/// position.
648pub 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        // Resolve the vertex index on the template
656        let vidx = lm
657            .vertex_index
658            .unwrap_or_else(|| nearest_vertex(template_mesh, lm.position).0);
659
660        // Get the corresponding position on the target mesh
661        let new_pos = if (vidx as usize) < target_mesh.positions.len() {
662            target_mesh.positions[vidx as usize]
663        } else {
664            // Fall back to nearest vertex on target if index out of range
665            let (nearest_vidx, _) = nearest_vertex(target_mesh, lm.position);
666            target_mesh.positions[nearest_vidx as usize]
667        };
668
669        // Snap to nearest target vertex and record index
670        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// ---------------------------------------------------------------------------
683// Tests
684// ---------------------------------------------------------------------------
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    use crate::engine::MeshBuffers;
690
691    /// Build a simple humanoid-shaped mesh with enough vertices to exercise the
692    /// landmark heuristics.  The mesh spans Y: 0..1.8 (height), X: -0.3..0.3,
693    /// Z: -0.2..0.2.  It is a rough grid of points placed at anatomical heights.
694    fn humanoid_mesh() -> MeshBuffers {
695        let mut positions = Vec::new();
696        // Lay down a 5×7×3 grid
697        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        // normals pointing +Y for simplicity
709        let normals = vec![[0.0, 1.0, 0.0]; n];
710        let uvs = vec![[0.0, 0.0]; n];
711        // Trivial indices: sequential triangles from consecutive triplets
712        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    /// Minimal 4-vertex mesh.
729    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 1: LandmarkId::all() returns correct count ─────────────────────
745
746    #[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 2: Bilateral landmarks have mirrors ─────────────────────────────
753
754    #[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 3: Mirror is symmetric (A.mirror == B => B.mirror == A) ─────────
774
775    #[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 4: All midline landmarks have no mirror ─────────────────────────
786
787    #[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 5: LandmarkSet insert and get ───────────────────────────────────
806
807    #[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 6: detect_landmarks produces non-empty set ──────────────────────
818
819    #[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 7: TopOfHead is near the top of the mesh ────────────────────────
827
828    #[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        // Should be in the top 20% of the mesh height
835        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 8: nearest_vertex returns correct index ──────────────────────────
845
846    #[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 9: nearest_vertex on empty mesh returns (0, 0.0) ────────────────
855
856    #[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 10: landmark_frame returns orthonormal basis ─────────────────────
871
872    #[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        // Each vector should be unit length
880        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        // Normal and tangent should be perpendicular
900        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 11: remap_landmarks preserves relative position ─────────────────
909
910    #[test]
911    fn remap_landmarks_preserves_relative() {
912        let mut set = LandmarkSet::new();
913        // Landmark at center of [0,1]^3
914        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        // Center of [0,2]^3 should be [1,1,1]
927        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 12: transfer_landmarks maps to target mesh ───────────────────────
938
939    #[test]
940    fn transfer_landmarks_maps_to_target() {
941        let template = humanoid_mesh();
942        let mut target = humanoid_mesh();
943        // Shift target mesh by 10 units in X
944        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        // All transferred positions should have X >= 10 - epsilon
950        for (pos, _conf) in transferred.all_positions() {
951            assert!(pos[0] >= 9.4, "transferred X={} expected >= ~10", pos[0]);
952        }
953    }
954
955    // ── Test 13: body_height returns plausible value ──────────────────────────
956
957    #[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        // Detected height should be within 10% of mesh height
965        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 14: shoulder_width and hip_width are positive ────────────────────
974
975    #[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 15: arm_length and leg_length are positive ───────────────────────
986
987    #[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 16: symmetry_error is 0 for a perfectly symmetric set ───────────
1010
1011    #[test]
1012    fn symmetry_error_zero_for_symmetric_set() {
1013        let mut set = LandmarkSet::new();
1014        // Place acromions at equal and opposite X
1015        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 17: to_map serializes all landmarks ──────────────────────────────
1036
1037    #[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        // Every name in the map should correspond to a known landmark name
1043        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 18: Write detected landmarks to /tmp/ ────────────────────────────
1050
1051    #[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 19: detect_landmarks on empty mesh returns empty set ─────────────
1072
1073    #[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 20: remap with zero-size source bbox does not panic ──────────────
1087
1088    #[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]); // zero size
1098        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 21: all_positions returns same count as count() ─────────────────
1104
1105    #[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 22: distance returns correct value ───────────────────────────────
1113
1114    #[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 23: LandmarkId names are unique ─────────────────────────────────
1136
1137    #[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}