Skip to main content

mittens_engine/engine/ecs/system/
bone_mapping_system.rs

1use crate::engine::ecs::component::TransformComponent;
2use crate::engine::ecs::{ComponentId, World};
3use crate::utils::math::{vec3_len, vec3_sub};
4
5/// Stateless utility for resolving semantic bone landmarks to ComponentIds in a live skeleton.
6///
7/// All functions are free (associated) functions — no instance state.
8/// Called once during `AvatarControlSystem::try_init_splices`; returns resolved IDs
9/// that AVC uses to wire IK chains.
10pub struct BoneMappingSystem;
11
12/// Resolved upper-arm → lower-arm → hand chain for TwoBoneIK setup.
13pub struct ResolvedArmChain {
14    pub upper_arm: ComponentId,
15    pub lower_arm: ComponentId,
16    pub hand: ComponentId,
17}
18
19/// Resolved spine chain for FABRIK setup.
20///
21/// `chain` is ordered hips → ... → head (root-first FABRIK convention).
22/// Intermediate joints (spine/chest/upper_chest/neck) are whatever TC ancestors
23/// the topology walk produced between hips and head — count varies by rig.
24pub struct ResolvedSpineChain {
25    pub hips: ComponentId,
26    pub head: ComponentId,
27    pub chain: Vec<ComponentId>,
28}
29
30impl BoneMappingSystem {
31    /// Resolve a 2-bone arm chain from (optional) explicit names + topology fallback.
32    ///
33    /// Resolution order for each joint:
34    ///   1. Explicit name, if provided — look up by `#name` MMQ selector under `model_root`.
35    ///   2. Topology derivation — walk TC parent chain from the joint below, using
36    ///      `tc_ancestor_at_distance` with the given `min_bone_length` threshold.
37    ///
38    /// `min_bone_length`: if `Some(d)`, topology derivation skips TC ancestors closer
39    /// than `d` metres to the previous anchor, filtering out short helper bones.
40    /// Pass `None` to always use the immediate TC parent.
41    ///
42    /// Returns `None` if `hand_name` is not found under `model_root`.
43    pub fn resolve_arm_chain(
44        world: &World,
45        model_root: ComponentId,
46        hand_name: &str,
47        lower_arm_name: Option<&str>,
48        upper_arm_name: Option<&str>,
49        min_bone_length: Option<f32>,
50    ) -> Option<ResolvedArmChain> {
51        let hand = world.find_component(model_root, &format!("#{}", hand_name))?;
52
53        let lower_arm = if let Some(name) = lower_arm_name {
54            world.find_component(model_root, &format!("#{}", name))?
55        } else {
56            Self::tc_ancestor_at_distance(world, hand, min_bone_length)?
57        };
58
59        let upper_arm = if let Some(name) = upper_arm_name {
60            world.find_component(model_root, &format!("#{}", name))?
61        } else {
62            Self::tc_ancestor_at_distance(world, lower_arm, min_bone_length)?
63        };
64
65        Some(ResolvedArmChain {
66            upper_arm,
67            lower_arm,
68            hand,
69        })
70    }
71
72    /// Resolve a spine chain from head bone up to (optionally named) hips bone.
73    ///
74    /// Walks UP from `head_id` via `tc_ancestor_at_distance` (threshold ~0.03m to
75    /// skip helper bones), collecting TC joints.  Stops when it hits `hips_name`
76    /// (by component name) if provided, or after at most 8 hops otherwise.
77    ///
78    /// Returns the chain in HIPS → HEAD order (FABRIK convention: root first).
79    /// Returns `None` if the walk produces fewer than 2 joints.
80    pub fn resolve_spine_chain(
81        world: &World,
82        model_root: ComponentId,
83        head_id: ComponentId,
84        hips_name: Option<&str>,
85        min_bone_length: Option<f32>,
86    ) -> Option<ResolvedSpineChain> {
87        let hips_id = hips_name.and_then(|n| world.find_component(model_root, &format!("#{}", n)));
88        let mut up: Vec<ComponentId> = vec![head_id];
89        let mut cur = head_id;
90        for _ in 0..8 {
91            let parent = Self::tc_ancestor_at_distance(world, cur, min_bone_length)?;
92            up.push(parent);
93            // Stop if we hit the named hips.
94            if Some(parent) == hips_id {
95                break;
96            }
97            // Or stop if we've stepped above model_root.
98            if parent == model_root {
99                break;
100            }
101            cur = parent;
102        }
103        if up.len() < 2 {
104            return None;
105        }
106        up.reverse();
107        let hips = *up.first().unwrap();
108        let head = *up.last().unwrap();
109        Some(ResolvedSpineChain {
110            hips,
111            head,
112            chain: up,
113        })
114    }
115
116    /// Walk upward from `start`, returning the nearest TC ancestor whose world position
117    /// is at least `min_dist` metres away from `start`.
118    ///
119    /// If `min_dist` is `None`, returns the immediate TC parent (no distance filtering).
120    /// Returns `None` if no TC parent is found, or if the walk exceeds 32 steps.
121    pub fn tc_ancestor_at_distance(
122        world: &World,
123        start: ComponentId,
124        min_dist: Option<f32>,
125    ) -> Option<ComponentId> {
126        let anchor_pos = tc_world_pos(world, start)?;
127        let mut cur = start;
128
129        for _ in 0..32 {
130            let parent = world.parent_of(cur)?;
131            // Parent must be a TC to count as an arm joint.
132            if world
133                .get_component_by_id_as::<TransformComponent>(parent)
134                .is_none()
135            {
136                return None;
137            }
138            if let Some(min_d) = min_dist {
139                let parent_pos = tc_world_pos(world, parent)?;
140                let dist = vec3_len(vec3_sub(parent_pos, anchor_pos));
141                if dist < min_d {
142                    // Too close — this is a helper bone; keep walking.
143                    cur = parent;
144                    continue;
145                }
146            }
147            return Some(parent);
148        }
149        None
150    }
151
152    /// Find the first TC ancestor of `start` that has >= `min_tc_children` TC children.
153    ///
154    /// Used for shoulder girdle detection (first 3-way split above hips) and hip detection.
155    /// Returns `None` if no such ancestor exists within 32 steps.
156    pub fn find_branching_ancestor(
157        world: &World,
158        start: ComponentId,
159        min_tc_children: usize,
160    ) -> Option<ComponentId> {
161        let mut cur = start;
162        for _ in 0..32 {
163            let parent = world.parent_of(cur)?;
164            if world
165                .get_component_by_id_as::<TransformComponent>(parent)
166                .is_none()
167            {
168                return None;
169            }
170            let tc_child_count = world
171                .children_of(parent)
172                .iter()
173                .filter(|&&ch| {
174                    world
175                        .get_component_by_id_as::<TransformComponent>(ch)
176                        .is_some()
177                })
178                .count();
179            if tc_child_count >= min_tc_children {
180                return Some(parent);
181            }
182            cur = parent;
183        }
184        None
185    }
186}
187
188// ---------------------------------------------------------------------------
189// Private helpers
190// ---------------------------------------------------------------------------
191
192fn tc_world_pos(world: &World, id: ComponentId) -> Option<[f32; 3]> {
193    world
194        .get_component_by_id_as::<TransformComponent>(id)
195        .map(|t| {
196            let m = t.transform.matrix_world;
197            [m[3][0], m[3][1], m[3][2]]
198        })
199}