mittens_engine/engine/ecs/component/ik_chain.rs
1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::{Component, ComponentRef};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub(crate) struct TwoBoneIkDebugVisuals {
6 pub target_line: ComponentId,
7 pub pole_line: ComponentId,
8 pub plane_normal_line: ComponentId,
9 pub elbow_line: ComponentId,
10 pub elbow_point: ComponentId,
11}
12
13/// Solver configuration for an `IKChainComponent`.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum IKSolver {
16 /// Single-bone pose match.
17 ///
18 /// Sets the root joint's world rotation to match the target TC's world rotation,
19 /// post-multiplied by a fixed yaw offset. Used for neck/head alignment from InputXR.
20 ///
21 /// `offset_yaw`: rotation applied after copying target world rotation.
22 /// Use `std::f32::consts::PI` for the OpenXR (−Z forward) → VRM (+Z forward) flip.
23 ///
24 /// `copy_position`: when true, also overrides the joint's world position to the
25 /// target's world position. Required for HMD-driven head bones so the bone tracks
26 /// physical head translation (HMD moves forward+down when you pitch), not just
27 /// rotation. Visually detaches the bone from its FK parent until a spine FABRIK
28 /// solver bends the chain to follow. Default false (rotation-only behavior).
29 ///
30 /// `target_position_offset`: offset applied in the **target's local frame** before
31 /// copying its world position. Used to compensate for the gap between the head
32 /// bone pivot (typically at the skull base) and the camera/eye position: passing
33 /// `(0, -eye_height, 0)` shifts the bone down so the eye mesh (which sits above
34 /// the bone pivot) lands at the HMD position. Ignored when `copy_position` is
35 /// false.
36 AimConstraint {
37 offset_yaw: f32,
38 copy_position: bool,
39 target_position_offset: [f32; 3],
40 },
41
42 /// Closed-form 2-bone IK.
43 ///
44 /// Used for arms: UpperArm → LowerArm → Hand. All three joints are referenced
45 /// by explicit `ComponentId` — the solver does NO topology discovery and is
46 /// resilient to sibling helper / collider / cloth bones under the arm joints.
47 /// The chain's `parent_of` is ignored for this solver; `end_effector_id` (the
48 /// hand) lives on `IKChainComponent`, root + mid are here on the variant.
49 ///
50 /// `root_joint_id`: upper-arm TC (chain root).
51 /// `mid_joint_id`: lower-arm TC (elbow).
52 /// `pole_direction`: direction hint for the middle joint (elbow / knee).
53 /// Interpreted in body-local space when an ancestor `AvatarControlComponent`
54 /// exists, otherwise world-space. Body-local mode rotates the pole by the
55 /// model root's world rotation each tick so the elbow stays anatomically
56 /// correct when the body turns.
57 /// `copy_end_rotation`: if true, also aligns the end-effector bone to the target's rotation.
58 TwoBoneIK {
59 root_joint_id: ComponentId,
60 mid_joint_id: ComponentId,
61 pole_direction: [f32; 3],
62 copy_end_rotation: bool,
63 },
64
65 /// Iterative FABRIK solver — works for any chain length ≥ 2.
66 ///
67 /// Used for spine bending: chain hips → ... → splice_head with the head pose
68 /// driver as the target. The spine rotates so the end-effector (splice_head)
69 /// FK-lands at the target position.
70 ///
71 /// `target_position_offset`: same semantics as `AimConstraint` — offset in the
72 /// target's local frame, added to its world position before chasing. Used to
73 /// shift the target down by `eye_offset` so the head bone pivot (not the eye
74 /// mesh above it) lines up with the HMD position.
75 Fabrik {
76 max_iterations: u32,
77 tolerance: f32,
78 target_position_offset: [f32; 3],
79 },
80}
81
82/// Marks the root joint of an IK chain.
83///
84/// Place this as a **child of the root joint TC** (e.g. `J_Bip_L_UpperArm`, `splice_head`).
85/// The IKSystem finds this component, reads its parent TC as the root joint, walks down to
86/// `end_effector_id` to collect the chain, reads the target pose from `target_id`, solves,
87/// and emits `UpdateTransform` for each joint.
88///
89/// All three solver types are expressed through this single component; no separate
90/// end-effector or pole-vector marker components are required.
91#[derive(Debug, Clone)]
92pub struct IKChainComponent {
93 /// Which solver to run.
94 pub solver: IKSolver,
95
96 /// TC whose world pose is the IK target this frame.
97 ///
98 /// For `AimConstraint`: target world rotation is read here.
99 /// For `TwoBoneIK` / `Fabrik`: target world position (and optionally rotation) is read here.
100 pub target_id: ComponentId,
101
102 /// TC at the end of the bone chain.
103 ///
104 /// For `AimConstraint`: set to the root joint itself (chain length = 1).
105 /// For `TwoBoneIK`: set to the hand/foot bone (2 TCs below the root joint).
106 /// For `Fabrik`: set to the last bone in the spine/neck chain.
107 pub end_effector_id: ComponentId,
108
109 /// Blend weight: 0.0 = no IK applied, 1.0 = full solve.
110 pub weight: f32,
111
112 /// Authored form of `target_id` for round-trip dump. `None` for
113 /// IKChains wired purely at runtime (e.g. by `AvatarControlSystem`),
114 /// which have no MMS source to preserve.
115 pub target_source: Option<ComponentRef>,
116 /// Authored form of `end_effector_id` for round-trip dump.
117 pub end_effector_source: Option<ComponentRef>,
118
119 /// Cached ancestor `AvatarControlComponent` ID, discovered by `IKSystem`
120 /// on first tick via a parent-chain walk. When `Some`, the solver
121 /// transforms `TwoBoneIK.pole_direction` from body-local to world space
122 /// using the AVC's model root rotation. `None` → world-space pole
123 /// (current behavior for non-AVC chains).
124 pub(crate) avc_id: Option<ComponentId>,
125
126 /// Cached InputXR/XRHand component governing this runtime AVC target.
127 pub(crate) xr_pose_driver: Option<ComponentId>,
128
129 /// Lazily created runtime-only debug visual ids for TwoBoneIK inspection.
130 pub(crate) two_bone_debug_visuals: Option<TwoBoneIkDebugVisuals>,
131
132 component: Option<ComponentId>,
133}
134
135impl IKChainComponent {
136 pub fn new(solver: IKSolver, target_id: ComponentId, end_effector_id: ComponentId) -> Self {
137 Self {
138 solver,
139 target_id,
140 end_effector_id,
141 weight: 1.0,
142 target_source: None,
143 end_effector_source: None,
144 avc_id: None,
145 xr_pose_driver: None,
146 two_bone_debug_visuals: None,
147 component: None,
148 }
149 }
150
151 pub fn with_weight(mut self, w: f32) -> Self {
152 self.weight = w;
153 self
154 }
155
156 pub fn with_target_source(mut self, src: ComponentRef) -> Self {
157 self.target_source = Some(src);
158 self
159 }
160
161 pub fn with_end_effector_source(mut self, src: ComponentRef) -> Self {
162 self.end_effector_source = Some(src);
163 self
164 }
165}
166
167impl Component for IKChainComponent {
168 fn name(&self) -> &'static str {
169 "ik_chain"
170 }
171
172 fn set_id(&mut self, id: ComponentId) {
173 self.component = Some(id);
174 }
175
176 fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
177 emit.push_intent_now(
178 component,
179 crate::engine::ecs::IntentValue::RegisterIkChain {
180 component_ids: vec![component],
181 },
182 );
183 }
184
185 fn as_any(&self) -> &dyn std::any::Any {
186 self
187 }
188
189 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
190 self
191 }
192
193 fn to_mms_ast(
194 &self,
195 _world: &crate::engine::ecs::World,
196 ) -> crate::scripting::ast::ComponentExpression {
197 use crate::engine::ecs::component::ce_helpers::*;
198 use crate::scripting::ast::Expression;
199 let solver_call = match self.solver {
200 IKSolver::AimConstraint {
201 offset_yaw,
202 copy_position,
203 target_position_offset,
204 } => (
205 "aim_constraint",
206 vec![
207 num(offset_yaw as f64),
208 b(copy_position),
209 array(nums(target_position_offset.iter().map(|&v| v as f64))),
210 ],
211 ),
212 IKSolver::TwoBoneIK {
213 pole_direction,
214 copy_end_rotation,
215 ..
216 } => (
217 "two_bone_ik",
218 vec![
219 array(nums(pole_direction.iter().map(|&v| v as f64))),
220 b(copy_end_rotation),
221 ],
222 ),
223 IKSolver::Fabrik {
224 max_iterations,
225 tolerance,
226 target_position_offset,
227 } => (
228 "fabrik",
229 vec![
230 num(max_iterations as f64),
231 num(tolerance as f64),
232 array(nums(target_position_offset.iter().map(|&v| v as f64))),
233 ],
234 ),
235 };
236 fn target_expr(t: &ComponentRef) -> Expression {
237 match t {
238 ComponentRef::Guid(u) => Expression::String(format!("@uuid:{u}")),
239 ComponentRef::Query(s) => Expression::String(s.clone()),
240 }
241 }
242 let mut ce = ce_call("IKChain", solver_call.0, solver_call.1)
243 .with_call("weight", vec![num(self.weight as f64)]);
244 if let Some(src) = &self.target_source {
245 ce = ce.with_call("target", vec![target_expr(src)]);
246 }
247 if let Some(src) = &self.end_effector_source {
248 ce = ce.with_call("end_effector", vec![target_expr(src)]);
249 }
250 ce
251 }
252}