Skip to main content

mittens_engine/engine/ecs/system/
avatar_control_system.rs

1use crate::engine::ecs::component::{
2    AvatarControlComponent, BoneRestPoseComponent, Camera3DComponent, CameraXRComponent,
3    ControllerHand, ControllerXRComponent, IKChainComponent, IKSolver, QuatYawFollowComponent,
4    SerializeComponent, TransformComponent, TransformForkTRSComponent,
5    TransformMapRotationComponent,
6};
7use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};
8use crate::engine::user_input::InputState;
9use crate::utils::math::{
10    mat_to_quat, quat_conjugate, quat_mul, quat_rotate_vec3, quat_rotation_y,
11};
12use std::collections::HashSet;
13use winit::keyboard::{Key, NamedKey};
14
15#[derive(Debug, Default)]
16pub struct AvatarControlSystem {
17    avatars: HashSet<ComponentId>,
18}
19
20impl AvatarControlSystem {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn tick(
26        &mut self,
27        world: &mut World,
28        input: &InputState,
29        emit: &mut dyn SignalEmitter,
30        dt_sec: f32,
31    ) {
32        let ids: Vec<_> = self.avatars.iter().copied().collect();
33
34        let mut calibration_consumed = false;
35        let calibrate_pressed = input.key_pressed(&Key::Named(NamedKey::Enter));
36        for id in ids {
37            let allow_calibration = calibrate_pressed && !calibration_consumed;
38            if tick_one(id, world, emit, dt_sec, allow_calibration) {
39                calibration_consumed = true;
40            }
41        }
42    }
43
44    pub fn register(&mut self, component: ComponentId) {
45        self.avatars.insert(component);
46    }
47
48    pub fn remove(&mut self, component: ComponentId) {
49        self.avatars.remove(&component);
50    }
51}
52
53fn tick_one(
54    id: ComponentId,
55    world: &mut World,
56    emit: &mut dyn SignalEmitter,
57    _dt_sec: f32,
58    allow_calibration: bool,
59) -> bool {
60    // --- Init phase ---
61    let needs_init = {
62        let Some(c) = world.get_component_by_id_as::<AvatarControlComponent>(id) else {
63            return false;
64        };
65        c.splice_head.is_none()
66    };
67
68    if needs_init {
69        // Runtime splicing reparents and rewrites avatar bones, so it is itself a
70        // pose-changing operation. An XR-authored avatar must remain in its authored
71        // pose until the headset has supplied a valid pose. Non-XR AVC trees continue
72        // to initialize immediately.
73        if !ancestor_input_xr_is_ready(world, id) {
74            return false;
75        }
76        try_init_splices(id, world, emit);
77        // Head rotation is handled by IKSystem (AimConstraint on splice_head) after init.
78    }
79
80    // Keep the displaced head bone anchored under splice_head. This prevents
81    // animation/FK from reintroducing a local head translation that would move
82    // the camera wrapper relative to the solved head pivot.
83    let displaced_head_id = world
84        .get_component_by_id_as::<AvatarControlComponent>(id)
85        .and_then(|c| c.displaced_head);
86    if let Some(head_bone_id) = displaced_head_id {
87        if let Some(head_t) = world.get_component_by_id_as::<TransformComponent>(head_bone_id) {
88            if head_t.transform.translation != [0.0, 0.0, 0.0] {
89                emit.push_intent_now(
90                    head_bone_id,
91                    IntentValue::UpdateTransform {
92                        component_ids: vec![head_bone_id],
93                        translation: [0.0, 0.0, 0.0],
94                        rotation_quat_xyzw: head_t.transform.rotation,
95                        scale: head_t.transform.scale,
96                    },
97                );
98            }
99        }
100    }
101
102    if allow_calibration && capture_hand_grip_calibration(id, world, emit) {
103        return true;
104    }
105    false
106}
107
108fn ancestor_input_xr_is_ready(world: &World, start: ComponentId) -> bool {
109    let mut current = Some(start);
110    while let Some(component) = current {
111        if let Some(input) = world
112            .get_component_by_id_as::<crate::engine::ecs::component::InputXRComponent>(component)
113        {
114            return input.pose_valid;
115        }
116        current = world.parent_of(component);
117    }
118    true
119}
120
121/// First-time setup: splice bones, create body pipeline, and (optionally) hand smoothing pipelines.
122///
123/// Controllers are discovered by topology: any `ControllerXRComponent` that is a
124/// **direct child** of this `AvatarControlComponent` is treated as a hand driver.
125/// Its `hand` field (`Left` / `Right`) determines which hand bone it drives.
126///
127/// Body pipeline created here reads `driven_t`'s world matrix, strips pitch/roll via `YawFollow`,
128/// and writes the result to `model_root` (which is re-parented under the pipeline output).
129fn try_init_splices(id: ComponentId, world: &mut World, emit: &mut dyn SignalEmitter) {
130    let (
131        head_bone_name,
132        left_hand_bone,
133        right_hand_bone,
134        left_upper_arm_bone,
135        left_lower_arm_bone,
136        right_upper_arm_bone,
137        right_lower_arm_bone,
138        left_arm_pole_direction,
139        right_arm_pole_direction,
140        body_yaw_threshold,
141        body_yaw_rate,
142        authored_forward_plus_z,
143        forward_plus_z_overridden,
144        authored_initial_body_yaw,
145        initial_body_yaw_overridden,
146        skip_body_pipeline,
147        camera_bone_name,
148        avatar_height_override,
149        eye_height_from_head_bone,
150        head_ik_eye_height,
151        neck_bone_name,
152        hand_grip_rotation_left,
153        hand_grip_rotation_right,
154    ) = {
155        let Some(c) = world.get_component_by_id_as::<AvatarControlComponent>(id) else {
156            return;
157        };
158        (
159            c.head_bone.clone(),
160            c.left_hand_bone.clone(),
161            c.right_hand_bone.clone(),
162            c.left_upper_arm_bone.clone(),
163            c.left_lower_arm_bone.clone(),
164            c.right_upper_arm_bone.clone(),
165            c.right_lower_arm_bone.clone(),
166            c.left_arm_pole_direction,
167            c.right_arm_pole_direction,
168            c.body_yaw_threshold,
169            c.body_yaw_rate,
170            c.forward_plus_z,
171            c.forward_plus_z_overridden,
172            c.initial_body_yaw,
173            c.initial_body_yaw_overridden,
174            c.skip_body_pipeline,
175            c.camera_bone.clone(),
176            c.avatar_height,
177            c.eye_height_from_head_bone,
178            c.head_ik_eye_height,
179            c.neck_bone.clone(),
180            c.hand_grip_rotation_left,
181            c.hand_grip_rotation_right,
182        )
183    };
184
185    // Find model_root: first TransformComponent child of AvatarControlComponent.
186    let Some(model_root_id) = world.children_of(id).iter().copied().find(|&ch| {
187        world
188            .get_component_by_id_as::<TransformComponent>(ch)
189            .is_some()
190    }) else {
191        return;
192    };
193
194    // Discover hand controllers by topology: direct ControllerXRComponent children.
195    let left_ctrl = world.children_of(id).iter().copied().find(|&ch| {
196        world
197            .get_component_by_id_as::<ControllerXRComponent>(ch)
198            .map(|c| c.hand == ControllerHand::Left)
199            .unwrap_or(false)
200    });
201    let right_ctrl = world.children_of(id).iter().copied().find(|&ch| {
202        world
203            .get_component_by_id_as::<ControllerXRComponent>(ch)
204            .map(|c| c.hand == ControllerHand::Right)
205            .unwrap_or(false)
206    });
207
208    // driven_t is the parent of AVC — needed as IK target for the head AimConstraint.
209    let Some(driven_t_id) = world.parent_of(id) else {
210        return;
211    };
212    let resolved_body_forward_plus_z = if forward_plus_z_overridden {
213        authored_forward_plus_z
214    } else {
215        false
216    };
217    let resolved_head_target_forward_plus_z = if forward_plus_z_overridden {
218        authored_forward_plus_z
219    } else {
220        false
221    };
222    let resolved_initial_body_yaw = if initial_body_yaw_overridden {
223        authored_initial_body_yaw
224    } else {
225        std::f32::consts::PI
226    };
227
228    // Head bone is required — retry next tick if GLTF hasn't spawned yet.
229    let head_selector = format!("#{}", head_bone_name);
230    let Some(head_bone_id) = world.find_component(model_root_id, &head_selector) else {
231        return;
232    };
233    let Some(head_parent_id) = world.parent_of(head_bone_id) else {
234        return;
235    };
236
237    // Read head_bone's true bind-pose local TRS via the `BoneRestPoseComponent`
238    // sidecar that `GLTFSystem` stamped at node-spawn time.  Falls back to the
239    // current `TransformComponent` only if no rest-pose sidecar is present
240    // (non-GLTF skeletons).  Reading the live `TransformComponent` would
241    // pick up whatever pose `AnimationSystem` wrote earlier this tick, which
242    // bakes the current animation frame into `head_rest_rot` and produces a
243    // permanently rotated visible head.
244    let (head_rest_t, head_rest_rot, head_rest_s) = read_bone_rest_pose(world, head_bone_id);
245    let head_splice_id = world.add_component(TransformComponent::new().with_position(
246        head_rest_t[0],
247        head_rest_t[1],
248        head_rest_t[2],
249    ));
250
251    // Resolve hand bones + controller drivers for 2-bone arm IK.
252    // The hand bone stays in the armature; IKSystem rotates UpperArm + LowerArm
253    // each tick so the hand reaches the controller world pose, optionally
254    // through a rotated child target that compensates for grip-vs-palm framing.
255    let left = resolve_hand_splice(
256        world,
257        model_root_id,
258        left_hand_bone.as_deref(),
259        left_ctrl,
260        hand_grip_rotation_left,
261    );
262    let right = resolve_hand_splice(
263        world,
264        model_root_id,
265        right_hand_bone.as_deref(),
266        right_ctrl,
267        hand_grip_rotation_right,
268    );
269
270    // --- Camera bone: auto-calibrate model_root.y + discover camera children ---
271    //
272    // Priority:
273    //   1. avatar_height_override — use directly, skip bone measurement.
274    //   2. camera_bone auto-calibration — measure bone local Y in rest pose.
275    // Either way, emit UpdateTransform(model_root, y = -height).
276    //
277    // Any Camera3D or CameraXR direct children of AVC are re-parented under the
278    // camera bone so they inherit its world transform each tick.
279    let actual_camera_bone_name = camera_bone_name.as_deref().or(Some(&head_bone_name));
280    let camera_bone_id: Option<ComponentId> = actual_camera_bone_name.and_then(|name| {
281        let sel = format!("#{}", name);
282        let found = world.find_component(model_root_id, &sel);
283        found
284    });
285
286    // Discover camera children + derive eye_offset_head_local FIRST — the
287    // model_root xz compensation below needs the offset, and the eye_offset
288    // also feeds the head IK target_position_offset (used much later).
289    let camera_children: Vec<(ComponentId, [f32; 3], bool)> = world
290        .children_of(id)
291        .iter()
292        .copied()
293        .filter_map(|ch| {
294            let is_c3d = world
295                .get_component_by_id_as::<Camera3DComponent>(ch)
296                .is_some();
297            let is_cxr = world
298                .get_component_by_id_as::<CameraXRComponent>(ch)
299                .is_some();
300            if is_c3d || is_cxr {
301                return Some((ch, [0.0, 0.0, 0.0], is_c3d));
302            }
303            if let Some(tc) = world.get_component_by_id_as::<TransformComponent>(ch) {
304                let wraps_c3d = world.children_of(ch).iter().any(|&gc| {
305                    world
306                        .get_component_by_id_as::<Camera3DComponent>(gc)
307                        .is_some()
308                });
309                let wraps_cxr = world.children_of(ch).iter().any(|&gc| {
310                    world
311                        .get_component_by_id_as::<CameraXRComponent>(gc)
312                        .is_some()
313                });
314                let wraps_cam = wraps_c3d || wraps_cxr;
315                if wraps_cam {
316                    let eye_offset = tc.transform.translation;
317                    return Some((ch, eye_offset, wraps_c3d));
318                }
319            }
320            None
321        })
322        .collect();
323    let eye_offset_head_local: [f32; 3] = camera_children
324        .iter()
325        .map(|&(_, off, _)| off)
326        .find(|off| off != &[0.0, 0.0, 0.0])
327        .unwrap_or([0.0, eye_height_from_head_bone.unwrap_or(0.0), 0.0]);
328
329    // Eye offset mapped from head-local into driven_t-local space.
330    // This remains the source for the head target offset. It no longer owns
331    // body/root XZ placement; steady-state body XZ is handled by
332    // HeadPoseBodyXzFollowSystem.
333    let head_ik_offset_yaw = if resolved_head_target_forward_plus_z {
334        0.0
335    } else {
336        std::f32::consts::PI
337    };
338
339    // Body Y is anchored to `displaced_head.world.y` (which already has
340    // -eye_offset.y baked in via the head_target chain) in
341    // HeadPoseBodyXzFollowSystem, so model_root.y must NOT also include an
342    // eye-offset term — that would subtract it twice and stretch the
343    // rest-pose neck by `eye_offset.y`.
344    let model_root_translation: Option<[f32; 3]> = if let Some(h) = avatar_height_override {
345        Some([0.0, -h, 0.0])
346    } else if let Some(cam_bone_id) = camera_bone_id {
347        let cam_bone_world_y = world
348            .get_component_by_id_as::<TransformComponent>(cam_bone_id)
349            .map(|t| t.transform.matrix_world[3][1])
350            .unwrap_or(0.0);
351        let model_root_world_y = world
352            .get_component_by_id_as::<TransformComponent>(model_root_id)
353            .map(|t| t.transform.matrix_world[3][1])
354            .unwrap_or(0.0);
355        let bone_local_y = cam_bone_world_y - model_root_world_y;
356        Some([0.0, -bone_local_y, 0.0])
357    } else {
358        None
359    };
360
361    // model_root baseline calibration plus authored eye offset compensation.
362    // This moves the whole avatar relative to the fixed XR camera pose.  The
363    // initial UpdateTransform sets the body in roughly the right place before
364    // SimpleHumanoidSystem takes over translation each tick.
365    if let Some(txyz) = model_root_translation {
366        emit.push_intent_now(
367            model_root_id,
368            IntentValue::UpdateTransform {
369                component_ids: vec![model_root_id],
370                translation: txyz,
371                rotation_quat_xyzw: [0.0, 0.0, 0.0, 1.0],
372                scale: [1.0, 1.0, 1.0],
373            },
374        );
375    }
376
377    // Resolve neck bone (for the Phase 2 rest-pin) and cache its rest local
378    // translation from the `BoneRestPoseComponent` sidecar — same reasoning
379    // as the head_rest read above: the live `TransformComponent` would
380    // already carry whatever animation wrote this tick.
381    let (neck_bone_id, neck_rest_t) = match neck_bone_name.as_deref() {
382        Some(name) => {
383            let sel = format!("#{}", name);
384            match world.find_component(model_root_id, &sel) {
385                Some(nid) => {
386                    let (rest_t, _, _) = read_bone_rest_pose(world, nid);
387                    (Some(nid), Some(rest_t))
388                }
389                None => {
390                    println!(
391                        "[AVC] neck bone '{}' not found under model_root — neck pin disabled",
392                        name
393                    );
394                    (None, None)
395                }
396            }
397        }
398        None => (None, None),
399    };
400
401    // Y component of model_root.local stashed for the body-follow system's
402    // future Step 1 (head-rotation-compensated world XZ target).  Step 0
403    // doesn't use it; the body relies on the AVC-init single-shot
404    // UpdateTransform plus the parent-chain transform inheritance.
405    let model_root_local_y = model_root_translation.map(|t| t[1]).unwrap_or(0.0);
406
407    // Store runtime IDs (body_pipeline_id stored after pipeline creation below).
408    if let Some(c) = world.get_component_by_id_as_mut::<AvatarControlComponent>(id) {
409        c.splice_head = Some(head_splice_id);
410        c.displaced_head = Some(head_bone_id);
411        c.splice_camera_bone = camera_bone_id;
412        c.model_root_id = Some(model_root_id);
413        c.model_root_local_y = model_root_local_y;
414        c.neck_bone_id = neck_bone_id;
415        c.neck_rest_translation = neck_rest_t;
416        if let Some((_, _, _, bone)) = left {
417            c.left_hand_bone_id = Some(bone);
418        }
419        if let Some((_, _, _, bone)) = right {
420            c.right_hand_bone_id = Some(bone);
421        }
422        if let Some((_, raw_driver, hand_driver, _)) = left {
423            c.left_hand_raw_target_id = Some(raw_driver);
424            c.left_hand_visual_target_id = Some(hand_driver);
425        }
426        if let Some((_, raw_driver, hand_driver, _)) = right {
427            c.right_hand_raw_target_id = Some(raw_driver);
428            c.right_hand_visual_target_id = Some(hand_driver);
429        }
430    }
431
432    // -----------------------------------------------------------------------
433    // Body pipeline: created as a child of AVC; model_root re-parented under it.
434    //
435    // Topology:
436    //   AVC
437    //     └── body_pipeline  (TransformForkTRSComponent)
438    //           TransformMapRotationComponent
439    //             QuatYawFollowComponent { threshold, rate, initial_yaw, forward_plus_z }
440    //           model_root  ← re-parented here
441    // -----------------------------------------------------------------------
442    if !skip_body_pipeline {
443        let body_pipeline_id = world.add_component(TransformForkTRSComponent::new());
444        let body_pipeline_serialize_id = world.add_component(SerializeComponent::off());
445        let map_rot_id = world.add_component(TransformMapRotationComponent::new());
446        let yaw_follow_id = world.add_component(
447            QuatYawFollowComponent::new(body_yaw_threshold, body_yaw_rate)
448                .with_initial_yaw(resolved_initial_body_yaw)
449                .with_forward_plus_z_if(resolved_body_forward_plus_z),
450        );
451
452        let _ = world.set_parent(body_pipeline_serialize_id, Some(body_pipeline_id));
453        let _ = world.set_parent(map_rot_id, Some(body_pipeline_id));
454        let _ = world.set_parent(yaw_follow_id, Some(map_rot_id));
455
456        if let Some(c) = world.get_component_by_id_as_mut::<AvatarControlComponent>(id) {
457            c.body_pipeline_id = Some(body_pipeline_id);
458        }
459
460        emit_attach(emit, id, body_pipeline_id);
461        emit_attach(emit, body_pipeline_id, model_root_id);
462    }
463
464    // Head IK target offset: default to authored eye offset (CXR wrapper), with
465    // optional Y override for neck-height fine tuning.
466    let mut ik_eye_offset_head_local = eye_offset_head_local;
467    if let Some(y) = head_ik_eye_height {
468        ik_eye_offset_head_local[1] = y;
469    }
470    let neg_eye = [
471        -ik_eye_offset_head_local[0],
472        -ik_eye_offset_head_local[1],
473        -ik_eye_offset_head_local[2],
474    ];
475    // Full desired head-pivot offset in driven_t local space.
476    let head_target_offset = quat_rotate_vec3(quat_rotation_y(head_ik_offset_yaw), neg_eye);
477
478    // Dedicated fixed visible-head mount under driven_t.
479    let head_target_id = world.add_component(
480        TransformComponent::new()
481            .with_position(
482                head_target_offset[0],
483                head_target_offset[1],
484                head_target_offset[2],
485            )
486            .with_rotation_quat(quat_rotation_y(head_ik_offset_yaw)),
487    );
488    let _ = world.set_parent(head_target_id, Some(driven_t_id));
489
490    emit_attach(emit, head_parent_id, head_splice_id);
491    emit_attach(emit, driven_t_id, head_target_id);
492    emit_attach(emit, head_target_id, head_bone_id);
493
494    // Zero head_bone's local translation — splice_head now carries the rest offset
495    // from neck. Preserve the authored head rest rotation/scale so the visible
496    // head mesh and camera anchor share the same convention across desktop and XR.
497    // Emitted *after* the reparent attach so the UpdateTransform lands on
498    // head_bone in its new parent without fighting the attach intent's matrix recompute.
499    emit.push_intent_now(
500        head_bone_id,
501        IntentValue::UpdateTransform {
502            component_ids: vec![head_bone_id],
503            translation: [0.0, 0.0, 0.0],
504            rotation_quat_xyzw: head_rest_rot,
505            scale: head_rest_s,
506        },
507    );
508
509    // -----------------------------------------------------------------------
510    // Arm IK (TwoBoneIK) with explicit joint IDs.
511    //
512    // For each side: resolve all three arm bones (upper + lower + hand) and
513    // hand them to the solver via `IKSolver::TwoBoneIK { root_joint_id,
514    // mid_joint_id, .. }` + `IKChainComponent::end_effector_id`. The solver
515    // does no topology discovery, so sibling cloth / collider / helper bones
516    // under the arm joints (e.g. bisket's `J_Sec_L_TopsUpperArm_*` and
517    // `J_Bip_L_UpperArm_collider_*`) are irrelevant.
518    //
519    // Resolution per bone:
520    //   - if `*_upper_arm_bone` / `*_lower_arm_bone` set → name lookup under
521    //     model_root (skip the chain if the name is wrong — fail loudly).
522    //   - else fall back to `parent_of` walk-up from the hand bone (works for
523    //     clean VRM-style rigs with no twist bones).
524    // -----------------------------------------------------------------------
525    for (hand_opt, upper_name, lower_name, pole_dir, side_label, grip_rotation_offset) in [
526        (
527            left,
528            left_upper_arm_bone.as_deref(),
529            left_lower_arm_bone.as_deref(),
530            left_arm_pole_direction,
531            "left",
532            hand_grip_rotation_left,
533        ),
534        (
535            right,
536            right_upper_arm_bone.as_deref(),
537            right_lower_arm_bone.as_deref(),
538            right_arm_pole_direction,
539            "right",
540            hand_grip_rotation_right,
541        ),
542    ] {
543        let Some((_, raw_driver, hand_driver, hand_bone)) = hand_opt else {
544            continue;
545        };
546
547        let upper_arm = match upper_name {
548            Some(name) => {
549                let sel = format!("#{}", name);
550                let res = world.find_component(model_root_id, &sel);
551                if res.is_none() {
552                    println!(
553                        "[AVC] explicit {}_upper_arm_bone \"{}\" not found under model_root — {} arm IK disabled",
554                        side_label, name, side_label
555                    );
556                }
557                res
558            }
559            None => world
560                .parent_of(hand_bone)
561                .and_then(|lower| world.parent_of(lower)),
562        };
563        let Some(upper_arm) = upper_arm else { continue };
564
565        let lower_arm = match lower_name {
566            Some(name) => {
567                let sel = format!("#{}", name);
568                let res = world.find_component(model_root_id, &sel);
569                if res.is_none() {
570                    println!(
571                        "[AVC] explicit {}_lower_arm_bone \"{}\" not found under model_root — {} arm IK disabled",
572                        side_label, name, side_label
573                    );
574                }
575                res
576            }
577            None => world.parent_of(hand_bone),
578        };
579        let Some(lower_arm) = lower_arm else { continue };
580
581        let bone_name =
582            |id: ComponentId| -> String { world.component_name(id).unwrap_or("?").to_string() };
583        let upper_name_s = bone_name(upper_arm);
584        let lower_name_s = bone_name(lower_arm);
585        let hand_name_s = bone_name(hand_bone);
586        println!(
587            "[AVC] {} arm IK: root={} (id={:?}), mid={} (id={:?}), hand={} (id={:?}), target=(id={:?})",
588            side_label,
589            upper_name_s,
590            upper_arm,
591            lower_name_s,
592            lower_arm,
593            hand_name_s,
594            hand_bone,
595            hand_driver,
596        );
597        let looks_suspicious = |n: &str| {
598            n.contains("Twist")
599                || n.contains("Roll")
600                || n.contains("Helper")
601                || n.contains("_collider")
602                || n.contains("J_Sec_")
603        };
604        if looks_suspicious(&upper_name_s) || looks_suspicious(&lower_name_s) {
605            println!(
606                "[AVC] WARNING: {} arm IK resolved to a helper/cloth/collider bone — \
607                set explicit {}_upper_arm_bone(\"...\") and {}_lower_arm_bone(\"...\") \
608                in your AvatarControl block.",
609                side_label, side_label, side_label
610            );
611        }
612
613        let mut chain = IKChainComponent::new(
614            IKSolver::TwoBoneIK {
615                root_joint_id: upper_arm,
616                mid_joint_id: lower_arm,
617                pole_direction: pole_dir,
618                copy_end_rotation: true,
619            },
620            hand_driver,
621            hand_bone,
622        );
623        chain.xr_pose_driver = find_xr_pose_driver(world, hand_driver);
624        let chain_id = world.add_component(chain);
625        let chain_serialize_id = world.add_component(SerializeComponent::off());
626        let _ = world.set_parent(chain_serialize_id, Some(chain_id));
627        if let Some(offset_q) = grip_rotation_offset {
628            println!(
629                "[AVC] {} hand IK target rotation offset = {:?}",
630                side_label, offset_q
631            );
632        } else {
633            let _ = raw_driver;
634        }
635        // Parent under AVC for cleanup; the solver itself ignores the chain's parent.
636        emit_attach(emit, id, chain_id);
637    }
638
639    // -----------------------------------------------------------------------
640    // Camera re-parenting: move discovered Camera3D/CameraXR children of AVC
641    // under the camera bone so they inherit its world transform each tick.
642    // -----------------------------------------------------------------------
643    if let Some(cam_bone_id) = camera_bone_id {
644        for &(cam, _eye_offset, is_desktop_camera_path) in &camera_children {
645            if is_desktop_camera_path {
646                if let Some(tc) = world.get_component_by_id_as_mut::<TransformComponent>(cam) {
647                    if tc.transform.rotation != quat_rotation_y(std::f32::consts::PI) {
648                        tc.transform.rotation = quat_rotation_y(std::f32::consts::PI);
649                        tc.transform.recompute_model();
650                    }
651                } else {
652                    let desktop_camera_mount = world.add_component(
653                        TransformComponent::new()
654                            .with_rotation_quat(quat_rotation_y(std::f32::consts::PI)),
655                    );
656                    let desktop_camera_mount_serialize_id =
657                        world.add_component(SerializeComponent::off());
658                    let _ = world.set_parent(
659                        desktop_camera_mount_serialize_id,
660                        Some(desktop_camera_mount),
661                    );
662                    emit_attach(emit, desktop_camera_mount, cam);
663                    println!(
664                        "[AVC] inserted desktop camera yaw-correction mount {:?} for camera {:?}",
665                        desktop_camera_mount, cam
666                    );
667                    emit_attach(emit, cam_bone_id, desktop_camera_mount);
668                    continue;
669                }
670            }
671            println!(
672                "[AVC] re-parenting camera {:?} under camera anchor {:?}",
673                cam, cam_bone_id
674            );
675            emit_attach(emit, cam_bone_id, cam);
676        }
677    } else if !camera_children.is_empty() {
678        println!(
679            "[AVC] WARNING: camera children found but camera_bone not resolved — no re-parenting"
680        );
681    }
682}
683
684fn find_xr_pose_driver(world: &World, start: ComponentId) -> Option<ComponentId> {
685    let mut current = Some(start);
686    while let Some(component) = current {
687        if world
688            .get_component_by_id_as::<ControllerXRComponent>(component)
689            .is_some()
690            || world
691                .get_component_by_id_as::<crate::engine::ecs::component::InputXRComponent>(
692                    component,
693                )
694                .is_some()
695        {
696            return Some(component);
697        }
698        current = world.parent_of(component);
699    }
700    None
701}
702
703/// Find a hand bone by name and determine its raw driver node.
704///
705/// Returns `(bone_original_parent, raw_driver, hand_driver, bone_id)` or `None` if the bone
706/// wasn't found (model may not have this joint — silently skip).
707fn resolve_hand_splice(
708    world: &mut World,
709    model_root: ComponentId,
710    bone_name: Option<&str>,
711    controller: Option<ComponentId>,
712    rotation_offset: Option<[f32; 4]>,
713) -> Option<(ComponentId, ComponentId, ComponentId, ComponentId)> {
714    let bone_name = bone_name?;
715    let sel = format!("#{}", bone_name);
716    let bone = world.find_component(model_root, &sel)?;
717    let bone_parent = world.parent_of(bone)?;
718
719    let driver = if let Some(ctrl) = controller {
720        world
721            .children_of(ctrl)
722            .iter()
723            .copied()
724            .find(|&ch| {
725                world
726                    .get_component_by_id_as::<TransformComponent>(ch)
727                    .is_some()
728            })
729            .unwrap_or_else(|| world.add_component(TransformComponent::new()))
730    } else {
731        world.add_component(TransformComponent::new())
732    };
733
734    let hand_driver = if let Some(offset_q) = rotation_offset {
735        let offset = world.add_component(TransformComponent::new().with_rotation_quat(offset_q));
736        let offset_serialize_id = world.add_component(SerializeComponent::off());
737        let _ = world.set_parent(offset_serialize_id, Some(offset));
738        let _ = world.set_parent(offset, Some(driver));
739        offset
740    } else {
741        driver
742    };
743
744    Some((bone_parent, driver, hand_driver, bone))
745}
746
747fn emit_attach(emit: &mut dyn SignalEmitter, parent: ComponentId, child: ComponentId) {
748    emit.push_intent_now(
749        parent,
750        IntentValue::Attach {
751            parents: vec![parent],
752            child,
753        },
754    );
755}
756
757fn capture_hand_grip_calibration(
758    avc_id: ComponentId,
759    world: &mut World,
760    emit: &mut dyn SignalEmitter,
761) -> bool {
762    let (enabled, left_raw, right_raw, left_visual, right_visual, left_hand, right_hand) = {
763        let Some(c) = world.get_component_by_id_as::<AvatarControlComponent>(avc_id) else {
764            return false;
765        };
766        (
767            c.calibrate_hand_transforms,
768            c.left_hand_raw_target_id,
769            c.right_hand_raw_target_id,
770            c.left_hand_visual_target_id,
771            c.right_hand_visual_target_id,
772            c.left_hand_bone_id,
773            c.right_hand_bone_id,
774        )
775    };
776
777    if !enabled {
778        return false;
779    }
780
781    let (
782        Some(left_raw),
783        Some(right_raw),
784        Some(left_visual),
785        Some(right_visual),
786        Some(left_hand),
787        Some(right_hand),
788    ) = (
789        left_raw,
790        right_raw,
791        left_visual,
792        right_visual,
793        left_hand,
794        right_hand,
795    )
796    else {
797        println!(
798            "[AVC][calibrate] AVC {:?} is enabled for hand calibration but arm targets are not initialized yet.",
799            avc_id
800        );
801        return true;
802    };
803
804    let left_offset = quat_mul(
805        quat_conjugate(tc_world_rot(world, left_raw)),
806        tc_world_rot(world, left_hand),
807    );
808    let right_offset = quat_mul(
809        quat_conjugate(tc_world_rot(world, right_raw)),
810        tc_world_rot(world, right_hand),
811    );
812
813    if let Some(c) = world.get_component_by_id_as_mut::<AvatarControlComponent>(avc_id) {
814        c.hand_grip_rotation_left = Some(left_offset);
815        c.hand_grip_rotation_right = Some(right_offset);
816    }
817
818    if left_visual != left_raw {
819        update_local_rotation(world, emit, left_visual, left_offset);
820    }
821    if right_visual != right_raw {
822        update_local_rotation(world, emit, right_visual, right_offset);
823    }
824
825    println!(
826        "[AVC][calibrate] captured hand grip offsets for AVC {:?}:",
827        avc_id
828    );
829    println!(
830        "  hand_grip_rotation_left([{:.7}, {:.7}, {:.7}, {:.7}])",
831        left_offset[0], left_offset[1], left_offset[2], left_offset[3]
832    );
833    println!(
834        "  hand_grip_rotation_right([{:.7}, {:.7}, {:.7}, {:.7}])",
835        right_offset[0], right_offset[1], right_offset[2], right_offset[3]
836    );
837
838    true
839}
840
841fn update_local_rotation(
842    world: &World,
843    emit: &mut dyn SignalEmitter,
844    component_id: ComponentId,
845    rotation: [f32; 4],
846) {
847    let (translation, scale) = world
848        .get_component_by_id_as::<TransformComponent>(component_id)
849        .map(|t| (t.transform.translation, t.transform.scale))
850        .unwrap_or(([0.0; 3], [1.0, 1.0, 1.0]));
851    emit.push_intent_now(
852        component_id,
853        IntentValue::UpdateTransform {
854            component_ids: vec![component_id],
855            translation,
856            rotation_quat_xyzw: rotation,
857            scale,
858        },
859    );
860}
861
862/// Read a bone's authored bind-pose local TRS via the `BoneRestPoseComponent`
863/// sidecar that `GLTFSystem` stamps at node-spawn time.  Falls back to the
864/// live `TransformComponent` (then to identity) for non-GLTF skeletons that
865/// never had a rest-pose snapshot attached.
866fn read_bone_rest_pose(world: &World, bone_id: ComponentId) -> ([f32; 3], [f32; 4], [f32; 3]) {
867    if let Some(rest) = world
868        .children_of(bone_id)
869        .iter()
870        .find_map(|&c| world.get_component_by_id_as::<BoneRestPoseComponent>(c))
871    {
872        return (rest.translation, rest.rotation, rest.scale);
873    }
874    world
875        .get_component_by_id_as::<TransformComponent>(bone_id)
876        .map(|t| {
877            (
878                t.transform.translation,
879                t.transform.rotation,
880                t.transform.scale,
881            )
882        })
883        .unwrap_or(([0.0; 3], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0]))
884}
885
886fn tc_world_rot(world: &World, id: ComponentId) -> [f32; 4] {
887    world
888        .get_component_by_id_as::<TransformComponent>(id)
889        .map(|t| mat_to_quat(t.transform.matrix_world))
890        .unwrap_or([0.0, 0.0, 0.0, 1.0])
891}