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}